maddy/smtp.go
fox.cpp ee553a4cc4 Rework logging
Implement debug log (can be enabled using `debug` config directive)
Remove errors directive for IMAP endpoint module.
2019-03-30 17:34:19 +02:00

379 lines
9.5 KiB
Go

package maddy
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/mail"
"os"
"strings"
"sync"
"github.com/emersion/go-smtp"
"github.com/emersion/maddy/config"
"github.com/emersion/maddy/log"
"github.com/emersion/maddy/module"
)
type SMTPUser struct {
endp *SMTPEndpoint
anonymous bool
username string
state *smtp.ConnectionState
}
func (u SMTPUser) Send(from string, to []string, r io.Reader) error {
ctx := module.DeliveryContext{
From: from,
To: to,
Anonymous: u.anonymous,
AuthUser: u.username,
SrcTLSState: u.state.TLS,
SrcHostname: u.state.Hostname,
SrcAddr: u.state.RemoteAddr,
OurHostname: u.endp.serv.Domain,
Ctx: make(map[string]interface{}),
}
// Check if TLS connection state struct is poplated.
// If it is - we are using TLS.
if u.state.TLS.Version != 0 {
ctx.SrcProto = "ESMTPS"
} else {
ctx.SrcProto = "ESMTP"
}
// TODO: Execute pipeline steps in parallel.
// https://github.com/emersion/maddy/pull/17#discussion_r267573580
var buf bytes.Buffer
if _, err := buf.ReadFrom(r); err != nil {
return err
}
currentMsg := bytes.NewReader(buf.Bytes())
for _, step := range u.endp.pipeline {
r, cont, err := step.Pass(&ctx, currentMsg)
if !cont {
if err == module.ErrSilentDrop {
return nil
}
if err != nil {
u.endp.Log.Printf("%T failed: %v", step, err)
return err
}
}
if r != nil && r != io.Reader(currentMsg) {
buf.Reset()
if _, err := io.Copy(&buf, r); err != nil {
u.endp.Log.Printf("failed to buffer message: %v", err)
return err
}
currentMsg.Reset(buf.Bytes())
}
currentMsg.Seek(0, io.SeekStart)
}
u.endp.Log.Printf("accepted incoming message from %s (%s)", ctx.SrcHostname, ctx.SrcAddr)
return nil
}
func (u SMTPUser) Logout() error {
return nil
}
type SMTPEndpoint struct {
Auth module.AuthProvider
serv *smtp.Server
name string
listeners []net.Listener
pipeline []SMTPPipelineStep
submission bool
listenersWg sync.WaitGroup
debug bool
Log log.Logger
}
func (endp *SMTPEndpoint) Name() string {
return "smtp"
}
func (endp *SMTPEndpoint) InstanceName() string {
return endp.name
}
func (endp *SMTPEndpoint) Version() string {
return VersionStr
}
func NewSMTPEndpoint(instName string, globalCfg map[string]config.Node, cfg config.Node) (module.Module, error) {
endp := &SMTPEndpoint{
name: instName,
Log: log.Logger{Out: log.StderrLog, Name: "smtp"},
}
endp.serv = smtp.NewServer(endp)
endp.name = instName
endp.serv = smtp.NewServer(endp)
if err := endp.setConfig(globalCfg, cfg); err != nil {
return nil, err
}
endp.Log.Debugf("authentication provider: %s %s", endp.Auth.(module.Module).Name(), endp.Auth.(module.Module).InstanceName())
endp.Log.Debugf("pipeline: %#v", endp.pipeline)
addresses := make([]Address, 0, len(cfg.Args))
for _, addr := range cfg.Args {
saddr, err := standardizeAddress(addr)
if err != nil {
return nil, fmt.Errorf("smtp: invalid address: %s", addr)
}
addresses = append(addresses, saddr)
}
if err := endp.setupListeners(addresses); err != nil {
for _, l := range endp.listeners {
l.Close()
}
return nil, err
}
if endp.serv.AllowInsecureAuth {
endp.Log.Println("authentication over unencrypted connections is allowed, this is insecure configuration and should be used only for testing!")
}
if endp.serv.TLSConfig == nil && !endp.serv.LMTP {
endp.Log.Println("TLS is disabled, this is insecure configuration and should be used only for testing!")
endp.serv.AllowInsecureAuth = true
}
return endp, nil
}
func (endp *SMTPEndpoint) setConfig(globalCfg map[string]config.Node, rawCfg config.Node) error {
var (
err error
ioDebug bool
localDeliveryDefault string
localDeliveryOpts map[string]string
remoteDeliveryDefault string
remoteDeliveryOpts map[string]string
)
cfg := config.Map{}
cfg.Custom("auth", false, true, defaultAuthProvider, authDirective, &endp.Auth)
cfg.String("hostname", true, true, "", &endp.serv.Domain)
cfg.Int("max_idle", false, false, 60, &endp.serv.MaxIdleSeconds)
cfg.Int("max_message_size", false, false, 32*1024*1024, &endp.serv.MaxMessageBytes)
cfg.Int("max_recipients", false, false, 255, &endp.serv.MaxRecipients)
cfg.Custom("tls", true, true, nil, tlsDirective, &endp.serv.TLSConfig)
cfg.Bool("insecure_auth", false, &endp.serv.AllowInsecureAuth)
cfg.Bool("io_debug", false, &ioDebug)
cfg.Bool("debug", true, &endp.Log.Debug)
cfg.Bool("submission", false, &endp.submission)
cfg.AllowUnknown()
remainingDirs, err := cfg.Process(globalCfg, &rawCfg)
if err != nil {
return err
}
for _, entry := range remainingDirs {
switch entry.Name {
case "local_delivery":
if len(entry.Args) == 0 {
return errors.New("smtp: local_delivery: expected at least 1 argument")
}
if len(endp.pipeline) != 0 {
return errors.New("smtp: can't use custom pipeline with local_delivery or remote_delivery")
}
localDeliveryDefault = entry.Args[0]
localDeliveryOpts = readOpts(entry.Args[1:])
case "remote_delivery":
if len(entry.Args) == 0 {
return errors.New("smtp: remote_delivery: expected at least 1 argument")
}
if len(endp.pipeline) != 0 {
return errors.New("smtp: can't use custom pipeline with local_delivery or remote_delivery")
}
remoteDeliveryDefault = entry.Args[0]
remoteDeliveryOpts = readOpts(entry.Args[1:])
case "filter", "delivery", "match", "stop", "require_auth":
if localDeliveryDefault != "" || remoteDeliveryDefault != "" {
return errors.New("smtp: can't use custom pipeline with local_delivery or remote_delivery")
}
step, err := StepFromCfg(entry)
if err != nil {
return err
}
endp.pipeline = append(endp.pipeline, step)
default:
return fmt.Errorf("smtp: unknown config directive: %v", entry.Name)
}
}
if len(endp.pipeline) == 0 {
err := endp.setDefaultPipeline(localDeliveryDefault, remoteDeliveryDefault, localDeliveryOpts, remoteDeliveryOpts)
if err != nil {
return err
}
}
if endp.submission {
endp.pipeline = append([]SMTPPipelineStep{submissionPrepareStep{}, requireAuthStep{}}, endp.pipeline...)
}
if ioDebug {
endp.serv.Debug = os.Stderr
}
return nil
}
func (endp *SMTPEndpoint) setupListeners(addresses []Address) error {
var smtpUsed, lmtpUsed bool
for _, addr := range addresses {
if addr.Scheme == "smtp" || addr.Scheme == "smtps" {
if lmtpUsed {
return errors.New("smtp: can't mix LMTP with SMTP in one endpoint block")
}
smtpUsed = true
}
if addr.Scheme == "lmtp+unix" || addr.Scheme == "lmtp" {
if smtpUsed {
return errors.New("smtp: can't mix LMTP with SMTP in one endpoint block")
}
lmtpUsed = true
}
var l net.Listener
var err error
l, err = net.Listen(addr.Network(), addr.Address())
if err != nil {
return fmt.Errorf("failed to bind on %v: %v", addr, err)
}
endp.Log.Printf("listening on %v", addr)
if addr.IsTLS() {
if endp.serv.TLSConfig == nil {
return errors.New("smtp: can't bind on SMTPS endpoint without TLS configuration")
}
l = tls.NewListener(l, endp.serv.TLSConfig)
}
endp.listeners = append(endp.listeners, l)
endp.listenersWg.Add(1)
addr := addr
go func() {
if err := endp.serv.Serve(l); err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") {
endp.Log.Printf("failed to serve %s: %s", addr, err)
}
endp.listenersWg.Done()
}()
}
if lmtpUsed {
endp.serv.LMTP = true
}
return nil
}
func (endp *SMTPEndpoint) setDefaultPipeline(localDeliveryName, remoteDeliveryName string, localOpts, remoteOpts map[string]string) error {
var err error
var localDelivery module.DeliveryTarget
if localDeliveryName == "" {
localDelivery, err = deliveryTarget("default_local_delivery")
if err != nil {
localDelivery, err = deliveryTarget("default")
if err != nil {
return err
}
}
localOpts = map[string]string{"local_only": ""}
} else {
localDelivery, err = deliveryTarget(localDeliveryName)
if err != nil {
return err
}
}
if endp.submission {
if remoteDeliveryName == "" {
remoteDeliveryName = "default_remote_delivery"
remoteOpts = map[string]string{"remote_only": ""}
}
remoteDelivery, err := deliveryTarget(remoteDeliveryName)
if err != nil {
return err
}
endp.pipeline = append(endp.pipeline,
// require_auth and submission_check are always prepended to pipeline
//TODO: DKIM sign
deliveryStep{t: localDelivery, opts: localOpts},
deliveryStep{t: remoteDelivery, opts: remoteOpts},
)
} else {
endp.pipeline = append(endp.pipeline,
//TODO: DKIM verify
deliveryStep{t: localDelivery, opts: localOpts},
)
}
return nil
}
func (endp *SMTPEndpoint) Login(state *smtp.ConnectionState, username, password string) (smtp.User, error) {
if !endp.Auth.CheckPlain(username, password) {
return nil, errors.New("Invalid credentials")
}
return SMTPUser{
endp: endp,
username: username,
state: state,
}, nil
}
func (endp *SMTPEndpoint) AnonymousLogin(state *smtp.ConnectionState) (smtp.User, error) {
return SMTPUser{
endp: endp,
anonymous: true,
state: state,
}, nil
}
func formatAddr(raw string) string {
addr := mail.Address{Address: raw}
return addr.String()
}
func addReturnPath(ctx module.DeliveryContext, msg io.Reader) io.Reader {
hdr := "Return-Path: " + formatAddr(ctx.From) + "\r\n"
return io.MultiReader(strings.NewReader(hdr), msg)
}
func (endp *SMTPEndpoint) Close() error {
for _, l := range endp.listeners {
l.Close()
}
endp.serv.Close()
endp.listenersWg.Wait()
return nil
}
func init() {
module.Register("smtp", NewSMTPEndpoint)
}