Implement the POP3 UIDL command.
[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 default:
96 conn.log.Error("unknown command")
97 conn.err("unknown command")
98 }
99 }
100 }
101
102 func (conn *connection) ok(msg string) {
103 if len(msg) > 0 {
104 msg = " " + msg
105 }
106 conn.tp.PrintfLine("+OK%s", msg)
107 }
108
109 func (conn *connection) err(msg string) {
110 conn.log.Error("error", zap.String("message", msg))
111 if len(msg) > 0 {
112 msg = " " + msg
113 conn.tp.PrintfLine("-ERR%s", msg)
114 }
115 }
116
117 func (conn *connection) doQUIT() {
118 defer conn.tp.Close()
119
120 if conn.mb != nil {
121 err := conn.mb.Close()
122 if err != nil {
123 conn.err("failed to remove some messages")
124 return
125 }
126 }
127 conn.ok("goodbye")
128 }
129
130 func (conn *connection) doUSER() {
131 if conn.state != stateAuth {
132 conn.err(errStateAuth)
133 return
134 }
135
136 conn.user = conn.line[len("USER "):]
137 conn.ok("")
138 }
139
140 func (conn *connection) doPASS() {
141 if conn.state != stateAuth {
142 conn.err(errStateAuth)
143 return
144 }
145
146 if len(conn.user) == 0 {
147 conn.err("no USER")
148 return
149 }
150
151 pass := conn.line[len("PASS "):]
152 if mbox, err := conn.po.OpenMailbox(conn.user, pass); err == nil {
153 conn.log.Info("authenticated", zap.String("user", conn.user))
154 conn.state = stateTxn
155 conn.mb = mbox
156 conn.ok("")
157 } else {
158 conn.log.Error("failed to open mailbox", zap.Error(err))
159 conn.err(err.Error())
160 }
161 }
162
163 func (conn *connection) doSTAT() {
164 if conn.state != stateTxn {
165 conn.err(errStateTxn)
166 return
167 }
168
169 msgs, err := conn.mb.ListMessages()
170 if err != nil {
171 conn.log.Error("failed to list messages", zap.Error(err))
172 conn.err(err.Error())
173 return
174 }
175
176 size := 0
177 num := 0
178 for _, msg := range msgs {
179 if msg.Deleted() {
180 continue
181 }
182 size += msg.Size()
183 num++
184 }
185
186 conn.ok(fmt.Sprintf("%d %d", num, size))
187 }
188
189 func (conn *connection) doLIST() {
190 if conn.state != stateTxn {
191 conn.err(errStateTxn)
192 return
193 }
194
195 msgs, err := conn.mb.ListMessages()
196 if err != nil {
197 conn.log.Error("failed to list messages", zap.Error(err))
198 conn.err(err.Error())
199 return
200 }
201
202 conn.ok("scan listing")
203 for _, msg := range msgs {
204 conn.tp.PrintfLine("%d %d", msg.ID(), msg.Size())
205 }
206 conn.tp.PrintfLine(".")
207 }
208
209 func (conn *connection) doRETR() {
210 if conn.state != stateTxn {
211 conn.err(errStateTxn)
212 return
213 }
214
215 msg := conn.getRequestedMessage()
216 if msg == nil {
217 return
218 }
219
220 if msg.Deleted() {
221 conn.err(errDeletedMsg)
222 return
223 }
224
225 rc, err := conn.mb.Retrieve(msg)
226 if err != nil {
227 conn.log.Error("failed to retrieve messages", zap.Error(err))
228 conn.err(err.Error())
229 return
230 }
231
232 conn.ok(fmt.Sprintf("%d", msg.Size()))
233
234 w := conn.tp.DotWriter()
235 io.Copy(w, rc)
236 w.Close()
237 }
238
239 func (conn *connection) doDELE() {
240 if conn.state != stateTxn {
241 conn.err(errStateTxn)
242 return
243 }
244
245 msg := conn.getRequestedMessage()
246 if msg == nil {
247 return
248 }
249
250 if msg.Deleted() {
251 conn.err(errDeletedMsg)
252 return
253 }
254
255 if err := conn.mb.Delete(msg); err != nil {
256 conn.log.Error("failed to delete message", zap.Error(err))
257 conn.err(err.Error())
258 } else {
259 conn.ok("")
260 }
261 }
262
263 func (conn *connection) doRSET() {
264 if conn.state != stateTxn {
265 conn.err(errStateTxn)
266 return
267 }
268 conn.mb.Reset()
269 conn.ok("")
270 }
271
272 func (conn *connection) doUIDL() {
273 if conn.state != stateTxn {
274 conn.err(errStateTxn)
275 return
276 }
277
278 msgs, err := conn.mb.ListMessages()
279 if err != nil {
280 conn.log.Error("failed to list messages", zap.Error(err))
281 conn.err(err.Error())
282 return
283 }
284
285 conn.ok("unique-id listing")
286 for _, msg := range msgs {
287 if msg.Deleted() {
288 continue
289 }
290 conn.tp.PrintfLine("%d %s", msg.ID(), msg.UniqueID())
291 }
292 conn.tp.PrintfLine(".")
293 }
294
295 func (conn *connection) getRequestedMessage() Message {
296 var cmd string
297 var idx int
298 if _, err := fmt.Sscanf(conn.line, "%s %d", &cmd, &idx); err != nil {
299 conn.err(errSyntax)
300 return nil
301 }
302
303 if idx < 1 {
304 conn.err("invalid message-number")
305 return nil
306 }
307
308 msg := conn.mb.GetMessage(idx)
309 if msg == nil {
310 conn.err("no such message")
311 return nil
312 }
313 return msg
314 }