Drop most of the implicit defaults in favor of explicit configuration (#43)

* Drop most of the implicit defaults in favor of explicit configuration

We no longer follow caddy's "zero-configuration" approach. Mail is much
more complex than HTTP and we want to be explicit about things, always.

* Remove commented out directives from maddy.conf
This commit is contained in:
fox.cpp 2019-04-13 09:28:45 +00:00 committed by Simon Ser
parent d10fbd0a9e
commit 7db67acad8
9 changed files with 78 additions and 278 deletions

View file

@ -23,14 +23,12 @@ Valid configuration directives and their forms:
Generate a self-signed certificate on startup. Useful only for testing.
* `auth <instance_name>`
Use the specified authentication provider module instead of default-auth or
default. `instance_name` is the name of the corresponding configuration
block.
Use the specified authentication provider module. `instance_name` is the name
of the corresponding configuration block. **Required.**
* `storage <instance_name>`
Use the specified storage backend module instead of default-storage or
default. `instance_name` is the name of the corresponding configuration
block.
Use the specified storage backend module. `instance_name` is the name of the
corresponding configuration block. **Required.**
* `insecure_auth`
Allow plaintext authentication over unprotected (unencrypted) connections.
@ -77,16 +75,16 @@ Valid configuration directives and their forms:
Generate a self-signed certificate on startup. Useful only for testing.
* `auth <instance_name>`
Use the specified authentication provider module instead of default-auth or
default. `instance_name` is the name of the corresponding configuration
block.
Use the specified authentication provider module. `instance_name` is the name
of the corresponding configuration block. **Required.**
* `insecure_auth`
Allow plaintext authentication over unprotected (unencrypted)
connections. Use only for testing!
* `submission`
When no pipeline is specified - use submission pipeline instead of relay.
Preprocess messages before pushing them to pipeline and require
authentication for all operations.
You should use it for Submission protocol endpoints.
* `io_debug`
@ -108,46 +106,26 @@ Valid configuration directives and their forms:
* `max_message_size <value>`
Limit size of incoming messages to `value` bytes. Default is 32 MiB.
* `local_delivery <instance_name> [opts]`
Replace delivery target for local email without replace the whole pipeline
(with DKIM and stuff).
* `remote_delivery <instance_name> [opts]`
Replace delivery target for non-local email without replace the whole pipeline
(with DKIM and stuff).
```
smtp smtp://0.0.0.0:25 smtps://0.0.0.0:587 {
tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key
auth pam
hostname emersion.fr
deliver dummy
}
```
### 'submission' module
Alias to smtp module with submission pipeline used by default.
Alias to smtp module with submission directive used by default.
##### SMTP pipeline
SMTP module does have a flexible mechanism that allows you to define a custom
sequence of actions to apply on each incoming message.
By default, it just passes emails with recipients with domain same as the
specified hostname to `default_delivery` or `default` delivery target (usually IMAP
mailbox). If the message does have non-local recipients it will be passed to
message queue for outgoing transfer.
Here are configuration directives doing the same (almost):
```
deliver default local_only
deliver out_queue remote_only
```
You can add any number of steps you want using following directives (note that
if you specify any of them default steps will not be used so you need to
specify them explicitly!)
You can add any number of steps you want using following directives:
* `filter <instnace_name> [opts]`
Apply a "filter" to a message, `instance_name` is the configuration set name.
You can pass additional parameters to filter by adding key=value pairs to the
@ -198,7 +176,6 @@ specify them explicitly!)
```
smtp smtp://0.0.0.0:25 smtps://0.0.0.0:587 {
tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key
auth pam
@ -211,7 +188,7 @@ smtp smtp://0.0.0.0:25 smtps://0.0.0.0:587 {
match no rcpt "/@emersion.fr$/" {
require_auth
filter dkim sign
deliver out-queue
deliver out_queue
}
}
```
@ -261,7 +238,7 @@ code is 1 - authentication is failed. If status code is 2 - other unrelated
error happened. Additional information should be written to stderr.
```
extauth default_auth
extauth
```
Valid configuration directives:

View file

@ -23,6 +23,12 @@ Build tags:
Read from `/etc/maddy/maddy.conf` by default.
Start by copying contents of the [maddy.conf][maddy.conf] in this repository.
With this configuration, maddy will create an SQLite3 database for messages in
/var/lib/maddy and use it to store all messages. You need to ensure that this
directory exists and maddy can write to it.
### Syntax
Maddy uses configuration format similar (but not the same!) to Caddy's
@ -70,7 +76,6 @@ are options that can be used like that:
Default TLS certificate to use. See
[CONFIG_REFERENCE.md](CONFIG_REFERENCE.md) for details.
<<<<<<< HEAD
* `debug`
Write verbose logs describing what exactly is happening and how its going.
Default mode is relatively quiet and still produces useful logs so
@ -114,57 +119,6 @@ These can be specified only outside of any configuration block.
go build --ldflags '-X github.com/emersion/maddy.defaultLibexecDirectory=/opt/maddy/bin'
```
### Defaults
Maddy provides reasonable defaults so you can start using it without spending
hours writing configuration files. All you need it so define smtp and imap
modules in your configuration, configure TLS (see below) and set domain name.
Here is the minimal example to get you started:
```
tls cert_file pkey_file
hostname emersion.fr
imap imap://0.0.0.0 imaps://0.0.0.0
smtp smtp://0.0.0.0:25
submission smtp://0.0.0.0:587 smtps://0.0.0.0:465
```
Don't forget to use actual values instead of placeholders.
With this configuration, maddy will create an SQLite3 database for messages in
/var/lib/maddy and use it to store all messages. You need to ensure that this
directory exists and maddy can write to it.
### go-imap-sql: Database location
If you don't like SQLite3 or don't want to have it in /var/lib/maddy,
you can override the configuration of the default module.
See [go-imap-sql repository](https://github.com/foxcpp/go-imap-sql) for
information on RDBMS support.
```
sql default {
driver sqlite3
dsn file_path
}
```
You can then replace SQL driver and DSN values. Note that maddy needs to be
built with a build tag corresponding to the name of the used driver (`mysql`,
`postgresql`) for SQL engines other than sqlite3.
DSN is a driver-specific value that describes the database to use.
For SQLite3 this is just a file path.
For MySQL: https://github.com/go-sql-driver/mysql#dsn-data-source-name
For PostgreSQL: https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
Note that you can also change default DSN or SQL driver during compilation
by building maddy using following command:
```shell
go build -ldflags "-X github,com/emersion/maddy.defaultDriver=DRIVER -X github.com/emersion/maddy.defaultDsn=DSN"
```
### TLS
Currently, maddy doesn't implement any form of automatic TLS like Caddy. But

View file

@ -100,28 +100,6 @@ func deliverDirective(m *config.Map, node *config.Node) (interface{}, error) {
return modObj, nil
}
func defaultAuthProvider() (interface{}, error) {
res, err := authProvider("default_auth")
if err != nil {
res, err = authProvider("default")
if err != nil {
return nil, errors.New("missing default auth. provider, must set custom")
}
}
return res, nil
}
func defaultStorage() (interface{}, error) {
res, err := storageBackend("default_storage")
if err != nil {
res, err = storageBackend("default")
if err != nil {
return nil, errors.New("missing default storage backend, must set custom")
}
}
return res, nil
}
func logOutput(m *config.Map, node *config.Node) (interface{}, error) {
if len(node.Args) == 0 {
return nil, m.MatchErr("expected at least 1 argument")

View file

@ -376,7 +376,7 @@ func (m *Map) ProcessWith(globalCfg map[string]interface{}, block []Node) (unmat
}
matched[subnode.Name] = true
}
m.curNode = nil
m.curNode = m.Block
for _, matcher := range m.entries {
if matched[matcher.name] {

View file

@ -1,49 +0,0 @@
package maddy
import (
"database/sql"
"fmt"
"path/filepath"
"github.com/emersion/maddy/config"
"github.com/emersion/maddy/module"
)
var defaultDriver = "sqlite3"
var defaultDsn string
func createDefaultStorage(globals *config.Map, _ string) (module.Module, error) {
driverSupported := false
for _, driver := range sql.Drivers() {
if driver == defaultDriver {
driverSupported = true
}
}
if !driverSupported {
return nil, fmt.Errorf("maddy is not compiled with %s support", defaultDriver)
}
return NewSQLStorage("sql", "default")
}
func defaultStorageConfig(globals *config.Map, name string) config.Node {
return config.Node{
Name: "sql",
Args: []string{name},
Children: []config.Node{
{
Name: "driver",
Args: []string{defaultDriver},
},
{
Name: "dsn",
Args: []string{filepath.Join(StateDirectory(globals.Values), "maddy.db")},
},
},
}
}
func createDefaultRemoteDelivery(_ *config.Map, name string) (module.Module, error) {
return Dummy{instName: name}, nil
}

View file

@ -52,8 +52,8 @@ func (endp *IMAPEndpoint) Init(cfg *config.Map) error {
ioDebug bool
)
cfg.Custom("auth", false, false, defaultAuthProvider, authDirective, &endp.Auth)
cfg.Custom("storage", false, false, defaultStorage, storageDirective, &endp.Store)
cfg.Custom("auth", false, true, nil, authDirective, &endp.Auth)
cfg.Custom("storage", false, true, nil, storageDirective, &endp.Store)
cfg.Custom("tls", true, true, nil, tlsDirective, &endp.tlsConfig)
cfg.Bool("insecure_auth", false, &insecureAuth)
cfg.Bool("io_debug", false, &ioDebug)

40
maddy.conf Normal file
View file

@ -0,0 +1,40 @@
# Location of TLS certificate and private key. Global directive is used for all
# endpoints.
tls cert_file_path pkey_file
# hostname is used in several places, mainly in gretting for IMAP and SMTP.
# For now and is asummed to be equal to
# local domain and used to distishguish local and non-local recipients.
hostname example.org
# Create and initialize sql module, it provides simple authentication and
# storage backend using one database for everything.
sql {
driver sqlite3
dsn /var/lib/maddy/all.db
}
smtp smtp://0.0.0.0:25 {
# Deliver all mail for @example.org into sql module storage.
delivery sql local_only
}
submission smtps://0.0.0.0:465 smtp://0.0.0.0:587 {
# Use sql module for authentication.
auth sql
match rcpt_domain example.org {
# Deliver all mail for @example.org into sql module storage.
delivery sql local_only
}
match no rcpt_domain example.org {
# No remote delivery is implemented now, just deliver it to /dev/null for now.
delivery dummy
}
}
imap imaps://0.0.0.0:993 imap://0.0.0.0:143 {
# Use sql module for authentication.
auth sql
# And also for storage.
storage sql
}

View file

@ -62,9 +62,6 @@ func Start(cfg []config.Node) error {
instances[instName] = modInfo{instance: inst, cfg: block}
}
addDefaultModule(instances, globals, "default", createDefaultStorage, defaultStorageConfig)
addDefaultModule(instances, globals, "default_remote_delivery", createDefaultRemoteDelivery, nil)
for _, inst := range instances {
if module.Initialized[inst.instance.InstanceName()] {
log.Debugln("module init", inst.instance.Name(), inst.instance.InstanceName(), "skipped because it was lazily initialized before")
@ -98,21 +95,3 @@ func Start(cfg []config.Node) error {
return nil
}
func addDefaultModule(insts map[string]modInfo, globals *config.Map, name string, factory func(*config.Map, string) (module.Module, error), cfgFactory func(*config.Map, string) config.Node) {
if _, ok := insts[name]; !ok {
if mod, err := factory(globals, name); err != nil {
log.Printf("failed to register %s: %v", name, err)
} else {
log.Debugf("module create %s %s (built-in)", mod.Name(), name)
info := modInfo{instance: mod}
if cfgFactory != nil {
info.cfg = cfgFactory(globals, name)
}
module.RegisterInstance(mod, config.NewMap(globals.Values, &info.cfg))
insts[name] = info
}
} else {
log.Debugf("module create %s (built-in) skipped because user-defined exists", name)
}
}

109
smtp.go
View file

@ -119,7 +119,9 @@ func (endp *SMTPEndpoint) Init(cfg *config.Map) error {
return err
}
endp.Log.Debugf("authentication provider: %s %s", endp.Auth.(module.Module).Name(), endp.Auth.(module.Module).InstanceName())
if endp.Auth != nil {
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.Block.Args))
@ -152,20 +154,14 @@ func (endp *SMTPEndpoint) Init(cfg *config.Map) error {
func (endp *SMTPEndpoint) setConfig(cfg *config.Map) error {
var (
err error
ioDebug bool
submission bool
err error
ioDebug bool
writeTimeoutSecs uint
readTimeoutSecs uint
localDeliveryDefault string
localDeliveryOpts map[string]string
remoteDeliveryDefault string
remoteDeliveryOpts map[string]string
)
cfg.Custom("auth", false, false, defaultAuthProvider, authDirective, &endp.Auth)
cfg.Custom("auth", false, false, nil, authDirective, &endp.Auth)
cfg.String("hostname", true, false, "", &endp.serv.Domain)
// TODO: Parse human-readable duration values.
cfg.UInt("write_timeout", false, false, 60, &writeTimeoutSecs)
@ -176,7 +172,7 @@ func (endp *SMTPEndpoint) setConfig(cfg *config.Map) error {
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, &submission)
cfg.Bool("submission", false, &endp.submission)
cfg.AllowUnknown()
remainingDirs, err := cfg.Process()
@ -187,41 +183,9 @@ func (endp *SMTPEndpoint) setConfig(cfg *config.Map) error {
endp.serv.WriteTimeout = time.Duration(writeTimeoutSecs) * time.Second
endp.serv.ReadTimeout = time.Duration(readTimeoutSecs) * time.Second
// If endp.submission is set by NewSMTPEndpoint because module name is "submission"
// - don't use value from configuration.
if !endp.submission {
endp.submission = submission
} else if submission {
endp.Log.Println("redundant submission statement")
}
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", "deliver", "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
@ -238,15 +202,13 @@ func (endp *SMTPEndpoint) setConfig(cfg *config.Map) error {
}
}
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...)
endp.authAlwaysRequired = true
if endp.Auth == nil {
return fmt.Errorf("smtp: auth. provider must be set for submission endpoint")
}
}
if ioDebug {
@ -307,52 +269,11 @@ func (endp *SMTPEndpoint) setupListeners(addresses []Address) error {
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
deliverStep{t: localDelivery, opts: localOpts},
deliverStep{t: remoteDelivery, opts: remoteOpts},
)
} else {
endp.pipeline = append(endp.pipeline,
//TODO: DKIM verify
deliverStep{t: localDelivery, opts: localOpts},
)
}
return nil
}
func (endp *SMTPEndpoint) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
if endp.Auth == nil {
return nil, smtp.ErrAuthUnsupported
}
if !endp.Auth.CheckPlain(username, password) {
endp.Log.Printf("authentication failed for %s (from %v)", username, state.RemoteAddr)
return nil, errors.New("Invalid credentials")