mirror of
https://github.com/stalwartlabs/smtp-server.git
synced 2024-10-22 22:46:40 +00:00
Unit tests part 7
This commit is contained in:
parent
71b9912f17
commit
cbae2e22d6
49 changed files with 2050 additions and 378 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,6 +1,4 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
resources/config/stalwart.toml
|
||||
run.sh
|
||||
|
|
31
Cargo.lock
generated
31
Cargo.lock
generated
|
@ -1091,6 +1091,20 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"pin-utils",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
@ -1354,6 +1368,16 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "privdrop"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81ed9e5437d82d5f2cde999a21571474c5f09b3d76e33eab94bf0e8e42a4fd96"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"nix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.50"
|
||||
|
@ -1792,6 +1816,7 @@ dependencies = [
|
|||
"mail-send",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"privdrop",
|
||||
"rand 0.8.5",
|
||||
"rayon",
|
||||
"regex",
|
||||
|
@ -1835,6 +1860,12 @@ dependencies = [
|
|||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.1"
|
||||
|
|
|
@ -30,6 +30,7 @@ reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
num_cpus = "1.15.0"
|
||||
privdrop = "0.5.3"
|
||||
|
||||
[dev-dependencies]
|
||||
mail-auth = { path = "/home/vagrant/code/mail-auth", features = ["test"] }
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
Stalwart SMTP Server
|
||||
|
||||
# TODO
|
||||
- Dashmap cleanup
|
||||
- RBL
|
||||
- Sieve
|
||||
- Spam filter
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
[server]
|
||||
hostname = "mx.example.org"
|
||||
greeting = "Stalwart SMTP at your service"
|
||||
hostname = "%%HOST%%"
|
||||
#greeting = "Stalwart SMTP at your service"
|
||||
protocol = "smtp"
|
||||
|
||||
[server.listener."smtp"]
|
||||
bind = ["0.0.0.0:9025"]
|
||||
tls.implicit = false
|
||||
[server.run-as]
|
||||
user = "stalwart-smtp"
|
||||
group = "stalwart-smtp"
|
||||
|
||||
[server.listener."smtps"]
|
||||
bind = ["0.0.0.0:9465"]
|
||||
#tls.sni = [{subject = "domain.org", pki = "abc"}]
|
||||
#socket.backlog = 1024
|
||||
[server.listener."smtp"]
|
||||
bind = ["0.0.0.0:25"]
|
||||
|
||||
[server.listener."submission"]
|
||||
bind = ["0.0.0.0:9587"]
|
||||
#tls = {implicit = true}
|
||||
bind = ["0.0.0.0:587"]
|
||||
|
||||
[server.listener."submissions"]
|
||||
bind = ["0.0.0.0:465"]
|
||||
tls.implicit = true
|
||||
|
||||
[server.tls]
|
||||
enable = true
|
||||
implicit = true
|
||||
implicit = false
|
||||
timeout = "1m"
|
||||
certificate = "default"
|
||||
#sni = [{subject = "domain.org", pki = "abc"}]
|
||||
#sni = [{subject = "", certificate = ""}]
|
||||
#protocols = ["TLSv1.2", TLSv1.3"]
|
||||
#ciphers = []
|
||||
ignore-client-order = true
|
||||
|
@ -39,11 +40,12 @@ backlog = 1024
|
|||
[global]
|
||||
log-level = "trace"
|
||||
concurrency = 1024
|
||||
throttle-map = {shard = 32, capacity = 10}
|
||||
shared-map = {shard = 32, capacity = 10}
|
||||
#thread-pool = 8
|
||||
|
||||
[session]
|
||||
timeout = "5m"
|
||||
transfer-limit = 5000000
|
||||
transfer-limit = 262144000 # 250 MB
|
||||
duration = "10m"
|
||||
|
||||
[session.connect]
|
||||
|
@ -58,21 +60,22 @@ pipelining = true
|
|||
chunking = true
|
||||
requiretls = true
|
||||
no-soliciting = ""
|
||||
dsn = true
|
||||
future-release = [
|
||||
{ if = "listener", eq = "submission", then = "5d"},
|
||||
{ else = false }
|
||||
]
|
||||
deliver-by = false
|
||||
mt-priority = false
|
||||
dsn = [ { if = "authenticated-as", ne = "", then = true},
|
||||
{ else = false } ]
|
||||
future-release = [ { if = "authenticated-as", ne = "", then = "7d"},
|
||||
{ else = false } ]
|
||||
deliver-by = [ { if = "authenticated-as", ne = "", then = "15d"},
|
||||
{ else = false } ]
|
||||
mt-priority = [ { if = "authenticated-as", ne = "", then = "mixer"},
|
||||
{ else = false } ]
|
||||
|
||||
[session.auth]
|
||||
mechanisms = [
|
||||
{ if = "listener", eq = "submission", then = ["plain", "login"]},
|
||||
{ else = false }
|
||||
]
|
||||
lookup = [ { if = "listener", eq = "submission", then = "local-addresses" },
|
||||
mechanisms = [ { if = "listener", ne = "smtp", then = ["plain", "login"]},
|
||||
{ else = [] } ]
|
||||
lookup = [ { if = "listener", ne = "smtp", then = "authentication" },
|
||||
{ else = false } ]
|
||||
require = [ { if = "listener", ne = "smtp", then = true},
|
||||
{ else = false } ]
|
||||
|
||||
[session.auth.errors]
|
||||
total = 3
|
||||
|
@ -80,13 +83,12 @@ wait = "5s"
|
|||
|
||||
[session.mail]
|
||||
#script = mail-from.sieve
|
||||
timeout = 10
|
||||
|
||||
[session.rcpt]
|
||||
#script = rcpt-to.sieve
|
||||
relay = [ { if = "authenticated-as", ne = "", then = true },
|
||||
{ else = false } ]
|
||||
max-recipients = 100
|
||||
{ else = false } ]
|
||||
max-recipients = 25
|
||||
|
||||
[session.rcpt.lookup]
|
||||
domains = "local-domains"
|
||||
|
@ -97,7 +99,7 @@ expn = [ { if = "authenticated-as", ne = "", then = "local-addresses" },
|
|||
{ else = false } ]
|
||||
|
||||
[session.rcpt.errors]
|
||||
total = 3
|
||||
total = 5
|
||||
wait = "5s"
|
||||
|
||||
[session.data]
|
||||
|
@ -105,222 +107,245 @@ wait = "5s"
|
|||
|
||||
[session.data.limits]
|
||||
messages = 10
|
||||
size = 100000
|
||||
size = 104857600
|
||||
received-headers = 50
|
||||
mime-parts = 50
|
||||
nested-messages = 3
|
||||
|
||||
[session.data.add-headers]
|
||||
received = true
|
||||
received-spf = true
|
||||
return-path = true
|
||||
auth-results = true
|
||||
message-id = true
|
||||
date = true
|
||||
received = [ { if = "listener", eq = "smtp", then = true },
|
||||
{ else = false } ]
|
||||
received-spf = [ { if = "listener", eq = "smtp", then = true },
|
||||
{ else = false } ]
|
||||
auth-results = [ { if = "listener", eq = "smtp", then = true },
|
||||
{ else = false } ]
|
||||
message-id = [ { if = "listener", eq = "smtp", then = false },
|
||||
{ else = true } ]
|
||||
date = [ { if = "listener", eq = "smtp", then = false },
|
||||
{ else = true } ]
|
||||
return-path = false
|
||||
|
||||
[[session.throttle]]
|
||||
match = {if = "remote-ip", eq = "127.0.0.1"}
|
||||
key = ["remote-ip", "authenticated-as"]
|
||||
concurrency = 100
|
||||
rate = "50/30s"
|
||||
#match = {if = "remote-ip", eq = "10.0.0.1"}
|
||||
key = ["remote-ip"]
|
||||
concurrency = 5
|
||||
#rate = "5/1h"
|
||||
|
||||
[[session.throttle]]
|
||||
key = "sender-domain"
|
||||
concurrency = 10000
|
||||
key = ["sender-domain", "rcpt"]
|
||||
rate = "25/1h"
|
||||
|
||||
[auth.iprev]
|
||||
verify = "strict"
|
||||
verify = [ { if = "listener", eq = "smtp", then = "relaxed" },
|
||||
{ else = "disable" } ]
|
||||
|
||||
[auth.dkim]
|
||||
verify = "strict"
|
||||
sign = true
|
||||
verify = "relaxed"
|
||||
sign = [ { if = "listener", ne = "smtp", then = ["rsa"] },
|
||||
{ else = [] } ]
|
||||
|
||||
[signature."default"]
|
||||
public-key = "cert-name"
|
||||
private-key = "cert-name"
|
||||
domain = "example.org"
|
||||
selector = ""
|
||||
headers = ["From", "To", "Date", "Subject", "Message-ID"]
|
||||
algorithm = "rsa-sha256"
|
||||
canonicalization = "simple/relaxed"
|
||||
expire = "10d"
|
||||
third-party = ""
|
||||
third-party-algo = ""
|
||||
auid = ""
|
||||
set-body-length = false
|
||||
report = true
|
||||
|
||||
[auth.spf]
|
||||
verify.ehlo = "relaxed"
|
||||
verify.mail-from = "relaxed"
|
||||
[auth.spf.verify]
|
||||
ehlo = [ { if = "listener", eq = "smtp", then = "relaxed" },
|
||||
{ else = "disable" } ]
|
||||
mail-from = [ { if = "listener", eq = "smtp", then = "relaxed" },
|
||||
{ else = "disable" } ]
|
||||
|
||||
[auth.arc]
|
||||
verify = "strict"
|
||||
seal = true
|
||||
verify = "relaxed"
|
||||
seal = ["rsa"]
|
||||
|
||||
[auth.dmarc]
|
||||
verify = "strict"
|
||||
|
||||
[remote."lmtp"]
|
||||
address = 192.168.0.1
|
||||
port = 25
|
||||
protocol = "lmtp"
|
||||
|
||||
[remote."lmtp".auth]
|
||||
username = "hello"
|
||||
secret = "world"
|
||||
|
||||
[remote."lmtp".cache]
|
||||
entries = 1000
|
||||
ttl = {positive = 10, negative = 5}
|
||||
|
||||
[remote."lmtp".tls]
|
||||
implicit = true
|
||||
allow-invalid-certs = true
|
||||
verify = [ { if = "listener", eq = "smtp", then = "relaxed" },
|
||||
{ else = "disable" } ]
|
||||
|
||||
[queue]
|
||||
path = "/var/spool/queue"
|
||||
hash = 123
|
||||
path = "%%QUEUE_DIR%%"
|
||||
hash = 64
|
||||
|
||||
[queue.schedule]
|
||||
retry = ["0m", "2m", "5m", "10m", "15m", "30m", "1h", "2h"]
|
||||
retry = ["2m", "5m", "10m", "15m", "30m", "1h", "2h"]
|
||||
notify = ["1d", "3d"]
|
||||
expire = "5d"
|
||||
|
||||
[queue.outbound]
|
||||
#hostname = mx.domain.org
|
||||
next-hop = "lmtp"
|
||||
next-hop = [ { if = "rcpt-domain", in-list = "local-domains", then = "lmtp" },
|
||||
{ else = false } ]
|
||||
ip-strategy = "ipv4-then-ipv6"
|
||||
|
||||
[queue.outbound.tls]
|
||||
dane = require
|
||||
mta-sts = disabled
|
||||
starttls = optional
|
||||
dane = "optional"
|
||||
mta-sts = "optional"
|
||||
starttls = "require"
|
||||
|
||||
[queue.outbound.source-ip]
|
||||
v4 = ["192.168.0.2", "162.168.0.1"]
|
||||
v6 = ["192.168.0.2", "162.168.0.1"]
|
||||
#[queue.outbound.source-ip]
|
||||
#v4 = ["10.0.0.10", "10.0.0.11"]
|
||||
#v6 = ["a::b", "a::c"]
|
||||
|
||||
[queue.outbound.limits]
|
||||
mx = 5
|
||||
mx = 7
|
||||
multihomed = 2
|
||||
|
||||
[queue.outbound.timeouts]
|
||||
connect = "1m"
|
||||
greeting = "1m"
|
||||
tls = "1m"
|
||||
ehlo = "1m"
|
||||
mail-from = "1m"
|
||||
rcpt-to = "1m"
|
||||
data = "5m"
|
||||
mta-sts = "1m"
|
||||
connect = "3m"
|
||||
greeting = "3m"
|
||||
tls = "2m"
|
||||
ehlo = "3m"
|
||||
mail-from = "3m"
|
||||
rcpt-to = "3m"
|
||||
data = "10m"
|
||||
mta-sts = "2m"
|
||||
|
||||
[[queue.quota]]
|
||||
match = {if = "remote-ip", eq = "127.0.0.1"}
|
||||
key = [""]
|
||||
messages = 10000
|
||||
size = 1000000
|
||||
#match = {if = "remote-ip", eq = "10.0.0.1"}
|
||||
#key = ["rcpt"]
|
||||
messages = 100000
|
||||
size = 10737418240 # 10gb
|
||||
|
||||
[[queue.throttle]]
|
||||
rate = "1/60s"
|
||||
concurrency = 1000
|
||||
key = ["remote-ip"]
|
||||
key = ["rcpt-domain"]
|
||||
#rate = "100/1h"
|
||||
concurrency = 5
|
||||
|
||||
[resolver]
|
||||
type = "cloudflare"
|
||||
strategy = "ipv6"
|
||||
dnssec = true
|
||||
preserve-intermediates = true
|
||||
type = "cloudflare-tls"
|
||||
#preserve-intermediates = true
|
||||
concurrency = 2
|
||||
timeout = 100
|
||||
attempts = 3
|
||||
timeout = "5s"
|
||||
attempts = 2
|
||||
try-tcp-on-error = true
|
||||
|
||||
[resolver.cache]
|
||||
a = 1000
|
||||
mx = 9393
|
||||
txt = 3233
|
||||
tlsa = 333
|
||||
txt = 2048
|
||||
mx = 1024
|
||||
ipv4 = 1024
|
||||
ipv6 = 1024
|
||||
ptr = 1024
|
||||
tlsa = 1024
|
||||
mta-sts = 1024
|
||||
|
||||
[report]
|
||||
path = "%%QUEUE_DIR%%"
|
||||
hash = 64
|
||||
#submitter = "mx.domain.org"
|
||||
|
||||
[scripts]
|
||||
[report.analysis]
|
||||
addresses = ["dmarc@*", "abuse@*"]
|
||||
forward = true
|
||||
#store = "/var/spool/report"
|
||||
|
||||
[scripts]
|
||||
ehlo = "this is my script"
|
||||
[report.dsn]
|
||||
from-name = "Mail Delivery Subsystem"
|
||||
from-address = "MAILER-DAEMON@%%DOMAIN%%"
|
||||
sign = ["rsa"]
|
||||
|
||||
[report.dkim]
|
||||
from-name = "Report Subsystem"
|
||||
from-address = "noreply-dkim@%%DOMAIN%%"
|
||||
subject = "DKIM Authentication Failure Report"
|
||||
sign = ["rsa"]
|
||||
send = "1/1d"
|
||||
|
||||
[report.spf]
|
||||
from-name = "Report Subsystem"
|
||||
from-address = "noreply-spf@%%DOMAIN%%"
|
||||
subject = "SPF Authentication Failure Report"
|
||||
send = "1/1d"
|
||||
sign = ["rsa"]
|
||||
|
||||
[report.dmarc]
|
||||
from-name = "Report Subsystem"
|
||||
from-address = "noreply-dmarc@%%DOMAIN%%"
|
||||
subject = "DMARC Authentication Failure Report"
|
||||
send = "1/1d"
|
||||
sign = ["rsa"]
|
||||
|
||||
[report.dmarc.aggregate]
|
||||
from-name = "DMARC Report"
|
||||
from-address = "noreply-dmarc@%%DOMAIN%%"
|
||||
org-name = "%%DOMAIN%%"
|
||||
#contact-info = ""
|
||||
send = "daily"
|
||||
max-size = 26214400 # 25mb
|
||||
sign = ["rsa"]
|
||||
|
||||
[report.tls.aggregate]
|
||||
from-name = "TLS Report"
|
||||
from-address = "noreply-tls@%%DOMAIN%%"
|
||||
org-name = "%%DOMAIN%%"
|
||||
#contact-info = ""
|
||||
send = "daily"
|
||||
max-size = 26214400 # 25 mb
|
||||
sign = ["rsa"]
|
||||
|
||||
[signature."rsa"]
|
||||
public-key = "%%DKIM_PUBLIC%%"
|
||||
private-key = "%%DKIM_PRIVATE%%"
|
||||
domain = "%%DOMAIN%%"
|
||||
selector = "%%DKIM_SELECTOR%%"
|
||||
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
|
||||
|
||||
[remote."lmtp"]
|
||||
address = "%LMTP_HOST%"
|
||||
port = 25
|
||||
protocol = "lmtp"
|
||||
timeout = "1m"
|
||||
|
||||
[remote."lmtp".cache]
|
||||
entries = 1000
|
||||
ttl = {positive = "1d", negative = "1h"}
|
||||
|
||||
[remote."lmtp".tls]
|
||||
implicit = false
|
||||
allow-invalid-certs = true
|
||||
|
||||
#[remote."lmtp".auth]
|
||||
#username = ""
|
||||
#secret = ""
|
||||
|
||||
[remote."lmtp".limits]
|
||||
errors = 3
|
||||
requests = 50
|
||||
|
||||
[remote."imap"]
|
||||
address = "%IMAP_HOST%"
|
||||
port = 143
|
||||
protocol = "imap"
|
||||
timeout = "1m"
|
||||
|
||||
[remote."imap".cache]
|
||||
entries = 1000
|
||||
ttl = {positive = "1d", negative = "1h"}
|
||||
|
||||
[remote."imap".tls]
|
||||
implicit = false
|
||||
allow-invalid-certs = true
|
||||
|
||||
#[scripts]
|
||||
#ehlo = "this is my script"
|
||||
|
||||
[list."local-domains"]
|
||||
type = "inline"
|
||||
items = ["example.org", "*.example.net"]
|
||||
items = ["%%DOMAIN%%"]
|
||||
|
||||
[list."local-addresses"]
|
||||
type = "remote"
|
||||
host = "lmtp"
|
||||
|
||||
[list."authentication"]
|
||||
type = "remote"
|
||||
host = "imap"
|
||||
|
||||
#[list."local-users"]
|
||||
#type = "file"
|
||||
#path = "/tmp/file.txt"
|
||||
|
||||
[report]
|
||||
path = "/var/spool/report"
|
||||
hash = 16
|
||||
submitter = "mx.domain.org"
|
||||
|
||||
[report.analysis]
|
||||
addresses = ["dmarc@*", "abuse@*"]
|
||||
forward = true
|
||||
store = "/var/spool/report"
|
||||
|
||||
[report.dsn]
|
||||
from-name = "Mail Delivery Subsystem"
|
||||
from-address = "MAILER-DAEMON@domain.org"
|
||||
subject = "Delivery Status Notification"
|
||||
sign = []
|
||||
|
||||
[report.dkim]
|
||||
from-name = "Autentication Report"
|
||||
from-address = "noreply-auth-failure"
|
||||
subject = "SPF Authentication Failure Report"
|
||||
send = "1/20d"
|
||||
|
||||
[report.spf]
|
||||
from-name = "Autentication Report"
|
||||
from-address = "noreply-auth-failure"
|
||||
subject = "SPF Authentication Failure Report"
|
||||
sign = []
|
||||
send = "1/20d"
|
||||
|
||||
[report.dmarc]
|
||||
from-name = "DMARC report"
|
||||
from-address = "noreply-dmarc"
|
||||
subject = "DMARC Failure Report"
|
||||
send = "1/20d"
|
||||
sign = []
|
||||
|
||||
[report.dmarc.aggregate]
|
||||
from-name = "DMARC report"
|
||||
from-address = "noreply-dmarc"
|
||||
subject = "DMARC aggregate report for $1"
|
||||
org-name = "Mycorp"
|
||||
contact-info = ""
|
||||
send = never
|
||||
max-size = 10000
|
||||
sign = []
|
||||
|
||||
[report.tls.aggregate]
|
||||
from-name = "Autentication Report"
|
||||
from-address = "noreply-auth-failure"
|
||||
subject = "TLS Failure Report"
|
||||
org-name = "Mycorp"
|
||||
contact-info = ""
|
||||
send = never
|
||||
max-size = 10000
|
||||
sign = []
|
||||
|
||||
[servers."relay".dmarc]
|
||||
send-reports = true
|
||||
#report-frequency = requested, 86400
|
||||
incoming-address = "dmarc@*"
|
||||
|
||||
[certificate]
|
||||
[certificate."default"]
|
||||
cert = '''
|
||||
-----BEGIN CERTIFICATE-----
|
||||
|
|
44
resources/tests/reports/arf1.eml
Normal file
44
resources/tests/reports/arf1.eml
Normal file
|
@ -0,0 +1,44 @@
|
|||
From: <abusedesk@example.com>
|
||||
Date: Thu, 8 Mar 2005 17:40:36 EDT
|
||||
Subject: FW: Earn money
|
||||
To: <abuse@example.net>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report; report-type=feedback-report;
|
||||
boundary="part1_13d.2e68ed54_boundary"
|
||||
|
||||
--part1_13d.2e68ed54_boundary
|
||||
Content-Type: text/plain; charset="US-ASCII"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is an email abuse report for an email message received from IP
|
||||
192.0.2.1 on Thu, 8 Mar 2005 14:00:00 EDT. For more information
|
||||
about this format please see http://www.mipassoc.org/arf/.
|
||||
|
||||
--part1_13d.2e68ed54_boundary
|
||||
Content-Type: message/feedback-report
|
||||
|
||||
Feedback-Type: abuse
|
||||
User-Agent: SomeGenerator/1.0
|
||||
Version: 1
|
||||
|
||||
--part1_13d.2e68ed54_boundary
|
||||
Content-Type: message/rfc822
|
||||
Content-Disposition: inline
|
||||
|
||||
Received: from mailserver.example.net
|
||||
(mailserver.example.net [192.0.2.1])
|
||||
by example.com with ESMTP id M63d4137594e46;
|
||||
Thu, 08 Mar 2005 14:00:00 -0400
|
||||
From: <somespammer@example.net>
|
||||
To: <Undisclosed Recipients>
|
||||
Subject: Earn money
|
||||
MIME-Version: 1.0
|
||||
Content-type: text/plain
|
||||
Message-ID: 8787KJKJ3K4J3K4J3K4J3.mail@example.net
|
||||
Date: Thu, 02 Sep 2004 12:31:03 -0500
|
||||
|
||||
Spam Spam Spam
|
||||
Spam Spam Spam
|
||||
Spam Spam Spam
|
||||
Spam Spam Spam
|
||||
--part1_13d.2e68ed54_boundary--
|
55
resources/tests/reports/arf2.eml
Normal file
55
resources/tests/reports/arf2.eml
Normal file
|
@ -0,0 +1,55 @@
|
|||
From: <abusedesk@example.com>
|
||||
Date: Thu, 8 Mar 2005 17:40:36 EDT
|
||||
Subject: FW: Earn money
|
||||
To: <abuse@example.net>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report; report-type=feedback-report;
|
||||
boundary="part1_13d.2e68ed54_boundary"
|
||||
|
||||
--part1_13d.2e68ed54_boundary
|
||||
Content-Type: text/plain; charset="US-ASCII"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is an email abuse report for an email message received from IP
|
||||
192.0.2.1 on Thu, 8 Mar 2005 14:00:00 EDT. For more information
|
||||
about this format please see http://www.mipassoc.org/arf/.
|
||||
|
||||
--part1_13d.2e68ed54_boundary
|
||||
Content-Type: message/feedback-report
|
||||
|
||||
Feedback-Type: abuse
|
||||
User-Agent: SomeGenerator/1.0
|
||||
Version: 1
|
||||
Original-Mail-From: <somespammer@example.net>
|
||||
Original-Rcpt-To: <user@example.com>
|
||||
Arrival-Date: Thu, 8 Mar 2005 14:00:00 EDT
|
||||
Reporting-MTA: dns; mail.example.com
|
||||
Source-IP: 192.0.2.1
|
||||
Authentication-Results: mail.example.com;
|
||||
spf=fail smtp.mail=somespammer@example.com
|
||||
Reported-Domain: example.net
|
||||
Reported-Uri: http://example.net/earn_money.html
|
||||
Reported-Uri: mailto:user@example.com
|
||||
Removal-Recipient: user@example.com
|
||||
|
||||
--part1_13d.2e68ed54_boundary
|
||||
Content-Type: message/rfc822
|
||||
Content-Disposition: inline
|
||||
|
||||
From: <somespammer@example.net>
|
||||
Received: from mailserver.example.net (mailserver.example.net
|
||||
[192.0.2.1]) by example.com with ESMTP id M63d4137594e46;
|
||||
Thu, 08 Mar 2005 14:00:00 -0400
|
||||
|
||||
To: <Undisclosed Recipients>
|
||||
Subject: Earn money
|
||||
MIME-Version: 1.0
|
||||
Content-type: text/plain
|
||||
Message-ID: 8787KJKJ3K4J3K4J3K4J3.mail@example.net
|
||||
Date: Thu, 02 Sep 2004 12:31:03 -0500
|
||||
|
||||
Spam Spam Spam
|
||||
Spam Spam Spam
|
||||
Spam Spam Spam
|
||||
Spam Spam Spam
|
||||
--part1_13d.2e68ed54_boundary--
|
58
resources/tests/reports/arf3.eml
Normal file
58
resources/tests/reports/arf3.eml
Normal file
|
@ -0,0 +1,58 @@
|
|||
From: arf-daemon@example.com
|
||||
To: recipient@example.net
|
||||
Subject: This is a test
|
||||
Date: Wed, 14 Apr 2010 12:17:45 -0700 (PDT)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report; report-type=feedback-report;
|
||||
boundary="part1_13d.2e68ed54_boundary"
|
||||
|
||||
--part1_13d.2e68ed54_boundary
|
||||
Content-Type: text/plain; charset="US-ASCII"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is an email abuse report for an email message received
|
||||
from IP 192.0.2.1 on Wed, 14 Apr 2010 12:15:31 PDT. For more
|
||||
information about this format please see
|
||||
http://www.mipassoc.org/arf/.
|
||||
|
||||
--part1_13d.2e68ed54_boundary
|
||||
Content-Type: message/feedback-report
|
||||
|
||||
Feedback-Type: auth-failure
|
||||
User-Agent: SomeDKIMFilter/1.0
|
||||
Version: 1
|
||||
Original-Mail-From: <randomuser@example.net>
|
||||
Original-Rcpt-To: <user@example.com>
|
||||
Received-Date: Wed, 14 Apr 2010 12:15:31 -0700 (PDT)
|
||||
Source-IP: 192.0.2.1
|
||||
Authentication-Results: mail.example.com; dkim=fail
|
||||
header.d=example.net
|
||||
Reported-Domain: example.net
|
||||
DKIM-Domain: example.net
|
||||
Auth-Failure: bodyhash
|
||||
|
||||
--part1_13d.2e68ed54_boundary
|
||||
Content-Type: message/rfc822
|
||||
|
||||
DKIM-Signature: v=1; c=relaxed/simple; a=rsa-sha256;
|
||||
s=testkey; d=example.net; h=From:To:Subject:Date;
|
||||
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
|
||||
b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB
|
||||
4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut
|
||||
KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV
|
||||
4bmp/YzhwvcubU4=
|
||||
Received: from smtp-out.example.net by mail.example.com
|
||||
with SMTP id o3F52gxO029144;
|
||||
Wed, 14 Apr 2010 12:15:31 -0700 (PDT)
|
||||
Received: from internal-client-001.example.com
|
||||
by mail.example.com
|
||||
with SMTP id o3F3BwdY028431;
|
||||
Wed, 14 Apr 2010 12:12:09 -0700 (PDT)
|
||||
From: randomuser@example.net
|
||||
To: user@example.com
|
||||
Date: Wed, 14 Apr 2010 12:12:09 -0700 (PDT)
|
||||
Subject: This is a test
|
||||
|
||||
Hi, just making sure DKIM is working!
|
||||
|
||||
--part1_13d.2e68ed54_boundary--
|
73
resources/tests/reports/arf4.eml
Normal file
73
resources/tests/reports/arf4.eml
Normal file
|
@ -0,0 +1,73 @@
|
|||
Return-Path: <opendmarc@box.mydomain.name>
|
||||
Received: by box.mydomain.name (Postfix, from userid 116)
|
||||
id CF8FA658E0; Tue, 5 Oct 2021 17:37:02 +1300 (NZDT)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=box.mydomain.name;
|
||||
s=mail; t=1633408622;
|
||||
bh=yDlkGfe4dFwlsFeoKaIHG6xiRgQs2/PqPnLtiPm5ewk=;
|
||||
h=From:To:Date:Subject:From;
|
||||
b=TwXvJJFoFwJDcb6IKMKsxp2BiRDsrjLOESyQPh/Cc4tRZltVAud/k6f0XP4l5a/T8
|
||||
kh0iDOGImc0O1WZNFt0MUcwLsfW4qbYjCBtthQDnbPApvv6MJDASwau+wipu5Nrkjc
|
||||
flg+nMaD97pVgR0LevMoVIWoiy1f5PNC/z0xkY2wnvyoGn91WuDsdocOqyoPo4RmIT
|
||||
A/f3M4CjOv/QmMEAWBIsa7kAZwf+rNmzDahFOtp2vFLqHt0iZi5vs40fa6O/I0snTM
|
||||
fRkv2GMZAug7NMU8MN/MhuO87FV6ATZXvB0Kxvsy9z0zZYK7tM1OYHjiCYot45erG3
|
||||
dlKrYiXsfd3BQ==
|
||||
From: OpenDMARC Filter <opendmarc@box.mydomain.name>
|
||||
To: postmaster@vericty.interpublication.org
|
||||
Date: Tue, 5 Oct 2021 17:37:02 +1300 (NZDT)
|
||||
Subject: FW: Wir kaufen dein Auto!
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report;
|
||||
report-type=feedback-report;
|
||||
boundary="box.mydomain.name:8BE2660E72"
|
||||
Message-Id: <20211005043702.CF8FA658E0@box.mydomain.name>
|
||||
|
||||
--box.mydomain.name:8BE2660E72
|
||||
Content-Type: text/plain
|
||||
|
||||
This is an authentication failure report for an email message received from IP
|
||||
148.163.85.135 on Tue, 5 Oct 2021 17:37:02 +1300 (NZDT).
|
||||
|
||||
--box.mydomain.name:8BE2660E72
|
||||
Content-Type: message/feedback-report
|
||||
|
||||
Feedback-Type: auth-failure
|
||||
Version: 1
|
||||
User-Agent: OpenDMARC-Filter/1.3.2
|
||||
Auth-Failure: dmarc
|
||||
Authentication-Results: box.mydomain.name; dmarc=fail header.from=interpublication.org
|
||||
Original-Envelope-Id: 8BE2660E72
|
||||
Original-Mail-From: info@interpublication.org
|
||||
Source-IP: 148.163.85.135 (sainay.interpublication.org)
|
||||
Reported-Domain: interpublication.org
|
||||
|
||||
--box.mydomain.name:8BE2660E72
|
||||
Content-Type: text/rfc822-headers
|
||||
|
||||
Authentication-Results: box.mydomain.name;
|
||||
dkim=fail reason="signature verification failed" (2048-bit key; unprotected) header.d=interpublication.org header.i=@interpublication.org header.b="PrsTNnuH";
|
||||
dkim-atps=neutral
|
||||
Received: from dslb-002-202-150-127.002.202.pools.vodafone-ip.de (dslb-188-099-080-029.188.099.pools.vodafone-ip.de [188.99.80.29])
|
||||
by sainay.interpublication.org (Postfix) with ESMTPA id 6BB23A2D3
|
||||
for <address@myotherdomain.name>; Tue, 5 Oct 2021 00:36:52 -0400 (EDT)
|
||||
DKIM-Filter: OpenDKIM Filter v2.11.0 sainay.interpublication.org 6BB23A2D3
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=interpublication.org; s=default; t=1633408612;
|
||||
bh=q1/OPSn+VXteY2+DHXqOIgs5LsNCJisEcQIKVW9it6I=;
|
||||
h=From:Subject:To:Reply-To:Date:From;
|
||||
b=PrsTNnuH8D0Ch3gcWqGmXiYc2Kvu1CHGJBsqS521uYazd3G/urp7MHQvmNwK0r1gS
|
||||
DR3A3KwGejI5uuqzxDCqz28Mq6AkdTkOjFyXw65MLlsKTQddWTgciVnoqJempa6yzw
|
||||
PSM5550XqVFqqkNxEcYBUBYEwUdy1tY8rc4zhq8cIrsonQVxJJSbc3cdonICM1kLBV
|
||||
WASv16p3376ZBcKqFLc8UQ58YQKaFm51VZGEjtabfmWbgOQ7VikFFECDG3aRt8fZa6
|
||||
D03MrzUSngwPUdcRQZuqS/sApW/a9N2YwdbR51OFzPBr4ypUEIw/qprgBG4BfQQKeS
|
||||
1PhinNvVtgQpQ==
|
||||
From: "Rolf Bader" <info@interpublication.org>
|
||||
Subject: Wir kaufen dein Auto!
|
||||
To: "address" <address@myotherdomain.name>
|
||||
Content-Type: multipart/alternative; boundary="TD6gM3Blv=_XBZYNFT7dCsH1DHHOKUuSyA"
|
||||
MIME-Version: 1.0
|
||||
Reply-To: "Rolf Bader" <auto24-export@gmx.de>
|
||||
Organization: AutoTEAM24
|
||||
Date: Tue, 5 Oct 2021 06:36:51 +0200
|
||||
|
||||
--box.mydomain.name:8BE2660E72--
|
||||
|
87
resources/tests/reports/arf5.eml
Normal file
87
resources/tests/reports/arf5.eml
Normal file
|
@ -0,0 +1,87 @@
|
|||
Message-ID: <433689.81121.example@mta.mail.receiver.example>
|
||||
From: "SomeISP Antispam Feedback" <feedback@mail.receiver.example>
|
||||
To: arf-failure@sender.example
|
||||
Subject: FW: You have a new bill from your bank
|
||||
Date: Sat, 8 Oct 2011 15:15:59 -0500 (CDT)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report;
|
||||
boundary="------------Boundary-00=_3BCR4Y7kX93yP9uUPRhg";
|
||||
report-type=feedback-report
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
--------------Boundary-00=_3BCR4Y7kX93yP9uUPRhg
|
||||
Content-Type: text/plain; charset="us-ascii"
|
||||
Content-Disposition: inline
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is an authentication failure report for an email message
|
||||
received from a.sender.example on 8 Oct 2011 20:15:58 +0000 (GMT).
|
||||
For more information about this format, please see [RFC6591].
|
||||
|
||||
--------------Boundary-00=_3BCR4Y7kX93yP9uUPRhg
|
||||
Content-Type: message/feedback-report
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Feedback-Type: auth-failure
|
||||
User-Agent: Someisp!Mail-Feedback/1.0
|
||||
Version: 1
|
||||
Original-Mail-From: anexample.reply@a.sender.example
|
||||
Original-Envelope-Id: o3F52gxO029144
|
||||
Authentication-Results: mta1011.mail.tp2.receiver.example;
|
||||
dkim=fail (bodyhash) header.d=sender.example
|
||||
Auth-Failure: bodyhash
|
||||
DKIM-Canonicalized-Body: VGhpcyBpcyBhIG1lc3NhZ2UgYm9keSB0
|
||||
aGF0IGdvdCBtb2RpZmllZCBpbiB0cmFuc2l0LgoKQXQgdGhlIHNhbWU
|
||||
gdGltZSB0aGF0IHRoZSBib2R5aGFzaCBmYWlscyB0byB2ZXJpZnksIH
|
||||
RoZQptZXNzYWdlIGNvbnRlbnQgaXMgY2xlYXJseSBhYnVzaXZlIG9yI
|
||||
HBoaXNoeSwgYXMgdGhlClN1YmplY3QgYWxyZWFkeSBoaW50cy4gIElu
|
||||
ZGVlZCwgdGhpcyBib2R5IGFsc28gY29udGFpbnMKdGhlIGZvbGxvd2l
|
||||
uZyB0ZXh0OgoKICAgUGxlYXNlIGVudGVyIHlvdXIgZnVsbCBiYW5rIG
|
||||
NyZWRlbnRpYWxzIGF0CiAgIGh0dHA6Ly93d3cuc2VuZGVyLmV4YW1wb
|
||||
GUvCgpXZSBhcmUgaW1wbHlpbmcgdGhhdCwgYWx0aG91Z2ggbXVsdGlw
|
||||
bGUgZmFpbHVyZXMKcmVxdWlyZSBtdWx0aXBsZSByZXBvcnRzLCBhIHN
|
||||
pbmdsZSBmYWlsdXJlIGNhbiBiZQpyZXBvcnRlZCBhbG9uZyB3aXRoIH
|
||||
BoaXNoaW5nIGluIGEgc2luZ2xlIHJlcG9ydC4K
|
||||
DKIM-Domain: sender.example
|
||||
DKIM-Identity: @sender.example
|
||||
DKIM-Selector: testkey
|
||||
Arrival-Date: 8 Oct 2011 20:15:58 +0000 (GMT)
|
||||
Source-IP: 192.0.2.1
|
||||
Reported-Domain: a.sender.example
|
||||
Reported-URI: http://www.sender.example/
|
||||
|
||||
--------------Boundary-00=_3BCR4Y7kX93yP9uUPRhg
|
||||
Content-Type: text/rfc822-headers
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Authentication-Results: mta1011.mail.tp2.receiver.example;
|
||||
dkim=fail (bodyhash) header.d=sender.example;
|
||||
spf=pass smtp.mailfrom=anexample.reply@a.sender.example
|
||||
Received: from smtp-out.sender.example
|
||||
by mta1011.mail.tp2.receiver.example
|
||||
with SMTP id oB85W8xV000169;
|
||||
Sat, 08 Oct 2011 13:15:58 -0700 (PDT)
|
||||
DKIM-Signature: v=1; c=relaxed/simple; a=rsa-sha256;
|
||||
s=testkey; d=sender.example; h=From:To:Subject:Date;
|
||||
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
|
||||
b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB
|
||||
4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut
|
||||
KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV
|
||||
4bmp/YzhwvcubU4=
|
||||
Received: from mail.sender.example
|
||||
by smtp-out.sender.example
|
||||
with SMTP id o3F52gxO029144;
|
||||
Sat, 08 Oct 2011 13:15:31 -0700 (PDT)
|
||||
Received: from internal-client-001.sender.example
|
||||
by mail.sender.example
|
||||
with SMTP id o3F3BwdY028431;
|
||||
Sat, 08 Oct 2011 13:15:24 -0700 (PDT)
|
||||
Date: Sat, 8 Oct 2011 16:15:24 -0400 (EDT)
|
||||
Reply-To: anexample.reply@a.sender.example
|
||||
From: anexample@a.sender.example
|
||||
To: someuser@receiver.example
|
||||
Subject: You have a new bill from your bank
|
||||
Message-ID: <87913910.1318094604546@out.sender.example>
|
||||
|
||||
--------------Boundary-00=_3BCR4Y7kX93yP9uUPRhg--
|
||||
|
66
resources/tests/reports/dmarc1.eml
Normal file
66
resources/tests/reports/dmarc1.eml
Normal file
|
@ -0,0 +1,66 @@
|
|||
Received: from mail.stalw.art ([mail.stalw.art])
|
||||
by 127.0.0.1 (Stalwart JMAP) with LMTP;
|
||||
Mon, 28 Nov 2022 10:51:56 +0000
|
||||
Received: from mail-qv1-xf4a.google.com (mail-qv1-xf4a.google.com [IPv6:2607:f8b0:4864:20::f4a])
|
||||
(using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits)
|
||||
key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256)
|
||||
(No client certificate requested)
|
||||
by mail.stalw.art (Postfix) with ESMTPS id 1145E7CC0B
|
||||
for <domains@stalw.art>; Mon, 28 Nov 2022 10:51:53 +0000 (UTC)
|
||||
Received: by mail-qv1-xf4a.google.com with SMTP id 71-20020a0c804d000000b004b2fb260447so12985969qva.10
|
||||
for <domains@stalw.art>; Mon, 28 Nov 2022 02:51:52 -0800 (PST)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=google.com; s=20210112;
|
||||
h=content-transfer-encoding:content-disposition:to:from:subject
|
||||
:message-id:date:mime-version:from:to:cc:subject:date:message-id
|
||||
:reply-to;
|
||||
bh=sMF/38UFRhmUYFRJST4vLBu/U1BXgsdCUE02HF8nXx8=;
|
||||
b=I7WONP7tMsULp4eKjJeeKtM+nDYqMSIYMxqNHqCP1bTsnUiW2xM278I2+F8EjtFNYf
|
||||
XOgusNn8kqbSnA4w1+q4G87zTF4K3tGnxNpuUMQ7GzcofBKtr7VPv9XFqvTPJ+N8YSwe
|
||||
926ec7xi71BpSHAgqp5Wqocj8ruIVjcCZ37hYrG0C4s+FVBtbaU3EeyPpkESaaY2vE5y
|
||||
Qa2KsrMsyJXlbyW/sFJ7AGDDuXwyGkTa+btP/xIiQM2HlBKy7vNOFZKkxInOuQsXJgZy
|
||||
3H7ivlpD3hMrszwU77o5jBArVwN0RIkUSosAPQf+pzgvRlkseRlDrmzKQutvYWIaTP3/
|
||||
FHPA==
|
||||
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=1e100.net; s=20210112;
|
||||
h=content-transfer-encoding:content-disposition:to:from:subject
|
||||
:message-id:date:mime-version:x-gm-message-state:from:to:cc:subject
|
||||
:date:message-id:reply-to;
|
||||
bh=sMF/38UFRhmUYFRJST4vLBu/U1BXgsdCUE02HF8nXx8=;
|
||||
b=D3oClvT5AKcTpEjjffHQqPPQ9j5mmtExiviSq7iBYkoq+322LtR2hGqGxtvlAwRDsQ
|
||||
VIfuKVExygw3c9bckjzKtJYX128HGK35gHnmsrzqvCC93JlRaC/55kcM9Bhks0xJnl7i
|
||||
yNFHPZ0DY/jasdUdQ1QqnI+8qiPy+/12JvD+/TGlaDuS+RWYFU4/ky46S3vMXwXmRt6D
|
||||
IGggXoW7snSaM4s88DzMUl0U7DH823UPQrUxnA5Oxscwn9M1ENJUWD/3EJo5ZEUMw0ll
|
||||
Y8AlyhjWFgVqs1Y4V/LVWeXdF10fpm78+jm8QyIZYZJjh4I33AekdsWVM71ZNNYGL0+8
|
||||
+GDg==
|
||||
X-Gm-Message-State: ANoB5pn0zRZSWXdFXd9G0tawbSeUYuhxToVkIYoLf8OJzoBLcIc0wKcB
|
||||
AfU9Coz5vAuiM1mASWJhbg==
|
||||
X-Google-Smtp-Source: AA0mqf6WWsnqMD4cHE40jB89/zblmT7yNKeHKlsvvCmlYANKmpKLTQTaCm5qCA0mVmxR/PTQogsNndWH/qe0ug==
|
||||
MIME-Version: 1.0
|
||||
X-Received: by 2002:ac8:5182:0:b0:39c:cb6a:300b with SMTP id
|
||||
c2-20020ac85182000000b0039ccb6a300bmr48409299qtn.181.1669632711968; Mon, 28
|
||||
Nov 2022 02:51:51 -0800 (PST)
|
||||
Date: Sun, 27 Nov 2022 15:59:59 -0800
|
||||
Message-ID: <5264580628977113351@google.com>
|
||||
Subject: Report domain: stalw.art Submitter: google.com Report-ID: 5264580628977113351
|
||||
From: noreply-dmarc-support@google.com
|
||||
To: domains@stalw.art
|
||||
Content-Type: application/zip;
|
||||
name="google.com!stalw.art!1669507200!1669593599.zip"
|
||||
Content-Disposition: attachment;
|
||||
filename="google.com!stalw.art!1669507200!1669593599.zip"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
UEsDBAoAAAAIAHFUfFWAeOSU8QEAAKkEAAAuAAAAZ29vZ2xlLmNvbSFzdGFsdy5hcnQhMTY2OTUw
|
||||
NzIwMCExNjY5NTkzNTk5LnhtbKVUwZKjIBC9z1ekck9Qk5hoMcye9gt2zxbB1lBBoACTmb9fHNCw
|
||||
ma257El83f2632sUv70PYnUDY7mSr+t8m61XIJlquexf179//dyc1qs38oI7gPZM2ZW8rFbYgFbG
|
||||
NQM42lJHJ8yjyvSNpAOQXqlewJapAaMFDDkwUC6IVJ5BfGzagRq2saOe6H6kZSEv1rw7QxumpKPM
|
||||
NVx2ilyc07ZGKJZuH6WIIirtHQwq9mV5OGWe62t9II4yeEsORbn3uWVxqo7HPN/tDjlGj3BI91Kh
|
||||
MVT2UYyHztBzSfKyrA7Zsch8s4DMcZBtiFa7Q1X5UeRMhv5mW7qlnmKtBGcfjR7PgtsLLIMo744k
|
||||
1lFx31LjPFlAQpi2Vz4Qg1E4RNDq7hObngHSfg8SMNLx3c6AnRHNHMknVdPhc8p/TeR9ZMrMwxl1
|
||||
X+RbNRoGDdekoFle77uqZlme1+f9jtW1t/iRMJcwNUrfFKNwmOHYF25UjN64dg5MbnCrleXOX+A4
|
||||
f4okeZMZmlrrExZfovAuBhZzEq1PPf2mZoWYtyAd77j/fJayC9AWTNMZNaQbSuHI86Ua09FdGgN2
|
||||
FO5B+DTs98uP93piiJLiS6IWBDCnDLmB4FdujaayKLz2GV8MSDvjxJr/niIx2t/IJ9FTcrhPGD3+
|
||||
On8AUEsBAgoACgAAAAgAcVR8VYB45JTxAQAAqQQAAC4AAAAAAAAAAAAAAAAAAAAAAGdvb2dsZS5j
|
||||
b20hc3RhbHcuYXJ0ITE2Njk1MDcyMDAhMTY2OTU5MzU5OS54bWxQSwUGAAAAAAEAAQBcAAAAPQIA
|
||||
AAAA
|
68
resources/tests/reports/dmarc2.eml
Normal file
68
resources/tests/reports/dmarc2.eml
Normal file
|
@ -0,0 +1,68 @@
|
|||
Received: from mail.stalw.art ([mail.stalw.art])
|
||||
by 127.0.0.1 (Stalwart JMAP) with LMTP;
|
||||
Thu, 10 Nov 2022 03:27:19 +0000
|
||||
Received: from mx0.backschues.net (lnxs001.backschues.net [85.183.142.13])
|
||||
(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
|
||||
key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256)
|
||||
(No client certificate requested)
|
||||
by mail.stalw.art (Postfix) with ESMTPS id 6DD117CC0B
|
||||
for <domains@stalw.art>; Thu, 10 Nov 2022 03:27:16 +0000 (UTC)
|
||||
Received: from mx0.backschues.net (localhost [127.0.0.1])
|
||||
by mx0.backschues.net with SMTP id 4N76hg4lNgz9ryP
|
||||
for <domains@stalw.art>; Thu, 10 Nov 2022 04:27:15 +0100 (CET)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=backschues.net;
|
||||
s=mail-2014-01; t=1668050835;
|
||||
h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
|
||||
to:to:cc:mime-version:mime-version:content-type:content-type;
|
||||
bh=LTj1tdFz9JQFL/mVJASN0b9hGcolcCtY5v0bhnChJYY=;
|
||||
b=AtRegYc51PTYqDOy/6fB4xETTWAbVc2ivf8AfF4ygu3+6+oqBPyloTuOnEt7xYmjLFnll/
|
||||
SMZFFpRETsMlkiVg/1O0VpPRpIpiTbh4dwtUrRyo1Uw/cDJv5auz4rBMxcRNnDKypHwUKs
|
||||
BUahHWsVKH/TL5SzV79kqyjlYAs1HdJvS+wRINYBaptkeT6UeHGZakL21NnQUdOGt0fj4y
|
||||
eJvWVtCYHZ5DUJ8K8h2W1NlTAWP8nTBoQVVDQrI5Zi1AEvnUWw+H7E8d/q2cF756/IBYso
|
||||
rT56D3PYo2iuSt3aIBth1wL7/GJwc6N4JHcNpJ9XPV6xQbt+lm2b3+W59osL0Q==
|
||||
DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed; d=backschues.net;
|
||||
s=ed25519-mail-2018-10; t=1668050835;
|
||||
h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
|
||||
to:to:cc:mime-version:mime-version:content-type:content-type;
|
||||
bh=LTj1tdFz9JQFL/mVJASN0b9hGcolcCtY5v0bhnChJYY=;
|
||||
b=y7d79OWWCrDX40k91FoBdGnUcrjN7xvWYyqskPfQmMoaSqFNSlTHH8gMXC/vXwiYIP3Oxp
|
||||
d/hVvEuIIQBlwMDQ==
|
||||
From: "DMARC Aggregate Report" <noreply-dmarc-support@backschues.net>
|
||||
To: domains@stalw.art
|
||||
Subject: Report Domain: stalw.art
|
||||
Submitter: backschues.net
|
||||
Report-ID: stalw.art.1667948400.1668034800
|
||||
Date: Thu, 10 Nov 2022 03:27:02 GMT
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <afe541eab3ec091f@backschues.net>
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="----=_NextPart_84e1fdd0-b285-4922-9fc7-88b070204303"
|
||||
|
||||
This is a multipart message in MIME format.
|
||||
|
||||
------=_NextPart_84e1fdd0-b285-4922-9fc7-88b070204303
|
||||
Content-Type: text/plain; charset="us-ascii"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is an aggregate report from backschues.net.
|
||||
|
||||
Report domain: stalw.art
|
||||
Submitter: backschues.net
|
||||
Report ID: stalw.art.1667948400.1668034800
|
||||
|
||||
------=_NextPart_84e1fdd0-b285-4922-9fc7-88b070204303
|
||||
Content-Type: application/gzip
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment;
|
||||
filename="backschues.net!stalw.art!1667948400!1668034800.xml.gz"
|
||||
|
||||
H4sIAAAAAAAAA5VUsXLbMAzd/RU6D9ksSo6b2heG6dKOndJZR5OQzYtEsiSVNH9fUqQoqXWHT
|
||||
gIfgAfgASf8/KvvijcwVij5tK3LaluAZIoLeXna/nj5tjtui2eywS0AP1P2SjZFgQ1oZVzTg6
|
||||
OcOhowjypzaSTtgdz9HJR7DNGWXQew5fevLxhld4yGnoqOSOW5uo8d76lhOzvoQPxlkSrBYRR
|
||||
jY16qLTixjnbvJTWurB8ePp8Ox0NVBfNY3R+OVYXRHBpTfa/QGCovqQcPneEiJJnzMYrI5AfJ
|
||||
yZIyvCMZWrPlaktRsFadYB+NHs6dsFfIjSg/kJwH8GQRiW7KX0VPDEbRSKDV7YiFb4S0l08CR
|
||||
jq97QTYCdHMkTr0HYyxy7878ooyZXhcrHqfuNRgGDRCk09Vud/fl/X+VNangyfPnhjJ1CB9FY
|
||||
yikQrHMvBGu8HrxLOgXFitrHD+3FKzSyRHhblbv3TvzhKME7YJnlVAt2r5dcRRsOAgnWiFP/G
|
||||
UcAXKwTStUf1yBUt4ZPgjE9PBXRsDdujcRLVqLu1QgGtLf+zrpY6XG1KJptaGaxkf0y0Fnpts
|
||||
/7iRmS7KcYNukxX7zwbjWtaMicaf30qEEBaPB6P8h/gNNHLX4VQEAAA=
|
||||
------=_NextPart_84e1fdd0-b285-4922-9fc7-88b070204303--
|
52
resources/tests/reports/dmarc3.eml
Normal file
52
resources/tests/reports/dmarc3.eml
Normal file
|
@ -0,0 +1,52 @@
|
|||
Received: from mail.stalw.art ([mail.stalw.art])
|
||||
by 127.0.0.1 (Stalwart JMAP) with LMTP;
|
||||
Tue, 08 Nov 2022 23:26:41 +0000
|
||||
Received: from relay7.m.smailru.net (relay7.m.smailru.net [94.100.178.51])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mail.stalw.art (Postfix) with ESMTPS id DD4337CC09
|
||||
for <domains@stalw.art>; Tue, 8 Nov 2022 23:26:38 +0000 (UTC)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=corp.mail.ru; s=mail4;
|
||||
h=Date:Message-ID:To:From:Subject:MIME-Version:Content-Type:From:Subject:Content-Type:Content-Transfer-Encoding:To:Cc; bh=fooa0+RBCZvyV2mP8Nx/UsLQ5RhazFg+SPGNtxZrCX0=;
|
||||
t=1667950001;x=1668040001;
|
||||
b=J9aMEkY9eVdOxjkNxaPFJ2Yk+/NCux9uOZl3iJXI0hEFaeYj9g7l+WtmXczk+YvgH3yhVhtvONUEYFsValRWWCAfmePm429N3mSuclVktk7t6RPJ4O5EcMjwrD9882vmX1xpI7ecPOzd5AD67HPt5SIA1RIa5injaOI5CWUXBBa5c0zDfmciyANAiDw0gm1axEMK4AUc61txPsX7H1qRq/FxGNITnnpYdqkkT2lR8sTl5HPwTjEsw4sYGKr5SiMpROhhbLZTM8RpojkP73bmw3UBZ9FI8iKApJUFB8i9tu0hjzHkev4uoDXgOXFYs/RAI1JkCWEp2Rjb3LpTSHT6cA==;
|
||||
Received: from [10.161.4.115] (port=60844 helo=60)
|
||||
by relay7.m.smailru.net with esmtp (envelope-from <dmarc_support@corp.mail.ru>)
|
||||
id 1osXzK-0007VC-BD
|
||||
for domains@stalw.art; Wed, 09 Nov 2022 02:26:38 +0300
|
||||
Content-Type: multipart/mixed; boundary="===============5640625649776607409=="
|
||||
MIME-Version: 1.0
|
||||
Subject: Report Domain: stalw.art; Submitter: Mail.Ru;
|
||||
Report-ID: 28551467700969547611667865600
|
||||
From: dmarc_support@corp.mail.ru
|
||||
To: domains@stalw.art
|
||||
Message-ID: <dmarc-1667949998@corp.mail.ru>
|
||||
Date: Wed, 09 Nov 2022 02:26:38 +0300
|
||||
Auto-Submitted: auto-generated
|
||||
Authentication-Results: relay7.m.smailru.net; auth=pass smtp.auth=dmarc_support@corp.mail.ru smtp.mailfrom=dmarc_support@corp.mail.ru; iprev=pass policy.iprev=10.161.4.115
|
||||
|
||||
--===============5640625649776607409==
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
VGhpcyBpcyBhbiBhZ2dyZWdhdGUgcmVwb3J0IGZyb20gTWFpbC5SdS4=
|
||||
|
||||
--===============5640625649776607409==
|
||||
Content-Type: application/gzip
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment;
|
||||
filename="mail.ru!stalw.art!1667865600!1667952000.xml.gz"
|
||||
|
||||
H4sICK7lamMC/21haWwucnUhc3RhbHcuYXJ0ITE2Njc4NjU2MDAhMTY2Nzk1MjAwMC54bWwAdVNB
|
||||
cqMwELzvK3LzKQhYg01qouwHctkPULIYjMogqSThJL/fEQSC18kFzbRmWt0jAS/vQ/9wReeV0c+7
|
||||
LEl3D6ilaZQ+P+/G0D4edy/8F7SIzUnICweH1rhQDxhEI4LgYNy51mJA/ipUn/wdga0I4EAYbwbh
|
||||
ZO1HGzv/SONsEvHEUe1cAfgenKil0UHIUCvdGt6FYJ8Y67Bfy1lcHyNCjfcdizbV8PxYFNm+PBzS
|
||||
tCqrYn8os6wsD8eyKNMU2FchkAmsndBnknvCs9J8WzgjgLqZ4KrI0wjHHNi2ld3NxZpeyY/ajqde
|
||||
+Q7jUYb0a+6D6N8S4QIxzAiI5qIG7oDNAQhv2ymNK1iujUZgloNfYgrAysCzKCcG9L070CENO67m
|
||||
jVrN6CTWyvIiTfL8d5LlVZJVe+Jad0CaURMpsDlYTOBV9CO5jSaUt8arQO/lU8oWgUl/S9dE+GQl
|
||||
OpjzyQu7Z2STPNWgDqpV9BQ5dCgadHXrzLAd1xYGdtMhxtDVDv3YB/+pYpm3wtAm9Ca/xu2xRxmM
|
||||
m7bI7JrDzMCt8D5e6ZQsTm5Iv7nEleWKvboo76zQef4N+zyO/9in6fysWBqLfIjOiXBKftA6T/l2
|
||||
HGx5CGz9j/8BQWPZIPkDAAA=
|
||||
--===============5640625649776607409==--
|
126
resources/tests/reports/dmarc4.eml
Normal file
126
resources/tests/reports/dmarc4.eml
Normal file
|
@ -0,0 +1,126 @@
|
|||
Received: from mail.stalw.art ([mail.stalw.art]) by 127.0.0.1 (Stalwart JMAP) with LMTP; Tue, 25 Oct 2022 04:08:22 +0000
|
||||
Received: from NAM12-MW2-obe.outbound.protection.outlook.com (mail-mw2nam12on2073.outbound.protection.outlook.com [40.107.244.73])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mail.stalw.art (Postfix) with ESMTPS id A24107CC0A
|
||||
for <domains@stalw.art>; Tue, 25 Oct 2022 04:08:22 +0000 (UTC)
|
||||
ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none;
|
||||
b=SvolQ1oIEgdfCI6dbwmJ1jS0ovWmprW6kT3q9NgrbX+CMhIsdrqyS3Q1sO16KT2wCQAyNofiEZ5tKY0e1PzzMqeR29jUWvEye9T43fCfUeLFx9b45YrfkGYwqLeDIq0Ywl+ggVmsm7X83XqI6+9EC6qMukCb0cbLazu3rW/Rbyc6d5+fq6QTFZovATGRvHz71H9t7e//hYI23XjU5Q3Enw0Qq3xPSyusWDi3t7CfGXn9i2120XlNLnPxef5PCmwy4E+OTJ5qC5WtMthOskKKuFvx8onOYmc/JjJ3VrtZwALx9C+ulzix5US6H7pFvZ2jtDbMnW4U7ir/hp5xn5adFw==
|
||||
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
|
||||
s=arcselector9901;
|
||||
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1;
|
||||
bh=65M39Uvc5w8zbmK6TxEdoblSMXlIyqTXJHNalJ80wk4=;
|
||||
b=PN7QPeXJLr6tmH2CxydbDQjHqBtFKNN9HjGimeUHaIeSr82WHf4R295QbVX7gxw6sFE7Z9lZMTrMSqbRVI7rhbx+SEkxCfAothf9207FDX6t37Zt0wd/5EwR6dzfbcNJBL+U0/iG4J03L5b1geWY+e68mHKYH4/ybGcr+SBKuv/LgfZNtOfbQ3ioiKvFcpSDqd/qGUs4U9l2tVlXgbcKkct04sCuPciqgLEuIGirPLLbDUaBRJc51ZZB6CeporySRdHp6uFXyy3VBvvLVuwDNnnPrW4BUL05AuutzK7rc8ZQEpWf7r0gUEg2ArSrvs6Znnfe97oRa01L2SeFwuZsMA==
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none
|
||||
action=none header.from=microsoft.com; dkim=none (message not signed);
|
||||
arc=none
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=notification.microsoft.com; s=selector1;
|
||||
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
|
||||
bh=65M39Uvc5w8zbmK6TxEdoblSMXlIyqTXJHNalJ80wk4=;
|
||||
b=NjqsA7D6sEq1WgCZ/E1f5/B+XUXe5F4uv6CF2KQYVuyRnxItdox09LCWqZQ+fNQ6BbJ4Ne05Cb1BPbPP9yvb8Y6B1s2QvuxkUb69UFbAhoFgsRT6A4K76ykKQQyiPoYpxlO6FEyy+gel4y7c9XRLiWW6OxMIBcjBGB5ziP7mGFaJx4qXJ2mROfO7uZfrCu5pzOimkjPw6extWv4i0Kl3XKvBtXZnsr9eoC10mJvEAp7E2cpnaZnP46RQc9cmXzlmvhKPvCQCUWipJN9f1BTTvFjJ9ff6ehmN9RSzCckj3SZGw9XAnd0WYqh4evt6Y1RxQ4iQDSaZHNRpyMOtmkWc/w==
|
||||
Authentication-Results: dkim=none (message not signed)
|
||||
header.d=none;dmarc=none action=none header.from=microsoft.com;
|
||||
Received: from BN9PR03CA0046.namprd03.prod.outlook.com (2603:10b6:408:fb::21)
|
||||
by SJ0PR18MB3916.namprd18.prod.outlook.com (2603:10b6:a03:2c9::21) with
|
||||
Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5746.21; Tue, 25 Oct
|
||||
2022 04:08:19 +0000
|
||||
Received: from BN7NAM10FT048.eop-nam10.prod.protection.outlook.com
|
||||
(2603:10b6:408:fb:cafe::d7) by BN9PR03CA0046.outlook.office365.com
|
||||
(2603:10b6:408:fb::21) with Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5746.27 via Frontend
|
||||
Transport; Tue, 25 Oct 2022 04:08:19 +0000
|
||||
Received: from nam10.map.protection.outlook.com (2a01:111:f400:fe53::30) by
|
||||
BN7NAM10FT048.mail.protection.outlook.com (2a01:111:e400:7e8f::199) with
|
||||
Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5746.16 via Frontend
|
||||
Transport; Tue, 25 Oct 2022 04:08:19 +0000
|
||||
Message-ID: <725cbfbe133940149987cfc528387235@microsoft.com>
|
||||
X-Sender: <dmarcreport@microsoft.com> XATTRDIRECT=Originating XATTRORGID=xorgid:96f9e21d-a1c4-44a3-99e4-37191ac61848
|
||||
MIME-Version: 1.0
|
||||
From: "DMARC Aggregate Report" <dmarcreport@microsoft.com>
|
||||
To: <domains@stalw.art>
|
||||
Subject: =?utf-8?B?UmVwb3J0IERvbWFpbjogc3RhbHcuYXJ0IFN1Ym1pdHRlcjogcHJvdGVjdGlvbi5vdXRsb29rLmNvbSBSZXBvcnQtSUQ6IDcyNWNiZmJlMTMzOTQwMTQ5OTg3Y2ZjNTI4Mzg3MjM1?=
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="_mpm_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_"
|
||||
Date: Tue, 25 Oct 2022 04:08:19 +0000
|
||||
X-EOPAttributedMessage: 0
|
||||
X-MS-PublicTrafficType: Email
|
||||
X-MS-TrafficTypeDiagnostic: BN7NAM10FT048:EE_|SJ0PR18MB3916:EE_
|
||||
X-MS-Office365-Filtering-Correlation-Id: 7f843e40-ccc6-4c17-1ce7-08dab63e8cd1
|
||||
X-MS-Exchange-SenderADCheck: 2
|
||||
X-MS-Exchange-AntiSpam-Relay: 0
|
||||
X-Microsoft-Antispam: BCL:0;
|
||||
X-Microsoft-Antispam-Message-Info:
|
||||
PSiI3L4DKj/cyRBl8/bmbyQMrr1DvsYEB1+aTn/3Y39oHnyJ5HXcxu6jNUl32WcPW6Gfqmhc6P1RFE5L/9ev0cWnqh4GgIs2qmHicLexPmMjP8viPdjb1N7TSSOv1hhXMT+gVLx889X5sltd4qpfIAWhoxNonjQpVIgt4VOVnbCWTu1hyOjOVplq0rKqIF04BQGHZnBRfkcD1No+mZrvx8RLWIwInU3fpPeGz77Wn3TIvHtzypR/d22WpZ8eHk3aIxxdjwp5WLg4unpiJaieyQN7BRhD/v6b3pLFVJP8Ii2+FGjTsKASczEL4dHnIoIrHYE0wwaFFPcSNzovLhzYguDV42EGS8Fm7soiew4ch+hICM0LPNTGTZIDe7wm2eSwhN2tkJK4QCfh1DON39jXninVr88ZlzMcDXnXpgvWHHiur8az7Gvs9zHH/1tFMsPVSh7BS+8fHEcBYpdtihrP22GcjbOd98IiTAs/dVzSy0TUg6WEgJO6oUklGjqVbi99CrNZI1BtLP4vH4aSlz9JYg4et6SxiJlKyoSzqUr2NN9/pyFdQ//5d/EEjKJz8CAcQmCjjPEObGFttT3maY2+zsa2THodZgpfMyDbA3WUKxE=
|
||||
X-Forefront-Antispam-Report:
|
||||
CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:nam10.map.protection.outlook.com;PTR:;CAT:NONE;SFS:(13230022)(396003)(39860400002)(346002)(34036004)(366004)(376002)(136003)(47540400005)(451199015)(2616005)(52230400001)(121820200001)(83380400001)(166002)(86362001)(41300700001)(2906002)(4001150100001)(8936002)(316002)(5660300002)(235185007)(41320700001)(508600001)(6486002)(6512007)(6506007)(24736004)(108616005)(68406010)(85236043)(8676002)(10290500003)(6916009)(36736006)(36756003)(66899015);DIR:OUT;SFP:1101;
|
||||
X-OriginatorOrg: dmarcrep.onmicrosoft.com
|
||||
X-MS-Exchange-CrossTenant-OriginalArrivalTime: 25 Oct 2022 04:08:19.1682
|
||||
(UTC)
|
||||
X-MS-Exchange-CrossTenant-Network-Message-Id: 7f843e40-ccc6-4c17-1ce7-08dab63e8cd1
|
||||
X-MS-Exchange-CrossTenant-AuthSource: BN7NAM10FT048.eop-nam10.prod.protection.outlook.com
|
||||
X-MS-Exchange-CrossTenant-AuthAs: Internal
|
||||
X-MS-Exchange-CrossTenant-Id: 96f9e21d-a1c4-44a3-99e4-37191ac61848
|
||||
X-MS-Exchange-CrossTenant-FromEntityHeader: Internet
|
||||
X-MS-Exchange-Transport-CrossTenantHeadersStamped: SJ0PR18MB3916
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
|
||||
--_mpm_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_
|
||||
Content-Type: multipart/related;
|
||||
boundary="_rv_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_"
|
||||
|
||||
--_rv_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="_av_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_"
|
||||
|
||||
--_av_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_
|
||||
|
||||
|
||||
--_av_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_
|
||||
Content-Type: text/html; charset=us-ascii
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
PGRpdiBzdHlsZSA9ImZvbnQtZmFtaWx5OlNlZ29lIFVJOyBmb250LXNpemU6MTRweDsiPlRoaXMgaX
|
||||
MgYSBETUFSQyBhZ2dyZWdhdGUgcmVwb3J0IGZyb20gTWljcm9zb2Z0IENvcnBvcmF0aW9uLiBGb3Ig
|
||||
RW1haWxzIHJlY2VpdmVkIGJldHdlZW4gMjAyMi0xMC0yMyAwMDowMDowMCBVVEMgdG8gMjAyMi0xMC
|
||||
0yNCAwMDowMDowMCBVVEMuPC8gZGl2PjxiciAvPjxiciAvPllvdSdyZSByZWNlaXZpbmcgdGhpcyBl
|
||||
bWFpbCBiZWNhdXNlIHlvdSBoYXZlIGluY2x1ZGVkIHlvdXIgZW1haWwgYWRkcmVzcyBpbiB0aGUgJ3
|
||||
J1YScgdGFnIG9mIHlvdXIgRE1BUkMgcmVjb3JkIGluIEROUyBmb3Igc3RhbHcuYXJ0LiBQbGVhc2Ug
|
||||
cmVtb3ZlIHlvdXIgZW1haWwgYWRkcmVzcyBmcm9tIHRoZSAncnVhJyB0YWcgaWYgeW91IGRvbid0IH
|
||||
dhbnQgdG8gcmVjZWl2ZSB0aGlzIGVtYWlsLjxiciAvPjxiciAvPjxkaXYgc3R5bGUgPSJmb250LWZh
|
||||
bWlseTpTZWdvZSBVSTsgZm9udC1zaXplOjEycHg7IGNvbG9yOiM2NjY2NjY7Ij5QbGVhc2UgZG8gbm
|
||||
90IHJlc3BvbmQgdG8gdGhpcyBlLW1haWwuIFRoaXMgbWFpbGJveCBpcyBub3QgbW9uaXRvcmVkIGFu
|
||||
ZCB5b3Ugd2lsbCBub3QgcmVjZWl2ZSBhIHJlc3BvbnNlLiBGb3IgYW55IGZlZWRiYWNrL3N1Z2dlc3
|
||||
Rpb25zLCBraW5kbHkgbWFpbCB0byBkbWFyY3JlcG9ydGZlZWRiYWNrQG1pY3Jvc29mdC5jb20uPGJy
|
||||
IC8+PGJyIC8+TWljcm9zb2Z0IHJlc3BlY3RzIHlvdXIgcHJpdmFjeS4gUmV2aWV3IG91ciBPbmxpbm
|
||||
UgU2VydmljZXMgPGEgaHJlZiA9Imh0dHBzOi8vcHJpdmFjeS5taWNyb3NvZnQuY29tL2VuLXVzL3By
|
||||
aXZhY3lzdGF0ZW1lbnQiPlByaXZhY3kgU3RhdGVtZW50PC9hPi48YnIgLz5PbmUgTWljcm9zb2Z0IF
|
||||
dheSwgUmVkbW9uZCwgV0EsIFVTQSA5ODA1Mi48LyBkaXYgPg==
|
||||
|
||||
--_av_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_--
|
||||
|
||||
--_rv_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_--
|
||||
|
||||
--_mpm_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_
|
||||
Content-Type: application/gzip
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <3ff45643-7977-4f3c-a97d-14b9e7faa5e7>
|
||||
Content-Description: protection.outlook.com!stalw.art!1666483200!1666569600.xml.gz
|
||||
Content-Disposition: attachment; filename="protection.outlook.com!stalw.art!1666483200!1666569600.xml.gz";
|
||||
|
||||
H4sIAAAAAAAEAM1VzY7bIBi8V+o7RLnXxHZ+VyzbB2jVQy+9WQTjBMUGBDjZvn0/G4JJsu3usZcE5h
|
||||
vzDcNg45fXrp2dubFCyed5ni3mL+TzJ9xwXu8pO82gLO3Tq62f50fn9BNCl8slu5SZMgdULBY5+vX9
|
||||
20925B2dR7J4n/xFSOuoZHwO7WYzHCQQUIDRdTJWDNfKuKrjjtbU0REEGJasJO04+dG7VqlTxlSHUU
|
||||
QDCzqJltQdNcyv87UTzCirGucf8ITADq1ETTbFiu2bPc/Lcrdc5MvdbrthDVsV23K7KcoVRhM3PAzi
|
||||
eGWoPFybA7bnBwF7Wq/Xy20JBmDkkUjgsh7Lq/VuPZSHeVgP3S0YW944gbVqBftd6X7fCnvkkxwFO5
|
||||
METG4vGTUO1vNIqNP6JDpiMPKDK2p1M4LDf8A0kUpyjPQVsFfERkgzR/JhA8MgYI0iAMCvV/+mULCc
|
||||
KRNFG3WZvLGqN4xXQpPVIiuKMsuLXZbvltA3ViKZqV6CBIz8IOKhKz/Ttgc/61gZLBJWKyvcEDW/oR
|
||||
RJiYNDDQQFGJNZwYsmVCbHkt3e94VDjFvEoubSiUZA2tNEnHmrNK+cIiqNdlp4ZDdGdURw1wx3LSGP
|
||||
eKQfOa258WCSjBS+6nwUh2nvjpXhtm9dIvjekRCzSctN7rxpvOXMKTOS4MziPOH4PkRTa4fkj5PJ3p
|
||||
um/7GEv12/Ww1wVuIkrNFUFsU/tfiovaMlTeIH3WCQFdINAYD24+TDPiRvCvSQkIEfLji8CsJHhfwB
|
||||
wJC79XYGAAA=
|
||||
|
||||
--_mpm_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_--
|
54
resources/tests/reports/dmarc5.eml
Normal file
54
resources/tests/reports/dmarc5.eml
Normal file
|
@ -0,0 +1,54 @@
|
|||
Received: from mail.stalw.art ([mail.stalw.art]) by 127.0.0.1 (Stalwart JMAP) with LMTP; Tue, 20 Sep 2022 10:28:19 +0000
|
||||
Received: from a14-92.smtp-out.amazonses.com (a14-92.smtp-out.amazonses.com [54.240.14.92])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES128-SHA256 (128/128 bits))
|
||||
(No client certificate requested)
|
||||
by mail.stalw.art (Postfix) with ESMTPS id 1337D7E19D
|
||||
for <domains@stalw.art>; Tue, 20 Sep 2022 10:28:18 +0000 (UTC)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;
|
||||
s=a66wkfbz3zwxdt2n5p6d7lj2ja7sdwuc; d=amazonses.com; t=1663669697;
|
||||
h=From:To:Message-ID:Subject:MIME-Version:Content-Type:Date;
|
||||
bh=h9v7dueDYfUxVokuKSLTqLuOwisdgdDRQ6TLwJOzXes=;
|
||||
b=dHR5EJhoY9s8g2/Y4K4rHdz44k67r7fyC4wr2AWZmemrVBoxYHJPwa295S2VJQtY
|
||||
kxTxppN2GEcNxhUMw8TXBrRwNKdoOLU38ZtrAN1a4hWVxmlwky1dtjXETQ/qJ257Nzg
|
||||
bsXkAo4S1RABFmkQQJ0zSPZGkMW+lpZTBCDzlOHU=
|
||||
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;
|
||||
s=6gbrjpgwjskckoa6a5zn6fwqkn67xbtw; d=amazonses.com; t=1663669697;
|
||||
h=From:To:Message-ID:Subject:MIME-Version:Content-Type:Date:Feedback-ID;
|
||||
bh=h9v7dueDYfUxVokuKSLTqLuOwisdgdDRQ6TLwJOzXes=;
|
||||
b=UDIvc6rvbihyGbzGRsmSSSzVNFgpfb3V3j0UivcNjlX2y63vjLinol463Z/+3Xh3
|
||||
BmxAOiLHF/DbVnqqNg5ygdxsa7MBHXEJ5we3W8vQr37xNk5DqhV7HPBSFttWP5sy0dg
|
||||
rdjyfMIjqJ1J/2+aM4opFA/6EWif7TGmjo7N1KKM=
|
||||
From: postmaster@amazonses.com
|
||||
To: domains@stalw.art
|
||||
Message-ID: <010001835a70fc8d-a3d7eff5-7adb-41cc-87bd-a646d9776a69-000000@email.amazonses.com>
|
||||
Subject: Dmarc Aggregate Report Domain: {stalw.art} Submitter: {Amazon SES}
|
||||
Date: {2022-09-19} Report-ID: {6b06c366-0631-4ca0-8337-f5aecf137918}
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="----=_Part_42492_694130218.1663669697673"
|
||||
Date: Tue, 20 Sep 2022 10:28:17 +0000
|
||||
Feedback-ID: 1.us-east-1.CTa/CO4t1eWkL0VlHBu5/eINCZhxZraAIsQC/FZHIgk=:AmazonSES
|
||||
X-SES-Outgoing: 2022.09.20-54.240.14.92
|
||||
|
||||
------=_Part_42492_694130218.1663669697673
|
||||
Content-Type: text/plain; charset=us-ascii
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This MIME email was sent through Amazon SES.
|
||||
------=_Part_42492_694130218.1663669697673
|
||||
Content-Type: application/octet-stream;
|
||||
name=amazonses.com!stalw.art!1663545600!1663632000.xml.gz
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment;
|
||||
filename=amazonses.com!stalw.art!1663545600!1663632000.xml.gz
|
||||
|
||||
H4sIAAAAAAAAAG1TwXLbIBA9O1/RyV1CWLHszlDSHHJMe8itFw1GK5uJBAwgp+3XlwXJVjK9SOzb
|
||||
1b59vBV7/D0OXy7gvDL62z0tq/tHfsd6gO4o5Bu/27A5yauSMrIEEXdgjQvtCEF0IogIbZhxp1aL
|
||||
EfjTy9Ovnz+K1+dXRq4gVsAo1MCt8WEUPoD7Lkbx12gPvpRmZCTnsXLurzreHKtG1k1TVE1Niwcp
|
||||
quJQ1/ui3wmQPa33X+mBkVs9fh1HgtYJfUq0G3aEk9KcNk29e9g1VcVIRlISdJdSTb2tMIUxNiEf
|
||||
ulwpVpKZNYOSf1o7HQflzzCTm6hCcx/E8F4KF2KjjGBSdG9q5I6RfEiQt31C8I2A5dpoYMSmyC+h
|
||||
z7GVgVOcEw8I9IbHKD5xyP9MFO9SGpdnc+Y9i/ZmchJaZfm22pd0T0t6OJRb7HtLpUppJh0ZGcmH
|
||||
hM0scBHDFC8p9UblykdvVcAdyTOvkbkGZffR5picbyCJ7GdwvoSblA8k0YWsgKkOdFC9iiu52HiB
|
||||
wVhoe2dGnher1AP6uU6k2jOIDlwGVj6t4UT2iYSJKZxbB34awsy6jHu1fUV8sz0tNH7FrfAeVykF
|
||||
WediO/nUHcuycdHd5Zf8BxMenbqzAwAA
|
||||
------=_Part_42492_694130218.1663669697673--
|
41
resources/tests/reports/tls1.eml
Normal file
41
resources/tests/reports/tls1.eml
Normal file
|
@ -0,0 +1,41 @@
|
|||
From: tlsrpt@mail.sender.example.com
|
||||
Date: Fri, May 09 2017 16:54:30 -0800
|
||||
To: mts-sts-tlsrpt@example.net
|
||||
Subject: Report Domain: example.net
|
||||
Submitter: mail.sender.example.com
|
||||
Report-ID: <735ff.e317+bf22029@example.net>
|
||||
TLS-Report-Domain: example.net
|
||||
TLS-Report-Submitter: mail.sender.example.com
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report; report-type="tlsrpt";
|
||||
boundary="----=_NextPart_000_024E_01CC9B0A.AFE54C00"
|
||||
Content-Language: en-us
|
||||
|
||||
This is a multipart message in MIME format.
|
||||
|
||||
------=_NextPart_000_024E_01CC9B0A.AFE54C00
|
||||
Content-Type: text/plain; charset="us-ascii"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is an aggregate TLS report from mail.sender.example.com
|
||||
|
||||
------=_NextPart_000_024E_01CC9B0A.AFE54C00
|
||||
Content-Type: application/tlsrpt+gzip
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment;
|
||||
filename="mail.sender.example!example.com!1013662812!1013749130.json.gz"
|
||||
|
||||
H4sICCpFtWMAA3JwdDAxLmpzb24uMQCtVVtr2zAYfe+vEN7bmFzZjt3EMLbRhu1hdCUJI+soRpGU
|
||||
VMy2jCSHZCX/fZIvzVLHpc0WDDHSOfqOznfxwxkwP0fIFc75b6y5yGGOM+bEwLkUWYHzLZw772oU
|
||||
xZpBifOV3X6o1qp1pbHU0O5qXlN95EUQDSDyZgjF1XPbnFIxWE778H4QhyPz3DoVfNfEJiLXmGjI
|
||||
86WwDKUVlKwQUvN89ZE0Ujcu2+CsSFkruYZATi0nRFE48C8I9AMawMEFwXARMQRHg4hhxIZksHgk
|
||||
FiLlhDNleD8fde/vvMdsD7x4sgf1tmCN3L/u/xSltDS3OAh1AFszqUxmYjCdTdfekYMqVCYoi4Fm
|
||||
ylrSC9rE4K2bYZ66rWnbJ6Z1OXiT4JU5exgNEHI6oLv+m1FhQuXWgZdEM+rgvVC634le6V1RByu7
|
||||
w2COKrMMy57caaFxClVJCFNqWZpX8287g4gyt+LCwI+OqK95SyOwlKxDClDwrKSWR5k2b+qoB12x
|
||||
FVUyVab6sdgIM12x5MS2K9sUXDLal1plOtFUC8w0hryoWxF5MV0MY7wg1PSt58dxb8lJRhhfVwfU
|
||||
mWtnR7bxXllk9vqMdlzzEOrgd90jXmZMNah0qmAutMlvYWfDP3kTnOaN/0pv9ke1OgIXuZ4XuGH0
|
||||
Sj/NFXoImFJu578pYTtkZVZ9DWy4e60LFZ+f18NUuZ1p2+wklgc+AE7Be9AMW0AABH4AaPAGTBv7
|
||||
r4WetuaDbueenN41Tjmtv2FNM708td5o6Iaea8rNjfwT8jA8oQzgApNfZfF/GiV4Bm7HimRYVXBa
|
||||
RZ+HaJR8T8aTSXIz+Tb/kdx8mn1Jvo6vP5u/8fxyPL4aXx3JzcHKfsbW63dnuz+8byfQUQgAAA==
|
||||
|
||||
------=_NextPart_000_024E_01CC9B0A.AFE54C00--
|
64
resources/tests/reports/tls2.eml
Normal file
64
resources/tests/reports/tls2.eml
Normal file
|
@ -0,0 +1,64 @@
|
|||
From: tlsrpt@mail.sender.example.com
|
||||
Date: Fri, May 09 2017 16:54:30 -0800
|
||||
To: mts-sts-tlsrpt@example.net
|
||||
Subject: Report Domain: example.net
|
||||
Submitter: mail.sender.example.com
|
||||
Report-ID: <735ff.e317+bf22029@example.net>
|
||||
TLS-Report-Domain: example.net
|
||||
TLS-Report-Submitter: mail.sender.example.com
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report; report-type="tlsrpt";
|
||||
boundary="----=_NextPart_000_024E_01CC9B0A.AFE54C00"
|
||||
Content-Language: en-us
|
||||
|
||||
This is a multipart message in MIME format.
|
||||
|
||||
------=_NextPart_000_024E_01CC9B0A.AFE54C00
|
||||
Content-Type: text/plain; charset="us-ascii"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is an aggregate TLS report from mail.sender.example.com
|
||||
|
||||
------=_NextPart_000_024E_01CC9B0A.AFE54C00
|
||||
Content-Type: application/tlsrpt
|
||||
Content-Disposition: attachment;
|
||||
filename="mail.sender.example!example.com!1013662812!1013749130.json"
|
||||
|
||||
{
|
||||
"report-id": "2020-01-01T00:00:00Z_example.com",
|
||||
"date-range": {
|
||||
"start-datetime": "2020-01-01T00:00:00Z",
|
||||
"end-datetime": "2020-01-07T23:59:59Z"
|
||||
},
|
||||
"organization-name": "Google Inc.",
|
||||
"contact-info": "smtp-tls-reporting@google.com",
|
||||
"policies": [
|
||||
{
|
||||
"policy": {
|
||||
"policy-type": "sts",
|
||||
"policy-string": [
|
||||
"version: STSv1",
|
||||
"mode: enforce",
|
||||
"mx: demo.example.com",
|
||||
"max_age: 604800"
|
||||
],
|
||||
"policy-domain": "example.com"
|
||||
},
|
||||
"summary": {
|
||||
"total-successful-session-count": 23,
|
||||
"total-failure-session-count": 1
|
||||
},
|
||||
"failure-details": [
|
||||
{
|
||||
"result-type": "certificate-host-mismatch",
|
||||
"sending-mta-ip": "123.123.123.123",
|
||||
"receiving-ip": "234.234.234.234",
|
||||
"receiving-mx-hostname": "demo.example.com",
|
||||
"failed-session-count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
------=_NextPart_000_024E_01CC9B0A.AFE54C00--
|
|
@ -80,14 +80,14 @@ impl Config {
|
|||
&String::from_utf8(self.file_contents((
|
||||
"signature",
|
||||
id,
|
||||
"public-key",
|
||||
"private-key",
|
||||
))?)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"Failed to build RSA key for {}: {}",
|
||||
("signature", id, "public-key",).as_key(),
|
||||
("signature", id, "private-key",).as_key(),
|
||||
err
|
||||
)
|
||||
})?;
|
||||
|
@ -95,14 +95,14 @@ impl Config {
|
|||
&String::from_utf8(self.file_contents((
|
||||
"signature",
|
||||
id,
|
||||
"public-key",
|
||||
"private-key",
|
||||
))?)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"Failed to build RSA key for {}: {}",
|
||||
("signature", id, "public-key",).as_key(),
|
||||
("signature", id, "private-key",).as_key(),
|
||||
err
|
||||
)
|
||||
})?;
|
||||
|
|
|
@ -259,6 +259,7 @@ pub struct Auth {
|
|||
pub script: IfBlock<Option<Arc<Script>>>,
|
||||
pub lookup: IfBlock<Option<Arc<List>>>,
|
||||
pub mechanisms: IfBlock<u64>,
|
||||
pub require: IfBlock<bool>,
|
||||
pub errors_max: IfBlock<usize>,
|
||||
pub errors_wait: IfBlock<Duration>,
|
||||
}
|
||||
|
@ -397,7 +398,6 @@ pub struct Dsn {
|
|||
pub struct AggregateReport {
|
||||
pub name: IfBlock<String>,
|
||||
pub address: IfBlock<String>,
|
||||
pub subject: IfBlock<String>,
|
||||
pub org_name: IfBlock<Option<String>>,
|
||||
pub contact_info: IfBlock<Option<String>>,
|
||||
pub send: IfBlock<AggregateFrequency>,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{fs, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use mail_send::Credentials;
|
||||
|
||||
|
@ -103,10 +103,10 @@ impl Config {
|
|||
.parse_if_block("queue.outbound.tls.dane", ctx, &mx_envelope_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(RequireOptional::Optional)),
|
||||
mta_sts: self
|
||||
.parse_if_block("queue.outbound.tls.mta_sts", ctx, &rcpt_envelope_keys)?
|
||||
.parse_if_block("queue.outbound.tls.mta-sts", ctx, &rcpt_envelope_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(RequireOptional::Optional)),
|
||||
start: self
|
||||
.parse_if_block("queue.outbound.tls.tls", ctx, &mx_envelope_keys)?
|
||||
.parse_if_block("queue.outbound.tls.starttls", ctx, &mx_envelope_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(RequireOptional::Optional)),
|
||||
},
|
||||
throttle: self.parse_queue_throttle(ctx)?,
|
||||
|
@ -430,11 +430,10 @@ impl ParseValue for PathBuf {
|
|||
fn parse_value(_key: impl utils::AsKey, value: &str) -> super::Result<Self> {
|
||||
let path = PathBuf::from(value);
|
||||
|
||||
if !path.exists() {
|
||||
fs::create_dir(&path)
|
||||
.map_err(|err| format!("Failed to create spool directory {:?}: {}", path, err))?;
|
||||
if path.exists() {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(format!("Directory {} does not exist.", path.display()))
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ impl Config {
|
|||
.unwrap_or(Duration::from_secs(86400)),
|
||||
cache_ttl_negative: self
|
||||
.property(("remote", id, "cache.ttl.positive"))?
|
||||
.unwrap_or(Duration::from_secs(86400)),
|
||||
.unwrap_or(Duration::from_secs(3600)),
|
||||
timeout: self
|
||||
.property(("remote", id, "timeout"))?
|
||||
.unwrap_or(Duration::from_secs(60)),
|
||||
|
|
|
@ -111,15 +111,6 @@ impl Config {
|
|||
&rcpt_envelope_keys,
|
||||
)?
|
||||
.unwrap_or_else(|| IfBlock::new(format!("noreply-{}@{}", id, default_hostname))),
|
||||
subject: self
|
||||
.parse_if_block(
|
||||
("report", id, "aggregate.subject"),
|
||||
ctx,
|
||||
&rcpt_envelope_keys,
|
||||
)?
|
||||
.unwrap_or_else(|| {
|
||||
IfBlock::new(format!("{} Aggregage Report", id.to_ascii_uppercase()))
|
||||
}),
|
||||
org_name: self
|
||||
.parse_if_block(
|
||||
("report", id, "aggregate.org-name"),
|
||||
|
|
|
@ -35,10 +35,12 @@ impl Config {
|
|||
if let Some(preserve) = self.property("resolver.preserve-intermediates")? {
|
||||
opts.preserve_intermediates = preserve;
|
||||
}
|
||||
|
||||
if let Some(try_tcp_on_error) = self.property("resolver.try-tcp-on-error")? {
|
||||
opts.try_tcp_on_error = try_tcp_on_error;
|
||||
}
|
||||
if let Some(attempts) = self.property("resolver.attempts")? {
|
||||
opts.attempts = attempts;
|
||||
}
|
||||
|
||||
// Prepare DNSSEC resolver options
|
||||
let config_dnssec = config.clone();
|
||||
|
|
|
@ -9,6 +9,36 @@ use super::{
|
|||
|
||||
impl Config {
|
||||
pub fn parse_session_config(&self, ctx: &ConfigContext) -> super::Result<SessionConfig> {
|
||||
let available_keys = [
|
||||
EnvelopeKey::Listener,
|
||||
EnvelopeKey::RemoteIp,
|
||||
EnvelopeKey::LocalIp,
|
||||
];
|
||||
|
||||
Ok(SessionConfig {
|
||||
duration: self
|
||||
.parse_if_block("session.duration", ctx, &available_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(Duration::from_secs(15 * 60))),
|
||||
transfer_limit: self
|
||||
.parse_if_block("session.transfer-limit", ctx, &available_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(250 * 1024 * 1024)),
|
||||
timeout: self
|
||||
.parse_if_block::<Option<Duration>>("session.timeout", ctx, &available_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(Some(Duration::from_secs(5 * 60))))
|
||||
.try_unwrap("session.timeout")
|
||||
.unwrap_or_else(|_| IfBlock::new(Duration::from_secs(5 * 60))),
|
||||
throttle: self.parse_session_throttle(ctx)?,
|
||||
connect: self.parse_session_connect(ctx)?,
|
||||
ehlo: self.parse_session_ehlo(ctx)?,
|
||||
auth: self.parse_session_auth(ctx)?,
|
||||
mail: self.parse_session_mail(ctx)?,
|
||||
rcpt: self.parse_session_rcpt(ctx)?,
|
||||
data: self.parse_session_data(ctx)?,
|
||||
extensions: self.parse_extensions(ctx)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_session_throttle(&self, ctx: &ConfigContext) -> super::Result<SessionThrottle> {
|
||||
// Parse throttle
|
||||
let mut throttle = SessionThrottle {
|
||||
connect: Vec::new(),
|
||||
|
@ -78,33 +108,7 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
let available_keys = [
|
||||
EnvelopeKey::Listener,
|
||||
EnvelopeKey::RemoteIp,
|
||||
EnvelopeKey::LocalIp,
|
||||
];
|
||||
|
||||
Ok(SessionConfig {
|
||||
duration: self
|
||||
.parse_if_block("session.duration", ctx, &available_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(Duration::from_secs(15 * 60))),
|
||||
transfer_limit: self
|
||||
.parse_if_block("session.transfer-limit", ctx, &available_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(500 * 1024 * 1024)),
|
||||
timeout: self
|
||||
.parse_if_block::<Option<Duration>>("session.timeout", ctx, &available_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(Some(Duration::from_secs(5 * 60))))
|
||||
.try_unwrap("session.timeout")
|
||||
.unwrap_or_else(|_| IfBlock::new(Duration::from_secs(5 * 60))),
|
||||
throttle,
|
||||
connect: self.parse_session_connect(ctx)?,
|
||||
ehlo: self.parse_session_ehlo(ctx)?,
|
||||
auth: self.parse_session_auth(ctx)?,
|
||||
mail: self.parse_session_mail(ctx)?,
|
||||
rcpt: self.parse_session_rcpt(ctx)?,
|
||||
data: self.parse_session_data(ctx)?,
|
||||
extensions: self.parse_extensions(ctx)?,
|
||||
})
|
||||
Ok(throttle)
|
||||
}
|
||||
|
||||
fn parse_session_connect(&self, ctx: &ConfigContext) -> super::Result<Connect> {
|
||||
|
@ -212,6 +216,9 @@ impl Config {
|
|||
.into_iter()
|
||||
.fold(0, |acc, m| acc | m.mechanism),
|
||||
},
|
||||
require: self
|
||||
.parse_if_block("session.auth.require", ctx, &available_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(false)),
|
||||
errors_max: self
|
||||
.parse_if_block("session.auth.errors.max", ctx, &available_keys)?
|
||||
.unwrap_or_else(|| IfBlock::new(3)),
|
||||
|
|
|
@ -161,6 +161,7 @@ pub struct SessionParameters {
|
|||
// Auth parameters
|
||||
pub auth_script: Option<Arc<Script>>,
|
||||
pub auth_lookup: Option<Arc<List>>,
|
||||
pub auth_require: bool,
|
||||
pub auth_errors_max: usize,
|
||||
pub auth_errors_wait: Duration,
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ impl<T: AsyncRead + AsyncWrite> Session<T> {
|
|||
let ac = &self.core.session.config.auth;
|
||||
self.params.auth_script = ac.script.eval(self).await.clone();
|
||||
self.params.auth_lookup = ac.lookup.eval(self).await.clone();
|
||||
self.params.auth_require = *ac.require.eval(self).await;
|
||||
self.params.auth_errors_max = *ac.errors_max.eval(self).await;
|
||||
self.params.auth_errors_wait = *ac.errors_wait.eval(self).await;
|
||||
|
||||
|
|
|
@ -76,6 +76,10 @@ impl RateLimiter {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn elapsed(&self) -> Duration {
|
||||
self.limiter.0.elapsed()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.limiter = (Instant::now(), self.max_requests);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::sync::{atomic::Ordering, Arc};
|
||||
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use super::Core;
|
||||
|
@ -26,4 +28,33 @@ impl Core {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup(&self) {
|
||||
for throttle in [&self.session.throttle, &self.queue.throttle] {
|
||||
throttle.retain(|_, v| {
|
||||
v.concurrency
|
||||
.as_ref()
|
||||
.map_or(false, |c| c.concurrent.load(Ordering::Relaxed) > 0)
|
||||
|| v.rate
|
||||
.as_ref()
|
||||
.map_or(false, |r| r.elapsed().as_secs_f64() < r.max_interval)
|
||||
});
|
||||
}
|
||||
self.queue.quota.retain(|_, v| {
|
||||
v.messages.load(Ordering::Relaxed) > 0 || v.size.load(Ordering::Relaxed) > 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SpawnCleanup {
|
||||
fn spawn_cleanup(&self);
|
||||
}
|
||||
|
||||
impl SpawnCleanup for Arc<Core> {
|
||||
fn spawn_cleanup(&self) {
|
||||
let core = self.clone();
|
||||
self.worker_pool.spawn(move || {
|
||||
core.cleanup();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,10 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
|
|||
return self
|
||||
.write(b"503 5.5.1 Multiple MAIL commands not allowed.\r\n")
|
||||
.await;
|
||||
} else if self.params.auth_require && self.data.authenticated_as.is_empty() {
|
||||
return self
|
||||
.write(b"503 5.5.1 You must authenticate first.\r\n")
|
||||
.await;
|
||||
} else if self.data.iprev.is_none() && self.params.iprev.verify() {
|
||||
let iprev = self
|
||||
.core
|
||||
|
|
|
@ -33,10 +33,6 @@ impl Server {
|
|||
for listener_config in self.listeners {
|
||||
// Bind socket
|
||||
let local_ip = listener_config.addr.ip();
|
||||
listener_config
|
||||
.socket
|
||||
.bind(listener_config.addr)
|
||||
.map_err(|err| format!("Failed to bind to {}: {}", listener_config.addr, err))?;
|
||||
let listener = listener_config
|
||||
.socket
|
||||
.listen(listener_config.backlog.unwrap_or(1024))
|
||||
|
|
59
src/main.rs
59
src/main.rs
|
@ -68,40 +68,43 @@ async fn main() -> std::io::Result<()> {
|
|||
),
|
||||
throttle: DashMap::with_capacity_and_hasher_and_shard_amount(
|
||||
config
|
||||
.property("global.throttle-map.capacity")
|
||||
.failed("Failed to parse throttle map capacity")
|
||||
.property("global.shared-map.capacity")
|
||||
.failed("Failed to parse shared map capacity")
|
||||
.unwrap_or(2),
|
||||
ThrottleKeyHasherBuilder::default(),
|
||||
config
|
||||
.property("global.throttle-map.shard")
|
||||
.failed("Failed to parse throttle map shard amount")
|
||||
.unwrap_or(32),
|
||||
.property::<u64>("global.shared-map.shard")
|
||||
.failed("Failed to parse shared map shard amount")
|
||||
.unwrap_or(32)
|
||||
.next_power_of_two() as usize,
|
||||
),
|
||||
},
|
||||
queue: QueueCore {
|
||||
config: queue_config,
|
||||
throttle: DashMap::with_capacity_and_hasher_and_shard_amount(
|
||||
config
|
||||
.property("global.throttle-map.capacity")
|
||||
.failed("Failed to parse throttle map capacity")
|
||||
.property("global.shared-map.capacity")
|
||||
.failed("Failed to parse shared map capacity")
|
||||
.unwrap_or(2),
|
||||
ThrottleKeyHasherBuilder::default(),
|
||||
config
|
||||
.property("global.throttle-map.shard")
|
||||
.failed("Failed to parse throttle map shard amount")
|
||||
.unwrap_or(32),
|
||||
.property::<u64>("global.shared-map.shard")
|
||||
.failed("Failed to parse shared map shard amount")
|
||||
.unwrap_or(32)
|
||||
.next_power_of_two() as usize,
|
||||
),
|
||||
id_seq: 0.into(),
|
||||
quota: DashMap::with_capacity_and_hasher_and_shard_amount(
|
||||
config
|
||||
.property("global.throttle-map.capacity")
|
||||
.failed("Failed to parse throttle map capacity")
|
||||
.property("global.shared-map.capacity")
|
||||
.failed("Failed to parse shared map capacity")
|
||||
.unwrap_or(2),
|
||||
ThrottleKeyHasherBuilder::default(),
|
||||
config
|
||||
.property("global.throttle-map.shard")
|
||||
.failed("Failed to parse throttle map shard amount")
|
||||
.unwrap_or(32),
|
||||
.property::<u64>("global.shared-map.shard")
|
||||
.failed("Failed to parse shared map shard amount")
|
||||
.unwrap_or(32)
|
||||
.next_power_of_two() as usize,
|
||||
),
|
||||
tx: queue_tx,
|
||||
connectors: TlsConnectors {
|
||||
|
@ -116,6 +119,25 @@ async fn main() -> std::io::Result<()> {
|
|||
mail_auth: mail_auth_config,
|
||||
});
|
||||
|
||||
// Bind ports before dropping privileges
|
||||
for server in &config_context.servers {
|
||||
for listener in &server.listeners {
|
||||
listener
|
||||
.socket
|
||||
.bind(listener.addr)
|
||||
.failed(&format!("Failed to bind to {}", listener.addr));
|
||||
}
|
||||
}
|
||||
|
||||
// Drop privileges
|
||||
if let Some(run_as_user) = config.value("server.run-as.user") {
|
||||
let mut pd = privdrop::PrivDrop::default().user(run_as_user);
|
||||
if let Some(run_as_group) = config.value("server.run-as.group") {
|
||||
pd = pd.group(run_as_group);
|
||||
}
|
||||
pd.apply().failed("Failed to drop privileges");
|
||||
}
|
||||
|
||||
// Enable logging
|
||||
tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::FmtSubscriber::builder()
|
||||
|
@ -139,6 +161,13 @@ async fn main() -> std::io::Result<()> {
|
|||
// Spawn report manager
|
||||
report_rx.spawn(core.clone(), core.report.read_reports().await);
|
||||
|
||||
// Spawn remote hosts
|
||||
for host in config_context.hosts.into_values() {
|
||||
if host.ref_count != 0 {
|
||||
host.spawn(&config);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn listeners
|
||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||
for server in config_context.servers {
|
||||
|
|
|
@ -56,21 +56,20 @@ pub trait RemoteLookup: Clone {
|
|||
}
|
||||
|
||||
impl Host {
|
||||
pub fn spawn(self, config: &Config) -> mpsc::Sender<Event> {
|
||||
pub fn spawn(self, config: &Config) -> LookupChannel {
|
||||
// Create channel
|
||||
let (tx, rx) = mpsc::channel(1024);
|
||||
let local_host = config
|
||||
.value("server.hostname")
|
||||
.unwrap_or("[127.0.0.1]")
|
||||
.to_string();
|
||||
let tx_ = tx.clone();
|
||||
let tx_ = self.channel_tx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Prepare builders
|
||||
match self.protocol {
|
||||
ServerProtocol::Smtp | ServerProtocol::Lmtp => {
|
||||
RemoteHost {
|
||||
tx,
|
||||
tx: self.channel_tx,
|
||||
host: Arc::new(SmtpClientBuilder {
|
||||
builder: mail_send::SmtpClientBuilder {
|
||||
addr: format!("{}:{}", self.address, self.port),
|
||||
|
@ -87,7 +86,7 @@ impl Host {
|
|||
}),
|
||||
}
|
||||
.run(
|
||||
rx,
|
||||
self.channel_rx,
|
||||
self.cache_entries,
|
||||
self.cache_ttl_positive,
|
||||
self.cache_ttl_negative,
|
||||
|
@ -97,7 +96,7 @@ impl Host {
|
|||
}
|
||||
ServerProtocol::Imap => {
|
||||
RemoteHost {
|
||||
tx,
|
||||
tx: self.channel_tx,
|
||||
host: Arc::new(
|
||||
ImapAuthClientBuilder::new(
|
||||
format!("{}:{}", self.address, self.port),
|
||||
|
@ -111,7 +110,7 @@ impl Host {
|
|||
),
|
||||
}
|
||||
.run(
|
||||
rx,
|
||||
self.channel_rx,
|
||||
self.cache_entries,
|
||||
self.cache_ttl_positive,
|
||||
self.cache_ttl_negative,
|
||||
|
@ -122,7 +121,7 @@ impl Host {
|
|||
}
|
||||
});
|
||||
|
||||
tx_
|
||||
LookupChannel { tx: tx_ }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ impl AnalyzeReport for Arc<Core> {
|
|||
let message = if let Some(message) = Message::parse(&message) {
|
||||
message
|
||||
} else {
|
||||
tracing::debug!(context = "report", "Failed to parse message.");
|
||||
return;
|
||||
};
|
||||
let from = match message.from() {
|
||||
|
@ -114,7 +115,9 @@ impl AnalyzeReport for Arc<Core> {
|
|||
("xml", _) => Format::Dmarc,
|
||||
("tlsrpt", _) | (_, "json") => Format::Tls,
|
||||
_ => {
|
||||
if attachment_name.map_or(false, |n| n.contains(".xml")) {
|
||||
if attachment_name
|
||||
.map_or(false, |n| n.contains(".xml") || n.contains('!'))
|
||||
{
|
||||
Format::Dmarc
|
||||
} else {
|
||||
continue;
|
||||
|
|
|
@ -29,10 +29,10 @@ use super::{
|
|||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct DmarcFormat {
|
||||
rua: Vec<URI>,
|
||||
policy: PolicyPublished,
|
||||
records: Vec<Record>,
|
||||
pub struct DmarcFormat {
|
||||
pub rua: Vec<URI>,
|
||||
pub policy: PolicyPublished,
|
||||
pub records: Vec<Record>,
|
||||
}
|
||||
|
||||
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
|
||||
|
@ -412,7 +412,7 @@ impl Scheduler {
|
|||
Entry::Occupied(e) => (None, e.into_mut().dmarc_path()),
|
||||
Entry::Vacant(e) => {
|
||||
let domain = e.key().domain_name().to_string();
|
||||
let created = event.interval.from_timestamp();
|
||||
let created = event.interval.to_timestamp();
|
||||
let deliver_at = created + event.interval.as_secs();
|
||||
|
||||
self.main.push(Schedule {
|
||||
|
@ -452,62 +452,7 @@ impl Scheduler {
|
|||
}
|
||||
} else if path.size < *max_size {
|
||||
// Append to existing report
|
||||
path.size += json_append(&path.path, &event.report_record).await;
|
||||
path.size += json_append(&path.path, &event.report_record, *max_size - path.size).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mail_auth::{
|
||||
dmarc::URI,
|
||||
report::{Alignment, Disposition, PolicyPublished, Record},
|
||||
};
|
||||
|
||||
use crate::reporting::dmarc::DmarcFormat;
|
||||
|
||||
#[test]
|
||||
fn strip_json() {
|
||||
let mut d = DmarcFormat {
|
||||
rua: vec![
|
||||
URI {
|
||||
uri: "hello".to_string(),
|
||||
max_size: 0,
|
||||
},
|
||||
URI {
|
||||
uri: "world".to_string(),
|
||||
max_size: 0,
|
||||
},
|
||||
],
|
||||
policy: PolicyPublished {
|
||||
domain: "example.org".to_string(),
|
||||
version_published: None,
|
||||
adkim: Alignment::Relaxed,
|
||||
aspf: Alignment::Strict,
|
||||
p: Disposition::Quarantine,
|
||||
sp: Disposition::Reject,
|
||||
testing: false,
|
||||
fo: None,
|
||||
},
|
||||
records: vec![Record::default()
|
||||
.with_count(1)
|
||||
.with_envelope_from("domain.net")
|
||||
.with_envelope_to("other.org")],
|
||||
};
|
||||
let mut s = serde_json::to_string(&d).unwrap();
|
||||
s.truncate(s.len() - 2);
|
||||
|
||||
let r = Record::default()
|
||||
.with_count(2)
|
||||
.with_envelope_from("otherdomain.net")
|
||||
.with_envelope_to("otherother.org");
|
||||
let rs = serde_json::to_string(&r).unwrap();
|
||||
|
||||
d.records.push(r);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<DmarcFormat>(&format!("{},{}]}}", s, rs)).unwrap(),
|
||||
d
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,12 +167,15 @@ impl Message {
|
|||
}
|
||||
|
||||
impl AggregateFrequency {
|
||||
pub fn from_timestamp(&self) -> u64 {
|
||||
let mut dt = DateTime::from_timestamp(
|
||||
pub fn to_timestamp(&self) -> u64 {
|
||||
self.to_timestamp_(DateTime::from_timestamp(
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_secs()) as i64,
|
||||
);
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_timestamp_(&self, mut dt: DateTime) -> u64 {
|
||||
(match self {
|
||||
AggregateFrequency::Hourly => {
|
||||
dt.minute = 0;
|
||||
|
@ -190,7 +193,7 @@ impl AggregateFrequency {
|
|||
dt.hour = 0;
|
||||
dt.minute = 0;
|
||||
dt.second = 0;
|
||||
dt.to_timestamp() - (86400 * 7 * dow as i64)
|
||||
dt.to_timestamp() - (86400 * dow as i64)
|
||||
}
|
||||
AggregateFrequency::Never => dt.to_timestamp(),
|
||||
}) as u64
|
||||
|
@ -251,3 +254,53 @@ impl From<(&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)> for PolicyType {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mail_parser::DateTime;
|
||||
|
||||
use crate::config::AggregateFrequency;
|
||||
|
||||
#[test]
|
||||
fn aggregate_to_timestamp() {
|
||||
for (freq, date, expected) in [
|
||||
(
|
||||
AggregateFrequency::Hourly,
|
||||
"2023-01-24T09:10:40Z",
|
||||
"2023-01-24T09:00:00Z",
|
||||
),
|
||||
(
|
||||
AggregateFrequency::Daily,
|
||||
"2023-01-24T09:10:40Z",
|
||||
"2023-01-24T00:00:00Z",
|
||||
),
|
||||
(
|
||||
AggregateFrequency::Weekly,
|
||||
"2023-01-24T09:10:40Z",
|
||||
"2023-01-22T00:00:00Z",
|
||||
),
|
||||
(
|
||||
AggregateFrequency::Weekly,
|
||||
"2023-01-28T23:59:59Z",
|
||||
"2023-01-22T00:00:00Z",
|
||||
),
|
||||
(
|
||||
AggregateFrequency::Weekly,
|
||||
"2023-01-22T23:59:59Z",
|
||||
"2023-01-22T00:00:00Z",
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
DateTime::from_timestamp(
|
||||
freq.to_timestamp_(DateTime::parse_rfc3339(date).unwrap()) as i64
|
||||
)
|
||||
.to_rfc3339(),
|
||||
expected,
|
||||
"failed for {:?} {} {}",
|
||||
freq,
|
||||
date,
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ use tokio::{
|
|||
|
||||
use crate::{
|
||||
config::AggregateFrequency,
|
||||
core::{Core, ReportCore},
|
||||
core::{worker::SpawnCleanup, Core, ReportCore},
|
||||
queue::{InstantFromTimestamp, Schedule},
|
||||
};
|
||||
|
||||
|
@ -39,12 +39,13 @@ pub struct Scheduler {
|
|||
pub reports: AHashMap<ReportKey, ReportValue>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ReportType<T, U> {
|
||||
Dmarc(T),
|
||||
Tls(U),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ReportPath<T> {
|
||||
pub path: T,
|
||||
pub size: usize,
|
||||
|
@ -52,7 +53,7 @@ pub struct ReportPath<T> {
|
|||
pub deliver_at: AggregateFrequency,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ReportPolicy<T> {
|
||||
pub inner: T,
|
||||
pub policy: u64,
|
||||
|
@ -61,6 +62,8 @@ pub struct ReportPolicy<T> {
|
|||
impl SpawnReport for mpsc::Receiver<Event> {
|
||||
fn spawn(mut self, core: Arc<Core>, mut scheduler: Scheduler) {
|
||||
tokio::spawn(async move {
|
||||
let mut last_cleanup = Instant::now();
|
||||
|
||||
loop {
|
||||
match tokio::time::timeout(scheduler.wake_up_time(), self.recv()).await {
|
||||
Ok(Some(event)) => match event {
|
||||
|
@ -85,6 +88,12 @@ impl SpawnReport for mpsc::Receiver<Event> {
|
|||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup expired throttles
|
||||
if last_cleanup.elapsed().as_secs() >= 86400 {
|
||||
last_cleanup = Instant::now();
|
||||
core.spawn_cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,8 +110,8 @@ impl Core {
|
|||
interval: AggregateFrequency,
|
||||
) -> PathBuf {
|
||||
let (ext, domain) = match domain {
|
||||
ReportType::Dmarc(domain) => ("t", domain),
|
||||
ReportType::Tls(domain) => ("d", domain),
|
||||
ReportType::Dmarc(domain) => ("d", domain),
|
||||
ReportType::Tls(domain) => ("t", domain),
|
||||
};
|
||||
|
||||
// Build base path
|
||||
|
@ -153,7 +162,7 @@ impl ReportCore {
|
|||
Ok(Some(file)) => {
|
||||
let file = file.path();
|
||||
if file.is_dir() {
|
||||
match tokio::fs::read_dir(path).await {
|
||||
match tokio::fs::read_dir(&file).await {
|
||||
Ok(mut dir) => {
|
||||
let file_ = file;
|
||||
loop {
|
||||
|
@ -399,24 +408,26 @@ pub async fn json_write(path: &PathBuf, entry: &impl Serialize) -> usize {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn json_append(path: &PathBuf, entry: &impl Serialize) -> usize {
|
||||
pub async fn json_append(path: &PathBuf, entry: &impl Serialize, bytes_left: usize) -> usize {
|
||||
let mut bytes = Vec::with_capacity(128);
|
||||
bytes.push(b',');
|
||||
if serde_json::to_writer(&mut bytes, entry).is_ok() {
|
||||
let err = match OpenOptions::new().append(true).open(&path).await {
|
||||
Ok(mut file) => match file.write_all(&bytes).await {
|
||||
Ok(_) => return bytes.len() + 1,
|
||||
if bytes.len() <= bytes_left {
|
||||
let err = match OpenOptions::new().append(true).open(&path).await {
|
||||
Ok(mut file) => match file.write_all(&bytes).await {
|
||||
Ok(_) => return bytes.len(),
|
||||
Err(err) => err,
|
||||
},
|
||||
Err(err) => err,
|
||||
},
|
||||
Err(err) => err,
|
||||
};
|
||||
tracing::error!(
|
||||
context = "report",
|
||||
event = "error",
|
||||
"Failed to append report to {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
};
|
||||
tracing::error!(
|
||||
context = "report",
|
||||
event = "error",
|
||||
"Failed to append report to {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ pub struct TlsRptOptions {
|
|||
pub interval: AggregateFrequency,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct TlsFormat {
|
||||
rua: Vec<ReportUri>,
|
||||
policy: PolicyDetails,
|
||||
|
@ -131,6 +131,7 @@ impl GenerateTlsReport for Arc<Core> {
|
|||
event = "empty-report",
|
||||
"No policies found in report"
|
||||
);
|
||||
path.cleanup_blocking();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -161,6 +162,15 @@ impl GenerateTlsReport for Arc<Core> {
|
|||
.timeout(Duration::from_secs(2 * 60))
|
||||
.build()
|
||||
{
|
||||
#[cfg(test)]
|
||||
if uri == "https://127.0.0.1/tls" {
|
||||
crate::tests::reporting::tls::TLS_HTTP_REPORT
|
||||
.lock()
|
||||
.extend_from_slice(&json);
|
||||
path.cleanup_blocking();
|
||||
return;
|
||||
}
|
||||
|
||||
match client
|
||||
.post(uri)
|
||||
.header(CONTENT_TYPE, "application/tlsrpt+gzip")
|
||||
|
@ -169,6 +179,15 @@ impl GenerateTlsReport for Arc<Core> {
|
|||
{
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
tracing::info!(
|
||||
parent: &span,
|
||||
context = "http",
|
||||
event = "success",
|
||||
url = uri,
|
||||
);
|
||||
path.cleanup_blocking();
|
||||
return;
|
||||
} else {
|
||||
tracing::debug!(
|
||||
parent: &span,
|
||||
context = "http",
|
||||
|
@ -176,7 +195,6 @@ impl GenerateTlsReport for Arc<Core> {
|
|||
url = uri,
|
||||
status = %response.status()
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -271,7 +289,7 @@ impl Scheduler {
|
|||
}
|
||||
}
|
||||
Entry::Vacant(e) => {
|
||||
let created = event.interval.from_timestamp();
|
||||
let created = event.interval.to_timestamp();
|
||||
let deliver_at = created + event.interval.as_secs();
|
||||
|
||||
self.main.push(Schedule {
|
||||
|
@ -366,7 +384,7 @@ impl Scheduler {
|
|||
let bytes_written = json_write(&path.path[pos].inner, &entry).await;
|
||||
|
||||
if bytes_written > 0 {
|
||||
path.size = bytes_written;
|
||||
path.size += bytes_written;
|
||||
} else {
|
||||
// Something went wrong, remove record
|
||||
if let Entry::Occupied(mut e) = self
|
||||
|
@ -383,7 +401,8 @@ impl Scheduler {
|
|||
}
|
||||
} else if path.size < *max_size {
|
||||
// Append to existing report
|
||||
path.size += json_append(&path.path[pos].inner, &event.failure).await;
|
||||
path.size +=
|
||||
json_append(&path.path[pos].inner, &event.failure, *max_size - path.size).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,9 @@ async fn auth() {
|
|||
|
||||
let mut config = &mut core.session.config.auth;
|
||||
|
||||
config.require = r"[{if = 'remote-ip', eq = '10.0.0.1', then = true},
|
||||
{else = false}]"
|
||||
.parse_if(&ctx);
|
||||
config.lookup = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 'plain'},
|
||||
{else = false}]"
|
||||
.parse_if::<Option<String>>(&ctx)
|
||||
|
@ -77,12 +80,17 @@ async fn auth() {
|
|||
.unwrap_err();
|
||||
session.response().assert_code("421 4.3.0");
|
||||
|
||||
// Should not be able to send without authenticating
|
||||
session.state = State::default();
|
||||
session.mail_from("bill@foobar.org", "503 5.5.1").await;
|
||||
|
||||
// Successful PLAIN authentication
|
||||
session.data.auth_errors = 0;
|
||||
session.state = State::default();
|
||||
session
|
||||
.cmd("AUTH PLAIN AGpvaG4Ac2VjcmV0", "235 2.7.0")
|
||||
.await;
|
||||
session.mail_from("bill@foobar.org", "250").await;
|
||||
session.data.mail_from.take();
|
||||
|
||||
// Should not be able to authenticate twice
|
||||
session
|
||||
|
|
|
@ -99,7 +99,7 @@ async fn data() {
|
|||
.await;
|
||||
assert_eq!(
|
||||
qr.read_event().await.unwrap_message().read_message(),
|
||||
load_test_message("no_msgid")
|
||||
load_test_message("no_msgid", "messages")
|
||||
);
|
||||
|
||||
// Maximum one message per session is allowed for 10.0.0.1
|
||||
|
|
|
@ -17,7 +17,7 @@ use crate::{
|
|||
|
||||
const SIGNATURES: &str = "
|
||||
[signature.rsa]
|
||||
public-key = '''
|
||||
private-key = '''
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXwIBAAKBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYtIxN2SnFC
|
||||
jxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v/RtdC2UzJ1lWT947qR+Rcac2gb
|
||||
|
|
|
@ -30,6 +30,7 @@ pub mod inbound;
|
|||
pub mod outbound;
|
||||
pub mod queue;
|
||||
pub mod remote;
|
||||
pub mod reporting;
|
||||
pub mod session;
|
||||
|
||||
pub trait ParseTestConfig {
|
||||
|
@ -173,6 +174,7 @@ impl SessionConfig {
|
|||
script: IfBlock::new(None),
|
||||
lookup: IfBlock::new(None),
|
||||
mechanisms: IfBlock::new(AUTH_PLAIN | AUTH_LOGIN),
|
||||
require: IfBlock::new(false),
|
||||
errors_max: IfBlock::new(10),
|
||||
errors_wait: IfBlock::new(Duration::from_secs(1)),
|
||||
},
|
||||
|
@ -353,7 +355,6 @@ impl AggregateReport {
|
|||
Self {
|
||||
name: IfBlock::default(),
|
||||
address: IfBlock::default(),
|
||||
subject: IfBlock::default(),
|
||||
org_name: IfBlock::default(),
|
||||
contact_info: IfBlock::default(),
|
||||
send: IfBlock::default(),
|
||||
|
|
|
@ -12,7 +12,7 @@ use tokio_rustls::TlsAcceptor;
|
|||
use crate::{
|
||||
config::{Config, ConfigContext},
|
||||
core::throttle::{ConcurrencyLimiter, InFlight},
|
||||
remote::lookup::{Item, LookupChannel, LookupResult},
|
||||
remote::lookup::{Item, LookupResult},
|
||||
};
|
||||
|
||||
use super::dummy_tls_acceptor;
|
||||
|
@ -54,9 +54,7 @@ async fn remote_imap() {
|
|||
let mut ctx = ConfigContext::default();
|
||||
let config = Config::parse(REMOTE).unwrap();
|
||||
config.parse_remote_hosts(&mut ctx).unwrap();
|
||||
let lookup = LookupChannel {
|
||||
tx: ctx.hosts.remove("imap").unwrap().spawn(&config),
|
||||
};
|
||||
let lookup = ctx.hosts.remove("imap").unwrap().spawn(&config);
|
||||
|
||||
// Basic lookup
|
||||
let tests = vec![
|
||||
|
|
|
@ -12,7 +12,7 @@ use tokio_rustls::TlsAcceptor;
|
|||
use crate::{
|
||||
config::{Config, ConfigContext},
|
||||
core::throttle::{ConcurrencyLimiter, InFlight},
|
||||
remote::lookup::{Item, LookupChannel, LookupResult},
|
||||
remote::lookup::{Item, LookupResult},
|
||||
};
|
||||
|
||||
use super::dummy_tls_acceptor;
|
||||
|
@ -46,9 +46,7 @@ async fn remote_smtp() {
|
|||
let mut ctx = ConfigContext::default();
|
||||
let config = Config::parse(REMOTE).unwrap();
|
||||
config.parse_remote_hosts(&mut ctx).unwrap();
|
||||
let lookup = LookupChannel {
|
||||
tx: ctx.hosts.remove("lmtp").unwrap().spawn(&config),
|
||||
};
|
||||
let lookup = ctx.hosts.remove("lmtp").unwrap().spawn(&config);
|
||||
|
||||
// Basic lookup
|
||||
let tests = vec![
|
||||
|
|
74
src/tests/reporting/analyze.rs
Normal file
74
src/tests/reporting/analyze.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use std::{fs, sync::Arc, time::Duration};
|
||||
|
||||
use crate::{
|
||||
config::{AddressMatch, IfBlock},
|
||||
core::{Core, Session},
|
||||
tests::make_temp_dir,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_analyze() {
|
||||
let mut core = Core::test();
|
||||
|
||||
// Create temp dir for queue
|
||||
let mut qr = core.init_test_queue("smtp_analyze_report_test");
|
||||
let report_dir = make_temp_dir("smtp_report_incoming", true);
|
||||
|
||||
let mut config = &mut core.session.config.rcpt;
|
||||
config.relay = IfBlock::new(true);
|
||||
let mut config = &mut core.session.config.data;
|
||||
config.max_messages = IfBlock::new(1024);
|
||||
let mut config = &mut core.report.config.analysis;
|
||||
config.addresses = vec![
|
||||
AddressMatch::StartsWith("reports@".to_string()),
|
||||
AddressMatch::EndsWith("@dmarc.foobar.org".to_string()),
|
||||
AddressMatch::Equals("feedback@foobar.org".to_string()),
|
||||
];
|
||||
config.forward = false;
|
||||
config.store = report_dir.temp_dir.clone().into();
|
||||
|
||||
// Create test message
|
||||
let core = Arc::new(core);
|
||||
let mut session = Session::test(core.clone());
|
||||
session.data.remote_ip = "10.0.0.1".parse().unwrap();
|
||||
session.eval_session_params().await;
|
||||
session.ehlo("mx.test.org").await;
|
||||
|
||||
let addresses = [
|
||||
"reports@foobar.org",
|
||||
"rep@dmarc.foobar.org",
|
||||
"feedback@foobar.org",
|
||||
];
|
||||
let mut ac = 0;
|
||||
let mut total_reports_received = 0;
|
||||
for (test, num_tests) in [("arf", 5), ("dmarc", 5), ("tls", 2)] {
|
||||
for num_test in 1..=num_tests {
|
||||
total_reports_received += 1;
|
||||
session
|
||||
.send_message(
|
||||
"john@test.org",
|
||||
&[addresses[ac % addresses.len()]],
|
||||
&format!("report:{}{}", test, num_test),
|
||||
"250",
|
||||
)
|
||||
.await;
|
||||
qr.assert_empty_queue();
|
||||
ac += 1;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
let mut total_reports = 0;
|
||||
for entry in fs::read_dir(&report_dir.temp_dir).unwrap() {
|
||||
let path = entry.unwrap().path();
|
||||
assert_ne!(fs::metadata(&path).unwrap().len(), 0);
|
||||
total_reports += 1;
|
||||
}
|
||||
assert_eq!(total_reports, total_reports_received);
|
||||
|
||||
// Test delivery to non-report addresses
|
||||
session
|
||||
.send_message("john@test.org", &["bill@foobar.org"], "test:no_dkim", "250")
|
||||
.await;
|
||||
qr.read_event().await.unwrap_message();
|
||||
}
|
160
src/tests/reporting/dmarc.rs
Normal file
160
src/tests/reporting/dmarc.rs
Normal file
|
@ -0,0 +1,160 @@
|
|||
use std::{
|
||||
net::IpAddr,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use mail_auth::{
|
||||
common::parse::TxtRecordParser,
|
||||
dmarc::Dmarc,
|
||||
report::{ActionDisposition, Disposition, DmarcResult, Record, Report},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{AggregateFrequency, ConfigContext, IfBlock},
|
||||
core::Core,
|
||||
reporting::{
|
||||
dmarc::GenerateDmarcReport,
|
||||
scheduler::{ReportType, Scheduler},
|
||||
DmarcEvent,
|
||||
},
|
||||
tests::{make_temp_dir, session::VerifyResponse, ParseTestConfig},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_dmarc() {
|
||||
tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::DEBUG)
|
||||
.finish(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Create scheduler
|
||||
let mut core = Core::test();
|
||||
let ctx = ConfigContext::default().parse_signatures();
|
||||
let temp_dir = make_temp_dir("smtp_report_dmarc_test", true);
|
||||
let config = &mut core.report.config;
|
||||
config.path = IfBlock::new(temp_dir.temp_dir.clone());
|
||||
config.hash = IfBlock::new(16);
|
||||
config.dmarc_aggregate.sign = "['rsa']"
|
||||
.parse_if::<Vec<String>>(&ctx)
|
||||
.map_if_block(&ctx.signers, "", "")
|
||||
.unwrap();
|
||||
config.dmarc_aggregate.max_size = IfBlock::new(4096);
|
||||
config.submitter = IfBlock::new("mx.example.org".to_string());
|
||||
config.dmarc_aggregate.address = IfBlock::new("reports@example.org".to_string());
|
||||
config.dmarc_aggregate.org_name = IfBlock::new("Foobar, Inc.".to_string().into());
|
||||
config.dmarc_aggregate.contact_info =
|
||||
IfBlock::new("https://foobar.org/contact".to_string().into());
|
||||
let mut scheduler = Scheduler::default();
|
||||
|
||||
// Authorize external report for foobar.org
|
||||
core.resolvers.dns.txt_add(
|
||||
"foobar.org._report._dmarc.foobar.net",
|
||||
Dmarc::parse(b"v=DMARC1;").unwrap(),
|
||||
Instant::now() + Duration::from_secs(10),
|
||||
);
|
||||
|
||||
// Create temp dir for queue
|
||||
let mut qr = core.init_test_queue("smtp_report_dmarc_test");
|
||||
let core = Arc::new(core);
|
||||
|
||||
// Schedule two events with a same policy and another one with a different policy
|
||||
let dmarc_record = Arc::new(
|
||||
Dmarc::parse(
|
||||
b"v=DMARC1; p=quarantine; rua=mailto:reports@foobar.net,mailto:reports@example.net",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(dmarc_record.rua().len(), 2);
|
||||
for _ in 0..2 {
|
||||
scheduler
|
||||
.schedule_dmarc(
|
||||
Box::new(DmarcEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
report_record: Record::new()
|
||||
.with_source_ip("192.168.1.2".parse().unwrap())
|
||||
.with_action_disposition(ActionDisposition::Pass)
|
||||
.with_dmarc_dkim_result(DmarcResult::Pass)
|
||||
.with_dmarc_spf_result(DmarcResult::Fail)
|
||||
.with_envelope_from("hello@example.org")
|
||||
.with_envelope_to("other@example.org")
|
||||
.with_header_from("bye@example.org"),
|
||||
dmarc_record: dmarc_record.clone(),
|
||||
interval: AggregateFrequency::Weekly,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
scheduler
|
||||
.schedule_dmarc(
|
||||
Box::new(DmarcEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
report_record: Record::new()
|
||||
.with_source_ip("a:b:c::e:f".parse().unwrap())
|
||||
.with_action_disposition(ActionDisposition::Reject)
|
||||
.with_dmarc_dkim_result(DmarcResult::Fail)
|
||||
.with_dmarc_spf_result(DmarcResult::Pass),
|
||||
dmarc_record: dmarc_record.clone(),
|
||||
interval: AggregateFrequency::Weekly,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(scheduler.reports.len(), 1);
|
||||
let report_path;
|
||||
match scheduler.reports.into_iter().next().unwrap() {
|
||||
(ReportType::Dmarc(domain), ReportType::Dmarc(path)) => {
|
||||
report_path = path.path.clone();
|
||||
core.generate_dmarc_report(domain, path);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Expect report
|
||||
let message = qr.read_event().await.unwrap_message();
|
||||
qr.assert_empty_queue();
|
||||
assert_eq!(message.recipients.len(), 1);
|
||||
assert_eq!(
|
||||
message.recipients.last().unwrap().address,
|
||||
"reports@foobar.net"
|
||||
);
|
||||
assert_eq!(message.return_path, "reports@example.org");
|
||||
message
|
||||
.read_lines()
|
||||
.assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=example.com;")
|
||||
.assert_contains("To: <reports@foobar.net>")
|
||||
.assert_contains("Report Domain: foobar.org")
|
||||
.assert_contains("Submitter: mx.example.org");
|
||||
|
||||
// Verify generated report
|
||||
let report = Report::parse_rfc5322(message.read_message().as_bytes()).unwrap();
|
||||
assert_eq!(report.domain(), "foobar.org");
|
||||
assert_eq!(report.email(), "reports@example.org");
|
||||
assert_eq!(report.org_name(), "Foobar, Inc.");
|
||||
assert_eq!(
|
||||
report.extra_contact_info().unwrap(),
|
||||
"https://foobar.org/contact"
|
||||
);
|
||||
assert_eq!(report.p(), Disposition::Quarantine);
|
||||
assert_eq!(report.records().len(), 2);
|
||||
for record in report.records() {
|
||||
let source_ip = record.source_ip().unwrap();
|
||||
if source_ip == "192.168.1.2".parse::<IpAddr>().unwrap() {
|
||||
assert_eq!(record.count(), 2);
|
||||
assert_eq!(record.action_disposition(), ActionDisposition::Pass);
|
||||
assert_eq!(record.envelope_from(), "hello@example.org");
|
||||
assert_eq!(record.header_from(), "bye@example.org");
|
||||
assert_eq!(record.envelope_to().unwrap(), "other@example.org");
|
||||
} else if source_ip == "a:b:c::e:f".parse::<IpAddr>().unwrap() {
|
||||
assert_eq!(record.count(), 1);
|
||||
assert_eq!(record.action_disposition(), ActionDisposition::Reject);
|
||||
} else {
|
||||
panic!("unexpected ip {}", source_ip);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(!report_path.exists());
|
||||
}
|
4
src/tests/reporting/mod.rs
Normal file
4
src/tests/reporting/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod analyze;
|
||||
pub mod dmarc;
|
||||
pub mod scheduler;
|
||||
pub mod tls;
|
251
src/tests/reporting/scheduler.rs
Normal file
251
src/tests/reporting/scheduler.rs
Normal file
|
@ -0,0 +1,251 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use mail_auth::{
|
||||
common::parse::TxtRecordParser,
|
||||
dmarc::{Dmarc, URI},
|
||||
mta_sts::TlsRpt,
|
||||
report::{ActionDisposition, Alignment, Disposition, DmarcResult, PolicyPublished, Record},
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
config::{AggregateFrequency, IfBlock},
|
||||
core::Core,
|
||||
reporting::{
|
||||
dmarc::DmarcFormat,
|
||||
scheduler::{ReportType, Scheduler},
|
||||
DmarcEvent, PolicyType, TlsEvent,
|
||||
},
|
||||
tests::make_temp_dir,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_scheduler() {
|
||||
tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::DEBUG)
|
||||
.finish(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Create scheduler
|
||||
let mut core = Core::test();
|
||||
let temp_dir = make_temp_dir("smtp_report_scheduler_test", true);
|
||||
let config = &mut core.report.config;
|
||||
config.path = IfBlock::new(temp_dir.temp_dir.clone());
|
||||
config.hash = IfBlock::new(16);
|
||||
config.dmarc_aggregate.max_size = IfBlock::new(500);
|
||||
config.tls.max_size = IfBlock::new(550);
|
||||
let mut scheduler = Scheduler::default();
|
||||
|
||||
// Schedule two events with a same policy and another one with a different policy
|
||||
let dmarc_record =
|
||||
Arc::new(Dmarc::parse(b"v=DMARC1; p=quarantine; rua=mailto:dmarc@foobar.org").unwrap());
|
||||
scheduler
|
||||
.schedule_dmarc(
|
||||
Box::new(DmarcEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
report_record: Record::new()
|
||||
.with_source_ip("192.168.1.2".parse().unwrap())
|
||||
.with_action_disposition(ActionDisposition::Pass)
|
||||
.with_dmarc_dkim_result(DmarcResult::Pass)
|
||||
.with_dmarc_spf_result(DmarcResult::Fail)
|
||||
.with_envelope_from("hello@example.org")
|
||||
.with_envelope_to("other@example.org")
|
||||
.with_header_from("bye@example.org"),
|
||||
dmarc_record: dmarc_record.clone(),
|
||||
interval: AggregateFrequency::Weekly,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
|
||||
// No records should be added once the 550 bytes max size is reached
|
||||
for _ in 0..10 {
|
||||
scheduler
|
||||
.schedule_dmarc(
|
||||
Box::new(DmarcEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
report_record: Record::new()
|
||||
.with_source_ip("192.168.1.2".parse().unwrap())
|
||||
.with_action_disposition(ActionDisposition::Pass)
|
||||
.with_dmarc_dkim_result(DmarcResult::Pass)
|
||||
.with_dmarc_spf_result(DmarcResult::Fail)
|
||||
.with_envelope_from("hello@example.org")
|
||||
.with_envelope_to("other@example.org")
|
||||
.with_header_from("bye@example.org"),
|
||||
dmarc_record: dmarc_record.clone(),
|
||||
interval: AggregateFrequency::Weekly,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let dmarc_record =
|
||||
Arc::new(Dmarc::parse(b"v=DMARC1; p=reject; rua=mailto:dmarc@foobar.org").unwrap());
|
||||
scheduler
|
||||
.schedule_dmarc(
|
||||
Box::new(DmarcEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
report_record: Record::new()
|
||||
.with_source_ip("a:b:c::e:f".parse().unwrap())
|
||||
.with_action_disposition(ActionDisposition::Reject)
|
||||
.with_dmarc_dkim_result(DmarcResult::Fail)
|
||||
.with_dmarc_spf_result(DmarcResult::Pass),
|
||||
dmarc_record: dmarc_record.clone(),
|
||||
interval: AggregateFrequency::Weekly,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Schedule TLS event
|
||||
let tls_record = Arc::new(TlsRpt::parse(b"v=TLSRPTv1;rua=mailto:reports@foobar.org").unwrap());
|
||||
scheduler
|
||||
.schedule_tls(
|
||||
Box::new(TlsEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
policy: PolicyType::Tlsa(None),
|
||||
failure: None,
|
||||
tls_record: tls_record.clone(),
|
||||
interval: AggregateFrequency::Daily,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
scheduler
|
||||
.schedule_tls(
|
||||
Box::new(TlsEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
policy: PolicyType::Tlsa(None),
|
||||
failure: None,
|
||||
tls_record: tls_record.clone(),
|
||||
interval: AggregateFrequency::Daily,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
scheduler
|
||||
.schedule_tls(
|
||||
Box::new(TlsEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
policy: PolicyType::Sts(None),
|
||||
failure: None,
|
||||
tls_record: tls_record.clone(),
|
||||
interval: AggregateFrequency::Daily,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
scheduler
|
||||
.schedule_tls(
|
||||
Box::new(TlsEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
policy: PolicyType::None,
|
||||
failure: None,
|
||||
tls_record: tls_record.clone(),
|
||||
interval: AggregateFrequency::Daily,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Verify sizes and counts
|
||||
let mut total_tls = 0;
|
||||
let mut total_tls_policies = 0;
|
||||
let mut total_dmarc_policies = 0;
|
||||
for report in scheduler.reports.values() {
|
||||
match report {
|
||||
ReportType::Dmarc(r) => {
|
||||
assert!(r.size <= 550, "{}", r.size);
|
||||
assert_eq!(fs::metadata(&r.path).await.unwrap().len() as usize, r.size);
|
||||
assert_eq!(r.deliver_at, AggregateFrequency::Weekly);
|
||||
total_dmarc_policies += 1;
|
||||
}
|
||||
ReportType::Tls(r) => {
|
||||
total_tls += 1;
|
||||
total_tls_policies += r.path.len();
|
||||
assert!(r.size <= 550);
|
||||
assert_eq!(r.deliver_at, AggregateFrequency::Daily);
|
||||
let mut sizes = 0;
|
||||
for p in &r.path {
|
||||
sizes += fs::metadata(&p.inner).await.unwrap().len() as usize;
|
||||
}
|
||||
assert_eq!(r.size, sizes);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(total_tls, 1);
|
||||
assert_eq!(total_tls_policies, 3);
|
||||
assert_eq!(total_dmarc_policies, 2);
|
||||
|
||||
// Verify deserialized report queue
|
||||
let mut scheduler_deser = core.report.read_reports().await;
|
||||
for (key, value) in scheduler.reports {
|
||||
let a = Some(value);
|
||||
let b = scheduler_deser.reports.remove(&key);
|
||||
match (&a, &b) {
|
||||
(Some(ReportType::Tls(a)), Some(ReportType::Tls(b))) => {
|
||||
assert_eq!(a.created, b.created);
|
||||
assert_eq!(a.size, b.size);
|
||||
assert_eq!(a.deliver_at, b.deliver_at);
|
||||
assert_eq!(a.path.len(), b.path.len());
|
||||
for p in &a.path {
|
||||
assert!(b.path.contains(&p));
|
||||
}
|
||||
for p in &b.path {
|
||||
assert!(a.path.contains(&p));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(a, b, "failed for {:?}", key);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(scheduler.main.len(), scheduler_deser.main.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_strip_json() {
|
||||
let mut d = DmarcFormat {
|
||||
rua: vec![
|
||||
URI {
|
||||
uri: "hello".to_string(),
|
||||
max_size: 0,
|
||||
},
|
||||
URI {
|
||||
uri: "world".to_string(),
|
||||
max_size: 0,
|
||||
},
|
||||
],
|
||||
policy: PolicyPublished {
|
||||
domain: "example.org".to_string(),
|
||||
version_published: None,
|
||||
adkim: Alignment::Relaxed,
|
||||
aspf: Alignment::Strict,
|
||||
p: Disposition::Quarantine,
|
||||
sp: Disposition::Reject,
|
||||
testing: false,
|
||||
fo: None,
|
||||
},
|
||||
records: vec![Record::default()
|
||||
.with_count(1)
|
||||
.with_envelope_from("domain.net")
|
||||
.with_envelope_to("other.org")],
|
||||
};
|
||||
let mut s = serde_json::to_string(&d).unwrap();
|
||||
s.truncate(s.len() - 2);
|
||||
|
||||
let r = Record::default()
|
||||
.with_count(2)
|
||||
.with_envelope_from("otherdomain.net")
|
||||
.with_envelope_to("otherother.org");
|
||||
let rs = serde_json::to_string(&r).unwrap();
|
||||
|
||||
d.records.push(r);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<DmarcFormat>(&format!("{},{}]}}", s, rs)).unwrap(),
|
||||
d
|
||||
);
|
||||
}
|
237
src/tests/reporting/tls.rs
Normal file
237
src/tests/reporting/tls.rs
Normal file
|
@ -0,0 +1,237 @@
|
|||
use std::{io::Read, sync::Arc, time::Duration};
|
||||
|
||||
use mail_auth::{
|
||||
common::parse::TxtRecordParser,
|
||||
flate2::read::GzDecoder,
|
||||
mta_sts::TlsRpt,
|
||||
report::tlsrpt::{FailureDetails, PolicyType, ResultType, TlsReport},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{
|
||||
config::{AggregateFrequency, ConfigContext, IfBlock},
|
||||
core::Core,
|
||||
reporting::{
|
||||
scheduler::{ReportType, Scheduler},
|
||||
tls::GenerateTlsReport,
|
||||
TlsEvent,
|
||||
},
|
||||
tests::{make_temp_dir, session::VerifyResponse, ParseTestConfig},
|
||||
};
|
||||
|
||||
pub static TLS_HTTP_REPORT: Mutex<Vec<u8>> = Mutex::new(Vec::new());
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_tls() {
|
||||
tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::DEBUG)
|
||||
.finish(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Create scheduler
|
||||
let mut core = Core::test();
|
||||
let ctx = ConfigContext::default().parse_signatures();
|
||||
let temp_dir = make_temp_dir("smtp_report_tls_test", true);
|
||||
let config = &mut core.report.config;
|
||||
config.path = IfBlock::new(temp_dir.temp_dir.clone());
|
||||
config.hash = IfBlock::new(16);
|
||||
config.tls.sign = "['rsa']"
|
||||
.parse_if::<Vec<String>>(&ctx)
|
||||
.map_if_block(&ctx.signers, "", "")
|
||||
.unwrap();
|
||||
config.tls.max_size = IfBlock::new(4096);
|
||||
config.submitter = IfBlock::new("mx.example.org".to_string());
|
||||
config.tls.address = IfBlock::new("reports@example.org".to_string());
|
||||
config.tls.org_name = IfBlock::new("Foobar, Inc.".to_string().into());
|
||||
config.tls.contact_info = IfBlock::new("https://foobar.org/contact".to_string().into());
|
||||
let mut scheduler = Scheduler::default();
|
||||
|
||||
// Create temp dir for queue
|
||||
let mut qr = core.init_test_queue("smtp_report_tls_test");
|
||||
let core = Arc::new(core);
|
||||
|
||||
// Schedule TLS reports to be delivered via email
|
||||
let tls_record = Arc::new(TlsRpt::parse(b"v=TLSRPTv1;rua=mailto:reports@foobar.org").unwrap());
|
||||
|
||||
for _ in 0..2 {
|
||||
// Add two successful records
|
||||
scheduler
|
||||
.schedule_tls(
|
||||
Box::new(TlsEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
policy: crate::reporting::PolicyType::None,
|
||||
failure: None,
|
||||
tls_record: tls_record.clone(),
|
||||
interval: AggregateFrequency::Daily,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
for (policy, rt) in [
|
||||
(
|
||||
crate::reporting::PolicyType::None,
|
||||
ResultType::CertificateExpired,
|
||||
),
|
||||
(
|
||||
crate::reporting::PolicyType::Tlsa(None),
|
||||
ResultType::TlsaInvalid,
|
||||
),
|
||||
(
|
||||
crate::reporting::PolicyType::Sts(None),
|
||||
ResultType::StsPolicyFetchError,
|
||||
),
|
||||
(
|
||||
crate::reporting::PolicyType::Sts(None),
|
||||
ResultType::StsPolicyInvalid,
|
||||
),
|
||||
] {
|
||||
scheduler
|
||||
.schedule_tls(
|
||||
Box::new(TlsEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
policy,
|
||||
failure: FailureDetails::new(rt).into(),
|
||||
tls_record: tls_record.clone(),
|
||||
interval: AggregateFrequency::Daily,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Wait for flush
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
assert_eq!(scheduler.reports.len(), 1);
|
||||
let mut report_path = Vec::new();
|
||||
match scheduler.reports.into_iter().next().unwrap() {
|
||||
(ReportType::Tls(domain), ReportType::Tls(path)) => {
|
||||
for p in &path.path {
|
||||
report_path.push(p.inner.clone());
|
||||
}
|
||||
core.generate_tls_report(domain, path);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Expect report
|
||||
let message = qr.read_event().await.unwrap_message();
|
||||
assert_eq!(
|
||||
message.recipients.last().unwrap().address,
|
||||
"reports@foobar.org"
|
||||
);
|
||||
assert_eq!(message.return_path, "reports@example.org");
|
||||
message
|
||||
.read_lines()
|
||||
.assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=example.com;")
|
||||
.assert_contains("To: <reports@foobar.org>")
|
||||
.assert_contains("Report Domain: foobar.org")
|
||||
.assert_contains("Submitter: mx.example.org");
|
||||
|
||||
// Verify generated report
|
||||
let report = TlsReport::parse_rfc5322(message.read_message().as_bytes()).unwrap();
|
||||
assert_eq!(report.organization_name.unwrap(), "Foobar, Inc.");
|
||||
assert_eq!(report.contact_info.unwrap(), "https://foobar.org/contact");
|
||||
assert_eq!(report.policies.len(), 3);
|
||||
let mut seen = [false; 3];
|
||||
for policy in report.policies {
|
||||
match policy.policy.policy_type {
|
||||
PolicyType::Tlsa => {
|
||||
seen[0] = true;
|
||||
assert_eq!(policy.summary.total_failure, 1);
|
||||
assert_eq!(policy.summary.total_success, 0);
|
||||
assert_eq!(policy.policy.policy_domain, "foobar.org");
|
||||
assert_eq!(policy.failure_details.len(), 1);
|
||||
assert_eq!(
|
||||
policy.failure_details.first().unwrap().result_type,
|
||||
ResultType::TlsaInvalid
|
||||
);
|
||||
}
|
||||
PolicyType::Sts => {
|
||||
seen[1] = true;
|
||||
assert_eq!(policy.summary.total_failure, 2);
|
||||
assert_eq!(policy.summary.total_success, 0);
|
||||
assert_eq!(policy.policy.policy_domain, "foobar.org");
|
||||
assert_eq!(policy.failure_details.len(), 2);
|
||||
assert!(policy
|
||||
.failure_details
|
||||
.iter()
|
||||
.any(|d| d.result_type == ResultType::StsPolicyFetchError));
|
||||
assert!(policy
|
||||
.failure_details
|
||||
.iter()
|
||||
.any(|d| d.result_type == ResultType::StsPolicyInvalid));
|
||||
}
|
||||
PolicyType::NoPolicyFound => {
|
||||
seen[2] = true;
|
||||
assert_eq!(policy.summary.total_failure, 1);
|
||||
assert_eq!(policy.summary.total_success, 2);
|
||||
assert_eq!(policy.policy.policy_domain, "foobar.org");
|
||||
assert_eq!(policy.failure_details.len(), 1);
|
||||
assert_eq!(
|
||||
policy.failure_details.first().unwrap().result_type,
|
||||
ResultType::CertificateExpired
|
||||
);
|
||||
}
|
||||
PolicyType::Other => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
assert!(seen[0]);
|
||||
assert!(seen[1]);
|
||||
assert!(seen[2]);
|
||||
|
||||
for path in report_path {
|
||||
assert!(!path.exists());
|
||||
}
|
||||
|
||||
// Schedule TLS reports to be delivered via https
|
||||
let mut scheduler = Scheduler::default();
|
||||
let tls_record = Arc::new(TlsRpt::parse(b"v=TLSRPTv1;rua=https://127.0.0.1/tls").unwrap());
|
||||
|
||||
for _ in 0..2 {
|
||||
// Add two successful records
|
||||
scheduler
|
||||
.schedule_tls(
|
||||
Box::new(TlsEvent {
|
||||
domain: "foobar.org".to_string(),
|
||||
policy: crate::reporting::PolicyType::None,
|
||||
failure: None,
|
||||
tls_record: tls_record.clone(),
|
||||
interval: AggregateFrequency::Daily,
|
||||
}),
|
||||
&core,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let mut report_path = Vec::new();
|
||||
match scheduler.reports.into_iter().next().unwrap() {
|
||||
(ReportType::Tls(domain), ReportType::Tls(path)) => {
|
||||
for p in &path.path {
|
||||
report_path.push(p.inner.clone());
|
||||
}
|
||||
core.generate_tls_report(domain, path);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
// Uncompress report
|
||||
let gz_report = TLS_HTTP_REPORT.lock();
|
||||
let mut file = GzDecoder::new(&gz_report[..]);
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
let report = TlsReport::parse_json(&buf).unwrap();
|
||||
assert_eq!(report.organization_name.unwrap(), "Foobar, Inc.");
|
||||
assert_eq!(report.contact_info.unwrap(), "https://foobar.org/contact");
|
||||
assert_eq!(report.policies.len(), 1);
|
||||
|
||||
for path in report_path {
|
||||
assert!(!path.exists());
|
||||
}
|
||||
}
|
|
@ -157,7 +157,11 @@ impl Session<DummyIo> {
|
|||
self.ingest(b"DATA\r\n").await.unwrap();
|
||||
self.response().assert_code("354");
|
||||
if let Some(file) = data.strip_prefix("test:") {
|
||||
self.ingest(load_test_message(file).as_bytes())
|
||||
self.ingest(load_test_message(file, "messages").as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
} else if let Some(file) = data.strip_prefix("report:") {
|
||||
self.ingest(load_test_message(file, "reports").as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
|
@ -176,11 +180,11 @@ impl Session<DummyIo> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn load_test_message(file: &str) -> String {
|
||||
pub fn load_test_message(file: &str, test: &str) -> String {
|
||||
let mut test_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
test_file.push("resources");
|
||||
test_file.push("tests");
|
||||
test_file.push("messages");
|
||||
test_file.push(test);
|
||||
test_file.push(format!("{}.eml", file));
|
||||
std::fs::read_to_string(test_file).unwrap()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue