445 lines
14 KiB
Go
445 lines
14 KiB
Go
package v1
|
|
|
|
// EmailChannelHandler handles Email (SMTP/IMAP) channel-specific configuration CRUD.
|
|
// Reference: Chatwoot app/controllers/api/v1/accounts/channels/email_controller.rb
|
|
//
|
|
// Email channel in gochat maps to:
|
|
// - POST /api/v1/accounts/:id/channels/email_channel → create Email inbox
|
|
// - GET /api/v1/accounts/:id/channels/email_channel/:em_id → get Email channel
|
|
// - PATCH /api/v1/accounts/:id/channels/email_channel/:em_id → update Email channel
|
|
// - DELETE /api/v1/accounts/:id/channels/email_channel/:em_id → delete Email channel
|
|
// - GET /api/v1/accounts/:id/channels/email_channel → list Email channels
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/service"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// EmailChannelHandler handles Email channel management.
|
|
// Follows LINEChannelHandler pattern: uses top-level ChannelEmailService for CRUD,
|
|
// generic InboxService for inbox lifecycle.
|
|
type EmailChannelHandler struct {
|
|
emailChannelSvc *service.ChannelEmailService
|
|
inboxSvc *service.InboxService
|
|
emailRepo *repository.ChannelEmailRepo
|
|
}
|
|
|
|
// NewEmailChannelHandler creates a new Email channel handler.
|
|
func NewEmailChannelHandler(
|
|
emailChannelSvc *service.ChannelEmailService,
|
|
inboxSvc *service.InboxService,
|
|
emailRepo *repository.ChannelEmailRepo,
|
|
) *EmailChannelHandler {
|
|
return &EmailChannelHandler{
|
|
emailChannelSvc: emailChannelSvc,
|
|
inboxSvc: inboxSvc,
|
|
emailRepo: emailRepo,
|
|
}
|
|
}
|
|
|
|
// CreateEmailChannelRequest is the DTO for creating an Email channel.
|
|
type CreateEmailChannelRequest struct {
|
|
Email string `json:"email" validate:"required"`
|
|
MailboxName string `json:"mailbox_name"`
|
|
// IMAP configuration
|
|
IMAPEnabled bool `json:"imap_enabled"`
|
|
IMAPAddress string `json:"imap_address"`
|
|
IMAPPort int `json:"imap_port"`
|
|
IMAPLogin string `json:"imap_login"`
|
|
IMAPPassword string `json:"imap_password"`
|
|
IMAPSSLMode string `json:"imap_ssl_mode"`
|
|
IMAPFolder string `json:"imap_folder"`
|
|
// SMTP configuration
|
|
SMTPEnabled bool `json:"smtp_enabled"`
|
|
SMTPAddress string `json:"smtp_address"`
|
|
SMTPPort int `json:"smtp_port"`
|
|
SMTPLogin string `json:"smtp_login"`
|
|
SMTPPassword string `json:"smtp_password"`
|
|
SMTPSSLMode string `json:"smtp_ssl_mode"`
|
|
// Behavioral configuration
|
|
InboxName string `json:"inbox_name"`
|
|
}
|
|
|
|
// Create adds a new Email channel and creates the associated inbox.
|
|
// POST /api/v1/accounts/:id/channels/email_channel
|
|
func (h *EmailChannelHandler) Create(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
var req CreateEmailChannelRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
applogger.L().Errorf("Failed to bind Email channel create request: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
if err := h.inboxSvc.EnsureCanCreateInbox(ctx, accountID); err != nil {
|
|
if renderInboxLimitExceeded(c, err) {
|
|
return
|
|
}
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create inbox"})
|
|
return
|
|
}
|
|
|
|
// Create the channel record first (without InboxID)
|
|
ch := &channelmodel.ChannelEmail{
|
|
Email: req.Email,
|
|
MailboxName: req.MailboxName,
|
|
Domain: extractDomain(req.Email),
|
|
// IMAP config
|
|
IMAPEnabled: req.IMAPEnabled,
|
|
IMAPAddress: req.IMAPAddress,
|
|
IMAPPort: req.IMAPPort,
|
|
IMAPLogin: req.IMAPLogin,
|
|
IMAPPassword: req.IMAPPassword,
|
|
IMAPSSLMode: req.IMAPSSLMode,
|
|
IMAPFolder: req.IMAPFolder,
|
|
// SMTP config
|
|
SMTPEnabled: req.SMTPEnabled,
|
|
SMTPAddress: req.SMTPAddress,
|
|
SMTPPort: req.SMTPPort,
|
|
SMTPLogin: req.SMTPLogin,
|
|
SMTPPassword: req.SMTPPassword,
|
|
SMTPSSLMode: req.SMTPSSLMode,
|
|
AccountID: accountID,
|
|
}
|
|
|
|
if err := h.emailChannelSvc.Create(ctx, ch); err != nil {
|
|
applogger.L().Errorf("Failed to create Email channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create Email channel"})
|
|
return
|
|
}
|
|
|
|
// Create inbox for the Email channel
|
|
inboxName := req.InboxName
|
|
if inboxName == "" {
|
|
inboxName = req.MailboxName
|
|
}
|
|
if inboxName == "" {
|
|
inboxName = req.Email
|
|
}
|
|
inboxReq := service.CreateInboxRequest{
|
|
Name: inboxName,
|
|
ChannelType: "email",
|
|
Enabled: true,
|
|
}
|
|
|
|
inboxReq.Channel = emailChannelConfig(req)
|
|
inbox, err := h.inboxSvc.Create(ctx, accountID, inboxReq)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to create inbox for Email channel: %v", err)
|
|
// Rollback channel creation
|
|
if delErr := h.emailChannelSvc.Delete(ctx, ch.ID); delErr != nil {
|
|
applogger.L().Warnf("Failed to rollback Email channel after inbox creation failure: %v", delErr)
|
|
}
|
|
if renderInboxLimitExceeded(c, err) {
|
|
return
|
|
}
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create inbox"})
|
|
return
|
|
}
|
|
|
|
// Update channel with InboxID
|
|
ch.InboxID = inbox.ID
|
|
if err := h.emailChannelSvc.Update(ctx, ch); err != nil {
|
|
applogger.L().Errorf("Failed to update Email channel with inbox_id: %v", err)
|
|
}
|
|
inbox, err = h.inboxSvc.BindChannel(ctx, accountID, inbox.ID, ch.ID, emailChannelConfig(req))
|
|
if err != nil {
|
|
applogger.L().Warnf("Failed to bind Email inbox channel config: %v", err)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, serializeInbox(inbox))
|
|
}
|
|
|
|
// Get retrieves an Email channel by ID.
|
|
// GET /api/v1/accounts/:id/channels/email_channel/:em_id
|
|
func (h *EmailChannelHandler) Get(c *gin.Context) {
|
|
emID, err := parseUintParam(c, "em_id")
|
|
if err != nil {
|
|
applogger.L().Errorf("Invalid em_id: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"})
|
|
return
|
|
}
|
|
|
|
ch, err := h.emailChannelSvc.GetByID(c.Request.Context(), emID)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to get Email channel: %v", err)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Email channel not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, h.serializeInboxForEmailChannel(c, ch))
|
|
}
|
|
|
|
// UpdateEmailChannelRequest is the DTO for updating an Email channel.
|
|
type UpdateEmailChannelRequest struct {
|
|
Email *string `json:"email"`
|
|
MailboxName *string `json:"mailbox_name"`
|
|
IMAPEnabled *bool `json:"imap_enabled"`
|
|
IMAPAddress *string `json:"imap_address"`
|
|
IMAPPort *int `json:"imap_port"`
|
|
IMAPLogin *string `json:"imap_login"`
|
|
IMAPPassword *string `json:"imap_password"`
|
|
IMAPSSLMode *string `json:"imap_ssl_mode"`
|
|
IMAPFolder *string `json:"imap_folder"`
|
|
SMTPEnabled *bool `json:"smtp_enabled"`
|
|
SMTPAddress *string `json:"smtp_address"`
|
|
SMTPPort *int `json:"smtp_port"`
|
|
SMTPLogin *string `json:"smtp_login"`
|
|
SMTPPassword *string `json:"smtp_password"`
|
|
SMTPSSLMode *string `json:"smtp_ssl_mode"`
|
|
InboxName *string `json:"inbox_name"`
|
|
}
|
|
|
|
// Update updates an Email channel configuration.
|
|
// PATCH /api/v1/accounts/:id/channels/email_channel/:em_id
|
|
func (h *EmailChannelHandler) Update(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
emID, err := parseUintParam(c, "em_id")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
ch, err := h.emailChannelSvc.GetByID(ctx, emID)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to get Email channel for update: %v", err)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Email channel not found"})
|
|
return
|
|
}
|
|
|
|
// Verify account ownership
|
|
if ch.AccountID != accountID {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Email channel does not belong to this account"})
|
|
return
|
|
}
|
|
|
|
var req UpdateEmailChannelRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Apply updates
|
|
if req.Email != nil {
|
|
ch.Email = *req.Email
|
|
ch.Domain = extractDomain(*req.Email)
|
|
}
|
|
if req.MailboxName != nil {
|
|
ch.MailboxName = *req.MailboxName
|
|
}
|
|
if req.IMAPEnabled != nil {
|
|
ch.IMAPEnabled = *req.IMAPEnabled
|
|
}
|
|
if req.IMAPAddress != nil {
|
|
ch.IMAPAddress = *req.IMAPAddress
|
|
}
|
|
if req.IMAPPort != nil {
|
|
ch.IMAPPort = *req.IMAPPort
|
|
}
|
|
if req.IMAPLogin != nil {
|
|
ch.IMAPLogin = *req.IMAPLogin
|
|
}
|
|
if req.IMAPPassword != nil {
|
|
ch.IMAPPassword = *req.IMAPPassword
|
|
}
|
|
if req.IMAPSSLMode != nil {
|
|
ch.IMAPSSLMode = *req.IMAPSSLMode
|
|
}
|
|
if req.IMAPFolder != nil {
|
|
ch.IMAPFolder = *req.IMAPFolder
|
|
}
|
|
if req.SMTPEnabled != nil {
|
|
ch.SMTPEnabled = *req.SMTPEnabled
|
|
}
|
|
if req.SMTPAddress != nil {
|
|
ch.SMTPAddress = *req.SMTPAddress
|
|
}
|
|
if req.SMTPPort != nil {
|
|
ch.SMTPPort = *req.SMTPPort
|
|
}
|
|
if req.SMTPLogin != nil {
|
|
ch.SMTPLogin = *req.SMTPLogin
|
|
}
|
|
if req.SMTPPassword != nil {
|
|
ch.SMTPPassword = *req.SMTPPassword
|
|
}
|
|
if req.SMTPSSLMode != nil {
|
|
ch.SMTPSSLMode = *req.SMTPSSLMode
|
|
}
|
|
|
|
if err := h.emailChannelSvc.Update(ctx, ch); err != nil {
|
|
applogger.L().Errorf("Failed to update Email channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update Email channel"})
|
|
return
|
|
}
|
|
|
|
// Update inbox name if provided
|
|
if req.InboxName != nil && ch.InboxID > 0 {
|
|
updateReq := service.UpdateInboxRequest{
|
|
Name: *req.InboxName,
|
|
}
|
|
if _, inboxErr := h.inboxSvc.Update(ctx, accountID, ch.InboxID, updateReq); inboxErr != nil {
|
|
applogger.L().Warnf("Failed to update Email inbox name: %v", inboxErr)
|
|
}
|
|
}
|
|
inbox, bindErr := h.inboxSvc.BindChannel(ctx, accountID, ch.InboxID, ch.ID, emailUpdateChannelConfig(req, ch))
|
|
if bindErr != nil {
|
|
applogger.L().Warnf("Failed to update Email inbox channel config: %v", bindErr)
|
|
c.JSON(http.StatusOK, h.serializeInboxForEmailChannel(c, ch))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, serializeInbox(inbox))
|
|
}
|
|
|
|
// Delete removes an Email channel and its associated inbox.
|
|
// DELETE /api/v1/accounts/:id/channels/email_channel/:em_id
|
|
func (h *EmailChannelHandler) Delete(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
emID, err := parseUintParam(c, "em_id")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
ch, err := h.emailChannelSvc.GetByID(ctx, emID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Email channel not found"})
|
|
return
|
|
}
|
|
|
|
// Verify account ownership
|
|
if ch.AccountID != accountID {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Email channel does not belong to this account"})
|
|
return
|
|
}
|
|
|
|
// Delete the channel record
|
|
if err := h.emailChannelSvc.Delete(ctx, emID); err != nil {
|
|
applogger.L().Errorf("Failed to delete Email channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete Email channel"})
|
|
return
|
|
}
|
|
|
|
// Delete the associated inbox
|
|
if ch.InboxID > 0 {
|
|
if delErr := h.inboxSvc.DeleteByAccount(ctx, accountID, ch.InboxID); delErr != nil {
|
|
applogger.L().Warnf("Failed to delete inbox for Email channel: %v", delErr)
|
|
}
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// List lists all Email channels for an account.
|
|
// GET /api/v1/accounts/:id/channels/email_channel
|
|
func (h *EmailChannelHandler) List(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
channels, err := h.emailChannelSvc.ListByAccount(ctx, accountID)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to list Email channels: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list Email channels"})
|
|
return
|
|
}
|
|
|
|
payload := make([]map[string]any, 0, len(channels))
|
|
for i := range channels {
|
|
payload = append(payload, h.serializeInboxForEmailChannel(c, &channels[i]))
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"payload": payload})
|
|
}
|
|
|
|
func (h *EmailChannelHandler) serializeInboxForEmailChannel(c *gin.Context, ch *channelmodel.ChannelEmail) map[string]any {
|
|
if ch != nil && ch.InboxID > 0 {
|
|
if inbox, err := h.inboxSvc.GetByAccountAndID(c.Request.Context(), ch.AccountID, ch.InboxID); err == nil {
|
|
return serializeInbox(inbox)
|
|
}
|
|
}
|
|
return gin.H{"id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "email": ch.Email}
|
|
}
|
|
|
|
func emailChannelConfig(req CreateEmailChannelRequest) map[string]any {
|
|
return map[string]any{
|
|
"email": req.Email,
|
|
"mailbox_name": req.MailboxName,
|
|
"domain": extractDomain(req.Email),
|
|
"imap_enabled": req.IMAPEnabled,
|
|
"imap_address": req.IMAPAddress,
|
|
"imap_port": req.IMAPPort,
|
|
"imap_login": req.IMAPLogin,
|
|
"imap_password": req.IMAPPassword,
|
|
"imap_ssl_mode": req.IMAPSSLMode,
|
|
"imap_folder": req.IMAPFolder,
|
|
"smtp_enabled": req.SMTPEnabled,
|
|
"smtp_address": req.SMTPAddress,
|
|
"smtp_port": req.SMTPPort,
|
|
"smtp_login": req.SMTPLogin,
|
|
"smtp_password": req.SMTPPassword,
|
|
"smtp_ssl_mode": req.SMTPSSLMode,
|
|
}
|
|
}
|
|
|
|
func emailUpdateChannelConfig(_ UpdateEmailChannelRequest, ch *channelmodel.ChannelEmail) map[string]any {
|
|
return map[string]any{
|
|
"email": ch.Email,
|
|
"mailbox_name": ch.MailboxName,
|
|
"domain": ch.Domain,
|
|
"imap_enabled": ch.IMAPEnabled,
|
|
"imap_address": ch.IMAPAddress,
|
|
"imap_port": ch.IMAPPort,
|
|
"imap_login": ch.IMAPLogin,
|
|
"imap_password": ch.IMAPPassword,
|
|
"imap_ssl_mode": ch.IMAPSSLMode,
|
|
"imap_folder": ch.IMAPFolder,
|
|
"smtp_enabled": ch.SMTPEnabled,
|
|
"smtp_address": ch.SMTPAddress,
|
|
"smtp_port": ch.SMTPPort,
|
|
"smtp_login": ch.SMTPLogin,
|
|
"smtp_password": ch.SMTPPassword,
|
|
"smtp_ssl_mode": ch.SMTPSSLMode,
|
|
}
|
|
}
|
|
|
|
// extractDomain extracts the domain portion from an email address.
|
|
func extractDomain(email string) string {
|
|
parts := strings.SplitN(email, "@", 2)
|
|
if len(parts) == 2 {
|
|
return parts[1]
|
|
}
|
|
return ""
|
|
}
|