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" "strconv" "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) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { applogger.L().Errorf("Invalid account_id: %v", err) 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() // 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: uint(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, } inbox, err := h.inboxSvc.Create(ctx, uint(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) } 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) } c.JSON(http.StatusCreated, gin.H{ "channel": ch, "inbox": 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) { emIDStr := c.Param("em_id") emID, err := strconv.ParseUint(emIDStr, 10, 64) 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(), uint(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, gin.H{"channel": 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) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } emIDStr := c.Param("em_id") emID, err := strconv.ParseUint(emIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"}) return } ctx := c.Request.Context() ch, err := h.emailChannelSvc.GetByID(ctx, uint(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 != uint(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, uint(accountID), ch.InboxID, updateReq); inboxErr != nil { applogger.L().Warnf("Failed to update Email inbox name: %v", inboxErr) } } c.JSON(http.StatusOK, gin.H{ "channel": ch, "message": "Email channel updated successfully", }) } // 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) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } emIDStr := c.Param("em_id") emID, err := strconv.ParseUint(emIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"}) return } ctx := c.Request.Context() ch, err := h.emailChannelSvc.GetByID(ctx, uint(emID)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Email channel not found"}) return } // Verify account ownership if ch.AccountID != uint(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, uint(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, uint(accountID), ch.InboxID); delErr != nil { applogger.L().Warnf("Failed to delete inbox for Email channel: %v", delErr) } } c.JSON(http.StatusOK, gin.H{"message": "Email channel deleted successfully"}) } // List lists all Email channels for an account. // GET /api/v1/accounts/:id/channels/email_channel func (h *EmailChannelHandler) List(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } ctx := c.Request.Context() channels, err := h.emailChannelSvc.ListByAccount(ctx, uint(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 } c.JSON(http.StatusOK, gin.H{"channels": channels}) } // 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 "" }