1package mail
2
3import (
4	"bytes"
5	"io"
6	"os"
7	"path/filepath"
8	"time"
9)
10
11// Message represents an email.
12type Message struct {
13	header      header
14	parts       []*part
15	attachments []*file
16	embedded    []*file
17	charset     string
18	encoding    Encoding
19	hEncoder    mimeEncoder
20	buf         bytes.Buffer
21	boundary    string
22}
23
24type header map[string][]string
25
26type part struct {
27	contentType string
28	copier      func(io.Writer) error
29	encoding    Encoding
30}
31
32// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding
33// by default.
34func NewMessage(settings ...MessageSetting) *Message {
35	m := &Message{
36		header:   make(header),
37		charset:  "UTF-8",
38		encoding: QuotedPrintable,
39	}
40
41	m.applySettings(settings)
42
43	if m.encoding == Base64 {
44		m.hEncoder = bEncoding
45	} else {
46		m.hEncoder = qEncoding
47	}
48
49	return m
50}
51
52// Reset resets the message so it can be reused. The message keeps its previous
53// settings so it is in the same state that after a call to NewMessage.
54func (m *Message) Reset() {
55	for k := range m.header {
56		delete(m.header, k)
57	}
58	m.parts = nil
59	m.attachments = nil
60	m.embedded = nil
61}
62
63func (m *Message) applySettings(settings []MessageSetting) {
64	for _, s := range settings {
65		s(m)
66	}
67}
68
69// A MessageSetting can be used as an argument in NewMessage to configure an
70// email.
71type MessageSetting func(m *Message)
72
73// SetCharset is a message setting to set the charset of the email.
74func SetCharset(charset string) MessageSetting {
75	return func(m *Message) {
76		m.charset = charset
77	}
78}
79
80// SetEncoding is a message setting to set the encoding of the email.
81func SetEncoding(enc Encoding) MessageSetting {
82	return func(m *Message) {
83		m.encoding = enc
84	}
85}
86
87// Encoding represents a MIME encoding scheme like quoted-printable or base64.
88type Encoding string
89
90const (
91	// QuotedPrintable represents the quoted-printable encoding as defined in
92	// RFC 2045.
93	QuotedPrintable Encoding = "quoted-printable"
94	// Base64 represents the base64 encoding as defined in RFC 2045.
95	Base64 Encoding = "base64"
96	// Unencoded can be used to avoid encoding the body of an email. The headers
97	// will still be encoded using quoted-printable encoding.
98	Unencoded Encoding = "8bit"
99)
100
101// SetBoundary sets a custom multipart boundary.
102func (m *Message) SetBoundary(boundary string) {
103	m.boundary = boundary
104}
105
106// SetHeader sets a value to the given header field.
107func (m *Message) SetHeader(field string, value ...string) {
108	m.encodeHeader(value)
109	m.header[field] = value
110}
111
112func (m *Message) encodeHeader(values []string) {
113	for i := range values {
114		values[i] = m.encodeString(values[i])
115	}
116}
117
118func (m *Message) encodeString(value string) string {
119	return m.hEncoder.Encode(m.charset, value)
120}
121
122// SetHeaders sets the message headers.
123func (m *Message) SetHeaders(h map[string][]string) {
124	for k, v := range h {
125		m.SetHeader(k, v...)
126	}
127}
128
129// SetAddressHeader sets an address to the given header field.
130func (m *Message) SetAddressHeader(field, address, name string) {
131	m.header[field] = []string{m.FormatAddress(address, name)}
132}
133
134// FormatAddress formats an address and a name as a valid RFC 5322 address.
135func (m *Message) FormatAddress(address, name string) string {
136	if name == "" {
137		return address
138	}
139
140	enc := m.encodeString(name)
141	if enc == name {
142		m.buf.WriteByte('"')
143		for i := 0; i < len(name); i++ {
144			b := name[i]
145			if b == '\\' || b == '"' {
146				m.buf.WriteByte('\\')
147			}
148			m.buf.WriteByte(b)
149		}
150		m.buf.WriteByte('"')
151	} else if hasSpecials(name) {
152		m.buf.WriteString(bEncoding.Encode(m.charset, name))
153	} else {
154		m.buf.WriteString(enc)
155	}
156	m.buf.WriteString(" <")
157	m.buf.WriteString(address)
158	m.buf.WriteByte('>')
159
160	addr := m.buf.String()
161	m.buf.Reset()
162	return addr
163}
164
165func hasSpecials(text string) bool {
166	for i := 0; i < len(text); i++ {
167		switch c := text[i]; c {
168		case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"':
169			return true
170		}
171	}
172
173	return false
174}
175
176// SetDateHeader sets a date to the given header field.
177func (m *Message) SetDateHeader(field string, date time.Time) {
178	m.header[field] = []string{m.FormatDate(date)}
179}
180
181// FormatDate formats a date as a valid RFC 5322 date.
182func (m *Message) FormatDate(date time.Time) string {
183	return date.Format(time.RFC1123Z)
184}
185
186// GetHeader gets a header field.
187func (m *Message) GetHeader(field string) []string {
188	return m.header[field]
189}
190
191// SetBody sets the body of the message. It replaces any content previously set
192// by SetBody, SetBodyWriter, AddAlternative or AddAlternativeWriter.
193func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
194	m.SetBodyWriter(contentType, newCopier(body), settings...)
195}
196
197// SetBodyWriter sets the body of the message. It can be useful with the
198// text/template or html/template packages.
199func (m *Message) SetBodyWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
200	m.parts = []*part{m.newPart(contentType, f, settings)}
201}
202
203// AddAlternative adds an alternative part to the message.
204//
205// It is commonly used to send HTML emails that default to the plain text
206// version for backward compatibility. AddAlternative appends the new part to
207// the end of the message. So the plain text part should be added before the
208// HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative
209func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) {
210	m.AddAlternativeWriter(contentType, newCopier(body), settings...)
211}
212
213func newCopier(s string) func(io.Writer) error {
214	return func(w io.Writer) error {
215		_, err := io.WriteString(w, s)
216		return err
217	}
218}
219
220// AddAlternativeWriter adds an alternative part to the message. It can be
221// useful with the text/template or html/template packages.
222func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
223	m.parts = append(m.parts, m.newPart(contentType, f, settings))
224}
225
226func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part {
227	p := &part{
228		contentType: contentType,
229		copier:      f,
230		encoding:    m.encoding,
231	}
232
233	for _, s := range settings {
234		s(p)
235	}
236
237	return p
238}
239
240// A PartSetting can be used as an argument in Message.SetBody,
241// Message.SetBodyWriter, Message.AddAlternative or Message.AddAlternativeWriter
242// to configure the part added to a message.
243type PartSetting func(*part)
244
245// SetPartEncoding sets the encoding of the part added to the message. By
246// default, parts use the same encoding than the message.
247func SetPartEncoding(e Encoding) PartSetting {
248	return PartSetting(func(p *part) {
249		p.encoding = e
250	})
251}
252
253type file struct {
254	Name     string
255	Header   map[string][]string
256	CopyFunc func(w io.Writer) error
257}
258
259func (f *file) setHeader(field, value string) {
260	f.Header[field] = []string{value}
261}
262
263// A FileSetting can be used as an argument in Message.Attach or Message.Embed.
264type FileSetting func(*file)
265
266// SetHeader is a file setting to set the MIME header of the message part that
267// contains the file content.
268//
269// Mandatory headers are automatically added if they are not set when sending
270// the email.
271func SetHeader(h map[string][]string) FileSetting {
272	return func(f *file) {
273		for k, v := range h {
274			f.Header[k] = v
275		}
276	}
277}
278
279// Rename is a file setting to set the name of the attachment if the name is
280// different than the filename on disk.
281func Rename(name string) FileSetting {
282	return func(f *file) {
283		f.Name = name
284	}
285}
286
287// SetCopyFunc is a file setting to replace the function that runs when the
288// message is sent. It should copy the content of the file to the io.Writer.
289//
290// The default copy function opens the file with the given filename, and copy
291// its content to the io.Writer.
292func SetCopyFunc(f func(io.Writer) error) FileSetting {
293	return func(fi *file) {
294		fi.CopyFunc = f
295	}
296}
297
298// AttachReader attaches a file using an io.Reader
299func (m *Message) AttachReader(name string, r io.Reader, settings ...FileSetting) {
300	m.attachments = m.appendFile(m.attachments, fileFromReader(name, r), settings)
301}
302
303// Attach attaches the files to the email.
304func (m *Message) Attach(filename string, settings ...FileSetting) {
305	m.attachments = m.appendFile(m.attachments, fileFromFilename(filename), settings)
306}
307
308// EmbedReader embeds the images to the email.
309func (m *Message) EmbedReader(name string, r io.Reader, settings ...FileSetting) {
310	m.embedded = m.appendFile(m.embedded, fileFromReader(name, r), settings)
311}
312
313// Embed embeds the images to the email.
314func (m *Message) Embed(filename string, settings ...FileSetting) {
315	m.embedded = m.appendFile(m.embedded, fileFromFilename(filename), settings)
316}
317
318func fileFromFilename(name string) *file {
319	return &file{
320		Name:   filepath.Base(name),
321		Header: make(map[string][]string),
322		CopyFunc: func(w io.Writer) error {
323			h, err := os.Open(name)
324			if err != nil {
325				return err
326			}
327			if _, err := io.Copy(w, h); err != nil {
328				h.Close()
329				return err
330			}
331			return h.Close()
332		},
333	}
334}
335
336func fileFromReader(name string, r io.Reader) *file {
337	return &file{
338		Name:   filepath.Base(name),
339		Header: make(map[string][]string),
340		CopyFunc: func(w io.Writer) error {
341			if _, err := io.Copy(w, r); err != nil {
342				return err
343			}
344			return nil
345		},
346	}
347}
348
349func (m *Message) appendFile(list []*file, f *file, settings []FileSetting) []*file {
350	for _, s := range settings {
351		s(f)
352	}
353
354	if list == nil {
355		return []*file{f}
356	}
357
358	return append(list, f)
359}
360