Rough in mailgun support

This commit is contained in:
Matthew Dillon 2015-06-29 06:16:48 -08:00
parent c78998739d
commit 2f0fd351a8
51 changed files with 4749 additions and 5 deletions

8
Godeps/Godeps.json generated
View file

@ -37,6 +37,14 @@
"Comment": "go1.0-cutoff-29-g30ed220",
"Rev": "30ed2200d7ec99cf17272292f1d4b7b0bd7165db"
},
{
"ImportPath": "github.com/mailgun/mailgun-go",
"Rev": "9578dc67692294bb7e2a6f4b15dd18c97af19440"
},
{
"ImportPath": "github.com/mbanzon/simplehttp",
"Rev": "04c542e7ac706a25820090f274ea6a4f39a63326"
},
{
"ImportPath": "github.com/thermokarst/jwt",
"Rev": "66ca404d841ed908aa6cc9361107b0c363c94cd8"

View file

@ -0,0 +1,10 @@
language: go
go:
- 1.1.2
- 1.2
- tip
env:
- GOARCH=amd64
- GOARCH=386
script:
- go test

View file

@ -0,0 +1,27 @@
Copyright (c) 2013-2014, Michael Banzon
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the names of Mailgun, Michael Banzon, nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,20 @@
Mailgun with Go
===============
[![Build Status](https://travis-ci.org/mbanzon/mailgun.png?branch=master)](https://travis-ci.org/mbanzon/mailgun)
[![GoDoc](https://godoc.org/github.com/mailgun/mailgun-go?status.svg)](https://godoc.org/github.com/mailgun/mailgun-go)
Go library for sending mail with the Mailgun API.
See these examples on how how to use use the library with various parts of the Mailgun API:
* [Messages](https://gist.github.com/mbanzon/8179682 "mailgun-message-example.go")
* [E-mail validation](https://gist.github.com/mbanzon/8179989 "mailgun-validation-example.go")
* [Bounces](https://gist.github.com/mbanzon/8179951 "mailgun-bounces-example.go")
* [Stats](https://gist.github.com/mbanzon/8206266 "mailgun-stats-example.go")
* [File Attachment from Memory](https://gist.github.com/sym3tri/8a29ddecd65ec4f8ccfc)
More examples are coming soon.
The code is released under a 3-clause BSD license. See the LICENSE file for more information.

View file

@ -0,0 +1,29 @@
// +build acceptance
// The acceptance test package includes utilities supporting acceptance tests in *_test.go
// files. To execute these acceptance tests, you must invoke them using the acceptance
// build tag, like so:
//
// $ go test -tags acceptance github.com/mailgun/mailgun-go
//
// Note that some API calls may potentially cost the user money! By default, such tests
// do NOT run. However, you will then not be testing the full capability of Mailgun.
// To run them, you'll also need to specify the spendMoney build tag:
//
// $ go test -tags "acceptance spendMoney" github.com/mailgun/mailgun-go
package acceptance
import (
"os"
"testing"
)
// Many tests require configuration settings unique to the user, passed in via
// environment variables. If these variables aren't set, we need to fail the test early.
func reqEnv(t *testing.T, variableName string) string {
value := os.Getenv(variableName)
if value == "" {
t.Fatalf("Expected environment variable %s to be set", variableName)
}
return value
}

View file

@ -0,0 +1,119 @@
// +build acceptance
package acceptance
import (
"fmt"
mailgun "github.com/mailgun/mailgun-go"
"testing"
)
func TestGetBounces(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
n, bounces, err := mg.GetBounces(-1, -1)
if err != nil {
t.Fatal(err)
}
if n > 0 {
t.Fatal("Expected no bounces for what should be a clean domain.")
}
if n != len(bounces) {
t.Fatalf("Expected length of bounces %d to equal returned length %d", len(bounces), n)
}
}
func TestGetSingleBounce(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
exampleEmail := fmt.Sprintf("baz@%s", domain)
_, err := mg.GetSingleBounce(exampleEmail)
if err == nil {
t.Fatal("Did not expect a bounce to exist")
}
ure, ok := err.(*mailgun.UnexpectedResponseError)
if !ok {
t.Fatal("Expected UnexpectedResponseError")
}
if ure.Actual != 404 {
t.Fatalf("Expected 404 response code; got %d", ure.Actual)
}
}
func TestAddDelBounces(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
// Compute an e-mail address for our domain.
exampleEmail := fmt.Sprintf("baz@%s", domain)
// First, basic sanity check.
// Fail early if we have bounces for a fictitious e-mail address.
n, _, err := mg.GetBounces(-1, -1)
if err != nil {
t.Fatal(err)
}
if n > 0 {
t.Fatal("Expected no bounces for what should be a clean domain.")
}
bounce, err := mg.GetSingleBounce(exampleEmail)
if err == nil {
t.Fatalf("Expected no bounces for %s", exampleEmail)
}
// Add the bounce for our address.
err = mg.AddBounce(exampleEmail, "550", "TestAddDelBounces-generated error")
if err != nil {
t.Fatal(err)
}
// We should now have one bounce listed when we query the API.
n, bounces, err := mg.GetBounces(-1, -1)
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatal("Expected one bounce for this domain.")
}
if bounces[0].Address != exampleEmail {
t.Fatalf("Expected bounce for address %s; got %s", exampleEmail, bounces[0].Address)
}
bounce, err = mg.GetSingleBounce(exampleEmail)
if err != nil {
t.Fatal(err)
}
if bounce.CreatedAt == "" {
t.Fatalf("Expected at least one bounce for %s", exampleEmail)
}
// Delete it. This should put us back the way we were.
err = mg.DeleteBounce(exampleEmail)
if err != nil {
t.Fatal(err)
}
// Make sure we're back to the way we were.
n, _, err = mg.GetBounces(-1, -1)
if err != nil {
t.Fatal(err)
}
if n > 0 {
t.Fatal("Expected no bounces for what should be a clean domain.")
}
_, err = mg.GetSingleBounce(exampleEmail)
if err == nil {
t.Fatalf("Expected no bounces for %s", exampleEmail)
}
}

View file

@ -0,0 +1,53 @@
// +build acceptance
package acceptance
import (
"fmt"
mailgun "github.com/mailgun/mailgun-go"
"os"
"testing"
"text/tabwriter"
)
func TestGetCredentials(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
n, cs, err := mg.GetCredentials(mailgun.DefaultLimit, mailgun.DefaultSkip)
if err != nil {
t.Fatal(err)
}
tw := &tabwriter.Writer{}
tw.Init(os.Stdout, 2, 8, 2, ' ', 0)
fmt.Fprintf(tw, "Login\tCreated At\t\n")
for _, c := range cs {
fmt.Fprintf(tw, "%s\t%s\t\n", c.Login, c.CreatedAt)
}
tw.Flush()
fmt.Printf("%d credentials listed out of %d\n", len(cs), n)
}
func TestCreateDeleteCredentials(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
randomPassword := randomString(16, "pw")
randomID := randomString(16, "usr")
randomLogin := fmt.Sprintf("%s@%s", randomID, domain)
err := mg.CreateCredential(randomLogin, randomPassword)
if err != nil {
t.Fatal(err)
}
err = mg.ChangeCredentialPassword(randomID, randomString(16, "pw2"))
if err != nil {
t.Fatal(err)
}
err = mg.DeleteCredential(randomID)
if err != nil {
t.Fatal(err)
}
}

View file

@ -0,0 +1,96 @@
// +build acceptance
package acceptance
import (
"crypto/rand"
"fmt"
"github.com/mailgun/mailgun-go"
"testing"
)
func TestGetDomains(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
n, domains, err := mg.GetDomains(mailgun.DefaultLimit, mailgun.DefaultSkip)
if err != nil {
t.Fatal(err)
}
fmt.Printf("TestGetDomains: %d domains retrieved\n", n)
for _, d := range domains {
fmt.Printf("TestGetDomains: %#v\n", d)
}
}
func TestGetSingleDomain(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
_, domains, err := mg.GetDomains(mailgun.DefaultLimit, mailgun.DefaultSkip)
if err != nil {
t.Fatal(err)
}
dr, rxDnsRecords, txDnsRecords, err := mg.GetSingleDomain(domains[0].Name)
if err != nil {
t.Fatal(err)
}
fmt.Printf("TestGetSingleDomain: %#v\n", dr)
for _, rxd := range rxDnsRecords {
fmt.Printf("TestGetSingleDomains: %#v\n", rxd)
}
for _, txd := range txDnsRecords {
fmt.Printf("TestGetSingleDomains: %#v\n", txd)
}
}
func TestGetSingleDomainNotExist(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
_, _, _, err := mg.GetSingleDomain(randomString(32, "com.edu.org.")+".com")
if err == nil {
t.Fatal("Did not expect a domain to exist")
}
ure, ok := err.(*mailgun.UnexpectedResponseError)
if !ok {
t.Fatal("Expected UnexpectedResponseError")
}
if ure.Actual != 404 {
t.Fatalf("Expected 404 response code; got %d", ure.Actual)
}
}
func TestAddDeleteDomain(t *testing.T) {
// First, we need to add the domain.
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
randomDomainName := randomString(16, "DOMAIN") + ".example.com"
randomPassword := randomString(16, "PASSWD")
err := mg.CreateDomain(randomDomainName, randomPassword, mailgun.Tag, false)
if err != nil {
t.Fatal(err)
}
// Next, we delete it.
err = mg.DeleteDomain(randomDomainName)
if err != nil {
t.Fatal(err)
}
}
// randomString generates a string of given length, but random content.
// All content will be within the ASCII graphic character set.
// (Implementation from Even Shaw's contribution on
// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go).
func randomString(n int, prefix string) string {
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var bytes = make([]byte, n)
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = alphanum[b%byte(len(alphanum))]
}
return prefix + string(bytes)
}

View file

@ -0,0 +1,52 @@
// +build acceptance
package acceptance
import (
"github.com/mailgun/mailgun-go"
"testing"
)
func TestEmailValidation(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, "", apiKey)
ev, err := mg.ValidateEmail("foo@mailgun.com")
if err != nil {
t.Fatal(err)
}
if ev.IsValid != true {
t.Fatal("Expected a valid e-mail address")
}
if ev.Parts.DisplayName != "" {
t.Fatal("No display name should exist")
}
if ev.Parts.LocalPart != "foo" {
t.Fatal("Expected local part of foo; got ", ev.Parts.LocalPart)
}
if ev.Parts.Domain != "mailgun.com" {
t.Fatal("Expected mailgun.com domain; got ", ev.Parts.Domain)
}
}
func TestParseAddresses(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, "", apiKey)
addressesThatParsed, unparsableAddresses, err := mg.ParseAddresses("Alice <alice@example.com>", "bob@example.com", "example.com")
if err != nil {
t.Fatal(err)
}
hittest := map[string]bool{
"Alice <alice@example.com>": true,
"bob@example.com": true,
}
for _, a := range addressesThatParsed {
if !hittest[a] {
t.Fatalf("Expected %s to be parsable", a)
}
}
if len(unparsableAddresses) != 1 {
t.Fatalf("Expected 1 address to be unparsable; got %d", len(unparsableAddresses))
}
}

View file

@ -0,0 +1,41 @@
// +build acceptance
package acceptance
import (
"fmt"
"github.com/mailgun/mailgun-go"
"os"
"testing"
"text/tabwriter"
)
func TestEventIterator(t *testing.T) {
// Grab the list of events (as many as we can get)
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
ei := mg.NewEventIterator()
err := ei.GetFirstPage(mailgun.GetEventsOptions{})
if err != nil {
t.Fatal(err)
}
// Print out the kind of event and timestamp.
// Specifics about each event will depend on the "event" type.
events := ei.Events()
tw := &tabwriter.Writer{}
tw.Init(os.Stdout, 2, 8, 2, ' ', tabwriter.AlignRight)
fmt.Fprintln(tw, "Event\tTimestamp\t")
for _, event := range events {
fmt.Fprintf(tw, "%s\t%v\t\n", event["event"], event["timestamp"])
}
tw.Flush()
fmt.Printf("%d events dumped\n\n", len(events))
// We're on the first page. We must at the beginning.
ei.GetPrevious()
if len(ei.Events()) != 0 {
t.Fatal("Expected to be at the beginning")
}
}

View file

@ -0,0 +1,205 @@
// +build acceptance,spendMoney
package acceptance
import (
"fmt"
mailgun "github.com/mailgun/mailgun-go"
"testing"
)
func setup(t *testing.T) (mailgun.Mailgun, string) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
address := fmt.Sprintf("list5@%s", domain)
_, err := mg.CreateList(mailgun.List{
Address: address,
Name: address,
Description: "TestMailingListMembers-related mailing list",
AccessLevel: mailgun.Members,
})
if err != nil {
t.Fatal(err)
}
return mg, address
}
func teardown(t *testing.T, mg mailgun.Mailgun, address string) {
err := mg.DeleteList(address)
if err != nil {
t.Fatal(err)
}
}
func TestMailingListMembers(t *testing.T) {
mg, address := setup(t)
defer teardown(t, mg, address)
var countPeople = func() int {
n, _, err := mg.GetMembers(mailgun.DefaultLimit, mailgun.DefaultSkip, mailgun.All, address)
if err != nil {
t.Fatal(err)
}
return n
}
startCount := countPeople()
protoJoe := mailgun.Member{
Address: "joe@example.com",
Name: "Joe Example",
Subscribed: mailgun.Subscribed,
}
err := mg.CreateMember(true, address, protoJoe)
if err != nil {
t.Fatal(err)
}
newCount := countPeople()
if newCount <= startCount {
t.Fatalf("Expected %d people subscribed; got %d", startCount+1, newCount)
}
theMember, err := mg.GetMemberByAddress("joe@example.com", address)
if err != nil {
t.Fatal(err)
}
if (theMember.Address != protoJoe.Address) ||
(theMember.Name != protoJoe.Name) ||
(*theMember.Subscribed != *protoJoe.Subscribed) ||
(len(theMember.Vars) != 0) {
t.Fatalf("Unexpected Member: Expected [%#v], Got [%#v]", protoJoe, theMember)
}
_, err = mg.UpdateMember("joe@example.com", address, mailgun.Member{
Name: "Joe Cool",
})
if err != nil {
t.Fatal(err)
}
theMember, err = mg.GetMemberByAddress("joe@example.com", address)
if err != nil {
t.Fatal(err)
}
if theMember.Name != "Joe Cool" {
t.Fatal("Expected Joe Cool; got " + theMember.Name)
}
err = mg.DeleteMember("joe@example.com", address)
if err != nil {
t.Fatal(err)
}
if countPeople() != startCount {
t.Fatalf("Expected %d people; got %d instead", startCount, countPeople())
}
err = mg.CreateMemberList(nil, address, []interface{}{
mailgun.Member{
Address: "joe.user1@example.com",
Name: "Joe's debugging account",
Subscribed: mailgun.Unsubscribed,
},
mailgun.Member{
Address: "Joe Cool <joe.user2@example.com>",
Name: "Joe's Cool Account",
Subscribed: mailgun.Subscribed,
},
mailgun.Member{
Address: "joe.user3@example.com",
Vars: map[string]interface{}{
"packet-email": "KW9ABC @ BOGBBS-4.#NCA.CA.USA.NOAM",
},
},
})
if err != nil {
t.Fatal(err)
}
theMember, err = mg.GetMemberByAddress("joe.user2@example.com", address)
if err != nil {
t.Fatal(err)
}
if theMember.Name != "Joe's Cool Account" {
t.Fatalf("Expected Joe's Cool Account; got %s", theMember.Name)
}
if theMember.Subscribed != nil {
if *theMember.Subscribed != true {
t.Fatalf("Expected subscribed to be true; got %v", *theMember.Subscribed)
}
} else {
t.Fatal("Expected some kind of subscription status; got nil.")
}
}
func TestMailingLists(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
listAddr := fmt.Sprintf("list2@%s", domain)
protoList := mailgun.List{
Address: listAddr,
Name: "List1",
Description: "A list created by an acceptance test.",
AccessLevel: mailgun.Members,
}
var countLists = func() int {
total, _, err := mg.GetLists(mailgun.DefaultLimit, mailgun.DefaultSkip, "")
if err != nil {
t.Fatal(err)
}
return total
}
startCount := countLists()
_, err := mg.CreateList(protoList)
if err != nil {
t.Fatal(err)
}
defer func() {
err = mg.DeleteList(listAddr)
if err != nil {
t.Fatal(err)
}
newCount := countLists()
if newCount != startCount {
t.Fatalf("Expected %d lists defined; got %d", startCount, newCount)
}
}()
newCount := countLists()
if newCount <= startCount {
t.Fatalf("Expected %d lists defined; got %d", startCount+1, newCount)
}
theList, err := mg.GetListByAddress(listAddr)
if err != nil {
t.Fatal(err)
}
protoList.CreatedAt = theList.CreatedAt // ignore this field when comparing.
if theList != protoList {
t.Fatalf("Unexpected list descriptor: Expected [%#v], Got [%#v]", protoList, theList)
}
_, err = mg.UpdateList(listAddr, mailgun.List{
Description: "A list whose description changed",
})
if err != nil {
t.Fatal(err)
}
theList, err = mg.GetListByAddress(listAddr)
if err != nil {
t.Fatal(err)
}
newList := protoList
newList.Description = "A list whose description changed"
if theList != newList {
t.Fatalf("Expected [%#v], Got [%#v]", newList, theList)
}
}

View file

@ -0,0 +1,329 @@
// +build acceptance,spendMoney
package acceptance
import (
"fmt"
mailgun "github.com/mailgun/mailgun-go"
"io/ioutil"
"strings"
"testing"
"time"
)
const (
fromUser = "=?utf-8?q?Katie_Brewer=2C_CFP=C2=AE?= <joe@example.com>"
exampleSubject = "Joe's Example Subject"
exampleText = "Testing some Mailgun awesomeness!"
exampleHtml = "<html><head /><body><p>Testing some <a href=\"http://google.com?q=abc&r=def&s=ghi\">Mailgun HTML awesomeness!</a> at www.kc5tja@yahoo.com</p></body></html>"
exampleMime = `Content-Type: text/plain; charset="ascii"
Subject: Joe's Example Subject
From: Joe Example <joe@example.com>
To: BARGLEGARF <sam.falvo@rackspace.com>
Content-Transfer-Encoding: 7bit
Date: Thu, 6 Mar 2014 00:37:52 +0000
Testing some Mailgun MIME awesomeness!
`
templateText = "Greetings %recipient.name%! Your reserved seat is at table %recipient.table%."
)
func TestSendLegacyPlain(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendPlain:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendLegacyPlainWithTracking(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
m.SetTracking(true)
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendPlainWithTracking:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendLegacyPlainAt(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
m.SetDeliveryTime(time.Now().Add(5 * time.Minute))
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendPlainAt:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendLegacyHtml(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mailgun.NewMessage(fromUser, exampleSubject, exampleText, toUser)
m.SetHtml(exampleHtml)
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendHtml:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendLegacyTracking(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mailgun.NewMessage(fromUser, exampleSubject, exampleText+"Tracking!\n", toUser)
m.SetTracking(false)
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendTracking:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendLegacyTag(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mailgun.NewMessage(fromUser, exampleSubject, exampleText+"Tags Galore!\n", toUser)
m.AddTag("FooTag")
m.AddTag("BarTag")
m.AddTag("BlortTag")
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendTag:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendLegacyMIME(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
m := mailgun.NewMIMEMessage(ioutil.NopCloser(strings.NewReader(exampleMime)), toUser)
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendMIME:MSG(" + msg + "),ID(" + id + ")")
}
func TestGetStoredMessage(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
id, err := findStoredMessageID(mg) // somehow...
if err != nil {
t.Fatal(err)
}
// First, get our stored message.
msg, err := mg.GetStoredMessage(id)
if err != nil {
t.Fatal(err)
}
fields := map[string]string{
" From": msg.From,
" Sender": msg.Sender,
" Subject": msg.Subject,
"Attachments": fmt.Sprintf("%d", len(msg.Attachments)),
" Headers": fmt.Sprintf("%d", len(msg.MessageHeaders)),
}
for k, v := range fields {
fmt.Printf("%13s: %s\n", k, v)
}
// We're done with it; now delete it.
err = mg.DeleteStoredMessage(id)
if err != nil {
t.Fatal(err)
}
}
// Tries to locate the first stored event type, returning the associated stored message key.
func findStoredMessageID(mg mailgun.Mailgun) (string, error) {
ei := mg.NewEventIterator()
err := ei.GetFirstPage(mailgun.GetEventsOptions{})
for{
if err != nil {
return "", err
}
if len(ei.Events()) == 0 {
break
}
for _, event := range ei.Events() {
if event["event"] == "stored" {
s := event["storage"].(map[string]interface{})
k := s["key"]
return k.(string), nil
}
}
err = ei.GetNext()
}
return "", fmt.Errorf("No stored messages found. Try changing MG_EMAIL_TO to an address that stores messages and try again.")
}
func TestSendMGPlain(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mg.NewMessage(fromUser, exampleSubject, exampleText, toUser)
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendPlain:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendMGPlainWithTracking(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mg.NewMessage(fromUser, exampleSubject, exampleText, toUser)
m.SetTracking(true)
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendPlainWithTracking:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendMGPlainAt(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mg.NewMessage(fromUser, exampleSubject, exampleText, toUser)
m.SetDeliveryTime(time.Now().Add(5 * time.Minute))
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendPlainAt:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendMGHtml(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mg.NewMessage(fromUser, exampleSubject, exampleText, toUser)
m.SetHtml(exampleHtml)
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendHtml:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendMGTracking(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mg.NewMessage(fromUser, exampleSubject, exampleText+"Tracking!\n", toUser)
m.SetTracking(false)
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendTracking:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendMGTag(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
m := mg.NewMessage(fromUser, exampleSubject, exampleText+"Tags Galore!\n", toUser)
m.AddTag("FooTag")
m.AddTag("BarTag")
m.AddTag("BlortTag")
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendTag:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendMGMIME(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
m := mg.NewMIMEMessage(ioutil.NopCloser(strings.NewReader(exampleMime)), toUser)
msg, id, err := mg.Send(m)
if err != nil {
t.Fatal(err)
}
fmt.Println("TestSendMIME:MSG(" + msg + "),ID(" + id + ")")
}
func TestSendMGBatchFailRecipients(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
m := mg.NewMessage(fromUser, exampleSubject, exampleText+"Batch\n")
for i := 0; i < mailgun.MaxNumberOfRecipients; i++ {
m.AddRecipient("") // We expect this to indicate a failure at the API
}
err := m.AddRecipientAndVariables(toUser, nil)
if err == nil {
// If we're here, either the SDK didn't send the message,
// OR the API didn't check for empty To: headers.
t.Fatal("Expected to fail!!")
}
}
func TestSendMGBatchRecipientVariables(t *testing.T) {
toUser := reqEnv(t, "MG_EMAIL_TO")
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
m := mg.NewMessage(fromUser, exampleSubject, templateText)
err := m.AddRecipientAndVariables(toUser, map[string]interface{}{
"name": "Joe Cool Example",
"table": 42,
})
if err != nil {
t.Fatal(err)
}
_, _, err = mg.Send(m)
if err != nil {
t.Fatal(err)
}
}

View file

@ -0,0 +1,87 @@
// +build acceptance
package acceptance
import (
"github.com/mailgun/mailgun-go"
"testing"
)
func TestRouteCRUD(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
var countRoutes = func() int {
count, _, err := mg.GetRoutes(mailgun.DefaultLimit, mailgun.DefaultSkip)
if err != nil {
t.Fatal(err)
}
return count
}
routeCount := countRoutes()
newRoute, err := mg.CreateRoute(mailgun.Route{
Priority: 1,
Description: "Sample Route",
Expression: "match_recipient(\".*@samples.mailgun.org\")",
Actions: []string{
"forward(\"http://example.com/messages/\")",
"stop()",
},
})
if err != nil {
t.Fatal(err)
}
if newRoute.ID == "" {
t.Fatal("I expected the route created to have an ID associated with it.")
}
defer func() {
err = mg.DeleteRoute(newRoute.ID)
if err != nil {
t.Fatal(err)
}
newCount := countRoutes()
if newCount != routeCount {
t.Fatalf("Expected %d routes defined; got %d", routeCount, newCount)
}
}()
newCount := countRoutes()
if newCount <= routeCount {
t.Fatalf("Expected %d routes defined; got %d", routeCount+1, newCount)
}
theRoute, err := mg.GetRouteByID(newRoute.ID)
if err != nil {
t.Fatal(err)
}
if ((newRoute.Priority) != (theRoute.Priority)) ||
((newRoute.Description) != (theRoute.Description)) ||
((newRoute.Expression) != (theRoute.Expression)) ||
(len(newRoute.Actions) != len(theRoute.Actions)) ||
((newRoute.CreatedAt) != (theRoute.CreatedAt)) ||
((newRoute.ID) != (theRoute.ID)) {
t.Fatalf("Expected %#v, got %#v", newRoute, theRoute)
}
for i, action := range newRoute.Actions {
if action != theRoute.Actions[i] {
t.Fatalf("Expected %#v, got %#v", newRoute, theRoute)
}
}
changedRoute, err := mg.UpdateRoute(newRoute.ID, mailgun.Route{
Priority: 2,
})
if err != nil {
t.Fatal(err)
}
if changedRoute.Priority != 2 {
t.Fatalf("Expected a priority of 2; got %d", changedRoute.Priority)
}
if len(changedRoute.Actions) != 2 {
t.Fatalf("Expected actions to not be touched; got %d entries now", len(changedRoute.Actions))
}
}

View file

@ -0,0 +1,71 @@
// +build acceptance
package acceptance
import (
"github.com/mailgun/mailgun-go"
"testing"
)
func TestGetComplaints(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
n, complaints, err := mg.GetComplaints(-1, -1)
if err != nil {
t.Fatal(err)
}
if len(complaints) != n {
t.Fatalf("Expected %d complaints; got %d", n, len(complaints))
}
}
func TestGetComplaintFromBazNoComplaint(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
publicApiKey := reqEnv(t, "MG_PUBLIC_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, publicApiKey)
_, err := mg.GetSingleComplaint("baz@example.com")
if err == nil {
t.Fatal("Expected not-found error for missing complaint")
}
ure, ok := err.(*mailgun.UnexpectedResponseError)
if !ok {
t.Fatal("Expected UnexpectedResponseError")
}
if ure.Actual != 404 {
t.Fatalf("Expected 404 response code; got %d", ure.Actual)
}
}
func TestCreateDeleteComplaint(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
var check = func(count int) {
c, _, err := mg.GetComplaints(mailgun.DefaultLimit, mailgun.DefaultSkip)
if err != nil {
t.Fatal(err)
}
if c != count {
t.Fatalf("Expected baz@example.com to have %d complaints; got %d", count, c)
}
}
check(0)
err := mg.CreateComplaint("baz@example.com")
if err != nil {
t.Fatal(err)
}
check(1)
err = mg.DeleteComplaint("baz@example.com")
if err != nil {
t.Fatal(err)
}
check(0)
}

View file

@ -0,0 +1,40 @@
// +build acceptance
package acceptance
import (
"fmt"
mailgun "github.com/mailgun/mailgun-go"
"os"
"testing"
"text/tabwriter"
)
func TestGetStats(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
totalCount, stats, err := mg.GetStats(-1, -1, nil, "sent", "opened")
if err != nil {
t.Fatal(err)
}
fmt.Printf("Total Count: %d\n", totalCount)
tw := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', tabwriter.AlignRight)
fmt.Fprintf(tw, "Id\tEvent\tCreatedAt\tTotalCount\t\n")
for _, stat := range stats {
fmt.Fprintf(tw, "%s\t%s\t%s\t%d\t\n", stat.Id, stat.Event, stat.CreatedAt, stat.TotalCount)
}
tw.Flush()
}
func TestDeleteTag(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
err := mg.DeleteTag("newsletter")
if err != nil {
t.Fatal(err)
}
}

View file

@ -0,0 +1,71 @@
// +build acceptance
package acceptance
import (
"fmt"
mailgun "github.com/mailgun/mailgun-go"
"os"
"testing"
"text/tabwriter"
)
func TestGetUnsubscribes(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
n, us, err := mg.GetUnsubscribes(mailgun.DefaultLimit, mailgun.DefaultSkip)
if err != nil {
t.Fatal(err)
}
fmt.Printf("Received %d out of %d unsubscribe records.\n", len(us), n)
if len(us) > 0 {
tw := &tabwriter.Writer{}
tw.Init(os.Stdout, 2, 8, 2, ' ', 0)
fmt.Fprintln(tw, "ID\tAddress\tCreated At\tTag\t")
for _, u := range us {
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t\n", u.ID, u.Address, u.CreatedAt, u.Tag)
}
tw.Flush()
}
}
func TestGetUnsubscriptionByAddress(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
email := reqEnv(t, "MG_EMAIL_ADDR")
mg := mailgun.NewMailgun(domain, apiKey, "")
n, us, err := mg.GetUnsubscribesByAddress(email)
if err != nil {
t.Fatal(err)
}
fmt.Printf("Received %d out of %d unsubscribe records.\n", len(us), n)
if len(us) > 0 {
tw := &tabwriter.Writer{}
tw.Init(os.Stdout, 2, 8, 2, ' ', 0)
fmt.Fprintln(tw, "ID\tAddress\tCreated At\tTag\t")
for _, u := range us {
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t\n", u.ID, u.Address, u.CreatedAt, u.Tag)
}
tw.Flush()
}
}
func TestCreateDestroyUnsubscription(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
email := reqEnv(t, "MG_EMAIL_ADDR")
mg := mailgun.NewMailgun(domain, apiKey, "")
// Create unsubscription record
err := mg.Unsubscribe(email, "*")
if err != nil {
t.Fatal(err)
}
// Destroy the unsubscription record
err = mg.RemoveUnsubscribe(email)
if err != nil {
t.Fatal(err)
}
}

View file

@ -0,0 +1,66 @@
// +build acceptance
package acceptance
import (
"github.com/mailgun/mailgun-go"
"testing"
)
func TestWebhookCRUD(t *testing.T) {
domain := reqEnv(t, "MG_DOMAIN")
apiKey := reqEnv(t, "MG_API_KEY")
mg := mailgun.NewMailgun(domain, apiKey, "")
var countHooks = func() int {
hooks, err := mg.GetWebhooks()
if err != nil {
t.Fatal(err)
}
return len(hooks)
}
hookCount := countHooks()
err := mg.CreateWebhook("deliver", "http://www.example.com")
if err != nil {
t.Fatal(err)
}
defer func() {
err = mg.DeleteWebhook("deliver")
if err != nil {
t.Fatal(err)
}
newCount := countHooks()
if newCount != hookCount {
t.Fatalf("Expected %d routes defined; got %d", hookCount, newCount)
}
}()
newCount := countHooks()
if newCount <= hookCount {
t.Fatalf("Expected %d routes defined; got %d", hookCount+1, newCount)
}
theURL, err := mg.GetWebhookByType("deliver")
if err != nil {
t.Fatal(err)
}
if theURL != "http://www.example.com" {
t.Fatalf("Expected http://www.example.com, got %#v", theURL)
}
err = mg.UpdateWebhook("deliver", "http://api.example.com")
if err != nil {
t.Fatal(err)
}
hooks, err := mg.GetWebhooks()
if err != nil {
t.Fatal(err)
}
if hooks["deliver"] != "http://api.example.com" {
t.Fatalf("Expected http://api.example.com, got %#v", hooks["deliver"])
}
}

View file

@ -0,0 +1,122 @@
package mailgun
import (
"github.com/mbanzon/simplehttp"
"strconv"
"time"
)
// Bounce aggregates data relating to undeliverable messages to a specific intended recipient,
// identified by Address.
// Code provides the SMTP error code causing the bounce,
// while Error provides a human readable reason why.
// CreatedAt provides the time at which Mailgun detected the bounce.
type Bounce struct {
CreatedAt string `json:"created_at"`
code interface{} `json:"code"`
Address string `json:"address"`
Error string `json:"error"`
}
type bounceEnvelope struct {
TotalCount int `json:"total_count"`
Items []Bounce `json:"items"`
}
type singleBounceEnvelope struct {
Bounce Bounce `json:"bounce"`
}
// GetCreatedAt parses the textual, RFC-822 timestamp into a standard Go-compatible
// Time structure.
func (i Bounce) GetCreatedAt() (t time.Time, err error) {
return parseMailgunTime(i.CreatedAt)
}
// GetCode will return the bounce code for the message, regardless if it was
// returned as a string or as an integer. This method overcomes a protocol
// bug in the Mailgun API.
func (b Bounce) GetCode() (int, error) {
switch c := b.code.(type) {
case int:
return c, nil
case string:
return strconv.Atoi(c)
default:
return -1, strconv.ErrSyntax
}
}
// GetBounces returns a complete set of bounces logged against the sender's domain, if any.
// The results include the total number of bounces (regardless of skip or limit settings),
// and the slice of bounces specified, if successful.
// Note that the length of the slice may be smaller than the total number of bounces.
func (m *MailgunImpl) GetBounces(limit, skip int) (int, []Bounce, error) {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, bouncesEndpoint))
if limit != -1 {
r.AddParameter("limit", strconv.Itoa(limit))
}
if skip != -1 {
r.AddParameter("skip", strconv.Itoa(skip))
}
r.SetBasicAuth(basicAuthUser, m.ApiKey())
var response bounceEnvelope
err := getResponseFromJSON(r, &response)
if err != nil {
return -1, nil, err
}
return response.TotalCount, response.Items, nil
}
// GetSingleBounce retrieves a single bounce record, if any exist, for the given recipient address.
func (m *MailgunImpl) GetSingleBounce(address string) (Bounce, error) {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, bouncesEndpoint) + "/" + address)
r.SetBasicAuth(basicAuthUser, m.ApiKey())
var response singleBounceEnvelope
err := getResponseFromJSON(r, &response)
return response.Bounce, err
}
// AddBounce files a bounce report.
// Address identifies the intended recipient of the message that bounced.
// Code corresponds to the numeric response given by the e-mail server which rejected the message.
// Error providees the corresponding human readable reason for the problem.
// For example,
// here's how the these two fields relate.
// Suppose the SMTP server responds with an error, as below.
// Then, . . .
//
// 550 Requested action not taken: mailbox unavailable
// \___/\_______________________________________________/
// | |
// `-- Code `-- Error
//
// Note that both code and error exist as strings, even though
// code will report as a number.
func (m *MailgunImpl) AddBounce(address, code, error string) error {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, bouncesEndpoint))
r.SetBasicAuth(basicAuthUser, m.ApiKey())
payload := simplehttp.NewUrlEncodedPayload()
payload.AddValue("address", address)
if code != "" {
payload.AddValue("code", code)
}
if error != "" {
payload.AddValue("error", error)
}
_, err := makePostRequest(r, payload)
return err
}
// DeleteBounce removes all bounces associted with the provided e-mail address.
func (m *MailgunImpl) DeleteBounce(address string) error {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, bouncesEndpoint) + "/" + address)
r.SetBasicAuth(basicAuthUser, m.ApiKey())
_, err := makeDeleteRequest(r)
return err
}

View file

@ -0,0 +1,79 @@
package mailgun
import (
"github.com/mbanzon/simplehttp"
)
// Campaigns have been deprecated since development work on this SDK commenced.
// Please refer to http://documentation.mailgun.com/api_reference .
type Campaign struct {
Id string `json:"id"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
DeliveredCount int `json:"delivered_count"`
ClickedCount int `json:"clicked_count"`
OpenedCount int `json:"opened_count"`
SubmittedCount int `json:"submitted_count"`
UnsubscribedCount int `json:"unsubscribed_count"`
BouncedCount int `json:"bounced_count"`
ComplainedCount int `json:"complained_count"`
DroppedCount int `json:"dropped_count"`
}
type campaignsEnvelope struct {
TotalCount int `json:"total_count"`
Items []Campaign `json:"items"`
}
// Campaigns have been deprecated since development work on this SDK commenced.
// Please refer to http://documentation.mailgun.com/api_reference .
func (m *MailgunImpl) GetCampaigns() (int, []Campaign, error) {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, campaignsEndpoint))
r.SetBasicAuth(basicAuthUser, m.ApiKey())
var envelope campaignsEnvelope
err := getResponseFromJSON(r, &envelope)
if err != nil {
return -1, nil, err
}
return envelope.TotalCount, envelope.Items, nil
}
// Campaigns have been deprecated since development work on this SDK commenced.
// Please refer to http://documentation.mailgun.com/api_reference .
func (m *MailgunImpl) CreateCampaign(name, id string) error {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, campaignsEndpoint))
r.SetBasicAuth(basicAuthUser, m.ApiKey())
payload := simplehttp.NewUrlEncodedPayload()
payload.AddValue("name", name)
if id != "" {
payload.AddValue("id", id)
}
_, err := makePostRequest(r, payload)
return err
}
// Campaigns have been deprecated since development work on this SDK commenced.
// Please refer to http://documentation.mailgun.com/api_reference .
func (m *MailgunImpl) UpdateCampaign(oldId, name, newId string) error {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, campaignsEndpoint) + "/" + oldId)
r.SetBasicAuth(basicAuthUser, m.ApiKey())
payload := simplehttp.NewUrlEncodedPayload()
payload.AddValue("name", name)
if newId != "" {
payload.AddValue("id", newId)
}
_, err := makePostRequest(r, payload)
return err
}
// Campaigns have been deprecated since development work on this SDK commenced.
// Please refer to http://documentation.mailgun.com/api_reference .
func (m *MailgunImpl) DeleteCampaign(id string) error {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, campaignsEndpoint) + "/" + id)
r.SetBasicAuth(basicAuthUser, m.ApiKey())
_, err := makeDeleteRequest(r)
return err
}

View file

@ -0,0 +1,76 @@
package mailgun
import (
"fmt"
"github.com/mbanzon/simplehttp"
"strconv"
)
// A Credential structure describes a principle allowed to send or receive mail at the domain.
type Credential struct {
CreatedAt string `json:"created_at"`
Login string `json:"login"`
Password string `json:"password"`
}
// ErrEmptyParam results occur when a required parameter is missing.
var ErrEmptyParam = fmt.Errorf("empty or illegal parameter")
// GetCredentials returns the (possibly zero-length) list of credentials associated with your domain.
func (mg *MailgunImpl) GetCredentials(limit, skip int) (int, []Credential, error) {
r := simplehttp.NewHTTPRequest(generateCredentialsUrl(mg, ""))
if limit != DefaultLimit {
r.AddParameter("limit", strconv.Itoa(limit))
}
if skip != DefaultSkip {
r.AddParameter("skip", strconv.Itoa(skip))
}
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
var envelope struct {
TotalCount int `json:"total_count"`
Items []Credential `json:"items"`
}
err := getResponseFromJSON(r, &envelope)
if err != nil {
return -1, nil, err
}
return envelope.TotalCount, envelope.Items, nil
}
// CreateCredential attempts to create associate a new principle with your domain.
func (mg *MailgunImpl) CreateCredential(login, password string) error {
if (login == "") || (password == "") {
return ErrEmptyParam
}
r := simplehttp.NewHTTPRequest(generateCredentialsUrl(mg, ""))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
p.AddValue("login", login)
p.AddValue("password", password)
_, err := makePostRequest(r, p)
return err
}
// ChangeCredentialPassword attempts to alter the indicated credential's password.
func (mg *MailgunImpl) ChangeCredentialPassword(id, password string) error {
if (id == "") || (password == "") {
return ErrEmptyParam
}
r := simplehttp.NewHTTPRequest(generateCredentialsUrl(mg, id))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
p.AddValue("password", password)
_, err := makePutRequest(r, p)
return err
}
// DeleteCredential attempts to remove the indicated principle from the domain.
func (mg *MailgunImpl) DeleteCredential(id string) error {
if id == "" {
return ErrEmptyParam
}
r := simplehttp.NewHTTPRequest(generateCredentialsUrl(mg, id))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
_, err := makeDeleteRequest(r)
return err
}

View file

@ -0,0 +1,121 @@
package mailgun
import (
"github.com/mbanzon/simplehttp"
"strconv"
"time"
)
// DefaultLimit and DefaultSkip instruct the SDK to rely on Mailgun's reasonable defaults for pagination settings.
const (
DefaultLimit = -1
DefaultSkip = -1
)
// Disabled, Tag, and Delete indicate spam actions.
// Disabled prevents Mailgun from taking any action on what it perceives to be spam.
// Tag instruments the received message with headers providing a measure of its spamness.
// Delete instructs Mailgun to just block or delete the message all-together.
const (
Tag = "tag"
Disabled = "disabled"
Delete = "delete"
)
// A Domain structure holds information about a domain used when sending mail.
// The SpamAction field must be one of Tag, Disabled, or Delete.
type Domain struct {
CreatedAt string `json:"created_at"`
SMTPLogin string `json:"smtp_login"`
Name string `json:"name"`
SMTPPassword string `json:"smtp_password"`
Wildcard bool `json:"wildcard"`
SpamAction string `json:"spam_action"`
}
// DNSRecord structures describe intended records to properly configure your domain for use with Mailgun.
// Note that Mailgun does not host DNS records.
type DNSRecord struct {
Priority string
RecordType string `json:"record_type"`
Valid string
Name string
Value string
}
type domainsEnvelope struct {
TotalCount int `json:"total_count"`
Items []Domain `json:"items"`
}
type singleDomainEnvelope struct {
Domain Domain `json:"domain"`
ReceivingDNSRecords []DNSRecord `json:"receiving_dns_records"`
SendingDNSRecords []DNSRecord `json:"sending_dns_records"`
}
// GetCreatedAt returns the time the domain was created as a normal Go time.Time type.
func (d Domain) GetCreatedAt() (t time.Time, err error) {
t, err = parseMailgunTime(d.CreatedAt)
return
}
// GetDomains retrieves a set of domains from Mailgun.
//
// Assuming no error, both the number of items retrieved and a slice of Domain instances.
// The number of items returned may be less than the specified limit, if it's specified.
// Note that zero items and a zero-length slice do not necessarily imply an error occurred.
// Except for the error itself, all results are undefined in the event of an error.
func (m *MailgunImpl) GetDomains(limit, skip int) (int, []Domain, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(domainsEndpoint))
if limit != DefaultLimit {
r.AddParameter("limit", strconv.Itoa(limit))
}
if skip != DefaultSkip {
r.AddParameter("skip", strconv.Itoa(skip))
}
r.SetBasicAuth(basicAuthUser, m.ApiKey())
var envelope domainsEnvelope
err := getResponseFromJSON(r, &envelope)
if err != nil {
return -1, nil, err
}
return envelope.TotalCount, envelope.Items, nil
}
// Retrieve detailed information about the named domain.
func (m *MailgunImpl) GetSingleDomain(domain string) (Domain, []DNSRecord, []DNSRecord, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(domainsEndpoint) + "/" + domain)
r.SetBasicAuth(basicAuthUser, m.ApiKey())
var envelope singleDomainEnvelope
err := getResponseFromJSON(r, &envelope)
return envelope.Domain, envelope.ReceivingDNSRecords, envelope.SendingDNSRecords, err
}
// CreateDomain instructs Mailgun to create a new domain for your account.
// The name parameter identifies the domain.
// The smtpPassword parameter provides an access credential for the domain.
// 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 (m *MailgunImpl) CreateDomain(name string, smtpPassword string, spamAction string, wildcard bool) error {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(domainsEndpoint))
r.SetBasicAuth(basicAuthUser, m.ApiKey())
payload := simplehttp.NewUrlEncodedPayload()
payload.AddValue("name", name)
payload.AddValue("smtp_password", smtpPassword)
payload.AddValue("spam_action", spamAction)
payload.AddValue("wildcard", strconv.FormatBool(wildcard))
_, err := makePostRequest(r, payload)
return err
}
// DeleteDomain instructs Mailgun to dispose of the named domain name.
func (m *MailgunImpl) DeleteDomain(name string) error {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(domainsEndpoint) + "/" + name)
r.SetBasicAuth(basicAuthUser, m.ApiKey())
_, err := makeDeleteRequest(r)
return err
}

View file

@ -0,0 +1,71 @@
package mailgun
import (
"github.com/mbanzon/simplehttp"
"strings"
)
// The EmailVerificationParts structure breaks out the basic elements of an email address.
// LocalPart includes everything up to the '@' in an e-mail address.
// Domain includes everything after the '@'.
// DisplayName is no longer used, and will appear as "".
type EmailVerificationParts struct {
LocalPart string `json:"local_part"`
Domain string `json:"domain"`
DisplayName string `json:"display_name"`
}
// EmailVerification records basic facts about a validated e-mail address.
// See the ValidateEmail method and example for more details.
//
// IsValid indicates whether an email address conforms to IETF RFC standards.
// Parts records the different subfields of an email address.
// Address echoes the address provided.
// DidYouMean provides a simple recommendation in case the address is invalid or
// Mailgun thinks you might have a typo.
// DidYouMean may be empty (""), in which case Mailgun has no recommendation to give.
// The existence of DidYouMean does NOT imply the email provided has anything wrong with it.
type EmailVerification struct {
IsValid bool `json:"is_valid"`
Parts EmailVerificationParts `json:"parts"`
Address string `json:"address"`
DidYouMean string `json:"did_you_mean"`
}
type addressParseResult struct {
Parsed []string `json:"parsed"`
Unparseable []string `json:"unparseable"`
}
// ValidateEmail performs various checks on the email address provided to ensure it's correctly formatted.
// It may also be used to break an email address into its sub-components. (See example.)
// NOTE: Use of this function requires a proper public API key. The private API key will not work.
func (m *MailgunImpl) ValidateEmail(email string) (EmailVerification, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(addressValidateEndpoint))
r.AddParameter("address", email)
r.SetBasicAuth(basicAuthUser, m.PublicApiKey())
var response EmailVerification
err := getResponseFromJSON(r, &response)
if err != nil {
return EmailVerification{}, err
}
return response, nil
}
// ParseAddresses takes a list of addresses and sorts them into valid and invalid address categories.
// NOTE: Use of this function requires a proper public API key. The private API key will not work.
func (m *MailgunImpl) ParseAddresses(addresses ...string) ([]string, []string, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(addressParseEndpoint))
r.AddParameter("addresses", strings.Join(addresses, ","))
r.SetBasicAuth(basicAuthUser, m.PublicApiKey())
var response addressParseResult
err := getResponseFromJSON(r, &response)
if err != nil {
return nil, nil, err
}
return response.Parsed, response.Unparseable, nil
}

View file

@ -0,0 +1,130 @@
package mailgun
import (
"fmt"
"github.com/mbanzon/simplehttp"
"time"
)
// Events are open-ended, loosely-defined JSON documents.
// They will always have an event and a timestamp field, however.
type Event map[string]interface{}
// noTime always equals an uninitialized Time structure.
// It's used to detect when a time parameter is provided.
var noTime time.Time
// GetEventsOptions lets the caller of GetEvents() specify how the results are to be returned.
// Begin and End time-box the results returned.
// ForceAscending and ForceDescending are used to force Mailgun to use a given traversal order of the events.
// If both ForceAscending and ForceDescending are true, an error will result.
// If none, the default will be inferred from the Begin and End parameters.
// Limit caps the number of results returned. If left unspecified, Mailgun assumes 100.
// Compact, if true, compacts the returned JSON to minimize transmission bandwidth.
// Otherwise, the JSON is spaced appropriately for human consumption.
// Filter allows the caller to provide more specialized filters on the query.
// Consult the Mailgun documentation for more details.
type GetEventsOptions struct {
Begin, End time.Time
ForceAscending, ForceDescending, Compact bool
Limit int
Filter map[string]string
}
// EventIterator maintains the state necessary for paging though small parcels of a larger set of events.
type EventIterator struct {
events []Event
nextURL, prevURL string
mg Mailgun
}
// NewEventIterator creates a new iterator for events.
// Use GetFirstPage to retrieve the first batch of events.
// Use Next and Previous thereafter as appropriate to iterate through sets of data.
func (mg *MailgunImpl) NewEventIterator() *EventIterator {
return &EventIterator{mg: mg}
}
// Events returns the most recently retrieved batch of events.
// The length is guaranteed to fall between 0 and the limit set in the GetEventsOptions structure passed to GetFirstPage.
func (ei *EventIterator) Events() []Event {
return ei.events
}
// GetFirstPage retrieves the first batch of events, according to your criteria.
// See the GetEventsOptions structure for more details on how the fields affect the data returned.
func (ei *EventIterator) GetFirstPage(opts GetEventsOptions) error {
if opts.ForceAscending && opts.ForceDescending {
return fmt.Errorf("collation cannot at once be both ascending and descending")
}
payload := simplehttp.NewUrlEncodedPayload()
if opts.Limit != 0 {
payload.AddValue("limit", fmt.Sprintf("%d", opts.Limit))
}
if opts.Compact {
payload.AddValue("pretty", "no")
}
if opts.ForceAscending {
payload.AddValue("ascending", "yes")
}
if opts.ForceDescending {
payload.AddValue("ascending", "no")
}
if opts.Begin != noTime {
payload.AddValue("begin", formatMailgunTime(&opts.Begin))
}
if opts.End != noTime {
payload.AddValue("end", formatMailgunTime(&opts.End))
}
if opts.Filter != nil {
for k, v := range opts.Filter {
payload.AddValue(k, v)
}
}
url, err := generateParameterizedUrl(ei.mg, eventsEndpoint, payload)
if err != nil {
return err
}
return ei.fetch(url)
}
// Retrieves the chronologically previous batch of events, if any exist.
// You know you're at the end of the list when len(Events())==0.
func (ei *EventIterator) GetPrevious() error {
return ei.fetch(ei.prevURL)
}
// Retrieves the chronologically next batch of events, if any exist.
// You know you're at the end of the list when len(Events())==0.
func (ei *EventIterator) GetNext() error {
return ei.fetch(ei.nextURL)
}
// GetFirstPage, GetPrevious, and GetNext all have a common body of code.
// fetch completes the API fetch common to all three of these functions.
func (ei *EventIterator) fetch(url string) error {
r := simplehttp.NewHTTPRequest(url)
r.SetBasicAuth(basicAuthUser, ei.mg.ApiKey())
var response map[string]interface{}
err := getResponseFromJSON(r, &response)
if err != nil {
return err
}
items := response["items"].([]interface{})
ei.events = make([]Event, len(items))
for i, item := range items {
ei.events[i] = item.(map[string]interface{})
}
pagings := response["paging"].(map[string]interface{})
links := make(map[string]string, len(pagings))
for key, page := range pagings {
links[key] = page.(string)
}
ei.nextURL = links["next"]
ei.prevURL = links["previous"]
return err
}

View file

@ -0,0 +1,117 @@
package mailgun
import (
"io/ioutil"
"log"
"strings"
"time"
)
func ExampleMailgunImpl_ValidateEmail() {
mg := NewMailgun("example.com", "", "my_public_api_key")
ev, err := mg.ValidateEmail("joe@example.com")
if err != nil {
log.Fatal(err)
}
if !ev.IsValid {
log.Fatal("Expected valid e-mail address")
}
log.Printf("Parts local_part=%s domain=%s display_name=%s", ev.Parts.LocalPart, ev.Parts.Domain, ev.Parts.DisplayName)
if ev.DidYouMean != "" {
log.Printf("The address is syntactically valid, but perhaps has a typo.")
log.Printf("Did you mean %s instead?", ev.DidYouMean)
}
}
func ExampleMailgunImpl_ParseAddresses() {
mg := NewMailgun("example.com", "", "my_public_api_key")
addressesThatParsed, unparsableAddresses, err := mg.ParseAddresses("Alice <alice@example.com>", "bob@example.com", "example.com")
if err != nil {
log.Fatal(err)
}
hittest := map[string]bool{
"Alice <alice@example.com>": true,
"bob@example.com": true,
}
for _, a := range addressesThatParsed {
if !hittest[a] {
log.Fatalf("Expected %s to be parsable", a)
}
}
if len(unparsableAddresses) != 1 {
log.Fatalf("Expected 1 address to be unparsable; got %d", len(unparsableAddresses))
}
}
func ExampleMailgunImpl_UpdateList() {
mg := NewMailgun("example.com", "my_api_key", "")
_, err := mg.UpdateList("joe-stat@example.com", List{
Name: "Joe Stat",
Description: "Joe's status report list",
})
if err != nil {
log.Fatal(err)
}
}
func ExampleMailgunImpl_Send_constructed() {
mg := NewMailgun("example.com", "my_api_key", "")
m := NewMessage(
"Excited User <me@example.com>",
"Hello World",
"Testing some Mailgun Awesomeness!",
"baz@example.com",
"bar@example.com",
)
m.SetTracking(true)
m.SetDeliveryTime(time.Now().Add(24 * time.Hour))
m.SetHtml("<html><body><h1>Testing some Mailgun Awesomeness!!</h1></body></html>")
_, id, err := mg.Send(m)
if err != nil {
log.Fatal(err)
}
log.Printf("Message id=%s", id)
}
func ExampleMailgunImpl_Send_mime() {
exampleMime := `Content-Type: text/plain; charset="ascii"
Subject: Joe's Example Subject
From: Joe Example <joe@example.com>
To: BARGLEGARF <bargle.garf@example.com>
Content-Transfer-Encoding: 7bit
Date: Thu, 6 Mar 2014 00:37:52 +0000
Testing some Mailgun MIME awesomeness!
`
mg := NewMailgun("example.com", "my_api_key", "")
m := NewMIMEMessage(ioutil.NopCloser(strings.NewReader(exampleMime)), "bargle.garf@example.com")
_, id, err := mg.Send(m)
if err != nil {
log.Fatal(err)
}
log.Printf("Message id=%s", id)
}
func ExampleMailgunImpl_GetRoutes() {
mg := NewMailgun("example.com", "my_api_key", "")
n, routes, err := mg.GetRoutes(DefaultLimit, DefaultSkip)
if err != nil {
log.Fatal(err)
}
if n > len(routes) {
log.Printf("More routes exist than has been returned.")
}
for _, r := range routes {
log.Printf("Route pri=%d expr=%s desc=%s", r.Priority, r.Expression, r.Description)
}
}
func ExampleMailgunImpl_UpdateRoute() {
mg := NewMailgun("example.com", "my_api_key", "")
_, err := mg.UpdateRoute("route-id-here", Route{
Priority: 2,
})
if err != nil {
log.Fatal(err)
}
}

View file

@ -0,0 +1,291 @@
// TODO(sfalvo):
// Document how to run acceptance tests.
// The mailgun package provides methods for interacting with the Mailgun API.
// It automates the HTTP request/response cycle, encodings, and other details needed by the API.
// This SDK lets you do everything the API lets you, in a more Go-friendly way.
//
// For further information please see the Mailgun documentation at
// http://documentation.mailgun.com/
//
// Original Author: Michael Banzon
// Contributions: Samuel A. Falvo II <sam.falvo %at% rackspace.com>
// Version: 0.99.0
//
// Examples
//
// This document includes a number of examples which illustrates some aspects of the GUI which might be misleading or confusing.
// All examples included are derived from an acceptance test.
// Note that every SDK function has a corresponding acceptance test, so
// if you don't find an example for a function you'd like to know more about,
// please check the acceptance sub-package for a corresponding test.
// Of course, contributions to the documentation are always welcome as well.
// Feel free to submit a pull request or open a Github issue if you cannot find an example to suit your needs.
//
// Limit and Skip Settings
//
// Many SDK functions consume a pair of parameters called limit and skip.
// These help control how much data Mailgun sends over the wire.
// Limit, as you'd expect, gives a count of the number of records you want to receive.
// Note that, at present, Mailgun imposes its own cap of 100, for all API endpoints.
// Skip indicates where in the data set you want to start receiving from.
// Mailgun defaults to the very beginning of the dataset if not specified explicitly.
//
// If you don't particularly care how much data you receive, you may specify DefaultLimit.
// If you similarly don't care about where the data starts, you may specify DefaultSkip.
//
// Functions that Return Totals
//
// Functions which accept a limit and skip setting, in general,
// will also return a total count of the items returned.
// Note that this total count is not the total in the bundle returned by the call.
// You can determine that easily enough with Go's len() function.
// The total that you receive actually refers to the complete set of data on the server.
// This total may well exceed the size returned from the API.
//
// If this happens, you may find yourself needing to iterate over the dataset of interest.
// For example:
//
// // Get total amount of stuff we have to work with.
// mg := NewMailgun("example.com", "my_api_key", "")
// n, _, err := mg.GetStats(1, 0, nil, "sent", "opened")
// if err != nil {
// t.Fatal(err)
// }
// // Loop over it all.
// for sk := 0; sk < n; sk += limit {
// _, stats, err := mg.GetStats(limit, sk, nil, "sent", "opened")
// if err != nil {
// t.Fatal(err)
// }
// doSomethingWith(stats)
// }
//
// License
//
// Copyright (c) 2013-2014, Michael Banzon.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice, this
// list of conditions and the following disclaimer in the documentation and/or
// other materials provided with the distribution.
//
// * Neither the names of Mailgun, Michael Banzon, nor the names of their
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package mailgun
import (
"fmt"
"github.com/mbanzon/simplehttp"
"io"
"time"
)
const (
apiBase = "https://api.mailgun.net/v2"
messagesEndpoint = "messages"
mimeMessagesEndpoint = "messages.mime"
addressValidateEndpoint = "address/validate"
addressParseEndpoint = "address/parse"
bouncesEndpoint = "bounces"
statsEndpoint = "stats"
domainsEndpoint = "domains"
deleteTagEndpoint = "tags"
campaignsEndpoint = "campaigns"
eventsEndpoint = "events"
credentialsEndpoint = "credentials"
unsubscribesEndpoint = "unsubscribes"
routesEndpoint = "routes"
webhooksEndpoint = "webhooks"
listsEndpoint = "lists"
basicAuthUser = "api"
)
// Mailgun defines the supported subset of the Mailgun API.
// The Mailgun API may contain additional features which have been deprecated since writing this SDK.
// This SDK only covers currently supported interface endpoints.
//
// Note that Mailgun reserves the right to deprecate endpoints.
// Some endpoints listed in this interface may, at any time, become obsolete.
// Always double-check with the Mailgun API Documentation to
// determine the currently supported feature set.
type Mailgun interface {
Domain() string
ApiKey() string
PublicApiKey() string
Send(m *Message) (string, string, error)
ValidateEmail(email string) (EmailVerification, error)
ParseAddresses(addresses ...string) ([]string, []string, error)
GetBounces(limit, skip int) (int, []Bounce, error)
GetSingleBounce(address string) (Bounce, error)
AddBounce(address, code, error string) error
DeleteBounce(address string) error
GetStats(limit int, skip int, startDate *time.Time, event ...string) (int, []Stat, error)
DeleteTag(tag string) error
GetDomains(limit, skip int) (int, []Domain, error)
GetSingleDomain(domain string) (Domain, []DNSRecord, []DNSRecord, error)
CreateDomain(name string, smtpPassword string, spamAction string, wildcard bool) error
DeleteDomain(name string) error
GetCampaigns() (int, []Campaign, error)
CreateCampaign(name, id string) error
UpdateCampaign(oldId, name, newId string) error
DeleteCampaign(id string) error
GetComplaints(limit, skip int) (int, []Complaint, error)
GetSingleComplaint(address string) (Complaint, error)
GetStoredMessage(id string) (StoredMessage, error)
GetStoredMessageRaw(id string) (StoredMessageRaw, error)
DeleteStoredMessage(id string) error
GetCredentials(limit, skip int) (int, []Credential, error)
CreateCredential(login, password string) error
ChangeCredentialPassword(id, password string) error
DeleteCredential(id string) error
GetUnsubscribes(limit, skip int) (int, []Unsubscription, error)
GetUnsubscribesByAddress(string) (int, []Unsubscription, error)
Unsubscribe(address, tag string) error
RemoveUnsubscribe(string) error
CreateComplaint(string) error
DeleteComplaint(string) error
GetRoutes(limit, skip int) (int, []Route, error)
GetRouteByID(string) (Route, error)
CreateRoute(Route) (Route, error)
DeleteRoute(string) error
UpdateRoute(string, Route) (Route, error)
GetWebhooks() (map[string]string, error)
CreateWebhook(kind, url string) error
DeleteWebhook(kind string) error
GetWebhookByType(kind string) (string, error)
UpdateWebhook(kind, url string) error
GetLists(limit, skip int, filter string) (int, []List, error)
CreateList(List) (List, error)
DeleteList(string) error
GetListByAddress(string) (List, error)
UpdateList(string, List) (List, error)
GetMembers(limit, skip int, subfilter *bool, address string) (int, []Member, error)
GetMemberByAddress(MemberAddr, listAddr string) (Member, error)
CreateMember(merge bool, addr string, prototype Member) error
CreateMemberList(subscribed *bool, addr string, newMembers []interface{}) error
UpdateMember(Member, list string, prototype Member) (Member, error)
DeleteMember(Member, list string) error
NewMessage(from, subject, text string, to ...string) *Message
NewMIMEMessage(body io.ReadCloser, to ...string) *Message
NewEventIterator() *EventIterator
}
// MailgunImpl bundles data needed by a large number of methods in order to interact with the Mailgun API.
// Colloquially, we refer to instances of this structure as "clients."
type MailgunImpl struct {
domain string
apiKey string
publicApiKey string
}
// NewMailGun creates a new client instance.
func NewMailgun(domain, apiKey, publicApiKey string) Mailgun {
m := MailgunImpl{domain: domain, apiKey: apiKey, publicApiKey: publicApiKey}
return &m
}
// Domain returns the domain configured for this client.
func (m *MailgunImpl) Domain() string {
return m.domain
}
// ApiKey returns the API key configured for this client.
func (m *MailgunImpl) ApiKey() string {
return m.apiKey
}
// PublicApiKey returns the public API key configured for this client.
func (m *MailgunImpl) PublicApiKey() string {
return m.publicApiKey
}
// generateApiUrl renders a URL for an API endpoint using the domain and endpoint name.
func generateApiUrl(m Mailgun, endpoint string) string {
return fmt.Sprintf("%s/%s/%s", apiBase, m.Domain(), endpoint)
}
// generateMemberApiUrl renders a URL relevant for specifying mailing list members.
// The address parameter refers to the mailing list in question.
func generateMemberApiUrl(endpoint, address string) string {
return fmt.Sprintf("%s/%s/%s/members", apiBase, endpoint, address)
}
// generateApiUrlWithTarget works as generateApiUrl,
// but consumes an additional resource parameter called 'target'.
func generateApiUrlWithTarget(m Mailgun, endpoint, target string) string {
tail := ""
if target != "" {
tail = fmt.Sprintf("/%s", target)
}
return fmt.Sprintf("%s%s", generateApiUrl(m, endpoint), tail)
}
// generateDomainApiUrl renders a URL as generateApiUrl, but
// addresses a family of functions which have a non-standard URL structure.
// Most URLs consume a domain in the 2nd position, but some endpoints
// require the word "domains" to be there instead.
func generateDomainApiUrl(m Mailgun, endpoint string) string {
return fmt.Sprintf("%s/domains/%s/%s", apiBase, m.Domain(), endpoint)
}
// generateCredentialsUrl renders a URL as generateDomainApiUrl,
// but focuses on the SMTP credentials family of API functions.
func generateCredentialsUrl(m Mailgun, id string) string {
tail := ""
if id != "" {
tail = fmt.Sprintf("/%s", id)
}
return generateDomainApiUrl(m, fmt.Sprintf("credentials%s", tail))
// return fmt.Sprintf("%s/domains/%s/credentials%s", apiBase, m.Domain(), tail)
}
// generateStoredMessageUrl generates the URL needed to acquire a copy of a stored message.
func generateStoredMessageUrl(m Mailgun, endpoint, id string) string {
return generateDomainApiUrl(m, fmt.Sprintf("%s/%s", endpoint, id))
// return fmt.Sprintf("%s/domains/%s/%s/%s", apiBase, m.Domain(), endpoint, id)
}
// generatePublicApiUrl works as generateApiUrl, except that generatePublicApiUrl has no need for the domain.
func generatePublicApiUrl(endpoint string) string {
return fmt.Sprintf("%s/%s", apiBase, endpoint)
}
// generateParameterizedUrl works as generateApiUrl, but supports query parameters.
func generateParameterizedUrl(m Mailgun, endpoint string, payload simplehttp.Payload) (string, error) {
paramBuffer, err := payload.GetPayloadBuffer()
if err != nil {
return "", err
}
params := string(paramBuffer.Bytes())
return fmt.Sprintf("%s?%s", generateApiUrl(m, eventsEndpoint), params), nil
}
// parseMailgunTime translates a timestamp as returned by Mailgun into a Go standard timestamp.
func parseMailgunTime(ts string) (t time.Time, err error) {
t, err = time.Parse("Mon, 2 Jan 2006 15:04:05 MST", ts)
return
}
// formatMailgunTime translates a timestamp into a human-readable form.
func formatMailgunTime(t *time.Time) string {
return t.Format("Mon, 2 Jan 2006 15:04:05 -0700")
}

View file

@ -0,0 +1,68 @@
package mailgun
import (
"strconv"
"testing"
)
const domain = "valid-mailgun-domain"
const apiKey = "valid-mailgun-api-key"
const publicApiKey = "valid-mailgun-public-api-key"
func TestMailgun(t *testing.T) {
m := NewMailgun(domain, apiKey, publicApiKey)
if domain != m.Domain() {
t.Fatal("Domain not equal!")
}
if apiKey != m.ApiKey() {
t.Fatal("ApiKey not equal!")
}
if publicApiKey != m.PublicApiKey() {
t.Fatal("PublicApiKey not equal!")
}
}
func TestBounceGetCode(t *testing.T) {
b1 := &Bounce{
CreatedAt: "blah",
code: 123,
Address: "blort",
Error: "bletch",
}
c, err := b1.GetCode()
if err != nil {
t.Fatal(err)
}
if c != 123 {
t.Fatal("Expected 123; got ", c)
}
b2 := &Bounce{
CreatedAt: "blah",
code: "456",
Address: "blort",
Error: "Bletch",
}
c, err = b2.GetCode()
if err != nil {
t.Fatal(err)
}
if c != 456 {
t.Fatal("Expected 456; got ", c)
}
b3 := &Bounce{
CreatedAt: "blah",
code: "456H",
Address: "blort",
Error: "Bletch",
}
c, err = b3.GetCode()
e, ok := err.(*strconv.NumError)
if !ok && e != nil {
t.Fatal("Expected a syntax error in numeric conversion: got ", err)
}
}

View file

@ -0,0 +1,307 @@
package mailgun
import (
"encoding/json"
"fmt"
"github.com/mbanzon/simplehttp"
"strconv"
)
// A mailing list may have one of three membership modes.
// ReadOnly specifies that nobody, including Members,
// may send messages to the mailing list.
// Messages distributed on such lists come from list administrator accounts only.
// Members specifies that only those who subscribe to the mailing list may send messages.
// Everyone specifies that anyone and everyone may both read and submit messages
// to the mailing list, including non-subscribers.
const (
ReadOnly = "readonly"
Members = "members"
Everyone = "everyone"
)
// Mailing list members have an attribute that determines if they've subscribed to the mailing list or not.
// This attribute may be used to filter the results returned by GetSubscribers().
// All, Subscribed, and Unsubscribed provides a convenient and readable syntax for specifying the scope of the search.
var (
All *bool = nil
Subscribed *bool = &yes
Unsubscribed *bool = &no
)
// yes and no are variables which provide us the ability to take their addresses.
// Subscribed and Unsubscribed are pointers to these booleans.
//
// We use a pointer to boolean as a kind of trinary data type:
// if nil, the relevant data type remains unspecified.
// Otherwise, its value is either true or false.
var (
yes bool = true
no bool = false
)
// A List structure provides information for a mailing list.
//
// AccessLevel may be one of ReadOnly, Members, or Everyone.
type List struct {
Address string `json:"address",omitempty"`
Name string `json:"name",omitempty"`
Description string `json:"description",omitempty"`
AccessLevel string `json:"access_level",omitempty"`
CreatedAt string `json:"created_at",omitempty"`
MembersCount int `json:"members_count",omitempty"`
}
// A Member structure represents a member of the mailing list.
// The Vars field can represent any JSON-encodable data.
type Member struct {
Address string `json:"address,omitempty"`
Name string `json:"name,omitempty"`
Subscribed *bool `json:"subscribed,omitempty"`
Vars map[string]interface{} `json:"vars,omitempty"`
}
// GetLists returns the specified set of mailing lists administered by your account.
func (mg *MailgunImpl) GetLists(limit, skip int, filter string) (int, []List, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(listsEndpoint))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
if limit != DefaultLimit {
p.AddValue("limit", strconv.Itoa(limit))
}
if skip != DefaultSkip {
p.AddValue("skip", strconv.Itoa(skip))
}
if filter != "" {
p.AddValue("address", filter)
}
var envelope struct {
Items []List `json:"items"`
TotalCount int `json:"total_count"`
}
response, err := makeRequest(r, "GET", p)
if err != nil {
return -1, nil, err
}
err = response.ParseFromJSON(&envelope)
return envelope.TotalCount, envelope.Items, err
}
// CreateList creates a new mailing list under your Mailgun account.
// You need specify only the Address and Name members of the prototype;
// Description, and AccessLevel are optional.
// If unspecified, Description remains blank,
// while AccessLevel defaults to Everyone.
func (mg *MailgunImpl) CreateList(prototype List) (List, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(listsEndpoint))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
if prototype.Address != "" {
p.AddValue("address", prototype.Address)
}
if prototype.Name != "" {
p.AddValue("name", prototype.Name)
}
if prototype.Description != "" {
p.AddValue("description", prototype.Description)
}
if prototype.AccessLevel != "" {
p.AddValue("access_level", prototype.AccessLevel)
}
response, err := makePostRequest(r, p)
if err != nil {
return List{}, err
}
var l List
err = response.ParseFromJSON(&l)
return l, err
}
// DeleteList removes all current members of the list, then removes the list itself.
// Attempts to send e-mail to the list will fail subsequent to this call.
func (mg *MailgunImpl) DeleteList(addr string) error {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(listsEndpoint) + "/" + addr)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
_, err := makeDeleteRequest(r)
return err
}
// GetListByAddress allows your application to recover the complete List structure
// representing a mailing list, so long as you have its e-mail address.
func (mg *MailgunImpl) GetListByAddress(addr string) (List, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(listsEndpoint) + "/" + addr)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
response, err := makeGetRequest(r)
var envelope struct {
List `json:"list"`
}
err = response.ParseFromJSON(&envelope)
return envelope.List, err
}
// UpdateList allows you to change various attributes of a list.
// Address, Name, Description, and AccessLevel are all optional;
// only those fields which are set in the prototype will change.
//
// Be careful! If changing the address of a mailing list,
// e-mail sent to the old address will not succeed.
// Make sure you account for the change accordingly.
func (mg *MailgunImpl) UpdateList(addr string, prototype List) (List, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(listsEndpoint) + "/" + addr)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
if prototype.Address != "" {
p.AddValue("address", prototype.Address)
}
if prototype.Name != "" {
p.AddValue("name", prototype.Name)
}
if prototype.Description != "" {
p.AddValue("description", prototype.Description)
}
if prototype.AccessLevel != "" {
p.AddValue("access_level", prototype.AccessLevel)
}
var l List
response, err := makePutRequest(r, p)
if err != nil {
return l, err
}
err = response.ParseFromJSON(&l)
return l, err
}
// GetMembers returns the list of members belonging to the indicated mailing list.
// The s parameter can be set to one of three settings to help narrow the returned data set:
// All indicates that you want both Members and unsubscribed members alike, while
// Subscribed and Unsubscribed indicate you want only those eponymous subsets.
func (mg *MailgunImpl) GetMembers(limit, skip int, s *bool, addr string) (int, []Member, error) {
r := simplehttp.NewHTTPRequest(generateMemberApiUrl(listsEndpoint, addr))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
if limit != DefaultLimit {
p.AddValue("limit", strconv.Itoa(limit))
}
if skip != DefaultSkip {
p.AddValue("skip", strconv.Itoa(skip))
}
if s != nil {
p.AddValue("subscribed", yesNo(*s))
}
var envelope struct {
TotalCount int `json:"total_count"`
Items []Member `json:"items"`
}
response, err := makeRequest(r, "GET", p)
if err != nil {
return -1, nil, err
}
err = response.ParseFromJSON(&envelope)
return envelope.TotalCount, envelope.Items, err
}
// GetMemberByAddress returns a complete Member structure for a member of a mailing list,
// given only their subscription e-mail address.
func (mg *MailgunImpl) GetMemberByAddress(s, l string) (Member, error) {
r := simplehttp.NewHTTPRequest(generateMemberApiUrl(listsEndpoint, l) + "/" + s)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
response, err := makeGetRequest(r)
if err != nil {
return Member{}, err
}
var envelope struct {
Member Member `json:"member"`
}
err = response.ParseFromJSON(&envelope)
return envelope.Member, err
}
// CreateMember registers a new member of the indicated mailing list.
// If merge is set to true, then the registration may update an existing Member's settings.
// Otherwise, an error will occur if you attempt to add a member with a duplicate e-mail address.
func (mg *MailgunImpl) CreateMember(merge bool, addr string, prototype Member) error {
vs, err := json.Marshal(prototype.Vars)
if err != nil {
return err
}
r := simplehttp.NewHTTPRequest(generateMemberApiUrl(listsEndpoint, addr))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewFormDataPayload()
p.AddValue("upsert", yesNo(merge))
p.AddValue("address", prototype.Address)
p.AddValue("name", prototype.Name)
p.AddValue("vars", string(vs))
if prototype.Subscribed != nil {
p.AddValue("subscribed", yesNo(*prototype.Subscribed))
}
_, err = makePostRequest(r, p)
return err
}
// UpdateMember lets you change certain details about the indicated mailing list member.
// Address, Name, Vars, and Subscribed fields may be changed.
func (mg *MailgunImpl) UpdateMember(s, l string, prototype Member) (Member, error) {
r := simplehttp.NewHTTPRequest(generateMemberApiUrl(listsEndpoint, l) + "/" + s)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewFormDataPayload()
if prototype.Address != "" {
p.AddValue("address", prototype.Address)
}
if prototype.Name != "" {
p.AddValue("name", prototype.Name)
}
if prototype.Vars != nil {
vs, err := json.Marshal(prototype.Vars)
if err != nil {
return Member{}, err
}
p.AddValue("vars", string(vs))
}
if prototype.Subscribed != nil {
p.AddValue("subscribed", yesNo(*prototype.Subscribed))
}
response, err := makePutRequest(r, p)
if err != nil {
return Member{}, err
}
var envelope struct {
Member Member `json:"member"`
}
err = response.ParseFromJSON(&envelope)
return envelope.Member, err
}
// DeleteMember removes the member from the list.
func (mg *MailgunImpl) DeleteMember(member, addr string) error {
r := simplehttp.NewHTTPRequest(generateMemberApiUrl(listsEndpoint, addr) + "/" + member)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
_, err := makeDeleteRequest(r)
return err
}
// CreateMemberList registers multiple Members and non-Member members to a single mailing list
// in a single round-trip.
// s indicates the default subscribed status (Subscribed or Unsubscribed).
// Use All to elect not to provide a default.
// The newMembers list can take one of two JSON-encodable forms: an slice of strings, or
// a slice of Member structures.
// If a simple slice of strings is passed, each string refers to the member's e-mail address.
// Otherwise, each Member needs to have at least the Address field filled out.
// Other fields are optional, but may be set according to your needs.
func (mg *MailgunImpl) CreateMemberList(s *bool, addr string, newMembers []interface{}) error {
r := simplehttp.NewHTTPRequest(generateMemberApiUrl(listsEndpoint, addr) + ".json")
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewFormDataPayload()
if s != nil {
p.AddValue("subscribed", yesNo(*s))
}
bs, err := json.Marshal(newMembers)
if err != nil {
return err
}
fmt.Println(string(bs))
p.AddValue("members", string(bs))
_, err = makePostRequest(r, p)
return err
}

View file

@ -0,0 +1,647 @@
package mailgun
import (
"encoding/json"
"errors"
"io"
"time"
"github.com/mbanzon/simplehttp"
)
// MaxNumberOfRecipients represents the largest batch of recipients that Mailgun can support in a single API call.
// This figure includes To:, Cc:, Bcc:, etc. recipients.
const MaxNumberOfRecipients = 1000
// Message structures contain both the message text and the envelop for an e-mail message.
type Message struct {
to []string
tags []string
campaigns []string
dkim bool
deliveryTime *time.Time
attachments []string
readerAttachments []ReaderAttachment
inlines []string
testMode bool
tracking bool
trackingClicks bool
trackingOpens bool
headers map[string]string
variables map[string]string
recipientVariables map[string]map[string]interface{}
dkimSet bool
trackingSet bool
trackingClicksSet bool
trackingOpensSet bool
specific features
mg Mailgun
}
type ReaderAttachment struct {
Filename string
ReadCloser io.ReadCloser
}
// StoredMessage structures contain the (parsed) message content for an email
// sent to a Mailgun account.
//
// The MessageHeaders field is special, in that it's formatted as a slice of pairs.
// Each pair consists of a name [0] and value [1]. Array notation is used instead of a map
// because that's how it's sent over the wire, and it's how encoding/json expects this field
// to be.
type StoredMessage struct {
Recipients string `json:"recipients"`
Sender string `json:"sender"`
From string `json:"from"`
Subject string `json:"subject"`
BodyPlain string `json:"body-plain"`
StrippedText string `json:"stripped-text"`
StrippedSignature string `json:"stripped-signature"`
BodyHtml string `json:"body-html"`
StrippedHtml string `json:"stripped-html"`
Attachments []StoredAttachment `json:"attachments"`
MessageUrl string `json:"message-url"`
ContentIDMap map[string]interface{} `json:"content-id-map"`
MessageHeaders [][]string `json:"message-headers"`
}
// StoredAttachment structures contain information on an attachment associated with a stored message.
type StoredAttachment struct {
Size int `json:"size"`
Url string `json:"url"`
Name string `json:"name"`
ContentType string `json:"content-type"`
}
type StoredMessageRaw struct {
Recipients string `json:"recipients"`
Sender string `json:"sender"`
From string `json:"from"`
Subject string `json:"subject"`
BodyMime string `json:"body-mime"`
}
// plainMessage contains fields relevant to plain API-synthesized messages.
// You're expected to use various setters to set most of these attributes,
// although from, subject, and text are set when the message is created with
// NewMessage.
type plainMessage struct {
from string
cc []string
bcc []string
subject string
text string
html string
}
// mimeMessage contains fields relevant to pre-packaged MIME messages.
type mimeMessage struct {
body io.ReadCloser
}
type sendMessageResponse struct {
Message string `json:"message"`
Id string `json:"id"`
}
// features abstracts the common characteristics between regular and MIME messages.
// addCC, addBCC, recipientCount, and setHTML are invoked via the package-global AddCC, AddBCC,
// RecipientCount, and SetHtml 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 yeilds 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)
addValues(*simplehttp.FormDataPayload)
isValid() bool
endpoint() string
recipientCount() int
}
// NewMessage returns a new e-mail message with the simplest envelop needed to send.
//
// DEPRECATED.
// The package will panic if you use AddRecipient(), AddBcc(), AddCc(), et. al.
// on a message already equipped with MaxNumberOfRecipients recipients.
// Use Mailgun.NewMessage() instead.
// It works similarly to this function, but supports larger lists of recipients.
func NewMessage(from string, subject string, text string, to ...string) *Message {
return &Message{
specific: &plainMessage{
from: from,
subject: subject,
text: text,
},
to: to,
}
}
// NewMessage returns a new e-mail message with the simplest envelop needed to send.
//
// Unlike the global function,
// this method supports arbitrary-sized recipient lists by
// automatically sending mail in batches of up to MaxNumberOfRecipients.
//
// To support batch sending, you don't want to provide a fixed To: header at this point.
// Pass nil as the to parameter to skip adding the To: header at this stage.
// You can do this explicitly, or implicitly, as follows:
//
// // Note absence of To parameter(s)!
// m := mg.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.
func (mg *MailgunImpl) NewMessage(from, subject, text string, to ...string) *Message {
return &Message{
specific: &plainMessage{
from: from,
subject: subject,
text: text,
},
to: to,
mg: mg,
}
}
// 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.
//
// DEPRECATED.
// The package will panic if you use AddRecipient(), AddBcc(), AddCc(), et. al.
// on a message already equipped with MaxNumberOfRecipients recipients.
// Use Mailgun.NewMIMEMessage() instead.
// It works similarly to this function, but supports larger lists of recipients.
func NewMIMEMessage(body io.ReadCloser, to ...string) *Message {
return &Message{
specific: &mimeMessage{
body: body,
},
to: to,
}
}
// 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.
//
// Unlike the global function,
// this method supports arbitrary-sized recipient lists by
// automatically sending mail in batches of up to MaxNumberOfRecipients.
//
// To support batch sending, you don't want to provide a fixed To: header at this point.
// Pass nil as the to parameter to skip adding the To: header at this stage.
// You can do this explicitly, or implicitly, as follows:
//
// // Note absence of To parameter(s)!
// m := mg.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.
func (mg *MailgunImpl) NewMIMEMessage(body io.ReadCloser, to ...string) *Message {
return &Message{
specific: &mimeMessage{
body: body,
},
to: to,
mg: mg,
}
}
// 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 *Message) AddReaderAttachment(filename string, readCloser io.ReadCloser) {
ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser}
m.readerAttachments = append(m.readerAttachments, ra)
}
// 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) {
m.attachments = append(m.attachments, attachment)
}
// 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 *Message) AddInline(inline string) {
m.inlines = append(m.inlines, inline)
}
// AddRecipient appends a receiver to the To: header of a message.
//
// NOTE: Above a certain limit (currently 1000 recipients),
// this function will cause the message as it's currently defined to be sent.
// This allows you to support large mailing lists without running into Mailgun's API limitations.
func (m *Message) 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.
//
// NOTE: Above a certain limit (see MaxNumberOfRecipients),
// this function will cause the message as it's currently defined to be sent.
// This allows you to support large mailing lists without running into Mailgun's API limitations.
func (m *Message) AddRecipientAndVariables(r string, vars map[string]interface{}) error {
if m.RecipientCount() >= MaxNumberOfRecipients {
_, _, err := m.send()
if err != nil {
return err
}
m.to = make([]string, len(m.to))
m.recipientVariables = make(map[string]map[string]interface{}, len(m.recipientVariables))
}
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
}
// 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 (pm *plainMessage) recipientCount() int {
return len(pm.bcc) + len(pm.cc)
}
func (mm *mimeMessage) recipientCount() int {
return 10
}
func (m *Message) send() (string, string, error) {
return m.mg.Send(m)
}
// AddCC appends a receiver to the carbon-copy header of a message.
func (m *Message) AddCC(recipient string) {
m.specific.addCC(recipient)
}
func (pm *plainMessage) addCC(r string) {
pm.cc = append(pm.cc, r)
}
func (mm *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 (pm *plainMessage) addBCC(r string) {
pm.bcc = append(pm.bcc, r)
}
func (mm *mimeMessage) addBCC(_ string) {}
// 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 (pm *plainMessage) setHtml(h string) {
pm.html = h
}
func (mm *mimeMessage) setHtml(_ string) {}
// AddTag attaches a tag 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) {
m.tags = append(m.tags, tag)
}
// This feature is deprecated for new software.
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) {
m.dkim = dkim
m.dkimSet = 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() {
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) {
pdt := new(time.Time)
*pdt = dt
m.deliveryTime = pdt
}
// 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 *Message) SetTracking(tracking bool) {
m.tracking = tracking
m.trackingSet = true
}
// Refer to the Mailgun documentation for more information.
func (m *Message) SetTrackingClicks(trackingClicks bool) {
m.trackingClicks = trackingClicks
m.trackingClicksSet = true
}
// Refer to the Mailgun documentation for more information.
func (m *Message) SetTrackingOpens(trackingOpens bool) {
m.trackingOpens = trackingOpens
m.trackingOpensSet = true
}
// AddHeader allows you to send custom MIME headers with the message.
func (m *Message) 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 *Message) AddVariable(variable string, value interface{}) error {
j, err := json.Marshal(value)
if err != nil {
return err
}
if m.variables == nil {
m.variables = make(map[string]string)
}
m.variables[variable] = string(j)
return nil
}
// 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, and a message ID. The status and message ID are set only
// if no error occurred.
func (m *MailgunImpl) Send(message *Message) (mes string, id string, err error) {
if !isValid(message) {
err = errors.New("Message not valid")
} else {
payload := simplehttp.NewFormDataPayload()
message.specific.addValues(payload)
for _, to := range message.to {
payload.AddValue("to", to)
}
for _, tag := range message.tags {
payload.AddValue("o:tag", tag)
}
for _, campaign := range message.campaigns {
payload.AddValue("o:campaign", campaign)
}
if message.dkimSet {
payload.AddValue("o:dkim", yesNo(message.dkim))
}
if message.deliveryTime != nil {
payload.AddValue("o:deliverytime", formatMailgunTime(message.deliveryTime))
}
if message.testMode {
payload.AddValue("o:testmode", "yes")
}
if message.trackingSet {
payload.AddValue("o:tracking", yesNo(message.tracking))
}
if message.trackingClicksSet {
payload.AddValue("o:tracking-clicks", yesNo(message.trackingClicks))
}
if message.trackingOpensSet {
payload.AddValue("o:tracking-opens", yesNo(message.trackingOpens))
}
if message.headers != nil {
for header, value := range message.headers {
payload.AddValue("h:"+header, value)
}
}
if message.variables != nil {
for variable, value := range message.variables {
payload.AddValue("v:"+variable, value)
}
}
if message.recipientVariables != nil {
j, err := json.Marshal(message.recipientVariables)
if err != nil {
return "", "", err
}
payload.AddValue("recipient-variables", string(j))
}
if message.attachments != nil {
for _, attachment := range message.attachments {
payload.AddFile("attachment", attachment)
}
}
if message.readerAttachments != nil {
for _, readerAttachment := range message.readerAttachments {
payload.AddReadCloser("attachment", readerAttachment.Filename, readerAttachment.ReadCloser)
}
}
if message.inlines != nil {
for _, inline := range message.inlines {
payload.AddFile("inline", inline)
}
}
r := simplehttp.NewHTTPRequest(generateApiUrl(m, message.specific.endpoint()))
r.SetBasicAuth(basicAuthUser, m.ApiKey())
var response sendMessageResponse
err = postResponseFromJSON(r, payload, &response)
if err == nil {
mes = response.Message
id = response.Id
}
}
return
}
func (pm *plainMessage) addValues(p *simplehttp.FormDataPayload) {
p.AddValue("from", pm.from)
p.AddValue("subject", pm.subject)
p.AddValue("text", pm.text)
for _, cc := range pm.cc {
p.AddValue("cc", cc)
}
for _, bcc := range pm.bcc {
p.AddValue("bcc", bcc)
}
if pm.html != "" {
p.AddValue("html", pm.html)
}
}
func (mm *mimeMessage) addValues(p *simplehttp.FormDataPayload) {
p.AddReadCloser("message", "message.mime", mm.body)
}
func (pm *plainMessage) endpoint() string {
return messagesEndpoint
}
func (mm *mimeMessage) endpoint() string {
return mimeMessagesEndpoint
}
// yesNo translates a true/false boolean value into a yes/no setting suitable for the Mailgun API.
func yesNo(b bool) string {
if b {
return "yes"
} else {
return "no"
}
}
// isValid returns true if, and only if,
// a Message instance is sufficiently initialized to send via the Mailgun interface.
func isValid(m *Message) bool {
if m == nil {
return false
}
if !m.specific.isValid() {
return false
}
if !validateStringList(m.to, true) {
return false
}
if !validateStringList(m.tags, false) {
return false
}
if !validateStringList(m.campaigns, false) || len(m.campaigns) > 3 {
return false
}
return true
}
func (pm *plainMessage) isValid() bool {
if pm.from == "" {
return false
}
if !validateStringList(pm.cc, false) {
return false
}
if !validateStringList(pm.bcc, false) {
return false
}
if pm.text == "" {
return false
}
return true
}
func (mm *mimeMessage) isValid() bool {
return mm.body != nil
}
// validateStringList returns true if, and only if,
// a slice of strings exists AND all of its elements exist,
// OR if the slice doesn't exist AND it's not required to exist.
// The requireOne parameter indicates whether the list is required to exist.
func validateStringList(list []string, requireOne bool) bool {
hasOne := false
if list == nil {
return !requireOne
} else {
for _, a := range list {
if a == "" {
return false
} else {
hasOne = hasOne || true
}
}
}
return hasOne
}
// GetStoredMessage retrieves information about a received e-mail message.
// This provides visibility into, e.g., replies to a message sent to a mailing list.
func (mg *MailgunImpl) GetStoredMessage(id string) (StoredMessage, error) {
url := generateStoredMessageUrl(mg, messagesEndpoint, id)
r := simplehttp.NewHTTPRequest(url)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
var response StoredMessage
err := getResponseFromJSON(r, &response)
return response, err
}
// GetStoredMessageRaw retrieves the raw MIME body of a received e-mail message.
// Compared to GetStoredMessage, it gives access to the unparsed MIME body, and
// thus delegates to the caller the required parsing.
func (mg *MailgunImpl) GetStoredMessageRaw(id string) (StoredMessageRaw, error) {
url := generateStoredMessageUrl(mg, messagesEndpoint, id)
r := simplehttp.NewHTTPRequest(url)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
r.AddHeader("Accept", "message/rfc2822")
var response StoredMessageRaw
err := getResponseFromJSON(r, &response)
return response, err
}
// DeleteStoredMessage removes a previously stored message.
// Note that Mailgun institutes a policy of automatically deleting messages after a set time.
// Consult the current Mailgun API documentation for more details.
func (mg *MailgunImpl) DeleteStoredMessage(id string) error {
url := generateStoredMessageUrl(mg, messagesEndpoint, id)
r := simplehttp.NewHTTPRequest(url)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
_, err := makeDeleteRequest(r)
return err
}

