From 845b64dd18939acde62c8006fe4ce04b517c2953 Mon Sep 17 00:00:00 2001 From: Alexandru Plugaru Date: Tue, 4 Nov 2014 11:25:25 +0100 Subject: [PATCH] Functional configuration refactoring. Fixes #5 Inspired by @davecheney post: http://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis --- README.md | 10 ++-- demo/main.go | 46 ++++++++-------- imap.go | 146 +++++++++++++++++++++++++++++++++++++++++---------- mailstore.go | 48 +++++++++++++++++ parser.go | 1 - session.go | 87 +++++++++++++----------------- 6 files changed, 229 insertions(+), 109 deletions(-) create mode 100644 mailstore.go diff --git a/README.md b/README.md index d4c3b5a..101fd30 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,7 @@ This is an IMAP server written in Go. It is a work in progress. In the demo subdirectory is an example IMAP server that starts up on port 1193. To run this server: ``` -$ cd demo -$ go build -$ ./demo +$ go run ./demo/main.go ``` You can connect to this server using telnet or netcat. For example: @@ -37,7 +35,11 @@ $ nc -C localhost 1193 # Developing -The server is not fully operational on its own. It defines an interface in session.go which describes the service it needs from a Mailstore. The Mailstore can be a database or filesystem or combination of both. The IMAP server does not care and it is up to the user to supply this service. +The server is not fully operational on its own. It requires a mailstore and an authentication mechanism. + +It defines an interface in mailstore.go which describes the service it needs from a Mailstore. You can use multiple mailstores at the same time: database, filesystem, maildir, etc... + +There are plans to add basic support for maildir and a basic database storage. To add a new IMAP command the usual steps are: diff --git a/demo/main.go b/demo/main.go index 41645fe..4f55774 100644 --- a/demo/main.go +++ b/demo/main.go @@ -1,55 +1,53 @@ - package main import ( imap "github.com/alienscience/imapsrv" ) -// A dummy mailstore used for demonstrating the IMAP server -type Mailstore struct { -} - func main() { - // Configure an IMAP server on localhost port 1193 - config := imap.DefaultConfig() - config.Interface = "127.0.0.1:1193" - - // Configure a dummy mailstore - mailstore := &Mailstore{} - config.Store = mailstore - - // Start the server - server := imap.Create(config) - server.Start() - + // The simplest possible server - zero config + // It will find a free tcp port, create some temporary directories.. - just give me a server! + //s := imap.NewServer() + //s.Start() + + // More advanced config + m := &imap.DummyMailstore{} + + s := imap.NewServer( + imap.Listen("127.0.0.1:1193"), + imap.Store(m), + ) + s.Start() } -//------ Dummy Mailstore ------------------------------------------------------- +// A dummy mailstore used for demonstrating the IMAP server +type DummyMailstore struct { +} // Get mailbox information -func (m *Mailstore) GetMailbox(name string) (*imap.Mailbox, error) { +func (m *DummyMailstore) GetMailbox(name string) (*imap.Mailbox, error) { return &imap.Mailbox{ Name: "inbox", - Id: 1, + Id: 1, }, nil } // Get the sequence number of the first unseen message -func (m *Mailstore) FirstUnseen(mbox int64) (int64, error) { +func (m *DummyMailstore) FirstUnseen(mbox int64) (int64, error) { return 4, nil } // Get the total number of messages in an IMAP mailbox -func (m *Mailstore) TotalMessages(mbox int64) (int64, error) { +func (m *DummyMailstore) TotalMessages(mbox int64) (int64, error) { return 8, nil } // Get the total number of unread messages in an IMAP mailbox -func (m *Mailstore) RecentMessages(mbox int64) (int64, error) { +func (m *DummyMailstore) RecentMessages(mbox int64) (int64, error) { return 4, nil } // Get the next available uid in an IMAP mailbox -func (m *Mailstore) NextUid(mbox int64) (int64, error) { +func (m *DummyMailstore) NextUid(mbox int64) (int64, error) { return 9, nil } diff --git a/imap.go b/imap.go index fb8af09..6d9ea07 100644 --- a/imap.go +++ b/imap.go @@ -1,4 +1,3 @@ - // An IMAP server package imapsrv @@ -10,9 +9,9 @@ import ( // IMAP server configuration type Config struct { - Interface string - MaxClients int - Store Mailstore + MaxClients uint + Listeners []Listener + Mailstores []Mailstore } // An IMAP Server @@ -20,7 +19,12 @@ type Server struct { // Server configuration config *Config // Number of active clients - activeClients int + activeClients uint +} + +// A listener is listening on a given address. Ex: 0.0.0.0:193 +type Listener struct { + Addr string } // An IMAP Client as seen by an IMAP server @@ -42,47 +46,131 @@ func Create(config *Config) *Server { // Return the default server configuration func DefaultConfig() *Config { + listeners := []Listener{ + Listener{ + Addr: "0.0.0.0:143", + }, + } + return &Config{ - Interface: "0.0.0.0:193", + Listeners: listeners, MaxClients: 8, } } -// Start an IMAP server -func (s *Server) Start() { +// Add a mailstore to the config +func Store(m Mailstore) func(*Server) error { + return func(s *Server) error { + s.config.Mailstores = append(s.config.Mailstores, m) + return nil + } +} - // Start listening for IMAP connections - iface := s.config.Interface - listener, err := net.Listen("tcp", iface) - if err != nil { - log.Fatalf("IMAP cannot listen on %s, %v", iface, err) +// test if 2 listeners are equal +func equalListeners(l1, l2 []Listener) bool { + for i, l := range l1 { + if l != l2[i] { + return false + } } + return true +} - log.Print("IMAP server listening on ", iface) +// Add an interface to listen to +func Listen(Addr string) func(*Server) error { + return func(s *Server) error { + // if we only have the default config we should override it + dc := DefaultConfig() + l := Listener{ + Addr: Addr, + } + if equalListeners(dc.Listeners, s.config.Listeners) { + s.config.Listeners = []Listener{l} + } else { + s.config.Listeners = append(s.config.Listeners, l) + } - clientNumber := 1 + return nil + } +} - for { - // Accept a connection from a new client - conn, err := listener.Accept() +// Set MaxClients config +func MaxClients(max uint) func(*Server) error { + return func(s *Server) error { + s.config.MaxClients = max + return nil + } +} + +func NewServer(options ...func(*Server) error) *Server { + // set the default config + s := &Server{} + dc := DefaultConfig() + s.config = dc + + // override the config with the functional options + for _, option := range options { + err := option(s) if err != nil { - log.Print("IMAP accept error, ", err) - continue + panic(err) } + } + + //Check if we can listen on default ports, if not try to find a free port + if equalListeners(dc.Listeners, s.config.Listeners) { + listener := s.config.Listeners[0] + l, err := net.Listen("tcp", listener.Addr) + if err != nil { + l, err = net.Listen("tcp4", ":0") // this will ask the OS to give us a free port + if err != nil { + panic("Can't listen on any port") + } + l.Close() + s.config.Listeners[0].Addr = l.Addr().String() + } else { + l.Close() + } + } + + return s +} - // Handle the client - client := &client{ - conn: conn, - bufin: bufio.NewReader(conn), - bufout: bufio.NewWriter(conn), - id: clientNumber, - config: s.config, +// Start an IMAP server +func (s *Server) Start() error { + // Start listening for IMAP connections + for _, iface := range s.config.Listeners { + listener, err := net.Listen("tcp", iface.Addr) + if err != nil { + log.Fatalf("IMAP cannot listen on %s, %v", iface.Addr, err) } - go client.handle() + log.Print("IMAP server listening on ", iface.Addr) + + clientNumber := 1 - clientNumber += 1 + for { + // Accept a connection from a new client + conn, err := listener.Accept() + if err != nil { + log.Print("IMAP accept error, ", err) + continue + } + + // Handle the client + client := &client{ + conn: conn, + bufin: bufio.NewReader(conn), + bufout: bufio.NewWriter(conn), + id: clientNumber, + config: s.config, + } + + go client.handle() + + clientNumber += 1 + } } + return nil } // Handle requests from an IMAP client diff --git a/mailstore.go b/mailstore.go new file mode 100644 index 0000000..2ea0643 --- /dev/null +++ b/mailstore.go @@ -0,0 +1,48 @@ +package imapsrv + +// A service that is needed to read mail messages +type Mailstore interface { + // Get IMAP mailbox information + // Returns nil if the mailbox does not exist + GetMailbox(name string) (*Mailbox, error) + // Get the sequence number of the first unseen message + FirstUnseen(mbox int64) (int64, error) + // Get the total number of messages in an IMAP mailbox + TotalMessages(mbox int64) (int64, error) + // Get the total number of unread messages in an IMAP mailbox + RecentMessages(mbox int64) (int64, error) + // Get the next available uid in an IMAP mailbox + NextUid(mbox int64) (int64, error) +} + +// A dummy mailstore used for demonstrating the IMAP server +type DummyMailstore struct { +} + +// Get mailbox information +func (m *DummyMailstore) GetMailbox(name string) (*Mailbox, error) { + return &Mailbox{ + Name: "inbox", + Id: 1, + }, nil +} + +// Get the sequence number of the first unseen message +func (m *DummyMailstore) FirstUnseen(mbox int64) (int64, error) { + return 4, nil +} + +// Get the total number of messages in an IMAP mailbox +func (m *DummyMailstore) TotalMessages(mbox int64) (int64, error) { + return 8, nil +} + +// Get the total number of unread messages in an IMAP mailbox +func (m *DummyMailstore) RecentMessages(mbox int64) (int64, error) { + return 4, nil +} + +// Get the next available uid in an IMAP mailbox +func (m *DummyMailstore) NextUid(mbox int64) (int64, error) { + return 9, nil +} diff --git a/parser.go b/parser.go index 7d1b9d3..9df5f8a 100644 --- a/parser.go +++ b/parser.go @@ -1,4 +1,3 @@ - package imapsrv import ( diff --git a/session.go b/session.go index cc51354..5d2518c 100644 --- a/session.go +++ b/session.go @@ -1,4 +1,3 @@ - package imapsrv import ( @@ -15,21 +14,6 @@ const ( selected ) -// A service that is needed to read mail messages -type Mailstore interface { - // Get IMAP mailbox information - // Returns nil if the mailbox does not exist - GetMailbox(name string) (*Mailbox, error) - // Get the sequence number of the first unseen message - FirstUnseen(mbox int64) (int64, error) - // Get the total number of messages in an IMAP mailbox - TotalMessages(mbox int64) (int64, error) - // Get the total number of unread messages in an IMAP mailbox - RecentMessages(mbox int64) (int64, error) - // Get the next available uid in an IMAP mailbox - NextUid(mbox int64) (int64, error) -} - // An IMAP mailbox type Mailbox struct { Name string // The name of the mailbox @@ -66,50 +50,51 @@ func (s *session) log(info ...interface{}) { // Select a mailbox - returns true if the mailbox exists func (s *session) selectMailbox(name string) (bool, error) { - mailstore := s.config.Store + for _, mailstore := range s.config.Mailstores { + // Lookup the mailbox + mbox, err := mailstore.GetMailbox(name) - // Lookup the mailbox - mbox, err := mailstore.GetMailbox(name) + if err != nil { + return false, err + } - if err != nil { - return false, err - } + if mbox == nil { + return false, nil + } - if mbox == nil { - return false, nil + // Make note of the mailbox + s.mailbox = mbox + break } - - // Make note of the mailbox - s.mailbox = mbox return true, nil } // Add mailbox information to the given response func (s *session) addMailboxInfo(resp *response) error { - mailstore := s.config.Store - - // Get the mailbox information from the mailstore - firstUnseen, err := mailstore.FirstUnseen(s.mailbox.Id) - if err != nil { - return err - } - totalMessages, err := mailstore.TotalMessages(s.mailbox.Id) - if err != nil { - return err - } - recentMessages, err := mailstore.RecentMessages(s.mailbox.Id) - if err != nil { - return err + for _, mailstore := range s.config.Mailstores { + // Get the mailbox information from the mailstore + firstUnseen, err := mailstore.FirstUnseen(s.mailbox.Id) + if err != nil { + return err + } + totalMessages, err := mailstore.TotalMessages(s.mailbox.Id) + if err != nil { + return err + } + recentMessages, err := mailstore.RecentMessages(s.mailbox.Id) + if err != nil { + return err + } + nextUid, err := mailstore.NextUid(s.mailbox.Id) + if err != nil { + return err + } + + resp.extra(fmt.Sprint(totalMessages, " EXISTS")) + resp.extra(fmt.Sprint(recentMessages, " RECENT")) + resp.extra(fmt.Sprintf("OK [UNSEEN %d] Message %d is first unseen", firstUnseen, firstUnseen)) + resp.extra(fmt.Sprintf("OK [UIDVALIDITY %d] UIDs valid", s.mailbox.Id)) + resp.extra(fmt.Sprintf("OK [UIDNEXT %d] Predicted next UID", nextUid)) } - nextUid, err := mailstore.NextUid(s.mailbox.Id) - if err != nil { - return err - } - - resp.extra(fmt.Sprint(totalMessages, " EXISTS")) - resp.extra(fmt.Sprint(recentMessages, " RECENT")) - resp.extra(fmt.Sprintf("OK [UNSEEN %d] Message %d is first unseen", firstUnseen, firstUnseen)) - resp.extra(fmt.Sprintf("OK [UIDVALIDITY %d] UIDs valid", s.mailbox.Id)) - resp.extra(fmt.Sprintf("OK [UIDNEXT %d] Predicted next UID", nextUid)) return nil }