email.go (view raw)
1package mail
2
3import (
4 "bytes"
5 "crypto/tls"
6 "errors"
7 "fmt"
8 "io/ioutil"
9 "mime"
10 "net"
11 "net/mail"
12 "net/smtp"
13 "net/textproto"
14 "path/filepath"
15 "time"
16)
17
18// Email represents an email message.
19type Email struct {
20 from string
21 sender string
22 replyTo string
23 returnPath string
24 recipients []string
25 headers textproto.MIMEHeader
26 parts []part
27 attachments []*file
28 inlines []*file
29 Charset string
30 Encoding encoding
31 Encryption encryption
32 Username string
33 Password string
34 TLSConfig *tls.Config
35 ConnectTimeout int
36 Error error
37}
38
39// part represents the different content parts of an email body.
40type part struct {
41 contentType string
42 body *bytes.Buffer
43}
44
45// file represents the files that can be added to the email message.
46type file struct {
47 filename string
48 mimeType string
49 data []byte
50}
51
52type encryption int
53
54const (
55 // EncryptionTLS sets encryption type to TLS when sending email
56 EncryptionTLS encryption = iota
57 // EncryptionSSL sets encryption type to SSL when sending email
58 EncryptionSSL
59 // EncryptionNone uses no encryption when sending email
60 EncryptionNone
61)
62
63var encryptionTypes = [...]string{"TLS", "SSL", "None"}
64
65func (encryption encryption) String() string {
66 return encryptionTypes[encryption]
67}
68
69type encoding int
70
71const (
72 // EncodingQuotedPrintable sets the message body encoding to quoted-printable
73 EncodingQuotedPrintable encoding = iota
74 // EncodingBase64 sets the message body encoding to base64
75 EncodingBase64
76 // EncodingNone turns off encoding on the message body
77 EncodingNone
78)
79
80var encodingTypes = [...]string{"quoted-printable", "base64", "binary"}
81
82func (encoding encoding) String() string {
83 return encodingTypes[encoding]
84}
85
86// New creates a new email. It uses UTF-8 by default.
87func New() *Email {
88 email := &Email{
89 headers: make(textproto.MIMEHeader),
90 Charset: "UTF-8",
91 Encoding: EncodingQuotedPrintable,
92 Encryption: EncryptionNone,
93 TLSConfig: new(tls.Config),
94 }
95
96 email.AddHeader("MIME-Version", "1.0")
97
98 return email
99}
100
101// GetError returns the first email error encountered
102func (email *Email) GetError() error {
103 return email.Error
104}
105
106// SetFrom sets the From address.
107func (email *Email) SetFrom(address string) *Email {
108 if email.Error != nil {
109 return email
110 }
111
112 email.AddAddresses("From", address)
113
114 return email
115}
116
117// SetSender sets the Sender address.
118func (email *Email) SetSender(address string) *Email {
119 if email.Error != nil {
120 return email
121 }
122
123 email.AddAddresses("Sender", address)
124
125 return email
126}
127
128// SetReplyTo sets the Reply-To address.
129func (email *Email) SetReplyTo(address string) *Email {
130 if email.Error != nil {
131 return email
132 }
133
134 email.AddAddresses("Reply-To", address)
135
136 return email
137}
138
139// SetReturnPath sets the Return-Path address. This is most often used
140// to send bounced emails to a different email address.
141func (email *Email) SetReturnPath(address string) *Email {
142 if email.Error != nil {
143 return email
144 }
145
146 email.AddAddresses("Return-Path", address)
147
148 return email
149}
150
151// AddTo adds a To address. You can provide multiple
152// addresses at the same time.
153func (email *Email) AddTo(addresses ...string) *Email {
154 if email.Error != nil {
155 return email
156 }
157
158 email.AddAddresses("To", addresses...)
159
160 return email
161}
162
163// AddCc adds a Cc address. You can provide multiple
164// addresses at the same time.
165func (email *Email) AddCc(addresses ...string) *Email {
166 if email.Error != nil {
167 return email
168 }
169
170 email.AddAddresses("Cc", addresses...)
171
172 return email
173}
174
175// AddBcc adds a Bcc address. You can provide multiple
176// addresses at the same time.
177func (email *Email) AddBcc(addresses ...string) *Email {
178 if email.Error != nil {
179 return email
180 }
181
182 email.AddAddresses("Bcc", addresses...)
183
184 return email
185}
186
187// AddAddresses allows you to add addresses to the specified address header.
188func (email *Email) AddAddresses(header string, addresses ...string) *Email {
189 if email.Error != nil {
190 return email
191 }
192
193 found := false
194
195 // check for a valid address header
196 for _, h := range []string{"To", "Cc", "Bcc", "From", "Sender", "Reply-To", "Return-Path"} {
197 if header == h {
198 found = true
199 }
200 }
201
202 if !found {
203 email.Error = errors.New("Mail Error: Invalid address header; Header: [" + header + "]")
204 return email
205 }
206
207 // check to see if the addresses are valid
208 for i := range addresses {
209 address, err := mail.ParseAddress(addresses[i])
210 if err != nil {
211 email.Error = errors.New("Mail Error: " + err.Error() + "; Header: [" + header + "] Address: [" + addresses[i] + "]")
212 return email
213 }
214
215 // check for more than one address
216 switch {
217 case header == "From" && len(email.from) > 0:
218 fallthrough
219 case header == "Sender" && len(email.sender) > 0:
220 fallthrough
221 case header == "Reply-To" && len(email.replyTo) > 0:
222 fallthrough
223 case header == "Return-Path" && len(email.returnPath) > 0:
224 email.Error = errors.New("Mail Error: There can only be one \"" + header + "\" address; Header: [" + header + "] Address: [" + addresses[i] + "]")
225 return email
226 default:
227 // other address types can have more than one address
228 }
229
230 // save the address
231 switch header {
232 case "From":
233 email.from = address.Address
234 case "Sender":
235 email.sender = address.Address
236 case "Reply-To":
237 email.replyTo = address.Address
238 case "Return-Path":
239 email.returnPath = address.Address
240 default:
241 // check that the address was added to the recipients list
242 email.recipients, err = addAddress(email.recipients, address.Address)
243 if err != nil {
244 email.Error = errors.New("Mail Error: " + err.Error() + "; Header: [" + header + "] Address: [" + addresses[i] + "]")
245 return email
246 }
247 }
248
249 // make sure the from and sender addresses are different
250 if email.from != "" && email.sender != "" && email.from == email.sender {
251 email.sender = ""
252 email.headers.Del("Sender")
253 email.Error = errors.New("Mail Error: From and Sender should not be set to the same address.")
254 return email
255 }
256
257 // add all addresses to the headers except for Bcc and Return-Path
258 if header != "Bcc" && header != "Return-Path" {
259 // add the address to the headers
260 email.headers.Add(header, address.String())
261 }
262 }
263
264 return email
265}
266
267// addAddress adds an address to the address list if it hasn't already been added
268func addAddress(addressList []string, address string) ([]string, error) {
269 // loop through the address list to check for dups
270 for _, a := range addressList {
271 if address == a {
272 return addressList, errors.New("Mail Error: Address: [" + address + "] has already been added")
273 }
274 }
275
276 return append(addressList, address), nil
277}
278
279type priority int
280
281const (
282 // PriorityHigh sets the email priority to High
283 PriorityHigh priority = iota
284 // PriorityLow sets the email priority to Low
285 PriorityLow
286)
287
288var priorities = [...]string{"High", "Low"}
289
290func (priority priority) String() string {
291 return priorities[priority]
292}
293
294// SetPriority sets the email message priority. Use with
295// either "High" or "Low".
296func (email *Email) SetPriority(priority priority) *Email {
297 if email.Error != nil {
298 return email
299 }
300
301 switch priority {
302 case PriorityHigh:
303 email.AddHeaders(textproto.MIMEHeader{
304 "X-Priority": {"1 (Highest)"},
305 "X-MSMail-Priority": {"High"},
306 "Importance": {"High"},
307 })
308 case PriorityLow:
309 email.AddHeaders(textproto.MIMEHeader{
310 "X-Priority": {"5 (Lowest)"},
311 "X-MSMail-Priority": {"Low"},
312 "Importance": {"Low"},
313 })
314 default:
315 }
316
317 return email
318}
319
320// SetDate sets the date header to the provided date/time.
321// The format of the string should be YYYY-MM-DD HH:MM:SS Time Zone.
322//
323// Example: SetDate("2015-04-28 10:32:00 CDT")
324func (email *Email) SetDate(dateTime string) *Email {
325 if email.Error != nil {
326 return email
327 }
328
329 const dateFormat = "2006-01-02 15:04:05 MST"
330
331 // Try to parse the provided date/time
332 dt, err := time.Parse(dateFormat, dateTime)
333 if err != nil {
334 email.Error = errors.New("Mail Error: Setting date failed with: " + err.Error())
335 return email
336 }
337
338 email.headers.Set("Date", dt.Format(time.RFC1123Z))
339
340 return email
341}
342
343// SetSubject sets the subject of the email message.
344func (email *Email) SetSubject(subject string) *Email {
345 if email.Error != nil {
346 return email
347 }
348
349 email.AddHeader("Subject", subject)
350
351 return email
352}
353
354// SetBody sets the body of the email message.
355func (email *Email) SetBody(contentType, body string) *Email {
356 if email.Error != nil {
357 return email
358 }
359
360 email.parts = []part{
361 part{
362 contentType: contentType,
363 body: bytes.NewBufferString(body),
364 },
365 }
366
367 return email
368}
369
370// AddHeader adds the given "header" with the passed "value".
371func (email *Email) AddHeader(header string, values ...string) *Email {
372 if email.Error != nil {
373 return email
374 }
375
376 // check that there is actually a value
377 if len(values) < 1 {
378 email.Error = errors.New("Mail Error: no value provided; Header: [" + header + "]")
379 return email
380 }
381
382 switch header {
383 case "Sender":
384 fallthrough
385 case "From":
386 fallthrough
387 case "To":
388 fallthrough
389 case "Bcc":
390 fallthrough
391 case "Cc":
392 fallthrough
393 case "Reply-To":
394 fallthrough
395 case "Return-Path":
396 email.AddAddresses(header, values...)
397 case "Date":
398 if len(values) > 1 {
399 email.Error = errors.New("Mail Error: To many dates provided")
400 return email
401 }
402 email.SetDate(values[0])
403 default:
404 email.headers[header] = values
405 }
406
407 return email
408}
409
410// AddHeaders is used to add mulitple headers at once
411func (email *Email) AddHeaders(headers textproto.MIMEHeader) *Email {
412 if email.Error != nil {
413 return email
414 }
415
416 for header, values := range headers {
417 email.AddHeader(header, values...)
418 }
419
420 return email
421}
422
423// AddAlternative allows you to add alternative parts to the body
424// of the email message. This is most commonly used to add an
425// html version in addition to a plain text version that was
426// already added with SetBody.
427func (email *Email) AddAlternative(contentType, body string) *Email {
428 if email.Error != nil {
429 return email
430 }
431
432 email.parts = append(email.parts,
433 part{
434 contentType: contentType,
435 body: bytes.NewBufferString(body),
436 },
437 )
438
439 return email
440}
441
442// AddAttachment allows you to add an attachment to the email message.
443// You can optionally provide a different name for the file.
444func (email *Email) AddAttachment(file string, name ...string) *Email {
445 if email.Error != nil {
446 return email
447 }
448
449 if len(name) > 1 {
450 email.Error = errors.New("Mail Error: Attach can only have a file and an optional name")
451 return email
452 }
453
454 email.Error = email.attach(file, false, name...)
455
456 return email
457}
458
459// AddInline allows you to add an inline attachment to the email message.
460// You can optionally provide a different name for the file.
461func (email *Email) AddInline(file string, name ...string) *Email {
462 if email.Error != nil {
463 return email
464 }
465
466 if len(name) > 1 {
467 email.Error = errors.New("Mail Error: Inline can only have a file and an optional name")
468 return email
469 }
470
471 email.Error = email.attach(file, true, name...)
472
473 return email
474}
475
476// attach does the low level attaching of the files
477func (email *Email) attach(f string, inline bool, name ...string) error {
478 // Get the file data
479 data, err := ioutil.ReadFile(f)
480 if err != nil {
481 return errors.New("Mail Error: Failed to add file with following error: " + err.Error())
482 }
483
484 // get the file mime type
485 mimeType := mime.TypeByExtension(filepath.Ext(f))
486 if mimeType == "" {
487 mimeType = "application/octet-stream"
488 }
489
490 // get the filename
491 _, filename := filepath.Split(f)
492
493 // if an alternative filename was provided, use that instead
494 if len(name) == 1 {
495 filename = name[0]
496 }
497
498 if inline {
499 email.inlines = append(email.inlines, &file{
500 filename: filename,
501 mimeType: mimeType,
502 data: data,
503 })
504 } else {
505 email.attachments = append(email.attachments, &file{
506 filename: filename,
507 mimeType: mimeType,
508 data: data,
509 })
510 }
511
512 return nil
513}
514
515// getFrom returns the sender of the email, if any
516func (email *Email) getFrom() string {
517 from := email.returnPath
518 if from == "" {
519 from = email.sender
520 if from == "" {
521 from = email.from
522 if from == "" {
523 from = email.replyTo
524 }
525 }
526 }
527
528 return from
529}
530
531func (email *Email) hasMixedPart() bool {
532 return (len(email.parts) > 0 && len(email.attachments) > 0) || len(email.attachments) > 1
533}
534
535func (email *Email) hasRelatedPart() bool {
536 return (len(email.parts) > 0 && len(email.inlines) > 0) || len(email.inlines) > 1
537}
538
539func (email *Email) hasAlternativePart() bool {
540 return len(email.parts) > 1
541}
542
543// GetMessage builds and returns the email message
544func (email *Email) GetMessage() string {
545 msg := newMessage(email)
546
547 if email.hasMixedPart() {
548 msg.openMultipart("mixed")
549 }
550
551 if email.hasRelatedPart() {
552 msg.openMultipart("related")
553 }
554
555 if email.hasAlternativePart() {
556 msg.openMultipart("alternative")
557 }
558
559 for _, part := range email.parts {
560 msg.addBody(part.contentType, part.body.Bytes())
561 }
562
563 if email.hasAlternativePart() {
564 msg.closeMultipart()
565 }
566
567 msg.addFiles(email.inlines, true)
568 if email.hasRelatedPart() {
569 msg.closeMultipart()
570 }
571
572 msg.addFiles(email.attachments, false)
573 if email.hasMixedPart() {
574 msg.closeMultipart()
575 }
576
577 return msg.getHeaders() + msg.body.String()
578}
579
580// Send sends the composed email
581func (email *Email) Send(address string) error {
582 if email.Error != nil {
583 return email.Error
584 }
585
586 var auth smtp.Auth
587
588 from := email.getFrom()
589 if from == "" {
590 return errors.New(`Mail Error: No "From" address specified.`)
591 }
592
593 if len(email.recipients) < 1 {
594 return errors.New("Mail Error: No recipient specified.")
595 }
596
597 msg := email.GetMessage()
598
599 host, port, err := net.SplitHostPort(address)
600 if err != nil {
601 return errors.New("Mail Error: " + err.Error())
602 }
603
604 if email.Username != "" || email.Password != "" {
605 auth = smtp.PlainAuth("", email.Username, email.Password, host)
606 }
607
608 return send(host, port, from, email.recipients, msg, auth, email.Encryption, email.TLSConfig, email.ConnectTimeout)
609}
610
611// dial connects to the smtp server with the request encryption type
612func dial(host string, port string, encryption encryption, config *tls.Config) (*smtp.Client, error) {
613 var conn net.Conn
614 var err error
615
616 address := host + ":" + port
617
618 // do the actual dial
619 switch encryption {
620 case EncryptionSSL:
621 conn, err = tls.Dial("tcp", address, config)
622 default:
623 conn, err = net.Dial("tcp", address)
624 }
625
626 if err != nil {
627 return nil, errors.New("Mail Error on dailing with encryption type " + encryption.String() + ": " + err.Error())
628 }
629
630 c, err := smtp.NewClient(conn, host)
631
632 if err != nil {
633 return nil, errors.New("Mail Error on smtp dial: " + err.Error())
634 }
635
636 return c, err
637}
638
639// smtpConnect connects to the smtp server and starts TLS and passes auth
640// if necessary
641func smtpConnect(host string, port string, from string, to []string, msg string, auth smtp.Auth, encryption encryption, config *tls.Config) (*smtp.Client, error) {
642 // connect to the mail server
643 c, err := dial(host, port, encryption, config)
644
645 if err != nil {
646 return nil, err
647 }
648
649 // send Hello
650 if err = c.Hello("localhost"); err != nil {
651 c.Close()
652 return nil, errors.New("Mail Error on Hello: " + err.Error())
653 }
654
655 // start TLS if necessary
656 if encryption == EncryptionTLS {
657 if ok, _ := c.Extension("STARTTLS"); ok {
658 if config.ServerName == "" {
659 config = &tls.Config{ServerName: host}
660 }
661
662 if err = c.StartTLS(config); err != nil {
663 c.Close()
664 return nil, errors.New("Mail Error on Start TLS: " + err.Error())
665 }
666 }
667 }
668
669 // pass the authentication if necessary
670 if auth != nil {
671 if ok, _ := c.Extension("AUTH"); ok {
672 if err = c.Auth(auth); err != nil {
673 c.Close()
674 return nil, errors.New("Mail Error on Auth: " + err.Error())
675 }
676 }
677 }
678
679 return c, nil
680}
681
682type smtpConnectErrorChannel struct {
683 client *smtp.Client
684 err error
685}
686
687// send does the low level sending of the email
688func send(host string, port string, from string, to []string, msg string, auth smtp.Auth, encryption encryption, config *tls.Config, connectTimeout int) error {
689 var smtpConnectChannel chan smtpConnectErrorChannel
690 var c *smtp.Client
691 var err error
692
693 // set the timeout value
694 timeout := time.Duration(connectTimeout) * time.Second
695
696 // if there is a timeout, setup the channel and do the connect under a goroutine
697 if timeout != 0 {
698 smtpConnectChannel = make(chan smtpConnectErrorChannel, 2)
699 go func() {
700 c, err = smtpConnect(host, port, from, to, msg, auth, encryption, config)
701 // send the result
702 smtpConnectChannel <- smtpConnectErrorChannel{
703 client: c,
704 err: err,
705 }
706 }()
707 }
708
709 if timeout == 0 {
710 // no timeout, just fire the connect
711 c, err = smtpConnect(host, port, from, to, msg, auth, encryption, config)
712 } else {
713 // get the connect result or timeout result, which ever happens first
714 select {
715 case result := <-smtpConnectChannel:
716 c = result.client
717 err = result.err
718 case <-time.After(timeout):
719 return errors.New("Mail Error: SMTP Connection timed out")
720 }
721 }
722
723 // check for connect error
724 if err != nil {
725 return err
726 }
727
728 defer c.Close()
729
730 // Set the sender
731 if err := c.Mail(from); err != nil {
732 return err
733 }
734
735 // Set the recipients
736 for _, address := range to {
737 if err := c.Rcpt(address); err != nil {
738 return err
739 }
740 }
741
742 // Send the data command
743 w, err := c.Data()
744 if err != nil {
745 return err
746 }
747
748 // write the message
749 _, err = fmt.Fprint(w, msg)
750 if err != nil {
751 return err
752 }
753
754 err = w.Close()
755 if err != nil {
756 return err
757 }
758
759 return c.Quit()
760}