View file

@ -0,0 +1,152 @@
package mailgun
import (
"fmt"
"github.com/mbanzon/simplehttp"
)
// The MailgunGoUserAgent identifies the client to the server, for logging purposes.
// In the event of problems requiring a human administrator's assistance,
// this user agent allows them to identify the client from human-generated activity.
const MailgunGoUserAgent = "mailgun-go/1.0.0"
// This error will be returned whenever a Mailgun API returns an error response.
// Your application can check the Actual field to see the actual HTTP response code returned.
// URL contains the base URL accessed, sans any query parameters.
type UnexpectedResponseError struct {
Expected []int
Actual int
URL string
}
// String() converts the error into a human-readable, logfmt-compliant string.
// See http://godoc.org/github.com/kr/logfmt for details on logfmt formatting.
func (e *UnexpectedResponseError) String() string {
return fmt.Sprintf("UnexpectedResponseError URL=%s ExpectedOneOf=%#v Got=%d", e.URL, e.Expected, e.Actual)
}
// Error() performs as String().
func (e *UnexpectedResponseError) Error() string {
return e.String()
}
// newError creates a new error condition to be returned.
func newError(url string, expected []int, got int) error {
return &UnexpectedResponseError{
URL: url,
Expected: expected,
Actual: got,
}
}
// notGood searches a list of response codes (the haystack) for a matching entry (the needle).
// If found, the response code is considered good, and thus false is returned.
// Otherwise true is returned.
func notGood(needle int, haystack []int) bool {
for _, i := range haystack {
if needle == i {
return false
}
}
return true
}
// expected denotes the expected list of known-good HTTP response codes possible from the Mailgun API.
var expected = []int{200, 202, 204}
// makeRequest shim performs a generic request, checking for a positive outcome.
// See simplehttp.MakeRequest for more details.
func makeRequest(r *simplehttp.HTTPRequest, kind string, p simplehttp.Payload) (*simplehttp.HTTPResponse, error) {
r.AddHeader("User-Agent", MailgunGoUserAgent)
rsp, err := r.MakeRequest(kind, p)
if (err == nil) && notGood(rsp.Code, expected) {
return rsp, newError(r.URL, expected, rsp.Code)
}
return rsp, err
}
// getResponseFromJSON shim performs a GET request, checking for a positive outcome.
// See simplehttp.GetResponseFromJSON for more details.
func getResponseFromJSON(r *simplehttp.HTTPRequest, v interface{}) error {
r.AddHeader("User-Agent", MailgunGoUserAgent)
response, err := r.MakeGetRequest()
if err != nil {
return err
}
if notGood(response.Code, expected) {
return newError(r.URL, expected, response.Code)
}
return response.ParseFromJSON(v)
}
// postResponseFromJSON shim performs a POST request, checking for a positive outcome.
// See simplehttp.PostResponseFromJSON for more details.
func postResponseFromJSON(r *simplehttp.HTTPRequest, p simplehttp.Payload, v interface{}) error {
r.AddHeader("User-Agent", MailgunGoUserAgent)
response, err := r.MakePostRequest(p)
if err != nil {
return err
}
if notGood(response.Code, expected) {
return newError(r.URL, expected, response.Code)
}
return response.ParseFromJSON(v)
}
// putResponseFromJSON shim performs a PUT request, checking for a positive outcome.
// See simplehttp.PutResponseFromJSON for more details.
func putResponseFromJSON(r *simplehttp.HTTPRequest, p simplehttp.Payload, v interface{}) error {
r.AddHeader("User-Agent", MailgunGoUserAgent)
response, err := r.MakePutRequest(p)
if err != nil {
return err
}
if notGood(response.Code, expected) {
return newError(r.URL, expected, response.Code)
}
return response.ParseFromJSON(v)
}
// makeGetRequest shim performs a GET request, checking for a positive outcome.
// See simplehttp.MakeGetRequest for more details.
func makeGetRequest(r *simplehttp.HTTPRequest) (*simplehttp.HTTPResponse, error) {
r.AddHeader("User-Agent", MailgunGoUserAgent)
rsp, err := r.MakeGetRequest()
if (err == nil) && notGood(rsp.Code, expected) {
return rsp, newError(r.URL, expected, rsp.Code)
}
return rsp, err
}
// makePostRequest shim performs a POST request, checking for a positive outcome.
// See simplehttp.MakePostRequest for more details.
func makePostRequest(r *simplehttp.HTTPRequest, p simplehttp.Payload) (*simplehttp.HTTPResponse, error) {
r.AddHeader("User-Agent", MailgunGoUserAgent)
rsp, err := r.MakePostRequest(p)
if (err == nil) && notGood(rsp.Code, expected) {
return rsp, newError(r.URL, expected, rsp.Code)
}
return rsp, err
}
// makePutRequest shim performs a PUT request, checking for a positive outcome.
// See simplehttp.MakePutRequest for more details.
func makePutRequest(r *simplehttp.HTTPRequest, p simplehttp.Payload) (*simplehttp.HTTPResponse, error) {
r.AddHeader("User-Agent", MailgunGoUserAgent)
rsp, err := r.MakePutRequest(p)
if (err == nil) && notGood(rsp.Code, expected) {
return rsp, newError(r.URL, expected, rsp.Code)
}
return rsp, err
}
// makeDeleteRequest shim performs a DELETE request, checking for a positive outcome.
// See simplehttp.MakeDeleteRequest for more details.
func makeDeleteRequest(r *simplehttp.HTTPRequest) (*simplehttp.HTTPResponse, error) {
r.AddHeader("User-Agent", MailgunGoUserAgent)
rsp, err := r.MakeDeleteRequest()
if (err == nil) && notGood(rsp.Code, expected) {
return rsp, newError(r.URL, expected, rsp.Code)
}
return rsp, err
}

