github.com/elliott5/community@v0.14.1-0.20160709191136-823126fb026a/documize/api/mail/smtp.go (about)

     1  // Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
     2  //
     3  // This software (Documize Community Edition) is licensed under 
     4  // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
     5  //
     6  // You can operate outside the AGPL restrictions by purchasing
     7  // Documize Enterprise Edition and obtaining a commercial license
     8  // by contacting <sales@documize.com>. 
     9  //
    10  // https://documize.com
    11  
    12  /*
    13  Elements of the software in this file were modified from github.com/jordan-wright/email and
    14  are subject to the licence below:
    15  
    16  The MIT License (MIT)
    17  
    18  Copyright (c) 2013 Jordan Wright
    19  
    20  Permission is hereby granted, free of charge, to any person obtaining a copy of
    21  this software and associated documentation files (the "Software"), to deal in
    22  the Software without restriction, including without limitation the rights to
    23  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
    24  the Software, and to permit persons to whom the Software is furnished to do so,
    25  subject to the following conditions:
    26  
    27  The above copyright notice and this permission notice shall be included in all
    28  copies or substantial portions of the Software.
    29  
    30  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    31  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
    32  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
    33  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
    34  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
    35  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    36  */
    37  
    38  // Package mail sends transactional emails.
    39  // The code in smtp.go is designed to provide an "email interface for humans".
    40  // Designed to be robust and flexible, the email package aims to make sending email easy without getting in the way.
    41  package mail
    42  
    43  import (
    44  	"bytes"
    45  	"encoding/base64"
    46  	"errors"
    47  	"fmt"
    48  	"github.com/documize/community/wordsmith/log"
    49  	"io"
    50  	"mime"
    51  	"mime/multipart"
    52  	"net/mail"
    53  	"net/smtp"
    54  	"net/textproto"
    55  	"os"
    56  	"path/filepath"
    57  	"strings"
    58  	"time"
    59  )
    60  
    61  const (
    62  	// MaxLineLength is the maximum line length per RFC 2045
    63  	MaxLineLength = 76
    64  )
    65  
    66  // Email is the type used for email messages
    67  type Email struct {
    68  	From        string
    69  	To          []string
    70  	Bcc         []string
    71  	Cc          []string
    72  	Subject     string
    73  	Text        []byte // Plaintext message (optional)
    74  	HTML        []byte // Html message (optional)
    75  	Headers     textproto.MIMEHeader
    76  	Attachments []*Attachment
    77  	ReadReceipt []string
    78  }
    79  
    80  // newEmail creates an Email, and returns the pointer to it.
    81  func newEmail() *Email {
    82  	return &Email{Headers: textproto.MIMEHeader{}}
    83  }
    84  
    85  // Attach is used to attach content from an io.Reader to the email.
    86  // Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type
    87  // The function will return the created Attachment for reference, as well as nil for the error, if successful.
    88  func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) {
    89  	var buffer bytes.Buffer
    90  	if _, err = io.Copy(&buffer, r); err != nil {
    91  		return
    92  	}
    93  	at := &Attachment{
    94  		Filename: filename,
    95  		Header:   textproto.MIMEHeader{},
    96  		Content:  buffer.Bytes(),
    97  	}
    98  	// Get the Content-Type to be used in the MIMEHeader
    99  	if c != "" {
   100  		at.Header.Set("Content-Type", c)
   101  	} else {
   102  		// If the Content-Type is blank, set the Content-Type to "application/octet-stream"
   103  		at.Header.Set("Content-Type", "application/octet-stream")
   104  	}
   105  	at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename))
   106  	at.Header.Set("Content-ID", fmt.Sprintf("<%s>", filename))
   107  	at.Header.Set("Content-Transfer-Encoding", "base64")
   108  	e.Attachments = append(e.Attachments, at)
   109  	return at, nil
   110  }
   111  
   112  // AttachFile is used to attach content to the email.
   113  // It attempts to open the file referenced by filename and, if successful, creates an Attachment.
   114  // This Attachment is then appended to the slice of Email.Attachments.
   115  // The function will then return the Attachment for reference, as well as nil for the error, if successful.
   116  func (e *Email) AttachFile(filename string) (a *Attachment, err error) {
   117  	f, err := os.Open(filename)
   118  	if err != nil {
   119  		return
   120  	}
   121  	ct := mime.TypeByExtension(filepath.Ext(filename))
   122  	basename := filepath.Base(filename)
   123  	return e.Attach(f, basename, ct)
   124  }
   125  
   126  // msgHeaders merges the Email's various fields and custom headers together in a
   127  // standards compliant way to create a MIMEHeader to be used in the resulting
   128  // message. It does not alter e.Headers.
   129  //
   130  // "e"'s fields To, Cc, From, Subject will be used unless they are present in
   131  // e.Headers. Unless set in e.Headers, "Date" will filled with the current time.
   132  func (e *Email) msgHeaders() textproto.MIMEHeader {
   133  	res := make(textproto.MIMEHeader, len(e.Headers)+4)
   134  	if e.Headers != nil {
   135  		for _, h := range []string{"To", "Cc", "From", "Subject", "Date"} {
   136  			if v, ok := e.Headers[h]; ok {
   137  				res[h] = v
   138  			}
   139  		}
   140  	}
   141  	// Set headers if there are values.
   142  	if _, ok := res["To"]; !ok && len(e.To) > 0 {
   143  		res.Set("To", strings.Join(e.To, ", "))
   144  	}
   145  	if _, ok := res["Cc"]; !ok && len(e.Cc) > 0 {
   146  		res.Set("Cc", strings.Join(e.Cc, ", "))
   147  	}
   148  	if _, ok := res["Subject"]; !ok && e.Subject != "" {
   149  		res.Set("Subject", e.Subject)
   150  	}
   151  	// Date and From are required headers.
   152  	if _, ok := res["From"]; !ok {
   153  		res.Set("From", e.From)
   154  	}
   155  	if _, ok := res["Date"]; !ok {
   156  		res.Set("Date", time.Now().Format(time.RFC1123Z))
   157  	}
   158  	if _, ok := res["Mime-Version"]; !ok {
   159  		res.Set("Mime-Version", "1.0")
   160  	}
   161  	for field, vals := range e.Headers {
   162  		if _, ok := res[field]; !ok {
   163  			res[field] = vals
   164  		}
   165  	}
   166  	return res
   167  }
   168  
   169  // Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc.
   170  func (e *Email) Bytes() ([]byte, error) {
   171  	// TODO: better guess buffer size
   172  	buff := bytes.NewBuffer(make([]byte, 0, 4096))
   173  
   174  	headers := e.msgHeaders()
   175  	w := multipart.NewWriter(buff)
   176  	// TODO: determine the content type based on message/attachment mix.
   177  	headers.Set("Content-Type", "multipart/mixed;\r\n boundary="+w.Boundary())
   178  	headerToBytes(buff, headers)
   179  	_, err := io.WriteString(buff, "\r\n")
   180  	log.IfErr(err)
   181  	// Start the multipart/mixed part
   182  	fmt.Fprintf(buff, "--%s\r\n", w.Boundary())
   183  	header := textproto.MIMEHeader{}
   184  	// Check to see if there is a Text or HTML field
   185  	if len(e.Text) > 0 || len(e.HTML) > 0 {
   186  		subWriter := multipart.NewWriter(buff)
   187  		// Create the multipart alternative part
   188  		header.Set("Content-Type", fmt.Sprintf("multipart/alternative;\r\n boundary=%s\r\n", subWriter.Boundary()))
   189  		// Write the header
   190  		headerToBytes(buff, header)
   191  		// Create the body sections
   192  		if len(e.Text) > 0 {
   193  			header.Set("Content-Type", fmt.Sprintf("text/plain; charset=UTF-8"))
   194  			header.Set("Content-Transfer-Encoding", "quoted-printable")
   195  			if _, err := subWriter.CreatePart(header); err != nil {
   196  				return nil, err
   197  			}
   198  			// Write the text
   199  			if err := quotePrintEncode(buff, e.Text); err != nil {
   200  				return nil, err
   201  			}
   202  		}
   203  		if len(e.HTML) > 0 {
   204  			header.Set("Content-Type", fmt.Sprintf("text/html; charset=UTF-8"))
   205  			header.Set("Content-Transfer-Encoding", "quoted-printable")
   206  			if _, err := subWriter.CreatePart(header); err != nil {
   207  				return nil, err
   208  			}
   209  			// Write the text
   210  			if err := quotePrintEncode(buff, e.HTML); err != nil {
   211  				return nil, err
   212  			}
   213  		}
   214  		if err := subWriter.Close(); err != nil {
   215  			return nil, err
   216  		}
   217  	}
   218  	// Create attachment part, if necessary
   219  	for _, a := range e.Attachments {
   220  		ap, err := w.CreatePart(a.Header)
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  		// Write the base64Wrapped content to the part
   225  		base64Wrap(ap, a.Content)
   226  	}
   227  	if err := w.Close(); err != nil {
   228  		return nil, err
   229  	}
   230  	return buff.Bytes(), nil
   231  }
   232  
   233  // Send an email using the given host and SMTP auth (optional), returns any error thrown by smtp.SendMail
   234  // This function merges the To, Cc, and Bcc fields and calls the smtp.SendMail function using the Email.Bytes() output as the message
   235  func (e *Email) Send(addr string, a smtp.Auth) error {
   236  	// Merge the To, Cc, and Bcc fields
   237  	to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
   238  	to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
   239  	for i := 0; i < len(to); i++ {
   240  		addr, err := mail.ParseAddress(to[i])
   241  		if err != nil {
   242  			return err
   243  		}
   244  		to[i] = addr.Address
   245  	}
   246  	// Check to make sure there is at least one recipient and one "From" address
   247  	if e.From == "" || len(to) == 0 {
   248  		return errors.New("Must specify at least one From address and one To address")
   249  	}
   250  	from, err := mail.ParseAddress(e.From)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	raw, err := e.Bytes()
   255  	if err != nil {
   256  		return err
   257  	}
   258  	return smtpSendMail(addr, a, from.Address, to, raw)
   259  }
   260  
   261  var smtpSendMail = smtp.SendMail // so that it can be overloaded for testing
   262  
   263  // Attachment is a struct representing an email attachment.
   264  // Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question
   265  type Attachment struct {
   266  	Filename string
   267  	Header   textproto.MIMEHeader
   268  	Content  []byte
   269  }
   270  
   271  // quotePrintEncode writes the quoted-printable text to the IO Writer (according to RFC 2045)
   272  func quotePrintEncode(w io.Writer, body []byte) error {
   273  	var buf [3]byte
   274  	mc := 0
   275  	for _, c := range body {
   276  		// We're assuming Unix style text formats as input (LF line break), and
   277  		// quoted-printable uses CRLF line breaks. (Literal CRs will become
   278  		// "=0D", but probably shouldn't be there to begin with!)
   279  		if c == '\n' {
   280  			_, err := io.WriteString(w, "\r\n")
   281  			if err != nil {
   282  				return err
   283  			}
   284  			mc = 0
   285  			continue
   286  		}
   287  
   288  		var nextOut []byte
   289  		if isPrintable[c] {
   290  			buf[0] = c
   291  			nextOut = buf[:1]
   292  		} else {
   293  			nextOut = buf[:]
   294  			qpEscape(nextOut, c)
   295  		}
   296  
   297  		// Add a soft line break if the next (encoded) byte would push this line
   298  		// to or past the limit.
   299  		if mc+len(nextOut) >= MaxLineLength {
   300  			if _, err := io.WriteString(w, "=\r\n"); err != nil {
   301  				return err
   302  			}
   303  			mc = 0
   304  		}
   305  
   306  		if _, err := w.Write(nextOut); err != nil {
   307  			return err
   308  		}
   309  		mc += len(nextOut)
   310  	}
   311  	// No trailing end-of-line?? Soft line break, then. TODO: is this sane?
   312  	if mc > 0 {
   313  		_, err := io.WriteString(w, "=\r\n")
   314  		if err != nil {
   315  			return err
   316  		}
   317  	}
   318  	return nil
   319  }
   320  
   321  // isPrintable holds true if the byte given is "printable" according to RFC 2045, false otherwise
   322  var isPrintable [256]bool
   323  
   324  func init() {
   325  	for c := '!'; c <= '<'; c++ {
   326  		isPrintable[c] = true
   327  	}
   328  	for c := '>'; c <= '~'; c++ {
   329  		isPrintable[c] = true
   330  	}
   331  	isPrintable[' '] = true
   332  	isPrintable['\n'] = true
   333  	isPrintable['\t'] = true
   334  }
   335  
   336  // qpEscape is a helper function for quotePrintEncode which escapes a
   337  // non-printable byte. Expects len(dest) == 3.
   338  func qpEscape(dest []byte, c byte) {
   339  	const nums = "0123456789ABCDEF"
   340  	dest[0] = '='
   341  	dest[1] = nums[(c&0xf0)>>4]
   342  	dest[2] = nums[(c & 0xf)]
   343  }
   344  
   345  // base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars)
   346  // The output is then written to the specified io.Writer
   347  func base64Wrap(w io.Writer, b []byte) {
   348  	// 57 raw bytes per 76-byte base64 line.
   349  	const maxRaw = 57
   350  	// Buffer for each line, including trailing CRLF.
   351  	buffer := make([]byte, MaxLineLength+len("\r\n"))
   352  	copy(buffer[MaxLineLength:], "\r\n")
   353  	// Process raw chunks until there's no longer enough to fill a line.
   354  	for len(b) >= maxRaw {
   355  		base64.StdEncoding.Encode(buffer, b[:maxRaw])
   356  		_, err := w.Write(buffer)
   357  		log.IfErr(err)
   358  		b = b[maxRaw:]
   359  	}
   360  	// Handle the last chunk of bytes.
   361  	if len(b) > 0 {
   362  		out := buffer[:base64.StdEncoding.EncodedLen(len(b))]
   363  		base64.StdEncoding.Encode(out, b)
   364  		out = append(out, "\r\n"...)
   365  		_, err := w.Write(out)
   366  		log.IfErr(err)
   367  	}
   368  }
   369  
   370  // headerToBytes renders "header" to "buff". If there are multiple values for a
   371  // field, multiple "Field: value\r\n" lines will be emitted.
   372  func headerToBytes(buff *bytes.Buffer, header textproto.MIMEHeader) {
   373  	for field, vals := range header {
   374  		for _, subval := range vals {
   375  			_, err := io.WriteString(buff, field+": "+subval+"\r\n")
   376  			log.IfErr(err)
   377  		}
   378  	}
   379  }