1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-20 15:07:03 +00:00
Files
go-chasquid-smtp/internal/courier/smtp.go
Alberto Bertogli 927a74aa3c Improve handling of <> addresses
We can send and received with a <> "mail from", which is explicitly allowed.

Internally, we use "<>" to represent it. This requires special-casing in a
couple of places, but makes sure the handling is explicit, and we don't
accidentally confuse it with not having a source address.

This patch fixes some inconsistencies with this handling.
2016-10-10 00:51:04 +01:00

153 lines
3.6 KiB
Go

package courier
import (
"crypto/tls"
"flag"
"net"
"net/smtp"
"time"
"github.com/golang/glog"
"blitiri.com.ar/go/chasquid/internal/envelope"
"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")
// Bypass the MX lookup, for testing purposes.
bypassMX = flag.Bool("testing__bypass_mx_lookup", false,
"bypass MX lookup, ONLY FOR TESTING")
// Fake MX records, used for testing only.
fakeMX = map[string]string{}
)
// SMTP delivers remote mail via outgoing SMTP.
type SMTP struct {
}
func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) {
tr := trace.New("goingSMTP", "Deliver")
defer tr.Finish()
tr.LazyPrintf("%s -> %s", from, to)
// TODO: Fall back to A if MX is not available.
mx, err := lookupMX(envelope.DomainOf(to))
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
}
tr.LazyPrintf("MX: %s", mx)
// Do we use insecure TLS?
// Set as fallback when retrying.
insecure := false
retry:
conn, err := net.DialTimeout("tcp", mx+":"+*smtpPort, smtpDialTimeout)
if err != nil {
return tr.Errorf("Could not dial: %v", err), false
}
conn.SetDeadline(time.Now().Add(smtpTotalTimeout))
c, err := smtp.NewClient(conn, mx)
if err != nil {
return tr.Errorf("Error creating client: %v", err), false
}
// TODO: Keep track of hosts and MXs that we've successfully done TLS
// against, and enforce it.
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 {
return tr.Errorf("TLS error: %v", err), false
}
insecure = true
tr.LazyPrintf("TLS error, retrying insecurely")
goto retry
}
if config.InsecureSkipVerify {
tr.LazyPrintf("Insecure - self-signed certificate")
} else {
tr.LazyPrintf("Secure - using TLS")
}
} else {
tr.LazyPrintf("Insecure - not using TLS")
}
// TODO: check if the errors we get back are transient or not.
// Go's smtp does not allow us to do this, so leave for when we do it
// ourselves.
// c.Mail will add the <> for us when the address is empty.
if from == "<>" {
from = ""
}
if err = c.Mail(from); err != nil {
return tr.Errorf("MAIL %v", err), false
}
if err = c.Rcpt(to); err != nil {
return tr.Errorf("RCPT TO %v", err), false
}
w, err := c.Data()
if err != nil {
return tr.Errorf("DATA %v", err), false
}
_, err = w.Write(data)
if err != nil {
return tr.Errorf("DATA writing: %v", err), false
}
err = w.Close()
if err != nil {
return tr.Errorf("DATA closing %v", err), false
}
c.Quit()
return nil, false
}
func lookupMX(domain string) (string, error) {
if v, ok := fakeMX[domain]; ok {
return v, nil
}
if *bypassMX {
return domain, nil
}
mxs, err := net.LookupMX(domain)
if err != nil {
return "", err
} else if len(mxs) == 0 {
glog.Infof("domain %q has no MX, falling back to A", domain)
return domain, nil
}
return mxs[0].Host, nil
}