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