Handle case-insensitivity properly for SMTP commands.
[mailpopbox.git] / smtp / conn.go
1 package smtp
2
3 import (
4 "fmt"
5 "net"
6 "net/mail"
7 "net/textproto"
8 "strings"
9 )
10
11 type state int
12
13 const (
14 stateNew state = iota // Before EHLO.
15 stateInitial
16 stateMail
17 stateRecipient
18 stateData
19 )
20
21 type connection struct {
22 server Server
23
24 tp *textproto.Conn
25 remoteAddr net.Addr
26
27 state
28 line string
29
30 ehlo string
31 mailFrom *mail.Address
32 rcptTo []mail.Address
33 }
34
35 func AcceptConnection(netConn net.Conn, server Server) error {
36 conn := connection{
37 server: server,
38 tp: textproto.NewConn(netConn),
39 remoteAddr: netConn.RemoteAddr(),
40 state: stateNew,
41 }
42
43 var err error
44
45 conn.writeReply(220, fmt.Sprintf("%s ESMTP [%s] (mailpopbox)", server.Name(), netConn.LocalAddr()))
46
47 for {
48 conn.line, err = conn.tp.ReadLine()
49 if err != nil {
50 conn.writeReply(500, "line too long")
51 continue
52 }
53
54 var cmd string
55 if _, err = fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
56 conn.reply(ReplyBadSyntax)
57 continue
58 }
59
60 switch strings.ToUpper(cmd) {
61 case "QUIT":
62 conn.writeReply(221, "Goodbye")
63 conn.tp.Close()
64 break
65 case "HELO":
66 fallthrough
67 case "EHLO":
68 conn.doEHLO()
69 case "MAIL":
70 conn.doMAIL()
71 case "RCPT":
72 conn.doRCPT()
73 case "DATA":
74 conn.doDATA()
75 case "RSET":
76 conn.doRSET()
77 case "VRFY":
78 conn.writeReply(252, "I'll do my best")
79 case "EXPN":
80 conn.writeReply(550, "access denied")
81 case "NOOP":
82 conn.reply(ReplyOK)
83 case "HELP":
84 conn.writeReply(250, "https://tools.ietf.org/html/rfc5321")
85 default:
86 conn.writeReply(500, "unrecognized command")
87 }
88 }
89
90 return err
91 }
92
93 func (conn *connection) reply(reply ReplyLine) {
94 conn.writeReply(reply.Code, reply.Message)
95 }
96
97 func (conn *connection) writeReply(code int, msg string) {
98 if len(msg) > 0 {
99 conn.tp.PrintfLine("%d %s", code, msg)
100 } else {
101 conn.tp.PrintfLine("%d", code)
102 }
103 }
104
105 // parsePath parses out either a forward-, reverse-, or return-path from the
106 // current connection line. Returns a (valid-path, ReplyOK) if it was
107 // successfully parsed.
108 func (conn *connection) parsePath(command string) (string, ReplyLine) {
109 if len(conn.line) < len(command) {
110 return "", ReplyBadSyntax
111 }
112 if strings.ToUpper(command) != strings.ToUpper(conn.line[:len(command)]) {
113 return "", ReplyLine{500, "unrecognized command"}
114 }
115 return conn.line[len(command):], ReplyOK
116 }
117
118 func (conn *connection) doEHLO() {
119 conn.resetBuffers()
120
121 var cmd string
122 _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &conn.ehlo)
123 if err != nil {
124 conn.reply(ReplyBadSyntax)
125 return
126 }
127
128 if cmd == "HELO" {
129 conn.writeReply(250, fmt.Sprintf("Hello %s [%s]", conn.ehlo, conn.remoteAddr))
130 } else {
131 conn.tp.PrintfLine("250-Hello %s [%s]", conn.ehlo, conn.remoteAddr)
132 if conn.server.TLSConfig() != nil {
133 conn.tp.PrintfLine("250-STARTTLS")
134 }
135 conn.tp.PrintfLine("250 SIZE %d", 40960000)
136 }
137
138 conn.state = stateInitial
139 }
140
141 func (conn *connection) doMAIL() {
142 if conn.state != stateInitial {
143 conn.reply(ReplyBadSequence)
144 return
145 }
146
147 mailFrom, reply := conn.parsePath("MAIL FROM:")
148 if reply != ReplyOK {
149 conn.reply(reply)
150 return
151 }
152
153 var err error
154 conn.mailFrom, err = mail.ParseAddress(mailFrom)
155 if err != nil {
156 conn.reply(ReplyBadSyntax)
157 return
158 }
159
160 conn.state = stateMail
161 conn.reply(ReplyOK)
162 }
163
164 func (conn *connection) doRCPT() {
165 if conn.state != stateMail && conn.state != stateRecipient {
166 conn.reply(ReplyBadSequence)
167 return
168 }
169
170 rcptTo, reply := conn.parsePath("RCPT TO:")
171 if reply != ReplyOK {
172 conn.reply(reply)
173 return
174 }
175
176 address, err := mail.ParseAddress(rcptTo)
177 if err != nil {
178 conn.reply(ReplyBadSyntax)
179 }
180
181 if reply := conn.server.VerifyAddress(*address); reply != ReplyOK {
182 conn.reply(reply)
183 return
184 }
185
186 conn.rcptTo = append(conn.rcptTo, *address)
187
188 conn.state = stateRecipient
189 conn.reply(ReplyOK)
190 }
191
192 func (conn *connection) doDATA() {
193 if conn.state != stateRecipient {
194 conn.reply(ReplyBadSequence)
195 return
196 }
197
198 conn.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
199
200 data, err := conn.tp.ReadDotBytes()
201 if err != nil {
202 // TODO: log error
203 conn.writeReply(552, "transaction failed")
204 return
205 }
206
207 env := Envelope{
208 RemoteAddr: conn.remoteAddr,
209 EHLO: conn.ehlo,
210 MailFrom: *conn.mailFrom,
211 RcptTo: conn.rcptTo,
212 Data: data,
213 }
214
215 if reply := conn.server.OnMessageDelivered(env); reply != nil {
216 conn.reply(*reply)
217 return
218 }
219
220 conn.state = stateInitial
221 conn.reply(ReplyOK)
222 }
223
224 func (conn *connection) doRSET() {
225 conn.state = stateInitial
226 conn.resetBuffers()
227 conn.reply(ReplyOK)
228 }
229
230 func (conn *connection) resetBuffers() {
231 conn.mailFrom = nil
232 conn.rcptTo = make([]mail.Address, 0)
233 }