Initial POP3 server.
authorRobert Sesek <rsesek@bluestatic.org>
Tue, 13 Dec 2016 05:27:13 +0000 (00:27 -0500)
committerRobert Sesek <rsesek@bluestatic.org>
Tue, 13 Dec 2016 05:27:13 +0000 (00:27 -0500)
config.go
mailpopbox.go
pop3.go [new file with mode: 0644]
pop3/conn.go [new file with mode: 0644]
pop3/server.go [new file with mode: 0644]

index c50104bf628b0cd9d896897acd88577b815f8226..a66c37055e0d38f5ebe7fb05b1da85c3e78e80cf 100644 (file)
--- 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
 }
index 2765ce99290e80c8534359f56b5ab8d99d0adcd5..4b05b6a6198fb0a32fa6d8c38ba54dbebefbc3e7 100644 (file)
@@ -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 (file)
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 (file)
index 0000000..c909bae
--- /dev/null
@@ -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 (file)
index 0000000..1bce28f
--- /dev/null
@@ -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)
+}