mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
8096441f67
the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
191 lines
6.3 KiB
Go
191 lines
6.3 KiB
Go
package queue
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/mjl-/mox/config"
|
|
"github.com/mjl-/mox/dns"
|
|
"github.com/mjl-/mox/dsn"
|
|
"github.com/mjl-/mox/mlog"
|
|
"github.com/mjl-/mox/mox-"
|
|
"github.com/mjl-/mox/sasl"
|
|
"github.com/mjl-/mox/smtpclient"
|
|
"github.com/mjl-/mox/store"
|
|
)
|
|
|
|
// todo: reuse connection? do fewer concurrently (other than with direct delivery).
|
|
|
|
// deliver via another SMTP server, e.g. relaying to a smart host, possibly
|
|
// with authentication (submission).
|
|
func deliverSubmit(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer contextDialer, m Msg, backoff time.Duration, transportName string, transport *config.TransportSMTP, dialTLS bool, defaultPort int) {
|
|
// todo: configurable timeouts
|
|
|
|
port := transport.Port
|
|
if port == 0 {
|
|
port = defaultPort
|
|
}
|
|
|
|
tlsMode := smtpclient.TLSStrictStartTLS
|
|
if dialTLS {
|
|
tlsMode = smtpclient.TLSStrictImmediate
|
|
} else if transport.STARTTLSInsecureSkipVerify {
|
|
tlsMode = smtpclient.TLSOpportunistic
|
|
} else if transport.NoSTARTTLS {
|
|
tlsMode = smtpclient.TLSSkip
|
|
}
|
|
start := time.Now()
|
|
var deliveryResult string
|
|
var permanent bool
|
|
var secodeOpt string
|
|
var errmsg string
|
|
var success bool
|
|
defer func() {
|
|
metricDelivery.WithLabelValues(fmt.Sprintf("%d", m.Attempts), transportName, string(tlsMode), deliveryResult).Observe(float64(time.Since(start)) / float64(time.Second))
|
|
qlog.Debug("queue deliversubmit result", mlog.Field("host", transport.DNSHost), mlog.Field("port", port), mlog.Field("attempt", m.Attempts), mlog.Field("permanent", permanent), mlog.Field("secodeopt", secodeOpt), mlog.Field("errmsg", errmsg), mlog.Field("ok", success), mlog.Field("duration", time.Since(start)))
|
|
}()
|
|
|
|
dialctx, dialcancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer dialcancel()
|
|
addr := net.JoinHostPort(transport.Host, fmt.Sprintf("%d", port))
|
|
conn, _, _, err := dialHost(dialctx, qlog, resolver, dialer, dns.IPDomain{Domain: transport.DNSHost}, port, &m)
|
|
var result string
|
|
switch {
|
|
case err == nil:
|
|
result = "ok"
|
|
case errors.Is(err, os.ErrDeadlineExceeded), errors.Is(err, context.DeadlineExceeded):
|
|
result = "timeout"
|
|
case errors.Is(err, context.Canceled):
|
|
result = "canceled"
|
|
default:
|
|
result = "error"
|
|
}
|
|
metricConnection.WithLabelValues(result).Inc()
|
|
if err != nil {
|
|
if conn != nil {
|
|
err := conn.Close()
|
|
qlog.Check(err, "closing connection")
|
|
}
|
|
qlog.Errorx("dialing for submission", err, mlog.Field("remote", addr))
|
|
errmsg = fmt.Sprintf("transport %s: dialing %s for submission: %v", transportName, addr, err)
|
|
fail(qlog, m, backoff, false, dsn.NameIP{}, "", errmsg)
|
|
return
|
|
}
|
|
dialcancel()
|
|
|
|
var auth []sasl.Client
|
|
if transport.Auth != nil {
|
|
a := transport.Auth
|
|
for _, mech := range a.EffectiveMechanisms {
|
|
switch mech {
|
|
case "PLAIN":
|
|
auth = append(auth, sasl.NewClientPlain(a.Username, a.Password))
|
|
case "CRAM-MD5":
|
|
auth = append(auth, sasl.NewClientCRAMMD5(a.Username, a.Password))
|
|
case "SCRAM-SHA-1":
|
|
auth = append(auth, sasl.NewClientSCRAMSHA1(a.Username, a.Password))
|
|
case "SCRAM-SHA-256":
|
|
auth = append(auth, sasl.NewClientSCRAMSHA256(a.Username, a.Password))
|
|
default:
|
|
// Should not happen.
|
|
qlog.Error("missing smtp authentication mechanisms implementation", mlog.Field("mechanism", mech))
|
|
errmsg = fmt.Sprintf("transport %s: authentication mechanisms %q not implemented", transportName, mech)
|
|
fail(qlog, m, backoff, false, dsn.NameIP{}, "", errmsg)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
clientctx, clientcancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer clientcancel()
|
|
client, err := smtpclient.New(clientctx, qlog, conn, tlsMode, mox.Conf.Static.HostnameDomain, transport.DNSHost, auth)
|
|
if err != nil {
|
|
smtperr, ok := err.(smtpclient.Error)
|
|
var remoteMTA dsn.NameIP
|
|
if ok {
|
|
remoteMTA.Name = transport.Host
|
|
}
|
|
qlog.Errorx("establishing smtp session for submission", err, mlog.Field("remote", addr))
|
|
errmsg = fmt.Sprintf("transport %s: establishing smtp session with %s for submission: %v", transportName, addr, err)
|
|
secodeOpt = smtperr.Secode
|
|
fail(qlog, m, backoff, false, remoteMTA, secodeOpt, errmsg)
|
|
return
|
|
}
|
|
defer func() {
|
|
err := client.Close()
|
|
qlog.Check(err, "closing smtp client after delivery")
|
|
}()
|
|
clientcancel()
|
|
|
|
var msgr io.ReadCloser
|
|
var size int64
|
|
var req8bit, reqsmtputf8 bool
|
|
if len(m.DSNUTF8) > 0 && client.SupportsSMTPUTF8() {
|
|
msgr = io.NopCloser(bytes.NewReader(m.DSNUTF8))
|
|
reqsmtputf8 = true
|
|
size = int64(len(m.DSNUTF8))
|
|
} else {
|
|
req8bit = m.Has8bit // todo: not require this, but just try to submit?
|
|
size = m.Size
|
|
|
|
p := m.MessagePath()
|
|
f, err := os.Open(p)
|
|
if err != nil {
|
|
qlog.Errorx("opening message for delivery", err, mlog.Field("remote", addr), mlog.Field("path", p))
|
|
errmsg = fmt.Sprintf("transport %s: opening message file for submission: %v", transportName, err)
|
|
fail(qlog, m, backoff, false, dsn.NameIP{}, "", errmsg)
|
|
return
|
|
}
|
|
msgr = store.FileMsgReader(m.MsgPrefix, f)
|
|
defer func() {
|
|
err := msgr.Close()
|
|
qlog.Check(err, "closing message after delivery attempt")
|
|
}()
|
|
}
|
|
|
|
deliverctx, delivercancel := context.WithTimeout(context.Background(), time.Duration(60+size/(1024*1024))*time.Second)
|
|
defer delivercancel()
|
|
err = client.Deliver(deliverctx, m.Sender().String(), m.Recipient().String(), size, msgr, req8bit, reqsmtputf8)
|
|
if err != nil {
|
|
qlog.Infox("delivery failed", err)
|
|
}
|
|
var cerr smtpclient.Error
|
|
switch {
|
|
case err == nil:
|
|
deliveryResult = "ok"
|
|
success = true
|
|
case errors.Is(err, os.ErrDeadlineExceeded), errors.Is(err, context.DeadlineExceeded):
|
|
deliveryResult = "timeout"
|
|
case errors.Is(err, context.Canceled):
|
|
deliveryResult = "canceled"
|
|
case errors.As(err, &cerr):
|
|
deliveryResult = "temperror"
|
|
if cerr.Permanent {
|
|
deliveryResult = "permerror"
|
|
}
|
|
default:
|
|
deliveryResult = "error"
|
|
}
|
|
if err != nil {
|
|
smtperr, ok := err.(smtpclient.Error)
|
|
var remoteMTA dsn.NameIP
|
|
if ok {
|
|
remoteMTA.Name = transport.Host
|
|
}
|
|
qlog.Errorx("submitting email", err, mlog.Field("remote", addr))
|
|
permanent = smtperr.Permanent
|
|
secodeOpt = smtperr.Secode
|
|
errmsg = fmt.Sprintf("transport %s: submitting email to %s: %v", transportName, addr, err)
|
|
fail(qlog, m, backoff, permanent, remoteMTA, secodeOpt, errmsg)
|
|
return
|
|
}
|
|
qlog.Info("delivered from queue with transport")
|
|
if err := queueDelete(context.Background(), m.ID); err != nil {
|
|
qlog.Errorx("deleting message from queue after delivery", err)
|
|
}
|
|
}
|