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