保存邮件超时修复工作进度,准备接入可选登录验证
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-18 20:34:48 +08:00
parent 47777feb6d
commit 04cc5774e0
4 changed files with 108 additions and 18 deletions

View File

@ -1,6 +1,7 @@
package verification
import (
"context"
"errors"
"fmt"
"mime"
@ -9,6 +10,7 @@ import (
"net/smtp"
"strconv"
"strings"
"time"
"git.pcux.cn/xuans/Smail/services/logstore"
smailsmtp "git.pcux.cn/xuans/Smail/utils/smtp"
@ -17,6 +19,7 @@ import (
type SecurityMode string
const (
MailDeliveryTimeout = 45 * time.Second
SecurityStartTLS SecurityMode = "starttls"
SecurityImplicitTLS SecurityMode = "tls"
SecurityPlain SecurityMode = "plain"
@ -166,6 +169,13 @@ func isValidSMTPHost(host string) bool {
}
func sendSMTPMessage(settings SMTPSettings, recipient string, message []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), MailDeliveryTimeout)
defer cancel()
return SendSMTPMessageContext(ctx, settings, recipient, message)
}
// SendSMTPMessageContext 允许验证码中继与后备投递共享同一总时限。
func SendSMTPMessageContext(ctx context.Context, settings SMTPSettings, recipient string, message []byte) error {
addr := net.JoinHostPort(settings.Host, strconv.Itoa(settings.Port))
var auth smtp.Auth
if settings.Username != "" {
@ -180,11 +190,11 @@ func sendSMTPMessage(settings SMTPSettings, recipient string, message []byte) er
}
switch settings.Security {
case SecurityStartTLS:
return smailsmtp.SendMail(settings.Host, addr, auth, settings.From, fromDomain, []string{recipient}, message)
return smailsmtp.SendMailContext(ctx, settings.Host, addr, auth, settings.From, fromDomain, []string{recipient}, message)
case SecurityImplicitTLS:
return smailsmtp.SendMailWithTls(settings.Host, addr, auth, settings.From, fromDomain, []string{recipient}, message)
return smailsmtp.SendMailWithTLSContext(ctx, settings.Host, addr, auth, settings.From, fromDomain, []string{recipient}, message)
case SecurityPlain:
return smailsmtp.SendMailUnsafe(settings.Host, addr, auth, settings.From, fromDomain, []string{recipient}, message)
return smailsmtp.SendMailUnsafeContext(ctx, settings.Host, addr, auth, settings.From, fromDomain, []string{recipient}, message)
default:
return ErrUnsupportedSMTP
}

View File

@ -18,7 +18,11 @@ var errNullMX = errors.New("收件域名声明不接收邮件")
type mxLookup func(stdcontext.Context, string) ([]*net.MX, error)
func resolveMX(domain string, lookup mxLookup) ([]string, error) {
ctx, cancel := stdcontext.WithTimeout(stdcontext.Background(), 10*time.Second)
return resolveMXContext(stdcontext.Background(), domain, lookup)
}
func resolveMXContext(parent stdcontext.Context, domain string, lookup mxLookup) ([]string, error) {
ctx, cancel := stdcontext.WithTimeout(parent, 10*time.Second)
defer cancel()
records, err := lookup(ctx, domain)
if err != nil {
@ -60,9 +64,16 @@ func stopFallback(err error) bool {
// deliverMX 只在确认本次未接收时切换安全通道,DATA 不确定结果不进入下一条路由。
func deliverMX(hosts []string, attempt func(host, port string) error) error {
return deliverMXContext(stdcontext.Background(), hosts, attempt)
}
func deliverMXContext(ctx stdcontext.Context, hosts []string, attempt func(host, port string) error) error {
var lastErr error
for _, host := range hosts {
for _, port := range []string{"25", "587", "465"} {
if err := ctx.Err(); err != nil {
return err
}
lastErr = attempt(host, port)
if lastErr == nil || stopFallback(lastErr) {
return lastErr

View File

@ -1,6 +1,7 @@
package send
import (
stdcontext "context"
"crypto/tls"
"errors"
"fmt"
@ -152,6 +153,10 @@ func parseRecipientAddress(user *parsemail.User) (string, string, error) {
}
func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemail.User, from string, triggers ...string) (sendErr error, recipientErrors map[string]error) {
deliveryContext := stdcontext.Background()
if ctx != nil && ctx.Context != nil {
deliveryContext = ctx.Context
}
var accepted atomic.Int64
trigger := "邮件发送"
if len(triggers) > 0 {
@ -243,7 +248,7 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai
}()
if domain == consts.TEST_DOMAIN {
err := smtp.SendMailUnsafe("localhost", "127.0.0.1:25", nil, from, fromDomain, buildAddress(tos), data)
err := smtp.SendMailUnsafeContext(deliveryContext, "localhost", "127.0.0.1:25", nil, from, fromDomain, buildAddress(tos), data)
if err != nil {
recordFailure(err)
} else {
@ -252,17 +257,17 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai
return
}
hosts, err := resolveMX(domain, net.DefaultResolver.LookupMX)
hosts, err := resolveMXContext(deliveryContext, domain, net.DefaultResolver.LookupMX)
if err != nil {
recordFailure(err)
return
}
err = deliverMX(hosts, func(host, port string) error {
err = deliverMXContext(deliveryContext, hosts, func(host, port string) error {
address := net.JoinHostPort(host, port)
if port == "465" {
return smtp.SendMailWithTls(host, address, nil, from, fromDomain, buildAddress(tos), data)
return smtp.SendMailWithTLSContext(deliveryContext, host, address, nil, from, fromDomain, buildAddress(tos), data)
}
return smtp.SendMail(host, address, nil, from, fromDomain, buildAddress(tos), data)
return smtp.SendMailContext(deliveryContext, host, address, nil, from, fromDomain, buildAddress(tos), data)
})
if err != nil {
recordFailure(err)

View File

@ -63,6 +63,7 @@ func IsDeliveryUncertain(err error) bool {
const (
outboundSMTPDialTimeout = 2 * time.Second
outboundSMTPGreetingTimeout = 5 * time.Second
outboundSMTPTLSHandshakeTimeout = 2 * time.Second
outboundSMTPTransactionTimeout = 2 * time.Minute
outboundSMTPQuitWriteTimeout = 250 * time.Millisecond
@ -89,6 +90,8 @@ type Client struct {
localName string // the name to use in HELO/EHLO
didHello bool // whether we've said HELO/EHLO
helloError error // the error from the hello
ctx context.Context
stopCancel func() bool
}
// Dial returns a new Client connected to an SMTP server at addr.
@ -98,7 +101,11 @@ func Dial(addr, fromDomain string) (*Client, error) {
}
func dial(addr, fromDomain string, publicOnly bool) (*Client, error) {
conn, err := dialTCP(addr, publicOnly)
return dialContext(context.Background(), addr, fromDomain, publicOnly)
}
func dialContext(ctx context.Context, addr, fromDomain string, publicOnly bool) (*Client, error) {
conn, err := dialTCPContext(ctx, addr, publicOnly)
if err != nil {
return nil, err
}
@ -107,7 +114,7 @@ func dial(addr, fromDomain string, publicOnly bool) (*Client, error) {
_ = conn.Close()
return nil, err
}
return newClientWithTimeout(conn, host, fromDomain, outboundSMTPTransactionTimeout)
return newClientWithContext(ctx, conn, host, fromDomain)
}
// with tls
@ -116,6 +123,10 @@ func DialTls(addr, domain, fromDomain string) (*Client, error) {
}
func dialTLS(addr, domain, fromDomain string, publicOnly bool) (*Client, error) {
return dialTLSContext(context.Background(), addr, domain, fromDomain, publicOnly)
}
func dialTLSContext(ctx context.Context, addr, domain, fromDomain string, publicOnly bool) (*Client, error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
@ -129,16 +140,50 @@ func dialTLS(addr, domain, fromDomain string, publicOnly bool) (*Client, error)
ServerName: domain,
}
conn, err := dialTCP(addr, publicOnly)
conn, err := dialTCPContext(ctx, addr, publicOnly)
if err != nil {
return nil, err
}
stopCancel := context.AfterFunc(ctx, func() { _ = conn.Close() })
defer stopCancel()
tlsConn, err := handshakeTLS(conn, tlsconfig, outboundSMTPTLSHandshakeTimeout)
if err != nil {
_ = conn.Close()
return nil, err
}
return newClientWithTimeout(tlsConn, host, fromDomain, outboundSMTPTransactionTimeout)
return newClientWithContext(ctx, tlsConn, host, fromDomain)
}
// newClientWithContext 将欢迎响应、命令和 TLS 升级限制在同一次投递时限内。
func newClientWithContext(ctx context.Context, conn net.Conn, host, fromDomain string) (*Client, error) {
stopCancel := context.AfterFunc(ctx, func() { _ = conn.Close() })
if err := conn.SetDeadline(smtpDeadline(ctx, outboundSMTPGreetingTimeout)); err != nil {
stopCancel()
_ = conn.Close()
return nil, err
}
client, err := NewClient(conn, host, fromDomain)
if err != nil {
stopCancel()
return nil, err
}
client.ctx = ctx
client.stopCancel = stopCancel
if err := conn.SetDeadline(smtpDeadline(ctx, outboundSMTPTransactionTimeout)); err != nil {
_ = client.Close()
return nil, err
}
return client, nil
}
func smtpDeadline(ctx context.Context, timeout time.Duration) time.Time {
deadline := time.Now().Add(timeout)
if ctx != nil {
if limit, ok := ctx.Deadline(); ok && limit.Before(deadline) {
return limit
}
}
return deadline
}
func handshakeTLS(conn net.Conn, config *tls.Config, timeout time.Duration) (*tls.Conn, error) {
@ -161,12 +206,16 @@ func newClientWithTimeout(conn net.Conn, host, fromDomain string, timeout time.D
}
func dialTCP(addr string, publicOnly bool) (net.Conn, error) {
return dialTCPContext(context.Background(), addr, publicOnly)
}
func dialTCPContext(parent context.Context, addr string, publicOnly bool) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil || host == "" || port == "" {
return nil, errors.New("smtp: invalid remote address")
}
ctx, cancel := context.WithTimeout(context.Background(), outboundSMTPDialTimeout)
ctx, cancel := context.WithTimeout(parent, outboundSMTPDialTimeout)
defer cancel()
addresses, err := resolveRemoteAddresses(ctx, host)
if err != nil {
@ -326,6 +375,9 @@ func NewClient(conn net.Conn, host, fromDomain string) (*Client, error) {
// Close closes the connection.
func (c *Client) Close() error {
if c.stopCancel != nil {
c.stopCancel()
}
return c.Text.Close()
}
@ -426,7 +478,7 @@ func (c *Client) StartTLS(config *tls.Config) error {
if err != nil {
return err
}
if err = tlsConn.SetDeadline(time.Now().Add(outboundSMTPTransactionTimeout)); err != nil {
if err = tlsConn.SetDeadline(smtpDeadline(c.ctx, outboundSMTPTransactionTimeout)); err != nil {
return err
}
c.conn = tlsConn
@ -593,6 +645,10 @@ func finishDelivery(c *Client, w io.WriteCloser, msg []byte) error {
}
func SendMailWithTls(domain string, addr string, a smtp.Auth, from string, fromDomain string, to []string, msg []byte) error {
return SendMailWithTLSContext(context.Background(), domain, addr, a, from, fromDomain, to, msg)
}
func SendMailWithTLSContext(ctx context.Context, domain string, addr string, a smtp.Auth, from string, fromDomain string, to []string, msg []byte) error {
if err := validateLine(from); err != nil {
return err
}
@ -601,7 +657,7 @@ func SendMailWithTls(domain string, addr string, a smtp.Auth, from string, fromD
return err
}
}
c, err := DialTls(addr, domain, fromDomain)
c, err := dialTLSContext(ctx, addr, domain, fromDomain, true)
if err != nil {
return err
}
@ -654,6 +710,10 @@ func SendMailWithTls(domain string, addr string, a smtp.Auth, from string, fromD
// library.
// 修复TSL验证问题
func SendMail(domain string, addr string, a smtp.Auth, from string, fromDomain string, to []string, msg []byte) error {
return SendMailContext(context.Background(), domain, addr, a, from, fromDomain, to, msg)
}
func SendMailContext(ctx context.Context, domain string, addr string, a smtp.Auth, from string, fromDomain string, to []string, msg []byte) error {
log.Debugf("SMTP delivery started: recipient_count=%d", len(to))
@ -665,7 +725,7 @@ func SendMail(domain string, addr string, a smtp.Auth, from string, fromDomain s
return err
}
}
c, err := Dial(addr, fromDomain)
c, err := dialContext(ctx, addr, fromDomain, true)
if err != nil {
return err
}
@ -713,6 +773,10 @@ func SendMail(domain string, addr string, a smtp.Auth, from string, fromDomain s
// SendMailUnsafe 无TLS加密的邮件发送方式
func SendMailUnsafe(domain string, addr string, a smtp.Auth, from string, fromDomain string, to []string, msg []byte) error {
return SendMailUnsafeContext(context.Background(), domain, addr, a, from, fromDomain, to, msg)
}
func SendMailUnsafeContext(ctx context.Context, domain string, addr string, a smtp.Auth, from string, fromDomain string, to []string, msg []byte) error {
if err := validateUnsafePlaintextTarget(domain, addr); err != nil {
return err
}
@ -724,7 +788,7 @@ func SendMailUnsafe(domain string, addr string, a smtp.Auth, from string, fromDo
return err
}
}
c, err := dial(addr, fromDomain, false)
c, err := dialContext(ctx, addr, fromDomain, false)
if err != nil {
return err
}