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