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 "" }