修复部分功能
This commit is contained in:
+190
-156
@@ -12,16 +12,24 @@ import (
|
||||
"mail_go/config"
|
||||
"mail_go/internal/db"
|
||||
"mail_go/internal/mailutil"
|
||||
"mail_go/internal/store"
|
||||
"mail_go/internal/storage"
|
||||
"mail_go/internal/store"
|
||||
|
||||
"github.com/emersion/go-message/mail"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
)
|
||||
|
||||
// SMTPServer wraps a go-smtp Server and provides mail receiving capability.
|
||||
type smtpMode int
|
||||
|
||||
const (
|
||||
smtpModeInbound smtpMode = iota
|
||||
smtpModeSubmission
|
||||
smtpModeImplicitTLS
|
||||
)
|
||||
|
||||
// SMTPServer wraps go-smtp servers and provides local mail delivery.
|
||||
type SMTPServer struct {
|
||||
server *smtp.Server
|
||||
stores *store.Stores
|
||||
storage *storage.AttachmentStorage
|
||||
cfg config.SMTPConfig
|
||||
@@ -29,161 +37,228 @@ type SMTPServer struct {
|
||||
|
||||
// NewSMTPServer creates a new SMTP server instance.
|
||||
func NewSMTPServer(cfg config.SMTPConfig, stores *store.Stores, attStorage *storage.AttachmentStorage) *SMTPServer {
|
||||
s := &SMTPServer{
|
||||
stores: stores,
|
||||
storage: attStorage,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
be := &smtpBackend{server: s}
|
||||
srv := smtp.NewServer(be)
|
||||
srv.Addr = cfg.Addr
|
||||
srv.Domain = cfg.Domain
|
||||
srv.MaxMessageBytes = cfg.MaxMessage
|
||||
srv.AllowInsecureAuth = true
|
||||
srv.ReadTimeout = 60 * time.Second
|
||||
srv.WriteTimeout = 60 * time.Second
|
||||
|
||||
s.server = srv
|
||||
return s
|
||||
return &SMTPServer{stores: stores, storage: attStorage, cfg: cfg}
|
||||
}
|
||||
|
||||
// Start starts the SMTP server on the plain-text port.
|
||||
func (s *SMTPServer) Start() error {
|
||||
log.Printf("SMTP server listening on %s", s.cfg.Addr)
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
|
||||
// StartTLS starts the SMTP server on the TLS port.
|
||||
func (s *SMTPServer) StartTLS() error {
|
||||
func (s *SMTPServer) tlsConfig() (*tls.Config, error) {
|
||||
if s.cfg.TLSCert == "" || s.cfg.TLSKey == "" {
|
||||
return fmt.Errorf("SMTP TLS certificate or key not configured")
|
||||
return nil, fmt.Errorf("SMTP TLS certificate or key not configured")
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(s.cfg.TLSCert, s.cfg.TLSKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load SMTP TLS certificate: %w", err)
|
||||
return nil, fmt.Errorf("failed to load SMTP TLS certificate: %w", err)
|
||||
}
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
|
||||
}
|
||||
|
||||
// 创建一个新的 SMTP 服务器实例用于 TLS 端口
|
||||
be := &smtpBackend{server: s}
|
||||
func (s *SMTPServer) newServer(addr string, mode smtpMode, tlsConfig *tls.Config) *smtp.Server {
|
||||
be := &smtpBackend{server: s, mode: mode}
|
||||
srv := smtp.NewServer(be)
|
||||
srv.Addr = s.cfg.TLSAddr
|
||||
srv.Addr = addr
|
||||
srv.Domain = s.cfg.Domain
|
||||
srv.MaxMessageBytes = s.cfg.MaxMessage
|
||||
srv.AllowInsecureAuth = false
|
||||
srv.AllowInsecureAuth = tlsConfig == nil
|
||||
srv.ReadTimeout = 60 * time.Second
|
||||
srv.WriteTimeout = 60 * time.Second
|
||||
srv.TLSConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
srv.TLSConfig = tlsConfig
|
||||
return srv
|
||||
}
|
||||
|
||||
// Start starts the inbound SMTP server.
|
||||
func (s *SMTPServer) Start() error {
|
||||
tlsConfig, err := s.tlsConfig()
|
||||
if err != nil {
|
||||
log.Printf("SMTP STARTTLS 未启用: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("SMTP server listening on %s", s.cfg.Addr)
|
||||
return s.newServer(s.cfg.Addr, smtpModeInbound, tlsConfig).ListenAndServe()
|
||||
}
|
||||
|
||||
// StartTLS starts the implicit TLS SMTP submission server.
|
||||
func (s *SMTPServer) StartTLS() error {
|
||||
tlsConfig, err := s.tlsConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("SMTPS server listening on %s", s.cfg.TLSAddr)
|
||||
return srv.ListenAndServeTLS()
|
||||
return s.newServer(s.cfg.TLSAddr, smtpModeImplicitTLS, tlsConfig).ListenAndServeTLS()
|
||||
}
|
||||
|
||||
// StartSubmission starts the SMTP submission server with STARTTLS support.
|
||||
func (s *SMTPServer) StartSubmission() error {
|
||||
tlsConfig, err := s.tlsConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("SMTP submission server listening on %s", s.cfg.SubmissionAddr)
|
||||
return s.newServer(s.cfg.SubmissionAddr, smtpModeSubmission, tlsConfig).ListenAndServe()
|
||||
}
|
||||
|
||||
// smtpBackend implements the smtp.Backend interface.
|
||||
type smtpBackend struct {
|
||||
server *SMTPServer
|
||||
mode smtpMode
|
||||
}
|
||||
|
||||
// NewSession creates a new SMTP session for the incoming connection.
|
||||
func (be *smtpBackend) NewSession(c *smtp.Conn) (smtp.Session, error) {
|
||||
return &smtpSession{
|
||||
backend: be,
|
||||
rcpts: make([]string, 0),
|
||||
attachments: make([]*db.Attachment, 0),
|
||||
backend: be,
|
||||
mode: be.mode,
|
||||
rcpts: make([]string, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// smtpSession implements the smtp.Session interface for handling a single connection.
|
||||
type smtpSession struct {
|
||||
backend *smtpBackend
|
||||
from string
|
||||
rcpts []string
|
||||
backend *smtpBackend
|
||||
mode smtpMode
|
||||
from string
|
||||
rcpts []string
|
||||
authenticated bool
|
||||
username string
|
||||
attachments []*db.Attachment
|
||||
userID uint
|
||||
email string
|
||||
}
|
||||
|
||||
// AuthPlain authenticates the user with plain-text credentials.
|
||||
func (s *smtpSession) AuthPlain(username, password string) error {
|
||||
user, err := s.backend.server.stores.Users.Authenticate(username, password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("authentication failed: %w", err)
|
||||
// AuthMechanisms returns supported SMTP AUTH mechanisms.
|
||||
func (s *smtpSession) AuthMechanisms() []string {
|
||||
return []string{sasl.Plain}
|
||||
}
|
||||
|
||||
// Auth authenticates the user with SASL PLAIN credentials.
|
||||
func (s *smtpSession) Auth(mech string) (sasl.Server, error) {
|
||||
if mech != sasl.Plain {
|
||||
return nil, smtp.ErrAuthUnknownMechanism
|
||||
}
|
||||
s.authenticated = true
|
||||
s.username = user.Username
|
||||
return nil
|
||||
return sasl.NewPlainServer(func(identity, username, password string) error {
|
||||
user, err := s.backend.server.stores.Users.Authenticate(username, password)
|
||||
if err != nil {
|
||||
return smtp.ErrAuthFailed
|
||||
}
|
||||
|
||||
domainName := user.Domain.Name
|
||||
if domainName == "" {
|
||||
domain, err := s.backend.server.stores.Domains.GetByID(user.DomainID)
|
||||
if err == nil {
|
||||
domainName = domain.Name
|
||||
}
|
||||
}
|
||||
if domainName == "" {
|
||||
return smtp.ErrAuthFailed
|
||||
}
|
||||
|
||||
s.authenticated = true
|
||||
s.userID = user.ID
|
||||
s.email = user.Username + "@" + domainName
|
||||
return nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Mail records the sender address (MAIL FROM command).
|
||||
func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
|
||||
if s.mode != smtpModeInbound {
|
||||
if !s.authenticated {
|
||||
return smtp.ErrAuthRequired
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(from), s.email) {
|
||||
return fmt.Errorf("sender address must match authenticated user")
|
||||
}
|
||||
}
|
||||
|
||||
s.from = from
|
||||
s.rcpts = s.rcpts[:0]
|
||||
s.attachments = s.attachments[:0]
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rcpt validates and records a recipient address (RCPT TO command).
|
||||
// It verifies that the recipient domain exists in the system and the user exists.
|
||||
func (s *smtpSession) Rcpt(to string, opts *smtp.RcptOptions) error {
|
||||
parts := strings.SplitN(to, "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid recipient address: %s", to)
|
||||
}
|
||||
|
||||
userName := parts[0]
|
||||
domainName := parts[1]
|
||||
|
||||
// Check if domain is managed by this system
|
||||
domain, err := s.backend.server.stores.Domains.GetByName(domainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("domain not found: %s", domainName)
|
||||
}
|
||||
|
||||
// Check if the user exists in this domain
|
||||
_, err = s.backend.server.stores.Users.GetByUsername(userName, domain.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %s", to)
|
||||
if _, err := s.localUserByEmail(to); err != nil {
|
||||
if s.authenticated {
|
||||
return fmt.Errorf("external relay is not supported yet: %s", to)
|
||||
}
|
||||
return fmt.Errorf("relay access denied: %s", to)
|
||||
}
|
||||
|
||||
s.rcpts = append(s.rcpts, to)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Data handles the message body (DATA command). It parses the MIME message,
|
||||
// extracts fields and attachments, and stores the message for each recipient.
|
||||
func (s *smtpSession) localUserByEmail(email string) (*db.User, error) {
|
||||
return s.backend.server.stores.Users.GetByEmail(strings.TrimSpace(email))
|
||||
}
|
||||
|
||||
// Data handles the message body and stores it for local recipients.
|
||||
func (s *smtpSession) Data(r io.Reader) error {
|
||||
// Read all message data
|
||||
if len(s.rcpts) == 0 {
|
||||
return fmt.Errorf("no accepted local recipients")
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read message data: %w", err)
|
||||
}
|
||||
|
||||
// Parse as MIME message
|
||||
parsed, err := parseSMTPMessage(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rcpt := range s.rcpts {
|
||||
user, err := s.localUserByEmail(rcpt)
|
||||
if err != nil {
|
||||
log.Printf("SMTP: recipient not found %s, skipping", rcpt)
|
||||
continue
|
||||
}
|
||||
if err := s.saveMessage(user.ID, "INBOX", parsed, data, false); err != nil {
|
||||
log.Printf("SMTP: failed to create message for %s: %v", rcpt, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("SMTP: message delivered to %s", rcpt)
|
||||
}
|
||||
|
||||
if s.authenticated && s.userID != 0 && s.mode != smtpModeInbound {
|
||||
if err := s.saveMessage(s.userID, "Sent", parsed, data, true); err != nil {
|
||||
log.Printf("SMTP: failed to save sent copy for %s: %v", s.email, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type parsedSMTPMessage struct {
|
||||
messageID string
|
||||
fromAddr string
|
||||
toAddr string
|
||||
ccAddr string
|
||||
subject string
|
||||
textBody string
|
||||
htmlBody string
|
||||
date time.Time
|
||||
attachments []*db.Attachment
|
||||
}
|
||||
|
||||
func parseSMTPMessage(data []byte) (*parsedSMTPMessage, error) {
|
||||
mr, err := mail.CreateReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse MIME message: %w", err)
|
||||
return nil, fmt.Errorf("failed to parse MIME message: %w", err)
|
||||
}
|
||||
|
||||
// Extract headers — 使用 AddressList 解码 RFC 2047 编码的地址
|
||||
header := mr.Header
|
||||
|
||||
fromAddr := mailutil.FormatAddressList(&header, "From")
|
||||
toAddr := mailutil.FormatAddressList(&header, "To")
|
||||
ccAddr := mailutil.FormatAddressList(&header, "Cc")
|
||||
subject, _ := header.Subject()
|
||||
messageID, _ := header.MessageID()
|
||||
date, _ := header.Date()
|
||||
if date.IsZero() {
|
||||
date = time.Now()
|
||||
msg := &parsedSMTPMessage{}
|
||||
msg.fromAddr = mailutil.FormatAddressList(&header, "From")
|
||||
msg.toAddr = mailutil.FormatAddressList(&header, "To")
|
||||
msg.ccAddr = mailutil.FormatAddressList(&header, "Cc")
|
||||
msg.subject, _ = header.Subject()
|
||||
msg.messageID, _ = header.MessageID()
|
||||
msg.date, _ = header.Date()
|
||||
if msg.date.IsZero() {
|
||||
msg.date = time.Now()
|
||||
}
|
||||
|
||||
var textBody, htmlBody string
|
||||
var attachments []*db.Attachment
|
||||
|
||||
// Iterate through all MIME parts
|
||||
for {
|
||||
p, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
@@ -202,16 +277,15 @@ func (s *smtpSession) Data(r io.Reader) error {
|
||||
log.Printf("SMTP: error reading inline part: %v", readErr)
|
||||
continue
|
||||
}
|
||||
// 检测并转换字符集
|
||||
charset := ""
|
||||
if cs, ok := params["charset"]; ok {
|
||||
charset = cs
|
||||
}
|
||||
decoded := mailutil.DecodeCharset(buf, charset)
|
||||
if strings.HasPrefix(contentType, "text/plain") {
|
||||
textBody = decoded
|
||||
msg.textBody = decoded
|
||||
} else if strings.HasPrefix(contentType, "text/html") {
|
||||
htmlBody = decoded
|
||||
msg.htmlBody = decoded
|
||||
}
|
||||
|
||||
case *mail.AttachmentHeader:
|
||||
@@ -225,83 +299,43 @@ func (s *smtpSession) Data(r io.Reader) error {
|
||||
log.Printf("SMTP: error reading attachment part: %v", readErr)
|
||||
continue
|
||||
}
|
||||
|
||||
relPath, saveErr := s.backend.server.storage.Save(filename, buf)
|
||||
if saveErr != nil {
|
||||
log.Printf("SMTP: failed to save attachment %s: %v", filename, saveErr)
|
||||
continue
|
||||
}
|
||||
|
||||
attachments = append(attachments, &db.Attachment{
|
||||
msg.attachments = append(msg.attachments, &db.Attachment{
|
||||
FileName: filename,
|
||||
FilePath: relPath,
|
||||
ContentType: contentType,
|
||||
FileSize: int64(len(buf)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no text body was extracted from MIME parts, use the raw data
|
||||
if textBody == "" && htmlBody == "" {
|
||||
textBody = string(data)
|
||||
if msg.textBody == "" && msg.htmlBody == "" {
|
||||
msg.textBody = string(data)
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// Create a Message record for each verified recipient
|
||||
for _, rcpt := range s.rcpts {
|
||||
user, err := s.backend.server.stores.Users.GetByEmail(rcpt)
|
||||
if err != nil {
|
||||
log.Printf("SMTP: recipient not found %s, skipping", rcpt)
|
||||
continue
|
||||
}
|
||||
|
||||
msg := &db.Message{
|
||||
UserID: user.ID,
|
||||
MessageID: messageID,
|
||||
Folder: "INBOX",
|
||||
FromAddr: fromAddr,
|
||||
ToAddr: toAddr,
|
||||
CcAddr: ccAddr,
|
||||
Subject: subject,
|
||||
TextBody: textBody,
|
||||
HtmlBody: htmlBody,
|
||||
RawData: string(data),
|
||||
IsRead: false,
|
||||
IsFlagged: false,
|
||||
Date: date,
|
||||
}
|
||||
|
||||
if createErr := s.backend.server.stores.Mails.Create(msg); createErr != nil {
|
||||
log.Printf("SMTP: failed to create message for %s: %v", rcpt, createErr)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create Attachment records linked to the new message
|
||||
for _, att := range attachments {
|
||||
attCopy := db.Attachment{
|
||||
MessageID: msg.ID,
|
||||
FileName: att.FileName,
|
||||
FilePath: att.FilePath,
|
||||
ContentType: att.ContentType,
|
||||
FileSize: att.FileSize,
|
||||
}
|
||||
if attErr := s.backend.server.stores.Attachments.Create(&attCopy); attErr != nil {
|
||||
log.Printf("SMTP: failed to create attachment record: %v", attErr)
|
||||
}
|
||||
// Update user used bytes for received attachments
|
||||
_ = s.backend.server.stores.Users.UpdateUsedBytes(user.ID, attCopy.FileSize)
|
||||
}
|
||||
|
||||
log.Printf("SMTP: message delivered to %s (ID=%d)", rcpt, msg.ID)
|
||||
func (s *smtpSession) saveMessage(userID uint, folder string, parsed *parsedSMTPMessage, data []byte, read bool) error {
|
||||
msg := &db.Message{
|
||||
UserID: userID,
|
||||
MessageID: parsed.messageID,
|
||||
Folder: folder,
|
||||
FromAddr: parsed.fromAddr,
|
||||
ToAddr: parsed.toAddr,
|
||||
CcAddr: parsed.ccAddr,
|
||||
Subject: parsed.subject,
|
||||
TextBody: parsed.textBody,
|
||||
HtmlBody: parsed.htmlBody,
|
||||
RawData: string(data),
|
||||
IsRead: read,
|
||||
IsFlagged: false,
|
||||
Date: parsed.date,
|
||||
}
|
||||
|
||||
return nil
|
||||
return s.backend.server.stores.Mails.Create(msg)
|
||||
}
|
||||
|
||||
// Reset clears the session state for the next message on the same connection.
|
||||
func (s *smtpSession) Reset() {
|
||||
s.from = ""
|
||||
s.rcpts = s.rcpts[:0]
|
||||
s.attachments = s.attachments[:0]
|
||||
}
|
||||
|
||||
// Logout is called when the SMTP connection is closed.
|
||||
|
||||
Reference in New Issue
Block a user