Initial POP3 server.
[mailpopbox.git] / pop3 / conn.go
1 package pop3
2
3 import (
4 "fmt"
5 "net"
6 "net/textproto"
7 "strings"
8 )
9
10 type state int
11
12 const (
13 stateAuth state = iota
14 stateTxn
15 stateUpdate
16 )
17
18 const (
19 errStateAuth = "not in AUTHORIZATION"
20 errStateTxn = "not in TRANSACTION"
21 errSyntax = "syntax error"
22 )
23
24 type connection struct {
25 po PostOffice
26 mb Mailbox
27
28 tp *textproto.Conn
29 remoteAddr net.Addr
30
31 state
32 line string
33
34 user string
35 }
36
37 func AcceptConnection(netConn net.Conn, po PostOffice) {
38 conn := connection{
39 po: po,
40 tp: textproto.NewConn(netConn),
41 state: stateAuth,
42 }
43
44 var err error
45 conn.ok(fmt.Sprintf("POP3 (mailpopbox) server %s", po.Name()))
46
47 for {
48 conn.line, err = conn.tp.ReadLine()
49 if err != nil {
50 conn.err("did't catch that")
51 continue
52 }
53
54 var cmd string
55 if _, err := fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
56 conn.err("invalid command")
57 continue
58 }
59
60 switch cmd {
61 case "QUIT":
62 conn.doQUIT()
63 break
64 case "USER":
65 conn.doUSER()
66 case "PASS":
67 conn.doPASS()
68 case "STAT":
69 conn.doSTAT()
70 case "LIST":
71 conn.doLIST()
72 case "RETR":
73 conn.doRETR()
74 case "DELE":
75 conn.doDELE()
76 case "NOOP":
77 conn.ok("")
78 case "RSET":
79 conn.doRSET()
80 default:
81 conn.err("unknown command")
82 }
83 }
84 }
85
86 func (conn *connection) ok(msg string) {
87 if len(msg) > 0 {
88 msg = " " + msg
89 }
90 conn.tp.PrintfLine("+OK%s", msg)
91 }
92
93 func (conn *connection) err(msg string) {
94 if len(msg) > 0 {
95 msg = " " + msg
96 conn.tp.PrintfLine("-ERR%s", msg)
97 }
98 }
99
100 func (conn *connection) doQUIT() {
101 defer conn.tp.Close()
102
103 if conn.mb != nil {
104 err := conn.mb.Close()
105 if err != nil {
106 conn.err("failed to remove some messages")
107 return
108 }
109 }
110 conn.ok("goodbye")
111 }
112
113 func (conn *connection) doUSER() {
114 if conn.state != stateAuth {
115 conn.err(errStateAuth)
116 return
117 }
118
119 if _, err := fmt.Sscanf(conn.line, "USER %s", &conn.user); err != nil {
120 conn.err(errSyntax)
121 return
122 }
123
124 conn.ok("")
125 }
126
127 func (conn *connection) doPASS() {
128 if conn.state != stateAuth {
129 conn.err(errStateAuth)
130 return
131 }
132
133 if len(conn.user) == 0 {
134 conn.err("no USER")
135 return
136 }
137
138 pass := strings.TrimPrefix(conn.line, "PASS ")
139 if mbox, err := conn.po.OpenMailbox(conn.user, pass); err == nil {
140 conn.state = stateTxn
141 conn.mb = mbox
142 conn.ok("")
143 } else {
144 conn.err(err.Error())
145 }
146 }
147
148 func (conn *connection) doSTAT() {
149 if conn.state != stateTxn {
150 conn.err(errStateTxn)
151 return
152 }
153 }
154
155 func (conn *connection) doLIST() {
156 if conn.state != stateTxn {
157 conn.err(errStateTxn)
158 return
159 }
160 }
161
162 func (conn *connection) doRETR() {
163 if conn.state != stateTxn {
164 conn.err(errStateTxn)
165 return
166 }
167 }
168
169 func (conn *connection) doDELE() {
170 if conn.state != stateTxn {
171 conn.err(errStateTxn)
172 return
173 }
174 }
175
176 func (conn *connection) doRSET() {
177 if conn.state != stateTxn {
178 conn.err(errStateTxn)
179 return
180 }
181 conn.mb.Reset()
182 conn.ok("")
183 }