Repository: mailgun/mailgun-go
Pull request #368 (2024-12-15 18:05:53)

DE-1388 (mg *MailgunImpl) Send should accept interface, not struct
https://bryanftan.medium.com/accept-interfaces-return-structs-in-go-d4cab29a301b


Patch:
diff --git a/README.md b/README.md
index f78c9fe..f540720 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ func main() {
 	recipient := "recipient@example.com"
 
 	// The message object allows you to add attachments and Bcc recipients
-	message := mailgun.NewMessage(sender, subject, body, recipient)
+	message := mailgun.NewMessage(yourDomain, sender, subject, body, recipient)
 
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
 	defer cancel()
@@ -290,7 +290,7 @@ func main() {
 	subject := "HTML email!"
 	recipient := "recipient@example.com"
 
-	message := mailgun.NewMessage(sender, subject, "", recipient)
+	message := mailgun.NewMessage(yourDomain, sender, subject, "", recipient)
 	body := `
 <html>
 <body>
@@ -351,7 +351,7 @@ func main() {
 	recipient := "recipient@example.com"
 
 	// The message object allows you to add attachments and Bcc recipients
-	message := mailgun.NewMessage(sender, subject, body, recipient)
+	message := mailgun.NewMessage(yourDomain, sender, subject, body, recipient)
 	message.SetTemplate("passwordReset")
 	err := message.AddTemplateVariable("passwordResetLink", "some link to your site unique to your user")
 	if err != nil {
diff --git a/examples/examples.go b/examples/examples.go
index 8c43abe..da15c7e 100644
--- a/examples/examples.go
+++ b/examples/examples.go
@@ -779,7 +779,7 @@ func SendMimeMessage(domain, apiKey string) (string, error) {
 		return "", err
 	}
 
-	m := mailgun.NewMIMEMessage(mimeMsgReader, "bar@example.com")
+	m := mailgun.NewMIMEMessage(domain, mimeMsgReader, "bar@example.com")
 
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
 	defer cancel()
@@ -824,6 +824,7 @@ func SendSimpleMessage(domain, apiKey string) (string, error) {
 func SendTaggedMessage(domain, apiKey string) (string, error) {
 	mg := mailgun.NewMailgun(domain, apiKey)
 	m := mailgun.NewMessage(
+		domain,
 		"Excited User <YOU@YOUR_DOMAIN_NAME>",
 		"Hello",
 		"Testing some Mailgun awesomeness!",
@@ -845,6 +846,7 @@ func SendTaggedMessage(domain, apiKey string) (string, error) {
 func SendTemplateMessage(domain, apiKey string) (string, error) {
 	mg := mailgun.NewMailgun(domain, apiKey)
 	m := mailgun.NewMessage(
+		domain,
 		"Excited User <YOU@YOUR_DOMAIN_NAME>",
 		"Hey %recipient.first%",
 		"If you wish to unsubscribe, click http://mailgun/unsubscribe/%recipient.id%",
@@ -939,7 +941,7 @@ func SendMessageWithTemplate(domain, apiKey string) error {
 	time.Sleep(time.Second * 1)
 
 	// Create a new message with template
-	m := mailgun.NewMessage("Excited User <excited@example.com>", "Template example", "")
+	m := mailgun.NewMessage(domain, "Excited User <excited@example.com>", "Template example", "")
 	m.SetTemplate("my-template")
 
 	// Add recipients
diff --git a/mailgun.go b/mailgun.go
index a11927c..2b8bf7f 100644
--- a/mailgun.go
+++ b/mailgun.go
@@ -139,9 +139,8 @@ type Mailgun interface {
 	AddOverrideHeader(k string, v string)
 	GetCurlOutput() string
 
-	// Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery.
-	// TODO(v5): switch m to SendableMessage interface
-	Send(ctx context.Context, m *Message) (mes string, id string, err error)
+	// Send attempts to queue a message (see CommonMessage, NewMessage, and its methods) for delivery.
+	Send(ctx context.Context, m SendableMessage) (mes string, id string, err error)
 	ReSend(ctx context.Context, id string, recipients ...string) (string, string, error)
 
 	ListBounces(opts *ListOptions) *BouncesIterator
diff --git a/messages.go b/messages.go
index c68bcf1..918e31b 100644
--- a/messages.go
+++ b/messages.go
@@ -19,21 +19,19 @@ const MaxNumberOfRecipients = 1000
 // MaxNumberOfTags represents the maximum number of tags that can be added for a message
 const MaxNumberOfTags = 3
 
-// Message structures contain both the message text and the envelope for an e-mail message.
-// TODO(v5): rename to CommonMessage and remove Specific
-type Message struct {
-	to                []string
-	tags              []string
-	campaigns         []string
-	dkim              *bool
-	deliveryTime      time.Time
-	stoPeriod         string
-	attachments       []string
-	readerAttachments []ReaderAttachment
-	inlines           []string
-	readerInlines     []ReaderAttachment
-	bufferAttachments []BufferAttachment
-
+// CommonMessage structures contain both the message text and the envelope for an e-mail message.
+type CommonMessage struct {
+	domain             string
+	to                 []string
+	tags               []string
+	dkim               *bool
+	deliveryTime       time.Time
+	stoPeriod          string
+	attachments        []string
+	readerAttachments  []ReaderAttachment
+	inlines            []string
+	readerInlines      []ReaderAttachment
+	bufferAttachments  []BufferAttachment
 	nativeSend         bool
 	testMode           bool
 	tracking           *bool
@@ -43,14 +41,10 @@ type Message struct {
 	variables          map[string]string
 	templateVariables  map[string]any
 	recipientVariables map[string]map[string]any
-	domain             string
 	templateVersionTag string
 	templateRenderText bool
-
-	requireTLS       bool
-	skipVerification bool
-
-	Specific
+	requireTLS         bool
+	skipVerification   bool
 }
 
 type ReaderAttachment struct {
@@ -67,8 +61,9 @@ type BufferAttachment struct {
 // You're expected to use various setters to set most of these attributes,
 // although from, subject, and text are set when the message is created with
 // NewMessage.
-// TODO(v5): embed CommonMessage
 type PlainMessage struct {
+	CommonMessage
+
 	from     string
 	cc       []string
 	bcc      []string
@@ -112,8 +107,9 @@ func (m *PlainMessage) Template() string {
 }
 
 // MimeMessage contains fields relevant to pre-packaged MIME messages.
-// TODO(v5): embed CommonMessage
 type MimeMessage struct {
+	CommonMessage
+
 	body io.ReadCloser
 }
 
@@ -122,14 +118,14 @@ type sendMessageResponse struct {
 	Id      string `json:"id"`
 }
 
-// TrackingOptions contains fields relevant to trackings.
+// TrackingOptions contains fields relevant to tracking.
 type TrackingOptions struct {
 	Tracking       bool
 	TrackingClicks string
 	TrackingOpens  bool
 }
 
-// Specific abstracts the common characteristics between regular and MIME messages.
+// Specific abstracts the common characteristics between plain text and MIME messages.
 type Specific interface {
 	// AddCC appends a receiver to the carbon-copy header of a message.
 	AddCC(string)
@@ -142,7 +138,7 @@ type Specific interface {
 	SetHTML(string)
 
 	// SetAmpHTML If you're sending a message that isn't already MIME encoded, it will arrange to bundle
-	// an AMP-For-Email representation of your message in addition to your html & plain-text content.
+	// an AMP-For-Email representation of your message in addition to your HTML & plain-text content.
 	SetAmpHTML(string)
 
 	// AddValues invoked by Send() to add message-type-specific MIME headers for the API call
@@ -172,11 +168,6 @@ type Specific interface {
 	// SetTemplate sets the name of a template stored via the template API.
 	// See https://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/#templates
 	SetTemplate(string)
-
-	// // TODO(v5):
-	// // AddRecipient appends a receiver to the To: header of a message.
-	// // It will return an error if the limit of recipients have been exceeded for this message
-	// AddRecipient(recipient string) error
 }
 
 // NewMessage returns a new e-mail message with the simplest envelop needed to send.
@@ -188,19 +179,20 @@ type Specific interface {
 // You can do this explicitly, or implicitly, as follows:
 //
 //	// Note absence of `to` parameter(s)!
-//	m := NewMessage("me@example.com", "Help save our planet", "Hello world!")
+//	m := NewMessage("example.com", "me@example.com", "Help save our planet", "Hello world!")
 //
 // Note that you'll need to invoke the AddRecipientAndVariables or AddRecipient method
 // before sending, though.
-// TODO(v5): should return PlainMessage
-func NewMessage(from, subject, text string, to ...string) *Message {
-	return &Message{
-		Specific: &PlainMessage{
-			from:    from,
-			subject: subject,
-			text:    text,
+func NewMessage(domain, from, subject, text string, to ...string) *PlainMessage {
+	return &PlainMessage{
+		CommonMessage: CommonMessage{
+			domain: domain,
+			to:     to,
 		},
-		to: to,
+
+		from:    from,
+		subject: subject,
+		text:    text,
 	}
 }
 
@@ -215,122 +207,118 @@ func NewMessage(from, subject, text string, to ...string) *Message {
 // You can do this explicitly, or implicitly, as follows:
 //
 //	// Note absence of `to` parameter(s)!
-//	m := NewMIMEMessage(body)
+//	m := NewMIMEMessage(domain, body)
 //
 // Note that you'll need to invoke the AddRecipientAndVariables or AddRecipient method
 // before sending, though.
-// TODO(v5): should return MimeMessage
-func NewMIMEMessage(body io.ReadCloser, to ...string) *Message {
-	return &Message{
-		Specific: &MimeMessage{
-			body: body,
+func NewMIMEMessage(domain string, body io.ReadCloser, to ...string) *MimeMessage {
+	return &MimeMessage{
+		CommonMessage: CommonMessage{
+			domain: domain,
+			to:     to,
 		},
-		to: to,
+		body: body,
 	}
 }
 
-func (m *Message) Domain() string {
+func (m *CommonMessage) Domain() string {
 	return m.domain
 }
 
-func (m *Message) To() []string {
+func (m *CommonMessage) To() []string {
 	return m.to
 }
 
-func (m *Message) Tags() []string {
+func (m *CommonMessage) Tags() []string {
 	return m.tags
 }
 
-func (m *Message) Campaigns() []string {
-	return m.campaigns
-}
-
-func (m *Message) DKIM() *bool {
+func (m *CommonMessage) DKIM() *bool {
 	return m.dkim
 }
 
-func (m *Message) DeliveryTime() time.Time {
+func (m *CommonMessage) DeliveryTime() time.Time {
 	return m.deliveryTime
 }
 
-func (m *Message) STOPeriod() string {
+func (m *CommonMessage) STOPeriod() string {
 	return m.stoPeriod
 }
 
-func (m *Message) Attachments() []string {
+func (m *CommonMessage) Attachments() []string {
 	return m.attachments
 }
 
-func (m *Message) ReaderAttachments() []ReaderAttachment {
+func (m *CommonMessage) ReaderAttachments() []ReaderAttachment {
 	return m.readerAttachments
 }
 
-func (m *Message) Inlines() []string {
+func (m *CommonMessage) Inlines() []string {
 	return m.inlines
 }
 
-func (m *Message) ReaderInlines() []ReaderAttachment {
+func (m *CommonMessage) ReaderInlines() []ReaderAttachment {
 	return m.readerInlines
 }
 
-func (m *Message) BufferAttachments() []BufferAttachment {
+func (m *CommonMessage) BufferAttachments() []BufferAttachment {
 	return m.bufferAttachments
 }
 
-func (m *Message) NativeSend() bool {
+func (m *CommonMessage) NativeSend() bool {
 	return m.nativeSend
 }
 
-func (m *Message) TestMode() bool {
+func (m *CommonMessage) TestMode() bool {
 	return m.testMode
 }
 
-func (m *Message) Tracking() *bool {
+func (m *CommonMessage) Tracking() *bool {
 	return m.tracking
 }
 
-func (m *Message) TrackingClicks() *string {
+func (m *CommonMessage) TrackingClicks() *string {
 	return m.trackingClicks
 }
 
-func (m *Message) TrackingOpens() *bool {
+func (m *CommonMessage) TrackingOpens() *bool {
 	return m.trackingOpens
 }
 
-func (m *Message) Variables() map[string]string {
+func (m *CommonMessage) Variables() map[string]string {
 	return m.variables
 }
 
-func (m *Message) TemplateVariables() map[string]any {
+func (m *CommonMessage) TemplateVariables() map[string]any {
 	return m.templateVariables
 }
 
-func (m *Message) RecipientVariables() map[string]map[string]any {
+func (m *CommonMessage) RecipientVariables() map[string]map[string]any {
 	return m.recipientVariables
 }
 
-func (m *Message) TemplateVersionTag() string {
+func (m *CommonMessage) TemplateVersionTag() string {
 	return m.templateVersionTag
 }
 
-func (m *Message) TemplateRenderText() bool {
+func (m *CommonMessage) TemplateRenderText() bool {
 	return m.templateRenderText
 }
 
-func (m *Message) RequireTLS() bool {
+func (m *CommonMessage) RequireTLS() bool {
 	return m.requireTLS
 }
 
-func (m *Message) SkipVerification() bool {
+func (m *CommonMessage) SkipVerification() bool {
 	return m.skipVerification
 }
 
 // AddReaderAttachment arranges to send a file along with the e-mail message.
-// File contents are read from a io.ReadCloser.
+// File contents are read from an io.ReadCloser.
 // The filename parameter is the resulting filename of the attachment.
-// The readCloser parameter is the io.ReadCloser which reads the actual bytes to be used
+// The readCloser parameter is the io.ReadCloser that reads the actual bytes to be used
 // as the contents of the attached file.
-func (m *Message) AddReaderAttachment(filename string, readCloser io.ReadCloser) {
+func (m *CommonMessage) AddReaderAttachment(filename string, readCloser io.ReadCloser) {
 	ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser}
 	m.readerAttachments = append(m.readerAttachments, ra)
 }
@@ -340,7 +328,7 @@ func (m *Message) AddReaderAttachment(filename string, readCloser io.ReadCloser)
 // The filename parameter is the resulting filename of the attachment.
 // The buffer parameter is the []byte array which contains the actual bytes to be used
 // as the contents of the attached file.
-func (m *Message) AddBufferAttachment(filename string, buffer []byte) {
+func (m *CommonMessage) AddBufferAttachment(filename string, buffer []byte) {
 	ba := BufferAttachment{Filename: filename, Buffer: buffer}
 	m.bufferAttachments = append(m.bufferAttachments, ba)
 }
@@ -348,16 +336,16 @@ func (m *Message) AddBufferAttachment(filename string, buffer []byte) {
 // AddAttachment arranges to send a file from the filesystem along with the e-mail message.
 // The attachment parameter is a filename, which must refer to a file which actually resides
 // in the local filesystem.
-func (m *Message) AddAttachment(attachment string) {
+func (m *CommonMessage) AddAttachment(attachment string) {
 	m.attachments = append(m.attachments, attachment)
 }
 
 // AddReaderInline arranges to send a file along with the e-mail message.
-// File contents are read from a io.ReadCloser.
+// File contents are read from an io.ReadCloser.
 // The filename parameter is the resulting filename of the attachment.
-// The readCloser parameter is the io.ReadCloser which reads the actual bytes to be used
+// The readCloser parameter is the io.ReadCloser that reads the actual bytes to be used
 // as the contents of the attached file.
-func (m *Message) AddReaderInline(filename string, readCloser io.ReadCloser) {
+func (m *CommonMessage) AddReaderInline(filename string, readCloser io.ReadCloser) {
 	ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser}
 	m.readerInlines = append(m.readerInlines, ra)
 }
@@ -367,20 +355,20 @@ func (m *Message) AddReaderInline(filename string, readCloser io.ReadCloser) {
 // can be used to send image or font data along with an HTML-encoded message body.
 // The attachment parameter is a filename, which must refer to a file which actually resides
 // in the local filesystem.
-func (m *Message) AddInline(inline string) {
+func (m *CommonMessage) AddInline(inline string) {
 	m.inlines = append(m.inlines, inline)
 }
 
 // AddRecipient appends a receiver to the To: header of a message.
-// It will return an error if the limit of recipients have been exceeded for this message
-func (m *Message) AddRecipient(recipient string) error {
+// It will return an error if the limit of recipients has been exceeded for this message
+func (m *PlainMessage) AddRecipient(recipient string) error {
 	return m.AddRecipientAndVariables(recipient, nil)
 }
 
 // AddRecipientAndVariables appends a receiver to the To: header of a message,
 // and as well attaches a set of variables relevant for this recipient.
-// It will return an error if the limit of recipients have been exceeded for this message
-func (m *Message) AddRecipientAndVariables(r string, vars map[string]any) error {
+// It will return an error if the limit of recipients has been exceeded for this message
+func (m *PlainMessage) AddRecipientAndVariables(r string, vars map[string]any) error {
 	if m.RecipientCount() >= MaxNumberOfRecipients {
 		return fmt.Errorf("recipient limit exceeded (max %d)", MaxNumberOfRecipients)
 	}
@@ -394,32 +382,25 @@ func (m *Message) AddRecipientAndVariables(r string, vars map[string]any) error
 	return nil
 }
 
-// RecipientCount returns the total number of recipients for the message.
-// This includes To:, Cc:, and Bcc: fields.
-//
-// NOTE: At present, this method is reliable only for non-MIME messages, as the
-// Bcc: and Cc: fields are easily accessible.
-// For MIME messages, only the To: field is considered.
-// A fix for this issue is planned for a future release.
-// For now, MIME messages are always assumed to have 10 recipients between Cc: and Bcc: fields.
-// If your MIME messages have more than 10 non-To: field recipients,
-// you may find that some recipients will not receive your e-mail.
-// It's perfectly OK, of course, for a MIME message to not have any Cc: or Bcc: recipients.
-func (m *Message) RecipientCount() int {
-	return len(m.To()) + m.Specific.RecipientCount()
+func (m *MimeMessage) AddRecipient(recipient string) error {
+	if m.RecipientCount() >= MaxNumberOfRecipients {
+		return fmt.Errorf("recipient limit exceeded (max %d)", MaxNumberOfRecipients)
+	}
+	m.to = append(m.to, recipient)
+
+	return nil
 }
 
 func (m *PlainMessage) RecipientCount() int {
-	return len(m.BCC()) + len(m.CC())
+	return len(m.To()) + len(m.BCC()) + len(m.CC())
 }
 
-func (*MimeMessage) RecipientCount() int {
-	// TODO(v5): 10 + len(m.To())
-	return 10
+func (m *MimeMessage) RecipientCount() int {
+	return 10 + len(m.To())
 }
 
 // SetReplyTo sets the receiver who should receive replies
-func (m *Message) SetReplyTo(recipient string) {
+func (m *CommonMessage) SetReplyTo(recipient string) {
 	m.AddHeader("Reply-To", recipient)
 }
 
@@ -435,25 +416,12 @@ func (m *PlainMessage) AddBCC(r string) {
 
 func (*MimeMessage) AddBCC(_ string) {}
 
-// Deprecated: use SetHTML instead.
-//
-// TODO(v5): remove this method
-func (m *Message) SetHtml(html string) {
-	m.SetHTML(html)
-}
-
 func (m *PlainMessage) SetHTML(h string) {
 	m.html = h
 }
 
 func (*MimeMessage) SetHTML(_ string) {}
 
-// Deprecated: use SetAmpHTML instead.
-// TODO(v5): remove this method
-func (m *Message) SetAMPHtml(html string) {
-	m.SetAmpHTML(html)
-}
-
 func (m *PlainMessage) SetAmpHTML(h string) {
 	m.ampHtml = h
 }
@@ -462,7 +430,7 @@ func (*MimeMessage) SetAmpHTML(_ string) {}
 
 // AddTag attaches tags to the message.  Tags are useful for metrics gathering and event tracking purposes.
 // Refer to the Mailgun documentation for further details.
-func (m *Message) AddTag(tag ...string) error {
+func (m *CommonMessage) AddTag(tag ...string) error {
 	if len(m.Tags()) >= MaxNumberOfTags {
 		return fmt.Errorf("cannot add any new tags. Message tag limit (%d) reached", MaxNumberOfTags)
 	}
@@ -477,34 +445,28 @@ func (m *PlainMessage) SetTemplate(t string) {
 
 func (*MimeMessage) SetTemplate(_ string) {}
 
-// Deprecated: is no longer supported and is deprecated for new software.
-// TODO(v5): remove this method.
-func (m *Message) AddCampaign(campaign string) {
-	m.campaigns = append(m.Campaigns(), campaign)
-}
-
 // SetDKIM arranges to send the o:dkim header with the message, and sets its value accordingly.
 // Refer to the Mailgun documentation for more information.
-func (m *Message) SetDKIM(dkim bool) {
+func (m *CommonMessage) SetDKIM(dkim bool) {
 	m.dkim = &dkim
 }
 
-// EnableNativeSend allows the return path to match the address in the Message.Headers.From:
+// EnableNativeSend allows the return path to match the address in the CommonMessage.Headers.From:
 // field when sending from Mailgun rather than the usual bounce+ address in the return path.
-func (m *Message) EnableNativeSend() {
+func (m *CommonMessage) EnableNativeSend() {
 	m.nativeSend = true
 }
 
 // EnableTestMode allows submittal of a message, such that it will be discarded by Mailgun.
 // This facilitates testing client-side software without actually consuming e-mail resources.
-func (m *Message) EnableTestMode() {
+func (m *CommonMessage) EnableTestMode() {
 	m.testMode = true
 }
 
 // SetDeliveryTime schedules the message for transmission at the indicated time.
 // Pass nil to remove any installed schedule.
 // Refer to the Mailgun documentation for more information.
-func (m *Message) SetDeliveryTime(dt time.Time) {
+func (m *CommonMessage) SetDeliveryTime(dt time.Time) {
 	m.deliveryTime = dt
 }
 
@@ -512,7 +474,7 @@ func (m *Message) SetDeliveryTime(dt time.Time) {
 // String should be set to the number of hours in [0-9]+h format,
 // with the minimum being 24h and the maximum being 72h.
 // Refer to the Mailgun documentation for more information.
-func (m *Message) SetSTOPeriod(stoPeriod string) error {
+func (m *CommonMessage) SetSTOPeriod(stoPeriod string) error {
 	validPattern := `^([2-6][4-9]|[3-6][0-9]|7[0-2])h$`
 	// TODO(vtopc): regexp.Compile, which is called by regexp.MatchString, is a heave operation, move into global variable
 	// or just parse using time.ParseDuration().
@@ -536,17 +498,17 @@ func (m *Message) SetSTOPeriod(stoPeriod string) error {
 // Its yes/no setting is determined by the call's parameter.
 // Note that this header is not passed on to the final recipient(s).
 // Refer to the Mailgun documentation for more information.
-func (m *Message) SetTracking(tracking bool) {
+func (m *CommonMessage) SetTracking(tracking bool) {
 	m.tracking = &tracking
 }
 
 // SetTrackingClicks information is found in the Mailgun documentation.
-func (m *Message) SetTrackingClicks(trackingClicks bool) {
+func (m *CommonMessage) SetTrackingClicks(trackingClicks bool) {
 	m.trackingClicks = ptr(yesNo(trackingClicks))
 }
 
 // SetTrackingOptions sets the o:tracking, o:tracking-clicks and o:tracking-opens at once.
-func (m *Message) SetTrackingOptions(options *TrackingOptions) {
+func (m *CommonMessage) SetTrackingOptions(options *TrackingOptions) {
 	m.tracking = &options.Tracking
 
 	m.trackingClicks = &options.TrackingClicks
@@ -555,32 +517,32 @@ func (m *Message) SetTrackingOptions(options *TrackingOptions) {
 }
 
 // SetRequireTLS information is found in the Mailgun documentation.
-func (m *Message) SetRequireTLS(b bool) {
+func (m *CommonMessage) SetRequireTLS(b bool) {
 	m.requireTLS = b
 }
 
 // SetSkipVerification information is found in the Mailgun documentation.
-func (m *Message) SetSkipVerification(b bool) {
+func (m *CommonMessage) SetSkipVerification(b bool) {
 	m.skipVerification = b
 }
 
 // SetTrackingOpens information is found in the Mailgun documentation.
-func (m *Message) SetTrackingOpens(trackingOpens bool) {
+func (m *CommonMessage) SetTrackingOpens(trackingOpens bool) {
 	m.trackingOpens = &trackingOpens
 }
 
 // SetTemplateVersion information is found in the Mailgun documentation.
-func (m *Message) SetTemplateVersion(tag string) {
+func (m *CommonMessage) SetTemplateVersion(tag string) {
 	m.templateVersionTag = tag
 }
 
 // SetTemplateRenderText information is found in the Mailgun documentation.
-func (m *Message) SetTemplateRenderText(render bool) {
+func (m *CommonMessage) SetTemplateRenderText(render bool) {
 	m.templateRenderText = render
 }
 
 // AddHeader allows you to send custom MIME headers with the message.
-func (m *Message) AddHeader(header, value string) {
+func (m *CommonMessage) AddHeader(header, value string) {
 	if m.headers == nil {
 		m.headers = make(map[string]string)
 	}
@@ -590,7 +552,7 @@ func (m *Message) AddHeader(header, value string) {
 // AddVariable lets you associate a set of variables with messages you send,
 // which Mailgun can use to, in essence, complete form-mail.
 // Refer to the Mailgun documentation for more information.
-func (m *Message) AddVariable(variable string, value any) error {
+func (m *CommonMessage) AddVariable(variable string, value any) error {
 	if m.variables == nil {
 		m.variables = make(map[string]string)
 	}
@@ -613,7 +575,7 @@ func (m *Message) AddVariable(variable string, value any) error {
 // AddTemplateVariable adds a template variable to the map of template variables, replacing the variable if it is already there.
 // This is used for server-side message templates and can nest arbitrary values. At send time, the resulting map will be converted into
 // a JSON string and sent as a header in the X-Mailgun-Variables header.
-func (m *Message) AddTemplateVariable(variable string, value any) error {
+func (m *CommonMessage) AddTemplateVariable(variable string, value any) error {
 	if m.templateVariables == nil {
 		m.templateVariables = make(map[string]any)
 	}
@@ -622,31 +584,22 @@ func (m *Message) AddTemplateVariable(variable string, value any) error {
 }
 
 // AddDomain allows you to use a separate domain for the type of messages you are sending.
-func (m *Message) AddDomain(domain string) {
+func (m *CommonMessage) AddDomain(domain string) {
 	m.domain = domain
 }
 
-// Headers retrieves the http headers associated with this message
-func (m *Message) Headers() map[string]string {
-	return m.headers
-}
-
-// Deprecated: use func Headers() instead.
-// TODO(v5): remove this method, it violates https://go.dev/doc/effective_go#Getters
-func (m *Message) GetHeaders() map[string]string {
+// Headers retrieve the http headers associated with this message
+func (m *CommonMessage) Headers() map[string]string {
 	return m.headers
 }
 
-// ErrInvalidMessage is returned by `Send()` when the `mailgun.Message` struct is incomplete
+// ErrInvalidMessage is returned by `Send()` when the `mailgun.CommonMessage` struct is incomplete
 var ErrInvalidMessage = errors.New("message not valid")
 
 type SendableMessage interface {
 	Domain() string
 	To() []string
 	Tags() []string
-	// Deprecated: is no longer supported and is deprecated for new software.
-	// TODO(v5): remove this method
-	Campaigns() []string
 	DKIM() *bool
 	DeliveryTime() time.Time
 	STOPeriod() string
@@ -672,12 +625,12 @@ type SendableMessage interface {
 	Specific
 }
 
-// Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery.
+// Send attempts to queue a message (see PlainMessage, MimeMessage and its methods) for delivery.
 // It returns the Mailgun server response, which consists of two components:
 //   - A human-readable status message, typically "Queued. Thank you."
 //   - A Message ID, which is the id used to track the queued message. The message id is useful
 //     when contacting support to report an issue with a specific message or to relate a
-//     delivered, accepted or failed event back to specific message.
+//     delivered, accepted or failed event back to a specific message.
 //
 // The status and message ID are only returned if no error occurred.
 //
@@ -693,15 +646,14 @@ type SendableMessage interface {
 //	}
 //
 // See the public mailgun documentation for all possible return codes and error messages
-// TODO(v5): switch m to SendableMessage interface - https://bryanftan.medium.com/accept-interfaces-return-structs-in-go-d4cab29a301b
-func (mg *MailgunImpl) Send(ctx context.Context, m *Message) (mes, id string, err error) {
-	if mg.domain == "" {
+func (mg *MailgunImpl) Send(ctx context.Context, m SendableMessage) (mes, id string, err error) {
+	if m.Domain() == "" {
 		err = errors.New("you must provide a valid domain before calling Send()")
 		return "", "", err
 	}
 
 	invalidChars := ":&'@(),!?#;%+=<>"
-	if i := strings.ContainsAny(mg.domain, invalidChars); i {
+	if i := strings.ContainsAny(m.Domain(), invalidChars); i {
 		err = fmt.Errorf("you called Send() with a domain that contains invalid characters")
 		return "", "", err
 	}
@@ -722,18 +674,14 @@ func (mg *MailgunImpl) Send(ctx context.Context, m *Message) (mes, id string, er
 	}
 	payload := NewFormDataPayload()
 
-	m.Specific.AddValues(payload)
+	m.AddValues(payload)
 
+	// TODO: make (CommonMessage).AddValues()?
 	err = addMessageValues(payload, m)
 	if err != nil {
 		return "", "", err
 	}
 
-	// TODO(v5): remove due for domain agnostic API:
-	if m.Domain() == "" {
-		m.domain = mg.Domain()
-	}
-
 	r := newHTTPRequest(generateApiUrlWithDomain(mg, m.Endpoint(), m.Domain()))
 	r.setClient(mg.Client())
 	r.setBasicAuth(basicAuthUser, mg.APIKey())
@@ -782,10 +730,6 @@ func addMessageOptions(dst *FormDataPayload, src SendableMessage) {
 	for _, tag := range src.Tags() {
 		dst.addValue("o:tag", tag)
 	}
-	for _, campaign := range src.Campaigns() {
-		// TODO(v5): deprecated - https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Messages/
-		dst.addValue("o:campaign", campaign)
-	}
 	if src.DKIM() != nil {
 		dst.addValue("o:dkim", yesNo(*src.DKIM()))
 	}
@@ -842,7 +786,7 @@ func addMessageVariables(dst *FormDataPayload, src SendableMessage) error {
 	if src.TemplateVariables() != nil {
 		variableString, err := json.Marshal(src.TemplateVariables())
 		if err == nil {
-			// the map was marshalled as json so add it
+			// the map was marshaled as JSON so add it
 			dst.addValue("h:X-Mailgun-Variables", string(variableString))
 		}
 	}
@@ -931,7 +875,7 @@ func trueFalse(b bool) string {
 }
 
 // isValid returns true if, and only if,
-// a Message instance is sufficiently initialized to send via the Mailgun interface.
+// a CommonMessage instance is sufficiently initialized to send via the Mailgun interface.
 func isValid(m SendableMessage) bool {
 	if m == nil {
 		return false
@@ -949,10 +893,6 @@ func isValid(m SendableMessage) bool {
 		return false
 	}
 
-	if !validateStringList(m.Campaigns(), false) || len(m.Campaigns()) > 3 {
-		return false
-	}
-
 	return true
 }
 
diff --git a/messages_types_v5.go b/messages_types_v5.go
index 0cc843f..9213908 100644
--- a/messages_types_v5.go
+++ b/messages_types_v5.go
@@ -1,13 +1,14 @@
 package mailgun
 
 // This file contains a draft for new v5 messages.
+// TODO(v5): remove this file
 
 import (
 	"io"
 	"time"
 )
 
-// Message structures contain both the message text and the envelope for an e-mail message.
+// CommonMessage structures contain both the message text and the envelope for an e-mail message.
 // TODO(v5): rename to CommonMessage
 type commonMessageV5 struct {
 	domain             string
diff --git a/messages_v5.go b/messages_v5.go
index 23cca5e..4342ed7 100644
--- a/messages_v5.go
+++ b/messages_v5.go
@@ -1,6 +1,7 @@
 package mailgun
 
 // This file contains methods for new v5 messages.
+// TODO(v5): remove this file
 
 import (
 	"context"
@@ -119,7 +120,7 @@ func (m *commonMessageV5) SetDKIM(dkim bool) {
 	m.dkim = &dkim
 }
 
-// EnableNativeSend allows the return path to match the address in the Message.Headers.From:
+// EnableNativeSend allows the return path to match the address in the CommonMessage.Headers.From:
 // field when sending from Mailgun rather than the usual bounce+ address in the return path.
 func (m *commonMessageV5) EnableNativeSend() {
 	m.nativeSend = true
@@ -381,7 +382,7 @@ func (*mimeMessageV5) Endpoint() string {
 	return mimeMessagesEndpoint
 }
 
-// Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery.
+// Send attempts to queue a message (see CommonMessage, NewMessage, and its methods) for delivery.
 // It returns the Mailgun server response, which consists of two components:
 //   - A human-readable status message, typically "Queued. Thank you."
 //   - A Message ID, which is the id used to track the queued message. The message id is useful


Test patch:
diff --git a/attachments_test.go b/attachments_test.go
index 9812eb8..e307b08 100644
--- a/attachments_test.go
+++ b/attachments_test.go
@@ -31,7 +31,7 @@ func TestMultipleAttachments(t *testing.T) {
 
 	var ctx = context.Background()
 
-	m := mailgun.NewMessage("root@"+testDomain, "Subject", "Text Body", "attachment@"+testDomain)
+	m := mailgun.NewMessage(testDomain, "root@"+testDomain, "Subject", "Text Body", "attachment@"+testDomain)
 
 	// Add 2 attachments
 	m.AddAttachment(createAttachment(t))
diff --git a/events_test.go b/events_test.go
index 2621ff6..a8eeb3e 100644
--- a/events_test.go
+++ b/events_test.go
@@ -87,7 +87,7 @@ func TestEventPoller(t *testing.T) {
 	}()
 
 	// Send an email
-	m := mailgun.NewMessage("root@"+testDomain, "Subject", "Text Body", "user@"+testDomain)
+	m := mailgun.NewMessage(testDomain, "root@"+testDomain, "Subject", "Text Body", "user@"+testDomain)
 	msg, id, err := mg.Send(ctx, m)
 	require.NoError(t, err)
 
diff --git a/examples_test.go b/examples_test.go
index 216b8c0..7018765 100644
--- a/examples_test.go
+++ b/examples_test.go
@@ -112,7 +112,7 @@ Testing some Mailgun MIME awesomeness!
 	defer cancel()
 
 	mg := mailgun.NewMailgun("example.com", "my_api_key")
-	m := mailgun.NewMIMEMessage(io.NopCloser(strings.NewReader(exampleMime)), "bargle.garf@example.com")
+	m := mailgun.NewMIMEMessage("example.com", io.NopCloser(strings.NewReader(exampleMime)), "bargle.garf@example.com")
 	_, id, err := mg.Send(ctx, m)
 	if err != nil {
 		log.Fatal(err)
diff --git a/messages_test.go b/messages_test.go
index 408297e..8b2f1ae 100644
--- a/messages_test.go
+++ b/messages_test.go
@@ -242,7 +242,7 @@ func TestSendMGMIME(t *testing.T) {
 		require.NoError(t, err)
 
 		ctx := context.Background()
-		m := mailgun.NewMIMEMessage(io.NopCloser(strings.NewReader(exampleMime)), toUser)
+		m := mailgun.NewMIMEMessage(os.Getenv("MG_DOMAIN"), io.NopCloser(strings.NewReader(exampleMime)), toUser)
 		msg, id, err := mg.Send(ctx, m)
 		require.NoError(t, err)
 		t.Log("TestSendMIME:MSG(" + msg + "),ID(" + id + ")")
@@ -257,7 +257,7 @@ func TestSendMGBatchFailRecipients(t *testing.T) {
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
 
-		m := mailgun.NewMessage(fromUser, exampleSubject, exampleText+"Batch\n")
+		m := mailgun.NewMessage(os.Getenv("MG_DOMAIN"), fromUser, exampleSubject, exampleText+"Batch\n")
 		for i := 0; i < mailgun.MaxNumberOfRecipients; i++ {
 			err := m.AddRecipient("") // We expect this to indicate a failure at the API
 			require.NoError(t, err)
@@ -280,7 +280,7 @@ func TestSendMGBatchRecipientVariables(t *testing.T) {
 		require.NoError(t, err)
 
 		ctx := context.Background()
-		m := mailgun.NewMessage(fromUser, exampleSubject, templateText)
+		m := mailgun.NewMessage(os.Getenv("MG_DOMAIN"), fromUser, exampleSubject, templateText)
 		err = m.AddRecipientAndVariables(toUser, map[string]any{
 			"name":  "Joe Cool Example",
 			"table": 42,
@@ -315,7 +315,7 @@ func TestSendMGOffline(t *testing.T) {
 	mg.SetAPIBase(srv.URL + "/v3")
 	ctx := context.Background()
 
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser)
 	msg, id, err := mg.Send(ctx, m)
 	require.NoError(t, err)
 	assert.Equal(t, exampleMessage, msg)
@@ -348,7 +348,7 @@ func TestSendMGSeparateDomain(t *testing.T) {
 	mg.SetAPIBase(srv.URL + "/v3")
 
 	ctx := context.Background()
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser)
 	m.AddDomain(signingDomain)
 
 	msg, id, err := mg.Send(ctx, m)
@@ -401,7 +401,7 @@ func TestSendMGMessageVariables(t *testing.T) {
 	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser)
 	err := m.AddVariable(exampleStrVarKey, exampleStrVarVal)
 	require.NoError(t, err)
 	err = m.AddVariable(exampleBoolVarKey, false)
@@ -418,7 +418,7 @@ func TestSendMGMessageVariables(t *testing.T) {
 }
 
 func TestAddRecipientsError(t *testing.T) {
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText)
+	m := mailgun.NewMessage(domain, fromUser, exampleSubject, exampleText)
 
 	for i := 0; i < 1000; i++ {
 		recipient := fmt.Sprintf("recipient_%d@example.com", i)
@@ -433,7 +433,7 @@ func TestAddRecipientsError(t *testing.T) {
 func TestAddRecipientAndVariablesError(t *testing.T) {
 	var err error
 
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText)
+	m := mailgun.NewMessage(domain, fromUser, exampleSubject, exampleText)
 
 	for i := 0; i < 1000; i++ {
 		recipient := fmt.Sprintf("recipient_%d@example.com", i)
@@ -472,7 +472,7 @@ func TestSendDomainError(t *testing.T) {
 		ctx := context.Background()
 		mg := mailgun.NewMailgun(c.domain, exampleAPIKey)
 		mg.SetAPIBase(srv.URL + "/v3")
-		m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, "test@test.com")
+		m := mailgun.NewMessage(c.domain, fromUser, exampleSubject, exampleText, "test@test.com")
 
 		_, _, err := mg.Send(ctx, m)
 		if c.isValid {
@@ -498,7 +498,7 @@ func TestSendEOFError(t *testing.T) {
 	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser)
 	_, _, err := mg.Send(context.Background(), m)
 	require.NotNil(t, err)
 	// TODO(vtopc): do not compare strings, use errors.Is or errors.As:
@@ -524,18 +524,18 @@ func TestHasRecipient(t *testing.T) {
 	mg.SetAPIBase(srv.URL + "/v3")
 
 	// No recipient
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText)
 	_, _, err := mg.Send(context.Background(), m)
 	require.EqualError(t, err, "message not valid")
 
 	// Provided Bcc
-	m = mailgun.NewMessage(fromUser, exampleSubject, exampleText)
+	m = mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText)
 	m.AddBCC(recipient)
 	_, _, err = mg.Send(context.Background(), m)
 	require.NoError(t, err)
 
 	// Provided cc
-	m = mailgun.NewMessage(fromUser, exampleSubject, exampleText)
+	m = mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText)
 	m.AddCC(recipient)
 	_, _, err = mg.Send(context.Background(), m)
 	require.NoError(t, err)
@@ -597,7 +597,7 @@ func TestAddOverrideHeader(t *testing.T) {
 	mg.AddOverrideHeader("CustomHeader", "custom-value")
 	ctx := context.Background()
 
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser)
 	m.SetRequireTLS(true)
 	m.SetSkipVerification(true)
 
@@ -636,7 +636,7 @@ func TestOnBehalfOfSubaccount(t *testing.T) {
 	mg.SetOnBehalfOfSubaccount("mailgun.subaccount")
 	ctx := context.Background()
 
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser)
 	m.SetRequireTLS(true)
 	m.SetSkipVerification(true)
 
@@ -668,7 +668,7 @@ func TestCaptureCurlOutput(t *testing.T) {
 	mg.SetAPIBase(srv.URL + "/v3")
 	ctx := context.Background()
 
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser)
 	msg, id, err := mg.Send(ctx, m)
 	require.NoError(t, err)
 	assert.Equal(t, exampleMessage, msg)
@@ -704,7 +704,7 @@ func TestSendTLSOptions(t *testing.T) {
 	mg.SetAPIBase(srv.URL + "/v3")
 	ctx := context.Background()
 
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, exampleText, toUser)
 	m.SetRequireTLS(true)
 	m.SetSkipVerification(true)
 
@@ -734,7 +734,7 @@ func TestSendTemplate(t *testing.T) {
 	mg.SetAPIBase(srv.URL + "/v3")
 	ctx := context.Background()
 
-	m := mailgun.NewMessage(fromUser, exampleSubject, "", toUser)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, "", toUser)
 	m.SetTemplate(templateName)
 
 	msg, id, err := mg.Send(ctx, m)
@@ -767,7 +767,7 @@ func TestSendTemplateOptions(t *testing.T) {
 	mg.SetAPIBase(srv.URL + "/v3")
 	ctx := context.Background()
 
-	m := mailgun.NewMessage(fromUser, exampleSubject, "", toUser)
+	m := mailgun.NewMessage(exampleDomain, fromUser, exampleSubject, "", toUser)
 	m.SetTemplate(templateName)
 	m.SetTemplateRenderText(true)
 	m.SetTemplateVersion(templateVersionTag)
@@ -779,7 +779,7 @@ func TestSendTemplateOptions(t *testing.T) {
 }
 
 func TestSendableMessageIface(t *testing.T) {
-	m := mailgun.NewMessage(fromUser, exampleSubject, exampleText)
+	m := mailgun.NewMessage(domain, fromUser, exampleSubject, exampleText)
 
 	assert.Implements(t, (*mailgun.SendableMessage)(nil), m)
 }
diff --git a/storage_test.go b/storage_test.go
index e22434e..33a0df2 100644
--- a/storage_test.go
+++ b/storage_test.go
@@ -18,7 +18,7 @@ func TestStorage(t *testing.T) {
 
 	var ctx = context.Background()
 
-	m := mailgun.NewMessage("root@"+testDomain, "Subject", "Text Body", "stored@"+testDomain)
+	m := mailgun.NewMessage(testDomain, "root@"+testDomain, "Subject", "Text Body", "stored@"+testDomain)
 	msg, id, err := mg.Send(ctx, m)
 	require.NoError(t, err)
 
diff --git a/tags_test.go b/tags_test.go
index 1e64252..e7cf314 100644
--- a/tags_test.go
+++ b/tags_test.go
@@ -20,7 +20,7 @@ const (
 func TestTags(t *testing.T) {
 	mg := mailgun.NewMailgun(testDomain, testKey)
 	mg.SetAPIBase(server.URL())
-	msg := mailgun.NewMessage(fromUser, exampleSubject, exampleText, "test@example.com")
+	msg := mailgun.NewMessage(testDomain, fromUser, exampleSubject, exampleText, "test@example.com")
 	require.NoError(t, msg.AddTag("newsletter"))
 	require.NoError(t, msg.AddTag("homer"))
 	require.NoError(t, msg.AddTag("bart"))