Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
123 lines
4.4 KiB
Go
123 lines
4.4 KiB
Go
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
|
|
} |