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