Bump the version to 2.1.0.
[mailpopbox.git] / pop3.go
diff --git a/pop3.go b/pop3.go
index ad4d1a4739fcd563e02702acf4648cf508e80bef..a03c1b81c181a0965e11c00780ed9294392ac52d 100644 (file)
--- a/pop3.go
+++ b/pop3.go
@@ -1,6 +1,13 @@
+// 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"
@@ -9,45 +16,86 @@ import (
        "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 {
@@ -56,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)
                }
        }
@@ -66,8 +114,8 @@ 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 {
-               // TODO: hide error, log instead
-               return nil, err
+               server.log.Error("failed read maildrop dir", zap.String("dir", maildrop), zap.Error(err))
+               return nil, errors.New("error opening maildrop")
        }
 
        mb := &mailbox{
@@ -103,6 +151,11 @@ type message struct {
        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
 }
@@ -123,20 +176,20 @@ func (mb *mailbox) ListMessages() ([]pop3.Message, error) {
        return msgs, nil
 }
 
-func (mb *mailbox) Retrieve(idx int) (io.ReadCloser, error) {
-       if idx > len(mb.messages) {
-               return nil, errors.New("no such message")
+func (mb *mailbox) GetMessage(id int) pop3.Message {
+       if id == 0 || id > len(mb.messages) {
+               return nil
        }
-       filename := mb.messages[idx-1].filename
+       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(idx int) error {
-       message := &mb.messages[idx-1]
-       if message.deleted {
-               return errors.New("already deleted")
-       }
-       message.deleted = true
+func (mb *mailbox) Delete(msg pop3.Message) error {
+       msg.(*message).deleted = true
        return nil
 }
 
@@ -150,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
        }
 }