github.com/la5nta/wl2k-go@v0.11.8/fbb/message.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  	"bufio"
     9  	"bytes"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"mime"
    15  	"net/textproto"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  )
    20  
    21  // ValidationError is the error type returned by functions validating a message.
    22  type ValidationError struct {
    23  	Field string // The field/part of the message that is not valid
    24  	Err   string // Description of the error
    25  }
    26  
    27  func (e ValidationError) Error() string { return e.Err }
    28  
    29  // Representation of a receiver/sender address.
    30  type Address struct {
    31  	Proto string
    32  	Addr  string
    33  }
    34  
    35  // File represents an attachment.
    36  type File struct {
    37  	data []byte
    38  	name string
    39  	err  error
    40  }
    41  
    42  // Message represent the Winlink 2000 Message Structure as defined in http://winlink.org/B2F.
    43  type Message struct {
    44  	// The header names are case-insensitive.
    45  	//
    46  	// Users should normally access common header fields
    47  	// using the appropriate Message methods.
    48  	Header Header
    49  
    50  	body  []byte
    51  	files []*File
    52  }
    53  
    54  type MsgType string
    55  
    56  const (
    57  	Private        MsgType = "Private"
    58  	Service                = "Service"
    59  	Inquiry                = "Inquiry"
    60  	PositionReport         = "Position Report"
    61  	Option                 = "Option"
    62  	System                 = "System"
    63  )
    64  
    65  // Slice of date layouts that should be tried when parsing the Date header.
    66  var dateLayouts = []string{
    67  	DateLayout,         // The correct layout according to Winlink (2006/01/02 15:04).
    68  	`2006.01.02 15:04`, // Undocumented layout seen when RMS Relay-3.0.27.1 was operating in store-and-forward mode.
    69  	`2006-01-02 15:04`, // Undocumented layout seen in a Radio Only message forwarded with RMS Relay-3.0.30.0.
    70  	`20060102150405`,   // Older BPQ format
    71  }
    72  
    73  // From golang.org/src/net/mail/message.go
    74  func init() {
    75  	// Generate layouts based on RFC 5322, section 3.3.
    76  
    77  	dows := [...]string{"", "Mon, "}   // day-of-week
    78  	days := [...]string{"2", "02"}     // day = 1*2DIGIT
    79  	years := [...]string{"2006", "06"} // year = 4*DIGIT / 2*DIGIT
    80  	seconds := [...]string{":05", ""}  // second
    81  	// "-0700 (MST)" is not in RFC 5322, but is common.
    82  	zones := [...]string{"-0700", "MST", "-0700 (MST)"} // zone = (("+" / "-") 4DIGIT) / "GMT" / ...
    83  
    84  	for _, dow := range dows {
    85  		for _, day := range days {
    86  			for _, year := range years {
    87  				for _, second := range seconds {
    88  					for _, zone := range zones {
    89  						s := dow + day + " Jan " + year + " 15:04" + second + " " + zone
    90  						dateLayouts = append(dateLayouts, s)
    91  					}
    92  				}
    93  			}
    94  		}
    95  	}
    96  }
    97  
    98  // NewMessage initializes and returns a new message with Type, Mbo, From and Date set.
    99  //
   100  // If the message type t is empty, it defaults to Private.
   101  func NewMessage(t MsgType, mycall string) *Message {
   102  	msg := &Message{
   103  		Header: make(Header),
   104  	}
   105  
   106  	msg.Header.Set(HEADER_MID, GenerateMid(mycall))
   107  
   108  	msg.SetDate(time.Now())
   109  	msg.SetFrom(mycall)
   110  	msg.Header.Set(HEADER_MBO, mycall)
   111  
   112  	if t == "" {
   113  		t = Private
   114  	}
   115  	msg.Header.Set(HEADER_TYPE, string(t))
   116  
   117  	return msg
   118  }
   119  
   120  // Validate returns an error if this message violates any Winlink Message Structure constraints
   121  func (m *Message) Validate() error {
   122  	switch {
   123  	case m.MID() == "":
   124  		return ValidationError{"MID", "Empty MID"}
   125  	case len(m.MID()) > 12:
   126  		return ValidationError{"MID", "MID too long"}
   127  	case len(m.Receivers()) == 0:
   128  		// This is not documented, but the CMS refuses to accept such messages (with good reason)
   129  		return ValidationError{"To/Cc", "No recipient"}
   130  	case m.Header.Get(HEADER_FROM) == "":
   131  		return ValidationError{"From", "Empty From field"}
   132  	case m.BodySize() == 0:
   133  		return ValidationError{"Body", "Empty body"}
   134  	case len(m.Header.Get(HEADER_SUBJECT)) == 0:
   135  		// This is not documented, but the CMS writes the proposal title if this is empty
   136  		// (which I guess is a compatibility hack on their end).
   137  		return ValidationError{HEADER_SUBJECT, "Empty subject"}
   138  	case len(m.Header.Get(HEADER_SUBJECT)) > 128:
   139  		return ValidationError{HEADER_SUBJECT, "Subject too long"}
   140  	}
   141  
   142  	// The CMS seems to accept this, but according to the winlink.org/B2F document it is not allowed:
   143  	//  "... and the file name (up to 50 characters) of the original file."
   144  	// WDT made an amendment to the B2F specification 2020-05-27: New limit is 255 characters.
   145  	for _, f := range m.Files() {
   146  		if len(f.Name()) > 255 {
   147  			return ValidationError{"Files", fmt.Sprintf("Attachment file name too long: %s", f.Name())}
   148  		}
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  // MID returns the unique identifier of this message across the winlink system.
   155  func (m *Message) MID() string { return m.Header.Get(HEADER_MID) }
   156  
   157  // SetSubject sets this message's subject field.
   158  //
   159  // The Winlink Message Format only allow ASCII characters. Words containing non-ASCII characters are Q-encoded with DefaultCharset (as defined by RFC 2047).
   160  func (m *Message) SetSubject(str string) {
   161  	encoded, _ := toCharset(DefaultCharset, str)
   162  	encoded = mime.QEncoding.Encode(DefaultCharset, encoded)
   163  
   164  	m.Header.Set(HEADER_SUBJECT, encoded)
   165  }
   166  
   167  // Subject returns this message's subject header decoded using WordDecoder.
   168  func (m *Message) Subject() string {
   169  	str, _ := new(WordDecoder).DecodeHeader(m.Header.Get(HEADER_SUBJECT))
   170  	return str
   171  }
   172  
   173  // Type returns the message type.
   174  //
   175  // See MsgType consts for details.
   176  func (m *Message) Type() MsgType { return MsgType(m.Header.Get(HEADER_TYPE)) }
   177  
   178  // Mbo returns the mailbox operator origin of this message.
   179  func (m *Message) Mbo() string { return m.Header.Get(HEADER_MBO) }
   180  
   181  // Body returns this message's body encoded as utf8.
   182  func (m *Message) Body() (string, error) { return BodyFromBytes(m.body, m.Charset()) }
   183  
   184  // Files returns the message attachments.
   185  func (m *Message) Files() []*File { return m.files }
   186  
   187  // SetFrom sets the From header field.
   188  //
   189  // SMTP: prefix is automatically added if needed, see AddressFromString.
   190  func (m *Message) SetFrom(addr string) { m.Header.Set(HEADER_FROM, AddressFromString(addr).String()) }
   191  
   192  // From returns the From header field as an Address.
   193  func (m *Message) From() Address { return AddressFromString(m.Header.Get(HEADER_FROM)) }
   194  
   195  // Set date sets the Date header field.
   196  //
   197  // The field is set in the format DateLayout, UTC.
   198  func (m *Message) SetDate(t time.Time) { m.Header.Set(HEADER_DATE, t.UTC().Format(DateLayout)) }
   199  
   200  // Date parses the Date header field according to the winlink format.
   201  //
   202  // Parse errors are omitted, but it's checked at serialization.
   203  func (m *Message) Date() time.Time {
   204  	date, _ := ParseDate(m.Header.Get(HEADER_DATE))
   205  	return date
   206  }
   207  
   208  // SetBodyWithCharset translates and sets the body according to given charset.
   209  //
   210  // Header field Content-Transfer-Encoding is set to DefaultTransferEncoding.
   211  // Header field Content-Type is set according to charset.
   212  // All lines are modified to ensure CRLF.
   213  //
   214  // Use SetBody to use default character encoding.
   215  func (m *Message) SetBodyWithCharset(charset, body string) error {
   216  	m.Header.Set(HEADER_CONTENT_TRANSFER_ENCODING, DefaultTransferEncoding)
   217  	m.Header.Set(HEADER_CONTENT_TYPE, mime.FormatMediaType(
   218  		"text/plain",
   219  		map[string]string{"charset": DefaultCharset},
   220  	))
   221  
   222  	bytes, err := StringToBody(body, DefaultCharset)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	m.body = bytes
   228  	m.Header.Set(HEADER_BODY, fmt.Sprintf("%d", len(bytes)))
   229  	return nil
   230  }
   231  
   232  // SetBody sets the given string as message body using DefaultCharset.
   233  //
   234  // See SetBodyWithCharset for more info.
   235  func (m *Message) SetBody(body string) error {
   236  	return m.SetBodyWithCharset(DefaultCharset, body)
   237  }
   238  
   239  // BodySize returns the expected size of the body (in bytes) as defined in the header.
   240  func (m *Message) BodySize() int { size, _ := strconv.Atoi(m.Header.Get(HEADER_BODY)); return size }
   241  
   242  // Charset returns the body character encoding as defined in the ContentType header field.
   243  //
   244  // If the header field is unset, DefaultCharset is returned.
   245  func (m *Message) Charset() string {
   246  	_, params, err := mime.ParseMediaType(m.Header.Get(HEADER_CONTENT_TYPE))
   247  	if err != nil {
   248  		return DefaultCharset
   249  	}
   250  
   251  	if v, ok := params["charset"]; ok {
   252  		return v
   253  	}
   254  	return DefaultCharset
   255  }
   256  
   257  // AddTo adds a new receiver for this message.
   258  //
   259  // It adds a new To header field per given address.
   260  // SMTP: prefix is automatically added if needed, see AddressFromString.
   261  func (m *Message) AddTo(addr ...string) {
   262  	for _, a := range addr {
   263  		m.Header.Add(HEADER_TO, AddressFromString(a).String())
   264  	}
   265  }
   266  
   267  // AddCc adds a new carbon copy receiver to this message.
   268  //
   269  // It adds a new Cc header field per given address.
   270  // SMTP: prefix is automatically added if needed, see AddressFromString.
   271  func (m *Message) AddCc(addr ...string) {
   272  	for _, a := range addr {
   273  		m.Header.Add(HEADER_CC, AddressFromString(a).String())
   274  	}
   275  }
   276  
   277  // To returns primary receivers of this message.
   278  func (m *Message) To() (to []Address) {
   279  	for _, str := range m.Header[HEADER_TO] {
   280  		to = append(to, AddressFromString(str))
   281  	}
   282  	return
   283  }
   284  
   285  // Cc returns the carbon copy receivers of this message.
   286  func (m *Message) Cc() (cc []Address) {
   287  	for _, str := range m.Header[HEADER_CC] {
   288  		cc = append(cc, AddressFromString(str))
   289  	}
   290  	return
   291  }
   292  
   293  // copied from from stdlib's bytes/bytes.go
   294  var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
   295  
   296  // trimLeft advances the reader until the first byte not
   297  func trimLeftSpace(r *bufio.Reader) {
   298  	for {
   299  		b, err := r.Peek(1)
   300  		if err != nil || asciiSpace[b[0]] == 0 {
   301  			break
   302  		}
   303  		r.Discard(len(b))
   304  	}
   305  }
   306  
   307  // Implements ReaderFrom for Message.
   308  //
   309  // Reads the given io.Reader and fills in values fetched from the stream.
   310  func (m *Message) ReadFrom(r io.Reader) error {
   311  	reader := bufio.NewReader(r)
   312  
   313  	// Trim leading whitespace before reading the header:
   314  	// Got a mysterious bug that traced back to the possibility of a
   315  	// received message with leading CRLFs. Trimming space characters
   316  	// before reading the header should be safe, as the worst case scenario
   317  	// is that we fail to parse the header as opposed to definitely
   318  	// failing.
   319  	trimLeftSpace(reader)
   320  
   321  	if h, err := textproto.NewReader(reader).ReadMIMEHeader(); err != nil {
   322  		return err
   323  	} else {
   324  		m.Header = Header(h)
   325  	}
   326  
   327  	// Read body
   328  	var err error
   329  	m.body, err = readSection(reader, m.BodySize())
   330  	if err != nil {
   331  		return err
   332  	}
   333  
   334  	// Read files
   335  	m.files = make([]*File, len(m.Header[HEADER_FILE]))
   336  	dec := new(WordDecoder)
   337  	for i, value := range m.Header[HEADER_FILE] {
   338  		file := new(File)
   339  		m.files[i] = file
   340  
   341  		slice := strings.SplitN(value, ` `, 2)
   342  		if len(slice) != 2 {
   343  			file.err = errors.New(`Failed to parse file header. Got: ` + value)
   344  			continue
   345  		}
   346  
   347  		size, _ := strconv.Atoi(slice[0])
   348  
   349  		// The name part of this header may be utf8 encoded by Winlink Express. Use WordDecoder to be safe.
   350  		file.name, _ = dec.DecodeHeader(slice[1])
   351  
   352  		file.data, err = readSection(reader, size)
   353  		if err != nil {
   354  			file.err = err
   355  		}
   356  	}
   357  
   358  	// Return error if date field is not parseable
   359  	if err == nil {
   360  		_, err = ParseDate(m.Header.Get(HEADER_DATE))
   361  	}
   362  
   363  	return err
   364  }
   365  
   366  func readSection(reader *bufio.Reader, readN int) ([]byte, error) {
   367  	buf := make([]byte, readN)
   368  
   369  	var err error
   370  	n := 0
   371  	for n < readN {
   372  		m, err := reader.Read(buf[n:])
   373  		if err != nil {
   374  			break
   375  		}
   376  		n += m
   377  	}
   378  
   379  	if err != nil {
   380  		return buf, err
   381  	}
   382  
   383  	end, err := reader.ReadString('\n')
   384  	switch {
   385  	case n != readN:
   386  		return buf, io.ErrUnexpectedEOF
   387  	case err == io.EOF:
   388  		// That's ok
   389  	case err != nil:
   390  		return buf, err
   391  	case end != "\r\n":
   392  		return buf, errors.New("Unexpected end of section")
   393  	}
   394  	return buf, nil
   395  }
   396  
   397  // Returns true if the given Address is the only receiver of this Message.
   398  func (m *Message) IsOnlyReceiver(addr Address) bool {
   399  	receivers := m.Receivers()
   400  	if len(receivers) != 1 {
   401  		return false
   402  	}
   403  	return strings.EqualFold(receivers[0].String(), addr.String())
   404  }
   405  
   406  // Method for generating a proposal of the message.
   407  //
   408  // An error is returned if the Validate method fails.
   409  func (m *Message) Proposal(code PropCode) (*Proposal, error) {
   410  	data, err := m.Bytes()
   411  	if err != nil {
   412  		return nil, err
   413  	}
   414  
   415  	return NewProposal(m.MID(), m.Subject(), code, data), m.Validate()
   416  }
   417  
   418  // Receivers returns a slice of all receivers of this message.
   419  func (m *Message) Receivers() []Address {
   420  	to, cc := m.To(), m.Cc()
   421  	addrs := make([]Address, 0, len(to)+len(cc))
   422  	if len(to) > 0 {
   423  		addrs = append(addrs, to...)
   424  	}
   425  	if len(cc) > 0 {
   426  		addrs = append(addrs, cc...)
   427  	}
   428  	return addrs
   429  }
   430  
   431  // AddFile adds the given File as an attachment to m.
   432  func (m *Message) AddFile(f *File) {
   433  	m.files = append(m.files, f)
   434  
   435  	// According to spec, only ASCII is allowed.
   436  	encodedName, _ := toCharset(DefaultCharset, f.Name())
   437  	encodedName = mime.QEncoding.Encode(DefaultCharset, encodedName)
   438  
   439  	// Add header
   440  	m.Header.Add(HEADER_FILE, fmt.Sprintf("%d %s", f.Size(), encodedName))
   441  }
   442  
   443  // Bytes returns the message in the Winlink Message format.
   444  func (m *Message) Bytes() ([]byte, error) {
   445  	var buf bytes.Buffer
   446  	if err := m.Write(&buf); err != nil {
   447  		return nil, err
   448  	}
   449  	return buf.Bytes(), nil
   450  }
   451  
   452  // Writes Message to the given Writer in the Winlink Message format.
   453  //
   454  // If the Date header field is not formatted correctly, an error will be returned.
   455  func (m *Message) Write(w io.Writer) (err error) {
   456  	// Ensure Date field is in correct format
   457  	if _, err = ParseDate(m.Header.Get(HEADER_DATE)); err != nil {
   458  		return
   459  	}
   460  
   461  	// We use a bufio.Writer to defer error handling until Flush
   462  	writer := bufio.NewWriter(w)
   463  
   464  	// Header
   465  	m.Header.Write(writer)
   466  	writer.WriteString("\r\n") // end of headers
   467  
   468  	// Body
   469  	writer.Write(m.body)
   470  	if len(m.Files()) > 0 {
   471  		writer.WriteString("\r\n") // end of body
   472  	}
   473  
   474  	// Files (the order must be the same as they appear in the header)
   475  	for _, f := range m.Files() {
   476  		writer.Write(f.data)
   477  		writer.WriteString("\r\n") // end of file
   478  	}
   479  
   480  	return writer.Flush()
   481  }
   482  
   483  // Message stringer.
   484  func (m *Message) String() string {
   485  	buf := bytes.NewBufferString(``)
   486  	w := bufio.NewWriter(buf)
   487  
   488  	fmt.Fprintln(w, "MID: ", m.MID())
   489  	fmt.Fprintln(w, `Date:`, m.Date())
   490  	fmt.Fprintln(w, `From:`, m.From())
   491  	for _, to := range m.To() {
   492  		fmt.Fprintln(w, `To:`, to)
   493  	}
   494  	for _, cc := range m.Cc() {
   495  		fmt.Fprintln(w, `Cc:`, cc)
   496  	}
   497  	fmt.Fprintln(w, `Subject:`, m.Subject())
   498  
   499  	body, _ := m.Body()
   500  	fmt.Fprintf(w, "\n%s\n", body)
   501  
   502  	fmt.Fprintln(w, "Attachments:")
   503  	for _, f := range m.Files() {
   504  		fmt.Fprintf(w, "\t%s [%d bytes]\n", f.Name(), f.Size())
   505  	}
   506  
   507  	w.Flush()
   508  	return string(buf.Bytes())
   509  }
   510  
   511  // JSON marshaller for File.
   512  func (f *File) MarshalJSON() ([]byte, error) {
   513  	b, err := json.Marshal(struct {
   514  		Name string
   515  		Size int
   516  	}{f.Name(), f.Size()})
   517  	return b, err
   518  }
   519  
   520  // Name returns the attachment's filename.
   521  func (f *File) Name() string { return f.name }
   522  
   523  // Size returns the attachments's size in bytes.
   524  func (f *File) Size() int { return len(f.data) }
   525  
   526  // Data returns a copy of the attachment content.
   527  func (f *File) Data() []byte {
   528  	cpy := make([]byte, len(f.data))
   529  	copy(cpy, f.data)
   530  	return cpy
   531  }
   532  
   533  // Create a new file (attachment) with the given name and data.
   534  //
   535  // A B2F file must have an associated name. If the name is empty, NewFile will panic.
   536  func NewFile(name string, data []byte) *File {
   537  	if name == "" {
   538  		panic("Empty filename is not allowed")
   539  	}
   540  	return &File{
   541  		data: data,
   542  		name: name,
   543  	}
   544  }
   545  
   546  // Textual representation of Address.
   547  func (a Address) String() string {
   548  	if a.Proto == "" {
   549  		return a.Addr
   550  	} else {
   551  		return fmt.Sprintf("%s:%s", a.Proto, a.Addr)
   552  	}
   553  }
   554  
   555  // IsZero reports whether the Address is unset.
   556  func (a Address) IsZero() bool { return len(a.Addr) == 0 }
   557  
   558  // EqualString reports whether the given address string is equal to this address.
   559  func (a Address) EqualString(b string) bool { return a == AddressFromString(b) }
   560  
   561  // Function that constructs a proper Address from a string.
   562  //
   563  // Supported formats: foo@bar.baz (SMTP proto), N0CALL (short winlink address) or N0CALL@winlink.org (full winlink address).
   564  func AddressFromString(addr string) Address {
   565  	var a Address
   566  
   567  	if parts := strings.Split(addr, ":"); len(parts) == 2 {
   568  		a = Address{Proto: parts[0], Addr: parts[1]}
   569  	} else if parts := strings.Split(addr, "@"); len(parts) == 1 {
   570  		a = Address{Addr: addr}
   571  	} else if strings.EqualFold(parts[1], "winlink.org") {
   572  		a = Address{Addr: parts[0]}
   573  	} else {
   574  		a = Address{Proto: "SMTP", Addr: addr}
   575  	}
   576  
   577  	if a.Proto == "" {
   578  		a.Addr = strings.ToUpper(a.Addr)
   579  	}
   580  
   581  	return a
   582  }
   583  
   584  func ParseDate(dateStr string) (time.Time, error) {
   585  	if dateStr == "" {
   586  		return time.Time{}, nil
   587  	}
   588  
   589  	var date time.Time
   590  	var err error
   591  	for _, layout := range dateLayouts {
   592  		date, err = time.Parse(layout, dateStr)
   593  		if err == nil {
   594  			break
   595  		}
   596  	}
   597  
   598  	return date.Local(), err
   599  }