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