diff options
author | Mike Crute <mike@crute.us> | 2019-03-02 06:33:13 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2019-03-02 07:02:51 +0000 |
commit | 837762f56718fab3a4d4b34f1dc0151065ed38c2 (patch) | |
tree | 7dad48f3f23ba245d317878aff925c7ef7682cad | |
download | ses-smtpd-proxy-837762f56718fab3a4d4b34f1dc0151065ed38c2.tar.bz2 ses-smtpd-proxy-837762f56718fab3a4d4b34f1dc0151065ed38c2.tar.xz ses-smtpd-proxy-837762f56718fab3a4d4b34f1dc0151065ed38c2.zip |
Initial import
-rw-r--r-- | .dockerignore | 5 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Dockerfile | 6 | ||||
-rw-r--r-- | LICENSE.txt | 19 | ||||
-rw-r--r-- | Makefile | 16 | ||||
-rw-r--r-- | README.md | 58 | ||||
-rw-r--r-- | go.mod | 9 | ||||
-rw-r--r-- | go.sum | 17 | ||||
-rw-r--r-- | main.go | 91 |
9 files changed, 222 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e9eafac --- /dev/null +++ b/.dockerignore | |||
@@ -0,0 +1,5 @@ | |||
1 | Dockerfile | ||
2 | go.mod | ||
3 | go.sum | ||
4 | main.go | ||
5 | Makefile | ||
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8769110 --- /dev/null +++ b/.gitignore | |||
@@ -0,0 +1 @@ | |||
ses-smtpd-proxy | |||
diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f32f39f --- /dev/null +++ b/Dockerfile | |||
@@ -0,0 +1,6 @@ | |||
1 | FROM alpine:latest | ||
2 | |||
3 | RUN apk add --no-cache ca-certificates | ||
4 | ADD ses-smtpd-proxy / | ||
5 | |||
6 | CMD [ "/ses-smtpd-proxy" ] | ||
diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..205da82 --- /dev/null +++ b/LICENSE.txt | |||
@@ -0,0 +1,19 @@ | |||
1 | Copyright (c) 2019 Michael Crute | ||
2 | |||
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
4 | this software and associated documentation files (the "Software"), to deal in | ||
5 | the Software without restriction, including without limitation the rights to | ||
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | ||
7 | of the Software, and to permit persons to whom the Software is furnished to do | ||
8 | so, subject to the following conditions: | ||
9 | |||
10 | The above copyright notice and this permission notice shall be included in all | ||
11 | copies or substantial portions of the Software. | ||
12 | |||
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
19 | SOFTWARE. | ||
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..76bbcc6 --- /dev/null +++ b/Makefile | |||
@@ -0,0 +1,16 @@ | |||
1 | DOCKER_REGISTRY ?= docker.crute.me | ||
2 | DOCKER_IMAGE_NAME ?= ses-email-proxy | ||
3 | DOCKER_TAG ?= latest | ||
4 | |||
5 | DOCKER_IMAGE_SPEC = $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_NAME):$(DOCKER_TAG) | ||
6 | |||
7 | .PHONY: docker | ||
8 | docker: ses-smtpd-proxy | ||
9 | docker build -t $(DOCKER_IMAGE_SPEC) . | ||
10 | |||
11 | .PHONY: publish | ||
12 | publish: docker | ||
13 | docker push $(DOCKER_IMAGE_SPEC) | ||
14 | |||
15 | ses-smtpd-proxy: main.go | ||
16 | CGO_ENABLED=0 go build -o $@ $< | ||
diff --git a/README.md b/README.md new file mode 100644 index 0000000..080e39c --- /dev/null +++ b/README.md | |||
@@ -0,0 +1,58 @@ | |||
1 | # SMTP to SES Mail Proxy | ||
2 | |||
3 | This is a tiny little proxy that speaks unauthenticated SMTP on the front side | ||
4 | and makes calls to the SES | ||
5 | [SendRawEmail](https://docs.aws.amazon.com/ses/latest/APIReference/API_SendRawEmail.html) | ||
6 | on the back side. | ||
7 | |||
8 | Everything this software does is possible with a more fully-featured mail | ||
9 | server like Postfix but requires setting up Postfix (which is complicated) and, | ||
10 | if following best practices, rotating credentials every 90 days (which is | ||
11 | annoying). Because this integrates with the AWS SDK it can be configured | ||
12 | through the normal SDK configuration channels such as the instance metadata | ||
13 | service which provides dynamic credentials or environment variables, in which | ||
14 | case you should still manually rotate credentials but have one choke-point to | ||
15 | do that. | ||
16 | |||
17 | ## Usage | ||
18 | By default the command takes no arguments and will listen on port 2500 on all | ||
19 | interfaces. The listen interfaces and port can be specified as the only | ||
20 | argument separated with a colon like so: | ||
21 | |||
22 | ``` | ||
23 | ./ses-smtpd-proxy 127.0.0.1:2600 | ||
24 | ``` | ||
25 | |||
26 | ## Security Warning | ||
27 | This server speaks plain unauthenticated SMTP (no TLS) so it's not suitable for | ||
28 | use in an untrusted environment nor on the public internet. I don't have these | ||
29 | use-cases but I would accept pull requests implementing these features if you | ||
30 | do have the use-case and want to add them. | ||
31 | |||
32 | ## Building | ||
33 | To build the binary run `make ses-smtpd-proxy`. | ||
34 | |||
35 | To build a Docker image, which is based on Alpine Latest, run `make docker` or | ||
36 | `make publish`. The later command will build and push the image. To override | ||
37 | the defaults specify `DOCKER_REGISTRY`, `DOCKER_IMAGE_NAME`, and `DOCKER_TAG` | ||
38 | in the make command like so: | ||
39 | |||
40 | ``` | ||
41 | make DOCKER_REGISTRY=reg.example.com DOCKER_IMAGE_NAME=ses-proxy DOCKER_TAG=foo docker | ||
42 | ``` | ||
43 | ## Contributing | ||
44 | If you would like to contribute please visit the project's GitHub page and open | ||
45 | a pull request with your changes. To have the best experience contributing, | ||
46 | please: | ||
47 | |||
48 | * Don't break backwards compatibility of public interfaces | ||
49 | * Update the readme, if necessary | ||
50 | * Follow the coding style of the current code-base | ||
51 | * Ensure that your code is formatted by gofmt | ||
52 | * Validate that your changes work with Go 1.11+ | ||
53 | |||
54 | All code is reviewed before acceptance and changes may be requested to better | ||
55 | follow the conventions of the existing API. | ||
56 | |||
57 | ## Contributors | ||
58 | * Mike Crute (@mcrute) | ||
@@ -0,0 +1,9 @@ | |||
1 | module github.com/mcrute/ses-smtpd-proxy | ||
2 | |||
3 | require ( | ||
4 | github.com/aws/aws-sdk-go v1.17.9 | ||
5 | github.com/mcrute/go-smtpd v0.0.0-20190302041702-3bbdd47ced7e | ||
6 | github.com/stretchr/testify v1.3.0 // indirect | ||
7 | golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 // indirect | ||
8 | golang.org/x/text v0.3.0 // indirect | ||
9 | ) | ||
@@ -0,0 +1,17 @@ | |||
1 | github.com/aws/aws-sdk-go v1.17.9 h1:umGyqfZNxB4waFNvARXzBalEwoYz+8Cqk3xM45No9GI= | ||
2 | github.com/aws/aws-sdk-go v1.17.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= | ||
3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= | ||
4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
5 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= | ||
6 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= | ||
7 | github.com/mcrute/go-smtpd v0.0.0-20190302041702-3bbdd47ced7e h1:/FsR+P4lNGpbB/yh/PEg36RLbPHPM6dVi3mSY0Rwu5Q= | ||
8 | github.com/mcrute/go-smtpd v0.0.0-20190302041702-3bbdd47ced7e/go.mod h1:m8WdV6PyiYKhiWhfdLfBG0UksKK1cKtZzNE/DZgiVTg= | ||
9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
12 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= | ||
13 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||
14 | golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU= | ||
15 | golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||
16 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= | ||
17 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
@@ -0,0 +1,91 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "fmt" | ||
6 | "log" | ||
7 | "os" | ||
8 | |||
9 | "github.com/aws/aws-sdk-go/aws/session" | ||
10 | "github.com/aws/aws-sdk-go/service/ses" | ||
11 | "github.com/mcrute/go-smtpd/smtpd" | ||
12 | ) | ||
13 | |||
14 | const ( | ||
15 | SES_SIZE_LIMIT = 10000000 | ||
16 | DEFAULT_ADDR = ":2500" | ||
17 | ) | ||
18 | |||
19 | var sesClient *ses.SES | ||
20 | |||
21 | type Envelope struct { | ||
22 | rcpts []*string | ||
23 | b bytes.Buffer | ||
24 | } | ||
25 | |||
26 | func (e *Envelope) AddRecipient(rcpt smtpd.MailAddress) error { | ||
27 | email := rcpt.Email() | ||
28 | e.rcpts = append(e.rcpts, &email) | ||
29 | return nil | ||
30 | } | ||
31 | |||
32 | func (e *Envelope) BeginData() error { | ||
33 | if len(e.rcpts) == 0 { | ||
34 | return smtpd.SMTPError("554 5.5.1 Error: no valid recipients") | ||
35 | } | ||
36 | return nil | ||
37 | } | ||
38 | |||
39 | func (e *Envelope) Write(line []byte) error { | ||
40 | e.b.Write(line) | ||
41 | if e.b.Len() > SES_SIZE_LIMIT { // SES limitation | ||
42 | log.Println("message size %d exceeds SES limit of %d", e.b.Len(), SES_SIZE_LIMIT) | ||
43 | return smtpd.SMTPError("554 5.5.1 Error: maximum message size exceeded") | ||
44 | } | ||
45 | return nil | ||
46 | } | ||
47 | |||
48 | func (e *Envelope) logMessageSend() { | ||
49 | dr := make([]string, len(e.rcpts)) | ||
50 | for i := range e.rcpts { | ||
51 | dr[i] = *e.rcpts[i] | ||
52 | } | ||
53 | log.Printf("sending message to %+v", dr) | ||
54 | } | ||
55 | |||
56 | func (e *Envelope) Close() error { | ||
57 | e.logMessageSend() | ||
58 | r := &ses.SendRawEmailInput{ | ||
59 | Destinations: e.rcpts, | ||
60 | RawMessage: &ses.RawMessage{Data: e.b.Bytes()}, | ||
61 | } | ||
62 | _, err := sesClient.SendRawEmail(r) | ||
63 | if err != nil { | ||
64 | log.Printf("ERROR: ses: %v", err) | ||
65 | return smtpd.SMTPError(fmt.Sprintf("554 5.5.1 Error: %v", err)) | ||
66 | } | ||
67 | return err | ||
68 | } | ||
69 | |||
70 | func main() { | ||
71 | sesClient = ses.New(session.New()) | ||
72 | addr := DEFAULT_ADDR | ||
73 | |||
74 | if len(os.Args) == 2 { | ||
75 | addr = os.Args[1] | ||
76 | } else if len(os.Args) > 2 { | ||
77 | log.Fatalf("usage: %s [listen_host:port]", os.Args[0]) | ||
78 | } | ||
79 | |||
80 | s := &smtpd.Server{ | ||
81 | Addr: addr, | ||
82 | OnNewMail: func(c smtpd.Connection, from smtpd.MailAddress) (smtpd.Envelope, error) { | ||
83 | return &Envelope{}, nil | ||
84 | }, | ||
85 | } | ||
86 | log.Printf("ListenAndServe on %s", addr) | ||
87 | err := s.ListenAndServe() | ||
88 | if err != nil { | ||
89 | log.Fatalf("ListenAndServe: %v", err) | ||
90 | } | ||
91 | } | ||