From cbae2e22d6107eac9c16e45de7f5ceec3239c281 Mon Sep 17 00:00:00 2001 From: Mauro D Date: Tue, 24 Jan 2023 18:19:51 +0000 Subject: [PATCH] Unit tests part 7 --- .gitignore | 6 +- Cargo.lock | 31 +++ Cargo.toml | 1 + README.md | 1 - resources/config/config.toml | 403 +++++++++++++++-------------- resources/tests/reports/arf1.eml | 44 ++++ resources/tests/reports/arf2.eml | 55 ++++ resources/tests/reports/arf3.eml | 58 +++++ resources/tests/reports/arf4.eml | 73 ++++++ resources/tests/reports/arf5.eml | 87 +++++++ resources/tests/reports/dmarc1.eml | 66 +++++ resources/tests/reports/dmarc2.eml | 68 +++++ resources/tests/reports/dmarc3.eml | 52 ++++ resources/tests/reports/dmarc4.eml | 126 +++++++++ resources/tests/reports/dmarc5.eml | 54 ++++ resources/tests/reports/tls1.eml | 41 +++ resources/tests/reports/tls2.eml | 64 +++++ src/config/auth.rs | 8 +- src/config/mod.rs | 2 +- src/config/queue.rs | 15 +- src/config/remote.rs | 2 +- src/config/report.rs | 9 - src/config/resolver.rs | 4 +- src/config/session.rs | 61 +++-- src/core/mod.rs | 1 + src/core/params.rs | 1 + src/core/throttle.rs | 4 + src/core/worker.rs | 31 +++ src/inbound/mail.rs | 4 + src/inbound/spawn.rs | 4 - src/main.rs | 59 +++-- src/remote/lookup.rs | 15 +- src/reporting/analysis.rs | 5 +- src/reporting/dmarc.rs | 67 +---- src/reporting/mod.rs | 61 ++++- src/reporting/scheduler.rs | 51 ++-- src/reporting/tls.rs | 29 ++- src/tests/inbound/auth.rs | 10 +- src/tests/inbound/data.rs | 2 +- src/tests/inbound/sign.rs | 2 +- src/tests/mod.rs | 3 +- src/tests/remote/imap.rs | 6 +- src/tests/remote/smtp.rs | 6 +- src/tests/reporting/analyze.rs | 74 ++++++ src/tests/reporting/dmarc.rs | 160 ++++++++++++ src/tests/reporting/mod.rs | 4 + src/tests/reporting/scheduler.rs | 251 ++++++++++++++++++ src/tests/reporting/tls.rs | 237 +++++++++++++++++ src/tests/session.rs | 10 +- 49 files changed, 2050 insertions(+), 378 deletions(-) create mode 100644 resources/tests/reports/arf1.eml create mode 100644 resources/tests/reports/arf2.eml create mode 100644 resources/tests/reports/arf3.eml create mode 100644 resources/tests/reports/arf4.eml create mode 100644 resources/tests/reports/arf5.eml create mode 100644 resources/tests/reports/dmarc1.eml create mode 100644 resources/tests/reports/dmarc2.eml create mode 100644 resources/tests/reports/dmarc3.eml create mode 100644 resources/tests/reports/dmarc4.eml create mode 100644 resources/tests/reports/dmarc5.eml create mode 100644 resources/tests/reports/tls1.eml create mode 100644 resources/tests/reports/tls2.eml create mode 100644 src/tests/reporting/analyze.rs create mode 100644 src/tests/reporting/dmarc.rs create mode 100644 src/tests/reporting/mod.rs create mode 100644 src/tests/reporting/scheduler.rs create mode 100644 src/tests/reporting/tls.rs diff --git a/.gitignore b/.gitignore index f2e972d..a0ad1cd 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Cargo.lock b/Cargo.lock index b5207e7..91f6136 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 8f1c376..1d917c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/README.md b/README.md index 66240ca..923fee7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ Stalwart SMTP Server # TODO -- Dashmap cleanup - RBL - Sieve - Spam filter diff --git a/resources/config/config.toml b/resources/config/config.toml index 2575189..3e3133f 100644 --- a/resources/config/config.toml +++ b/resources/config/config.toml @@ -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----- diff --git a/resources/tests/reports/arf1.eml b/resources/tests/reports/arf1.eml new file mode 100644 index 0000000..a40fda2 --- /dev/null +++ b/resources/tests/reports/arf1.eml @@ -0,0 +1,44 @@ +From: +Date: Thu, 8 Mar 2005 17:40:36 EDT +Subject: FW: Earn money +To: +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: +To: +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-- diff --git a/resources/tests/reports/arf2.eml b/resources/tests/reports/arf2.eml new file mode 100644 index 0000000..d95f2f2 --- /dev/null +++ b/resources/tests/reports/arf2.eml @@ -0,0 +1,55 @@ +From: +Date: Thu, 8 Mar 2005 17:40:36 EDT +Subject: FW: Earn money +To: +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: +Original-Rcpt-To: +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: +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: +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-- diff --git a/resources/tests/reports/arf3.eml b/resources/tests/reports/arf3.eml new file mode 100644 index 0000000..d0f8cbf --- /dev/null +++ b/resources/tests/reports/arf3.eml @@ -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: +Original-Rcpt-To: +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-- diff --git a/resources/tests/reports/arf4.eml b/resources/tests/reports/arf4.eml new file mode 100644 index 0000000..cb1883a --- /dev/null +++ b/resources/tests/reports/arf4.eml @@ -0,0 +1,73 @@ +Return-Path: +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 +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 ; 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" +Subject: Wir kaufen dein Auto! +To: "address" +Content-Type: multipart/alternative; boundary="TD6gM3Blv=_XBZYNFT7dCsH1DHHOKUuSyA" +MIME-Version: 1.0 +Reply-To: "Rolf Bader" +Organization: AutoTEAM24 +Date: Tue, 5 Oct 2021 06:36:51 +0200 + +--box.mydomain.name:8BE2660E72-- + diff --git a/resources/tests/reports/arf5.eml b/resources/tests/reports/arf5.eml new file mode 100644 index 0000000..50dfd02 --- /dev/null +++ b/resources/tests/reports/arf5.eml @@ -0,0 +1,87 @@ +Message-ID: <433689.81121.example@mta.mail.receiver.example> +From: "SomeISP Antispam Feedback" +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-- + diff --git a/resources/tests/reports/dmarc1.eml b/resources/tests/reports/dmarc1.eml new file mode 100644 index 0000000..9a2ed1d --- /dev/null +++ b/resources/tests/reports/dmarc1.eml @@ -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 ; Mon, 28 Nov 2022 10:51:53 +0000 (UTC) +Received: by mail-qv1-xf4a.google.com with SMTP id 71-20020a0c804d000000b004b2fb260447so12985969qva.10 + for ; 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 \ No newline at end of file diff --git a/resources/tests/reports/dmarc2.eml b/resources/tests/reports/dmarc2.eml new file mode 100644 index 0000000..3a66174 --- /dev/null +++ b/resources/tests/reports/dmarc2.eml @@ -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 ; 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 ; 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" +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: +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-- diff --git a/resources/tests/reports/dmarc3.eml b/resources/tests/reports/dmarc3.eml new file mode 100644 index 0000000..7acd93e --- /dev/null +++ b/resources/tests/reports/dmarc3.eml @@ -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 ; 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 ) + 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: +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==-- \ No newline at end of file diff --git a/resources/tests/reports/dmarc4.eml b/resources/tests/reports/dmarc4.eml new file mode 100644 index 0000000..2fe512f --- /dev/null +++ b/resources/tests/reports/dmarc4.eml @@ -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 ; 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: XATTRDIRECT=Originating XATTRORGID=xorgid:96f9e21d-a1c4-44a3-99e4-37191ac61848 +MIME-Version: 1.0 +From: "DMARC Aggregate Report" +To: +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_-- diff --git a/resources/tests/reports/dmarc5.eml b/resources/tests/reports/dmarc5.eml new file mode 100644 index 0000000..3463d8b --- /dev/null +++ b/resources/tests/reports/dmarc5.eml @@ -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 ; 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-- diff --git a/resources/tests/reports/tls1.eml b/resources/tests/reports/tls1.eml new file mode 100644 index 0000000..e54c9e8 --- /dev/null +++ b/resources/tests/reports/tls1.eml @@ -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-- diff --git a/resources/tests/reports/tls2.eml b/resources/tests/reports/tls2.eml new file mode 100644 index 0000000..afb8687 --- /dev/null +++ b/resources/tests/reports/tls2.eml @@ -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-- diff --git a/src/config/auth.rs b/src/config/auth.rs index 167dec8..c976878 100644 --- a/src/config/auth.rs +++ b/src/config/auth.rs @@ -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 ) })?; diff --git a/src/config/mod.rs b/src/config/mod.rs index 578d955..9fda5e7 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -259,6 +259,7 @@ pub struct Auth { pub script: IfBlock>>, pub lookup: IfBlock>>, pub mechanisms: IfBlock, + pub require: IfBlock, pub errors_max: IfBlock, pub errors_wait: IfBlock, } @@ -397,7 +398,6 @@ pub struct Dsn { pub struct AggregateReport { pub name: IfBlock, pub address: IfBlock, - pub subject: IfBlock, pub org_name: IfBlock>, pub contact_info: IfBlock>, pub send: IfBlock, diff --git a/src/config/queue.rs b/src/config/queue.rs index a690734..8b77213 100644 --- a/src/config/queue.rs +++ b/src/config/queue.rs @@ -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 { 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) } } diff --git a/src/config/remote.rs b/src/config/remote.rs index a926639..3e19263 100644 --- a/src/config/remote.rs +++ b/src/config/remote.rs @@ -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)), diff --git a/src/config/report.rs b/src/config/report.rs index dc35040..7fede04 100644 --- a/src/config/report.rs +++ b/src/config/report.rs @@ -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"), diff --git a/src/config/resolver.rs b/src/config/resolver.rs index 3b304b1..ed15b35 100644 --- a/src/config/resolver.rs +++ b/src/config/resolver.rs @@ -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(); diff --git a/src/config/session.rs b/src/config/session.rs index bad901e..082e501 100644 --- a/src/config/session.rs +++ b/src/config/session.rs @@ -9,6 +9,36 @@ use super::{ impl Config { pub fn parse_session_config(&self, ctx: &ConfigContext) -> super::Result { + 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::>("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 { // 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::>("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 { @@ -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)), diff --git a/src/core/mod.rs b/src/core/mod.rs index 8ae7018..40ed3cd 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -161,6 +161,7 @@ pub struct SessionParameters { // Auth parameters pub auth_script: Option>, pub auth_lookup: Option>, + pub auth_require: bool, pub auth_errors_max: usize, pub auth_errors_wait: Duration, diff --git a/src/core/params.rs b/src/core/params.rs index 1893923..d811430 100644 --- a/src/core/params.rs +++ b/src/core/params.rs @@ -22,6 +22,7 @@ impl Session { 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; diff --git a/src/core/throttle.rs b/src/core/throttle.rs index 1b84332..af7f5ed 100644 --- a/src/core/throttle.rs +++ b/src/core/throttle.rs @@ -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); } diff --git a/src/core/worker.rs b/src/core/worker.rs index cd7814a..3fe70df 100644 --- a/src/core/worker.rs +++ b/src/core/worker.rs @@ -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 { + fn spawn_cleanup(&self) { + let core = self.clone(); + self.worker_pool.spawn(move || { + core.cleanup(); + }); + } } diff --git a/src/inbound/mail.rs b/src/inbound/mail.rs index 2176ca9..3890a56 100644 --- a/src/inbound/mail.rs +++ b/src/inbound/mail.rs @@ -20,6 +20,10 @@ impl Session { 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 diff --git a/src/inbound/spawn.rs b/src/inbound/spawn.rs index f99b3d9..da0fbe3 100644 --- a/src/inbound/spawn.rs +++ b/src/inbound/spawn.rs @@ -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)) diff --git a/src/main.rs b/src/main.rs index 3bf06fc..4140576 100644 --- a/src/main.rs +++ b/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::("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::("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::("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 { diff --git a/src/remote/lookup.rs b/src/remote/lookup.rs index c98d46f..58bfc28 100644 --- a/src/remote/lookup.rs +++ b/src/remote/lookup.rs @@ -56,21 +56,20 @@ pub trait RemoteLookup: Clone { } impl Host { - pub fn spawn(self, config: &Config) -> mpsc::Sender { + 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_ } } } diff --git a/src/reporting/analysis.rs b/src/reporting/analysis.rs index 8c2502c..1f51afc 100644 --- a/src/reporting/analysis.rs +++ b/src/reporting/analysis.rs @@ -45,6 +45,7 @@ impl AnalyzeReport for Arc { 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 { ("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; diff --git a/src/reporting/dmarc.rs b/src/reporting/dmarc.rs index b4ce4ac..2cf49ea 100644 --- a/src/reporting/dmarc.rs +++ b/src/reporting/dmarc.rs @@ -29,10 +29,10 @@ use super::{ }; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -struct DmarcFormat { - rua: Vec, - policy: PolicyPublished, - records: Vec, +pub struct DmarcFormat { + pub rua: Vec, + pub policy: PolicyPublished, + pub records: Vec, } impl Session { @@ -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::(&format!("{},{}]}}", s, rs)).unwrap(), - d - ); - } -} diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index b475f99..ce9a9d8 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -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>, &Option>)> 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 + ); + } + } +} diff --git a/src/reporting/scheduler.rs b/src/reporting/scheduler.rs index 3790215..170dede 100644 --- a/src/reporting/scheduler.rs +++ b/src/reporting/scheduler.rs @@ -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, } -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ReportType { Dmarc(T), Tls(U), } +#[derive(Debug, PartialEq, Eq)] pub struct ReportPath { pub path: T, pub size: usize, @@ -52,7 +53,7 @@ pub struct ReportPath { pub deliver_at: AggregateFrequency, } -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ReportPolicy { pub inner: T, pub policy: u64, @@ -61,6 +62,8 @@ pub struct ReportPolicy { impl SpawnReport for mpsc::Receiver { fn spawn(mut self, core: Arc, 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 { _ => 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 } diff --git a/src/reporting/tls.rs b/src/reporting/tls.rs index f49e029..2b7a0a6 100644 --- a/src/reporting/tls.rs +++ b/src/reporting/tls.rs @@ -37,7 +37,7 @@ pub struct TlsRptOptions { pub interval: AggregateFrequency, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] struct TlsFormat { rua: Vec, policy: PolicyDetails, @@ -131,6 +131,7 @@ impl GenerateTlsReport for Arc { event = "empty-report", "No policies found in report" ); + path.cleanup_blocking(); return; } @@ -161,6 +162,15 @@ impl GenerateTlsReport for Arc { .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 { { 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 { 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; } } } diff --git a/src/tests/inbound/auth.rs b/src/tests/inbound/auth.rs index 83348b9..3a0f428 100644 --- a/src/tests/inbound/auth.rs +++ b/src/tests/inbound/auth.rs @@ -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::>(&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 diff --git a/src/tests/inbound/data.rs b/src/tests/inbound/data.rs index 8dc13e1..dd8f512 100644 --- a/src/tests/inbound/data.rs +++ b/src/tests/inbound/data.rs @@ -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 diff --git a/src/tests/inbound/sign.rs b/src/tests/inbound/sign.rs index df30a79..69412bc 100644 --- a/src/tests/inbound/sign.rs +++ b/src/tests/inbound/sign.rs @@ -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 diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 7a06c92..7c11160 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -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(), diff --git a/src/tests/remote/imap.rs b/src/tests/remote/imap.rs index 8d592e7..366b36c 100644 --- a/src/tests/remote/imap.rs +++ b/src/tests/remote/imap.rs @@ -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![ diff --git a/src/tests/remote/smtp.rs b/src/tests/remote/smtp.rs index b0e354f..12e34a4 100644 --- a/src/tests/remote/smtp.rs +++ b/src/tests/remote/smtp.rs @@ -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![ diff --git a/src/tests/reporting/analyze.rs b/src/tests/reporting/analyze.rs new file mode 100644 index 0000000..a722e2e --- /dev/null +++ b/src/tests/reporting/analyze.rs @@ -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(); +} diff --git a/src/tests/reporting/dmarc.rs b/src/tests/reporting/dmarc.rs new file mode 100644 index 0000000..004ef94 --- /dev/null +++ b/src/tests/reporting/dmarc.rs @@ -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::>(&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: ") + .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::().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::().unwrap() { + assert_eq!(record.count(), 1); + assert_eq!(record.action_disposition(), ActionDisposition::Reject); + } else { + panic!("unexpected ip {}", source_ip); + } + } + + assert!(!report_path.exists()); +} diff --git a/src/tests/reporting/mod.rs b/src/tests/reporting/mod.rs new file mode 100644 index 0000000..b5632b5 --- /dev/null +++ b/src/tests/reporting/mod.rs @@ -0,0 +1,4 @@ +pub mod analyze; +pub mod dmarc; +pub mod scheduler; +pub mod tls; diff --git a/src/tests/reporting/scheduler.rs b/src/tests/reporting/scheduler.rs new file mode 100644 index 0000000..9efbe31 --- /dev/null +++ b/src/tests/reporting/scheduler.rs @@ -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::(&format!("{},{}]}}", s, rs)).unwrap(), + d + ); +} diff --git a/src/tests/reporting/tls.rs b/src/tests/reporting/tls.rs new file mode 100644 index 0000000..9410a08 --- /dev/null +++ b/src/tests/reporting/tls.rs @@ -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> = 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::>(&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: ") + .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()); + } +} diff --git a/src/tests/session.rs b/src/tests/session.rs index dfb5c27..9f160a5 100644 --- a/src/tests/session.rs +++ b/src/tests/session.rs @@ -157,7 +157,11 @@ impl Session { 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 { } } -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() }