├── .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 | [![Build Status](https://circleci.com/gh/DusanKasan/parsemail.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/DusanKasan/parsemail) [![Coverage Status](https://coveralls.io/repos/github/DusanKasan/Parsemail/badge.svg?branch=master)](https://coveralls.io/github/DusanKasan/Parsemail?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/DusanKasan/parsemail)](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: "

", 206 | attachments: []attachmentData{ 207 | { 208 | filename: "Peter Paholík 1 4 2017 2017-04-07.json", 209 | contentType: "application/json", 210 | data: "[1, 2, 3]", 211 | }, 212 | }, 213 | }, 214 | 7: { 215 | mailData: data2, 216 | contentType: `multipart/alternative; boundary="------------C70C0458A558E585ACB75FB4"`, 217 | content: "", 218 | subject: "Re: Test Subject 2", 219 | from: []mail.Address{ 220 | { 221 | Name: "Sender Man", 222 | Address: "sender@domain.com", 223 | }, 224 | }, 225 | to: []mail.Address{ 226 | { 227 | Name: "", 228 | Address: "info@receiver.com", 229 | }, 230 | }, 231 | cc: []mail.Address{ 232 | { 233 | Name: "Cc Man", 234 | Address: "ccman@gmail.com", 235 | }, 236 | }, 237 | messageID: "0e9a21b4-01dc-e5c1-dcd6-58ce5aa61f4f@receiver.com", 238 | inReplyTo: []string{"9ff38d03-c4ab-89b7-9328-e99d5e24e3ba@receiver.eu"}, 239 | references: []string{"2f6b7595-c01e-46e5-42bc-f263e1c4282d@receiver.com", "9ff38d03-c4ab-89b7-9328-e99d5e24e3ba@domain.com"}, 240 | date: parseDate("Fri, 07 Apr 2017 12:59:55 +0200"), 241 | htmlBody: `data`, 242 | textBody: `First level 243 | > Second level 244 | >> Third level 245 | > 246 | `, 247 | embeddedFiles: []embeddedFileData{ 248 | { 249 | cid: "part2.9599C449.04E5EC81@develhell.com", 250 | contentType: "image/png", 251 | base64data: "iVBORw0KGgoAAAANSUhEUgAAAQEAAAAYCAIAAAB1IN9NAAAACXBIWXMAAAsTAAALEwEAmpwYYKUKF+Os3baUndC0pDnwNAmLy1SUr2Gw0luxQuV/AwC6cEhVV5VRrwAAAABJRU5ErkJggg==", 252 | }, 253 | }, 254 | }, 255 | 8: { 256 | mailData: imageContentExample, 257 | subject: "Saying Hello", 258 | from: []mail.Address{ 259 | { 260 | Name: "John Doe", 261 | Address: "jdoe@machine.example", 262 | }, 263 | }, 264 | to: []mail.Address{ 265 | { 266 | Name: "Mary Smith", 267 | Address: "mary@example.net", 268 | }, 269 | }, 270 | sender: mail.Address{ 271 | Name: "Michael Jones", 272 | Address: "mjones@machine.example", 273 | }, 274 | messageID: "1234@local.machine.example", 275 | date: parseDate("Fri, 21 Nov 1997 09:55:06 -0600"), 276 | contentType: `image/jpeg; x-unix-mode=0644; name="image.gif"`, 277 | content: `GIF89a;`, 278 | }, 279 | 9: { 280 | contentType: `multipart/mixed; boundary="0000000000007e2bb40587e36196"`, 281 | mailData: textPlainInMultipart, 282 | subject: "Re: kern/54143 (virtualbox)", 283 | from: []mail.Address{ 284 | { 285 | Name: "Rares", 286 | Address: "rares@example.com", 287 | }, 288 | }, 289 | to: []mail.Address{ 290 | { 291 | Name: "", 292 | Address: "bugs@example.com", 293 | }, 294 | }, 295 | date: parseDate("Fri, 02 May 2019 11:25:35 +0300"), 296 | textBody: `plain text part`, 297 | }, 298 | 10: { 299 | contentType: `multipart/mixed; boundary="0000000000007e2bb40587e36196"`, 300 | mailData: textHTMLInMultipart, 301 | subject: "Re: kern/54143 (virtualbox)", 302 | from: []mail.Address{ 303 | { 304 | Name: "Rares", 305 | Address: "rares@example.com", 306 | }, 307 | }, 308 | to: []mail.Address{ 309 | { 310 | Name: "", 311 | Address: "bugs@example.com", 312 | }, 313 | }, 314 | date: parseDate("Fri, 02 May 2019 11:25:35 +0300"), 315 | textBody: ``, 316 | htmlBody: "
html text part



", 317 | }, 318 | 11: { 319 | mailData: rfc5322exampleA12WithTimezone, 320 | from: []mail.Address{ 321 | { 322 | Name: "Joe Q. Public", 323 | Address: "john.q.public@example.com", 324 | }, 325 | }, 326 | to: []mail.Address{ 327 | { 328 | Name: "Mary Smith", 329 | Address: "mary@x.test", 330 | }, 331 | { 332 | Name: "", 333 | Address: "jdoe@example.org", 334 | }, 335 | { 336 | Name: "Who?", 337 | Address: "one@y.test", 338 | }, 339 | }, 340 | cc: []mail.Address{ 341 | { 342 | Name: "", 343 | Address: "boss@nil.test", 344 | }, 345 | { 346 | Name: "Giant; \"Big\" Box", 347 | Address: "sysservices@example.net", 348 | }, 349 | }, 350 | messageID: "5678.21-Nov-1997@example.com", 351 | date: parseDate("Tue, 01 Jul 2003 10:52:37 +0200"), 352 | textBody: `Hi everyone.`, 353 | }, 354 | 12: { 355 | contentType: "multipart/mixed; boundary=f403045f1dcc043a44054c8e6bbf", 356 | mailData: attachment7bit, 357 | subject: "Peter Foobar", 358 | from: []mail.Address{ 359 | { 360 | Name: "Peter Foobar", 361 | Address: "peter.foobar@gmail.com", 362 | }, 363 | }, 364 | to: []mail.Address{ 365 | { 366 | Name: "", 367 | Address: "dusan@kasan.sk", 368 | }, 369 | }, 370 | messageID: "CACtgX4kNXE7T5XKSKeH_zEcfUUmf2vXVASxYjaaK9cCn-3zb_g@mail.gmail.com", 371 | date: parseDate("Tue, 02 Apr 2019 11:12:26 +0000"), 372 | htmlBody: "

", 373 | attachments: []attachmentData{ 374 | { 375 | filename: "unencoded.csv", 376 | contentType: "application/csv", 377 | data: fmt.Sprintf("\n"+`"%s", "%s", "%s", "%s", "%s"`+"\n"+`"%s", "%s", "%s", "%s", "%s"`+"\n", "Some", "Data", "In", "Csv", "Format", "Foo", "Bar", "Baz", "Bum", "Poo"), 378 | }, 379 | }, 380 | }, 381 | 13: { 382 | contentType: "multipart/related; boundary=\"000000000000ab2e2205a26de587\"", 383 | mailData: multipartRelatedExample, 384 | subject: "Saying Hello", 385 | from: []mail.Address{ 386 | { 387 | Name: "John Doe", 388 | Address: "jdoe@machine.example", 389 | }, 390 | }, 391 | sender: mail.Address{ 392 | Name: "Michael Jones", 393 | Address: "mjones@machine.example", 394 | }, 395 | to: []mail.Address{ 396 | { 397 | Name: "Mary Smith", 398 | Address: "mary@example.net", 399 | }, 400 | }, 401 | messageID: "1234@local.machine.example", 402 | date: parseDate("Fri, 21 Nov 1997 09:55:06 -0600"), 403 | htmlBody: "
Time for the egg.



", 404 | textBody: "Time for the egg.", 405 | }, 406 | } 407 | 408 | for index, td := range testData { 409 | e, err := Parse(strings.NewReader(td.mailData)) 410 | if err != nil { 411 | t.Error(err) 412 | } 413 | 414 | if td.contentType != e.ContentType { 415 | t.Errorf("[Test Case %v] Wrong content type. Expected: %s, Got: %s", index, td.contentType, e.ContentType) 416 | } 417 | 418 | if td.content != "" { 419 | b, err := ioutil.ReadAll(e.Content) 420 | if err != nil { 421 | t.Error(err) 422 | } else if td.content != string(b) { 423 | t.Errorf("[Test Case %v] Wrong content. Expected: %s, Got: %s", index, td.content, string(b)) 424 | } 425 | } 426 | 427 | if td.subject != e.Subject { 428 | t.Errorf("[Test Case %v] Wrong subject. Expected: %s, Got: %s", index, td.subject, e.Subject) 429 | } 430 | 431 | if td.messageID != e.MessageID { 432 | t.Errorf("[Test Case %v] Wrong messageID. Expected: '%s', Got: '%s'", index, td.messageID, e.MessageID) 433 | } 434 | 435 | if !td.date.Equal(e.Date) { 436 | t.Errorf("[Test Case %v] Wrong date. Expected: %v, Got: %v", index, td.date, e.Date) 437 | } 438 | 439 | d := dereferenceAddressList(e.From) 440 | if !assertAddressListEq(td.from, d) { 441 | t.Errorf("[Test Case %v] Wrong from. Expected: %s, Got: %s", index, td.from, d) 442 | } 443 | 444 | var sender mail.Address 445 | if e.Sender != nil { 446 | sender = *e.Sender 447 | } 448 | if td.sender != sender { 449 | t.Errorf("[Test Case %v] Wrong sender. Expected: %s, Got: %s", index, td.sender, sender) 450 | } 451 | 452 | d = dereferenceAddressList(e.To) 453 | if !assertAddressListEq(td.to, d) { 454 | t.Errorf("[Test Case %v] Wrong to. Expected: %s, Got: %s", index, td.to, d) 455 | } 456 | 457 | d = dereferenceAddressList(e.Cc) 458 | if !assertAddressListEq(td.cc, d) { 459 | t.Errorf("[Test Case %v] Wrong cc. Expected: %s, Got: %s", index, td.cc, d) 460 | } 461 | 462 | d = dereferenceAddressList(e.Bcc) 463 | if !assertAddressListEq(td.bcc, d) { 464 | t.Errorf("[Test Case %v] Wrong bcc. Expected: %s, Got: %s", index, td.bcc, d) 465 | } 466 | 467 | if td.resentMessageID != e.ResentMessageID { 468 | t.Errorf("[Test Case %v] Wrong resent messageID. Expected: '%s', Got: '%s'", index, td.resentMessageID, e.ResentMessageID) 469 | } 470 | 471 | if !td.resentDate.Equal(e.ResentDate) && !td.resentDate.IsZero() && !e.ResentDate.IsZero() { 472 | t.Errorf("[Test Case %v] Wrong resent date. Expected: %v, Got: %v", index, td.resentDate, e.ResentDate) 473 | } 474 | 475 | d = dereferenceAddressList(e.ResentFrom) 476 | if !assertAddressListEq(td.resentFrom, d) { 477 | t.Errorf("[Test Case %v] Wrong resent from. Expected: %s, Got: %s", index, td.resentFrom, d) 478 | } 479 | 480 | var resentSender mail.Address 481 | if e.ResentSender != nil { 482 | resentSender = *e.ResentSender 483 | } 484 | if td.resentSender != resentSender { 485 | t.Errorf("[Test Case %v] Wrong resent sender. Expected: %s, Got: %s", index, td.resentSender, resentSender) 486 | } 487 | 488 | d = dereferenceAddressList(e.ResentTo) 489 | if !assertAddressListEq(td.resentTo, d) { 490 | t.Errorf("[Test Case %v] Wrong resent to. Expected: %s, Got: %s", index, td.resentTo, d) 491 | } 492 | 493 | d = dereferenceAddressList(e.ResentCc) 494 | if !assertAddressListEq(td.resentCc, d) { 495 | t.Errorf("[Test Case %v] Wrong resent cc. Expected: %s, Got: %s", index, td.resentCc, d) 496 | } 497 | 498 | d = dereferenceAddressList(e.ResentBcc) 499 | if !assertAddressListEq(td.resentBcc, d) { 500 | t.Errorf("[Test Case %v] Wrong resent bcc. Expected: %s, Got: %s", index, td.resentBcc, d) 501 | } 502 | 503 | if !assertSliceEq(td.inReplyTo, e.InReplyTo) { 504 | t.Errorf("[Test Case %v] Wrong in reply to. Expected: %s, Got: %s", index, td.inReplyTo, e.InReplyTo) 505 | } 506 | 507 | if !assertSliceEq(td.references, e.References) { 508 | t.Errorf("[Test Case %v] Wrong references. Expected: %s, Got: %s", index, td.references, e.References) 509 | } 510 | 511 | d = dereferenceAddressList(e.ReplyTo) 512 | if !assertAddressListEq(td.replyTo, d) { 513 | t.Errorf("[Test Case %v] Wrong reply to. Expected: %s, Got: %s", index, td.replyTo, d) 514 | } 515 | 516 | if td.htmlBody != e.HTMLBody { 517 | t.Errorf("[Test Case %v] Wrong html body. Expected: '%s', Got: '%s'", index, td.htmlBody, e.HTMLBody) 518 | } 519 | 520 | if td.textBody != e.TextBody { 521 | t.Errorf("[Test Case %v] Wrong text body. Expected: '%s', Got: '%s'", index, td.textBody, e.TextBody) 522 | } 523 | 524 | if len(td.attachments) != len(e.Attachments) { 525 | t.Errorf("[Test Case %v] Incorrect number of attachments! Expected: %v, Got: %v.", index, len(td.attachments), len(e.Attachments)) 526 | } else { 527 | attachs := e.Attachments[:] 528 | 529 | for _, ad := range td.attachments { 530 | found := false 531 | 532 | for i, ra := range attachs { 533 | b, err := ioutil.ReadAll(ra.Data) 534 | if err != nil { 535 | t.Error(err) 536 | } 537 | 538 | if ra.Filename == ad.filename && string(b) == ad.data && ra.ContentType == ad.contentType { 539 | found = true 540 | attachs = append(attachs[:i], attachs[i+1:]...) 541 | } 542 | } 543 | 544 | if !found { 545 | t.Errorf("[Test Case %v] Attachment not found: %s", index, ad.filename) 546 | } 547 | } 548 | 549 | if len(attachs) != 0 { 550 | t.Errorf("[Test Case %v] Email contains %v unexpected attachments: %v", index, len(attachs), attachs) 551 | } 552 | } 553 | 554 | if len(td.embeddedFiles) != len(e.EmbeddedFiles) { 555 | t.Errorf("[Test Case %v] Incorrect number of embedded files! Expected: %v, Got: %v.", index, len(td.embeddedFiles), len(e.EmbeddedFiles)) 556 | } else { 557 | embeds := e.EmbeddedFiles[:] 558 | 559 | for _, ad := range td.embeddedFiles { 560 | found := false 561 | 562 | for i, ra := range embeds { 563 | b, err := ioutil.ReadAll(ra.Data) 564 | if err != nil { 565 | t.Error(err) 566 | } 567 | 568 | encoded := base64.StdEncoding.EncodeToString(b) 569 | 570 | if ra.CID == ad.cid && encoded == ad.base64data && ra.ContentType == ad.contentType { 571 | found = true 572 | embeds = append(embeds[:i], embeds[i+1:]...) 573 | } 574 | } 575 | 576 | if !found { 577 | t.Errorf("[Test Case %v] Embedded file not found: %s", index, ad.cid) 578 | } 579 | } 580 | 581 | if len(embeds) != 0 { 582 | t.Errorf("[Test Case %v] Email contains %v unexpected embedded files: %v", index, len(embeds), embeds) 583 | } 584 | } 585 | } 586 | } 587 | 588 | func parseDate(in string) time.Time { 589 | out, err := time.Parse(time.RFC1123Z, in) 590 | if err != nil { 591 | panic(err) 592 | } 593 | 594 | return out 595 | } 596 | 597 | type attachmentData struct { 598 | filename string 599 | contentType string 600 | data string 601 | } 602 | 603 | type embeddedFileData struct { 604 | cid string 605 | contentType string 606 | base64data string 607 | } 608 | 609 | func assertSliceEq(a, b []string) bool { 610 | if len(a) == len(b) && len(a) == 0 { 611 | return true 612 | } 613 | 614 | if a == nil && b == nil { 615 | return true 616 | } 617 | 618 | if a == nil || b == nil { 619 | return false 620 | } 621 | 622 | if len(a) != len(b) { 623 | return false 624 | } 625 | 626 | for i := range a { 627 | if a[i] != b[i] { 628 | return false 629 | } 630 | } 631 | 632 | return true 633 | } 634 | 635 | func assertAddressListEq(a, b []mail.Address) bool { 636 | if len(a) == len(b) && len(a) == 0 { 637 | return true 638 | } 639 | 640 | if a == nil && b == nil { 641 | return true 642 | } 643 | 644 | if a == nil || b == nil { 645 | return false 646 | } 647 | 648 | if len(a) != len(b) { 649 | return false 650 | } 651 | 652 | for i := range a { 653 | if a[i] != b[i] { 654 | return false 655 | } 656 | } 657 | 658 | return true 659 | } 660 | 661 | func dereferenceAddressList(al []*mail.Address) (result []mail.Address) { 662 | for _, a := range al { 663 | result = append(result, *a) 664 | } 665 | 666 | return 667 | } 668 | 669 | var data1 = `From: =?UTF-8?Q?Peter_Pahol=C3=ADk?= 670 | Date: Fri, 7 Apr 2017 09:17:26 +0200 671 | Message-ID: 672 | Subject: =?UTF-8?Q?Peter_Pahol=C3=ADk?= 673 | To: dusan@kasan.sk 674 | Content-Type: multipart/mixed; boundary=f403045f1dcc043a44054c8e6bbf 675 | 676 | --f403045f1dcc043a44054c8e6bbf 677 | Content-Type: multipart/alternative; boundary=f403045f1dcc043a3f054c8e6bbd 678 | 679 | --f403045f1dcc043a3f054c8e6bbd 680 | Content-Type: text/plain; charset=UTF-8 681 | 682 | 683 | 684 | --f403045f1dcc043a3f054c8e6bbd 685 | Content-Type: text/html; charset=UTF-8 686 | 687 |

688 | 689 | --f403045f1dcc043a3f054c8e6bbd-- 690 | --f403045f1dcc043a44054c8e6bbf 691 | Content-Type: application/json; 692 | name="=?UTF-8?Q?Peter_Paholi=CC=81k_1?= 693 | =?UTF-8?Q?_4_2017_2017=2D04=2D07=2Ejson?=" 694 | Content-Disposition: attachment; 695 | filename="=?UTF-8?Q?Peter_Paholi=CC=81k_1?= 696 | =?UTF-8?Q?_4_2017_2017=2D04=2D07=2Ejson?=" 697 | Content-Transfer-Encoding: base64 698 | X-Attachment-Id: f_j17i0f0d0 699 | 700 | WzEsIDIsIDNd 701 | --f403045f1dcc043a44054c8e6bbf-- 702 | ` 703 | 704 | var data2 = `Subject: Re: Test Subject 2 705 | To: info@receiver.com 706 | References: <2f6b7595-c01e-46e5-42bc-f263e1c4282d@receiver.com> 707 | <9ff38d03-c4ab-89b7-9328-e99d5e24e3ba@domain.com> 708 | Cc: Cc Man 709 | From: Sender Man 710 | Message-ID: <0e9a21b4-01dc-e5c1-dcd6-58ce5aa61f4f@receiver.com> 711 | Date: Fri, 7 Apr 2017 12:59:55 +0200 712 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:45.0) 713 | Gecko/20100101 Thunderbird/45.8.0 714 | MIME-Version: 1.0 715 | In-Reply-To: <9ff38d03-c4ab-89b7-9328-e99d5e24e3ba@receiver.eu> 716 | Content-Type: multipart/alternative; 717 | boundary="------------C70C0458A558E585ACB75FB4" 718 | 719 | This is a multi-part message in MIME format. 720 | --------------C70C0458A558E585ACB75FB4 721 | Content-Type: text/plain; charset=utf-8; format=flowed 722 | Content-Transfer-Encoding: 8bit 723 | 724 | First level 725 | > Second level 726 | >> Third level 727 | > 728 | 729 | 730 | --------------C70C0458A558E585ACB75FB4 731 | Content-Type: multipart/related; 732 | boundary="------------5DB4A1356834BB602A5F88B2" 733 | 734 | 735 | --------------5DB4A1356834BB602A5F88B2 736 | Content-Type: text/html; charset=utf-8 737 | Content-Transfer-Encoding: 8bit 738 | 739 | data 740 | 741 | --------------5DB4A1356834BB602A5F88B2 742 | Content-Type: image/png 743 | Content-Transfer-Encoding: base64 744 | Content-ID: 745 | 746 | iVBORw0KGgoAAAANSUhEUgAAAQEAAAAYCAIAAAB1IN9NAAAACXBIWXMAAAsTAAALEwEAmpwY 747 | YKUKF+Os3baUndC0pDnwNAmLy1SUr2Gw0luxQuV/AwC6cEhVV5VRrwAAAABJRU5ErkJggg== 748 | --------------5DB4A1356834BB602A5F88B2 749 | 750 | --------------C70C0458A558E585ACB75FB4-- 751 | ` 752 | 753 | var textPlainInMultipart = `From: Rares 754 | Date: Thu, 2 May 2019 11:25:35 +0300 755 | Subject: Re: kern/54143 (virtualbox) 756 | To: bugs@example.com 757 | Content-Type: multipart/mixed; boundary="0000000000007e2bb40587e36196" 758 | 759 | --0000000000007e2bb40587e36196 760 | Content-Type: text/plain; charset="UTF-8" 761 | 762 | plain text part 763 | --0000000000007e2bb40587e36196-- 764 | ` 765 | 766 | var textHTMLInMultipart = `From: Rares 767 | Date: Thu, 2 May 2019 11:25:35 +0300 768 | Subject: Re: kern/54143 (virtualbox) 769 | To: bugs@example.com 770 | Content-Type: multipart/mixed; boundary="0000000000007e2bb40587e36196" 771 | 772 | --0000000000007e2bb40587e36196 773 | Content-Type: text/html; charset="UTF-8" 774 | 775 |
html text part



