github.com/mailgun/mailgun-go/v3@v3.6.4/messages.go (about)

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