Files
gochat/internal/handler/api/v1/email_channel_handler.go
T
2026-06-04 15:44:48 +08:00

383 lines
12 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"
"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 ""
}