internal/mail/mail.go
112 lines · 3036 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 return SendReplyTo(cfg, to, "", subject, body)
26}
27
28// SendReplyTo is Send with a Reply-To header, left out when replyTo is
29// empty.
30func SendReplyTo(cfg config.Config, to, replyTo, subject, body string) error {
31 m := cfg.Mail
32 if m.SMTPHost == "" || m.From == "" {
33 return fmt.Errorf("[mail] smtp_host and from must be configured")
34 }
35 if strings.ContainsAny(replyTo, "\r\n") {
36 return fmt.Errorf("reply-to address contains a line break")
37 }
38 header := ""
39 if replyTo != "" {
40 header = "Reply-To: " + replyTo + "\n"
41 }
42 implicit := m.TLS == "implicit"
43 host := m.SMTPHost
44 if !strings.Contains(host, ":") {
45 if implicit {
46 host += ":465"
47 } else {
48 host += ":587"
49 }
50 }
51 hostname, _, _ := net.SplitHostPort(host)
52 tlsCfg := &tls.Config{ServerName: hostname, RootCAs: rootCAs}
53
54 msg := strings.NewReplacer("\n", "\r\n").Replace(fmt.Sprintf(
55 "From: %s\nTo: %s\n%sSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=utf-8\n\n%s\n",
56 m.From, to, header, subject, time.Now().Format(time.RFC1123Z), body))
57
58 c, err := dial(host, hostname, implicit, tlsCfg)
59 if err != nil {
60 return fmt.Errorf("smtp dial %s: %w", host, err)
61 }
62 defer c.Close()
63 if !implicit {
64 if ok, _ := c.Extension("STARTTLS"); ok {
65 if err := c.StartTLS(tlsCfg); err != nil {
66 return fmt.Errorf("starttls: %w", err)
67 }
68 } else if m.TLSRequired() {
69 return fmt.Errorf("%s does not offer STARTTLS and mail.require_tls is on; not sending in clear", host)
70 }
71 }
72 if m.SMTPUser != "" {
73 if err := c.Auth(smtp.PlainAuth("", m.SMTPUser, m.SMTPPass, hostname)); err != nil {
74 return fmt.Errorf("smtp auth: %w", err)
75 }
76 }
77 if err := c.Mail(m.From); err != nil {
78 return err
79 }
80 if err := c.Rcpt(to); err != nil {
81 return err
82 }
83 w, err := c.Data()
84 if err != nil {
85 return err
86 }
87 if _, err := w.Write([]byte(msg)); err != nil {
88 return err
89 }
90 if err := w.Close(); err != nil {
91 return err
92 }
93 return c.Quit()
94}
95
96// dial opens the SMTP session: plain TCP for STARTTLS, or TLS from the
97// first byte.
98func dial(addr, hostname string, implicit bool, tlsCfg *tls.Config) (*smtp.Client, error) {
99 if !implicit {
100 return smtp.Dial(addr)
101 }
102 conn, err := tls.Dial("tcp", addr, tlsCfg)
103 if err != nil {
104 return nil, err
105 }
106 c, err := smtp.NewClient(conn, hostname)
107 if err != nil {
108 conn.Close()
109 return nil, err
110 }
111 return c, nil
112}