From 69d03212bb81c5244509754c7fa7ea7b3e5d6694 Mon Sep 17 00:00:00 2001 From: K Date: Sat, 1 Mar 2025 20:12:48 -0500 Subject: [PATCH 1/4] WIP: POP3 support --- go.mod | 3 + internal/endpoint/pop3/pop3.go | 466 +++++++++++++++++++++++++++++++++ maddy.go | 1 + 3 files changed, 470 insertions(+) create mode 100644 internal/endpoint/pop3/pop3.go diff --git a/go.mod b/go.mod index 668bf27b..8b83abc4 100644 --- a/go.mod +++ b/go.mod @@ -107,6 +107,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/kiwiz/popgun v0.0.0 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/magiconair/properties v1.8.9 // indirect @@ -171,3 +172,5 @@ replace github.com/emersion/go-imap => github.com/foxcpp/go-imap v1.0.0-beta.1.0 replace github.com/emersion/go-smtp => github.com/foxcpp/go-smtp v1.21.4-0.20250124171104-c8519ae4fb23 // v1.21.3+maddy.1 replace github.com/libdns/gandi => github.com/foxcpp/libdns-gandi v1.0.4-0.20240127130558-4782f9d5ce3e // v1.0.3+maddy.1 + +replace github.com/kiwiz/popgun v0.0.0 => ../popgun diff --git a/internal/endpoint/pop3/pop3.go b/internal/endpoint/pop3/pop3.go new file mode 100644 index 00000000..16e398ab --- /dev/null +++ b/internal/endpoint/pop3/pop3.go @@ -0,0 +1,466 @@ +/* +Maddy Mail Server - Composable all-in-one email server. +Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package pop3 + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "strings" + "sync" + "strconv" + + "github.com/kiwiz/popgun" + pop3backend "github.com/kiwiz/popgun/backends" + "github.com/emersion/go-imap" + sortthread "github.com/emersion/go-imap-sortthread" + imapbackend "github.com/emersion/go-imap/backend" + _ "github.com/emersion/go-message/charset" + i18nlevel "github.com/foxcpp/go-imap-i18nlevel" + "github.com/foxcpp/go-imap-mess" + "github.com/foxcpp/maddy/framework/config" + modconfig "github.com/foxcpp/maddy/framework/config/module" + tls2 "github.com/foxcpp/maddy/framework/config/tls" + "github.com/foxcpp/maddy/framework/log" + "github.com/foxcpp/maddy/framework/module" + "github.com/foxcpp/maddy/internal/auth" + "github.com/foxcpp/maddy/internal/authz" + "github.com/foxcpp/maddy/internal/proxy_protocol" + "github.com/foxcpp/maddy/internal/updatepipe" +) + +type Endpoint struct { + addrs []string + serv *popgun.Server + listeners []net.Listener + proxyProtocol *proxy_protocol.ProxyProtocol + Store module.Storage + + tlsConfig *tls.Config + listenersWg sync.WaitGroup + + saslAuth auth.SASLAuth + + storageNormalize authz.NormalizeFunc + storageMap module.Table + + Log log.Logger +} + +func New(modName string, addrs []string) (module.Module, error) { + endp := &Endpoint{ + addrs: addrs, + Log: log.Logger{Name: modName}, + saslAuth: auth.SASLAuth{ + Log: log.Logger{Name: modName + "/sasl"}, + }, + } + + return endp, nil +} + +func (endp *Endpoint) Init(cfg *config.Map) error { + var ( + insecureAuth bool + debug bool + errors bool + ) + + cfg.Callback("auth", func(m *config.Map, node config.Node) error { + return endp.saslAuth.AddProvider(m, node) + }) + cfg.Bool("sasl_login", false, false, &endp.saslAuth.EnableLogin) + cfg.Custom("storage", false, true, nil, modconfig.StorageDirective, &endp.Store) + cfg.Custom("tls", true, true, nil, tls2.TLSDirective, &endp.tlsConfig) + cfg.Custom("proxy_protocol", false, false, nil, proxy_protocol.ProxyProtocolDirective, &endp.proxyProtocol) + cfg.Bool("insecure_auth", false, false, &insecureAuth) + cfg.Bool("errors", false, false, &errors) + cfg.Bool("debug", true, false, &debug) + config.EnumMapped(cfg, "storage_map_normalize", false, false, authz.NormalizeFuncs, authz.NormalizeAuto, + &endp.storageNormalize) + modconfig.Table(cfg, "storage_map", false, false, nil, &endp.storageMap) + config.EnumMapped(cfg, "auth_map_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto, + &endp.saslAuth.AuthNormalize) + modconfig.Table(cfg, "auth_map", true, false, nil, &endp.saslAuth.AuthMap) + if _, err := cfg.Process(); err != nil { + return err + } + + if updBe, ok := endp.Store.(updatepipe.Backend); ok { + if err := updBe.EnableUpdatePipe(updatepipe.ModeReplicate); err != nil { + endp.Log.Error("failed to initialize updates pipe", err) + } + } + + endp.saslAuth.Log.Debug = endp.Log.Debug + + addresses := make([]config.Endpoint, 0, len(endp.addrs)) + for _, addr := range endp.addrs { + saddr, err := config.ParseEndpoint(addr) + if err != nil { + return fmt.Errorf("pop3: invalid address: %s", addr) + } + addresses = append(addresses, saddr) + } + + endp.serv = popgun.NewServer(endp, endp) + if errors { + endp.serv.ErrorLog = &endp.Log + } + if debug { + endp.serv.DebugLog = &endp.Log + } + endp.serv.AllowInsecureAuth = insecureAuth + + return endp.setupListeners(addresses) +} + +func (endp *Endpoint) setupListeners(addresses []config.Endpoint) error { + for _, addr := range addresses { + var l net.Listener + var err error + l, err = net.Listen(addr.Network(), addr.Address()) + if err != nil { + return fmt.Errorf("pop3: %v", err) + } + endp.Log.Printf("listening on %v", addr) + + if addr.IsTLS() { + if endp.tlsConfig == nil { + return errors.New("pop3: can't bind on POPS endpoint without TLS configuration") + } + l = tls.NewListener(l, endp.tlsConfig) + } + + if endp.proxyProtocol != nil { + l = proxy_protocol.NewListener(l, endp.proxyProtocol, endp.Log) + } + + endp.listeners = append(endp.listeners, l) + + endp.listenersWg.Add(1) + go func() { + if err := endp.serv.Serve(l); err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") { + endp.Log.Printf("pop3: failed to serve %s: %s", addr, err) + } + endp.listenersWg.Done() + }() + } + + if endp.serv.AllowInsecureAuth { + endp.Log.Println("authentication over unencrypted connections is allowed, this is insecure configuration and should be used only for testing!") + } + + return nil +} + +func (endp *Endpoint) Name() string { + return "pop3" +} + +func (endp *Endpoint) InstanceName() string { + return "pop3" +} + +func (endp *Endpoint) Close() error { + for _, l := range endp.listeners { + l.Close() + } + endp.listenersWg.Wait() + return nil +} + +func (endp *Endpoint) getMailbox(user pop3backend.User) (imapbackend.Mailbox, error) { + backendUser, ok := user.(imapbackend.User) + if !ok { + return nil, fmt.Errorf("internal server error") + } + _, mailbox, err := backendUser.GetMailbox(imap.InboxName, true, nil) + if err != nil { + return nil, fmt.Errorf("unable to get maildrop") + } + return mailbox, nil +} + +func (endp *Endpoint) usernameForStorage(ctx context.Context, saslUsername string) (string, error) { + saslUsername, err := endp.storageNormalize(saslUsername) + if err != nil { + return "", err + } + + if endp.storageMap == nil { + return saslUsername, nil + } + + mapped, ok, err := endp.storageMap.Lookup(ctx, saslUsername) + if err != nil { + return "", err + } + if !ok { + return "", imapbackend.ErrInvalidCredentials + } + + if saslUsername != mapped { + endp.Log.DebugMsg("using mapped username for storage", "username", saslUsername, "mapped_username", mapped) + } + + return mapped, nil +} + +// interface implementation for popgun.Authorizator +func (endp *Endpoint) Authorize(conn net.Conn, user, pass string) (pop3backend.User, error) { + // saslAuth handles AuthMap calling. + err := endp.saslAuth.AuthPlain(user, pass) + if err != nil { + endp.Log.Error("authentication failed", err, "username", user, "src_ip", conn.RemoteAddr()) + return nil, imapbackend.ErrInvalidCredentials + } + + storageUsername, err := endp.usernameForStorage(context.TODO(), user) + if err != nil { + if errors.Is(err, imapbackend.ErrInvalidCredentials) { + return nil, err + } + endp.Log.Error("authentication failed due to an internal error", err, "username", user, "src_ip", conn.RemoteAddr()) + return nil, fmt.Errorf("internal server error") + } + + return endp.Store.GetOrCreateIMAPAcct(storageUsername) +} + +// interface implementation for popgun.Backend +func (endp *Endpoint) Stat(user pop3backend.User) (messages, octets int, err error) { + mailbox, err := endp.getMailbox(user) + if err != nil { + return 0, 0, err + } + + c := make(chan *imap.Message) + err = mailbox.ListMessages(false, nil, []imap.FetchItem{imap.FetchRFC822Size}, c) + if err != nil && err != mess.ErrNoMessages { + return 0, 0, err + } + + count := 0 + size := 0 + for msg := range(c) { + count += 1 + size += int(msg.Size) + } + + return count, size, nil +} + +// List of sizes of all messages in bytes (octets) +func (endp *Endpoint) List(user pop3backend.User) (octets []int, err error) { + mailbox, err := endp.getMailbox(user) + if err != nil { + return nil, err + } + + c := make(chan *imap.Message) + err = mailbox.ListMessages(false, nil, []imap.FetchItem{imap.FetchRFC822Size}, c) + if err != nil && err != mess.ErrNoMessages { + return nil, err + } + + items := make([]int, 0) + for msg := range(c) { + items = append(items, int(msg.Size)) + } + + return items, nil +} + +// Returns whether message exists and if yes, then return size of the message in bytes (octets) +func (endp *Endpoint) ListMessage(user pop3backend.User, msgId int) (exists bool, octets int, err error) { + mailbox, err := endp.getMailbox(user) + if err != nil { + return false, 0, err + } + + seqset := imap.SeqSet{} + seqset.AddNum(uint32(msgId)) + c := make(chan *imap.Message) + err = mailbox.ListMessages(true, &seqset, []imap.FetchItem{imap.FetchRFC822Size}, c) + if err != nil && err != mess.ErrNoMessages { + return false, 0, err + } + + msg, ok := <- c + if !ok { + return false, 0, nil + } + + return true, int(msg.Size), nil +} + +// Retrieve whole message by ID - note that message ID is a message position returned +// by List() function, so be sure to keep that order unchanged while client is connected +// See Lock() function for more details +func (endp *Endpoint) Retr(user pop3backend.User, msgId int) (message string, err error) { + mailbox, err := endp.getMailbox(user) + if err != nil { + return "", err + } + + seqset := imap.SeqSet{} + seqset.AddNum(uint32(msgId)) + c := make(chan *imap.Message) + err = mailbox.ListMessages(true, &seqset, []imap.FetchItem{imap.FetchRFC822Size}, c) + if err != nil && err != mess.ErrNoMessages { + return "", err + } + + msg, ok := <- c + if !ok { + return "", fmt.Errorf("internal server error") + } + + return strconv.FormatUint(uint64(msg.Uid), 10), nil +} + +// Delete message by message ID - message should be just marked as deleted until +// Update() is called. Be aware that after Dele() is called, functions like List() etc. +// should ignore all these messages even if Update() hasn't been called yet +func (endp *Endpoint) Dele(user pop3backend.User, msgId int) error { + mailbox, err := endp.getMailbox(user) + if err != nil { + return err + } + + seqset := imap.SeqSet{} + seqset.AddNum(uint32(msgId)) + return mailbox.UpdateMessagesFlags(true, &seqset, imap.SetFlags, false, []string{imap.DeletedFlag}) +} + +// Undelete all messages marked as deleted in single connection +func (endp *Endpoint) Rset(user pop3backend.User) error { + return fmt.Errorf("pop3: unimplemented") +} + +// List of unique IDs of all message, similar to List(), but instead of size there +// is a unique ID which persists the same across all connections. Uid (unique id) is +// used to allow client to be able to keep messages on the server. +func (endp *Endpoint) Uidl(user pop3backend.User) (uids []string, err error) { + mailbox, err := endp.getMailbox(user) + if err != nil { + return nil, err + } + + c := make(chan *imap.Message) + err = mailbox.ListMessages(false, nil, nil, c) + if err != nil && err != mess.ErrNoMessages { + return nil, err + } + + items := make([]string, 0) + for msg := range(c) { + items = append(items, strconv.FormatUint(uint64(msg.Uid), 10)) + } + + return items, nil +} + +// Similar to ListMessage, but returns unique ID by message ID instead of size. +func (endp *Endpoint) UidlMessage(user pop3backend.User, msgId int) (exists bool, uid string, err error) { + mailbox, err := endp.getMailbox(user) + if err != nil { + return false, "", err + } + + seqset := imap.SeqSet{} + seqset.AddNum(uint32(msgId)) + c := make(chan *imap.Message) + err = mailbox.ListMessages(true, &seqset, nil, c) + if err != nil && err != mess.ErrNoMessages { + return false, "", err + } + + msg, ok := <- c + if !ok { + return false, "", nil + } + + return true, strconv.FormatUint(uint64(msg.Uid), 10), nil +} + +// Write all changes to persistent storage, i.e. delete all messages marked as deleted. +func (endp *Endpoint) Update(user pop3backend.User) error { + mailbox, err := endp.getMailbox(user) + if err != nil { + return err + } + + return mailbox.Expunge() +} + +// If the POP3 server issues a positive response, then the +// response given is multi-line. After the initial +OK, the +// POP3 server sends the headers of the message, the blank +// line separating the headers from the body, and then the +// number of lines of the indicated message's body, being +// careful to byte-stuff the termination character (as with +// all multi-line responses). +// Note that if the number of lines requested by the POP3 +// client is greater than than the number of lines in the +// body, then the POP3 server sends the entire message. +func (endp *Endpoint) Top(user pop3backend.User, msgId int, n int) (lines []string, err error) { + return nil, fmt.Errorf("pop3: unimplemented") +} + +// Lock is called immediately after client is connected. The best way what to use Lock() for +// is to read all the messages into cache after client is connected. If another user +// tries to lock the storage, you should return an error to avoid data race. +func (endp *Endpoint) Lock(user pop3backend.User) error { + return nil // FIXME: NOT IMPLEMENTED +} + +// Release lock on storage, Unlock() is called after client is disconnected. +func (endp *Endpoint) Unlock(user pop3backend.User) error { + backendUser, ok := user.(imapbackend.User) + if !ok { + return fmt.Errorf("internal server error") + } + + return backendUser.Logout() +} + +func (endp *Endpoint) I18NLevel() int { + be, ok := endp.Store.(i18nlevel.Backend) + if !ok { + return 0 + } + return be.I18NLevel() +} + +func (endp *Endpoint) SupportedThreadAlgorithms() []sortthread.ThreadAlgorithm { + be, ok := endp.Store.(sortthread.ThreadBackend) + if !ok { + return nil + } + + return be.SupportedThreadAlgorithms() +} + +func init() { + module.RegisterEndpoint("pop3", New) +} diff --git a/maddy.go b/maddy.go index 5439fba1..45280be5 100644 --- a/maddy.go +++ b/maddy.go @@ -61,6 +61,7 @@ import ( _ "github.com/foxcpp/maddy/internal/endpoint/dovecot_sasld" _ "github.com/foxcpp/maddy/internal/endpoint/imap" _ "github.com/foxcpp/maddy/internal/endpoint/openmetrics" + _ "github.com/foxcpp/maddy/internal/endpoint/pop3" _ "github.com/foxcpp/maddy/internal/endpoint/smtp" _ "github.com/foxcpp/maddy/internal/imap_filter" _ "github.com/foxcpp/maddy/internal/imap_filter/command" From e2dcff9d8b05b43e99c1e48cb4b0bc4f6e82a34c Mon Sep 17 00:00:00 2001 From: K Date: Sun, 2 Mar 2025 21:51:52 -0500 Subject: [PATCH 2/4] Remove unused functions --- internal/endpoint/pop3/pop3.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/internal/endpoint/pop3/pop3.go b/internal/endpoint/pop3/pop3.go index 16e398ab..d38453b1 100644 --- a/internal/endpoint/pop3/pop3.go +++ b/internal/endpoint/pop3/pop3.go @@ -444,23 +444,6 @@ func (endp *Endpoint) Unlock(user pop3backend.User) error { return backendUser.Logout() } -func (endp *Endpoint) I18NLevel() int { - be, ok := endp.Store.(i18nlevel.Backend) - if !ok { - return 0 - } - return be.I18NLevel() -} - -func (endp *Endpoint) SupportedThreadAlgorithms() []sortthread.ThreadAlgorithm { - be, ok := endp.Store.(sortthread.ThreadBackend) - if !ok { - return nil - } - - return be.SupportedThreadAlgorithms() -} - func init() { module.RegisterEndpoint("pop3", New) } From cfcdec7f51d375055f2f0d26e5d4ad59a10c25e4 Mon Sep 17 00:00:00 2001 From: K Date: Sun, 2 Mar 2025 21:51:52 -0500 Subject: [PATCH 3/4] Remove unused functions --- internal/endpoint/pop3/pop3.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/endpoint/pop3/pop3.go b/internal/endpoint/pop3/pop3.go index d38453b1..67e7bb62 100644 --- a/internal/endpoint/pop3/pop3.go +++ b/internal/endpoint/pop3/pop3.go @@ -104,12 +104,6 @@ func (endp *Endpoint) Init(cfg *config.Map) error { return err } - if updBe, ok := endp.Store.(updatepipe.Backend); ok { - if err := updBe.EnableUpdatePipe(updatepipe.ModeReplicate); err != nil { - endp.Log.Error("failed to initialize updates pipe", err) - } - } - endp.saslAuth.Log.Debug = endp.Log.Debug addresses := make([]config.Endpoint, 0, len(endp.addrs)) From c3d73475ec90541f7dc28c2d467bb6b08173f108 Mon Sep 17 00:00:00 2001 From: K Date: Sat, 15 Mar 2025 22:30:49 -0400 Subject: [PATCH 4/4] Fix message fetching logic --- internal/endpoint/pop3/pop3.go | 239 ++++++++++++++++++++++++--------- 1 file changed, 178 insertions(+), 61 deletions(-) diff --git a/internal/endpoint/pop3/pop3.go b/internal/endpoint/pop3/pop3.go index 67e7bb62..c9c8cdf7 100644 --- a/internal/endpoint/pop3/pop3.go +++ b/internal/endpoint/pop3/pop3.go @@ -24,17 +24,13 @@ import ( "errors" "fmt" "net" + "strconv" "strings" "sync" - "strconv" - "github.com/kiwiz/popgun" - pop3backend "github.com/kiwiz/popgun/backends" "github.com/emersion/go-imap" - sortthread "github.com/emersion/go-imap-sortthread" imapbackend "github.com/emersion/go-imap/backend" _ "github.com/emersion/go-message/charset" - i18nlevel "github.com/foxcpp/go-imap-i18nlevel" "github.com/foxcpp/go-imap-mess" "github.com/foxcpp/maddy/framework/config" modconfig "github.com/foxcpp/maddy/framework/config/module" @@ -44,9 +40,16 @@ import ( "github.com/foxcpp/maddy/internal/auth" "github.com/foxcpp/maddy/internal/authz" "github.com/foxcpp/maddy/internal/proxy_protocol" - "github.com/foxcpp/maddy/internal/updatepipe" + "github.com/kiwiz/popgun" + pop3backend "github.com/kiwiz/popgun/backends" ) +type Session struct { + imapbackend.User + Mailbox imapbackend.Mailbox + deletedItems *imap.SeqSet +} + type Endpoint struct { addrs []string serv *popgun.Server @@ -54,8 +57,10 @@ type Endpoint struct { proxyProtocol *proxy_protocol.ProxyProtocol Store module.Storage - tlsConfig *tls.Config - listenersWg sync.WaitGroup + tlsConfig *tls.Config + listenersWg sync.WaitGroup + lockMutex sync.Mutex + activeUsersMap map[string]bool saslAuth auth.SASLAuth @@ -72,6 +77,7 @@ func New(modName string, addrs []string) (module.Module, error) { saslAuth: auth.SASLAuth{ Log: log.Logger{Name: modName + "/sasl"}, }, + activeUsersMap: make(map[string]bool), } return endp, nil @@ -182,16 +188,13 @@ func (endp *Endpoint) Close() error { return nil } -func (endp *Endpoint) getMailbox(user pop3backend.User) (imapbackend.Mailbox, error) { - backendUser, ok := user.(imapbackend.User) +func (endp *Endpoint) getSession(user pop3backend.User) (*Session, error) { + sess, ok := user.(*Session) if !ok { return nil, fmt.Errorf("internal server error") } - _, mailbox, err := backendUser.GetMailbox(imap.InboxName, true, nil) - if err != nil { - return nil, fmt.Errorf("unable to get maildrop") - } - return mailbox, nil + + return sess, nil } func (endp *Endpoint) usernameForStorage(ctx context.Context, saslUsername string) (string, error) { @@ -237,70 +240,106 @@ func (endp *Endpoint) Authorize(conn net.Conn, user, pass string) (pop3backend.U return nil, fmt.Errorf("internal server error") } - return endp.Store.GetOrCreateIMAPAcct(storageUsername) + imapUser, err := endp.Store.GetOrCreateIMAPAcct(storageUsername) + if err != nil { + return nil, err + } + + _, mailbox, err := imapUser.GetMailbox(imap.InboxName, true, nil) + if err != nil { + return nil, fmt.Errorf("unable to get maildrop") + } + + return &Session{ + imapUser, + mailbox, + &imap.SeqSet{}, + }, nil } // interface implementation for popgun.Backend func (endp *Endpoint) Stat(user pop3backend.User) (messages, octets int, err error) { - mailbox, err := endp.getMailbox(user) + sess, err := endp.getSession(user) if err != nil { return 0, 0, err } - c := make(chan *imap.Message) - err = mailbox.ListMessages(false, nil, []imap.FetchItem{imap.FetchRFC822Size}, c) - if err != nil && err != mess.ErrNoMessages { - return 0, 0, err - } + msgChan := make(chan *imap.Message) + errChan := make(chan error, 1) + + go func() { + errChan <- sess.Mailbox.ListMessages(true, nil, []imap.FetchItem{imap.FetchRFC822Size}, msgChan) + }() count := 0 size := 0 - for msg := range(c) { + for msg := range msgChan { count += 1 size += int(msg.Size) } + err = <-errChan + if err != nil && err != mess.ErrNoMessages { + return 0, 0, err + } + return count, size, nil } // List of sizes of all messages in bytes (octets) func (endp *Endpoint) List(user pop3backend.User) (octets []int, err error) { - mailbox, err := endp.getMailbox(user) + sess, err := endp.getSession(user) if err != nil { return nil, err } - c := make(chan *imap.Message) - err = mailbox.ListMessages(false, nil, []imap.FetchItem{imap.FetchRFC822Size}, c) - if err != nil && err != mess.ErrNoMessages { - return nil, err - } + msgChan := make(chan *imap.Message) + errChan := make(chan error, 1) + + seqset := imap.SeqSet{} + seqset.AddNum(0) + go func() { + errChan <- sess.Mailbox.ListMessages(true, &seqset, []imap.FetchItem{imap.FetchRFC822Size}, msgChan) + }() items := make([]int, 0) - for msg := range(c) { + for msg := range msgChan { items = append(items, int(msg.Size)) } + err = <-errChan + if err != nil && err != mess.ErrNoMessages { + return nil, err + } + return items, nil } // Returns whether message exists and if yes, then return size of the message in bytes (octets) func (endp *Endpoint) ListMessage(user pop3backend.User, msgId int) (exists bool, octets int, err error) { - mailbox, err := endp.getMailbox(user) + sess, err := endp.getSession(user) if err != nil { return false, 0, err } + msgChan := make(chan *imap.Message, 1) + errChan := make(chan error, 1) + seqset := imap.SeqSet{} seqset.AddNum(uint32(msgId)) - c := make(chan *imap.Message) - err = mailbox.ListMessages(true, &seqset, []imap.FetchItem{imap.FetchRFC822Size}, c) + go func() { + errChan <- sess.Mailbox.ListMessages(true, &seqset, []imap.FetchItem{imap.FetchRFC822Size}, msgChan) + }() + + var msg *imap.Message + msg = <-msgChan + + err = <-errChan if err != nil && err != mess.ErrNoMessages { return false, 0, err } - msg, ok := <- c - if !ok { + if msg == nil { return false, 0, nil } @@ -311,22 +350,30 @@ func (endp *Endpoint) ListMessage(user pop3backend.User, msgId int) (exists bool // by List() function, so be sure to keep that order unchanged while client is connected // See Lock() function for more details func (endp *Endpoint) Retr(user pop3backend.User, msgId int) (message string, err error) { - mailbox, err := endp.getMailbox(user) + sess, err := endp.getSession(user) if err != nil { return "", err } + msgChan := make(chan *imap.Message) + errChan := make(chan error, 1) + seqset := imap.SeqSet{} seqset.AddNum(uint32(msgId)) - c := make(chan *imap.Message) - err = mailbox.ListMessages(true, &seqset, []imap.FetchItem{imap.FetchRFC822Size}, c) + go func() { + errChan <- sess.Mailbox.ListMessages(true, &seqset, []imap.FetchItem{imap.FetchRFC822Size}, msgChan) + }() + + var msg *imap.Message + msg = <-msgChan + + err = <-errChan if err != nil && err != mess.ErrNoMessages { return "", err } - msg, ok := <- c - if !ok { - return "", fmt.Errorf("internal server error") + if msg == nil { + return "", fmt.Errorf("not found") } return strconv.FormatUint(uint64(msg.Uid), 10), nil @@ -336,61 +383,93 @@ func (endp *Endpoint) Retr(user pop3backend.User, msgId int) (message string, er // Update() is called. Be aware that after Dele() is called, functions like List() etc. // should ignore all these messages even if Update() hasn't been called yet func (endp *Endpoint) Dele(user pop3backend.User, msgId int) error { - mailbox, err := endp.getMailbox(user) + sess, err := endp.getSession(user) if err != nil { return err } seqset := imap.SeqSet{} seqset.AddNum(uint32(msgId)) - return mailbox.UpdateMessagesFlags(true, &seqset, imap.SetFlags, false, []string{imap.DeletedFlag}) + err = sess.Mailbox.UpdateMessagesFlags(true, &seqset, imap.SetFlags, false, []string{imap.DeletedFlag}) + if err != nil { + return err + } + + sess.deletedItems.AddNum(uint32(msgId)) + return nil } // Undelete all messages marked as deleted in single connection func (endp *Endpoint) Rset(user pop3backend.User) error { - return fmt.Errorf("pop3: unimplemented") + sess, err := endp.getSession(user) + if err != nil { + return err + } + + err = sess.Mailbox.UpdateMessagesFlags(true, sess.deletedItems, imap.RemoveFlags, false, []string{imap.DeletedFlag}) + if err != nil { + return fmt.Errorf("pop3: internal server error") + } + + sess.deletedItems = &imap.SeqSet{} + return nil } // List of unique IDs of all message, similar to List(), but instead of size there // is a unique ID which persists the same across all connections. Uid (unique id) is // used to allow client to be able to keep messages on the server. func (endp *Endpoint) Uidl(user pop3backend.User) (uids []string, err error) { - mailbox, err := endp.getMailbox(user) + sess, err := endp.getSession(user) if err != nil { return nil, err } - c := make(chan *imap.Message) - err = mailbox.ListMessages(false, nil, nil, c) - if err != nil && err != mess.ErrNoMessages { - return nil, err - } + msgChan := make(chan *imap.Message) + errChan := make(chan error, 1) + + go func() { + errChan <- sess.Mailbox.ListMessages(false, nil, nil, msgChan) + }() items := make([]string, 0) - for msg := range(c) { + for msg := range msgChan { items = append(items, strconv.FormatUint(uint64(msg.Uid), 10)) } + err = <-errChan + if err != nil && err != mess.ErrNoMessages { + return nil, err + } + return items, nil } // Similar to ListMessage, but returns unique ID by message ID instead of size. func (endp *Endpoint) UidlMessage(user pop3backend.User, msgId int) (exists bool, uid string, err error) { - mailbox, err := endp.getMailbox(user) + sess, err := endp.getSession(user) if err != nil { return false, "", err } + msgChan := make(chan *imap.Message, 1) + errChan := make(chan error, 1) + seqset := imap.SeqSet{} seqset.AddNum(uint32(msgId)) - c := make(chan *imap.Message) - err = mailbox.ListMessages(true, &seqset, nil, c) + + go func() { + errChan <- sess.Mailbox.ListMessages(true, &seqset, nil, msgChan) + }() + + var msg *imap.Message + msg = <-msgChan + + err = <-errChan if err != nil && err != mess.ErrNoMessages { return false, "", err } - msg, ok := <- c - if !ok { + if msg == nil { return false, "", nil } @@ -399,12 +478,12 @@ func (endp *Endpoint) UidlMessage(user pop3backend.User, msgId int) (exists bool // Write all changes to persistent storage, i.e. delete all messages marked as deleted. func (endp *Endpoint) Update(user pop3backend.User) error { - mailbox, err := endp.getMailbox(user) + sess, err := endp.getSession(user) if err != nil { return err } - return mailbox.Expunge() + return sess.Mailbox.Expunge() } // If the POP3 server issues a positive response, then the @@ -425,17 +504,55 @@ func (endp *Endpoint) Top(user pop3backend.User, msgId int, n int) (lines []stri // is to read all the messages into cache after client is connected. If another user // tries to lock the storage, you should return an error to avoid data race. func (endp *Endpoint) Lock(user pop3backend.User) error { - return nil // FIXME: NOT IMPLEMENTED + endp.lockMutex.Lock() + defer endp.lockMutex.Unlock() + + backendUser, ok := user.(imapbackend.User) + if !ok { + return fmt.Errorf("pop3: internal server error") + } + username := backendUser.Username() + + // Only one simultaneous connection is supported + if endp.activeUsersMap[username] { + return fmt.Errorf("pop3: internal server error") + } + + endp.activeUsersMap[username] = true + + return nil } // Release lock on storage, Unlock() is called after client is disconnected. func (endp *Endpoint) Unlock(user pop3backend.User) error { + endp.lockMutex.Lock() + defer endp.lockMutex.Unlock() + backendUser, ok := user.(imapbackend.User) if !ok { - return fmt.Errorf("internal server error") + return fmt.Errorf("pop3: internal server error") } - return backendUser.Logout() + username := backendUser.Username() + + // Not locked + if !endp.activeUsersMap[username] { + return fmt.Errorf("pop3: internal server error") + } + + err := endp.Rset(user) + if err != nil { + return err + } + + err = backendUser.Logout() + if err != nil { + return err + } + + endp.activeUsersMap[username] = false + + return nil } func init() {