From acf05f40319364e091d983a6ccf6b0017986cca4 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:02:10 +0200 Subject: [PATCH 1/8] Refactor Message --- mailgun_test.go | 4 - members.go | 4 +- messages.go | 263 ++++++++++++---------------------------------- stored_message.go | 126 ++++++++++++++++++++++ 4 files changed, 193 insertions(+), 204 deletions(-) create mode 100644 stored_message.go diff --git a/mailgun_test.go b/mailgun_test.go index f4e11c74..3a6519a0 100644 --- a/mailgun_test.go +++ b/mailgun_test.go @@ -61,7 +61,3 @@ func TestValidBaseAPI(t *testing.T) { require.NoError(t, err) } } - -func ptr[T any](v T) *T { - return &v -} diff --git a/members.go b/members.go index 1ad4588c..1d94e1ce 100644 --- a/members.go +++ b/members.go @@ -13,8 +13,8 @@ import ( // if nil, the relevant data type remains unspecified. // Otherwise, its value is either true or false. var ( - yes bool = true - no bool = false + yes = true + no = false ) // Mailing list members have an attribute that determines if they've subscribed to the mailing list or not. diff --git a/messages.go b/messages.go index 06ef7e87..8f928203 100644 --- a/messages.go +++ b/messages.go @@ -24,7 +24,7 @@ type Message struct { to []string tags []string campaigns []string - dkim bool + dkim *bool deliveryTime time.Time stoPeriod string attachments []string @@ -35,9 +35,9 @@ type Message struct { nativeSend bool testMode bool - tracking bool - trackingClicks string - trackingOpens bool + tracking *bool + trackingClicks *string + trackingOpens *bool headers map[string]string variables map[string]string templateVariables map[string]interface{} @@ -46,12 +46,8 @@ type Message struct { templateVersionTag string templateRenderText bool - dkimSet bool - trackingSet bool - trackingClicksSet bool - trackingOpensSet bool - requireTLS bool - skipVerification bool + requireTLS bool + skipVerification bool specific features } @@ -66,50 +62,6 @@ type BufferAttachment struct { Buffer []byte } -// StoredMessage structures contain the (parsed) message content for an email -// sent to a Mailgun account. -// -// The MessageHeaders field is special, in that it's formatted as a slice of pairs. -// Each pair consists of a name [0] and value [1]. Array notation is used instead of a map -// because that's how it's sent over the wire, and it's how encoding/json expects this field -// to be. -type StoredMessage struct { - Recipients string `json:"recipients"` - Sender string `json:"sender"` - From string `json:"from"` - Subject string `json:"subject"` - BodyPlain string `json:"body-plain"` - StrippedText string `json:"stripped-text"` - StrippedSignature string `json:"stripped-signature"` - BodyHtml string `json:"body-html"` - StrippedHtml string `json:"stripped-html"` - Attachments []StoredAttachment `json:"attachments"` - MessageUrl string `json:"message-url"` - ContentIDMap map[string]struct { - Url string `json:"url"` - ContentType string `json:"content-type"` - Name string `json:"name"` - Size int64 `json:"size"` - } `json:"content-id-map"` - MessageHeaders [][]string `json:"message-headers"` -} - -// StoredAttachment structures contain information on an attachment associated with a stored message. -type StoredAttachment struct { - Size int `json:"size"` - Url string `json:"url"` - Name string `json:"name"` - ContentType string `json:"content-type"` -} - -type StoredMessageRaw struct { - Recipients string `json:"recipients"` - Sender string `json:"sender"` - From string `json:"from"` - Subject string `json:"subject"` - BodyMime string `json:"body-mime"` -} - // plainMessage contains fields relevant to plain API-synthesized messages. // You're expected to use various setters to set most of these attributes, // although from, subject, and text are set when the message is created with @@ -405,8 +357,7 @@ func (m *Message) AddCampaign(campaign string) { // SetDKIM arranges to send the o:dkim header with the message, and sets its value accordingly. // Refer to the Mailgun documentation for more information. func (m *Message) SetDKIM(dkim bool) { - m.dkim = dkim - m.dkimSet = true + m.dkim = &dkim } // EnableNativeSend allows the return path to match the address in the Message.Headers.From: @@ -457,26 +408,21 @@ func (m *Message) SetSTOPeriod(stoPeriod string) error { // Note that this header is not passed on to the final recipient(s). // Refer to the Mailgun documentation for more information. func (m *Message) SetTracking(tracking bool) { - m.tracking = tracking - m.trackingSet = true + m.tracking = &tracking } // SetTrackingClicks information is found in the Mailgun documentation. func (m *Message) SetTrackingClicks(trackingClicks bool) { - m.trackingClicks = yesNo(trackingClicks) - m.trackingClicksSet = true + 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) { - m.tracking = options.Tracking - m.trackingSet = true + m.tracking = &options.Tracking - m.trackingClicks = options.TrackingClicks - m.trackingClicksSet = true + m.trackingClicks = &options.TrackingClicks - m.trackingOpens = options.TrackingOpens - m.trackingOpensSet = true + m.trackingOpens = &options.TrackingOpens } // SetRequireTLS information is found in the Mailgun documentation. @@ -491,8 +437,7 @@ func (m *Message) SetSkipVerification(b bool) { // SetTrackingOpens information is found in the Mailgun documentation. func (m *Message) SetTrackingOpens(trackingOpens bool) { - m.trackingOpens = trackingOpens - m.trackingOpensSet = true + m.trackingOpens = &trackingOpens } // SetTemplateVersion information is found in the Mailgun documentation. @@ -581,7 +526,7 @@ var ErrInvalidMessage = errors.New("message not valid") // } // // See the public mailgun documentation for all possible return codes and error messages -func (mg *MailgunImpl) Send(ctx context.Context, message *Message) (mes string, id string, err error) { +func (mg *MailgunImpl) Send(ctx context.Context, m *Message) (mes string, id string, err error) { if mg.domain == "" { err = errors.New("you must provide a valid domain before calling Send()") return @@ -598,121 +543,121 @@ func (mg *MailgunImpl) Send(ctx context.Context, message *Message) (mes string, return } - if !isValid(message) { + if !isValid(m) { err = ErrInvalidMessage return } - if message.stoPeriod != "" && message.RecipientCount() > 1 { + if m.stoPeriod != "" && m.RecipientCount() > 1 { err = errors.New("STO can only be used on a per-message basis") return } payload := newFormDataPayload() - message.specific.addValues(payload) - for _, to := range message.to { + m.specific.addValues(payload) + for _, to := range m.to { payload.addValue("to", to) } - for _, tag := range message.tags { + for _, tag := range m.tags { payload.addValue("o:tag", tag) } - for _, campaign := range message.campaigns { + for _, campaign := range m.campaigns { payload.addValue("o:campaign", campaign) } - if message.dkimSet { - payload.addValue("o:dkim", yesNo(message.dkim)) + if m.dkim != nil { + payload.addValue("o:dkim", yesNo(*m.dkim)) } - if !message.deliveryTime.IsZero() { - payload.addValue("o:deliverytime", formatMailgunTime(message.deliveryTime)) + if !m.deliveryTime.IsZero() { + payload.addValue("o:deliverytime", formatMailgunTime(m.deliveryTime)) } - if message.stoPeriod != "" { - payload.addValue("o:deliverytime-optimize-period", message.stoPeriod) + if m.stoPeriod != "" { + payload.addValue("o:deliverytime-optimize-period", m.stoPeriod) } - if message.nativeSend { + if m.nativeSend { payload.addValue("o:native-send", "yes") } - if message.testMode { + if m.testMode { payload.addValue("o:testmode", "yes") } - if message.trackingSet { - payload.addValue("o:tracking", yesNo(message.tracking)) + if m.tracking != nil { + payload.addValue("o:tracking", yesNo(*m.tracking)) } - if message.trackingClicksSet { - payload.addValue("o:tracking-clicks", message.trackingClicks) + if m.trackingClicks != nil { + payload.addValue("o:tracking-clicks", *m.trackingClicks) } - if message.trackingOpensSet { - payload.addValue("o:tracking-opens", yesNo(message.trackingOpens)) + if m.trackingOpens != nil { + payload.addValue("o:tracking-opens", yesNo(*m.trackingOpens)) } - if message.requireTLS { - payload.addValue("o:require-tls", trueFalse(message.requireTLS)) + if m.requireTLS { + payload.addValue("o:require-tls", trueFalse(m.requireTLS)) } - if message.skipVerification { - payload.addValue("o:skip-verification", trueFalse(message.skipVerification)) + if m.skipVerification { + payload.addValue("o:skip-verification", trueFalse(m.skipVerification)) } - if message.headers != nil { - for header, value := range message.headers { + if m.headers != nil { + for header, value := range m.headers { payload.addValue("h:"+header, value) } } - if message.variables != nil { - for variable, value := range message.variables { + if m.variables != nil { + for variable, value := range m.variables { payload.addValue("v:"+variable, value) } } - if message.templateVariables != nil { - variableString, err := json.Marshal(message.templateVariables) + if m.templateVariables != nil { + variableString, err := json.Marshal(m.templateVariables) if err == nil { // the map was marshalled as json so add it payload.addValue("h:X-Mailgun-Variables", string(variableString)) } } - if message.recipientVariables != nil { - j, err := json.Marshal(message.recipientVariables) + if m.recipientVariables != nil { + j, err := json.Marshal(m.recipientVariables) if err != nil { return "", "", err } payload.addValue("recipient-variables", string(j)) } - if message.attachments != nil { - for _, attachment := range message.attachments { + if m.attachments != nil { + for _, attachment := range m.attachments { payload.addFile("attachment", attachment) } } - if message.readerAttachments != nil { - for _, readerAttachment := range message.readerAttachments { + if m.readerAttachments != nil { + for _, readerAttachment := range m.readerAttachments { payload.addReadCloser("attachment", readerAttachment.Filename, readerAttachment.ReadCloser) } } - if message.bufferAttachments != nil { - for _, bufferAttachment := range message.bufferAttachments { + if m.bufferAttachments != nil { + for _, bufferAttachment := range m.bufferAttachments { payload.addBuffer("attachment", bufferAttachment.Filename, bufferAttachment.Buffer) } } - if message.inlines != nil { - for _, inline := range message.inlines { + if m.inlines != nil { + for _, inline := range m.inlines { payload.addFile("inline", inline) } } - if message.readerInlines != nil { - for _, readerAttachment := range message.readerInlines { + if m.readerInlines != nil { + for _, readerAttachment := range m.readerInlines { payload.addReadCloser("inline", readerAttachment.Filename, readerAttachment.ReadCloser) } } - if message.domain == "" { - message.domain = mg.Domain() + if m.domain == "" { + m.domain = mg.Domain() } - if message.templateVersionTag != "" { - payload.addValue("t:version", message.templateVersionTag) + if m.templateVersionTag != "" { + payload.addValue("t:version", m.templateVersionTag) } - if message.templateRenderText { - payload.addValue("t:text", yesNo(message.templateRenderText)) + if m.templateRenderText { + payload.addValue("t:text", yesNo(m.templateRenderText)) } - r := newHTTPRequest(generateApiUrlWithDomain(mg, message.specific.endpoint(), message.domain)) + r := newHTTPRequest(generateApiUrlWithDomain(mg, m.specific.endpoint(), m.domain)) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.APIKey()) // Override any HTTP headers if provided @@ -780,10 +725,7 @@ func yesNo(b bool) string { } func trueFalse(b bool) string { - if b { - return "true" - } - return "false" + return strconv.FormatBool(b) } // isValid returns true if, and only if, @@ -855,6 +797,7 @@ func validateStringList(list []string, requireOne bool) bool { if a == "" { return false } else { + // TODO(vtopc): hasOne is always true: hasOne = hasOne || true } } @@ -862,79 +805,3 @@ func validateStringList(list []string, requireOne bool) bool { return hasOne } - -// GetStoredMessage retrieves information about a received e-mail message. -// This provides visibility into, e.g., replies to a message sent to a mailing list. -func (mg *MailgunImpl) GetStoredMessage(ctx context.Context, url string) (StoredMessage, error) { - r := newHTTPRequest(url) - r.setClient(mg.Client()) - r.setBasicAuth(basicAuthUser, mg.APIKey()) - - var response StoredMessage - err := getResponseFromJSON(ctx, r, &response) - return response, err -} - -// Given a storage id resend the stored message to the specified recipients -func (mg *MailgunImpl) ReSend(ctx context.Context, url string, recipients ...string) (string, string, error) { - r := newHTTPRequest(url) - r.setClient(mg.Client()) - r.setBasicAuth(basicAuthUser, mg.APIKey()) - - payload := newFormDataPayload() - - if len(recipients) == 0 { - return "", "", errors.New("must provide at least one recipient") - } - - for _, to := range recipients { - payload.addValue("to", to) - } - - var resp sendMessageResponse - err := postResponseFromJSON(ctx, r, payload, &resp) - if err != nil { - return "", "", err - } - return resp.Message, resp.Id, nil - -} - -// GetStoredMessageRaw retrieves the raw MIME body of a received e-mail message. -// Compared to GetStoredMessage, it gives access to the unparsed MIME body, and -// thus delegates to the caller the required parsing. -func (mg *MailgunImpl) GetStoredMessageRaw(ctx context.Context, url string) (StoredMessageRaw, error) { - r := newHTTPRequest(url) - r.setClient(mg.Client()) - r.setBasicAuth(basicAuthUser, mg.APIKey()) - r.addHeader("Accept", "message/rfc2822") - - var response StoredMessageRaw - err := getResponseFromJSON(ctx, r, &response) - return response, err -} - -// Deprecated: Use GetStoreMessage() instead -func (mg *MailgunImpl) GetStoredMessageForURL(ctx context.Context, url string) (StoredMessage, error) { - return mg.GetStoredMessage(ctx, url) -} - -// Deprecated: Use GetStoreMessageRaw() instead -func (mg *MailgunImpl) GetStoredMessageRawForURL(ctx context.Context, url string) (StoredMessageRaw, error) { - return mg.GetStoredMessageRaw(ctx, url) -} - -// GetStoredAttachment retrieves the raw MIME body of a received e-mail message attachment. -func (mg *MailgunImpl) GetStoredAttachment(ctx context.Context, url string) ([]byte, error) { - r := newHTTPRequest(url) - r.setClient(mg.Client()) - r.setBasicAuth(basicAuthUser, mg.APIKey()) - r.addHeader("Accept", "message/rfc2822") - - response, err := makeGetRequest(ctx, r) - if err != nil { - return nil, err - } - - return response.Data, err -} diff --git a/stored_message.go b/stored_message.go new file mode 100644 index 00000000..5d1bc398 --- /dev/null +++ b/stored_message.go @@ -0,0 +1,126 @@ +package mailgun + +import ( + "context" + "errors" +) + +// StoredMessage structures contain the (parsed) message content for an email +// sent to a Mailgun account. +// +// The MessageHeaders field is special, in that it's formatted as a slice of pairs. +// Each pair consists of a name [0] and value [1]. Array notation is used instead of a map +// because that's how it's sent over the wire, and it's how encoding/json expects this field +// to be. +type StoredMessage struct { + Recipients string `json:"recipients"` + Sender string `json:"sender"` + From string `json:"from"` + Subject string `json:"subject"` + BodyPlain string `json:"body-plain"` + StrippedText string `json:"stripped-text"` + StrippedSignature string `json:"stripped-signature"` + BodyHtml string `json:"body-html"` + StrippedHtml string `json:"stripped-html"` + Attachments []StoredAttachment `json:"attachments"` + MessageUrl string `json:"message-url"` + ContentIDMap map[string]struct { + Url string `json:"url"` + ContentType string `json:"content-type"` + Name string `json:"name"` + Size int64 `json:"size"` + } `json:"content-id-map"` + MessageHeaders [][]string `json:"message-headers"` +} + +// StoredAttachment structures contain information on an attachment associated with a stored message. +type StoredAttachment struct { + Size int `json:"size"` + Url string `json:"url"` + Name string `json:"name"` + ContentType string `json:"content-type"` +} + +type StoredMessageRaw struct { + Recipients string `json:"recipients"` + Sender string `json:"sender"` + From string `json:"from"` + Subject string `json:"subject"` + BodyMime string `json:"body-mime"` +} + +// GetStoredMessage retrieves information about a received e-mail message. +// This provides visibility into, e.g., replies to a message sent to a mailing list. +func (mg *MailgunImpl) GetStoredMessage(ctx context.Context, url string) (StoredMessage, error) { + r := newHTTPRequest(url) + r.setClient(mg.Client()) + r.setBasicAuth(basicAuthUser, mg.APIKey()) + + var response StoredMessage + err := getResponseFromJSON(ctx, r, &response) + return response, err +} + +// Given a storage id resend the stored message to the specified recipients +func (mg *MailgunImpl) ReSend(ctx context.Context, url string, recipients ...string) (string, string, error) { + r := newHTTPRequest(url) + r.setClient(mg.Client()) + r.setBasicAuth(basicAuthUser, mg.APIKey()) + + payload := newFormDataPayload() + + if len(recipients) == 0 { + return "", "", errors.New("must provide at least one recipient") + } + + for _, to := range recipients { + payload.addValue("to", to) + } + + var resp sendMessageResponse + err := postResponseFromJSON(ctx, r, payload, &resp) + if err != nil { + return "", "", err + } + return resp.Message, resp.Id, nil + +} + +// GetStoredMessageRaw retrieves the raw MIME body of a received e-mail message. +// Compared to GetStoredMessage, it gives access to the unparsed MIME body, and +// thus delegates to the caller the required parsing. +func (mg *MailgunImpl) GetStoredMessageRaw(ctx context.Context, url string) (StoredMessageRaw, error) { + r := newHTTPRequest(url) + r.setClient(mg.Client()) + r.setBasicAuth(basicAuthUser, mg.APIKey()) + r.addHeader("Accept", "message/rfc2822") + + var response StoredMessageRaw + err := getResponseFromJSON(ctx, r, &response) + return response, err +} + +// Deprecated: Use GetStoreMessage() instead +func (mg *MailgunImpl) GetStoredMessageForURL(ctx context.Context, url string) (StoredMessage, error) { + return mg.GetStoredMessage(ctx, url) +} + +// Deprecated: Use GetStoreMessageRaw() instead +func (mg *MailgunImpl) GetStoredMessageRawForURL(ctx context.Context, url string) (StoredMessageRaw, error) { + return mg.GetStoredMessageRaw(ctx, url) +} + +// GetStoredAttachment retrieves the raw MIME body of a received e-mail message attachment. +func (mg *MailgunImpl) GetStoredAttachment(ctx context.Context, url string) ([]byte, error) { + r := newHTTPRequest(url) + r.setClient(mg.Client()) + r.setBasicAuth(basicAuthUser, mg.APIKey()) + r.addHeader("Accept", "message/rfc2822") + + response, err := makeGetRequest(ctx, r) + if err != nil { + return nil, err + } + + return response.Data, err +} From 9a37ca46e6d0882ef1cbce050c6c567ad62605d4 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:04:07 +0200 Subject: [PATCH 2/8] wip --- messages_types_v5.go | 136 +++++++++ messages_v5.go | 639 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 775 insertions(+) create mode 100644 messages_types_v5.go create mode 100644 messages_v5.go diff --git a/messages_types_v5.go b/messages_types_v5.go new file mode 100644 index 00000000..aab79311 --- /dev/null +++ b/messages_types_v5.go @@ -0,0 +1,136 @@ +package mailgun + +import ( + "io" + "time" +) + +// Message structures contain both the message text and the envelope for an e-mail message. +// TODO(v5): remove v5 from the name +type commonMessageV5 struct { + domain string + to []string + tags []string + campaigns []string + dkim *bool + deliveryTime time.Time + stoPeriod string + attachments []string + readerAttachments []ReaderAttachment + inlines []string + readerInlines []ReaderAttachment + bufferAttachments []BufferAttachment + nativeSend bool + testMode bool + tracking *bool + trackingClicks *string + trackingOpens *bool + headers map[string]string + variables map[string]string + templateVariables map[string]interface{} + recipientVariables map[string]map[string]interface{} + templateVersionTag string + templateRenderText bool + requireTLS bool + skipVerification bool + + // specific featuresV5 +} + +// plainMessage contains fields relevant to plain API-synthesized messages. +// You're expected to use various setters to set most of these attributes, +// although from, subject, and text are set when the message is created with +// NewMessage. +type plainMessageV5 struct { + commonMessageV5 + + from string + cc []string + bcc []string + subject string + text string + html string + ampHtml string + template string +} + +// mimeMessage contains fields relevant to pre-packaged MIME messages. +type mimeMessageV5 struct { + commonMessageV5 + + body io.ReadCloser +} + +// features abstracts the common characteristics between regular and MIME messages. +// addCC, addBCC, recipientCount, setHtml and setAMPHtml are invoked via the AddCC, AddBCC, +// RecipientCount, SetHTML and SetAMPHtml calls, as these functions are ignored for MIME messages. +// Send() invokes addValues to add message-type-specific MIME headers for the API call +// to Mailgun. +// isValid yields true if and only if the message is valid enough for sending +// through the API. +// Finally, endpoint() tells Send() which endpoint to use to submit the API call. +// TODO(v5): remove? +type featuresV5 interface { + // AddCC appends a receiver to the carbon-copy header of a message. + addCC(string) + + addBCC(string) + + setHtml(string) + + setAMPHtml(string) + + addValues(*formDataPayload) + + isValid() bool + + endpoint() string + + // 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. + recipientCount() int + + setTemplate(string) +} + +type messageIfaceV5 interface { + Domain() string + To() []string + Tags() []string + Campaigns() []string + DKIM() *bool + DeliveryTime() time.Time + STOPeriod() string + Attachments() []string + ReaderAttachments() []ReaderAttachment + Inlines() []string + ReaderInlines() []ReaderAttachment + BufferAttachments() []BufferAttachment + NativeSend() bool + TestMode() bool + Tracking() *bool + TrackingClicks() *string + TrackingOpens() *bool + Headers() map[string]string + Variables() map[string]string + TemplateVariables() map[string]interface{} + RecipientVariables() map[string]map[string]interface{} + TemplateVersionTag() string + TemplateRenderText() bool + RequireTLS() bool + SkipVerification() bool + + RecipientCount() int + AddValues(p *formDataPayload) + + featuresV5 +} diff --git a/messages_v5.go b/messages_v5.go new file mode 100644 index 00000000..7436a1c8 --- /dev/null +++ b/messages_v5.go @@ -0,0 +1,639 @@ +package mailgun + +// This file contains the new v5 Message and its methods. + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "regexp" + "strconv" + "strings" + "time" +) + +// NewMessage returns a new e-mail message with the simplest envelop needed to send. +// +// Supports arbitrary-sized recipient lists by +// automatically sending mail in batches of up to MaxNumberOfRecipients. +// +// To support batch sending, do not provide `to` at this point. +// 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!") +// +// Note that you'll need to invoke the AddRecipientAndVariables or AddRecipient method +// before sending, though. +// TODO(v5): rename to NewMessage +func newMessageV5(domain, from, subject, text string, to ...string) *plainMessageV5 { + return &plainMessageV5{ + commonMessageV5: commonMessageV5{ + domain: domain, + to: to, + }, + + from: from, + subject: subject, + text: text, + } +} + +// NewMIMEMessage creates a new MIME message. These messages are largely canned; +// you do not need to invoke setters to set message-related headers. +// However, you do still need to call setters for Mailgun-specific settings. +// +// Supports arbitrary-sized recipient lists by +// automatically sending mail in batches of up to MaxNumberOfRecipients. +// +// To support batch sending, do not provide `to` at this point. +// You can do this explicitly, or implicitly, as follows: +// +// // Note absence of `to` parameter(s)! +// m := NewMIMEMessage(body) +// +// Note that you'll need to invoke the AddRecipientAndVariables or AddRecipient method +// before sending, though. +// TODO(v5): rename to NewMIMEMessage +func newMIMEMessage(domain string, body io.ReadCloser, to ...string) *mimeMessageV5 { + return &mimeMessageV5{ + commonMessageV5: commonMessageV5{ + domain: domain, + to: to, + }, + body: body, + } +} + +// AddReaderAttachment arranges to send a file along with the e-mail message. +// File contents are read from a io.ReadCloser. +// The filename parameter is the resulting filename of the attachment. +// The readCloser parameter is the io.ReadCloser which reads the actual bytes to be used +// as the contents of the attached file. +func (m *commonMessageV5) AddReaderAttachment(filename string, readCloser io.ReadCloser) { + ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser} + m.readerAttachments = append(m.readerAttachments, ra) +} + +// AddBufferAttachment arranges to send a file along with the e-mail message. +// File contents are read from the []byte array provided +// The filename parameter is the resulting filename of the attachment. +// The buffer parameter is the []byte array which contains the actual bytes to be used +// as the contents of the attached file. +func (m *commonMessageV5) AddBufferAttachment(filename string, buffer []byte) { + ba := BufferAttachment{Filename: filename, Buffer: buffer} + m.bufferAttachments = append(m.bufferAttachments, ba) +} + +// AddAttachment arranges to send a file from the filesystem along with the e-mail message. +// The attachment parameter is a filename, which must refer to a file which actually resides +// in the local filesystem. +func (m *commonMessageV5) AddAttachment(attachment string) { + m.attachments = append(m.attachments, attachment) +} + +// AddReaderInline arranges to send a file along with the e-mail message. +// File contents are read from a io.ReadCloser. +// The filename parameter is the resulting filename of the attachment. +// The readCloser parameter is the io.ReadCloser which reads the actual bytes to be used +// as the contents of the attached file. +func (m *commonMessageV5) AddReaderInline(filename string, readCloser io.ReadCloser) { + ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser} + m.readerInlines = append(m.readerInlines, ra) +} + +// AddInline arranges to send a file along with the e-mail message, but does so +// in a way that its data remains "inline" with the rest of the message. This +// can be used to send image or font data along with an HTML-encoded message body. +// The attachment parameter is a filename, which must refer to a file which actually resides +// in the local filesystem. +func (m *commonMessageV5) 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 *commonMessageV5) 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 *commonMessageV5) AddRecipientAndVariables(r string, vars map[string]interface{}) error { + if m.RecipientCount() >= MaxNumberOfRecipients { // ?????????????????????????????????????????????? + return fmt.Errorf("recipient limit exceeded (max %d)", MaxNumberOfRecipients) + } + m.to = append(m.to, r) + if vars != nil { + if m.recipientVariables == nil { + m.recipientVariables = make(map[string]map[string]interface{}) + } + m.recipientVariables[r] = vars + } + return nil +} + +func (m *plainMessageV5) RecipientCount() int { + return len(m.to) + len(m.bcc) + len(m.cc) +} + +func (m *mimeMessageV5) recipientCount() int { + return 10 +} + +// SetReplyTo sets the receiver who should receive replies +func (m *commonMessageV5) SetReplyTo(recipient string) { + m.AddHeader("Reply-To", recipient) +} + +// AddCC appends a receiver to the carbon-copy header of a message. + +func (m *plainMessageV5) AddCC(r string) { + m.cc = append(m.cc, r) +} + +func (m *mimeMessageV5) AddCC(_ string) {} + +// AddBCC appends a receiver to the blind-carbon-copy header of a message. +func (m *commonMessageV5) AddBCC(recipient string) { + m.specific.addBCC(recipient) +} + +func (m *plainMessageV5) addBCC(r string) { + m.bcc = append(m.bcc, r) +} + +func (m *mimeMessageV5) addBCC(_ string) {} + +// SetHTML is a helper. If you're sending a message that isn't already MIME encoded, SetHtml() will arrange to bundle +// an HTML representation of your message in addition to your plain-text body. +func (m *commonMessageV5) SetHTML(html string) { + m.specific.setHtml(html) +} + +// Deprecated: use SetHTML instead. +// +// TODO(v5): remove this method +func (m *commonMessageV5) SetHtml(html string) { + m.specific.setHtml(html) +} + +func (m *plainMessageV5) setHtml(h string) { + m.html = h +} + +func (m *mimeMessageV5) setHtml(_ string) {} + +// SetAMPHtml is a helper. If you're sending a message that isn't already MIME encoded, SetAMP() will arrange to bundle +// an AMP-For-Email representation of your message in addition to your html & plain-text content. +func (m *commonMessageV5) SetAMPHtml(html string) { + m.specific.setAMPHtml(html) +} + +func (m *plainMessageV5) setAMPHtml(h string) { + m.ampHtml = h +} + +func (m *mimeMessageV5) 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 *commonMessageV5) AddTag(tag ...string) error { + if len(m.tags) >= MaxNumberOfTags { + return fmt.Errorf("cannot add any new tags. Message tag limit (%d) reached", MaxNumberOfTags) + } + + m.tags = append(m.tags, tag...) + return nil +} + +// SetTemplate sets the name of a template stored via the template API. +// See https://documentation.mailgun.com/en/latest/user_manual.html#templating +func (m *commonMessageV5) SetTemplate(t string) { + m.specific.setTemplate(t) +} + +func (m *plainMessageV5) setTemplate(t string) { + m.template = t +} + +func (m *mimeMessageV5) setTemplate(t string) {} + +// AddCampaign is no longer supported and is deprecated for new software. +func (m *commonMessageV5) 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 *commonMessageV5) SetDKIM(dkim bool) { + m.dkim = &dkim +} + +// EnableNativeSend allows the return path to match the address in the Message.Headers.From: +// field when sending from Mailgun rather than the usual bounce+ address in the return path. +func (m *commonMessageV5) 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 *commonMessageV5) 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 *commonMessageV5) SetDeliveryTime(dt time.Time) { + m.deliveryTime = dt +} + +// SetSTOPeriod toggles Send Time Optimization (STO) on a per-message basis. +// 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 *commonMessageV5) 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(). + match, err := regexp.MatchString(validPattern, stoPeriod) + if err != nil { + return err + } + + if !match { + return errors.New("STO period is invalid. Valid range is 24h to 72h") + } + + m.stoPeriod = stoPeriod + return nil +} + +// SetTracking sets the o:tracking message parameter to adjust, on a message-by-message basis, +// whether or not Mailgun will rewrite URLs to facilitate event tracking. +// Events tracked includes opens, clicks, unsubscribes, etc. +// Note: simply calling this method ensures that the o:tracking header is passed in with the message. +// Its yes/no setting is determined by the call's parameter. +// Note that this header is not passed on to the final recipient(s). +// Refer to the Mailgun documentation for more information. +func (m *commonMessageV5) SetTracking(tracking bool) { + m.tracking = &tracking +} + +// SetTrackingClicks information is found in the Mailgun documentation. +func (m *commonMessageV5) SetTrackingClicks(trackingClicks bool) { + m.trackingClicks = ptr(yesNo(trackingClicks)) +} + +// SetTrackingOptions sets the o:tracking, o:tracking-clicks and o:tracking-opens at once. +func (m *commonMessageV5) SetTrackingOptions(options *TrackingOptions) { + m.tracking = &options.Tracking + + m.trackingClicks = &options.TrackingClicks + + m.trackingOpens = &options.TrackingOpens +} + +// SetRequireTLS information is found in the Mailgun documentation. +func (m *commonMessageV5) SetRequireTLS(b bool) { + m.requireTLS = b +} + +// SetSkipVerification information is found in the Mailgun documentation. +func (m *commonMessageV5) SetSkipVerification(b bool) { + m.skipVerification = b +} + +// SetTrackingOpens information is found in the Mailgun documentation. +func (m *commonMessageV5) SetTrackingOpens(trackingOpens bool) { + m.trackingOpens = &trackingOpens +} + +// SetTemplateVersion information is found in the Mailgun documentation. +func (m *commonMessageV5) SetTemplateVersion(tag string) { + m.templateVersionTag = tag +} + +// SetTemplateRenderText information is found in the Mailgun documentation. +func (m *commonMessageV5) SetTemplateRenderText(render bool) { + m.templateRenderText = render +} + +// AddHeader allows you to send custom MIME headers with the message. +func (m *commonMessageV5) AddHeader(header, value string) { + if m.headers == nil { + m.headers = make(map[string]string) + } + m.headers[header] = value +} + +// AddVariable lets you associate a set of variables with messages you send, +// which Mailgun can use to, in essence, complete form-mail. +// Refer to the Mailgun documentation for more information. +func (m *commonMessageV5) AddVariable(variable string, value interface{}) error { + if m.variables == nil { + m.variables = make(map[string]string) + } + + j, err := json.Marshal(value) + if err != nil { + return err + } + + encoded := string(j) + v, err := strconv.Unquote(encoded) + if err != nil { + v = encoded + } + + m.variables[variable] = v + return nil +} + +// AddTemplateVariable adds a template variable to the map of template variables, replacing the variable if it is already there. +// This is used for server-side message templates and can nest arbitrary values. At send time, the resulting map will be converted into +// a JSON string and sent as a header in the X-Mailgun-Variables header. +func (m *commonMessageV5) AddTemplateVariable(variable string, value interface{}) error { + if m.templateVariables == nil { + m.templateVariables = make(map[string]interface{}) + } + m.templateVariables[variable] = value + return nil +} + +// AddDomain allows you to use a separate domain for the type of messages you are sending. +func (m *commonMessageV5) AddDomain(domain string) { + m.domain = domain +} + +// GetHeaders retrieves the http headers associated with this message +func (m *commonMessageV5) GetHeaders() map[string]string { + return m.headers +} + +// Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery. +// It returns the Mailgun server response, which consists of two components: +// - A human-readable status message, 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. +// +// The status and message ID are only returned if no error occurred. +// +// Error returns can be of type `error.Error` which wrap internal and standard +// golang errors like `url.Error`. The error can also be of type +// mailgun.UnexpectedResponseError which contains the error returned by the mailgun API. +// +// mailgun.UnexpectedResponseError { +// URL: "https://api.mailgun.com/v3/messages", +// Expected: 200, +// Actual: 400, +// Data: "Domain not found: example.com", +// } +// +// See the public mailgun documentation for all possible return codes and error messages +func (mg *MailgunImpl) sendV5(ctx context.Context, m messageIfaceV5) (mes string, id string, err error) { + if mg.domain == "" { + err = errors.New("you must provide a valid domain before calling Send()") + return + } + + invalidChars := ":&'@(),!?#;%+=<>" + if i := strings.ContainsAny(mg.domain, invalidChars); i { + err = fmt.Errorf("you called Send() with a domain that contains invalid characters") + return + } + + if mg.apiKey == "" { + err = errors.New("you must provide a valid api-key before calling Send()") + return + } + + // TODO(v5): uncomment + // if !isValidIface(m) { + // err = ErrInvalidMessage + // return + // } + + if m.STOPeriod() != "" && m.RecipientCount() > 1 { + err = errors.New("STO can only be used on a per-message basis") + return + } + payload := newFormDataPayload() + + m.AddValues(payload) + for _, to := range m.To() { + payload.addValue("to", to) + } + for _, tag := range m.Tags() { + payload.addValue("o:tag", tag) + } + for _, campaign := range m.Campaigns() { + payload.addValue("o:campaign", campaign) + } + if m.DKIM() != nil { + payload.addValue("o:dkim", yesNo(*m.DKIM())) + } + if !m.DeliveryTime().IsZero() { + payload.addValue("o:deliverytime", formatMailgunTime(m.DeliveryTime())) + } + if m.STOPeriod() != "" { + payload.addValue("o:deliverytime-optimize-period", m.STOPeriod()) + } + if m.NativeSend() { + payload.addValue("o:native-send", "yes") + } + if m.TestMode() { + payload.addValue("o:testmode", "yes") + } + if m.Tracking() != nil { + payload.addValue("o:tracking", yesNo(*m.Tracking())) + } + if m.TrackingClicks() != nil { + payload.addValue("o:tracking-clicks", *m.TrackingClicks()) + } + if m.TrackingOpens() != nil { + payload.addValue("o:tracking-opens", yesNo(*m.TrackingOpens())) + } + if m.RequireTLS() { + payload.addValue("o:require-tls", trueFalse(m.RequireTLS())) + } + if m.SkipVerification() { + payload.addValue("o:skip-verification", trueFalse(m.SkipVerification())) + } + if m.Headers() != nil { + for header, value := range m.Headers() { + payload.addValue("h:"+header, value) + } + } + if m.Variables() != nil { + for variable, value := range m.Variables() { + payload.addValue("v:"+variable, value) + } + } + if m.TemplateVariables() != nil { + variableString, err := json.Marshal(m.TemplateVariables) + if err == nil { + // the map was marshalled as json so add it + payload.addValue("h:X-Mailgun-Variables", string(variableString)) + } + } + if m.RecipientVariables() != nil { + j, err := json.Marshal(m.RecipientVariables()) + if err != nil { + return "", "", err + } + payload.addValue("recipient-variables", string(j)) + } + if m.Attachments() != nil { + for _, attachment := range m.Attachments() { + payload.addFile("attachment", attachment) + } + } + if m.ReaderAttachments() != nil { + for _, readerAttachment := range m.ReaderAttachments() { + payload.addReadCloser("attachment", readerAttachment.Filename, readerAttachment.ReadCloser) + } + } + if m.BufferAttachments() != nil { + for _, bufferAttachment := range m.BufferAttachments() { + payload.addBuffer("attachment", bufferAttachment.Filename, bufferAttachment.Buffer) + } + } + if m.Inlines() != nil { + for _, inline := range m.Inlines() { + payload.addFile("inline", inline) + } + } + + if m.ReaderInlines() != nil { + for _, readerAttachment := range m.ReaderInlines() { + payload.addReadCloser("inline", readerAttachment.Filename, readerAttachment.ReadCloser) + } + } + + if m.TemplateVersionTag() != "" { + payload.addValue("t:version", m.TemplateVersionTag()) + } + + if m.TemplateRenderText() { + payload.addValue("t:text", yesNo(m.TemplateRenderText())) + } + + r := newHTTPRequest(generateApiUrlWithDomain(mg, m.endpoint(), m.Domain())) + r.setClient(mg.Client()) + r.setBasicAuth(basicAuthUser, mg.APIKey()) + // Override any HTTP headers if provided + for k, v := range mg.overrideHeaders { + r.addHeader(k, v) + } + + var response sendMessageResponse + err = postResponseFromJSON(ctx, r, payload, &response) + if err == nil { + mes = response.Message + id = response.Id + } + + r.mu.RLock() + defer r.mu.RUnlock() + if r.capturedCurlOutput != "" { + mg.mu.Lock() + defer mg.mu.Unlock() + mg.capturedCurlOutput = r.capturedCurlOutput + } + + return +} + +func (m *plainMessageV5) addValues(p *formDataPayload) { + p.addValue("from", m.from) + p.addValue("subject", m.subject) + p.addValue("text", m.text) + for _, cc := range m.cc { + p.addValue("cc", cc) + } + for _, bcc := range m.bcc { + p.addValue("bcc", bcc) + } + if m.html != "" { + p.addValue("html", m.html) + } + if m.template != "" { + p.addValue("template", m.template) + } + if m.ampHtml != "" { + p.addValue("amp-html", m.ampHtml) + } +} + +func (m *mimeMessageV5) addValues(p *formDataPayload) { + p.addReadCloser("message", "message.mime", m.body) +} + +func (m *plainMessageV5) endpoint() string { + return messagesEndpoint +} + +func (m *mimeMessageV5) endpoint() string { + return mimeMessagesEndpoint +} + +// isValid returns true if, and only if, +// a Message instance is sufficiently initialized to send via the Mailgun interface. +func isValidIface(m messageIfaceV5) bool { + if m == nil { + return false + } + + if !m.isValid() { + return false + } + + if m.RecipientCount() == 0 { + return false + } + + if !validateStringList(m.Tags(), false) { + return false + } + + if !validateStringList(m.Campaigns(), false) || len(m.Campaigns()) > 3 { + return false + } + + return true +} + +func (m *plainMessageV5) isValid() bool { + if !validateStringList(m.cc, false) { + return false + } + + if !validateStringList(m.bcc, false) { + return false + } + + if m.template != "" { + // m.text or m.html not needed if template is supplied + return true + } + + if m.from == "" { + return false + } + + if m.text == "" && m.html == "" { + return false + } + + return true +} + +func (m *mimeMessageV5) isValid() bool { + return m.body != nil +} From ccf435c3ebe146a2d43a96c4cd6324c7ca957fc4 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:13:20 +0200 Subject: [PATCH 3/8] revert ptr --- mailgun_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mailgun_test.go b/mailgun_test.go index 3a6519a0..f4e11c74 100644 --- a/mailgun_test.go +++ b/mailgun_test.go @@ -61,3 +61,7 @@ func TestValidBaseAPI(t *testing.T) { require.NoError(t, err) } } + +func ptr[T any](v T) *T { + return &v +} From e6b23bcb937d881ef76bfd8d07e2b20632472892 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:13:58 +0200 Subject: [PATCH 4/8] master --- stored_message.go | 126 ---------------------------------------------- 1 file changed, 126 deletions(-) delete mode 100644 stored_message.go diff --git a/stored_message.go b/stored_message.go deleted file mode 100644 index 5d1bc398..00000000 --- a/stored_message.go +++ /dev/null @@ -1,126 +0,0 @@ -package mailgun - -import ( - "context" - "errors" -) - -// StoredMessage structures contain the (parsed) message content for an email -// sent to a Mailgun account. -// -// The MessageHeaders field is special, in that it's formatted as a slice of pairs. -// Each pair consists of a name [0] and value [1]. Array notation is used instead of a map -// because that's how it's sent over the wire, and it's how encoding/json expects this field -// to be. -type StoredMessage struct { - Recipients string `json:"recipients"` - Sender string `json:"sender"` - From string `json:"from"` - Subject string `json:"subject"` - BodyPlain string `json:"body-plain"` - StrippedText string `json:"stripped-text"` - StrippedSignature string `json:"stripped-signature"` - BodyHtml string `json:"body-html"` - StrippedHtml string `json:"stripped-html"` - Attachments []StoredAttachment `json:"attachments"` - MessageUrl string `json:"message-url"` - ContentIDMap map[string]struct { - Url string `json:"url"` - ContentType string `json:"content-type"` - Name string `json:"name"` - Size int64 `json:"size"` - } `json:"content-id-map"` - MessageHeaders [][]string `json:"message-headers"` -} - -// StoredAttachment structures contain information on an attachment associated with a stored message. -type StoredAttachment struct { - Size int `json:"size"` - Url string `json:"url"` - Name string `json:"name"` - ContentType string `json:"content-type"` -} - -type StoredMessageRaw struct { - Recipients string `json:"recipients"` - Sender string `json:"sender"` - From string `json:"from"` - Subject string `json:"subject"` - BodyMime string `json:"body-mime"` -} - -// GetStoredMessage retrieves information about a received e-mail message. -// This provides visibility into, e.g., replies to a message sent to a mailing list. -func (mg *MailgunImpl) GetStoredMessage(ctx context.Context, url string) (StoredMessage, error) { - r := newHTTPRequest(url) - r.setClient(mg.Client()) - r.setBasicAuth(basicAuthUser, mg.APIKey()) - - var response StoredMessage - err := getResponseFromJSON(ctx, r, &response) - return response, err -} - -// Given a storage id resend the stored message to the specified recipients -func (mg *MailgunImpl) ReSend(ctx context.Context, url string, recipients ...string) (string, string, error) { - r := newHTTPRequest(url) - r.setClient(mg.Client()) - r.setBasicAuth(basicAuthUser, mg.APIKey()) - - payload := newFormDataPayload() - - if len(recipients) == 0 { - return "", "", errors.New("must provide at least one recipient") - } - - for _, to := range recipients { - payload.addValue("to", to) - } - - var resp sendMessageResponse - err := postResponseFromJSON(ctx, r, payload, &resp) - if err != nil { - return "", "", err - } - return resp.Message, resp.Id, nil - -} - -// GetStoredMessageRaw retrieves the raw MIME body of a received e-mail message. -// Compared to GetStoredMessage, it gives access to the unparsed MIME body, and -// thus delegates to the caller the required parsing. -func (mg *MailgunImpl) GetStoredMessageRaw(ctx context.Context, url string) (StoredMessageRaw, error) { - r := newHTTPRequest(url) - r.setClient(mg.Client()) - r.setBasicAuth(basicAuthUser, mg.APIKey()) - r.addHeader("Accept", "message/rfc2822") - - var response StoredMessageRaw - err := getResponseFromJSON(ctx, r, &response) - return response, err -} - -// Deprecated: Use GetStoreMessage() instead -func (mg *MailgunImpl) GetStoredMessageForURL(ctx context.Context, url string) (StoredMessage, error) { - return mg.GetStoredMessage(ctx, url) -} - -// Deprecated: Use GetStoreMessageRaw() instead -func (mg *MailgunImpl) GetStoredMessageRawForURL(ctx context.Context, url string) (StoredMessageRaw, error) { - return mg.GetStoredMessageRaw(ctx, url) -} - -// GetStoredAttachment retrieves the raw MIME body of a received e-mail message attachment. -func (mg *MailgunImpl) GetStoredAttachment(ctx context.Context, url string) ([]byte, error) { - r := newHTTPRequest(url) - r.setClient(mg.Client()) - r.setBasicAuth(basicAuthUser, mg.APIKey()) - r.addHeader("Accept", "message/rfc2822") - - response, err := makeGetRequest(ctx, r) - if err != nil { - return nil, err - } - - return response.Data, err -} From 47c1e0f42b4208fd90ad7c9a9a9c81337fbaaf5a Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:50:21 +0200 Subject: [PATCH 5/8] WIP --- messages_types_v5.go | 52 ++++---- messages_v5.go | 277 +++++++++++++++++++------------------------ 2 files changed, 150 insertions(+), 179 deletions(-) diff --git a/messages_types_v5.go b/messages_types_v5.go index aab79311..c5ac8298 100644 --- a/messages_types_v5.go +++ b/messages_types_v5.go @@ -11,7 +11,6 @@ type commonMessageV5 struct { domain string to []string tags []string - campaigns []string dkim *bool deliveryTime time.Time stoPeriod string @@ -33,8 +32,6 @@ type commonMessageV5 struct { templateRenderText bool requireTLS bool skipVerification bool - - // specific featuresV5 } // plainMessage contains fields relevant to plain API-synthesized messages. @@ -62,29 +59,27 @@ type mimeMessageV5 struct { } // features abstracts the common characteristics between regular and MIME messages. -// addCC, addBCC, recipientCount, setHtml and setAMPHtml are invoked via the AddCC, AddBCC, -// RecipientCount, SetHTML and SetAMPHtml calls, as these functions are ignored for MIME messages. -// Send() invokes addValues to add message-type-specific MIME headers for the API call -// to Mailgun. -// isValid yields true if and only if the message is valid enough for sending -// through the API. -// Finally, endpoint() tells Send() which endpoint to use to submit the API call. -// TODO(v5): remove? -type featuresV5 interface { +type specificV5 interface { // AddCC appends a receiver to the carbon-copy header of a message. - addCC(string) - - addBCC(string) + AddCC(string) - setHtml(string) + // AddBCC appends a receiver to the blind-carbon-copy header of a message. + AddBCC(string) - setAMPHtml(string) + // SetHTML If you're sending a message that isn't already MIME encoded, it will arrange to bundle + // an HTML representation of your message in addition to your plain-text body. + SetHTML(string) - addValues(*formDataPayload) + // 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. + SetAmpHTML(string) - isValid() bool + // AddValues invoked by Send() to add message-type-specific MIME headers for the API call + // to Mailgun. + AddValues(*formDataPayload) - endpoint() string + // Endpoint tells Send() which endpoint to use to submit the API call. + Endpoint() string // RecipientCount returns the total number of recipients for the message. // This includes To:, Cc:, and Bcc: fields. @@ -97,11 +92,22 @@ type featuresV5 interface { // 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. - recipientCount() int + RecipientCount() int - setTemplate(string) + // SetTemplate sets the name of a template stored via the template API. + // See https://documentation.mailgun.com/en/latest/user_manual.html#templating + SetTemplate(string) + + // 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) + + // isValid yields true if and only if the message is valid enough for sending + // through the API. + isValid() bool } +// TODO(v5): implement for plain and MIME messages type messageIfaceV5 interface { Domain() string To() []string @@ -132,5 +138,5 @@ type messageIfaceV5 interface { RecipientCount() int AddValues(p *formDataPayload) - featuresV5 + specificV5 } diff --git a/messages_v5.go b/messages_v5.go index 7436a1c8..894bef5a 100644 --- a/messages_v5.go +++ b/messages_v5.go @@ -113,120 +113,6 @@ func (m *commonMessageV5) 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 *commonMessageV5) 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 *commonMessageV5) AddRecipientAndVariables(r string, vars map[string]interface{}) error { - if m.RecipientCount() >= MaxNumberOfRecipients { // ?????????????????????????????????????????????? - return fmt.Errorf("recipient limit exceeded (max %d)", MaxNumberOfRecipients) - } - m.to = append(m.to, r) - if vars != nil { - if m.recipientVariables == nil { - m.recipientVariables = make(map[string]map[string]interface{}) - } - m.recipientVariables[r] = vars - } - return nil -} - -func (m *plainMessageV5) RecipientCount() int { - return len(m.to) + len(m.bcc) + len(m.cc) -} - -func (m *mimeMessageV5) recipientCount() int { - return 10 -} - -// SetReplyTo sets the receiver who should receive replies -func (m *commonMessageV5) SetReplyTo(recipient string) { - m.AddHeader("Reply-To", recipient) -} - -// AddCC appends a receiver to the carbon-copy header of a message. - -func (m *plainMessageV5) AddCC(r string) { - m.cc = append(m.cc, r) -} - -func (m *mimeMessageV5) AddCC(_ string) {} - -// AddBCC appends a receiver to the blind-carbon-copy header of a message. -func (m *commonMessageV5) AddBCC(recipient string) { - m.specific.addBCC(recipient) -} - -func (m *plainMessageV5) addBCC(r string) { - m.bcc = append(m.bcc, r) -} - -func (m *mimeMessageV5) addBCC(_ string) {} - -// SetHTML is a helper. If you're sending a message that isn't already MIME encoded, SetHtml() will arrange to bundle -// an HTML representation of your message in addition to your plain-text body. -func (m *commonMessageV5) SetHTML(html string) { - m.specific.setHtml(html) -} - -// Deprecated: use SetHTML instead. -// -// TODO(v5): remove this method -func (m *commonMessageV5) SetHtml(html string) { - m.specific.setHtml(html) -} - -func (m *plainMessageV5) setHtml(h string) { - m.html = h -} - -func (m *mimeMessageV5) setHtml(_ string) {} - -// SetAMPHtml is a helper. If you're sending a message that isn't already MIME encoded, SetAMP() will arrange to bundle -// an AMP-For-Email representation of your message in addition to your html & plain-text content. -func (m *commonMessageV5) SetAMPHtml(html string) { - m.specific.setAMPHtml(html) -} - -func (m *plainMessageV5) setAMPHtml(h string) { - m.ampHtml = h -} - -func (m *mimeMessageV5) 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 *commonMessageV5) AddTag(tag ...string) error { - if len(m.tags) >= MaxNumberOfTags { - return fmt.Errorf("cannot add any new tags. Message tag limit (%d) reached", MaxNumberOfTags) - } - - m.tags = append(m.tags, tag...) - return nil -} - -// SetTemplate sets the name of a template stored via the template API. -// See https://documentation.mailgun.com/en/latest/user_manual.html#templating -func (m *commonMessageV5) SetTemplate(t string) { - m.specific.setTemplate(t) -} - -func (m *plainMessageV5) setTemplate(t string) { - m.template = t -} - -func (m *mimeMessageV5) setTemplate(t string) {} - -// AddCampaign is no longer supported and is deprecated for new software. -func (m *commonMessageV5) 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 *commonMessageV5) SetDKIM(dkim bool) { @@ -375,6 +261,126 @@ func (m *commonMessageV5) GetHeaders() map[string]string { return m.headers } +// specific message methods + +func (m *plainMessageV5) 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 *plainMessageV5) AddRecipientAndVariables(r string, vars map[string]interface{}) error { + if m.RecipientCount() >= MaxNumberOfRecipients { + return fmt.Errorf("recipient limit exceeded (max %d)", MaxNumberOfRecipients) + } + m.to = append(m.to, r) + if vars != nil { + if m.recipientVariables == nil { + m.recipientVariables = make(map[string]map[string]interface{}) + } + m.recipientVariables[r] = vars + } + + return nil +} + +func (m *mimeMessageV5) 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 *plainMessageV5) RecipientCount() int { + return len(m.to) + len(m.bcc) + len(m.cc) +} + +func (m *mimeMessageV5) RecipientCount() int { + return len(m.to) +} + +// SetReplyTo sets the receiver who should receive replies +func (m *commonMessageV5) SetReplyTo(recipient string) { + m.AddHeader("Reply-To", recipient) +} + +func (m *plainMessageV5) AddCC(r string) { + m.cc = append(m.cc, r) +} + +func (m *mimeMessageV5) AddCC(_ string) {} + +func (m *plainMessageV5) AddBCC(r string) { + m.bcc = append(m.bcc, r) +} + +func (m *mimeMessageV5) AddBCC(_ string) {} + +func (m *plainMessageV5) SetHTML(h string) { + m.html = h +} + +func (m *mimeMessageV5) SetHTML(_ string) {} + +func (m *plainMessageV5) SetAmpHTML(h string) { + m.ampHtml = h +} + +func (m *mimeMessageV5) 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 *commonMessageV5) AddTag(tag ...string) error { + if len(m.tags) >= MaxNumberOfTags { + return fmt.Errorf("cannot add any new tags. Message tag limit (%d) reached", MaxNumberOfTags) + } + + m.tags = append(m.tags, tag...) + return nil +} + +func (m *plainMessageV5) SetTemplate(t string) { + m.template = t +} + +func (m *mimeMessageV5) SetTemplate(_ string) {} + +func (m *plainMessageV5) AddValues(p *formDataPayload) { + p.addValue("from", m.from) + p.addValue("subject", m.subject) + p.addValue("text", m.text) + for _, cc := range m.cc { + p.addValue("cc", cc) + } + for _, bcc := range m.bcc { + p.addValue("bcc", bcc) + } + if m.html != "" { + p.addValue("html", m.html) + } + if m.template != "" { + p.addValue("template", m.template) + } + if m.ampHtml != "" { + p.addValue("amp-html", m.ampHtml) + } +} + +func (m *mimeMessageV5) AddValues(p *formDataPayload) { + p.addReadCloser("message", "message.mime", m.body) +} + +func (m *plainMessageV5) Endpoint() string { + return messagesEndpoint +} + +func (m *mimeMessageV5) Endpoint() string { + return mimeMessagesEndpoint +} + // Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery. // It returns the Mailgun server response, which consists of two components: // - A human-readable status message, typically "Queued. Thank you." @@ -524,7 +530,7 @@ func (mg *MailgunImpl) sendV5(ctx context.Context, m messageIfaceV5) (mes string payload.addValue("t:text", yesNo(m.TemplateRenderText())) } - r := newHTTPRequest(generateApiUrlWithDomain(mg, m.endpoint(), m.Domain())) + r := newHTTPRequest(generateApiUrlWithDomain(mg, m.Endpoint(), m.Domain())) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.APIKey()) // Override any HTTP headers if provided @@ -539,50 +545,9 @@ func (mg *MailgunImpl) sendV5(ctx context.Context, m messageIfaceV5) (mes string id = response.Id } - r.mu.RLock() - defer r.mu.RUnlock() - if r.capturedCurlOutput != "" { - mg.mu.Lock() - defer mg.mu.Unlock() - mg.capturedCurlOutput = r.capturedCurlOutput - } - return } -func (m *plainMessageV5) addValues(p *formDataPayload) { - p.addValue("from", m.from) - p.addValue("subject", m.subject) - p.addValue("text", m.text) - for _, cc := range m.cc { - p.addValue("cc", cc) - } - for _, bcc := range m.bcc { - p.addValue("bcc", bcc) - } - if m.html != "" { - p.addValue("html", m.html) - } - if m.template != "" { - p.addValue("template", m.template) - } - if m.ampHtml != "" { - p.addValue("amp-html", m.ampHtml) - } -} - -func (m *mimeMessageV5) addValues(p *formDataPayload) { - p.addReadCloser("message", "message.mime", m.body) -} - -func (m *plainMessageV5) endpoint() string { - return messagesEndpoint -} - -func (m *mimeMessageV5) endpoint() string { - return mimeMessagesEndpoint -} - // isValid returns true if, and only if, // a Message instance is sufficiently initialized to send via the Mailgun interface. func isValidIface(m messageIfaceV5) bool { From f2b468b3db0f680910300563b704e87059ef1174 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:56:45 +0200 Subject: [PATCH 6/8] lint --- messages_v5.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages_v5.go b/messages_v5.go index 894bef5a..59986255 100644 --- a/messages_v5.go +++ b/messages_v5.go @@ -482,7 +482,7 @@ func (mg *MailgunImpl) sendV5(ctx context.Context, m messageIfaceV5) (mes string } } if m.TemplateVariables() != nil { - variableString, err := json.Marshal(m.TemplateVariables) + variableString, err := json.Marshal(m.TemplateVariables()) if err == nil { // the map was marshalled as json so add it payload.addValue("h:X-Mailgun-Variables", string(variableString)) From b71bd5d1a5e44260c02478fa90191ae53670abb7 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:57:40 +0200 Subject: [PATCH 7/8] TODO --- messages_types_v5.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/messages_types_v5.go b/messages_types_v5.go index c5ac8298..68e54c81 100644 --- a/messages_types_v5.go +++ b/messages_types_v5.go @@ -38,6 +38,7 @@ type commonMessageV5 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): rename to PlainMessage type plainMessageV5 struct { commonMessageV5 @@ -52,6 +53,7 @@ type plainMessageV5 struct { } // mimeMessage contains fields relevant to pre-packaged MIME messages. +// TODO(v5): rename to MimeMessage type mimeMessageV5 struct { commonMessageV5 From 93ba34b00fb90b3acae770ec7be22ab10035482d Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:02:44 +0200 Subject: [PATCH 8/8] removed specific --- messages_types_v5.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/messages_types_v5.go b/messages_types_v5.go index 68e54c81..e8be8f89 100644 --- a/messages_types_v5.go +++ b/messages_types_v5.go @@ -137,8 +137,5 @@ type messageIfaceV5 interface { RequireTLS() bool SkipVerification() bool - RecipientCount() int - AddValues(p *formDataPayload) - specificV5 }