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