├── .circleci └── config.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── go.mod ├── parsemail.go └── parsemail_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2 5 | jobs: 6 | build: 7 | docker: 8 | - image: circleci/golang:1.14 9 | working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}} 10 | steps: 11 | - checkout 12 | - run: go test -v ./... -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## No versions tagged yet -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | 3 | This project is open to contribution from anyone, as long as you cover your changes with tests. Your pull requests will be merged after your code passe CI and manual code review. 4 | 5 | Every change merges to master. No development is done in other branches. 6 | 7 | ## Typical contribution use case 8 | 9 | - You need a feature that is not implemented yet 10 | - Search for open/closed issues relating to what you need 11 | - If you don't find anything, create new issue 12 | - Fork this repository and create fix/feature in the fork 13 | - Write tests for your change 14 | - If you changed API, document the change in README 15 | - Create pull request, describe what you did 16 | - Wait for CI to verify you didn't break anything 17 | - If you did, rewrite it 18 | - If CI passes, wait for manual review by repo's owner 19 | - Your pull request will be merged into master -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Dusan Kasan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parsemail - simple email parsing Go library 2 | 3 | [](https://circleci.com/gh/DusanKasan/parsemail) [](https://coveralls.io/github/DusanKasan/Parsemail?branch=master) [](https://goreportcard.com/report/github.com/DusanKasan/parsemail) 4 | 5 | This library allows for parsing an email message into a more convenient form than the `net/mail` provides. Where the `net/mail` just gives you a map of header fields and a `io.Reader` of its body, Parsemail allows access to all the standard header fields set in [RFC5322](https://tools.ietf.org/html/rfc5322), html/text body as well as attachements/embedded content as binary streams with metadata. 6 | 7 | ## Simple usage 8 | 9 | You just parse a io.Reader that holds the email data. The returned Email struct contains all the standard email information/headers as public fields. 10 | 11 | ```go 12 | var reader io.Reader // this reads an email message 13 | email, err := parsemail.Parse(reader) // returns Email struct and error 14 | if err != nil { 15 | // handle error 16 | } 17 | 18 | fmt.Println(email.Subject) 19 | fmt.Println(email.From) 20 | fmt.Println(email.To) 21 | fmt.Println(email.HTMLBody) 22 | ``` 23 | 24 | ## Retrieving attachments 25 | 26 | Attachments are a easily accessible as `Attachment` type, containing their mime type, filename and data stream. 27 | 28 | ```go 29 | var reader io.Reader 30 | email, err := parsemail.Parse(reader) 31 | if err != nil { 32 | // handle error 33 | } 34 | 35 | for _, a := range(email.Attachments) { 36 | fmt.Println(a.Filename) 37 | fmt.Println(a.ContentType) 38 | //and read a.Data 39 | } 40 | ``` 41 | 42 | ## Retrieving embedded files 43 | 44 | You can access embedded files in the same way you can access attachments. They contain the mime type, data stream and content id that is used to reference them through the email. 45 | 46 | ```go 47 | var reader io.Reader 48 | email, err := parsemail.Parse(reader) 49 | if err != nil { 50 | // handle error 51 | } 52 | 53 | for _, a := range(email.EmbeddedFiles) { 54 | fmt.Println(a.CID) 55 | fmt.Println(a.ContentType) 56 | //and read a.Data 57 | } 58 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DusanKasan/parsemail 2 | 3 | go 1.12 -------------------------------------------------------------------------------- /parsemail.go: -------------------------------------------------------------------------------- 1 | package parsemail 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "mime" 10 | "mime/multipart" 11 | "net/mail" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const contentTypeMultipartMixed = "multipart/mixed" 17 | const contentTypeMultipartAlternative = "multipart/alternative" 18 | const contentTypeMultipartRelated = "multipart/related" 19 | const contentTypeTextHtml = "text/html" 20 | const contentTypeTextPlain = "text/plain" 21 | 22 | // Parse an email message read from io.Reader into parsemail.Email struct 23 | func Parse(r io.Reader) (email Email, err error) { 24 | msg, err := mail.ReadMessage(r) 25 | if err != nil { 26 | return 27 | } 28 | 29 | email, err = createEmailFromHeader(msg.Header) 30 | if err != nil { 31 | return 32 | } 33 | 34 | email.ContentType = msg.Header.Get("Content-Type") 35 | contentType, params, err := parseContentType(email.ContentType) 36 | if err != nil { 37 | return 38 | } 39 | 40 | switch contentType { 41 | case contentTypeMultipartMixed: 42 | email.TextBody, email.HTMLBody, email.Attachments, email.EmbeddedFiles, err = parseMultipartMixed(msg.Body, params["boundary"]) 43 | case contentTypeMultipartAlternative: 44 | email.TextBody, email.HTMLBody, email.EmbeddedFiles, err = parseMultipartAlternative(msg.Body, params["boundary"]) 45 | case contentTypeMultipartRelated: 46 | email.TextBody, email.HTMLBody, email.EmbeddedFiles, err = parseMultipartRelated(msg.Body, params["boundary"]) 47 | case contentTypeTextPlain: 48 | message, _ := ioutil.ReadAll(msg.Body) 49 | email.TextBody = strings.TrimSuffix(string(message[:]), "\n") 50 | case contentTypeTextHtml: 51 | message, _ := ioutil.ReadAll(msg.Body) 52 | email.HTMLBody = strings.TrimSuffix(string(message[:]), "\n") 53 | default: 54 | email.Content, err = decodeContent(msg.Body, msg.Header.Get("Content-Transfer-Encoding")) 55 | } 56 | 57 | return 58 | } 59 | 60 | func createEmailFromHeader(header mail.Header) (email Email, err error) { 61 | hp := headerParser{header: &header} 62 | 63 | email.Subject = decodeMimeSentence(header.Get("Subject")) 64 | email.From = hp.parseAddressList(header.Get("From")) 65 | email.Sender = hp.parseAddress(header.Get("Sender")) 66 | email.ReplyTo = hp.parseAddressList(header.Get("Reply-To")) 67 | email.To = hp.parseAddressList(header.Get("To")) 68 | email.Cc = hp.parseAddressList(header.Get("Cc")) 69 | email.Bcc = hp.parseAddressList(header.Get("Bcc")) 70 | email.Date = hp.parseTime(header.Get("Date")) 71 | email.ResentFrom = hp.parseAddressList(header.Get("Resent-From")) 72 | email.ResentSender = hp.parseAddress(header.Get("Resent-Sender")) 73 | email.ResentTo = hp.parseAddressList(header.Get("Resent-To")) 74 | email.ResentCc = hp.parseAddressList(header.Get("Resent-Cc")) 75 | email.ResentBcc = hp.parseAddressList(header.Get("Resent-Bcc")) 76 | email.ResentMessageID = hp.parseMessageId(header.Get("Resent-Message-ID")) 77 | email.MessageID = hp.parseMessageId(header.Get("Message-ID")) 78 | email.InReplyTo = hp.parseMessageIdList(header.Get("In-Reply-To")) 79 | email.References = hp.parseMessageIdList(header.Get("References")) 80 | email.ResentDate = hp.parseTime(header.Get("Resent-Date")) 81 | 82 | if hp.err != nil { 83 | err = hp.err 84 | return 85 | } 86 | 87 | //decode whole header for easier access to extra fields 88 | //todo: should we decode? aren't only standard fields mime encoded? 89 | email.Header, err = decodeHeaderMime(header) 90 | if err != nil { 91 | return 92 | } 93 | 94 | return 95 | } 96 | 97 | func parseContentType(contentTypeHeader string) (contentType string, params map[string]string, err error) { 98 | if contentTypeHeader == "" { 99 | contentType = contentTypeTextPlain 100 | return 101 | } 102 | 103 | return mime.ParseMediaType(contentTypeHeader) 104 | } 105 | 106 | func parseMultipartRelated(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) { 107 | pmr := multipart.NewReader(msg, boundary) 108 | for { 109 | part, err := pmr.NextPart() 110 | 111 | if err == io.EOF { 112 | break 113 | } else if err != nil { 114 | return textBody, htmlBody, embeddedFiles, err 115 | } 116 | 117 | contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type")) 118 | if err != nil { 119 | return textBody, htmlBody, embeddedFiles, err 120 | } 121 | 122 | switch contentType { 123 | case contentTypeTextPlain: 124 | ppContent, err := ioutil.ReadAll(part) 125 | if err != nil { 126 | return textBody, htmlBody, embeddedFiles, err 127 | } 128 | 129 | textBody += strings.TrimSuffix(string(ppContent[:]), "\n") 130 | case contentTypeTextHtml: 131 | ppContent, err := ioutil.ReadAll(part) 132 | if err != nil { 133 | return textBody, htmlBody, embeddedFiles, err 134 | } 135 | 136 | htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n") 137 | case contentTypeMultipartAlternative: 138 | tb, hb, ef, err := parseMultipartAlternative(part, params["boundary"]) 139 | if err != nil { 140 | return textBody, htmlBody, embeddedFiles, err 141 | } 142 | 143 | htmlBody += hb 144 | textBody += tb 145 | embeddedFiles = append(embeddedFiles, ef...) 146 | default: 147 | if isEmbeddedFile(part) { 148 | ef, err := decodeEmbeddedFile(part) 149 | if err != nil { 150 | return textBody, htmlBody, embeddedFiles, err 151 | } 152 | 153 | embeddedFiles = append(embeddedFiles, ef) 154 | } else { 155 | return textBody, htmlBody, embeddedFiles, fmt.Errorf("Can't process multipart/related inner mime type: %s", contentType) 156 | } 157 | } 158 | } 159 | 160 | return textBody, htmlBody, embeddedFiles, err 161 | } 162 | 163 | func parseMultipartAlternative(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) { 164 | pmr := multipart.NewReader(msg, boundary) 165 | for { 166 | part, err := pmr.NextPart() 167 | 168 | if err == io.EOF { 169 | break 170 | } else if err != nil { 171 | return textBody, htmlBody, embeddedFiles, err 172 | } 173 | 174 | contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type")) 175 | if err != nil { 176 | return textBody, htmlBody, embeddedFiles, err 177 | } 178 | 179 | switch contentType { 180 | case contentTypeTextPlain: 181 | ppContent, err := ioutil.ReadAll(part) 182 | if err != nil { 183 | return textBody, htmlBody, embeddedFiles, err 184 | } 185 | 186 | textBody += strings.TrimSuffix(string(ppContent[:]), "\n") 187 | case contentTypeTextHtml: 188 | ppContent, err := ioutil.ReadAll(part) 189 | if err != nil { 190 | return textBody, htmlBody, embeddedFiles, err 191 | } 192 | 193 | htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n") 194 | case contentTypeMultipartRelated: 195 | tb, hb, ef, err := parseMultipartRelated(part, params["boundary"]) 196 | if err != nil { 197 | return textBody, htmlBody, embeddedFiles, err 198 | } 199 | 200 | htmlBody += hb 201 | textBody += tb 202 | embeddedFiles = append(embeddedFiles, ef...) 203 | default: 204 | if isEmbeddedFile(part) { 205 | ef, err := decodeEmbeddedFile(part) 206 | if err != nil { 207 | return textBody, htmlBody, embeddedFiles, err 208 | } 209 | 210 | embeddedFiles = append(embeddedFiles, ef) 211 | } else { 212 | return textBody, htmlBody, embeddedFiles, fmt.Errorf("Can't process multipart/alternative inner mime type: %s", contentType) 213 | } 214 | } 215 | } 216 | 217 | return textBody, htmlBody, embeddedFiles, err 218 | } 219 | 220 | func parseMultipartMixed(msg io.Reader, boundary string) (textBody, htmlBody string, attachments []Attachment, embeddedFiles []EmbeddedFile, err error) { 221 | mr := multipart.NewReader(msg, boundary) 222 | for { 223 | part, err := mr.NextPart() 224 | if err == io.EOF { 225 | break 226 | } else if err != nil { 227 | return textBody, htmlBody, attachments, embeddedFiles, err 228 | } 229 | 230 | contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type")) 231 | if err != nil { 232 | return textBody, htmlBody, attachments, embeddedFiles, err 233 | } 234 | 235 | if contentType == contentTypeMultipartAlternative { 236 | textBody, htmlBody, embeddedFiles, err = parseMultipartAlternative(part, params["boundary"]) 237 | if err != nil { 238 | return textBody, htmlBody, attachments, embeddedFiles, err 239 | } 240 | } else if contentType == contentTypeMultipartRelated { 241 | textBody, htmlBody, embeddedFiles, err = parseMultipartRelated(part, params["boundary"]) 242 | if err != nil { 243 | return textBody, htmlBody, attachments, embeddedFiles, err 244 | } 245 | } else if contentType == contentTypeTextPlain { 246 | ppContent, err := ioutil.ReadAll(part) 247 | if err != nil { 248 | return textBody, htmlBody, attachments, embeddedFiles, err 249 | } 250 | 251 | textBody += strings.TrimSuffix(string(ppContent[:]), "\n") 252 | } else if contentType == contentTypeTextHtml { 253 | ppContent, err := ioutil.ReadAll(part) 254 | if err != nil { 255 | return textBody, htmlBody, attachments, embeddedFiles, err 256 | } 257 | 258 | htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n") 259 | } else if isAttachment(part) { 260 | at, err := decodeAttachment(part) 261 | if err != nil { 262 | return textBody, htmlBody, attachments, embeddedFiles, err 263 | } 264 | 265 | attachments = append(attachments, at) 266 | } else { 267 | return textBody, htmlBody, attachments, embeddedFiles, fmt.Errorf("Unknown multipart/mixed nested mime type: %s", contentType) 268 | } 269 | } 270 | 271 | return textBody, htmlBody, attachments, embeddedFiles, err 272 | } 273 | 274 | func decodeMimeSentence(s string) string { 275 | result := []string{} 276 | ss := strings.Split(s, " ") 277 | 278 | for _, word := range ss { 279 | dec := new(mime.WordDecoder) 280 | w, err := dec.Decode(word) 281 | if err != nil { 282 | if len(result) == 0 { 283 | w = word 284 | } else { 285 | w = " " + word 286 | } 287 | } 288 | 289 | result = append(result, w) 290 | } 291 | 292 | return strings.Join(result, "") 293 | } 294 | 295 | func decodeHeaderMime(header mail.Header) (mail.Header, error) { 296 | parsedHeader := map[string][]string{} 297 | 298 | for headerName, headerData := range header { 299 | 300 | parsedHeaderData := []string{} 301 | for _, headerValue := range headerData { 302 | parsedHeaderData = append(parsedHeaderData, decodeMimeSentence(headerValue)) 303 | } 304 | 305 | parsedHeader[headerName] = parsedHeaderData 306 | } 307 | 308 | return mail.Header(parsedHeader), nil 309 | } 310 | 311 | func isEmbeddedFile(part *multipart.Part) bool { 312 | return part.Header.Get("Content-Transfer-Encoding") != "" 313 | } 314 | 315 | func decodeEmbeddedFile(part *multipart.Part) (ef EmbeddedFile, err error) { 316 | cid := decodeMimeSentence(part.Header.Get("Content-Id")) 317 | decoded, err := decodeContent(part, part.Header.Get("Content-Transfer-Encoding")) 318 | if err != nil { 319 | return 320 | } 321 | 322 | ef.CID = strings.Trim(cid, "<>") 323 | ef.Data = decoded 324 | ef.ContentType = part.Header.Get("Content-Type") 325 | 326 | return 327 | } 328 | 329 | func isAttachment(part *multipart.Part) bool { 330 | return part.FileName() != "" 331 | } 332 | 333 | func decodeAttachment(part *multipart.Part) (at Attachment, err error) { 334 | filename := decodeMimeSentence(part.FileName()) 335 | decoded, err := decodeContent(part, part.Header.Get("Content-Transfer-Encoding")) 336 | if err != nil { 337 | return 338 | } 339 | 340 | at.Filename = filename 341 | at.Data = decoded 342 | at.ContentType = strings.Split(part.Header.Get("Content-Type"), ";")[0] 343 | 344 | return 345 | } 346 | 347 | func decodeContent(content io.Reader, encoding string) (io.Reader, error) { 348 | switch encoding { 349 | case "base64": 350 | decoded := base64.NewDecoder(base64.StdEncoding, content) 351 | b, err := ioutil.ReadAll(decoded) 352 | if err != nil { 353 | return nil, err 354 | } 355 | 356 | return bytes.NewReader(b), nil 357 | case "7bit": 358 | dd, err := ioutil.ReadAll(content) 359 | if err != nil { 360 | return nil, err 361 | } 362 | 363 | return bytes.NewReader(dd), nil 364 | case "": 365 | return content, nil 366 | default: 367 | return nil, fmt.Errorf("unknown encoding: %s", encoding) 368 | } 369 | } 370 | 371 | type headerParser struct { 372 | header *mail.Header 373 | err error 374 | } 375 | 376 | func (hp headerParser) parseAddress(s string) (ma *mail.Address) { 377 | if hp.err != nil { 378 | return nil 379 | } 380 | 381 | if strings.Trim(s, " \n") != "" { 382 | ma, hp.err = mail.ParseAddress(s) 383 | 384 | return ma 385 | } 386 | 387 | return nil 388 | } 389 | 390 | func (hp headerParser) parseAddressList(s string) (ma []*mail.Address) { 391 | if hp.err != nil { 392 | return 393 | } 394 | 395 | if strings.Trim(s, " \n") != "" { 396 | ma, hp.err = mail.ParseAddressList(s) 397 | return 398 | } 399 | 400 | return 401 | } 402 | 403 | func (hp headerParser) parseTime(s string) (t time.Time) { 404 | if hp.err != nil || s == "" { 405 | return 406 | } 407 | 408 | formats := []string{ 409 | time.RFC1123Z, 410 | "Mon, 2 Jan 2006 15:04:05 -0700", 411 | time.RFC1123Z + " (MST)", 412 | "Mon, 2 Jan 2006 15:04:05 -0700 (MST)", 413 | } 414 | 415 | for _, format := range formats { 416 | t, hp.err = time.Parse(format, s) 417 | if hp.err == nil { 418 | return 419 | } 420 | } 421 | 422 | return 423 | } 424 | 425 | func (hp headerParser) parseMessageId(s string) string { 426 | if hp.err != nil { 427 | return "" 428 | } 429 | 430 | return strings.Trim(s, "<> ") 431 | } 432 | 433 | func (hp headerParser) parseMessageIdList(s string) (result []string) { 434 | if hp.err != nil { 435 | return 436 | } 437 | 438 | for _, p := range strings.Split(s, " ") { 439 | if strings.Trim(p, " \n") != "" { 440 | result = append(result, hp.parseMessageId(p)) 441 | } 442 | } 443 | 444 | return 445 | } 446 | 447 | // Attachment with filename, content type and data (as a io.Reader) 448 | type Attachment struct { 449 | Filename string 450 | ContentType string 451 | Data io.Reader 452 | } 453 | 454 | // EmbeddedFile with content id, content type and data (as a io.Reader) 455 | type EmbeddedFile struct { 456 | CID string 457 | ContentType string 458 | Data io.Reader 459 | } 460 | 461 | // Email with fields for all the headers defined in RFC5322 with it's attachments and 462 | type Email struct { 463 | Header mail.Header 464 | 465 | Subject string 466 | Sender *mail.Address 467 | From []*mail.Address 468 | ReplyTo []*mail.Address 469 | To []*mail.Address 470 | Cc []*mail.Address 471 | Bcc []*mail.Address 472 | Date time.Time 473 | MessageID string 474 | InReplyTo []string 475 | References []string 476 | 477 | ResentFrom []*mail.Address 478 | ResentSender *mail.Address 479 | ResentTo []*mail.Address 480 | ResentDate time.Time 481 | ResentCc []*mail.Address 482 | ResentBcc []*mail.Address 483 | ResentMessageID string 484 | 485 | ContentType string 486 | Content io.Reader 487 | 488 | HTMLBody string 489 | TextBody string 490 | 491 | Attachments []Attachment 492 | EmbeddedFiles []EmbeddedFile 493 | } -------------------------------------------------------------------------------- /parsemail_test.go: -------------------------------------------------------------------------------- 1 | package parsemail 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io/ioutil" 7 | "net/mail" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestParseEmail(t *testing.T) { 14 | var testData = map[int]struct { 15 | mailData string 16 | 17 | contentType string 18 | content string 19 | subject string 20 | date time.Time 21 | from []mail.Address 22 | sender mail.Address 23 | to []mail.Address 24 | replyTo []mail.Address 25 | cc []mail.Address 26 | bcc []mail.Address 27 | messageID string 28 | resentDate time.Time 29 | resentFrom []mail.Address 30 | resentSender mail.Address 31 | resentTo []mail.Address 32 | resentReplyTo []mail.Address 33 | resentCc []mail.Address 34 | resentBcc []mail.Address 35 | resentMessageID string 36 | inReplyTo []string 37 | references []string 38 | htmlBody string 39 | textBody string 40 | attachments []attachmentData 41 | embeddedFiles []embeddedFileData 42 | headerCheck func(mail.Header, *testing.T) 43 | }{ 44 | 1: { 45 | mailData: rfc5322exampleA11, 46 | subject: "Saying Hello", 47 | from: []mail.Address{ 48 | { 49 | Name: "John Doe", 50 | Address: "jdoe@machine.example", 51 | }, 52 | }, 53 | to: []mail.Address{ 54 | { 55 | Name: "Mary Smith", 56 | Address: "mary@example.net", 57 | }, 58 | }, 59 | sender: mail.Address{ 60 | Name: "Michael Jones", 61 | Address: "mjones@machine.example", 62 | }, 63 | messageID: "1234@local.machine.example", 64 | date: parseDate("Fri, 21 Nov 1997 09:55:06 -0600"), 65 | textBody: `This is a message just to say hello. 66 | So, "Hello".`, 67 | }, 68 | 2: { 69 | mailData: rfc5322exampleA12, 70 | from: []mail.Address{ 71 | { 72 | Name: "Joe Q. Public", 73 | Address: "john.q.public@example.com", 74 | }, 75 | }, 76 | to: []mail.Address{ 77 | { 78 | Name: "Mary Smith", 79 | Address: "mary@x.test", 80 | }, 81 | { 82 | Name: "", 83 | Address: "jdoe@example.org", 84 | }, 85 | { 86 | Name: "Who?", 87 | Address: "one@y.test", 88 | }, 89 | }, 90 | cc: []mail.Address{ 91 | { 92 | Name: "", 93 | Address: "boss@nil.test", 94 | }, 95 | { 96 | Name: "Giant; \"Big\" Box", 97 | Address: "sysservices@example.net", 98 | }, 99 | }, 100 | messageID: "5678.21-Nov-1997@example.com", 101 | date: parseDate("Tue, 01 Jul 2003 10:52:37 +0200"), 102 | textBody: `Hi everyone.`, 103 | }, 104 | 3: { 105 | mailData: rfc5322exampleA2a, 106 | subject: "Re: Saying Hello", 107 | from: []mail.Address{ 108 | { 109 | Name: "Mary Smith", 110 | Address: "mary@example.net", 111 | }, 112 | }, 113 | replyTo: []mail.Address{ 114 | { 115 | Name: "Mary Smith: Personal Account", 116 | Address: "smith@home.example", 117 | }, 118 | }, 119 | to: []mail.Address{ 120 | { 121 | Name: "John Doe", 122 | Address: "jdoe@machine.example", 123 | }, 124 | }, 125 | messageID: "3456@example.net", 126 | inReplyTo: []string{"1234@local.machine.example"}, 127 | references: []string{"1234@local.machine.example"}, 128 | date: parseDate("Fri, 21 Nov 1997 10:01:10 -0600"), 129 | textBody: `This is a reply to your hello.`, 130 | }, 131 | 4: { 132 | mailData: rfc5322exampleA2b, 133 | subject: "Re: Saying Hello", 134 | from: []mail.Address{ 135 | { 136 | Name: "John Doe", 137 | Address: "jdoe@machine.example", 138 | }, 139 | }, 140 | to: []mail.Address{ 141 | { 142 | Name: "Mary Smith: Personal Account", 143 | Address: "smith@home.example", 144 | }, 145 | }, 146 | messageID: "abcd.1234@local.machine.test", 147 | inReplyTo: []string{"3456@example.net"}, 148 | references: []string{"1234@local.machine.example", "3456@example.net"}, 149 | date: parseDate("Fri, 21 Nov 1997 11:00:00 -0600"), 150 | textBody: `This is a reply to your reply.`, 151 | }, 152 | 5: { 153 | mailData: rfc5322exampleA3, 154 | subject: "Saying Hello", 155 | from: []mail.Address{ 156 | { 157 | Name: "John Doe", 158 | Address: "jdoe@machine.example", 159 | }, 160 | }, 161 | to: []mail.Address{ 162 | { 163 | Name: "Mary Smith", 164 | Address: "mary@example.net", 165 | }, 166 | }, 167 | messageID: "1234@local.machine.example", 168 | date: parseDate("Fri, 21 Nov 1997 09:55:06 -0600"), 169 | resentFrom: []mail.Address{ 170 | { 171 | Name: "Mary Smith", 172 | Address: "mary@example.net", 173 | }, 174 | }, 175 | resentTo: []mail.Address{ 176 | { 177 | Name: "Jane Brown", 178 | Address: "j-brown@other.example", 179 | }, 180 | }, 181 | resentMessageID: "78910@example.net", 182 | resentDate: parseDate("Mon, 24 Nov 1997 14:22:01 -0800"), 183 | textBody: `This is a message just to say hello. 184 | So, "Hello".`, 185 | }, 186 | 6: { 187 | mailData: data1, 188 | contentType: `multipart/mixed; boundary=f403045f1dcc043a44054c8e6bbf`, 189 | content: "", 190 | subject: "Peter Paholík", 191 | from: []mail.Address{ 192 | { 193 | Name: "Peter Paholík", 194 | Address: "peter.paholik@gmail.com", 195 | }, 196 | }, 197 | to: []mail.Address{ 198 | { 199 | Name: "", 200 | Address: "dusan@kasan.sk", 201 | }, 202 | }, 203 | messageID: "CACtgX4kNXE7T5XKSKeH_zEcfUUmf2vXVASxYjaaK9cCn-3zb_g@mail.gmail.com", 204 | date: parseDate("Fri, 07 Apr 2017 09:17:26 +0200"), 205 | htmlBody: "