Skip to content

Commit

Permalink
Added initial LMTP support according to #29.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
EtienneBruines committed Jun 29, 2015
1 parent acd787d commit 7672968
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 6 deletions.
File renamed without changes.
File renamed without changes.
18 changes: 18 additions & 0 deletions demo/lmtp/main.go
Original file line number Diff line number Diff line change
@@ -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()
}
2 changes: 1 addition & 1 deletion demo/starttls/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 12 additions & 5 deletions imap.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type config struct {
mailstore Mailstore

authBackend auth.AuthStore

lmtpEndpoints []string
}

type option func(*Server) error
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand All @@ -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()
Expand Down Expand Up @@ -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)
}
219 changes: 219 additions & 0 deletions lmtp.go
Original file line number Diff line number Diff line change
@@ -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 <CRLF>.<CRLF>", 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)
}

0 comments on commit 7672968

Please sign in to comment.