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  }