Add zap logging through the servers.
[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 conn := connection{
45 po: po,
46 tp: textproto.NewConn(netConn),
47 state: stateAuth,
48 log: log.With(zap.Stringer("client", netConn.RemoteAddr())),
49 }
50
51 var err error
52 conn.ok(fmt.Sprintf("POP3 (mailpopbox) server %s", po.Name()))
53
54 for {
55 conn.line, err = conn.tp.ReadLine()
56 if err != nil {
57 conn.err("did't catch that")
58 conn.log.Error("ReadLine()", zap.Error(err))
59 continue
60 }
61
62 var cmd string
63 if _, err := fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
64 conn.err("invalid command")
65 continue
66 }
67
68 switch strings.ToUpper(cmd) {
69 case "QUIT":
70 conn.doQUIT()
71 break
72 case "USER":
73 conn.doUSER()
74 case "PASS":
75 conn.doPASS()
76 case "STAT":
77 conn.doSTAT()
78 case "LIST":
79 conn.doLIST()
80 case "RETR":
81 conn.doRETR()
82 case "DELE":
83 conn.doDELE()
84 case "NOOP":
85 conn.ok("")
86 case "RSET":
87 conn.doRSET()
88 default:
89 conn.err("unknown command")
90 }
91 }
92 }
93
94 func (conn *connection) ok(msg string) {
95 if len(msg) > 0 {
96 msg = " " + msg
97 }
98 conn.tp.PrintfLine("+OK%s", msg)
99 }
100
101 func (conn *connection) err(msg string) {
102 if len(msg) > 0 {
103 msg = " " + msg
104 conn.tp.PrintfLine("-ERR%s", msg)
105 }
106 }
107
108 func (conn *connection) doQUIT() {
109 defer conn.tp.Close()
110
111 if conn.mb != nil {
112 err := conn.mb.Close()
113 if err != nil {
114 conn.err("failed to remove some messages")
115 return
116 }
117 }
118 conn.ok("goodbye")
119 }
120
121 func (conn *connection) doUSER() {
122 if conn.state != stateAuth {
123 conn.err(errStateAuth)
124 return
125 }
126
127 conn.user = conn.line[len("USER "):]
128 conn.ok("")
129 }
130
131 func (conn *connection) doPASS() {
132 if conn.state != stateAuth {
133 conn.err(errStateAuth)
134 return
135 }
136
137 if len(conn.user) == 0 {
138 conn.err("no USER")
139 return
140 }
141
142 pass := conn.line[len("PASS "):]
143 if mbox, err := conn.po.OpenMailbox(conn.user, pass); err == nil {
144 conn.log.Info("authenticated", zap.String("user", conn.user))
145 conn.state = stateTxn
146 conn.mb = mbox
147 conn.ok("")
148 } else {
149 conn.log.Error("PASS", zap.Error(err))
150 conn.err(err.Error())
151 }
152 }
153
154 func (conn *connection) doSTAT() {
155 if conn.state != stateTxn {
156 conn.err(errStateTxn)
157 return
158 }
159
160 msgs, err := conn.mb.ListMessages()
161 if err != nil {
162 conn.log.Error("STAT", zap.Error(err))
163 conn.err(err.Error())
164 return
165 }
166
167 size := 0
168 num := 0
169 for _, msg := range msgs {
170 if msg.Deleted() {
171 continue
172 }
173 size += msg.Size()
174 num++
175 }
176
177 conn.ok(fmt.Sprintf("%d %d", num, size))
178 }
179
180 func (conn *connection) doLIST() {
181 if conn.state != stateTxn {
182 conn.err(errStateTxn)
183 return
184 }
185
186 msgs, err := conn.mb.ListMessages()
187 if err != nil {
188 conn.log.Error("LIST", zap.Error(err))
189 conn.err(err.Error())
190 return
191 }
192
193 conn.ok("scan listing")
194 for _, msg := range msgs {
195 conn.tp.PrintfLine("%d %d", msg.ID(), msg.Size())
196 }
197 conn.tp.PrintfLine(".")
198 }
199
200 func (conn *connection) doRETR() {
201 if conn.state != stateTxn {
202 conn.err(errStateTxn)
203 return
204 }
205
206 msg := conn.getRequestedMessage()
207 if msg == nil {
208 return
209 }
210
211 if msg.Deleted() {
212 conn.err(errDeletedMsg)
213 return
214 }
215
216 rc, err := conn.mb.Retrieve(msg)
217 if err != nil {
218 conn.log.Error("RETR", zap.Error(err))
219 conn.err(err.Error())
220 return
221 }
222
223 conn.ok(fmt.Sprintf("%d", msg.Size()))
224
225 w := conn.tp.DotWriter()
226 io.Copy(w, rc)
227 w.Close()
228 }
229
230 func (conn *connection) doDELE() {
231 if conn.state != stateTxn {
232 conn.err(errStateTxn)
233 return
234 }
235
236 msg := conn.getRequestedMessage()
237 if msg == nil {
238 return
239 }
240
241 if msg.Deleted() {
242 conn.err(errDeletedMsg)
243 return
244 }
245
246 if err := conn.mb.Delete(msg); err != nil {
247 conn.log.Error("DELE", zap.Error(err))
248 conn.err(err.Error())
249 } else {
250 conn.ok("")
251 }
252 }
253
254 func (conn *connection) doRSET() {
255 if conn.state != stateTxn {
256 conn.err(errStateTxn)
257 return
258 }
259 conn.mb.Reset()
260 conn.ok("")
261 }
262
263 func (conn *connection) getRequestedMessage() Message {
264 var cmd string
265 var idx int
266 if _, err := fmt.Sscanf(conn.line, "%s %d", &cmd, &idx); err != nil {
267 conn.err(errSyntax)
268 return nil
269 }
270
271 if idx < 1 {
272 conn.err("invalid message-number")
273 return nil
274 }
275
276 msg := conn.mb.GetMessage(idx)
277 if msg == nil {
278 conn.err("no such message")
279 return nil
280 }
281 return msg
282 }