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