Use safe defaults when settings are missing

This commit is contained in:
mdecimus
2024-03-30 18:12:40 +01:00
parent cb4d2f15ae
commit 35562bb9fd
120 changed files with 11732 additions and 2069 deletions

94
resources/config/build.py Normal file
View File

@@ -0,0 +1,94 @@
import os
# Define the scripts and their component files
scripts = {
"spam-filter": [
"config.sieve",
"prelude.sieve",
"from.sieve",
"recipient.sieve",
"subject.sieve",
"replyto.sieve",
"date.sieve",
"messageid.sieve",
"received.sieve",
"headers.sieve",
"bounce.sieve",
"html.sieve",
"mime.sieve",
"dmarc.sieve",
"ip.sieve",
"helo.sieve",
"replies_in.sieve",
"spamtrap.sieve",
"bayes_classify.sieve",
"url.sieve",
"rbl.sieve",
"pyzor.sieve",
"composites.sieve",
"scores.sieve",
"reputation.sieve",
"epilogue.sieve"
],
"track-replies": [
"config.sieve",
"replies_out.sieve"
],
"greylist": [
"config.sieve",
"greylist.sieve"
]
}
script_names = {
"spam-filter" : "Spam Filter",
"track-replies" : "Track Replies",
"greylist" : "Greylisting"
}
maps = ["scores.map",
"allow_dmarc.list",
"allow_domains.list",
"allow_spf_dkim.list",
"domains_disposable.list",
"domains_free.list",
"mime_types.map",
"url_redirectors.list"]
def read_and_concatenate(files):
content = ""
for file in files:
with open(os.path.join("./spamfilter/scripts", file), "r", encoding="utf-8") as f:
content += "\n#### Script " + file + " ####\n\n"
content += f.read() + "\n"
return content
def read_file(file):
with open(file, "r", encoding="utf-8") as f:
return f.read() + "\n"
def build_spam_filters(scripts):
spam_filter = read_file("./spamfilter/settings.toml")
for script_name, file_list in scripts.items():
script_content = read_and_concatenate(file_list).replace("'''", "\\'\\'\\'")
script_description = script_names[script_name]
spam_filter += f"[sieve.trusted.scripts.{script_name}]\nname = \"{script_description}\"\ncontents = '''\n{script_content}'''\n\n"
spam_filter += "\n[lookup]\n"
for map in maps :
with open(os.path.join("./spamfilter/maps", map), "r", encoding="utf-8") as f:
spam_filter += f.read() + "\n"
return spam_filter
def main():
spam_filter = build_spam_filters(scripts)
with open("spamfilter.toml", "w", encoding="utf-8") as toml_file:
toml_file.write(spam_filter)
config = read_file("./minimal.toml") + read_file("./security.toml") + spam_filter
with open("config.toml", "w", encoding="utf-8") as toml_file:
toml_file.write(config)
print("Stalwart TOML configuration files have been generated.")
if __name__ == "__main__":
main()

View File

@@ -1,35 +0,0 @@
#############################################
# Cache configuration
#############################################
[cache]
capacity = 512
shard = 32
[cache.session]
ttl = "1h"
[cache.account]
size = 2048
[cache.mailbox]
size = 2048
[cache.thread]
size = 2048
[cache.bayes]
capacity = 8192
[cache.bayes.ttl]
positive = "1h"
negative = "1h"
[cache.resolver]
txt = 2048
mx = 1024
ipv4 = 1024
ipv6 = 1024
ptr = 1024
tlsa = 1024
mta-sts = 1024

View File

@@ -1,37 +0,0 @@
#############################################
# Server configuration
#############################################
[server]
hostname = "%{HOST}%"
max-connections = 8192
#[server.proxy]
#trusted-networks = ["127.0.0.0/8", "::1", "10.0.0.0/8"]
[authentication]
fail2ban = "100/1d"
rate-limit = "10/1m"
[server.run-as]
user = "stalwart-mail"
group = "stalwart-mail"
[server.socket]
nodelay = true
reuse-addr = true
#reuse-port = true
backlog = 1024
#ttl = 3600
#send-buffer-size = 65535
#recv-buffer-size = 65535
#linger = 1
#tos = 1
[global]
#thread-pool = 8
[server.http]
#headers = ["Access-Control-Allow-Origin: *",
# "Access-Control-Allow-Methods: POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONS",
# "Access-Control-Allow-Headers: Authorization, Content-Type, Accept, X-Requested-With"]

View File

@@ -1,73 +0,0 @@
#############################################
# Sieve untrusted runtime configuration
#############################################
[sieve.untrusted]
disable-capabilities = []
notification-uris = ["mailto"]
protected-headers = ["Original-Subject", "Original-From", "Received", "Auto-Submitted"]
[sieve.untrusted.limits]
name-length = 512
max-scripts = 256
script-size = 102400
string-length = 4096
variable-name-length = 32
variable-size = 4096
nested-blocks = 15
nested-tests = 15
nested-foreverypart = 3
match-variables = 30
local-variables = 128
header-size = 1024
includes = 3
nested-includes = 3
cpu = 5000
redirects = 1
received-headers = 10
outgoing-messages = 3
[sieve.untrusted.vacation]
default-subject = "Automated reply"
subject-prefix = "Auto: "
[sieve.untrusted.default-expiry]
vacation = "30d"
duplicate = "7d"
#############################################
# Sieve trusted runtime configuration
#############################################
[sieve.trusted]
from-name = "Automated Message"
from-addr = "no-reply@%{DEFAULT_DOMAIN}%"
return-path = ""
#hostname = "%{HOST}%"
no-capability-check = true
sign = ["rsa"]
[sieve.trusted.limits]
redirects = 3
out-messages = 5
received-headers = 50
cpu = 1048576
nested-includes = 5
duplicate-expiry = "7d"
[sieve.trusted.scripts]
#connect = '''require ["variables", "extlists", "reject"];
# if string :list "${env.remote_ip}" "default/blocked-ips" {
# reject "Your IP '${env.remote_ip}' is not welcomed here.";
# }'''
#ehlo = '''require ["variables", "extlists", "reject"];
# if string :list "${env.helo_domain}" "default/blocked-domains" {
# reject "551 5.1.1 Your domain '${env.helo_domain}' has been blacklisted.";
# }'''
#mail = '''require ["variables", "envelope", "reject"];
# if envelope :localpart :is "from" "known_spammer" {
# reject "We do not accept SPAM.";
# }'''

