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