From a2a4cfd168b37b591aa7a266a7f70f1d6952fb01 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sun, 15 Dec 2024 20:18:29 +0200 Subject: [PATCH] DE-1388 (mg *MailgunImpl) Send should accept interface, not struct (#368) https://bryanftan.medium.com/accept-interfaces-return-structs-in-go-d4cab29a301b --- README.md | 6 +- attachments_test.go | 2 +- events_test.go | 2 +- examples/examples.go | 6 +- examples_test.go | 2 +- mailgun.go | 5 +- messages.go | 294 +++++++++++++++++-------------------------- messages_test.go | 40 +++--- messages_types_v5.go | 3 +- messages_v5.go | 5 +- storage_test.go | 2 +- tags_test.go | 2 +- 12 files changed, 156 insertions(+), 213 deletions(-) diff --git a/README.md b/README.md index f78c9fe3..f5407204 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ func main() { recipient := "recipient@example.com" // The message object allows you to add attachments and Bcc recipients - message := mailgun.NewMessage(sender, subject, body, recipient) + message := mailgun.NewMessage(yourDomain, sender, subject, body, recipient) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() @@ -290,7 +290,7 @@ func main() { subject := "HTML email!" recipient := "recipient@example.com" - message := mailgun.NewMessage(sender, subject, "", recipient) + message := mailgun.NewMessage(yourDomain, sender, subject, "", recipient) body := ` @@ -351,7 +351,7 @@ func main() { recipient := "recipient@example.com" // The message object allows you to add attachments and Bcc recipients - message := mailgun.NewMessage(sender, subject, body, recipient) + message := mailgun.NewMessage(yourDomain, sender, subject, body, recipient) message.SetTemplate("passwordReset") err := message.AddTemplateVariable("passwordResetLink", "some link to your site unique to your user") if err != nil { diff --git a/attachments_test.go b/attachments_test.go index 9812eb85..e307b088 100644 --- a/attachments_test.go +++ b/attachments_test.go @@ -31,7 +31,7 @@ func TestMultipleAttachments(t *testing.T) { var ctx = context.Background() - m := mailgun.NewMessage("root@"+testDomain, "Subject", "Text Body", "attachment@"+testDomain) + m := mailgun.NewMessage(testDomain, "root@"+testDomain, "Subject", "Text Body", "attachment@"+testDomain) // Add 2 attachments m.AddAttachment(createAttachment(t)) diff --git a/events_test.go b/events_test.go index 2621ff67..a8eeb3e4 100644 --- a/events_test.go +++ b/events_test.go @@ -87,7 +87,7 @@ func TestEventPoller(t *testing.T) { }() // Send an email - m := mailgun.NewMessage("root@"+testDomain, "Subject", "Text Body", "user@"+testDomain) + m := mailgun.NewMessage(testDomain, "root@"+testDomain, "Subject", "Text Body", "user@"+testDomain) msg, id, err := mg.Send(ctx, m) require.NoError(t, err) diff --git a/examples/examples.go b/examples/examples.go index 8c43abef..da15c7e6 100644 --- a/examples/examples.go +++ b/examples/examples.go @@ -779,7 +779,7 @@ func SendMimeMessage(domain, apiKey string) (string, error) { return "", err } - m := mailgun.NewMIMEMessage(mimeMsgReader, "bar@example.com") + m := mailgun.NewMIMEMessage(domain, mimeMsgReader, "bar@example.com") ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() @@ -824,6 +824,7 @@ func SendSimpleMessage(domain, apiKey string) (string, error) { func SendTaggedMessage(domain, apiKey string) (string, error) { mg := mailgun.NewMailgun(domain, apiKey) m := mailgun.NewMessage( + domain, "Excited User ", "Hello", "Testing some Mailgun awesomeness!", @@ -845,6 +846,7 @@ func SendTaggedMessage(domain, apiKey string) (string, error) { func SendTemplateMessage(domain, apiKey string) (string, error) { mg := mailgun.NewMailgun(domain, apiKey) m := mailgun.NewMessage( + domain, "Excited User ", "Hey %recipient.first%", "If you wish to unsubscribe, click http://mailgun/unsubscribe/%recipient.id%", @@ -939,7 +941,7 @@ func SendMessageWithTemplate(domain, apiKey string) error { time.Sleep(time.Second * 1) // Create a new message with template - m := mailgun.NewMessage("Excited User ", "Template example", "") + m := mailgun.NewMessage(domain, "Excited User ", "Template example", "") m.SetTemplate("my-template") // Add recipients diff --git a/examples_test.go b/examples_test.go index 216b8c09..7018765c 100644 --- a/examples_test.go +++ b/examples_test.go @@ -112,7 +112,7 @@ Testing some Mailgun MIME awesomeness! defer cancel() mg := mailgun.NewMailgun("example.com", "my_api_key") - m := mailgun.NewMIMEMessage(io.NopCloser(strings.NewReader(exampleMime)), "bargle.garf@example.com") + m := mailgun.NewMIMEMessage("example.com", io.NopCloser(strings.NewReader(exampleMime)), "bargle.garf@example.com") _, id, err := mg.Send(ctx, m) if err != nil { log.Fatal(err) diff --git a/mailgun.go b/mailgun.go index a11927ce..2b8bf7ff 100644 --- a/mailgun.go +++ b/mailgun.go @@ -139,9 +139,8 @@ type Mailgun interface { AddOverrideHeader(k string, v string) GetCurlOutput() string - // Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery. - // TODO(v5): switch m to SendableMessage interface - Send(ctx context.Context, m *Message) (mes string, id string, err error) + // Send attempts to queue a message (see CommonMessage, NewMessage, and its methods) for delivery. + Send(ctx context.Context, m SendableMessage) (mes string, id string, err error) ReSend(ctx context.Context, id string, recipients ...string) (string, string, error) ListBounces(opts *ListOptions) *BouncesIterator diff --git a/messages.go b/messages.go index c68bcf1a..918e31bd 100644 --- a/messages.go +++ b/messages.go @@ -19,21 +19,19 @@ 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 envelope for an e-mail message. -// TODO(v5): rename to CommonMessage and remove Specific -type Message struct { - to []string - tags []string - campaigns []string - dkim *bool - deliveryTime time.Time - stoPeriod string - attachments []string - readerAttachments []ReaderAttachment - inlines []string - readerInlines []ReaderAttachment - bufferAttachments []BufferAttachment - +// CommonMessage structures contain both the message text and the envelope for an e-mail message. +type CommonMessage struct { + domain string + to []string + tags []string + dkim *bool + deliveryTime time.Time + stoPeriod string + attachments []string + readerAttachments []ReaderAttachment + inlines []string + readerInlines []ReaderAttachment + bufferAttachments []BufferAttachment nativeSend bool testMode bool tracking *bool @@ -43,14 +41,10 @@ type Message struct { variables map[string]string templateVariables map[string]any recipientVariables map[string]map[string]any - domain string templateVersionTag string templateRenderText bool - - requireTLS bool - skipVerification bool - - Specific + requireTLS bool + skipVerification bool } type ReaderAttachment struct { @@ -67,8 +61,9 @@ type BufferAttachment struct { // 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. -// TODO(v5): embed CommonMessage type PlainMessage struct { + CommonMessage + from string cc []string bcc []string @@ -112,8 +107,9 @@ func (m *PlainMessage) Template() string { } // MimeMessage contains fields relevant to pre-packaged MIME messages. -// TODO(v5): embed CommonMessage type MimeMessage struct { + CommonMessage + body io.ReadCloser } @@ -122,14 +118,14 @@ type sendMessageResponse struct { Id string `json:"id"` } -// TrackingOptions contains fields relevant to trackings. +// TrackingOptions contains fields relevant to tracking. type TrackingOptions struct { Tracking bool TrackingClicks string TrackingOpens bool } -// Specific abstracts the common characteristics between regular and MIME messages. +// Specific abstracts the common characteristics between plain text and MIME messages. type Specific interface { // AddCC appends a receiver to the carbon-copy header of a message. AddCC(string) @@ -142,7 +138,7 @@ type Specific interface { SetHTML(string) // SetAmpHTML If you're sending a message that isn't already MIME encoded, it will arrange to bundle - // an AMP-For-Email representation of your message in addition to your html & plain-text content. + // an AMP-For-Email representation of your message in addition to your HTML & plain-text content. SetAmpHTML(string) // AddValues invoked by Send() to add message-type-specific MIME headers for the API call @@ -172,11 +168,6 @@ type Specific interface { // SetTemplate sets the name of a template stored via the template API. // See https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/#templates SetTemplate(string) - - // // TODO(v5): - // // 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 - // AddRecipient(recipient string) error } // NewMessage returns a new e-mail message with the simplest envelop needed to send. @@ -188,19 +179,20 @@ type Specific interface { // You can do this explicitly, or implicitly, as follows: // // // Note absence of `to` parameter(s)! -// m := NewMessage("me@example.com", "Help save our planet", "Hello world!") +// m := NewMessage("example.com", "me@example.com", "Help save our planet", "Hello world!") // // Note that you'll need to invoke the AddRecipientAndVariables or AddRecipient method // before sending, though. -// TODO(v5): should return PlainMessage -func NewMessage(from, subject, text string, to ...string) *Message { - return &Message{ - Specific: &PlainMessage{ - from: from, - subject: subject, - text: text, +func NewMessage(domain, from, subject, text string, to ...string) *PlainMessage { + return &PlainMessage{ + CommonMessage: CommonMessage{ + domain: domain, + to: to, }, - to: to, + + from: from, + subject: subject, + text: text, } } @@ -215,122 +207,118 @@ func NewMessage(from, subject, text string, to ...string) *Message { // You can do this explicitly, or implicitly, as follows: // // // Note absence of `to` parameter(s)! -// m := NewMIMEMessage(body) +// m := NewMIMEMessage(domain, body) // // Note that you'll need to invoke the AddRecipientAndVariables or AddRecipient method // before sending, though. -// TODO(v5): should return MimeMessage -func NewMIMEMessage(body io.ReadCloser, to ...string) *Message { - return &Message{ - Specific: &MimeMessage{ - body: body, +func NewMIMEMessage(domain string, body io.ReadCloser, to ...string) *MimeMessage { + return &MimeMessage{ + CommonMessage: CommonMessage{ + domain: domain, + to: to, }, - to: to, + body: body, } } -func (m *Message) Domain() string { +func (m *CommonMessage) Domain() string { return m.domain } -func (m *Message) To() []string { +func (m *CommonMessage) To() []string { return m.to } -func (m *Message) Tags() []string { +func (m *CommonMessage) Tags() []string { return m.tags } -func (m *Message) Campaigns() []string { - return m.campaigns -} - -func (m *Message) DKIM() *bool { +func (m *CommonMessage) DKIM() *bool { return m.dkim } -func (m *Message) DeliveryTime() time.Time { +func (m *CommonMessage) DeliveryTime() time.Time { return m.deliveryTime } -func (m *Message) STOPeriod() string { +func (m *CommonMessage) STOPeriod() string { return m.stoPeriod } -func (m *Message) Attachments() []string { +func (m *CommonMessage) Attachments() []string { return m.attachments } -func (m *Message) ReaderAttachments() []ReaderAttachment { +func (m *CommonMessage) ReaderAttachments() []ReaderAttachment { return m.readerAttachments } -func (m *Message) Inlines() []string { +func (m *CommonMessage) Inlines() []string { return m.inlines } -func (m *Message) ReaderInlines() []ReaderAttachment { +func (m *CommonMessage) ReaderInlines() []ReaderAttachment { return m.readerInlines } -func (m *Message) BufferAttachments() []BufferAttachment { +func (m *CommonMessage) BufferAttachments() []BufferAttachment { return m.bufferAttachments } -func (m *Message) NativeSend() bool { +func (m *CommonMessage) NativeSend() bool { return m.nativeSend } -func (m *Message) TestMode() bool { +func (m *CommonMessage) TestMode() bool { return m.testMode } -func (m *Message) Tracking() *bool { +func (m *CommonMessage) Tracking() *bool { return m.tracking } -func (m *Message) TrackingClicks() *string { +func (m *CommonMessage) TrackingClicks() *string { return m.trackingClicks } -func (m *Message) TrackingOpens() *bool { +func (m *CommonMessage) TrackingOpens() *bool { return m.trackingOpens } -func (m *Message) Variables() map[string]string { +func (m *CommonMessage) Variables() map[string]string { return m.variables } -func (m *Message) TemplateVariables() map[string]any { +func (m *CommonMessage) TemplateVariables() map[string]any { return m.templateVariables } -func (m *Message) RecipientVariables() map[string]map[string]any { +func (m *CommonMessage) RecipientVariables() map[string]map[string]any { return m.recipientVariables } -func (m *Message) TemplateVersionTag() string { +func (m *CommonMessage) TemplateVersionTag() string { return m.templateVersionTag } -func (m *Message) TemplateRenderText() bool { +func (m *CommonMessage) TemplateRenderText() bool { return m.templateRenderText } -func (m *Message) RequireTLS() bool { +func (m *CommonMessage) RequireTLS() bool { return m.requireTLS } -func (m *Message) SkipVerification() bool { +func (m *CommonMessage) SkipVerification() bool { return m.skipVerification } // AddReaderAttachment arranges to send a file along with the e-mail message. -// File contents are read from a io.ReadCloser. +// File contents are read from an 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 +// The readCloser parameter is the io.ReadCloser that reads the actual bytes to be used // as the contents of the attached file. -func (m *Message) AddReaderAttachment(filename string, readCloser io.ReadCloser) { +func (m *CommonMessage) AddReaderAttachment(filename string, readCloser io.ReadCloser) { ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser} m.readerAttachments = append(m.readerAttachments, ra) } @@ -340,7 +328,7 @@ func (m *Message) AddReaderAttachment(filename string, readCloser io.ReadCloser) // 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) { +func (m *CommonMessage) AddBufferAttachment(filename string, buffer []byte) { ba := BufferAttachment{Filename: filename, Buffer: buffer} m.bufferAttachments = append(m.bufferAttachments, ba) } @@ -348,16 +336,16 @@ func (m *Message) AddBufferAttachment(filename string, buffer []byte) { // 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) { +func (m *CommonMessage) 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. +// File contents are read from an 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 +// The readCloser parameter is the io.ReadCloser that reads the actual bytes to be used // as the contents of the attached file. -func (m *Message) AddReaderInline(filename string, readCloser io.ReadCloser) { +func (m *CommonMessage) AddReaderInline(filename string, readCloser io.ReadCloser) { ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser} m.readerInlines = append(m.readerInlines, ra) } @@ -367,20 +355,20 @@ func (m *Message) AddReaderInline(filename string, readCloser io.ReadCloser) { // 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) { +func (m *CommonMessage) 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 { +// It will return an error if the limit of recipients has been exceeded for this message +func (m *PlainMessage) 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]any) error { +// It will return an error if the limit of recipients has been exceeded for this message +func (m *PlainMessage) AddRecipientAndVariables(r string, vars map[string]any) error { if m.RecipientCount() >= MaxNumberOfRecipients { return fmt.Errorf("recipient limit exceeded (max %d)", MaxNumberOfRecipients) } @@ -394,32 +382,25 @@ func (m *Message) AddRecipientAndVariables(r string, vars map[string]any) error 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 (m *MimeMessage) AddRecipient(recipient string) error { + if m.RecipientCount() >= MaxNumberOfRecipients { + return fmt.Errorf("recipient limit exceeded (max %d)", MaxNumberOfRecipients) + } + m.to = append(m.to, recipient) + + return nil } func (m *PlainMessage) RecipientCount() int { - return len(m.BCC()) + len(m.CC()) + return len(m.To()) + len(m.BCC()) + len(m.CC()) } -func (*MimeMessage) RecipientCount() int { - // TODO(v5): 10 + len(m.To()) - return 10 +func (m *MimeMessage) RecipientCount() int { + return 10 + len(m.To()) } // SetReplyTo sets the receiver who should receive replies -func (m *Message) SetReplyTo(recipient string) { +func (m *CommonMessage) SetReplyTo(recipient string) { m.AddHeader("Reply-To", recipient) } @@ -435,25 +416,12 @@ func (m *PlainMessage) AddBCC(r string) { func (*MimeMessage) AddBCC(_ string) {} -// Deprecated: use SetHTML instead. -// -// TODO(v5): remove this method -func (m *Message) SetHtml(html string) { - m.SetHTML(html) -} - func (m *PlainMessage) SetHTML(h string) { m.html = h } func (*MimeMessage) SetHTML(_ string) {} -// Deprecated: use SetAmpHTML instead. -// TODO(v5): remove this method -func (m *Message) SetAMPHtml(html string) { - m.SetAmpHTML(html) -} - func (m *PlainMessage) SetAmpHTML(h string) { m.ampHtml = h } @@ -462,7 +430,7 @@ func (*MimeMessage) SetAmpHTML(_ 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 { +func (m *CommonMessage) AddTag(tag ...string) error { if len(m.Tags()) >= MaxNumberOfTags { return fmt.Errorf("cannot add any new tags. Message tag limit (%d) reached", MaxNumberOfTags) } @@ -477,34 +445,28 @@ func (m *PlainMessage) SetTemplate(t string) { func (*MimeMessage) SetTemplate(_ string) {} -// Deprecated: is no longer supported and is deprecated for new software. -// TODO(v5): remove this method. -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) { +func (m *CommonMessage) SetDKIM(dkim bool) { m.dkim = &dkim } -// EnableNativeSend allows the return path to match the address in the Message.Headers.From: +// EnableNativeSend allows the return path to match the address in the CommonMessage.Headers.From: // field when sending from Mailgun rather than the usual bounce+ address in the return path. -func (m *Message) EnableNativeSend() { +func (m *CommonMessage) 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() { +func (m *CommonMessage) 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) { +func (m *CommonMessage) SetDeliveryTime(dt time.Time) { m.deliveryTime = dt } @@ -512,7 +474,7 @@ func (m *Message) SetDeliveryTime(dt time.Time) { // String should be set to the number of hours in [0-9]+h format, // with the minimum being 24h and the maximum being 72h. // Refer to the Mailgun documentation for more information. -func (m *Message) SetSTOPeriod(stoPeriod string) error { +func (m *CommonMessage) SetSTOPeriod(stoPeriod string) error { validPattern := `^([2-6][4-9]|[3-6][0-9]|7[0-2])h$` // TODO(vtopc): regexp.Compile, which is called by regexp.MatchString, is a heave operation, move into global variable // or just parse using time.ParseDuration(). @@ -536,17 +498,17 @@ func (m *Message) SetSTOPeriod(stoPeriod string) error { // 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) { +func (m *CommonMessage) SetTracking(tracking bool) { m.tracking = &tracking } // SetTrackingClicks information is found in the Mailgun documentation. -func (m *Message) SetTrackingClicks(trackingClicks bool) { +func (m *CommonMessage) SetTrackingClicks(trackingClicks bool) { m.trackingClicks = ptr(yesNo(trackingClicks)) } // SetTrackingOptions sets the o:tracking, o:tracking-clicks and o:tracking-opens at once. -func (m *Message) SetTrackingOptions(options *TrackingOptions) { +func (m *CommonMessage) SetTrackingOptions(options *TrackingOptions) { m.tracking = &options.Tracking m.trackingClicks = &options.TrackingClicks @@ -555,32 +517,32 @@ func (m *Message) SetTrackingOptions(options *TrackingOptions) { } // SetRequireTLS information is found in the Mailgun documentation. -func (m *Message) SetRequireTLS(b bool) { +func (m *CommonMessage) SetRequireTLS(b bool) { m.requireTLS = b } // SetSkipVerification information is found in the Mailgun documentation. -func (m *Message) SetSkipVerification(b bool) { +func (m *CommonMessage) SetSkipVerification(b bool) { m.skipVerification = b } // SetTrackingOpens information is found in the Mailgun documentation. -func (m *Message) SetTrackingOpens(trackingOpens bool) { +func (m *CommonMessage) SetTrackingOpens(trackingOpens bool) { m.trackingOpens = &trackingOpens } // SetTemplateVersion information is found in the Mailgun documentation. -func (m *Message) SetTemplateVersion(tag string) { +func (m *CommonMessage) SetTemplateVersion(tag string) { m.templateVersionTag = tag } // SetTemplateRenderText information is found in the Mailgun documentation. -func (m *Message) SetTemplateRenderText(render bool) { +func (m *CommonMessage) SetTemplateRenderText(render bool) { m.templateRenderText = render } // AddHeader allows you to send custom MIME headers with the message. -func (m *Message) AddHeader(header, value string) { +func (m *CommonMessage) AddHeader(header, value string) { if m.headers == nil { m.headers = make(map[string]string) } @@ -590,7 +552,7 @@ func (m *Message) AddHeader(header, value string) { // 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 any) error { +func (m *CommonMessage) AddVariable(variable string, value any) error { if m.variables == nil { m.variables = make(map[string]string) } @@ -613,7 +575,7 @@ func (m *Message) AddVariable(variable string, value any) error { // 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 any) error { +func (m *CommonMessage) AddTemplateVariable(variable string, value any) error { if m.templateVariables == nil { m.templateVariables = make(map[string]any) } @@ -622,31 +584,22 @@ func (m *Message) AddTemplateVariable(variable string, value any) error { } // AddDomain allows you to use a separate domain for the type of messages you are sending. -func (m *Message) AddDomain(domain string) { +func (m *CommonMessage) AddDomain(domain string) { m.domain = domain } -// Headers retrieves the http headers associated with this message -func (m *Message) Headers() map[string]string { - return m.headers -} - -// Deprecated: use func Headers() instead. -// TODO(v5): remove this method, it violates https://go.dev/doc/effective_go#Getters -func (m *Message) GetHeaders() map[string]string { +// Headers retrieve the http headers associated with this message +func (m *CommonMessage) Headers() map[string]string { return m.headers } -// ErrInvalidMessage is returned by `Send()` when the `mailgun.Message` struct is incomplete +// ErrInvalidMessage is returned by `Send()` when the `mailgun.CommonMessage` struct is incomplete var ErrInvalidMessage = errors.New("message not valid") type SendableMessage interface { Domain() string To() []string Tags() []string - // Deprecated: is no longer supported and is deprecated for new software. - // TODO(v5): remove this method - Campaigns() []string DKIM() *bool DeliveryTime() time.Time STOPeriod() string @@ -672,12 +625,12 @@ type SendableMessage interface { Specific } -// Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery. +// Send attempts to queue a message (see PlainMessage, MimeMessage and its methods) for delivery. // It returns the Mailgun server response, which consists of two components: // - A human-readable status message, typically "Queued. Thank you." // - A Message ID, which is the id used to track the queued message. The message id is useful // when contacting support to report an issue with a specific message or to relate a -// delivered, accepted or failed event back to specific message. +// delivered, accepted or failed event back to a specific message. // // The status and message ID are only returned if no error occurred. // @@ -693,15 +646,14 @@ type SendableMessage interface { // } // // See the public mailgun documentation for all possible return codes and error messages -// TODO(v5): switch m to SendableMessage interface - https://bryanftan.medium.com/accept-interfaces-return-structs-in-go-d4cab29a301b -func (mg *MailgunImpl) Send(ctx context.Context, m *Message) (mes, id string, err error) { - if mg.domain == "" { +func (mg *MailgunImpl) Send(ctx context.Context, m SendableMessage) (mes, id string, err error) { + if m.Domain() == "" { err = errors.New("you must provide a valid domain before calling Send()") return "", "", err } invalidChars := ":&'@(),!?#;%+=<>" - if i := strings.ContainsAny(mg.domain, invalidChars); i { + if i := strings.ContainsAny(m.Domain(), invalidChars); i { err = fmt.Errorf("you called Send() with a domain that contains invalid characters") return "", "", err } @@ -722,18 +674,14 @@ func (mg *MailgunImpl) Send(ctx context.Context, m *Message) (mes, id string, er } payload := NewFormDataPayload() - m.Specific.AddValues(payload) + m.AddValues(payload) + // TODO: make (CommonMessage).AddValues()? err = addMessageValues(payload, m) if err != nil { return "", "", err } - // TODO(v5): remove due for domain agnostic API: - if m.Domain() == "" { - m.domain = mg.Domain() - } - r := newHTTPRequest(generateApiUrlWithDomain(mg, m.Endpoint(), m.Domain())) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.APIKey()) @@ -782,10 +730,6 @@ func addMessageOptions(dst *FormDataPayload, src SendableMessage) { for _, tag := range src.Tags() { dst.addValue("o:tag", tag) } - for _, campaign := range src.Campaigns() { - // TODO(v5): deprecated - https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Messages/ - dst.addValue("o:campaign", campaign) - } if src.DKIM() != nil { dst.addValue("o:dkim", yesNo(*src.DKIM())) } @@ -842,7 +786,7 @@ func addMessageVariables(dst *FormDataPayload, src SendableMessage) error { if src.TemplateVariables() != nil { variableString, err := json.Marshal(src.TemplateVariables()) if err == nil { - // the map was marshalled as json so add it + // the map was marshaled as JSON so add it dst.addValue("h:X-Mailgun-Variables", string(variableString)) } } @@ -931,7 +875,7 @@ func trueFalse(b bool) string { } // isValid returns true if, and only if, -// a Message instance is sufficiently initialized to send via the Mailgun interface. +// a CommonMessage instance is sufficiently initialized to send via the Mailgun interface. func isValid(m SendableMessage) bool { if m == nil { return false @@ -949,10 +893,6 @@ func isValid(m SendableMessage) bool { return false } - if !validateStringList(m.Campaigns(), false) || len(m.Campaigns()) > 3 { - return false - } - return true } diff --git a/messages_test.go b/messages_test.go index 408297e7..8b2f1ae7 100644 --- a/messages_test.go +++ b/messages_test.go @@ -242,7 +242,7 @@ func TestSendMGMIME(t *testing.T) { require.NoError(t, err) ctx := context.Background() - m := mailgun.NewMIMEMessage(io.NopCloser(strings.NewReader(exampleMime)), toUser) + m := mailgun.NewMIMEMessage(os.Getenv("MG_DOMAIN"), io.NopCloser(strings.NewReader(exampleMime)), toUser) msg, id, err := mg.Send(ctx, m) require.NoError(t, err) t.Log("TestSendMIME:MSG(" + msg + "),ID(" + id + ")") @@ -257,7 +257,7 @@ func TestSendMGBatchFailRecipients(t *testing.T) { spendMoney(t, func() { toUser := os.Getenv("MG_EMAIL_TO") - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText+"Batch\n") + m := mailgun.NewMessage(os.Getenv("MG_DOMAIN"), fromUser, exampleSubject, exampleText+"Batch\n") for i := 0; i < mailgun.MaxNumberOfRecipients; i++ { err := m.AddRecipient("") // We expect this to indicate a failure at the API require.NoError(t, err) @@ -280,7 +280,7 @@ func TestSendMGBatchRecipientVariables(t *testing.T) { require.NoError(t, err) ctx := context.Background() - m := mailgun.NewMessage(fromUser, exampleSubject, templateText) + m := mailgun.NewMessage(os.Getenv("MG_DOMAIN"), fromUser, exampleSubject, templateText) err = m.AddRecipientAndVariables(toUser, map[string]any{ "name": "Joe Cool Example", "table": 42, @@ -315,7 +315,7 @@ func TestSendMGOffline(t *testing.T) { mg.SetAPIBase(srv.URL + "/v3") ctx := context.Background() - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser) msg, id, err := mg.Send(ctx, m) require.NoError(t, err) assert.Equal(t, exampleMessage, msg) @@ -348,7 +348,7 @@ func TestSendMGSeparateDomain(t *testing.T) { mg.SetAPIBase(srv.URL + "/v3") ctx := context.Background() - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser) m.AddDomain(signingDomain) msg, id, err := mg.Send(ctx, m) @@ -401,7 +401,7 @@ func TestSendMGMessageVariables(t *testing.T) { mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey) mg.SetAPIBase(srv.URL + "/v3") - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser) err := m.AddVariable(exampleStrVarKey, exampleStrVarVal) require.NoError(t, err) err = m.AddVariable(exampleBoolVarKey, false) @@ -418,7 +418,7 @@ func TestSendMGMessageVariables(t *testing.T) { } func TestAddRecipientsError(t *testing.T) { - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText) + m := mailgun.NewMessage(domain, fromUser, exampleSubject, exampleText) for i := 0; i < 1000; i++ { recipient := fmt.Sprintf("recipient_%d@example.com", i) @@ -433,7 +433,7 @@ func TestAddRecipientsError(t *testing.T) { func TestAddRecipientAndVariablesError(t *testing.T) { var err error - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText) + m := mailgun.NewMessage(domain, fromUser, exampleSubject, exampleText) for i := 0; i < 1000; i++ { recipient := fmt.Sprintf("recipient_%d@example.com", i) @@ -472,7 +472,7 @@ func TestSendDomainError(t *testing.T) { ctx := context.Background() mg := mailgun.NewMailgun(c.domain, exampleAPIKey) mg.SetAPIBase(srv.URL + "/v3") - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, "test@test.com") + m := mailgun.NewMessage(c.domain, fromUser, exampleSubject, exampleText, "test@test.com") _, _, err := mg.Send(ctx, m) if c.isValid { @@ -498,7 +498,7 @@ func TestSendEOFError(t *testing.T) { mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey) mg.SetAPIBase(srv.URL + "/v3") - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser) _, _, err := mg.Send(context.Background(), m) require.NotNil(t, err) // TODO(vtopc): do not compare strings, use errors.Is or errors.As: @@ -524,18 +524,18 @@ func TestHasRecipient(t *testing.T) { mg.SetAPIBase(srv.URL + "/v3") // No recipient - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText) _, _, err := mg.Send(context.Background(), m) require.EqualError(t, err, "message not valid") // Provided Bcc - m = mailgun.NewMessage(fromUser, exampleSubject, exampleText) + m = mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText) m.AddBCC(recipient) _, _, err = mg.Send(context.Background(), m) require.NoError(t, err) // Provided cc - m = mailgun.NewMessage(fromUser, exampleSubject, exampleText) + m = mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText) m.AddCC(recipient) _, _, err = mg.Send(context.Background(), m) require.NoError(t, err) @@ -597,7 +597,7 @@ func TestAddOverrideHeader(t *testing.T) { mg.AddOverrideHeader("CustomHeader", "custom-value") ctx := context.Background() - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser) m.SetRequireTLS(true) m.SetSkipVerification(true) @@ -636,7 +636,7 @@ func TestOnBehalfOfSubaccount(t *testing.T) { mg.SetOnBehalfOfSubaccount("mailgun.subaccount") ctx := context.Background() - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser) m.SetRequireTLS(true) m.SetSkipVerification(true) @@ -668,7 +668,7 @@ func TestCaptureCurlOutput(t *testing.T) { mg.SetAPIBase(srv.URL + "/v3") ctx := context.Background() - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser) msg, id, err := mg.Send(ctx, m) require.NoError(t, err) assert.Equal(t, exampleMessage, msg) @@ -704,7 +704,7 @@ func TestSendTLSOptions(t *testing.T) { mg.SetAPIBase(srv.URL + "/v3") ctx := context.Background() - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser) m.SetRequireTLS(true) m.SetSkipVerification(true) @@ -734,7 +734,7 @@ func TestSendTemplate(t *testing.T) { mg.SetAPIBase(srv.URL + "/v3") ctx := context.Background() - m := mailgun.NewMessage(fromUser, exampleSubject, "", toUser) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, "", toUser) m.SetTemplate(templateName) msg, id, err := mg.Send(ctx, m) @@ -767,7 +767,7 @@ func TestSendTemplateOptions(t *testing.T) { mg.SetAPIBase(srv.URL + "/v3") ctx := context.Background() - m := mailgun.NewMessage(fromUser, exampleSubject, "", toUser) + m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, "", toUser) m.SetTemplate(templateName) m.SetTemplateRenderText(true) m.SetTemplateVersion(templateVersionTag) @@ -779,7 +779,7 @@ func TestSendTemplateOptions(t *testing.T) { } func TestSendableMessageIface(t *testing.T) { - m := mailgun.NewMessage(fromUser, exampleSubject, exampleText) + m := mailgun.NewMessage(domain, fromUser, exampleSubject, exampleText) assert.Implements(t, (*mailgun.SendableMessage)(nil), m) } diff --git a/messages_types_v5.go b/messages_types_v5.go index 0cc843f0..92139081 100644 --- a/messages_types_v5.go +++ b/messages_types_v5.go @@ -1,13 +1,14 @@ package mailgun // This file contains a draft for new v5 messages. +// TODO(v5): remove this file import ( "io" "time" ) -// Message structures contain both the message text and the envelope for an e-mail message. +// CommonMessage structures contain both the message text and the envelope for an e-mail message. // TODO(v5): rename to CommonMessage type commonMessageV5 struct { domain string diff --git a/messages_v5.go b/messages_v5.go index 23cca5e9..4342ed7a 100644 --- a/messages_v5.go +++ b/messages_v5.go @@ -1,6 +1,7 @@ package mailgun // This file contains methods for new v5 messages. +// TODO(v5): remove this file import ( "context" @@ -119,7 +120,7 @@ func (m *commonMessageV5) SetDKIM(dkim bool) { m.dkim = &dkim } -// EnableNativeSend allows the return path to match the address in the Message.Headers.From: +// EnableNativeSend allows the return path to match the address in the CommonMessage.Headers.From: // field when sending from Mailgun rather than the usual bounce+ address in the return path. func (m *commonMessageV5) EnableNativeSend() { m.nativeSend = true @@ -381,7 +382,7 @@ func (*mimeMessageV5) Endpoint() string { return mimeMessagesEndpoint } -// Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery. +// Send attempts to queue a message (see CommonMessage, NewMessage, and its methods) for delivery. // It returns the Mailgun server response, which consists of two components: // - A human-readable status message, typically "Queued. Thank you." // - A Message ID, which is the id used to track the queued message. The message id is useful diff --git a/storage_test.go b/storage_test.go index e22434e1..33a0df2c 100644 --- a/storage_test.go +++ b/storage_test.go @@ -18,7 +18,7 @@ func TestStorage(t *testing.T) { var ctx = context.Background() - m := mailgun.NewMessage("root@"+testDomain, "Subject", "Text Body", "stored@"+testDomain) + m := mailgun.NewMessage(testDomain, "root@"+testDomain, "Subject", "Text Body", "stored@"+testDomain) msg, id, err := mg.Send(ctx, m) require.NoError(t, err) diff --git a/tags_test.go b/tags_test.go index 1e642529..e7cf3145 100644 --- a/tags_test.go +++ b/tags_test.go @@ -20,7 +20,7 @@ const ( func TestTags(t *testing.T) { mg := mailgun.NewMailgun(testDomain, testKey) mg.SetAPIBase(server.URL()) - msg := mailgun.NewMessage(fromUser, exampleSubject, exampleText, "test@example.com") + msg := mailgun.NewMessage(testDomain, fromUser, exampleSubject, exampleText, "test@example.com") require.NoError(t, msg.AddTag("newsletter")) require.NoError(t, msg.AddTag("homer")) require.NoError(t, msg.AddTag("bart"))