1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-17 14:37:02 +00:00
Files
go-chasquid-smtp/internal/courier/smtp.go
Alberto Bertogli 1bc111f783 Improve the readability of some log messages
This patch contains a few changes to logging messages, to improve log
readability.

While at it, an obsolete TODO in the SMTP courier is removed.
2016-11-01 23:56:04 +00:00

201 lines
5.2 KiB
Go

package courier
import (
"crypto/tls"
"expvar"
"flag"
"net"
"os"
"time"
"golang.org/x/net/idna"
"blitiri.com.ar/go/chasquid/internal/domaininfo"
"blitiri.com.ar/go/chasquid/internal/envelope"
"blitiri.com.ar/go/chasquid/internal/smtp"
"blitiri.com.ar/go/chasquid/internal/trace"
)
var (
// Timeouts for SMTP delivery.
smtpDialTimeout = 1 * time.Minute
smtpTotalTimeout = 10 * time.Minute
// Port for outgoing SMTP.
// Tests can override this.
smtpPort = flag.String("testing__outgoing_smtp_port", "25",
"port to use for outgoing SMTP connections, ONLY FOR TESTING")
// Fake MX records, used for testing only.
fakeMX = map[string]string{}
)
// Exported variables.
var (
tlsCount = expvar.NewMap("chasquid/smtpOut/tlsCount")
slcResults = expvar.NewMap("chasquid/smtpOut/securityLevelChecks")
)
// SMTP delivers remote mail via outgoing SMTP.
type SMTP struct {
Dinfo *domaininfo.DB
}
func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) {
tr := trace.New("Courier.SMTP", to)
defer tr.Finish()
tr.Debugf("%s -> %s", from, to)
toDomain := envelope.DomainOf(to)
mx, err := lookupMX(tr, toDomain)
if err != nil {
// Note this is considered a permanent error.
// This is in line with what other servers (Exim) do. However, the
// downside is that temporary DNS issues can affect delivery, so we
// have to make sure we try hard enough on the lookup above.
return tr.Errorf("Could not find mail server: %v", err), true
}
// Issue an EHLO with a valid domain; otherwise, some servers like postfix
// will complain.
helloDomain, err := idna.ToASCII(envelope.DomainOf(from))
if err != nil {
return tr.Errorf("Sender domain not IDNA compliant: %v", err), true
}
if helloDomain == "" {
// This can happen when sending bounces. Last resort.
helloDomain, _ = os.Hostname()
}
// Do we use insecure TLS?
// Set as fallback when retrying.
insecure := false
secLevel := domaininfo.SecLevel_PLAIN
retry:
conn, err := net.DialTimeout("tcp", mx+":"+*smtpPort, smtpDialTimeout)
if err != nil {
return tr.Errorf("Could not dial: %v", err), false
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(smtpTotalTimeout))
c, err := smtp.NewClient(conn, mx)
if err != nil {
return tr.Errorf("Error creating client: %v", err), false
}
if err = c.Hello(helloDomain); err != nil {
return tr.Errorf("Error saying hello: %v", err), false
}
if ok, _ := c.Extension("STARTTLS"); ok {
config := &tls.Config{
ServerName: mx,
InsecureSkipVerify: insecure,
}
err = c.StartTLS(config)
if err != nil {
// Unfortunately, many servers use self-signed certs, so if we
// fail verification we just try again without validating.
if insecure {
tlsCount.Add("tls:failed", 1)
return tr.Errorf("TLS error: %v", err), false
}
insecure = true
tr.Debugf("TLS error, retrying insecurely")
goto retry
}
if config.InsecureSkipVerify {
tr.Debugf("Insecure - using TLS, but cert does not match %s", mx)
tlsCount.Add("tls:insecure", 1)
secLevel = domaininfo.SecLevel_TLS_INSECURE
} else {
tlsCount.Add("tls:secure", 1)
tr.Debugf("Secure - using TLS")
secLevel = domaininfo.SecLevel_TLS_SECURE
}
} else {
tlsCount.Add("plain", 1)
tr.Debugf("Insecure - NOT using TLS")
}
if toDomain != "" && !s.Dinfo.OutgoingSecLevel(toDomain, secLevel) {
// We consider the failure transient, so transient misconfigurations
// do not affect deliveries.
slcResults.Add("fail", 1)
return tr.Errorf("Security level check failed (level:%s)", secLevel), false
}
slcResults.Add("pass", 1)
// c.Mail will add the <> for us when the address is empty.
if from == "<>" {
from = ""
}
if err = c.MailAndRcpt(from, to); err != nil {
return tr.Errorf("MAIL+RCPT %v", err), smtp.IsPermanent(err)
}
w, err := c.Data()
if err != nil {
return tr.Errorf("DATA %v", err), smtp.IsPermanent(err)
}
_, err = w.Write(data)
if err != nil {
return tr.Errorf("DATA writing: %v", err), smtp.IsPermanent(err)
}
err = w.Close()
if err != nil {
return tr.Errorf("DATA closing %v", err), smtp.IsPermanent(err)
}
c.Quit()
tr.Debugf("done")
return nil, false
}
func lookupMX(tr *trace.Trace, domain string) (string, error) {
if v, ok := fakeMX[domain]; ok {
return v, nil
}
domain, err := idna.ToASCII(domain)
if err != nil {
return "", err
}
mxs, err := net.LookupMX(domain)
if err == nil {
if len(mxs) == 0 {
tr.Debugf("domain %q has no MX, falling back to A", domain)
return domain, nil
}
tr.Debugf("MX %s", mxs[0].Host)
return mxs[0].Host, nil
}
// There was an error. It could be that the domain has no MX, in which
// case we have to fall back to A, or a bigger problem.
// Unfortunately, go's API doesn't let us easily distinguish between them.
// For now, if the error is permanent, we assume it's because there was no
// MX and fall back, otherwise we return.
// TODO: Find a better way to do this.
dnsErr, ok := err.(*net.DNSError)
if !ok {
tr.Debugf("MX lookup error: %v", err)
return "", err
} else if dnsErr.Temporary() {
tr.Debugf("temporary DNS error: %v", dnsErr)
return "", err
}
// Permanent error, we assume MX does not exist and fall back to A.
tr.Debugf("failed to resolve MX for %s, falling back to A", domain)
return domain, nil
}