From ea41521766f023e6c20c62e78a894c40e602115e Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Tue, 13 Dec 2016 00:27:13 -0500 Subject: [PATCH] Initial POP3 server. --- config.go | 3 + mailpopbox.go | 10 ++- pop3.go | 125 +++++++++++++++++++++++++++++++++ pop3/conn.go | 183 +++++++++++++++++++++++++++++++++++++++++++++++++ pop3/server.go | 23 +++++++ 5 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 pop3.go create mode 100644 pop3/conn.go create mode 100644 pop3/server.go diff --git a/config.go b/config.go index c50104b..a66c370 100644 --- a/config.go +++ b/config.go @@ -20,6 +20,9 @@ type Server struct { // Password for the POP3 mailbox user, mailbox@domain.com. MailboxPassword string + // Location to store the mail messages. + MaildropPath string + // Blacklisted addresses that should not accept mail. BlacklistedAddresses []string } diff --git a/mailpopbox.go b/mailpopbox.go index 2765ce9..4b05b6a 100644 --- a/mailpopbox.go +++ b/mailpopbox.go @@ -25,7 +25,13 @@ func main() { } configFile.Close() + pop3 := runPOP3Server(config) smtp := runSMTPServer(config) - err = <-smtp - fmt.Println(err) + + select { + case err := <-pop3: + fmt.Println(err) + case err := <-smtp: + fmt.Println(err) + } } diff --git a/pop3.go b/pop3.go new file mode 100644 index 0000000..84e075b --- /dev/null +++ b/pop3.go @@ -0,0 +1,125 @@ +package main + +import ( + "errors" + "fmt" + "io" + "net" + "os" + + "src.bluestatic.org/mailpopbox/pop3" +) + +func runPOP3Server(config Config) <-chan error { + server := pop3Server{ + config: config, + rc: make(chan error), + } + go server.run() + return server.rc +} + +type pop3Server struct { + config Config + rc chan error +} + +func (server *pop3Server) run() { + for _, s := range server.config.Servers { + if err := os.Mkdir(s.MaildropPath, 0700); err != nil && !os.IsExist(err) { + server.rc <- err + } + } + + l, err := net.Listen("tcp", fmt.Sprintf(":%d", server.config.POP3Port)) + if err != nil { + server.rc <- err + return + } + + for { + conn, err := l.Accept() + if err != nil { + server.rc <- err + break + } + + go pop3.AcceptConnection(conn, server) + } +} + +func (server *pop3Server) Name() string { + return server.config.Hostname +} + +func (server *pop3Server) OpenMailbox(user, pass string) (pop3.Mailbox, error) { + for _, s := range server.config.Servers { + if user == "mailbox@"+s.Domain && pass == s.MailboxPassword { + return server.openMailbox(s.MaildropPath) + } + } + return nil, errors.New("permission denied") +} + +func (server *pop3Server) openMailbox(maildrop string) (*mailbox, error) { + mb := &mailbox{ + messages: make([]message, 0), + } + return mb, nil +} + +type mailbox struct { + messages []message +} + +type message struct { + filename string + index int + size int + deleted bool +} + +func (m message) ID() int { + return m.index + 1 +} + +func (m message) Size() int { + return m.size +} + +func (mb *mailbox) ListMessages() ([]pop3.Message, error) { + msgs := make([]pop3.Message, len(mb.messages)) + for i := 0; i < len(mb.messages); i++ { + msgs[i] = &mb.messages[i] + } + return msgs, nil +} + +func (mb *mailbox) Retrieve(msg pop3.Message) (io.ReadCloser, error) { + filename := msg.(*message).filename + return os.Open(filename) +} + +func (mb *mailbox) Delete(msg pop3.Message) error { + message := msg.(*message) + if message.deleted { + return errors.New("already deleted") + } + message.deleted = true + return nil +} + +func (mb *mailbox) Close() error { + for _, message := range mb.messages { + if message.deleted { + os.Remove(message.filename) + } + } + return nil +} + +func (mb *mailbox) Reset() { + for _, message := range mb.messages { + message.deleted = false + } +} diff --git a/pop3/conn.go b/pop3/conn.go new file mode 100644 index 0000000..c909bae --- /dev/null +++ b/pop3/conn.go @@ -0,0 +1,183 @@ +package pop3 + +import ( + "fmt" + "net" + "net/textproto" + "strings" +) + +type state int + +const ( + stateAuth state = iota + stateTxn + stateUpdate +) + +const ( + errStateAuth = "not in AUTHORIZATION" + errStateTxn = "not in TRANSACTION" + errSyntax = "syntax error" +) + +type connection struct { + po PostOffice + mb Mailbox + + tp *textproto.Conn + remoteAddr net.Addr + + state + line string + + user string +} + +func AcceptConnection(netConn net.Conn, po PostOffice) { + conn := connection{ + po: po, + tp: textproto.NewConn(netConn), + state: stateAuth, + } + + var err error + conn.ok(fmt.Sprintf("POP3 (mailpopbox) server %s", po.Name())) + + for { + conn.line, err = conn.tp.ReadLine() + if err != nil { + conn.err("did't catch that") + continue + } + + var cmd string + if _, err := fmt.Sscanf(conn.line, "%s", &cmd); err != nil { + conn.err("invalid command") + continue + } + + switch cmd { + case "QUIT": + conn.doQUIT() + break + case "USER": + conn.doUSER() + case "PASS": + conn.doPASS() + case "STAT": + conn.doSTAT() + case "LIST": + conn.doLIST() + case "RETR": + conn.doRETR() + case "DELE": + conn.doDELE() + case "NOOP": + conn.ok("") + case "RSET": + conn.doRSET() + default: + conn.err("unknown command") + } + } +} + +func (conn *connection) ok(msg string) { + if len(msg) > 0 { + msg = " " + msg + } + conn.tp.PrintfLine("+OK%s", msg) +} + +func (conn *connection) err(msg string) { + if len(msg) > 0 { + msg = " " + msg + conn.tp.PrintfLine("-ERR%s", msg) + } +} + +func (conn *connection) doQUIT() { + defer conn.tp.Close() + + if conn.mb != nil { + err := conn.mb.Close() + if err != nil { + conn.err("failed to remove some messages") + return + } + } + conn.ok("goodbye") +} + +func (conn *connection) doUSER() { + if conn.state != stateAuth { + conn.err(errStateAuth) + return + } + + if _, err := fmt.Sscanf(conn.line, "USER %s", &conn.user); err != nil { + conn.err(errSyntax) + return + } + + conn.ok("") +} + +func (conn *connection) doPASS() { + if conn.state != stateAuth { + conn.err(errStateAuth) + return + } + + if len(conn.user) == 0 { + conn.err("no USER") + return + } + + pass := strings.TrimPrefix(conn.line, "PASS ") + if mbox, err := conn.po.OpenMailbox(conn.user, pass); err == nil { + conn.state = stateTxn + conn.mb = mbox + conn.ok("") + } else { + conn.err(err.Error()) + } +} + +func (conn *connection) doSTAT() { + if conn.state != stateTxn { + conn.err(errStateTxn) + return + } +} + +func (conn *connection) doLIST() { + if conn.state != stateTxn { + conn.err(errStateTxn) + return + } +} + +func (conn *connection) doRETR() { + if conn.state != stateTxn { + conn.err(errStateTxn) + return + } +} + +func (conn *connection) doDELE() { + if conn.state != stateTxn { + conn.err(errStateTxn) + return + } +} + +func (conn *connection) doRSET() { + if conn.state != stateTxn { + conn.err(errStateTxn) + return + } + conn.mb.Reset() + conn.ok("") +} diff --git a/pop3/server.go b/pop3/server.go new file mode 100644 index 0000000..1bce28f --- /dev/null +++ b/pop3/server.go @@ -0,0 +1,23 @@ +package pop3 + +import ( + "io" +) + +type Message interface { + ID() int + Size() int +} + +type Mailbox interface { + ListMessages() ([]Message, error) + Retrieve(Message) (io.ReadCloser, error) + Delete(Message) error + Close() error + Reset() +} + +type PostOffice interface { + Name() string + OpenMailbox(user, pass string) (Mailbox, error) +} -- 2.22.5