View File

@@ -1,20 +0,0 @@
#############################################
# Storage configuration
#############################################
[storage]
data = "%{DEFAULT_STORE}%"
fts = "%{DEFAULT_STORE}%"
blob = "%{DEFAULT_STORE}%"
lookup = "%{DEFAULT_STORE}%"
directory = "%{DEFAULT_DIRECTORY}%"
[storage.encryption]
enable = true
append = false
[storage.full-text]
default-language = "en"
[storage.cluster]
node-id = 1

View File

@@ -1,30 +0,0 @@
#############################################
# TLS default configuration
#############################################
[server.tls]
enable = true
implicit = false
timeout = "1m"
certificate = "default"
#acme = "letsencrypt"
#protocols = ["TLSv1.2", "TLSv1.3"]
#ciphers = [ "TLS13_AES_256_GCM_SHA384", "TLS13_AES_128_GCM_SHA256",
# "TLS13_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
# "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
# "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
# "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"]
ignore-client-order = true
[acme."letsencrypt"]
directory = "https://acme-v02.api.letsencrypt.org/directory"
#directory = "https://acme-staging-v02.api.letsencrypt.org/directory"
contact = ["postmaster@%{DEFAULT_DOMAIN}%"]
cache = "%{BASE_PATH}%/etc/acme"
port = 443
renew-before = "30d"
[certificate."default"]
sni-subjects = []
cert = "file://__CERT_PATH__"
private-key = "file://__PK_PATH__"

View File

@@ -1,24 +0,0 @@
#############################################
# Tracing & logging configuration
#############################################
[tracing."stdout"]
method = "stdout"
level = "trace"
enable = false
[tracing."ot"]
method = "open-telemetry"
transport = "http"
endpoint = "https://127.0.0.1/otel"
headers = ["Authorization: <place_auth_here>"]
level = "debug"
enable = false
[tracing."log"]
method = "log"
path = "%{BASE_PATH}%/logs"
prefix = "stalwart.log"
rotate = "daily"
level = "info"
enable = true

View File

@@ -1,52 +0,0 @@
#############################################
# Stalwart Mail Server Configuration File
#############################################
[macros]
host = "__HOST__"
default_domain = "__DOMAIN__"
base_path = "__BASE_PATH__"
default_directory = "__DIRECTORY__"
default_store = "__STORE__"
[include]
files = [ "%{BASE_PATH}%/etc/common/server.toml",
"%{BASE_PATH}%/etc/common/tls.toml",
"%{BASE_PATH}%/etc/common/store.toml",
"%{BASE_PATH}%/etc/common/tracing.toml",
"%{BASE_PATH}%/etc/common/sieve.toml",
"%{BASE_PATH}%/etc/common/cache.toml",
"%{BASE_PATH}%/etc/directory/imap.toml",
"%{BASE_PATH}%/etc/directory/internal.toml",
"%{BASE_PATH}%/etc/directory/ldap.toml",
"%{BASE_PATH}%/etc/directory/lmtp.toml",
"%{BASE_PATH}%/etc/directory/memory.toml",
"%{BASE_PATH}%/etc/directory/sql.toml",
"%{BASE_PATH}%/etc/store/elasticsearch.toml",
"%{BASE_PATH}%/etc/store/fs.toml",
"%{BASE_PATH}%/etc/store/foundationdb.toml",
"%{BASE_PATH}%/etc/store/mysql.toml",
"%{BASE_PATH}%/etc/store/postgresql.toml",
"%{BASE_PATH}%/etc/store/redis.toml",
"%{BASE_PATH}%/etc/store/rocksdb.toml",
"%{BASE_PATH}%/etc/store/s3.toml",
"%{BASE_PATH}%/etc/store/sqlite.toml",
"%{BASE_PATH}%/etc/imap/listener.toml",
"%{BASE_PATH}%/etc/imap/settings.toml",
"%{BASE_PATH}%/etc/jmap/auth.toml",
"%{BASE_PATH}%/etc/jmap/listener.toml",
"%{BASE_PATH}%/etc/jmap/oauth.toml",
"%{BASE_PATH}%/etc/jmap/protocol.toml",
"%{BASE_PATH}%/etc/jmap/push.toml",
"%{BASE_PATH}%/etc/jmap/ratelimit.toml",
"%{BASE_PATH}%/etc/jmap/websockets.toml",
"%{BASE_PATH}%/etc/smtp/auth.toml",
"%{BASE_PATH}%/etc/smtp/listener.toml",
"%{BASE_PATH}%/etc/smtp/milter.toml",
"%{BASE_PATH}%/etc/smtp/queue.toml",
"%{BASE_PATH}%/etc/smtp/remote.toml",
"%{BASE_PATH}%/etc/smtp/report.toml",
"%{BASE_PATH}%/etc/smtp/resolver.toml",
"%{BASE_PATH}%/etc/smtp/session.toml",
"%{BASE_PATH}%/etc/smtp/signature.toml",
"%{BASE_PATH}%/etc/smtp/spamfilter.toml" ]

View File

