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