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 }