@@ -1,29 +0,0 @@
#############################################
# IMAP Directory configuration
#############################################
[directory."imap"]
type = "imap"
host = "127.0.0.1"
port = 993
disable = true
[directory."imap".pool]
max-connections = 10
[directory."imap".pool.timeout]
create = "30s"
wait = "30s"
recycle = "30s"
[directory."imap".tls]
enable = true
allow-invalid-certs = true
[directory."imap".cache]
entries = 500
ttl = {positive = '1h', negative = '10m'}
[directory."imap".lookup]
domains = ["%{DEFAULT_DOMAIN}%"]

View File

@@ -1,20 +0,0 @@
#############################################
# Internal Directory configuration
#############################################
[directory."internal"]
type = "internal"
store = "%{DEFAULT_STORE}%"
disable = true
[directory."internal".options]
catch-all = true
#catch-all = [ { if = "matches('(.+)@(.+)$', address)", then = "'info@' + $2" },
# { else = false } ]
subaddressing = true
#subaddressing = [ { if = "matches('^([^.]+)\\.([^.]+)@(.+)$', address)", then = "$2 + '@' + $3" },
# { else = false } ]
[directory."internal".cache]
entries = 500
ttl = {positive = '1h', negative = '10m'}

View File

@@ -1,60 +0,0 @@
#############################################
# LDAP Directory configuration
#############################################
[directory."ldap"]
type = "ldap"
url = "ldap://localhost:389"
base-dn = "dc=example,dc=org"
timeout = "30s"
disable = true
[directory."ldap".bind]
dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org"
secret = "mysecret"
[directory."ldap".bind.auth]
enable = false
dn = "cn=?,ou=svcaccts,dc=example,dc=org"
[directory."ldap".tls]
enable = false
allow-invalid-certs = false
[directory."ldap".cache]
entries = 500
ttl = {positive = '1h', negative = '10m'}
[directory."ldap".options]
catch-all = true
#catch-all = [ { if = "matches('(.+)@(.+)$', address)", then = "'info@' + $2" },
# { else = false } ]
subaddressing = true
#subaddressing = [ { if = "matches('^([^.]+)\\.([^.]+)@(.+)$', address)", then = "$2 + '@' + $3" },
# { else = false } ]
[directory."ldap".pool]
max-connections = 10
[directory."ldap".pool.timeout]
create = "30s"
wait = "30s"
recycle = "30s"
[directory."ldap".filter]
name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(uid=?))"
email = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=?)(mailAlias=?)(mailList=?)))"
verify = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*?*)(mailAlias=*?*)))"
expand = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(mailList=?))"
domains = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*@?)(mailAlias=*@?)))"
[directory."ldap".attributes]
name = "uid"
class = "objectClass"
description = ["principalName", "description"]
secret = "userPassword"
groups = ["memberOf", "otherGroups"]
email = "mail"
email-alias = "mailAlias"
quota = "diskQuota"

View File

@@ -1,33 +0,0 @@
#############################################
# LMTP Directory configuration
#############################################
[directory."lmtp"]
type = "lmtp"
host = "127.0.0.1"
port = 11200
disable = true
[directory."lmtp".limits]
auth-errors = 3
rcpt = 5
[directory."lmtp".pool]
max-connections = 10
[directory."lmtp".pool.timeout]
create = "30s"
wait = "30s"
recycle = "30s"
[directory."lmtp".tls]
enable = false
allow-invalid-certs = true
[directory."lmtp".cache]
entries = 500
ttl = {positive = '1h', negative = '10m'}
[directory."lmtp".lookup]
domains = ["%{DEFAULT_DOMAIN}%"]

View File

@@ -1,59 +0,0 @@
#############################################
# In-Memory Directory configuration
#############################################
[directory."memory"]
type = "memory"
disable = true
[directory."memory".options]
catch-all = true
#catch-all = [ { if = "matches('(.+)@(.+)$', address)", then = "'info@' + $2" },
# { else = false } ]
subaddressing = true
#subaddressing = [ { if = "matches('^([^.]+)\\.([^.]+)@(.+)$', address)", then = "$2 + '@' + $3" },
# { else = false } ]
[[directory."memory".principals]]
name = "admin"
class = "admin"
description = "Superuser"
secret = "changeme"
email = ["postmaster@%{DEFAULT_DOMAIN}%"]
[[directory."memory".principals]]
name = "john"
class = "individual"
description = "John Doe"
secret = "12345"
email = ["john@%{DEFAULT_DOMAIN}%", "jdoe@%{DEFAULT_DOMAIN}%", "john.doe@%{DEFAULT_DOMAIN}%"]
email-list = ["info@%{DEFAULT_DOMAIN}%"]
member-of = ["sales"]
[[directory."memory".principals]]
name = "jane"
class = "individual"
description = "Jane Doe"
secret = "abcde"
email = ["jane@%{DEFAULT_DOMAIN}%", "jane.doe@%{DEFAULT_DOMAIN}%"]
email-list = ["info@%{DEFAULT_DOMAIN}%"]
member-of = ["sales", "support"]
[[directory."memory".principals]]
name = "bill"
class = "individual"
description = "Bill Foobar"
secret = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe"
quota = 50000000
email = ["bill@%{DEFAULT_DOMAIN}%", "bill.foobar@%{DEFAULT_DOMAIN}%"]
email-list = ["info@%{DEFAULT_DOMAIN}%"]
[[directory."memory".principals]]
name = "sales"
class = "group"
description = "Sales Team"
[[directory."memory".principals]]
name = "support"
class = "group"
description = "Support Team"

View File

