Recently Written · git

recentlywritten

my writings (done recently)

git clone https://github.com/equwal/recentlywritten

Log | Files | Refs


accounts/mail.go (3017 bytes)

1 package main
2 
3 import (
4 	"bytes"
5 	"fmt"
6 	"io"
7 	"mime"
8 	"mime/quotedprintable"
9 	"net"
10 	"net/mail"
11 	"net/smtp"
12 	"strings"
13 	"time"
14 )
15 
16 // message is one plain text email to one address.
17 type message struct {
18 	To      string
19 	Subject string
20 	Body    string
21 	// Unsubscribe is a URL for the List-Unsubscribe header (RFC 2369) and
22 	// for one-click unsubscribe (RFC 8058). It is empty for sign-in email.
23 	Unsubscribe string
24 }
25 
26 type mailer interface {
27 	Send(m message) error
28 }
29 
30 // smtpMailer sends through any SMTP server: a local MTA, or the SMTP
31 // endpoint of a hosted provider (Resend, Postmark, Amazon SES).
32 type smtpMailer struct {
33 	Addr     string // host:port
34 	Username string // empty for a server that needs no sign-in
35 	Password string
36 	From     *mail.Address
37 }
38 
39 // Send uses STARTTLS when the server offers it. net/smtp PlainAuth does
40 // not send the password on a connection without TLS, except to localhost.
41 func (s smtpMailer) Send(m message) error {
42 	var auth smtp.Auth
43 	if s.Username != "" {
44 		host, _, _ := net.SplitHostPort(s.Addr)
45 		auth = smtp.PlainAuth("", s.Username, s.Password, host)
46 	}
47 	data, err := formatMessage(s.From, m, time.Now())
48 	if err != nil {
49 		return err
50 	}
51 	return smtp.SendMail(s.Addr, auth, s.From.Address, []string{m.To}, data)
52 }
53 
54 // logMailer writes each message to w. Use it only for development: the
55 // messages contain sign-in links.
56 type logMailer struct{ W io.Writer }
57 
58 func (l logMailer) Send(m message) error {
59 	_, err := fmt.Fprintf(l.W, "--- mail to %s\nSubject: %s\n\n%s\n---\n", m.To, m.Subject, m.Body)
60 	return err
61 }
62 
63 // formatMessage makes an RFC 5322 message. The subject is encoded as an
64 // RFC 2047 word, and the body as quoted-printable, so that non-ASCII text
65 // and long lines arrive intact.
66 func formatMessage(from *mail.Address, m message, now time.Time) ([]byte, error) {
67 	to, err := mail.ParseAddress(m.To)
68 	if err != nil {
69 		return nil, err
70 	}
71 	// A line break in a header value would start a new header.
72 	subject := strings.NewReplacer("\r", " ", "\n", " ").Replace(m.Subject)
73 	domain := from.Address[strings.LastIndex(from.Address, "@")+1:]
74 
75 	var b bytes.Buffer
76 	h := func(k, v string) { fmt.Fprintf(&b, "%s: %s\r\n", k, v) }
77 	h("From", from.String())
78 	h("To", to.String())
79 	h("Subject", mime.QEncoding.Encode("utf-8", subject))
80 	h("Date", now.Format(time.RFC1123Z))
81 	h("Message-ID", "<"+strings.ToLower(newToken())+"@"+domain+">")
82 	h("MIME-Version", "1.0")
83 	h("Content-Type", "text/plain; charset=utf-8")
84 	h("Content-Transfer-Encoding", "quoted-printable")
85 	if m.Unsubscribe != "" {
86 		h("List-Unsubscribe", "<"+m.Unsubscribe+">")
87 		h("List-Unsubscribe-Post", "List-Unsubscribe=One-Click")
88 	}
89 	b.WriteString("\r\n")
90 	qp := quotedprintable.NewWriter(&b)
91 	// Email lines end with CRLF. A CR or LF alone becomes CRLF.
92 	crlf := strings.NewReplacer("\r\n", "\r\n", "\r", "\r\n", "\n", "\r\n")
93 	if _, err := qp.Write([]byte(crlf.Replace(m.Body))); err != nil {
94 		return nil, err
95 	}
96 	if err := qp.Close(); err != nil {
97 		return nil, err
98 	}
99 	return b.Bytes(), nil
100 }