Add a test for the POP3 server.
[mailpopbox.git] / pop3 / conn.go
1 package pop3
2
3 import (
4 "fmt"
5 "io"
6 "net"
7 "net/textproto"
8 "strings"
9 )
10
11 type state int
12
13 const (
14 stateAuth state = iota
15 stateTxn
16 stateUpdate
17 )
18
19 const (
20 errStateAuth = "not in AUTHORIZATION"
21 errStateTxn = "not in TRANSACTION"
22 errSyntax = "syntax error"
23 errDeletedMsg = "no such message - deleted"
24 )
25
26 type connection struct {
27 po PostOffice
28 mb Mailbox
29
30 tp *textproto.Conn
31 remoteAddr net.Addr
32
33 state
34 line string
35
36 user string
37 }
38
39 func AcceptConnection(netConn net.Conn, po PostOffice) {
40 conn := connection{
41 po: po,
42 tp: textproto.NewConn(netConn),
43 state: stateAuth,
44 }
45
46 var err error
47 conn.ok(fmt.Sprintf("POP3 (mailpopbox) server %s", po.Name()))
48
49 for {
50 conn.line, err = conn.tp.ReadLine()
51 if err != nil {
52 conn.err("did't catch that")
53 continue
54 }
55
56 var cmd string
57 if _, err := fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
58 conn.err("invalid command")
59 continue
60 }
61
62 switch cmd {
63 case "QUIT":
64 conn.doQUIT()
65 break
66 case "USER":
67 conn.doUSER()
68 case "PASS":
69 conn.doPASS()
70 case "STAT":
71 conn.doSTAT()
72 case "LIST":
73 conn.doLIST()
74 case "RETR":
75 conn.doRETR()
76 case "DELE":
77 conn.doDELE()
78 case "NOOP":
79 conn.ok("")
80 case "RSET":
81 conn.doRSET()
82 default:
83 conn.err("unknown command")
84 }
85 }
86 }
87
88 func (conn *connection) ok(msg string) {
89 if len(msg) > 0 {
90 msg = " " + msg
91 }
92 conn.tp.PrintfLine("+OK%s", msg)
93 }
94
95 func (conn *connection) err(msg string) {
96 if len(msg) > 0 {
97 msg = " " + msg
98 conn.tp.PrintfLine("-ERR%s", msg)
99 }
100 }
101
102 func (conn *connection) doQUIT() {
103 defer conn.tp.Close()
104
105 if conn.mb != nil {
106 err := conn.mb.Close()
107 if err != nil {
108 conn.err("failed to remove some messages")
109 return
110 }
111 }
112 conn.ok("goodbye")
113 }
114
115 func (conn *connection) doUSER() {
116 if conn.state != stateAuth {
117 conn.err(errStateAuth)
118 return
119 }
120
121 if _, err := fmt.Sscanf(conn.line, "USER %s", &conn.user); err != nil {
122 conn.err(errSyntax)
123 return
124 }
125
126 conn.ok("")
127 }
128
129 func (conn *connection) doPASS() {
130 if conn.state != stateAuth {
131 conn.err(errStateAuth)
132 return
133 }
134
135 if len(conn.user) == 0 {
136 conn.err("no USER")
137 return
138 }
139
140 pass := strings.TrimPrefix(conn.line, "PASS ")
141 if mbox, err := conn.po.OpenMailbox(conn.user, pass); err == nil {
142 conn.state = stateTxn
143 conn.mb = mbox
144 conn.ok("")
145 } else {
146 conn.err(err.Error())
147 }
148 }
149
150 func (conn *connection) doSTAT() {
151 if conn.state != stateTxn {
152 conn.err(errStateTxn)
153 return
154 }
155
156 msgs, err := conn.mb.ListMessages()
157 if err != nil {
158 conn.err(err.Error())
159 return
160 }
161
162 size := 0
163 num := 0
164 for _, msg := range msgs {
165 if msg.Deleted() {
166 continue
167 }
168 size += msg.Size()
169 num++
170 }
171
172 conn.ok(fmt.Sprintf("%d %d", num, size))
173 }
174
175 func (conn *connection) doLIST() {
176 if conn.state != stateTxn {
177 conn.err(errStateTxn)
178 return
179 }
180
181 msgs, err := conn.mb.ListMessages()
182 if err != nil {
183 conn.err(err.Error())
184 return
185 }
186
187 conn.ok("scan listing")
188 for _, msg := range msgs {
189 conn.tp.PrintfLine("%d %d", msg.ID(), msg.Size())
190 }
191 conn.tp.PrintfLine(".")
192 }
193
194 func (conn *connection) doRETR() {
195 if conn.state != stateTxn {
196 conn.err(errStateTxn)
197 return
198 }
199
200 msg := conn.getRequestedMessage()
201 if msg == nil {
202 return
203 }
204
205 if msg.Deleted() {
206 conn.err(errDeletedMsg)
207 return
208 }
209
210 rc, err := conn.mb.Retrieve(msg)
211 if err != nil {
212 conn.err(err.Error())
213 return
214 }
215
216 w := conn.tp.DotWriter()
217 io.Copy(w, rc)
218 w.Close()
219 }
220
221 func (conn *connection) doDELE() {
222 if conn.state != stateTxn {
223 conn.err(errStateTxn)
224 return
225 }
226
227 msg := conn.getRequestedMessage()
228 if msg == nil {
229 return
230 }
231
232 if msg.Deleted() {
233 conn.err(errDeletedMsg)
234 return
235 }
236
237 if err := conn.mb.Delete(msg); err != nil {
238 conn.err(err.Error())
239 } else {
240 conn.ok("")
241 }
242 }
243
244 func (conn *connection) doRSET() {
245 if conn.state != stateTxn {
246 conn.err(errStateTxn)
247 return
248 }
249 conn.mb.Reset()
250 conn.ok("")
251 }
252
253 func (conn *connection) getRequestedMessage() Message {
254 var cmd string
255 var idx int
256 if _, err := fmt.Sscanf(conn.line, "%s %d", &cmd, &idx); err != nil {
257 conn.err(errSyntax)
258 return nil
259 }
260
261 if idx < 1 {
262 conn.err("invalid message-number")
263 return nil
264 }
265
266 msg := conn.mb.GetMessage(idx)
267 if msg == nil {
268 conn.err("no such message")
269 return nil
270 }
271 return msg
272 }