Add zap logging through the servers.
[mailpopbox.git] / smtp / conn.go
1 package smtp
2
3 import (
4 "crypto/rand"
5 "crypto/tls"
6 "fmt"
7 "net"
8 "net/mail"
9 "net/textproto"
10 "strings"
11 "time"
12
13 "github.com/uber-go/zap"
14 )
15
16 type state int
17
18 const (
19 stateNew state = iota // Before EHLO.
20 stateInitial
21 stateMail
22 stateRecipient
23 stateData
24 )
25
26 type connection struct {
27 server Server
28
29 tp *textproto.Conn
30
31 nc net.Conn
32 tlsNc *tls.Conn
33 remoteAddr net.Addr
34
35 esmtp bool
36
37 log zap.Logger
38
39 state
40 line string
41
42 ehlo string
43 mailFrom *mail.Address
44 rcptTo []mail.Address
45 }
46
47 func AcceptConnection(netConn net.Conn, server Server, log zap.Logger) error {
48 conn := connection{
49 server: server,
50 tp: textproto.NewConn(netConn),
51 nc: netConn,
52 remoteAddr: netConn.RemoteAddr(),
53 log: log.With(zap.Stringer("client", netConn.RemoteAddr())),
54 state: stateNew,
55 }
56
57 var err error
58
59 conn.writeReply(220, fmt.Sprintf("%s ESMTP [%s] (mailpopbox)", server.Name(), netConn.LocalAddr()))
60
61 for {
62 conn.line, err = conn.tp.ReadLine()
63 if err != nil {
64 conn.writeReply(500, "line too long")
65 continue
66 }
67
68 var cmd string
69 if _, err = fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
70 conn.reply(ReplyBadSyntax)
71 continue
72 }
73
74 switch strings.ToUpper(cmd) {
75 case "QUIT":
76 conn.writeReply(221, "Goodbye")
77 conn.tp.Close()
78 break
79 case "HELO":
80 conn.esmtp = false
81 fallthrough
82 case "EHLO":
83 conn.esmtp = true
84 conn.doEHLO()
85 case "STARTTLS":
86 conn.doSTARTTLS()
87 case "MAIL":
88 conn.doMAIL()
89 case "RCPT":
90 conn.doRCPT()
91 case "DATA":
92 conn.doDATA()
93 case "RSET":
94 conn.doRSET()
95 case "VRFY":
96 conn.writeReply(252, "I'll do my best")
97 case "EXPN":
98 conn.writeReply(550, "access denied")
99 case "NOOP":
100 conn.reply(ReplyOK)
101 case "HELP":
102 conn.writeReply(250, "https://tools.ietf.org/html/rfc5321")
103 default:
104 conn.writeReply(500, "unrecognized command")
105 }
106 }
107
108 return err
109 }
110
111 func (conn *connection) reply(reply ReplyLine) error {
112 return conn.writeReply(reply.Code, reply.Message)
113 }
114
115 func (conn *connection) writeReply(code int, msg string) error {
116 if len(msg) > 0 {
117 return conn.tp.PrintfLine("%d %s", code, msg)
118 } else {
119 return conn.tp.PrintfLine("%d", code)
120 }
121 }
122
123 // parsePath parses out either a forward-, reverse-, or return-path from the
124 // current connection line. Returns a (valid-path, ReplyOK) if it was
125 // successfully parsed.
126 func (conn *connection) parsePath(command string) (string, ReplyLine) {
127 if len(conn.line) < len(command) {
128 return "", ReplyBadSyntax
129 }
130 if strings.ToUpper(command) != strings.ToUpper(conn.line[:len(command)]) {
131 return "", ReplyLine{500, "unrecognized command"}
132 }
133 return conn.line[len(command):], ReplyOK
134 }
135
136 func (conn *connection) doEHLO() {
137 conn.resetBuffers()
138
139 var cmd string
140 _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &conn.ehlo)
141 if err != nil {
142 conn.reply(ReplyBadSyntax)
143 return
144 }
145
146 if cmd == "HELO" {
147 conn.writeReply(250, fmt.Sprintf("Hello %s [%s]", conn.ehlo, conn.remoteAddr))
148 } else {
149 conn.tp.PrintfLine("250-Hello %s [%s]", conn.ehlo, conn.remoteAddr)
150 if conn.server.TLSConfig() != nil && conn.tlsNc == nil {
151 conn.tp.PrintfLine("250-STARTTLS")
152 }
153 conn.tp.PrintfLine("250 SIZE %d", 40960000)
154 }
155
156 conn.log.Info("doEHLO()", zap.String("ehlo", conn.ehlo))
157
158 conn.state = stateInitial
159 }
160
161 func (conn *connection) doSTARTTLS() {
162 if conn.state != stateInitial {
163 conn.reply(ReplyBadSequence)
164 return
165 }
166
167 tlsConfig := conn.server.TLSConfig()
168 if !conn.esmtp || tlsConfig == nil {
169 conn.writeReply(500, "unrecognized command")
170 return
171 }
172
173 conn.log.Info("doSTARTTLS()")
174 conn.writeReply(220, "initiate TLS connection")
175
176 newConn := tls.Server(conn.nc, tlsConfig)
177 if err := newConn.Handshake(); err != nil {
178 return
179 }
180
181 conn.tlsNc = newConn
182 conn.tp = textproto.NewConn(conn.tlsNc)
183 conn.state = stateInitial
184
185 conn.log.Info("HELO again")
186
187 conn.writeReply(220, fmt.Sprintf("%s ESMTPS [%s] (mailpopbox)",
188 conn.server.Name(), newConn.LocalAddr()))
189 }
190
191 func (conn *connection) doMAIL() {
192 if conn.state != stateInitial {
193 conn.reply(ReplyBadSequence)
194 return
195 }
196
197 mailFrom, reply := conn.parsePath("MAIL FROM:")
198 if reply != ReplyOK {
199 conn.reply(reply)
200 return
201 }
202
203 var err error
204 conn.mailFrom, err = mail.ParseAddress(mailFrom)
205 if err != nil {
206 conn.reply(ReplyBadSyntax)
207 return
208 }
209
210 conn.log.Info("doMAIL()", zap.String("address", conn.mailFrom.Address))
211
212 conn.state = stateMail
213 conn.reply(ReplyOK)
214 }
215
216 func (conn *connection) doRCPT() {
217 if conn.state != stateMail && conn.state != stateRecipient {
218 conn.reply(ReplyBadSequence)
219 return
220 }
221
222 rcptTo, reply := conn.parsePath("RCPT TO:")
223 if reply != ReplyOK {
224 conn.reply(reply)
225 return
226 }
227
228 address, err := mail.ParseAddress(rcptTo)
229 if err != nil {
230 conn.reply(ReplyBadSyntax)
231 }
232
233 if reply := conn.server.VerifyAddress(*address); reply != ReplyOK {
234 conn.log.Warn("invalid address",
235 zap.String("address", address.Address),
236 zap.Stringer("reply", reply))
237 conn.reply(reply)
238 return
239 }
240
241 conn.log.Info("doRCPT()", zap.String("address", address.Address))
242
243 conn.rcptTo = append(conn.rcptTo, *address)
244
245 conn.state = stateRecipient
246 conn.reply(ReplyOK)
247 }
248
249 func (conn *connection) doDATA() {
250 if conn.state != stateRecipient {
251 conn.reply(ReplyBadSequence)
252 return
253 }
254
255 conn.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
256 conn.log.Info("doDATA()")
257
258 data, err := conn.tp.ReadDotBytes()
259 if err != nil {
260 conn.log.Error("failed to ReadDotBytes()", zap.Error(err))
261 conn.writeReply(552, "transaction failed")
262 return
263 }
264
265 received := time.Now()
266 env := Envelope{
267 RemoteAddr: conn.remoteAddr,
268 EHLO: conn.ehlo,
269 MailFrom: *conn.mailFrom,
270 RcptTo: conn.rcptTo,
271 Received: received,
272 ID: conn.envelopeID(received),
273 }
274
275 conn.log.Info("received message",
276 zap.Int("bytes", len(data)),
277 zap.Time("date", received),
278 zap.String("id", env.ID))
279
280 trace := conn.getReceivedInfo(env)
281
282 env.Data = append(trace, data...)
283
284 if reply := conn.server.OnMessageDelivered(env); reply != nil {
285 conn.log.Warn("message was rejected", zap.String("id", env.ID))
286 conn.reply(*reply)
287 return
288 }
289
290 conn.state = stateInitial
291 conn.reply(ReplyOK)
292 }
293
294 func (conn *connection) envelopeID(t time.Time) string {
295 var idBytes [4]byte
296 rand.Read(idBytes[:])
297 return fmt.Sprintf("m.%d.%x", t.UnixNano(), idBytes)
298 }
299
300 func (conn *connection) getReceivedInfo(envelope Envelope) []byte {
301 rhost, _, err := net.SplitHostPort(conn.remoteAddr.String())
302 if err != nil {
303 rhost = conn.remoteAddr.String()
304 }
305
306 rhosts, err := net.LookupAddr(rhost)
307 if err == nil {
308 rhost = fmt.Sprintf("%s [%s]", rhosts[0], rhost)
309 }
310
311 base := fmt.Sprintf("Received: from %s (%s)\r\n ", conn.ehlo, rhost)
312
313 with := "SMTP"
314 if conn.esmtp {
315 with = "E" + with
316 }
317 if conn.tlsNc != nil {
318 with += "S"
319 }
320 base += fmt.Sprintf("by %s (mailpopbox) with %s id %s\r\n ", conn.server.Name(), with, envelope.ID)
321
322 base += fmt.Sprintf("for <%s>\r\n ", envelope.RcptTo[0].Address)
323
324 transport := conn.getTransportString()
325 date := envelope.Received.Format(time.RFC1123Z) // Same as RFC 5322 ยง 3.3
326 base += fmt.Sprintf("(using %s);\r\n %s\r\n", transport, date)
327
328 return []byte(base)
329 }
330
331 func (conn *connection) getTransportString() string {
332 if conn.tlsNc == nil {
333 return "PLAINTEXT"
334 }
335
336 ciphers := map[uint16]string{
337 tls.TLS_RSA_WITH_RC4_128_SHA: "TLS_RSA_WITH_RC4_128_SHA",
338 tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
339 tls.TLS_RSA_WITH_AES_128_CBC_SHA: "TLS_RSA_WITH_AES_128_CBC_SHA",
340 tls.TLS_RSA_WITH_AES_256_CBC_SHA: "TLS_RSA_WITH_AES_256_CBC_SHA",
341 tls.TLS_RSA_WITH_AES_128_GCM_SHA256: "TLS_RSA_WITH_AES_128_GCM_SHA256",
342 tls.TLS_RSA_WITH_AES_256_GCM_SHA384: "TLS_RSA_WITH_AES_256_GCM_SHA384",
343 tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
344 tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
345 tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
346 tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
347 tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
348 tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
349 tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
350 tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
351 tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
352 tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
353 tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
354 }
355 versions := map[uint16]string{
356 tls.VersionSSL30: "SSLv3.0",
357 tls.VersionTLS10: "TLSv1.0",
358 tls.VersionTLS11: "TLSv1.1",
359 tls.VersionTLS12: "TLSv1.2",
360 }
361
362 state := conn.tlsNc.ConnectionState()
363
364 version := versions[state.Version]
365 cipher := ciphers[state.CipherSuite]
366
367 if version == "" {
368 version = fmt.Sprintf("%x", state.Version)
369 }
370 if cipher == "" {
371 cipher = fmt.Sprintf("%x", state.CipherSuite)
372 }
373
374 name := ""
375 if state.ServerName != "" {
376 name = fmt.Sprintf(" name=%s", state.ServerName)
377 }
378
379 return fmt.Sprintf("%s cipher=%s%s", version, cipher, name)
380 }
381
382 func (conn *connection) doRSET() {
383 conn.log.Info("doRSET()")
384 conn.state = stateInitial
385 conn.resetBuffers()
386 conn.reply(ReplyOK)
387 }
388
389 func (conn *connection) resetBuffers() {
390 conn.mailFrom = nil
391 conn.rcptTo = make([]mail.Address, 0)
392 }