@@ -1,26 +0,0 @@
#############################################
# SQL Directory configuration
#############################################
[directory."sql"]
type = "sql"
store = "__SQL_STORE__"
disable = true
[directory."sql".options]
catch-all = true
#catch-all = [ { if = "matches('(.+)@(.+)$', address)", then = "'info@' + $2" },
# { else = false } ]
subaddressing = true
#subaddressing = [ { if = "matches('^([^.]+)\\.([^.]+)@(.+)$', address)", then = "$2 + '@' + $3" },
# { else = false } ]
[directory."sql".cache]
entries = 500
ttl = {positive = '1h', negative = '10m'}
[directory."sql".columns]
class = "type"
secret = "secret"
description = "description"
quota = "quota"

View File

@@ -1,16 +0,0 @@
#############################################
# IMAP server listeners configuration
#############################################
[server.listener."imap"]
bind = ["[::]:143"]
protocol = "imap"
[server.listener."imaptls"]
bind = ["[::]:993"]
protocol = "imap"
tls.implicit = true
[server.listener."sieve"]
bind = ["[::]:4190"]
protocol = "managesieve"

View File

@@ -1,22 +0,0 @@
#############################################
# IMAP server settings
#############################################
[imap.request]
max-size = 52428800
[imap.auth]
max-failures = 3
allow-plain-text = false
[imap.folders.name]
shared = "Shared Folders"
[imap.timeout]
authenticated = "30m"
anonymous = "1m"
idle = "30m"
[imap.rate-limit]
requests = "2000/1m"
concurrent = 6

View File

@@ -1,6 +0,0 @@
#############################################
# JMAP authentication & session configuration
#############################################
[jmap.session.purge]
frequency = "15 * *"

View File

@@ -1,14 +0,0 @@
#############################################
# JMAP server listener configuration
#############################################
[server.listener."jmap"]
protocol = "jmap"
bind = ["[::]:443"]
url = "https://%{HOST}%"
[server.listener."jmap".tls]
implicit = true
#bind = ["[::]:8080"]
#url = "https://%{HOST}%:8080"

View File

@@ -1,16 +0,0 @@
#############################################
# JMAP OAuth server configuration
#############################################
[oauth]
key = "__OAUTH_KEY__"
[oauth.auth]
max-attempts = 3
[oauth.expiry]
user-code = "30m"
auth-code = "10m"
token = "1h"
refresh-token = "30d"
refresh-token-renew = "4d"

View File

@@ -1,43 +0,0 @@
#############################################
# JMAP protocol configuration
#############################################
[jmap.protocol.get]
max-objects = 500
[jmap.protocol.set]
max-objects = 500
[jmap.protocol.request]
max-concurrent = 4
max-size = 10000000
max-calls = 16
[jmap.protocol.query]
max-results = 5000
[jmap.protocol.upload]
max-size = 50000000
max-concurrent = 4
ttl = "1h"
[jmap.protocol.upload.quota]
files = 1000
size = 50000000
[jmap.protocol.changes]
max-results = 5000
[jmap.mailbox]
max-depth = 10
max-name-length = 255
[jmap.email]
max-attachment-size = 50000000
max-size = 75000000
[jmap.email.parse]
max-items = 10
[jmap.principal]
allow-lookups = true

View File

@@ -1,21 +0,0 @@
#############################################
# JMAP Push & EventSource configuration
#############################################
[jmap.push]
max-total = 100
throttle = "1ms"
[jmap.push.attempts]
interval = "1m"
max = 3
[jmap.push.retry]
interval = "1s"
[jmap.push.timeout]
request = "10s"
verify = "1s"
[jmap.event-source]
throttle = "1s"

View File

@@ -1,9 +0,0 @@
#############################################
# JMAP server rate limiter configuration
#############################################
[jmap.rate-limit]
account = "1000/1m"
anonymous = "100/1m"
use-forwarded = false

View File

@@ -1,8 +0,0 @@
#############################################
# JMAP WebSockets server configuration
#############################################
[jmap.web-sockets]
throttle = "1s"
timeout = "10m"
heartbeat = "1m"

View File

@@ -0,0 +1,72 @@
#############################################
# Stalwart Mail Server Configuration File
#############################################
[server.listener."smtp"]
bind = ["[::]:25"]
protocol = "smtp"
[server.listener."submission"]
bind = ["[::]:587"]
protocol = "smtp"
[server.listener."submissions"]
bind = ["[::]:465"]
protocol = "smtp"
tls.implicit = true
[server.listener."imap"]
bind = ["[::]:143"]
protocol = "imap"
[server.listener."imaptls"]
bind = ["[::]:993"]
protocol = "imap"
tls.implicit = true
[server.listener."sieve"]
bind = ["[::]:4190"]
protocol = "managesieve"
[server.listener."https"]
protocol = "http"
bind = ["[::]:443"]
tls.implicit = true
[storage]
data = "rocksdb"
fts = "rocksdb"
blob = "rocksdb"
lookup = "rocksdb"
directory = "internal"
[store."rocksdb"]
type = "rocksdb"
path = "%{env:STALWART_PATH}%/data"
compression = "lz4"
[directory."internal"]
type = "internal"
store = "rocksdb"
[lookup.default]
domain = "%{env:DOMAIN}%"
hostname = "%{env:HOSTNAME}%"
[oauth]
key = "%{env:OAUTH_KEY}%"
[tracer."stdout"]
type = "stdout"
level = "info"
ansi = false
enable = true
#[server.run-as]
#user = "stalwart-mail"
#group = "stalwart-mail"
[server.http]
headers = ["Access-Control-Allow-Origin: *",
"Access-Control-Allow-Methods: POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONS",
"Access-Control-Allow-Headers: Authorization, Content-Type, Accept, X-Requested-With"]

View File

@@ -0,0 +1,19 @@
[[queue.quota]]
messages = 100000
size = 10737418240 # 10gb
enable = true
[[queue.throttle]]
key = ["rcpt_domain"]
concurrency = 5
enable = true
[[session.throttle]]
key = ["remote_ip"]
concurrency = 5
enable = true
[[session.throttle]]
key = ["sender_domain", "rcpt"]
rate = "25/1h"
enable = true

