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 }