all repos — mail2 @ 1cc410a92186b748cbe850f1b514f210a3807494

fork of github.com/joegrasse/mail with some changes

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}