View File

@@ -1,28 +0,0 @@
#############################################
# SMTP DMARC, DKIM, SPF, ARC & IpRev
#############################################
[auth.iprev]
verify = [ { if = "listener = 'smtp'", then = "relaxed" },
{ else = "disable" } ]
[auth.dkim]
verify = "relaxed"
sign = [ { if = "listener != 'smtp'", then = "['rsa']" },
{ else = false } ]
[auth.spf.verify]
ehlo = [ { if = "listener = 'smtp'", then = "relaxed" },
{ else = "disable" } ]
mail-from = [ { if = "listener = 'smtp'", then = "relaxed" },
{ else = "disable" } ]
[auth.arc]
verify = "relaxed"
seal = "['rsa']"
[auth.dmarc]
verify = [ { if = "listener = 'smtp'", then = "relaxed" },
{ else = "disable" } ]

View File

@@ -1,21 +0,0 @@
#############################################
# SMTP server listener configuration
#############################################
[server.listener."smtp"]
bind = ["[::]:25"]
#greeting = "Stalwart SMTP at your service"
protocol = "smtp"
[server.listener."submission"]
bind = ["[::]:587"]
protocol = "smtp"
[server.listener."submissions"]
bind = ["[::]:465"]
protocol = "smtp"
tls.implicit = true
#[server.listener."management"]
#bind = ["127.0.0.1:8080"]
#protocol = "http"

View File

@@ -1,26 +0,0 @@
#############################################
# SMTP inbound Milter configuration
#############################################
#[session.data.milter."rspamd"]
#enable = [ { if = "listener = 'smtp'", then = true },
# { else = false } ]
#hostname = "127.0.0.1"
#port = 11332
#tls = false
#allow-invalid-certs = false
#[session.data.milter."rspamd".timeout]
#connect = "30s"
#command = "30s"
#data = "60s"
#[session.data.milter."rspamd".options]
#tempfail-on-error = true
#max-response-size = 52428800 # 50mb
#version = 6
#[session.data.pipe."spam-assassin"]
#command = "spamc"
#arguments = []
#timeout = "10s"

View File

@@ -1,49 +0,0 @@
#############################################
# SMTP server queue configuration
#############################################
[queue.schedule]
retry = "[2m, 5m, 10m, 15m, 30m, 1h, 2h]"
notify = "[1d, 3d]"
expire = "5d"
[queue.outbound]
#hostname = "%{HOST}%"
next-hop = [ { if = "is_local_domain('%{DEFAULT_DIRECTORY}%', rcpt_domain)", then = "'local'" },
{ else = false } ]
ip-strategy = "ipv4_then_ipv6"
[queue.outbound.tls]
dane = "optional"
mta-sts = "optional"
starttls = "require"
allow-invalid-certs = false
#[queue.outbound.source-ip]
#v4 = "['10.0.0.10', '10.0.0.11']"
#v6 = "['a::b', 'a::c']"
[queue.outbound.limits]
mx = 7
multihomed = 2
[queue.outbound.timeouts]
connect = "3m"
greeting = "3m"
tls = "2m"
ehlo = "3m"
mail-from = "3m"
rcpt-to = "3m"
data = "10m"
mta-sts = "2m"
[[queue.quota]]
#match = "sender_domain = 'foobar.org'"
#key = ["rcpt"]
messages = 100000
size = 10737418240 # 10gb
[[queue.throttle]]
key = ["rcpt_domain"]
#rate = "100/1h"
concurrency = 5

View File

@@ -1,18 +0,0 @@
#############################################
# SMTP remote servers configuration
#############################################
[remote."local"]
address = "127.0.0.1"
port = 11200
protocol = "lmtp"
concurrency = 10
timeout = "1m"
[remote."local".tls]
implicit = false
allow-invalid-certs = true
#[remote."local".auth]
#username = ""
#secret = ""

View File

@@ -1,55 +0,0 @@
#############################################
# SMTP reporting configuration
#############################################
[report]
#submitter = "'%{HOST}%'"
[report.analysis]
addresses = ["dmarc@*", "abuse@*", "postmaster@*"]
forward = true
store = "30d"
[report.dsn]
from-name = "'Mail Delivery Subsystem'"
from-address = "'MAILER-DAEMON@%{DEFAULT_DOMAIN}%'"
sign = "['rsa']"
[report.dkim]
from-name = "'Report Subsystem'"
from-address = "'noreply-dkim@%{DEFAULT_DOMAIN}%'"
subject = "'DKIM Authentication Failure Report'"
sign = "['rsa']"
send = "[1, 1d]"
[report.spf]
from-name = "'Report Subsystem'"
from-address = "'noreply-spf@%{DEFAULT_DOMAIN}%'"
subject = "'SPF Authentication Failure Report'"
send = "[1, 1d]"
sign = "['rsa']"
[report.dmarc]
from-name = "'Report Subsystem'"
from-address = "'noreply-dmarc@%{DEFAULT_DOMAIN}%'"
subject = "'DMARC Authentication Failure Report'"
send = "[1, 1d]"
sign = "['rsa']"
[report.dmarc.aggregate]
from-name = "'DMARC Report'"
from-address = "'noreply-dmarc@%{DEFAULT_DOMAIN}%'"
org-name = "'%{DEFAULT_DOMAIN}%'"
#contact-info = ""
send = "daily"
max-size = 26214400 # 25mb
sign = "['rsa']"
[report.tls.aggregate]
from-name = "'TLS Report'"
from-address = "'noreply-tls@%{DEFAULT_DOMAIN}%'"
org-name = "'%{DEFAULT_DOMAIN}%'"
#contact-info = ""
send = "daily"
max-size = 26214400 # 25 mb
sign = "['rsa']"

