1 package mail
2
3 import (
4 "bytes"
5 "io"
6 "os"
7 "path/filepath"
8 "time"
9 )
10
11
12 type 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
24 type header map[string][]string
25
26 type part struct {
27 contentType string
28 copier func(io.Writer) error
29 encoding Encoding
30 }
31
32
33
34 func 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
53
54 func (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
63 func (m *Message) applySettings(settings []MessageSetting) {
64 for _, s := range settings {
65 s(m)
66 }
67 }
68
69
70
71 type MessageSetting func(m *Message)
72
73
74 func SetCharset(charset string) MessageSetting {
75 return func(m *Message) {
76 m.charset = charset
77 }
78 }
79
80
81 func SetEncoding(enc Encoding) MessageSetting {
82 return func(m *Message) {
83 m.encoding = enc
84 }
85 }
86
87
88 type Encoding string
89
90 const (
91
92
93 QuotedPrintable Encoding = "quoted-printable"
94
95 Base64 Encoding = "base64"
96
97
98 Unencoded Encoding = "8bit"
99 )
100
101
102 func (m *Message) SetBoundary(boundary string) {
103 m.boundary = boundary
104 }
105
106
107 func (m *Message) SetHeader(field string, value ...string) {
108 m.encodeHeader(value)
109 m.header[field] = value
110 }
111
112 func (m *Message) encodeHeader(values []string) {
113 for i := range values {
114 values[i] = m.encodeString(values[i])
115 }
116 }
117
118 func (m *Message) encodeString(value string) string {
119 return m.hEncoder.Encode(m.charset, value)
120 }
121
122
123 func (m *Message) SetHeaders(h map[string][]string) {
124 for k, v := range h {
125 m.SetHeader(k, v...)
126 }
127 }
128
129
130 func (m *Message) SetAddressHeader(field, address, name string) {
131 m.header[field] = []string{m.FormatAddress(address, name)}
132 }
133
134
135 func (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
165 func 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
177 func (m *Message) SetDateHeader(field string, date time.Time) {
178 m.header[field] = []string{m.FormatDate(date)}
179 }
180
181
182 func (m *Message) FormatDate(date time.Time) string {
183 return date.Format(time.RFC1123Z)
184 }
185
186
187 func (m *Message) GetHeader(field string) []string {
188 return m.header[field]
189 }
190
191
192
193 func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
194 m.SetBodyWriter(contentType, newCopier(body), settings...)
195 }
196
197
198
199 func (m *Message) SetBodyWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
200 m.parts = []*part{m.newPart(contentType, f, settings)}
201 }
202
203
204
205
206
207
208
209 func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) {
210 m.AddAlternativeWriter(contentType, newCopier(body), settings...)
211 }
212
213 func 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
221
222 func (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
226 func (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
241
242
243 type PartSetting func(*part)
244
245
246
247 func SetPartEncoding(e Encoding) PartSetting {
248 return PartSetting(func(p *part) {
249 p.encoding = e
250 })
251 }
252
253 type file struct {
254 Name string
255 Header map[string][]string
256 CopyFunc func(w io.Writer) error
257 }
258
259 func (f *file) setHeader(field, value string) {
260 f.Header[field] = []string{value}
261 }
262
263
264 type FileSetting func(*file)
265
266
267
268
269
270
271 func 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
280
281 func Rename(name string) FileSetting {
282 return func(f *file) {
283 f.Name = name
284 }
285 }
286
287
288
289
290
291
292 func SetCopyFunc(f func(io.Writer) error) FileSetting {
293 return func(fi *file) {
294 fi.CopyFunc = f
295 }
296 }
297
298
299 func (m *Message) AttachReader(name string, r io.Reader, settings ...FileSetting) {
300 m.attachments = m.appendFile(m.attachments, fileFromReader(name, r), settings)
301 }
302
303
304 func (m *Message) Attach(filename string, settings ...FileSetting) {
305 m.attachments = m.appendFile(m.attachments, fileFromFilename(filename), settings)
306 }
307
308
309 func (m *Message) EmbedReader(name string, r io.Reader, settings ...FileSetting) {
310 m.embedded = m.appendFile(m.embedded, fileFromReader(name, r), settings)
311 }
312
313
314 func (m *Message) Embed(filename string, settings ...FileSetting) {
315 m.embedded = m.appendFile(m.embedded, fileFromFilename(filename), settings)
316 }
317
318 func 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
336 func 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
349 func (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
View as plain text