Files
NewSzxcn-Email/apps/api/internal/app/forwarding_delivery.go
T
2026-08-02 20:32:34 +08:00

240 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"strings"
"unicode"
)
const forwardingHeaderName = "X-LanQin-Forwarded-By"
func (a *App) processInboundForwarding(ctx context.Context, messageID, mailboxID string, raw []byte) {
targets, userID, mailboxAddress, err := a.inboundForwardingTargets(ctx, mailboxID)
if err != nil {
a.log.Warn("failed to load forwarding target", "message", messageID, "mailbox", mailboxID, "error", err)
return
}
if len(targets) == 0 || userID == "" || mailboxAddress == "" {
return
}
self := normalizeEmail(mailboxAddress)
filteredTargets := make([]string, 0, len(targets))
for _, target := range targets {
if normalizeEmail(target) == self {
continue
}
filteredTargets = append(filteredTargets, target)
}
targets = dedupeEmails(filteredTargets)
if len(targets) == 0 {
return
}
if len(raw) == 0 {
raw, err = a.forwardingRawMessage(ctx, messageID)
if err != nil {
a.log.Warn("failed to load raw message for forwarding", "message", messageID, "error", err)
return
}
}
if hasForwardingHeader(raw) {
a.log.Warn("skip forwarding message that already has LanQin forwarding header", "message", messageID, "mailbox", mailboxID)
return
}
forwarded := addForwardingHeaders(raw, mailboxAddress, a.cfg.PublicHostname)
var rfcMessageID string
_ = a.db.QueryRowContext(ctx, `SELECT message_id FROM messages WHERE id=?`, messageID).Scan(&rfcMessageID)
if strings.TrimSpace(rfcMessageID) == "" {
rfcMessageID = messageID
}
queueID, err := a.enqueueSend(ctx, sendQueueInput{
UserID: userID,
MailboxID: mailboxID,
SentMessageID: messageID,
MessageID: rfcMessageID,
Source: sendSourceForwarding,
MailFrom: mailboxAddress,
HeaderFrom: mailboxAddress,
Recipients: targets,
MIMEBytes: forwarded,
Now: a.now().UTC(),
})
if err != nil {
a.log.Warn("failed to enqueue inbound forwarding", "message", messageID, "mailbox", mailboxID, "targets", strings.Join(targets, ","), "error", err)
return
}
if queueID == "" {
a.log.Warn("forwarding target configured but SMTP sending is not configured", "message", messageID, "mailbox", mailboxID, "targets", strings.Join(targets, ","))
}
}
func (a *App) processRuleForwarding(ctx context.Context, messageID, mailboxID string, action MailRuleAction) error {
var userID, mailboxAddress string
if err := a.db.QueryRowContext(ctx, `SELECT user_id,address FROM mailboxes WHERE id=? AND status='active'`, mailboxID).Scan(&userID, &mailboxAddress); err != nil {
return err
}
targets, err := a.cleanForwardingTargets(ctx, userID, splitRuleForwardTargets(action.Value))
if err != nil {
return err
}
self := normalizeEmail(mailboxAddress)
filteredTargets := make([]string, 0, len(targets))
for _, target := range targets {
if normalizeEmail(target) == self {
continue
}
filteredTargets = append(filteredTargets, target)
}
targets = dedupeEmails(filteredTargets)
if len(targets) == 0 {
return nil
}
raw, err := a.forwardingRawMessage(ctx, messageID)
if err != nil {
return err
}
if hasForwardingHeader(raw) {
a.log.Warn("skip rule forwarding message that already has LanQin forwarding header", "message", messageID, "mailbox", mailboxID)
return nil
}
forwarded := addForwardingHeaders(raw, mailboxAddress, a.cfg.PublicHostname)
var rfcMessageID string
_ = a.db.QueryRowContext(ctx, `SELECT message_id FROM messages WHERE id=?`, messageID).Scan(&rfcMessageID)
if strings.TrimSpace(rfcMessageID) == "" {
rfcMessageID = messageID
}
queueID, err := a.enqueueSend(ctx, sendQueueInput{
UserID: userID,
MailboxID: mailboxID,
SentMessageID: messageID,
MessageID: ruleForwardQueueMessageID(rfcMessageID, targets),
Source: sendSourceRuleForwarding,
MailFrom: mailboxAddress,
HeaderFrom: mailboxAddress,
Recipients: targets,
MIMEBytes: forwarded,
Now: a.now().UTC(),
})
if err != nil {
return err
}
if queueID == "" {
a.log.Warn("rule forwarding target configured but SMTP sending is not configured", "message", messageID, "mailbox", mailboxID, "targets", strings.Join(targets, ","))
}
return nil
}
func (a *App) inboundForwardingTargets(ctx context.Context, mailboxID string) (targetEmails []string, userID, mailboxAddress string, err error) {
var mailboxTarget, mailboxTargetsJSON, accountTarget, accountTargetsJSON string
err = a.db.QueryRowContext(ctx, `SELECT mb.user_id,mb.address,COALESCE(mfs.target_email,''),COALESCE(mfs.target_emails,'[]'),COALESCE(afs.target_email,''),COALESCE(afs.target_emails,'[]')
FROM mailboxes mb
LEFT JOIN mailbox_forwarding_settings mfs ON mfs.mailbox_id=mb.id
LEFT JOIN account_forwarding_settings afs ON afs.user_id=mb.user_id
WHERE mb.id=? AND mb.status='active'`, mailboxID).Scan(&userID, &mailboxAddress, &mailboxTarget, &mailboxTargetsJSON, &accountTarget, &accountTargetsJSON)
if err != nil {
return nil, "", "", err
}
targets := forwardingTargetsFromStored(mailboxTarget, mailboxTargetsJSON)
if len(targets) == 0 {
targets = forwardingTargetsFromStored(accountTarget, accountTargetsJSON)
}
if len(targets) == 0 {
return nil, userID, mailboxAddress, nil
}
verifiedTargets := make([]string, 0, len(targets))
for _, target := range targets {
verified, err := a.forwardingEmailVerified(ctx, userID, target)
if err != nil {
return nil, "", "", err
}
if verified {
verifiedTargets = append(verifiedTargets, target)
}
}
return dedupeEmails(verifiedTargets), userID, mailboxAddress, nil
}
func (a *App) forwardingRawMessage(ctx context.Context, messageID string) ([]byte, error) {
msg, err := a.storedMessageByID(ctx, messageID)
if err != nil {
return nil, err
}
if strings.TrimSpace(msg.RawPath) != "" {
if ok, err := a.pathIsUnderMaildirRoot(msg.RawPath); err == nil && ok {
if raw, err := os.ReadFile(msg.RawPath); err == nil {
return raw, nil
}
}
}
attachments, err := a.attachmentInputsForMessage(ctx, messageID)
if err != nil {
return nil, err
}
return BuildMIME(MIMEMessage{
From: msg.From,
FromName: msg.FromName,
To: msg.To,
CC: msg.CC,
BCC: msg.BCC,
Subject: msg.Subject,
Text: msg.BodyText,
HTML: msg.BodyHTML,
MessageID: msg.MessageID,
Date: messageDate(msg),
Attachments: attachments,
})
}
func splitRuleForwardTargets(value string) []string {
return strings.FieldsFunc(value, func(r rune) bool {
return unicode.IsSpace(r) || r == ',' || r == '' || r == ';' || r == ''
})
}
func ruleForwardQueueMessageID(messageID string, targets []string) string {
base := strings.TrimSpace(messageID)
if base == "" {
base = newID("ruleforward")
}
sum := sha256.Sum256([]byte(strings.Join(dedupeEmails(targets), ",")))
return base + "#rule-forward-" + hex.EncodeToString(sum[:])[:12]
}
func hasForwardingHeader(raw []byte) bool {
header := raw
if idx := bytes.Index(raw, []byte("\r\n\r\n")); idx >= 0 {
header = raw[:idx]
} else if idx := bytes.Index(raw, []byte("\n\n")); idx >= 0 {
header = raw[:idx]
}
return strings.Contains(strings.ToLower(string(header)), strings.ToLower(forwardingHeaderName)+":")
}
func addForwardingHeaders(raw []byte, mailboxAddress, hostname string) []byte {
hostname = strings.TrimSpace(hostname)
if hostname == "" {
hostname = "lanqin.local"
}
header := fmt.Sprintf("%s: %s\r\nX-LanQin-Forwarded-For: %s\r\n", forwardingHeaderName, hostname, normalizeEmail(mailboxAddress))
if idx := bytes.Index(raw, []byte("\r\n\r\n")); idx >= 0 {
out := make([]byte, 0, len(raw)+len(header))
out = append(out, raw[:idx]...)
out = append(out, []byte("\r\n"+header)...)
out = append(out, raw[idx+2:]...)
return out
}
if idx := bytes.Index(raw, []byte("\n\n")); idx >= 0 {
lfHeader := strings.ReplaceAll(header, "\r\n", "\n")
out := make([]byte, 0, len(raw)+len(lfHeader))
out = append(out, raw[:idx]...)
out = append(out, []byte("\n"+lfHeader)...)
out = append(out, raw[idx+1:]...)
return out
}
return append([]byte(header+"\r\n"), raw...)
}