Futuristic SMTP INBOUND-only server for home usage.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
zangtumb/smtpd/smtp.go

757 lines
22 KiB

// Package smtpd implements a basic SMTP server.
// https://github.com/mhale/smtpd
package smtpd
import (
"bufio"
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"regexp"
"strconv"
"strings"
"time"
)
var (
// Debug `true` enables verbose logging.
Debug = false
rcptToRE = regexp.MustCompile(`[Tt][Oo]:[ ]*<(.+[@].+)>`) // We don't receive for local users with no domain.
mailFromRE = regexp.MustCompile(`[Ff][Rr][Oo][Mm]:[ ]*<(.*)>(\s(.*))?`) // Delivery Status Notifications are sent with "MAIL FROM:<>"
mailSizeRE = regexp.MustCompile(`[Ss][Ii][Zz][Ee]=(\d+)`)
)
// Handler function called upon successful receipt of an email.
type Handler func(remoteAddr net.Addr, from string, to []string, data []byte)
// HandlerRcpt function called on RCPT. Return accept status.
type HandlerRcpt func(remoteAddr net.Addr, from string, to string) bool
// AuthHandler function called when a login attempt is performed. Returns true if credentials are correct.
type AuthHandler func(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error)
// ListenAndServe listens on the TCP network address addr
// and then calls Serve with handler to handle requests
// on incoming connections.
func ListenAndServe(addr string, handler Handler, rcpthandler HandlerRcpt, appname string, hostname string, instance *Server) error {
srv := instance
srv.Addr = addr
srv.HandlerRcpt = rcpthandler
srv.Handler = handler
srv.Appname = appname
srv.Hostname = hostname
return srv.ListenAndServe()
}
type maxSizeExceededError struct {
limit int
}
func maxSizeExceeded(limit int) maxSizeExceededError {
return maxSizeExceededError{limit}
}
// Error uses the RFC 5321 response message in preference to RFC 1870.
// RFC 3463 defines enhanced status code x.3.4 as "Message too big for system".
func (err maxSizeExceededError) Error() string {
return fmt.Sprintf("552 5.3.4 Requested mail action aborted: exceeded storage allocation (%d)", err.limit)
}
// LogFunc is a function capable of logging the client-server communication.
type LogFunc func(remoteIP, verb, line string)
// Server is an SMTP server.
type Server struct {
Addr string // TCP address to listen on, defaults to ":25" (all addresses, port 25) if empty
Appname string
AuthHandler AuthHandler
AuthMechs map[string]bool // Override list of allowed authentication mechanisms. Currently supported: LOGIN, PLAIN, CRAM-MD5. Enabling LOGIN and PLAIN will reduce RFC 4954 compliance.
AuthRequired bool // Require authentication for every command except AUTH, EHLO, HELO, NOOP, RSET or QUIT as per RFC 4954. Ignored if AuthHandler is not configured.
Handler Handler
HandlerRcpt HandlerRcpt
Hostname string
LogRead LogFunc
LogWrite LogFunc
MaxSize int // Maximum message size allowed, in bytes
MaxRcpt int // Maximum number of recipients
Timeout time.Duration
TLSConfig *tls.Config
TLSListener bool // Listen for incoming TLS connections only (not recommended as it may reduce compatibility). Ignored if TLS is not configured.
TLSRequired bool // Require TLS for every command except NOOP, EHLO, STARTTLS, or QUIT as per RFC 3207. Ignored if TLS is not configured.
}
// ConfigureTLS creates a TLS configuration from certificate and key files.
func (srv *Server) ConfigureTLS(certFile string, keyFile string) error {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
return nil
}
// ConfigureTLSWithPassphrase creates a TLS configuration from a certificate,
// an encrypted key file and the associated passphrase:
func (srv *Server) ConfigureTLSWithPassphrase(
certFile string,
keyFile string,
passphrase string,
) error {
certPEMBlock, err := ioutil.ReadFile(certFile)
if err != nil {
return err
}
keyPEMBlock, err := ioutil.ReadFile(keyFile)
if err != nil {
return err
}
keyDERBlock, _ := pem.Decode(keyPEMBlock)
keyPEMDecrypted, err := x509.DecryptPEMBlock(keyDERBlock, []byte(passphrase))
if err != nil {
return err
}
var pemBlock pem.Block
pemBlock.Type = keyDERBlock.Type
pemBlock.Bytes = keyPEMDecrypted
keyPEMBlock = pem.EncodeToMemory(&pemBlock)
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return err
}
srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
return nil
}
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections. If
// srv.Addr is blank, ":25" is used.
func (srv *Server) ListenAndServe() error {
if srv.Addr == "" {
srv.Addr = ":25"
}
if srv.Appname == "" {
srv.Appname = "smtpd"
}
if srv.Hostname == "" {
srv.Hostname, _ = os.Hostname()
}
if srv.Timeout == 0 {
srv.Timeout = 5 * time.Minute
}
var ln net.Listener
var err error
// If TLSListener is enabled, listen for TLS connections only.
if srv.TLSConfig != nil && srv.TLSListener {
ln, err = tls.Listen("tcp", srv.Addr, srv.TLSConfig)
} else {
ln, err = net.Listen("tcp", srv.Addr)
}
if err != nil {
return err
}
return srv.Serve(ln)
}
// Serve creates a new SMTP session after a network connection is established.
func (srv *Server) Serve(ln net.Listener) error {
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
continue
}
return err
}
session := srv.newSession(conn)
go session.serve()
}
}
type session struct {
srv *Server
conn net.Conn
br *bufio.Reader
bw *bufio.Writer
remoteIP string // Remote IP address
remoteHost string // Remote hostname according to reverse DNS lookup
remoteName string // Remote hostname as supplied with EHLO
tls bool
authenticated bool
}
// Create new session from connection.
func (srv *Server) newSession(conn net.Conn) (s *session) {
s = &session{
srv: srv,
conn: conn,
br: bufio.NewReader(conn),
bw: bufio.NewWriter(conn),
}
// Get remote end info for the Received header.
s.remoteIP, _, _ = net.SplitHostPort(s.conn.RemoteAddr().String())
names, err := net.LookupAddr(s.remoteIP)
if err == nil && len(names) > 0 {
s.remoteHost = names[0]
} else {
s.remoteHost = "unknown"
}
// Set tls = true if TLS is already in use.
_, s.tls = s.conn.(*tls.Conn)
return
}
// Function called to handle connection requests.
func (s *session) serve() {
defer s.conn.Close()
var from string
var gotFrom bool
var to []string
var buffer bytes.Buffer
// Send banner.
s.writef("220 %s %s ESMTP Service ready", s.srv.Hostname, s.srv.Appname)
loop:
for {
// Attempt to read a line from the socket.
// On timeout, send a timeout message and return from serve().
// On error, assume the client has gone away i.e. return from serve().
line, err := s.readLine()
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
s.writef("421 4.4.2 %s %s ESMTP Service closing transmission channel after timeout exceeded", s.srv.Hostname, s.srv.Appname)
}
break
}
verb, args := s.parseLine(line)
switch verb {
case "HELO":
s.remoteName = args
s.writef("250 %s greets %s", s.srv.Hostname, s.remoteName)
// RFC 2821 section 4.1.4 specifies that EHLO has the same effect as RSET, so reset for HELO too.
from = ""
gotFrom = false
to = nil
buffer.Reset()
case "EHLO":
s.remoteName = args
s.writef(s.makeEHLOResponse())
// RFC 2821 section 4.1.4 specifies that EHLO has the same effect as RSET.
from = ""
gotFrom = false
to = nil
buffer.Reset()
case "MAIL":
if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls {
s.writef("530 5.7.0 Must issue a STARTTLS command first")
break
}
if s.srv.AuthHandler != nil && s.srv.AuthRequired && !s.authenticated {
s.writef("530 5.7.0 Authentication required")
break
}
match := mailFromRE.FindStringSubmatch(args)
if match == nil {
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid FROM parameter)")
} else {
// Validate the SIZE parameter if one was sent.
if len(match[2]) > 0 { // A parameter is present
sizeMatch := mailSizeRE.FindStringSubmatch(match[3])
if sizeMatch == nil {
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid SIZE parameter)")
} else {
// Enforce the maximum message size if one is set.
size, err := strconv.Atoi(sizeMatch[1])
if err != nil { // Bad SIZE parameter
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid SIZE parameter)")
} else if s.srv.MaxSize > 0 && size > s.srv.MaxSize { // SIZE above maximum size, if set
err = maxSizeExceeded(s.srv.MaxSize)
s.writef(err.Error())
} else { // SIZE ok
from = match[1]
gotFrom = true
s.writef("250 2.1.0 Ok")
}
}
} else { // No parameters after FROM
from = match[1]
gotFrom = true
s.writef("250 2.1.0 Ok")
}
}
to = nil
buffer.Reset()
case "RCPT":
if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls {
s.writef("530 5.7.0 Must issue a STARTTLS command first")
break
}
if s.srv.AuthHandler != nil && s.srv.AuthRequired && !s.authenticated {
s.writef("530 5.7.0 Authentication required")
break
}
if !gotFrom {
s.writef("503 5.5.1 Bad sequence of commands (MAIL required before RCPT)")
break
}
match := rcptToRE.FindStringSubmatch(args)
if match == nil {
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid TO parameter)")
} else {
// RFC 5321 specifies 100 minimum recipients
// but I give a shit of RFC5321
if len(to) == s.srv.MaxRcpt {
s.writef("452 4.5.3 Too many recipients, %d > %d ", len(to), s.srv.MaxRcpt)
} else {
accept := true
if s.srv.HandlerRcpt != nil {
accept = s.srv.HandlerRcpt(s.conn.RemoteAddr(), from, match[1])
}
if accept {
to = append(to, match[1])
s.writef("250 2.1.5 Ok")
} else {
s.writef("550 5.1.0 Requested action not taken: mailbox unavailable")
}
}
}
case "DATA":
if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls {
s.writef("530 5.7.0 Must issue a STARTTLS command first")
break
}
if s.srv.AuthHandler != nil && s.srv.AuthRequired && !s.authenticated {
s.writef("530 5.7.0 Authentication required")
break
}
if !gotFrom || len(to) == 0 {
s.writef("503 5.5.1 Bad sequence of commands (MAIL & RCPT required before DATA)")
break
}
s.writef("354 Start mail input; end with <CR><LF>.<CR><LF>")
// Attempt to read message body from the socket.
// On timeout, send a timeout message and return from serve().
// On net.Error, assume the client has gone away i.e. return from serve().
// On other errors, allow the client to try again.
data, err := s.readData()
if err != nil {
switch err.(type) {
case net.Error:
if err.(net.Error).Timeout() {
s.writef("421 4.4.2 %s %s ESMTP Service closing transmission channel after timeout exceeded", s.srv.Hostname, s.srv.Appname)
}
break loop
case maxSizeExceededError:
s.writef(err.Error())
continue
default:
s.writef("451 4.3.0 Requested action aborted: local error in processing")
continue
}
}
// Create Received header & write message body into buffer.
buffer.Reset()
buffer.Write(s.makeHeaders(to))
buffer.Write(data)
s.writef("250 2.0.0 Ok: queued")
// Pass mail on to handler.
if s.srv.Handler != nil {
go s.srv.Handler(s.conn.RemoteAddr(), from, to, buffer.Bytes())
}
// Reset for next mail.
from = ""
gotFrom = false
to = nil
buffer.Reset()
case "QUIT":
s.writef("221 2.0.0 %s %s ESMTP Service closing transmission channel", s.srv.Hostname, s.srv.Appname)
break loop
case "RSET":
if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls {
s.writef("530 5.7.0 Must issue a STARTTLS command first")
break
}
s.writef("250 2.0.0 Ok")
from = ""
gotFrom = false
to = nil
buffer.Reset()
case "NOOP":
s.writef("250 2.0.0 Ok")
case "HELP", "VRFY", "EXPN":
// See RFC 5321 section 4.2.4 for usage of 500 & 502 response codes.
s.writef("502 5.5.1 Command not implemented")
case "STARTTLS":
// Parameters are not allowed (RFC 3207 section 4).
if args != "" {
s.writef("501 5.5.2 Syntax error (no parameters allowed)")
break
}
// Handle case where TLS is requested but not configured (and therefore not listed as a service extension).
if s.srv.TLSConfig == nil {
s.writef("502 5.5.1 Command not implemented")
break
}
// Handle case where STARTTLS is received when TLS is already in use.
if s.tls {
s.writef("503 5.5.1 Bad sequence of commands (TLS already in use)")
break
}
s.writef("220 2.0.0 Ready to start TLS")
// Establish a TLS connection with the client.
tlsConn := tls.Server(s.conn, s.srv.TLSConfig)
err := tlsConn.Handshake()
if err != nil {
s.writef("403 4.7.0 TLS handshake failed")
break
}
// TLS handshake succeeded, switch to using the TLS connection.
s.conn = tlsConn
s.br = bufio.NewReader(s.conn)
s.bw = bufio.NewWriter(s.conn)
s.tls = true
// RFC 3207 specifies that the server must discard any prior knowledge obtained from the client.
s.remoteName = ""
from = ""
gotFrom = false
to = nil
buffer.Reset()
case "AUTH":
if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls {
s.writef("530 5.7.0 Must issue a STARTTLS command first")
break
}
// Handle case where AUTH is requested but not configured (and therefore not listed as a service extension).
if s.srv.AuthHandler == nil {
s.writef("502 5.5.1 Command not implemented")
break
}
// Handle case where AUTH is received when already authenticated.
if s.authenticated {
s.writef("503 5.5.1 Bad sequence of commands (already authenticated for this session)")
break
}
// RFC 4954 specifies that AUTH is not permitted during mail transactions.
if gotFrom || len(to) > 0 {
s.writef("503 5.5.1 Bad sequence of commands (AUTH not permitted during mail transaction)")
break
}
// RFC 4954 requires a mechanism parameter.
authType, authArgs := s.parseLine(args)
if authType == "" {
s.writef("501 5.5.4 Malformed AUTH input (argument required)")
break
}
// RFC 4954 requires rejecting unsupported authentication mechanisms with a 504 response.
allowedAuth := s.authMechs()
if allowed, found := allowedAuth[authType]; !found || !allowed {
s.writef("504 5.5.4 Unrecognized authentication type")
break
}
// RFC 4954 also specifies that ESMTP code 5.5.4 ("Invalid command arguments") should be returned
// when attempting to use an unsupported authentication type.
// Many servers return 5.7.4 ("Security features not supported") instead.
switch authType {
case "PLAIN":
s.authenticated, err = s.handleAuthPlain(authArgs)
case "LOGIN":
s.authenticated, err = s.handleAuthLogin(authArgs)
case "CRAM-MD5":
s.authenticated, err = s.handleAuthCramMD5()
}
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
s.writef("421 4.4.2 %s %s ESMTP Service closing transmission channel after timeout exceeded", s.srv.Hostname, s.srv.Appname)
break loop
}
s.writef(err.Error())
break
}
if s.authenticated {
s.writef("235 2.7.0 Authentication successful")
} else {
s.writef("535 5.7.8 Authentication credentials invalid")
}
default:
// See RFC 5321 section 4.2.4 for usage of 500 & 502 response codes.
s.writef("500 5.5.2 Syntax error, command unrecognized")
}
}
}
// Wrapper function for writing a complete line to the socket.
func (s *session) writef(format string, args ...interface{}) error {
if s.srv.Timeout > 0 {
s.conn.SetWriteDeadline(time.Now().Add(s.srv.Timeout))
}
line := fmt.Sprintf(format, args...)
fmt.Fprintf(s.bw, line+"\r\n")
err := s.bw.Flush()
if Debug {
verb := "WROTE"
if s.srv.LogWrite != nil {
s.srv.LogWrite(s.remoteIP, verb, line)
} else {
log.Println(s.remoteIP, verb, line)
}
}
return err
}
// Read a complete line from the socket.
func (s *session) readLine() (string, error) {
if s.srv.Timeout > 0 {
s.conn.SetReadDeadline(time.Now().Add(s.srv.Timeout))
}
line, err := s.br.ReadString('\n')
if err != nil {
return "", err
}
line = strings.TrimSpace(line) // Strip trailing \r\n
if Debug {
verb := "READ"
if s.srv.LogRead != nil {
s.srv.LogRead(s.remoteIP, verb, line)
} else {
log.Println(s.remoteIP, verb, line)
}
}
return line, err
}
// Parse a line read from the socket.
func (s *session) parseLine(line string) (verb string, args string) {
if idx := strings.Index(line, " "); idx != -1 {
verb = strings.ToUpper(line[:idx])
args = strings.TrimSpace(line[idx+1:])
} else {
verb = strings.ToUpper(line)
args = ""
}
return verb, args
}
// Read the message data following a DATA command.
func (s *session) readData() ([]byte, error) {
var data []byte
for {
if s.srv.Timeout > 0 {
s.conn.SetReadDeadline(time.Now().Add(s.srv.Timeout))
}
line, err := s.br.ReadBytes('\n')
if err != nil {
return nil, err
}
// Handle end of data denoted by lone period (\r\n.\r\n)
if bytes.Equal(line, []byte(".\r\n")) {
break
}
// Remove leading period (RFC 5321 section 4.5.2)
if line[0] == '.' {
line = line[1:]
}
// Enforce the maximum message size limit.
if s.srv.MaxSize > 0 {
if len(data)+len(line) > s.srv.MaxSize {
_, _ = s.br.Discard(s.br.Buffered()) // Discard the buffer remnants.
return nil, maxSizeExceeded(s.srv.MaxSize)
}
}
data = append(data, line...)
}
return data, nil
}
// Create the Received header to comply with RFC 2821 section 3.8.2.
// TODO: Work out what to do with multiple to addresses.
func (s *session) makeHeaders(to []string) []byte {
var buffer bytes.Buffer
now := time.Now().Format("Mon, _2 Jan 2006 15:04:05 -0700 (MST)")
buffer.WriteString(fmt.Sprintf("Received: from %s (%s [%s])\r\n", s.remoteName, s.remoteHost, s.remoteIP))
buffer.WriteString(fmt.Sprintf(" by %s (%s) with SMTP\r\n", s.srv.Hostname, s.srv.Appname))
buffer.WriteString(fmt.Sprintf(" for <%s>; %s\r\n", to[0], now))
return buffer.Bytes()
}
// Determine allowed authentication mechanisms.
// RFC 4954 specifies that plaintext authentication mechanisms such as LOGIN and PLAIN require a TLS connection.
// This can be explicitly overridden e.g. setting s.srv.AuthMechs["LOGIN"] = true.
func (s *session) authMechs() (mechs map[string]bool) {
mechs = map[string]bool{"LOGIN": s.tls, "PLAIN": s.tls, "CRAM-MD5": true}
for mech := range mechs {
allowed, found := s.srv.AuthMechs[mech]
if found {
mechs[mech] = allowed
}
}
return
}
// Create the greeting string sent in response to an EHLO command.
func (s *session) makeEHLOResponse() (response string) {
response = fmt.Sprintf("250-%s greets %s\r\n", s.srv.Hostname, s.remoteName)
// RFC 1870 specifies that "SIZE 0" indicates no maximum size is in force.
response += fmt.Sprintf("250-SIZE %d\r\n", s.srv.MaxSize)
// Only list STARTTLS if TLS is configured, but not currently in use.
if s.srv.TLSConfig != nil && !s.tls {
response += "250-STARTTLS\r\n"
}
// Only list AUTH if an AuthHandler is configured and at least one mechanism is allowed.
if s.srv.AuthHandler != nil {
var mechs []string
for mech, allowed := range s.authMechs() {
if allowed {
mechs = append(mechs, mech)
}
}
if len(mechs) > 0 {
response += "250-AUTH " + strings.Join(mechs, " ") + "\r\n"
}
}
response += "250 ENHANCEDSTATUSCODES"
return
}
func (s *session) handleAuthLogin(arg string) (bool, error) {
var err error
if arg == "" {
s.writef("334 " + base64.StdEncoding.EncodeToString([]byte("Username:")))
arg, err = s.readLine()
if err != nil {
return false, err
}
}
username, err := base64.StdEncoding.DecodeString(arg)
if err != nil {
return false, errors.New("501 5.5.2 Syntax error (unable to decode)")
}
s.writef("334 " + base64.StdEncoding.EncodeToString([]byte("Password:")))
line, err := s.readLine()
if err != nil {
return false, err
}
password, err := base64.StdEncoding.DecodeString(line)
if err != nil {
return false, errors.New("501 5.5.2 Syntax error (unable to decode)")
}
// Validate credentials.
authenticated, err := s.srv.AuthHandler(s.conn.RemoteAddr(), "LOGIN", username, password, nil)
return authenticated, err
}
func (s *session) handleAuthPlain(arg string) (bool, error) {
var err error
// If fast mode (AUTH PLAIN [arg]) is not used, prompt for credentials.
if arg == "" {
s.writef("334 ")
arg, err = s.readLine()
if err != nil {
return false, err
}
}
data, err := base64.StdEncoding.DecodeString(arg)
if err != nil {
return false, errors.New("501 5.5.2 Syntax error (unable to decode)")
}
parts := bytes.Split(data, []byte{0})
if len(parts) != 3 {
return false, errors.New("501 5.5.2 Syntax error (unable to parse)")
}
// Validate credentials.
authenticated, err := s.srv.AuthHandler(s.conn.RemoteAddr(), "PLAIN", parts[1], parts[2], nil)
return authenticated, err
}
func (s *session) handleAuthCramMD5() (bool, error) {
shared := "<" + strconv.Itoa(os.Getpid()) + "." + strconv.Itoa(time.Now().Nanosecond()) + "@" + s.srv.Hostname + ">"
s.writef("334 " + base64.StdEncoding.EncodeToString([]byte(shared)))
data, err := s.readLine()
if err != nil {
return false, err
}
if data == "*" {
return false, errors.New("501 5.7.0 Authentication cancelled")
}
buf, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return false, errors.New("501 5.5.2 Syntax error (unable to decode)")
}
fields := strings.Split(string(buf), " ")
if len(fields) < 2 {
return false, errors.New("501 5.5.2 Syntax error (unable to parse)")
}
// Validate credentials.
authenticated, err := s.srv.AuthHandler(s.conn.RemoteAddr(), "CRAM-MD5", []byte(fields[0]), []byte(fields[1]), []byte(shared))
return authenticated, err
}