...
Tawesoft Logo

Source file src/tawesoft.co.uk/go/email/message.go

Documentation: src/tawesoft.co.uk/go/email/message.go

     1  package email
     2  
     3  import (
     4      "bufio"
     5      "crypto/rand"
     6      "fmt"
     7      "io"
     8      "mime/quotedprintable"
     9      "net/mail"
    10      "net/textproto"
    11      "strings"
    12      "time"
    13  )
    14  
    15  // Message defines a multipart e-mail (including headers, HTML body, plain text body, and attachments).
    16  //
    17  // Use the `headers` parameter to specify additional headers. Note that `mail.Header` maps keys to a *list* of
    18  // strings, because some headers may appear multiple times.
    19  type Message struct {
    20      ID  string // Message-ID header, excluding angle brackets
    21      From mail.Address
    22      To   []mail.Address
    23      Cc   []mail.Address
    24      Bcc  []mail.Address
    25      Subject string
    26  
    27      // Headers are additional headers for the message. The combination of a
    28      // header and a value as strings must not exceed a length of 996
    29      // characters. Longer values CANNOT be supported with folding white space
    30      // syntax without advance knowledge (special cases are possible but not
    31      // currently implemented).
    32      Headers mail.Header
    33  
    34      // Html is a HTML-encoded version of the message. Lines must not exceed
    35      // 998 characters.
    36      Html string
    37  
    38      // Text is a plain-text version of the message. It is your responsibility
    39      // to ensure word-wrapping. Lines must not exceed 998 characters.
    40      Text string
    41  
    42      // Attachments is a lazily-loaded sequence of attachments. May be nil.
    43      Attachments []*Attachment
    44  }
    45  
    46  // Envelope wraps an Email with some SMTP protocol information for extra control.
    47  type Envelope struct {
    48      // ReturnPath is the sender in the FROM SMTP command.
    49      //
    50      // Often, this should match the Email From address.
    51      //
    52      // In the cause of autoreplies (like "Out of Office" or bounces or delivery
    53      // status notifications) this should be an empty string to stop an infinite
    54      // loop of bounces.
    55      ReturnPath string
    56  
    57      // ReceiptTo is a list of recipients. This is normally automatically
    58      // generated from the Email To/CC/BCC addresses.
    59      ReceiptTo []string
    60  }
    61  
    62  // bound defines an email message boundary
    63  type bound struct {
    64      label string
    65  }
    66  
    67  func (b bound) start(w io.Writer, contentType string) {
    68      fmt.Fprintf(w, "Content-Type: %s; boundary=\"%s\"\r\n", contentType, b.label)
    69      fmt.Fprintf(w, "\r\n")
    70      fmt.Fprintf(w, "--%s\r\n", b.label)
    71  }
    72  
    73  func (b bound) next(w io.Writer) {
    74      fmt.Fprintf(w, "--%s\r\n", b.label)
    75  }
    76  
    77  func (b bound) end(w io.Writer) {
    78      fmt.Fprintf(w, "--%s--\r\n", b.label)
    79  }
    80  
    81  // boundary returns a randomly-generated RFC 1341 boundary with a label of exactly 70 characters with a given prefix.
    82  // The randomly generated prefix is cryptographically secure iff `rand` is `crypto/rand`.
    83  func boundary(prefix string) (bound, error) {
    84  
    85      // RFC 1341 sets this maximum length for a boundary
    86      const maxlen int = 70
    87  
    88      // NOTE: RFC 1341 says we can use space in a boundary as long as it isn't a trailing space, but for simplicity
    89      // of implementation we avoid this.
    90      const bchars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-./:=?"
    91  
    92      // This is a purely arbitrary limit but if the prefix is too long then we run the risk of collisions between the
    93      // boundary and a message.
    94      if (len(prefix) > 16) { return bound{}, fmt.Errorf("boundary prefix too long") }
    95  
    96      var randlen = maxlen - len(prefix)
    97      xs := make([]byte, randlen)
    98  
    99      _, err := rand.Read(xs)
   100      if err != nil { return bound{}, fmt.Errorf("boundary random source read error: %v", err) }
   101  
   102      // for bytes in range [0-255] cast down to bcharsnospace
   103      for i, x := range xs {
   104          xs[i] = bchars[int(x) % len(bchars)]
   105      }
   106  
   107      return bound{prefix + string(xs)}, nil
   108  }
   109  
   110  // Write writes a multipart Email to dest.
   111  func (e *Message) Write(dest io.Writer) error {
   112      return e.write(dest, false)
   113  }
   114  
   115  // WriteCompat is like Write, however very long headers (caused, for example,
   116  // by sending a message with many To addresses, or a subject that is too long)
   117  // are not encoded using folding white space and instead cause an error.
   118  func (e *Message) WriteCompat(dest io.Writer) error {
   119      return e.write(dest, true)
   120  }
   121  
   122  func (e *Message) write(dest io.Writer, compat bool) (ret error) {
   123      defer func() {
   124          if r := recover(); r != nil {
   125              if err, ok := r.(error); ok {
   126                  ret = err
   127              } else {
   128                  ret = fmt.Errorf("panic: %v", r)
   129              }
   130          }
   131      }()
   132  
   133      // folding white space support
   134      var fws func(value string, keylen int) string
   135      if compat {
   136          fws = func(value string, keylen int) string {
   137              result, err := fwsNone(value, keylen, 998)
   138              if err != nil { panic(err) }
   139              return result
   140          }
   141      } else {
   142          fws = func(value string, keylen int) string {
   143              result, err := fwsWrap(value, keylen, 998)
   144              if err != nil { panic(err) }
   145              return result
   146          }
   147      }
   148  
   149      // "RFC 5332 date format with (comment)" for time.Format
   150      const RFC5332C = "Mon, 02 Jan 2006 15:04:05 -0700 (MST)"
   151  
   152      var qp *quotedprintable.Writer
   153      var bufferedDest = bufio.NewWriter(dest)
   154      var dw = textproto.NewWriter(bufferedDest).DotWriter()
   155      defer dw.Close()
   156  
   157      // format a list of mail.Address objects as a comma-separated string
   158      var addresses = func (xs []mail.Address) string {
   159          var s = make([]string, 0, len(xs))
   160  
   161          for _, x := range xs {
   162              s = append(s, x.String())
   163          }
   164  
   165          return strings.Join(s, ",")
   166      }
   167  
   168      var coreHeaders = []struct{left string; right string} {
   169          {"From",         fws(e.From.String(), len("From"))},
   170          {"To",           fws(addresses(e.To), len("To"))},
   171          {"Cc",           fws(addresses(e.Cc), len("Cc"))},
   172          {"Bcc",          fws(addresses(e.Bcc), len("Bcc"))},
   173          {"Date",         time.Now().Format(RFC5332C)},
   174          {"Subject",      fws(optionalQEncode(e.Subject), len("Subject"))},
   175          {"MIME-Version", "1.0"},
   176      }
   177  
   178      if e.ID != "" {
   179          coreHeaders = append(coreHeaders,
   180              struct{left string; right string}{"Message-ID", "<"+e.ID+">"})
   181      }
   182  
   183      for _, v := range coreHeaders {
   184          fmt.Fprintf(dw, "%s: %s\r\n", textproto.CanonicalMIMEHeaderKey(v.left), v.right)
   185      }
   186  
   187      for k, vs := range e.Headers {
   188          for _, v := range vs {
   189              ck := textproto.CanonicalMIMEHeaderKey(k)
   190              fwsValue, err := fwsNone(v, len(ck), 998)
   191              if err != nil { return err }
   192              fmt.Fprintf(dw, "%s: %s\r\n", ck, fwsValue)
   193          }
   194      }
   195  
   196      bndMxd, err := boundary("MXD-")
   197      if err != nil { return err }
   198      bndMxd.start(dw, "multipart/mixed")
   199  
   200      bndRel, err := boundary("REL-")
   201      if err != nil { return err }
   202      bndRel.start(dw, "multipart/related")
   203  
   204      bndAlt, err := boundary("ALT-")
   205      if err != nil { return err }
   206      bndAlt.start(dw, "multipart/alternative")
   207  
   208      fmt.Fprintf(dw, "Content-Type: text/plain; charset=utf-8\r\n")
   209      fmt.Fprintf(dw, "Content-Transfer-Encoding: quoted-printable\r\n")
   210      fmt.Fprintf(dw, "\r\n")
   211      qp = quotedprintable.NewWriter(dw)
   212      io.WriteString(qp, strings.TrimSpace(e.Text))
   213      qp.Close()
   214      fmt.Fprintf(dw, "\r\n")
   215  
   216      bndAlt.next(dw)
   217  
   218      fmt.Fprintf(dw, "Content-Type: text/html; charset=utf-8\r\n")
   219      fmt.Fprintf(dw, "Content-Transfer-Encoding: quoted-printable\r\n")
   220      fmt.Fprintf(dw, "\r\n")
   221      qp = quotedprintable.NewWriter(dw)
   222      io.WriteString(qp, e.Html)
   223      qp.Close()
   224      fmt.Fprintf(dw, "\r\n")
   225  
   226      bndAlt.end(dw)
   227      bndRel.end(dw)
   228  
   229      for _, attachment := range e.Attachments {
   230          bndMxd.next(dw)
   231          err = attachment.write(dw)
   232          if err != nil {
   233              return fmt.Errorf("error writing attachment %q: %v", attachment.Filename, err)
   234          }
   235      }
   236  
   237      bndMxd.end(dw)
   238  
   239      return nil
   240  }
   241  

View as plain text