1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package main

// Recipients.
//
// An address that mail is about to be sent to is the one string in this program
// where being approximately right is worthless. Everything else mailweb does
// with an address is a question about what to show — which messages belong to a
// contact, which name to print — and a substring match is a reasonable answer
// to those. This is not one of those. A wrong recipient means a letter written
// for one person arriving at another, under the account owner's name, and
// nothing in the result says so.
//
// So a recipient is its own type, and the only way to make one is to parse a
// string into it. The point is not that parsing is difficult; it is that
// sendMail and the envelope take a Recipient rather than a string, so a value
// that was never checked cannot reach them. The rule is enforced by the
// compiler instead of by everyone remembering it — the same reason the read
// pool is opened mode=ro rather than documented as read-only.
//
// What it refuses is as important as what it accepts:
//
//   - A petname. A model that has read `~klara <klara@example.org>` in a text
//     rendering has been shown a name and an address together, and the name is
//     the memorable half; writing `to: ~klara` is the natural mistake. It
//     cannot be resolved, because a petname may name several addresses — see
//     "Petnames are not addresses" in mailweb(1) — so it is refused with an
//     explanation naming the address instead.
//   - A display name. "Klara Meyer" is not an address either, and the same
//     argument applies with less force: it is not even unique to this machine.
//   - Anything with a newline in it, which is header injection.

import (
	"fmt"
	"strings"
)

// Recipient is one address mail may be sent to.
//
// The unexported field is what makes the type worth having: a Recipient cannot
// be built by writing Recipient{addr}, only by parseRecipient, so possessing
// one is proof that something checked it.
type Recipient struct {
	addr string
}

// Address returns the address as it will appear in the envelope.
func (r Recipient) Address() string { return r.addr }

// String makes a Recipient print as its address in logs and errors.
func (r Recipient) String() string { return r.addr }

// recipientError explains why a string is not usable as a recipient. It is
// worth a type because these messages are shown to whoever tried — a person
// looking at a form, or a model reading a 400 — and telling them what to write
// instead is the entire value of refusing.
type recipientError struct {
	input  string
	reason string
}

func (e *recipientError) Error() string {
	return fmt.Sprintf("%q is not an address: %s", e.input, e.reason)
}

// parseRecipient checks a string and returns the Recipient it names.
//
// It is deliberately strict and deliberately dumb: it does not look anything
// up, consult the petname table or try to be helpful about near misses. A
// recipient is either written as an address or it is refused.
func parseRecipient(s string) (Recipient, error) {
	trimmed := strings.TrimSpace(s)

	if trimmed == "" {
		return Recipient{}, &recipientError{s, "it is empty"}
	}

	// A petname is refused by name rather than falling through to the generic
	// "no @" message, because the caller that wrote one is not confused about
	// syntax — they read a name mailweb printed and used it. Telling them the
	// rule, and that the address is on the same line they read the name from,
	// is what makes the refusal actionable.
	if strings.HasPrefix(trimmed, "~") {
		return Recipient{}, &recipientError{s,
			"it looks like a petname. A petname is local to this mailweb and may " +
				"name several addresses, so it cannot identify a recipient. Use the " +
				"address it is shown beside, e.g. klara@example.org"}
	}

	// "Klara Meyer <klara@example.org>" is a display name plus an address, and
	// the display name is the sender's or the reader's, never checked. Rather
	// than unwrapping it — which invites passing whole From: headers in here —
	// the address is asked for on its own.
	if strings.ContainsAny(trimmed, "<>") {
		return Recipient{}, &recipientError{s,
			"it carries a display name. Pass the address by itself, without the " +
				"name and without angle brackets"}
	}

	// A newline would end the header and begin another one, which is how a
	// recipient becomes a Bcc: or a second From:.
	if strings.ContainsAny(trimmed, "\r\n") {
		return Recipient{}, &recipientError{s, "it contains a line break"}
	}

	// Everything below is the shape of an addr-spec, checked loosely: exactly
	// one @, something on each side, and a dot in the domain. RFC 5322 permits
	// a great deal more than anybody uses, and a stricter parser here would
	// refuse real addresses to prevent nothing — what matters is that this is
	// one address and not a name, a list or a header.
	at := strings.Index(trimmed, "@")
	if at < 0 {
		return Recipient{}, &recipientError{s, "it has no @"}
	}
	if strings.Count(trimmed, "@") != 1 {
		return Recipient{}, &recipientError{s,
			"it has more than one @, so it is not a single address"}
	}
	local, domain := trimmed[:at], trimmed[at+1:]
	if local == "" {
		return Recipient{}, &recipientError{s, "it has nothing before the @"}
	}
	if domain == "" {
		return Recipient{}, &recipientError{s, "it has no domain after the @"}
	}
	if !strings.Contains(domain, ".") {
		return Recipient{}, &recipientError{s,
			"its domain has no dot, so it names no host that mail can reach"}
	}
	if strings.ContainsAny(trimmed, " \t,;") {
		return Recipient{}, &recipientError{s,
			"it contains a space or a separator. Pass one address at a time"}
	}

	// Canonicalised the same way every other address in this program is, so a
	// recipient parsed here and a contact derived from a message compare equal.
	return Recipient{addr: contactAddress(trimmed)}, nil
}

// parseRecipients parses several addresses, failing on the first that is not
// one.
//
// It refuses the whole list rather than dropping what it cannot parse. A send
// that quietly went to four of the five addresses it was given, with the fifth
// discarded because it was misspelled, is the failure this type exists to
// prevent: the person reading the result has no way to notice.
func parseRecipients(ss []string) ([]Recipient, error) {
	out := make([]Recipient, 0, len(ss))
	for _, s := range ss {
		r, err := parseRecipient(s)
		if err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, nil
}

// recipientAddresses renders recipients for the SMTP envelope, which takes
// plain strings.
//
// This is the one place a Recipient becomes a string again, and it is a
// deliberate choke point: every conversion back is here, so there is a single
// place to look when asking what reaches the wire.
func recipientAddresses(rs []Recipient) []string {
	out := make([]string, 0, len(rs))
	for _, r := range rs {
		out = append(out, r.addr)
	}
	return out
}

// mustRecipient builds a Recipient from a string already known to be an
// address, for the addresses mailweb itself supplies rather than reads from a
// request: the account's own --from, and the hardcoded complaints-office
// addresses.
//
// It returns an error rather than panicking despite the name, because the one
// value it is used on that could be wrong — --from — comes off the command
// line, and a bad one should stop the program at startup with an explanation
// rather than crash it on the first send.
func mustRecipient(s string) (Recipient, error) {
	return parseRecipient(s)
}