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