173 lines
6.3 KiB
Go
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
|
|
} |