When receiving a message, the SMTP server must add its trace information.
[mailpopbox.git] / smtp / conn.go
1 package smtp
2
3 import (
4 "crypto/rand"
5 "fmt"
6 "net"
7 "net/mail"
8 "net/textproto"
9 "strings"
10 "time"
11 )
12
13 type state int
14
15 const (
16 stateNew state = iota // Before EHLO.
17 stateInitial
18 stateMail
19 stateRecipient
20 stateData
21 )
22
23 type connection struct {
24 server Server
25
26 tp *textproto.Conn
27 remoteAddr net.Addr
28
29 esmtp bool
30 tls bool
31
32 state
33 line string
34
35 ehlo string
36 mailFrom *mail.Address
37 rcptTo []mail.Address
38 }
39
40 func AcceptConnection(netConn net.Conn, server Server) error {
41 conn := connection{
42 server: server,
43 tp: textproto.NewConn(netConn),
44 remoteAddr: netConn.RemoteAddr(),
45 state: stateNew,
46 }
47
48 var err error
49
50 conn.writeReply(220, fmt.Sprintf("%s ESMTP [%s] (mailpopbox)", server.Name(), netConn.LocalAddr()))
51
52 for {
53 conn.line, err = conn.tp.ReadLine()
54 if err != nil {
55 conn.writeReply(500, "line too long")
56 continue
57 }
58
59 var cmd string
60 if _, err = fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
61 conn.reply(ReplyBadSyntax)
62 continue
63 }
64
65 switch strings.ToUpper(cmd) {
66 case "QUIT":
67 conn.writeReply(221, "Goodbye")
68 conn.tp.Close()
69 break
70 case "HELO":
71 conn.esmtp = false
72 fallthrough
73 case "EHLO":
74 conn.esmtp = true
75 conn.doEHLO()
76 case "MAIL":
77 conn.doMAIL()
78 case "RCPT":
79 conn.doRCPT()
80 case "DATA":
81 conn.doDATA()
82 case "RSET":
83 conn.doRSET()
84 case "VRFY":
85 conn.writeReply(252, "I'll do my best")
86 case "EXPN":
87 conn.writeReply(550, "access denied")
88 case "NOOP":
89 conn.reply(ReplyOK)
90 case "HELP":
91 conn.writeReply(250, "https://tools.ietf.org/html/rfc5321")
92 default:
93 conn.writeReply(500, "unrecognized command")
94 }
95 }
96
97 return err
98 }
99
100 func (conn *connection) reply(reply ReplyLine) {
101 conn.writeReply(reply.Code, reply.Message)
102 }
103
104 func (conn *connection) writeReply(code int, msg string) {
105 if len(msg) > 0 {
106 conn.tp.PrintfLine("%d %s", code, msg)
107 } else {
108 conn.tp.PrintfLine("%d", code)
109 }
110 }
111
112 // parsePath parses out either a forward-, reverse-, or return-path from the
113 // current connection line. Returns a (valid-path, ReplyOK) if it was
114 // successfully parsed.
115 func (conn *connection) parsePath(command string) (string, ReplyLine) {
116 if len(conn.line) < len(command) {
117 return "", ReplyBadSyntax
118 }
119 if strings.ToUpper(command) != strings.ToUpper(conn.line[:len(command)]) {
120 return "", ReplyLine{500, "unrecognized command"}
121 }
122 return conn.line[len(command):], ReplyOK
123 }
124
125 func (conn *connection) doEHLO() {
126 conn.resetBuffers()
127
128 var cmd string
129 _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &conn.ehlo)
130 if err != nil {
131 conn.reply(ReplyBadSyntax)
132 return
133 }
134
135 if cmd == "HELO" {
136 conn.writeReply(250, fmt.Sprintf("Hello %s [%s]", conn.ehlo, conn.remoteAddr))
137 } else {
138 conn.tp.PrintfLine("250-Hello %s [%s]", conn.ehlo, conn.remoteAddr)
139 if conn.server.TLSConfig() != nil {
140 conn.tp.PrintfLine("250-STARTTLS")
141 }
142 conn.tp.PrintfLine("250 SIZE %d", 40960000)
143 }
144
145 conn.state = stateInitial
146 }
147
148 func (conn *connection) doMAIL() {
149 if conn.state != stateInitial {
150 conn.reply(ReplyBadSequence)
151 return
152 }
153
154 mailFrom, reply := conn.parsePath("MAIL FROM:")
155 if reply != ReplyOK {
156 conn.reply(reply)
157 return
158 }
159
160 var err error
161 conn.mailFrom, err = mail.ParseAddress(mailFrom)
162 if err != nil {
163 conn.reply(ReplyBadSyntax)
164 return
165 }
166
167 conn.state = stateMail
168 conn.reply(ReplyOK)
169 }
170
171 func (conn *connection) doRCPT() {
172 if conn.state != stateMail && conn.state != stateRecipient {
173 conn.reply(ReplyBadSequence)
174 return
175 }
176
177 rcptTo, reply := conn.parsePath("RCPT TO:")
178 if reply != ReplyOK {
179 conn.reply(reply)
180 return
181 }
182
183 address, err := mail.ParseAddress(rcptTo)
184 if err != nil {
185 conn.reply(ReplyBadSyntax)
186 }
187
188 if reply := conn.server.VerifyAddress(*address); reply != ReplyOK {
189 conn.reply(reply)
190 return
191 }
192
193 conn.rcptTo = append(conn.rcptTo, *address)
194
195 conn.state = stateRecipient
196 conn.reply(ReplyOK)
197 }
198
199 func (conn *connection) doDATA() {
200 if conn.state != stateRecipient {
201 conn.reply(ReplyBadSequence)
202 return
203 }
204
205 conn.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
206
207 data, err := conn.tp.ReadDotBytes()
208 if err != nil {
209 // TODO: log error
210 conn.writeReply(552, "transaction failed")
211 return
212 }
213
214 received := time.Now()
215 env := Envelope{
216 RemoteAddr: conn.remoteAddr,
217 EHLO: conn.ehlo,
218 MailFrom: *conn.mailFrom,
219 RcptTo: conn.rcptTo,
220 Received: received,
221 ID: conn.envelopeID(received),
222 }
223
224 trace := conn.getReceivedInfo(env)
225
226 env.Data = append(trace, data...)
227
228 if reply := conn.server.OnMessageDelivered(env); reply != nil {
229 conn.reply(*reply)
230 return
231 }
232
233 conn.state = stateInitial
234 conn.reply(ReplyOK)
235 }
236
237 func (conn *connection) envelopeID(t time.Time) string {
238 var idBytes [4]byte
239 rand.Read(idBytes[:])
240 return fmt.Sprintf("m.%d.%x", t.UnixNano(), idBytes)
241 }
242
243 func (conn *connection) getReceivedInfo(envelope Envelope) []byte {
244 rhost, _, err := net.SplitHostPort(conn.remoteAddr.String())
245 if err != nil {
246 rhost = conn.remoteAddr.String()
247 }
248
249 rhosts, err := net.LookupAddr(rhost)
250 if err == nil {
251 rhost = fmt.Sprintf("%s [%s]", rhosts[0], rhost)
252 }
253
254 base := fmt.Sprintf("Received: from %s (%s)\r\n ", conn.ehlo, rhost)
255
256 with := "SMTP"
257 if conn.esmtp {
258 with = "E" + with
259 }
260 if conn.tls {
261 with += "S"
262 }
263 base += fmt.Sprintf("by %s (mailpopbox) with %s id %s\r\n ", conn.server.Name(), with, envelope.ID)
264
265 base += fmt.Sprintf("for <%s>\r\n ", envelope.RcptTo[0].Address)
266
267 transport := "PLAINTEXT"
268 if conn.tls {
269 // TODO: TLS version, cipher, bits
270 }
271 date := envelope.Received.Format(time.RFC1123Z) // Same as RFC 5322 ยง 3.3
272 base += fmt.Sprintf("(using %s);\r\n %s\r\n", transport, date)
273
274 return []byte(base)
275 }
276
277 func (conn *connection) doRSET() {
278 conn.state = stateInitial
279 conn.resetBuffers()
280 conn.reply(ReplyOK)
281 }
282
283 func (conn *connection) resetBuffers() {
284 conn.mailFrom = nil
285 conn.rcptTo = make([]mail.Address, 0)
286 }