internal/mail/mail.go

99 lines · 2628 bytes

 1// Package mail sends transactional email over SMTP: verification codes and
 2// invites. The connection is encrypted with STARTTLS, or with TLS from the
 3// first byte when mail.tls = "implicit"; a relay that offers neither gets
 4// nothing unless mail.require_tls is off. PLAIN auth when credentials are
 5// configured.
 6package mail
 7
 8import (
 9	"crypto/tls"
10	"crypto/x509"
11	"fmt"
12	"net"
13	"net/smtp"
14	"strings"
15	"time"
16
17	"gitbay.org/gitbay/internal/config"
18)
19
20// rootCAs verifies the relay's certificate; nil is the system pool.
21var rootCAs *x509.CertPool
22
23// Send delivers one plain-text message. cfg.Mail.SMTPHost is host:port.
24func Send(cfg config.Config, to, subject, body string) error {
25	m := cfg.Mail
26	if m.SMTPHost == "" || m.From == "" {
27		return fmt.Errorf("[mail] smtp_host and from must be configured")
28	}
29	implicit := m.TLS == "implicit"
30	host := m.SMTPHost
31	if !strings.Contains(host, ":") {
32		if implicit {
33			host += ":465"
34		} else {
35			host += ":587"
36		}
37	}
38	hostname, _, _ := net.SplitHostPort(host)
39	tlsCfg := &tls.Config{ServerName: hostname, RootCAs: rootCAs}
40
41	msg := strings.NewReplacer("\n", "\r\n").Replace(fmt.Sprintf(
42		"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=utf-8\n\n%s\n",
43		m.From, to, subject, time.Now().Format(time.RFC1123Z), body))
44
45	c, err := dial(host, hostname, implicit, tlsCfg)
46	if err != nil {
47		return fmt.Errorf("smtp dial %s: %w", host, err)
48	}
49	defer c.Close()
50	if !implicit {
51		if ok, _ := c.Extension("STARTTLS"); ok {
52			if err := c.StartTLS(tlsCfg); err != nil {
53				return fmt.Errorf("starttls: %w", err)
54			}
55		} else if m.TLSRequired() {
56			return fmt.Errorf("%s does not offer STARTTLS and mail.require_tls is on; not sending in clear", host)
57		}
58	}
59	if m.SMTPUser != "" {
60		if err := c.Auth(smtp.PlainAuth("", m.SMTPUser, m.SMTPPass, hostname)); err != nil {
61			return fmt.Errorf("smtp auth: %w", err)
62		}
63	}
64	if err := c.Mail(m.From); err != nil {
65		return err
66	}
67	if err := c.Rcpt(to); err != nil {
68		return err
69	}
70	w, err := c.Data()
71	if err != nil {
72		return err
73	}
74	if _, err := w.Write([]byte(msg)); err != nil {
75		return err
76	}
77	if err := w.Close(); err != nil {
78		return err
79	}
80	return c.Quit()
81}
82
83// dial opens the SMTP session: plain TCP for STARTTLS, or TLS from the
84// first byte.
85func dial(addr, hostname string, implicit bool, tlsCfg *tls.Config) (*smtp.Client, error) {
86	if !implicit {
87		return smtp.Dial(addr)
88	}
89	conn, err := tls.Dial("tcp", addr, tlsCfg)
90	if err != nil {
91		return nil, err
92	}
93	c, err := smtp.NewClient(conn, hostname)
94	if err != nil {
95		conn.Close()
96		return nil, err
97	}
98	return c, nil
99}