View File

@@ -1,13 +0,0 @@
#############################################
# SMTP server resolver configuration
#############################################
[resolver]
type = "system"
#preserve-intermediates = true
concurrency = 2
timeout = "5s"
attempts = 2
try-tcp-on-error = true
public-suffix = ["https://publicsuffix.org/list/public_suffix_list.dat",
"file://%{BASE_PATH}%/etc/spamfilter/maps/suffix_list.dat.gz"]

View File

@@ -1,100 +0,0 @@
#############################################
# SMTP inbound session configuration
#############################################
[session]
timeout = "5m"
transfer-limit = 262144000 # 250 MB
duration = "10m"
[session.connect]
#script = "'connect'"
[session.ehlo]
require = true
reject-non-fqdn = [ { if = "listener = 'smtp'", then = true},
{ else = false } ]
#script = "'ehlo'"
[session.extensions]
pipelining = true
chunking = true
requiretls = true
no-soliciting = ""
dsn = [ { if = "!is_empty(authenticated_as)", then = true},
{ else = false } ]
expn = [ { if = "!is_empty(authenticated_as)", then = true},
{ else = false } ]
vrfy = [ { if = "!is_empty(authenticated_as)", then = true},
{ else = false } ]
future-release = [ { if = "!is_empty(authenticated_as)", then = "7d"},
{ else = false } ]
deliver-by = [ { if = "!is_empty(authenticated_as)", then = "15d"},
{ else = false } ]
mt-priority = [ { if = "!is_empty(authenticated_as)", then = "mixer"},
{ else = false } ]
[session.auth]
mechanisms = [ { if = "listener != 'smtp'", then = "[plain, login]"},
{ else = false } ]
directory = [ { if = "listener != 'smtp'", then = "'%{DEFAULT_DIRECTORY}%'" },
{ else = false } ]
require = [ { if = "listener != 'smtp'", then = true},
{ else = false } ]
allow-plain-text = false
[session.auth.errors]
total = 3
wait = "5s"
[session.mail]
#script = "mail-from"
#rewrite = [ { if = "listener != 'smtp' & matches('^([^.]+)@([^.]+)\\.(.+)$', rcpt)", then = "$1 + '@' + $3" },
# { else = false } ]
[session.rcpt]
#script = "greylist"
relay = [ { if = "!is_empty(authenticated_as)", then = true },
{ else = false } ]
#rewrite = [ { if = "is_local_domain('%{DEFAULT_DIRECTORY}%', rcpt_domain) & matches('^([^.]+)\\.([^.]+)@(.+)$', rcpt)", then = "$1 + '+' + $2 + '@' + $3" },
# { else = false } ]
max-recipients = 25
directory = "'%{DEFAULT_DIRECTORY}%'"
[session.rcpt.errors]
total = 5
wait = "5s"
[session.data]
script = [ { if = "is_empty(authenticated_as)", then = "'spam-filter'"},
{ else = "'track-replies'" } ]
[session.data.limits]
messages = 10
size = 104857600
received-headers = 50
[session.data.add-headers]
received = [ { if = "listener = 'smtp'", then = true },
{ else = false } ]
received-spf = [ { if = "listener = 'smtp'", then = true },
{ else = false } ]
auth-results = [ { if = "listener = 'smtp'", then = true },
{ else = false } ]
message-id = [ { if = "listener = 'smtp'", then = false },
{ else = true } ]
date = [ { if = "listener = 'smtp'", then = false },
{ else = true } ]
return-path = false
[[session.throttle]]
#match = "remote_ip = '10.0.0.1'"
key = ["remote_ip"]
concurrency = 5
#rate = "5/1h"
enable = true
[[session.throttle]]
key = ["sender_domain", "rcpt"]
rate = "25/1h"
enable = true

View File

@@ -1,18 +0,0 @@
#############################################
# SMTP DKIM & ARC signatures
#############################################
[signature."rsa"]
#public-key = "file://%{BASE_PATH}%/etc/dkim/%{DEFAULT_DOMAIN}%.cert"
private-key = "file://%{BASE_PATH}%/etc/dkim/%{DEFAULT_DOMAIN}%.key"
domain = "%{DEFAULT_DOMAIN}%"
selector = "stalwart"
headers = ["From", "To", "Date", "Subject", "Message-ID"]
algorithm = "rsa-sha256"
canonicalization = "relaxed/relaxed"
#expire = "10d"
#third-party = ""
#third-party-algo = ""
#auid = ""
set-body-length = false
report = true

View File

