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

109 lines
3.6 KiB
Go

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 <john@example.com>" 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@example.com>" → "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
}