From 76729687a7d5342cb7b92a9a17e7b422da66f9a3 Mon Sep 17 00:00:00 2001 From: Etienne Bruines Date: Mon, 29 Jun 2015 17:01:59 +0200 Subject: [PATCH] Added initial LMTP support according to #29. It can now receive emails from Postfix. This works with Postfix, when configuring 'mailbox_transport = ltmp:unix:/tmp/imapsrv-lmtp' It does require quite some testing (Windows, Mac OS, without OS (Docker?)) Some refactoring would probably be desirable as well. --- demo/{starttls => certificates}/private.pem | 0 demo/{starttls => certificates}/public.pem | 0 demo/lmtp/main.go | 18 ++ demo/starttls/main.go | 2 +- imap.go | 17 +- lmtp.go | 219 ++++++++++++++++++++ 6 files changed, 250 insertions(+), 6 deletions(-) rename demo/{starttls => certificates}/private.pem (100%) rename demo/{starttls => certificates}/public.pem (100%) create mode 100644 demo/lmtp/main.go create mode 100644 lmtp.go diff --git a/demo/starttls/private.pem b/demo/certificates/private.pem similarity index 100% rename from demo/starttls/private.pem rename to demo/certificates/private.pem diff --git a/demo/starttls/public.pem b/demo/certificates/public.pem similarity index 100% rename from demo/starttls/public.pem rename to demo/certificates/public.pem diff --git a/demo/lmtp/main.go b/demo/lmtp/main.go new file mode 100644 index 0000000..98bc11f --- /dev/null +++ b/demo/lmtp/main.go @@ -0,0 +1,18 @@ +package main + +import ( + imap "github.com/alienscience/imapsrv" +) + +func main() { + // This package allows to receive e-mail using the LMTP protocol, + // and allowing STARTTLS to connect to the imap server. + + lmtp := imap.LMTPOption("/tmp/imapsrv-lmtp") + + s := imap.NewServer( + imap.ListenSTARTTLSOoption("127.0.0.1:1194", "demo/certificates/public.pem", "demo/certificates/private.pem"), + lmtp, + ) + s.Start() +} diff --git a/demo/starttls/main.go b/demo/starttls/main.go index 71ff182..ecfd49b 100644 --- a/demo/starttls/main.go +++ b/demo/starttls/main.go @@ -11,7 +11,7 @@ func main() { s := imap.NewServer( imap.ListenOption("127.0.0.1:1193"), // optionally also listen to non-STARTTLS ports - imap.ListenSTARTTLSOoption("127.0.0.1:1194", "demo/starttls/public.pem", "demo/starttls/private.pem"), + imap.ListenSTARTTLSOoption("127.0.0.1:1194", "demo/certificates/public.pem", "demo/certificates/private.pem"), ) fmt.Println("Starting server, you can test by doing:\n", diff --git a/imap.go b/imap.go index 29b6047..cec8f57 100644 --- a/imap.go +++ b/imap.go @@ -20,6 +20,8 @@ type config struct { mailstore Mailstore authBackend auth.AuthStore + + lmtpEndpoints []string } type option func(*Server) error @@ -41,7 +43,7 @@ type Server struct { } // client is an IMAP Client as seen by an IMAP server -type client struct { +type imapClient struct { // conn is the lowest-level connection layer conn net.Conn // listener refers to the listener that's handling this client @@ -154,6 +156,11 @@ func (s *Server) Start() error { } } + // Start the LMTP entrypoints as desired + for i, entrypoint := range s.config.lmtpEndpoints { + go s.runLMTPListener(entrypoint, i) + } + // Start the server on each port n := len(s.config.listeners) for i := 0; i < n; i += 1 { @@ -187,7 +194,7 @@ func (s *Server) runListener(listener listener, id int) { } // Handle the client - client := &client{ + client := &imapClient{ conn: conn, listener: listener, bufin: bufio.NewReader(conn), @@ -205,7 +212,7 @@ func (s *Server) runListener(listener listener, id int) { } // handle requests from an IMAP client -func (c *client) handle(s *Server) { +func (c *imapClient) handle(s *Server) { // Close the client on exit from this function defer c.close() @@ -263,11 +270,11 @@ func (c *client) handle(s *Server) { } // close closes an IMAP client -func (c *client) close() { +func (c *imapClient) close() { c.conn.Close() } // logError sends a log message to the default Logger -func (c *client) logError(err error) { +func (c *imapClient) logError(err error) { log.Printf("IMAP client %s, %v", c.id, err) } diff --git a/lmtp.go b/lmtp.go new file mode 100644 index 0000000..2b7abc9 --- /dev/null +++ b/lmtp.go @@ -0,0 +1,219 @@ +package imapsrv + +import ( + "bufio" + "bytes" + "fmt" + "log" + "net" + "net/textproto" + "strings" +) + +const ( + unixNetwork = "unix" +) + +type lmtpSession struct { + receivingData bool + data bytes.Buffer + + rw *textproto.Conn + + recipients []string + from string +} + +type lmtpClient struct { + // conn is the lowest-level connection layer + innerConn net.Conn + // listener refers to the listener that's handling this client + listener *net.UnixListener + + bufin *bufio.Reader + bufout *bufio.Writer + id string + config *config + + session *lmtpSession +} + +func LMTPOption(entrypoint string) func(*Server) error { + return func(s *Server) error { + s.config.lmtpEndpoints = append(s.config.lmtpEndpoints, entrypoint) + return nil + } +} + +func (s *Server) runLMTPListener(entrypoint string, number int) { + // TODO: what if someone wants the entrypoint to be a address + port? + + addr, err := net.ResolveUnixAddr(unixNetwork, entrypoint) + if err != nil { + log.Fatalln(err) // TODO: do we want to crash fatally? Does this also crash other goroutines? + } + + // TODO: stop listening when this all goes south + unixListener, err := net.ListenUnix(unixNetwork, addr) + if err != nil { + log.Fatalln(err) + } + + log.Printf("LMTP entrypoint %d available at %s", number, entrypoint) + + clientNumber := 0 + for { + conn, err := unixListener.AcceptUnix() + if err != nil { + log.Println("Warning: accepting failed:", err) + continue + } + log.Println("Got connection") + + // Handle the client + client := &lmtpClient{ + innerConn: conn, + listener: unixListener, + bufin: bufio.NewReader(conn), + bufout: bufio.NewWriter(conn), + // TODO: perhaps we can do this without Sprint, maybe strconv.Itoa() + id: fmt.Sprint(number, "/", clientNumber), + config: s.config, + } + + go client.handle(s) + + clientNumber += 1 + } +} + +func (c *lmtpClient) handle(s *Server) { + // Close the client on exit from this function + defer c.close() + + // Handle parser panics gracefully + defer func() { + if e := recover(); e != nil { + log.Println("Panic received:", e) + } + }() + + c.session = &lmtpSession{ + rw: textproto.NewConn(c.innerConn), + } + + // Write the welcome message + err := writeLine("220 deskserver.local LMTP server ready", c.session.rw.W) + if err != nil { + log.Println("Error while writing:", err) + return + } + + for { + line, err := c.session.rw.ReadLine() + if err != nil { + log.Println(err) + } + tags := strings.Fields(line) + + if c.session.receivingData { + if line == "." { + c.session.receivingData = false + log.Println("DATA:", c.session.data.String()) + writeOK(c.session.rw.W) + // TODO: we should now process the session, before continuing + continue + } + c.session.data.WriteString(line) + c.session.data.WriteRune('\r') + c.session.data.WriteRune('\n') + + } else { + switch strings.ToUpper(tags[0]) { + case "LHLO": + err = writeLine("250 deskserver.local", c.session.rw.W) + if err != nil { + log.Println("Error while sending LHLO response", err) + return + } + + case "MAIL": + start := strings.IndexRune(line, '<') + end := strings.IndexRune(line, '>') + if start < 6 || end < start { + writeErrorArgs(c.session.rw.W) + continue + } + c.session.from = line[start+1 : end] + writeOK(c.session.rw.W) + + case "RCPT": + start := strings.IndexRune(line, '<') + end := strings.IndexRune(line, '>') + if start < 6 || end < start { + writeErrorArgs(c.session.rw.W) + continue + } + c.session.recipients = append(c.session.recipients, line[start+1:end]) + writeOK(c.session.rw.W) + + case "QUIT": + err = writeLine("221 deskserver.local closing connection", c.session.rw.W) + if err != nil { + log.Println("Error while sending QUIT response", err) + } + return // closes because of defer + + case "DATA": + if len(c.session.recipients) == 0 { + writeLine("503 Bad sequence of commands", c.session.rw.W) + continue + } + writeLine("354 Start mail input; end with .", c.session.rw.W) + c.session.receivingData = true + + default: + log.Println("idk; received:", line) + writeLine("500 command unrecognised", c.session.rw.W) + } + } + } +} + +func writeOK(w *bufio.Writer) { + err := writeLine("250 OK", w) + if err != nil { + log.Println("Error while writing OK:", err) + } +} + +func writeErrorArgs(w *bufio.Writer) { + err := writeLine("501 Syntax error in parameters or arguments", w) + if err != nil { + log.Println("Error while writing 501:", err) + } +} + +func writeLine(mes string, w *bufio.Writer) error { + _, err := w.WriteString(mes + "\r\n") + if err != nil { + return err + } + err = w.Flush() + if err != nil { + return err + } + + return nil +} + +// close closes an LMTP client +func (c *lmtpClient) close() { + defer c.innerConn.Close() + c.session.rw.Close() +} + +// logError sends a log message to the default Logger +func (c *lmtpClient) logError(err error) { + log.Printf("LMTP client %s, %v", c.id, err) +}