View file

@ -0,0 +1,127 @@
package mailgun
import (
"github.com/mbanzon/simplehttp"
"strconv"
)
// A Route structure contains information on a configured or to-be-configured route.
// The Priority field indicates how soon the route works relative to other configured routes.
// Routes of equal priority are consulted in chronological order.
// The Description field provides a human-readable description for the route.
// Mailgun ignores this field except to provide the description when viewing the Mailgun web control panel.
// The Expression field lets you specify a pattern to match incoming messages against.
// The Actions field contains strings specifying what to do
// with any message which matches the provided expression.
// The CreatedAt field provides a time-stamp for when the route came into existence.
// Finally, the ID field provides a unique identifier for this route.
//
// When creating a new route, the SDK only uses a subset of the fields of this structure.
// In particular, CreatedAt and ID are meaningless in this context, and will be ignored.
// Only Priority, Description, Expression, and Actions need be provided.
type Route struct {
Priority int `json:"priority,omitempty"`
Description string `json:"description,omitempty"`
Expression string `json:"expression,omitempty"`
Actions []string `json:"actions,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
ID string `json:"id,omitempty"`
}
// GetRoutes returns the complete set of routes configured for your domain.
// You use routes to configure how to handle returned messages, or
// messages sent to a specfic address on your domain.
// See the Mailgun documentation for more information.
func (mg *MailgunImpl) GetRoutes(limit, skip int) (int, []Route, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(routesEndpoint))
if limit != DefaultLimit {
r.AddParameter("limit", strconv.Itoa(limit))
}
if skip != DefaultSkip {
r.AddParameter("skip", strconv.Itoa(skip))
}
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
var envelope struct {
TotalCount int `json:"total_count"`
Items []Route `json:"items"`
}
err := getResponseFromJSON(r, &envelope)
if err != nil {
return -1, nil, err
}
return envelope.TotalCount, envelope.Items, nil
}
// CreateRoute installs a new route for your domain.
// The route structure you provide serves as a template, and
// only a subset of the fields influence the operation.
// See the Route structure definition for more details.
func (mg *MailgunImpl) CreateRoute(prototype Route) (Route, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(routesEndpoint))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
p.AddValue("priority", strconv.Itoa(prototype.Priority))
p.AddValue("description", prototype.Description)
p.AddValue("expression", prototype.Expression)
for _, action := range prototype.Actions {
p.AddValue("action", action)
}
var envelope struct {
Message string `json:"message"`
*Route `json:"route"`
}
err := postResponseFromJSON(r, p, &envelope)
return *envelope.Route, err
}
// DeleteRoute removes the specified route from your domain's configuration.
// To avoid ambiguity, Mailgun identifies the route by unique ID.
// See the Route structure definition and the Mailgun API documentation for more details.
func (mg *MailgunImpl) DeleteRoute(id string) error {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(routesEndpoint) + "/" + id)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
_, err := makeDeleteRequest(r)
return err
}
// GetRouteByID retrieves the complete route definition associated with the unique route ID.
func (mg *MailgunImpl) GetRouteByID(id string) (Route, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(routesEndpoint) + "/" + id)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
var envelope struct {
Message string `json:"message"`
*Route `json:"route"`
}
err := getResponseFromJSON(r, &envelope)
return *envelope.Route, err
}
// UpdateRoute provides an "in-place" update of the specified route.
// Only those route fields which are non-zero or non-empty are updated.
// All other fields remain as-is.
func (mg *MailgunImpl) UpdateRoute(id string, route Route) (Route, error) {
r := simplehttp.NewHTTPRequest(generatePublicApiUrl(routesEndpoint) + "/" + id)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
if route.Priority != 0 {
p.AddValue("priority", strconv.Itoa(route.Priority))
}
if route.Description != "" {
p.AddValue("description", route.Description)
}
if route.Expression != "" {
p.AddValue("expression", route.Expression)
}
if route.Actions != nil {
for _, action := range route.Actions {
p.AddValue("action", action)
}
}
// For some reason, this API function just returns a bare Route on success.
// Unsure why this is the case; it seems like it ought to be a bug.
var envelope Route
err := putResponseFromJSON(r, p, &envelope)
return envelope, err
}

View file

@ -0,0 +1,78 @@
package mailgun
import (
"github.com/mbanzon/simplehttp"
"strconv"
)
const (
complaintsEndpoint = "complaints"
)
// Complaint structures track how many times one of your emails have been marked as spam.
// CreatedAt indicates when the first report arrives from a given recipient, identified by Address.
// Count provides a running counter of how many times
// the recipient thought your messages were not solicited.
type Complaint struct {
Count int `json:"count"`
CreatedAt string `json:"created_at"`
Address string `json:"address"`
}
type complaintsEnvelope struct {
TotalCount int `json:"total_count"`
Items []Complaint `json:"items"`
}
// GetComplaints returns a set of spam complaints registered against your domain.
// Recipients of your messages can click on a link which sends feedback to Mailgun
// indicating that the message they received is, to them, spam.
func (m *MailgunImpl) GetComplaints(limit, skip int) (int, []Complaint, error) {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, complaintsEndpoint))
r.SetBasicAuth(basicAuthUser, m.ApiKey())
if limit != -1 {
r.AddParameter("limit", strconv.Itoa(limit))
}
if skip != -1 {
r.AddParameter("skip", strconv.Itoa(skip))
}
var envelope complaintsEnvelope
err := getResponseFromJSON(r, &envelope)
if err != nil {
return -1, nil, err
}
return envelope.TotalCount, envelope.Items, nil
}
// GetSingleComplaint returns a single complaint record filed by a recipient at the email address provided.
// If no complaint exists, the Complaint instance returned will be empty.
func (m *MailgunImpl) GetSingleComplaint(address string) (Complaint, error) {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, complaintsEndpoint) + "/" + address)
r.SetBasicAuth(basicAuthUser, m.ApiKey())
var c Complaint
err := getResponseFromJSON(r, &c)
return c, err
}
// CreateComplaint registers the specified address as a recipient who has complained of receiving spam
// from your domain.
func (m *MailgunImpl) CreateComplaint(address string) error {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, complaintsEndpoint))
r.SetBasicAuth(basicAuthUser, m.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
p.AddValue("address", address)
_, err := makePostRequest(r, p)
return err
}
// DeleteComplaint removes a previously registered e-mail address from the list of people who complained
// of receiving spam from your domain.
func (m *MailgunImpl) DeleteComplaint(address string) error {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, complaintsEndpoint) + "/" + address)
r.SetBasicAuth(basicAuthUser, m.ApiKey())
_, err := makeDeleteRequest(r)
return err
}

View file

@ -0,0 +1,59 @@
package mailgun
import (
"github.com/mbanzon/simplehttp"
"strconv"
"time"
)
type Stat struct {
Event string `json:"event"`
TotalCount int `json:"total_count"`
CreatedAt string `json:"created_at"`
Id string `json:"id"`
Tags map[string]int `json:"tags"`
}
type statsEnvelope struct {
TotalCount int `json:"total_count"`
Items []Stat `json:"items"`
}
// GetStats returns a basic set of statistics for different events.
// Events start at the given start date, if one is provided.
// If not, this function will consider all stated events dating to the creation of the sending domain.
func (m *MailgunImpl) GetStats(limit int, skip int, startDate *time.Time, event ...string) (int, []Stat, error) {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, statsEndpoint))
if limit != -1 {
r.AddParameter("limit", strconv.Itoa(limit))
}
if skip != -1 {
r.AddParameter("skip", strconv.Itoa(skip))
}
if startDate != nil {
r.AddParameter("start-date", startDate.Format(time.RFC3339))
}
for _, e := range event {
r.AddParameter("event", e)
}
r.SetBasicAuth(basicAuthUser, m.ApiKey())
var res statsEnvelope
err := getResponseFromJSON(r, &res)
if err != nil {
return -1, nil, err
} else {
return res.TotalCount, res.Items, nil
}
}
// DeleteTag removes all counters for a particular tag, including the tag itself.
func (m *MailgunImpl) DeleteTag(tag string) error {
r := simplehttp.NewHTTPRequest(generateApiUrl(m, deleteTagEndpoint) + "/" + tag)
r.SetBasicAuth(basicAuthUser, m.ApiKey())
_, err := makeDeleteRequest(r)
return err
}

View file

@ -0,0 +1,66 @@
package mailgun
import (
"github.com/mbanzon/simplehttp"
"strconv"
)
type Unsubscription struct {
CreatedAt string `json:"created_at"`
Tag string `json:"tag"`
ID string `json:"id"`
Address string `json:"address"`
}
// GetUnsubscribes retrieves a list of unsubscriptions issued by recipients of mail from your domain.
// Zero is a valid list length.
func (mg *MailgunImpl) GetUnsubscribes(limit, skip int) (int, []Unsubscription, error) {
r := simplehttp.NewHTTPRequest(generateApiUrl(mg, unsubscribesEndpoint))
if limit != DefaultLimit {
r.AddParameter("limit", strconv.Itoa(limit))
}
if skip != DefaultSkip {
r.AddParameter("skip", strconv.Itoa(skip))
}
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
var envelope struct {
TotalCount int `json:"total_count"`
Items []Unsubscription `json:"items"`
}
err := getResponseFromJSON(r, &envelope)
return envelope.TotalCount, envelope.Items, err
}
// GetUnsubscribesByAddress retrieves a list of unsubscriptions by recipient address.
// Zero is a valid list length.
func (mg *MailgunImpl) GetUnsubscribesByAddress(a string) (int, []Unsubscription, error) {
r := simplehttp.NewHTTPRequest(generateApiUrlWithTarget(mg, unsubscribesEndpoint, a))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
var envelope struct {
TotalCount int `json:"total_count"`
Items []Unsubscription `json:"items"`
}
err := getResponseFromJSON(r, &envelope)
return envelope.TotalCount, envelope.Items, err
}
// Unsubscribe adds an e-mail address to the domain's unsubscription table.
func (mg *MailgunImpl) Unsubscribe(a, t string) error {
r := simplehttp.NewHTTPRequest(generateApiUrl(mg, unsubscribesEndpoint))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
p.AddValue("address", a)
p.AddValue("tag", t)
_, err := makePostRequest(r, p)
return err
}
// RemoveUnsubscribe removes the e-mail address given from the domain's unsubscription table.
// If passing in an ID (discoverable from, e.g., GetUnsubscribes()), the e-mail address associated
// with the given ID will be removed.
func (mg *MailgunImpl) RemoveUnsubscribe(a string) error {
r := simplehttp.NewHTTPRequest(generateApiUrlWithTarget(mg, unsubscribesEndpoint, a))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
_, err := makeDeleteRequest(r)
return err
}

View file

@ -0,0 +1,68 @@
package mailgun
import (
"github.com/mbanzon/simplehttp"
)
// GetWebhooks returns the complete set of webhooks configured for your domain.
// Note that a zero-length mapping is not an error.
func (mg *MailgunImpl) GetWebhooks() (map[string]string, error) {
r := simplehttp.NewHTTPRequest(generateDomainApiUrl(mg, webhooksEndpoint))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
var envelope struct {
Webhooks map[string]interface{} `json:"webhooks"`
}
err := getResponseFromJSON(r, &envelope)
hooks := make(map[string]string, 0)
if err != nil {
return hooks, err
}
for k, v := range envelope.Webhooks {
object := v.(map[string]interface{})
url := object["url"]
hooks[k] = url.(string)
}
return hooks, nil
}
// CreateWebhook installs a new webhook for your domain.
func (mg *MailgunImpl) CreateWebhook(t, u string) error {
r := simplehttp.NewHTTPRequest(generateDomainApiUrl(mg, webhooksEndpoint))
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
p.AddValue("id", t)
p.AddValue("url", u)
_, err := makePostRequest(r, p)
return err
}
// DeleteWebhook removes the specified webhook from your domain's configuration.
func (mg *MailgunImpl) DeleteWebhook(t string) error {
r := simplehttp.NewHTTPRequest(generateDomainApiUrl(mg, webhooksEndpoint) + "/" + t)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
_, err := makeDeleteRequest(r)
return err
}
// GetWebhookByType retrieves the currently assigned webhook URL associated with the provided type of webhook.
func (mg *MailgunImpl) GetWebhookByType(t string) (string, error) {
r := simplehttp.NewHTTPRequest(generateDomainApiUrl(mg, webhooksEndpoint) + "/" + t)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
var envelope struct {
Webhook struct {
Url *string `json:"url"`
} `json:"webhook"`
}
err := getResponseFromJSON(r, &envelope)
return *envelope.Webhook.Url, err
}
// UpdateWebhook replaces one webhook setting for another.
func (mg *MailgunImpl) UpdateWebhook(t, u string) error {
r := simplehttp.NewHTTPRequest(generateDomainApiUrl(mg, webhooksEndpoint) + "/" + t)
r.SetBasicAuth(basicAuthUser, mg.ApiKey())
p := simplehttp.NewUrlEncodedPayload()
p.AddValue("url", u)
_, err := makePutRequest(r, p)
return err
}

View file

@ -0,0 +1 @@
desktop.ini

View file

@ -0,0 +1,11 @@
language: go
go:
- 1.3
- 1.4
env:
- GOARCH=amd64
- GOARCH=386
script:
- go get github.com/mbanzon/callbackenv
- go get github.com/mbanzon/dummyserver
- go test

View file

@ -0,0 +1,27 @@
Copyright (c) 2013-2014, Michael Banzon
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of the {organization} nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,15 @@
simplehttp
==========
[![Build Status](https://travis-ci.org/mbanzon/simplehttp.png?branch=master)](https://travis-ci.org/mbanzon/simplehttp)
Simple HTTP library for Go.
This small library adds some utility functions for doing HTTP request and easilly gettings results as
structs from JSON and XML.
Supports alternative `http.Client` instances to support use on Google App Engine.
Examples are coming soon.
The code is released under a 3-clause BSD license. See the LICENSE file for more information.

View file

@ -0,0 +1,17 @@
package simplehttp
func (r *HTTPRequest) GetResponseFromJSON(v interface{}) error {
response, err := r.MakeGetRequest()
if err != nil {
return err
}
return response.ParseFromJSON(v)
}
func (r *HTTPRequest) PostResponseFromJSON(payload Payload, v interface{}) error {
response, err := r.MakePostRequest(payload)
if err != nil {
return err
}
return response.ParseFromJSON(v)
}

View file

@ -0,0 +1,133 @@
package simplehttp
import (
"encoding/json"
"encoding/xml"
"testing"
)
func TestParsingGetFromJson(t *testing.T) {
tmp := testStruct{
Value1: "1",
Value2: "2",
Value3: "3",
}
data, err := json.Marshal(tmp)
if err != nil {
t.Fail()
}
server.SetNextResponse(data)
request := NewHTTPRequest(dummyurl)
var retVal testStruct
err = request.GetResponseFromJSON(&retVal)
if err != nil {
t.Fail()
}
if tmp.Value1 != retVal.Value1 {
t.Fail()
}
if tmp.Value2 != retVal.Value2 {
t.Fail()
}
if tmp.Value3 != retVal.Value3 {
t.Fail()
}
}
func TestFailingParsingGetFromJson(t *testing.T) {
request := NewHTTPRequest(invalidurl)
var retVal testStruct
err := request.GetResponseFromJSON(&retVal)
if err == nil {
t.Fail()
}
}
func TestParsingPostFromJson(t *testing.T) {
tmp := testStruct{
Value1: "1",
Value2: "2",
Value3: "3",
}
data, err := json.Marshal(tmp)
if err != nil {
t.Fail()
}
server.SetNextResponse(data)
request := NewHTTPRequest(dummyurl)
var retVal testStruct
err = request.PostResponseFromJSON(nil, &retVal)
if err != nil {
t.Fail()
}
if tmp.Value1 != retVal.Value1 {
t.Fail()
}
if tmp.Value2 != retVal.Value2 {
t.Fail()
}
if tmp.Value3 != retVal.Value3 {
t.Fail()
}
}
func TestFailingParsingPostFromJson(t *testing.T) {
request := NewHTTPRequest(invalidurl)
var retVal testStruct
err := request.PostResponseFromJSON(nil, &retVal)
if err == nil {
t.Fail()
}
}
func TestParsingGetFromXml(t *testing.T) {
tmp := testStruct{
Value1: "1",
Value2: "2",
Value3: "3",
}
data, err := xml.Marshal(tmp)
if err != nil {
t.Fail()
}
server.SetNextResponse(data)
request := NewHTTPRequest(dummyurl)
var retVal testStruct
response, err := request.MakeGetRequest()
response.ParseFromXML(&retVal)
if err != nil {
t.Fail()
}
if tmp.Value1 != retVal.Value1 {
t.Fail()
}
if tmp.Value2 != retVal.Value2 {
t.Fail()
}
if tmp.Value3 != retVal.Value3 {
t.Fail()
}
}

View file

@ -0,0 +1,31 @@
package simplehttp
import (
"encoding/json"
"net/http"
)
func GetJSONInput(r *http.Request, w http.ResponseWriter, v interface{}) (err error) {
decoder := json.NewDecoder(r.Body)
err = decoder.Decode(v)
if err != nil {
http.Error(w, "Bad request.", http.StatusBadRequest)
return err
}
return nil
}
func OutputJSON(w http.ResponseWriter, v interface{}) (err error) {
var data []byte
data, err = json.Marshal(v)
if err != nil {
http.Error(w, "Internal error.", http.StatusInternalServerError)
return err
}
w.Header().Add("Content-Type", "application/json")
_, err = w.Write(data)
return nil
}

View file

@ -0,0 +1,15 @@
package simplehttp
import (
"encoding/json"
"encoding/xml"
)
// Parses the HTTPResponse as JSON to the given interface.
func (r *HTTPResponse) ParseFromJSON(v interface{}) error {
return json.Unmarshal(r.Data, v)
}
func (r *HTTPResponse) ParseFromXML(v interface{}) error {
return xml.Unmarshal(r.Data, v)
}

View file

@ -0,0 +1,38 @@
package simplehttp
import (
"bytes"
"github.com/mbanzon/callbackenv"
"io/ioutil"
"testing"
)
const (
FILE_ENV = "SIMPLEHTTP_TEST_FILE"
)
func TestFormDataPayloadPost(t *testing.T) {
payload := NewFormDataPayload()
payload.AddValue("key", "value")
buf := &bytes.Buffer{}
buf.Write([]byte("testing testing testing"))
rc := ioutil.NopCloser(buf)
payload.AddReadCloser("foo", "bar", rc)
callbackenv.RequireEnv(FILE_ENV,
func(file string) {
payload.AddFile("file", file)
}, nil)
request := NewHTTPRequest(dummyurl)
request.MakePostRequest(payload)
}
func TestUrlEncodedPayloadPost(t *testing.T) {
payload := NewUrlEncodedPayload()
payload.AddValue("key", "value")
request := NewHTTPRequest(dummyurl)
request.MakePostRequest(payload)
}

View file

@ -0,0 +1,141 @@
package simplehttp
import (
"bytes"
"io"
"mime/multipart"
"net/url"
"os"
"path"
)
type keyValuePair struct {
key string
value string
}
type keyNameRC struct {
key string
name string
value io.ReadCloser
}
type Payload interface {
GetPayloadBuffer() (*bytes.Buffer, error)
GetContentType() string
}
type RawPayload struct {
Data []byte
}
type FormDataPayload struct {
contentType string
Values []keyValuePair
Files []keyValuePair
ReadClosers []keyNameRC
}
type UrlEncodedPayload struct {
Values []keyValuePair
}
func NewRawPayload(data []byte) *RawPayload {
return &RawPayload{Data: data}
}
func (r *RawPayload) GetPayloadBuffer() (*bytes.Buffer, error) {
data := &bytes.Buffer{}
c, err := data.Write(r.Data)
if c != len(r.Data) || err != nil {
return data, err
}
return data, nil
}
func (r *RawPayload) GetContentType() string {
return ""
}
func NewFormDataPayload() *FormDataPayload {
return &FormDataPayload{}
}
func (f *FormDataPayload) AddValue(key, value string) {
f.Values = append(f.Values, keyValuePair{key: key, value: value})
}
func (f *FormDataPayload) AddFile(key, file string) {
f.Files = append(f.Files, keyValuePair{key: key, value: file})
}
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) {
data := &bytes.Buffer{}
writer := multipart.NewWriter(data)
defer writer.Close()
for _, keyVal := range f.Values {
if tmp, err := writer.CreateFormField(keyVal.key); err == nil {
tmp.Write([]byte(keyVal.value))
} else {
return nil, err
}
}
for _, file := range f.Files {
if tmp, err := writer.CreateFormFile(file.key, path.Base(file.value)); err == nil {
if fp, err := os.Open(file.value); err == nil {
defer fp.Close()
io.Copy(tmp, fp)
} else {
return nil, err
}
} else {
return nil, err
}
}
for _, file := range f.ReadClosers {
if tmp, err := writer.CreateFormFile(file.key, file.name); err == nil {
defer file.value.Close()
io.Copy(tmp, file.value)
} else {
return nil, err
}
}
f.contentType = writer.FormDataContentType()
return data, nil
}
func (f *FormDataPayload) GetContentType() string {
if f.contentType == "" {
f.GetPayloadBuffer()
}
return f.contentType
}
func NewUrlEncodedPayload() *UrlEncodedPayload {
return &UrlEncodedPayload{}
}
func (f *UrlEncodedPayload) AddValue(key, value string) {
f.Values = append(f.Values, keyValuePair{key: key, value: value})
}
func (f *UrlEncodedPayload) GetPayloadBuffer() (*bytes.Buffer, error) {
data := url.Values{}
for _, keyVal := range f.Values {
data.Add(keyVal.key, keyVal.value)
}
return bytes.NewBufferString(data.Encode()), nil
}
func (f *UrlEncodedPayload) GetContentType() string {
return "application/x-www-form-urlencoded"
}

View file

@ -0,0 +1,74 @@
package simplehttp
// Type to encapsulate basic authentication for requests.
type BasicAuthentication struct {
User string
Password string
}
// Type to wrap requests.
type Request struct {
Url string
Authentication BasicAuthentication
UserAgent string
Data []byte
}
func createHttpRequest(req Request) *HTTPRequest {
r := NewHTTPRequest(req.Url)
if req.Authentication.User != "" {
r.SetBasicAuth(req.Authentication.User, req.Authentication.Password)
}
if req.UserAgent != "" {
r.AddHeader("User-Agent", req.UserAgent)
}
return r
}
func (r Request) Get() (int, []byte, error) {
req := createHttpRequest(r)
res, err := req.MakeGetRequest()
if err == nil {
return res.Code, res.Data, err
} else {
return -1, nil, err
}
}
func (r Request) Post() (int, []byte, error) {
req := createHttpRequest(r)
var payload Payload = nil
if r.Data != nil {
payload = NewRawPayload(r.Data)
}
res, err := req.MakePostRequest(payload)
if err == nil {
return res.Code, res.Data, err
} else {
return -1, nil, err
}
}
func (r Request) Put() (int, []byte, error) {
req := createHttpRequest(r)
var payload Payload = nil
if r.Data != nil {
payload = NewRawPayload(r.Data)
}
res, err := req.MakePutRequest(payload)
if err == nil {
return res.Code, res.Data, err
} else {
return -1, nil, err
}
}
func (r Request) Delete() (int, []byte, error) {
req := createHttpRequest(r)
res, err := req.MakeDeleteRequest()
if err == nil {
return res.Code, res.Data, err
} else {
return -1, nil, err
}
}

View file

@ -0,0 +1,104 @@
package simplehttp
import (
"testing"
)
func TestShorthandFailingPayload(t *testing.T) {
Request{
Url: dummyurl,
Data: nil,
}.Post()
}
func TestShorthandGet(t *testing.T) {
code, _, err := Request{
Url: dummyurl,
UserAgent: "simplehttp go test",
}.Get()
if code == -1 || err != nil {
t.Fail()
}
}
func TestShorthandPost(t *testing.T) {
code, _, err := Request{
Url: dummyurl,
Data: []byte("foobar"),
UserAgent: "simplehttp go test",
Authentication: BasicAuthentication{
User: "test",
Password: "test",
},
}.Post()
if code == -1 || err != nil {
t.Fail()
}
}
func TestShorthandPut(t *testing.T) {
code, _, err := Request{
Url: dummyurl,
Data: []byte("foobar"),
UserAgent: "simplehttp go test",
}.Put()
if code == -1 || err != nil {
t.Fail()
}
}
func TestShorthandDelete(t *testing.T) {
code, _, err := Request{
Url: dummyurl,
UserAgent: "simplehttp go test",
}.Delete()
if code == -1 || err != nil {
t.Fail()
}
}
func TestFailingShorthandGet(t *testing.T) {
code, _, err := Request{
Url: invalidurl,
}.Get()
if code != -1 || err == nil {
t.Fail()
}
}
func TestFailingShorthandPost(t *testing.T) {
code, _, err := Request{
Url: invalidurl,
Data: []byte("foobar"),
}.Post()
if code != -1 || err == nil {
t.Fail()
}
}
func TestFailingShorthandPut(t *testing.T) {
code, _, err := Request{
Url: invalidurl,
Data: []byte("foobar"),
}.Put()
if code != -1 || err == nil {
t.Fail()
}
}
func TestFailingShorthandDelete(t *testing.T) {
code, _, err := Request{
Url: invalidurl,
}.Delete()
if code != -1 || err == nil {
t.Fail()
}
}

View file

@ -0,0 +1,147 @@
// Package simplehttp provides some simple methods and types to do
// HTTP queries with form values and parameters easily - especially
// if the returned result is expected to be JSON or XML.
//
// Author: Michael Banzon
package simplehttp
import (
"io"
"io/ioutil"
"net/http"
"net/url"
)
// Holds all information used to make a HTTP request.
type HTTPRequest struct {
URL string
Parameters map[string][]string
Headers map[string]string
BasicAuthUser string
BasicAuthPassword string
Client *http.Client
}
type HTTPResponse struct {
Code int
Data []byte
}
// Creates a new HTTPRequest instance.
func NewHTTPRequest(url string) *HTTPRequest {
return &HTTPRequest{URL: url, Client: http.DefaultClient}
}
// Adds a parameter to the generated query string.
func (r *HTTPRequest) AddParameter(name, value string) {
if r.Parameters == nil {
r.Parameters = make(map[string][]string)
}
r.Parameters[name] = append(r.Parameters[name], value)
}
// Adds a header that will be sent with the HTTP request.
func (r *HTTPRequest) AddHeader(name, value string) {
// hej
if r.Headers == nil {
r.Headers = make(map[string]string)
}
r.Headers[name] = value
}
// Sets username and password for basic authentication.
func (r *HTTPRequest) SetBasicAuth(user, password string) {
r.BasicAuthUser = user
r.BasicAuthPassword = password
}
func (r *HTTPRequest) MakeGetRequest() (*HTTPResponse, error) {
return r.MakeRequest("GET", nil)
}
func (r *HTTPRequest) MakePostRequest(payload Payload) (*HTTPResponse, error) {
return r.MakeRequest("POST", payload)
}
func (r *HTTPRequest) MakePutRequest(payload Payload) (*HTTPResponse, error) {
return r.MakeRequest("PUT", payload)
}
func (r *HTTPRequest) MakeDeleteRequest() (*HTTPResponse, error) {
return r.MakeRequest("DELETE", nil)
}
func (r *HTTPRequest) MakeRequest(method string, payload Payload) (*HTTPResponse, error) {
url, err := r.generateUrlWithParameters()
if err != nil {
return nil, err
}
var body io.Reader
if payload != nil {
if body, err = payload.GetPayloadBuffer(); err != nil {
return nil, err
}
} else {
body = nil
}
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
if payload != nil && payload.GetContentType() != "" {
req.Header.Add("Content-Type", payload.GetContentType())
}
if r.BasicAuthUser != "" && r.BasicAuthPassword != "" {
req.SetBasicAuth(r.BasicAuthUser, r.BasicAuthPassword)
}
for header, value := range r.Headers {
req.Header.Add(header, value)
}
response := HTTPResponse{}
resp, err := r.Client.Do(req)
if resp != nil {
response.Code = resp.StatusCode
}
if err != nil {
return nil, err
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
response.Data = responseBody
return &response, nil
}
// Generates the complete URL using GET parameters.
func (r *HTTPRequest) generateUrlWithParameters() (string, error) {
url, err := url.Parse(r.URL)
if err != nil {
return "", err
}
q := url.Query()
if r.Parameters != nil && len(r.Parameters) > 0 {
for name, values := range r.Parameters {
for _, value := range values {
q.Add(name, value)
}
}
}
url.RawQuery = q.Encode()
return url.String(), nil
}
func (r *HTTPRequest) SetClient(c *http.Client) {
r.Client = c
}

View file

@ -0,0 +1,37 @@
package simplehttp
import (
"github.com/mbanzon/dummyserver"
"log"
"strconv"
"testing"
)
var (
server *dummyserver.DummyServer
dummyurl string
invalidurl string
)
type testStruct struct {
Value1 string `json:"value1" xml:"value1"`
Value2 string `json:"value2" xml:"value2"`
Value3 string `json:"value3" xml:"value3"`
}
func init() {
server = dummyserver.NewRandomServer()
go func() {
err := server.Start()
log.Fatal(err)
}()
dummyurl = "http://localhost:" + strconv.Itoa(server.GetPort()) + "/"
invalidurl = "invalid://invalid"
}
func TestAddParameters(t *testing.T) {
request := NewHTTPRequest(dummyurl)
request.AddParameter("p1", "v1")
request.MakeGetRequest()
}

View file

@ -108,6 +108,9 @@ func getClaims(r *http.Request) Claims {
if con != nil {
claims = con.(Claims)
}
claims.Ref = r.Header.Get("Origin")
origin := r.Header.Get("Origin")
if origin != "" {
claims.Ref = origin
}
return claims
}

19
main.go
View file

@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
@ -13,12 +14,14 @@ import (
"github.com/jmoiron/modl"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"github.com/mailgun/mailgun-go"
)
var (
DB = &modl.DbMap{Dialect: modl.PostgresDialect{}}
DBH modl.SqlExecutor = DB
schemaDecoder = schema.NewDecoder()
mgAccts = make(map[string]mailgun.Mailgun)
)
func main() {
@ -84,6 +87,22 @@ func main() {
func cmdServe(c *cli.Context) {
var err error
// Set up Mailgun handlers:
// [{"ref":"hymenobacter","domain":"hymenobacter.info","public":"abc","private":"123"}]
type account struct {
Ref string
Domain string
Public string
Private string
}
var accounts []account
json.Unmarshal([]byte(os.Getenv("ACCOUNT_KEYS")), &accounts)
log.Printf("Mailgun: %+v", accounts)
for _, a := range accounts {
mgAccts[a.Ref] = mailgun.NewMailgun(a.Domain, a.Private, a.Public)
}
addr := os.Getenv("PORT")
if addr == "" {
addr = "8901"

View file

@ -12,6 +12,7 @@ import (
"github.com/gorilla/mux"
"github.com/lib/pq"
"github.com/mailgun/mailgun-go"
"golang.org/x/crypto/bcrypt"
)
@ -136,7 +137,7 @@ func (u UserService) list(val *url.Values) (entity, *appError) {
sql := `SELECT id, email, 'password' AS password, name, role,
created_at, updated_at, deleted_at
FROM users
WHERE verified IS NOT FALSE
WHERE verified IS TRUE
AND deleted_at IS NULL;`
if err := DBH.Select(&users, sql); err != nil {
return nil, newJSONError(err, http.StatusInternalServerError)
@ -150,7 +151,7 @@ func (u UserService) get(id int64, genus string) (entity, *appError) {
created_at, updated_at, deleted_at
FROM users
WHERE id=$1
AND verified IS NOT FALSE
AND verified IS TRUE
AND deleted_at IS NULL;`
if err := DBH.SelectOne(&user, q, id); err != nil {
if err == sql.ErrNoRows {
@ -213,13 +214,36 @@ func (u UserService) create(e *entity, claims Claims) *appError {
return newJSONError(err, http.StatusInternalServerError)
}
// Send out confirmation email
mg, ok := mgAccts[claims.Ref]
if ok {
sender := fmt.Sprintf("%s Admin <admin@%s>", mg.Domain(), mg.Domain())
recipient := fmt.Sprintf("%s <%s>", user.Name, user.Email)
subject := fmt.Sprintf("New Account Confirmation - %s", mg.Domain())
message := fmt.Sprintf("You are receiving this message because this email "+
"address was used to sign up for an account at %s. Please visit this "+
"URL to complete the sign up process: %s/users/new/verify/%s. If you "+
"did not request an account, please disregard this message.",
mg.Domain(), claims.Ref, nonce)
m := mailgun.NewMessage(sender, subject, message, recipient)
_, _, err := mg.Send(m)
if err != nil {
log.Printf("%+v\n", err)
return newJSONError(err, http.StatusInternalServerError)
}
}
return nil
}
// for thermokarst/jwt: authentication callback
func dbAuthenticate(email string, password string) error {
var user User
q := `SELECT * FROM users WHERE lower(email)=lower($1);`
q := `SELECT *
FROM users
WHERE lower(email)=lower($1)
AND verified IS TRUE
AND deleted_at IS NULL;`
if err := DBH.SelectOne(&user, q, email); err != nil {
return ErrInvalidEmailOrPassword
}
@ -232,7 +256,11 @@ func dbAuthenticate(email string, password string) error {
// for thermokarst/jwt: setting user in claims bundle
func dbGetUserByEmail(email string) (*User, error) {
var user User
q := `SELECT * FROM users WHERE lower(email)=lower($1);`
q := `SELECT *
FROM users
WHERE lower(email)=lower($1)
AND verified IS TRUE
AND deleted_at IS NULL;`
if err := DBH.SelectOne(&user, q, email); err != nil {
if err == sql.ErrNoRows {
return nil, ErrUserNotFound