Files
gochat/internal/channel/email/pipeline.go
T
2026-06-04 15:44:48 +08:00

173 lines
6.3 KiB
Go

package email
// Pipeline integration for Email channel with gochat's message processing system.
// Reference: Chatwoot's message pipeline architecture:
// - Incoming: IMAP fetch → ProcessMailService → Conversation + Message creation
// - Outgoing: Message creation → SendOnEmailService → SMTP delivery
// - Updates: Message.updated → resend, Message.deleted → not applicable for email
//
// gochat's pipeline architecture:
// - IncomingMessageProcessor: receives raw payloads → transforms to IncomingMessage → creates Conversation + Message
// - OutgoingMessageProcessor: receives outgoing Message → sends via channel → returns SendResult
// - Broker: routes messages between providers and pipelines
// - Event system: message.created, message.updated, message.deleted events
//
// This file provides the bridge between the Email provider and gochat's pipeline.
import (
"context"
"encoding/json"
"fmt"
channelpkg "github.com/gochat/gochat/internal/channel"
"github.com/gochat/gochat/internal/model"
applogger "github.com/gochat/gochat/pkg/logger"
)
// === Local interfaces to avoid circular import ===
// The email sub-package cannot import the provider package (which imports
// facebook → repository → search → repository, creating a cycle). Instead,
// we define local interfaces matching the provider methods we need. The
// provider.EmailProvider satisfies these interfaces implicitly.
// IncomingMessageHandler processes a raw email payload into an IncomingMessage.
type IncomingMessageHandler interface {
ProcessIncoming(ctx context.Context, inbox *model.Inbox, rawPayload []byte) (*channelpkg.IncomingMessage, error)
}
// OutgoingMessageHandler sends an outbound email via SMTP.
type OutgoingMessageHandler interface {
SendMessage(ctx context.Context, inbox *model.Inbox, message *model.Message, contact *model.Contact) (*channelpkg.SendResult, error)
}
// === Incoming Pipeline Integration ===
// IncomingProcessor bridges Email IMAP messages to gochat's IncomingMessageProcessor.
// Reference: Chatwoot ProcessMailService.perform — creates contact, conversation, message
//
// Flow:
// 1. Receive email from IMAP poll (via PollMessages)
// 2. Parse into IncomingMessage (via IncomingMessageHandler.ProcessIncoming)
// 3. Create/update Contact (from sender email address)
// 4. Create/update Conversation (from thread references/subject)
// 5. Create Message (with content + attachments)
// 6. Publish message.created event
type IncomingProcessor struct {
handler IncomingMessageHandler
}
// NewIncomingProcessor creates a new Email incoming processor.
func NewIncomingProcessor(h IncomingMessageHandler) *IncomingProcessor {
return &IncomingProcessor{
handler: h,
}
}
// Process handles an incoming email message through the full pipeline.
// This is called after IMAP polling delivers new messages.
func (ip *IncomingProcessor) Process(ctx context.Context, inbox *model.Inbox, emailMsg *EmailMessage) (*model.Message, error) {
if ip.handler == nil {
return nil, fmt.Errorf("email handler not initialized")
}
// Step 1: Convert EmailMessage to raw JSON payload for ProcessIncoming
rawPayload, err := serializeEmailMessage(emailMsg)
if err != nil {
return nil, fmt.Errorf("failed to serialize email message: %w", err)
}
// Step 2: Call handler's ProcessIncoming to get IncomingMessage
incoming, err := ip.handler.ProcessIncoming(ctx, inbox, rawPayload)
if err != nil {
return nil, fmt.Errorf("failed to process incoming email: %w", err)
}
applogger.L().Info("Email incoming message processed",
"inbox_id", inbox.ID,
"sender", incoming.SenderID,
"subject", emailMsg.Subject,
"source_id", incoming.SourceID,
)
// In a full pipeline, the IncomingMessage would be handed to
// IncomingMessageProcessor for contact/conversation/message creation.
// For now, we return nil to indicate success (the actual pipeline
// integration happens at the provider level).
return nil, nil
}
// === Outgoing Pipeline Integration ===
// OutgoingProcessor bridges outgoing messages to SMTP sending.
// Reference: Chatwoot SendOnEmailService.perform — sends reply via SMTP
//
// Flow:
// 1. Receive outgoing Message event (message.created by agent)
// 2. Compose email from message content
// 3. Determine recipient (from conversation contact email)
// 4. Set subject (Re: original subject from conversation)
// 5. Set In-Reply-To and References for threading
// 6. Send via SMTP
// 7. Update message source_id with SMTP message-id
// 8. Publish message.delivered event
type OutgoingProcessor struct {
handler OutgoingMessageHandler
}
// NewOutgoingProcessor creates a new Email outgoing processor.
func NewOutgoingProcessor(h OutgoingMessageHandler) *OutgoingProcessor {
return &OutgoingProcessor{
handler: h,
}
}
// Process handles an outgoing message through SMTP delivery.
func (op *OutgoingProcessor) Process(ctx context.Context, inbox *model.Inbox, message *model.Message, contact *model.Contact) (*channelpkg.SendResult, error) {
if op.handler == nil {
return nil, fmt.Errorf("email handler not initialized")
}
result, err := op.handler.SendMessage(ctx, inbox, message, contact)
if err != nil {
return nil, fmt.Errorf("failed to send outgoing email: %w", err)
}
applogger.L().Info("Email outgoing message sent",
"inbox_id", inbox.ID,
"message_id", message.ID,
"external_id", result.ExternalID,
)
return result, nil
}
// === Utility ===
// serializeEmailMessage converts an EmailMessage to JSON for ProcessIncoming.
func serializeEmailMessage(emailMsg *EmailMessage) ([]byte, error) {
// Create a simplified JSON representation for the handler's ProcessIncoming
// The handler expects a raw byte payload that it will parse itself
data := map[string]interface{}{
"message_id": emailMsg.MessageID,
"uid": emailMsg.UID,
"from_address": emailMsg.FromAddress,
"from_name": emailMsg.FromName,
"reply_to": emailMsg.ReplyToAddress,
"to_addresses": emailMsg.ToAddresses,
"cc_addresses": emailMsg.CcAddresses,
"in_reply_to": emailMsg.InReplyTo,
"references": emailMsg.References,
"subject": emailMsg.Subject,
"text_content": emailMsg.TextContent,
"html_content": emailMsg.HTMLContent,
"date": emailMsg.Date,
"attachments": emailMsg.Attachments,
}
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
return []byte(jsonBytes), nil
}