Commit 8ef17e45 authored by ale's avatar ale
Browse files

Use a tiny Go SMTP daemon

The Python one proved to be quite problematic.
parent e73d8dc6
FROM registry.git.autistici.org/ai3/docker/apache2-base:master
# Stage 1: build the tiny smtp server
FROM golang:1.12.7 AS build
WORKDIR /go/src
RUN go get -d github.com/chrj/smtpd
COPY smtpd.go .
RUN go build -o smtpd smtpd.go
# Stage 2: build the final container
FROM registry.git.autistici.org/ai3/docker/apache2-base:master
COPY build.sh /tmp/build.sh
COPY smtpd.py /usr/bin/smtpd
COPY conf/ /etc/
COPY --from=build /go/src/smtpd /usr/bin/smtpd
RUN /tmp/build.sh && rm /tmp/build.sh
ENTRYPOINT ["/usr/local/bin/chaperone"]
// Simple SMTP server that runs lurker-index.
//
// Use the --addr-rx command-line option to extract the list name from
// the recipient address. The same regexp defines valid recipient
// addresses (messages whose recipient do not match are rejected).
//
package main
import (
"bytes"
"flag"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/chrj/smtpd"
)
var (
port = flag.Int("port", getenvInt("SMTP_PORT"), "smtp port")
hostname = flag.String("ehlo-hostname", os.Getenv("SMTP_EHLO_HOSTNAME"), "EHLO hostname")
lurkerLibDir = flag.String("lurker-lib-dir", os.Getenv("LURKER_LIB_DIR"), "lurker library dir")
addrPattern = flag.String("addr-rx", getenvDefault("LURKER_ADDR_RX", "lurker+([^@]+)@.*"), "recipient address regular expression")
)
var addrRx *regexp.Regexp
func getenvInt(key string) int {
n, _ := strconv.Atoi(os.Getenv(key))
return n
}
func getenvDefault(key, dflt string) string {
if s := os.Getenv(key); s != "" {
return s
}
return dflt
}
func isMaintenanceModeOn() bool {
_, err := os.Stat(filepath.Join(*lurkerLibDir, ".tmp"))
return !os.IsNotExist(err)
}
func lurkerIndexMsg(listName string, data []byte) error {
cmd := exec.Command("lurker-index", "-m", "-l", listName)
cmd.Stdin = bytes.NewReader(data)
return cmd.Run()
}
func parseRecipientAddr(addr string) (string, error) {
m := addrRx.FindStringSubmatch(addr)
if len(m) == 0 {
return "", smtpd.Error{550, "Unknown recipient"}
}
listName := m[1]
if listName == "" {
return "", smtpd.Error{550, "No list name found"}
}
return listName, nil
}
func checkRecipient(peer smtpd.Peer, addr string) error {
_, err := parseRecipientAddr(addr)
return err
}
func handleMessage(peer smtpd.Peer, env smtpd.Envelope) error {
if isMaintenanceModeOn() {
return smtpd.Error{421, "Maintenance mode, retry later"}
}
log.Printf("received msg: %s -> %s", env.Sender, strings.Join(env.Recipients, ","))
for _, rcpt := range env.Recipients {
listName, err := parseRecipientAddr(rcpt)
if err != nil {
return err
}
if err := lurkerIndexMsg(listName, env.Data); err != nil {
return smtpd.Error{550, fmt.Sprintf("lurker-index error: %v", err)}
}
}
return nil
}
func main() {
log.SetFlags(0)
flag.Parse()
// Auto-anchor the addr regexp.
var err error
addrRx, err = regexp.Compile(fmt.Sprintf("^%s$", *addrPattern))
if err != nil {
log.Fatalf("error in --addr-rx: %v", err)
}
server := &smtpd.Server{
Hostname: *hostname,
RecipientChecker: checkRecipient,
Handler: handleMessage,
ProtocolLogger: log.New(os.Stderr, "smtp: ", 0),
}
if err := server.ListenAndServe(fmt.Sprintf(":%d", *port)); err != nil {
log.Fatal(err)
}
}
#!/usr/bin/python3
#
# SMTP server for lurker.
#
# Listens on an arbitrary port and forwards messages with a recipient of
#
# lurker+<list_name>@<mydomain>
#
# to the lurker-index binary.
import asyncio
import logging
import os
from aiosmtpd.controller import Controller
LURKER_LIBDIR = os.getenv('LURKER_LIBDIR', '/var/lib/lurker/')
LURKER_DOMAIN_NAME = os.getenv('LURKER_DOMAIN_NAME', 'lurker.autistici.org')
def _is_lurker_in_maintenance():
return os.path.exists(os.path.join(LURKER_LIBDIR, '.tmp'))
class LurkerHandler:
async def handle_RCPT(server, session, envelope, address, rcpt_options):
if _is_lurker_in_maintenance():
return '450 lurker is in maintenance mode'
userpart, domain = address.split('@', 1)
if domain != LURKER_DOMAIN_NAME:
return '550 unknown recipient domain'
if not userpart.startswith('lurker+'):
return '550 unknown recipient user'
envelope.rcpt_tos.append(address)
return '250 OK'
async def handle_DATA(server, session, envelope):
logging.debug('message from %s to %s',
envelope.mail_from, envelope.rcpt_tos)
data = envelope.content.decode('utf8', errors='replace')
for rcpt in envelope.rcpt_tos:
list_name = rcpt.split('@')[0].split('+', 1)[1]
cmd = ['/usr/bin/lurker-index', '-m', '-l', list_name]
proc = await asyncio.create_subprocess_exec(
'/usr/bin/lurker-index', '-m', '-l', list_name,
stdin=asyncio.subprocess.PIPE)
await proc.communicate(data)
await proc.wait()
return '250 Message accepted for delivery'
async def amain(loop):
port = int(os.getenv('SMTP_PORT', '5525'))
controller = Controller(LurkerHandler, hostname='', port=port)
controller.start()
if __name__ == '__main__':
logging.basicConfig(level=logging.WARNING)
loop = asyncio.get_event_loop()
loop.create_task(amain(loop=loop))
loop.run_forever()
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment