Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Steven Wolfe committed Oct 24, 2017
0 parents commit b5a4e63
Show file tree
Hide file tree
Showing 30 changed files with 1,717 additions and 0 deletions.
29 changes: 29 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
version: 2
jobs:
build:
working_directory: /go/src/github.com/sdwolfe32/trumail
environment:
- DOCKER_TAG: sdwolfe32/trumail
docker:
- image: circleci/golang:1.9.1
steps:
- checkout
- run:
name: Install Glide
command: go get github.com/Masterminds/glide
- run:
name: Download vendored Go dependencies
command: glide install
- run:
name: Build binary for Alpine linux
command: env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
- setup_remote_docker
- run:
name: Build Docker image with binary
command: docker build --no-cache -t $DOCKER_TAG .
- run:
name: Login to DockerHub
command: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run:
name: Push image to DockerHub
command: docker push $DOCKER_TAG
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/vendor/*
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM alpine:latest
RUN apk add --no-cache ca-certificates
ADD trumail /usr/local/bin/trumail
ADD /web /web/
EXPOSE 8000
CMD ["trumail"]
26 changes: 26 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Copyright (c) 2017, Steven Wolfe. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

- Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

- Neither the name of Trumail 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.
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Trumail

[![CircleCI](https://circleci.com/gh/sdwolfe32/trumail.svg?style=svg)](https://circleci.com/gh/sdwolfe32/trumail)
[![GoDoc](https://godoc.org/github.com/sdwolfe32/trumail/verifier?status.svg)](https://godoc.org/github.com/sdwolfe32/trumail/verifier)

Trumail is a free and open source email validation/verification system. It is available in three forms, the Golang client library `verifier` for use in your own Go projects, a public API endpoint (more info: https://trumail.io), and a public Docker image on DockerHub (see: https://hub.docker.com/r/sdwolfe32/trumail/).

Our own API endpoint allows up to 1RPS per IP. This is to prevent abuse and to leave the tubes open for other users. If you perform requests in excess of the specified rate-limit a `429 Too Many Requests` will be returned instead.

NOTE: It is highly recommended (due to potential heroku IP blacklisting resulting in failed validations) that you host the service yourself either using a Docker image or by forking and serving this project on your own instance. However, self-hosting Trumail requires bidirectional communication on port 25 which most residential ISPs restrict. AWS and Digitalocean both allow this.

## Using the API (public or self-hosted)

Using the API is very simple. All that's needed to validate an address is to send a `GET` request using the below URL with one of our two supported formats (json/xml).
```
trumail.io/{format}/{email}
```

## Running with Docker

```
$ docker run sdwolfe32/trumail
```

## Using the library

```
package main
import (
"log"
trumail "github.com/sdwolfe32/trumail/verifier"
)
func main() {
v := trumail.NewVerifier(20, "YOUR_HOSTNAME.COM", "[email protected]")
res := v.Verify("[email protected]")
log.Println(*res[0])
}
```

## Running with Docker

```
docker run -p 8000:8000 -e [email protected] sdwolfe32/trumail
```

The BSD 3-clause License
========================

Copyright (c) 2017, Steven Wolfe. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

- Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

- Neither the name of Trumail 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.
41 changes: 41 additions & 0 deletions api/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package api

import (
"encoding/xml"
"fmt"

"github.com/Sirupsen/logrus"
)

// Error is a generic API error
type Error struct {
XMLName xml.Name `json:"-" xml:"error"`
Message string `json:"message,omitempty" xml:"message,omitempty"`
StatusCode int `json:"status,omitempty" xml:"status,omitempty"`
Err string `json:"err,omitempty" xml:"err,omitempty"`
}

// NewError is a generic http error type used for all error responses
func NewError(message string, statusCode int, err error) *Error {
var errString string
if err != nil {
errString = err.Error()
}
return &Error{
Message: message,
StatusCode: statusCode,
Err: errString,
}
}

// Error returns a string representation of the error and
// helps to satisfy the error interface
func (e *Error) Error() string {
return fmt.Sprintf("Error %d: '%s'", e.StatusCode, e.Message)
}

// Log will log the Error before returning it
func (e *Error) Log(log *logrus.Entry) *Error {
log.WithFields(logrus.Fields{"error": e.Err, "status": e.StatusCode}).Error(e.Message)
return e
}
139 changes: 139 additions & 0 deletions api/router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package api

import (
"encoding/json"
"encoding/xml"
"net/http"
"strings"
"time"

"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"golang.org/x/sync/syncmap"
)

// Router defines all functionality for our api service router
type Router interface {
HandleStatic(path string) *mux.Route
HandleEndpoint(pattern string, endpoint Endpoint) *mux.Route
ListenAndServe(port string) error
}

// Router contains an embedded router that we can use to bind
// Endpoints to
type router struct {
router *mux.Router
rateLimit bool
rateMap *syncmap.Map
}

// NewRouter generates a new Router that will be used to bind
// handlers to the *mux.Router
func NewRouter(rateLimit bool) Router {
return &router{
router: mux.NewRouter(),
rateLimit: rateLimit,
rateMap: &syncmap.Map{}, // ip-address -> last request time
}
}

// HandleStatic binds a new fileserver using the passed path to the router
func (r *router) HandleStatic(path string) *mux.Route {
return r.router.PathPrefix("/").Handler(http.FileServer(http.Dir(path)))
}

// HandleEndpoint binds a new Endpoint handler to the router
func (r *router) HandleEndpoint(pattern string, endpoint Endpoint) *mux.Route {
return r.router.HandleFunc(pattern, r.endpointWrapper(endpoint))
}

// ListenAndServe applies CORS headers and starts the server
// using the embedded router
func (r *router) ListenAndServe(port string) error {
// Create the basic HTTP server with base parameters
srv := &http.Server{
Handler: r.router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}

// Apply CORS headers
srv.Handler = handlers.CORS(
handlers.AllowedHeaders([]string{"Authorization", "Content-Type"}),
handlers.AllowedMethods([]string{"POST", "PUT", "GET", "OPTIONS", "HEAD"}),
handlers.AllowedOrigins([]string{"*"}),
)(r.router)

// Set the port to run on and serve
srv.Addr = ":" + port
return srv.ListenAndServe()
}

// An Endpoint is a service endpoint that receives a request and returns either
// a successfully processed response body or an Error. In either case both
// responses are encoded and returned to the user with the appropriate status
// code
type Endpoint func(*http.Request) (interface{}, error)

// endpointWrapper transforms an endpoint into a standard http.Handlerfunc
func (r *router) endpointWrapper(e Endpoint) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
// Extract the request params
ipAddr := req.Header.Get("X-Forwarded-For")
format := strings.ToLower(mux.Vars(req)["format"])

// Execute rate limit logic if the server is configured to do so
if r.rateLimit {
if last, ok := r.rateMap.Load(ipAddr); ok {
if last.(time.Time).Add(time.Second).After(time.Now()) {
respond(rw, http.StatusTooManyRequests, format,
NewError("You have exceeded the rate-limit", http.StatusTooManyRequests, nil))
return
}
}
// Defer store fresh rate-limit time
defer r.rateMap.Store(ipAddr, time.Now())
}

// Handle the request and respond appropriately
res, err := e(req)
if err != nil {
if e, ok := err.(*Error); ok {
respond(rw, e.StatusCode, format, e)
} else {
respond(rw, http.StatusInternalServerError, format,
NewError("An error has occurred", http.StatusInternalServerError, err))
}
return
}
respond(rw, http.StatusOK, format, res)
}
}

// respond is responsible for encoding the response and writing the desired
// format to the http.ResponseWriter
func respond(w http.ResponseWriter, status int, format string, res interface{}) {
// Encode the response using the format passed
switch format {
case "xml":
encodeXML(w, status, res)
case "json":
encodeJSON(w, status, res)
default:
encodeJSON(w, status, res)
}
}

// encodeXML encodes the response to XML and writes it to the ResponseWriter
func encodeXML(w http.ResponseWriter, status int, res interface{}) {
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(status)
xml.NewEncoder(w).Encode(res)
}

// encodeJSON encodes the response to JSON and writes it to the ResponseWriter
func encodeJSON(w http.ResponseWriter, status int, res interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(res)
}
Loading

0 comments on commit b5a4e63

Please sign in to comment.