保存邮件超时修复工作进度,准备接入可选登录验证
This commit is contained in:
parent
b33126e11d
commit
9706e52a29
@ -1,6 +1,7 @@
|
|||||||
package verification
|
package verification
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
@ -9,6 +10,7 @@ import (
|
|||||||
"net/smtp"
|
"net/smtp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.pcux.cn/xuans/Smail/services/logstore"
|
"git.pcux.cn/xuans/Smail/services/logstore"
|
||||||
smailsmtp "git.pcux.cn/xuans/Smail/utils/smtp"
|
smailsmtp "git.pcux.cn/xuans/Smail/utils/smtp"
|
||||||
@ -17,6 +19,7 @@ import (
|
|||||||
type SecurityMode string
|
type SecurityMode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
MailDeliveryTimeout = 45 * time.Second
|
||||||
SecurityStartTLS SecurityMode = "starttls"
|
SecurityStartTLS SecurityMode = "starttls"
|
||||||
SecurityImplicitTLS SecurityMode = "tls"
|
SecurityImplicitTLS SecurityMode = "tls"
|
||||||
SecurityPlain SecurityMode = "plain"
|
SecurityPlain SecurityMode = "plain"
|
||||||
@ -166,6 +169,13 @@ func isValidSMTPHost(host string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendSMTPMessage(settings SMTPSettings, recipient string, message []byte) error {
|
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))
|
addr := net.JoinHostPort(settings.Host, strconv.Itoa(settings.Port))
|
||||||
var auth smtp.Auth
|
var auth smtp.Auth
|
||||||
if settings.Username != "" {
|
if settings.Username != "" {
|
||||||
@ -180,11 +190,11 @@ func sendSMTPMessage(settings SMTPSettings, recipient string, message []byte) er
|
|||||||
}
|
}
|
||||||
switch settings.Security {
|
switch settings.Security {
|
||||||
case SecurityStartTLS:
|
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:
|
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:
|
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:
|
default:
|
||||||
return ErrUnsupportedSMTP
|
return ErrUnsupportedSMTP
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,11 @@ var errNullMX = errors.New("收件域名声明不接收邮件")
|
|||||||
type mxLookup func(stdcontext.Context, string) ([]*net.MX, error)
|
type mxLookup func(stdcontext.Context, string) ([]*net.MX, error)
|
||||||
|
|
||||||
func resolveMX(domain string, lookup mxLookup) ([]string, 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()
|
defer cancel()
|
||||||
records, err := lookup(ctx, domain)
|
records, err := lookup(ctx, domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -60,9 +64,16 @@ func stopFallback(err error) bool {
|
|||||||
|
|
||||||
// deliverMX 只在确认本次未接收时切换安全通道,DATA 不确定结果不进入下一条路由。
|
// deliverMX 只在确认本次未接收时切换安全通道,DATA 不确定结果不进入下一条路由。
|
||||||
func deliverMX(hosts []string, attempt func(host, port string) error) error {
|
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
|
var lastErr error
|
||||||
for _, host := range hosts {
|
for _, host := range hosts {
|
||||||
for _, port := range []string{"25", "587", "465"} {
|
for _, port := range []string{"25", "587", "465"} {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
lastErr = attempt(host, port)
|
lastErr = attempt(host, port)
|
||||||
if lastErr == nil || stopFallback(lastErr) {
|
if lastErr == nil || stopFallback(lastErr) {
|
||||||
return lastErr
|
return lastErr
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package send
|
package send
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
stdcontext "context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"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) {
|
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
|
var accepted atomic.Int64
|
||||||
trigger := "邮件发送"
|
trigger := "邮件发送"
|
||||||
if len(triggers) > 0 {
|
if len(triggers) > 0 {
|
||||||
@ -243,7 +248,7 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if domain == consts.TEST_DOMAIN {
|
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 {
|
if err != nil {
|
||||||
recordFailure(err)
|
recordFailure(err)
|
||||||
} else {
|
} else {
|
||||||
@ -252,17 +257,17 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts, err := resolveMX(domain, net.DefaultResolver.LookupMX)
|
hosts, err := resolveMXContext(deliveryContext, domain, net.DefaultResolver.LookupMX)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordFailure(err)
|
recordFailure(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = deliverMX(hosts, func(host, port string) error {
|
err = deliverMXContext(deliveryContext, hosts, func(host, port string) error {
|
||||||
address := net.JoinHostPort(host, port)
|
address := net.JoinHostPort(host, port)
|
||||||
if port == "465" {
|
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 {
|
if err != nil {
|
||||||
recordFailure(err)
|
recordFailure(err)
|
||||||
|
|||||||
@ -63,6 +63,7 @@ func IsDeliveryUncertain(err error) bool {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
outboundSMTPDialTimeout = 2 * time.Second
|
outboundSMTPDialTimeout = 2 * time.Second
|
||||||
|
outboundSMTPGreetingTimeout = 5 * time.Second
|
||||||
outboundSMTPTLSHandshakeTimeout = 2 * time.Second
|
outboundSMTPTLSHandshakeTimeout = 2 * time.Second
|
||||||
outboundSMTPTransactionTimeout = 2 * time.Minute
|
outboundSMTPTransactionTimeout = 2 * time.Minute
|
||||||
outboundSMTPQuitWriteTimeout = 250 * time.Millisecond
|
outboundSMTPQuitWriteTimeout = 250 * time.Millisecond
|
||||||
@ -89,6 +90,8 @@ type Client struct {
|
|||||||
localName string // the name to use in HELO/EHLO
|
localName string // the name to use in HELO/EHLO
|
||||||
didHello bool // whether we've said HELO/EHLO
|
didHello bool // whether we've said HELO/EHLO
|
||||||
helloError error // the error from the hello
|
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.
|
// 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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -107,7 +114,7 @@ func dial(addr, fromDomain string, publicOnly bool) (*Client, error) {
|
|||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return newClientWithTimeout(conn, host, fromDomain, outboundSMTPTransactionTimeout)
|
return newClientWithContext(ctx, conn, host, fromDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// with tls
|
// with tls
|
||||||
@ -116,6 +123,10 @@ func DialTls(addr, domain, fromDomain string) (*Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func dialTLS(addr, domain, fromDomain string, publicOnly bool) (*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)
|
host, _, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -129,16 +140,50 @@ func dialTLS(addr, domain, fromDomain string, publicOnly bool) (*Client, error)
|
|||||||
ServerName: domain,
|
ServerName: domain,
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := dialTCP(addr, publicOnly)
|
conn, err := dialTCPContext(ctx, addr, publicOnly)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
stopCancel := context.AfterFunc(ctx, func() { _ = conn.Close() })
|
||||||
|
defer stopCancel()
|
||||||
tlsConn, err := handshakeTLS(conn, tlsconfig, outboundSMTPTLSHandshakeTimeout)
|
tlsConn, err := handshakeTLS(conn, tlsconfig, outboundSMTPTLSHandshakeTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, err
|
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) {
|
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) {
|
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)
|
host, port, err := net.SplitHostPort(addr)
|
||||||
if err != nil || host == "" || port == "" {
|
if err != nil || host == "" || port == "" {
|
||||||
return nil, errors.New("smtp: invalid remote address")
|
return nil, errors.New("smtp: invalid remote address")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), outboundSMTPDialTimeout)
|
ctx, cancel := context.WithTimeout(parent, outboundSMTPDialTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
addresses, err := resolveRemoteAddresses(ctx, host)
|
addresses, err := resolveRemoteAddresses(ctx, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -326,6 +375,9 @@ func NewClient(conn net.Conn, host, fromDomain string) (*Client, error) {
|
|||||||
|
|
||||||
// Close closes the connection.
|
// Close closes the connection.
|
||||||
func (c *Client) Close() error {
|
func (c *Client) Close() error {
|
||||||
|
if c.stopCancel != nil {
|
||||||
|
c.stopCancel()
|
||||||
|
}
|
||||||
return c.Text.Close()
|
return c.Text.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,7 +478,7 @@ func (c *Client) StartTLS(config *tls.Config) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = tlsConn.SetDeadline(time.Now().Add(outboundSMTPTransactionTimeout)); err != nil {
|
if err = tlsConn.SetDeadline(smtpDeadline(c.ctx, outboundSMTPTransactionTimeout)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.conn = tlsConn
|
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 {
|
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 {
|
if err := validateLine(from); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -601,7 +657,7 @@ func SendMailWithTls(domain string, addr string, a smtp.Auth, from string, fromD
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c, err := DialTls(addr, domain, fromDomain)
|
c, err := dialTLSContext(ctx, addr, domain, fromDomain, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -654,6 +710,10 @@ func SendMailWithTls(domain string, addr string, a smtp.Auth, from string, fromD
|
|||||||
// library.
|
// library.
|
||||||
// 修复TSL验证问题
|
// 修复TSL验证问题
|
||||||
func SendMail(domain string, addr string, a smtp.Auth, from string, fromDomain string, to []string, msg []byte) error {
|
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))
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c, err := Dial(addr, fromDomain)
|
c, err := dialContext(ctx, addr, fromDomain, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -713,6 +773,10 @@ func SendMail(domain string, addr string, a smtp.Auth, from string, fromDomain s
|
|||||||
|
|
||||||
// SendMailUnsafe 无TLS加密的邮件发送方式
|
// SendMailUnsafe 无TLS加密的邮件发送方式
|
||||||
func SendMailUnsafe(domain string, addr string, a smtp.Auth, from string, fromDomain string, to []string, msg []byte) error {
|
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 {
|
if err := validateUnsafePlaintextTarget(domain, addr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -724,7 +788,7 @@ func SendMailUnsafe(domain string, addr string, a smtp.Auth, from string, fromDo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c, err := dial(addr, fromDomain, false)
|
c, err := dialContext(ctx, addr, fromDomain, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user