// 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
}
}
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)
+ }
}
--- /dev/null
+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
+ }
+}
--- /dev/null
+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("")
+}
--- /dev/null
+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)
+}