-
Notifications
You must be signed in to change notification settings - Fork 6
/
mail.go
180 lines (157 loc) · 5 KB
/
mail.go
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package main
import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/http"
"net/mail"
"net/smtp"
"strings"
log "github.com/Sirupsen/logrus"
)
// MailHandler implements simple RESTish API and holds host and from address
// for validation process
type MailHandler struct {
Hostname string
From string
}
// NewHandler creates new handler with given host and from address
func NewHandler(host, from string) *MailHandler {
return &MailHandler{
Hostname: host,
From: from,
}
}
type apiResponse struct {
Email string `json:"email"`
IsValid bool `json:"is_valid"`
Description string `json:"description"`
Error string `json:"error"`
}
// ServerHTTP is handler for RESTish API
// It understands two arguments: email and callback
// Example: GET /[email protected]
// Example: POST /[email protected]&callback=http://test.com/emailCallback
// Result wiil be in bool field "is_valid"
func (handler *MailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Cache-Control")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
email := r.URL.Query().Get("email")
if email == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
callback := r.URL.Query().Get("callback")
if callback != "" && r.Method == "POST" {
log.WithFields(log.Fields{
"email": email,
"callback": callback,
}).Info("Callback requested")
w.WriteHeader(http.StatusCreated)
go handler.handleCallback(email, callback)
return
}
resp := handler.validateEmail(email)
logFields := log.Fields{"email": email, "valid": resp.IsValid}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.WithFields(logFields).Errorf("Failed to encode callback data: %v", err)
}
log.WithFields(logFields).Info("Email validated")
}
func (handler *MailHandler) handleCallback(email, callback string) {
validateResp := handler.validateEmail(email)
logFields := log.Fields{
"email": email,
"valid": validateResp.IsValid,
"callback": callback,
"error": validateResp.Error,
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(validateResp); err != nil {
log.WithFields(logFields).Errorf("Failed to encode callback data: %v", err)
// Fallback
buf.WriteString(fmt.Sprintf("{\"email\": \"%s\", \"valid\": %v, \"error\": \"\"}",
email, validateResp.IsValid))
}
resp, err := http.Post(callback, "application/json", &buf)
if err != nil {
log.WithFields(logFields).Errorf("Failed to POST: %v", err)
return
}
if err := resp.Body.Close(); err != nil {
log.Errorf("resp.Body.Close() failed: %v", err)
}
}
func (handler *MailHandler) validateEmail(emailAddr string) *apiResponse {
response := &apiResponse{
Email: emailAddr,
IsValid: false,
Description: "Unknown",
Error: "",
}
logFields := log.Fields{
"email": emailAddr,
}
// Step 1: validate format
email, err := mail.ParseAddress(emailAddr)
if err != nil {
response.Error = fmt.Sprintf("invalid email: %v", err)
response.Description = "Format validation failed"
return response
}
log.WithFields(logFields).Debug("Format is valid")
parts := strings.SplitN(email.Address, "@", 2)
domain := parts[1]
// Step 2: validate MX record
// TODO: add cache
nss, err := net.LookupMX(domain)
if err != nil {
response.Error = fmt.Sprintf("MX lookup failed for %v: %v", domain, err)
response.Description = "Coudn't find MX server for this address"
return response
}
if len(nss) == 0 {
response.Error = fmt.Sprintf("no MX records found for %v", domain)
response.Description = "Coudn't find MX server for this address"
return response
}
log.WithFields(logFields).Debugf("Found MX servers: %v", nss)
// Step 3: try to "send" email
// TODO: add connection pooling?
// TODO: add timeout?
client, err := smtp.Dial(nss[0].Host + ":25")
if err != nil {
response.Error = fmt.Sprintf("can't connect to %s: %v", domain, err)
response.Description = "MX server is unreachable"
return response
}
log.WithFields(logFields).Debugf("Connected to: %v", nss[0].Host+":25")
if err := client.Hello(handler.Hostname); err != nil {
response.Error = fmt.Sprintf("HELO failed: %v", err)
return response
}
log.WithFields(logFields).Debug("HELO is ok")
if err := client.Mail(handler.From); err != nil {
response.Error = fmt.Sprintf("MAIL failed: %v", err)
return response
}
log.WithFields(logFields).Debug("MAIL is ok")
if err := client.Rcpt(email.Address); err != nil {
response.Error = fmt.Sprintf("RCPT failed for %s: %v", email.Address, err)
response.Description = err.Error()
return response
}
log.WithFields(logFields).Debug("RCPT is ok")
if err := client.Quit(); err != nil {
response.Error = fmt.Sprintf("QUIT failed: %v", err)
return response
}
log.WithFields(logFields).Debug("QUIT is ok")
response.Description = "Ok"
response.IsValid = true
return response
}