Bump the version to 2.1.0.
[mailpopbox.git] / pop3.go
diff --git a/pop3.go b/pop3.go
index 84e075b7c26a9d954c3ed0f6a3fd6bb2049db2d2..a03c1b81c181a0965e11c00780ed9294392ac52d 100644 (file)
--- a/pop3.go
+++ b/pop3.go
+// mailpopbox
+// Copyright 2020 Blue Static <https://www.bluestatic.org>
+// This program is free software licensed under the GNU General Public License,
+// version 3.0. The full text of the license can be found in LICENSE.txt.
+// SPDX-License-Identifier: GPL-3.0-only
+
 package main
 
 import (
+       "crypto/tls"
        "errors"
        "fmt"
        "io"
+       "io/ioutil"
        "net"
        "os"
+       "path"
+
+       "go.uber.org/zap"
 
        "src.bluestatic.org/mailpopbox/pop3"
 )
 
-func runPOP3Server(config Config) <-chan error {
+func runPOP3Server(config Config, log *zap.Logger) <-chan ServerControlMessage {
        server := pop3Server{
-               config: config,
-               rc:     make(chan error),
+               config:      config,
+               controlChan: make(chan ServerControlMessage),
+               log:         log.With(zap.String("server", "pop3")),
        }
        go server.run()
-       return server.rc
+       return server.controlChan
 }
 
 type pop3Server struct {
-       config Config
-       rc     chan error
+       config      Config
+       controlChan chan ServerControlMessage
+       log         *zap.Logger
 }
 
 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
+                       server.log.Error("failed to open maildrop", zap.Error(err))
+                       server.controlChan <- ServerControlFatalError
                }
        }
 
-       l, err := net.Listen("tcp", fmt.Sprintf(":%d", server.config.POP3Port))
+       l, err := server.newListener()
        if err != nil {
-               server.rc <- err
+               server.controlChan <- ServerControlFatalError
                return
        }
 
+       connChan := make(chan net.Conn)
+       go RunAcceptLoop(l, connChan, server.log)
+
+       reloadChan := CreateReloadSignal()
+
        for {
-               conn, err := l.Accept()
-               if err != nil {
-                       server.rc <- err
+               select {
+               case <-reloadChan:
+                       server.log.Info("restarting server")
+                       l.Close()
+                       server.controlChan <- ServerControlRestart
                        break
+               case conn, ok := <-connChan:
+                       if ok {
+                               go pop3.AcceptConnection(conn, server, server.log)
+                       } else {
+                               server.controlChan <- ServerControlFatalError
+                               break
+                       }
                }
+       }
+}
+
+func (server *pop3Server) newListener() (net.Listener, error) {
+       tlsConfig, err := server.config.GetTLSConfig()
+       if err != nil {
+               server.log.Error("failed to configure TLS", zap.Error(err))
+               return nil, err
+       }
+
+       addr := fmt.Sprintf(":%d", server.config.POP3Port)
+       server.log.Info("starting server", zap.String("address", addr))
 
-               go pop3.AcceptConnection(conn, server)
+       var l net.Listener
+       if tlsConfig == nil {
+               l, err = net.Listen("tcp", addr)
+       } else {
+               l, err = tls.Listen("tcp", addr, tlsConfig)
+       }
+       if err != nil {
+               server.log.Error("listen", zap.Error(err))
+               return nil, err
        }
+
+       return l, nil
 }
 
 func (server *pop3Server) Name() string {
@@ -54,7 +104,7 @@ func (server *pop3Server) Name() string {
 
 func (server *pop3Server) OpenMailbox(user, pass string) (pop3.Mailbox, error) {
        for _, s := range server.config.Servers {
-               if user == "mailbox@"+s.Domain && pass == s.MailboxPassword {
+               if user == MailboxAccount+s.Domain && pass == s.MailboxPassword {
                        return server.openMailbox(s.MaildropPath)
                }
        }
@@ -62,9 +112,31 @@ func (server *pop3Server) OpenMailbox(user, pass string) (pop3.Mailbox, error) {
 }
 
 func (server *pop3Server) openMailbox(maildrop string) (*mailbox, error) {
+       files, err := ioutil.ReadDir(maildrop)
+       if err != nil {
+               server.log.Error("failed read maildrop dir", zap.String("dir", maildrop), zap.Error(err))
+               return nil, errors.New("error opening maildrop")
+       }
+
        mb := &mailbox{
-               messages: make([]message, 0),
+               messages: make([]message, 0, len(files)),
        }
+
+       i := 0
+       for _, file := range files {
+               if file.IsDir() {
+                       continue
+               }
+
+               msg := message{
+                       filename: path.Join(maildrop, file.Name()),
+                       index:    i,
+                       size:     file.Size(),
+               }
+               mb.messages = append(mb.messages, msg)
+               i++
+       }
+
        return mb, nil
 }
 
@@ -75,16 +147,25 @@ type mailbox struct {
 type message struct {
        filename string
        index    int
-       size     int
+       size     int64
        deleted  bool
 }
 
+func (m message) UniqueID() string {
+       l := len(m.filename)
+       return path.Base(m.filename[:l-len(".msg")])
+}
+
 func (m message) ID() int {
        return m.index + 1
 }
 
 func (m message) Size() int {
-       return m.size
+       return int(m.size)
+}
+
+func (m message) Deleted() bool {
+       return m.deleted
 }
 
 func (mb *mailbox) ListMessages() ([]pop3.Message, error) {
@@ -95,17 +176,20 @@ func (mb *mailbox) ListMessages() ([]pop3.Message, error) {
        return msgs, nil
 }
 
+func (mb *mailbox) GetMessage(id int) pop3.Message {
+       if id == 0 || id > len(mb.messages) {
+               return nil
+       }
+       return &mb.messages[id-1]
+}
+
 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
+       msg.(*message).deleted = true
        return nil
 }
 
@@ -119,7 +203,7 @@ func (mb *mailbox) Close() error {
 }
 
 func (mb *mailbox) Reset() {
-       for _, message := range mb.messages {
-               message.deleted = false
+       for i, _ := range mb.messages {
+               mb.messages[i].deleted = false
        }
 }