Compare commits
2 Commits
563907d477
...
155c006740
Author | SHA1 | Date |
---|---|---|
AJ ONeal | 155c006740 | |
AJ ONeal | aab56909cb |
|
@ -1,3 +1,5 @@
|
|||
.env
|
||||
|
||||
/public-jwks
|
||||
/go-mockid
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
mailgun "github.com/mailgun/mailgun-go/v3"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
)
|
||||
|
||||
func main() {
|
||||
/*
|
||||
MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
MAILGUN_DOMAIN=mail.example.com
|
||||
MAILGUN_FROM="Rob the Robot <rob.the.robot@mail.example.com>"
|
||||
*/
|
||||
|
||||
to := flag.String("to", "", "message recipient in the format of 'John Doe <john@example.com>'")
|
||||
replyTo := flag.String("reply-to", "", "reply-to in the format of 'John Doe <john@example.com>'")
|
||||
subject := flag.String("subject", "Test Subject", "the utf8-encoded subject of the email")
|
||||
text := flag.String(
|
||||
"text",
|
||||
"Testing some Mailgun awesomeness!",
|
||||
"the body of the email as utf8-encoded plain-text format",
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if 0 == len(*to) {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
domain := os.Getenv("MAILGUN_DOMAIN")
|
||||
apiKey := os.Getenv("MAILGUN_API_KEY")
|
||||
from := os.Getenv("MAILGUN_FROM")
|
||||
|
||||
if 0 == len(*text) {
|
||||
*text = "Testing some Mailgun awesomeness!"
|
||||
}
|
||||
|
||||
msgId, err := SendSimpleMessage(domain, apiKey, *to, from, *subject, *text, *replyTo)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Queued with Message ID %q\n", msgId)
|
||||
}
|
||||
|
||||
func SendSimpleMessage(domain, apiKey, to, from, subject, text, replyTo string) (string, error) {
|
||||
mg := mailgun.NewMailgun(domain, apiKey)
|
||||
m := mg.NewMessage(from, subject, text, to)
|
||||
if 0 != len(replyTo) {
|
||||
// mailgun's required "h:" prefix is added by the library
|
||||
m.AddHeader("Reply-To", replyTo)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
_, id, err := mg.Send(ctx, m)
|
||||
return id, err
|
||||
}
|
1
go.mod
1
go.mod
|
@ -5,4 +5,5 @@ go 1.13
|
|||
require (
|
||||
git.rootprojects.org/root/keypairs v0.5.2
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/mailgun/mailgun-go/v3 v3.6.4
|
||||
)
|
||||
|
|
16
go.sum
16
go.sum
|
@ -1,4 +1,20 @@
|
|||
git.rootprojects.org/root/keypairs v0.5.2 h1:jr+drUUm/REaCDJTl5gT3kF2PwlXygcLsBZlqoKTZZw=
|
||||
git.rootprojects.org/root/keypairs v0.5.2/go.mod h1:WGI8PadOp+4LjUuI+wNlSwcJwFtY8L9XuNjuO3213HA=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4=
|
||||
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/mailgun/mailgun-go/v3 v3.6.4 h1:+cvbZRgLSHivbz/w1iWLmxVl6Bqf4geD2D7QMj4+8PE=
|
||||
github.com/mailgun/mailgun-go/v3 v3.6.4/go.mod h1:ZjVnH8S0dR2BLjvkZc/rxwerdcirzlA12LQDuGAadR0=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.idea
|
||||
*.sw?
|
||||
.vscode
|
|
@ -0,0 +1,17 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
|
||||
script:
|
||||
- go get -d -t ./...
|
||||
- go vet ./...
|
||||
- go test ./...
|
||||
- >
|
||||
go_version=$(go version);
|
||||
if [ ${go_version:13:4} = "1.11" ]; then
|
||||
go get -u golang.org/x/tools/cmd/goimports;
|
||||
goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || :;
|
||||
fi
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
# Changelog
|
||||
|
||||
## v4.0.0 (2019-01-10)
|
||||
|
||||
- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8
|
||||
- router: respond with 404 on router with no routes (#362)
|
||||
- router: additional check to ensure wildcard is at the end of a url pattern (#333)
|
||||
- middleware: deprecate use of http.CloseNotifier (#347)
|
||||
- middleware: fix RedirectSlashes to include query params on redirect (#334)
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0
|
||||
|
||||
|
||||
## v3.3.4 (2019-01-07)
|
||||
|
||||
- Minor middleware improvements. No changes to core library/router. Moving v3 into its
|
||||
- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4
|
||||
|
||||
|
||||
## v3.3.3 (2018-08-27)
|
||||
|
||||
- Minor release
|
||||
- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3
|
||||
|
||||
|
||||
## v3.3.2 (2017-12-22)
|
||||
|
||||
- Support to route trailing slashes on mounted sub-routers (#281)
|
||||
- middleware: new `ContentCharset` to check matching charsets. Thank you
|
||||
@csucu for your community contribution!
|
||||
|
||||
|
||||
## v3.3.1 (2017-11-20)
|
||||
|
||||
- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types
|
||||
- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value
|
||||
- Minor bug fixes
|
||||
|
||||
|
||||
## v3.3.0 (2017-10-10)
|
||||
|
||||
- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage
|
||||
- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function
|
||||
|
||||
|
||||
## v3.2.1 (2017-08-31)
|
||||
|
||||
- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface
|
||||
and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path
|
||||
- Add new `RouteMethod` to `*Context`
|
||||
- Add new `Routes` pointer to `*Context`
|
||||
- Add new `middleware.GetHead` to route missing HEAD requests to GET handler
|
||||
- Updated benchmarks (see README)
|
||||
|
||||
|
||||
## v3.1.5 (2017-08-02)
|
||||
|
||||
- Setup golint and go vet for the project
|
||||
- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler`
|
||||
to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler`
|
||||
|
||||
|
||||
## v3.1.0 (2017-07-10)
|
||||
|
||||
- Fix a few minor issues after v3 release
|
||||
- Move `docgen` sub-pkg to https://github.com/go-chi/docgen
|
||||
- Move `render` sub-pkg to https://github.com/go-chi/render
|
||||
- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime
|
||||
suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in
|
||||
https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage.
|
||||
|
||||
|
||||
## v3.0.0 (2017-06-21)
|
||||
|
||||
- Major update to chi library with many exciting updates, but also some *breaking changes*
|
||||
- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as
|
||||
`/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the
|
||||
same router
|
||||
- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example:
|
||||
`r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")`
|
||||
- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as
|
||||
`r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like
|
||||
in `_examples/custom-handler`
|
||||
- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their
|
||||
own using file handler with the stdlib, see `_examples/fileserver` for an example
|
||||
- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()`
|
||||
- Moved the chi project to its own organization, to allow chi-related community packages to
|
||||
be easily discovered and supported, at: https://github.com/go-chi
|
||||
- *NOTE:* please update your import paths to `"github.com/go-chi/chi"`
|
||||
- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2
|
||||
|
||||
|
||||
## v2.1.0 (2017-03-30)
|
||||
|
||||
- Minor improvements and update to the chi core library
|
||||
- Introduced a brand new `chi/render` sub-package to complete the story of building
|
||||
APIs to offer a pattern for managing well-defined request / response payloads. Please
|
||||
check out the updated `_examples/rest` example for how it works.
|
||||
- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface
|
||||
|
||||
|
||||
## v2.0.0 (2017-01-06)
|
||||
|
||||
- After many months of v2 being in an RC state with many companies and users running it in
|
||||
production, the inclusion of some improvements to the middlewares, we are very pleased to
|
||||
announce v2.0.0 of chi.
|
||||
|
||||
|
||||
## v2.0.0-rc1 (2016-07-26)
|
||||
|
||||
- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular
|
||||
community `"net/context"` package has been included in the standard library as `"context"` and
|
||||
utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other
|
||||
request-scoped values. We're very excited about the new context addition and are proud to
|
||||
introduce chi v2, a minimal and powerful routing package for building large HTTP services,
|
||||
with zero external dependencies. Chi focuses on idiomatic design and encourages the use of
|
||||
stdlib HTTP handlers and middlwares.
|
||||
- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc`
|
||||
- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()`
|
||||
- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`,
|
||||
which provides direct access to URL routing parameters, the routing path and the matching
|
||||
routing patterns.
|
||||
- Users upgrading from chi v1 to v2, need to:
|
||||
1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to
|
||||
the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)`
|
||||
2. Use `chi.URLParam(r *http.Request, paramKey string) string`
|
||||
or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value
|
||||
|
||||
|
||||
## v1.0.0 (2016-07-01)
|
||||
|
||||
- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older.
|
||||
|
||||
|
||||
## v0.9.0 (2016-03-31)
|
||||
|
||||
- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33)
|
||||
- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters
|
||||
has changed to: `chi.URLParam(ctx, "id")`
|
|
@ -0,0 +1,31 @@
|
|||
# Contributing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. [Install Go][go-install].
|
||||
2. Download the sources and switch the working directory:
|
||||
|
||||
```bash
|
||||
go get -u -d github.com/go-chi/chi
|
||||
cd $GOPATH/src/github.com/go-chi/chi
|
||||
```
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
A typical workflow is:
|
||||
|
||||
1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip]
|
||||
2. [Create a topic branch.][branch]
|
||||
3. Add tests for your change.
|
||||
4. Run `go test`. If your tests pass, return to the step 3.
|
||||
5. Implement the change and ensure the steps from the previous step pass.
|
||||
6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline.
|
||||
7. [Add, commit and push your changes.][git-help]
|
||||
8. [Submit a pull request.][pull-req]
|
||||
|
||||
[go-install]: https://golang.org/doc/install
|
||||
[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html
|
||||
[fork]: https://help.github.com/articles/fork-a-repo
|
||||
[branch]: http://learn.github.com/p/branching.html
|
||||
[git-help]: https://guides.github.com
|
||||
[pull-req]: https://help.github.com/articles/using-pull-requests
|
|
@ -0,0 +1,20 @@
|
|||
Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,438 @@
|
|||
# <img alt="chi" src="https://cdn.rawgit.com/go-chi/chi/master/_examples/chi.svg" width="220" />
|
||||
|
||||
|
||||
[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis]
|
||||
|
||||
`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's
|
||||
especially good at helping you write large REST API services that are kept maintainable as your
|
||||
project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to
|
||||
handle signaling, cancelation and request-scoped values across a handler chain.
|
||||
|
||||
The focus of the project has been to seek out an elegant and comfortable design for writing
|
||||
REST API servers, written during the development of the Pressly API service that powers our
|
||||
public API service, which in turn powers all of our client-side applications.
|
||||
|
||||
The key considerations of chi's design are: project structure, maintainability, standard http
|
||||
handlers (stdlib-only), developer productivity, and deconstructing a large system into many small
|
||||
parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also
|
||||
included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too!
|
||||
|
||||
## Install
|
||||
|
||||
`go get -u github.com/go-chi/chi`
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* **Lightweight** - cloc'd in ~1000 LOC for the chi router
|
||||
* **Fast** - yes, see [benchmarks](#benchmarks)
|
||||
* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http`
|
||||
* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and subrouter mounting
|
||||
* **Context control** - built on new `context` package, providing value chaining, cancelations and timeouts
|
||||
* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91))
|
||||
* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown
|
||||
* **No external dependencies** - plain ol' Go stdlib + net/http
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples.
|
||||
|
||||
|
||||
**As easy as:**
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("welcome"))
|
||||
})
|
||||
http.ListenAndServe(":3000", r)
|
||||
}
|
||||
```
|
||||
|
||||
**REST Preview:**
|
||||
|
||||
Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs
|
||||
in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in
|
||||
Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)).
|
||||
|
||||
I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed
|
||||
above, they will show you all the features of chi and serve as a good form of documentation.
|
||||
|
||||
```go
|
||||
import (
|
||||
//...
|
||||
"context"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// A good base middleware stack
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
// Set a timeout value on the request context (ctx), that will signal
|
||||
// through ctx.Done() that the request has timed out and further
|
||||
// processing should be stopped.
|
||||
r.Use(middleware.Timeout(60 * time.Second))
|
||||
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("hi"))
|
||||
})
|
||||
|
||||
// RESTy routes for "articles" resource
|
||||
r.Route("/articles", func(r chi.Router) {
|
||||
r.With(paginate).Get("/", listArticles) // GET /articles
|
||||
r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017
|
||||
|
||||
r.Post("/", createArticle) // POST /articles
|
||||
r.Get("/search", searchArticles) // GET /articles/search
|
||||
|
||||
// Regexp url parameters:
|
||||
r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto
|
||||
|
||||
// Subrouters:
|
||||
r.Route("/{articleID}", func(r chi.Router) {
|
||||
r.Use(ArticleCtx)
|
||||
r.Get("/", getArticle) // GET /articles/123
|
||||
r.Put("/", updateArticle) // PUT /articles/123
|
||||
r.Delete("/", deleteArticle) // DELETE /articles/123
|
||||
})
|
||||
})
|
||||
|
||||
// Mount the admin sub-router
|
||||
r.Mount("/admin", adminRouter())
|
||||
|
||||
http.ListenAndServe(":3333", r)
|
||||
}
|
||||
|
||||
func ArticleCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
articleID := chi.URLParam(r, "articleID")
|
||||
article, err := dbGetArticle(articleID)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), "article", article)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func getArticle(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
article, ok := ctx.Value("article").(*Article)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(422), 422)
|
||||
return
|
||||
}
|
||||
w.Write([]byte(fmt.Sprintf("title:%s", article.Title)))
|
||||
}
|
||||
|
||||
// A completely separate router for administrator routes
|
||||
func adminRouter() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Use(AdminOnly)
|
||||
r.Get("/", adminIndex)
|
||||
r.Get("/accounts", adminListAccounts)
|
||||
return r
|
||||
}
|
||||
|
||||
func AdminOnly(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
perm, ok := ctx.Value("acl.permission").(YourPermissionType)
|
||||
if !ok || !perm.IsAdmin() {
|
||||
http.Error(w, http.StatusText(403), 403)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Router design
|
||||
|
||||
chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree).
|
||||
The router is fully compatible with `net/http`.
|
||||
|
||||
Built on top of the tree is the `Router` interface:
|
||||
|
||||
```go
|
||||
// Router consisting of the core routing methods used by chi's Mux,
|
||||
// using only the standard net/http.
|
||||
type Router interface {
|
||||
http.Handler
|
||||
Routes
|
||||
|
||||
// Use appends one of more middlewares onto the Router stack.
|
||||
Use(middlewares ...func(http.Handler) http.Handler)
|
||||
|
||||
// With adds inline middlewares for an endpoint handler.
|
||||
With(middlewares ...func(http.Handler) http.Handler) Router
|
||||
|
||||
// Group adds a new inline-Router along the current routing
|
||||
// path, with a fresh middleware stack for the inline-Router.
|
||||
Group(fn func(r Router)) Router
|
||||
|
||||
// Route mounts a sub-Router along a `pattern`` string.
|
||||
Route(pattern string, fn func(r Router)) Router
|
||||
|
||||
// Mount attaches another http.Handler along ./pattern/*
|
||||
Mount(pattern string, h http.Handler)
|
||||
|
||||
// Handle and HandleFunc adds routes for `pattern` that matches
|
||||
// all HTTP methods.
|
||||
Handle(pattern string, h http.Handler)
|
||||
HandleFunc(pattern string, h http.HandlerFunc)
|
||||
|
||||
// Method and MethodFunc adds routes for `pattern` that matches
|
||||
// the `method` HTTP method.
|
||||
Method(method, pattern string, h http.Handler)
|
||||
MethodFunc(method, pattern string, h http.HandlerFunc)
|
||||
|
||||
// HTTP-method routing along `pattern`
|
||||
Connect(pattern string, h http.HandlerFunc)
|
||||
Delete(pattern string, h http.HandlerFunc)
|
||||
Get(pattern string, h http.HandlerFunc)
|
||||
Head(pattern string, h http.HandlerFunc)
|
||||
Options(pattern string, h http.HandlerFunc)
|
||||
Patch(pattern string, h http.HandlerFunc)
|
||||
Post(pattern string, h http.HandlerFunc)
|
||||
Put(pattern string, h http.HandlerFunc)
|
||||
Trace(pattern string, h http.HandlerFunc)
|
||||
|
||||
// NotFound defines a handler to respond whenever a route could
|
||||
// not be found.
|
||||
NotFound(h http.HandlerFunc)
|
||||
|
||||
// MethodNotAllowed defines a handler to respond whenever a method is
|
||||
// not allowed.
|
||||
MethodNotAllowed(h http.HandlerFunc)
|
||||
}
|
||||
|
||||
// Routes interface adds two methods for router traversal, which is also
|
||||
// used by the github.com/go-chi/docgen package to generate documentation for Routers.
|
||||
type Routes interface {
|
||||
// Routes returns the routing tree in an easily traversable structure.
|
||||
Routes() []Route
|
||||
|
||||
// Middlewares returns the list of middlewares in use by the router.
|
||||
Middlewares() Middlewares
|
||||
|
||||
// Match searches the routing tree for a handler that matches
|
||||
// the method/path - similar to routing a http request, but without
|
||||
// executing the handler thereafter.
|
||||
Match(rctx *Context, method, path string) bool
|
||||
}
|
||||
```
|
||||
|
||||
Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern
|
||||
supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters
|
||||
can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters
|
||||
and `chi.URLParam(r, "*")` for a wildcard parameter.
|
||||
|
||||
|
||||
### Middleware handlers
|
||||
|
||||
chi's middlewares are just stdlib net/http middleware handlers. There is nothing special
|
||||
about them, which means the router and all the tooling is designed to be compatible and
|
||||
friendly with any middleware in the community. This offers much better extensibility and reuse
|
||||
of packages and is at the heart of chi's purpose.
|
||||
|
||||
Here is an example of a standard net/http middleware handler using the new request context
|
||||
available in Go. This middleware sets a hypothetical user identifier on the request
|
||||
context and calls the next handler in the chain.
|
||||
|
||||
```go
|
||||
// HTTP middleware setting a value on the request context
|
||||
func MyMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), "user", "123")
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Request handlers
|
||||
|
||||
chi uses standard net/http request handlers. This little snippet is an example of a http.Handler
|
||||
func that reads a user identifier from the request context - hypothetically, identifying
|
||||
the user sending an authenticated request, validated+set by a previous middleware handler.
|
||||
|
||||
```go
|
||||
// HTTP handler accessing data from the request context.
|
||||
func MyRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.Context().Value("user").(string)
|
||||
w.Write([]byte(fmt.Sprintf("hi %s", user)))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### URL parameters
|
||||
|
||||
chi's router parses and stores URL parameters right onto the request context. Here is
|
||||
an example of how to access URL params in your net/http handlers. And of course, middlewares
|
||||
are able to access the same information.
|
||||
|
||||
```go
|
||||
// HTTP handler accessing the url routing parameters.
|
||||
func MyRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "userID") // from a route like /users/{userID}
|
||||
|
||||
ctx := r.Context()
|
||||
key := ctx.Value("key").(string)
|
||||
|
||||
w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key)))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Middlewares
|
||||
|
||||
chi comes equipped with an optional `middleware` package, providing a suite of standard
|
||||
`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible
|
||||
with `net/http` can be used with chi's mux.
|
||||
|
||||
### Core middlewares
|
||||
|
||||
-----------------------------------------------------------------------------------------------------------
|
||||
| chi/middleware Handler | description |
|
||||
|:----------------------|:---------------------------------------------------------------------------------
|
||||
| AllowContentType | Explicit whitelist of accepted request Content-Types |
|
||||
| Compress | Gzip compression for clients that accept compressed responses |
|
||||
| GetHead | Automatically route undefined HEAD requests to GET handlers |
|
||||
| Heartbeat | Monitoring endpoint to check the servers pulse |
|
||||
| Logger | Logs the start and end of each request with the elapsed processing time |
|
||||
| NoCache | Sets response headers to prevent clients from caching |
|
||||
| Profiler | Easily attach net/http/pprof to your routers |
|
||||
| RealIP | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP |
|
||||
| Recoverer | Gracefully absorb panics and prints the stack trace |
|
||||
| RequestID | Injects a request ID into the context of each request |
|
||||
| RedirectSlashes | Redirect slashes on routing paths |
|
||||
| SetHeader | Short-hand middleware to set a response header key/value |
|
||||
| StripSlashes | Strip slashes on routing paths |
|
||||
| Throttle | Puts a ceiling on the number of concurrent requests |
|
||||
| Timeout | Signals to the request context when the timeout deadline is reached |
|
||||
| URLFormat | Parse extension from url and put it on request context |
|
||||
| WithValue | Short-hand middleware to set a key/value on the request context |
|
||||
-----------------------------------------------------------------------------------------------------------
|
||||
|
||||
### Auxiliary middlewares & packages
|
||||
|
||||
Please see https://github.com/go-chi for additional packages.
|
||||
|
||||
--------------------------------------------------------------------------------------------------------------------
|
||||
| package | description |
|
||||
|:---------------------------------------------------|:-------------------------------------------------------------
|
||||
| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) |
|
||||
| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime |
|
||||
| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication |
|
||||
| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing |
|
||||
| [httpcoala](https://github.com/go-chi/httpcoala) | HTTP request coalescer |
|
||||
| [chi-authz](https://github.com/casbin/chi-authz) | Request ACL via https://github.com/hsluoyz/casbin |
|
||||
| [phi](https://github.com/fate-lovely/phi) | Port chi to [fasthttp](https://github.com/valyala/fasthttp) |
|
||||
--------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
please [submit a PR](./CONTRIBUTING.md) if you'd like to include a link to a chi-compatible middleware
|
||||
|
||||
|
||||
## context?
|
||||
|
||||
`context` is a tiny pkg that provides simple interface to signal context across call stacks
|
||||
and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani)
|
||||
and is available in stdlib since go1.7.
|
||||
|
||||
Learn more at https://blog.golang.org/context
|
||||
|
||||
and..
|
||||
* Docs: https://golang.org/pkg/context
|
||||
* Source: https://github.com/golang/go/tree/master/src/context
|
||||
|
||||
|
||||
## Benchmarks
|
||||
|
||||
The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark
|
||||
|
||||
Results as of Jan 9, 2019 with Go 1.11.4 on Linux X1 Carbon laptop
|
||||
|
||||
```shell
|
||||
BenchmarkChi_Param 3000000 475 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Param5 2000000 696 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Param20 1000000 1275 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParamWrite 3000000 505 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubStatic 3000000 508 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubParam 2000000 669 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GithubAll 10000 134627 ns/op 87699 B/op 609 allocs/op
|
||||
BenchmarkChi_GPlusStatic 3000000 402 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlusParam 3000000 500 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlus2Params 3000000 586 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_GPlusAll 200000 7237 ns/op 5616 B/op 39 allocs/op
|
||||
BenchmarkChi_ParseStatic 3000000 408 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParseParam 3000000 488 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_Parse2Params 3000000 551 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkChi_ParseAll 100000 13508 ns/op 11232 B/op 78 allocs/op
|
||||
BenchmarkChi_StaticAll 20000 81933 ns/op 67826 B/op 471 allocs/op
|
||||
```
|
||||
|
||||
Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc
|
||||
|
||||
NOTE: the allocs in the benchmark above are from the calls to http.Request's
|
||||
`WithContext(context.Context)` method that clones the http.Request, sets the `Context()`
|
||||
on the duplicated (alloc'd) request and returns it the new request object. This is just
|
||||
how setting context on a request in Go works.
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
* Carl Jackson for https://github.com/zenazn/goji
|
||||
* Parts of chi's thinking comes from goji, and chi's middleware package
|
||||
sources from goji.
|
||||
* Armon Dadgar for https://github.com/armon/go-radix
|
||||
* Contributions: [@VojtechVitek](https://github.com/VojtechVitek)
|
||||
|
||||
We'll be more than happy to see [your contributions](./CONTRIBUTING.md)!
|
||||
|
||||
|
||||
## Beyond REST
|
||||
|
||||
chi is just a http router that lets you decompose request handling into many smaller layers.
|
||||
Many companies including Pressly.com (of course) use chi to write REST services for their public
|
||||
APIs. But, REST is just a convention for managing state via HTTP, and there's a lot of other pieces
|
||||
required to write a complete client-server system or network of microservices.
|
||||
|
||||
Looking ahead beyond REST, I also recommend some newer works in the field coming from
|
||||
[gRPC](https://github.com/grpc/grpc-go), [NATS](https://nats.io), [go-kit](https://github.com/go-kit/kit)
|
||||
and even [graphql](https://github.com/graphql-go/graphql). They're all pretty cool with their
|
||||
own unique approaches and benefits. Specifically, I'd look at gRPC since it makes client-server
|
||||
communication feel like a single program on a single computer, no need to hand-write a client library
|
||||
and the request/response payloads are typed contracts. NATS is pretty amazing too as a super
|
||||
fast and lightweight pub-sub transport that can speak protobufs, with nice service discovery -
|
||||
an excellent combination with gRPC.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka)
|
||||
|
||||
Licensed under [MIT License](./LICENSE)
|
||||
|
||||
[GoDoc]: https://godoc.org/github.com/go-chi/chi
|
||||
[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg
|
||||
[Travis]: https://travis-ci.org/go-chi/chi
|
||||
[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master
|
|
@ -0,0 +1,49 @@
|
|||
package chi
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Chain returns a Middlewares type from a slice of middleware handlers.
|
||||
func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares {
|
||||
return Middlewares(middlewares)
|
||||
}
|
||||
|
||||
// Handler builds and returns a http.Handler from the chain of middlewares,
|
||||
// with `h http.Handler` as the final handler.
|
||||
func (mws Middlewares) Handler(h http.Handler) http.Handler {
|
||||
return &ChainHandler{mws, h, chain(mws, h)}
|
||||
}
|
||||
|
||||
// HandlerFunc builds and returns a http.Handler from the chain of middlewares,
|
||||
// with `h http.Handler` as the final handler.
|
||||
func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler {
|
||||
return &ChainHandler{mws, h, chain(mws, h)}
|
||||
}
|
||||
|
||||
// ChainHandler is a http.Handler with support for handler composition and
|
||||
// execution.
|
||||
type ChainHandler struct {
|
||||
Middlewares Middlewares
|
||||
Endpoint http.Handler
|
||||
chain http.Handler
|
||||
}
|
||||
|
||||
func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.chain.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// chain builds a http.Handler composed of an inline middleware stack and endpoint
|
||||
// handler in the order they are passed.
|
||||
func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler {
|
||||
// Return ahead of time if there aren't any middlewares for the chain
|
||||
if len(middlewares) == 0 {
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// Wrap the end handler with the middleware chain
|
||||
h := middlewares[len(middlewares)-1](endpoint)
|
||||
for i := len(middlewares) - 2; i >= 0; i-- {
|
||||
h = middlewares[i](h)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
//
|
||||
// Package chi is a small, idiomatic and composable router for building HTTP services.
|
||||
//
|
||||
// chi requires Go 1.7 or newer.
|
||||
//
|
||||
// Example:
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "net/http"
|
||||
//
|
||||
// "github.com/go-chi/chi"
|
||||
// "github.com/go-chi/chi/middleware"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// r := chi.NewRouter()
|
||||
// r.Use(middleware.Logger)
|
||||
// r.Use(middleware.Recoverer)
|
||||
//
|
||||
// r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// w.Write([]byte("root."))
|
||||
// })
|
||||
//
|
||||
// http.ListenAndServe(":3333", r)
|
||||
// }
|
||||
//
|
||||
// See github.com/go-chi/chi/_examples/ for more in-depth examples.
|
||||
//
|
||||
// URL patterns allow for easy matching of path components in HTTP
|
||||
// requests. The matching components can then be accessed using
|
||||
// chi.URLParam(). All patterns must begin with a slash.
|
||||
//
|
||||
// A simple named placeholder {name} matches any sequence of characters
|
||||
// up to the next / or the end of the URL. Trailing slashes on paths must
|
||||
// be handled explicitly.
|
||||
//
|
||||
// A placeholder with a name followed by a colon allows a regular
|
||||
// expression match, for example {number:\\d+}. The regular expression
|
||||
// syntax is Go's normal regexp RE2 syntax, except that regular expressions
|
||||
// including { or } are not supported, and / will never be
|
||||
// matched. An anonymous regexp pattern is allowed, using an empty string
|
||||
// before the colon in the placeholder, such as {:\\d+}
|
||||
//
|
||||
// The special placeholder of asterisk matches the rest of the requested
|
||||
// URL. Any trailing characters in the pattern are ignored. This is the only
|
||||
// placeholder which will match / characters.
|
||||
//
|
||||
// Examples:
|
||||
// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/"
|
||||
// "/user/{name}/info" matches "/user/jsmith/info"
|
||||
// "/page/*" matches "/page/intro/latest"
|
||||
// "/page/*/index" also matches "/page/intro/latest"
|
||||
// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01"
|
||||
//
|
||||
package chi
|
||||
|
||||
import "net/http"
|
||||
|
||||
// NewRouter returns a new Mux object that implements the Router interface.
|
||||
func NewRouter() *Mux {
|
||||
return NewMux()
|
||||
}
|
||||
|
||||
// Router consisting of the core routing methods used by chi's Mux,
|
||||
// using only the standard net/http.
|
||||
type Router interface {
|
||||
http.Handler
|
||||
Routes
|
||||
|
||||
// Use appends one of more middlewares onto the Router stack.
|
||||
Use(middlewares ...func(http.Handler) http.Handler)
|
||||
|
||||
// With adds inline middlewares for an endpoint handler.
|
||||
With(middlewares ...func(http.Handler) http.Handler) Router
|
||||
|
||||
// Group adds a new inline-Router along the current routing
|
||||
// path, with a fresh middleware stack for the inline-Router.
|
||||
Group(fn func(r Router)) Router
|
||||
|
||||
// Route mounts a sub-Router along a `pattern`` string.
|
||||
Route(pattern string, fn func(r Router)) Router
|
||||
|
||||
// Mount attaches another http.Handler along ./pattern/*
|
||||
Mount(pattern string, h http.Handler)
|
||||
|
||||
// Handle and HandleFunc adds routes for `pattern` that matches
|
||||
// all HTTP methods.
|
||||
Handle(pattern string, h http.Handler)
|
||||
HandleFunc(pattern string, h http.HandlerFunc)
|
||||
|
||||
// Method and MethodFunc adds routes for `pattern` that matches
|
||||
// the `method` HTTP method.
|
||||
Method(method, pattern string, h http.Handler)
|
||||
MethodFunc(method, pattern string, h http.HandlerFunc)
|
||||
|
||||
// HTTP-method routing along `pattern`
|
||||
Connect(pattern string, h http.HandlerFunc)
|
||||
Delete(pattern string, h http.HandlerFunc)
|
||||
Get(pattern string, h http.HandlerFunc)
|
||||
Head(pattern string, h http.HandlerFunc)
|
||||
Options(pattern string, h http.HandlerFunc)
|
||||
Patch(pattern string, h http.HandlerFunc)
|
||||
Post(pattern string, h http.HandlerFunc)
|
||||
Put(pattern string, h http.HandlerFunc)
|
||||
Trace(pattern string, h http.HandlerFunc)
|
||||
|
||||
// NotFound defines a handler to respond whenever a route could
|
||||
// not be found.
|
||||
NotFound(h http.HandlerFunc)
|
||||
|
||||
// MethodNotAllowed defines a handler to respond whenever a method is
|
||||
// not allowed.
|
||||
MethodNotAllowed(h http.HandlerFunc)
|
||||
}
|
||||
|
||||
// Routes interface adds two methods for router traversal, which is also
|
||||
// used by the `docgen` subpackage to generation documentation for Routers.
|
||||
type Routes interface {
|
||||
// Routes returns the routing tree in an easily traversable structure.
|
||||
Routes() []Route
|
||||
|
||||
// Middlewares returns the list of middlewares in use by the router.
|
||||
Middlewares() Middlewares
|
||||
|
||||
// Match searches the routing tree for a handler that matches
|
||||
// the method/path - similar to routing a http request, but without
|
||||
// executing the handler thereafter.
|
||||
Match(rctx *Context, method, path string) bool
|
||||
}
|
||||
|
||||
// Middlewares type is a slice of standard middleware handlers with methods
|
||||
// to compose middleware chains and http.Handler's.
|
||||
type Middlewares []func(http.Handler) http.Handler
|
|
@ -0,0 +1,161 @@
|
|||
package chi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// RouteCtxKey is the context.Context key to store the request context.
|
||||
RouteCtxKey = &contextKey{"RouteContext"}
|
||||
)
|
||||
|
||||
// Context is the default routing context set on the root node of a
|
||||
// request context to track route patterns, URL parameters and
|
||||
// an optional routing path.
|
||||
type Context struct {
|
||||
Routes Routes
|
||||
|
||||
// Routing path/method override used during the route search.
|
||||
// See Mux#routeHTTP method.
|
||||
RoutePath string
|
||||
RouteMethod string
|
||||
|
||||
// Routing pattern stack throughout the lifecycle of the request,
|
||||
// across all connected routers. It is a record of all matching
|
||||
// patterns across a stack of sub-routers.
|
||||
RoutePatterns []string
|
||||
|
||||
// URLParams are the stack of routeParams captured during the
|
||||
// routing lifecycle across a stack of sub-routers.
|
||||
URLParams RouteParams
|
||||
|
||||
// The endpoint routing pattern that matched the request URI path
|
||||
// or `RoutePath` of the current sub-router. This value will update
|
||||
// during the lifecycle of a request passing through a stack of
|
||||
// sub-routers.
|
||||
routePattern string
|
||||
|
||||
// Route parameters matched for the current sub-router. It is
|
||||
// intentionally unexported so it cant be tampered.
|
||||
routeParams RouteParams
|
||||
|
||||
// methodNotAllowed hint
|
||||
methodNotAllowed bool
|
||||
}
|
||||
|
||||
// NewRouteContext returns a new routing Context object.
|
||||
func NewRouteContext() *Context {
|
||||
return &Context{}
|
||||
}
|
||||
|
||||
// Reset a routing context to its initial state.
|
||||
func (x *Context) Reset() {
|
||||
x.Routes = nil
|
||||
x.RoutePath = ""
|
||||
x.RouteMethod = ""
|
||||
x.RoutePatterns = x.RoutePatterns[:0]
|
||||
x.URLParams.Keys = x.URLParams.Keys[:0]
|
||||
x.URLParams.Values = x.URLParams.Values[:0]
|
||||
|
||||
x.routePattern = ""
|
||||
x.routeParams.Keys = x.routeParams.Keys[:0]
|
||||
x.routeParams.Values = x.routeParams.Values[:0]
|
||||
x.methodNotAllowed = false
|
||||
}
|
||||
|
||||
// URLParam returns the corresponding URL parameter value from the request
|
||||
// routing context.
|
||||
func (x *Context) URLParam(key string) string {
|
||||
for k := len(x.URLParams.Keys) - 1; k >= 0; k-- {
|
||||
if x.URLParams.Keys[k] == key {
|
||||
return x.URLParams.Values[k]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RoutePattern builds the routing pattern string for the particular
|
||||
// request, at the particular point during routing. This means, the value
|
||||
// will change throughout the execution of a request in a router. That is
|
||||
// why its advised to only use this value after calling the next handler.
|
||||
//
|
||||
// For example,
|
||||
//
|
||||
// func Instrument(next http.Handler) http.Handler {
|
||||
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// next.ServeHTTP(w, r)
|
||||
// routePattern := chi.RouteContext(r.Context()).RoutePattern()
|
||||
// measure(w, r, routePattern)
|
||||
// })
|
||||
// }
|
||||
func (x *Context) RoutePattern() string {
|
||||
routePattern := strings.Join(x.RoutePatterns, "")
|
||||
return strings.Replace(routePattern, "/*/", "/", -1)
|
||||
}
|
||||
|
||||
// RouteContext returns chi's routing Context object from a
|
||||
// http.Request Context.
|
||||
func RouteContext(ctx context.Context) *Context {
|
||||
return ctx.Value(RouteCtxKey).(*Context)
|
||||
}
|
||||
|
||||
// URLParam returns the url parameter from a http.Request object.
|
||||
func URLParam(r *http.Request, key string) string {
|
||||
if rctx := RouteContext(r.Context()); rctx != nil {
|
||||
return rctx.URLParam(key)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// URLParamFromCtx returns the url parameter from a http.Request Context.
|
||||
func URLParamFromCtx(ctx context.Context, key string) string {
|
||||
if rctx := RouteContext(ctx); rctx != nil {
|
||||
return rctx.URLParam(key)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RouteParams is a structure to track URL routing parameters efficiently.
|
||||
type RouteParams struct {
|
||||
Keys, Values []string
|
||||
}
|
||||
|
||||
// Add will append a URL parameter to the end of the route param
|
||||
func (s *RouteParams) Add(key, value string) {
|
||||
(*s).Keys = append((*s).Keys, key)
|
||||
(*s).Values = append((*s).Values, value)
|
||||
}
|
||||
|
||||
// ServerBaseContext wraps an http.Handler to set the request context to the
|
||||
// `baseCtx`.
|
||||
func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler {
|
||||
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
baseCtx := baseCtx
|
||||
|
||||
// Copy over default net/http server context keys
|
||||
if v, ok := ctx.Value(http.ServerContextKey).(*http.Server); ok {
|
||||
baseCtx = context.WithValue(baseCtx, http.ServerContextKey, v)
|
||||
}
|
||||
if v, ok := ctx.Value(http.LocalAddrContextKey).(net.Addr); ok {
|
||||
baseCtx = context.WithValue(baseCtx, http.LocalAddrContextKey, v)
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r.WithContext(baseCtx))
|
||||
})
|
||||
return fn
|
||||
}
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation. This technique
|
||||
// for defining context keys was copied from Go 1.7's new use of context in net/http.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (k *contextKey) String() string {
|
||||
return "chi context value " + k.name
|
||||
}
|
|
@ -0,0 +1,460 @@
|
|||
package chi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var _ Router = &Mux{}
|
||||
|
||||
// Mux is a simple HTTP route multiplexer that parses a request path,
|
||||
// records any URL params, and executes an end handler. It implements
|
||||
// the http.Handler interface and is friendly with the standard library.
|
||||
//
|
||||
// Mux is designed to be fast, minimal and offer a powerful API for building
|
||||
// modular and composable HTTP services with a large set of handlers. It's
|
||||
// particularly useful for writing large REST API services that break a handler
|
||||
// into many smaller parts composed of middlewares and end handlers.
|
||||
type Mux struct {
|
||||
// The radix trie router
|
||||
tree *node
|
||||
|
||||
// The middleware stack
|
||||
middlewares []func(http.Handler) http.Handler
|
||||
|
||||
// Controls the behaviour of middleware chain generation when a mux
|
||||
// is registered as an inline group inside another mux.
|
||||
inline bool
|
||||
parent *Mux
|
||||
|
||||
// The computed mux handler made of the chained middleware stack and
|
||||
// the tree router
|
||||
handler http.Handler
|
||||
|
||||
// Routing context pool
|
||||
pool *sync.Pool
|
||||
|
||||
// Custom route not found handler
|
||||
notFoundHandler http.HandlerFunc
|
||||
|
||||
// Custom method not allowed handler
|
||||
methodNotAllowedHandler http.HandlerFunc
|
||||
}
|
||||
|
||||
// NewMux returns a newly initialized Mux object that implements the Router
|
||||
// interface.
|
||||
func NewMux() *Mux {
|
||||
mux := &Mux{tree: &node{}, pool: &sync.Pool{}}
|
||||
mux.pool.New = func() interface{} {
|
||||
return NewRouteContext()
|
||||
}
|
||||
return mux
|
||||
}
|
||||
|
||||
// ServeHTTP is the single method of the http.Handler interface that makes
|
||||
// Mux interoperable with the standard library. It uses a sync.Pool to get and
|
||||
// reuse routing contexts for each request.
|
||||
func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Ensure the mux has some routes defined on the mux
|
||||
if mx.handler == nil {
|
||||
mx.NotFoundHandler().ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if a routing context already exists from a parent router.
|
||||
rctx, _ := r.Context().Value(RouteCtxKey).(*Context)
|
||||
if rctx != nil {
|
||||
mx.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch a RouteContext object from the sync pool, and call the computed
|
||||
// mx.handler that is comprised of mx.middlewares + mx.routeHTTP.
|
||||
// Once the request is finished, reset the routing context and put it back
|
||||
// into the pool for reuse from another request.
|
||||
rctx = mx.pool.Get().(*Context)
|
||||
rctx.Reset()
|
||||
rctx.Routes = mx
|
||||
r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx))
|
||||
mx.handler.ServeHTTP(w, r)
|
||||
mx.pool.Put(rctx)
|
||||
}
|
||||
|
||||
// Use appends a middleware handler to the Mux middleware stack.
|
||||
//
|
||||
// The middleware stack for any Mux will execute before searching for a matching
|
||||
// route to a specific handler, which provides opportunity to respond early,
|
||||
// change the course of the request execution, or set request-scoped values for
|
||||
// the next http.Handler.
|
||||
func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
|
||||
if mx.handler != nil {
|
||||
panic("chi: all middlewares must be defined before routes on a mux")
|
||||
}
|
||||
mx.middlewares = append(mx.middlewares, middlewares...)
|
||||
}
|
||||
|
||||
// Handle adds the route `pattern` that matches any http method to
|
||||
// execute the `handler` http.Handler.
|
||||
func (mx *Mux) Handle(pattern string, handler http.Handler) {
|
||||
mx.handle(mALL, pattern, handler)
|
||||
}
|
||||
|
||||
// HandleFunc adds the route `pattern` that matches any http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.handle(mALL, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// Method adds the route `pattern` that matches `method` http method to
|
||||
// execute the `handler` http.Handler.
|
||||
func (mx *Mux) Method(method, pattern string, handler http.Handler) {
|
||||
m, ok := methodMap[strings.ToUpper(method)]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("chi: '%s' http method is not supported.", method))
|
||||
}
|
||||
mx.handle(m, pattern, handler)
|
||||
}
|
||||
|
||||
// MethodFunc adds the route `pattern` that matches `method` http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.Method(method, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// Connect adds the route `pattern` that matches a CONNECT http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.handle(mCONNECT, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// Delete adds the route `pattern` that matches a DELETE http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.handle(mDELETE, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// Get adds the route `pattern` that matches a GET http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.handle(mGET, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// Head adds the route `pattern` that matches a HEAD http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.handle(mHEAD, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// Options adds the route `pattern` that matches a OPTIONS http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.handle(mOPTIONS, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// Patch adds the route `pattern` that matches a PATCH http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.handle(mPATCH, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// Post adds the route `pattern` that matches a POST http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.handle(mPOST, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// Put adds the route `pattern` that matches a PUT http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.handle(mPUT, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// Trace adds the route `pattern` that matches a TRACE http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) {
|
||||
mx.handle(mTRACE, pattern, handlerFn)
|
||||
}
|
||||
|
||||
// NotFound sets a custom http.HandlerFunc for routing paths that could
|
||||
// not be found. The default 404 handler is `http.NotFound`.
|
||||
func (mx *Mux) NotFound(handlerFn http.HandlerFunc) {
|
||||
// Build NotFound handler chain
|
||||
m := mx
|
||||
hFn := handlerFn
|
||||
if mx.inline && mx.parent != nil {
|
||||
m = mx.parent
|
||||
hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP
|
||||
}
|
||||
|
||||
// Update the notFoundHandler from this point forward
|
||||
m.notFoundHandler = hFn
|
||||
m.updateSubRoutes(func(subMux *Mux) {
|
||||
if subMux.notFoundHandler == nil {
|
||||
subMux.NotFound(hFn)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the
|
||||
// method is unresolved. The default handler returns a 405 with an empty body.
|
||||
func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) {
|
||||
// Build MethodNotAllowed handler chain
|
||||
m := mx
|
||||
hFn := handlerFn
|
||||
if mx.inline && mx.parent != nil {
|
||||
m = mx.parent
|
||||
hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP
|
||||
}
|
||||
|
||||
// Update the methodNotAllowedHandler from this point forward
|
||||
m.methodNotAllowedHandler = hFn
|
||||
m.updateSubRoutes(func(subMux *Mux) {
|
||||
if subMux.methodNotAllowedHandler == nil {
|
||||
subMux.MethodNotAllowed(hFn)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// With adds inline middlewares for an endpoint handler.
|
||||
func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router {
|
||||
// Similarly as in handle(), we must build the mux handler once further
|
||||
// middleware registration isn't allowed for this stack, like now.
|
||||
if !mx.inline && mx.handler == nil {
|
||||
mx.buildRouteHandler()
|
||||
}
|
||||
|
||||
// Copy middlewares from parent inline muxs
|
||||
var mws Middlewares
|
||||
if mx.inline {
|
||||
mws = make(Middlewares, len(mx.middlewares))
|
||||
copy(mws, mx.middlewares)
|
||||
}
|
||||
mws = append(mws, middlewares...)
|
||||
|
||||
im := &Mux{pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws}
|
||||
|
||||
return im
|
||||
}
|
||||
|
||||
// Group creates a new inline-Mux with a fresh middleware stack. It's useful
|
||||
// for a group of handlers along the same routing path that use an additional
|
||||
// set of middlewares. See _examples/.
|
||||
func (mx *Mux) Group(fn func(r Router)) Router {
|
||||
im := mx.With().(*Mux)
|
||||
if fn != nil {
|
||||
fn(im)
|
||||
}
|
||||
return im
|
||||
}
|
||||
|
||||
// Route creates a new Mux with a fresh middleware stack and mounts it
|
||||
// along the `pattern` as a subrouter. Effectively, this is a short-hand
|
||||
// call to Mount. See _examples/.
|
||||
func (mx *Mux) Route(pattern string, fn func(r Router)) Router {
|
||||
subRouter := NewRouter()
|
||||
if fn != nil {
|
||||
fn(subRouter)
|
||||
}
|
||||
mx.Mount(pattern, subRouter)
|
||||
return subRouter
|
||||
}
|
||||
|
||||
// Mount attaches another http.Handler or chi Router as a subrouter along a routing
|
||||
// path. It's very useful to split up a large API as many independent routers and
|
||||
// compose them as a single service using Mount. See _examples/.
|
||||
//
|
||||
// Note that Mount() simply sets a wildcard along the `pattern` that will continue
|
||||
// routing at the `handler`, which in most cases is another chi.Router. As a result,
|
||||
// if you define two Mount() routes on the exact same pattern the mount will panic.
|
||||
func (mx *Mux) Mount(pattern string, handler http.Handler) {
|
||||
// Provide runtime safety for ensuring a pattern isn't mounted on an existing
|
||||
// routing pattern.
|
||||
if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") {
|
||||
panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern))
|
||||
}
|
||||
|
||||
// Assign sub-Router's with the parent not found & method not allowed handler if not specified.
|
||||
subr, ok := handler.(*Mux)
|
||||
if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil {
|
||||
subr.NotFound(mx.notFoundHandler)
|
||||
}
|
||||
if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil {
|
||||
subr.MethodNotAllowed(mx.methodNotAllowedHandler)
|
||||
}
|
||||
|
||||
// Wrap the sub-router in a handlerFunc to scope the request path for routing.
|
||||
mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rctx := RouteContext(r.Context())
|
||||
rctx.RoutePath = mx.nextRoutePath(rctx)
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
if pattern == "" || pattern[len(pattern)-1] != '/' {
|
||||
mx.handle(mALL|mSTUB, pattern, mountHandler)
|
||||
mx.handle(mALL|mSTUB, pattern+"/", mountHandler)
|
||||
pattern += "/"
|
||||
}
|
||||
|
||||
method := mALL
|
||||
subroutes, _ := handler.(Routes)
|
||||
if subroutes != nil {
|
||||
method |= mSTUB
|
||||
}
|
||||
n := mx.handle(method, pattern+"*", mountHandler)
|
||||
|
||||
if subroutes != nil {
|
||||
n.subroutes = subroutes
|
||||
}
|
||||
}
|
||||
|
||||
// Routes returns a slice of routing information from the tree,
|
||||
// useful for traversing available routes of a router.
|
||||
func (mx *Mux) Routes() []Route {
|
||||
return mx.tree.routes()
|
||||
}
|
||||
|
||||
// Middlewares returns a slice of middleware handler functions.
|
||||
func (mx *Mux) Middlewares() Middlewares {
|
||||
return mx.middlewares
|
||||
}
|
||||
|
||||
// Match searches the routing tree for a handler that matches the method/path.
|
||||
// It's similar to routing a http request, but without executing the handler
|
||||
// thereafter.
|
||||
//
|
||||
// Note: the *Context state is updated during execution, so manage
|
||||
// the state carefully or make a NewRouteContext().
|
||||
func (mx *Mux) Match(rctx *Context, method, path string) bool {
|
||||
m, ok := methodMap[method]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
node, _, h := mx.tree.FindRoute(rctx, m, path)
|
||||
|
||||
if node != nil && node.subroutes != nil {
|
||||
rctx.RoutePath = mx.nextRoutePath(rctx)
|
||||
return node.subroutes.Match(rctx, method, rctx.RoutePath)
|
||||
}
|
||||
|
||||
return h != nil
|
||||
}
|
||||
|
||||
// NotFoundHandler returns the default Mux 404 responder whenever a route
|
||||
// cannot be found.
|
||||
func (mx *Mux) NotFoundHandler() http.HandlerFunc {
|
||||
if mx.notFoundHandler != nil {
|
||||
return mx.notFoundHandler
|
||||
}
|
||||
return http.NotFound
|
||||
}
|
||||
|
||||
// MethodNotAllowedHandler returns the default Mux 405 responder whenever
|
||||
// a method cannot be resolved for a route.
|
||||
func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc {
|
||||
if mx.methodNotAllowedHandler != nil {
|
||||
return mx.methodNotAllowedHandler
|
||||
}
|
||||
return methodNotAllowedHandler
|
||||
}
|
||||
|
||||
// buildRouteHandler builds the single mux handler that is a chain of the middleware
|
||||
// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this
|
||||
// point, no other middlewares can be registered on this Mux's stack. But you can still
|
||||
// compose additional middlewares via Group()'s or using a chained middleware handler.
|
||||
func (mx *Mux) buildRouteHandler() {
|
||||
mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP))
|
||||
}
|
||||
|
||||
// handle registers a http.Handler in the routing tree for a particular http method
|
||||
// and routing pattern.
|
||||
func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node {
|
||||
if len(pattern) == 0 || pattern[0] != '/' {
|
||||
panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern))
|
||||
}
|
||||
|
||||
// Build the final routing handler for this Mux.
|
||||
if !mx.inline && mx.handler == nil {
|
||||
mx.buildRouteHandler()
|
||||
}
|
||||
|
||||
// Build endpoint handler with inline middlewares for the route
|
||||
var h http.Handler
|
||||
if mx.inline {
|
||||
mx.handler = http.HandlerFunc(mx.routeHTTP)
|
||||
h = Chain(mx.middlewares...).Handler(handler)
|
||||
} else {
|
||||
h = handler
|
||||
}
|
||||
|
||||
// Add the endpoint to the tree and return the node
|
||||
return mx.tree.InsertRoute(method, pattern, h)
|
||||
}
|
||||
|
||||
// routeHTTP routes a http.Request through the Mux routing tree to serve
|
||||
// the matching handler for a particular http method.
|
||||
func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Grab the route context object
|
||||
rctx := r.Context().Value(RouteCtxKey).(*Context)
|
||||
|
||||
// The request routing path
|
||||
routePath := rctx.RoutePath
|
||||
if routePath == "" {
|
||||
if r.URL.RawPath != "" {
|
||||
routePath = r.URL.RawPath
|
||||
} else {
|
||||
routePath = r.URL.Path
|
||||
}
|
||||
}
|
||||
|
||||
// Check if method is supported by chi
|
||||
if rctx.RouteMethod == "" {
|
||||
rctx.RouteMethod = r.Method
|
||||
}
|
||||
method, ok := methodMap[rctx.RouteMethod]
|
||||
if !ok {
|
||||
mx.MethodNotAllowedHandler().ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Find the route
|
||||
if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if rctx.methodNotAllowed {
|
||||
mx.MethodNotAllowedHandler().ServeHTTP(w, r)
|
||||
} else {
|
||||
mx.NotFoundHandler().ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (mx *Mux) nextRoutePath(rctx *Context) string {
|
||||
routePath := "/"
|
||||
nx := len(rctx.routeParams.Keys) - 1 // index of last param in list
|
||||
if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx {
|
||||
routePath += rctx.routeParams.Values[nx]
|
||||
}
|
||||
return routePath
|
||||
}
|
||||
|
||||
// Recursively update data on child routers.
|
||||
func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) {
|
||||
for _, r := range mx.tree.routes() {
|
||||
subMux, ok := r.SubRoutes.(*Mux)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fn(subMux)
|
||||
}
|
||||
}
|
||||
|
||||
// methodNotAllowedHandler is a helper function to respond with a 405,
|
||||
// method not allowed.
|
||||
func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(405)
|
||||
w.Write(nil)
|
||||
}
|
|
@ -0,0 +1,847 @@
|
|||
package chi
|
||||
|
||||
// Radix tree implementation below is a based on the original work by
|
||||
// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go
|
||||
// (MIT licensed). It's been heavily modified for use as a HTTP routing tree.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type methodTyp int
|
||||
|
||||
const (
|
||||
mSTUB methodTyp = 1 << iota
|
||||
mCONNECT
|
||||
mDELETE
|
||||
mGET
|
||||
mHEAD
|
||||
mOPTIONS
|
||||
mPATCH
|
||||
mPOST
|
||||
mPUT
|
||||
mTRACE
|
||||
)
|
||||
|
||||
var mALL = mCONNECT | mDELETE | mGET | mHEAD |
|
||||
mOPTIONS | mPATCH | mPOST | mPUT | mTRACE
|
||||
|
||||
var methodMap = map[string]methodTyp{
|
||||
http.MethodConnect: mCONNECT,
|
||||
http.MethodDelete: mDELETE,
|
||||
http.MethodGet: mGET,
|
||||
http.MethodHead: mHEAD,
|
||||
http.MethodOptions: mOPTIONS,
|
||||
http.MethodPatch: mPATCH,
|
||||
http.MethodPost: mPOST,
|
||||
http.MethodPut: mPUT,
|
||||
http.MethodTrace: mTRACE,
|
||||
}
|
||||
|
||||
// RegisterMethod adds support for custom HTTP method handlers, available
|
||||
// via Router#Method and Router#MethodFunc
|
||||
func RegisterMethod(method string) {
|
||||
if method == "" {
|
||||
return
|
||||
}
|
||||
method = strings.ToUpper(method)
|
||||
if _, ok := methodMap[method]; ok {
|
||||
return
|
||||
}
|
||||
n := len(methodMap)
|
||||
if n > strconv.IntSize {
|
||||
panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize))
|
||||
}
|
||||
mt := methodTyp(math.Exp2(float64(n)))
|
||||
methodMap[method] = mt
|
||||
mALL |= mt
|
||||
}
|
||||
|
||||
type nodeTyp uint8
|
||||
|
||||
const (
|
||||
ntStatic nodeTyp = iota // /home
|
||||
ntRegexp // /{id:[0-9]+}
|
||||
ntParam // /{user}
|
||||
ntCatchAll // /api/v1/*
|
||||
)
|
||||
|
||||
type node struct {
|
||||
// node type: static, regexp, param, catchAll
|
||||
typ nodeTyp
|
||||
|
||||
// first byte of the prefix
|
||||
label byte
|
||||
|
||||
// first byte of the child prefix
|
||||
tail byte
|
||||
|
||||
// prefix is the common prefix we ignore
|
||||
prefix string
|
||||
|
||||
// regexp matcher for regexp nodes
|
||||
rex *regexp.Regexp
|
||||
|
||||
// HTTP handler endpoints on the leaf node
|
||||
endpoints endpoints
|
||||
|
||||
// subroutes on the leaf node
|
||||
subroutes Routes
|
||||
|
||||
// child nodes should be stored in-order for iteration,
|
||||
// in groups of the node type.
|
||||
children [ntCatchAll + 1]nodes
|
||||
}
|
||||
|
||||
// endpoints is a mapping of http method constants to handlers
|
||||
// for a given route.
|
||||
type endpoints map[methodTyp]*endpoint
|
||||
|
||||
type endpoint struct {
|
||||
// endpoint handler
|
||||
handler http.Handler
|
||||
|
||||
// pattern is the routing pattern for handler nodes
|
||||
pattern string
|
||||
|
||||
// parameter keys recorded on handler nodes
|
||||
paramKeys []string
|
||||
}
|
||||
|
||||
func (s endpoints) Value(method methodTyp) *endpoint {
|
||||
mh, ok := s[method]
|
||||
if !ok {
|
||||
mh = &endpoint{}
|
||||
s[method] = mh
|
||||
}
|
||||
return mh
|
||||
}
|
||||
|
||||
func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node {
|
||||
var parent *node
|
||||
search := pattern
|
||||
|
||||
for {
|
||||
// Handle key exhaustion
|
||||
if len(search) == 0 {
|
||||
// Insert or update the node's leaf handler
|
||||
n.setEndpoint(method, handler, pattern)
|
||||
return n
|
||||
}
|
||||
|
||||
// We're going to be searching for a wild node next,
|
||||
// in this case, we need to get the tail
|
||||
var label = search[0]
|
||||
var segTail byte
|
||||
var segEndIdx int
|
||||
var segTyp nodeTyp
|
||||
var segRexpat string
|
||||
if label == '{' || label == '*' {
|
||||
segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search)
|
||||
}
|
||||
|
||||
var prefix string
|
||||
if segTyp == ntRegexp {
|
||||
prefix = segRexpat
|
||||
}
|
||||
|
||||
// Look for the edge to attach to
|
||||
parent = n
|
||||
n = n.getEdge(segTyp, label, segTail, prefix)
|
||||
|
||||
// No edge, create one
|
||||
if n == nil {
|
||||
child := &node{label: label, tail: segTail, prefix: search}
|
||||
hn := parent.addChild(child, search)
|
||||
hn.setEndpoint(method, handler, pattern)
|
||||
|
||||
return hn
|
||||
}
|
||||
|
||||
// Found an edge to match the pattern
|
||||
|
||||
if n.typ > ntStatic {
|
||||
// We found a param node, trim the param from the search path and continue.
|
||||
// This param/wild pattern segment would already be on the tree from a previous
|
||||
// call to addChild when creating a new node.
|
||||
search = search[segEndIdx:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Static nodes fall below here.
|
||||
// Determine longest prefix of the search key on match.
|
||||
commonPrefix := longestPrefix(search, n.prefix)
|
||||
if commonPrefix == len(n.prefix) {
|
||||
// the common prefix is as long as the current node's prefix we're attempting to insert.
|
||||
// keep the search going.
|
||||
search = search[commonPrefix:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Split the node
|
||||
child := &node{
|
||||
typ: ntStatic,
|
||||
prefix: search[:commonPrefix],
|
||||
}
|
||||
parent.replaceChild(search[0], segTail, child)
|
||||
|
||||
// Restore the existing node
|
||||
n.label = n.prefix[commonPrefix]
|
||||
n.prefix = n.prefix[commonPrefix:]
|
||||
child.addChild(n, n.prefix)
|
||||
|
||||
// If the new key is a subset, set the method/handler on this node and finish.
|
||||
search = search[commonPrefix:]
|
||||
if len(search) == 0 {
|
||||
child.setEndpoint(method, handler, pattern)
|
||||
return child
|
||||
}
|
||||
|
||||
// Create a new edge for the node
|
||||
subchild := &node{
|
||||
typ: ntStatic,
|
||||
label: search[0],
|
||||
prefix: search,
|
||||
}
|
||||
hn := child.addChild(subchild, search)
|
||||
hn.setEndpoint(method, handler, pattern)
|
||||
return hn
|
||||
}
|
||||
}
|
||||
|
||||
// addChild appends the new `child` node to the tree using the `pattern` as the trie key.
|
||||
// For a URL router like chi's, we split the static, param, regexp and wildcard segments
|
||||
// into different nodes. In addition, addChild will recursively call itself until every
|
||||
// pattern segment is added to the url pattern tree as individual nodes, depending on type.
|
||||
func (n *node) addChild(child *node, prefix string) *node {
|
||||
search := prefix
|
||||
|
||||
// handler leaf node added to the tree is the child.
|
||||
// this may be overridden later down the flow
|
||||
hn := child
|
||||
|
||||
// Parse next segment
|
||||
segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search)
|
||||
|
||||
// Add child depending on next up segment
|
||||
switch segTyp {
|
||||
|
||||
case ntStatic:
|
||||
// Search prefix is all static (that is, has no params in path)
|
||||
// noop
|
||||
|
||||
default:
|
||||
// Search prefix contains a param, regexp or wildcard
|
||||
|
||||
if segTyp == ntRegexp {
|
||||
rex, err := regexp.Compile(segRexpat)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat))
|
||||
}
|
||||
child.prefix = segRexpat
|
||||
child.rex = rex
|
||||
}
|
||||
|
||||
if segStartIdx == 0 {
|
||||
// Route starts with a param
|
||||
child.typ = segTyp
|
||||
|
||||
if segTyp == ntCatchAll {
|
||||
segStartIdx = -1
|
||||
} else {
|
||||
segStartIdx = segEndIdx
|
||||
}
|
||||
if segStartIdx < 0 {
|
||||
segStartIdx = len(search)
|
||||
}
|
||||
child.tail = segTail // for params, we set the tail
|
||||
|
||||
if segStartIdx != len(search) {
|
||||
// add static edge for the remaining part, split the end.
|
||||
// its not possible to have adjacent param nodes, so its certainly
|
||||
// going to be a static node next.
|
||||
|
||||
search = search[segStartIdx:] // advance search position
|
||||
|
||||
nn := &node{
|
||||
typ: ntStatic,
|
||||
label: search[0],
|
||||
prefix: search,
|
||||
}
|
||||
hn = child.addChild(nn, search)
|
||||
}
|
||||
|
||||
} else if segStartIdx > 0 {
|
||||
// Route has some param
|
||||
|
||||
// starts with a static segment
|
||||
child.typ = ntStatic
|
||||
child.prefix = search[:segStartIdx]
|
||||
child.rex = nil
|
||||
|
||||
// add the param edge node
|
||||
search = search[segStartIdx:]
|
||||
|
||||
nn := &node{
|
||||
typ: segTyp,
|
||||
label: search[0],
|
||||
tail: segTail,
|
||||
}
|
||||
hn = child.addChild(nn, search)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
n.children[child.typ] = append(n.children[child.typ], child)
|
||||
n.children[child.typ].Sort()
|
||||
return hn
|
||||
}
|
||||
|
||||
func (n *node) replaceChild(label, tail byte, child *node) {
|
||||
for i := 0; i < len(n.children[child.typ]); i++ {
|
||||
if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail {
|
||||
n.children[child.typ][i] = child
|
||||
n.children[child.typ][i].label = label
|
||||
n.children[child.typ][i].tail = tail
|
||||
return
|
||||
}
|
||||
}
|
||||
panic("chi: replacing missing child")
|
||||
}
|
||||
|
||||
func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node {
|
||||
nds := n.children[ntyp]
|
||||
for i := 0; i < len(nds); i++ {
|
||||
if nds[i].label == label && nds[i].tail == tail {
|
||||
if ntyp == ntRegexp && nds[i].prefix != prefix {
|
||||
continue
|
||||
}
|
||||
return nds[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) {
|
||||
// Set the handler for the method type on the node
|
||||
if n.endpoints == nil {
|
||||
n.endpoints = make(endpoints, 0)
|
||||
}
|
||||
|
||||
paramKeys := patParamKeys(pattern)
|
||||
|
||||
if method&mSTUB == mSTUB {
|
||||
n.endpoints.Value(mSTUB).handler = handler
|
||||
}
|
||||
if method&mALL == mALL {
|
||||
h := n.endpoints.Value(mALL)
|
||||
h.handler = handler
|
||||
h.pattern = pattern
|
||||
h.paramKeys = paramKeys
|
||||
for _, m := range methodMap {
|
||||
h := n.endpoints.Value(m)
|
||||
h.handler = handler
|
||||
h.pattern = pattern
|
||||
h.paramKeys = paramKeys
|
||||
}
|
||||
} else {
|
||||
h := n.endpoints.Value(method)
|
||||
h.handler = handler
|
||||
h.pattern = pattern
|
||||
h.paramKeys = paramKeys
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) {
|
||||
// Reset the context routing pattern and params
|
||||
rctx.routePattern = ""
|
||||
rctx.routeParams.Keys = rctx.routeParams.Keys[:0]
|
||||
rctx.routeParams.Values = rctx.routeParams.Values[:0]
|
||||
|
||||
// Find the routing handlers for the path
|
||||
rn := n.findRoute(rctx, method, path)
|
||||
if rn == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Record the routing params in the request lifecycle
|
||||
rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...)
|
||||
rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...)
|
||||
|
||||
// Record the routing pattern in the request lifecycle
|
||||
if rn.endpoints[method].pattern != "" {
|
||||
rctx.routePattern = rn.endpoints[method].pattern
|
||||
rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern)
|
||||
}
|
||||
|
||||
return rn, rn.endpoints, rn.endpoints[method].handler
|
||||
}
|
||||
|
||||
// Recursive edge traversal by checking all nodeTyp groups along the way.
|
||||
// It's like searching through a multi-dimensional radix trie.
|
||||
func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
|
||||
nn := n
|
||||
search := path
|
||||
|
||||
for t, nds := range nn.children {
|
||||
ntyp := nodeTyp(t)
|
||||
if len(nds) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var xn *node
|
||||
xsearch := search
|
||||
|
||||
var label byte
|
||||
if search != "" {
|
||||
label = search[0]
|
||||
}
|
||||
|
||||
switch ntyp {
|
||||
case ntStatic:
|
||||
xn = nds.findEdge(label)
|
||||
if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) {
|
||||
continue
|
||||
}
|
||||
xsearch = xsearch[len(xn.prefix):]
|
||||
|
||||
case ntParam, ntRegexp:
|
||||
// short-circuit and return no matching route for empty param values
|
||||
if xsearch == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// serially loop through each node grouped by the tail delimiter
|
||||
for idx := 0; idx < len(nds); idx++ {
|
||||
xn = nds[idx]
|
||||
|
||||
// label for param nodes is the delimiter byte
|
||||
p := strings.IndexByte(xsearch, xn.tail)
|
||||
|
||||
if p < 0 {
|
||||
if xn.tail == '/' {
|
||||
p = len(xsearch)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if ntyp == ntRegexp && xn.rex != nil {
|
||||
if xn.rex.Match([]byte(xsearch[:p])) == false {
|
||||
continue
|
||||
}
|
||||
} else if strings.IndexByte(xsearch[:p], '/') != -1 {
|
||||
// avoid a match across path segments
|
||||
continue
|
||||
}
|
||||
|
||||
rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p])
|
||||
xsearch = xsearch[p:]
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
// catch-all nodes
|
||||
rctx.routeParams.Values = append(rctx.routeParams.Values, search)
|
||||
xn = nds[0]
|
||||
xsearch = ""
|
||||
}
|
||||
|
||||
if xn == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// did we find it yet?
|
||||
if len(xsearch) == 0 {
|
||||
if xn.isLeaf() {
|
||||
h, _ := xn.endpoints[method]
|
||||
if h != nil && h.handler != nil {
|
||||
rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...)
|
||||
return xn
|
||||
}
|
||||
|
||||
// flag that the routing context found a route, but not a corresponding
|
||||
// supported method
|
||||
rctx.methodNotAllowed = true
|
||||
}
|
||||
}
|
||||
|
||||
// recursively find the next node..
|
||||
fin := xn.findRoute(rctx, method, xsearch)
|
||||
if fin != nil {
|
||||
return fin
|
||||
}
|
||||
|
||||
// Did not find final handler, let's remove the param here if it was set
|
||||
if xn.typ > ntStatic {
|
||||
if len(rctx.routeParams.Values) > 0 {
|
||||
rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *node) findEdge(ntyp nodeTyp, label byte) *node {
|
||||
nds := n.children[ntyp]
|
||||
num := len(nds)
|
||||
idx := 0
|
||||
|
||||
switch ntyp {
|
||||
case ntStatic, ntParam, ntRegexp:
|
||||
i, j := 0, num-1
|
||||
for i <= j {
|
||||
idx = i + (j-i)/2
|
||||
if label > nds[idx].label {
|
||||
i = idx + 1
|
||||
} else if label < nds[idx].label {
|
||||
j = idx - 1
|
||||
} else {
|
||||
i = num // breaks cond
|
||||
}
|
||||
}
|
||||
if nds[idx].label != label {
|
||||
return nil
|
||||
}
|
||||
return nds[idx]
|
||||
|
||||
default: // catch all
|
||||
return nds[idx]
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) isEmpty() bool {
|
||||
for _, nds := range n.children {
|
||||
if len(nds) > 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *node) isLeaf() bool {
|
||||
return n.endpoints != nil
|
||||
}
|
||||
|
||||
func (n *node) findPattern(pattern string) bool {
|
||||
nn := n
|
||||
for _, nds := range nn.children {
|
||||
if len(nds) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
n = nn.findEdge(nds[0].typ, pattern[0])
|
||||
if n == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var idx int
|
||||
var xpattern string
|
||||
|
||||
switch n.typ {
|
||||
case ntStatic:
|
||||
idx = longestPrefix(pattern, n.prefix)
|
||||
if idx < len(n.prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
case ntParam, ntRegexp:
|
||||
idx = strings.IndexByte(pattern, '}') + 1
|
||||
|
||||
case ntCatchAll:
|
||||
idx = longestPrefix(pattern, "*")
|
||||
|
||||
default:
|
||||
panic("chi: unknown node type")
|
||||
}
|
||||
|
||||
xpattern = pattern[idx:]
|
||||
if len(xpattern) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return n.findPattern(xpattern)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *node) routes() []Route {
|
||||
rts := []Route{}
|
||||
|
||||
n.walk(func(eps endpoints, subroutes Routes) bool {
|
||||
if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Group methodHandlers by unique patterns
|
||||
pats := make(map[string]endpoints, 0)
|
||||
|
||||
for mt, h := range eps {
|
||||
if h.pattern == "" {
|
||||
continue
|
||||
}
|
||||
p, ok := pats[h.pattern]
|
||||
if !ok {
|
||||
p = endpoints{}
|
||||
pats[h.pattern] = p
|
||||
}
|
||||
p[mt] = h
|
||||
}
|
||||
|
||||
for p, mh := range pats {
|
||||
hs := make(map[string]http.Handler, 0)
|
||||
if mh[mALL] != nil && mh[mALL].handler != nil {
|
||||
hs["*"] = mh[mALL].handler
|
||||
}
|
||||
|
||||
for mt, h := range mh {
|
||||
if h.handler == nil {
|
||||
continue
|
||||
}
|
||||
m := methodTypString(mt)
|
||||
if m == "" {
|
||||
continue
|
||||
}
|
||||
hs[m] = h.handler
|
||||
}
|
||||
|
||||
rt := Route{p, hs, subroutes}
|
||||
rts = append(rts, rt)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return rts
|
||||
}
|
||||
|
||||
func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool {
|
||||
// Visit the leaf values if any
|
||||
if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Recurse on the children
|
||||
for _, ns := range n.children {
|
||||
for _, cn := range ns {
|
||||
if cn.walk(fn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// patNextSegment returns the next segment details from a pattern:
|
||||
// node type, param key, regexp string, param tail byte, param starting index, param ending index
|
||||
func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) {
|
||||
ps := strings.Index(pattern, "{")
|
||||
ws := strings.Index(pattern, "*")
|
||||
|
||||
if ps < 0 && ws < 0 {
|
||||
return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
if ws >= 0 && ws != len(pattern)-1 {
|
||||
panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead")
|
||||
}
|
||||
if ps >= 0 && ws >= 0 && ws < ps {
|
||||
panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'")
|
||||
}
|
||||
|
||||
var tail byte = '/' // Default endpoint tail to / byte
|
||||
|
||||
if ps >= 0 {
|
||||
// Param/Regexp pattern is next
|
||||
nt := ntParam
|
||||
|
||||
// Read to closing } taking into account opens and closes in curl count (cc)
|
||||
cc := 0
|
||||
pe := ps
|
||||
for i, c := range pattern[ps:] {
|
||||
if c == '{' {
|
||||
cc++
|
||||
} else if c == '}' {
|
||||
cc--
|
||||
if cc == 0 {
|
||||
pe = ps + i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if pe == ps {
|
||||
panic("chi: route param closing delimiter '}' is missing")
|
||||
}
|
||||
|
||||
key := pattern[ps+1 : pe]
|
||||
pe++ // set end to next position
|
||||
|
||||
if pe < len(pattern) {
|
||||
tail = pattern[pe]
|
||||
}
|
||||
|
||||
var rexpat string
|
||||
if idx := strings.Index(key, ":"); idx >= 0 {
|
||||
nt = ntRegexp
|
||||
rexpat = key[idx+1:]
|
||||
key = key[:idx]
|
||||
}
|
||||
|
||||
if len(rexpat) > 0 {
|
||||
if rexpat[0] != '^' {
|
||||
rexpat = "^" + rexpat
|
||||
}
|
||||
if rexpat[len(rexpat)-1] != '$' {
|
||||
rexpat = rexpat + "$"
|
||||
}
|
||||
}
|
||||
|
||||
return nt, key, rexpat, tail, ps, pe
|
||||
}
|
||||
|
||||
// Wildcard pattern as finale
|
||||
// TODO: should we panic if there is stuff after the * ???
|
||||
return ntCatchAll, "*", "", 0, ws, len(pattern)
|
||||
}
|
||||
|
||||
func patParamKeys(pattern string) []string {
|
||||
pat := pattern
|
||||
paramKeys := []string{}
|
||||
for {
|
||||
ptyp, paramKey, _, _, _, e := patNextSegment(pat)
|
||||
if ptyp == ntStatic {
|
||||
return paramKeys
|
||||
}
|
||||
for i := 0; i < len(paramKeys); i++ {
|
||||
if paramKeys[i] == paramKey {
|
||||
panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey))
|
||||
}
|
||||
}
|
||||
paramKeys = append(paramKeys, paramKey)
|
||||
pat = pat[e:]
|
||||
}
|
||||
}
|
||||
|
||||
// longestPrefix finds the length of the shared prefix
|
||||
// of two strings
|
||||
func longestPrefix(k1, k2 string) int {
|
||||
max := len(k1)
|
||||
if l := len(k2); l < max {
|
||||
max = l
|
||||
}
|
||||
var i int
|
||||
for i = 0; i < max; i++ {
|
||||
if k1[i] != k2[i] {
|
||||
break
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func methodTypString(method methodTyp) string {
|
||||
for s, t := range methodMap {
|
||||
if method == t {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type nodes []*node
|
||||
|
||||
// Sort the list of nodes by label
|
||||
func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() }
|
||||
func (ns nodes) Len() int { return len(ns) }
|
||||
func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] }
|
||||
func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label }
|
||||
|
||||
// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes.
|
||||
// The list order determines the traversal order.
|
||||
func (ns nodes) tailSort() {
|
||||
for i := len(ns) - 1; i >= 0; i-- {
|
||||
if ns[i].typ > ntStatic && ns[i].tail == '/' {
|
||||
ns.Swap(i, len(ns)-1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ns nodes) findEdge(label byte) *node {
|
||||
num := len(ns)
|
||||
idx := 0
|
||||
i, j := 0, num-1
|
||||
for i <= j {
|
||||
idx = i + (j-i)/2
|
||||
if label > ns[idx].label {
|
||||
i = idx + 1
|
||||
} else if label < ns[idx].label {
|
||||
j = idx - 1
|
||||
} else {
|
||||
i = num // breaks cond
|
||||
}
|
||||
}
|
||||
if ns[idx].label != label {
|
||||
return nil
|
||||
}
|
||||
return ns[idx]
|
||||
}
|
||||
|
||||
// Route describes the details of a routing handler.
|
||||
type Route struct {
|
||||
Pattern string
|
||||
Handlers map[string]http.Handler
|
||||
SubRoutes Routes
|
||||
}
|
||||
|
||||
// WalkFunc is the type of the function called for each method and route visited by Walk.
|
||||
type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error
|
||||
|
||||
// Walk walks any router tree that implements Routes interface.
|
||||
func Walk(r Routes, walkFn WalkFunc) error {
|
||||
return walk(r, walkFn, "")
|
||||
}
|
||||
|
||||
func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error {
|
||||
for _, route := range r.Routes() {
|
||||
mws := make([]func(http.Handler) http.Handler, len(parentMw))
|
||||
copy(mws, parentMw)
|
||||
mws = append(mws, r.Middlewares()...)
|
||||
|
||||
if route.SubRoutes != nil {
|
||||
if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for method, handler := range route.Handlers {
|
||||
if method == "*" {
|
||||
// Ignore a "catchAll" method, since we pass down all the specific methods for each route.
|
||||
continue
|
||||
}
|
||||
|
||||
fullRoute := parentRoute + route.Pattern
|
||||
|
||||
if chain, ok := handler.(*ChainHandler); ok {
|
||||
if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := walkFn(method, fullRoute, handler, mws...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.DS_Store
|
||||
.idea/
|
||||
cmd/mailgun/mailgun
|
|
@ -0,0 +1,7 @@
|
|||
language: go
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
go:
|
||||
- 1.11.x
|
|
@ -0,0 +1,175 @@
|
|||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.6.3] - 2019-12-03
|
||||
### Changes
|
||||
* Calls to get stats now use epoch as the time format
|
||||
|
||||
## [3.6.2] - 2019-11-18
|
||||
### Added
|
||||
* Added `AddTemplateVariable()` to make adding variables to templates
|
||||
less confusing and error prone.
|
||||
|
||||
## [3.6.1] - 2019-10-24
|
||||
### Added
|
||||
* Added `VerifyWebhookSignature()` to mailgun interface
|
||||
|
||||
## [3.6.1-rc.3] - 2019-07-16
|
||||
### Added
|
||||
* APIBaseEU and APIBaseUS to help customers change regions
|
||||
* Documented how to change regions in the README
|
||||
|
||||
## [3.6.1-rc.2] - 2019-07-01
|
||||
### Changes
|
||||
* Fix the JSON response for `GetMember()`
|
||||
* Typo in format string in max number of tags error
|
||||
|
||||
## [3.6.0] - 2019-06-26
|
||||
### Added
|
||||
* Added UpdateClickTracking() to modify click tracking for a domain
|
||||
* Added UpdateUnsubscribeTracking() to modify unsubscribe tracking for a domain
|
||||
* Added UpdateOpenTracking() to modify open tracking for a domain
|
||||
|
||||
## [3.5.0] - 2019-05-21
|
||||
### Added
|
||||
* Added notice in README about go dep bug.
|
||||
* Added endpoints for webhooks in mock server
|
||||
### Changes
|
||||
* Change names of some parameters on public methods to make their use clearer.
|
||||
* Changed signature of `GetWebhook()` now returns []string.
|
||||
* Changed signature of `ListWebhooks()` now returns map[string][]string.
|
||||
* Both `GetWebhooks()` and `ListWebhooks()` now handle new and legacy webhooks properly.
|
||||
|
||||
## [3.4.0] - 2019-04-23
|
||||
### Added
|
||||
* Added `Message.SetTemplate()` to allow sending with the body of a template.
|
||||
### Changes
|
||||
* Changed signature of `CreateDomain()` moved password into `CreateDomainOptions`
|
||||
|
||||
## [3.4.0] - 2019-04-23
|
||||
### Added
|
||||
* Added `Message.SetTemplate()` to allow sending with the body of a template.
|
||||
### Changes
|
||||
* Changed signature of `CreateDomain()` moved password into `CreateDomainOptions`
|
||||
|
||||
## [3.3.2] - 2019-03-28
|
||||
### Changes
|
||||
* Uncommented DeliveryStatus.Code and change it to an integer (See #175)
|
||||
* Added UserVariables to all Message events (See #176)
|
||||
|
||||
## [3.3.1] - 2019-03-13
|
||||
### Changes
|
||||
* Updated Template calls to reflect the most recent Template API changes.
|
||||
* GetStoredMessage() now accepts a URL instead of an id
|
||||
* Deprecated GetStoredMessageForURL()
|
||||
* Deprecated GetStoredMessageRawForURL()
|
||||
* Fixed GetUnsubscribed()
|
||||
|
||||
### Added
|
||||
* Added `GetStoredAttachment()`
|
||||
|
||||
### Removed
|
||||
* Method `DeleteStoredMessage()` mailgun API no long allows this call
|
||||
|
||||
## [3.3.0] - 2019-01-28
|
||||
### Changes
|
||||
* Changed signature of CreateDomain() Now returns JSON response
|
||||
* Changed signature of GetDomain() Now returns a single DomainResponse
|
||||
* Clarified installation notes for non golang module users
|
||||
* Changed 'Public Key' to 'Public Validation Key' in readme
|
||||
* Fixed issue with Next() for limit/skip based iterators
|
||||
|
||||
### Added
|
||||
* Added VerifyDomain()
|
||||
|
||||
## [3.2.0] - 2019-01-21
|
||||
### Changes
|
||||
* Deprecated mg.VerifyWebhookRequest()
|
||||
|
||||
### Added
|
||||
* Added mailgun.ParseEvent()
|
||||
* Added mailgun.ParseEvents()
|
||||
* Added mg.VerifyWebhookSignature()
|
||||
|
||||
|
||||
## [3.1.0] - 2019-01-16
|
||||
### Changes
|
||||
* Removed context.Context from ListDomains() signature
|
||||
* ListEventOptions.Begin and End are no longer pointers to time.Time
|
||||
|
||||
### Added
|
||||
* Added mg.ReSend() to public Mailgun interface
|
||||
* Added Message.SetSkipVerification()
|
||||
* Added Message.SetRequireTLS()
|
||||
|
||||
## [3.0.0] - 2019-01-15
|
||||
### Added
|
||||
* Added CHANGELOG
|
||||
* Added `AddDomainIP()`
|
||||
* Added `ListDomainIPS()`
|
||||
* Added `DeleteDomainIP()`
|
||||
* Added `ListIPS()`
|
||||
* Added `GetIP()`
|
||||
* Added `GetDomainTracking()`
|
||||
* Added `GetDomainConnection()`
|
||||
* Added `UpdateDomainConnection()`
|
||||
* Added `CreateExport()`
|
||||
* Added `ListExports()`
|
||||
* Added `GetExports()`
|
||||
* Added `GetExportLink()`
|
||||
* Added `CreateTemplate()`
|
||||
* Added `GetTemplate()`
|
||||
* Added `UpdateTemplate()`
|
||||
* Added `DeleteTemplate()`
|
||||
* Added `ListTemplates()`
|
||||
* Added `AddTemplateVersion()`
|
||||
* Added `GetTemplateVersion()`
|
||||
* Added `UpdateTemplateVersion()`
|
||||
* Added `DeleteTemplateVersion()`
|
||||
* Added `ListTemplateVersions()`
|
||||
|
||||
### Changed
|
||||
* Added a `mailgun.MockServer` which duplicates part of the mailgun API; suitable for testing
|
||||
* `ListMailingLists()` now uses the `/pages` API and returns an iterator
|
||||
* `ListMembers()` now uses the `/pages` API and returns an iterator
|
||||
* Renamed public interface methods to be consistent. IE: `GetThing(), ListThing(), CreateThing()`
|
||||
* Moved event objects into the `mailgun/events` package, so names like
|
||||
`MailingList` returned by API calls and `MailingList` as an event object
|
||||
don't conflict and confuse users.
|
||||
* Now using context.Context for all network operations
|
||||
* Test suite will run without MG_ env vars defined
|
||||
* ListRoutes() now uses the iterator interface
|
||||
* Added SkipNetworkTest()
|
||||
* Renamed GetStatsTotals() to GetStats()
|
||||
* Renamed GetUnsubscribes to ListUnsubscribes()
|
||||
* Renamed Unsubscribe() to CreateUnsubscribe()
|
||||
* Renamed RemoveUnsubscribe() to DeleteUnsubscribe()
|
||||
* GetStats() now takes an `*opt` argument to pass optional parameters
|
||||
* Modified GetUnsubscribe() to follow the API
|
||||
* Now using golang modules
|
||||
* ListCredentials() now returns an iterator
|
||||
* ListUnsubscribes() now returns an paging iterator
|
||||
* CreateDomain now accepts CreateDomainOption{}
|
||||
* CreateDomain() now supports all optional parameters not just spam_action and wildcard.
|
||||
* ListComplaints() now returns a page iterator
|
||||
* Renamed `TagItem` to `Tag`
|
||||
* ListBounces() now returns a page iterator
|
||||
* API responses with CreatedAt fields are now unmarshalled into RFC2822
|
||||
* DomainList() now returns an iterator
|
||||
* Updated godoc documentation
|
||||
* Renamed ApiBase to APIBase
|
||||
* Updated copyright to 2019
|
||||
* `ListEvents()` now returns a list of typed events
|
||||
|
||||
### Removed
|
||||
* Removed more deprecated types
|
||||
* Removed gobuffalo/envy dependency
|
||||
* Remove mention of the CLI in the README
|
||||
* Removed mailgun cli from project
|
||||
* Removed GetCode() from `Bounce` struct. Verified API returns 'string' and not 'int'
|
||||
* Removed deprecated methods NewMessage and NewMIMEMessage
|
||||
* Removed ginkgo and gomega tests
|
||||
* Removed GetStats() As the /stats endpoint is depreciated
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2013-2016, 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.
|
|
@ -0,0 +1,21 @@
|
|||
.PHONY: all
|
||||
.DEFAULT_GOAL := all
|
||||
|
||||
PACKAGE := github.com/mailgun/mailgun-go
|
||||
|
||||
gen:
|
||||
rm events/events_easyjson.go
|
||||
easyjson --all events/events.go
|
||||
rm events/objects_easyjson.go
|
||||
easyjson --all events/objects.go
|
||||
|
||||
all:
|
||||
export GO111MODULE=on; go test . -v
|
||||
|
||||
godoc:
|
||||
mkdir -p /tmp/tmpgoroot/doc
|
||||
-rm -rf /tmp/tmpgopath/src/${PACKAGE}
|
||||
mkdir -p /tmp/tmpgopath/src/${PACKAGE}
|
||||
tar -c --exclude='.git' --exclude='tmp' . | tar -x -C /tmp/tmpgopath/src/${PACKAGE}
|
||||
echo -e "open http://localhost:6060/pkg/${PACKAGE}\n"
|
||||
GOROOT=/tmp/tmpgoroot/ GOPATH=/tmp/tmpgopath/ godoc -http=localhost:6060
|
|
@ -0,0 +1,364 @@
|
|||
# Mailgun with Go
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/mailgun/mailgun-go?status.svg)](https://godoc.org/github.com/mailgun/mailgun-go)
|
||||
[![Build Status](https://img.shields.io/travis/mailgun/mailgun-go/master.svg)](https://travis-ci.org/mailgun/mailgun-go)
|
||||
|
||||
Go library for interacting with the [Mailgun](https://mailgun.com/) [API](https://documentation.mailgun.com/api_reference.html).
|
||||
|
||||
**NOTE: Backward compatibility has been broken with the v3.0 release which includes versioned paths required by 1.11
|
||||
go modules (See [Releasing Modules](https://github.com/golang/go/wiki/Modules#releasing-modules-v2-or-higher)).
|
||||
Pin your dependencies to the v1.1.1 or v2.0 tag if you are not ready for v3.0**
|
||||
|
||||
## Usage
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mailgun/mailgun-go/v3"
|
||||
)
|
||||
|
||||
// Your available domain names can be found here:
|
||||
// (https://app.mailgun.com/app/domains)
|
||||
var yourDomain string = "your-domain-name" // e.g. mg.yourcompany.com
|
||||
|
||||
// You can find the Private API Key in your Account Menu, under "Settings":
|
||||
// (https://app.mailgun.com/app/account/security)
|
||||
var privateAPIKey string = "your-private-key"
|
||||
|
||||
|
||||
func main() {
|
||||
// Create an instance of the Mailgun Client
|
||||
mg := mailgun.NewMailgun(yourDomain, privateAPIKey)
|
||||
|
||||
sender := "sender@example.com"
|
||||
subject := "Fancy subject!"
|
||||
body := "Hello from Mailgun Go!"
|
||||
recipient := "recipient@example.com"
|
||||
|
||||
// The message object allows you to add attachments and Bcc recipients
|
||||
message := mg.NewMessage(sender, subject, body, recipient)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
// Send the message with a 10 second timeout
|
||||
resp, id, err := mg.Send(ctx, message)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("ID: %s Resp: %s\n", id, resp)
|
||||
}
|
||||
```
|
||||
|
||||
## Get Events
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mailgun/mailgun-go/v3"
|
||||
"github.com/mailgun/mailgun-go/v3/events"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// You can find the Private API Key in your Account Menu, under "Settings":
|
||||
// (https://app.mailgun.com/app/account/security)
|
||||
mg := mailgun.NewMailgun("your-domain.com", "your-private-key")
|
||||
|
||||
it := mg.ListEvents(&mailgun.ListEventOptions{Limit: 100})
|
||||
|
||||
var page []mailgun.Event
|
||||
|
||||
// The entire operation should not take longer than 30 seconds
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
// For each page of 100 events
|
||||
for it.Next(ctx, &page) {
|
||||
for _, e := range page {
|
||||
// You can access some fields via the interface
|
||||
fmt.Printf("Event: '%s' TimeStamp: '%s'\n", e.GetName(), e.GetTimestamp())
|
||||
|
||||
// and you can act upon each event by type
|
||||
switch event := e.(type) {
|
||||
case *events.Accepted:
|
||||
fmt.Printf("Accepted: auth: %t\n", event.Flags.IsAuthenticated)
|
||||
case *events.Delivered:
|
||||
fmt.Printf("Delivered transport: %s\n", event.Envelope.Transport)
|
||||
case *events.Failed:
|
||||
fmt.Printf("Failed reason: %s\n", event.Reason)
|
||||
case *events.Clicked:
|
||||
fmt.Printf("Clicked GeoLocation: %s\n", event.GeoLocation.Country)
|
||||
case *events.Opened:
|
||||
fmt.Printf("Opened GeoLocation: %s\n", event.GeoLocation.Country)
|
||||
case *events.Rejected:
|
||||
fmt.Printf("Rejected reason: %s\n", event.Reject.Reason)
|
||||
case *events.Stored:
|
||||
fmt.Printf("Stored URL: %s\n", event.Storage.URL)
|
||||
case *events.Unsubscribed:
|
||||
fmt.Printf("Unsubscribed client OS: %s\n", event.ClientInfo.ClientOS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Polling
|
||||
The mailgun library has built-in support for polling the events api
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mailgun/mailgun-go/v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// You can find the Private API Key in your Account Menu, under "Settings":
|
||||
// (https://app.mailgun.com/app/account/security)
|
||||
mg := mailgun.NewMailgun("your-domain.com", "your-private-key")
|
||||
|
||||
begin := time.Now().Add(time.Second * -3)
|
||||
|
||||
// Very short poll interval
|
||||
it := mg.PollEvents(&mailgun.ListEventOptions{
|
||||
// Only events with a timestamp after this date/time will be returned
|
||||
Begin: &begin,
|
||||
// How often we poll the api for new events
|
||||
PollInterval: time.Second * 30,
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Poll until our email event arrives
|
||||
var page []mailgun.Event
|
||||
for it.Poll(ctx, &page) {
|
||||
for _, e := range page {
|
||||
// Do something with event
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Email Validations
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mailgun/mailgun-go/v3"
|
||||
)
|
||||
|
||||
// If your plan does not include email validations but you have an account,
|
||||
// use your Public Validation api key. If your plan does include email validations,
|
||||
// use your Private API key. You can find both the Private and
|
||||
// Public Validation API Keys in your Account Menu, under "Settings":
|
||||
// (https://app.mailgun.com/app/account/security)
|
||||
var apiKey string = "your-api-key"
|
||||
|
||||
func main() {
|
||||
// Create an instance of the Validator
|
||||
v := mailgun.NewEmailValidator(apiKey)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
email, err := v.ValidateEmail(ctx, "recipient@example.com", false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Valid: %t\n", email.IsValid)
|
||||
}
|
||||
```
|
||||
|
||||
## Webhook Handling
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mailgun/mailgun-go/v3"
|
||||
"github.com/mailgun/mailgun-go/v3/events"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// You can find the Private API Key in your Account Menu, under "Settings":
|
||||
// (https://app.mailgun.com/app/account/security)
|
||||
mg := mailgun.NewMailgun("your-domain.com", "private-api-key")
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var payload mailgun.WebhookPayload
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
fmt.Printf("decode JSON error: %s", err)
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
|
||||
verified, err := mg.VerifyWebhookSignature(payload.Signature)
|
||||
if err != nil {
|
||||
fmt.Printf("verify error: %s\n", err)
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
|
||||
if !verified {
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
fmt.Printf("failed verification %+v\n", payload.Signature)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Verified Signature\n")
|
||||
|
||||
// Parse the event provided by the webhook payload
|
||||
e, err := mailgun.ParseEvent(payload.EventData)
|
||||
if err != nil {
|
||||
fmt.Printf("parse event error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch event := e.(type) {
|
||||
case *events.Accepted:
|
||||
fmt.Printf("Accepted: auth: %t\n", event.Flags.IsAuthenticated)
|
||||
case *events.Delivered:
|
||||
fmt.Printf("Delivered transport: %s\n", event.Envelope.Transport)
|
||||
}
|
||||
})
|
||||
|
||||
fmt.Println("Serve on :9090...")
|
||||
if err := http.ListenAndServe(":9090", nil); err != nil {
|
||||
fmt.Printf("serve error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using Templates
|
||||
|
||||
Templates enable you to create message templates on your Mailgun account and then populate the data variables at send-time. This allows you to have your layout and design managed on the server and handle the data on the client. The template variables are added as a JSON stringified `X-Mailgun-Variables` header. For example, if you have a template to send a password reset link, you could do the following:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mailgun/mailgun-go/v3"
|
||||
)
|
||||
|
||||
// Your available domain names can be found here:
|
||||
// (https://app.mailgun.com/app/domains)
|
||||
var yourDomain string = "your-domain-name" // e.g. mg.yourcompany.com
|
||||
|
||||
// You can find the Private API Key in your Account Menu, under "Settings":
|
||||
// (https://app.mailgun.com/app/account/security)
|
||||
var privateAPIKey string = "your-private-key"
|
||||
|
||||
|
||||
func main() {
|
||||
// Create an instance of the Mailgun Client
|
||||
mg := mailgun.NewMailgun(yourDomain, privateAPIKey)
|
||||
|
||||
sender := "sender@example.com"
|
||||
subject := "Fancy subject!"
|
||||
body := ""
|
||||
recipient := "recipient@example.com"
|
||||
|
||||
// The message object allows you to add attachments and Bcc recipients
|
||||
message := mg.NewMessage(sender, subject, body, recipient)
|
||||
message.SetTemplate("passwordReset")
|
||||
message.AddTemplateVariable("passwordResetLink", "some link to your site unique to your user")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
// Send the message with a 10 second timeout
|
||||
resp, id, err := mg.Send(ctx, message)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("ID: %s Resp: %s\n", id, resp)
|
||||
}
|
||||
```
|
||||
|
||||
The official mailgun documentation includes examples using this library. Go
|
||||
[here](https://documentation.mailgun.com/en/latest/api_reference.html#api-reference)
|
||||
and click on the "Go" button at the top of the page.
|
||||
|
||||
### EU Region
|
||||
European customers will need to change the default API Base to access your domains
|
||||
|
||||
```go
|
||||
mg := mailgun.NewMailgun("your-domain.com", "private-api-key")
|
||||
mg.SetAPIBase(mailgun.APIBaseEU)
|
||||
```
|
||||
## Installation
|
||||
|
||||
If you are using [golang modules](https://github.com/golang/go/wiki/Modules) make sure you
|
||||
include the `/v3` at the end of your import paths
|
||||
```bash
|
||||
$ go get github.com/mailgun/mailgun-go/v3
|
||||
```
|
||||
|
||||
If you are **not** using golang modules, you can drop the `/v3` at the end of the import path.
|
||||
As long as you are using the latest 1.10 or 1.11 golang release, import paths that end in `/v3`
|
||||
in your code should work fine even if you do not have golang modules enabled for your project.
|
||||
```bash
|
||||
$ go get github.com/mailgun/mailgun-go
|
||||
```
|
||||
|
||||
**NOTE for go dep users**
|
||||
|
||||
Using version 3 of the mailgun-go library with go dep currently results in the following error
|
||||
```
|
||||
"github.com/mailgun/mailgun-go/v3/events", which contains malformed code: no package exists at ...
|
||||
```
|
||||
This is a known bug in go dep. You can follow the PR to fix this bug [here](https://github.com/golang/dep/pull/1963)
|
||||
Until this bug is fixed, the best way to use version 3 of the mailgun-go library is to use the golang community
|
||||
supported [golang modules](https://github.com/golang/go/wiki/Modules).
|
||||
|
||||
## Testing
|
||||
|
||||
*WARNING* - running the tests will cost you money!
|
||||
|
||||
To run the tests various environment variables must be set. These are:
|
||||
|
||||
* `MG_DOMAIN` is the domain name - this is a value registered in the Mailgun admin interface.
|
||||
* `MG_PUBLIC_API_KEY` is the Public Validation API key - you can get this value from the Mailgun [security page](https://app.mailgun.com/app/account/security)
|
||||
* `MG_API_KEY` is the Private API key - you can get this value from the Mailgun [security page](https://app.mailgun.com/app/account/security)
|
||||
* `MG_EMAIL_TO` is the email address used in various sending tests.
|
||||
|
||||
and finally
|
||||
|
||||
* `MG_SPEND_MONEY` if this value is set the part of the test that use the API to actually send email will be run - be aware *this will count on your quota* and *this _will_ cost you money*.
|
||||
|
||||
The code is released under a 3-clause BSD license. See the LICENSE file for more information.
|
|
@ -0,0 +1,202 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Bounce aggregates data relating to undeliverable messages to a specific intended recipient,
|
||||
// identified by Address.
|
||||
type Bounce struct {
|
||||
// The time at which Mailgun detected the bounce.
|
||||
CreatedAt RFC2822Time `json:"created_at"`
|
||||
// Code provides the SMTP error code that caused the bounce
|
||||
Code string `json:"code"`
|
||||
// Address the bounce is for
|
||||
Address string `json:"address"`
|
||||
// human readable reason why
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type Paging struct {
|
||||
First string `json:"first,omitempty"`
|
||||
Next string `json:"next,omitempty"`
|
||||
Previous string `json:"previous,omitempty"`
|
||||
Last string `json:"last,omitempty"`
|
||||
}
|
||||
|
||||
type bouncesListResponse struct {
|
||||
Items []Bounce `json:"items"`
|
||||
Paging Paging `json:"paging"`
|
||||
}
|
||||
|
||||
// ListBounces 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 (mg *MailgunImpl) ListBounces(opts *ListOptions) *BouncesIterator {
|
||||
r := newHTTPRequest(generateApiUrl(mg, bouncesEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
if opts != nil {
|
||||
if opts.Limit != 0 {
|
||||
r.addParameter("limit", strconv.Itoa(opts.Limit))
|
||||
}
|
||||
}
|
||||
url, err := r.generateUrlWithParameters()
|
||||
return &BouncesIterator{
|
||||
mg: mg,
|
||||
bouncesListResponse: bouncesListResponse{Paging: Paging{Next: url, First: url}},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
type BouncesIterator struct {
|
||||
bouncesListResponse
|
||||
mg Mailgun
|
||||
err error
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (ci *BouncesIterator) Err() error {
|
||||
return ci.err
|
||||
}
|
||||
|
||||
// Next retrieves the next page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (ci *BouncesIterator) Next(ctx context.Context, items *[]Bounce) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.Next)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Bounce, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
if len(ci.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of items from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (ci *BouncesIterator) First(ctx context.Context, items *[]Bounce) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.First)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Bounce, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of items from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (ci *BouncesIterator) Last(ctx context.Context, items *[]Bounce) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.Last)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Bounce, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (ci *BouncesIterator) Previous(ctx context.Context, items *[]Bounce) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
if ci.Paging.Previous == "" {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.Previous)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Bounce, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
if len(ci.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ci *BouncesIterator) fetch(ctx context.Context, url string) error {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(ci.mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, ci.mg.APIKey())
|
||||
|
||||
return getResponseFromJSON(ctx, r, &ci.bouncesListResponse)
|
||||
}
|
||||
|
||||
// GetBounce retrieves a single bounce record, if any exist, for the given recipient address.
|
||||
func (mg *MailgunImpl) GetBounce(ctx context.Context, address string) (Bounce, error) {
|
||||
r := newHTTPRequest(generateApiUrl(mg, bouncesEndpoint) + "/" + address)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
var response Bounce
|
||||
err := getResponseFromJSON(ctx, r, &response)
|
||||
return response, 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 (mg *MailgunImpl) AddBounce(ctx context.Context, address, code, error string) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, bouncesEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
payload.addValue("address", address)
|
||||
if code != "" {
|
||||
payload.addValue("code", code)
|
||||
}
|
||||
if error != "" {
|
||||
payload.addValue("error", error)
|
||||
}
|
||||
_, err := makePostRequest(ctx, r, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteBounce removes all bounces associted with the provided e-mail address.
|
||||
func (mg *MailgunImpl) DeleteBounce(ctx context.Context, address string) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, bouncesEndpoint) + "/" + address)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// A Credential structure describes a principle allowed to send or receive mail at the domain.
|
||||
type Credential struct {
|
||||
CreatedAt RFC2822Time `json:"created_at"`
|
||||
Login string `json:"login"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type credentialsListResponse struct {
|
||||
// is -1 if Next() or First() have not been called
|
||||
TotalCount int `json:"total_count"`
|
||||
Items []Credential `json:"items"`
|
||||
}
|
||||
|
||||
// Returned when a required parameter is missing.
|
||||
var ErrEmptyParam = fmt.Errorf("empty or illegal parameter")
|
||||
|
||||
// ListCredentials returns the (possibly zero-length) list of credentials associated with your domain.
|
||||
func (mg *MailgunImpl) ListCredentials(opts *ListOptions) *CredentialsIterator {
|
||||
var limit int
|
||||
if opts != nil {
|
||||
limit = opts.Limit
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
return &CredentialsIterator{
|
||||
mg: mg,
|
||||
url: generateCredentialsUrl(mg, ""),
|
||||
credentialsListResponse: credentialsListResponse{TotalCount: -1},
|
||||
limit: limit,
|
||||
}
|
||||
}
|
||||
|
||||
type CredentialsIterator struct {
|
||||
credentialsListResponse
|
||||
|
||||
limit int
|
||||
mg Mailgun
|
||||
offset int
|
||||
url string
|
||||
err error
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (ri *CredentialsIterator) Err() error {
|
||||
return ri.err
|
||||
}
|
||||
|
||||
// Offset returns the current offset of the iterator
|
||||
func (ri *CredentialsIterator) Offset() int {
|
||||
return ri.offset
|
||||
}
|
||||
|
||||
// Next retrieves the next page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (ri *CredentialsIterator) Next(ctx context.Context, items *[]Credential) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ri.err = ri.fetch(ctx, ri.offset, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
cpy := make([]Credential, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
if len(ri.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
ri.offset = ri.offset + len(ri.Items)
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of items from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (ri *CredentialsIterator) First(ctx context.Context, items *[]Credential) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
ri.err = ri.fetch(ctx, 0, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Credential, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
ri.offset = len(ri.Items)
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of items from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (ri *CredentialsIterator) Last(ctx context.Context, items *[]Credential) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ri.TotalCount == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
ri.offset = ri.TotalCount - ri.limit
|
||||
if ri.offset < 0 {
|
||||
ri.offset = 0
|
||||
}
|
||||
|
||||
ri.err = ri.fetch(ctx, ri.offset, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Credential, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (ri *CredentialsIterator) Previous(ctx context.Context, items *[]Credential) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ri.TotalCount == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
ri.offset = ri.offset - (ri.limit * 2)
|
||||
if ri.offset < 0 {
|
||||
ri.offset = 0
|
||||
}
|
||||
|
||||
ri.err = ri.fetch(ctx, ri.offset, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Credential, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
if len(ri.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ri *CredentialsIterator) fetch(ctx context.Context, skip, limit int) error {
|
||||
r := newHTTPRequest(ri.url)
|
||||
r.setBasicAuth(basicAuthUser, ri.mg.APIKey())
|
||||
r.setClient(ri.mg.Client())
|
||||
|
||||
if skip != 0 {
|
||||
r.addParameter("skip", strconv.Itoa(skip))
|
||||
}
|
||||
if limit != 0 {
|
||||
r.addParameter("limit", strconv.Itoa(limit))
|
||||
}
|
||||
|
||||
return getResponseFromJSON(ctx, r, &ri.credentialsListResponse)
|
||||
}
|
||||
|
||||
// CreateCredential attempts to create associate a new principle with your domain.
|
||||
func (mg *MailgunImpl) CreateCredential(ctx context.Context, login, password string) error {
|
||||
if (login == "") || (password == "") {
|
||||
return ErrEmptyParam
|
||||
}
|
||||
r := newHTTPRequest(generateCredentialsUrl(mg, ""))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := newUrlEncodedPayload()
|
||||
p.addValue("login", login)
|
||||
p.addValue("password", password)
|
||||
_, err := makePostRequest(ctx, r, p)
|
||||
return err
|
||||
}
|
||||
|
||||
// ChangeCredentialPassword attempts to alter the indicated credential's password.
|
||||
func (mg *MailgunImpl) ChangeCredentialPassword(ctx context.Context, login, password string) error {
|
||||
if (login == "") || (password == "") {
|
||||
return ErrEmptyParam
|
||||
}
|
||||
r := newHTTPRequest(generateCredentialsUrl(mg, login))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := newUrlEncodedPayload()
|
||||
p.addValue("password", password)
|
||||
_, err := makePutRequest(ctx, r, p)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteCredential attempts to remove the indicated principle from the domain.
|
||||
func (mg *MailgunImpl) DeleteCredential(ctx context.Context, login string) error {
|
||||
if login == "" {
|
||||
return ErrEmptyParam
|
||||
}
|
||||
r := newHTTPRequest(generateCredentialsUrl(mg, login))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,386 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Use these to specify a spam action when creating a new domain.
|
||||
const (
|
||||
// Tag the received message with headers providing a measure of its spamness.
|
||||
SpamActionTag = SpamAction("tag")
|
||||
// Prevents Mailgun from taking any action on what it perceives to be spam.
|
||||
SpamActionDisabled = SpamAction("disabled")
|
||||
// instructs Mailgun to just block or delete the message all-together.
|
||||
SpamActionDelete = SpamAction("delete")
|
||||
)
|
||||
|
||||
type SpamAction string
|
||||
|
||||
// A Domain structure holds information about a domain used when sending mail.
|
||||
type Domain struct {
|
||||
CreatedAt RFC2822Time `json:"created_at"`
|
||||
SMTPLogin string `json:"smtp_login"`
|
||||
Name string `json:"name"`
|
||||
SMTPPassword string `json:"smtp_password"`
|
||||
Wildcard bool `json:"wildcard"`
|
||||
SpamAction SpamAction `json:"spam_action"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
// 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 DomainResponse struct {
|
||||
Domain Domain `json:"domain"`
|
||||
ReceivingDNSRecords []DNSRecord `json:"receiving_dns_records"`
|
||||
SendingDNSRecords []DNSRecord `json:"sending_dns_records"`
|
||||
}
|
||||
|
||||
type domainConnectionResponse struct {
|
||||
Connection DomainConnection `json:"connection"`
|
||||
}
|
||||
|
||||
type domainsListResponse struct {
|
||||
// is -1 if Next() or First() have not been called
|
||||
TotalCount int `json:"total_count"`
|
||||
Items []Domain `json:"items"`
|
||||
}
|
||||
|
||||
// Specify the domain connection options
|
||||
type DomainConnection struct {
|
||||
RequireTLS bool `json:"require_tls"`
|
||||
SkipVerification bool `json:"skip_verification"`
|
||||
}
|
||||
|
||||
// Specify the domain tracking options
|
||||
type DomainTracking struct {
|
||||
Click TrackingStatus `json:"click"`
|
||||
Open TrackingStatus `json:"open"`
|
||||
Unsubscribe TrackingStatus `json:"unsubscribe"`
|
||||
}
|
||||
|
||||
// The tracking status of a domain
|
||||
type TrackingStatus struct {
|
||||
Active bool `json:"active"`
|
||||
HTMLFooter string `json:"html_footer"`
|
||||
TextFooter string `json:"text_footer"`
|
||||
}
|
||||
|
||||
type domainTrackingResponse struct {
|
||||
Tracking DomainTracking `json:"tracking"`
|
||||
}
|
||||
|
||||
// ListDomains retrieves a set of domains from Mailgun.
|
||||
func (mg *MailgunImpl) ListDomains(opts *ListOptions) *DomainsIterator {
|
||||
var limit int
|
||||
if opts != nil {
|
||||
limit = opts.Limit
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
return &DomainsIterator{
|
||||
mg: mg,
|
||||
url: generatePublicApiUrl(mg, domainsEndpoint),
|
||||
domainsListResponse: domainsListResponse{TotalCount: -1},
|
||||
limit: limit,
|
||||
}
|
||||
}
|
||||
|
||||
type DomainsIterator struct {
|
||||
domainsListResponse
|
||||
|
||||
limit int
|
||||
mg Mailgun
|
||||
offset int
|
||||
url string
|
||||
err error
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (ri *DomainsIterator) Err() error {
|
||||
return ri.err
|
||||
}
|
||||
|
||||
// Offset returns the current offset of the iterator
|
||||
func (ri *DomainsIterator) Offset() int {
|
||||
return ri.offset
|
||||
}
|
||||
|
||||
// Next retrieves the next page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (ri *DomainsIterator) Next(ctx context.Context, items *[]Domain) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ri.err = ri.fetch(ctx, ri.offset, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
cpy := make([]Domain, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
if len(ri.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
ri.offset = ri.offset + len(ri.Items)
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of items from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (ri *DomainsIterator) First(ctx context.Context, items *[]Domain) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
ri.err = ri.fetch(ctx, 0, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Domain, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
ri.offset = len(ri.Items)
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of items from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (ri *DomainsIterator) Last(ctx context.Context, items *[]Domain) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ri.TotalCount == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
ri.offset = ri.TotalCount - ri.limit
|
||||
if ri.offset < 0 {
|
||||
ri.offset = 0
|
||||
}
|
||||
|
||||
ri.err = ri.fetch(ctx, ri.offset, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Domain, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (ri *DomainsIterator) Previous(ctx context.Context, items *[]Domain) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ri.TotalCount == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
ri.offset = ri.offset - (ri.limit * 2)
|
||||
if ri.offset < 0 {
|
||||
ri.offset = 0
|
||||
}
|
||||
|
||||
ri.err = ri.fetch(ctx, ri.offset, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Domain, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
if len(ri.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ri *DomainsIterator) fetch(ctx context.Context, skip, limit int) error {
|
||||
r := newHTTPRequest(ri.url)
|
||||
r.setBasicAuth(basicAuthUser, ri.mg.APIKey())
|
||||
r.setClient(ri.mg.Client())
|
||||
|
||||
if skip != 0 {
|
||||
r.addParameter("skip", strconv.Itoa(skip))
|
||||
}
|
||||
if limit != 0 {
|
||||
r.addParameter("limit", strconv.Itoa(limit))
|
||||
}
|
||||
|
||||
return getResponseFromJSON(ctx, r, &ri.domainsListResponse)
|
||||
}
|
||||
|
||||
// GetDomain retrieves detailed information about the named domain.
|
||||
func (mg *MailgunImpl) GetDomain(ctx context.Context, domain string) (DomainResponse, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + domain)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
var resp DomainResponse
|
||||
err := getResponseFromJSON(ctx, r, &resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (mg *MailgunImpl) VerifyDomain(ctx context.Context, domain string) (string, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + domain + "/verify")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
var resp DomainResponse
|
||||
err := putResponseFromJSON(ctx, r, payload, &resp)
|
||||
return resp.Domain.State, err
|
||||
}
|
||||
|
||||
// Optional parameters when creating a domain
|
||||
type CreateDomainOptions struct {
|
||||
Password string
|
||||
SpamAction SpamAction
|
||||
Wildcard bool
|
||||
ForceDKIMAuthority bool
|
||||
DKIMKeySize int
|
||||
IPS []string
|
||||
}
|
||||
|
||||
// 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 (mg *MailgunImpl) CreateDomain(ctx context.Context, name string, opts *CreateDomainOptions) (DomainResponse, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
payload.addValue("name", name)
|
||||
|
||||
if opts != nil {
|
||||
if opts.SpamAction != "" {
|
||||
payload.addValue("spam_action", string(opts.SpamAction))
|
||||
}
|
||||
if opts.Wildcard {
|
||||
payload.addValue("wildcard", boolToString(opts.Wildcard))
|
||||
}
|
||||
if opts.ForceDKIMAuthority {
|
||||
payload.addValue("force_dkim_authority", boolToString(opts.ForceDKIMAuthority))
|
||||
}
|
||||
if opts.DKIMKeySize != 0 {
|
||||
payload.addValue("dkim_key_size", strconv.Itoa(opts.DKIMKeySize))
|
||||
}
|
||||
if len(opts.IPS) != 0 {
|
||||
payload.addValue("ips", strings.Join(opts.IPS, ","))
|
||||
}
|
||||
if len(opts.Password) != 0 {
|
||||
payload.addValue("smtp_password", opts.Password)
|
||||
}
|
||||
}
|
||||
var resp DomainResponse
|
||||
err := postResponseFromJSON(ctx, r, payload, &resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// GetDomainConnection returns delivery connection settings for the defined domain
|
||||
func (mg *MailgunImpl) GetDomainConnection(ctx context.Context, domain string) (DomainConnection, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + domain + "/connection")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
var resp domainConnectionResponse
|
||||
err := getResponseFromJSON(ctx, r, &resp)
|
||||
return resp.Connection, err
|
||||
}
|
||||
|
||||
// Updates the specified delivery connection settings for the defined domain
|
||||
func (mg *MailgunImpl) UpdateDomainConnection(ctx context.Context, domain string, settings DomainConnection) error {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + domain + "/connection")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
payload.addValue("require_tls", boolToString(settings.RequireTLS))
|
||||
payload.addValue("skip_verification", boolToString(settings.SkipVerification))
|
||||
_, err := makePutRequest(ctx, r, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteDomain instructs Mailgun to dispose of the named domain name
|
||||
func (mg *MailgunImpl) DeleteDomain(ctx context.Context, name string) error {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + name)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDomainTracking returns tracking settings for a domain
|
||||
func (mg *MailgunImpl) GetDomainTracking(ctx context.Context, domain string) (DomainTracking, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + domain + "/tracking")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
var resp domainTrackingResponse
|
||||
err := getResponseFromJSON(ctx, r, &resp)
|
||||
return resp.Tracking, err
|
||||
}
|
||||
|
||||
func (mg *MailgunImpl) UpdateClickTracking(ctx context.Context, domain, active string) error {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + domain + "/tracking/click")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
payload.addValue("active", active)
|
||||
_, err := makePutRequest(ctx, r, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mg *MailgunImpl) UpdateUnsubscribeTracking(ctx context.Context, domain, active, htmlFooter, textFooter string) error {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + domain + "/tracking/unsubscribe")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
payload.addValue("active", active)
|
||||
payload.addValue("html_footer", htmlFooter)
|
||||
payload.addValue("text_footer", textFooter)
|
||||
_, err := makePutRequest(ctx, r, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mg *MailgunImpl) UpdateOpenTracking(ctx context.Context, domain, active string) error {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + domain + "/tracking/open")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
payload.addValue("active", active)
|
||||
_, err := makePutRequest(ctx, r, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
func boolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// 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.
|
||||
//
|
||||
type EmailVerification struct {
|
||||
// Indicates whether an email address conforms to IETF RFC standards.
|
||||
IsValid bool `json:"is_valid"`
|
||||
// Indicates whether an email address is deliverable.
|
||||
MailboxVerification string `json:"mailbox_verification"`
|
||||
// Parts records the different subfields of the parsed email address
|
||||
Parts EmailVerificationParts `json:"parts"`
|
||||
// Echoes the address provided.
|
||||
Address string `json:"address"`
|
||||
// Provides a simple recommendation in case the address is invalid or
|
||||
// Mailgun thinks you might have a typo. May be empty, in which case
|
||||
// Mailgun has no recommendation to give.
|
||||
DidYouMean string `json:"did_you_mean"`
|
||||
// Indicates whether Mailgun thinks the address is from a known
|
||||
// disposable mailbox provider.
|
||||
IsDisposableAddress bool `json:"is_disposable_address"`
|
||||
// Indicates whether Mailgun thinks the address is an email distribution list.
|
||||
IsRoleAddress bool `json:"is_role_address"`
|
||||
// A human readable reason the address is reported as invalid
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type addressParseResult struct {
|
||||
Parsed []string `json:"parsed"`
|
||||
Unparseable []string `json:"unparseable"`
|
||||
}
|
||||
|
||||
type EmailValidator interface {
|
||||
ValidateEmail(ctx context.Context, email string, mailBoxVerify bool) (EmailVerification, error)
|
||||
ParseAddresses(ctx context.Context, addresses ...string) ([]string, []string, error)
|
||||
}
|
||||
|
||||
type EmailValidatorImpl struct {
|
||||
client *http.Client
|
||||
isPublicKey bool
|
||||
apiBase string
|
||||
apiKey string
|
||||
}
|
||||
|
||||
// Creates a new validation instance.
|
||||
// * If a public key is provided, uses the public validation endpoints
|
||||
// * If a private key is provided, uses the private validation endpoints
|
||||
func NewEmailValidator(apiKey string) *EmailValidatorImpl {
|
||||
isPublicKey := false
|
||||
|
||||
// Did the user pass in a public key?
|
||||
if strings.HasPrefix(apiKey, "pubkey-") {
|
||||
isPublicKey = true
|
||||
}
|
||||
|
||||
return &EmailValidatorImpl{
|
||||
client: http.DefaultClient,
|
||||
isPublicKey: isPublicKey,
|
||||
apiBase: APIBase,
|
||||
apiKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEmailValidatorFromEnv returns a new EmailValidator using environment variables
|
||||
// If MG_PUBLIC_API_KEY is set, assume using the free validation subject to daily usage limits
|
||||
// If only MG_API_KEY is set, assume using the /private validation routes with no daily usage limits
|
||||
func NewEmailValidatorFromEnv() (*EmailValidatorImpl, error) {
|
||||
apiKey := os.Getenv("MG_PUBLIC_API_KEY")
|
||||
if apiKey == "" {
|
||||
apiKey = os.Getenv("MG_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil, errors.New(
|
||||
"environment variable MG_PUBLIC_API_KEY or MG_API_KEY required for email validation")
|
||||
}
|
||||
}
|
||||
v := NewEmailValidator(apiKey)
|
||||
url := os.Getenv("MG_URL")
|
||||
if url != "" {
|
||||
v.SetAPIBase(url)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// APIBase returns the API Base URL configured for this client.
|
||||
func (m *EmailValidatorImpl) APIBase() string {
|
||||
return m.apiBase
|
||||
}
|
||||
|
||||
// SetAPIBase updates the API Base URL for this client.
|
||||
func (m *EmailValidatorImpl) SetAPIBase(address string) {
|
||||
m.apiBase = address
|
||||
}
|
||||
|
||||
// SetClient updates the HTTP client for this client.
|
||||
func (m *EmailValidatorImpl) SetClient(c *http.Client) {
|
||||
m.client = c
|
||||
}
|
||||
|
||||
// Client returns the HTTP client configured for this client.
|
||||
func (m *EmailValidatorImpl) Client() *http.Client {
|
||||
return m.client
|
||||
}
|
||||
|
||||
// APIKey returns the API key used for validations
|
||||
func (m *EmailValidatorImpl) APIKey() string {
|
||||
return m.apiKey
|
||||
}
|
||||
|
||||
func (m *EmailValidatorImpl) getAddressURL(endpoint string) string {
|
||||
if m.isPublicKey {
|
||||
return fmt.Sprintf("%s/address/%s", m.APIBase(), endpoint)
|
||||
}
|
||||
return fmt.Sprintf("%s/address/private/%s", m.APIBase(), endpoint)
|
||||
}
|
||||
|
||||
// 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.)
|
||||
func (m *EmailValidatorImpl) ValidateEmail(ctx context.Context, email string, mailBoxVerify bool) (EmailVerification, error) {
|
||||
r := newHTTPRequest(m.getAddressURL("validate"))
|
||||
r.setClient(m.Client())
|
||||
r.addParameter("address", email)
|
||||
if mailBoxVerify {
|
||||
r.addParameter("mailbox_verification", "true")
|
||||
}
|
||||
r.setBasicAuth(basicAuthUser, m.APIKey())
|
||||
|
||||
var response EmailVerification
|
||||
err := getResponseFromJSON(ctx, 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 *EmailValidatorImpl) ParseAddresses(ctx context.Context, addresses ...string) ([]string, []string, error) {
|
||||
r := newHTTPRequest(m.getAddressURL("parse"))
|
||||
r.setClient(m.Client())
|
||||
r.addParameter("addresses", strings.Join(addresses, ","))
|
||||
r.setBasicAuth(basicAuthUser, m.APIKey())
|
||||
|
||||
var response addressParseResult
|
||||
err := getResponseFromJSON(ctx, r, &response)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return response.Parsed, response.Unparseable, nil
|
||||
}
|
|
@ -0,0 +1,282 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mailgun/mailgun-go/v3/events"
|
||||
"github.com/mailru/easyjson"
|
||||
)
|
||||
|
||||
// ListEventOptions{} modifies the behavior of ListEvents()
|
||||
type ListEventOptions struct {
|
||||
// Limits the results to a specific start and end time
|
||||
Begin, End time.Time
|
||||
// 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.
|
||||
ForceAscending, ForceDescending bool
|
||||
// Compact, if true, compacts the returned JSON to minimize transmission bandwidth.
|
||||
Compact bool
|
||||
// Limit caps the number of results returned. If left unspecified, MailGun assumes 100.
|
||||
Limit int
|
||||
// Filter allows the caller to provide more specialized filters on the query.
|
||||
// Consult the Mailgun documentation for more details.
|
||||
Filter map[string]string
|
||||
PollInterval time.Duration
|
||||
}
|
||||
|
||||
// EventIterator maintains the state necessary for paging though small parcels of a larger set of events.
|
||||
type EventIterator struct {
|
||||
events.Response
|
||||
mg Mailgun
|
||||
err error
|
||||
}
|
||||
|
||||
// Create an new iterator to fetch a page of events from the events api
|
||||
func (mg *MailgunImpl) ListEvents(opts *ListEventOptions) *EventIterator {
|
||||
req := newHTTPRequest(generateApiUrl(mg, eventsEndpoint))
|
||||
if opts != nil {
|
||||
if opts.Limit > 0 {
|
||||
req.addParameter("limit", fmt.Sprintf("%d", opts.Limit))
|
||||
}
|
||||
if opts.Compact {
|
||||
req.addParameter("pretty", "no")
|
||||
}
|
||||
if opts.ForceAscending {
|
||||
req.addParameter("ascending", "yes")
|
||||
} else if opts.ForceDescending {
|
||||
req.addParameter("ascending", "no")
|
||||
}
|
||||
if !opts.Begin.IsZero() {
|
||||
req.addParameter("begin", formatMailgunTime(opts.Begin))
|
||||
}
|
||||
if !opts.End.IsZero() {
|
||||
req.addParameter("end", formatMailgunTime(opts.End))
|
||||
}
|
||||
if opts.Filter != nil {
|
||||
for k, v := range opts.Filter {
|
||||
req.addParameter(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
url, err := req.generateUrlWithParameters()
|
||||
return &EventIterator{
|
||||
mg: mg,
|
||||
Response: events.Response{Paging: events.Paging{Next: url, First: url}},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (ei *EventIterator) Err() error {
|
||||
return ei.err
|
||||
}
|
||||
|
||||
// Next retrieves the next page of events from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (ei *EventIterator) Next(ctx context.Context, events *[]Event) bool {
|
||||
if ei.err != nil {
|
||||
return false
|
||||
}
|
||||
ei.err = ei.fetch(ctx, ei.Paging.Next)
|
||||
if ei.err != nil {
|
||||
return false
|
||||
}
|
||||
*events, ei.err = ParseEvents(ei.Items)
|
||||
if ei.err != nil {
|
||||
return false
|
||||
}
|
||||
if len(ei.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of events from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (ei *EventIterator) First(ctx context.Context, events *[]Event) bool {
|
||||
if ei.err != nil {
|
||||
return false
|
||||
}
|
||||
ei.err = ei.fetch(ctx, ei.Paging.First)
|
||||
if ei.err != nil {
|
||||
return false
|
||||
}
|
||||
*events, ei.err = ParseEvents(ei.Items)
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of events from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (ei *EventIterator) Last(ctx context.Context, events *[]Event) bool {
|
||||
if ei.err != nil {
|
||||
return false
|
||||
}
|
||||
ei.err = ei.fetch(ctx, ei.Paging.Last)
|
||||
if ei.err != nil {
|
||||
return false
|
||||
}
|
||||
*events, ei.err = ParseEvents(ei.Items)
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of events from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (ei *EventIterator) Previous(ctx context.Context, events *[]Event) bool {
|
||||
if ei.err != nil {
|
||||
return false
|
||||
}
|
||||
if ei.Paging.Previous == "" {
|
||||
return false
|
||||
}
|
||||
ei.err = ei.fetch(ctx, ei.Paging.Previous)
|
||||
if ei.err != nil {
|
||||
return false
|
||||
}
|
||||
*events, ei.err = ParseEvents(ei.Items)
|
||||
if len(ei.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ei *EventIterator) fetch(ctx context.Context, url string) error {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(ei.mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, ei.mg.APIKey())
|
||||
|
||||
resp, err := makeRequest(ctx, r, "GET", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := easyjson.Unmarshal(resp.Data, &ei.Response); err != nil {
|
||||
return fmt.Errorf("failed to un-marshall event.Response: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EventPoller maintains the state necessary for polling events
|
||||
type EventPoller struct {
|
||||
it *EventIterator
|
||||
opts ListEventOptions
|
||||
thresholdTime time.Time
|
||||
beginTime time.Time
|
||||
sleepUntil time.Time
|
||||
mg Mailgun
|
||||
err error
|
||||
}
|
||||
|
||||
// Poll the events api and return new events as they occur
|
||||
// it = mg.PollEvents(&ListEventOptions{
|
||||
// // Only events with a timestamp after this date/time will be returned
|
||||
// Begin: time.Now().Add(time.Second * -3),
|
||||
// // How often we poll the api for new events
|
||||
// PollInterval: time.Second * 4
|
||||
// })
|
||||
//
|
||||
// var events []Event
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
//
|
||||
// // Blocks until new events appear or context is cancelled
|
||||
// for it.Poll(ctx, &events) {
|
||||
// for _, event := range(events) {
|
||||
// fmt.Printf("Event %+v\n", event)
|
||||
// }
|
||||
// }
|
||||
// if it.Err() != nil {
|
||||
// log.Fatal(it.Err())
|
||||
// }
|
||||
func (mg *MailgunImpl) PollEvents(opts *ListEventOptions) *EventPoller {
|
||||
now := time.Now()
|
||||
// ForceAscending must be set
|
||||
opts.ForceAscending = true
|
||||
|
||||
// Default begin time is 30 minutes ago
|
||||
if opts.Begin.IsZero() {
|
||||
opts.Begin = now.Add(time.Minute * -30)
|
||||
}
|
||||
|
||||
// Set a 15 second poll interval if none set
|
||||
if opts.PollInterval.Nanoseconds() == 0 {
|
||||
opts.PollInterval = time.Duration(time.Second * 15)
|
||||
}
|
||||
|
||||
return &EventPoller{
|
||||
it: mg.ListEvents(opts),
|
||||
opts: *opts,
|
||||
mg: mg,
|
||||
}
|
||||
}
|
||||
|
||||
// If an error occurred during polling `Err()` will return non nil
|
||||
func (ep *EventPoller) Err() error {
|
||||
return ep.err
|
||||
}
|
||||
|
||||
func (ep *EventPoller) Poll(ctx context.Context, events *[]Event) bool {
|
||||
var currentPage string
|
||||
var results []Event
|
||||
|
||||
if ep.opts.Begin.IsZero() {
|
||||
ep.beginTime = time.Now().UTC()
|
||||
}
|
||||
|
||||
for {
|
||||
// Remember our current page url
|
||||
currentPage = ep.it.Paging.Next
|
||||
|
||||
// Attempt to get a page of events
|
||||
var page []Event
|
||||
if ep.it.Next(ctx, &page) == false {
|
||||
if ep.it.Err() == nil && len(page) == 0 {
|
||||
// No events, sleep for our poll interval
|
||||
goto SLEEP
|
||||
}
|
||||
ep.err = ep.it.Err()
|
||||
return false
|
||||
}
|
||||
|
||||
for _, e := range page {
|
||||
// If any events on the page are older than our being time
|
||||
if e.GetTimestamp().After(ep.beginTime) {
|
||||
results = append(results, e)
|
||||
}
|
||||
}
|
||||
|
||||
// If we have events to return
|
||||
if len(results) != 0 {
|
||||
*events = results
|
||||
results = nil
|
||||
return true
|
||||
}
|
||||
|
||||
SLEEP:
|
||||
// Since we didn't find an event older than our
|
||||
// threshold, fetch this same page again
|
||||
ep.it.Paging.Next = currentPage
|
||||
|
||||
// Sleep the rest of our duration
|
||||
tick := time.NewTicker(ep.opts.PollInterval)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-tick.C:
|
||||
tick.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Given time.Time{} return a float64 as given in mailgun event timestamps
|
||||
func TimeToFloat(t time.Time) float64 {
|
||||
return float64(t.Unix()) + (float64(t.Nanosecond()/int(time.Microsecond)) / float64(1000000))
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package events
|
||||
|
||||
const (
|
||||
EventAccepted = "accepted"
|
||||
EventRejected = "rejected"
|
||||
EventDelivered = "delivered"
|
||||
EventFailed = "failed"
|
||||
EventOpened = "opened"
|
||||
EventClicked = "clicked"
|
||||
EventUnsubscribed = "unsubscribed"
|
||||
EventComplained = "complained"
|
||||
EventStored = "stored"
|
||||
EventDropped = "dropped"
|
||||
EventListMemberUploaded = "list_member_uploaded"
|
||||
EventListMemberUploadError = "list_member_upload_error"
|
||||
EventListUploaded = "list_uploaded"
|
||||
)
|
||||
|
||||
const (
|
||||
TransportHTTP = "http"
|
||||
TransportSMTP = "smtp"
|
||||
|
||||
DeviceUnknown = "unknown"
|
||||
DeviceMobileBrowser = "desktop"
|
||||
DeviceBrowser = "mobile"
|
||||
DeviceEmail = "tablet"
|
||||
DeviceOther = "other"
|
||||
|
||||
ClientUnknown = "unknown"
|
||||
ClientMobileBrowser = "mobile browser"
|
||||
ClientBrowser = "browser"
|
||||
ClientEmail = "email client"
|
||||
ClientLibrary = "library"
|
||||
ClientRobot = "robot"
|
||||
ClientOther = "other"
|
||||
|
||||
ReasonUnknown = "unknown"
|
||||
ReasonGeneric = "generic"
|
||||
ReasonBounce = "bounce"
|
||||
ReasonESPBlock = "espblock"
|
||||
ReasonGreylisted = "greylisted"
|
||||
ReasonBlacklisted = "blacklisted"
|
||||
ReasonSuppressBounce = "suppress-bounce"
|
||||
ReasonSuppressComplaint = "suppress-complaint"
|
||||
ReasonSuppressUnsubscribe = "suppress-unsubscribe"
|
||||
ReasonOld = "old"
|
||||
ReasonHardFail = "hardfail"
|
||||
|
||||
SeverityUnknown = "unknown"
|
||||
SeverityTemporary = "temporary"
|
||||
SeverityPermanent = "permanent"
|
||||
SeverityInternal = "internal"
|
||||
|
||||
MethodUnknown = "unknown"
|
||||
MethodSMTP = "smtp"
|
||||
MethodHTTP = "http"
|
||||
)
|
|
@ -0,0 +1,262 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// An EventName is a struct with the event name.
|
||||
type EventName struct {
|
||||
Name string `json:"event"`
|
||||
}
|
||||
|
||||
// GetName returns the name of the event.
|
||||
func (e *EventName) GetName() string {
|
||||
return strings.ToLower(e.Name)
|
||||
}
|
||||
|
||||
func (e *EventName) SetName(name string) {
|
||||
e.Name = strings.ToLower(name)
|
||||
}
|
||||
|
||||
type Generic struct {
|
||||
EventName
|
||||
Timestamp float64 `json:"timestamp"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (g *Generic) GetTimestamp() time.Time {
|
||||
return time.Unix(0, int64(g.Timestamp*float64(time.Second))).UTC()
|
||||
}
|
||||
|
||||
func (g *Generic) SetTimestamp(t time.Time) {
|
||||
// convert := fmt.Sprintf("%d.%06d", t.Unix(), t.Nanosecond()/int(time.Microsecond))
|
||||
// ts, err := strconv.ParseFloat(convert, 64)
|
||||
g.Timestamp = float64(t.Unix()) + (float64(t.Nanosecond()/int(time.Microsecond)) / float64(1000000))
|
||||
}
|
||||
|
||||
func (g *Generic) GetID() string {
|
||||
return g.ID
|
||||
}
|
||||
|
||||
func (g *Generic) SetID(id string) {
|
||||
g.ID = id
|
||||
}
|
||||
|
||||
//
|
||||
// Message Events
|
||||
//
|
||||
|
||||
type Accepted struct {
|
||||
Generic
|
||||
|
||||
Envelope Envelope `json:"envelope"`
|
||||
Message Message `json:"message"`
|
||||
Flags Flags `json:"flags"`
|
||||
|
||||
Recipient string `json:"recipient"`
|
||||
RecipientDomain string `json:"recipient-domain"`
|
||||
Method string `json:"method"`
|
||||
OriginatingIP string `json:"originating-ip"`
|
||||
Tags []string `json:"tags"`
|
||||
Campaigns []Campaign `json:"campaigns"`
|
||||
UserVariables map[string]interface{} `json:"user-variables"`
|
||||
}
|
||||
|
||||
type Rejected struct {
|
||||
Generic
|
||||
|
||||
Reject struct {
|
||||
Reason string `json:"reason"`
|
||||
Description string `json:"description"`
|
||||
} `json:"reject"`
|
||||
|
||||
Message Message `json:"message"`
|
||||
Storage Storage `json:"storage"`
|
||||
Flags Flags `json:"flags"`
|
||||
|
||||
Tags []string `json:"tags"`
|
||||
Campaigns []Campaign `json:"campaigns"`
|
||||
UserVariables map[string]interface{} `json:"user-variables"`
|
||||
}
|
||||
|
||||
type Delivered struct {
|
||||
Generic
|
||||
|
||||
Envelope Envelope `json:"envelope"`
|
||||
Message Message `json:"message"`
|
||||
Flags Flags `json:"flags"`
|
||||
|
||||
Recipient string `json:"recipient"`
|
||||
RecipientDomain string `json:"recipient-domain"`
|
||||
Method string `json:"method"`
|
||||
Tags []string `json:"tags"`
|
||||
Campaigns []Campaign `json:"campaigns"`
|
||||
|
||||
DeliveryStatus DeliveryStatus `json:"delivery-status"`
|
||||
UserVariables map[string]interface{} `json:"user-variables"`
|
||||
}
|
||||
|
||||
type Failed struct {
|
||||
Generic
|
||||
|
||||
Envelope Envelope `json:"envelope"`
|
||||
Message Message `json:"message"`
|
||||
Flags Flags `json:"flags"`
|
||||
|
||||
Recipient string `json:"recipient"`
|
||||
RecipientDomain string `json:"recipient-domain"`
|
||||
Method string `json:"method"`
|
||||
Tags []string `json:"tags"`
|
||||
Campaigns []Campaign `json:"campaigns"`
|
||||
|
||||
DeliveryStatus DeliveryStatus `json:"delivery-status"`
|
||||
Severity string `json:"severity"`
|
||||
Reason string `json:"reason"`
|
||||
UserVariables map[string]interface{} `json:"user-variables"`
|
||||
}
|
||||
|
||||
type Stored struct {
|
||||
Generic
|
||||
|
||||
Message Message `json:"message"`
|
||||
Storage Storage `json:"storage"`
|
||||
Flags Flags `json:"flags"`
|
||||
|
||||
Tags []string `json:"tags"`
|
||||
Campaigns []Campaign `json:"campaigns"`
|
||||
UserVariables map[string]interface{} `json:"user-variables"`
|
||||
}
|
||||
|
||||
//
|
||||
// Message Events (User)
|
||||
//
|
||||
|
||||
type Opened struct {
|
||||
Generic
|
||||
|
||||
Message Message `json:"message"`
|
||||
Campaigns []Campaign `json:"campaigns"`
|
||||
MailingList MailingList `json:"mailing-list"`
|
||||
|
||||
Recipient string `json:"recipient"`
|
||||
RecipientDomain string `json:"recipient-domain"`
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
IP string `json:"ip"`
|
||||
ClientInfo ClientInfo `json:"client-info"`
|
||||
GeoLocation GeoLocation `json:"geolocation"`
|
||||
|
||||
UserVariables map[string]interface{} `json:"user-variables"`
|
||||
}
|
||||
|
||||
type Clicked struct {
|
||||
Generic
|
||||
|
||||
Url string `json:"url"`
|
||||
|
||||
Message Message `json:"message"`
|
||||
Campaigns []Campaign `json:"campaigns"`
|
||||
MailingList MailingList `json:"mailing-list"`
|
||||
|
||||
Recipient string `json:"recipient"`
|
||||
RecipientDomain string `json:"recipient-domain"`
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
IP string `json:"ip"`
|
||||
ClientInfo ClientInfo `json:"client-info"`
|
||||
GeoLocation GeoLocation `json:"geolocation"`
|
||||
|
||||
UserVariables map[string]interface{} `json:"user-variables"`
|
||||
}
|
||||
|
||||
type Unsubscribed struct {
|
||||
Generic
|
||||
|
||||
Message Message `json:"message"`
|
||||
Campaigns []Campaign `json:"campaigns"`
|
||||
MailingList MailingList `json:"mailing-list"`
|
||||
|
||||
Recipient string `json:"recipient"`
|
||||
RecipientDomain string `json:"recipient-domain"`
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
IP string `json:"ip"`
|
||||
ClientInfo ClientInfo `json:"client-info"`
|
||||
GeoLocation GeoLocation `json:"geolocation"`
|
||||
|
||||
UserVariables map[string]interface{} `json:"user-variables"`
|
||||
}
|
||||
|
||||
type Complained struct {
|
||||
Generic
|
||||
|
||||
Message Message `json:"message"`
|
||||
Campaigns []Campaign `json:"campaigns"`
|
||||
|
||||
Recipient string `json:"recipient"`
|
||||
Tags []string `json:"tags"`
|
||||
UserVariables map[string]interface{} `json:"user-variables"`
|
||||
}
|
||||
|
||||
//
|
||||
// Mailing List Events
|
||||
//
|
||||
|
||||
type MailingListMember struct {
|
||||
Subscribed bool
|
||||
Address string
|
||||
Name string
|
||||
Vars []string
|
||||
}
|
||||
|
||||
type MailingListError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
type ListMemberUploaded struct {
|
||||
Generic
|
||||
MailingList MailingList `json:"mailing-list"`
|
||||
Member MailingListMember `json:"member"`
|
||||
TaskID string `json:"task-id"`
|
||||
}
|
||||
|
||||
type ListMemberUploadError struct {
|
||||
Generic
|
||||
MailingList MailingList `json:"mailing-list"`
|
||||
TaskID string `json:"task-id"`
|
||||
Format string `json:"format"`
|
||||
MemberDescription string `json:"member-description"`
|
||||
Error MailingListError `json:"error"`
|
||||
}
|
||||
|
||||
type ListUploaded struct {
|
||||
Generic
|
||||
MailingList MailingList `json:"mailing-list"`
|
||||
IsUpsert bool `json:"is-upsert"`
|
||||
Format string `json:"format"`
|
||||
UpsertedCount int `json:"upserted-count"`
|
||||
FailedCount int `json:"failed-count"`
|
||||
Member MailingListMember `json:"member"`
|
||||
Subscribed bool `json:"subscribed"`
|
||||
TaskID string `json:"task-id"`
|
||||
}
|
||||
|
||||
type Paging struct {
|
||||
First string `json:"first,omitempty"`
|
||||
Next string `json:"next,omitempty"`
|
||||
Previous string `json:"previous,omitempty"`
|
||||
Last string `json:"last,omitempty"`
|
||||
}
|
||||
|
||||
type RawJSON []byte
|
||||
|
||||
func (v *RawJSON) UnmarshalJSON(data []byte) error {
|
||||
*v = data
|
||||
return nil
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Items []RawJSON `json:"items"`
|
||||
Paging Paging `json:"paging"`
|
||||
}
|
4070
vendor/github.com/mailgun/mailgun-go/v3/events/events_easyjson.go
generated
vendored
Normal file
4070
vendor/github.com/mailgun/mailgun-go/v3/events/events_easyjson.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,78 @@
|
|||
package events
|
||||
|
||||
type ClientInfo struct {
|
||||
AcceptLanguage string `json:"accept-language"`
|
||||
ClientName string `json:"client-name"`
|
||||
ClientOS string `json:"client-os"`
|
||||
ClientType string `json:"client-type"`
|
||||
DeviceType string `json:"device-type"`
|
||||
IP string `json:"ip"`
|
||||
UserAgent string `json:"user-agent"`
|
||||
}
|
||||
|
||||
type GeoLocation struct {
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
type MailingList struct {
|
||||
Address string `json:"address"`
|
||||
ListID string `json:"list-id"`
|
||||
SID string `json:"sid"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Headers MessageHeaders `json:"headers"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
type Envelope struct {
|
||||
MailFrom string `json:"mail-from"`
|
||||
Sender string `json:"sender"`
|
||||
Transport string `json:"transport"`
|
||||
Targets string `json:"targets"`
|
||||
SendingHost string `json:"sending-host"`
|
||||
SendingIP string `json:"sending-ip"`
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
Key string `json:"key"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type Flags struct {
|
||||
IsAuthenticated bool `json:"is-authenticated"`
|
||||
IsBig bool `json:"is-big"`
|
||||
IsSystemTest bool `json:"is-system-test"`
|
||||
IsTestMode bool `json:"is-test-mode"`
|
||||
IsDelayedBounce bool `json:"is-delayed-bounce"`
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
FileName string `json:"filename"`
|
||||
ContentType string `json:"content-type"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
type MessageHeaders struct {
|
||||
To string `json:"to"`
|
||||
MessageID string `json:"message-id"`
|
||||
From string `json:"from"`
|
||||
Subject string `json:"subject"`
|
||||
}
|
||||
|
||||
type Campaign struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type DeliveryStatus struct {
|
||||
Code int `json:"code"`
|
||||
AttemptNo int `json:"attempt-no"`
|
||||
Description string `json:"description"`
|
||||
Message string `json:"message"`
|
||||
SessionSeconds float64 `json:"session-seconds"`
|
||||
}
|
1264
vendor/github.com/mailgun/mailgun-go/v3/events/objects_easyjson.go
generated
vendored
Normal file
1264
vendor/github.com/mailgun/mailgun-go/v3/events/objects_easyjson.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,99 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ExportList struct {
|
||||
Items []Export `json:"items"`
|
||||
}
|
||||
|
||||
type Export struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// Create an export based on the URL given
|
||||
func (mg *MailgunImpl) CreateExport(ctx context.Context, url string) error {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, exportsEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
payload.addValue("url", url)
|
||||
_, err := makePostRequest(ctx, r, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
// List all exports created within the past 24 hours
|
||||
func (mg *MailgunImpl) ListExports(ctx context.Context, url string) ([]Export, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, exportsEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
if url != "" {
|
||||
r.addParameter("url", url)
|
||||
}
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
var resp ExportList
|
||||
if err := getResponseFromJSON(ctx, r, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []Export
|
||||
for _, item := range resp.Items {
|
||||
result = append(result, Export(item))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetExport gets an export by id
|
||||
func (mg *MailgunImpl) GetExport(ctx context.Context, id string) (Export, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, exportsEndpoint) + "/" + id)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
var resp Export
|
||||
err := getResponseFromJSON(ctx, r, &resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Download an export by ID. This will respond with a '302 Moved'
|
||||
// with the Location header of temporary S3 URL if it is available.
|
||||
func (mg *MailgunImpl) GetExportLink(ctx context.Context, id string) (string, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, exportsEndpoint) + "/" + id + "/download_url")
|
||||
c := mg.Client()
|
||||
|
||||
// Ensure the client doesn't attempt to retry
|
||||
c.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return errors.New("redirect")
|
||||
}
|
||||
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
r.addHeader("User-Agent", MailgunGoUserAgent)
|
||||
|
||||
req, err := r.NewRequest(ctx, "GET", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if Debug {
|
||||
fmt.Println(r.curlString(req, nil))
|
||||
}
|
||||
|
||||
resp, err := r.Client.Do(req)
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode == http.StatusFound {
|
||||
url, err := resp.Location()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("while parsing 302 redirect url: %s", err)
|
||||
}
|
||||
return url.String(), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("expected a 302 response, API returned a '%d' instead", resp.StatusCode)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
module github.com/mailgun/mailgun-go/v3
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
||||
github.com/go-chi/chi v4.0.0+incompatible
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329
|
||||
github.com/pkg/errors v0.8.1
|
||||
)
|
||||
|
||||
replace github.com/mailgun/mailgun-go/v3/events => ./events
|
||||
|
||||
go 1.13
|
|
@ -0,0 +1,14 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4=
|
||||
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
@ -0,0 +1,330 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var validURL = regexp.MustCompile(`^/v[2-4].*`)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type payload interface {
|
||||
getPayloadBuffer() (*bytes.Buffer, error)
|
||||
getContentType() string
|
||||
getValues() []keyValuePair
|
||||
}
|
||||
|
||||
type keyValuePair struct {
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
type keyNameRC struct {
|
||||
key string
|
||||
name string
|
||||
value io.ReadCloser
|
||||
}
|
||||
|
||||
type keyNameBuff struct {
|
||||
key string
|
||||
name string
|
||||
value []byte
|
||||
}
|
||||
|
||||
type formDataPayload struct {
|
||||
contentType string
|
||||
Values []keyValuePair
|
||||
Files []keyValuePair
|
||||
ReadClosers []keyNameRC
|
||||
Buffers []keyNameBuff
|
||||
}
|
||||
|
||||
type urlEncodedPayload struct {
|
||||
Values []keyValuePair
|
||||
}
|
||||
|
||||
func newHTTPRequest(url string) *httpRequest {
|
||||
return &httpRequest{URL: url, Client: http.DefaultClient}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (r *httpRequest) setClient(c *http.Client) {
|
||||
r.Client = c
|
||||
}
|
||||
|
||||
func (r *httpRequest) setBasicAuth(user, password string) {
|
||||
r.BasicAuthUser = user
|
||||
r.BasicAuthPassword = password
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
func (f *urlEncodedPayload) getValues() []keyValuePair {
|
||||
return f.Values
|
||||
}
|
||||
|
||||
func (r *httpResponse) parseFromJSON(v interface{}) error {
|
||||
return json.Unmarshal(r.Data, v)
|
||||
}
|
||||
|
||||
func newFormDataPayload() *formDataPayload {
|
||||
return &formDataPayload{}
|
||||
}
|
||||
|
||||
func (f *formDataPayload) getValues() []keyValuePair {
|
||||
return f.Values
|
||||
}
|
||||
|
||||
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) addBuffer(key, file string, buff []byte) {
|
||||
f.Buffers = append(f.Buffers, keyNameBuff{key: key, name: file, value: buff})
|
||||
}
|
||||
|
||||
func (f *formDataPayload) addReadCloser(key, name string, rc io.ReadCloser) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
for _, buff := range f.Buffers {
|
||||
if tmp, err := writer.CreateFormFile(buff.key, buff.name); err == nil {
|
||||
r := bytes.NewReader(buff.value)
|
||||
io.Copy(tmp, r)
|
||||
} 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 (r *httpRequest) addHeader(name, value string) {
|
||||
if r.Headers == nil {
|
||||
r.Headers = make(map[string]string)
|
||||
}
|
||||
r.Headers[name] = value
|
||||
}
|
||||
|
||||
func (r *httpRequest) makeGetRequest(ctx context.Context) (*httpResponse, error) {
|
||||
return r.makeRequest(ctx, "GET", nil)
|
||||
}
|
||||
|
||||
func (r *httpRequest) makePostRequest(ctx context.Context, payload payload) (*httpResponse, error) {
|
||||
return r.makeRequest(ctx, "POST", payload)
|
||||
}
|
||||
|
||||
func (r *httpRequest) makePutRequest(ctx context.Context, payload payload) (*httpResponse, error) {
|
||||
return r.makeRequest(ctx, "PUT", payload)
|
||||
}
|
||||
|
||||
func (r *httpRequest) makeDeleteRequest(ctx context.Context) (*httpResponse, error) {
|
||||
return r.makeRequest(ctx, "DELETE", nil)
|
||||
}
|
||||
|
||||
func (r *httpRequest) NewRequest(ctx context.Context, method string, payload payload) (*http.Request, 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
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
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)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (r *httpRequest) makeRequest(ctx context.Context, method string, payload payload) (*httpResponse, error) {
|
||||
req, err := r.NewRequest(ctx, method, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if Debug {
|
||||
fmt.Println(r.curlString(req, payload))
|
||||
}
|
||||
|
||||
response := httpResponse{}
|
||||
|
||||
resp, err := r.Client.Do(req)
|
||||
if resp != nil {
|
||||
response.Code = resp.StatusCode
|
||||
}
|
||||
if err != nil {
|
||||
if urlErr, ok := err.(*url.Error); ok {
|
||||
if urlErr.Err == io.EOF {
|
||||
return nil, errors.Wrap(err, "remote server prematurely closed connection")
|
||||
}
|
||||
}
|
||||
return nil, errors.Wrap(err, "while making http request")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
responseBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "while reading response body")
|
||||
}
|
||||
|
||||
response.Data = responseBody
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (r *httpRequest) generateUrlWithParameters() (string, error) {
|
||||
url, err := url.Parse(r.URL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !validURL.MatchString(url.Path) {
|
||||
return "", errors.New(`BaseAPI must end with a /v2, /v3 or /v4; setBaseAPI("https://host/v3")`)
|
||||
}
|
||||
|
||||
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) curlString(req *http.Request, p payload) string {
|
||||
|
||||
parts := []string{"curl", "-i", "-X", req.Method, req.URL.String()}
|
||||
for key, value := range req.Header {
|
||||
parts = append(parts, fmt.Sprintf("-H \"%s: %s\"", key, value[0]))
|
||||
}
|
||||
|
||||
//parts = append(parts, fmt.Sprintf(" --user '%s:%s'", r.BasicAuthUser, r.BasicAuthPassword))
|
||||
|
||||
if p != nil {
|
||||
for _, param := range p.getValues() {
|
||||
parts = append(parts, fmt.Sprintf(" -F %s='%s'", param.key, param.value))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package mailgun
|
||||
|
||||
import "context"
|
||||
|
||||
type ipAddressListResponse struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
Items []string `json:"items"`
|
||||
}
|
||||
|
||||
type IPAddress struct {
|
||||
IP string `json:"ip"`
|
||||
RDNS string `json:"rdns"`
|
||||
Dedicated bool `json:"dedicated"`
|
||||
}
|
||||
|
||||
type okResp struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ListIPS returns a list of IPs assigned to your account
|
||||
func (mg *MailgunImpl) ListIPS(ctx context.Context, dedicated bool) ([]IPAddress, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, ipsEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
if dedicated {
|
||||
r.addParameter("dedicated", "true")
|
||||
}
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
var resp ipAddressListResponse
|
||||
if err := getResponseFromJSON(ctx, r, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []IPAddress
|
||||
for _, ip := range resp.Items {
|
||||
result = append(result, IPAddress{IP: ip})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetIP returns information about the specified IP
|
||||
func (mg *MailgunImpl) GetIP(ctx context.Context, ip string) (IPAddress, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, ipsEndpoint) + "/" + ip)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
var resp IPAddress
|
||||
err := getResponseFromJSON(ctx, r, &resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// ListDomainIPS returns a list of IPs currently assigned to the specified domain.
|
||||
func (mg *MailgunImpl) ListDomainIPS(ctx context.Context) ([]IPAddress, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + mg.domain + "/ips")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
var resp ipAddressListResponse
|
||||
if err := getResponseFromJSON(ctx, r, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []IPAddress
|
||||
for _, ip := range resp.Items {
|
||||
result = append(result, IPAddress{IP: ip})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Assign a dedicated IP to the domain specified.
|
||||
func (mg *MailgunImpl) AddDomainIP(ctx context.Context, ip string) error {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + mg.domain + "/ips")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
payload.addValue("ip", ip)
|
||||
_, err := makePostRequest(ctx, r, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
// Unassign an IP from the domain specified.
|
||||
func (mg *MailgunImpl) DeleteDomainIP(ctx context.Context, ip string) error {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + mg.domain + "/ips/" + ip)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package mailgun
|
||||
|
||||
import "context"
|
||||
|
||||
type TagLimits struct {
|
||||
Limit int `json:"limit"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// GetTagLimits returns tracking settings for a domain
|
||||
func (mg *MailgunImpl) GetTagLimits(ctx context.Context, domain string) (TagLimits, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint) + "/" + domain + "/limits/tag")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
var resp TagLimits
|
||||
err := getResponseFromJSON(ctx, r, &resp)
|
||||
return resp, err
|
||||
}
|
|
@ -0,0 +1,392 @@
|
|||
// Package mailgun provides methods for interacting with the Mailgun API. It
|
||||
// automates the HTTP request/response cycle, encodings, and other details
|
||||
// needed by the API. This SDK lets you do everything the API lets you, in a
|
||||
// more Go-friendly way.
|
||||
//
|
||||
// For further information please see the Mailgun documentation at
|
||||
// http://documentation.mailgun.com/
|
||||
//
|
||||
// Original Author: Michael Banzon
|
||||
// Contributions: Samuel A. Falvo II <sam.falvo %at% rackspace.com>
|
||||
// Derrick J. Wippler <thrawn01 %at% gmail.com>
|
||||
//
|
||||
// Examples
|
||||
//
|
||||
// All functions and method have a corresponding test, so if you don't find an
|
||||
// example for a function you'd like to know more about, please check 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.
|
||||
//
|
||||
// List iterators
|
||||
//
|
||||
// Most methods that begin with `List` return an iterator which simplfies
|
||||
// paging through large result sets returned by the mailgun API. Most `List`
|
||||
// methods allow you to specify a `Limit` parameter which as you'd expect,
|
||||
// limits the number of items returned per page. Note that, at present,
|
||||
// Mailgun imposes its own cap of 100 items per page, for all API endpoints.
|
||||
//
|
||||
// For example, the following iterates over all pages of events 100 items at a time
|
||||
//
|
||||
// mg := mailgun.NewMailgun("your-domain.com", "your-api-key")
|
||||
// it := mg.ListEvents(&mailgun.ListEventOptions{Limit: 100})
|
||||
//
|
||||
// // The entire operation should not take longer than 30 seconds
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
// defer cancel()
|
||||
//
|
||||
// // For each page of 100 events
|
||||
// var page []mailgun.Event
|
||||
// for it.Next(ctx, &page) {
|
||||
// for _, e := range page {
|
||||
// // Do something with 'e'
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// License
|
||||
//
|
||||
// Copyright (c) 2013-2019, 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Set true to write the HTTP requests in curl for to stdout
|
||||
var Debug = false
|
||||
|
||||
const (
|
||||
// Base Url the library uses to contact mailgun. Use SetAPIBase() to override
|
||||
APIBase = "https://api.mailgun.net/v3"
|
||||
APIBaseUS = APIBase
|
||||
APIBaseEU = "https://api.eu.mailgun.net/v3"
|
||||
messagesEndpoint = "messages"
|
||||
mimeMessagesEndpoint = "messages.mime"
|
||||
bouncesEndpoint = "bounces"
|
||||
statsTotalEndpoint = "stats/total"
|
||||
domainsEndpoint = "domains"
|
||||
tagsEndpoint = "tags"
|
||||
eventsEndpoint = "events"
|
||||
unsubscribesEndpoint = "unsubscribes"
|
||||
routesEndpoint = "routes"
|
||||
ipsEndpoint = "ips"
|
||||
exportsEndpoint = "exports"
|
||||
webhooksEndpoint = "webhooks"
|
||||
listsEndpoint = "lists"
|
||||
basicAuthUser = "api"
|
||||
templatesEndpoint = "templates"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
APIBase() string
|
||||
Domain() string
|
||||
APIKey() string
|
||||
Client() *http.Client
|
||||
SetClient(client *http.Client)
|
||||
SetAPIBase(url string)
|
||||
|
||||
Send(ctx context.Context, m *Message) (string, string, error)
|
||||
ReSend(ctx context.Context, id string, recipients ...string) (string, string, error)
|
||||
NewMessage(from, subject, text string, to ...string) *Message
|
||||
NewMIMEMessage(body io.ReadCloser, to ...string) *Message
|
||||
|
||||
ListBounces(opts *ListOptions) *BouncesIterator
|
||||
GetBounce(ctx context.Context, address string) (Bounce, error)
|
||||
AddBounce(ctx context.Context, address, code, err string) error
|
||||
DeleteBounce(ctx context.Context, address string) error
|
||||
|
||||
GetStats(ctx context.Context, events []string, opts *GetStatOptions) ([]Stats, error)
|
||||
GetTag(ctx context.Context, tag string) (Tag, error)
|
||||
DeleteTag(ctx context.Context, tag string) error
|
||||
ListTags(*ListTagOptions) *TagIterator
|
||||
|
||||
ListDomains(opts *ListOptions) *DomainsIterator
|
||||
GetDomain(ctx context.Context, domain string) (DomainResponse, error)
|
||||
CreateDomain(ctx context.Context, name string, opts *CreateDomainOptions) (DomainResponse, error)
|
||||
DeleteDomain(ctx context.Context, name string) error
|
||||
VerifyDomain(ctx context.Context, name string) (string, error)
|
||||
UpdateDomainConnection(ctx context.Context, domain string, dc DomainConnection) error
|
||||
GetDomainConnection(ctx context.Context, domain string) (DomainConnection, error)
|
||||
GetDomainTracking(ctx context.Context, domain string) (DomainTracking, error)
|
||||
UpdateClickTracking(ctx context.Context, domain, active string) error
|
||||
UpdateUnsubscribeTracking(ctx context.Context, domain, active, htmlFooter, textFooter string) error
|
||||
UpdateOpenTracking(ctx context.Context, domain, active string) error
|
||||
|
||||
GetStoredMessage(ctx context.Context, url string) (StoredMessage, error)
|
||||
GetStoredMessageRaw(ctx context.Context, id string) (StoredMessageRaw, error)
|
||||
GetStoredAttachment(ctx context.Context, url string) ([]byte, error)
|
||||
|
||||
// Deprecated
|
||||
GetStoredMessageForURL(ctx context.Context, url string) (StoredMessage, error)
|
||||
// Deprecated
|
||||
GetStoredMessageRawForURL(ctx context.Context, url string) (StoredMessageRaw, error)
|
||||
|
||||
ListCredentials(opts *ListOptions) *CredentialsIterator
|
||||
CreateCredential(ctx context.Context, login, password string) error
|
||||
ChangeCredentialPassword(ctx context.Context, login, password string) error
|
||||
DeleteCredential(ctx context.Context, login string) error
|
||||
|
||||
ListUnsubscribes(opts *ListOptions) *UnsubscribesIterator
|
||||
GetUnsubscribe(ctx context.Context, address string) (Unsubscribe, error)
|
||||
CreateUnsubscribe(ctx context.Context, address, tag string) error
|
||||
DeleteUnsubscribe(ctx context.Context, address string) error
|
||||
DeleteUnsubscribeWithTag(ctx context.Context, a, t string) error
|
||||
|
||||
ListComplaints(opts *ListOptions) *ComplaintsIterator
|
||||
GetComplaint(ctx context.Context, address string) (Complaint, error)
|
||||
CreateComplaint(ctx context.Context, address string) error
|
||||
DeleteComplaint(ctx context.Context, address string) error
|
||||
|
||||
ListRoutes(opts *ListOptions) *RoutesIterator
|
||||
GetRoute(ctx context.Context, address string) (Route, error)
|
||||
CreateRoute(ctx context.Context, address Route) (Route, error)
|
||||
DeleteRoute(ctx context.Context, address string) error
|
||||
UpdateRoute(ctx context.Context, address string, r Route) (Route, error)
|
||||
|
||||
ListWebhooks(ctx context.Context) (map[string][]string, error)
|
||||
CreateWebhook(ctx context.Context, kind string, url []string) error
|
||||
DeleteWebhook(ctx context.Context, kind string) error
|
||||
GetWebhook(ctx context.Context, kind string) ([]string, error)
|
||||
UpdateWebhook(ctx context.Context, kind string, url []string) error
|
||||
VerifyWebhookRequest(req *http.Request) (verified bool, err error)
|
||||
VerifyWebhookSignature(sig Signature) (verified bool, err error)
|
||||
|
||||
ListMailingLists(opts *ListOptions) *ListsIterator
|
||||
CreateMailingList(ctx context.Context, address MailingList) (MailingList, error)
|
||||
DeleteMailingList(ctx context.Context, address string) error
|
||||
GetMailingList(ctx context.Context, address string) (MailingList, error)
|
||||
UpdateMailingList(ctx context.Context, address string, ml MailingList) (MailingList, error)
|
||||
|
||||
ListMembers(address string, opts *ListOptions) *MemberListIterator
|
||||
GetMember(ctx context.Context, MemberAddr, listAddr string) (Member, error)
|
||||
CreateMember(ctx context.Context, merge bool, addr string, prototype Member) error
|
||||
CreateMemberList(ctx context.Context, subscribed *bool, addr string, newMembers []interface{}) error
|
||||
UpdateMember(ctx context.Context, Member, list string, prototype Member) (Member, error)
|
||||
DeleteMember(ctx context.Context, Member, list string) error
|
||||
|
||||
ListEvents(*ListEventOptions) *EventIterator
|
||||
PollEvents(*ListEventOptions) *EventPoller
|
||||
|
||||
ListIPS(ctx context.Context, dedicated bool) ([]IPAddress, error)
|
||||
GetIP(ctx context.Context, ip string) (IPAddress, error)
|
||||
ListDomainIPS(ctx context.Context) ([]IPAddress, error)
|
||||
AddDomainIP(ctx context.Context, ip string) error
|
||||
DeleteDomainIP(ctx context.Context, ip string) error
|
||||
|
||||
ListExports(ctx context.Context, url string) ([]Export, error)
|
||||
GetExport(ctx context.Context, id string) (Export, error)
|
||||
GetExportLink(ctx context.Context, id string) (string, error)
|
||||
CreateExport(ctx context.Context, url string) error
|
||||
|
||||
GetTagLimits(ctx context.Context, domain string) (TagLimits, error)
|
||||
|
||||
CreateTemplate(ctx context.Context, template *Template) error
|
||||
GetTemplate(ctx context.Context, name string) (Template, error)
|
||||
UpdateTemplate(ctx context.Context, template *Template) error
|
||||
DeleteTemplate(ctx context.Context, name string) error
|
||||
ListTemplates(opts *ListTemplateOptions) *TemplatesIterator
|
||||
|
||||
AddTemplateVersion(ctx context.Context, templateName string, version *TemplateVersion) error
|
||||
GetTemplateVersion(ctx context.Context, templateName, tag string) (TemplateVersion, error)
|
||||
UpdateTemplateVersion(ctx context.Context, templateName string, version *TemplateVersion) error
|
||||
DeleteTemplateVersion(ctx context.Context, templateName, tag string) error
|
||||
ListTemplateVersions(templateName string, opts *ListOptions) *TemplateVersionsIterator
|
||||
}
|
||||
|
||||
// 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 {
|
||||
apiBase string
|
||||
domain string
|
||||
apiKey string
|
||||
client *http.Client
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// NewMailGun creates a new client instance.
|
||||
func NewMailgun(domain, apiKey string) *MailgunImpl {
|
||||
return &MailgunImpl{
|
||||
apiBase: APIBase,
|
||||
domain: domain,
|
||||
apiKey: apiKey,
|
||||
client: http.DefaultClient,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMailgunFromEnv returns a new Mailgun client using the environment variables
|
||||
// MG_API_KEY, MG_DOMAIN, and MG_URL
|
||||
func NewMailgunFromEnv() (*MailgunImpl, error) {
|
||||
apiKey := os.Getenv("MG_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil, errors.New("required environment variable MG_API_KEY not defined")
|
||||
}
|
||||
domain := os.Getenv("MG_DOMAIN")
|
||||
if domain == "" {
|
||||
return nil, errors.New("required environment variable MG_DOMAIN not defined")
|
||||
}
|
||||
|
||||
mg := NewMailgun(domain, apiKey)
|
||||
|
||||
url := os.Getenv("MG_URL")
|
||||
if url != "" {
|
||||
mg.SetAPIBase(url)
|
||||
}
|
||||
|
||||
return mg, nil
|
||||
}
|
||||
|
||||
// APIBase returns the API Base URL configured for this client.
|
||||
func (mg *MailgunImpl) APIBase() string {
|
||||
return mg.apiBase
|
||||
}
|
||||
|
||||
// Domain returns the domain configured for this client.
|
||||
func (mg *MailgunImpl) Domain() string {
|
||||
return mg.domain
|
||||
}
|
||||
|
||||
// ApiKey returns the API key configured for this client.
|
||||
func (mg *MailgunImpl) APIKey() string {
|
||||
return mg.apiKey
|
||||
}
|
||||
|
||||
// Client returns the HTTP client configured for this client.
|
||||
func (mg *MailgunImpl) Client() *http.Client {
|
||||
return mg.client
|
||||
}
|
||||
|
||||
// SetClient updates the HTTP client for this client.
|
||||
func (mg *MailgunImpl) SetClient(c *http.Client) {
|
||||
mg.client = c
|
||||
}
|
||||
|
||||
// SetAPIBase updates the API Base URL for this client.
|
||||
// // For EU Customers
|
||||
// mg.SetAPIBase(mailgun.APIBaseEU)
|
||||
//
|
||||
// // For US Customers
|
||||
// mg.SetAPIBase(mailgun.APIBaseUS)
|
||||
//
|
||||
// // Set a custom base API
|
||||
// mg.SetAPIBase("https://localhost/v3")
|
||||
func (mg *MailgunImpl) SetAPIBase(address string) {
|
||||
mg.apiBase = address
|
||||
}
|
||||
|
||||
// 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", m.APIBase(), m.Domain(), endpoint)
|
||||
}
|
||||
|
||||
// generateApiUrlWithDomain renders a URL for an API endpoint using a separate domain and endpoint name.
|
||||
func generateApiUrlWithDomain(m Mailgun, endpoint, domain string) string {
|
||||
return fmt.Sprintf("%s/%s/%s", m.APIBase(), domain, endpoint)
|
||||
}
|
||||
|
||||
// generateMemberApiUrl renders a URL relevant for specifying mailing list members.
|
||||
// The address parameter refers to the mailing list in question.
|
||||
func generateMemberApiUrl(m Mailgun, endpoint, address string) string {
|
||||
return fmt.Sprintf("%s/%s/%s/members", m.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", m.APIBase(), m.Domain(), endpoint)
|
||||
}
|
||||
|
||||
// generateCredentialsUrl renders a URL as generateDomainApiUrl,
|
||||
// but focuses on the SMTP credentials family of API functions.
|
||||
func generateCredentialsUrl(m Mailgun, login string) string {
|
||||
tail := ""
|
||||
if login != "" {
|
||||
tail = fmt.Sprintf("/%s", login)
|
||||
}
|
||||
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(m Mailgun, endpoint string) string {
|
||||
return fmt.Sprintf("%s/%s", m.APIBase(), endpoint)
|
||||
}
|
||||
|
||||
// generateParameterizedUrl works as generateApiUrl, but supports query parameters.
|
||||
func generateParameterizedUrl(m Mailgun, endpoint string, payload 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")
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// A mailing list may have one of three membership modes.
|
||||
const (
|
||||
// ReadOnly specifies that nobody, including Members, may send messages to
|
||||
// the mailing list. Messages distributed on such lists come from list
|
||||
// administrator accounts only.
|
||||
AccessLevelReadOnly = "readonly"
|
||||
// Members specifies that only those who subscribe to the mailing list may
|
||||
// send messages.
|
||||
AccessLevelMembers = "members"
|
||||
// Everyone specifies that anyone and everyone may both read and submit
|
||||
// messages to the mailing list, including non-subscribers.
|
||||
AccessLevelEveryone = "everyone"
|
||||
)
|
||||
|
||||
// Specify the access of a mailing list member
|
||||
type AccessLevel string
|
||||
|
||||
// A List structure provides information for a mailing list.
|
||||
//
|
||||
// AccessLevel may be one of ReadOnly, Members, or Everyone.
|
||||
type MailingList struct {
|
||||
Address string `json:"address,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AccessLevel AccessLevel `json:"access_level,omitempty"`
|
||||
CreatedAt RFC2822Time `json:"created_at,omitempty"`
|
||||
MembersCount int `json:"members_count,omitempty"`
|
||||
}
|
||||
|
||||
type listsResponse struct {
|
||||
Items []MailingList `json:"items"`
|
||||
Paging Paging `json:"paging"`
|
||||
}
|
||||
|
||||
type mailingListResponse struct {
|
||||
MailingList MailingList `json:"member"`
|
||||
}
|
||||
|
||||
type ListsIterator struct {
|
||||
listsResponse
|
||||
mg Mailgun
|
||||
err error
|
||||
}
|
||||
|
||||
// ListMailingLists returns the specified set of mailing lists administered by your account.
|
||||
func (mg *MailgunImpl) ListMailingLists(opts *ListOptions) *ListsIterator {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, listsEndpoint) + "/pages")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
if opts != nil {
|
||||
if opts.Limit != 0 {
|
||||
r.addParameter("limit", strconv.Itoa(opts.Limit))
|
||||
}
|
||||
}
|
||||
url, err := r.generateUrlWithParameters()
|
||||
return &ListsIterator{
|
||||
mg: mg,
|
||||
listsResponse: listsResponse{Paging: Paging{Next: url, First: url}},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (li *ListsIterator) Err() error {
|
||||
return li.err
|
||||
}
|
||||
|
||||
// Next retrieves the next page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (li *ListsIterator) Next(ctx context.Context, items *[]MailingList) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.Next)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]MailingList, len(li.Items))
|
||||
copy(cpy, li.Items)
|
||||
*items = cpy
|
||||
if len(li.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of items from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (li *ListsIterator) First(ctx context.Context, items *[]MailingList) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.First)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]MailingList, len(li.Items))
|
||||
copy(cpy, li.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of items from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (li *ListsIterator) Last(ctx context.Context, items *[]MailingList) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.Last)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]MailingList, len(li.Items))
|
||||
copy(cpy, li.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (li *ListsIterator) Previous(ctx context.Context, items *[]MailingList) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
if li.Paging.Previous == "" {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.Previous)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]MailingList, len(li.Items))
|
||||
copy(cpy, li.Items)
|
||||
*items = cpy
|
||||
if len(li.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (li *ListsIterator) fetch(ctx context.Context, url string) error {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(li.mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, li.mg.APIKey())
|
||||
|
||||
return getResponseFromJSON(ctx, r, &li.listsResponse)
|
||||
}
|
||||
|
||||
// CreateMailingList 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) CreateMailingList(ctx context.Context, prototype MailingList) (MailingList, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, listsEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := 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", string(prototype.AccessLevel))
|
||||
}
|
||||
response, err := makePostRequest(ctx, r, p)
|
||||
if err != nil {
|
||||
return MailingList{}, err
|
||||
}
|
||||
var l MailingList
|
||||
err = response.parseFromJSON(&l)
|
||||
return l, err
|
||||
}
|
||||
|
||||
// DeleteMailingList 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) DeleteMailingList(ctx context.Context, addr string) error {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, listsEndpoint) + "/" + addr)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetMailingList 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) GetMailingList(ctx context.Context, addr string) (MailingList, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, listsEndpoint) + "/" + addr)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
response, err := makeGetRequest(ctx, r)
|
||||
if err != nil {
|
||||
return MailingList{}, err
|
||||
}
|
||||
|
||||
var resp mailingListResponse
|
||||
err = response.parseFromJSON(&resp)
|
||||
return resp.MailingList, err
|
||||
}
|
||||
|
||||
// UpdateMailingList 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) UpdateMailingList(ctx context.Context, addr string, prototype MailingList) (MailingList, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, listsEndpoint) + "/" + addr)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := 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", string(prototype.AccessLevel))
|
||||
}
|
||||
var l MailingList
|
||||
response, err := makePutRequest(ctx, r, p)
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
err = response.parseFromJSON(&l)
|
||||
return l, err
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
type memberListResponse struct {
|
||||
Lists []Member `json:"items"`
|
||||
Paging Paging `json:"paging"`
|
||||
}
|
||||
|
||||
type memberResponse struct {
|
||||
Member Member `json:"member"`
|
||||
}
|
||||
|
||||
type MemberListIterator struct {
|
||||
memberListResponse
|
||||
mg Mailgun
|
||||
err error
|
||||
}
|
||||
|
||||
// Used by List methods to specify what list parameters to send to the mailgun API
|
||||
type ListOptions struct {
|
||||
Limit int
|
||||
}
|
||||
|
||||
func (mg *MailgunImpl) ListMembers(address string, opts *ListOptions) *MemberListIterator {
|
||||
r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, address) + "/pages")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
if opts != nil {
|
||||
if opts.Limit != 0 {
|
||||
r.addParameter("limit", strconv.Itoa(opts.Limit))
|
||||
}
|
||||
}
|
||||
url, err := r.generateUrlWithParameters()
|
||||
return &MemberListIterator{
|
||||
mg: mg,
|
||||
memberListResponse: memberListResponse{Paging: Paging{Next: url, First: url}},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (li *MemberListIterator) Err() error {
|
||||
return li.err
|
||||
}
|
||||
|
||||
// Next retrieves the next page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (li *MemberListIterator) Next(ctx context.Context, items *[]Member) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.Next)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
*items = li.Lists
|
||||
if len(li.Lists) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of items from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (li *MemberListIterator) First(ctx context.Context, items *[]Member) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.First)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
*items = li.Lists
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of items from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (li *MemberListIterator) Last(ctx context.Context, items *[]Member) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.Last)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
*items = li.Lists
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (li *MemberListIterator) Previous(ctx context.Context, items *[]Member) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
if li.Paging.Previous == "" {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.Previous)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
*items = li.Lists
|
||||
if len(li.Lists) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (li *MemberListIterator) fetch(ctx context.Context, url string) error {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(li.mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, li.mg.APIKey())
|
||||
|
||||
return getResponseFromJSON(ctx, r, &li.memberListResponse)
|
||||
}
|
||||
|
||||
// GetMember returns a complete Member structure for a member of a mailing list,
|
||||
// given only their subscription e-mail address.
|
||||
func (mg *MailgunImpl) GetMember(ctx context.Context, s, l string) (Member, error) {
|
||||
r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, l) + "/" + s)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
response, err := makeGetRequest(ctx, r)
|
||||
if err != nil {
|
||||
return Member{}, err
|
||||
}
|
||||
var resp memberResponse
|
||||
err = response.parseFromJSON(&resp)
|
||||
return resp.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(ctx context.Context, merge bool, addr string, prototype Member) error {
|
||||
vs, err := json.Marshal(prototype.Vars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, addr))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := 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(ctx, 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(ctx context.Context, s, l string, prototype Member) (Member, error) {
|
||||
r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, l) + "/" + s)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := 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(ctx, 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(ctx context.Context, member, addr string) error {
|
||||
r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, addr) + "/" + member)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateMemberList registers multiple Members and non-Member members to a single mailing list
|
||||
// in a single round-trip.
|
||||
// u indicates if the existing members should be updated or duplicates should be updated.
|
||||
// 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(ctx context.Context, u *bool, addr string, newMembers []interface{}) error {
|
||||
r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, addr) + ".json")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := newFormDataPayload()
|
||||
if u != nil {
|
||||
p.addValue("upsert", yesNo(*u))
|
||||
}
|
||||
bs, err := json.Marshal(newMembers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.addValue("members", string(bs))
|
||||
_, err = makePostRequest(ctx, r, p)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,802 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// MaxNumberOfTags represents the maximum number of tags that can be added for a message
|
||||
const MaxNumberOfTags = 3
|
||||
|
||||
// Message structures contain both the message text and the 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
|
||||
readerInlines []ReaderAttachment
|
||||
bufferAttachments []BufferAttachment
|
||||
|
||||
nativeSend bool
|
||||
testMode bool
|
||||
tracking bool
|
||||
trackingClicks bool
|
||||
trackingOpens bool
|
||||
headers map[string]string
|
||||
variables map[string]string
|
||||
templateVariables map[string]interface{}
|
||||
recipientVariables map[string]map[string]interface{}
|
||||
domain string
|
||||
|
||||
dkimSet bool
|
||||
trackingSet bool
|
||||
trackingClicksSet bool
|
||||
trackingOpensSet bool
|
||||
requireTLS bool
|
||||
skipVerification bool
|
||||
|
||||
specific features
|
||||
mg Mailgun
|
||||
}
|
||||
|
||||
type ReaderAttachment struct {
|
||||
Filename string
|
||||
ReadCloser io.ReadCloser
|
||||
}
|
||||
|
||||
type BufferAttachment struct {
|
||||
Filename string
|
||||
Buffer []byte
|
||||
}
|
||||
|
||||
// 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]struct {
|
||||
Url string `json:"url"`
|
||||
ContentType string `json:"content-type"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
} `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
|
||||
template 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(*formDataPayload)
|
||||
isValid() bool
|
||||
endpoint() string
|
||||
recipientCount() int
|
||||
setTemplate(string)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
// AddBufferAttachment arranges to send a file along with the e-mail message.
|
||||
// File contents are read from the []byte array provided
|
||||
// The filename parameter is the resulting filename of the attachment.
|
||||
// The buffer parameter is the []byte array which contains the actual bytes to be used
|
||||
// as the contents of the attached file.
|
||||
func (m *Message) AddBufferAttachment(filename string, buffer []byte) {
|
||||
ba := BufferAttachment{Filename: filename, Buffer: buffer}
|
||||
m.bufferAttachments = append(m.bufferAttachments, ba)
|
||||
}
|
||||
|
||||
// AddAttachment arranges to send a file from the filesystem along with the e-mail message.
|
||||
// The attachment parameter is a filename, which must refer to a file which actually resides
|
||||
// in the local filesystem.
|
||||
func (m *Message) AddAttachment(attachment string) {
|
||||
m.attachments = append(m.attachments, attachment)
|
||||
}
|
||||
|
||||
// AddReaderInline arranges to send a file along with the e-mail message.
|
||||
// File contents are read from a io.ReadCloser.
|
||||
// The filename parameter is the resulting filename of the attachment.
|
||||
// The readCloser parameter is the io.ReadCloser which reads the actual bytes to be used
|
||||
// as the contents of the attached file.
|
||||
func (m *Message) AddReaderInline(filename string, readCloser io.ReadCloser) {
|
||||
ra := ReaderAttachment{Filename: filename, ReadCloser: readCloser}
|
||||
m.readerInlines = append(m.readerInlines, ra)
|
||||
}
|
||||
|
||||
// AddInline arranges to send a file along with the e-mail message, but does so
|
||||
// in a way that its data remains "inline" with the rest of the message. This
|
||||
// can be used to send image or font data along with an HTML-encoded message body.
|
||||
// The attachment parameter is a filename, which must refer to a file which actually resides
|
||||
// in the local filesystem.
|
||||
func (m *Message) AddInline(inline string) {
|
||||
m.inlines = append(m.inlines, inline)
|
||||
}
|
||||
|
||||
// AddRecipient appends a receiver to the To: header of a message.
|
||||
// It will return an error if the limit of recipients have been exceeded for this message
|
||||
func (m *Message) AddRecipient(recipient string) error {
|
||||
return m.AddRecipientAndVariables(recipient, nil)
|
||||
}
|
||||
|
||||
// AddRecipientAndVariables appends a receiver to the To: header of a message,
|
||||
// and as well attaches a set of variables relevant for this recipient.
|
||||
// It will return an error if the limit of recipients have been exceeded for this message
|
||||
func (m *Message) AddRecipientAndVariables(r string, vars map[string]interface{}) error {
|
||||
if m.RecipientCount() >= MaxNumberOfRecipients {
|
||||
return fmt.Errorf("recipient limit exceeded (max %d)", MaxNumberOfRecipients)
|
||||
}
|
||||
m.to = append(m.to, r)
|
||||
if vars != nil {
|
||||
if m.recipientVariables == nil {
|
||||
m.recipientVariables = make(map[string]map[string]interface{})
|
||||
}
|
||||
m.recipientVariables[r] = vars
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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(ctx context.Context) (string, string, error) {
|
||||
return m.mg.Send(ctx, m)
|
||||
}
|
||||
|
||||
// SetReplyTo sets the receiver who should receive replies
|
||||
func (m *Message) SetReplyTo(recipient string) {
|
||||
m.AddHeader("Reply-To", recipient)
|
||||
}
|
||||
|
||||
// AddCC appends a receiver to the carbon-copy header of a message.
|
||||
func (m *Message) AddCC(recipient string) {
|
||||
m.specific.addCC(recipient)
|
||||
}
|
||||
|
||||
func (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) {}
|
||||
|
||||
// SetHtml is a helper. If you're sending a message that isn't already MIME encoded, SetHtml() will arrange to bundle
|
||||
// an HTML representation of your message in addition to your plain-text body.
|
||||
func (m *Message) SetHtml(html string) {
|
||||
m.specific.setHtml(html)
|
||||
}
|
||||
|
||||
func (pm *plainMessage) setHtml(h string) {
|
||||
pm.html = h
|
||||
}
|
||||
|
||||
func (mm *mimeMessage) setHtml(_ string) {}
|
||||
|
||||
// AddTag attaches tags to the message. Tags are useful for metrics gathering and event tracking purposes.
|
||||
// Refer to the Mailgun documentation for further details.
|
||||
func (m *Message) AddTag(tag ...string) error {
|
||||
if len(m.tags) >= MaxNumberOfTags {
|
||||
return fmt.Errorf("cannot add any new tags. Message tag limit (%d) reached", MaxNumberOfTags)
|
||||
}
|
||||
|
||||
m.tags = append(m.tags, tag...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTemplate sets the name of a template stored via the template API.
|
||||
// See https://documentation.mailgun.com/en/latest/user_manual.html#templating
|
||||
func (m *Message) SetTemplate(t string) {
|
||||
m.specific.setTemplate(t)
|
||||
}
|
||||
|
||||
func (pm *plainMessage) setTemplate(t string) {
|
||||
pm.template = t
|
||||
}
|
||||
|
||||
func (mm *mimeMessage) setTemplate(t string) {}
|
||||
|
||||
// AddCampaign is no longer supported and 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
|
||||
}
|
||||
|
||||
// EnableNativeSend allows the return path to match the address in the Message.Headers.From:
|
||||
// field when sending from Mailgun rather than the usual bounce+ address in the return path.
|
||||
func (m *Message) EnableNativeSend() {
|
||||
m.nativeSend = true
|
||||
}
|
||||
|
||||
// EnableTestMode allows submittal of a message, such that it will be discarded by Mailgun.
|
||||
// This facilitates testing client-side software without actually consuming e-mail resources.
|
||||
func (m *Message) EnableTestMode() {
|
||||
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) {
|
||||
m.deliveryTime = dt
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// SetTrackingClicks information is found in the Mailgun documentation.
|
||||
func (m *Message) SetTrackingClicks(trackingClicks bool) {
|
||||
m.trackingClicks = trackingClicks
|
||||
m.trackingClicksSet = true
|
||||
}
|
||||
|
||||
// SetRequireTLS information is found in the Mailgun documentation.
|
||||
func (m *Message) SetRequireTLS(b bool) {
|
||||
m.requireTLS = b
|
||||
}
|
||||
|
||||
// SetSkipVerification information is found in the Mailgun documentation.
|
||||
func (m *Message) SetSkipVerification(b bool) {
|
||||
m.skipVerification = b
|
||||
}
|
||||
|
||||
//SetTrackingOpens information is found in the Mailgun documentation.
|
||||
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 {
|
||||
if m.variables == nil {
|
||||
m.variables = make(map[string]string)
|
||||
}
|
||||
|
||||
j, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoded := string(j)
|
||||
v, err := strconv.Unquote(encoded)
|
||||
if err != nil {
|
||||
v = encoded
|
||||
}
|
||||
|
||||
m.variables[variable] = v
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTemplateVariable adds a template variable to the map of template variables, replacing the variable if it is already there.
|
||||
// This is used for server-side message templates and can nest arbitrary values. At send time, the resulting map will be converted into
|
||||
// a JSON string and sent as a header in the X-Mailgun-Variables header.
|
||||
func (m *Message) AddTemplateVariable(variable string, value interface{}) error {
|
||||
if m.templateVariables == nil {
|
||||
m.templateVariables = make(map[string]interface{})
|
||||
}
|
||||
m.templateVariables[variable] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddDomain allows you to use a separate domain for the type of messages you are sending.
|
||||
func (m *Message) AddDomain(domain string) {
|
||||
m.domain = domain
|
||||
}
|
||||
|
||||
// GetHeaders retrieves the http headers associated with this message
|
||||
func (m *Message) GetHeaders() map[string]string {
|
||||
return m.headers
|
||||
}
|
||||
|
||||
// ErrInvalidMessage is returned by `Send()` when the `mailgun.Message` struct is incomplete
|
||||
var ErrInvalidMessage = errors.New("message not valid")
|
||||
|
||||
// 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 (mg *MailgunImpl) Send(ctx context.Context, message *Message) (mes string, id string, err error) {
|
||||
if mg.domain == "" {
|
||||
err = errors.New("you must provide a valid domain before calling Send()")
|
||||
return
|
||||
}
|
||||
|
||||
if mg.apiKey == "" {
|
||||
err = errors.New("you must provide a valid api-key before calling Send()")
|
||||
return
|
||||
}
|
||||
|
||||
if !isValid(message) {
|
||||
err = ErrInvalidMessage
|
||||
return
|
||||
}
|
||||
payload := 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.IsZero() {
|
||||
payload.addValue("o:deliverytime", formatMailgunTime(message.deliveryTime))
|
||||
}
|
||||
if message.nativeSend {
|
||||
payload.addValue("o:native-send", "yes")
|
||||
}
|
||||
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.requireTLS {
|
||||
payload.addValue("o:require-tls", trueFalse(message.requireTLS))
|
||||
}
|
||||
if message.skipVerification {
|
||||
payload.addValue("o:skip-verification", trueFalse(message.skipVerification))
|
||||
}
|
||||
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.templateVariables != nil {
|
||||
variableString, err := json.Marshal(message.templateVariables)
|
||||
if err == nil {
|
||||
// the map was marshalled as json so add it
|
||||
payload.addValue("h:X-Mailgun-Variables", string(variableString))
|
||||
}
|
||||
}
|
||||
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.bufferAttachments != nil {
|
||||
for _, bufferAttachment := range message.bufferAttachments {
|
||||
payload.addBuffer("attachment", bufferAttachment.Filename, bufferAttachment.Buffer)
|
||||
}
|
||||
}
|
||||
if message.inlines != nil {
|
||||
for _, inline := range message.inlines {
|
||||
payload.addFile("inline", inline)
|
||||
}
|
||||
}
|
||||
|
||||
if message.readerInlines != nil {
|
||||
for _, readerAttachment := range message.readerInlines {
|
||||
payload.addReadCloser("inline", readerAttachment.Filename, readerAttachment.ReadCloser)
|
||||
}
|
||||
}
|
||||
|
||||
if message.domain == "" {
|
||||
message.domain = mg.Domain()
|
||||
}
|
||||
|
||||
r := newHTTPRequest(generateApiUrlWithDomain(mg, message.specific.endpoint(), message.domain))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
var response sendMessageResponse
|
||||
err = postResponseFromJSON(ctx, r, payload, &response)
|
||||
if err == nil {
|
||||
mes = response.Message
|
||||
id = response.Id
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (pm *plainMessage) addValues(p *formDataPayload) {
|
||||
p.addValue("from", pm.from)
|
||||
p.addValue("subject", pm.subject)
|
||||
p.addValue("text", pm.text)
|
||||
for _, cc := range pm.cc {
|
||||
p.addValue("cc", cc)
|
||||
}
|
||||
for _, bcc := range pm.bcc {
|
||||
p.addValue("bcc", bcc)
|
||||
}
|
||||
if pm.html != "" {
|
||||
p.addValue("html", pm.html)
|
||||
}
|
||||
if pm.template != "" {
|
||||
p.addValue("template", pm.template)
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *mimeMessage) addValues(p *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"
|
||||
}
|
||||
return "no"
|
||||
}
|
||||
|
||||
func trueFalse(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// 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 m.RecipientCount() == 0 {
|
||||
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.template != "" {
|
||||
// pm.text or pm.html not needed if template is supplied
|
||||
return true
|
||||
}
|
||||
|
||||
if pm.text == "" && pm.html == "" {
|
||||
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(ctx context.Context, url string) (StoredMessage, error) {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
var response StoredMessage
|
||||
err := getResponseFromJSON(ctx, r, &response)
|
||||
return response, err
|
||||
}
|
||||
|
||||
// Given a storage id resend the stored message to the specified recipients
|
||||
func (mg *MailgunImpl) ReSend(ctx context.Context, url string, recipients ...string) (string, string, error) {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newFormDataPayload()
|
||||
|
||||
if len(recipients) == 0 {
|
||||
return "", "", errors.New("must provide at least one recipient")
|
||||
}
|
||||
|
||||
for _, to := range recipients {
|
||||
payload.addValue("to", to)
|
||||
}
|
||||
|
||||
var resp sendMessageResponse
|
||||
err := postResponseFromJSON(ctx, r, payload, &resp)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return resp.Message, resp.Id, nil
|
||||
|
||||
}
|
||||
|
||||
// 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(ctx context.Context, url string) (StoredMessageRaw, error) {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
r.addHeader("Accept", "message/rfc2822")
|
||||
|
||||
var response StoredMessageRaw
|
||||
err := getResponseFromJSON(ctx, r, &response)
|
||||
return response, err
|
||||
}
|
||||
|
||||
// Deprecated: Use GetStoreMessage() instead
|
||||
func (mg *MailgunImpl) GetStoredMessageForURL(ctx context.Context, url string) (StoredMessage, error) {
|
||||
return mg.GetStoredMessage(ctx, url)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetStoreMessageRaw() instead
|
||||
func (mg *MailgunImpl) GetStoredMessageRawForURL(ctx context.Context, url string) (StoredMessageRaw, error) {
|
||||
return mg.GetStoredMessageRaw(ctx, url)
|
||||
}
|
||||
|
||||
// GetStoredAttachment retrieves the raw MIME body of a received e-mail message attachment.
|
||||
func (mg *MailgunImpl) GetStoredAttachment(ctx context.Context, url string) ([]byte, error) {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
r.addHeader("Accept", "message/rfc2822")
|
||||
|
||||
response, err := makeGetRequest(ctx, r)
|
||||
|
||||
return response.Data, err
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
// A mailgun api mock suitable for testing
|
||||
type MockServer struct {
|
||||
srv *httptest.Server
|
||||
|
||||
domainIPS []string
|
||||
domainList []domainContainer
|
||||
exportList []Export
|
||||
mailingList []mailingListContainer
|
||||
routeList []Route
|
||||
events []Event
|
||||
webhooks WebHooksListResponse
|
||||
}
|
||||
|
||||
// Create a new instance of the mailgun API mock server
|
||||
func NewMockServer() MockServer {
|
||||
ms := MockServer{}
|
||||
|
||||
// Add all our handlers
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/v3", func(r chi.Router) {
|
||||
ms.addIPRoutes(r)
|
||||
ms.addExportRoutes(r)
|
||||
ms.addDomainRoutes(r)
|
||||
ms.addMailingListRoutes(r)
|
||||
ms.addEventRoutes(r)
|
||||
ms.addMessagesRoutes(r)
|
||||
ms.addValidationRoutes(r)
|
||||
ms.addRoutes(r)
|
||||
ms.addWebhookRoutes(r)
|
||||
})
|
||||
|
||||
// Start the server
|
||||
ms.srv = httptest.NewServer(r)
|
||||
return ms
|
||||
}
|
||||
|
||||
// Stop the server
|
||||
func (ms *MockServer) Stop() {
|
||||
ms.srv.Close()
|
||||
}
|
||||
|
||||
// URL returns the URL used to connect to the mock server
|
||||
func (ms *MockServer) URL() string {
|
||||
return ms.srv.URL + "/v3"
|
||||
}
|
||||
|
||||
func toJSON(w http.ResponseWriter, obj interface{}) {
|
||||
if err := json.NewEncoder(w).Encode(obj); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
func stringToBool(v string) bool {
|
||||
lower := strings.ToLower(v)
|
||||
if lower == "yes" || lower == "no" {
|
||||
return lower == "yes"
|
||||
}
|
||||
|
||||
if v == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
result, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func stringToInt(v string) int {
|
||||
if v == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
result, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return int(result)
|
||||
}
|
||||
|
||||
func stringToMap(v string) map[string]interface{} {
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(v), &result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseAddress(v string) string {
|
||||
if v == "" {
|
||||
return ""
|
||||
}
|
||||
e, err := mail.ParseAddress(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return e.Address
|
||||
}
|
||||
|
||||
// Given the page direction, pivot value and limit, calculate the offsets for the slice
|
||||
func pageOffsets(pivotIdx []string, pivotDir, pivotVal string, limit int) (int, int) {
|
||||
switch pivotDir {
|
||||
case "first":
|
||||
if limit < len(pivotIdx) {
|
||||
return 0, limit
|
||||
}
|
||||
return 0, len(pivotIdx)
|
||||
case "last":
|
||||
if limit < len(pivotIdx) {
|
||||
return len(pivotIdx) - limit, len(pivotIdx)
|
||||
}
|
||||
return 0, len(pivotIdx)
|
||||
case "next":
|
||||
for i, item := range pivotIdx {
|
||||
if item == pivotVal {
|
||||
offset := i + 1 + limit
|
||||
if offset > len(pivotIdx) {
|
||||
offset = len(pivotIdx)
|
||||
}
|
||||
return i + 1, offset
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
case "prev":
|
||||
for i, item := range pivotIdx {
|
||||
if item == pivotVal {
|
||||
if i == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
offset := i - limit
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
return offset, i
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
if limit > len(pivotIdx) {
|
||||
return 0, len(pivotIdx)
|
||||
}
|
||||
return 0, limit
|
||||
}
|
||||
|
||||
func getPageURL(r *http.Request, params url.Values) string {
|
||||
if r.FormValue("limit") != "" {
|
||||
params.Add("limit", r.FormValue("limit"))
|
||||
}
|
||||
return "http://" + r.Host + r.URL.EscapedPath() + "?" + params.Encode()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func randomEmail(prefix, domain string) string {
|
||||
return strings.ToLower(fmt.Sprintf("%s@%s", randomString(20, prefix), domain))
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
type domainContainer struct {
|
||||
Domain Domain `json:"domain"`
|
||||
ReceivingDNSRecords []DNSRecord `json:"receiving_dns_records"`
|
||||
SendingDNSRecords []DNSRecord `json:"sending_dns_records"`
|
||||
Connection *DomainConnection `json:"connection,omitempty"`
|
||||
Tracking *DomainTracking `json:"tracking,omitempty"`
|
||||
TagLimits *TagLimits `json:"limits,omitempty"`
|
||||
}
|
||||
|
||||
func (ms *MockServer) addDomainRoutes(r chi.Router) {
|
||||
|
||||
ms.domainList = append(ms.domainList, domainContainer{
|
||||
Domain: Domain{
|
||||
CreatedAt: RFC2822Time(time.Now().UTC()),
|
||||
Name: "mailgun.test",
|
||||
SMTPLogin: "postmaster@mailgun.test",
|
||||
SMTPPassword: "4rtqo4p6rrx9",
|
||||
Wildcard: true,
|
||||
SpamAction: SpamActionDisabled,
|
||||
State: "active",
|
||||
},
|
||||
Connection: &DomainConnection{
|
||||
RequireTLS: true,
|
||||
SkipVerification: true,
|
||||
},
|
||||
TagLimits: &TagLimits{
|
||||
Limit: 50000,
|
||||
Count: 5000,
|
||||
},
|
||||
Tracking: &DomainTracking{
|
||||
Click: TrackingStatus{Active: true},
|
||||
Open: TrackingStatus{Active: true},
|
||||
Unsubscribe: TrackingStatus{
|
||||
Active: false,
|
||||
HTMLFooter: "\n<br>\n<p><a href=\"%unsubscribe_url%\">unsubscribe</a></p>\n",
|
||||
TextFooter: "\n\nTo unsubscribe click: <%unsubscribe_url%>\n\n",
|
||||
},
|
||||
},
|
||||
ReceivingDNSRecords: []DNSRecord{
|
||||
{
|
||||
Priority: "10",
|
||||
RecordType: "MX",
|
||||
Valid: "valid",
|
||||
Value: "mxa.mailgun.org",
|
||||
},
|
||||
{
|
||||
Priority: "10",
|
||||
RecordType: "MX",
|
||||
Valid: "valid",
|
||||
Value: "mxb.mailgun.org",
|
||||
},
|
||||
},
|
||||
SendingDNSRecords: []DNSRecord{
|
||||
{
|
||||
RecordType: "TXT",
|
||||
Valid: "valid",
|
||||
Name: "domain.com",
|
||||
Value: "v=spf1 include:mailgun.org ~all",
|
||||
},
|
||||
{
|
||||
RecordType: "TXT",
|
||||
Valid: "valid",
|
||||
Name: "domain.com",
|
||||
Value: "k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUA....",
|
||||
},
|
||||
{
|
||||
RecordType: "CNAME",
|
||||
Valid: "valid",
|
||||
Name: "email.domain.com",
|
||||
Value: "mailgun.org",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
r.Get("/domains", ms.listDomains)
|
||||
r.Post("/domains", ms.createDomain)
|
||||
r.Get("/domains/{domain}", ms.getDomain)
|
||||
r.Put("/domains/{domain}/verify", ms.getDomain)
|
||||
r.Delete("/domains/{domain}", ms.deleteDomain)
|
||||
//r.Get("/domains/{domain}/credentials", ms.getCredentials)
|
||||
//r.Post("/domains/{domain}/credentials", ms.createCredentials)
|
||||
//r.Put("/domains/{domain}/credentials/{login}", ms.updateCredentials)
|
||||
//r.Delete("/domains/{domain}/credentials/{login}", ms.deleteCredentials)
|
||||
r.Get("/domains/{domain}/connection", ms.getConnection)
|
||||
r.Put("/domains/{domain}/connection", ms.updateConnection)
|
||||
r.Get("/domains/{domain}/tracking", ms.getTracking)
|
||||
r.Put("/domains/{domain}/tracking/click", ms.updateClickTracking)
|
||||
r.Put("/domains/{domain}/tracking/open", ms.updateOpenTracking)
|
||||
r.Put("/domains/{domain}/tracking/unsubscribe", ms.updateUnsubTracking)
|
||||
r.Get("/domains/{domain}/limits/tag", ms.getTagLimits)
|
||||
}
|
||||
|
||||
func (ms *MockServer) listDomains(w http.ResponseWriter, r *http.Request) {
|
||||
var list []Domain
|
||||
for _, domain := range ms.domainList {
|
||||
list = append(list, domain.Domain)
|
||||
}
|
||||
|
||||
skip := stringToInt(r.FormValue("skip"))
|
||||
limit := stringToInt(r.FormValue("limit"))
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
if skip > len(list) {
|
||||
skip = len(list)
|
||||
}
|
||||
|
||||
end := limit + skip
|
||||
if end > len(list) {
|
||||
end = len(list)
|
||||
}
|
||||
|
||||
// If we are at the end of the list
|
||||
if skip == end {
|
||||
toJSON(w, domainsListResponse{
|
||||
TotalCount: len(list),
|
||||
Items: []Domain{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
toJSON(w, domainsListResponse{
|
||||
TotalCount: len(list),
|
||||
Items: list[skip:end],
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MockServer) getDomain(w http.ResponseWriter, r *http.Request) {
|
||||
for _, d := range ms.domainList {
|
||||
if d.Domain.Name == chi.URLParam(r, "domain") {
|
||||
d.Connection = nil
|
||||
toJSON(w, d)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "domain not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) createDomain(w http.ResponseWriter, r *http.Request) {
|
||||
ms.domainList = append(ms.domainList, domainContainer{
|
||||
Domain: Domain{
|
||||
CreatedAt: RFC2822Time(time.Now()),
|
||||
Name: r.FormValue("name"),
|
||||
SMTPLogin: r.FormValue("smtp_login"),
|
||||
SMTPPassword: r.FormValue("smtp_password"),
|
||||
Wildcard: stringToBool(r.FormValue("wildcard")),
|
||||
SpamAction: SpamAction(r.FormValue("spam_action")),
|
||||
State: "active",
|
||||
},
|
||||
})
|
||||
toJSON(w, okResp{Message: "Domain has been created"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) deleteDomain(w http.ResponseWriter, r *http.Request) {
|
||||
result := ms.domainList[:0]
|
||||
for _, domain := range ms.domainList {
|
||||
if domain.Domain.Name == chi.URLParam(r, "domain") {
|
||||
continue
|
||||
}
|
||||
result = append(result, domain)
|
||||
}
|
||||
|
||||
if len(result) != len(ms.domainList) {
|
||||
toJSON(w, okResp{Message: "success"})
|
||||
ms.domainList = result
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "domain not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) getConnection(w http.ResponseWriter, r *http.Request) {
|
||||
for _, d := range ms.domainList {
|
||||
if d.Domain.Name == chi.URLParam(r, "domain") {
|
||||
resp := domainConnectionResponse{
|
||||
Connection: *d.Connection,
|
||||
}
|
||||
toJSON(w, resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "domain not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) updateConnection(w http.ResponseWriter, r *http.Request) {
|
||||
for i, d := range ms.domainList {
|
||||
if d.Domain.Name == chi.URLParam(r, "domain") {
|
||||
ms.domainList[i].Connection = &DomainConnection{
|
||||
RequireTLS: stringToBool(r.FormValue("require_tls")),
|
||||
SkipVerification: stringToBool(r.FormValue("skip_verification")),
|
||||
}
|
||||
toJSON(w, okResp{Message: "Domain connection settings have been updated, may take 10 minutes to fully propagate"})
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "domain not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) getTracking(w http.ResponseWriter, r *http.Request) {
|
||||
for _, d := range ms.domainList {
|
||||
if d.Domain.Name == chi.URLParam(r, "domain") {
|
||||
resp := domainTrackingResponse{
|
||||
Tracking: *d.Tracking,
|
||||
}
|
||||
toJSON(w, resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "domain not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) updateClickTracking(w http.ResponseWriter, r *http.Request) {
|
||||
for i, d := range ms.domainList {
|
||||
if d.Domain.Name == chi.URLParam(r, "domain") {
|
||||
ms.domainList[i].Tracking.Click.Active = stringToBool(r.FormValue("active"))
|
||||
toJSON(w, okResp{Message: "Domain tracking settings have been updated"})
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "domain not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) updateOpenTracking(w http.ResponseWriter, r *http.Request) {
|
||||
for i, d := range ms.domainList {
|
||||
if d.Domain.Name == chi.URLParam(r, "domain") {
|
||||
ms.domainList[i].Tracking.Open.Active = stringToBool(r.FormValue("active"))
|
||||
toJSON(w, okResp{Message: "Domain tracking settings have been updated"})
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "domain not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) updateUnsubTracking(w http.ResponseWriter, r *http.Request) {
|
||||
for i, d := range ms.domainList {
|
||||
if d.Domain.Name == chi.URLParam(r, "domain") {
|
||||
ms.domainList[i].Tracking.Unsubscribe.Active = stringToBool(r.FormValue("active"))
|
||||
if len(r.FormValue("html_footer")) != 0 {
|
||||
ms.domainList[i].Tracking.Unsubscribe.HTMLFooter = r.FormValue("html_footer")
|
||||
}
|
||||
if len(r.FormValue("text_footer")) != 0 {
|
||||
ms.domainList[i].Tracking.Unsubscribe.TextFooter = r.FormValue("text_footer")
|
||||
}
|
||||
toJSON(w, okResp{Message: "Domain tracking settings have been updated"})
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "domain not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) getTagLimits(w http.ResponseWriter, r *http.Request) {
|
||||
for _, d := range ms.domainList {
|
||||
if d.Domain.Name == chi.URLParam(r, "domain") {
|
||||
if d.TagLimits == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "no limits defined for domain"})
|
||||
return
|
||||
}
|
||||
toJSON(w, d.TagLimits)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "domain not found"})
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/mailgun/mailgun-go/v3/events"
|
||||
)
|
||||
|
||||
func (ms *MockServer) addEventRoutes(r chi.Router) {
|
||||
r.Get("/{domain}/events", ms.listEvents)
|
||||
|
||||
var (
|
||||
tags = []string{"tag1", "tag2"}
|
||||
recipients = []string{"one@mailgun.test", "two@mailgun.test"}
|
||||
recipientDomain = "mailgun.test"
|
||||
timeStamp = TimeToFloat(time.Now().UTC())
|
||||
ipAddress = "192.168.1.1"
|
||||
message = events.Message{Headers: events.MessageHeaders{MessageID: "1234"}}
|
||||
clientInfo = events.ClientInfo{
|
||||
AcceptLanguage: "EN",
|
||||
ClientName: "Firefox",
|
||||
ClientOS: "OS X",
|
||||
ClientType: "browser",
|
||||
DeviceType: "desktop",
|
||||
IP: "8.8.8.8",
|
||||
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0",
|
||||
}
|
||||
geoLocation = events.GeoLocation{
|
||||
City: "San Antonio",
|
||||
Country: "US",
|
||||
Region: "TX",
|
||||
}
|
||||
)
|
||||
|
||||
// AcceptedNoAuth
|
||||
accepted := new(events.Accepted)
|
||||
accepted.ID = randomString(16, "ID-")
|
||||
accepted.Message.Headers.MessageID = accepted.ID
|
||||
accepted.Name = events.EventAccepted
|
||||
accepted.Tags = tags
|
||||
accepted.Timestamp = timeStamp
|
||||
accepted.Recipient = recipients[0]
|
||||
accepted.RecipientDomain = recipientDomain
|
||||
accepted.Flags = events.Flags{
|
||||
IsAuthenticated: false,
|
||||
}
|
||||
ms.events = append(ms.events, accepted)
|
||||
|
||||
// AcceptedAuth
|
||||
accepted = new(events.Accepted)
|
||||
accepted.ID = randomString(16, "ID-")
|
||||
accepted.Message.Headers.MessageID = accepted.ID
|
||||
accepted.Name = events.EventAccepted
|
||||
accepted.Tags = tags
|
||||
accepted.Timestamp = timeStamp
|
||||
accepted.Recipient = recipients[0]
|
||||
accepted.RecipientDomain = recipientDomain
|
||||
accepted.Campaigns = []events.Campaign{
|
||||
{ID: "test-id", Name: "test"},
|
||||
}
|
||||
accepted.Flags = events.Flags{
|
||||
IsAuthenticated: true,
|
||||
}
|
||||
ms.events = append(ms.events, accepted)
|
||||
|
||||
// DeliveredSMTP
|
||||
delivered := new(events.Delivered)
|
||||
delivered.ID = randomString(16, "ID-")
|
||||
delivered.Message.Headers.MessageID = delivered.ID
|
||||
delivered.Name = events.EventDelivered
|
||||
delivered.Tags = tags
|
||||
delivered.Timestamp = timeStamp
|
||||
delivered.Recipient = recipients[0]
|
||||
delivered.RecipientDomain = recipientDomain
|
||||
delivered.DeliveryStatus.Message = "We sent an email Yo"
|
||||
delivered.Envelope = events.Envelope{
|
||||
Transport: "smtp",
|
||||
SendingIP: ipAddress,
|
||||
}
|
||||
delivered.Flags = events.Flags{
|
||||
IsAuthenticated: true,
|
||||
}
|
||||
ms.events = append(ms.events, delivered)
|
||||
|
||||
// DeliveredHTTP
|
||||
delivered = new(events.Delivered)
|
||||
delivered.ID = randomString(16, "ID-")
|
||||
delivered.Message.Headers.MessageID = delivered.ID
|
||||
delivered.Name = events.EventDelivered
|
||||
delivered.Tags = tags
|
||||
delivered.Timestamp = timeStamp
|
||||
delivered.Recipient = recipients[0]
|
||||
delivered.RecipientDomain = recipientDomain
|
||||
delivered.DeliveryStatus.Message = "We sent an email Yo"
|
||||
delivered.Envelope = events.Envelope{
|
||||
Transport: "http",
|
||||
SendingIP: ipAddress,
|
||||
}
|
||||
delivered.Flags = events.Flags{
|
||||
IsAuthenticated: true,
|
||||
}
|
||||
ms.events = append(ms.events, delivered)
|
||||
|
||||
// Stored
|
||||
stored := new(events.Stored)
|
||||
stored.ID = randomString(16, "ID-")
|
||||
stored.Name = events.EventStored
|
||||
stored.Tags = tags
|
||||
stored.Timestamp = timeStamp
|
||||
stored.Storage.URL = "http://mailgun.text/some/url"
|
||||
ms.events = append(ms.events, stored)
|
||||
|
||||
// Clicked
|
||||
for _, recipient := range recipients {
|
||||
clicked := new(events.Clicked)
|
||||
clicked.ID = randomString(16, "ID-")
|
||||
clicked.Name = events.EventClicked
|
||||
clicked.Message = message
|
||||
clicked.Tags = tags
|
||||
clicked.Recipient = recipient
|
||||
clicked.ClientInfo = clientInfo
|
||||
clicked.GeoLocation = geoLocation
|
||||
clicked.Timestamp = timeStamp
|
||||
ms.events = append(ms.events, clicked)
|
||||
}
|
||||
|
||||
clicked := new(events.Clicked)
|
||||
clicked.ID = randomString(16, "ID-")
|
||||
clicked.Name = events.EventClicked
|
||||
clicked.Message = message
|
||||
clicked.Tags = tags
|
||||
clicked.Recipient = recipients[0]
|
||||
clicked.ClientInfo = clientInfo
|
||||
clicked.GeoLocation = geoLocation
|
||||
clicked.Timestamp = timeStamp
|
||||
ms.events = append(ms.events, clicked)
|
||||
|
||||
// Opened
|
||||
for _, recipient := range recipients {
|
||||
opened := new(events.Opened)
|
||||
opened.ID = randomString(16, "ID-")
|
||||
opened.Name = events.EventOpened
|
||||
opened.Message = message
|
||||
opened.Tags = tags
|
||||
opened.Recipient = recipient
|
||||
opened.ClientInfo = clientInfo
|
||||
opened.GeoLocation = geoLocation
|
||||
opened.Timestamp = timeStamp
|
||||
ms.events = append(ms.events, opened)
|
||||
}
|
||||
|
||||
opened := new(events.Opened)
|
||||
opened.ID = randomString(16, "ID-")
|
||||
opened.Name = events.EventOpened
|
||||
opened.Message = message
|
||||
opened.Tags = tags
|
||||
opened.Recipient = recipients[0]
|
||||
opened.ClientInfo = clientInfo
|
||||
opened.GeoLocation = geoLocation
|
||||
opened.Timestamp = timeStamp
|
||||
ms.events = append(ms.events, opened)
|
||||
|
||||
// Unsubscribed
|
||||
for _, recipient := range recipients {
|
||||
unsub := new(events.Unsubscribed)
|
||||
unsub.ID = randomString(16, "ID-")
|
||||
unsub.Name = events.EventUnsubscribed
|
||||
unsub.Tags = tags
|
||||
unsub.Recipient = recipient
|
||||
unsub.ClientInfo = clientInfo
|
||||
unsub.GeoLocation = geoLocation
|
||||
unsub.Timestamp = timeStamp
|
||||
ms.events = append(ms.events, unsub)
|
||||
}
|
||||
|
||||
// Complained
|
||||
for _, recipient := range recipients {
|
||||
complained := new(events.Complained)
|
||||
complained.ID = randomString(16, "ID-")
|
||||
complained.Name = events.EventComplained
|
||||
complained.Tags = tags
|
||||
complained.Recipient = recipient
|
||||
complained.Timestamp = timeStamp
|
||||
ms.events = append(ms.events, complained)
|
||||
}
|
||||
}
|
||||
|
||||
type eventsResponse struct {
|
||||
Items []Event `json:"items"`
|
||||
Paging Paging `json:"paging"`
|
||||
}
|
||||
|
||||
func (ms *MockServer) listEvents(w http.ResponseWriter, r *http.Request) {
|
||||
var idx []string
|
||||
|
||||
for _, e := range ms.events {
|
||||
idx = append(idx, e.GetID())
|
||||
}
|
||||
|
||||
limit := stringToInt(r.FormValue("limit"))
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
start, end := pageOffsets(idx, r.FormValue("page"), r.FormValue("address"), limit)
|
||||
|
||||
var nextAddress, prevAddress string
|
||||
var results []Event
|
||||
|
||||
if start != end {
|
||||
results = ms.events[start:end]
|
||||
nextAddress = results[len(results)-1].GetID()
|
||||
prevAddress = results[0].GetID()
|
||||
} else {
|
||||
results = []Event{}
|
||||
nextAddress = r.FormValue("address")
|
||||
prevAddress = r.FormValue("address")
|
||||
}
|
||||
|
||||
resp := eventsResponse{
|
||||
Paging: Paging{
|
||||
First: getPageURL(r, url.Values{
|
||||
"page": []string{"first"},
|
||||
}),
|
||||
Last: getPageURL(r, url.Values{
|
||||
"page": []string{"last"},
|
||||
}),
|
||||
Next: getPageURL(r, url.Values{
|
||||
"page": []string{"next"},
|
||||
"address": []string{nextAddress},
|
||||
}),
|
||||
Previous: getPageURL(r, url.Values{
|
||||
"page": []string{"prev"},
|
||||
"address": []string{prevAddress},
|
||||
}),
|
||||
},
|
||||
Items: results,
|
||||
}
|
||||
toJSON(w, resp)
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func (ms *MockServer) addExportRoutes(r chi.Router) {
|
||||
r.Post("/exports", ms.postExports)
|
||||
r.Get("/exports", ms.listExports)
|
||||
r.Get("/exports/{id}", ms.getExport)
|
||||
r.Get("/exports/{id}/download_url", ms.getExportLink)
|
||||
}
|
||||
|
||||
func (ms *MockServer) postExports(w http.ResponseWriter, r *http.Request) {
|
||||
e := Export{
|
||||
ID: strconv.Itoa(len(ms.exportList)),
|
||||
URL: r.FormValue("url"),
|
||||
Status: "complete",
|
||||
}
|
||||
|
||||
ms.exportList = append(ms.exportList, e)
|
||||
toJSON(w, okResp{Message: "success"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) listExports(w http.ResponseWriter, _ *http.Request) {
|
||||
toJSON(w, ExportList{
|
||||
Items: ms.exportList,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MockServer) getExport(w http.ResponseWriter, r *http.Request) {
|
||||
for _, export := range ms.exportList {
|
||||
if export.ID == chi.URLParam(r, "id") {
|
||||
toJSON(w, export)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "export not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) getExportLink(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", "/some/s3/url")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func (ms *MockServer) addIPRoutes(r chi.Router) {
|
||||
r.Get("/ips", ms.listIPS)
|
||||
r.Get("/ips/{ip}", ms.getIPAddress)
|
||||
r.Route("/domains/{domain}/ips", func(r chi.Router) {
|
||||
r.Get("/", ms.listDomainIPS)
|
||||
r.Get("/{ip}", ms.getIPAddress)
|
||||
r.Post("/", ms.postDomainIPS)
|
||||
r.Delete("/{ip}", ms.deleteDomainIPS)
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MockServer) listIPS(w http.ResponseWriter, _ *http.Request) {
|
||||
toJSON(w, ipAddressListResponse{
|
||||
TotalCount: 2,
|
||||
Items: []string{"172.0.0.1", "192.168.1.1"},
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MockServer) getIPAddress(w http.ResponseWriter, r *http.Request) {
|
||||
toJSON(w, IPAddress{
|
||||
IP: chi.URLParam(r, "ip"),
|
||||
RDNS: "luna.mailgun.net",
|
||||
Dedicated: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MockServer) listDomainIPS(w http.ResponseWriter, _ *http.Request) {
|
||||
toJSON(w, ipAddressListResponse{
|
||||
TotalCount: 2,
|
||||
Items: ms.domainIPS,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MockServer) postDomainIPS(w http.ResponseWriter, r *http.Request) {
|
||||
ms.domainIPS = append(ms.domainIPS, r.FormValue("ip"))
|
||||
toJSON(w, okResp{Message: "success"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) deleteDomainIPS(w http.ResponseWriter, r *http.Request) {
|
||||
result := ms.domainIPS[:0]
|
||||
for _, ip := range ms.domainIPS {
|
||||
if ip == chi.URLParam(r, "ip") {
|
||||
continue
|
||||
}
|
||||
result = append(result, ip)
|
||||
}
|
||||
|
||||
if len(result) != len(ms.domainIPS) {
|
||||
toJSON(w, okResp{Message: "success"})
|
||||
ms.domainIPS = result
|
||||
return
|
||||
}
|
||||
|
||||
// Not the actual error returned by the mailgun API
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "ip not found"})
|
||||
}
|
|
@ -0,0 +1,389 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
type mailingListContainer struct {
|
||||
MailingList MailingList
|
||||
Members []Member
|
||||
}
|
||||
|
||||
func (ms *MockServer) addMailingListRoutes(r chi.Router) {
|
||||
r.Get("/lists/pages", ms.listMailingLists)
|
||||
r.Get("/lists/{address}", ms.getMailingList)
|
||||
r.Post("/lists", ms.createMailingList)
|
||||
r.Put("/lists/{address}", ms.updateMailingList)
|
||||
r.Delete("/lists/{address}", ms.deleteMailingList)
|
||||
|
||||
r.Get("/lists/{address}/members/pages", ms.listMembers)
|
||||
r.Get("/lists/{address}/members/{member}", ms.getMember)
|
||||
r.Post("/lists/{address}/members", ms.createMember)
|
||||
r.Put("/lists/{address}/members/{member}", ms.updateMember)
|
||||
r.Delete("/lists/{address}/members/{member}", ms.deleteMember)
|
||||
r.Post("/lists/{address}/members.json", ms.bulkCreate)
|
||||
|
||||
ms.mailingList = append(ms.mailingList, mailingListContainer{
|
||||
MailingList: MailingList{
|
||||
AccessLevel: "everyone",
|
||||
Address: "foo@mailgun.test",
|
||||
CreatedAt: RFC2822Time(time.Now().UTC()),
|
||||
Description: "Mailgun developers list",
|
||||
MembersCount: 1,
|
||||
Name: "",
|
||||
},
|
||||
Members: []Member{
|
||||
{
|
||||
Address: "dev@samples.mailgun.org",
|
||||
Name: "Developer",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MockServer) listMailingLists(w http.ResponseWriter, r *http.Request) {
|
||||
var list []MailingList
|
||||
var idx []string
|
||||
|
||||
for _, ml := range ms.mailingList {
|
||||
list = append(list, ml.MailingList)
|
||||
idx = append(idx, ml.MailingList.Address)
|
||||
}
|
||||
|
||||
limit := stringToInt(r.FormValue("limit"))
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
start, end := pageOffsets(idx, r.FormValue("page"), r.FormValue("address"), limit)
|
||||
results := list[start:end]
|
||||
|
||||
if len(results) == 0 {
|
||||
toJSON(w, listsResponse{})
|
||||
return
|
||||
}
|
||||
|
||||
resp := listsResponse{
|
||||
Paging: Paging{
|
||||
First: getPageURL(r, url.Values{
|
||||
"page": []string{"first"},
|
||||
}),
|
||||
Last: getPageURL(r, url.Values{
|
||||
"page": []string{"last"},
|
||||
}),
|
||||
Next: getPageURL(r, url.Values{
|
||||
"page": []string{"next"},
|
||||
"address": []string{results[len(results)-1].Address},
|
||||
}),
|
||||
Previous: getPageURL(r, url.Values{
|
||||
"page": []string{"prev"},
|
||||
"address": []string{results[0].Address},
|
||||
}),
|
||||
},
|
||||
Items: results,
|
||||
}
|
||||
toJSON(w, resp)
|
||||
}
|
||||
|
||||
func (ms *MockServer) getMailingList(w http.ResponseWriter, r *http.Request) {
|
||||
for _, ml := range ms.mailingList {
|
||||
if ml.MailingList.Address == chi.URLParam(r, "address") {
|
||||
toJSON(w, mailingListResponse{MailingList: ml.MailingList})
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "mailing list not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) deleteMailingList(w http.ResponseWriter, r *http.Request) {
|
||||
result := ms.mailingList[:0]
|
||||
for _, ml := range ms.mailingList {
|
||||
if ml.MailingList.Address == chi.URLParam(r, "address") {
|
||||
continue
|
||||
}
|
||||
result = append(result, ml)
|
||||
}
|
||||
|
||||
if len(result) != len(ms.mailingList) {
|
||||
toJSON(w, okResp{Message: "success"})
|
||||
ms.mailingList = result
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "mailing list not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) updateMailingList(w http.ResponseWriter, r *http.Request) {
|
||||
for i, d := range ms.mailingList {
|
||||
if d.MailingList.Address == chi.URLParam(r, "address") {
|
||||
if r.FormValue("address") != "" {
|
||||
ms.mailingList[i].MailingList.Address = r.FormValue("address")
|
||||
}
|
||||
if r.FormValue("name") != "" {
|
||||
ms.mailingList[i].MailingList.Name = r.FormValue("name")
|
||||
}
|
||||
if r.FormValue("description") != "" {
|
||||
ms.mailingList[i].MailingList.Description = r.FormValue("description")
|
||||
}
|
||||
if r.FormValue("access_level") != "" {
|
||||
ms.mailingList[i].MailingList.AccessLevel = AccessLevel(r.FormValue("access_level"))
|
||||
}
|
||||
toJSON(w, okResp{Message: "Mailing list member has been updated"})
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "mailing list not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) createMailingList(w http.ResponseWriter, r *http.Request) {
|
||||
ms.mailingList = append(ms.mailingList, mailingListContainer{
|
||||
MailingList: MailingList{
|
||||
CreatedAt: RFC2822Time(time.Now().UTC()),
|
||||
Name: r.FormValue("name"),
|
||||
Address: r.FormValue("address"),
|
||||
Description: r.FormValue("description"),
|
||||
AccessLevel: AccessLevel(r.FormValue("access_level")),
|
||||
},
|
||||
})
|
||||
toJSON(w, okResp{Message: "Mailing list has been created"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) listMembers(w http.ResponseWriter, r *http.Request) {
|
||||
var list []Member
|
||||
var idx []string
|
||||
var found bool
|
||||
|
||||
for _, ml := range ms.mailingList {
|
||||
if ml.MailingList.Address == chi.URLParam(r, "address") {
|
||||
found = true
|
||||
for _, member := range ml.Members {
|
||||
list = append(list, member)
|
||||
idx = append(idx, member.Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "mailing list not found"})
|
||||
return
|
||||
}
|
||||
|
||||
limit := stringToInt(r.FormValue("limit"))
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
start, end := pageOffsets(idx, r.FormValue("page"), r.FormValue("address"), limit)
|
||||
results := list[start:end]
|
||||
|
||||
if len(results) == 0 {
|
||||
toJSON(w, memberListResponse{})
|
||||
return
|
||||
}
|
||||
|
||||
resp := memberListResponse{
|
||||
Paging: Paging{
|
||||
First: getPageURL(r, url.Values{
|
||||
"page": []string{"first"},
|
||||
}),
|
||||
Last: getPageURL(r, url.Values{
|
||||
"page": []string{"last"},
|
||||
}),
|
||||
Next: getPageURL(r, url.Values{
|
||||
"page": []string{"next"},
|
||||
"address": []string{results[len(results)-1].Address},
|
||||
}),
|
||||
Previous: getPageURL(r, url.Values{
|
||||
"page": []string{"prev"},
|
||||
"address": []string{results[0].Address},
|
||||
}),
|
||||
},
|
||||
Lists: results,
|
||||
}
|
||||
toJSON(w, resp)
|
||||
}
|
||||
|
||||
func (ms *MockServer) getMember(w http.ResponseWriter, r *http.Request) {
|
||||
var found bool
|
||||
for _, ml := range ms.mailingList {
|
||||
if ml.MailingList.Address == chi.URLParam(r, "address") {
|
||||
found = true
|
||||
for _, member := range ml.Members {
|
||||
if member.Address == chi.URLParam(r, "member") {
|
||||
toJSON(w, memberResponse{Member: member})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "mailing list not found"})
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "member not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) deleteMember(w http.ResponseWriter, r *http.Request) {
|
||||
idx := -1
|
||||
for i, ml := range ms.mailingList {
|
||||
if ml.MailingList.Address == chi.URLParam(r, "address") {
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "mailing list not found"})
|
||||
return
|
||||
}
|
||||
|
||||
result := ms.mailingList[idx].Members[:0]
|
||||
for _, m := range ms.mailingList[idx].Members {
|
||||
if m.Address == chi.URLParam(r, "member") {
|
||||
continue
|
||||
}
|
||||
result = append(result, m)
|
||||
}
|
||||
|
||||
if len(result) != len(ms.mailingList[idx].Members) {
|
||||
toJSON(w, okResp{Message: "Mailing list member has been deleted"})
|
||||
ms.mailingList[idx].Members = result
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "member not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) updateMember(w http.ResponseWriter, r *http.Request) {
|
||||
idx := -1
|
||||
for i, ml := range ms.mailingList {
|
||||
if ml.MailingList.Address == chi.URLParam(r, "address") {
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "mailing list not found"})
|
||||
return
|
||||
}
|
||||
|
||||
for i, m := range ms.mailingList[idx].Members {
|
||||
if m.Address == chi.URLParam(r, "member") {
|
||||
if r.FormValue("address") != "" {
|
||||
ms.mailingList[idx].Members[i].Address = parseAddress(r.FormValue("address"))
|
||||
}
|
||||
if r.FormValue("name") != "" {
|
||||
ms.mailingList[idx].Members[i].Name = r.FormValue("name")
|
||||
}
|
||||
if r.FormValue("vars") != "" {
|
||||
ms.mailingList[idx].Members[i].Vars = stringToMap(r.FormValue("vars"))
|
||||
}
|
||||
if r.FormValue("subscribed") != "" {
|
||||
sub := stringToBool(r.FormValue("subscribed"))
|
||||
ms.mailingList[idx].Members[i].Subscribed = &sub
|
||||
}
|
||||
toJSON(w, okResp{Message: "Mailing list member has been updated"})
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "member not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) createMember(w http.ResponseWriter, r *http.Request) {
|
||||
idx := -1
|
||||
for i, ml := range ms.mailingList {
|
||||
if ml.MailingList.Address == chi.URLParam(r, "address") {
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "mailing list not found"})
|
||||
return
|
||||
}
|
||||
|
||||
sub := stringToBool(r.FormValue("subscribed"))
|
||||
|
||||
if len(ms.mailingList[idx].Members) != 0 {
|
||||
for i, m := range ms.mailingList[idx].Members {
|
||||
if m.Address == r.FormValue("address") {
|
||||
if !stringToBool(r.FormValue("upsert")) {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
toJSON(w, okResp{Message: "member already exists"})
|
||||
return
|
||||
}
|
||||
|
||||
ms.mailingList[idx].Members[i].Address = parseAddress(r.FormValue("address"))
|
||||
ms.mailingList[idx].Members[i].Name = r.FormValue("name")
|
||||
ms.mailingList[idx].Members[i].Vars = stringToMap(r.FormValue("vars"))
|
||||
ms.mailingList[idx].Members[i].Subscribed = &sub
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ms.mailingList[idx].Members = append(ms.mailingList[idx].Members, Member{
|
||||
Name: r.FormValue("name"),
|
||||
Address: parseAddress(r.FormValue("address")),
|
||||
Vars: stringToMap(r.FormValue("vars")),
|
||||
Subscribed: &sub,
|
||||
})
|
||||
toJSON(w, okResp{Message: "Mailing list member has been created"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) bulkCreate(w http.ResponseWriter, r *http.Request) {
|
||||
idx := -1
|
||||
for i, ml := range ms.mailingList {
|
||||
if ml.MailingList.Address == chi.URLParam(r, "address") {
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "mailing list not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var bulkList []Member
|
||||
if err := json.Unmarshal([]byte(r.FormValue("members")), &bulkList); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
toJSON(w, okResp{Message: "while un-marshalling 'members' param - " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
BULK:
|
||||
for _, member := range bulkList {
|
||||
member.Address = parseAddress(member.Address)
|
||||
if len(ms.mailingList[idx].Members) != 0 {
|
||||
for i, m := range ms.mailingList[idx].Members {
|
||||
if m.Address == member.Address {
|
||||
if !stringToBool(r.FormValue("upsert")) {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
toJSON(w, okResp{Message: "member already exists"})
|
||||
return
|
||||
}
|
||||
ms.mailingList[idx].Members[i] = member
|
||||
continue BULK
|
||||
}
|
||||
}
|
||||
}
|
||||
ms.mailingList[idx].Members = append(ms.mailingList[idx].Members, member)
|
||||
}
|
||||
toJSON(w, okResp{Message: "Mailing list has been updated"})
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/mailgun/mailgun-go/v3/events"
|
||||
)
|
||||
|
||||
func (ms *MockServer) addMessagesRoutes(r chi.Router) {
|
||||
r.Post("/{domain}/messages", ms.createMessages)
|
||||
|
||||
// This path is made up; it could be anything as the storage url could change over time
|
||||
r.Get("/se.storage.url/messages/{id}", ms.getStoredMessages)
|
||||
r.Post("/se.storage.url/messages/{id}", ms.sendStoredMessages)
|
||||
}
|
||||
|
||||
// TODO: This implementation doesn't support multiple recipients
|
||||
func (ms *MockServer) createMessages(w http.ResponseWriter, r *http.Request) {
|
||||
to, err := mail.ParseAddress(r.FormValue("to"))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
toJSON(w, okResp{Message: "invalid 'to' address"})
|
||||
return
|
||||
}
|
||||
id := randomString(16, "ID-")
|
||||
|
||||
switch to.Address {
|
||||
case "stored@mailgun.test":
|
||||
stored := new(events.Stored)
|
||||
stored.Name = events.EventStored
|
||||
stored.Timestamp = TimeToFloat(time.Now().UTC())
|
||||
stored.ID = id
|
||||
stored.Storage.URL = ms.URL() + "/se.storage.url/messages/" + id
|
||||
stored.Storage.Key = id
|
||||
stored.Message.Headers = events.MessageHeaders{
|
||||
Subject: r.FormValue("subject"),
|
||||
From: r.FormValue("from"),
|
||||
To: to.Address,
|
||||
MessageID: id,
|
||||
}
|
||||
stored.Message.Recipients = []string{
|
||||
r.FormValue("to"),
|
||||
}
|
||||
stored.Message.Size = 10
|
||||
stored.Flags = events.Flags{
|
||||
IsTestMode: false,
|
||||
}
|
||||
ms.events = append(ms.events, stored)
|
||||
default:
|
||||
accepted := new(events.Accepted)
|
||||
accepted.Name = events.EventAccepted
|
||||
accepted.ID = id
|
||||
accepted.Timestamp = TimeToFloat(time.Now().UTC())
|
||||
accepted.Message.Headers.From = r.FormValue("from")
|
||||
accepted.Message.Headers.To = r.FormValue("to")
|
||||
accepted.Message.Headers.MessageID = accepted.ID
|
||||
accepted.Message.Headers.Subject = r.FormValue("subject")
|
||||
|
||||
accepted.Recipient = r.FormValue("to")
|
||||
accepted.RecipientDomain = strings.Split(to.Address, "@")[1]
|
||||
accepted.Flags = events.Flags{
|
||||
IsAuthenticated: true,
|
||||
}
|
||||
ms.events = append(ms.events, accepted)
|
||||
}
|
||||
|
||||
toJSON(w, okResp{ID: "<" + id + ">", Message: "Queued. Thank you."})
|
||||
}
|
||||
|
||||
func (ms *MockServer) getStoredMessages(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
// Find our stored event
|
||||
var stored *events.Stored
|
||||
for _, event := range ms.events {
|
||||
if event.GetID() == id {
|
||||
stored = event.(*events.Stored)
|
||||
}
|
||||
}
|
||||
|
||||
if stored == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "not found"})
|
||||
}
|
||||
|
||||
toJSON(w, StoredMessage{
|
||||
Recipients: strings.Join(stored.Message.Recipients, ","),
|
||||
Sender: stored.Message.Headers.From,
|
||||
Subject: stored.Message.Headers.Subject,
|
||||
From: stored.Message.Headers.From,
|
||||
MessageHeaders: [][]string{
|
||||
{"Sender", stored.Message.Headers.From},
|
||||
{"To", stored.Message.Headers.To},
|
||||
{"Subject", stored.Message.Headers.Subject},
|
||||
{"Content-Type", "text/plain"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MockServer) sendStoredMessages(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
// Find our stored event
|
||||
var stored *events.Stored
|
||||
for _, event := range ms.events {
|
||||
if event.GetID() == id {
|
||||
stored = event.(*events.Stored)
|
||||
}
|
||||
}
|
||||
|
||||
if stored == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "not found"})
|
||||
}
|
||||
|
||||
// DO NOTHING
|
||||
|
||||
toJSON(w, okResp{ID: "<" + id + ">", Message: "Queued. Thank you."})
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-chi/chi"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type routeResponse struct {
|
||||
Route Route `json:"route"`
|
||||
}
|
||||
|
||||
func (ms *MockServer) addRoutes(r chi.Router) {
|
||||
r.Post("/routes", ms.createRoute)
|
||||
r.Get("/routes", ms.listRoutes)
|
||||
r.Get("/routes/{id}", ms.getRoute)
|
||||
r.Put("/routes/{id}", ms.updateRoute)
|
||||
r.Delete("/routes/{id}", ms.deleteRoute)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
ms.routeList = append(ms.routeList, Route{
|
||||
Id: randomString(10, "ID-"),
|
||||
Priority: 0,
|
||||
Description: fmt.Sprintf("Sample Route %d", i),
|
||||
Actions: []string{
|
||||
`forward("http://myhost.com/messages/")`,
|
||||
`stop()`,
|
||||
},
|
||||
Expression: `match_recipient(".*@samples.mailgun.org")`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MockServer) listRoutes(w http.ResponseWriter, r *http.Request) {
|
||||
skip := stringToInt(r.FormValue("skip"))
|
||||
limit := stringToInt(r.FormValue("limit"))
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
if skip > len(ms.routeList) {
|
||||
skip = len(ms.routeList)
|
||||
}
|
||||
|
||||
end := limit + skip
|
||||
if end > len(ms.routeList) {
|
||||
end = len(ms.routeList)
|
||||
}
|
||||
|
||||
// If we are at the end of the list
|
||||
if skip == end {
|
||||
toJSON(w, routesListResponse{
|
||||
TotalCount: len(ms.routeList),
|
||||
Items: []Route{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
toJSON(w, routesListResponse{
|
||||
TotalCount: len(ms.routeList),
|
||||
Items: ms.routeList[skip:end],
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MockServer) getRoute(w http.ResponseWriter, r *http.Request) {
|
||||
for _, item := range ms.routeList {
|
||||
if item.Id == chi.URLParam(r, "id") {
|
||||
toJSON(w, routeResponse{Route: item})
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "route not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) createRoute(w http.ResponseWriter, r *http.Request) {
|
||||
if r.FormValue("action") == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
toJSON(w, okResp{Message: "'action' parameter is required"})
|
||||
return
|
||||
}
|
||||
|
||||
ms.routeList = append(ms.routeList, Route{
|
||||
CreatedAt: RFC2822Time(time.Now().UTC()),
|
||||
Id: randomString(10, "ID-"),
|
||||
Priority: stringToInt(r.FormValue("priority")),
|
||||
Description: r.FormValue("description"),
|
||||
Expression: r.FormValue("expression"),
|
||||
Actions: r.Form["action"],
|
||||
})
|
||||
toJSON(w, createRouteResp{
|
||||
Message: "Route has been created",
|
||||
Route: ms.routeList[len(ms.routeList)-1],
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MockServer) updateRoute(w http.ResponseWriter, r *http.Request) {
|
||||
for i, item := range ms.routeList {
|
||||
if item.Id == chi.URLParam(r, "id") {
|
||||
|
||||
if r.FormValue("action") != "" {
|
||||
ms.routeList[i].Actions = r.Form["action"]
|
||||
}
|
||||
if r.FormValue("priority") != "" {
|
||||
ms.routeList[i].Priority = stringToInt(r.FormValue("priority"))
|
||||
}
|
||||
if r.FormValue("description") != "" {
|
||||
ms.routeList[i].Description = r.FormValue("description")
|
||||
}
|
||||
if r.FormValue("expression") != "" {
|
||||
ms.routeList[i].Expression = r.FormValue("expression")
|
||||
}
|
||||
toJSON(w, ms.routeList[i])
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "route not found"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) deleteRoute(w http.ResponseWriter, r *http.Request) {
|
||||
result := ms.routeList[:0]
|
||||
for _, item := range ms.routeList {
|
||||
if item.Id == chi.URLParam(r, "id") {
|
||||
continue
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
if len(result) != len(ms.domainList) {
|
||||
toJSON(w, okResp{Message: "success"})
|
||||
ms.routeList = result
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "route not found"})
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func (ms *MockServer) addValidationRoutes(r chi.Router) {
|
||||
r.Get("/address/validate", ms.validateEmail)
|
||||
r.Get("/address/parse", ms.parseEmail)
|
||||
r.Get("/address/private/validate", ms.validateEmail)
|
||||
r.Get("/address/private/parse", ms.parseEmail)
|
||||
}
|
||||
|
||||
func (ms *MockServer) validateEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.FormValue("address") == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
toJSON(w, okResp{Message: "'address' parameter is required"})
|
||||
return
|
||||
}
|
||||
|
||||
var results EmailVerification
|
||||
parts, err := mail.ParseAddress(r.FormValue("address"))
|
||||
if err == nil {
|
||||
results.IsValid = true
|
||||
results.Parts.Domain = strings.Split(parts.Address, "@")[1]
|
||||
results.Parts.LocalPart = strings.Split(parts.Address, "@")[0]
|
||||
results.Parts.DisplayName = parts.Name
|
||||
}
|
||||
toJSON(w, results)
|
||||
}
|
||||
|
||||
func (ms *MockServer) parseEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.FormValue("addresses") == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
toJSON(w, okResp{Message: "'addresses' parameter is required"})
|
||||
return
|
||||
}
|
||||
|
||||
addresses := strings.Split(r.FormValue("addresses"), ",")
|
||||
|
||||
var results addressParseResult
|
||||
for _, address := range addresses {
|
||||
_, err := mail.ParseAddress(address)
|
||||
if err != nil {
|
||||
results.Unparseable = append(results.Unparseable, address)
|
||||
} else {
|
||||
results.Parsed = append(results.Parsed, address)
|
||||
}
|
||||
}
|
||||
toJSON(w, results)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func (ms *MockServer) addWebhookRoutes(r chi.Router) {
|
||||
r.Route("/domains/{domain}/webhooks", func(r chi.Router) {
|
||||
r.Get("/", ms.listWebHooks)
|
||||
r.Post("/", ms.postWebHook)
|
||||
r.Get("/{webhook}", ms.getWebHook)
|
||||
r.Put("/{webhook}", ms.putWebHook)
|
||||
r.Delete("/{webhook}", ms.deleteWebHook)
|
||||
})
|
||||
ms.webhooks = WebHooksListResponse{
|
||||
Webhooks: map[string]UrlOrUrls{
|
||||
"new-webhook": {
|
||||
Urls: []string{"http://example.com/new"},
|
||||
},
|
||||
"legacy-webhook": {
|
||||
Url: "http://example.com/legacy",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MockServer) listWebHooks(w http.ResponseWriter, _ *http.Request) {
|
||||
toJSON(w, ms.webhooks)
|
||||
}
|
||||
|
||||
func (ms *MockServer) getWebHook(w http.ResponseWriter, r *http.Request) {
|
||||
resp := WebHookResponse{
|
||||
Webhook: UrlOrUrls{
|
||||
Urls: ms.webhooks.Webhooks[chi.URLParam(r, "webhook")].Urls,
|
||||
},
|
||||
}
|
||||
toJSON(w, resp)
|
||||
}
|
||||
|
||||
func (ms *MockServer) postWebHook(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
toJSON(w, okResp{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var urls []string
|
||||
for _, url := range r.Form["url"] {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
ms.webhooks.Webhooks[r.FormValue("id")] = UrlOrUrls{Urls: urls}
|
||||
|
||||
toJSON(w, okResp{Message: "success"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) putWebHook(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
toJSON(w, okResp{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var urls []string
|
||||
for _, url := range r.Form["url"] {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
ms.webhooks.Webhooks[chi.URLParam(r, "webhook")] = UrlOrUrls{Urls: urls}
|
||||
|
||||
toJSON(w, okResp{Message: "success"})
|
||||
}
|
||||
|
||||
func (ms *MockServer) deleteWebHook(w http.ResponseWriter, r *http.Request) {
|
||||
_, ok := ms.webhooks.Webhooks[chi.URLParam(r, "webhook")]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
toJSON(w, okResp{Message: "webhook not found"})
|
||||
}
|
||||
|
||||
delete(ms.webhooks.Webhooks, chi.URLParam(r, "webhook"))
|
||||
toJSON(w, okResp{Message: "success"})
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/mailgun/mailgun-go/v3/events"
|
||||
"github.com/mailru/easyjson"
|
||||
)
|
||||
|
||||
// All events returned by the EventIterator conform to this interface
|
||||
type Event interface {
|
||||
easyjson.Unmarshaler
|
||||
easyjson.Marshaler
|
||||
GetName() string
|
||||
SetName(name string)
|
||||
GetTimestamp() time.Time
|
||||
SetTimestamp(time.Time)
|
||||
GetID() string
|
||||
SetID(id string)
|
||||
}
|
||||
|
||||
// A list of all JSON event types returned by the /events API
|
||||
var EventNames = map[string]func() Event{
|
||||
"accepted": new_(events.Accepted{}),
|
||||
"clicked": new_(events.Clicked{}),
|
||||
"complained": new_(events.Complained{}),
|
||||
"delivered": new_(events.Delivered{}),
|
||||
"failed": new_(events.Failed{}),
|
||||
"opened": new_(events.Opened{}),
|
||||
"rejected": new_(events.Rejected{}),
|
||||
"stored": new_(events.Stored{}),
|
||||
"unsubscribed": new_(events.Unsubscribed{}),
|
||||
"list_member_uploaded": new_(events.ListMemberUploaded{}),
|
||||
"list_member_upload_error": new_(events.ListMemberUploadError{}),
|
||||
"list_uploaded": new_(events.ListUploaded{}),
|
||||
}
|
||||
|
||||
// new_ is a universal event "constructor".
|
||||
func new_(e interface{}) func() Event {
|
||||
typ := reflect.TypeOf(e)
|
||||
return func() Event {
|
||||
return reflect.New(typ).Interface().(Event)
|
||||
}
|
||||
}
|
||||
|
||||
func parseResponse(raw []byte) ([]Event, error) {
|
||||
var resp events.Response
|
||||
if err := easyjson.Unmarshal(raw, &resp); err != nil {
|
||||
return nil, fmt.Errorf("failed to un-marshall event.Response: %s", err)
|
||||
}
|
||||
|
||||
var result []Event
|
||||
for _, value := range resp.Items {
|
||||
event, err := ParseEvent(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("while parsing event: %s", err)
|
||||
}
|
||||
result = append(result, event)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Given a slice of events.RawJSON events return a slice of Event for each parsed event
|
||||
func ParseEvents(raw []events.RawJSON) ([]Event, error) {
|
||||
var result []Event
|
||||
for _, value := range raw {
|
||||
event, err := ParseEvent(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("while parsing event: %s", err)
|
||||
}
|
||||
result = append(result, event)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Parse converts raw bytes data into an event struct. Can accept events.RawJSON as input
|
||||
func ParseEvent(raw []byte) (Event, error) {
|
||||
// Try to recognize the event first.
|
||||
var e events.EventName
|
||||
if err := easyjson.Unmarshal(raw, &e); err != nil {
|
||||
return nil, fmt.Errorf("failed to recognize event: %v", err)
|
||||
}
|
||||
|
||||
// Get the event "constructor" from the map.
|
||||
newEvent, ok := EventNames[e.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported event: '%s'", e.Name)
|
||||
}
|
||||
event := newEvent()
|
||||
|
||||
// Parse the known event.
|
||||
if err := easyjson.Unmarshal(raw, event); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse event '%s': %v", e.Name, err)
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package mailgun
|
||||
|
||||
import "fmt"
|
||||
import "strings"
|
||||
|
||||
type Recipient struct {
|
||||
Name string `json:"-"`
|
||||
Email string `json:"-"`
|
||||
}
|
||||
|
||||
func (r Recipient) String() string {
|
||||
if r.Name != "" {
|
||||
return fmt.Sprintf("%s <%s>", r.Name, r.Email)
|
||||
}
|
||||
return r.Email
|
||||
}
|
||||
|
||||
// MarshalText satisfies TextMarshaler
|
||||
func (r Recipient) MarshalText() ([]byte, error) {
|
||||
return []byte(r.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText satisfies TextUnmarshaler
|
||||
func (r *Recipient) UnmarshalText(text []byte) error {
|
||||
s := string(text)
|
||||
if s[len(s)-1:] != ">" {
|
||||
*r = Recipient{Email: s}
|
||||
return nil
|
||||
}
|
||||
|
||||
i := strings.Index(s, "<")
|
||||
// at least 1 char followed by a space
|
||||
if i < 2 {
|
||||
return fmt.Errorf("malformed recipient string '%s'", s)
|
||||
}
|
||||
*r = Recipient{
|
||||
Name: strings.TrimSpace(s[:i]),
|
||||
Email: s[i+1 : len(s)-1],
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 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/" + Version
|
||||
|
||||
// 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
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// 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 Error: %s", e.URL, e.Expected, e.Actual, string(e.Data))
|
||||
}
|
||||
|
||||
// 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 *httpResponse) error {
|
||||
return &UnexpectedResponseError{
|
||||
URL: url,
|
||||
Expected: expected,
|
||||
Actual: got.Code,
|
||||
Data: got.Data,
|
||||
}
|
||||
}
|
||||
|
||||
// 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(ctx context.Context, r *httpRequest, method string, p payload) (*httpResponse, error) {
|
||||
r.addHeader("User-Agent", MailgunGoUserAgent)
|
||||
rsp, err := r.makeRequest(ctx, method, p)
|
||||
if (err == nil) && notGood(rsp.Code, expected) {
|
||||
return rsp, newError(r.URL, expected, rsp)
|
||||
}
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
// getResponseFromJSON shim performs a GET request, checking for a positive outcome.
|
||||
// See simplehttp.GetResponseFromJSON for more details.
|
||||
func getResponseFromJSON(ctx context.Context, r *httpRequest, v interface{}) error {
|
||||
r.addHeader("User-Agent", MailgunGoUserAgent)
|
||||
response, err := r.makeGetRequest(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if notGood(response.Code, expected) {
|
||||
return newError(r.URL, expected, response)
|
||||
}
|
||||
return response.parseFromJSON(v)
|
||||
}
|
||||
|
||||
// postResponseFromJSON shim performs a POST request, checking for a positive outcome.
|
||||
// See simplehttp.PostResponseFromJSON for more details.
|
||||
func postResponseFromJSON(ctx context.Context, r *httpRequest, p payload, v interface{}) error {
|
||||
r.addHeader("User-Agent", MailgunGoUserAgent)
|
||||
response, err := r.makePostRequest(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if notGood(response.Code, expected) {
|
||||
return newError(r.URL, expected, response)
|
||||
}
|
||||
return response.parseFromJSON(v)
|
||||
}
|
||||
|
||||
// putResponseFromJSON shim performs a PUT request, checking for a positive outcome.
|
||||
// See simplehttp.PutResponseFromJSON for more details.
|
||||
func putResponseFromJSON(ctx context.Context, r *httpRequest, p payload, v interface{}) error {
|
||||
r.addHeader("User-Agent", MailgunGoUserAgent)
|
||||
response, err := r.makePutRequest(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if notGood(response.Code, expected) {
|
||||
return newError(r.URL, expected, response)
|
||||
}
|
||||
return response.parseFromJSON(v)
|
||||
}
|
||||
|
||||
// makeGetRequest shim performs a GET request, checking for a positive outcome.
|
||||
// See simplehttp.MakeGetRequest for more details.
|
||||
func makeGetRequest(ctx context.Context, r *httpRequest) (*httpResponse, error) {
|
||||
r.addHeader("User-Agent", MailgunGoUserAgent)
|
||||
rsp, err := r.makeGetRequest(ctx)
|
||||
if (err == nil) && notGood(rsp.Code, expected) {
|
||||
return rsp, newError(r.URL, expected, rsp)
|
||||
}
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
// makePostRequest shim performs a POST request, checking for a positive outcome.
|
||||
// See simplehttp.MakePostRequest for more details.
|
||||
func makePostRequest(ctx context.Context, r *httpRequest, p payload) (*httpResponse, error) {
|
||||
r.addHeader("User-Agent", MailgunGoUserAgent)
|
||||
rsp, err := r.makePostRequest(ctx, p)
|
||||
if (err == nil) && notGood(rsp.Code, expected) {
|
||||
return rsp, newError(r.URL, expected, rsp)
|
||||
}
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
// makePutRequest shim performs a PUT request, checking for a positive outcome.
|
||||
// See simplehttp.MakePutRequest for more details.
|
||||
func makePutRequest(ctx context.Context, r *httpRequest, p payload) (*httpResponse, error) {
|
||||
r.addHeader("User-Agent", MailgunGoUserAgent)
|
||||
rsp, err := r.makePutRequest(ctx, p)
|
||||
if (err == nil) && notGood(rsp.Code, expected) {
|
||||
return rsp, newError(r.URL, expected, rsp)
|
||||
}
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
// makeDeleteRequest shim performs a DELETE request, checking for a positive outcome.
|
||||
// See simplehttp.MakeDeleteRequest for more details.
|
||||
func makeDeleteRequest(ctx context.Context, r *httpRequest) (*httpResponse, error) {
|
||||
r.addHeader("User-Agent", MailgunGoUserAgent)
|
||||
rsp, err := r.makeDeleteRequest(ctx)
|
||||
if (err == nil) && notGood(rsp.Code, expected) {
|
||||
return rsp, newError(r.URL, expected, rsp)
|
||||
}
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
// Extract the http status code from error object
|
||||
func GetStatusFromErr(err error) int {
|
||||
obj, ok := err.(*UnexpectedResponseError)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return obj.Actual
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Mailgun uses RFC2822 format for timestamps everywhere ('Thu, 13 Oct 2011 18:02:00 GMT'), but
|
||||
// by default Go's JSON package uses another format when decoding/encoding timestamps.
|
||||
type RFC2822Time time.Time
|
||||
|
||||
func NewRFC2822Time(str string) (RFC2822Time, error) {
|
||||
t, err := time.Parse(time.RFC1123, str)
|
||||
if err != nil {
|
||||
return RFC2822Time{}, err
|
||||
}
|
||||
return RFC2822Time(t), nil
|
||||
}
|
||||
|
||||
func (t RFC2822Time) Unix() int64 {
|
||||
return time.Time(t).Unix()
|
||||
}
|
||||
|
||||
func (t RFC2822Time) IsZero() bool {
|
||||
return time.Time(t).IsZero()
|
||||
}
|
||||
|
||||
func (t RFC2822Time) MarshalJSON() ([]byte, error) {
|
||||
return []byte(strconv.Quote(time.Time(t).Format(time.RFC1123))), nil
|
||||
}
|
||||
|
||||
func (t *RFC2822Time) UnmarshalJSON(s []byte) error {
|
||||
q, err := strconv.Unquote(string(s))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *(*time.Time)(t), err = time.Parse(time.RFC1123, q); err != nil {
|
||||
if strings.Contains(err.Error(), "extra text") {
|
||||
if *(*time.Time)(t), err = time.Parse(time.RFC1123Z, q); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t RFC2822Time) String() string {
|
||||
return time.Time(t).Format(time.RFC1123)
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// A Route structure contains information on a configured or to-be-configured 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 {
|
||||
// The Priority field indicates how soon the route works relative to other configured routes.
|
||||
// Routes of equal priority are consulted in chronological order.
|
||||
Priority int `json:"priority,omitempty"`
|
||||
// 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.
|
||||
Description string `json:"description,omitempty"`
|
||||
// The Expression field lets you specify a pattern to match incoming messages against.
|
||||
Expression string `json:"expression,omitempty"`
|
||||
// The Actions field contains strings specifying what to do
|
||||
// with any message which matches the provided expression.
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
|
||||
// The CreatedAt field provides a time-stamp for when the route came into existence.
|
||||
CreatedAt RFC2822Time `json:"created_at,omitempty"`
|
||||
// ID field provides a unique identifier for this route.
|
||||
Id string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type routesListResponse struct {
|
||||
// is -1 if Next() or First() have not been called
|
||||
TotalCount int `json:"total_count"`
|
||||
Items []Route `json:"items"`
|
||||
}
|
||||
|
||||
type createRouteResp struct {
|
||||
Message string `json:"message"`
|
||||
Route `json:"route"`
|
||||
}
|
||||
|
||||
// ListRoutes allows you to iterate through a list of routes returned by the API
|
||||
func (mg *MailgunImpl) ListRoutes(opts *ListOptions) *RoutesIterator {
|
||||
var limit int
|
||||
if opts != nil {
|
||||
limit = opts.Limit
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
return &RoutesIterator{
|
||||
mg: mg,
|
||||
url: generatePublicApiUrl(mg, routesEndpoint),
|
||||
routesListResponse: routesListResponse{TotalCount: -1},
|
||||
limit: limit,
|
||||
}
|
||||
}
|
||||
|
||||
type RoutesIterator struct {
|
||||
routesListResponse
|
||||
|
||||
limit int
|
||||
mg Mailgun
|
||||
offset int
|
||||
url string
|
||||
err error
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (ri *RoutesIterator) Err() error {
|
||||
return ri.err
|
||||
}
|
||||
|
||||
// Offset returns the current offset of the iterator
|
||||
func (ri *RoutesIterator) Offset() int {
|
||||
return ri.offset
|
||||
}
|
||||
|
||||
// Next retrieves the next page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (ri *RoutesIterator) Next(ctx context.Context, items *[]Route) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ri.err = ri.fetch(ctx, ri.offset, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
cpy := make([]Route, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
if len(ri.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
ri.offset = ri.offset + len(ri.Items)
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of items from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (ri *RoutesIterator) First(ctx context.Context, items *[]Route) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
ri.err = ri.fetch(ctx, 0, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Route, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
ri.offset = len(ri.Items)
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of items from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (ri *RoutesIterator) Last(ctx context.Context, items *[]Route) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ri.TotalCount == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
ri.offset = ri.TotalCount - ri.limit
|
||||
if ri.offset < 0 {
|
||||
ri.offset = 0
|
||||
}
|
||||
|
||||
ri.err = ri.fetch(ctx, ri.offset, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Route, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (ri *RoutesIterator) Previous(ctx context.Context, items *[]Route) bool {
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ri.TotalCount == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
ri.offset = ri.offset - (ri.limit * 2)
|
||||
if ri.offset < 0 {
|
||||
ri.offset = 0
|
||||
}
|
||||
|
||||
ri.err = ri.fetch(ctx, ri.offset, ri.limit)
|
||||
if ri.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Route, len(ri.Items))
|
||||
copy(cpy, ri.Items)
|
||||
*items = cpy
|
||||
if len(ri.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ri *RoutesIterator) fetch(ctx context.Context, skip, limit int) error {
|
||||
r := newHTTPRequest(ri.url)
|
||||
r.setBasicAuth(basicAuthUser, ri.mg.APIKey())
|
||||
r.setClient(ri.mg.Client())
|
||||
|
||||
if skip != 0 {
|
||||
r.addParameter("skip", strconv.Itoa(skip))
|
||||
}
|
||||
if limit != 0 {
|
||||
r.addParameter("limit", strconv.Itoa(limit))
|
||||
}
|
||||
|
||||
return getResponseFromJSON(ctx, r, &ri.routesListResponse)
|
||||
}
|
||||
|
||||
// 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(ctx context.Context, prototype Route) (_ignored Route, err error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, routesEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := 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 resp createRouteResp
|
||||
if err = postResponseFromJSON(ctx, r, p, &resp); err != nil {
|
||||
return _ignored, err
|
||||
}
|
||||
return resp.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(ctx context.Context, id string) error {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, routesEndpoint) + "/" + id)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetRoute retrieves the complete route definition associated with the unique route ID.
|
||||
func (mg *MailgunImpl) GetRoute(ctx context.Context, id string) (Route, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, routesEndpoint) + "/" + id)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
var envelope struct {
|
||||
Message string `json:"message"`
|
||||
*Route `json:"route"`
|
||||
}
|
||||
err := getResponseFromJSON(ctx, r, &envelope)
|
||||
if err != nil {
|
||||
return Route{}, err
|
||||
}
|
||||
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(ctx context.Context, id string, route Route) (Route, error) {
|
||||
r := newHTTPRequest(generatePublicApiUrl(mg, routesEndpoint) + "/" + id)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := 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(ctx, r, p, &envelope)
|
||||
return envelope, err
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
complaintsEndpoint = "complaints"
|
||||
)
|
||||
|
||||
// Complaint structures track how many times one of your emails have been marked as spam.
|
||||
// the recipient thought your messages were not solicited.
|
||||
type Complaint struct {
|
||||
Count int `json:"count"`
|
||||
CreatedAt RFC2822Time `json:"created_at"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type complaintsResponse struct {
|
||||
Paging Paging `json:"paging"`
|
||||
Items []Complaint `json:"items"`
|
||||
}
|
||||
|
||||
// ListComplaints 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 (mg *MailgunImpl) ListComplaints(opts *ListOptions) *ComplaintsIterator {
|
||||
r := newHTTPRequest(generateApiUrl(mg, complaintsEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
if opts != nil {
|
||||
if opts.Limit != 0 {
|
||||
r.addParameter("limit", strconv.Itoa(opts.Limit))
|
||||
}
|
||||
}
|
||||
url, err := r.generateUrlWithParameters()
|
||||
return &ComplaintsIterator{
|
||||
mg: mg,
|
||||
complaintsResponse: complaintsResponse{Paging: Paging{Next: url, First: url}},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
type ComplaintsIterator struct {
|
||||
complaintsResponse
|
||||
mg Mailgun
|
||||
err error
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (ci *ComplaintsIterator) Err() error {
|
||||
return ci.err
|
||||
}
|
||||
|
||||
// Next retrieves the next page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (ci *ComplaintsIterator) Next(ctx context.Context, items *[]Complaint) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.Next)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Complaint, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
if len(ci.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of items from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (ci *ComplaintsIterator) First(ctx context.Context, items *[]Complaint) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.First)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Complaint, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of items from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (ci *ComplaintsIterator) Last(ctx context.Context, items *[]Complaint) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.Last)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Complaint, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (ci *ComplaintsIterator) Previous(ctx context.Context, items *[]Complaint) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
if ci.Paging.Previous == "" {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.Previous)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Complaint, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
if len(ci.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ci *ComplaintsIterator) fetch(ctx context.Context, url string) error {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(ci.mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, ci.mg.APIKey())
|
||||
|
||||
return getResponseFromJSON(ctx, r, &ci.complaintsResponse)
|
||||
}
|
||||
|
||||
// GetComplaint 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 (mg *MailgunImpl) GetComplaint(ctx context.Context, address string) (Complaint, error) {
|
||||
r := newHTTPRequest(generateApiUrl(mg, complaintsEndpoint) + "/" + address)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
var c Complaint
|
||||
err := getResponseFromJSON(ctx, r, &c)
|
||||
return c, err
|
||||
}
|
||||
|
||||
// CreateComplaint registers the specified address as a recipient who has complained of receiving spam
|
||||
// from your domain.
|
||||
func (mg *MailgunImpl) CreateComplaint(ctx context.Context, address string) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, complaintsEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := newUrlEncodedPayload()
|
||||
p.addValue("address", address)
|
||||
_, err := makePostRequest(ctx, 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 (mg *MailgunImpl) DeleteComplaint(ctx context.Context, address string) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, complaintsEndpoint) + "/" + address)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Stats on accepted messages
|
||||
type Accepted struct {
|
||||
Incoming int `json:"incoming"`
|
||||
Outgoing int `json:"outgoing"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// Stats on delivered messages
|
||||
type Delivered struct {
|
||||
Smtp int `json:"smtp"`
|
||||
Http int `json:"http"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// Stats on temporary failures
|
||||
type Temporary struct {
|
||||
Espblock int `json:"espblock"`
|
||||
}
|
||||
|
||||
// Stats on permanent failures
|
||||
type Permanent struct {
|
||||
SuppressBounce int `json:"suppress-bounce"`
|
||||
SuppressUnsubscribe int `json:"suppress-unsubscribe"`
|
||||
SuppressComplaint int `json:"suppress-complaint"`
|
||||
Bounce int `json:"bounce"`
|
||||
DelayedBounce int `json:"delayed-bounce"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// Stats on failed messages
|
||||
type Failed struct {
|
||||
Temporary Temporary `json:"temporary"`
|
||||
Permanent Permanent `json:"permanent"`
|
||||
}
|
||||
|
||||
// Total stats for messages
|
||||
type Total struct {
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// Stats as returned by `GetStats()`
|
||||
type Stats struct {
|
||||
Time string `json:"time"`
|
||||
Accepted Accepted `json:"accepted"`
|
||||
Delivered Delivered `json:"delivered"`
|
||||
Failed Failed `json:"failed"`
|
||||
Stored Total `json:"stored"`
|
||||
Opened Total `json:"opened"`
|
||||
Clicked Total `json:"clicked"`
|
||||
Unsubscribed Total `json:"unsubscribed"`
|
||||
Complained Total `json:"complained"`
|
||||
}
|
||||
|
||||
type statsTotalResponse struct {
|
||||
End string `json:"end"`
|
||||
Resolution string `json:"resolution"`
|
||||
Start string `json:"start"`
|
||||
Stats []Stats `json:"stats"`
|
||||
}
|
||||
|
||||
// Used by GetStats() to specify the resolution stats are for
|
||||
type Resolution string
|
||||
|
||||
// Indicate which resolution a stat response for request is for
|
||||
const (
|
||||
ResolutionHour = Resolution("hour")
|
||||
ResolutionDay = Resolution("day")
|
||||
ResolutionMonth = Resolution("month")
|
||||
)
|
||||
|
||||
// Options for GetStats()
|
||||
type GetStatOptions struct {
|
||||
Resolution Resolution
|
||||
Duration string
|
||||
Start time.Time
|
||||
End time.Time
|
||||
}
|
||||
|
||||
// GetStats returns total stats for a given domain for the specified time period
|
||||
func (mg *MailgunImpl) GetStats(ctx context.Context, events []string, opts *GetStatOptions) ([]Stats, error) {
|
||||
r := newHTTPRequest(generateApiUrl(mg, statsTotalEndpoint))
|
||||
|
||||
if opts != nil {
|
||||
if !opts.Start.IsZero() {
|
||||
r.addParameter("start", strconv.Itoa(int(opts.Start.Unix())))
|
||||
}
|
||||
if !opts.End.IsZero() {
|
||||
r.addParameter("end", strconv.Itoa(int(opts.End.Unix())))
|
||||
}
|
||||
if opts.Resolution != "" {
|
||||
r.addParameter("resolution", string(opts.Resolution))
|
||||
}
|
||||
if opts.Duration != "" {
|
||||
r.addParameter("duration", opts.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
r.addParameter("event", e)
|
||||
}
|
||||
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
var res statsTotalResponse
|
||||
err := getResponseFromJSON(ctx, r, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return res.Stats, nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Value string `json:"tag"`
|
||||
Description string `json:"description"`
|
||||
FirstSeen *time.Time `json:"first-seen,omitempty"`
|
||||
LastSeen *time.Time `json:"last-seen,omitempty"`
|
||||
}
|
||||
|
||||
type tagsResponse struct {
|
||||
Items []Tag `json:"items"`
|
||||
Paging Paging `json:"paging"`
|
||||
}
|
||||
|
||||
type ListTagOptions struct {
|
||||
// Restrict the page size to this limit
|
||||
Limit int
|
||||
// Return only the tags starting with the given prefix
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// DeleteTag removes all counters for a particular tag, including the tag itself.
|
||||
func (mg *MailgunImpl) DeleteTag(ctx context.Context, tag string) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, tagsEndpoint) + "/" + tag)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTag retrieves metadata about the tag from the api
|
||||
func (mg *MailgunImpl) GetTag(ctx context.Context, tag string) (Tag, error) {
|
||||
r := newHTTPRequest(generateApiUrl(mg, tagsEndpoint) + "/" + tag)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
var tagItem Tag
|
||||
return tagItem, getResponseFromJSON(ctx, r, &tagItem)
|
||||
}
|
||||
|
||||
// ListTags returns a cursor used to iterate through a list of tags
|
||||
// it := mg.ListTags(nil)
|
||||
// var page []mailgun.Tag
|
||||
// for it.Next(&page) {
|
||||
// for _, tag := range(page) {
|
||||
// // Do stuff with tags
|
||||
// }
|
||||
// }
|
||||
// if it.Err() != nil {
|
||||
// log.Fatal(it.Err())
|
||||
// }
|
||||
func (mg *MailgunImpl) ListTags(opts *ListTagOptions) *TagIterator {
|
||||
req := newHTTPRequest(generateApiUrl(mg, tagsEndpoint))
|
||||
if opts != nil {
|
||||
if opts.Limit != 0 {
|
||||
req.addParameter("limit", strconv.Itoa(opts.Limit))
|
||||
}
|
||||
if opts.Prefix != "" {
|
||||
req.addParameter("prefix", opts.Prefix)
|
||||
}
|
||||
}
|
||||
|
||||
url, err := req.generateUrlWithParameters()
|
||||
return &TagIterator{
|
||||
tagsResponse: tagsResponse{Paging: Paging{Next: url, First: url}},
|
||||
err: err,
|
||||
mg: mg,
|
||||
}
|
||||
}
|
||||
|
||||
type TagIterator struct {
|
||||
tagsResponse
|
||||
mg Mailgun
|
||||
err error
|
||||
}
|
||||
|
||||
// Next returns the next page in the list of tags
|
||||
func (ti *TagIterator) Next(ctx context.Context, items *[]Tag) bool {
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !canFetchPage(ti.Paging.Next) {
|
||||
return false
|
||||
}
|
||||
|
||||
ti.err = ti.fetch(ctx, ti.Paging.Next)
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
*items = ti.Items
|
||||
if len(ti.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous returns the previous page in the list of tags
|
||||
func (ti *TagIterator) Previous(ctx context.Context, items *[]Tag) bool {
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ti.Paging.Previous == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !canFetchPage(ti.Paging.Previous) {
|
||||
return false
|
||||
}
|
||||
|
||||
ti.err = ti.fetch(ctx, ti.Paging.Previous)
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
*items = ti.Items
|
||||
if len(ti.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// First returns the first page in the list of tags
|
||||
func (ti *TagIterator) First(ctx context.Context, items *[]Tag) bool {
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
ti.err = ti.fetch(ctx, ti.Paging.First)
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
*items = ti.Items
|
||||
return true
|
||||
}
|
||||
|
||||
// Last returns the last page in the list of tags
|
||||
func (ti *TagIterator) Last(ctx context.Context, items *[]Tag) bool {
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
ti.err = ti.fetch(ctx, ti.Paging.Last)
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
*items = ti.Items
|
||||
return true
|
||||
}
|
||||
|
||||
// Err returns any error if one occurred
|
||||
func (ti *TagIterator) Err() error {
|
||||
return ti.err
|
||||
}
|
||||
|
||||
func (ti *TagIterator) fetch(ctx context.Context, url string) error {
|
||||
req := newHTTPRequest(url)
|
||||
req.setClient(ti.mg.Client())
|
||||
req.setBasicAuth(basicAuthUser, ti.mg.APIKey())
|
||||
return getResponseFromJSON(ctx, req, &ti.tagsResponse)
|
||||
}
|
||||
|
||||
func canFetchPage(slug string) bool {
|
||||
parts, err := url.Parse(slug)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
params, _ := url.ParseQuery(parts.RawQuery)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
value, ok := params["tag"]
|
||||
// If tags doesn't exist, it's our first time fetching pages
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
// If tags has no value, there are no more pages to fetch
|
||||
return len(value) == 0
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TemplateEngine string
|
||||
|
||||
// Used by CreateTemplate() and AddTemplateVersion() to specify the template engine
|
||||
const (
|
||||
TemplateEngineMustache = TemplateEngine("mustache")
|
||||
TemplateEngineHandlebars = TemplateEngine("handlebars")
|
||||
TemplateEngineGo = TemplateEngine("go")
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt RFC2822Time `json:"createdAt"`
|
||||
Version TemplateVersion `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type templateResp struct {
|
||||
Item Template `json:"template"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type templateListResp struct {
|
||||
Items []Template `json:"items"`
|
||||
Paging Paging `json:"paging"`
|
||||
}
|
||||
|
||||
// Create a new template which can be used to attach template versions to
|
||||
func (mg *MailgunImpl) CreateTemplate(ctx context.Context, template *Template) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
|
||||
if template.Name != "" {
|
||||
payload.addValue("name", template.Name)
|
||||
}
|
||||
if template.Description != "" {
|
||||
payload.addValue("description", template.Description)
|
||||
}
|
||||
|
||||
if template.Version.Engine != "" {
|
||||
payload.addValue("engine", string(template.Version.Engine))
|
||||
}
|
||||
if template.Version.Template != "" {
|
||||
payload.addValue("template", template.Version.Template)
|
||||
}
|
||||
if template.Version.Comment != "" {
|
||||
payload.addValue("comment", template.Version.Comment)
|
||||
}
|
||||
|
||||
var resp templateResp
|
||||
if err := postResponseFromJSON(ctx, r, payload, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
*template = resp.Item
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTemplate gets a template given the template name
|
||||
func (mg *MailgunImpl) GetTemplate(ctx context.Context, name string) (Template, error) {
|
||||
r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint) + "/" + name)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
r.addParameter("active", "yes")
|
||||
|
||||
var resp templateResp
|
||||
err := getResponseFromJSON(ctx, r, &resp)
|
||||
if err != nil {
|
||||
return Template{}, err
|
||||
}
|
||||
return resp.Item, nil
|
||||
}
|
||||
|
||||
// Update the name and description of a template
|
||||
func (mg *MailgunImpl) UpdateTemplate(ctx context.Context, template *Template) error {
|
||||
if template.Name == "" {
|
||||
return errors.New("UpdateTemplate() Template.Name cannot be empty")
|
||||
}
|
||||
|
||||
r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint) + "/" + template.Name)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := newUrlEncodedPayload()
|
||||
|
||||
if template.Name != "" {
|
||||
p.addValue("name", template.Name)
|
||||
}
|
||||
if template.Description != "" {
|
||||
p.addValue("description", template.Description)
|
||||
}
|
||||
|
||||
var resp templateResp
|
||||
err := putResponseFromJSON(ctx, r, p, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*template = resp.Item
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete a template given a template name
|
||||
func (mg *MailgunImpl) DeleteTemplate(ctx context.Context, name string) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint) + "/" + name)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
||||
|
||||
type TemplatesIterator struct {
|
||||
templateListResp
|
||||
mg Mailgun
|
||||
err error
|
||||
}
|
||||
|
||||
type ListTemplateOptions struct {
|
||||
Limit int
|
||||
Active bool
|
||||
}
|
||||
|
||||
// List all available templates
|
||||
func (mg *MailgunImpl) ListTemplates(opts *ListTemplateOptions) *TemplatesIterator {
|
||||
r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
if opts != nil {
|
||||
if opts.Limit != 0 {
|
||||
r.addParameter("limit", strconv.Itoa(opts.Limit))
|
||||
}
|
||||
if opts.Active {
|
||||
r.addParameter("active", "yes")
|
||||
}
|
||||
}
|
||||
url, err := r.generateUrlWithParameters()
|
||||
return &TemplatesIterator{
|
||||
mg: mg,
|
||||
templateListResp: templateListResp{Paging: Paging{Next: url, First: url}},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (ti *TemplatesIterator) Err() error {
|
||||
return ti.err
|
||||
}
|
||||
|
||||
// Next retrieves the next page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (ti *TemplatesIterator) Next(ctx context.Context, items *[]Template) bool {
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
ti.err = ti.fetch(ctx, ti.Paging.Next)
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Template, len(ti.Items))
|
||||
copy(cpy, ti.Items)
|
||||
*items = cpy
|
||||
if len(ti.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of items from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (ti *TemplatesIterator) First(ctx context.Context, items *[]Template) bool {
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
ti.err = ti.fetch(ctx, ti.Paging.First)
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Template, len(ti.Items))
|
||||
copy(cpy, ti.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of items from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (ti *TemplatesIterator) Last(ctx context.Context, items *[]Template) bool {
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
ti.err = ti.fetch(ctx, ti.Paging.Last)
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Template, len(ti.Items))
|
||||
copy(cpy, ti.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (ti *TemplatesIterator) Previous(ctx context.Context, items *[]Template) bool {
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
if ti.Paging.Previous == "" {
|
||||
return false
|
||||
}
|
||||
ti.err = ti.fetch(ctx, ti.Paging.Previous)
|
||||
if ti.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Template, len(ti.Items))
|
||||
copy(cpy, ti.Items)
|
||||
*items = cpy
|
||||
if len(ti.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ti *TemplatesIterator) fetch(ctx context.Context, url string) error {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(ti.mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, ti.mg.APIKey())
|
||||
|
||||
return getResponseFromJSON(ctx, r, &ti.templateListResp)
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type TemplateVersion struct {
|
||||
Tag string `json:"tag"`
|
||||
Template string `json:"template,omitempty"`
|
||||
Engine TemplateEngine `json:"engine"`
|
||||
CreatedAt RFC2822Time `json:"createdAt"`
|
||||
Comment string `json:"comment"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
type templateVersionListResp struct {
|
||||
Template struct {
|
||||
Template
|
||||
Versions []TemplateVersion `json:"versions,omitempty"`
|
||||
} `json:"template"`
|
||||
Paging Paging `json:"paging"`
|
||||
}
|
||||
|
||||
// AddTemplateVersion adds a template version to a template
|
||||
func (mg *MailgunImpl) AddTemplateVersion(ctx context.Context, templateName string, version *TemplateVersion) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint) + "/" + templateName + "/versions")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
payload := newUrlEncodedPayload()
|
||||
payload.addValue("template", version.Template)
|
||||
|
||||
if version.Tag != "" {
|
||||
payload.addValue("tag", string(version.Tag))
|
||||
}
|
||||
if version.Engine != "" {
|
||||
payload.addValue("engine", string(version.Engine))
|
||||
}
|
||||
if version.Comment != "" {
|
||||
payload.addValue("comment", version.Comment)
|
||||
}
|
||||
if version.Active {
|
||||
payload.addValue("active", boolToString(version.Active))
|
||||
}
|
||||
|
||||
var resp templateResp
|
||||
if err := postResponseFromJSON(ctx, r, payload, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
*version = resp.Item.Version
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTemplateVersion gets a specific version of a template
|
||||
func (mg *MailgunImpl) GetTemplateVersion(ctx context.Context, templateName, tag string) (TemplateVersion, error) {
|
||||
r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint) + "/" + templateName + "/versions/" + tag)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
var resp templateResp
|
||||
err := getResponseFromJSON(ctx, r, &resp)
|
||||
if err != nil {
|
||||
return TemplateVersion{}, err
|
||||
}
|
||||
return resp.Item.Version, nil
|
||||
}
|
||||
|
||||
// Update the comment and mark a version of a template active
|
||||
func (mg *MailgunImpl) UpdateTemplateVersion(ctx context.Context, templateName string, version *TemplateVersion) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint) + "/" + templateName + "/versions/" + version.Tag)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := newUrlEncodedPayload()
|
||||
|
||||
if version.Comment != "" {
|
||||
p.addValue("comment", version.Comment)
|
||||
}
|
||||
if version.Active {
|
||||
p.addValue("active", boolToString(version.Active))
|
||||
}
|
||||
|
||||
var resp templateResp
|
||||
err := putResponseFromJSON(ctx, r, p, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*version = resp.Item.Version
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete a specific version of a template
|
||||
func (mg *MailgunImpl) DeleteTemplateVersion(ctx context.Context, templateName, tag string) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint) + "/" + templateName + "/versions/" + tag)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
||||
|
||||
type TemplateVersionsIterator struct {
|
||||
templateVersionListResp
|
||||
mg Mailgun
|
||||
err error
|
||||
}
|
||||
|
||||
// List all the versions of a specific template
|
||||
func (mg *MailgunImpl) ListTemplateVersions(templateName string, opts *ListOptions) *TemplateVersionsIterator {
|
||||
r := newHTTPRequest(generateApiUrl(mg, templatesEndpoint) + "/" + templateName + "/versions")
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
if opts != nil {
|
||||
if opts.Limit != 0 {
|
||||
r.addParameter("limit", strconv.Itoa(opts.Limit))
|
||||
}
|
||||
}
|
||||
url, err := r.generateUrlWithParameters()
|
||||
return &TemplateVersionsIterator{
|
||||
mg: mg,
|
||||
templateVersionListResp: templateVersionListResp{Paging: Paging{Next: url, First: url}},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (li *TemplateVersionsIterator) Err() error {
|
||||
return li.err
|
||||
}
|
||||
|
||||
// Next retrieves the next page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (li *TemplateVersionsIterator) Next(ctx context.Context, items *[]TemplateVersion) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.Next)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]TemplateVersion, len(li.Template.Versions))
|
||||
copy(cpy, li.Template.Versions)
|
||||
*items = cpy
|
||||
if len(li.Template.Versions) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of items from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (li *TemplateVersionsIterator) First(ctx context.Context, items *[]TemplateVersion) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.First)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]TemplateVersion, len(li.Template.Versions))
|
||||
copy(cpy, li.Template.Versions)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of items from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (li *TemplateVersionsIterator) Last(ctx context.Context, items *[]TemplateVersion) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.Last)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]TemplateVersion, len(li.Template.Versions))
|
||||
copy(cpy, li.Template.Versions)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (li *TemplateVersionsIterator) Previous(ctx context.Context, items *[]TemplateVersion) bool {
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
if li.Paging.Previous == "" {
|
||||
return false
|
||||
}
|
||||
li.err = li.fetch(ctx, li.Paging.Previous)
|
||||
if li.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]TemplateVersion, len(li.Template.Versions))
|
||||
copy(cpy, li.Template.Versions)
|
||||
*items = cpy
|
||||
if len(li.Template.Versions) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (li *TemplateVersionsIterator) fetch(ctx context.Context, url string) error {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(li.mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, li.mg.APIKey())
|
||||
|
||||
return getResponseFromJSON(ctx, r, &li.templateVersionListResp)
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Unsubscribe struct {
|
||||
CreatedAt RFC2822Time `json:"created_at"`
|
||||
Tags []string `json:"tags"`
|
||||
ID string `json:"id"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type unsubscribesResponse struct {
|
||||
Paging Paging `json:"paging"`
|
||||
Items []Unsubscribe `json:"items"`
|
||||
}
|
||||
|
||||
// Fetches the list of unsubscribes
|
||||
func (mg *MailgunImpl) ListUnsubscribes(opts *ListOptions) *UnsubscribesIterator {
|
||||
r := newHTTPRequest(generateApiUrl(mg, unsubscribesEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
if opts != nil {
|
||||
if opts.Limit != 0 {
|
||||
r.addParameter("limit", strconv.Itoa(opts.Limit))
|
||||
}
|
||||
}
|
||||
url, err := r.generateUrlWithParameters()
|
||||
return &UnsubscribesIterator{
|
||||
mg: mg,
|
||||
unsubscribesResponse: unsubscribesResponse{Paging: Paging{Next: url, First: url}},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
type UnsubscribesIterator struct {
|
||||
unsubscribesResponse
|
||||
mg Mailgun
|
||||
err error
|
||||
}
|
||||
|
||||
// If an error occurred during iteration `Err()` will return non nil
|
||||
func (ci *UnsubscribesIterator) Err() error {
|
||||
return ci.err
|
||||
}
|
||||
|
||||
// Next retrieves the next page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error
|
||||
func (ci *UnsubscribesIterator) Next(ctx context.Context, items *[]Unsubscribe) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.Next)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Unsubscribe, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
if len(ci.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// First retrieves the first page of items from the api. Returns false if there
|
||||
// was an error. It also sets the iterator object to the first page.
|
||||
// Use `.Err()` to retrieve the error.
|
||||
func (ci *UnsubscribesIterator) First(ctx context.Context, items *[]Unsubscribe) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.First)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Unsubscribe, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Last retrieves the last page of items from the api.
|
||||
// Calling Last() is invalid unless you first call First() or Next()
|
||||
// Returns false if there was an error. It also sets the iterator object
|
||||
// to the last page. Use `.Err()` to retrieve the error.
|
||||
func (ci *UnsubscribesIterator) Last(ctx context.Context, items *[]Unsubscribe) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.Last)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Unsubscribe, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous retrieves the previous page of items from the api. Returns false when there
|
||||
// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
|
||||
// the error if any
|
||||
func (ci *UnsubscribesIterator) Previous(ctx context.Context, items *[]Unsubscribe) bool {
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
if ci.Paging.Previous == "" {
|
||||
return false
|
||||
}
|
||||
ci.err = ci.fetch(ctx, ci.Paging.Previous)
|
||||
if ci.err != nil {
|
||||
return false
|
||||
}
|
||||
cpy := make([]Unsubscribe, len(ci.Items))
|
||||
copy(cpy, ci.Items)
|
||||
*items = cpy
|
||||
if len(ci.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ci *UnsubscribesIterator) fetch(ctx context.Context, url string) error {
|
||||
r := newHTTPRequest(url)
|
||||
r.setClient(ci.mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, ci.mg.APIKey())
|
||||
|
||||
return getResponseFromJSON(ctx, r, &ci.unsubscribesResponse)
|
||||
}
|
||||
|
||||
// Retreives a single unsubscribe record. Can be used to check if a given address is present in the list of unsubscribed users.
|
||||
func (mg *MailgunImpl) GetUnsubscribe(ctx context.Context, address string) (Unsubscribe, error) {
|
||||
r := newHTTPRequest(generateApiUrlWithTarget(mg, unsubscribesEndpoint, address))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
envelope := Unsubscribe{}
|
||||
err := getResponseFromJSON(ctx, r, &envelope)
|
||||
|
||||
return envelope, err
|
||||
}
|
||||
|
||||
// Unsubscribe adds an e-mail address to the domain's unsubscription table.
|
||||
func (mg *MailgunImpl) CreateUnsubscribe(ctx context.Context, address, tag string) error {
|
||||
r := newHTTPRequest(generateApiUrl(mg, unsubscribesEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := newUrlEncodedPayload()
|
||||
p.addValue("address", address)
|
||||
p.addValue("tag", tag)
|
||||
_, err := makePostRequest(ctx, r, p)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteUnsubscribe removes the e-mail address given from the domain's unsubscription table.
|
||||
// If passing in an ID (discoverable from, e.g., ListUnsubscribes()), the e-mail address associated
|
||||
// with the given ID will be removed.
|
||||
func (mg *MailgunImpl) DeleteUnsubscribe(ctx context.Context, address string) error {
|
||||
r := newHTTPRequest(generateApiUrlWithTarget(mg, unsubscribesEndpoint, address))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteUnsubscribeWithTag removes the e-mail address given from the domain's unsubscription table with a matching tag.
|
||||
// If passing in an ID (discoverable from, e.g., ListUnsubscribes()), the e-mail address associated
|
||||
// with the given ID will be removed.
|
||||
func (mg *MailgunImpl) DeleteUnsubscribeWithTag(ctx context.Context, a, t string) error {
|
||||
r := newHTTPRequest(generateApiUrlWithTarget(mg, unsubscribesEndpoint, a))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
r.addParameter("tag", t)
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package mailgun
|
||||
|
||||
// Version of current release
|
||||
const Version = "3.6.1"
|
|
@ -0,0 +1,157 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/mailgun/mailgun-go/v3/events"
|
||||
)
|
||||
|
||||
type UrlOrUrls struct {
|
||||
Urls []string `json:"urls"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type WebHooksListResponse struct {
|
||||
Webhooks map[string]UrlOrUrls `json:"webhooks"`
|
||||
}
|
||||
|
||||
type WebHookResponse struct {
|
||||
Webhook UrlOrUrls `json:"webhook"`
|
||||
}
|
||||
|
||||
// ListWebhooks returns the complete set of webhooks configured for your domain.
|
||||
// Note that a zero-length mapping is not an error.
|
||||
func (mg *MailgunImpl) ListWebhooks(ctx context.Context) (map[string][]string, error) {
|
||||
r := newHTTPRequest(generateDomainApiUrl(mg, webhooksEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
|
||||
var body WebHooksListResponse
|
||||
err := getResponseFromJSON(ctx, r, &body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hooks := make(map[string][]string, 0)
|
||||
for k, v := range body.Webhooks {
|
||||
if v.Url != "" {
|
||||
hooks[k] = []string{v.Url}
|
||||
}
|
||||
if len(v.Urls) != 0 {
|
||||
hooks[k] = append(hooks[k], v.Urls...)
|
||||
}
|
||||
}
|
||||
return hooks, nil
|
||||
}
|
||||
|
||||
// CreateWebhook installs a new webhook for your domain.
|
||||
func (mg *MailgunImpl) CreateWebhook(ctx context.Context, kind string, urls []string) error {
|
||||
r := newHTTPRequest(generateDomainApiUrl(mg, webhooksEndpoint))
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := newUrlEncodedPayload()
|
||||
p.addValue("id", kind)
|
||||
for _, url := range urls {
|
||||
p.addValue("url", url)
|
||||
}
|
||||
_, err := makePostRequest(ctx, r, p)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteWebhook removes the specified webhook from your domain's configuration.
|
||||
func (mg *MailgunImpl) DeleteWebhook(ctx context.Context, kind string) error {
|
||||
r := newHTTPRequest(generateDomainApiUrl(mg, webhooksEndpoint) + "/" + kind)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
_, err := makeDeleteRequest(ctx, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetWebhook retrieves the currently assigned webhook URL associated with the provided type of webhook.
|
||||
func (mg *MailgunImpl) GetWebhook(ctx context.Context, kind string) ([]string, error) {
|
||||
r := newHTTPRequest(generateDomainApiUrl(mg, webhooksEndpoint) + "/" + kind)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
var body WebHookResponse
|
||||
if err := getResponseFromJSON(ctx, r, &body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if body.Webhook.Url != "" {
|
||||
return []string{body.Webhook.Url}, nil
|
||||
}
|
||||
if len(body.Webhook.Urls) != 0 {
|
||||
return body.Webhook.Urls, nil
|
||||
}
|
||||
return nil, fmt.Errorf("webhook '%s' returned no urls", kind)
|
||||
}
|
||||
|
||||
// UpdateWebhook replaces one webhook setting for another.
|
||||
func (mg *MailgunImpl) UpdateWebhook(ctx context.Context, kind string, urls []string) error {
|
||||
r := newHTTPRequest(generateDomainApiUrl(mg, webhooksEndpoint) + "/" + kind)
|
||||
r.setClient(mg.Client())
|
||||
r.setBasicAuth(basicAuthUser, mg.APIKey())
|
||||
p := newUrlEncodedPayload()
|
||||
for _, url := range urls {
|
||||
p.addValue("url", url)
|
||||
}
|
||||
_, err := makePutRequest(ctx, r, p)
|
||||
return err
|
||||
}
|
||||
|
||||
// Represents the signature portion of the webhook POST body
|
||||
type Signature struct {
|
||||
TimeStamp string `json:"timestamp"`
|
||||
Token string `json:"token"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// Represents the JSON payload provided when a Webhook is called by mailgun
|
||||
type WebhookPayload struct {
|
||||
Signature Signature `json:"signature"`
|
||||
EventData events.RawJSON `json:"event-data"`
|
||||
}
|
||||
|
||||
// Use this method to parse the webhook signature given as JSON in the webhook response
|
||||
func (mg *MailgunImpl) VerifyWebhookSignature(sig Signature) (verified bool, err error) {
|
||||
h := hmac.New(sha256.New, []byte(mg.APIKey()))
|
||||
io.WriteString(h, sig.TimeStamp)
|
||||
io.WriteString(h, sig.Token)
|
||||
|
||||
calculatedSignature := h.Sum(nil)
|
||||
signature, err := hex.DecodeString(sig.Signature)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(calculatedSignature) != len(signature) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return subtle.ConstantTimeCompare(signature, calculatedSignature) == 1, nil
|
||||
}
|
||||
|
||||
// Deprecated: Please use the VerifyWebhookSignature() to parse the latest
|
||||
// version of WebHooks from mailgun
|
||||
func (mg *MailgunImpl) VerifyWebhookRequest(req *http.Request) (verified bool, err error) {
|
||||
h := hmac.New(sha256.New, []byte(mg.APIKey()))
|
||||
io.WriteString(h, req.FormValue("timestamp"))
|
||||
io.WriteString(h, req.FormValue("token"))
|
||||
|
||||
calculatedSignature := h.Sum(nil)
|
||||
signature, err := hex.DecodeString(req.FormValue("signature"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(calculatedSignature) != len(signature) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return subtle.ConstantTimeCompare(signature, calculatedSignature) == 1, nil
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.root
|
||||
*_easyjson.go
|
||||
*.iml
|
||||
.idea
|
||||
*.swp
|
|
@ -0,0 +1,9 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- tip
|
||||
install:
|
||||
- go get github.com/ugorji/go/codec
|
||||
- go get github.com/pquerna/ffjson/fflib/v1
|
||||
- go get github.com/json-iterator/go
|
||||
- go get github.com/golang/lint/golint
|
|
@ -0,0 +1,7 @@
|
|||
Copyright (c) 2016 Mail.Ru Group
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,61 @@
|
|||
PKG=github.com/mailru/easyjson
|
||||
GOPATH:=$(PWD)/.root:$(GOPATH)
|
||||
export GOPATH
|
||||
|
||||
all: test
|
||||
|
||||
.root/src/$(PKG):
|
||||
mkdir -p $@
|
||||
for i in $$PWD/* ; do ln -s $$i $@/`basename $$i` ; done
|
||||
|
||||
root: .root/src/$(PKG)
|
||||
|
||||
clean:
|
||||
rm -rf .root
|
||||
rm -rf tests/*_easyjson.go
|
||||
|
||||
build:
|
||||
go build -i -o .root/bin/easyjson $(PKG)/easyjson
|
||||
|
||||
generate: root build
|
||||
.root/bin/easyjson -stubs \
|
||||
.root/src/$(PKG)/tests/snake.go \
|
||||
.root/src/$(PKG)/tests/data.go \
|
||||
.root/src/$(PKG)/tests/omitempty.go \
|
||||
.root/src/$(PKG)/tests/nothing.go \
|
||||
.root/src/$(PKG)/tests/named_type.go \
|
||||
.root/src/$(PKG)/tests/custom_map_key_type.go \
|
||||
.root/src/$(PKG)/tests/embedded_type.go
|
||||
|
||||
.root/bin/easyjson -all .root/src/$(PKG)/tests/data.go
|
||||
.root/bin/easyjson -all .root/src/$(PKG)/tests/nothing.go
|
||||
.root/bin/easyjson -all .root/src/$(PKG)/tests/errors.go
|
||||
.root/bin/easyjson -snake_case .root/src/$(PKG)/tests/snake.go
|
||||
.root/bin/easyjson -omit_empty .root/src/$(PKG)/tests/omitempty.go
|
||||
.root/bin/easyjson -build_tags=use_easyjson .root/src/$(PKG)/benchmark/data.go
|
||||
.root/bin/easyjson .root/src/$(PKG)/tests/nested_easy.go
|
||||
.root/bin/easyjson .root/src/$(PKG)/tests/named_type.go
|
||||
.root/bin/easyjson .root/src/$(PKG)/tests/custom_map_key_type.go
|
||||
.root/bin/easyjson .root/src/$(PKG)/tests/embedded_type.go
|
||||
.root/bin/easyjson -disallow_unknown_fields .root/src/$(PKG)/tests/disallow_unknown.go
|
||||
|
||||
test: generate root
|
||||
go test \
|
||||
$(PKG)/tests \
|
||||
$(PKG)/jlexer \
|
||||
$(PKG)/gen \
|
||||
$(PKG)/buffer
|
||||
go test -benchmem -tags use_easyjson -bench . $(PKG)/benchmark
|
||||
golint -set_exit_status .root/src/$(PKG)/tests/*_easyjson.go
|
||||
|
||||
bench-other: generate root
|
||||
@go test -benchmem -bench . $(PKG)/benchmark
|
||||
@go test -benchmem -tags use_ffjson -bench . $(PKG)/benchmark
|
||||
@go test -benchmem -tags use_jsoniter -bench . $(PKG)/benchmark
|
||||
@go test -benchmem -tags use_codec -bench . $(PKG)/benchmark
|
||||
|
||||
bench-python:
|
||||
benchmark/ujson.sh
|
||||
|
||||
|
||||
.PHONY: root clean generate test build
|
|
@ -0,0 +1,333 @@
|
|||
# easyjson [![Build Status](https://travis-ci.org/mailru/easyjson.svg?branch=master)](https://travis-ci.org/mailru/easyjson) [![Go Report Card](https://goreportcard.com/badge/github.com/mailru/easyjson)](https://goreportcard.com/report/github.com/mailru/easyjson)
|
||||
|
||||
Package easyjson provides a fast and easy way to marshal/unmarshal Go structs
|
||||
to/from JSON without the use of reflection. In performance tests, easyjson
|
||||
outperforms the standard `encoding/json` package by a factor of 4-5x, and other
|
||||
JSON encoding packages by a factor of 2-3x.
|
||||
|
||||
easyjson aims to keep generated Go code simple enough so that it can be easily
|
||||
optimized or fixed. Another goal is to provide users with the ability to
|
||||
customize the generated code by providing options not available with the
|
||||
standard `encoding/json` package, such as generating "snake_case" names or
|
||||
enabling `omitempty` behavior by default.
|
||||
|
||||
## Usage
|
||||
```sh
|
||||
# install
|
||||
go get -u github.com/mailru/easyjson/...
|
||||
|
||||
# run
|
||||
easyjson -all <file>.go
|
||||
```
|
||||
|
||||
The above will generate `<file>_easyjson.go` containing the appropriate marshaler and
|
||||
unmarshaler funcs for all structs contained in `<file>.go`.
|
||||
|
||||
Please note that easyjson requires a full Go build environment and the `GOPATH`
|
||||
environment variable to be set. This is because easyjson code generation
|
||||
invokes `go run` on a temporary file (an approach to code generation borrowed
|
||||
from [ffjson](https://github.com/pquerna/ffjson)).
|
||||
|
||||
## Options
|
||||
```txt
|
||||
Usage of easyjson:
|
||||
-all
|
||||
generate marshaler/unmarshalers for all structs in a file
|
||||
-build_tags string
|
||||
build tags to add to generated file
|
||||
-leave_temps
|
||||
do not delete temporary files
|
||||
-no_std_marshalers
|
||||
don't generate MarshalJSON/UnmarshalJSON funcs
|
||||
-noformat
|
||||
do not run 'gofmt -w' on output file
|
||||
-omit_empty
|
||||
omit empty fields by default
|
||||
-output_filename string
|
||||
specify the filename of the output
|
||||
-pkg
|
||||
process the whole package instead of just the given file
|
||||
-snake_case
|
||||
use snake_case names instead of CamelCase by default
|
||||
-lower_camel_case
|
||||
use lowerCamelCase instead of CamelCase by default
|
||||
-stubs
|
||||
only generate stubs for marshaler/unmarshaler funcs
|
||||
-disallow_unknown_fields
|
||||
return error if some unknown field in json appeared
|
||||
```
|
||||
|
||||
Using `-all` will generate marshalers/unmarshalers for all Go structs in the
|
||||
file. If `-all` is not provided, then only those structs whose preceding
|
||||
comment starts with `easyjson:json` will have marshalers/unmarshalers
|
||||
generated. For example:
|
||||
|
||||
```go
|
||||
//easyjson:json
|
||||
type A struct {}
|
||||
```
|
||||
|
||||
Additional option notes:
|
||||
|
||||
* `-snake_case` tells easyjson to generate snake\_case field names by default
|
||||
(unless overridden by a field tag). The CamelCase to snake\_case conversion
|
||||
algorithm should work in most cases (ie, HTTPVersion will be converted to
|
||||
"http_version").
|
||||
|
||||
* `-build_tags` will add the specified build tags to generated Go sources.
|
||||
|
||||
## Generated Marshaler/Unmarshaler Funcs
|
||||
|
||||
For Go struct types, easyjson generates the funcs `MarshalEasyJSON` /
|
||||
`UnmarshalEasyJSON` for marshaling/unmarshaling JSON. In turn, these satisify
|
||||
the `easyjson.Marshaler` and `easyjson.Unmarshaler` interfaces and when used in
|
||||
conjunction with `easyjson.Marshal` / `easyjson.Unmarshal` avoid unnecessary
|
||||
reflection / type assertions during marshaling/unmarshaling to/from JSON for Go
|
||||
structs.
|
||||
|
||||
easyjson also generates `MarshalJSON` and `UnmarshalJSON` funcs for Go struct
|
||||
types compatible with the standard `json.Marshaler` and `json.Unmarshaler`
|
||||
interfaces. Please be aware that using the standard `json.Marshal` /
|
||||
`json.Unmarshal` for marshaling/unmarshaling will incur a significant
|
||||
performance penalty when compared to using `easyjson.Marshal` /
|
||||
`easyjson.Unmarshal`.
|
||||
|
||||
Additionally, easyjson exposes utility funcs that use the `MarshalEasyJSON` and
|
||||
`UnmarshalEasyJSON` for marshaling/unmarshaling to and from standard readers
|
||||
and writers. For example, easyjson provides `easyjson.MarshalToHTTPResponseWriter`
|
||||
which marshals to the standard `http.ResponseWriter`. Please see the [GoDoc
|
||||
listing](https://godoc.org/github.com/mailru/easyjson) for the full listing of
|
||||
utility funcs that are available.
|
||||
|
||||
## Controlling easyjson Marshaling and Unmarshaling Behavior
|
||||
|
||||
Go types can provide their own `MarshalEasyJSON` and `UnmarshalEasyJSON` funcs
|
||||
that satisify the `easyjson.Marshaler` / `easyjson.Unmarshaler` interfaces.
|
||||
These will be used by `easyjson.Marshal` and `easyjson.Unmarshal` when defined
|
||||
for a Go type.
|
||||
|
||||
Go types can also satisify the `easyjson.Optional` interface, which allows the
|
||||
type to define its own `omitempty` logic.
|
||||
|
||||
## Type Wrappers
|
||||
|
||||
easyjson provides additional type wrappers defined in the `easyjson/opt`
|
||||
package. These wrap the standard Go primitives and in turn satisify the
|
||||
easyjson interfaces.
|
||||
|
||||
The `easyjson/opt` type wrappers are useful when needing to distinguish between
|
||||
a missing value and/or when needing to specifying a default value. Type
|
||||
wrappers allow easyjson to avoid additional pointers and heap allocations and
|
||||
can significantly increase performance when used properly.
|
||||
|
||||
## Memory Pooling
|
||||
|
||||
easyjson uses a buffer pool that allocates data in increasing chunks from 128
|
||||
to 32768 bytes. Chunks of 512 bytes and larger will be reused with the help of
|
||||
`sync.Pool`. The maximum size of a chunk is bounded to reduce redundant memory
|
||||
allocation and to allow larger reusable buffers.
|
||||
|
||||
easyjson's custom allocation buffer pool is defined in the `easyjson/buffer`
|
||||
package, and the default behavior pool behavior can be modified (if necessary)
|
||||
through a call to `buffer.Init()` prior to any marshaling or unmarshaling.
|
||||
Please see the [GoDoc listing](https://godoc.org/github.com/mailru/easyjson/buffer)
|
||||
for more information.
|
||||
|
||||
## Issues, Notes, and Limitations
|
||||
|
||||
* easyjson is still early in its development. As such, there are likely to be
|
||||
bugs and missing features when compared to `encoding/json`. In the case of a
|
||||
missing feature or bug, please create a GitHub issue. Pull requests are
|
||||
welcome!
|
||||
|
||||
* Unlike `encoding/json`, object keys are case-sensitive. Case-insensitive
|
||||
matching is not currently provided due to the significant performance hit
|
||||
when doing case-insensitive key matching. In the future, case-insensitive
|
||||
object key matching may be provided via an option to the generator.
|
||||
|
||||
* easyjson makes use of `unsafe`, which simplifies the code and
|
||||
provides significant performance benefits by allowing no-copy
|
||||
conversion from `[]byte` to `string`. That said, `unsafe` is used
|
||||
only when unmarshaling and parsing JSON, and any `unsafe` operations
|
||||
/ memory allocations done will be safely deallocated by
|
||||
easyjson. Set the build tag `easyjson_nounsafe` to compile it
|
||||
without `unsafe`.
|
||||
|
||||
* easyjson is compatible with Google App Engine. The `appengine` build
|
||||
tag (set by App Engine's environment) will automatically disable the
|
||||
use of `unsafe`, which is not allowed in App Engine's Standard
|
||||
Environment. Note that the use with App Engine is still experimental.
|
||||
|
||||
* Floats are formatted using the default precision from Go's `strconv` package.
|
||||
As such, easyjson will not correctly handle high precision floats when
|
||||
marshaling/unmarshaling JSON. Note, however, that there are very few/limited
|
||||
uses where this behavior is not sufficient for general use. That said, a
|
||||
different package may be needed if precise marshaling/unmarshaling of high
|
||||
precision floats to/from JSON is required.
|
||||
|
||||
* While unmarshaling, the JSON parser does the minimal amount of work needed to
|
||||
skip over unmatching parens, and as such full validation is not done for the
|
||||
entire JSON value being unmarshaled/parsed.
|
||||
|
||||
* Currently there is no true streaming support for encoding/decoding as
|
||||
typically for many uses/protocols the final, marshaled length of the JSON
|
||||
needs to be known prior to sending the data. Currently this is not possible
|
||||
with easyjson's architecture.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Most benchmarks were done using the example
|
||||
[13kB example JSON](https://dev.twitter.com/rest/reference/get/search/tweets)
|
||||
(9k after eliminating whitespace). This example is similar to real-world data,
|
||||
is well-structured, and contains a healthy variety of different types, making
|
||||
it ideal for JSON serialization benchmarks.
|
||||
|
||||
Note:
|
||||
|
||||
* For small request benchmarks, an 80 byte portion of the above example was
|
||||
used.
|
||||
|
||||
* For large request marshaling benchmarks, a struct containing 50 regular
|
||||
samples was used, making a ~500kB output JSON.
|
||||
|
||||
* Benchmarks are showing the results of easyjson's default behaviour,
|
||||
which makes use of `unsafe`.
|
||||
|
||||
Benchmarks are available in the repository and can be run by invoking `make`.
|
||||
|
||||
### easyjson vs. encoding/json
|
||||
|
||||
easyjson is roughly 5-6 times faster than the standard `encoding/json` for
|
||||
unmarshaling, and 3-4 times faster for non-concurrent marshaling. Concurrent
|
||||
marshaling is 6-7x faster if marshaling to a writer.
|
||||
|
||||
### easyjson vs. ffjson
|
||||
|
||||
easyjson uses the same approach for JSON marshaling as
|
||||
[ffjson](https://github.com/pquerna/ffjson), but takes a significantly
|
||||
different approach to lexing and parsing JSON during unmarshaling. This means
|
||||
easyjson is roughly 2-3x faster for unmarshaling and 1.5-2x faster for
|
||||
non-concurrent unmarshaling.
|
||||
|
||||
As of this writing, `ffjson` seems to have issues when used concurrently:
|
||||
specifically, large request pooling hurts `ffjson`'s performance and causes
|
||||
scalability issues. These issues with `ffjson` can likely be fixed, but as of
|
||||
writing remain outstanding/known issues with `ffjson`.
|
||||
|
||||
easyjson and `ffjson` have similar performance for small requests, however
|
||||
easyjson outperforms `ffjson` by roughly 2-5x times for large requests when
|
||||
used with a writer.
|
||||
|
||||
### easyjson vs. go/codec
|
||||
|
||||
[go/codec](https://github.com/ugorji/go) provides
|
||||
compile-time helpers for JSON generation. In this case, helpers do not work
|
||||
like marshalers as they are encoding-independent.
|
||||
|
||||
easyjson is generally 2x faster than `go/codec` for non-concurrent benchmarks
|
||||
and about 3x faster for concurrent encoding (without marshaling to a writer).
|
||||
|
||||
In an attempt to measure marshaling performance of `go/codec` (as opposed to
|
||||
allocations/memcpy/writer interface invocations), a benchmark was done with
|
||||
resetting length of a byte slice rather than resetting the whole slice to nil.
|
||||
However, the optimization in this exact form may not be applicable in practice,
|
||||
since the memory is not freed between marshaling operations.
|
||||
|
||||
### easyjson vs 'ujson' python module
|
||||
|
||||
[ujson](https://github.com/esnme/ultrajson) is using C code for parsing, so it
|
||||
is interesting to see how plain golang compares to that. It is imporant to note
|
||||
that the resulting object for python is slower to access, since the library
|
||||
parses JSON object into dictionaries.
|
||||
|
||||
easyjson is slightly faster for unmarshaling and 2-3x faster than `ujson` for
|
||||
marshaling.
|
||||
|
||||
### Benchmark Results
|
||||
|
||||
`ffjson` results are from February 4th, 2016, using the latest `ffjson` and go1.6.
|
||||
`go/codec` results are from March 4th, 2016, using the latest `go/codec` and go1.6.
|
||||
|
||||
#### Unmarshaling
|
||||
|
||||
| lib | json size | MB/s | allocs/op | B/op |
|
||||
|:---------|:----------|-----:|----------:|------:|
|
||||
| standard | regular | 22 | 218 | 10229 |
|
||||
| standard | small | 9.7 | 14 | 720 |
|
||||
| | | | | |
|
||||
| easyjson | regular | 125 | 128 | 9794 |
|
||||
| easyjson | small | 67 | 3 | 128 |
|
||||
| | | | | |
|
||||
| ffjson | regular | 66 | 141 | 9985 |
|
||||
| ffjson | small | 17.6 | 10 | 488 |
|
||||
| | | | | |
|
||||
| codec | regular | 55 | 434 | 19299 |
|
||||
| codec | small | 29 | 7 | 336 |
|
||||
| | | | | |
|
||||
| ujson | regular | 103 | N/A | N/A |
|
||||
|
||||
#### Marshaling, one goroutine.
|
||||
|
||||
| lib | json size | MB/s | allocs/op | B/op |
|
||||
|:----------|:----------|-----:|----------:|------:|
|
||||
| standard | regular | 75 | 9 | 23256 |
|
||||
| standard | small | 32 | 3 | 328 |
|
||||
| standard | large | 80 | 17 | 1.2M |
|
||||
| | | | | |
|
||||
| easyjson | regular | 213 | 9 | 10260 |
|
||||
| easyjson* | regular | 263 | 8 | 742 |
|
||||
| easyjson | small | 125 | 1 | 128 |
|
||||
| easyjson | large | 212 | 33 | 490k |
|
||||
| easyjson* | large | 262 | 25 | 2879 |
|
||||
| | | | | |
|
||||
| ffjson | regular | 122 | 153 | 21340 |
|
||||
| ffjson** | regular | 146 | 152 | 4897 |
|
||||
| ffjson | small | 36 | 5 | 384 |
|
||||
| ffjson** | small | 64 | 4 | 128 |
|
||||
| ffjson | large | 134 | 7317 | 818k |
|
||||
| ffjson** | large | 125 | 7320 | 827k |
|
||||
| | | | | |
|
||||
| codec | regular | 80 | 17 | 33601 |
|
||||
| codec*** | regular | 108 | 9 | 1153 |
|
||||
| codec | small | 42 | 3 | 304 |
|
||||
| codec*** | small | 56 | 1 | 48 |
|
||||
| codec | large | 73 | 483 | 2.5M |
|
||||
| codec*** | large | 103 | 451 | 66007 |
|
||||
| | | | | |
|
||||
| ujson | regular | 92 | N/A | N/A |
|
||||
|
||||
\* marshaling to a writer,
|
||||
\*\* using `ffjson.Pool()`,
|
||||
\*\*\* reusing output slice instead of resetting it to nil
|
||||
|
||||
#### Marshaling, concurrent.
|
||||
|
||||
| lib | json size | MB/s | allocs/op | B/op |
|
||||
|:----------|:----------|-----:|----------:|------:|
|
||||
| standard | regular | 252 | 9 | 23257 |
|
||||
| standard | small | 124 | 3 | 328 |
|
||||
| standard | large | 289 | 17 | 1.2M |
|
||||
| | | | | |
|
||||
| easyjson | regular | 792 | 9 | 10597 |
|
||||
| easyjson* | regular | 1748 | 8 | 779 |
|
||||
| easyjson | small | 333 | 1 | 128 |
|
||||
| easyjson | large | 718 | 36 | 548k |
|
||||
| easyjson* | large | 2134 | 25 | 4957 |
|
||||
| | | | | |
|
||||
| ffjson | regular | 301 | 153 | 21629 |
|
||||
| ffjson** | regular | 707 | 152 | 5148 |
|
||||
| ffjson | small | 62 | 5 | 384 |
|
||||
| ffjson** | small | 282 | 4 | 128 |
|
||||
| ffjson | large | 438 | 7330 | 1.0M |
|
||||
| ffjson** | large | 131 | 7319 | 820k |
|
||||
| | | | | |
|
||||
| codec | regular | 183 | 17 | 33603 |
|
||||
| codec*** | regular | 671 | 9 | 1157 |
|
||||
| codec | small | 147 | 3 | 304 |
|
||||
| codec*** | small | 299 | 1 | 48 |
|
||||
| codec | large | 190 | 483 | 2.5M |
|
||||
| codec*** | large | 752 | 451 | 77574 |
|
||||
|
||||
\* marshaling to a writer,
|
||||
\*\* using `ffjson.Pool()`,
|
||||
\*\*\* reusing output slice instead of resetting it to nil
|
|
@ -0,0 +1,270 @@
|
|||
// Package buffer implements a buffer for serialization, consisting of a chain of []byte-s to
|
||||
// reduce copying and to allow reuse of individual chunks.
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// PoolConfig contains configuration for the allocation and reuse strategy.
|
||||
type PoolConfig struct {
|
||||
StartSize int // Minimum chunk size that is allocated.
|
||||
PooledSize int // Minimum chunk size that is reused, reusing chunks too small will result in overhead.
|
||||
MaxSize int // Maximum chunk size that will be allocated.
|
||||
}
|
||||
|
||||
var config = PoolConfig{
|
||||
StartSize: 128,
|
||||
PooledSize: 512,
|
||||
MaxSize: 32768,
|
||||
}
|
||||
|
||||
// Reuse pool: chunk size -> pool.
|
||||
var buffers = map[int]*sync.Pool{}
|
||||
|
||||
func initBuffers() {
|
||||
for l := config.PooledSize; l <= config.MaxSize; l *= 2 {
|
||||
buffers[l] = new(sync.Pool)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
initBuffers()
|
||||
}
|
||||
|
||||
// Init sets up a non-default pooling and allocation strategy. Should be run before serialization is done.
|
||||
func Init(cfg PoolConfig) {
|
||||
config = cfg
|
||||
initBuffers()
|
||||
}
|
||||
|
||||
// putBuf puts a chunk to reuse pool if it can be reused.
|
||||
func putBuf(buf []byte) {
|
||||
size := cap(buf)
|
||||
if size < config.PooledSize {
|
||||
return
|
||||
}
|
||||
if c := buffers[size]; c != nil {
|
||||
c.Put(buf[:0])
|
||||
}
|
||||
}
|
||||
|
||||
// getBuf gets a chunk from reuse pool or creates a new one if reuse failed.
|
||||
func getBuf(size int) []byte {
|
||||
if size < config.PooledSize {
|
||||
return make([]byte, 0, size)
|
||||
}
|
||||
|
||||
if c := buffers[size]; c != nil {
|
||||
v := c.Get()
|
||||
if v != nil {
|
||||
return v.([]byte)
|
||||
}
|
||||
}
|
||||
return make([]byte, 0, size)
|
||||
}
|
||||
|
||||
// Buffer is a buffer optimized for serialization without extra copying.
|
||||
type Buffer struct {
|
||||
|
||||
// Buf is the current chunk that can be used for serialization.
|
||||
Buf []byte
|
||||
|
||||
toPool []byte
|
||||
bufs [][]byte
|
||||
}
|
||||
|
||||
// EnsureSpace makes sure that the current chunk contains at least s free bytes,
|
||||
// possibly creating a new chunk.
|
||||
func (b *Buffer) EnsureSpace(s int) {
|
||||
if cap(b.Buf)-len(b.Buf) >= s {
|
||||
return
|
||||
}
|
||||
l := len(b.Buf)
|
||||
if l > 0 {
|
||||
if cap(b.toPool) != cap(b.Buf) {
|
||||
// Chunk was reallocated, toPool can be pooled.
|
||||
putBuf(b.toPool)
|
||||
}
|
||||
if cap(b.bufs) == 0 {
|
||||
b.bufs = make([][]byte, 0, 8)
|
||||
}
|
||||
b.bufs = append(b.bufs, b.Buf)
|
||||
l = cap(b.toPool) * 2
|
||||
} else {
|
||||
l = config.StartSize
|
||||
}
|
||||
|
||||
if l > config.MaxSize {
|
||||
l = config.MaxSize
|
||||
}
|
||||
b.Buf = getBuf(l)
|
||||
b.toPool = b.Buf
|
||||
}
|
||||
|
||||
// AppendByte appends a single byte to buffer.
|
||||
func (b *Buffer) AppendByte(data byte) {
|
||||
if cap(b.Buf) == len(b.Buf) { // EnsureSpace won't be inlined.
|
||||
b.EnsureSpace(1)
|
||||
}
|
||||
b.Buf = append(b.Buf, data)
|
||||
}
|
||||
|
||||
// AppendBytes appends a byte slice to buffer.
|
||||
func (b *Buffer) AppendBytes(data []byte) {
|
||||
for len(data) > 0 {
|
||||
if cap(b.Buf) == len(b.Buf) { // EnsureSpace won't be inlined.
|
||||
b.EnsureSpace(1)
|
||||
}
|
||||
|
||||
sz := cap(b.Buf) - len(b.Buf)
|
||||
if sz > len(data) {
|
||||
sz = len(data)
|
||||
}
|
||||
|
||||
b.Buf = append(b.Buf, data[:sz]...)
|
||||
data = data[sz:]
|
||||
}
|
||||
}
|
||||
|
||||
// AppendBytes appends a string to buffer.
|
||||
func (b *Buffer) AppendString(data string) {
|
||||
for len(data) > 0 {
|
||||
if cap(b.Buf) == len(b.Buf) { // EnsureSpace won't be inlined.
|
||||
b.EnsureSpace(1)
|
||||
}
|
||||
|
||||
sz := cap(b.Buf) - len(b.Buf)
|
||||
if sz > len(data) {
|
||||
sz = len(data)
|
||||
}
|
||||
|
||||
b.Buf = append(b.Buf, data[:sz]...)
|
||||
data = data[sz:]
|
||||
}
|
||||
}
|
||||
|
||||
// Size computes the size of a buffer by adding sizes of every chunk.
|
||||
func (b *Buffer) Size() int {
|
||||
size := len(b.Buf)
|
||||
for _, buf := range b.bufs {
|
||||
size += len(buf)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// DumpTo outputs the contents of a buffer to a writer and resets the buffer.
|
||||
func (b *Buffer) DumpTo(w io.Writer) (written int, err error) {
|
||||
var n int
|
||||
for _, buf := range b.bufs {
|
||||
if err == nil {
|
||||
n, err = w.Write(buf)
|
||||
written += n
|
||||
}
|
||||
putBuf(buf)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
n, err = w.Write(b.Buf)
|
||||
written += n
|
||||
}
|
||||
putBuf(b.toPool)
|
||||
|
||||
b.bufs = nil
|
||||
b.Buf = nil
|
||||
b.toPool = nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// BuildBytes creates a single byte slice with all the contents of the buffer. Data is
|
||||
// copied if it does not fit in a single chunk. You can optionally provide one byte
|
||||
// slice as argument that it will try to reuse.
|
||||
func (b *Buffer) BuildBytes(reuse ...[]byte) []byte {
|
||||
if len(b.bufs) == 0 {
|
||||
ret := b.Buf
|
||||
b.toPool = nil
|
||||
b.Buf = nil
|
||||
return ret
|
||||
}
|
||||
|
||||
var ret []byte
|
||||
size := b.Size()
|
||||
|
||||
// If we got a buffer as argument and it is big enought, reuse it.
|
||||
if len(reuse) == 1 && cap(reuse[0]) >= size {
|
||||
ret = reuse[0][:0]
|
||||
} else {
|
||||
ret = make([]byte, 0, size)
|
||||
}
|
||||
for _, buf := range b.bufs {
|
||||
ret = append(ret, buf...)
|
||||
putBuf(buf)
|
||||
}
|
||||
|
||||
ret = append(ret, b.Buf...)
|
||||
putBuf(b.toPool)
|
||||
|
||||
b.bufs = nil
|
||||
b.toPool = nil
|
||||
b.Buf = nil
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type readCloser struct {
|
||||
offset int
|
||||
bufs [][]byte
|
||||
}
|
||||
|
||||
func (r *readCloser) Read(p []byte) (n int, err error) {
|
||||
for _, buf := range r.bufs {
|
||||
// Copy as much as we can.
|
||||
x := copy(p[n:], buf[r.offset:])
|
||||
n += x // Increment how much we filled.
|
||||
|
||||
// Did we empty the whole buffer?
|
||||
if r.offset+x == len(buf) {
|
||||
// On to the next buffer.
|
||||
r.offset = 0
|
||||
r.bufs = r.bufs[1:]
|
||||
|
||||
// We can release this buffer.
|
||||
putBuf(buf)
|
||||
} else {
|
||||
r.offset += x
|
||||
}
|
||||
|
||||
if n == len(p) {
|
||||
break
|
||||
}
|
||||
}
|
||||
// No buffers left or nothing read?
|
||||
if len(r.bufs) == 0 {
|
||||
err = io.EOF
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *readCloser) Close() error {
|
||||
// Release all remaining buffers.
|
||||
for _, buf := range r.bufs {
|
||||
putBuf(buf)
|
||||
}
|
||||
// In case Close gets called multiple times.
|
||||
r.bufs = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadCloser creates an io.ReadCloser with all the contents of the buffer.
|
||||
func (b *Buffer) ReadCloser() io.ReadCloser {
|
||||
ret := &readCloser{0, append(b.bufs, b.Buf)}
|
||||
|
||||
b.bufs = nil
|
||||
b.toPool = nil
|
||||
b.Buf = nil
|
||||
|
||||
return ret
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// Package easyjson contains marshaler/unmarshaler interfaces and helper functions.
|
||||
package easyjson
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/mailru/easyjson/jlexer"
|
||||
"github.com/mailru/easyjson/jwriter"
|
||||
)
|
||||
|
||||
// Marshaler is an easyjson-compatible marshaler interface.
|
||||
type Marshaler interface {
|
||||
MarshalEasyJSON(w *jwriter.Writer)
|
||||
}
|
||||
|
||||
// Marshaler is an easyjson-compatible unmarshaler interface.
|
||||
type Unmarshaler interface {
|
||||
UnmarshalEasyJSON(w *jlexer.Lexer)
|
||||
}
|
||||
|
||||
// Optional defines an undefined-test method for a type to integrate with 'omitempty' logic.
|
||||
type Optional interface {
|
||||
IsDefined() bool
|
||||
}
|
||||
|
||||
// Marshal returns data as a single byte slice. Method is suboptimal as the data is likely to be copied
|
||||
// from a chain of smaller chunks.
|
||||
func Marshal(v Marshaler) ([]byte, error) {
|
||||
w := jwriter.Writer{}
|
||||
v.MarshalEasyJSON(&w)
|
||||
return w.BuildBytes()
|
||||
}
|
||||
|
||||
// MarshalToWriter marshals the data to an io.Writer.
|
||||
func MarshalToWriter(v Marshaler, w io.Writer) (written int, err error) {
|
||||
jw := jwriter.Writer{}
|
||||
v.MarshalEasyJSON(&jw)
|
||||
return jw.DumpTo(w)
|
||||
}
|
||||
|
||||
// MarshalToHTTPResponseWriter sets Content-Length and Content-Type headers for the
|
||||
// http.ResponseWriter, and send the data to the writer. started will be equal to
|
||||
// false if an error occurred before any http.ResponseWriter methods were actually
|
||||
// invoked (in this case a 500 reply is possible).
|
||||
func MarshalToHTTPResponseWriter(v Marshaler, w http.ResponseWriter) (started bool, written int, err error) {
|
||||
jw := jwriter.Writer{}
|
||||
v.MarshalEasyJSON(&jw)
|
||||
if jw.Error != nil {
|
||||
return false, 0, jw.Error
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(jw.Size()))
|
||||
|
||||
started = true
|
||||
written, err = jw.DumpTo(w)
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshal decodes the JSON in data into the object.
|
||||
func Unmarshal(data []byte, v Unmarshaler) error {
|
||||
l := jlexer.Lexer{Data: data}
|
||||
v.UnmarshalEasyJSON(&l)
|
||||
return l.Error()
|
||||
}
|
||||
|
||||
// UnmarshalFromReader reads all the data in the reader and decodes as JSON into the object.
|
||||
func UnmarshalFromReader(r io.Reader, v Unmarshaler) error {
|
||||
data, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l := jlexer.Lexer{Data: data}
|
||||
v.UnmarshalEasyJSON(&l)
|
||||
return l.Error()
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// This file will only be included to the build if neither
|
||||
// easyjson_nounsafe nor appengine build tag is set. See README notes
|
||||
// for more details.
|
||||
|
||||
//+build !easyjson_nounsafe
|
||||
//+build !appengine
|
||||
|
||||
package jlexer
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// bytesToStr creates a string pointing at the slice to avoid copying.
|
||||
//
|
||||
// Warning: the string returned by the function should be used with care, as the whole input data
|
||||
// chunk may be either blocked from being freed by GC because of a single string or the buffer.Data
|
||||
// may be garbage-collected even when the string exists.
|
||||
func bytesToStr(data []byte) string {
|
||||
h := (*reflect.SliceHeader)(unsafe.Pointer(&data))
|
||||
shdr := reflect.StringHeader{Data: h.Data, Len: h.Len}
|
||||
return *(*string)(unsafe.Pointer(&shdr))
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// This file is included to the build if any of the buildtags below
|
||||
// are defined. Refer to README notes for more details.
|
||||
|
||||
//+build easyjson_nounsafe appengine
|
||||
|
||||
package jlexer
|
||||
|
||||
// bytesToStr creates a string normally from []byte
|
||||
//
|
||||
// Note that this method is roughly 1.5x slower than using the 'unsafe' method.
|
||||
func bytesToStr(data []byte) string {
|
||||
return string(data)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package jlexer
|
||||
|
||||
import "fmt"
|
||||
|
||||
// LexerError implements the error interface and represents all possible errors that can be
|
||||
// generated during parsing the JSON data.
|
||||
type LexerError struct {
|
||||
Reason string
|
||||
Offset int
|
||||
Data string
|
||||
}
|
||||
|
||||
func (l *LexerError) Error() string {
|
||||
return fmt.Sprintf("parse error: %s near offset %d of '%s'", l.Reason, l.Offset, l.Data)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,390 @@
|
|||
// Package jwriter contains a JSON writer.
|
||||
package jwriter
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mailru/easyjson/buffer"
|
||||
)
|
||||
|
||||
// Flags describe various encoding options. The behavior may be actually implemented in the encoder, but
|
||||
// Flags field in Writer is used to set and pass them around.
|
||||
type Flags int
|
||||
|
||||
const (
|
||||
NilMapAsEmpty Flags = 1 << iota // Encode nil map as '{}' rather than 'null'.
|
||||
NilSliceAsEmpty // Encode nil slice as '[]' rather than 'null'.
|
||||
)
|
||||
|
||||
// Writer is a JSON writer.
|
||||
type Writer struct {
|
||||
Flags Flags
|
||||
|
||||
Error error
|
||||
Buffer buffer.Buffer
|
||||
NoEscapeHTML bool
|
||||
}
|
||||
|
||||
// Size returns the size of the data that was written out.
|
||||
func (w *Writer) Size() int {
|
||||
return w.Buffer.Size()
|
||||
}
|
||||
|
||||
// DumpTo outputs the data to given io.Writer, resetting the buffer.
|
||||
func (w *Writer) DumpTo(out io.Writer) (written int, err error) {
|
||||
return w.Buffer.DumpTo(out)
|
||||
}
|
||||
|
||||
// BuildBytes returns writer data as a single byte slice. You can optionally provide one byte slice
|
||||
// as argument that it will try to reuse.
|
||||
func (w *Writer) BuildBytes(reuse ...[]byte) ([]byte, error) {
|
||||
if w.Error != nil {
|
||||
return nil, w.Error
|
||||
}
|
||||
|
||||
return w.Buffer.BuildBytes(reuse...), nil
|
||||
}
|
||||
|
||||
// ReadCloser returns an io.ReadCloser that can be used to read the data.
|
||||
// ReadCloser also resets the buffer.
|
||||
func (w *Writer) ReadCloser() (io.ReadCloser, error) {
|
||||
if w.Error != nil {
|
||||
return nil, w.Error
|
||||
}
|
||||
|
||||
return w.Buffer.ReadCloser(), nil
|
||||
}
|
||||
|
||||
// RawByte appends raw binary data to the buffer.
|
||||
func (w *Writer) RawByte(c byte) {
|
||||
w.Buffer.AppendByte(c)
|
||||
}
|
||||
|
||||
// RawByte appends raw binary data to the buffer.
|
||||
func (w *Writer) RawString(s string) {
|
||||
w.Buffer.AppendString(s)
|
||||
}
|
||||
|
||||
// Raw appends raw binary data to the buffer or sets the error if it is given. Useful for
|
||||
// calling with results of MarshalJSON-like functions.
|
||||
func (w *Writer) Raw(data []byte, err error) {
|
||||
switch {
|
||||
case w.Error != nil:
|
||||
return
|
||||
case err != nil:
|
||||
w.Error = err
|
||||
case len(data) > 0:
|
||||
w.Buffer.AppendBytes(data)
|
||||
default:
|
||||
w.RawString("null")
|
||||
}
|
||||
}
|
||||
|
||||
// RawText encloses raw binary data in quotes and appends in to the buffer.
|
||||
// Useful for calling with results of MarshalText-like functions.
|
||||
func (w *Writer) RawText(data []byte, err error) {
|
||||
switch {
|
||||
case w.Error != nil:
|
||||
return
|
||||
case err != nil:
|
||||
w.Error = err
|
||||
case len(data) > 0:
|
||||
w.String(string(data))
|
||||
default:
|
||||
w.RawString("null")
|
||||
}
|
||||
}
|
||||
|
||||
// Base64Bytes appends data to the buffer after base64 encoding it
|
||||
func (w *Writer) Base64Bytes(data []byte) {
|
||||
if data == nil {
|
||||
w.Buffer.AppendString("null")
|
||||
return
|
||||
}
|
||||
w.Buffer.AppendByte('"')
|
||||
w.base64(data)
|
||||
w.Buffer.AppendByte('"')
|
||||
}
|
||||
|
||||
func (w *Writer) Uint8(n uint8) {
|
||||
w.Buffer.EnsureSpace(3)
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
|
||||
}
|
||||
|
||||
func (w *Writer) Uint16(n uint16) {
|
||||
w.Buffer.EnsureSpace(5)
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
|
||||
}
|
||||
|
||||
func (w *Writer) Uint32(n uint32) {
|
||||
w.Buffer.EnsureSpace(10)
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
|
||||
}
|
||||
|
||||
func (w *Writer) Uint(n uint) {
|
||||
w.Buffer.EnsureSpace(20)
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
|
||||
}
|
||||
|
||||
func (w *Writer) Uint64(n uint64) {
|
||||
w.Buffer.EnsureSpace(20)
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, n, 10)
|
||||
}
|
||||
|
||||
func (w *Writer) Int8(n int8) {
|
||||
w.Buffer.EnsureSpace(4)
|
||||
w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
|
||||
}
|
||||
|
||||
func (w *Writer) Int16(n int16) {
|
||||
w.Buffer.EnsureSpace(6)
|
||||
w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
|
||||
}
|
||||
|
||||
func (w *Writer) Int32(n int32) {
|
||||
w.Buffer.EnsureSpace(11)
|
||||
w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
|
||||
}
|
||||
|
||||
func (w *Writer) Int(n int) {
|
||||
w.Buffer.EnsureSpace(21)
|
||||
w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
|
||||
}
|
||||
|
||||
func (w *Writer) Int64(n int64) {
|
||||
w.Buffer.EnsureSpace(21)
|
||||
w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, n, 10)
|
||||
}
|
||||
|
||||
func (w *Writer) Uint8Str(n uint8) {
|
||||
w.Buffer.EnsureSpace(3)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) Uint16Str(n uint16) {
|
||||
w.Buffer.EnsureSpace(5)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) Uint32Str(n uint32) {
|
||||
w.Buffer.EnsureSpace(10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) UintStr(n uint) {
|
||||
w.Buffer.EnsureSpace(20)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) Uint64Str(n uint64) {
|
||||
w.Buffer.EnsureSpace(20)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, n, 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) UintptrStr(n uintptr) {
|
||||
w.Buffer.EnsureSpace(20)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) Int8Str(n int8) {
|
||||
w.Buffer.EnsureSpace(4)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) Int16Str(n int16) {
|
||||
w.Buffer.EnsureSpace(6)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) Int32Str(n int32) {
|
||||
w.Buffer.EnsureSpace(11)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) IntStr(n int) {
|
||||
w.Buffer.EnsureSpace(21)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) Int64Str(n int64) {
|
||||
w.Buffer.EnsureSpace(21)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, n, 10)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) Float32(n float32) {
|
||||
w.Buffer.EnsureSpace(20)
|
||||
w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 32)
|
||||
}
|
||||
|
||||
func (w *Writer) Float32Str(n float32) {
|
||||
w.Buffer.EnsureSpace(20)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 32)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) Float64(n float64) {
|
||||
w.Buffer.EnsureSpace(20)
|
||||
w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, n, 'g', -1, 64)
|
||||
}
|
||||
|
||||
func (w *Writer) Float64Str(n float64) {
|
||||
w.Buffer.EnsureSpace(20)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 64)
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, '"')
|
||||
}
|
||||
|
||||
func (w *Writer) Bool(v bool) {
|
||||
w.Buffer.EnsureSpace(5)
|
||||
if v {
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, "true"...)
|
||||
} else {
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, "false"...)
|
||||
}
|
||||
}
|
||||
|
||||
const chars = "0123456789abcdef"
|
||||
|
||||
func isNotEscapedSingleChar(c byte, escapeHTML bool) bool {
|
||||
// Note: might make sense to use a table if there are more chars to escape. With 4 chars
|
||||
// it benchmarks the same.
|
||||
if escapeHTML {
|
||||
return c != '<' && c != '>' && c != '&' && c != '\\' && c != '"' && c >= 0x20 && c < utf8.RuneSelf
|
||||
} else {
|
||||
return c != '\\' && c != '"' && c >= 0x20 && c < utf8.RuneSelf
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Writer) String(s string) {
|
||||
w.Buffer.AppendByte('"')
|
||||
|
||||
// Portions of the string that contain no escapes are appended as
|
||||
// byte slices.
|
||||
|
||||
p := 0 // last non-escape symbol
|
||||
|
||||
for i := 0; i < len(s); {
|
||||
c := s[i]
|
||||
|
||||
if isNotEscapedSingleChar(c, !w.NoEscapeHTML) {
|
||||
// single-width character, no escaping is required
|
||||
i++
|
||||
continue
|
||||
} else if c < utf8.RuneSelf {
|
||||
// single-with character, need to escape
|
||||
w.Buffer.AppendString(s[p:i])
|
||||
switch c {
|
||||
case '\t':
|
||||
w.Buffer.AppendString(`\t`)
|
||||
case '\r':
|
||||
w.Buffer.AppendString(`\r`)
|
||||
case '\n':
|
||||
w.Buffer.AppendString(`\n`)
|
||||
case '\\':
|
||||
w.Buffer.AppendString(`\\`)
|
||||
case '"':
|
||||
w.Buffer.AppendString(`\"`)
|
||||
default:
|
||||
w.Buffer.AppendString(`\u00`)
|
||||
w.Buffer.AppendByte(chars[c>>4])
|
||||
w.Buffer.AppendByte(chars[c&0xf])
|
||||
}
|
||||
|
||||
i++
|
||||
p = i
|
||||
continue
|
||||
}
|
||||
|
||||
// broken utf
|
||||
runeValue, runeWidth := utf8.DecodeRuneInString(s[i:])
|
||||
if runeValue == utf8.RuneError && runeWidth == 1 {
|
||||
w.Buffer.AppendString(s[p:i])
|
||||
w.Buffer.AppendString(`\ufffd`)
|
||||
i++
|
||||
p = i
|
||||
continue
|
||||
}
|
||||
|
||||
// jsonp stuff - tab separator and line separator
|
||||
if runeValue == '\u2028' || runeValue == '\u2029' {
|
||||
w.Buffer.AppendString(s[p:i])
|
||||
w.Buffer.AppendString(`\u202`)
|
||||
w.Buffer.AppendByte(chars[runeValue&0xf])
|
||||
i += runeWidth
|
||||
p = i
|
||||
continue
|
||||
}
|
||||
i += runeWidth
|
||||
}
|
||||
w.Buffer.AppendString(s[p:])
|
||||
w.Buffer.AppendByte('"')
|
||||
}
|
||||
|
||||
const encode = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
const padChar = '='
|
||||
|
||||
func (w *Writer) base64(in []byte) {
|
||||
|
||||
if len(in) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
w.Buffer.EnsureSpace(((len(in)-1)/3 + 1) * 4)
|
||||
|
||||
si := 0
|
||||
n := (len(in) / 3) * 3
|
||||
|
||||
for si < n {
|
||||
// Convert 3x 8bit source bytes into 4 bytes
|
||||
val := uint(in[si+0])<<16 | uint(in[si+1])<<8 | uint(in[si+2])
|
||||
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, encode[val>>18&0x3F], encode[val>>12&0x3F], encode[val>>6&0x3F], encode[val&0x3F])
|
||||
|
||||
si += 3
|
||||
}
|
||||
|
||||
remain := len(in) - si
|
||||
if remain == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Add the remaining small block
|
||||
val := uint(in[si+0]) << 16
|
||||
if remain == 2 {
|
||||
val |= uint(in[si+1]) << 8
|
||||
}
|
||||
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, encode[val>>18&0x3F], encode[val>>12&0x3F])
|
||||
|
||||
switch remain {
|
||||
case 2:
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, encode[val>>6&0x3F], byte(padChar))
|
||||
case 1:
|
||||
w.Buffer.Buf = append(w.Buffer.Buf, byte(padChar), byte(padChar))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package easyjson
|
||||
|
||||
import (
|
||||
"github.com/mailru/easyjson/jlexer"
|
||||
"github.com/mailru/easyjson/jwriter"
|
||||
)
|
||||
|
||||
// RawMessage is a raw piece of JSON (number, string, bool, object, array or
|
||||
// null) that is extracted without parsing and output as is during marshaling.
|
||||
type RawMessage []byte
|
||||
|
||||
// MarshalEasyJSON does JSON marshaling using easyjson interface.
|
||||
func (v *RawMessage) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
if len(*v) == 0 {
|
||||
w.RawString("null")
|
||||
} else {
|
||||
w.Raw(*v, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON does JSON unmarshaling using easyjson interface.
|
||||
func (v *RawMessage) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
*v = RawMessage(l.Raw())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements encoding/json.Unmarshaler interface.
|
||||
func (v *RawMessage) UnmarshalJSON(data []byte) error {
|
||||
*v = data
|
||||
return nil
|
||||
}
|
||||
|
||||
var nullBytes = []byte("null")
|
||||
|
||||
// MarshalJSON implements encoding/json.Marshaler interface.
|
||||
func (v RawMessage) MarshalJSON() ([]byte, error) {
|
||||
if len(v) == 0 {
|
||||
return nullBytes, nil
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// IsDefined is required for integration with omitempty easyjson logic.
|
||||
func (v *RawMessage) IsDefined() bool {
|
||||
return len(*v) > 0
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
|
@ -0,0 +1,15 @@
|
|||
language: go
|
||||
go_import_path: github.com/pkg/errors
|
||||
go:
|
||||
- 1.4.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- tip
|
||||
|
||||
script:
|
||||
- go test -v ./...
|
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
|
||||
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.
|
||||
|
||||
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.
|
|
@ -0,0 +1,52 @@
|
|||
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge)
|
||||
|
||||
Package errors provides simple error handling primitives.
|
||||
|
||||
`go get github.com/pkg/errors`
|
||||
|
||||
The traditional error handling idiom in Go is roughly akin to
|
||||
```go
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
|
||||
|
||||
## Adding context to an error
|
||||
|
||||
The errors.Wrap function returns a new error that adds context to the original error. For example
|
||||
```go
|
||||
_, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read failed")
|
||||
}
|
||||
```
|
||||
## Retrieving the cause of an error
|
||||
|
||||
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
|
||||
```go
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
```
|
||||
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
|
||||
```go
|
||||
switch err := errors.Cause(err).(type) {
|
||||
case *MyError:
|
||||
// handle specifically
|
||||
default:
|
||||
// unknown error
|
||||
}
|
||||
```
|
||||
|
||||
[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
|
||||
|
||||
Before proposing a change, please discuss your change by raising an issue.
|
||||
|
||||
## License
|
||||
|
||||
BSD-2-Clause
|
|
@ -0,0 +1,32 @@
|
|||
version: build-{build}.{branch}
|
||||
|
||||
clone_folder: C:\gopath\src\github.com\pkg\errors
|
||||
shallow_clone: true # for startup speed
|
||||
|
||||
environment:
|
||||
GOPATH: C:\gopath
|
||||
|
||||
platform:
|
||||
- x64
|
||||
|
||||
# http://www.appveyor.com/docs/installed-software
|
||||
install:
|
||||
# some helpful output for debugging builds
|
||||
- go version
|
||||
- go env
|
||||
# pre-installed MinGW at C:\MinGW is 32bit only
|
||||
# but MSYS2 at C:\msys64 has mingw64
|
||||
- set PATH=C:\msys64\mingw64\bin;%PATH%
|
||||
- gcc --version
|
||||
- g++ --version
|
||||
|
||||
build_script:
|
||||
- go install -v ./...
|
||||
|
||||
test_script:
|
||||
- set PATH=C:\gopath\bin;%PATH%
|
||||
- go test -v ./...
|
||||
|
||||
#artifacts:
|
||||
# - path: '%GOPATH%\bin\*.exe'
|
||||
deploy: off
|
|
@ -0,0 +1,282 @@
|
|||
// Package errors provides simple error handling primitives.
|
||||
//
|
||||
// The traditional error handling idiom in Go is roughly akin to
|
||||
//
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// which when applied recursively up the call stack results in error reports
|
||||
// without context or debugging information. The errors package allows
|
||||
// programmers to add context to the failure path in their code in a way
|
||||
// that does not destroy the original value of the error.
|
||||
//
|
||||
// Adding context to an error
|
||||
//
|
||||
// The errors.Wrap function returns a new error that adds context to the
|
||||
// original error by recording a stack trace at the point Wrap is called,
|
||||
// together with the supplied message. For example
|
||||
//
|
||||
// _, err := ioutil.ReadAll(r)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "read failed")
|
||||
// }
|
||||
//
|
||||
// If additional control is required, the errors.WithStack and
|
||||
// errors.WithMessage functions destructure errors.Wrap into its component
|
||||
// operations: annotating an error with a stack trace and with a message,
|
||||
// respectively.
|
||||
//
|
||||
// Retrieving the cause of an error
|
||||
//
|
||||
// Using errors.Wrap constructs a stack of errors, adding context to the
|
||||
// preceding error. Depending on the nature of the error it may be necessary
|
||||
// to reverse the operation of errors.Wrap to retrieve the original error
|
||||
// for inspection. Any error value which implements this interface
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
|
||||
// the topmost error that does not implement causer, which is assumed to be
|
||||
// the original cause. For example:
|
||||
//
|
||||
// switch err := errors.Cause(err).(type) {
|
||||
// case *MyError:
|
||||
// // handle specifically
|
||||
// default:
|
||||
// // unknown error
|
||||
// }
|
||||
//
|
||||
// Although the causer interface is not exported by this package, it is
|
||||
// considered a part of its stable public interface.
|
||||
//
|
||||
// Formatted printing of errors
|
||||
//
|
||||
// All error values returned from this package implement fmt.Formatter and can
|
||||
// be formatted by the fmt package. The following verbs are supported:
|
||||
//
|
||||
// %s print the error. If the error has a Cause it will be
|
||||
// printed recursively.
|
||||
// %v see %s
|
||||
// %+v extended format. Each Frame of the error's StackTrace will
|
||||
// be printed in detail.
|
||||
//
|
||||
// Retrieving the stack trace of an error or wrapper
|
||||
//
|
||||
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
|
||||
// invoked. This information can be retrieved with the following interface:
|
||||
//
|
||||
// type stackTracer interface {
|
||||
// StackTrace() errors.StackTrace
|
||||
// }
|
||||
//
|
||||
// The returned errors.StackTrace type is defined as
|
||||
//
|
||||
// type StackTrace []Frame
|
||||
//
|
||||
// The Frame type represents a call site in the stack trace. Frame supports
|
||||
// the fmt.Formatter interface that can be used for printing information about
|
||||
// the stack trace of this error. For example:
|
||||
//
|
||||
// if err, ok := err.(stackTracer); ok {
|
||||
// for _, f := range err.StackTrace() {
|
||||
// fmt.Printf("%+s:%d", f)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Although the stackTracer interface is not exported by this package, it is
|
||||
// considered a part of its stable public interface.
|
||||
//
|
||||
// See the documentation for Frame.Format for more details.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// New returns an error with the supplied message.
|
||||
// New also records the stack trace at the point it was called.
|
||||
func New(message string) error {
|
||||
return &fundamental{
|
||||
msg: message,
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf formats according to a format specifier and returns the string
|
||||
// as a value that satisfies error.
|
||||
// Errorf also records the stack trace at the point it was called.
|
||||
func Errorf(format string, args ...interface{}) error {
|
||||
return &fundamental{
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// fundamental is an error that has a message and a stack, but no caller.
|
||||
type fundamental struct {
|
||||
msg string
|
||||
*stack
|
||||
}
|
||||
|
||||
func (f *fundamental) Error() string { return f.msg }
|
||||
|
||||
func (f *fundamental) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
io.WriteString(s, f.msg)
|
||||
f.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, f.msg)
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", f.msg)
|
||||
}
|
||||
}
|
||||
|
||||
// WithStack annotates err with a stack trace at the point WithStack was called.
|
||||
// If err is nil, WithStack returns nil.
|
||||
func WithStack(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
type withStack struct {
|
||||
error
|
||||
*stack
|
||||
}
|
||||
|
||||
func (w *withStack) Cause() error { return w.error }
|
||||
|
||||
func (w *withStack) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v", w.Cause())
|
||||
w.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, w.Error())
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap returns an error annotating err with a stack trace
|
||||
// at the point Wrap is called, and the supplied message.
|
||||
// If err is nil, Wrap returns nil.
|
||||
func Wrap(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapf returns an error annotating err with a stack trace
|
||||
// at the point Wrapf is called, and the format specifier.
|
||||
// If err is nil, Wrapf returns nil.
|
||||
func Wrapf(err error, format string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessage annotates err with a new message.
|
||||
// If err is nil, WithMessage returns nil.
|
||||
func WithMessage(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessagef annotates err with the format specifier.
|
||||
// If err is nil, WithMessagef returns nil.
|
||||
func WithMessagef(err error, format string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withMessage{
|
||||
cause: err,
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
|
||||
type withMessage struct {
|
||||
cause error
|
||||
msg string
|
||||
}
|
||||
|
||||
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
|
||||
func (w *withMessage) Cause() error { return w.cause }
|
||||
|
||||
func (w *withMessage) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v\n", w.Cause())
|
||||
io.WriteString(s, w.msg)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's', 'q':
|
||||
io.WriteString(s, w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Cause returns the underlying cause of the error, if possible.
|
||||
// An error value has a cause if it implements the following
|
||||
// interface:
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// If the error does not implement Cause, the original error will
|
||||
// be returned. If the error is nil, nil will be returned without further
|
||||
// investigation.
|
||||
func Cause(err error) error {
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
for err != nil {
|
||||
cause, ok := err.(causer)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
err = cause.Cause()
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Frame represents a program counter inside a stack frame.
|
||||
type Frame uintptr
|
||||
|
||||
// pc returns the program counter for this frame;
|
||||
// multiple frames may have the same PC value.
|
||||
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
|
||||
|
||||
// file returns the full path to the file that contains the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) file() string {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return "unknown"
|
||||
}
|
||||
file, _ := fn.FileLine(f.pc())
|
||||
return file
|
||||
}
|
||||
|
||||
// line returns the line number of source code of the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) line() int {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return 0
|
||||
}
|
||||
_, line := fn.FileLine(f.pc())
|
||||
return line
|
||||
}
|
||||
|
||||
// Format formats the frame according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s source file
|
||||
// %d source line
|
||||
// %n function name
|
||||
// %v equivalent to %s:%d
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+s function name and path of source file relative to the compile time
|
||||
// GOPATH separated by \n\t (<funcname>\n\t<path>)
|
||||
// %+v equivalent to %+s:%d
|
||||
func (f Frame) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
pc := f.pc()
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
io.WriteString(s, "unknown")
|
||||
} else {
|
||||
file, _ := fn.FileLine(pc)
|
||||
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
|
||||
}
|
||||
default:
|
||||
io.WriteString(s, path.Base(f.file()))
|
||||
}
|
||||
case 'd':
|
||||
fmt.Fprintf(s, "%d", f.line())
|
||||
case 'n':
|
||||
name := runtime.FuncForPC(f.pc()).Name()
|
||||
io.WriteString(s, funcname(name))
|
||||
case 'v':
|
||||
f.Format(s, 's')
|
||||
io.WriteString(s, ":")
|
||||
f.Format(s, 'd')
|
||||
}
|
||||
}
|
||||
|
||||
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
|
||||
type StackTrace []Frame
|
||||
|
||||
// Format formats the stack of Frames according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s lists source files for each Frame in the stack
|
||||
// %v lists the source file and line number for each Frame in the stack
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+v Prints filename, function, and line number for each Frame in the stack.
|
||||
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
for _, f := range st {
|
||||
fmt.Fprintf(s, "\n%+v", f)
|
||||
}
|
||||
case s.Flag('#'):
|
||||
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||
default:
|
||||
fmt.Fprintf(s, "%v", []Frame(st))
|
||||
}
|
||||
case 's':
|
||||
fmt.Fprintf(s, "%s", []Frame(st))
|
||||
}
|
||||
}
|
||||
|
||||
// stack represents a stack of program counters.
|
||||
type stack []uintptr
|
||||
|
||||
func (s *stack) Format(st fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case st.Flag('+'):
|
||||
for _, pc := range *s {
|
||||
f := Frame(pc)
|
||||
fmt.Fprintf(st, "\n%+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stack) StackTrace() StackTrace {
|
||||
f := make([]Frame, len(*s))
|
||||
for i := 0; i < len(f); i++ {
|
||||
f[i] = Frame((*s)[i])
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func callers() *stack {
|
||||
const depth = 32
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(3, pcs[:])
|
||||
var st stack = pcs[0:n]
|
||||
return &st
|
||||
}
|
||||
|
||||
// funcname removes the path prefix component of a function's name reported by func.Name().
|
||||
func funcname(name string) string {
|
||||
i := strings.LastIndex(name, "/")
|
||||
name = name[i+1:]
|
||||
i = strings.Index(name, ".")
|
||||
return name[i+1:]
|
||||
}
|
|
@ -2,6 +2,18 @@
|
|||
git.rootprojects.org/root/keypairs
|
||||
git.rootprojects.org/root/keypairs/keyfetch
|
||||
git.rootprojects.org/root/keypairs/keyfetch/uncached
|
||||
# github.com/go-chi/chi v4.0.0+incompatible
|
||||
github.com/go-chi/chi
|
||||
# github.com/joho/godotenv v1.3.0
|
||||
github.com/joho/godotenv
|
||||
github.com/joho/godotenv/autoload
|
||||
# github.com/mailgun/mailgun-go/v3 v3.6.4
|
||||
github.com/mailgun/mailgun-go/v3
|
||||
github.com/mailgun/mailgun-go/v3/events
|
||||
# github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329
|
||||
github.com/mailru/easyjson
|
||||
github.com/mailru/easyjson/buffer
|
||||
github.com/mailru/easyjson/jlexer
|
||||
github.com/mailru/easyjson/jwriter
|
||||
# github.com/pkg/errors v0.8.1
|
||||
github.com/pkg/errors
|
||||
|
|
Loading…
Reference in New Issue