@@ -1,62 +0,0 @@
#############################################
# SMTP Spam & Phishing filter configuration
#############################################
[spam.header]
add-spam = true
add-spam-result = true
is-spam = "X-Spam-Status: Yes"
[spam.autolearn]
enable = true
balance = 0.9
[spam.autolearn.ham]
replies = true
threshold = -0.5
[spam.autolearn.spam]
threshold = 6.0
[spam.threshold]
spam = 5.0
discard = 0
reject = 0
[spam.data]
directory = ""
lookup = ""
[sieve.trusted.scripts]
spam-filter = ["file://%{BASE_PATH}%/etc/spamfilter/scripts/config.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/prelude.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/from.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/recipient.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/subject.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/replyto.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/date.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/messageid.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/received.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/headers.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/bounce.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/html.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/mime.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/dmarc.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/ip.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/helo.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/replies_in.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/spamtrap.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/bayes_classify.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/url.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/rbl.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/pyzor.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/composites.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/scores.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/reputation.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/epilogue.sieve"]
track-replies = ["file://%{BASE_PATH}%/etc/spamfilter/scripts/config.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/replies_out.sieve"]
greylist = ["file://%{BASE_PATH}%/etc/spamfilter/scripts/config.sieve",
"file://%{BASE_PATH}%/etc/spamfilter/scripts/greylist.sieve"]

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,6 @@ spam-dmarc = {"18f.gov",
"adp.com",
"advice.hmrc.gov.uk",
"aerocivil.gov.co",
"aerocivil.gov.co",
"afreximbank.com",
"agingstats.gov",
"agro.ru",

View File

@@ -150,7 +150,6 @@ spam-spdk = {"1cfresh.com",
"paypal.ca",
"paypal.cn",
"paypal.com",
"paypal.com",
"paypal.co.uk",
"paypal.de",
"paypal.es",

View File

@@ -1 +0,0 @@
spam-trap = {}

View File

@@ -1,35 +1,35 @@
# Whether to add an X-Spam-Status header
let "ADD_HEADER_SPAM" "%{cfg:spam.header.add-spam}%";
let "ADD_HEADER_SPAM" "key_get('spam-config', 'add-spam')";
# Whether to add an X-Spam-Result header
let "ADD_HEADER_SPAM_RESULT" "%{cfg:spam.header.add-spam-result}%";
let "ADD_HEADER_SPAM_RESULT" "key_get('spam-config', 'add-spam-result')";
# Whether message replies from authenticated users should be learned as ham
let "AUTOLEARN_REPLIES_HAM" "%{cfg:spam.autolearn.ham.replies}%";
let "AUTOLEARN_REPLIES_HAM" "key_get('spam-config', 'learn-ham-replies')";
# Whether the bayes classifier should be trained automatically
let "AUTOLEARN_ENABLE" "%{cfg:spam.autolearn.enable}%";
let "AUTOLEARN_ENABLE" "key_get('spam-config', 'learn-enable')";
# When to learn ham (score >= threshold)
let "AUTOLEARN_HAM_THRESHOLD" "%{cfg:spam.autolearn.ham.threshold}%";
let "AUTOLEARN_HAM_THRESHOLD" "key_get('spam-config', 'learn-ham-threshold')";
# When to learn spam (score <= threshold)
let "AUTOLEARN_SPAM_THRESHOLD" "%{cfg:spam.autolearn.spam.threshold}%";
let "AUTOLEARN_SPAM_THRESHOLD" "key_get('spam-config', 'learn-spam-threshold')";
# Keep difference for spam/ham learns for at least this value
let "AUTOLEARN_SPAM_HAM_BALANCE" "%{cfg:spam.autolearn.balance}%";
let "AUTOLEARN_SPAM_HAM_BALANCE" "key_get('spam-config', 'learn-balance')";
# If ADD_HEADER_SPAM is enabled, mark as SPAM messages with a score above this threshold
let "SCORE_SPAM_THRESHOLD" "%{cfg:spam.threshold.spam}%";
let "SCORE_SPAM_THRESHOLD" "key_get('spam-config', 'threshold-spam')";
# Discard messages with a score above this threshold
let "SCORE_DISCARD_THRESHOLD" "%{cfg:spam.threshold.discard}%";
let "SCORE_DISCARD_THRESHOLD" "key_get('spam-config', 'threshold-discard')";
# Reject messages with a score above this threshold
let "SCORE_REJECT_THRESHOLD" "%{cfg:spam.threshold.reject}%";
let "SCORE_REJECT_THRESHOLD" "key_get('spam-config', 'threshold-reject')";
# Directory name to use for local domain lookups (leave empty for default)
let "DOMAIN_DIRECTORY" "%{cfg:spam.data.directory}%";
let "DOMAIN_DIRECTORY" "key_get('spam-config', 'directory')";
# Store to use for Bayes tokens and ids (leave empty for default)
let "SPAM_DB" "%{cfg:spam.data.lookup}%";
let "SPAM_DB" "key_get('spam-config', 'lookup')";

View File

@@ -0,0 +1,20 @@
[spam.header]
is-spam = "X-Spam-Status: Yes"
[lookup.spam-config]
add-spam = true
add-spam-result = true
learn-enable = true
learn-balance = "0.9"
learn-ham-replies = true
learn-ham-threshold = "-0.5"
learn-spam-threshold = "6.0"
threshold-spam = "5.0"
threshold-discard = "0.0"
threshold-reject = "0.0"
directory = ""
lookup = ""
[session.data]
script = [ { if = "is_empty(authenticated_as)", then = "'spam-filter'"},
{ else = "'track-replies'" } ]

View File

@@ -1,18 +0,0 @@
#############################################
# ElasticSearch FTS Store configuration
#############################################
[store."elasticsearch"]
type = "elasticsearch"
url = "https://localhost:9200"
user = "elastic"
password = "myelasticpassword"
#cloud-id = "my-cloud-id"
disable = true
[store."elasticsearch".tls]
allow-invalid-certs = true
[store."elasticsearch".index]
shards = 3
replicas = 0

View File

@@ -1,20 +0,0 @@
#############################################
# FoundationDB Store configuration
#############################################
[store."foundationdb"]
type = "foundationdb"
#cluster-file = "/etc/foundationdb/fdb.cluster"
disable = true
#[store."foundationdb".transaction]
#timeout = "5s"
#retry-limit = 10
#max-retry-delay = "1s"
#[store."foundationdb".ids]
#machine = "stalwart"
#data-center = "my-datacenter"
[store."foundationdb".purge]
frequency = "0 3 *"

View File

@@ -1,12 +0,0 @@
#############################################
# File System Blob Store configuration
#############################################
[store."fs"]
type = "fs"
path = "%{BASE_PATH}%/data/blobs"
depth = 2
disable = true
[store."fs".purge]
frequency = "0 3 *"

View File

@@ -1,37 +0,0 @@
#############################################
# MySQL Store configuration
#############################################
[store."mysql"]
type = "mysql"
host = "localhost"
port = 3307
database = "stalwart"
user = "root"
password = "password"
disable = true
#max-allowed-packet = 1073741824
timeout = "15s"
#[store."mysql".pool]
#max-connections = 10
#min-connections = 5
#[store."mysql".init]
#execute = [
# "CREATE TABLE IF NOT EXISTS accounts (name VARCHAR(32) PRIMARY KEY, secret VARCHAR(1024), description VARCHAR(1024), type VARCHAR(32) NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)",
# "CREATE TABLE IF NOT EXISTS group_members (name VARCHAR(32) NOT NULL, member_of VARCHAR(32) NOT NULL, PRIMARY KEY (name, member_of))",
# "CREATE TABLE IF NOT EXISTS emails (name VARCHAR(32) NOT NULL, address VARCHAR(128) NOT NULL, type VARCHAR(32), PRIMARY KEY (name, address))"
#]
[store."mysql".query]
name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true"
members = "SELECT member_of FROM group_members WHERE name = ?"
recipients = "SELECT name FROM emails WHERE address = ? ORDER BY name ASC"
emails = "SELECT address FROM emails WHERE name = ? AND type != 'list' ORDER BY type DESC, address ASC"
verify = "SELECT address FROM emails WHERE address LIKE CONCAT('%', ?, '%') AND type = 'primary' ORDER BY address LIMIT 5"
expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.name = l.name WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50"
domains = "SELECT 1 FROM emails WHERE address LIKE CONCAT('%@', ?) LIMIT 1"
[store."mysql".purge]
frequency = "0 3 *"

View File

@@ -1,39 +0,0 @@
#############################################
# PostgreSQL Store configuration
#############################################
[store."postgresql"]
type = "postgresql"
host = "localhost"
port = 5432
database = "stalwart"
user = "postgres"
password = "mysecretpassword"
timeout = "15s"
disable = true
[store."postgresql".tls]
enable = false
allow-invalid-certs = false
#[store."postgresql".pool]
#max-connections = 10
#[store."postgresql".init]
#execute = [
# "CREATE TABLE IF NOT EXISTS accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT, type TEXT NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)",
# "CREATE TABLE IF NOT EXISTS group_members (name TEXT NOT NULL, member_of TEXT NOT NULL, PRIMARY KEY (name, member_of))",
# "CREATE TABLE IF NOT EXISTS emails (name TEXT NOT NULL, address TEXT NOT NULL, type TEXT, PRIMARY KEY (name, address))"
#]
[store."postgresql".query]
name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = $1 AND active = true"
members = "SELECT member_of FROM group_members WHERE name = $1"
recipients = "SELECT name FROM emails WHERE address = $1 ORDER BY name ASC"
emails = "SELECT address FROM emails WHERE name = $1 AND type != 'list' ORDER BY type DESC, address ASC"
verify = "SELECT address FROM emails WHERE address LIKE '%' || $1 || '%' AND type = 'primary' ORDER BY address LIMIT 5"
expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.name = l.name WHERE p.type = 'primary' AND l.address = $1 AND l.type = 'list' ORDER BY p.address LIMIT 50"
domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || $1 LIMIT 1"
[store."postgresql".purge]
frequency = "0 3 *"

View File

@@ -1,18 +0,0 @@
#############################################
# Redis Lookup Store configuration
#############################################
[store."redis"]
type = "redis"
redis-type = "single"
urls = ["redis://127.0.0.1"]
user = "my_username"
password = "secretpassword"
timeout = "10s"
#read-from-replicas = false
disable = true
#[store."redis".retry]
#total = 3
#max-wait = "1s"
#min-wait = "500ms"

View File

@@ -1,18 +0,0 @@
#############################################
# RocksDB Store configuration
#############################################
[store."rocksdb"]
type = "rocksdb"
path = "%{BASE_PATH}%/data"
disable = true
[store."rocksdb".settings]
min-blob-size = 16834
write-buffer-size = 134217728
#[store."rocksdb".pool]
#workers = 10
[store."rocksdb".purge]
frequency = "0 3 *"

View File

@@ -1,18 +0,0 @@
#############################################
# S3/MinIO Blob Store configuration
#############################################
[store."s3"]
type = "s3"
bucket = "stalwart"
region = "eu-central-1"
access-key = "minioadmin"
secret-key = "minioadmin"
#endpoint = ""
#security-token = ""
#profile = ""
timeout = "30s"
disable = true
[store."s3".purge]
frequency = "0 3 *"

View File

@@ -1,31 +0,0 @@
#############################################
# SQLite Store configuration
#############################################
[store."sqlite"]
type = "sqlite"
path = "%{BASE_PATH}%/data/index.sqlite3"
disable = true
#[store."sqlite".pool]
#max-connections = 10
#workers = 10
#[store."sqlite".init]
#execute = [
# "CREATE TABLE IF NOT EXISTS accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT, type TEXT NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)",
# "CREATE TABLE IF NOT EXISTS group_members (name TEXT NOT NULL, member_of TEXT NOT NULL, PRIMARY KEY (name, member_of))",
# "CREATE TABLE IF NOT EXISTS emails (name TEXT NOT NULL, address TEXT NOT NULL, type TEXT, PRIMARY KEY (name, address))"
#]
[store."sqlite".query]
name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true"
members = "SELECT member_of FROM group_members WHERE name = ?"
recipients = "SELECT name FROM emails WHERE address = ?"
emails = "SELECT address FROM emails WHERE name = ? AND type != 'list' ORDER BY type DESC, address ASC"
verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5"
expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.name = l.name WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50"
domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || ? LIMIT 1"
[store."sqlite".purge]
frequency = "0 3 *"