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