Implement the POP3 CAPA command from RFC 2449.
[mailpopbox.git] / pop3 / conn.go
1 package pop3
2
3 import (
4 "fmt"
5 "io"
6 "net"
7 "net/textproto"
8 "strings"
9
10 "github.com/uber-go/zap"
11 )
12
13 type state int
14
15 const (
16 stateAuth state = iota
17 stateTxn
18 stateUpdate
19 )
20
21 const (
22 errStateAuth = "not in AUTHORIZATION"
23 errStateTxn = "not in TRANSACTION"
24 errSyntax = "syntax error"
25 errDeletedMsg = "no such message - deleted"
26 )
27
28 type connection struct {
29 po PostOffice
30 mb Mailbox
31
32 tp *textproto.Conn
33 remoteAddr net.Addr
34
35 log zap.Logger
36
37 state
38 line string
39
40 user string
41 }
42
43 func AcceptConnection(netConn net.Conn, po PostOffice, log zap.Logger) {
44 log = log.With(zap.Stringer("client", netConn.RemoteAddr()))
45 conn := connection{
46 po: po,
47 tp: textproto.NewConn(netConn),
48 state: stateAuth,
49 log: log,
50 }
51
52 conn.log.Info("accepted connection")
53 conn.ok(fmt.Sprintf("POP3 (mailpopbox) server %s", po.Name()))
54
55 var err error
56
57 for {
58 conn.line, err = conn.tp.ReadLine()
59 if err != nil {
60 conn.log.Error("ReadLine()", zap.Error(err))
61 conn.tp.Close()
62 return
63 }
64
65 var cmd string
66 if _, err := fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
67 conn.err("invalid command")
68 continue
69 }
70
71 conn.log = log.With(zap.String("command", cmd))
72
73 switch strings.ToUpper(cmd) {
74 case "QUIT":
75 conn.doQUIT()
76 return
77 case "USER":
78 conn.doUSER()
79 case "PASS":
80 conn.doPASS()
81 case "STAT":
82 conn.doSTAT()
83 case "LIST":
84 conn.doLIST()
85 case "RETR":
86 conn.doRETR()
87 case "DELE":
88 conn.doDELE()
89 case "NOOP":
90 conn.ok("")
91 case "RSET":
92 conn.doRSET()
93 case "UIDL":
94 conn.doUIDL()
95 case "CAPA":
96 conn.doCAPA()
97 default:
98 conn.err("unknown command")
99 }
100 }
101 }
102
103 func (conn *connection) ok(msg string) {
104 if len(msg) > 0 {
105 msg = " " + msg
106 }
107 conn.tp.PrintfLine("+OK%s", msg)
108 }
109
110 func (conn *connection) err(msg string) {
111 conn.log.Error("error", zap.String("message", msg))
112 if len(msg) > 0 {
113 msg = " " + msg
114 conn.tp.PrintfLine("-ERR%s", msg)
115 }
116 }
117
118 func (conn *connection) doQUIT() {
119 defer conn.tp.Close()
120
121 if conn.mb != nil {
122 err := conn.mb.Close()
123 if err != nil {
124 conn.err("failed to remove some messages")
125 return
126 }
127 }
128 conn.ok("goodbye")
129 }
130
131 func (conn *connection) doUSER() {
132 if conn.state != stateAuth {
133 conn.err(errStateAuth)
134 return
135 }
136
137 cmd := len("USER ")
138 if len(conn.line) < cmd {
139 conn.err("invalid user")
140 return
141 }
142
143 conn.user = conn.line[cmd:]
144 conn.ok("")
145 }
146
147 func (conn *connection) doPASS() {
148 if conn.state != stateAuth {
149 conn.err(errStateAuth)
150 return
151 }
152
153 if len(conn.user) == 0 {
154 conn.err("no USER")
155 return
156 }
157
158 cmd := len("PASS ")
159 if len(conn.line) < cmd {
160 conn.err("invalid pass")
161 return
162 }
163
164 pass := conn.line[cmd:]
165 if mbox, err := conn.po.OpenMailbox(conn.user, pass); err == nil {
166 conn.log.Info("authenticated", zap.String("user", conn.user))
167 conn.state = stateTxn
168 conn.mb = mbox
169 conn.ok("")
170 } else {
171 conn.log.Error("failed to open mailbox", zap.Error(err))
172 conn.err(err.Error())
173 }
174 }
175
176 func (conn *connection) doSTAT() {
177 if conn.state != stateTxn {
178 conn.err(errStateTxn)
179 return
180 }
181
182 msgs, err := conn.mb.ListMessages()
183 if err != nil {
184 conn.log.Error("failed to list messages", zap.Error(err))
185 conn.err(err.Error())
186 return
187 }
188
189 size := 0
190 num := 0
191 for _, msg := range msgs {
192 if msg.Deleted() {
193 continue
194 }
195 size += msg.Size()
196 num++
197 }
198
199 conn.ok(fmt.Sprintf("%d %d", num, size))
200 }
201
202 func (conn *connection) doLIST() {
203 if conn.state != stateTxn {
204 conn.err(errStateTxn)
205 return
206 }
207
208 msgs, err := conn.mb.ListMessages()
209 if err != nil {
210 conn.log.Error("failed to list messages", zap.Error(err))
211 conn.err(err.Error())
212 return
213 }
214
215 conn.ok("scan listing")
216 for _, msg := range msgs {
217 conn.tp.PrintfLine("%d %d", msg.ID(), msg.Size())
218 }
219 conn.tp.PrintfLine(".")
220 }
221
222 func (conn *connection) doRETR() {
223 if conn.state != stateTxn {
224 conn.err(errStateTxn)
225 return
226 }
227
228 msg := conn.getRequestedMessage()
229 if msg == nil {
230 return
231 }
232
233 if msg.Deleted() {
234 conn.err(errDeletedMsg)
235 return
236 }
237
238 rc, err := conn.mb.Retrieve(msg)
239 if err != nil {
240 conn.log.Error("failed to retrieve messages", zap.Error(err))
241 conn.err(err.Error())
242 return
243 }
244
245 conn.log.Info("retreive message", zap.String("unique-id", msg.UniqueID()))
246 conn.ok(fmt.Sprintf("%d", msg.Size()))
247
248 w := conn.tp.DotWriter()
249 io.Copy(w, rc)
250 w.Close()
251 }
252
253 func (conn *connection) doDELE() {
254 if conn.state != stateTxn {
255 conn.err(errStateTxn)
256 return
257 }
258
259 msg := conn.getRequestedMessage()
260 if msg == nil {
261 return
262 }
263
264 if msg.Deleted() {
265 conn.err(errDeletedMsg)
266 return
267 }
268
269 if err := conn.mb.Delete(msg); err != nil {
270 conn.log.Error("failed to delete message", zap.Error(err))
271 conn.err(err.Error())
272 } else {
273 conn.log.Info("delete message", zap.String("unique-id", msg.UniqueID()))
274 conn.ok("")
275 }
276 }
277
278 func (conn *connection) doRSET() {
279 if conn.state != stateTxn {
280 conn.err(errStateTxn)
281 return
282 }
283 conn.mb.Reset()
284 conn.log.Info("reset")
285 conn.ok("")
286 }
287
288 func (conn *connection) doUIDL() {
289 if conn.state != stateTxn {
290 conn.err(errStateTxn)
291 return
292 }
293
294 msgs, err := conn.mb.ListMessages()
295 if err != nil {
296 conn.log.Error("failed to list messages", zap.Error(err))
297 conn.err(err.Error())
298 return
299 }
300
301 conn.ok("unique-id listing")
302 for _, msg := range msgs {
303 if msg.Deleted() {
304 continue
305 }
306 conn.tp.PrintfLine("%d %s", msg.ID(), msg.UniqueID())
307 }
308 conn.tp.PrintfLine(".")
309 }
310
311 func (conn *connection) doCAPA() {
312 conn.ok("capabilitiy list")
313
314 caps := []string{
315 "USER",
316 "UIDL",
317 ".",
318 }
319 for _, c := range caps {
320 conn.tp.PrintfLine(c)
321 }
322 }
323
324 func (conn *connection) getRequestedMessage() Message {
325 var cmd string
326 var idx int
327 if _, err := fmt.Sscanf(conn.line, "%s %d", &cmd, &idx); err != nil {
328 conn.err(errSyntax)
329 return nil
330 }
331
332 if idx < 1 {
333 conn.err("invalid message-number")
334 return nil
335 }
336
337 msg := conn.mb.GetMessage(idx)
338 if msg == nil {
339 conn.err("no such message")
340 return nil
341 }
342 return msg
343 }