diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 7eb8f29..e9fd01b 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -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" diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/.travis.yml b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/.travis.yml new file mode 100644 index 0000000..baf3341 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/.travis.yml @@ -0,0 +1,10 @@ +language: go +go: + - 1.1.2 + - 1.2 + - tip +env: + - GOARCH=amd64 + - GOARCH=386 +script: + - go test diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/LICENSE b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/LICENSE new file mode 100644 index 0000000..24b24c4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/LICENSE @@ -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. diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/README.md b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/README.md new file mode 100644 index 0000000..0931fbc --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/README.md @@ -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. diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/acceptance.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/acceptance.go new file mode 100644 index 0000000..13c5506 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/acceptance.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/bounces_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/bounces_test.go new file mode 100644 index 0000000..15f323a --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/bounces_test.go @@ -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) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/credentials_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/credentials_test.go new file mode 100644 index 0000000..5b90484 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/credentials_test.go @@ -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) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/domains_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/domains_test.go new file mode 100644 index 0000000..a6415c3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/domains_test.go @@ -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) +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/email_validation_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/email_validation_test.go new file mode 100644 index 0000000..76fc277 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/email_validation_test.go @@ -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 ", "bob@example.com", "example.com") + if err != nil { + t.Fatal(err) + } + hittest := map[string]bool{ + "Alice ": 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)) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/events_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/events_test.go new file mode 100644 index 0000000..7f756af --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/events_test.go @@ -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") + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/mailing_lists_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/mailing_lists_test.go new file mode 100644 index 0000000..69286bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/mailing_lists_test.go @@ -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 ", + 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) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/messages_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/messages_test.go new file mode 100644 index 0000000..1c27301 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/messages_test.go @@ -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?= " + exampleSubject = "Joe's Example Subject" + exampleText = "Testing some Mailgun awesomeness!" + exampleHtml = "

Testing some Mailgun HTML awesomeness! at www.kc5tja@yahoo.com

" + + exampleMime = `Content-Type: text/plain; charset="ascii" +Subject: Joe's Example Subject +From: Joe Example +To: BARGLEGARF +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) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/routes_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/routes_test.go new file mode 100644 index 0000000..3763b99 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/routes_test.go @@ -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)) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/spam_complaints_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/spam_complaints_test.go new file mode 100644 index 0000000..676d8a1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/spam_complaints_test.go @@ -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) +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/stats_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/stats_test.go new file mode 100644 index 0000000..cc47e96 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/stats_test.go @@ -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) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/unsubscribes_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/unsubscribes_test.go new file mode 100644 index 0000000..cef7229 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/unsubscribes_test.go @@ -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) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/webhooks_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/webhooks_test.go new file mode 100644 index 0000000..1663d7a --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/acceptance/webhooks_test.go @@ -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"]) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/bounces.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/bounces.go new file mode 100644 index 0000000..f12076a --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/bounces.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/campaigns.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/campaigns.go new file mode 100644 index 0000000..c90d0c5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/campaigns.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/credentials.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/credentials.go new file mode 100644 index 0000000..f5be9ef --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/credentials.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/domains.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/domains.go new file mode 100644 index 0000000..2807174 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/domains.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/email_validation.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/email_validation.go new file mode 100644 index 0000000..918453a --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/email_validation.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/events.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/events.go new file mode 100644 index 0000000..833a2b4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/events.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/examples_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/examples_test.go new file mode 100644 index 0000000..a4af5bf --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/examples_test.go @@ -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 ", "bob@example.com", "example.com") + if err != nil { + log.Fatal(err) + } + hittest := map[string]bool{ + "Alice ": 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 ", + "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("

Testing some Mailgun Awesomeness!!

") + _, 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 +To: BARGLEGARF +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) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/mailgun.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/mailgun.go new file mode 100644 index 0000000..490b739 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/mailgun.go @@ -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 +// 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") +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/mailgun_test.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/mailgun_test.go new file mode 100644 index 0000000..8abc3ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/mailgun_test.go @@ -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) + } +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/mailing_lists.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/mailing_lists.go new file mode 100644 index 0000000..a640a47 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/mailing_lists.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/messages.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/messages.go new file mode 100644 index 0000000..650502c --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/messages.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/rest_shim.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/rest_shim.go new file mode 100644 index 0000000..29279e0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/rest_shim.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/routes.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/routes.go new file mode 100644 index 0000000..3fde098 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/routes.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/spam_complaints.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/spam_complaints.go new file mode 100644 index 0000000..48d1cf3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/spam_complaints.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/stats.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/stats.go new file mode 100644 index 0000000..8cccf3c --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/stats.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/unsubscribes.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/unsubscribes.go new file mode 100644 index 0000000..b296908 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/unsubscribes.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mailgun/mailgun-go/webhooks.go b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/webhooks.go new file mode 100644 index 0000000..1b4ab61 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mailgun/mailgun-go/webhooks.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/.gitignore b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/.gitignore new file mode 100644 index 0000000..2b1c9a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/.gitignore @@ -0,0 +1 @@ +desktop.ini diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/.travis.yml b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/.travis.yml new file mode 100644 index 0000000..296b871 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/.travis.yml @@ -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 diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/LICENSE b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/LICENSE new file mode 100644 index 0000000..74f74ee --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/LICENSE @@ -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. diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/README.md b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/README.md new file mode 100644 index 0000000..cb032cf --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/README.md @@ -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. \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/helpers.go b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/helpers.go new file mode 100644 index 0000000..64e2b87 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/helpers.go @@ -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) +} diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/helpers_test.go b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/helpers_test.go new file mode 100644 index 0000000..ecf3dc2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/helpers_test.go @@ -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() + } +} diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/json_utils.go b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/json_utils.go new file mode 100644 index 0000000..47ff4b9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/json_utils.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/parsing.go b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/parsing.go new file mode 100644 index 0000000..0261ccf --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/parsing.go @@ -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) +} diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/payload_test.go b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/payload_test.go new file mode 100644 index 0000000..7adcbfb --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/payload_test.go @@ -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) +} diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/payloads.go b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/payloads.go new file mode 100644 index 0000000..2c3fa2d --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/payloads.go @@ -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" +} diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/shorthand.go b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/shorthand.go new file mode 100644 index 0000000..2d0c62c --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/shorthand.go @@ -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 + } +} diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/shorthand_test.go b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/shorthand_test.go new file mode 100644 index 0000000..5968224 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/shorthand_test.go @@ -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() + } +} diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/simplehttp.go b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/simplehttp.go new file mode 100644 index 0000000..6ea097e --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/simplehttp.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/mbanzon/simplehttp/simplehttp_test.go b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/simplehttp_test.go new file mode 100644 index 0000000..ef46737 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mbanzon/simplehttp/simplehttp_test.go @@ -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() +} diff --git a/helpers.go b/helpers.go index 377fd64..1c0d048 100644 --- a/helpers.go +++ b/helpers.go @@ -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 } diff --git a/main.go b/main.go index 818203c..c3d8674 100644 --- a/main.go +++ b/main.go @@ -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" diff --git a/users.go b/users.go index a5348a1..814a375 100644 --- a/users.go +++ b/users.go @@ -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 ", 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