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}