Recently Written · git

sbm-sync

Sync server for sbm bookmark files: one small Go program, plain files, AGPL

git clone https://github.com/equwal/sbm-sync

Log | Files | Refs


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 }