package webhook // Email Gin webhook adapter — bridges HTTP requests from the Gin router // to the Email channel package's WebhookHandler and IncomingProcessor. // Reference: LINE/TikTok webhook adapter pattern (line_webhook.go, tiktok_webhook.go) // // URL pattern: /webhooks/email/:inbox_id // Methods: // - POST: HandleEmailWebhook — processes incoming email relay requests // - GET: HandleEmailVerification — health check / verification endpoint // // Email webhook relay providers (Mailgun, SendGrid, SES, Postfix) send // inbound email data as HTTP POST to this endpoint. The adapter: // 1. Reads the request body // 2. Parses it via WebhookHandler.ParseWebhookBody into an EmailMessage // 3. Processes it via IncomingProcessor.Process for full pipeline handling // 4. Returns JSON response import ( "encoding/json" "io" "net/http" "strconv" emailchannel "github.com/gochat/gochat/internal/channel/email" "github.com/gochat/gochat/internal/model" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // EmailWebhookHandler adapts Email webhook handling to Gin HTTP requests. type EmailWebhookHandler struct { emailWebhook *emailchannel.WebhookHandler pipeline *emailchannel.IncomingProcessor db *gorm.DB } // NewEmailWebhookHandler creates a Gin-compatible Email webhook handler. func NewEmailWebhookHandler(emailWebhook *emailchannel.WebhookHandler, pipeline *emailchannel.IncomingProcessor, db *gorm.DB) *EmailWebhookHandler { return &EmailWebhookHandler{ emailWebhook: emailWebhook, pipeline: pipeline, db: db, } } // HandleEmailWebhook processes incoming email relay webhook HTTP requests. // This handles ActionMailbox-style inbound email relay from providers // like Mailgun, SendGrid, SES, or Postfix pipe-to-webhook. func (h *EmailWebhookHandler) HandleEmailWebhook(c *gin.Context) { inboxIDStr := c.Param("inbox_id") inboxID, err := strconv.ParseUint(inboxIDStr, 10, 32) if err != nil { applogger.L().Warnf("Email webhook: invalid inbox_id %s", inboxIDStr) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } // Lookup inbox from database inbox, err := h.lookupInbox(uint(inboxID)) if err != nil { applogger.L().Warnf("Email webhook: inbox lookup failed for id %d: %v", inboxID, err) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } // Read request body body, err := io.ReadAll(c.Request.Body) if err != nil { applogger.L().Errorf("Email webhook: failed to read body for inbox %d: %v", inboxID, err) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } defer c.Request.Body.Close() // Parse the email message using the channel-level handler emailMsg, err := h.emailWebhook.ParseWebhookBody(body, c.Request.Header) if err != nil { applogger.L().Errorf("Email webhook: parse request failed for inbox %d: %v", inboxID, err) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Process the message via the pipeline if _, err := h.pipeline.Process(c.Request.Context(), inbox, emailMsg); err != nil { applogger.L().Errorf("Email webhook: process message failed for inbox %d: %v", inboxID, err) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } applogger.L().Infof("Email webhook: processed message for inbox=%d from=%s", inboxID, emailMsg.FromAddress) c.JSON(http.StatusOK, gin.H{"status": "processed"}) } // HandleEmailVerification responds to email webhook URL verification / health check. // This endpoint can be used by relay providers to verify the webhook URL is active. func (h *EmailWebhookHandler) HandleEmailVerification(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "verified"}) } // lookupInbox fetches an Inbox record from the database. func (h *EmailWebhookHandler) lookupInbox(inboxID uint) (*model.Inbox, error) { var inbox model.Inbox if err := h.db.Where("id = ?", inboxID).First(&inbox).Error; err != nil { return nil, err } return &inbox, nil } // parseChannelConfig parses the JSON-encoded ChannelConfig string into a map. func (h *EmailWebhookHandler) parseChannelConfig(inbox *model.Inbox) map[string]interface{} { if inbox.ChannelConfig == "" { return map[string]interface{}{} } var config map[string]interface{} if err := json.Unmarshal([]byte(inbox.ChannelConfig), &config); err != nil { applogger.L().Warnf("Email: Failed to parse ChannelConfig for inbox %d: %v", inbox.ID, err) return map[string]interface{}{} } return config }