2 // Copyright 2020 Blue Static <https://www.bluestatic.org>
3 // This program is free software licensed under the GNU General Public License,
4 // version 3.0. The full text of the license can be found in LICENSE.txt.
5 // SPDX-License-Identifier: GPL-3.0-only
22 stateAuth state = iota
28 errStateAuth = "not in AUTHORIZATION"
29 errStateTxn = "not in TRANSACTION"
30 errSyntax = "syntax error"
31 errDeletedMsg = "no such message - deleted"
34 type connection struct {
49 func AcceptConnection(netConn net.Conn, po PostOffice, log *zap.Logger) {
50 log = log.With(zap.Stringer("client", netConn.RemoteAddr()))
53 tp: textproto.NewConn(netConn),
58 conn.log.Info("accepted connection")
59 conn.ok(fmt.Sprintf("POP3 (mailpopbox) server %s", po.Name()))
64 conn.line, err = conn.tp.ReadLine()
66 conn.log.Error("ReadLine()", zap.Error(err))
72 if _, err := fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
73 conn.err("invalid command")
77 conn.log = log.With(zap.String("command", cmd))
79 switch strings.ToUpper(cmd) {
104 conn.err("unknown command")
109 func (conn *connection) ok(msg string) {
110 conn.log.Info("ok", zap.String("reply", msg))
114 conn.tp.PrintfLine("+OK%s", msg)
117 func (conn *connection) err(msg string) {
118 conn.log.Error("error", zap.String("message", msg))
121 conn.tp.PrintfLine("-ERR%s", msg)
125 func (conn *connection) doQUIT() {
126 defer conn.tp.Close()
129 err := conn.mb.Close()
131 conn.err("failed to remove some messages")
138 func (conn *connection) doUSER() {
139 if conn.state != stateAuth {
140 conn.err(errStateAuth)
145 if len(conn.line) < cmd {
146 conn.err("invalid user")
150 conn.user = conn.line[cmd:]
154 func (conn *connection) doPASS() {
155 if conn.state != stateAuth {
156 conn.err(errStateAuth)
160 if len(conn.user) == 0 {
166 if len(conn.line) < cmd {
167 conn.err("invalid pass")
171 pass := conn.line[cmd:]
172 if mbox, err := conn.po.OpenMailbox(conn.user, pass); err == nil {
173 conn.log.Info("authenticated", zap.String("user", conn.user))
174 conn.state = stateTxn
178 conn.log.Error("failed to open mailbox", zap.Error(err))
179 conn.err(err.Error())
183 func (conn *connection) doSTAT() {
184 if conn.state != stateTxn {
185 conn.err(errStateTxn)
189 msgs, err := conn.mb.ListMessages()
191 conn.log.Error("failed to list messages", zap.Error(err))
192 conn.err(err.Error())
198 for _, msg := range msgs {
206 conn.ok(fmt.Sprintf("%d %d", num, size))
209 func (conn *connection) doLIST() {
210 if conn.state != stateTxn {
211 conn.err(errStateTxn)
215 msgs, err := conn.mb.ListMessages()
217 conn.log.Error("failed to list messages", zap.Error(err))
218 conn.err(err.Error())
222 conn.ok("scan listing")
223 for _, msg := range msgs {
224 conn.tp.PrintfLine("%d %d", msg.ID(), msg.Size())
226 conn.tp.PrintfLine(".")
229 func (conn *connection) doRETR() {
230 if conn.state != stateTxn {
231 conn.err(errStateTxn)
235 msg := conn.getRequestedMessage()
241 conn.err(errDeletedMsg)
245 rc, err := conn.mb.Retrieve(msg)
247 conn.log.Error("failed to retrieve messages", zap.Error(err))
248 conn.err(err.Error())
252 conn.log.Info("retrieve message", zap.String("unique-id", msg.UniqueID()))
253 conn.ok(fmt.Sprintf("%d", msg.Size()))
255 w := conn.tp.DotWriter()
260 func (conn *connection) doDELE() {
261 if conn.state != stateTxn {
262 conn.err(errStateTxn)
266 msg := conn.getRequestedMessage()
272 conn.err(errDeletedMsg)
276 if err := conn.mb.Delete(msg); err != nil {
277 conn.log.Error("failed to delete message", zap.Error(err))
278 conn.err(err.Error())
280 conn.log.Info("delete message", zap.String("unique-id", msg.UniqueID()))
285 func (conn *connection) doRSET() {
286 if conn.state != stateTxn {
287 conn.err(errStateTxn)
291 conn.log.Info("reset")
295 func (conn *connection) doUIDL() {
296 if conn.state != stateTxn {
297 conn.err(errStateTxn)
301 msgs, err := conn.mb.ListMessages()
303 conn.log.Error("failed to list messages", zap.Error(err))
304 conn.err(err.Error())
308 conn.ok("unique-id listing")
309 for _, msg := range msgs {
313 conn.tp.PrintfLine("%d %s", msg.ID(), msg.UniqueID())
315 conn.tp.PrintfLine(".")
318 func (conn *connection) doCAPA() {
319 conn.ok("capability list")
326 for _, c := range caps {
327 conn.tp.PrintfLine(c)
331 func (conn *connection) getRequestedMessage() Message {
334 if _, err := fmt.Sscanf(conn.line, "%s %d", &cmd, &idx); err != nil {
340 conn.err("invalid message-number")
344 msg := conn.mb.GetMessage(idx)
346 conn.err("no such message")