776 | 777 | --0000000000007e2bb40587e36196-- 778 | ` 779 | 780 | var rfc5322exampleA11 = `From: John Doe 781 | Sender: Michael Jones 782 | To: Mary Smith 783 | Subject: Saying Hello 784 | Date: Fri, 21 Nov 1997 09:55:06 -0600 785 | Message-ID: <1234@local.machine.example> 786 | 787 | This is a message just to say hello. 788 | So, "Hello". 789 | ` 790 | 791 | var rfc5322exampleA12 = `From: "Joe Q. Public" 792 | To: Mary Smith , jdoe@example.org, Who? 793 | Cc: , "Giant; \"Big\" Box" 794 | Date: Tue, 1 Jul 2003 10:52:37 +0200 795 | Message-ID: <5678.21-Nov-1997@example.com> 796 | 797 | Hi everyone. 798 | ` 799 | 800 | var rfc5322exampleA12WithTimezone = `From: "Joe Q. Public" 801 | To: Mary Smith , jdoe@example.org, Who? 802 | Cc: , "Giant; \"Big\" Box" 803 | Date: Tue, 1 Jul 2003 10:52:37 +0200 (GMT) 804 | Message-ID: <5678.21-Nov-1997@example.com> 805 | 806 | Hi everyone. 807 | ` 808 | 809 | //todo: not yet implemented in net/mail 810 | //once there is support for this, add it 811 | var rfc5322exampleA13 = `From: Pete 812 | To: A Group:Ed Jones ,joe@where.test,John ; 813 | Cc: Undisclosed recipients:; 814 | Date: Thu, 13 Feb 1969 23:32:54 -0330 815 | Message-ID: 816 | 817 | Testing. 818 | ` 819 | 820 | //we skipped the first message bcause it's the same as A 1.1 821 | var rfc5322exampleA2a = `From: Mary Smith 822 | To: John Doe 823 | Reply-To: "Mary Smith: Personal Account" 824 | Subject: Re: Saying Hello 825 | Date: Fri, 21 Nov 1997 10:01:10 -0600 826 | Message-ID: <3456@example.net> 827 | In-Reply-To: <1234@local.machine.example> 828 | References: <1234@local.machine.example> 829 | 830 | This is a reply to your hello. 831 | ` 832 | 833 | var rfc5322exampleA2b = `To: "Mary Smith: Personal Account" 834 | From: John Doe 835 | Subject: Re: Saying Hello 836 | Date: Fri, 21 Nov 1997 11:00:00 -0600 837 | Message-ID: 838 | In-Reply-To: <3456@example.net> 839 | References: <1234@local.machine.example> <3456@example.net> 840 | 841 | This is a reply to your reply. 842 | ` 843 | 844 | var rfc5322exampleA3 = `Resent-From: Mary Smith 845 | Resent-To: Jane Brown 846 | Resent-Date: Mon, 24 Nov 1997 14:22:01 -0800 847 | Resent-Message-ID: <78910@example.net> 848 | From: John Doe 849 | To: Mary Smith 850 | Subject: Saying Hello 851 | Date: Fri, 21 Nov 1997 09:55:06 -0600 852 | Message-ID: <1234@local.machine.example> 853 | 854 | This is a message just to say hello. 855 | So, "Hello".` 856 | 857 | var rfc5322exampleA4 = `Received: from x.y.test 858 | by example.net 859 | via TCP 860 | with ESMTP 861 | id ABC12345 862 | for ; 21 Nov 1997 10:05:43 -0600 863 | Received: from node.example by x.y.test; 21 Nov 1997 10:01:22 -0600 864 | From: John Doe 865 | To: Mary Smith 866 | Subject: Saying Hello 867 | Date: Fri, 21 Nov 1997 09:55:06 -0600 868 | Message-ID: <1234@local.node.example> 869 | 870 | This is a message just to say hello. 871 | So, "Hello".` 872 | 873 | var imageContentExample = `From: John Doe 874 | Sender: Michael Jones 875 | To: Mary Smith 876 | Subject: Saying Hello 877 | Date: Fri, 21 Nov 1997 09:55:06 -0600 878 | Message-ID: <1234@local.machine.example> 879 | Content-Type: image/jpeg; 880 | x-unix-mode=0644; 881 | name="image.gif" 882 | Content-Transfer-Encoding: base64 883 | 884 | R0lGODlhAQE7` 885 | 886 | var multipartRelatedExample = `MIME-Version: 1.0 887 | From: John Doe 888 | Sender: Michael Jones 889 | To: Mary Smith 890 | Subject: Saying Hello 891 | Date: Fri, 21 Nov 1997 09:55:06 -0600 892 | Message-ID: <1234@local.machine.example> 893 | Subject: ooops 894 | To: test@example.rocks 895 | Content-Type: multipart/related; boundary="000000000000ab2e2205a26de587" 896 | 897 | --000000000000ab2e2205a26de587 898 | Content-Type: multipart/alternative; boundary="000000000000ab2e1f05a26de586" 899 | 900 | --000000000000ab2e1f05a26de586 901 | Content-Type: text/plain; charset="UTF-8" 902 | 903 | Time for the egg. 904 | 905 | --000000000000ab2e1f05a26de586 906 | Content-Type: text/html; charset="UTF-8" 907 | 908 |
Time for the egg.



