github.com/la5nta/wl2k-go@v0.11.8/fbb/header.go (about) 1 // Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved. 2 // Use of this source code is governed by the MIT-license that can be 3 // found in the LICENSE file. 4 5 package fbb 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "mime" 14 "net/textproto" 15 "sort" 16 "strings" 17 "unicode/utf8" 18 19 "github.com/paulrosania/go-charset/charset" 20 _ "github.com/paulrosania/go-charset/data" 21 ) 22 23 // This file contains code from from net/http/header.go 24 25 // Common Winlink 2000 Message headers 26 const ( 27 HEADER_MID = `Mid` 28 HEADER_TO = `To` 29 HEADER_DATE = `Date` 30 HEADER_TYPE = `Type` 31 HEADER_FROM = `From` 32 HEADER_CC = `Cc` 33 HEADER_SUBJECT = `Subject` 34 HEADER_MBO = `Mbo` 35 HEADER_BODY = `Body` 36 HEADER_FILE = `File` 37 38 // These headers are stripped by the winlink system, but let's 39 // include it anyway... just in case the winlink team one day 40 // starts taking encoding seriously. 41 HEADER_CONTENT_TYPE = `Content-Type` 42 HEADER_CONTENT_TRANSFER_ENCODING = `Content-Transfer-Encoding` 43 44 // The default body charset seems to be ISO-8859-1 45 // 46 // The Winlink Message Structure docs says that the body should 47 // be ASCII-only, but RMS Express seems to encode the body as 48 // ISO-8859-1. This is also the charset set (Content-Type header) 49 // when a message reaches an SMTP server. 50 DefaultCharset = "ISO-8859-1" 51 52 // Mails going out over SMTP from the Winlink system is sent 53 // with the header 'Content-Transfer-Encoding: 7bit', but 54 // let's be reasonable... we don't send ASCII-only body. 55 DefaultTransferEncoding = "8bit" 56 57 // The date (in UTC) format as described in the Winlink 58 // Message Structure docs (YYYY/MM/DD HH:MM). 59 DateLayout = `2006/01/02 15:04` 60 ) 61 62 // A Header represents the key-value pairs in a Winlink 2000 Message header. 63 type Header map[string][]string 64 65 // Add adds the key, value pair to the header. 66 // It appends to any existing values associated with key. 67 func (h Header) Add(key, value string) { 68 textproto.MIMEHeader(h).Add(key, value) 69 } 70 71 // Set sets the header entries associated with key to 72 // the single element value. It replaces any existing 73 // values associated with key. 74 func (h Header) Set(key, value string) { 75 textproto.MIMEHeader(h).Set(key, value) 76 } 77 78 // Get gets the first value associated with the given key. 79 // If there are no values associated with the key, Get returns "". 80 // To access multiple values of a key, access the map directly 81 // with CanonicalHeaderKey. 82 func (h Header) Get(key string) string { 83 return textproto.MIMEHeader(h).Get(key) 84 } 85 86 // get is like Get, but key must already be in CanonicalHeaderKey form. 87 func (h Header) get(key string) string { 88 if v := h[key]; len(v) > 0 { 89 return v[0] 90 } 91 return "" 92 } 93 94 // Del deletes the values associated with key. 95 func (h Header) Del(key string) { 96 textproto.MIMEHeader(h).Del(key) 97 } 98 99 // Write writes a header in wire format. 100 func (h Header) Write(w io.Writer) error { 101 var err error 102 103 // Mid is required 104 if h.get(HEADER_MID) == "" { 105 return errors.New("Missing MID in header") 106 } 107 108 // Write mid, this is defined to be the first value 109 _, err = fmt.Fprintf(w, "Mid: %s\r\n", h.get(HEADER_MID)) 110 if err != nil { 111 return err 112 } 113 114 // The rest should be printed in a stable order to ensure reproducibility 115 keys := make([]string, 0, len(h)) 116 for k, _ := range h { 117 if !strings.EqualFold(k, HEADER_MID) { 118 keys = append(keys, k) 119 } 120 } 121 sort.Sort(sort.StringSlice(keys)) 122 for _, key := range keys { 123 for _, v := range h[key] { 124 v = textproto.TrimString(v) 125 _, err = fmt.Fprintf(w, "%s: %s\r\n", key, v) 126 if err != nil { 127 return err 128 } 129 } 130 } 131 132 return nil 133 } 134 135 // WordDecoder decodes MIME headers containing RFC 2047 encoded-words. 136 // 137 // (See DecodeHeader for mime.WordDecoder differences). 138 type WordDecoder struct{ mime.WordDecoder } 139 140 // Decode decodes an encoded-word. 141 // 142 // If word is not a valid RFC 2047 encoded-word, word is decoded as raw ISO-8859-1 as a work-around for RMS Express' non-conforming encoding of the Subject header. 143 func (d *WordDecoder) DecodeHeader(header string) (string, error) { 144 i := strings.Index(header, "=?") 145 if i > -1 { 146 return d.WordDecoder.DecodeHeader(header) 147 } 148 149 // If there is no encoded-word, the data may be ISO-8859-1 or UTF-8 depending on how CMS decoded it. 150 // 151 // It turns out that if CMS receives a Q-encoded subject it decodes it and forwards it as UTF-8. 152 // If CMS on the other hand receives an SMTP email from gmail, it is enocded as ISO-8859-1. 153 if utf8.Valid([]byte(header)) { 154 return header, nil 155 } 156 157 r, err := charset.NewReader(DefaultCharset, bytes.NewReader([]byte(header))) 158 if err != nil { 159 return header, err 160 } 161 162 decoded, _ := ioutil.ReadAll(r) 163 return string(decoded), nil 164 } 165 166 func toCharset(set, s string) (string, error) { 167 buf := new(bytes.Buffer) 168 w, err := charset.NewWriter(set, buf) 169 if err != nil { 170 return s, err 171 } 172 173 fmt.Fprint(w, s) 174 w.Close() 175 return buf.String(), nil 176 }