├── .idea
├── markdown-navigator
│ └── profiles_settings.xml
├── misc.xml
├── vcs.xml
├── modules.xml
├── check-smtp.iml
└── markdown-navigator.xml
├── .gitignore
├── test-email
├── test_mail_request.go
└── content.go
├── http
└── handlers
│ ├── health.go
│ └── check_transport.go
├── main.go
├── lib
├── mail-sender
│ ├── connection.go
│ ├── smtp-commands
│ │ └── commands.go
│ └── client.go
├── results.go
├── tls_parser
│ └── tls_parser.go
├── transport_server.go
└── environment-vars
│ └── environment.go
├── Dockerfile
├── Gopkg.toml
├── Gopkg.lock
├── Jenkinsfile
├── README.md
└── LICENSE
/.idea/markdown-navigator/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 | .idea
14 | main
15 |
16 | vendor/
17 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/check-smtp.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/test-email/test_mail_request.go:
--------------------------------------------------------------------------------
1 | package test_email
2 |
3 | import "github.com/zerospam/check-smtp/lib"
4 |
5 | type TestEmailRequest struct {
6 | From string `json:"from"`
7 | Body string `json:"body"`
8 | Subject string `json:"subject"`
9 | Server *lib.TransportServer `json:"server"`
10 | }
11 |
12 | func (t *TestEmailRequest) ToTestEmail() *Email {
13 | return NewTestEmail(t.Subject, t.Body, t.From, t.Server.TestEmail)
14 | }
15 |
--------------------------------------------------------------------------------
/http/handlers/health.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | )
7 |
8 | // e.g. http.HandleFunc("/healthz", HealthCheck)
9 | func HealthCheck(w http.ResponseWriter, r *http.Request) {
10 | // A very simple health check.
11 | w.WriteHeader(http.StatusOK)
12 | w.Header().Set("Content-Type", "application/json")
13 |
14 | // In the future we could report back on the status of our DB, or our cache
15 | // (e.g. Redis) by performing a simple PING, and include them in the response.
16 | io.WriteString(w, `{"alive": true}`)
17 | }
18 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/zerospam/check-smtp/http/handlers"
6 | "github.com/zerospam/check-smtp/lib/environment-vars"
7 | "net/http"
8 | "os"
9 | )
10 |
11 | func init() {
12 | http.HandleFunc("/check", handlers.CheckTransport)
13 | http.HandleFunc("/healthz", handlers.HealthCheck)
14 | }
15 |
16 | func main() {
17 | os.Setenv("GODEBUG", os.Getenv("GODEBUG")+",tls13=1")
18 | err := http.ListenAndServe(fmt.Sprintf(":%s", environmentvars.GetVars().ApplicationPort), nil)
19 | if err != nil {
20 | panic(err)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/mail-sender/connection.go:
--------------------------------------------------------------------------------
1 | package mail_sender
2 |
3 | import (
4 | "net"
5 | )
6 |
7 | type Conn struct {
8 | net.Conn
9 | firstRead []byte
10 | firstLineRead bool
11 | }
12 |
13 | func (c *Conn) Read(b []byte) (n int, err error) {
14 | read, err := c.Conn.Read(b)
15 | if err != nil {
16 | return read, err
17 | }
18 | if c.firstLineRead {
19 | return read, err
20 | }
21 |
22 | c.firstRead = b
23 | c.firstLineRead = true
24 |
25 | return read, err
26 | }
27 |
28 | func NewConnection(conn net.Conn) *Conn {
29 | return &Conn{Conn: conn, firstLineRead: false}
30 | }
31 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG APP_PACKAGE="github.com/zerospam/check-smtp"
2 | ARG APP_PATH="/go/src/${APP_PACKAGE}"
3 | ARG APP_NAME="smtpChecker"
4 |
5 | FROM zerospam/go-dep-docker as builder
6 |
7 | ENV CGO_ENABLED=0
8 | ENV GOOS=linux
9 |
10 | ARG APP_PACKAGE
11 | ARG APP_PATH
12 | ARG APP_NAME
13 |
14 | COPY . $APP_PATH
15 | WORKDIR $APP_PATH
16 |
17 | RUN dep ensure
18 | RUN go build -a -installsuffix cgo -o $APP_NAME
19 |
20 | FROM alpine:latest
21 |
22 | ARG APP_PATH
23 | ARG APP_NAME
24 | RUN sed -i -e 's/dl-cdn/dl-4/' /etc/apk/repositories \
25 | && apk --update --no-cache add ca-certificates
26 |
27 | COPY --from=builder ${APP_PATH}/${APP_NAME} /${APP_NAME}
28 |
29 | CMD ["/smtpChecker"]
30 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | # Gopkg.toml example
2 | #
3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
4 | # for detailed Gopkg.toml documentation.
5 | #
6 | # required = ["github.com/user/thing/cmd/thing"]
7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
8 | #
9 | # [[constraint]]
10 | # name = "github.com/user/project"
11 | # version = "1.0.0"
12 | #
13 | # [[constraint]]
14 | # name = "github.com/user/project2"
15 | # branch = "dev"
16 | # source = "github.com/myfork/project2"
17 | #
18 | # [[override]]
19 | # name = "github.com/x/y"
20 | # version = "2.4.0"
21 | #
22 | # [prune]
23 | # non-go = false
24 | # go-tests = true
25 | # unused-packages = true
26 |
27 |
28 | [prune]
29 | go-tests = true
30 | unused-packages = true
31 |
32 | [metadata.heroku]
33 | root-package = "github.com/zerospam/check-smtp"
34 | go-version = "1.11"
--------------------------------------------------------------------------------
/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | digest = "1:2e76a73cb51f42d63a2a1a85b3dc5731fd4faf6821b434bd0ef2c099186031d6"
6 | name = "github.com/rs/xid"
7 | packages = ["."]
8 | pruneopts = "UT"
9 | revision = "15d26544def341f036c5f8dca987a4cbe575032c"
10 | version = "v1.2.1"
11 |
12 | [[projects]]
13 | branch = "master"
14 | digest = "1:cd596abe1695f597a9acb7041e6f450b6c856452f9cbf643863784825fffeaaf"
15 | name = "github.com/zerospam/check-firewall"
16 | packages = ["lib/tls-generator"]
17 | pruneopts = "UT"
18 | revision = "ba18e040fc4052dc248c5a43aac02f4c785bb75c"
19 |
20 | [solve-meta]
21 | analyzer-name = "dep"
22 | analyzer-version = 1
23 | input-imports = [
24 | "github.com/rs/xid",
25 | "github.com/zerospam/check-firewall/lib/tls-generator",
26 | ]
27 | solver-name = "gps-cdcl"
28 | solver-version = 1
29 |
--------------------------------------------------------------------------------
/lib/results.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "fmt"
5 | "github.com/zerospam/check-smtp/lib/mail-sender/smtp-commands"
6 | )
7 |
8 | type SmtpError struct {
9 | Command smtp_commands.Commands `json:"command"`
10 | ErrorMessage string `json:"error_msg"`
11 | }
12 |
13 | type CheckResult struct {
14 | Success bool `json:"success"`
15 | HelloBanner string `json:"hello_banner"`
16 | TlsVersion string `json:"tls_version"`
17 | Error *SmtpError `json:"error_message,omitempty"`
18 | GeneralLog smtp_commands.CommandLog `json:"general_log"`
19 | SPFLog smtp_commands.CommandLog `json:"spf_log"`
20 | }
21 |
22 | func NewSmtpError(Op smtp_commands.Commands, err error) *SmtpError {
23 | return &SmtpError{Command: Op, ErrorMessage: err.Error()}
24 | }
25 |
26 | func (se *SmtpError) String() string {
27 | return fmt.Sprintf("%s: %s", se.Command, se.ErrorMessage)
28 | }
29 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | def label = "replicator-${UUID.randomUUID().toString()}"
2 | podTemplate(label: label, inheritFrom: 'docker') {
3 | def image="zerospam/check-smtp"
4 | def tag = "1.2.2"
5 | def builtImage = null
6 |
7 | node (label) {
8 | gitInfo = checkout scm
9 | container('docker') {
10 | stage('Build & Test') {
11 | builtImage = docker.build("${image}:${env.BUILD_ID}")
12 | }
13 |
14 | if (gitInfo.GIT_BRANCH.equals('master')) {
15 | // master branch release
16 | stage('Push docker image to Docker Hub') {
17 | docker.withRegistry('https://index.docker.io/v1/', 'docker-hub') {
18 | builtImage.push('latest')
19 | builtImage.push("${tag}")
20 | }
21 | } // stage
22 | } // if master branch
23 |
24 | } //container
25 | } //node
26 | } //pipeline
--------------------------------------------------------------------------------
/lib/tls_parser/tls_parser.go:
--------------------------------------------------------------------------------
1 | package tls_parser
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | )
7 |
8 | func DecodeString(str string) (uint16, error) {
9 | switch str {
10 | case "SSL30":
11 | return tls.VersionSSL30, nil
12 | case "TLS10":
13 | return tls.VersionTLS10, nil
14 | case "TLS11":
15 | return tls.VersionTLS11, nil
16 | case "TLS12":
17 | return tls.VersionTLS12, nil
18 | case "TLS13":
19 | return tls.VersionTLS13, nil
20 | default:
21 | return 0, fmt.Errorf("%s unrecognized TLS version", str)
22 | }
23 | }
24 |
25 | func ToString(version uint16) (string, error) {
26 | switch version {
27 | case tls.VersionSSL30:
28 | return "VersionSSL30", nil
29 | case tls.VersionTLS10:
30 | return "VersionTLS10", nil
31 | case tls.VersionTLS11:
32 | return "VersionTLS11", nil
33 | case tls.VersionTLS12:
34 | return "VersionTLS12", nil
35 | case tls.VersionTLS13:
36 | return "VersionTLS13", nil
37 | default:
38 | return "", fmt.Errorf("%x unrecognized TLS version", version)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/transport_server.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "time"
7 | )
8 |
9 | type TransportServer struct {
10 | Server string `json:"server"`
11 | Port int `json:"port"`
12 | OnMx bool `json:"mx"`
13 | TestEmail string `json:"test_email"`
14 | }
15 |
16 | func (t *TransportServer) Address() (string, error) {
17 |
18 | if !t.OnMx {
19 | return fmt.Sprintf("%s:%d", t.Server, t.Port), nil
20 | }
21 |
22 | mxRecords, errorMx := net.LookupMX(t.Server)
23 | if errorMx != nil {
24 | return "", fmt.Errorf("MX Records: no mx records found for %s\n%s", t.Server, errorMx)
25 | }
26 | if len(mxRecords) == 0 {
27 | return "", fmt.Errorf("MX Records: no mx records found for %s", t.Server)
28 | }
29 | return fmt.Sprintf("%s:%d", mxRecords[0].Host, t.Port), nil
30 |
31 | }
32 |
33 | func (t *TransportServer) Connect(timeout time.Duration) (conn net.Conn, err error) {
34 | address, err := t.Address()
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | return net.DialTimeout("tcp", address, timeout)
40 | }
41 |
--------------------------------------------------------------------------------
/lib/mail-sender/smtp-commands/commands.go:
--------------------------------------------------------------------------------
1 | package smtp_commands
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | )
8 |
9 | type Commands int
10 |
11 | const (
12 | Timeout Commands = iota
13 | Connection
14 | Ehlo
15 | StartTls
16 | MailFrom
17 | RcptTo
18 | Data
19 | Quit
20 | SpfFail
21 | )
22 |
23 | type CommandLog map[Commands]string
24 |
25 | func (c Commands) String() string {
26 | names := []string{
27 | "TIMEOUT",
28 | "CONNECTION",
29 | "EHLO",
30 | "STARTTLS",
31 | "MAIL FROM",
32 | "RCPT TO",
33 | "DATA",
34 | "QUIT",
35 | "SPF-FAIL"}
36 |
37 | if c < Timeout || c > SpfFail {
38 | return "Unknown"
39 | }
40 |
41 | return names[c]
42 | }
43 |
44 | func (c *Commands) MarshalJSON() ([]byte, error) {
45 | buffer := bytes.NewBufferString(`"`)
46 | buffer.WriteString(c.String())
47 | buffer.WriteString(`"`)
48 | return buffer.Bytes(), nil
49 | }
50 |
51 | func (i CommandLog) MarshalJSON() ([]byte, error) {
52 | x := make(map[string]string)
53 | for k, v := range i {
54 | x[fmt.Sprintf("%d/%s", k, k.String())] = v
55 | }
56 | marshal, e := json.Marshal(x)
57 | return marshal, e
58 | }
59 |
--------------------------------------------------------------------------------
/test-email/content.go:
--------------------------------------------------------------------------------
1 | package test_email
2 |
3 | import (
4 | "crypto/sha1"
5 | "fmt"
6 | "github.com/rs/xid"
7 | "io"
8 | "strings"
9 | "time"
10 | )
11 |
12 | type Email struct {
13 | From string
14 | To string
15 | Body string
16 | Subject string
17 | Headers map[string]string
18 | }
19 |
20 | func (e *Email) generateMessageId(localName string) string {
21 | hasherSha1 := sha1.New()
22 |
23 | io.WriteString(hasherSha1, e.Subject)
24 | io.WriteString(hasherSha1, e.From)
25 | io.WriteString(hasherSha1, e.To)
26 | guid := xid.New()
27 |
28 | return fmt.Sprintf("%s-%x@%s", guid, sha1.Sum(nil), localName)
29 | }
30 |
31 | func (e *Email) PrepareHeaders(localName string) {
32 |
33 | e.Headers = map[string]string{
34 | "From": fmt.Sprintf("Mail Server Tester <%s>", e.From),
35 | "To": fmt.Sprintf("Mail Server Tester Receiver <%s>", e.To),
36 | "Subject": e.Subject,
37 | "Date": time.Now().Format(time.RFC1123Z),
38 | "Message-Id": fmt.Sprintf("<%s>", e.generateMessageId(localName)),
39 |
40 | "MIME-Version": "1.0",
41 | "Content-Transfer-Encoding": "8bit",
42 |
43 | "Auto-Submitted": "auto-generated",
44 | "X-Mailer": "SMTP Server Tester",
45 | "Content-Type": "text/plain; charset=\"UTF-8\"",
46 | }
47 | }
48 |
49 | func NewTestEmail(subject string, body string, from string, to string) *Email {
50 |
51 | return &Email{
52 | Body: body,
53 | From: from,
54 | To: to,
55 | Subject: subject,
56 | }
57 | }
58 |
59 | func (e *Email) String() string {
60 | var builder strings.Builder
61 | builder.Grow(len(e.Body) + len(e.Headers)*10)
62 | for header, value := range e.Headers {
63 | builder.WriteString(fmt.Sprintf("%s: %s\r\n", header, value))
64 | }
65 |
66 | builder.WriteString("\r\n")
67 | builder.WriteString(e.Body)
68 | var replaced = strings.ReplaceAll(builder.String(),"\r\n", "\n")
69 |
70 | return strings.ReplaceAll(replaced, "\n", "\r\n")
71 | }
72 |
--------------------------------------------------------------------------------
/lib/environment-vars/environment.go:
--------------------------------------------------------------------------------
1 | package environmentvars
2 |
3 | import (
4 | "crypto/tls"
5 | "github.com/zerospam/check-smtp/lib"
6 | "github.com/zerospam/check-smtp/lib/mail-sender"
7 | "github.com/zerospam/check-smtp/lib/tls_parser"
8 | "net/mail"
9 | "os"
10 | "sync"
11 | "time"
12 | )
13 |
14 | type Env struct {
15 | ApplicationPort string
16 | SharedKey string
17 | SmtpCN string
18 | SmtpMailFrom *mail.Address
19 | SmtpConnectionTimeout time.Duration
20 | SmtpOperationTimeout time.Duration
21 | SmtpMailSpoof *mail.Address
22 | TLSMinVersion uint16
23 | }
24 |
25 | var instance *Env
26 | var once sync.Once
27 |
28 | func GetVars() *Env {
29 | once.Do(func() {
30 | hostname, err := os.Hostname()
31 | if err != nil {
32 | panic(err)
33 | }
34 |
35 | commonName := os.Getenv("SMTP_CN")
36 | if commonName == "" {
37 | commonName = hostname
38 | }
39 |
40 | var emailSpoof *mail.Address
41 | emailFromSpoof := os.Getenv("SMTP_FROM_SPOOF")
42 |
43 | if emailFromSpoof == "" {
44 | emailFromSpoof = "spoof@amazon.com"
45 | }
46 |
47 | emailSpoof, err = mail.ParseAddress(emailFromSpoof)
48 | if err != nil {
49 | panic(err)
50 | }
51 |
52 | port := os.Getenv("PORT")
53 | if port == "" {
54 | port = "80"
55 | }
56 |
57 | timeoutParsed := 30 * time.Second
58 | timeout := os.Getenv("SMTP_CONN_TIMEOUT")
59 | if timeout != "" {
60 | timeoutParsed, err = time.ParseDuration(timeout)
61 | if err != nil {
62 | panic(err)
63 | }
64 | }
65 |
66 | timeoutOptParsed := 30 * time.Second
67 | timeoutOpt := os.Getenv("SMTP_OPT_TIMEOUT")
68 | if timeoutOpt != "" {
69 | timeoutOptParsed, err = time.ParseDuration(timeoutOpt)
70 | if err != nil {
71 | panic(err)
72 | }
73 | }
74 |
75 | tlsVersionParsed := uint16(tls.VersionTLS12)
76 | tlsVersion := os.Getenv("TLS_MIN_VERSION")
77 | if tlsVersion != "" {
78 | tlsVersionParsed, err = tls_parser.DecodeString(tlsVersion)
79 | if err != nil {
80 | panic(err)
81 | }
82 | }
83 |
84 | instance = &Env{
85 | ApplicationPort: port,
86 | SharedKey: os.Getenv("SHARED_KEY"),
87 | SmtpCN: commonName,
88 | SmtpConnectionTimeout: timeoutParsed,
89 | SmtpOperationTimeout: timeoutOptParsed,
90 | SmtpMailSpoof: emailSpoof,
91 | TLSMinVersion: tlsVersionParsed,
92 | }
93 | })
94 | return instance
95 | }
96 |
97 | func (e *Env) NewSmtpClient(server *lib.TransportServer) (*mail_sender.Client, *lib.SmtpError) {
98 | return mail_sender.NewClient(server, e.SmtpCN, e.SmtpConnectionTimeout, e.SmtpOperationTimeout, e.TLSMinVersion)
99 | }
100 |
--------------------------------------------------------------------------------
/http/handlers/check_transport.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/zerospam/check-smtp/lib"
6 | "github.com/zerospam/check-smtp/lib/environment-vars"
7 | "github.com/zerospam/check-smtp/lib/mail-sender/smtp-commands"
8 | "github.com/zerospam/check-smtp/test-email"
9 | "log"
10 | "net/http"
11 | "strings"
12 | )
13 |
14 | func getRequestIp(req *http.Request) string {
15 | if header := req.Header.Get("X-Forwarded-For"); header != "" {
16 | exploded := strings.Split(header, ",")
17 | return strings.Trim(exploded[len(exploded)-1], " ")
18 | }
19 |
20 | return req.RemoteAddr
21 | }
22 |
23 | func generateResult(smtpError *lib.SmtpError, banner string, tlsVersion string, generalLog smtp_commands.CommandLog, spfLog smtp_commands.CommandLog) lib.CheckResult {
24 | success := smtpError == nil
25 | return lib.CheckResult{
26 | Error: smtpError,
27 | Success: success,
28 | TlsVersion: tlsVersion,
29 | HelloBanner: banner,
30 | GeneralLog: generalLog,
31 | SPFLog: spfLog,
32 | }
33 | }
34 |
35 | func testServer(server *lib.TransportServer, email *test_email.Email) lib.CheckResult {
36 | client, err := environmentvars.GetVars().NewSmtpClient(server)
37 | if err != nil {
38 | return generateResult(err, "", "", make(smtp_commands.CommandLog), make(smtp_commands.CommandLog))
39 | }
40 |
41 | err = client.SendTestEmail(email)
42 |
43 | if err != nil {
44 | banner, tlsVersion := client.GetHelloBanner()
45 | return generateResult(err, banner, tlsVersion, client.GetCommandLog(), make(smtp_commands.CommandLog))
46 | }
47 |
48 | generalLog := client.GetCommandLog()
49 | //new client to do the spoofing
50 | //Can't reuse previous client as it closed the connection
51 | client, err = environmentvars.GetVars().NewSmtpClient(server)
52 | if err != nil {
53 | banner, tlsVersion := client.GetHelloBanner()
54 | return generateResult(err, banner, tlsVersion, generalLog, client.GetCommandLog())
55 | }
56 |
57 | err = client.SpoofingTest(environmentvars.GetVars().SmtpMailSpoof.Address)
58 |
59 | if err != nil {
60 | if err.Command == smtp_commands.RcptTo || err.Command == smtp_commands.MailFrom {
61 | err.Command = smtp_commands.SpfFail
62 | }
63 | banner, tlsVersion := client.GetHelloBanner()
64 | return generateResult(err, banner, tlsVersion, generalLog, client.GetCommandLog())
65 | }
66 |
67 | banner, tlsVersion := client.GetHelloBanner()
68 | return generateResult(err, banner, tlsVersion, generalLog, client.GetCommandLog())
69 |
70 | }
71 |
72 | func CheckTransport(w http.ResponseWriter, req *http.Request) {
73 | var testEmailRequest test_email.TestEmailRequest
74 |
75 | if req.Method != "POST" {
76 | http.Error(w, "Only POST accepted.", 405)
77 | return
78 | }
79 |
80 | if req.Header.Get("Authorization") != environmentvars.GetVars().SharedKey {
81 | http.Error(w, "Wrong Key sent.", 402)
82 | log.Printf("[%s] - %s - %v\n", req.RemoteAddr, req.Method, "REJECT")
83 | return
84 | }
85 |
86 | if req.Body == nil {
87 | http.Error(w, "Please send a request body", 400)
88 | return
89 | }
90 |
91 | defer req.Body.Close()
92 | json.NewDecoder(req.Body).Decode(&testEmailRequest)
93 |
94 | w.Header().Add("Content-Type", "application/json")
95 | email := testEmailRequest.ToTestEmail()
96 | server := testEmailRequest.Server
97 |
98 | result := testServer(server, email)
99 |
100 | json.NewEncoder(w).Encode(result)
101 |
102 | log.Printf("[%s] - %s (%s:%d) - %v\n", getRequestIp(req), req.Method, testEmailRequest.Server.Server, testEmailRequest.Server.Port, result.Success)
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Check SMTP
2 |
3 | Mini HTTP service that takes a JSON with server information and check
4 | if it's accessible from the application and if it can receive emails.
5 |
6 | ## Env
7 |
8 | | Key | Requirement | Explanation |
9 | |------------|-----------------------|-----------------------------------------------------------------------------------------------|
10 | | SHARED_KEY | mandatory | Secret shared between main app and this one. (Needs to be sent in the header *Authorization*) |
11 | | PORT | optional (default 80) | Port used for the application |
12 | | SMTP_CN | optional (default hostname)| Common Name to use for client certificate when doing a STARTTLS |
13 | | SMTP_FROM_SPOOF | optional (default spoof@amazon.com)| Email to check for SPF checks (spoofing this email as MAIL FROM) |
14 | | SMTP_CONN_TIMEOUT | optional (30 seconds)| How long to wait for the SMTP server to answer|
15 | | SMTP_OPT_TIMEOUT | optional (30 seconds)| How long to wait for the SMTP server to answer each command|
16 | |TLS_MIN_VERSION| optional (TLS1.2) | Check TLS Table for acceptable values. This is used when doing a STARTTLS on server that supports it.|
17 |
18 | ### TLS
19 |
20 | | TLS Version | EnvVar |
21 | |-------------|--------|
22 | | SSLv3.0 | SSL30 |
23 | | TLSv1 | TLS10 |
24 | | TLSv1.1 | TLS11 |
25 | | TLS1.2 | TLS12 |
26 | | TLS1.3 | TLS13 |
27 |
28 | ## Data
29 | ```json
30 | {
31 | "from": "bounce@myserver.com",
32 | "body": "Hello World\n Are you doing well ?\n\nTester",
33 | "subject": "Hello World",
34 | "server": {
35 | "server": "example.com",
36 | "port": 25,
37 | "mx": false,
38 | "test_email": "test@example.com"
39 | }
40 | }
41 | ```
42 |
43 |
44 | | Key | Explanation |
45 | |--------|--------------------------------------------------------------------------------|
46 | | from | Address to use in the MAIL FROM|
47 | | body | Body of the mail|
48 | | subject | Subject of the mail|
49 | | server.server | Server to check |
50 | | server.port | Port to use to attempt the connection |
51 | | server.mx | Instead of resolving the IP, resolve the MX of the server first then check IPs |
52 | | server.test_email | Used as RCPT TO when doing SMTP checks |
53 |
54 |
55 | ## Response
56 |
57 | ### Success
58 | ```json
59 | {
60 | "success": true,
61 | "hello_banner": "220 example.com ESMTP Postfix (Debian/GNU)",
62 | "tls_version": "VersionTLS12",
63 | "general_log": {
64 | "1/CONNECTION": "192.168.22.3:25",
65 | "2/EHLO": "tardis.example.com",
66 | "3/STARTTLS": "VersionTLS12",
67 | "4/MAIL FROM": "test@example.com",
68 | "5/RCPT TO": "me@example.com",
69 | "6/DATA": "\nHello World\nAre you doing well ?\n\nTester",
70 | "7/QUIT": ""
71 | },
72 | "spf_log": {
73 | "1/CONNECTION": "192.168.22.3:25",
74 | "2/EHLO": "tardis.zerospam.ca",
75 | "3/STARTTLS": "",
76 | "8/SPF-FAIL": "antoineaf@admincmd.com"
77 | }
78 | }
79 | ```
80 |
81 | ### Error
82 | ```json
83 | {
84 | "success": false,
85 | "hello_banner": "220 set.example.com ESMTP Postfix (Debian/GNU)",
86 | "tls_version": "VersionTLS12",
87 | "error_message": {
88 | "command": "STARTTLS",
89 | "error_msg": "x509: certificate is valid for example.com, not set.example.com"
90 | },
91 | "general_log": {
92 | "1/CONNECTION": "192.168.22.3:25",
93 | "2/EHLO": "tardis.example.com",
94 | "3/STARTTLS": ""
95 | },
96 | "spf_log": {
97 | }
98 | }
99 | ```
100 |
--------------------------------------------------------------------------------
/.idea/markdown-navigator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/lib/mail-sender/client.go:
--------------------------------------------------------------------------------
1 | package mail_sender
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | "github.com/zerospam/check-firewall/lib/tls-generator"
7 | "github.com/zerospam/check-smtp/lib"
8 | "github.com/zerospam/check-smtp/lib/mail-sender/smtp-commands"
9 | "github.com/zerospam/check-smtp/lib/tls_parser"
10 | "github.com/zerospam/check-smtp/test-email"
11 | "net/smtp"
12 | "strings"
13 | "time"
14 | )
15 |
16 | type Client struct {
17 | *smtp.Client
18 | server *lib.TransportServer
19 | localName string
20 | tlsGenerator *tlsgenerator.CertificateGenerator
21 | optTimeout time.Duration
22 | lastError *lib.SmtpError
23 | lastCommand *smtp_commands.Commands
24 | sentTestEmail bool
25 | helloBanner string
26 | tlsMinVersion uint16
27 | tlsVersionUsed uint16
28 | commandLog smtp_commands.CommandLog
29 | }
30 |
31 | type SmtpOperation func() error
32 |
33 | //Create new client to send the test email and test the SMTP server
34 | func NewClient(server *lib.TransportServer, localName string, connTimeout time.Duration, optTimeout time.Duration, tlsMinVersion uint16) (*Client, *lib.SmtpError) {
35 | conn, err := server.Connect(connTimeout)
36 | if err != nil {
37 | return nil, lib.NewSmtpError(smtp_commands.Connection, err)
38 | }
39 |
40 | connection := NewConnection(conn)
41 | client, err := smtp.NewClient(connection, server.Server)
42 |
43 | if err != nil {
44 | return nil, lib.NewSmtpError(smtp_commands.Connection, err)
45 | }
46 |
47 | banner := strings.Trim(string(connection.firstRead), "\u0000\r\n")
48 |
49 | return &Client{
50 | Client: client,
51 | localName: localName,
52 | server: server,
53 | optTimeout: optTimeout,
54 | helloBanner: banner,
55 | tlsMinVersion: tlsMinVersion,
56 | tlsVersionUsed: 0,
57 | commandLog: smtp_commands.CommandLog{
58 | smtp_commands.Connection: conn.RemoteAddr().String(),
59 | },
60 | }, nil
61 | }
62 |
63 | func (c *Client) GetLastCommand() (*smtp_commands.Commands, *lib.SmtpError) {
64 | return c.lastCommand, c.lastError
65 | }
66 |
67 | func (c *Client) GetCommandLog() smtp_commands.CommandLog {
68 | return c.commandLog
69 | }
70 |
71 | func (c *Client) GetHelloBanner() (banner string, tlsVersion string) {
72 |
73 | if c.tlsVersionUsed == 0 {
74 | return c.helloBanner, "None"
75 | }
76 |
77 | tlsVersion, err := tls_parser.ToString(c.tlsVersionUsed)
78 | if err != nil {
79 | tlsVersion = err.Error()
80 | }
81 |
82 | return c.helloBanner, tlsVersion
83 | }
84 |
85 | func (c *Client) getClientTLSConfig(commonName string) *tls.Config {
86 | if c.tlsGenerator == nil {
87 | c.tlsGenerator = tlsgenerator.NewClient(time.Now(), 30*365*24*time.Hour)
88 | }
89 |
90 | return c.tlsGenerator.GetTlsClientConfig(commonName)
91 | }
92 |
93 | func (c *Client) doCommand(command smtp_commands.Commands, optCallback SmtpOperation, argument string) {
94 |
95 | if c.lastError != nil {
96 | return
97 | }
98 |
99 | c.lastCommand = &command
100 | c.commandLog[command] = argument
101 | //second parameter to not wait for a receiver.
102 | //This happen in the case the timeout returns before the command
103 | ch := make(chan error, 1)
104 |
105 | go func() {
106 | ch <- optCallback()
107 | }()
108 |
109 | timer := time.NewTimer(c.optTimeout)
110 | select {
111 | case err := <-ch:
112 | //Stop the timer as the command returned a result
113 | //@see https://golang.org/pkg/time/#After
114 | //Avoiding having a hanging timers
115 | timer.Stop()
116 | if err != nil {
117 | c.lastError = lib.NewSmtpError(command, err)
118 | }
119 | break
120 |
121 | case <-timer.C:
122 | c.lastError = lib.NewSmtpError(smtp_commands.Timeout, fmt.Errorf("CMD [%s] Timed out after %s", command, c.optTimeout))
123 | break
124 | }
125 |
126 | }
127 |
128 | func (c *Client) setTls() error {
129 | if tlsSupport, _ := c.Client.Extension("STARTTLS"); !tlsSupport {
130 | return nil
131 | }
132 | tlsConfig := c.getClientTLSConfig(c.localName)
133 | tlsConfig.ServerName = c.server.Server
134 | tlsConfig.MinVersion = c.tlsMinVersion
135 | //It's impossible to verify correctly the server in the case of a SMTP transaction
136 | //Better be permissive
137 | tlsConfig.InsecureSkipVerify = true
138 | err := c.Client.StartTLS(tlsConfig)
139 | if err != nil {
140 | return err
141 | }
142 | state, _ := c.Client.TLSConnectionState()
143 | c.tlsVersionUsed = state.Version
144 |
145 | return nil
146 | }
147 |
148 | //Try to send the test email
149 | func (c *Client) SendTestEmail(email *test_email.Email) *lib.SmtpError {
150 |
151 | defer c.Client.Close()
152 |
153 | c.doCommand(smtp_commands.Ehlo, func() error {
154 | return c.Client.Hello(c.localName)
155 | }, c.localName)
156 |
157 | c.doCommand(smtp_commands.StartTls, func() error {
158 | result := c.setTls()
159 | if c.tlsVersionUsed != 0 {
160 | versionUsed, _ := tls_parser.ToString(c.tlsVersionUsed)
161 | c.commandLog[smtp_commands.StartTls] = versionUsed
162 | }
163 | return result
164 | }, "")
165 |
166 | c.doCommand(smtp_commands.MailFrom, func() error {
167 | return c.Client.Mail(email.From)
168 | }, email.From)
169 | c.doCommand(smtp_commands.RcptTo, func() error {
170 | return c.Client.Rcpt(c.server.TestEmail)
171 | }, c.server.TestEmail)
172 |
173 | c.doCommand(smtp_commands.Data, func() error {
174 | w, err := c.Data()
175 |
176 | if err != nil {
177 | return err
178 | }
179 |
180 | email.PrepareHeaders(c.localName)
181 |
182 | _, err = w.Write([]byte(email.String()))
183 | if err != nil {
184 | return err
185 | }
186 |
187 | err = w.Close()
188 |
189 | return err
190 | }, email.String())
191 |
192 | c.doCommand(smtp_commands.Quit, func() error {
193 | return c.Client.Quit()
194 | }, "")
195 |
196 | return c.lastError
197 | }
198 |
199 | //Try to send the test email
200 | func (c *Client) SpoofingTest(from string) *lib.SmtpError {
201 |
202 | defer c.Client.Quit()
203 | defer c.Client.Close()
204 |
205 | c.doCommand(smtp_commands.Ehlo, func() error {
206 | return c.Client.Hello(c.localName)
207 | }, c.localName)
208 |
209 | c.doCommand(smtp_commands.StartTls, func() error {
210 | return c.setTls()
211 | }, "")
212 |
213 | c.doCommand(smtp_commands.MailFrom, func() error {
214 | return c.Client.Mail(from)
215 | }, from)
216 | c.doCommand(smtp_commands.RcptTo, func() error {
217 | return c.Client.Rcpt(c.server.TestEmail)
218 | }, c.server.TestEmail)
219 |
220 | return c.lastError
221 | }
222 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------