909 | 910 | --000000000000ab2e1f05a26de586-- 911 | 912 | 913 | --000000000000ab2e2205a26de587-- 914 | ` 915 | var attachment7bit = `From: =?UTF-8?Q?Peter_Foobar?= 916 | Date: Tue, 2 Apr 2019 11:12:26 +0000 917 | Message-ID: 918 | Subject: =?UTF-8?Q?Peter_Foobar?= 919 | To: dusan@kasan.sk 920 | Content-Type: multipart/mixed; boundary=f403045f1dcc043a44054c8e6bbf 921 | 922 | --f403045f1dcc043a44054c8e6bbf 923 | Content-Type: multipart/alternative; boundary=f403045f1dcc043a3f054c8e6bbd 924 | 925 | --f403045f1dcc043a3f054c8e6bbd 926 | Content-Type: text/plain; charset=UTF-8 927 | 928 | 929 | 930 | --f403045f1dcc043a3f054c8e6bbd 931 | Content-Type: text/html; charset=UTF-8 932 | 933 |

934 | 935 | --f403045f1dcc043a3f054c8e6bbd-- 936 | --f403045f1dcc043a44054c8e6bbf 937 | Content-Type: application/csv; 938 | name="unencoded.csv" 939 | Content-Transfer-Encoding: 7bit 940 | Content-Disposition: attachment; 941 | filename="unencoded.csv" 942 | 943 | 944 | "Some", "Data", "In", "Csv", "Format" 945 | "Foo", "Bar", "Baz", "Bum", "Poo" 946 | 947 | --f403045f1dcc043a44054c8e6bbf-- 948 | ` 949 | --------------------------------------------------------------------------------