...
Tawesoft Logo

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

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

     1  package email
     2  
     3  import (
     4      "crypto/rand"
     5      "fmt"
     6      "io"
     7      "mime"
     8      "strings"
     9      "time"
    10  )
    11  
    12  // NewMessageID generates a cryptographically unique RFC 2822 3.6.4 Message ID
    13  // (not including angle brackets).
    14  func NewMessageID(host string) string {
    15      var rnd = make([]byte, 16)
    16      rand.Read(rnd)
    17      var tnow = time.Now().UTC().Format("20060102150405")
    18      return fmt.Sprintf("%s.%x@%s", tnow, rnd, host)
    19  }
    20  
    21  // strall returns true iff f(x) is true for each rune in string xs
    22  func strall(
    23      xs string,
    24      f func(c rune) bool,
    25  ) bool {
    26      for _, x := range xs {
    27          // if c > unicode.MaxASCII {
    28          if !f(x) { return false }
    29      }
    30  
    31      return true
    32  }
    33  
    34  // optionalQEncode returns the original string if it is printable ASCII, or a UTF-8 encoded version otherwise.
    35  func optionalQEncode(x string) string {
    36      var onlyPrintableAscii = func(c rune) bool {
    37          return (c >= 0x20) && (c <= 0x7e)
    38      }
    39  
    40      if strall(x, onlyPrintableAscii) {
    41          return x
    42      } else {
    43          return mime.QEncoding.Encode("utf-8", x)
    44      }
    45  }
    46  
    47  // lineBreaker wraps a writer forcing a line break every 76 characters (RFC 2045)
    48  //
    49  // This is used e.g. to break up base64 encoded strings and is not suitable
    50  // for breaking up text across word boundaries.
    51  type lineBreaker struct {
    52      column int
    53      writer io.Writer
    54  }
    55  
    56  // Write writes p to the wrapped writer, forcing a line break every 76 characters (RFC 2045) across
    57  // multiple calls to Write.
    58  //
    59  // This is used e.g. to break up base64 encoded strings and is not suitable
    60  // for breaking up text across word boundaries.
    61  func (lb lineBreaker) Write(p []byte) (n int, err error) {
    62  
    63      const LIMIT = 76 // RFC 2045
    64      var offset int
    65      var written int
    66      lb.column += len(p)
    67  
    68      for offset = 0; lb.column >= 76; lb.column -= LIMIT {
    69          n, err := lb.writer.Write(p[offset:offset + LIMIT])
    70          written += n
    71          if err != nil { return written, err }
    72  
    73          offset += LIMIT
    74  
    75          if offset != len(p) {
    76              n, err := io.WriteString(lb.writer, "\r\n")
    77              written += n
    78              if err != nil { return written, err }
    79          }
    80      }
    81  
    82      if offset != len(p) {
    83          n, err := lb.writer.Write(p[offset:])
    84          written += n
    85          if err != nil { return written, err }
    86      }
    87  
    88      return written, nil
    89  }
    90  
    91  // folding whitespace for a key: value header
    92  var fwsWrapErr = fmt.Errorf("header value component too long")
    93  var fwsNoneErr = fmt.Errorf("header value too long")
    94  
    95  func fwsWrap(line string, keyLen int, maxLine int) (string, error) {
    96      //fmt.Printf("\n\n=== wrap (%q, %d, %d) ===\n", line, keyLen, maxLine)
    97  
    98      // thorny but tested code. Complicated by the fact that the first line has
    99      // a header "Key: " prefix, and subsequent lines have a single space indent.
   100  
   101      keyLen += 2 // ": "
   102      if len(line) + keyLen <= maxLine { return line, nil }
   103  
   104      result := make([]string, 0)
   105  
   106      offset := 0
   107      start, idx := 0, 0
   108      remainingBudget := maxLine - keyLen // special case for first line
   109      remainingBudget++                   // trailing space that we break on
   110      //lineBudget := remainingBudget
   111      lineBudgetIncludingIndent := remainingBudget // normal for first line
   112  
   113      for {
   114          // look at next space.
   115          // we could support break on "<" and ">" but it won't get us that much
   116          idx = strings.IndexByte(line[offset:], ' ')
   117          //fmt.Printf("consider %q... (space at %d)\n", line[offset:], idx)
   118  
   119          if (idx >= 0) && (idx <= remainingBudget) {
   120              //fmt.Printf("in budget, continue... (budget %d => %d)\n", remainingBudget, remainingBudget- (idx + 1))
   121              remainingBudget -= idx + 1
   122              offset += idx + 1
   123              continue
   124          } else if (idx < 0) && (len(line[start:]) <= lineBudgetIncludingIndent) {
   125              //fmt.Printf("finished (no further spaces, but line of length %d in budget %d): %q\n", len(line[start:]), lineBudget, line[start:])
   126              segment := line[start:]
   127              if len(segment) > lineBudgetIncludingIndent { return "", fwsWrapErr }
   128              result = append(result, segment)
   129              break
   130          } else if (idx < 0) {
   131              //fmt.Printf("finished (no further spaces, but line %q of length %d not in budget %d)\n", line[start:], len(line[start:]), lineBudget)
   132  
   133              if offset < 1 {
   134                  // first element too long to wrap
   135                  return "", fwsWrapErr
   136              }
   137  
   138              a := line[start:offset-1]
   139              b := line[offset:]
   140              if len(a) > lineBudgetIncludingIndent { return "", fwsWrapErr }
   141              if len(b) > maxLine - 1 { return "", fwsWrapErr }
   142              result = append(result, line[start:offset-1], line[offset:])
   143              break
   144          } else {
   145              if offset < 1 {
   146                  // first element too long to wrap
   147                  return "", fwsWrapErr
   148              }
   149  
   150              //fmt.Printf("not finished, but out of budget %d...\n", remainingBudget)
   151              segment :=  line[start:offset-1]
   152              if len(segment) > lineBudgetIncludingIndent { return "", fwsWrapErr }
   153              result = append(result, segment)
   154              remainingBudget = maxLine - 1 + 1 // -1 for leading whitespace, +1 for trailing space that we break on
   155              //lineBudget = remainingBudget
   156              lineBudgetIncludingIndent = remainingBudget
   157              start = offset
   158              continue
   159          }
   160      }
   161  
   162      //fmt.Printf("\n\n\n")
   163      return strings.Join(result, "\r\n "), nil
   164  }
   165  
   166  func fwsNone(line string, keyLen int, maxLine int) (string, error) {
   167      keyLen += 2 // ": "
   168      if len(line) + keyLen <= maxLine { return line, nil }
   169      return "", fwsNoneErr
   170  }
   171  

View as plain text