/* * Copyright (c) 2023 Stalwart Labs Ltd. * * This file is part of Stalwart Mail Server. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * in the LICENSE file at the top-level directory of this distribution. * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * You can be released from the requirements of the AGPLv3 license by * purchasing a commercial license. Please contact licensing@stalw.art * for more details. */ use std::sync::Arc; use ahash::{AHashMap, HashSet}; use directory::config::ConfigDirectory; use mail_auth::{ common::parse::TxtRecordParser, dmarc::Dmarc, mta_sts::TlsRpt, report::{ tlsrpt::{FailureDetails, ResultType}, ActionDisposition, DmarcResult, Record, }, }; use tokio::sync::mpsc; use utils::config::{Config, ServerProtocol}; use crate::smtp::{ make_temp_dir, management::send_manage_request, outbound::start_test_server, TestConfig, }; use smtp::{ config::{AggregateFrequency, IfBlock}, core::{management::Report, SMTP}, reporting::{ scheduler::{Scheduler, SpawnReport}, DmarcEvent, TlsEvent, }, }; const DIRECTORY: &str = r#" [directory."local"] type = "memory" [directory."local".options] superuser-group = "superusers" [[directory."local".users]] name = "admin" description = "Superuser" secret = "secret" member-of = ["superusers"] "#; #[tokio::test] #[serial_test::serial] async fn manage_reports() { /*tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() .with_max_level(tracing::Level::DEBUG) .finish(), ) .unwrap();*/ // Start reporting service let mut core = SMTP::test(); let temp_dir = make_temp_dir("smtp_report_management_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(1024); config.tls.max_size = IfBlock::new(1024); let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap(); core.queue.config.management_lookup = directory.directories.get("local").unwrap().clone(); let (report_tx, report_rx) = mpsc::channel(1024); core.report.tx = report_tx; let core = Arc::new(core); report_rx.spawn(core.clone(), Scheduler::default()); let _rx_manage = start_test_server(core.clone(), &[ServerProtocol::Http]); // Send test reporting events core.schedule_report(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: Arc::new( Dmarc::parse(b"v=DMARC1; p=reject; rua=mailto:reports@foobar.org").unwrap(), ), interval: AggregateFrequency::Daily, }) .await; core.schedule_report(DmarcEvent { domain: "foobar.net".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: Arc::new( Dmarc::parse( b"v=DMARC1; p=quarantine; rua=mailto:reports@foobar.net,mailto:reports@example.net", ) .unwrap(), ), interval: AggregateFrequency::Weekly, }) .await; core.schedule_report(TlsEvent { domain: "foobar.org".to_string(), policy: smtp::reporting::PolicyType::None, failure: None, tls_record: Arc::new(TlsRpt::parse(b"v=TLSRPTv1;rua=mailto:reports@foobar.org").unwrap()), interval: AggregateFrequency::Daily, }) .await; core.schedule_report(TlsEvent { domain: "foobar.net".to_string(), policy: smtp::reporting::PolicyType::Sts(None), failure: FailureDetails::new(ResultType::StsPolicyInvalid).into(), tls_record: Arc::new(TlsRpt::parse(b"v=TLSRPTv1;rua=mailto:reports@foobar.net").unwrap()), interval: AggregateFrequency::Weekly, }) .await; // List reports let ids = send_manage_request::>("/admin/report/list") .await .unwrap() .unwrap_data(); assert_eq!(ids.len(), 4); let mut id_map = AHashMap::new(); let mut id_map_rev = AHashMap::new(); for (report, id) in get_reports(&ids).await.into_iter().zip(ids) { let mut parts = id.split('!'); let report = report.unwrap(); let mut id_num = if parts.next().unwrap() == "t" { assert_eq!(report.type_, "tls"); 2 } else { assert_eq!(report.type_, "dmarc"); 0 }; assert_eq!(parts.next().unwrap(), report.domain); let diff = report.range_to.to_timestamp() - report.range_from.to_timestamp(); if report.domain == "foobar.org" { assert_eq!(diff, 86400); } else { assert_eq!(diff, 7 * 86400); id_num += 1; } id_map.insert(char::from(b'a' + id_num).to_string(), id.clone()); id_map_rev.insert(id, char::from(b'a' + id_num).to_string()); } // Test list search for (query, expected_ids) in [ ("/admin/report/list?type=dmarc", vec!["a", "b"]), ("/admin/report/list?type=tls", vec!["c", "d"]), ("/admin/report/list?domain=foobar.org", vec!["a", "c"]), ("/admin/report/list?domain=foobar.net", vec!["b", "d"]), ("/admin/report/list?domain=foobar.org&type=dmarc", vec!["a"]), ("/admin/report/list?domain=foobar.net&type=tls", vec!["d"]), ] { let expected_ids = HashSet::from_iter(expected_ids.into_iter().map(|s| s.to_string())); let ids = send_manage_request::>(query) .await .unwrap() .unwrap_data() .into_iter() .map(|id| id_map_rev.get(&id).unwrap().clone()) .collect::>(); assert_eq!(ids, expected_ids, "failed for {query}"); } // Cancel reports for id in ["a", "b"] { assert_eq!( send_manage_request::>(&format!( "/admin/report/cancel?id={}", id_map.get(id).unwrap(), )) .await .unwrap() .unwrap_data(), vec![true], "failed for {id}" ); } assert_eq!( send_manage_request::>("/admin/report/list") .await .unwrap() .unwrap_data() .len(), 2 ); let mut ids = get_reports(&[ id_map.get("a").unwrap().clone(), id_map.get("b").unwrap().clone(), id_map.get("c").unwrap().clone(), id_map.get("d").unwrap().clone(), ]) .await .into_iter(); assert!(ids.next().unwrap().is_none()); assert!(ids.next().unwrap().is_none()); assert!(ids.next().unwrap().is_some()); assert!(ids.next().unwrap().is_some()); } async fn get_reports(ids: &[String]) -> Vec> { send_manage_request(&format!("/admin/report/status?id={}", ids.join(","))) .await .unwrap() .unwrap_data() }