package email // EmailService provides high-level operations for the Email channel. // Reference: Chatwoot app/services/channel/email/process_mail_service.rb // + app/services/channel/email/fetch_imap_email_service.rb // // This service coordinates: // - IMAPListener (IMAP polling and message fetching) // - SMTPSender (outbound email delivery) // - Content processing (HTML/text conversion, sanitization) // - Address mapping (email → contact identification) // - Thread context (In-Reply-To, References for conversation threading) // // The service implements EmailServiceInterface defined in provider/email.go, // which is the contract between EmailProvider and the email sub-package. import ( "context" "net/mail" "strings" channelpkg "github.com/gochat/gochat/internal/channel" "github.com/gochat/gochat/internal/model" ) // Service coordinates Email channel operations. // This is the concrete implementation of EmailServiceInterface. type Service struct { imapListener *IMAPListener smtpSender *SMTPSender } // NewService creates a new Email service with IMAP and SMTP components. func NewService() *Service { return &Service{ imapListener: NewIMAPListener(), smtpSender: NewSMTPSender(), } } // === EmailServiceInterface implementation === // ValidateIMAPConnection checks IMAP server connectivity and auth. func (s *Service) ValidateIMAPConnection(config channelpkg.ChannelConfig) error { return s.imapListener.ValidateConnection(config) } // ValidateSMTPConnection checks SMTP server connectivity and auth. func (s *Service) ValidateSMTPConnection(config channelpkg.ChannelConfig) error { return s.smtpSender.ValidateConnection(config) } // FetchIMAPMessages polls IMAP for new messages and converts them to IncomingMessage. func (s *Service) FetchIMAPMessages(ctx context.Context, inbox *model.Inbox, config channelpkg.ChannelConfig) ([]*channelpkg.IncomingMessage, error) { return s.imapListener.Fetch(ctx, inbox, config) } // SendViaSMTP sends an outgoing email and returns the message ID. func (s *Service) SendViaSMTP(config channelpkg.ChannelConfig, from, to, subject, body string) (string, error) { return s.smtpSender.Send(config, from, to, subject, body) } // === Address processing helpers (unique to service.go) === // extractEmailAddress extracts just the email address from a full address string. // Handles formats like "John Doe " or "john@example.com". func extractEmailAddress(addr string) string { if addr == "" { return "" } // Try RFC 5322 parsing first addresses, err := mail.ParseAddressList(addr) if err == nil && len(addresses) > 0 { return addresses[0].Address } // Fallback: extract content between < and > start := strings.Index(addr, "<") end := strings.Index(addr, ">") if start >= 0 && end > start { return strings.TrimSpace(addr[start+1:end]) } // Plain email address return strings.TrimSpace(addr) } // extractDisplayName extracts the display name from a full address string. // Handles formats like "John Doe " → "John Doe". func extractDisplayName(addr string) string { if addr == "" { return "" } // Try RFC 5322 parsing first addresses, err := mail.ParseAddressList(addr) if err == nil && len(addresses) > 0 { if addresses[0].Name != "" { return addresses[0].Name } return "" // No display name, just email address } // Fallback: extract content before < if present start := strings.Index(addr, "<") if start >= 0 { name := strings.TrimSpace(addr[:start]) // Remove surrounding quotes name = strings.Trim(name, "\"") return name } return "" // Just an email address, no display name }