This commit is contained in:
parent
47777feb6d
commit
04cc5774e0
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user