mail.go (2716 bytes)
1 package main 2 3 import ( 4 "crypto/tls" 5 "crypto/x509" 6 "fmt" 7 "mime" 8 "net" 9 "net/mail" 10 "net/smtp" 11 "strings" 12 "time" 13 ) 14 15 // Mailer sends email through an SMTP server. With TLS set (port 465), the 16 // connection uses TLS from the start. Else the mailer starts TLS when the 17 // server offers STARTTLS. net/smtp never sends the password over a 18 // connection without TLS, except to localhost. 19 type Mailer struct { 20 Addr string // host:port of the SMTP server 21 TLS bool 22 User string // empty for a server that needs no sign-in 23 Password string 24 From *mail.Address 25 Hello string // name of this host in EHLO 26 Roots *x509.CertPool // nil for the roots of the system; tests replace it 27 } 28 29 // send sends a plain text email. The lines of body end in "\n". 30 func (m *Mailer) send(to, subject, body string) error { 31 host, _, err := net.SplitHostPort(m.Addr) 32 if err != nil { 33 return err 34 } 35 tc := &tls.Config{ServerName: host, RootCAs: m.Roots} 36 d := &net.Dialer{Timeout: 20 * time.Second} 37 var conn net.Conn 38 if m.TLS { 39 conn, err = tls.DialWithDialer(d, "tcp", m.Addr, tc) 40 } else { 41 conn, err = d.Dial("tcp", m.Addr) 42 } 43 if err != nil { 44 return err 45 } 46 defer conn.Close() 47 if err := conn.SetDeadline(time.Now().Add(30 * time.Second)); err != nil { 48 return err 49 } 50 c, err := smtp.NewClient(conn, host) 51 if err != nil { 52 return err 53 } 54 if err := c.Hello(m.Hello); err != nil { 55 return err 56 } 57 if ok, _ := c.Extension("STARTTLS"); ok && !m.TLS { 58 if err := c.StartTLS(tc); err != nil { 59 return err 60 } 61 } 62 if m.User != "" { 63 if err := c.Auth(smtp.PlainAuth("", m.User, m.Password, host)); err != nil { 64 return err 65 } 66 } 67 if err := c.Mail(m.From.Address); err != nil { 68 return err 69 } 70 if err := c.Rcpt(to); err != nil { 71 return err 72 } 73 w, err := c.Data() 74 if err != nil { 75 return err 76 } 77 if _, err := w.Write(m.message(to, subject, body)); err != nil { 78 return err 79 } 80 if err := w.Close(); err != nil { 81 return err 82 } 83 return c.Quit() 84 } 85 86 // message gives the email with its header. The lines end in CRLF, as SMTP 87 // needs. 88 func (m *Mailer) message(to, subject, body string) []byte { 89 _, domain, _ := strings.Cut(m.From.Address, "@") 90 var b strings.Builder 91 fmt.Fprintf(&b, "From: %s\r\n", m.From) 92 fmt.Fprintf(&b, "To: %s\r\n", to) 93 fmt.Fprintf(&b, "Subject: %s\r\n", mime.QEncoding.Encode("utf-8", subject)) 94 fmt.Fprintf(&b, "Date: %s\r\n", time.Now().Format(time.RFC1123Z)) 95 fmt.Fprintf(&b, "Message-ID: <%s@%s>\r\n", random(16), domain) 96 b.WriteString("MIME-Version: 1.0\r\n") 97 b.WriteString("Content-Type: text/plain; charset=utf-8\r\n") 98 b.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n") 99 b.WriteString(strings.ReplaceAll(body, "\n", "\r\n")) 100 return []byte(b.String()) 101 }