github.com/astaxie/beego@v1.12.3/utils/mail.go (about)

     1  // Copyright 2014 beego Author. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package utils
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/base64"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"mime"
    25  	"mime/multipart"
    26  	"net/mail"
    27  	"net/smtp"
    28  	"net/textproto"
    29  	"os"
    30  	"path"
    31  	"path/filepath"
    32  	"strconv"
    33  	"strings"
    34  	"sync"
    35  )
    36  
    37  const (
    38  	maxLineLength = 76
    39  
    40  	upperhex = "0123456789ABCDEF"
    41  )
    42  
    43  // Email is the type used for email messages
    44  type Email struct {
    45  	Auth        smtp.Auth
    46  	Identity    string `json:"identity"`
    47  	Username    string `json:"username"`
    48  	Password    string `json:"password"`
    49  	Host        string `json:"host"`
    50  	Port        int    `json:"port"`
    51  	From        string `json:"from"`
    52  	To          []string
    53  	Bcc         []string
    54  	Cc          []string
    55  	Subject     string
    56  	Text        string // Plaintext message (optional)
    57  	HTML        string // Html message (optional)
    58  	Headers     textproto.MIMEHeader
    59  	Attachments []*Attachment
    60  	ReadReceipt []string
    61  }
    62  
    63  // Attachment is a struct representing an email attachment.
    64  // Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question
    65  type Attachment struct {
    66  	Filename string
    67  	Header   textproto.MIMEHeader
    68  	Content  []byte
    69  }
    70  
    71  // NewEMail create new Email struct with config json.
    72  // config json is followed from Email struct fields.
    73  func NewEMail(config string) *Email {
    74  	e := new(Email)
    75  	e.Headers = textproto.MIMEHeader{}
    76  	err := json.Unmarshal([]byte(config), e)
    77  	if err != nil {
    78  		return nil
    79  	}
    80  	return e
    81  }
    82  
    83  // Bytes Make all send information to byte
    84  func (e *Email) Bytes() ([]byte, error) {
    85  	buff := &bytes.Buffer{}
    86  	w := multipart.NewWriter(buff)
    87  	// Set the appropriate headers (overwriting any conflicts)
    88  	// Leave out Bcc (only included in envelope headers)
    89  	e.Headers.Set("To", strings.Join(e.To, ","))
    90  	if e.Cc != nil {
    91  		e.Headers.Set("Cc", strings.Join(e.Cc, ","))
    92  	}
    93  	e.Headers.Set("From", e.From)
    94  	e.Headers.Set("Subject", e.Subject)
    95  	if len(e.ReadReceipt) != 0 {
    96  		e.Headers.Set("Disposition-Notification-To", strings.Join(e.ReadReceipt, ","))
    97  	}
    98  	e.Headers.Set("MIME-Version", "1.0")
    99  
   100  	// Write the envelope headers (including any custom headers)
   101  	if err := headerToBytes(buff, e.Headers); err != nil {
   102  		return nil, fmt.Errorf("Failed to render message headers: %s", err)
   103  	}
   104  
   105  	e.Headers.Set("Content-Type", fmt.Sprintf("multipart/mixed;\r\n boundary=%s\r\n", w.Boundary()))
   106  	fmt.Fprintf(buff, "%s:", "Content-Type")
   107  	fmt.Fprintf(buff, " %s\r\n", fmt.Sprintf("multipart/mixed;\r\n boundary=%s\r\n", w.Boundary()))
   108  
   109  	// Start the multipart/mixed part
   110  	fmt.Fprintf(buff, "--%s\r\n", w.Boundary())
   111  	header := textproto.MIMEHeader{}
   112  	// Check to see if there is a Text or HTML field
   113  	if e.Text != "" || e.HTML != "" {
   114  		subWriter := multipart.NewWriter(buff)
   115  		// Create the multipart alternative part
   116  		header.Set("Content-Type", fmt.Sprintf("multipart/alternative;\r\n boundary=%s\r\n", subWriter.Boundary()))
   117  		// Write the header
   118  		if err := headerToBytes(buff, header); err != nil {
   119  			return nil, fmt.Errorf("Failed to render multipart message headers: %s", err)
   120  		}
   121  		// Create the body sections
   122  		if e.Text != "" {
   123  			header.Set("Content-Type", fmt.Sprintf("text/plain; charset=UTF-8"))
   124  			header.Set("Content-Transfer-Encoding", "quoted-printable")
   125  			if _, err := subWriter.CreatePart(header); err != nil {
   126  				return nil, err
   127  			}
   128  			// Write the text
   129  			if err := quotePrintEncode(buff, e.Text); err != nil {
   130  				return nil, err
   131  			}
   132  		}
   133  		if e.HTML != "" {
   134  			header.Set("Content-Type", fmt.Sprintf("text/html; charset=UTF-8"))
   135  			header.Set("Content-Transfer-Encoding", "quoted-printable")
   136  			if _, err := subWriter.CreatePart(header); err != nil {
   137  				return nil, err
   138  			}
   139  			// Write the text
   140  			if err := quotePrintEncode(buff, e.HTML); err != nil {
   141  				return nil, err
   142  			}
   143  		}
   144  		if err := subWriter.Close(); err != nil {
   145  			return nil, err
   146  		}
   147  	}
   148  	// Create attachment part, if necessary
   149  	for _, a := range e.Attachments {
   150  		ap, err := w.CreatePart(a.Header)
   151  		if err != nil {
   152  			return nil, err
   153  		}
   154  		// Write the base64Wrapped content to the part
   155  		base64Wrap(ap, a.Content)
   156  	}
   157  	if err := w.Close(); err != nil {
   158  		return nil, err
   159  	}
   160  	return buff.Bytes(), nil
   161  }
   162  
   163  // AttachFile Add attach file to the send mail
   164  func (e *Email) AttachFile(args ...string) (a *Attachment, err error) {
   165  	if len(args) < 1 || len(args) > 2 { // change && to ||
   166  		err = errors.New("Must specify a file name and number of parameters can not exceed at least two")
   167  		return
   168  	}
   169  	filename := args[0]
   170  	id := ""
   171  	if len(args) > 1 {
   172  		id = args[1]
   173  	}
   174  	f, err := os.Open(filename)
   175  	if err != nil {
   176  		return
   177  	}
   178  	defer f.Close()
   179  	ct := mime.TypeByExtension(filepath.Ext(filename))
   180  	basename := path.Base(filename)
   181  	return e.Attach(f, basename, ct, id)
   182  }
   183  
   184  // Attach is used to attach content from an io.Reader to the email.
   185  // Parameters include an io.Reader, the desired filename for the attachment, and the Content-Type.
   186  func (e *Email) Attach(r io.Reader, filename string, args ...string) (a *Attachment, err error) {
   187  	if len(args) < 1 || len(args) > 2 { // change && to ||
   188  		err = errors.New("Must specify the file type and number of parameters can not exceed at least two")
   189  		return
   190  	}
   191  	c := args[0] //Content-Type
   192  	id := ""
   193  	if len(args) > 1 {
   194  		id = args[1] //Content-ID
   195  	}
   196  	var buffer bytes.Buffer
   197  	if _, err = io.Copy(&buffer, r); err != nil {
   198  		return
   199  	}
   200  	at := &Attachment{
   201  		Filename: filename,
   202  		Header:   textproto.MIMEHeader{},
   203  		Content:  buffer.Bytes(),
   204  	}
   205  	// Get the Content-Type to be used in the MIMEHeader
   206  	if c != "" {
   207  		at.Header.Set("Content-Type", c)
   208  	} else {
   209  		// If the Content-Type is blank, set the Content-Type to "application/octet-stream"
   210  		at.Header.Set("Content-Type", "application/octet-stream")
   211  	}
   212  	if id != "" {
   213  		at.Header.Set("Content-Disposition", fmt.Sprintf("inline;\r\n filename=\"%s\"", filename))
   214  		at.Header.Set("Content-ID", fmt.Sprintf("<%s>", id))
   215  	} else {
   216  		at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename))
   217  	}
   218  	at.Header.Set("Content-Transfer-Encoding", "base64")
   219  	e.Attachments = append(e.Attachments, at)
   220  	return at, nil
   221  }
   222  
   223  // Send will send out the mail
   224  func (e *Email) Send() error {
   225  	if e.Auth == nil {
   226  		e.Auth = smtp.PlainAuth(e.Identity, e.Username, e.Password, e.Host)
   227  	}
   228  	// Merge the To, Cc, and Bcc fields
   229  	to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
   230  	to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
   231  	// Check to make sure there is at least one recipient and one "From" address
   232  	if len(to) == 0 {
   233  		return errors.New("Must specify at least one To address")
   234  	}
   235  
   236  	// Use the username if no From is provided
   237  	if len(e.From) == 0 {
   238  		e.From = e.Username
   239  	}
   240  
   241  	from, err := mail.ParseAddress(e.From)
   242  	if err != nil {
   243  		return err
   244  	}
   245  
   246  	// use mail's RFC 2047 to encode any string
   247  	e.Subject = qEncode("utf-8", e.Subject)
   248  
   249  	raw, err := e.Bytes()
   250  	if err != nil {
   251  		return err
   252  	}
   253  	return smtp.SendMail(e.Host+":"+strconv.Itoa(e.Port), e.Auth, from.Address, to, raw)
   254  }
   255  
   256  // quotePrintEncode writes the quoted-printable text to the IO Writer (according to RFC 2045)
   257  func quotePrintEncode(w io.Writer, s string) error {
   258  	var buf [3]byte
   259  	mc := 0
   260  	for i := 0; i < len(s); i++ {
   261  		c := s[i]
   262  		// We're assuming Unix style text formats as input (LF line break), and
   263  		// quoted-printble uses CRLF line breaks. (Literal CRs will become
   264  		// "=0D", but probably shouldn't be there to begin with!)
   265  		if c == '\n' {
   266  			io.WriteString(w, "\r\n")
   267  			mc = 0
   268  			continue
   269  		}
   270  
   271  		var nextOut []byte
   272  		if isPrintable(c) {
   273  			nextOut = append(buf[:0], c)
   274  		} else {
   275  			nextOut = buf[:]
   276  			qpEscape(nextOut, c)
   277  		}
   278  
   279  		// Add a soft line break if the next (encoded) byte would push this line
   280  		// to or past the limit.
   281  		if mc+len(nextOut) >= maxLineLength {
   282  			if _, err := io.WriteString(w, "=\r\n"); err != nil {
   283  				return err
   284  			}
   285  			mc = 0
   286  		}
   287  
   288  		if _, err := w.Write(nextOut); err != nil {
   289  			return err
   290  		}
   291  		mc += len(nextOut)
   292  	}
   293  	// No trailing end-of-line?? Soft line break, then. TODO: is this sane?
   294  	if mc > 0 {
   295  		io.WriteString(w, "=\r\n")
   296  	}
   297  	return nil
   298  }
   299  
   300  // isPrintable returns true if the rune given is "printable" according to RFC 2045, false otherwise
   301  func isPrintable(c byte) bool {
   302  	return (c >= '!' && c <= '<') || (c >= '>' && c <= '~') || (c == ' ' || c == '\n' || c == '\t')
   303  }
   304  
   305  // qpEscape is a helper function for quotePrintEncode which escapes a
   306  // non-printable byte. Expects len(dest) == 3.
   307  func qpEscape(dest []byte, c byte) {
   308  	const nums = "0123456789ABCDEF"
   309  	dest[0] = '='
   310  	dest[1] = nums[(c&0xf0)>>4]
   311  	dest[2] = nums[(c & 0xf)]
   312  }
   313  
   314  // headerToBytes enumerates the key and values in the header, and writes the results to the IO Writer
   315  func headerToBytes(w io.Writer, t textproto.MIMEHeader) error {
   316  	for k, v := range t {
   317  		// Write the header key
   318  		_, err := fmt.Fprintf(w, "%s:", k)
   319  		if err != nil {
   320  			return err
   321  		}
   322  		// Write each value in the header
   323  		for _, c := range v {
   324  			_, err := fmt.Fprintf(w, " %s\r\n", c)
   325  			if err != nil {
   326  				return err
   327  			}
   328  		}
   329  	}
   330  	return nil
   331  }
   332  
   333  // base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars)
   334  // The output is then written to the specified io.Writer
   335  func base64Wrap(w io.Writer, b []byte) {
   336  	// 57 raw bytes per 76-byte base64 line.
   337  	const maxRaw = 57
   338  	// Buffer for each line, including trailing CRLF.
   339  	var buffer [maxLineLength + len("\r\n")]byte
   340  	copy(buffer[maxLineLength:], "\r\n")
   341  	// Process raw chunks until there's no longer enough to fill a line.
   342  	for len(b) >= maxRaw {
   343  		base64.StdEncoding.Encode(buffer[:], b[:maxRaw])
   344  		w.Write(buffer[:])
   345  		b = b[maxRaw:]
   346  	}
   347  	// Handle the last chunk of bytes.
   348  	if len(b) > 0 {
   349  		out := buffer[:base64.StdEncoding.EncodedLen(len(b))]
   350  		base64.StdEncoding.Encode(out, b)
   351  		out = append(out, "\r\n"...)
   352  		w.Write(out)
   353  	}
   354  }
   355  
   356  // Encode returns the encoded-word form of s. If s is ASCII without special
   357  // characters, it is returned unchanged. The provided charset is the IANA
   358  // charset name of s. It is case insensitive.
   359  // RFC 2047 encoded-word
   360  func qEncode(charset, s string) string {
   361  	if !needsEncoding(s) {
   362  		return s
   363  	}
   364  	return encodeWord(charset, s)
   365  }
   366  
   367  func needsEncoding(s string) bool {
   368  	for _, b := range s {
   369  		if (b < ' ' || b > '~') && b != '\t' {
   370  			return true
   371  		}
   372  	}
   373  	return false
   374  }
   375  
   376  // encodeWord encodes a string into an encoded-word.
   377  func encodeWord(charset, s string) string {
   378  	buf := getBuffer()
   379  
   380  	buf.WriteString("=?")
   381  	buf.WriteString(charset)
   382  	buf.WriteByte('?')
   383  	buf.WriteByte('q')
   384  	buf.WriteByte('?')
   385  
   386  	enc := make([]byte, 3)
   387  	for i := 0; i < len(s); i++ {
   388  		b := s[i]
   389  		switch {
   390  		case b == ' ':
   391  			buf.WriteByte('_')
   392  		case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_':
   393  			buf.WriteByte(b)
   394  		default:
   395  			enc[0] = '='
   396  			enc[1] = upperhex[b>>4]
   397  			enc[2] = upperhex[b&0x0f]
   398  			buf.Write(enc)
   399  		}
   400  	}
   401  	buf.WriteString("?=")
   402  
   403  	es := buf.String()
   404  	putBuffer(buf)
   405  	return es
   406  }
   407  
   408  var bufPool = sync.Pool{
   409  	New: func() interface{} {
   410  		return new(bytes.Buffer)
   411  	},
   412  }
   413  
   414  func getBuffer() *bytes.Buffer {
   415  	return bufPool.Get().(*bytes.Buffer)
   416  }
   417  
   418  func putBuffer(buf *bytes.Buffer) {
   419  	if buf.Len() > 1024 {
   420  		return
   421  	}
   422  	buf.Reset()
   423  	bufPool.Put(buf)
   424  }