803 lines
25 KiB
Go
803 lines
25 KiB
Go
|
package mailgun
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"strconv"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// MaxNumberOfRecipients represents the largest batch of recipients that Mailgun can support in a single API call.
|
||
|
// This figure includes To:, Cc:, Bcc:, etc. recipients.
|
||
|
const MaxNumberOfRecipients = 1000
|
||
|
|
||
|
// MaxNumberOfTags represents the maximum number of tags that can be added for a message
|
||
|
const MaxNumberOfTags = 3
|
||
|
|
||
|
// Message structures contain both the message text and the envelop for an e-mail message.
|
||
|
type Message struct {
|
||
|
to []string
|
||
|
tags []string
|
||
|
campaigns []string
|
||
|
dkim bool
|
||
|
deliveryTime time.Time
|
||
|
attachments []string
|
||
|
readerAttachments []ReaderAttachment
|
||
|
inlines []string
|
||
|
readerInlines []ReaderAttachment
|
||
|
bufferAttachments []BufferAttachment
|
||
|
|
||
|
nativeSend bool
|
||
|
testMode bool
|
||
|
tracking bool
|
||
|
trackingClicks bool
|
||
|
trackingOpens bool
|
||
|
headers map[string]string
|
||
|
variables map[string]string
|
||
|
templateVariables map[string]interface{}
|
||
|
recipientVariables map[string]map[string]interface{}
|
||
|
domain string
|
||
|
|
||
|
dkimSet bool
|
||
|
trackingSet bool
|
||
|
trackingClicksSet bool
|
||
|
trackingOpensSet bool
|
||
|
requireTLS bool
|
||
|
skipVerification bool
|
||
|
|
||
|
specific features
|
||
|
mg Mailgun
|
||
|
}
|
||
|
|
||
|
type ReaderAttachment struct {
|
||
|
Filename string
|
||
|
ReadCloser io.ReadCloser
|
||
|
}
|
||
|
|
||
|
type BufferAttachment struct {
|
||
|
Filename string
|
||
|
Buffer []byte
|
||
|
}
|
||
|
|
||
|
// StoredMessage structures contain the (parsed) message content for an email
|
||
|
// sent to a Mailgun account.
|
||
|
//
|
||
|
// The MessageHeaders field is special, in that it's formatted as a slice of pairs.
|
||
|
// Each pair consists of a name [0] and value [1]. Array notation is used instead of a map
|
||
|
// because that's how it's sent over the wire, and it's how encoding/json expects this field
|
||
|
// to be.
|
||
|
type StoredMessage struct {
|
||
|
Recipients string `json:"recipients"`
|
||
|
Sender string `json:"sender"`
|
||
|
From string `json:"from"`
|
||
|
Subject string `json:"subject"`
|
||
|
BodyPlain string `json:"body-plain"`
|
||
|
StrippedText string `json:"stripped-text"`
|
||
|
StrippedSignature string `json:"stripped-signature"`
|
||
|
BodyHtml string `json:"body-html"`
|
||
|
StrippedHtml string `json:"stripped-html"`
|
||
|
Attachments []StoredAttachment `json:"attachments"`
|
||
|
MessageUrl string `json:"message-url"`
|
||
|
ContentIDMap map[string]struct {
|
||
|
Url string `json:"url"`
|
||
|
ContentType string `json:"content-type"`
|
||
|
Name string `json:"name"`
|
||
|
Size int64 `json:"size"`
|
||
|
} `json:"content-id-map"`
|
||
|
MessageHeaders [][]string `json:"message-headers"`
|
||
|
}
|
||
|
|
||
|
// StoredAttachment structures contain information on an attachment associated with a stored message.
|
||
|
type StoredAttachment struct {
|
||
|
Size int `json:"size"`
|
||
|
Url string `json:"url"`
|
||
|
Name string `json:"name"`
|
||
|
ContentType string `json:"content-type"`
|
||
|
}
|
||
|
|
||
|
type StoredMessageRaw struct {
|
||
|
Recipients string `json:"recipients"`
|
||
|
Sender string `json:"sender"`
|
||
|
From string `json:"from"`
|
||
|
Subject string `json:"subject"`
|
||
|
BodyMime string `json:"body-mime"`
|
||
|
}
|
||
|
|
||
|
// plainMessage contains fields relevant to plain API-synthesized messages.
|
||
|
// You're expected to use various setters to set most of these attributes,
|
||
|
// although from, subject, and text are set when the message is created with
|
||
|
// NewMessage.
|
||
|
type plainMessage struct {
|
||
|
from string
|
||
|
cc []string
|
||
|
bcc []string
|
||
|
subject string
|
||
|
text string
|
||
|
html string
|
||
|
template string
|
||
|
}
|
||
|
|
||
|
// mimeMessage contains fields relevant to pre-packaged MIME messages.
|
||
|
type mimeMessage struct {
|
||
|
body io.ReadCloser
|
||
|
}
|
||
|
|
||
|
type sendMessageResponse struct {
|
||
|
Message string `json:"message"`
|
||
|
Id string `json:"id"`
|
||
|
}
|
||
|
|
||
|
// features abstracts the common characteristics between regular and MIME messages.
|
||
|
// addCC, addBCC, recipientCount, and setHTML are invoked via the package-global AddCC, AddBCC,
|
||
|
// RecipientCount, and SetHtml calls, as these functions are ignored for MIME messages.
|
||
|
// Send() invokes addValues to add message-type-specific MIME headers for the API call
|
||
|
// to Mailgun. isValid yeilds true if and only if the message is valid enough for sending
|
||
|
// through the API. Finally, endpoint() tells Send() which endpoint to use to submit the API call.
|
||
|
type features interface {
|
||
|
addCC(string)
|
||
|
addBCC(string)
|
||
|
setHtml(string)
|
||
|
addValues(*formDataPayload)
|
||
|
isValid() bool
|
||
|
endpoint() string
|
||
|
recipientCount() int
|
||
|
setTemplate(string)
|
||
|
}
|
||
|
|
||
|
// NewMessage returns a new e-mail message with the simplest envelop needed to send.
|
||
|
//
|
||
|
// Unlike the global function,
|
||
|
// this method supports arbitrary-sized recipient lists by
|
||
|
// automatically sending mail in batches of up to MaxNumberOfRecipients.
|
||
|
//
|
||
|
// To support batch sending, you don't want to provide a fixed To: header at this point.
|
||
|
// Pass nil as the to parameter to skip adding the To: header at this stage.
|
||
|
// You can do this explicitly, or implicitly, as follows:
|
||
|
//
|
||
|
// // Note absence of To parameter(s)!
|
||
|
// m := mg.NewMessage("me@example.com", "Help save our planet", "Hello world!")
|
||
|
//
|
||
|
// Note that you'll need to invoke the AddRecipientAndVariables or AddRecipient method
|
||
|
// before sending, though.
|
||
|
func (mg *MailgunImpl) NewMessage(from, subject, text string, to ...string) *Message {
|
||
|
return &Message{
|
||
|
specific: &plainMessage{
|
||
|
from: from,
|
||
|
subject: subject,
|
||
|
text: text,
|
||
|
},
|
||
|
to: to,
|
||
|
mg: mg,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewMIMEMessage creates a new MIME message. These messages are largely canned;
|
||
|
// you do not need to invoke setters to set message-related headers.
|
||
|
// However, you do still need to call setters for Mailgun-specific settings.
|
||
|
//
|
||
|
// Unlike the global function,
|
||
|
// this method supports arbitrary-sized recipient lists by
|
||
|
// automatically sending mail in batches of up to MaxNumberOfRecipients.
|
||
|
//
|
||
|
// To support batch sending, you don't want to provide a fixed To: header at this point.
|
||
|
// Pass nil as the to parameter to skip adding the To: header at this stage.
|
||
|
// You can do this explicitly, or implicitly, as follows:
|
||
|
//
|
||
|
// // Note absence of To parameter(s)!
|
||
|
// m := mg.NewMessage("me@example.com", "Help save our planet", "Hello world!")
|
||
|
//
|
||
|
// Note that you'll need to invoke the AddRecipientAndVariables or AddRecipient method
|
||
|
// before sending, though.
|
||
|
func (mg *MailgunImpl) NewMIMEMessage(body io.ReadCloser, to ...string) *Message {
|
||
|
return &Message{
|
||
|
specific: &mimeMessage{
|
||
|
body: body,
|
||
|
},
|
||
|
to: to,
|
||
|
mg: mg,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// AddReaderAttachment arranges to send a file along with the e-mail message.
|
||
|
// File contents are read from a io.ReadCloser.
|
||
|
// The filename parameter is the resulting filename of the attachment.
|
||
|
// The readCloser parameter is the io.ReadCloser which reads the actual bytes to be used
|
||
|
// as the contents of the attached file.
|
||
|
func (m *Message) AddReaderAttachment(filename string, readCloser io.ReadCloser) {
|
||
|
ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser}
|
||
|
m.readerAttachments = append(m.readerAttachments, ra)
|
||
|
}
|
||
|
|
||
|
// AddBufferAttachment arranges to send a file along with the e-mail message.
|
||
|
// File contents are read from the []byte array provided
|
||
|
// The filename parameter is the resulting filename of the attachment.
|
||
|
// The buffer parameter is the []byte array which contains the actual bytes to be used
|
||
|
// as the contents of the attached file.
|
||
|
func (m *Message) AddBufferAttachment(filename string, buffer []byte) {
|
||
|
ba := BufferAttachment{Filename: filename, Buffer: buffer}
|
||
|
m.bufferAttachments = append(m.bufferAttachments, ba)
|
||
|
}
|
||
|
|
||
|
// AddAttachment arranges to send a file from the filesystem along with the e-mail message.
|
||
|
// The attachment parameter is a filename, which must refer to a file which actually resides
|
||
|
// in the local filesystem.
|
||
|
func (m *Message) AddAttachment(attachment string) {
|
||
|
m.attachments = append(m.attachments, attachment)
|
||
|
}
|
||
|
|
||
|
// AddReaderInline arranges to send a file along with the e-mail message.
|
||
|
// File contents are read from a io.ReadCloser.
|
||
|
// The filename parameter is the resulting filename of the attachment.
|
||
|
// The readCloser parameter is the io.ReadCloser which reads the actual bytes to be used
|
||
|
// as the contents of the attached file.
|
||
|
func (m *Message) AddReaderInline(filename string, readCloser io.ReadCloser) {
|
||
|
ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser}
|
||
|
m.readerInlines = append(m.readerInlines, ra)
|
||
|
}
|
||
|
|
||
|
// AddInline arranges to send a file along with the e-mail message, but does so
|
||
|
// in a way that its data remains "inline" with the rest of the message. This
|
||
|
// can be used to send image or font data along with an HTML-encoded message body.
|
||
|
// The attachment parameter is a filename, which must refer to a file which actually resides
|
||
|
// in the local filesystem.
|
||
|
func (m *Message) AddInline(inline string) {
|
||
|
m.inlines = append(m.inlines, inline)
|
||
|
}
|
||
|
|
||
|
// AddRecipient appends a receiver to the To: header of a message.
|
||
|
// It will return an error if the limit of recipients have been exceeded for this message
|
||
|
func (m *Message) AddRecipient(recipient string) error {
|
||
|
return m.AddRecipientAndVariables(recipient, nil)
|
||
|
}
|
||
|
|
||
|
// AddRecipientAndVariables appends a receiver to the To: header of a message,
|
||
|
// and as well attaches a set of variables relevant for this recipient.
|
||
|
// It will return an error if the limit of recipients have been exceeded for this message
|
||
|
func (m *Message) AddRecipientAndVariables(r string, vars map[string]interface{}) error {
|
||
|
if m.RecipientCount() >= MaxNumberOfRecipients {
|
||
|
return fmt.Errorf("recipient limit exceeded (max %d)", MaxNumberOfRecipients)
|
||
|
}
|
||
|
m.to = append(m.to, r)
|
||
|
if vars != nil {
|
||
|
if m.recipientVariables == nil {
|
||
|
m.recipientVariables = make(map[string]map[string]interface{})
|
||
|
}
|
||
|
m.recipientVariables[r] = vars
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// RecipientCount returns the total number of recipients for the message.
|
||
|
// This includes To:, Cc:, and Bcc: fields.
|
||
|
//
|
||
|
// NOTE: At present, this method is reliable only for non-MIME messages, as the
|
||
|
// Bcc: and Cc: fields are easily accessible.
|
||
|
// For MIME messages, only the To: field is considered.
|
||
|
// A fix for this issue is planned for a future release.
|
||
|
// For now, MIME messages are always assumed to have 10 recipients between Cc: and Bcc: fields.
|
||
|
// If your MIME messages have more than 10 non-To: field recipients,
|
||
|
// you may find that some recipients will not receive your e-mail.
|
||
|
// It's perfectly OK, of course, for a MIME message to not have any Cc: or Bcc: recipients.
|
||
|
func (m *Message) RecipientCount() int {
|
||
|
return len(m.to) + m.specific.recipientCount()
|
||
|
}
|
||
|
|
||
|
func (pm *plainMessage) recipientCount() int {
|
||
|
return len(pm.bcc) + len(pm.cc)
|
||
|
}
|
||
|
|
||
|
func (mm *mimeMessage) recipientCount() int {
|
||
|
return 10
|
||
|
}
|
||
|
|
||
|
func (m *Message) send(ctx context.Context) (string, string, error) {
|
||
|
return m.mg.Send(ctx, m)
|
||
|
}
|
||
|
|
||
|
// SetReplyTo sets the receiver who should receive replies
|
||
|
func (m *Message) SetReplyTo(recipient string) {
|
||
|
m.AddHeader("Reply-To", recipient)
|
||
|
}
|
||
|
|
||
|
// AddCC appends a receiver to the carbon-copy header of a message.
|
||
|
func (m *Message) AddCC(recipient string) {
|
||
|
m.specific.addCC(recipient)
|
||
|
}
|
||
|
|
||
|
func (pm *plainMessage) addCC(r string) {
|
||
|
pm.cc = append(pm.cc, r)
|
||
|
}
|
||
|
|
||
|
func (mm *mimeMessage) addCC(_ string) {}
|
||
|
|
||
|
// AddBCC appends a receiver to the blind-carbon-copy header of a message.
|
||
|
func (m *Message) AddBCC(recipient string) {
|
||
|
m.specific.addBCC(recipient)
|
||
|
}
|
||
|
|
||
|
func (pm *plainMessage) addBCC(r string) {
|
||
|
pm.bcc = append(pm.bcc, r)
|
||
|
}
|
||
|
|
||
|
func (mm *mimeMessage) addBCC(_ string) {}
|
||
|
|
||
|
// SetHtml is a helper. If you're sending a message that isn't already MIME encoded, SetHtml() will arrange to bundle
|
||
|
// an HTML representation of your message in addition to your plain-text body.
|
||
|
func (m *Message) SetHtml(html string) {
|
||
|
m.specific.setHtml(html)
|
||
|
}
|
||
|
|
||
|
func (pm *plainMessage) setHtml(h string) {
|
||
|
pm.html = h
|
||
|
}
|
||
|
|
||
|
func (mm *mimeMessage) setHtml(_ string) {}
|
||
|
|
||
|
// AddTag attaches tags to the message. Tags are useful for metrics gathering and event tracking purposes.
|
||
|
// Refer to the Mailgun documentation for further details.
|
||
|
func (m *Message) AddTag(tag ...string) error {
|
||
|
if len(m.tags) >= MaxNumberOfTags {
|
||
|
return fmt.Errorf("cannot add any new tags. Message tag limit (%d) reached", MaxNumberOfTags)
|
||
|
}
|
||
|
|
||
|
m.tags = append(m.tags, tag...)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// SetTemplate sets the name of a template stored via the template API.
|
||
|
// See https://documentation.mailgun.com/en/latest/user_manual.html#templating
|
||
|
func (m *Message) SetTemplate(t string) {
|
||
|
m.specific.setTemplate(t)
|
||
|
}
|
||
|
|
||
|
func (pm *plainMessage) setTemplate(t string) {
|
||
|
pm.template = t
|
||
|
}
|
||
|
|
||
|
func (mm *mimeMessage) setTemplate(t string) {}
|
||
|
|
||
|
// AddCampaign is no longer supported and is deprecated for new software.
|
||
|
func (m *Message) AddCampaign(campaign string) {
|
||
|
m.campaigns = append(m.campaigns, campaign)
|
||
|
}
|
||
|
|
||
|
// SetDKIM arranges to send the o:dkim header with the message, and sets its value accordingly.
|
||
|
// Refer to the Mailgun documentation for more information.
|
||
|
func (m *Message) SetDKIM(dkim bool) {
|
||
|
m.dkim = dkim
|
||
|
m.dkimSet = true
|
||
|
}
|
||
|
|
||
|
// EnableNativeSend allows the return path to match the address in the Message.Headers.From:
|
||
|
// field when sending from Mailgun rather than the usual bounce+ address in the return path.
|
||
|
func (m *Message) EnableNativeSend() {
|
||
|
m.nativeSend = true
|
||
|
}
|
||
|
|
||
|
// EnableTestMode allows submittal of a message, such that it will be discarded by Mailgun.
|
||
|
// This facilitates testing client-side software without actually consuming e-mail resources.
|
||
|
func (m *Message) EnableTestMode() {
|
||
|
m.testMode = true
|
||
|
}
|
||
|
|
||
|
// SetDeliveryTime schedules the message for transmission at the indicated time.
|
||
|
// Pass nil to remove any installed schedule.
|
||
|
// Refer to the Mailgun documentation for more information.
|
||
|
func (m *Message) SetDeliveryTime(dt time.Time) {
|
||
|
m.deliveryTime = dt
|
||
|
}
|
||
|
|
||
|
// SetTracking sets the o:tracking message parameter to adjust, on a message-by-message basis,
|
||
|
// whether or not Mailgun will rewrite URLs to facilitate event tracking.
|
||
|
// Events tracked includes opens, clicks, unsubscribes, etc.
|
||
|
// Note: simply calling this method ensures that the o:tracking header is passed in with the message.
|
||
|
// Its yes/no setting is determined by the call's parameter.
|
||
|
// Note that this header is not passed on to the final recipient(s).
|
||
|
// Refer to the Mailgun documentation for more information.
|
||
|
func (m *Message) SetTracking(tracking bool) {
|
||
|
m.tracking = tracking
|
||
|
m.trackingSet = true
|
||
|
}
|
||
|
|
||
|
// SetTrackingClicks information is found in the Mailgun documentation.
|
||
|
func (m *Message) SetTrackingClicks(trackingClicks bool) {
|
||
|
m.trackingClicks = trackingClicks
|
||
|
m.trackingClicksSet = true
|
||
|
}
|
||
|
|
||
|
// SetRequireTLS information is found in the Mailgun documentation.
|
||
|
func (m *Message) SetRequireTLS(b bool) {
|
||
|
m.requireTLS = b
|
||
|
}
|
||
|
|
||
|
// SetSkipVerification information is found in the Mailgun documentation.
|
||
|
func (m *Message) SetSkipVerification(b bool) {
|
||
|
m.skipVerification = b
|
||
|
}
|
||
|
|
||
|
//SetTrackingOpens information is found in the Mailgun documentation.
|
||
|
func (m *Message) SetTrackingOpens(trackingOpens bool) {
|
||
|
m.trackingOpens = trackingOpens
|
||
|
m.trackingOpensSet = true
|
||
|
}
|
||
|
|
||
|
// AddHeader allows you to send custom MIME headers with the message.
|
||
|
func (m *Message) AddHeader(header, value string) {
|
||
|
if m.headers == nil {
|
||
|
m.headers = make(map[string]string)
|
||
|
}
|
||
|
m.headers[header] = value
|
||
|
}
|
||
|
|
||
|
// AddVariable lets you associate a set of variables with messages you send,
|
||
|
// which Mailgun can use to, in essence, complete form-mail.
|
||
|
// Refer to the Mailgun documentation for more information.
|
||
|
func (m *Message) AddVariable(variable string, value interface{}) error {
|
||
|
if m.variables == nil {
|
||
|
m.variables = make(map[string]string)
|
||
|
}
|
||
|
|
||
|
j, err := json.Marshal(value)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
encoded := string(j)
|
||
|
v, err := strconv.Unquote(encoded)
|
||
|
if err != nil {
|
||
|
v = encoded
|
||
|
}
|
||
|
|
||
|
m.variables[variable] = v
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// AddTemplateVariable adds a template variable to the map of template variables, replacing the variable if it is already there.
|
||
|
// This is used for server-side message templates and can nest arbitrary values. At send time, the resulting map will be converted into
|
||
|
// a JSON string and sent as a header in the X-Mailgun-Variables header.
|
||
|
func (m *Message) AddTemplateVariable(variable string, value interface{}) error {
|
||
|
if m.templateVariables == nil {
|
||
|
m.templateVariables = make(map[string]interface{})
|
||
|
}
|
||
|
m.templateVariables[variable] = value
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// AddDomain allows you to use a separate domain for the type of messages you are sending.
|
||
|
func (m *Message) AddDomain(domain string) {
|
||
|
m.domain = domain
|
||
|
}
|
||
|
|
||
|
// GetHeaders retrieves the http headers associated with this message
|
||
|
func (m *Message) GetHeaders() map[string]string {
|
||
|
return m.headers
|
||
|
}
|
||
|
|
||
|
// ErrInvalidMessage is returned by `Send()` when the `mailgun.Message` struct is incomplete
|
||
|
var ErrInvalidMessage = errors.New("message not valid")
|
||
|
|
||
|
// Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery.
|
||
|
// It returns the Mailgun server response, which consists of two components:
|
||
|
// a human-readable status message, and a message ID. The status and message ID are set only
|
||
|
// if no error occurred.
|
||
|
func (mg *MailgunImpl) Send(ctx context.Context, message *Message) (mes string, id string, err error) {
|
||
|
if mg.domain == "" {
|
||
|
err = errors.New("you must provide a valid domain before calling Send()")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if mg.apiKey == "" {
|
||
|
err = errors.New("you must provide a valid api-key before calling Send()")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if !isValid(message) {
|
||
|
err = ErrInvalidMessage
|
||
|
return
|
||
|
}
|
||
|
payload := newFormDataPayload()
|
||
|
|
||
|
message.specific.addValues(payload)
|
||
|
for _, to := range message.to {
|
||
|
payload.addValue("to", to)
|
||
|
}
|
||
|
for _, tag := range message.tags {
|
||
|
payload.addValue("o:tag", tag)
|
||
|
}
|
||
|
for _, campaign := range message.campaigns {
|
||
|
payload.addValue("o:campaign", campaign)
|
||
|
}
|
||
|
if message.dkimSet {
|
||
|
payload.addValue("o:dkim", yesNo(message.dkim))
|
||
|
}
|
||
|
if !message.deliveryTime.IsZero() {
|
||
|
payload.addValue("o:deliverytime", formatMailgunTime(message.deliveryTime))
|
||
|
}
|
||
|
if message.nativeSend {
|
||
|
payload.addValue("o:native-send", "yes")
|
||
|
}
|
||
|
if message.testMode {
|
||
|
payload.addValue("o:testmode", "yes")
|
||
|
}
|
||
|
if message.trackingSet {
|
||
|
payload.addValue("o:tracking", yesNo(message.tracking))
|
||
|
}
|
||
|
if message.trackingClicksSet {
|
||
|
payload.addValue("o:tracking-clicks", yesNo(message.trackingClicks))
|
||
|
}
|
||
|
if message.trackingOpensSet {
|
||
|
payload.addValue("o:tracking-opens", yesNo(message.trackingOpens))
|
||
|
}
|
||
|
if message.requireTLS {
|
||
|
payload.addValue("o:require-tls", trueFalse(message.requireTLS))
|
||
|
}
|
||
|
if message.skipVerification {
|
||
|
payload.addValue("o:skip-verification", trueFalse(message.skipVerification))
|
||
|
}
|
||
|
if message.headers != nil {
|
||
|
for header, value := range message.headers {
|
||
|
payload.addValue("h:"+header, value)
|
||
|
}
|
||
|
}
|
||
|
if message.variables != nil {
|
||
|
for variable, value := range message.variables {
|
||
|
payload.addValue("v:"+variable, value)
|
||
|
}
|
||
|
}
|
||
|
if message.templateVariables != nil {
|
||
|
variableString, err := json.Marshal(message.templateVariables)
|
||
|
if err == nil {
|
||
|
// the map was marshalled as json so add it
|
||
|
payload.addValue("h:X-Mailgun-Variables", string(variableString))
|
||
|
}
|
||
|
}
|
||
|
if message.recipientVariables != nil {
|
||
|
j, err := json.Marshal(message.recipientVariables)
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
payload.addValue("recipient-variables", string(j))
|
||
|
}
|
||
|
if message.attachments != nil {
|
||
|
for _, attachment := range message.attachments {
|
||
|
payload.addFile("attachment", attachment)
|
||
|
}
|
||
|
}
|
||
|
if message.readerAttachments != nil {
|
||
|
for _, readerAttachment := range message.readerAttachments {
|
||
|
payload.addReadCloser("attachment", readerAttachment.Filename, readerAttachment.ReadCloser)
|
||
|
}
|
||
|
}
|
||
|
if message.bufferAttachments != nil {
|
||
|
for _, bufferAttachment := range message.bufferAttachments {
|
||
|
payload.addBuffer("attachment", bufferAttachment.Filename, bufferAttachment.Buffer)
|
||
|
}
|
||
|
}
|
||
|
if message.inlines != nil {
|
||
|
for _, inline := range message.inlines {
|
||
|
payload.addFile("inline", inline)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if message.readerInlines != nil {
|
||
|
for _, readerAttachment := range message.readerInlines {
|
||
|
payload.addReadCloser("inline", readerAttachment.Filename, readerAttachment.ReadCloser)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if message.domain == "" {
|
||
|
message.domain = mg.Domain()
|
||
|
}
|
||
|
|
||
|
r := newHTTPRequest(generateApiUrlWithDomain(mg, message.specific.endpoint(), message.domain))
|
||
|
r.setClient(mg.Client())
|
||
|
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||
|
|
||
|
var response sendMessageResponse
|
||
|
err = postResponseFromJSON(ctx, r, payload, &response)
|
||
|
if err == nil {
|
||
|
mes = response.Message
|
||
|
id = response.Id
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (pm *plainMessage) addValues(p *formDataPayload) {
|
||
|
p.addValue("from", pm.from)
|
||
|
p.addValue("subject", pm.subject)
|
||
|
p.addValue("text", pm.text)
|
||
|
for _, cc := range pm.cc {
|
||
|
p.addValue("cc", cc)
|
||
|
}
|
||
|
for _, bcc := range pm.bcc {
|
||
|
p.addValue("bcc", bcc)
|
||
|
}
|
||
|
if pm.html != "" {
|
||
|
p.addValue("html", pm.html)
|
||
|
}
|
||
|
if pm.template != "" {
|
||
|
p.addValue("template", pm.template)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (mm *mimeMessage) addValues(p *formDataPayload) {
|
||
|
p.addReadCloser("message", "message.mime", mm.body)
|
||
|
}
|
||
|
|
||
|
func (pm *plainMessage) endpoint() string {
|
||
|
return messagesEndpoint
|
||
|
}
|
||
|
|
||
|
func (mm *mimeMessage) endpoint() string {
|
||
|
return mimeMessagesEndpoint
|
||
|
}
|
||
|
|
||
|
// yesNo translates a true/false boolean value into a yes/no setting suitable for the Mailgun API.
|
||
|
func yesNo(b bool) string {
|
||
|
if b {
|
||
|
return "yes"
|
||
|
}
|
||
|
return "no"
|
||
|
}
|
||
|
|
||
|
func trueFalse(b bool) string {
|
||
|
if b {
|
||
|
return "true"
|
||
|
}
|
||
|
return "false"
|
||
|
}
|
||
|
|
||
|
// isValid returns true if, and only if,
|
||
|
// a Message instance is sufficiently initialized to send via the Mailgun interface.
|
||
|
func isValid(m *Message) bool {
|
||
|
if m == nil {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if !m.specific.isValid() {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if m.RecipientCount() == 0 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if !validateStringList(m.tags, false) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if !validateStringList(m.campaigns, false) || len(m.campaigns) > 3 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (pm *plainMessage) isValid() bool {
|
||
|
if pm.from == "" {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if !validateStringList(pm.cc, false) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if !validateStringList(pm.bcc, false) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if pm.template != "" {
|
||
|
// pm.text or pm.html not needed if template is supplied
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
if pm.text == "" && pm.html == "" {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (mm *mimeMessage) isValid() bool {
|
||
|
return mm.body != nil
|
||
|
}
|
||
|
|
||
|
// validateStringList returns true if, and only if,
|
||
|
// a slice of strings exists AND all of its elements exist,
|
||
|
// OR if the slice doesn't exist AND it's not required to exist.
|
||
|
// The requireOne parameter indicates whether the list is required to exist.
|
||
|
func validateStringList(list []string, requireOne bool) bool {
|
||
|
hasOne := false
|
||
|
|
||
|
if list == nil {
|
||
|
return !requireOne
|
||
|
} else {
|
||
|
for _, a := range list {
|
||
|
if a == "" {
|
||
|
return false
|
||
|
} else {
|
||
|
hasOne = hasOne || true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return hasOne
|
||
|
}
|
||
|
|
||
|
// GetStoredMessage retrieves information about a received e-mail message.
|
||
|
// This provides visibility into, e.g., replies to a message sent to a mailing list.
|
||
|
func (mg *MailgunImpl) GetStoredMessage(ctx context.Context, url string) (StoredMessage, error) {
|
||
|
r := newHTTPRequest(url)
|
||
|
r.setClient(mg.Client())
|
||
|
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||
|
|
||
|
var response StoredMessage
|
||
|
err := getResponseFromJSON(ctx, r, &response)
|
||
|
return response, err
|
||
|
}
|
||
|
|
||
|
// Given a storage id resend the stored message to the specified recipients
|
||
|
func (mg *MailgunImpl) ReSend(ctx context.Context, url string, recipients ...string) (string, string, error) {
|
||
|
r := newHTTPRequest(url)
|
||
|
r.setClient(mg.Client())
|
||
|
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||
|
|
||
|
payload := newFormDataPayload()
|
||
|
|
||
|
if len(recipients) == 0 {
|
||
|
return "", "", errors.New("must provide at least one recipient")
|
||
|
}
|
||
|
|
||
|
for _, to := range recipients {
|
||
|
payload.addValue("to", to)
|
||
|
}
|
||
|
|
||
|
var resp sendMessageResponse
|
||
|
err := postResponseFromJSON(ctx, r, payload, &resp)
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
return resp.Message, resp.Id, nil
|
||
|
|
||
|
}
|
||
|
|
||
|
// GetStoredMessageRaw retrieves the raw MIME body of a received e-mail message.
|
||
|
// Compared to GetStoredMessage, it gives access to the unparsed MIME body, and
|
||
|
// thus delegates to the caller the required parsing.
|
||
|
func (mg *MailgunImpl) GetStoredMessageRaw(ctx context.Context, url string) (StoredMessageRaw, error) {
|
||
|
r := newHTTPRequest(url)
|
||
|
r.setClient(mg.Client())
|
||
|
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||
|
r.addHeader("Accept", "message/rfc2822")
|
||
|
|
||
|
var response StoredMessageRaw
|
||
|
err := getResponseFromJSON(ctx, r, &response)
|
||
|
return response, err
|
||
|
}
|
||
|
|
||
|
// Deprecated: Use GetStoreMessage() instead
|
||
|
func (mg *MailgunImpl) GetStoredMessageForURL(ctx context.Context, url string) (StoredMessage, error) {
|
||
|
return mg.GetStoredMessage(ctx, url)
|
||
|
}
|
||
|
|
||
|
// Deprecated: Use GetStoreMessageRaw() instead
|
||
|
func (mg *MailgunImpl) GetStoredMessageRawForURL(ctx context.Context, url string) (StoredMessageRaw, error) {
|
||
|
return mg.GetStoredMessageRaw(ctx, url)
|
||
|
}
|
||
|
|
||
|
// GetStoredAttachment retrieves the raw MIME body of a received e-mail message attachment.
|
||
|
func (mg *MailgunImpl) GetStoredAttachment(ctx context.Context, url string) ([]byte, error) {
|
||
|
r := newHTTPRequest(url)
|
||
|
r.setClient(mg.Client())
|
||
|
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||
|
r.addHeader("Accept", "message/rfc2822")
|
||
|
|
||
|
response, err := makeGetRequest(ctx, r)
|
||
|
|
||
|
return response.Data, err
|
||
|
}
|