...
Tawesoft Logo

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

Documentation: src/tawesoft.co.uk/go/legacy/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  // Messages 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      From mail.Address
    21      To   []mail.Address
    22      Cc   []mail.Address
    23      Bcc  []mail.Address
    24      Subject string
    25      Headers mail.Header
    26      Html string
    27      Text string
    28      Attachments []*Attachment
    29  }
    30  
    31  // Envelope wraps an Email with some SMTP protocol information for extra control.
    32  type Envelope struct {
    33      // From is the sender. Usually this should match the Email From address. In the cause of autoreplies (like "Out of
    34      // Office" or bounces or delivery status notifications) this should be an empty string to stop an infinite loop
    35      // of bounces)
    36      From string
    37      
    38      // Data is just a pointer to an Email struct
    39      Data *Message
    40      
    41      // ReceiptTo is normally automatically generated from the Email To/CC/BCC addresses
    42      ReceiptTo []string
    43  }
    44  
    45  // bound defines an email message boundary
    46  type bound struct {
    47      label string
    48  }
    49  
    50  func (b bound) start(w io.Writer, contentType string) {
    51      fmt.Fprintf(w, "Content-Type: %s; boundary=\"%s\"\r\n", contentType, b.label)
    52      fmt.Fprintf(w, "\r\n")
    53      fmt.Fprintf(w, "--%s\r\n", b.label)
    54  }
    55  
    56  func (b bound) next(w io.Writer) {
    57      fmt.Fprintf(w, "--%s\r\n", b.label)
    58  }
    59  
    60  func (b bound) end(w io.Writer) {
    61      fmt.Fprintf(w, "--%s--\r\n", b.label)
    62  }
    63  
    64  // boundary returns a randomly-generated RFC 1341 boundary with a label of exactly 70 characters with a given prefix.
    65  // The randomly generated prefix is cryptographically secure iff `rand` is `crypto/rand`.
    66  func boundary(prefix string) (bound, error) {
    67      
    68      // RFC 1341 sets this maximum length for a boundary
    69      const maxlen int = 70
    70      
    71      // NOTE: RFC 1341 says we can use space in a boundary as long as it isn't a trailing space, but for simplicity
    72      // of implementation we avoid this.
    73      const bchars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-./:=?"
    74      
    75      // This is a purely arbitrary limit but if the prefix is too long then we run the risk of collisions between the
    76      // boundary and a message.
    77      if (len(prefix) > 16) { return bound{}, fmt.Errorf("boundary prefix too long") }
    78      
    79      var randlen = maxlen - len(prefix)
    80      xs := make([]byte, randlen)
    81      
    82      _, err := rand.Read(xs)
    83      if err != nil { return bound{}, fmt.Errorf("boundary random source read error: %v", err) }
    84      
    85      // for bytes in range [0-255] cast down to bcharsnospace
    86      for i, x := range xs {
    87          xs[i] = bchars[int(x) % len(bchars)]
    88      }
    89      
    90      return bound{prefix + string(xs)}, nil
    91  }
    92  
    93  // Print writes a multipart Email to dest.
    94  //
    95  // NOTE: the maximum length of a email message line is 998 characters. If sending emails to multiple addresses
    96  // the caller should keep this limit in mind and divide the addresses over multiple calls to this function.
    97  func (e *Message) Print(dest io.Writer) error {
    98  
    99      // RFC 5332 date format with (comment) for time.Format
   100      const RFC5332C = "Mon, 02 Jan 2006 15:04:05 -0700 (MST)"
   101      
   102      var err error
   103      var qp *quotedprintable.Writer
   104      var bufferedDest = bufio.NewWriter(dest)
   105      var dw = textproto.NewWriter(bufferedDest).DotWriter()
   106      defer dw.Close()
   107      
   108      // format a list of mail.Address objects as a comma-separated string
   109      var addresses = func (xs []mail.Address) string {
   110          var s = make([]string, 0, len(xs))
   111          
   112          for _, x := range xs {
   113              s = append(s, x.String())
   114          }
   115          
   116          return strings.Join(s, ",")
   117      }
   118      
   119      var coreHeaders = []struct{left string; right string} {
   120          {"From",         e.From.String()},
   121          {"To",           addresses(e.To)},
   122          {"Cc",           addresses(e.Cc)},
   123          {"Bcc",          addresses(e.Bcc)},
   124          {"Date",         time.Now().Format(RFC5332C)},
   125          {"Subject",      optionalQEncode(e.Subject)},
   126          {"MIME-Version", "1.0"},
   127          {"Message-ID",   msgid(e.From)},
   128      }
   129      
   130      for _, v := range coreHeaders {
   131          fmt.Fprintf(dw, "%s: %s\r\n", textproto.CanonicalMIMEHeaderKey(v.left), v.right)
   132      }
   133      
   134      for k, vs := range e.Headers {
   135          for _, v := range vs {
   136              fmt.Fprintf(dw, "%s: %s\r\n", textproto.CanonicalMIMEHeaderKey(k), v)
   137          }
   138      }
   139      
   140      bndMxd, err := boundary("MXD-")
   141      if err != nil { return err }
   142      bndMxd.start(dw, "multipart/mixed")
   143      
   144      bndRel, err := boundary("REL-")
   145      if err != nil { return err }
   146      bndRel.start(dw, "multipart/related")
   147      
   148      bndAlt, err := boundary("ALT-")
   149      if err != nil { return err }
   150      bndAlt.start(dw, "multipart/alternative")
   151      
   152      fmt.Fprintf(dw, "Content-Type: text/plain; charset=utf-8\r\n")
   153      fmt.Fprintf(dw, "Content-Transfer-Encoding: quoted-printable\r\n")
   154      fmt.Fprintf(dw, "\r\n")
   155      qp = quotedprintable.NewWriter(dw)
   156      io.WriteString(qp, strings.TrimSpace(e.Text))
   157      qp.Close()
   158      fmt.Fprintf(dw, "\r\n")
   159      
   160      bndAlt.next(dw)
   161      
   162      fmt.Fprintf(dw, "Content-Type: text/html; charset=utf-8\r\n")
   163      fmt.Fprintf(dw, "Content-Transfer-Encoding: quoted-printable\r\n")
   164      fmt.Fprintf(dw, "\r\n")
   165      qp = quotedprintable.NewWriter(dw)
   166      io.WriteString(qp, e.Html)
   167      qp.Close()
   168      fmt.Fprintf(dw, "\r\n")
   169      
   170      bndAlt.end(dw)
   171      bndRel.end(dw)
   172      
   173      for _, attachment := range e.Attachments {
   174          bndMxd.next(dw)
   175          err = attachment.write(dw)
   176          if err != nil {
   177              return fmt.Errorf("error writing attachment %s: %v", attachment.Filename, err)
   178          }
   179      }
   180      
   181      bndMxd.end(dw)
   182      
   183      return nil
   184  }
   185  

View as plain text