Repository: mailgun/mailgun-go
Pull request #184 (2019-04-22 16:02:18)

Added support for sending with template using the new template API
## Purpose
* Added support for sending messages using a template from the new template API (See #183)
* Changed signature of `CreateDomain()` moved password into `CreateDomainOptions`

## Implementation
* Added `SetTemplate()`
* Message is now valid If template is provided and `text` or `html` are empty.
* Providing `text` parameters is valid, but will be ignored by the mailgun API if `template` parameter is provided. 
* Providing `html` parameter with `template` parameter will result in an error
* Added example usage for templates api in `examples/`


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index a11f277..0e45205 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [3.4.0] - 2019-04-23
+### Added
+* Added `Message.SetTemplate()` to allow sending with the body of a template.
+### Changes
+* Changed signature of `CreateDomain()` moved password into `CreateDomainOptions`
+
 ## [3.3.2] - 2019-03-28
 ### Changes
 * Uncommented DeliveryStatus.Code and change it to an integer (See #175)
diff --git a/domains.go b/domains.go
index 9a78234..4b04608 100644
--- a/domains.go
+++ b/domains.go
@@ -254,6 +254,7 @@ func (mg *MailgunImpl) VerifyDomain(ctx context.Context, domain string) (string,
 
 // Optional parameters when creating a domain
 type CreateDomainOptions struct {
+	Password           string
 	SpamAction         SpamAction
 	Wildcard           bool
 	ForceDKIMAuthority bool
@@ -267,14 +268,13 @@ type CreateDomainOptions struct {
 // The spamAction domain must be one of Delete, Tag, or Disabled.
 // The wildcard parameter instructs Mailgun to treat all subdomains of this domain uniformly if true,
 // and as different domains if false.
-func (mg *MailgunImpl) CreateDomain(ctx context.Context, name string, password string, opts *CreateDomainOptions) (DomainResponse, error) {
+func (mg *MailgunImpl) CreateDomain(ctx context.Context, name string, opts *CreateDomainOptions) (DomainResponse, error) {
 	r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint))
 	r.setClient(mg.Client())
 	r.setBasicAuth(basicAuthUser, mg.APIKey())
 
 	payload := newUrlEncodedPayload()
 	payload.addValue("name", name)
-	payload.addValue("smtp_password", password)
 
 	if opts != nil {
 		if opts.SpamAction != "" {
@@ -292,6 +292,9 @@ func (mg *MailgunImpl) CreateDomain(ctx context.Context, name string, password s
 		if len(opts.IPS) != 0 {
 			payload.addValue("ips", strings.Join(opts.IPS, ","))
 		}
+		if len(opts.Password) != 0 {
+			payload.addValue("smtp_password", opts.Password)
+		}
 	}
 	var resp DomainResponse
 	err := postResponseFromJSON(ctx, r, payload, &resp)
diff --git a/examples/examples.go b/examples/examples.go
index 06f1219..81a238f 100644
--- a/examples/examples.go
+++ b/examples/examples.go
@@ -1,4 +1,4 @@
-package main
+package examples
 
 import (
 	"context"
@@ -33,7 +33,8 @@ func AddDomain(domain, apiKey string) (mailgun.DomainResponse, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
 	defer cancel()
 
-	return mg.CreateDomain(ctx, "example.com", "super_secret", &mailgun.CreateDomainOptions{
+	return mg.CreateDomain(ctx, "example.com", &mailgun.CreateDomainOptions{
+		Password:   "super_secret",
 		SpamAction: mailgun.SpamActionTag,
 		Wildcard:   false,
 	})
@@ -142,7 +143,8 @@ func CreateDomain(domain, apiKey string) (mailgun.DomainResponse, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
 	defer cancel()
 
-	return mg.CreateDomain(ctx, "example.com", "super_secret", &mailgun.CreateDomainOptions{
+	return mg.CreateDomain(ctx, "example.com", &mailgun.CreateDomainOptions{
+		Password:   "super_secret",
 		SpamAction: mailgun.SpamActionTag,
 		Wildcard:   false,
 	})
@@ -894,3 +896,190 @@ func VerifyWebhookSignature(domain, apiKey, timestamp, token, signature string)
 		Signature: signature,
 	})
 }
+
+func SendMessageWithTemplate(domain, apiKey string) error {
+	mg := mailgun.NewMailgun(domain, apiKey)
+	var err error
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	// Create a new template
+	err = mg.CreateTemplate(ctx, &mailgun.Template{
+		Name: "my-template",
+		Version: mailgun.TemplateVersion{
+			Template: `'<div class="entry"> <h1>{{.title}}</h1> <div class="body"> {{.body}} </div> </div>'`,
+			Engine:   mailgun.TemplateEngineGo,
+			Tag:      "v1",
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	// Give time for template to show up in the system.
+	time.Sleep(time.Second * 1)
+
+	// Create a new message with template
+	m := mg.NewMessage("Excited User <excited@example.com>", "Template example", "")
+	m.SetTemplate("my-template")
+
+	// Add recipients
+	m.AddRecipient("bob@example.com")
+	m.AddRecipient("alice@example.com")
+
+	// Add the variables to be used by the template
+	m.AddVariable("title", "Hello Templates")
+	m.AddVariable("body", "Body of the message")
+
+	_, id, err := mg.Send(ctx, m)
+	fmt.Printf("Queued: %s", id)
+	return err
+}
+
+func CreateTemplate(domain, apiKey string) error {
+	mg := mailgun.NewMailgun(domain, apiKey)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	return mg.CreateTemplate(ctx, &mailgun.Template{
+		Name: "my-template",
+		Version: mailgun.TemplateVersion{
+			Template: `'<div class="entry"> <h1>{{.title}}</h1> <div class="body"> {{.body}} </div> </div>'`,
+			Engine:   mailgun.TemplateEngineGo,
+			Tag:      "v1",
+		},
+	})
+}
+
+func DeleteTemplate(domain, apiKey string) error {
+	mg := mailgun.NewMailgun(domain, apiKey)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	return mg.DeleteTemplate(ctx, "my-template")
+}
+
+func UpdateTemplate(domain, apiKey string) error {
+	mg := mailgun.NewMailgun(domain, apiKey)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	return mg.UpdateTemplate(ctx, &mailgun.Template{
+		Name:        "my-template",
+		Description: "Add a description to the template",
+	})
+}
+
+func GetTemplate(domain, apiKey string) (mailgun.Template, error) {
+	mg := mailgun.NewMailgun(domain, apiKey)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	return mg.GetTemplate(ctx, "my-template")
+}
+
+func ListActiveTemplates(domain, apiKey string) ([]mailgun.Template, error) {
+	mg := mailgun.NewMailgun(domain, apiKey)
+	it := mg.ListTemplates(&mailgun.ListTemplateOptions{Active: true})
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	var page, result []mailgun.Template
+	for it.Next(ctx, &page) {
+		result = append(result, page...)
+	}
+
+	if it.Err() != nil {
+		return nil, it.Err()
+	}
+	return result, nil
+}
+
+func ListTemplates(domain, apiKey string) ([]mailgun.Template, error) {
+	mg := mailgun.NewMailgun(domain, apiKey)
+	it := mg.ListTemplates(nil)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	var page, result []mailgun.Template
+	for it.Next(ctx, &page) {
+		result = append(result, page...)
+	}
+
+	if it.Err() != nil {
+		return nil, it.Err()
+	}
+	return result, nil
+}
+
+func AddTemplateVersion(domain, apiKey string) error {
+	mg := mailgun.NewMailgun(domain, apiKey)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	return mg.AddTemplateVersion(ctx, "my-template", &mailgun.TemplateVersion{
+		Template: `'<div class="entry"> <h1>{{.title}}</h1> <div class="body"> {{.body}} </div> </div>'`,
+		Engine:   mailgun.TemplateEngineGo,
+		Tag:      "v2",
+		Active:   true,
+	})
+}
+
+func DeleteTemplateVersion(domain, apiKey string) error {
+	mg := mailgun.NewMailgun(domain, apiKey)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	// Delete the template version tagged as 'v2'
+	return mg.DeleteTemplateVersion(ctx, "my-template", "v2")
+}
+
+func GetTemplateVersion(domain, apiKey string) (mailgun.TemplateVersion, error) {
+	mg := mailgun.NewMailgun(domain, apiKey)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	// Get the template version tagged as 'v2'
+	return mg.GetTemplateVersion(ctx, "my-template", "v2")
+}
+
+func UpdateTemplateVersion(domain, apiKey string) error {
+	mg := mailgun.NewMailgun(domain, apiKey)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	return mg.UpdateTemplateVersion(ctx, "my-template", &mailgun.TemplateVersion{
+		Comment: "Add a comment to the template and make it 'active'",
+		Tag:     "v2",
+		Active:  true,
+	})
+}
+
+func ListTemplateVersions(domain, apiKey string) ([]mailgun.TemplateVersion, error) {
+	mg := mailgun.NewMailgun(domain, apiKey)
+	it := mg.ListTemplateVersions("my-template", nil)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	var page, result []mailgun.TemplateVersion
+	for it.Next(ctx, &page) {
+		result = append(result, page...)
+	}
+
+	if it.Err() != nil {
+		return nil, it.Err()
+	}
+	return result, nil
+}
diff --git a/mailgun.go b/mailgun.go
index f2f40f9..a786de2 100644
--- a/mailgun.go
+++ b/mailgun.go
@@ -141,7 +141,7 @@ type Mailgun interface {
 
 	ListDomains(opts *ListOptions) *DomainsIterator
 	GetDomain(ctx context.Context, domain string) (DomainResponse, error)
-	CreateDomain(ctx context.Context, name string, pass string, opts *CreateDomainOptions) (DomainResponse, error)
+	CreateDomain(ctx context.Context, name string, opts *CreateDomainOptions) (DomainResponse, error)
 	DeleteDomain(ctx context.Context, name string) error
 	VerifyDomain(ctx context.Context, name string) (string, error)
 	UpdateDomainConnection(ctx context.Context, domain string, dc DomainConnection) error
@@ -219,7 +219,7 @@ type Mailgun interface {
 	GetTemplate(ctx context.Context, id string) (Template, error)
 	UpdateTemplate(ctx context.Context, template *Template) error
 	DeleteTemplate(ctx context.Context, id string) error
-	ListTemplates(opts *ListOptions) *TemplatesIterator
+	ListTemplates(opts *ListTemplateOptions) *TemplatesIterator
 
 	AddTemplateVersion(ctx context.Context, templateId string, version *TemplateVersion) error
 	GetTemplateVersion(ctx context.Context, templateId, versionId string) (TemplateVersion, error)
diff --git a/messages.go b/messages.go
index eacfa9d..229e0cd 100644
--- a/messages.go
+++ b/messages.go
@@ -110,12 +110,13 @@ type StoredMessageRaw struct {
 // although from, subject, and text are set when the message is created with
 // NewMessage.
 type plainMessage struct {
-	from    string
-	cc      []string
-	bcc     []string
-	subject string
-	text    string
-	html    string
+	from     string
+	cc       []string
+	bcc      []string
+	subject  string
+	text     string
+	html     string
+	template string
 }
 
 // mimeMessage contains fields relevant to pre-packaged MIME messages.
@@ -142,6 +143,7 @@ type features interface {
 	isValid() bool
 	endpoint() string
 	recipientCount() int
+	setTemplate(string)
 }
 
 // NewMessage returns a new e-mail message with the simplest envelop needed to send.
@@ -343,6 +345,18 @@ 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 (mm *mimeMessage) setTemplate(t string) {}
+
 // This feature is deprecated for new software.
 func (m *Message) AddCampaign(campaign string) {
 	m.campaigns = append(m.campaigns, campaign)
@@ -585,6 +599,9 @@ func (pm *plainMessage) addValues(p *formDataPayload) {
 	if pm.html != "" {
 		p.addValue("html", pm.html)
 	}
+	if pm.template != "" {
+		p.addValue("template", pm.template)
+	}
 }
 
 func (mm *mimeMessage) addValues(p *formDataPayload) {
@@ -653,6 +670,11 @@ func (pm *plainMessage) isValid() bool {
 		return false
 	}
 
+	if pm.template != "" {
+		// pm.text or pm.html not needed if template is supplied
+		return true
+	}
+
 	if pm.text == "" && pm.html == "" {
 		return false
 	}
diff --git a/template.go b/template.go
index 9e47bcb..f69852b 100644
--- a/template.go
+++ b/template.go
@@ -122,8 +122,13 @@ type TemplatesIterator struct {
 	err error
 }
 
+type ListTemplateOptions struct {
+	Limit  int
+	Active bool
+}
+
 // List all available templates
-func (mg *MailgunImpl) ListTemplates(opts *ListOptions) *TemplatesIterator {
+func (mg *MailgunImpl) ListTemplates(opts *ListTemplateOptions) *TemplatesIterator {
 	r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint))
 	r.setClient(mg.Client())
 	r.setBasicAuth(basicAuthUser, mg.APIKey())
@@ -131,6 +136,9 @@ func (mg *MailgunImpl) ListTemplates(opts *ListOptions) *TemplatesIterator {
 		if opts.Limit != 0 {
 			r.addParameter("limit", strconv.Itoa(opts.Limit))
 		}
+		if opts.Active {
+			r.addParameter("active", "yes")
+		}
 	}
 	url, err := r.generateUrlWithParameters()
 	return &TemplatesIterator{


Test patch:
diff --git a/domains_test.go b/domains_test.go
index 09f70bc..729e7be 100644
--- a/domains_test.go
+++ b/domains_test.go
@@ -75,8 +75,8 @@ func TestAddDeleteDomain(t *testing.T) {
 	ctx := context.Background()
 
 	// First, we need to add the domain.
-	_, err := mg.CreateDomain(ctx, "mx.mailgun.test", "supersecret",
-		&mailgun.CreateDomainOptions{SpamAction: mailgun.SpamActionTag})
+	_, err := mg.CreateDomain(ctx, "mx.mailgun.test",
+		&mailgun.CreateDomainOptions{SpamAction: mailgun.SpamActionTag, Password: "supersecret"})
 	ensure.Nil(t, err)
 
 	// Next, we delete it.
diff --git a/messages_test.go b/messages_test.go
index 272c011..dec5df7 100644
--- a/messages_test.go
+++ b/messages_test.go
@@ -479,3 +479,32 @@ func TestSendTLSOptions(t *testing.T) {
 	ensure.DeepEqual(t, msg, exampleMessage)
 	ensure.DeepEqual(t, id, exampleID)
 }
+
+func TestSendTemplate(t *testing.T) {
+	const (
+		exampleDomain  = "testDomain"
+		exampleAPIKey  = "testAPIKey"
+		toUser         = "test@test.com"
+		exampleMessage = "Queue. Thank you"
+		exampleID      = "<20111114174239.25659.5817@samples.mailgun.org>"
+		templateName   = "my-template"
+	)
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		ensure.DeepEqual(t, req.FormValue("template"), templateName)
+		rsp := fmt.Sprintf(`{"message":"%s", "id":"%s"}`, exampleMessage, exampleID)
+		fmt.Fprint(w, rsp)
+	}))
+	defer srv.Close()
+
+	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg.SetAPIBase(srv.URL)
+	ctx := context.Background()
+
+	m := mg.NewMessage(fromUser, exampleSubject, "", toUser)
+	m.SetTemplate(templateName)
+
+	msg, id, err := mg.Send(ctx, m)
+	ensure.Nil(t, err)
+	ensure.DeepEqual(t, msg, exampleMessage)
+	ensure.DeepEqual(t, id, exampleID)
+}