1package mailgun 2 3import ( 4 "encoding/json" 5 "errors" 6 "io" 7 "time" 8) 9 10// MaxNumberOfRecipients represents the largest batch of recipients that Mailgun can support in a single API call. 11// This figure includes To:, Cc:, Bcc:, etc. recipients. 12const MaxNumberOfRecipients = 1000 13 14// Message structures contain both the message text and the envelop for an e-mail message. 15type Message struct { 16 to []string 17 tags []string 18 campaigns []string 19 dkim bool 20 deliveryTime *time.Time 21 attachments []string 22 readerAttachments []ReaderAttachment 23 inlines []string 24 readerInlines []ReaderAttachment 25 26 nativeSend bool 27 testMode bool 28 tracking bool 29 trackingClicks bool 30 trackingOpens bool 31 headers map[string]string 32 variables map[string]string 33 recipientVariables map[string]map[string]interface{} 34 domain string 35 36 dkimSet bool 37 trackingSet bool 38 trackingClicksSet bool 39 trackingOpensSet bool 40 41 specific features 42 mg Mailgun 43} 44 45type ReaderAttachment struct { 46 Filename string 47 ReadCloser io.ReadCloser 48} 49 50// StoredMessage structures contain the (parsed) message content for an email 51// sent to a Mailgun account. 52// 53// The MessageHeaders field is special, in that it's formatted as a slice of pairs. 54// Each pair consists of a name [0] and value [1]. Array notation is used instead of a map 55// because that's how it's sent over the wire, and it's how encoding/json expects this field 56// to be. 57type StoredMessage struct { 58 Recipients string `json:"recipients"` 59 Sender string `json:"sender"` 60 From string `json:"from"` 61 Subject string `json:"subject"` 62 BodyPlain string `json:"body-plain"` 63 StrippedText string `json:"stripped-text"` 64 StrippedSignature string `json:"stripped-signature"` 65 BodyHtml string `json:"body-html"` 66 StrippedHtml string `json:"stripped-html"` 67 Attachments []StoredAttachment `json:"attachments"` 68 MessageUrl string `json:"message-url"` 69 ContentIDMap map[string]struct { 70 Url string `json:"url"` 71 ContentType string `json:"content-type"` 72 Name string `json:"name"` 73 Size int64 `json:"size"` 74 } `json:"content-id-map"` 75 MessageHeaders [][]string `json:"message-headers"` 76} 77 78// StoredAttachment structures contain information on an attachment associated with a stored message. 79type StoredAttachment struct { 80 Size int `json:"size"` 81 Url string `json:"url"` 82 Name string `json:"name"` 83 ContentType string `json:"content-type"` 84} 85 86type StoredMessageRaw struct { 87 Recipients string `json:"recipients"` 88 Sender string `json:"sender"` 89 From string `json:"from"` 90 Subject string `json:"subject"` 91 BodyMime string `json:"body-mime"` 92} 93 94// plainMessage contains fields relevant to plain API-synthesized messages. 95// You're expected to use various setters to set most of these attributes, 96// although from, subject, and text are set when the message is created with 97// NewMessage. 98type plainMessage struct { 99 from string 100 cc []string 101 bcc []string 102 subject string 103 text string 104 html string 105} 106 107// mimeMessage contains fields relevant to pre-packaged MIME messages. 108type mimeMessage struct { 109 body io.ReadCloser 110} 111 112type sendMessageResponse struct { 113 Message string `json:"message"` 114 Id string `json:"id"` 115} 116 117// features abstracts the common characteristics between regular and MIME messages. 118// addCC, addBCC, recipientCount, and setHTML are invoked via the package-global AddCC, AddBCC, 119// RecipientCount, and SetHtml calls, as these functions are ignored for MIME messages. 120// Send() invokes addValues to add message-type-specific MIME headers for the API call 121// to Mailgun. isValid yeilds true if and only if the message is valid enough for sending 122// through the API. Finally, endpoint() tells Send() which endpoint to use to submit the API call. 123type features interface { 124 addCC(string) 125 addBCC(string) 126 setHtml(string) 127 addValues(*formDataPayload) 128 isValid() bool 129 endpoint() string 130 recipientCount() int 131} 132 133// NewMessage returns a new e-mail message with the simplest envelop needed to send. 134// 135// DEPRECATED. 136// The package will panic if you use AddRecipient(), AddBcc(), AddCc(), et. al. 137// on a message already equipped with MaxNumberOfRecipients recipients. 138// Use Mailgun.NewMessage() instead. 139// It works similarly to this function, but supports larger lists of recipients. 140func NewMessage(from string, subject string, text string, to ...string) *Message { 141 return &Message{ 142 specific: &plainMessage{ 143 from: from, 144 subject: subject, 145 text: text, 146 }, 147 to: to, 148 } 149} 150 151// NewMessage returns a new e-mail message with the simplest envelop needed to send. 152// 153// Unlike the global function, 154// this method supports arbitrary-sized recipient lists by 155// automatically sending mail in batches of up to MaxNumberOfRecipients. 156// 157// To support batch sending, you don't want to provide a fixed To: header at this point. 158// Pass nil as the to parameter to skip adding the To: header at this stage. 159// You can do this explicitly, or implicitly, as follows: 160// 161// // Note absence of To parameter(s)! 162// m := mg.NewMessage("me@example.com", "Help save our planet", "Hello world!") 163// 164// Note that you'll need to invoke the AddRecipientAndVariables or AddRecipient method 165// before sending, though. 166func (mg *MailgunImpl) NewMessage(from, subject, text string, to ...string) *Message { 167 return &Message{ 168 specific: &plainMessage{ 169 from: from, 170 subject: subject, 171 text: text, 172 }, 173 to: to, 174 mg: mg, 175 } 176} 177 178// NewMIMEMessage creates a new MIME message. These messages are largely canned; 179// you do not need to invoke setters to set message-related headers. 180// However, you do still need to call setters for Mailgun-specific settings. 181// 182// DEPRECATED. 183// The package will panic if you use AddRecipient(), AddBcc(), AddCc(), et. al. 184// on a message already equipped with MaxNumberOfRecipients recipients. 185// Use Mailgun.NewMIMEMessage() instead. 186// It works similarly to this function, but supports larger lists of recipients. 187func NewMIMEMessage(body io.ReadCloser, to ...string) *Message { 188 return &Message{ 189 specific: &mimeMessage{ 190 body: body, 191 }, 192 to: to, 193 } 194} 195 196// NewMIMEMessage creates a new MIME message. These messages are largely canned; 197// you do not need to invoke setters to set message-related headers. 198// However, you do still need to call setters for Mailgun-specific settings. 199// 200// Unlike the global function, 201// this method supports arbitrary-sized recipient lists by 202// automatically sending mail in batches of up to MaxNumberOfRecipients. 203// 204// To support batch sending, you don't want to provide a fixed To: header at this point. 205// Pass nil as the to parameter to skip adding the To: header at this stage. 206// You can do this explicitly, or implicitly, as follows: 207// 208// // Note absence of To parameter(s)! 209// m := mg.NewMessage("me@example.com", "Help save our planet", "Hello world!") 210// 211// Note that you'll need to invoke the AddRecipientAndVariables or AddRecipient method 212// before sending, though. 213func (mg *MailgunImpl) NewMIMEMessage(body io.ReadCloser, to ...string) *Message { 214 return &Message{ 215 specific: &mimeMessage{ 216 body: body, 217 }, 218 to: to, 219 mg: mg, 220 } 221} 222 223// AddReaderAttachment arranges to send a file along with the e-mail message. 224// File contents are read from a io.ReadCloser. 225// The filename parameter is the resulting filename of the attachment. 226// The readCloser parameter is the io.ReadCloser which reads the actual bytes to be used 227// as the contents of the attached file. 228func (m *Message) AddReaderAttachment(filename string, readCloser io.ReadCloser) { 229 ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser} 230 m.readerAttachments = append(m.readerAttachments, ra) 231} 232 233// AddAttachment arranges to send a file from the filesystem along with the e-mail message. 234// The attachment parameter is a filename, which must refer to a file which actually resides 235// in the local filesystem. 236func (m *Message) AddAttachment(attachment string) { 237 m.attachments = append(m.attachments, attachment) 238} 239 240// AddReaderInline arranges to send a file along with the e-mail message. 241// File contents are read from a io.ReadCloser. 242// The filename parameter is the resulting filename of the attachment. 243// The readCloser parameter is the io.ReadCloser which reads the actual bytes to be used 244// as the contents of the attached file. 245func (m *Message) AddReaderInline(filename string, readCloser io.ReadCloser) { 246 ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser} 247 m.readerInlines = append(m.readerInlines, ra) 248} 249 250// AddInline arranges to send a file along with the e-mail message, but does so 251// in a way that its data remains "inline" with the rest of the message. This 252// can be used to send image or font data along with an HTML-encoded message body. 253// The attachment parameter is a filename, which must refer to a file which actually resides 254// in the local filesystem. 255func (m *Message) AddInline(inline string) { 256 m.inlines = append(m.inlines, inline) 257} 258 259// AddRecipient appends a receiver to the To: header of a message. 260// 261// NOTE: Above a certain limit (currently 1000 recipients), 262// this function will cause the message as it's currently defined to be sent. 263// This allows you to support large mailing lists without running into Mailgun's API limitations. 264func (m *Message) AddRecipient(recipient string) error { 265 return m.AddRecipientAndVariables(recipient, nil) 266} 267 268// AddRecipientAndVariables appends a receiver to the To: header of a message, 269// and as well attaches a set of variables relevant for this recipient. 270// 271// NOTE: Above a certain limit (see MaxNumberOfRecipients), 272// this function will cause the message as it's currently defined to be sent. 273// This allows you to support large mailing lists without running into Mailgun's API limitations. 274func (m *Message) AddRecipientAndVariables(r string, vars map[string]interface{}) error { 275 if m.RecipientCount() >= MaxNumberOfRecipients { 276 _, _, err := m.send() 277 if err != nil { 278 return err 279 } 280 m.to = make([]string, len(m.to)) 281 m.recipientVariables = make(map[string]map[string]interface{}, len(m.recipientVariables)) 282 } 283 m.to = append(m.to, r) 284 if vars != nil { 285 if m.recipientVariables == nil { 286 m.recipientVariables = make(map[string]map[string]interface{}) 287 } 288 m.recipientVariables[r] = vars 289 } 290 return nil 291} 292 293// RecipientCount returns the total number of recipients for the message. 294// This includes To:, Cc:, and Bcc: fields. 295// 296// NOTE: At present, this method is reliable only for non-MIME messages, as the 297// Bcc: and Cc: fields are easily accessible. 298// For MIME messages, only the To: field is considered. 299// A fix for this issue is planned for a future release. 300// For now, MIME messages are always assumed to have 10 recipients between Cc: and Bcc: fields. 301// If your MIME messages have more than 10 non-To: field recipients, 302// you may find that some recipients will not receive your e-mail. 303// It's perfectly OK, of course, for a MIME message to not have any Cc: or Bcc: recipients. 304func (m *Message) RecipientCount() int { 305 return len(m.to) + m.specific.recipientCount() 306} 307 308func (pm *plainMessage) recipientCount() int { 309 return len(pm.bcc) + len(pm.cc) 310} 311 312func (mm *mimeMessage) recipientCount() int { 313 return 10 314} 315 316func (m *Message) send() (string, string, error) { 317 return m.mg.Send(m) 318} 319 320func (m *Message) SetReplyTo(recipient string) { 321 m.AddHeader("Reply-To", recipient) 322} 323 324// AddCC appends a receiver to the carbon-copy header of a message. 325func (m *Message) AddCC(recipient string) { 326 m.specific.addCC(recipient) 327} 328 329func (pm *plainMessage) addCC(r string) { 330 pm.cc = append(pm.cc, r) 331} 332 333func (mm *mimeMessage) addCC(_ string) {} 334 335// AddBCC appends a receiver to the blind-carbon-copy header of a message. 336func (m *Message) AddBCC(recipient string) { 337 m.specific.addBCC(recipient) 338} 339 340func (pm *plainMessage) addBCC(r string) { 341 pm.bcc = append(pm.bcc, r) 342} 343 344func (mm *mimeMessage) addBCC(_ string) {} 345 346// If you're sending a message that isn't already MIME encoded, SetHtml() will arrange to bundle 347// an HTML representation of your message in addition to your plain-text body. 348func (m *Message) SetHtml(html string) { 349 m.specific.setHtml(html) 350} 351 352func (pm *plainMessage) setHtml(h string) { 353 pm.html = h 354} 355 356func (mm *mimeMessage) setHtml(_ string) {} 357 358// AddTag attaches a tag to the message. Tags are useful for metrics gathering and event tracking purposes. 359// Refer to the Mailgun documentation for further details. 360func (m *Message) AddTag(tag string) { 361 m.tags = append(m.tags, tag) 362} 363 364// This feature is deprecated for new software. 365func (m *Message) AddCampaign(campaign string) { 366 m.campaigns = append(m.campaigns, campaign) 367} 368 369// SetDKIM arranges to send the o:dkim header with the message, and sets its value accordingly. 370// Refer to the Mailgun documentation for more information. 371func (m *Message) SetDKIM(dkim bool) { 372 m.dkim = dkim 373 m.dkimSet = true 374} 375 376// EnableNativeSend allows the return path to match the address in the Message.Headers.From: 377// field when sending from Mailgun rather than the usual bounce+ address in the return path. 378func (m *Message) EnableNativeSend() { 379 m.nativeSend = true 380} 381 382// EnableTestMode allows submittal of a message, such that it will be discarded by Mailgun. 383// This facilitates testing client-side software without actually consuming e-mail resources. 384func (m *Message) EnableTestMode() { 385 m.testMode = true 386} 387 388// SetDeliveryTime schedules the message for transmission at the indicated time. 389// Pass nil to remove any installed schedule. 390// Refer to the Mailgun documentation for more information. 391func (m *Message) SetDeliveryTime(dt time.Time) { 392 pdt := new(time.Time) 393 *pdt = dt 394 m.deliveryTime = pdt 395} 396 397// SetTracking sets the o:tracking message parameter to adjust, on a message-by-message basis, 398// whether or not Mailgun will rewrite URLs to facilitate event tracking. 399// Events tracked includes opens, clicks, unsubscribes, etc. 400// Note: simply calling this method ensures that the o:tracking header is passed in with the message. 401// Its yes/no setting is determined by the call's parameter. 402// Note that this header is not passed on to the final recipient(s). 403// Refer to the Mailgun documentation for more information. 404func (m *Message) SetTracking(tracking bool) { 405 m.tracking = tracking 406 m.trackingSet = true 407} 408 409// Refer to the Mailgun documentation for more information. 410func (m *Message) SetTrackingClicks(trackingClicks bool) { 411 m.trackingClicks = trackingClicks 412 m.trackingClicksSet = true 413} 414 415// Refer to the Mailgun documentation for more information. 416func (m *Message) SetTrackingOpens(trackingOpens bool) { 417 m.trackingOpens = trackingOpens 418 m.trackingOpensSet = true 419} 420 421// AddHeader allows you to send custom MIME headers with the message. 422func (m *Message) AddHeader(header, value string) { 423 if m.headers == nil { 424 m.headers = make(map[string]string) 425 } 426 m.headers[header] = value 427} 428 429// AddVariable lets you associate a set of variables with messages you send, 430// which Mailgun can use to, in essence, complete form-mail. 431// Refer to the Mailgun documentation for more information. 432func (m *Message) AddVariable(variable string, value interface{}) error { 433 j, err := json.Marshal(value) 434 if err != nil { 435 return err 436 } 437 if m.variables == nil { 438 m.variables = make(map[string]string) 439 } 440 m.variables[variable] = string(j) 441 return nil 442} 443 444// AddDomain allows you to use a separate domain for the type of messages you are sending. 445func (m *Message) AddDomain(domain string) { 446 m.domain = domain 447} 448 449// Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery. 450// It returns the Mailgun server response, which consists of two components: 451// a human-readable status message, and a message ID. The status and message ID are set only 452// if no error occurred. 453func (m *MailgunImpl) Send(message *Message) (mes string, id string, err error) { 454 if !isValid(message) { 455 err = errors.New("Message not valid") 456 return 457 } 458 payload := newFormDataPayload() 459 460 message.specific.addValues(payload) 461 for _, to := range message.to { 462 payload.addValue("to", to) 463 } 464 for _, tag := range message.tags { 465 payload.addValue("o:tag", tag) 466 } 467 for _, campaign := range message.campaigns { 468 payload.addValue("o:campaign", campaign) 469 } 470 if message.dkimSet { 471 payload.addValue("o:dkim", yesNo(message.dkim)) 472 } 473 if message.deliveryTime != nil { 474 payload.addValue("o:deliverytime", formatMailgunTime(message.deliveryTime)) 475 } 476 if message.nativeSend { 477 payload.addValue("o:native-send", "yes") 478 } 479 if message.testMode { 480 payload.addValue("o:testmode", "yes") 481 } 482 if message.trackingSet { 483 payload.addValue("o:tracking", yesNo(message.tracking)) 484 } 485 if message.trackingClicksSet { 486 payload.addValue("o:tracking-clicks", yesNo(message.trackingClicks)) 487 } 488 if message.trackingOpensSet { 489 payload.addValue("o:tracking-opens", yesNo(message.trackingOpens)) 490 } 491 if message.headers != nil { 492 for header, value := range message.headers { 493 payload.addValue("h:"+header, value) 494 } 495 } 496 if message.variables != nil { 497 for variable, value := range message.variables { 498 payload.addValue("v:"+variable, value) 499 } 500 } 501 if message.recipientVariables != nil { 502 j, err := json.Marshal(message.recipientVariables) 503 if err != nil { 504 return "", "", err 505 } 506 payload.addValue("recipient-variables", string(j)) 507 } 508 if message.attachments != nil { 509 for _, attachment := range message.attachments { 510 payload.addFile("attachment", attachment) 511 } 512 } 513 if message.readerAttachments != nil { 514 for _, readerAttachment := range message.readerAttachments { 515 payload.addReadCloser("attachment", readerAttachment.Filename, readerAttachment.ReadCloser) 516 } 517 } 518 if message.inlines != nil { 519 for _, inline := range message.inlines { 520 payload.addFile("inline", inline) 521 } 522 } 523 524 if message.readerInlines != nil { 525 for _, readerAttachment := range message.readerInlines { 526 payload.addReadCloser("inline", readerAttachment.Filename, readerAttachment.ReadCloser) 527 } 528 } 529 530 if message.domain == "" { 531 message.domain = m.Domain() 532 } 533 534 r := newHTTPRequest(generateApiUrlWithDomain(m, message.specific.endpoint(), message.domain)) 535 r.setClient(m.Client()) 536 r.setBasicAuth(basicAuthUser, m.ApiKey()) 537 538 var response sendMessageResponse 539 err = postResponseFromJSON(r, payload, &response) 540 if err == nil { 541 mes = response.Message 542 id = response.Id 543 } 544 545 return 546} 547 548func (pm *plainMessage) addValues(p *formDataPayload) { 549 p.addValue("from", pm.from) 550 p.addValue("subject", pm.subject) 551 p.addValue("text", pm.text) 552 for _, cc := range pm.cc { 553 p.addValue("cc", cc) 554 } 555 for _, bcc := range pm.bcc { 556 p.addValue("bcc", bcc) 557 } 558 if pm.html != "" { 559 p.addValue("html", pm.html) 560 } 561} 562 563func (mm *mimeMessage) addValues(p *formDataPayload) { 564 p.addReadCloser("message", "message.mime", mm.body) 565} 566 567func (pm *plainMessage) endpoint() string { 568 return messagesEndpoint 569} 570 571func (mm *mimeMessage) endpoint() string { 572 return mimeMessagesEndpoint 573} 574 575// yesNo translates a true/false boolean value into a yes/no setting suitable for the Mailgun API. 576func yesNo(b bool) string { 577 if b { 578 return "yes" 579 } else { 580 return "no" 581 } 582} 583 584// isValid returns true if, and only if, 585// a Message instance is sufficiently initialized to send via the Mailgun interface. 586func isValid(m *Message) bool { 587 if m == nil { 588 return false 589 } 590 591 if !m.specific.isValid() { 592 return false 593 } 594 595 if !validateStringList(m.to, true) { 596 return false 597 } 598 599 if !validateStringList(m.tags, false) { 600 return false 601 } 602 603 if !validateStringList(m.campaigns, false) || len(m.campaigns) > 3 { 604 return false 605 } 606 607 return true 608} 609 610func (pm *plainMessage) isValid() bool { 611 if pm.from == "" { 612 return false 613 } 614 615 if !validateStringList(pm.cc, false) { 616 return false 617 } 618 619 if !validateStringList(pm.bcc, false) { 620 return false 621 } 622 623 if pm.text == "" && pm.html == "" { 624 return false 625 } 626 627 return true 628} 629 630func (mm *mimeMessage) isValid() bool { 631 return mm.body != nil 632} 633 634// validateStringList returns true if, and only if, 635// a slice of strings exists AND all of its elements exist, 636// OR if the slice doesn't exist AND it's not required to exist. 637// The requireOne parameter indicates whether the list is required to exist. 638func validateStringList(list []string, requireOne bool) bool { 639 hasOne := false 640 641 if list == nil { 642 return !requireOne 643 } else { 644 for _, a := range list { 645 if a == "" { 646 return false 647 } else { 648 hasOne = hasOne || true 649 } 650 } 651 } 652 653 return hasOne 654} 655 656// GetStoredMessage retrieves information about a received e-mail message. 657// This provides visibility into, e.g., replies to a message sent to a mailing list. 658func (mg *MailgunImpl) GetStoredMessage(id string) (StoredMessage, error) { 659 url := generateStoredMessageUrl(mg, messagesEndpoint, id) 660 r := newHTTPRequest(url) 661 r.setClient(mg.Client()) 662 r.setBasicAuth(basicAuthUser, mg.ApiKey()) 663 664 var response StoredMessage 665 err := getResponseFromJSON(r, &response) 666 return response, err 667} 668 669// GetStoredMessageRaw retrieves the raw MIME body of a received e-mail message. 670// Compared to GetStoredMessage, it gives access to the unparsed MIME body, and 671// thus delegates to the caller the required parsing. 672func (mg *MailgunImpl) GetStoredMessageRaw(id string) (StoredMessageRaw, error) { 673 url := generateStoredMessageUrl(mg, messagesEndpoint, id) 674 r := newHTTPRequest(url) 675 r.setClient(mg.Client()) 676 r.setBasicAuth(basicAuthUser, mg.ApiKey()) 677 r.addHeader("Accept", "message/rfc2822") 678 679 var response StoredMessageRaw 680 err := getResponseFromJSON(r, &response) 681 return response, err 682} 683 684// GetStoredMessageForURL retrieves information about a received e-mail message. 685// This provides visibility into, e.g., replies to a message sent to a mailing list. 686func (mg *MailgunImpl) GetStoredMessageForURL(url string) (StoredMessage, error) { 687 r := newHTTPRequest(url) 688 r.setClient(mg.Client()) 689 r.setBasicAuth(basicAuthUser, mg.ApiKey()) 690 691 var response StoredMessage 692 err := getResponseFromJSON(r, &response) 693 return response, err 694} 695 696// GetStoredMessageRawForURL retrieves the raw MIME body of a received e-mail message. 697// Compared to GetStoredMessage, it gives access to the unparsed MIME body, and 698// thus delegates to the caller the required parsing. 699func (mg *MailgunImpl) GetStoredMessageRawForURL(url string) (StoredMessageRaw, error) { 700 r := newHTTPRequest(url) 701 r.setClient(mg.Client()) 702 r.setBasicAuth(basicAuthUser, mg.ApiKey()) 703 r.addHeader("Accept", "message/rfc2822") 704 705 var response StoredMessageRaw 706 err := getResponseFromJSON(r, &response) 707 return response, err 708 709} 710 711// DeleteStoredMessage removes a previously stored message. 712// Note that Mailgun institutes a policy of automatically deleting messages after a set time. 713// Consult the current Mailgun API documentation for more details. 714func (mg *MailgunImpl) DeleteStoredMessage(id string) error { 715 url := generateStoredMessageUrl(mg, messagesEndpoint, id) 716 r := newHTTPRequest(url) 717 r.setClient(mg.Client()) 718 r.setBasicAuth(basicAuthUser, mg.ApiKey()) 719 _, err := makeDeleteRequest(r) 720 return err 721} 722