Repository: mailgun/mailgun-go
Pull request #355 (2024-12-02 17:41:22)

DE-1344 Add getters to Message to get the current state of the object


Patch:
diff --git a/httphelpers.go b/httphelpers.go
index d7a24bc..0628250 100644
--- a/httphelpers.go
+++ b/httphelpers.go
@@ -60,7 +60,7 @@ type keyNameBuff struct {
 	value []byte
 }
 
-type formDataPayload struct {
+type FormDataPayload struct {
 	contentType string
 	Values      []keyValuePair
 	Files       []keyValuePair
@@ -146,31 +146,31 @@ func (r *httpResponse) parseFromJSON(v interface{}) error {
 	return json.Unmarshal(r.Data, v)
 }
 
-func newFormDataPayload() *formDataPayload {
-	return &formDataPayload{}
+func NewFormDataPayload() *FormDataPayload {
+	return &FormDataPayload{}
 }
 
-func (f *formDataPayload) getValues() []keyValuePair {
+func (f *FormDataPayload) getValues() []keyValuePair {
 	return f.Values
 }
 
-func (f *formDataPayload) addValue(key, value string) {
+func (f *FormDataPayload) addValue(key, value string) {
 	f.Values = append(f.Values, keyValuePair{key: key, value: value})
 }
 
-func (f *formDataPayload) addFile(key, file string) {
+func (f *FormDataPayload) addFile(key, file string) {
 	f.Files = append(f.Files, keyValuePair{key: key, value: file})
 }
 
-func (f *formDataPayload) addBuffer(key, file string, buff []byte) {
+func (f *FormDataPayload) addBuffer(key, file string, buff []byte) {
 	f.Buffers = append(f.Buffers, keyNameBuff{key: key, name: file, value: buff})
 }
 
-func (f *formDataPayload) addReadCloser(key, name string, rc io.ReadCloser) {
+func (f *FormDataPayload) addReadCloser(key, name string, rc io.ReadCloser) {
 	f.ReadClosers = append(f.ReadClosers, keyNameRC{key: key, name: name, value: rc})
 }
 
-func (f *formDataPayload) getPayloadBuffer() (*bytes.Buffer, error) {
+func (f *FormDataPayload) getPayloadBuffer() (*bytes.Buffer, error) {
 	data := &bytes.Buffer{}
 	writer := multipart.NewWriter(data)
 	defer writer.Close()
@@ -239,7 +239,7 @@ func (f *formDataPayload) getPayloadBuffer() (*bytes.Buffer, error) {
 	return data, nil
 }
 
-func (f *formDataPayload) getContentType() (string, error) {
+func (f *FormDataPayload) getContentType() (string, error) {
 	if f.contentType == "" {
 		_, err := f.getPayloadBuffer()
 		if err != nil {
diff --git a/mailgun.go b/mailgun.go
index e5c8146..ff9e4bd 100644
--- a/mailgun.go
+++ b/mailgun.go
@@ -140,6 +140,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)
 	ReSend(ctx context.Context, id string, recipients ...string) (string, string, error)
 	// Deprecated: use func NewMessage instead of method.
diff --git a/members.go b/members.go
index 1d94e1c..f3781a1 100644
--- a/members.go
+++ b/members.go
@@ -179,7 +179,7 @@ func (mg *MailgunImpl) CreateMember(ctx context.Context, merge bool, addr string
 	r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, addr))
 	r.setClient(mg.Client())
 	r.setBasicAuth(basicAuthUser, mg.APIKey())
-	p := newFormDataPayload()
+	p := NewFormDataPayload()
 	p.addValue("upsert", yesNo(merge))
 	p.addValue("address", prototype.Address)
 	p.addValue("name", prototype.Name)
@@ -197,7 +197,7 @@ func (mg *MailgunImpl) UpdateMember(ctx context.Context, s, l string, prototype
 	r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, l) + "/" + s)
 	r.setClient(mg.Client())
 	r.setBasicAuth(basicAuthUser, mg.APIKey())
-	p := newFormDataPayload()
+	p := NewFormDataPayload()
 	if prototype.Address != "" {
 		p.addValue("address", prototype.Address)
 	}
@@ -247,7 +247,7 @@ func (mg *MailgunImpl) CreateMemberList(ctx context.Context, u *bool, addr strin
 	r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, addr) + ".json")
 	r.setClient(mg.Client())
 	r.setBasicAuth(basicAuthUser, mg.APIKey())
-	p := newFormDataPayload()
+	p := NewFormDataPayload()
 	if u != nil {
 		p.addValue("upsert", yesNo(*u))
 	}
diff --git a/messages.go b/messages.go
index 8f92820..7e190a2 100644
--- a/messages.go
+++ b/messages.go
@@ -20,6 +20,7 @@ const MaxNumberOfRecipients = 1000
 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
@@ -49,7 +50,7 @@ type Message struct {
 	requireTLS       bool
 	skipVerification bool
 
-	specific features
+	Specific
 }
 
 type ReaderAttachment struct {
@@ -66,6 +67,8 @@ 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): rename to PlainMessage
+// TODO(v5): embed CommonMessage
 type plainMessage struct {
 	from     string
 	cc       []string
@@ -78,6 +81,8 @@ type plainMessage struct {
 }
 
 // mimeMessage contains fields relevant to pre-packaged MIME messages.
+// TODO(v5): rename to MimeMessage
+// TODO(v5): embed CommonMessage
 type mimeMessage struct {
 	body io.ReadCloser
 }
@@ -94,24 +99,54 @@ type TrackingOptions struct {
 	TrackingOpens  bool
 }
 
-// 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.
-type features interface {
-	addCC(string)
-	addBCC(string)
-	setHtml(string)
-	setAMPHtml(string)
-	addValues(*formDataPayload)
-	isValid() bool
-	endpoint() string
-	recipientCount() int
-	setTemplate(string)
+// Specific abstracts the common characteristics between regular and MIME messages.
+type Specific interface {
+	// AddCC appends a receiver to the carbon-copy header of a message.
+	AddCC(string)
+
+	// AddBCC appends a receiver to the blind-carbon-copy header of a message.
+	AddBCC(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)
+
+	// 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)
+
+	// AddValues invoked by Send() to add message-type-specific MIME headers for the API call
+	// to Mailgun.
+	AddValues(*FormDataPayload)
+
+	// IsValid yields true if and only if the message is valid enough for sending
+	// through the API.
+	IsValid() bool
+
+	// 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.
+	//
+	// 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 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.
@@ -127,9 +162,10 @@ type features interface {
 //
 // 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{
+		Specific: &plainMessage{
 			from:    from,
 			subject: subject,
 			text:    text,
@@ -160,9 +196,10 @@ func (*MailgunImpl) NewMessage(from, subject, text string, to ...string) *Messag
 //
 // 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{
+		Specific: &mimeMessage{
 			body: body,
 		},
 		to: to,
@@ -176,6 +213,102 @@ func (*MailgunImpl) NewMIMEMessage(body io.ReadCloser, to ...string) *Message {
 	return NewMIMEMessage(body, to...)
 }
 
+func (m *Message) Domain() string {
+	return m.domain
+}
+
+func (m *Message) To() []string {
+	return m.to
+}
+
+func (m *Message) Tags() []string {
+	return m.tags
+}
+
+func (m *Message) Campaigns() []string {
+	return m.campaigns
+}
+
+func (m *Message) DKIM() *bool {
+	return m.dkim
+}
+
+func (m *Message) DeliveryTime() time.Time {
+	return m.deliveryTime
+}
+
+func (m *Message) STOPeriod() string {
+	return m.stoPeriod
+}
+
+func (m *Message) Attachments() []string {
+	return m.attachments
+}
+
+func (m *Message) ReaderAttachments() []ReaderAttachment {
+	return m.readerAttachments
+}
+
+func (m *Message) Inlines() []string {
+	return m.inlines
+}
+
+func (m *Message) ReaderInlines() []ReaderAttachment {
+	return m.readerInlines
+}
+
+func (m *Message) BufferAttachments() []BufferAttachment {
+	return m.bufferAttachments
+}
+
+func (m *Message) NativeSend() bool {
+	return m.nativeSend
+}
+
+func (m *Message) TestMode() bool {
+	return m.testMode
+}
+
+func (m *Message) Tracking() *bool {
+	return m.tracking
+}
+
+func (m *Message) TrackingClicks() *string {
+	return m.trackingClicks
+}
+
+func (m *Message) TrackingOpens() *bool {
+	return m.trackingOpens
+}
+
+func (m *Message) Variables() map[string]string {
+	return m.variables
+}
+
+func (m *Message) TemplateVariables() map[string]interface{} {
+	return m.templateVariables
+}
+
+func (m *Message) RecipientVariables() map[string]map[string]interface{} {
+	return m.recipientVariables
+}
+
+func (m *Message) TemplateVersionTag() string {
+	return m.templateVersionTag
+}
+
+func (m *Message) TemplateRenderText() bool {
+	return m.templateRenderText
+}
+
+func (m *Message) RequireTLS() bool {
+	return m.requireTLS
+}
+
+func (m *Message) 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.
 // The filename parameter is the resulting filename of the attachment.
@@ -257,14 +390,15 @@ func (m *Message) AddRecipientAndVariables(r string, vars map[string]interface{}
 // 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()
+	return len(m.to) + m.Specific.RecipientCount()
 }
 
-func (pm *plainMessage) recipientCount() int {
-	return len(pm.bcc) + len(pm.cc)
+func (m *plainMessage) RecipientCount() int {
+	return len(m.bcc) + len(m.cc)
 }
 
-func (mm *mimeMessage) recipientCount() int {
+func (m *mimeMessage) RecipientCount() int {
+	// TODO(v5): 10 + len(m.to)
 	return 10
 }
 
@@ -273,58 +407,42 @@ func (m *Message) SetReplyTo(recipient string) {
 	m.AddHeader("Reply-To", recipient)
 }
 
-// AddCC appends a receiver to the carbon-copy header of a message.
-func (m *Message) AddCC(recipient string) {
-	m.specific.addCC(recipient)
+func (m *plainMessage) AddCC(r string) {
+	m.cc = append(m.cc, r)
 }
 
-func (pm *plainMessage) addCC(r string) {
-	pm.cc = append(pm.cc, r)
-}
-
-func (mm *mimeMessage) addCC(_ string) {}
+func (m *mimeMessage) AddCC(_ string) {}
 
-// AddBCC appends a receiver to the blind-carbon-copy header of a message.
-func (m *Message) AddBCC(recipient string) {
-	m.specific.addBCC(recipient)
+func (m *plainMessage) AddBCC(r string) {
+	m.bcc = append(m.bcc, r)
 }
 
-func (pm *plainMessage) addBCC(r string) {
-	pm.bcc = append(pm.bcc, r)
-}
-
-func (mm *mimeMessage) addBCC(_ string) {}
-
-// SetHTML is a helper. If you're sending a message that isn't already MIME encoded, SetHtml() will arrange to bundle
-// an HTML representation of your message in addition to your plain-text body.
-func (m *Message) SetHTML(html string) {
-	m.specific.setHtml(html)
-}
+func (m *mimeMessage) AddBCC(_ string) {}
 
 // Deprecated: use SetHTML instead.
 //
 // TODO(v5): remove this method
 func (m *Message) SetHtml(html string) {
-	m.specific.setHtml(html)
+	m.SetHTML(html)
 }
 
-func (pm *plainMessage) setHtml(h string) {
-	pm.html = h
+func (m *plainMessage) SetHTML(h string) {
+	m.html = h
 }
 
-func (mm *mimeMessage) setHtml(_ string) {}
+func (m *mimeMessage) 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.
+// Deprecated: use SetAmpHTML instead.
+// TODO(v5): remove this method
 func (m *Message) SetAMPHtml(html string) {
-	m.specific.setAMPHtml(html)
+	m.SetAmpHTML(html)
 }
 
-func (pm *plainMessage) setAMPHtml(h string) {
-	pm.ampHtml = h
+func (m *plainMessage) SetAmpHTML(h string) {
+	m.ampHtml = h
 }
 
-func (mm *mimeMessage) setAMPHtml(_ string) {}
+func (m *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.
@@ -337,19 +455,14 @@ func (m *Message) AddTag(tag ...string) error {
 	return nil
 }
 
-// SetTemplate sets the name of a template stored via the template API.
-// See https://documentation.mailgun.com/en/latest/user_manual.html#templating
-func (m *Message) SetTemplate(t string) {
-	m.specific.setTemplate(t)
-}
-
-func (pm *plainMessage) setTemplate(t string) {
-	pm.template = t
+func (m *plainMessage) SetTemplate(t string) {
+	m.template = t
 }
 
-func (mm *mimeMessage) setTemplate(t string) {}
+func (m *mimeMessage) SetTemplate(_ string) {}
 
-// AddCampaign is no longer supported and is deprecated for new software.
+// 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)
 }
@@ -497,7 +610,13 @@ func (m *Message) AddDomain(domain string) {
 	m.domain = domain
 }
 
-// GetHeaders retrieves the http headers associated with this message
+// 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 {
 	return m.headers
 }
@@ -505,6 +624,38 @@ func (m *Message) GetHeaders() map[string]string {
 // ErrInvalidMessage is returned by `Send()` when the `mailgun.Message` 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
+	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
+}
+
 // 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."
@@ -525,7 +676,8 @@ var ErrInvalidMessage = errors.New("message not valid")
 //	  Data:     "Domain not found: example.com",
 //	}
 //
-//	See the public mailgun documentation for all possible return codes and error messages
+// 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 string, id string, err error) {
 	if mg.domain == "" {
 		err = errors.New("you must provide a valid domain before calling Send()")
@@ -548,99 +700,99 @@ func (mg *MailgunImpl) Send(ctx context.Context, m *Message) (mes string, id str
 		return
 	}
 
-	if m.stoPeriod != "" && m.RecipientCount() > 1 {
+	if m.STOPeriod() != "" && m.RecipientCount() > 1 {
 		err = errors.New("STO can only be used on a per-message basis")
 		return
 	}
-	payload := newFormDataPayload()
+	payload := NewFormDataPayload()
 
-	m.specific.addValues(payload)
-	for _, to := range m.to {
+	m.AddValues(payload)
+	for _, to := range m.To() {
 		payload.addValue("to", to)
 	}
-	for _, tag := range m.tags {
+	for _, tag := range m.Tags() {
 		payload.addValue("o:tag", tag)
 	}
-	for _, campaign := range m.campaigns {
+	for _, campaign := range m.Campaigns() {
 		payload.addValue("o:campaign", campaign)
 	}
-	if m.dkim != nil {
-		payload.addValue("o:dkim", yesNo(*m.dkim))
+	if m.DKIM() != nil {
+		payload.addValue("o:dkim", yesNo(*m.DKIM()))
 	}
-	if !m.deliveryTime.IsZero() {
-		payload.addValue("o:deliverytime", formatMailgunTime(m.deliveryTime))
+	if !m.DeliveryTime().IsZero() {
+		payload.addValue("o:deliverytime", formatMailgunTime(m.DeliveryTime()))
 	}
-	if m.stoPeriod != "" {
-		payload.addValue("o:deliverytime-optimize-period", m.stoPeriod)
+	if m.STOPeriod() != "" {
+		payload.addValue("o:deliverytime-optimize-period", m.STOPeriod())
 	}
-	if m.nativeSend {
+	if m.NativeSend() {
 		payload.addValue("o:native-send", "yes")
 	}
-	if m.testMode {
+	if m.TestMode() {
 		payload.addValue("o:testmode", "yes")
 	}
-	if m.tracking != nil {
-		payload.addValue("o:tracking", yesNo(*m.tracking))
+	if m.Tracking() != nil {
+		payload.addValue("o:tracking", yesNo(*m.Tracking()))
 	}
-	if m.trackingClicks != nil {
-		payload.addValue("o:tracking-clicks", *m.trackingClicks)
+	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.TrackingOpens() != nil {
+		payload.addValue("o:tracking-opens", yesNo(*m.TrackingOpens()))
 	}
-	if m.requireTLS {
-		payload.addValue("o:require-tls", trueFalse(m.requireTLS))
+	if m.RequireTLS() {
+		payload.addValue("o:require-tls", trueFalse(m.RequireTLS()))
 	}
-	if m.skipVerification {
-		payload.addValue("o:skip-verification", trueFalse(m.skipVerification))
+	if m.SkipVerification() {
+		payload.addValue("o:skip-verification", trueFalse(m.SkipVerification()))
 	}
-	if m.headers != nil {
-		for header, value := range m.headers {
+	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 {
+	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 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 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 {
+	if m.Attachments() != nil {
+		for _, attachment := range m.Attachments() {
 			payload.addFile("attachment", attachment)
 		}
 	}
-	if m.readerAttachments != nil {
-		for _, readerAttachment := range m.readerAttachments {
+	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 {
+	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 {
+	if m.Inlines() != nil {
+		for _, inline := range m.Inlines() {
 			payload.addFile("inline", inline)
 		}
 	}
 
-	if m.readerInlines != nil {
-		for _, readerAttachment := range m.readerInlines {
+	if m.ReaderInlines() != nil {
+		for _, readerAttachment := range m.ReaderInlines() {
 			payload.addReadCloser("inline", readerAttachment.Filename, readerAttachment.ReadCloser)
 		}
 	}
@@ -649,15 +801,15 @@ func (mg *MailgunImpl) Send(ctx context.Context, m *Message) (mes string, id str
 		m.domain = mg.Domain()
 	}
 
-	if m.templateVersionTag != "" {
-		payload.addValue("t:version", m.templateVersionTag)
+	if m.TemplateVersionTag() != "" {
+		payload.addValue("t:version", m.TemplateVersionTag())
 	}
 
-	if m.templateRenderText {
-		payload.addValue("t:text", yesNo(m.templateRenderText))
+	if m.TemplateRenderText() {
+		payload.addValue("t:text", yesNo(m.TemplateRenderText()))
 	}
 
-	r := newHTTPRequest(generateApiUrlWithDomain(mg, m.specific.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
@@ -683,36 +835,36 @@ func (mg *MailgunImpl) Send(ctx context.Context, m *Message) (mes string, id str
 	return
 }
 
-func (pm *plainMessage) addValues(p *formDataPayload) {
-	p.addValue("from", pm.from)
-	p.addValue("subject", pm.subject)
-	p.addValue("text", pm.text)
-	for _, cc := range pm.cc {
+func (m *plainMessage) 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 pm.bcc {
+	for _, bcc := range m.bcc {
 		p.addValue("bcc", bcc)
 	}
-	if pm.html != "" {
-		p.addValue("html", pm.html)
+	if m.html != "" {
+		p.addValue("html", m.html)
 	}
-	if pm.template != "" {
-		p.addValue("template", pm.template)
+	if m.template != "" {
+		p.addValue("template", m.template)
 	}
-	if pm.ampHtml != "" {
-		p.addValue("amp-html", pm.ampHtml)
+	if m.ampHtml != "" {
+		p.addValue("amp-html", m.ampHtml)
 	}
 }
 
-func (mm *mimeMessage) addValues(p *formDataPayload) {
-	p.addReadCloser("message", "message.mime", mm.body)
+func (m *mimeMessage) AddValues(p *FormDataPayload) {
+	p.addReadCloser("message", "message.mime", m.body)
 }
 
-func (pm *plainMessage) endpoint() string {
+func (m *plainMessage) Endpoint() string {
 	return messagesEndpoint
 }
 
-func (mm *mimeMessage) endpoint() string {
+func (m *mimeMessage) Endpoint() string {
 	return mimeMessagesEndpoint
 }
 
@@ -730,12 +882,12 @@ func trueFalse(b bool) string {
 
 // isValid returns true if, and only if,
 // a Message instance is sufficiently initialized to send via the Mailgun interface.
-func isValid(m *Message) bool {
+func isValid(m SendableMessage) bool {
 	if m == nil {
 		return false
 	}
 
-	if !m.specific.isValid() {
+	if !m.IsValid() {
 		return false
 	}
 
@@ -743,44 +895,44 @@ func isValid(m *Message) bool {
 		return false
 	}
 
-	if !validateStringList(m.tags, false) {
+	if !validateStringList(m.Tags(), false) {
 		return false
 	}
 
-	if !validateStringList(m.campaigns, false) || len(m.campaigns) > 3 {
+	if !validateStringList(m.Campaigns(), false) || len(m.Campaigns()) > 3 {
 		return false
 	}
 
 	return true
 }
 
-func (pm *plainMessage) isValid() bool {
-	if !validateStringList(pm.cc, false) {
+func (m *plainMessage) IsValid() bool {
+	if !validateStringList(m.cc, false) {
 		return false
 	}
 
-	if !validateStringList(pm.bcc, false) {
+	if !validateStringList(m.bcc, false) {
 		return false
 	}
 
-	if pm.template != "" {
-		// pm.text or pm.html not needed if template is supplied
-		return true
+	if m.from == "" {
+		return false
 	}
 
-	if pm.from == "" {
-		return false
+	if m.template != "" {
+		// m.text or m.html not needed if template is supplied
+		return true
 	}
 
-	if pm.text == "" && pm.html == "" {
+	if m.text == "" && m.html == "" {
 		return false
 	}
 
 	return true
 }
 
-func (mm *mimeMessage) isValid() bool {
-	return mm.body != nil
+func (m *mimeMessage) IsValid() bool {
+	return m.body != nil
 }
 
 // validateStringList returns true if, and only if,
diff --git a/messages_types_v5.go b/messages_types_v5.go
new file mode 100644
index 0000000..5b196c2
--- /dev/null
+++ b/messages_types_v5.go
@@ -0,0 +1,63 @@
+package mailgun
+
+// This file contains a draft for new v5 messages.
+
+import (
+	"io"
+	"time"
+)
+
+// Message structures contain both the message text and the envelope for an e-mail message.
+// TODO(v5): rename to CommonMessage
+type commonMessageV5 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
+	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
+}
+
+// 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.
+// TODO(v5): rename to PlainMessage
+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.
+// TODO(v5): rename to MimeMessage
+type mimeMessageV5 struct {
+	commonMessageV5
+
+	body io.ReadCloser
+}
diff --git a/messages_v5.go b/messages_v5.go
new file mode 100644
index 0000000..27529b1
--- /dev/null
+++ b/messages_v5.go
@@ -0,0 +1,572 @@
+package mailgun
+
+// This file contains methods for new v5 messages.
+
+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)
+}
+
+// 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
+}
+
+// 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."
+//   - 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 SendableMessage) (mes string, id string, err error) {
+	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
+	}
+
+	if !isValid(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
+	}
+
+	return
+}
+
+func (m *plainMessageV5) isValid() bool {
+	if !validateStringList(m.cc, false) {
+		return false
+	}
+
+	if !validateStringList(m.bcc, false) {
+		return false
+	}
+
+	if m.from == "" {
+		return false
+	}
+
+	if m.template != "" {
+		// m.text or m.html not needed if template is supplied
+		return true
+	}
+
+	if m.text == "" && m.html == "" {
+		return false
+	}
+
+	return true
+}
+
+func (m *mimeMessageV5) isValid() bool {
+	return m.body != nil
+}
diff --git a/stored_messages.go b/stored_messages.go
index 5d1bc39..ac224b1 100644
--- a/stored_messages.go
+++ b/stored_messages.go
@@ -67,7 +67,7 @@ func (mg *MailgunImpl) ReSend(ctx context.Context, url string, recipients ...str
 	r.setClient(mg.Client())
 	r.setBasicAuth(basicAuthUser, mg.APIKey())
 
-	payload := newFormDataPayload()
+	payload := NewFormDataPayload()
 
 	if len(recipients) == 0 {
 		return "", "", errors.New("must provide at least one recipient")


Test patch:
diff --git a/messages_test.go b/messages_test.go
index dc0fe65..e56f663 100644
--- a/messages_test.go
+++ b/messages_test.go
@@ -156,7 +156,7 @@ func TestSendMGAMPHtml(t *testing.T) {
 		ctx := context.Background()
 		m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
 		m.SetHTML(exampleHtml)
-		m.SetAMPHtml(exampleAMPHtml)
+		m.SetAmpHTML(exampleAMPHtml)
 		msg, id, err := mg.Send(ctx, m)
 		require.NoError(t, err)
 		t.Log("TestSendHtml:MSG(" + msg + "),ID(" + id + ")")
@@ -777,3 +777,9 @@ func TestSendTemplateOptions(t *testing.T) {
 	assert.Equal(t, exampleMessage, msg)
 	assert.Equal(t, exampleID, id)
 }
+
+func TestSendableMessageIface(t *testing.T) {
+	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText)
+
+	assert.Implements(t, (*mailgun.SendableMessage)(nil), m)
+}