Store incoming reports in the data store

This commit is contained in:
mdecimus
2024-03-02 12:21:03 +01:00
parent 417bc38288
commit 875b1fa744
40 changed files with 1992 additions and 1541 deletions

View File

@@ -208,6 +208,20 @@ impl Client {
url: &str,
body: Option<B>,
) -> R {
self.try_http_request(method, url, body)
.await
.unwrap_or_else(|| {
eprintln!("Request failed: No data returned.");
std::process::exit(1);
})
}
pub async fn try_http_request<R: DeserializeOwned, B: Serialize>(
&self,
method: Method,
url: &str,
body: Option<B>,
) -> Option<R> {
let url = format!(
"{}{}{}",
self.url,
@@ -240,6 +254,9 @@ impl Client {
match response.status() {
StatusCode::OK => (),
StatusCode::NOT_FOUND => {
return None;
}
StatusCode::UNAUTHORIZED => {
eprintln!("Authentication failed. Make sure the credentials are correct and that the account has administrator rights.");
std::process::exit(1);
@@ -258,7 +275,7 @@ impl Client {
)
.unwrap_result("deserialize response")
{
Response::Data { data } => data,
Response::Data { data } => Some(data),
Response::Error { error, details } => {
eprintln!("Request failed: {details} ({error:?})");
std::process::exit(1);

View File

@@ -117,6 +117,12 @@ pub enum PrincipalField {
Members,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct List<T> {
pub items: Vec<T>,
pub total: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PrincipalUpdate {
action: PrincipalAction,

View File

@@ -21,7 +21,10 @@
* for more details.
*/
use super::cli::{Client, QueueCommands};
use super::{
cli::{Client, QueueCommands},
List,
};
use console::Term;
use human_size::{Byte, SpecificSize};
use mail_parser::DateTime;
@@ -99,65 +102,62 @@ impl QueueCommands {
.map(|p| Cell::new(p).with_style(Attr::Bold))
.collect(),
));
for (message, id) in client
.http_request::<Vec<Option<Message>>, String>(
Method::GET,
&build_query("/api/queue/status?ids=", chunk),
None,
)
.await
.into_iter()
.zip(chunk)
{
if let Some(message) = message {
let mut rcpts = String::new();
let mut deliver_at = i64::MAX;
let mut deliver_pos = 0;
for (pos, domain) in message.domains.iter().enumerate() {
if let Some(next_retry) = &domain.next_retry {
let ts = next_retry.to_timestamp();
if ts < deliver_at {
deliver_at = ts;
deliver_pos = pos;
}
}
for rcpt in &domain.recipients {
if !rcpts.is_empty() {
rcpts.push('\n');
}
rcpts.push_str(&rcpt.address);
rcpts.push_str(" (");
rcpts.push_str(rcpt.status.status_short());
rcpts.push(')');
for id in chunk {
let message = client
.http_request::<Message, String>(
Method::GET,
&format!("/api/queue/messages/{id}"),
None,
)
.await;
let mut rcpts = String::new();
let mut deliver_at = i64::MAX;
let mut deliver_pos = 0;
for (pos, domain) in message.domains.iter().enumerate() {
if let Some(next_retry) = &domain.next_retry {
let ts = next_retry.to_timestamp();
if ts < deliver_at {
deliver_at = ts;
deliver_pos = pos;
}
}
let mut cells = Vec::new();
cells.push(Cell::new(&format!("{id:X}")));
cells.push(if deliver_at != i64::MAX {
Cell::new(
&message.domains[deliver_pos]
.next_retry
.as_ref()
.unwrap()
.to_rfc822(),
)
} else {
Cell::new("None")
});
cells.push(Cell::new(if !message.return_path.is_empty() {
&message.return_path
} else {
"<>"
}));
cells.push(Cell::new(&rcpts));
cells.push(Cell::new(
&SpecificSize::new(message.size as u32, Byte)
.unwrap()
.to_string(),
));
table.add_row(Row::new(cells));
for rcpt in &domain.recipients {
if !rcpts.is_empty() {
rcpts.push('\n');
}
rcpts.push_str(&rcpt.address);
rcpts.push_str(" (");
rcpts.push_str(rcpt.status.status_short());
rcpts.push(')');
}
}
let mut cells = Vec::new();
cells.push(Cell::new(&format!("{id:X}")));
cells.push(if deliver_at != i64::MAX {
Cell::new(
&message.domains[deliver_pos]
.next_retry
.as_ref()
.unwrap()
.to_rfc822(),
)
} else {
Cell::new("None")
});
cells.push(Cell::new(if !message.return_path.is_empty() {
&message.return_path
} else {
"<>"
}));
cells.push(Cell::new(&rcpts));
cells.push(Cell::new(
&SpecificSize::new(message.size as u32, Byte)
.unwrap()
.to_string(),
));
table.add_row(Row::new(cells));
}
eprintln!();
@@ -173,21 +173,20 @@ impl QueueCommands {
eprintln!("\n{ids_len} queued message(s) found.")
}
QueueCommands::Status { ids } => {
for (message, id) in client
.http_request::<Vec<Option<Message>>, String>(
Method::GET,
&build_query("/api/queue/status?ids=", &parse_ids(&ids)),
None,
)
.await
.into_iter()
.zip(&ids)
{
for (uid, id) in parse_ids(&ids).into_iter().zip(ids) {
let message = client
.try_http_request::<Message, String>(
Method::GET,
&format!("/api/queue/messages/{uid}"),
None,
)
.await;
let mut table = Table::new();
table.add_row(Row::new(vec![
Cell::new("ID").with_style(Attr::Bold),
Cell::new(id),
Cell::new(&id),
]));
if let Some(message) = message {
table.add_row(Row::new(vec![
Cell::new("Sender").with_style(Attr::Bold),
@@ -316,30 +315,31 @@ impl QueueCommands {
std::process::exit(1);
}
let mut query = form_urlencoded::Serializer::new("/api/queue/retry?".to_string());
if let Some(filter) = &domain {
query.append_pair("filter", filter);
}
if let Some(at) = time {
query.append_pair("at", &at.to_rfc3339());
}
query.append_pair("ids", &append_ids(String::new(), &parsed_ids));
let mut success_count = 0;
let mut failed_list = vec![];
for (success, id) in client
.http_request::<Vec<bool>, String>(Method::GET, &query.finish(), None)
.await
.into_iter()
.zip(ids)
{
if success {
for id in parsed_ids {
let mut query =
form_urlencoded::Serializer::new(format!("/api/queue/messages/{id}"));
if let Some(filter) = &domain {
query.append_pair("filter", filter);
}
if let Some(at) = time {
query.append_pair("at", &at.to_rfc3339());
}
if client
.try_http_request::<bool, String>(Method::PATCH, &query.finish(), None)
.await
.unwrap_or(false)
{
success_count += 1;
} else {
failed_list.push(id);
failed_list.push(id.to_string());
}
}
eprint!("\nSuccessfully rescheduled {success_count} message(s).");
if !failed_list.is_empty() {
eprint!(" Unable to reschedule id(s): {}.", failed_list.join(", "));
@@ -371,27 +371,28 @@ impl QueueCommands {
std::process::exit(1);
}
let mut query = form_urlencoded::Serializer::new("/api/queue/cancel?".to_string());
if let Some(filter) = &rcpt {
query.append_pair("filter", filter);
}
query.append_pair("ids", &append_ids(String::new(), &parsed_ids));
let mut success_count = 0;
let mut failed_list = vec![];
for (success, id) in client
.http_request::<Vec<bool>, String>(Method::GET, &query.finish(), None)
.await
.into_iter()
.zip(ids)
{
if success {
for id in parsed_ids {
let mut query =
form_urlencoded::Serializer::new(format!("/api/queue/messages/{id}"));
if let Some(filter) = &rcpt {
query.append_pair("filter", filter);
}
if client
.try_http_request::<bool, String>(Method::DELETE, &query.finish(), None)
.await
.unwrap_or(false)
{
success_count += 1;
} else {
failed_list.push(id);
failed_list.push(id.to_string());
}
}
eprint!("\nCancelled delivery of {success_count} message(s).");
if !failed_list.is_empty() {
eprint!(
@@ -413,7 +414,7 @@ impl Client {
before: &Option<DateTime>,
after: &Option<DateTime>,
) -> Vec<u64> {
let mut query = form_urlencoded::Serializer::new("/api/queue/list?".to_string());
let mut query = form_urlencoded::Serializer::new("/api/queue/messages".to_string());
if let Some(sender) = from {
query.append_pair("from", sender);
@@ -428,8 +429,9 @@ impl Client {
query.append_pair("after", &after.to_rfc3339());
}
self.http_request::<Vec<u64>, String>(Method::GET, &query.finish(), None)
self.http_request::<List<u64>, String>(Method::GET, &query.finish(), None)
.await
.items
}
}
@@ -479,22 +481,6 @@ fn parse_ids(ids: &[String]) -> Vec<u64> {
result
}
fn build_query(path: &str, ids: &[u64]) -> String {
let mut query = String::with_capacity(path.len() + (ids.len() * 10));
query.push_str(path);
append_ids(query, ids)
}
fn append_ids(mut query: String, ids: &[u64]) -> String {
for (pos, id) in ids.iter().enumerate() {
if pos != 0 {
query.push(',');
}
query.push_str(&id.to_string());
}
query
}
impl Status {
fn status_short(&self) -> &str {
match self {

View File

@@ -22,24 +22,83 @@
*/
use super::cli::{Client, ReportCommands, ReportFormat};
use crate::modules::queue::deserialize_datetime;
use crate::modules::{queue::deserialize_datetime, List};
use console::Term;
use human_size::{Byte, SpecificSize};
use mail_auth::{
dmarc::URI,
mta_sts::ReportUri,
report::{self, tlsrpt::TlsReport},
};
use mail_parser::DateTime;
use prettytable::{format::Alignment, Attr, Cell, Row, Table};
use prettytable::{format, Attr, Cell, Row, Table};
use reqwest::Method;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct Report {
pub domain: String,
#[serde(rename = "type")]
pub type_: ReportFormat,
#[serde(deserialize_with = "deserialize_datetime")]
pub range_from: DateTime,
#[serde(deserialize_with = "deserialize_datetime")]
pub range_to: DateTime,
pub size: usize,
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Report {
Tls {
id: String,
domain: String,
#[serde(deserialize_with = "deserialize_datetime")]
range_from: DateTime,
#[serde(deserialize_with = "deserialize_datetime")]
range_to: DateTime,
report: TlsReport,
rua: Vec<ReportUri>,
},
Dmarc {
id: String,
domain: String,
#[serde(deserialize_with = "deserialize_datetime")]
range_from: DateTime,
#[serde(deserialize_with = "deserialize_datetime")]
range_to: DateTime,
report: report::Report,
rua: Vec<URI>,
},
}
impl Report {
pub fn domain(&self) -> &str {
match self {
Report::Tls { domain, .. } => domain,
Report::Dmarc { domain, .. } => domain,
}
}
pub fn type_(&self) -> &str {
match self {
Report::Tls { .. } => "TLS",
Report::Dmarc { .. } => "DMARC",
}
}
pub fn range_from(&self) -> &DateTime {
match self {
Report::Tls { range_from, .. } => range_from,
Report::Dmarc { range_from, .. } => range_from,
}
}
pub fn range_to(&self) -> &DateTime {
match self {
Report::Tls { range_to, .. } => range_to,
Report::Dmarc { range_to, .. } => range_to,
}
}
pub fn num_records(&self) -> usize {
match self {
Report::Tls { report, .. } => report
.policies
.iter()
.map(|p| p.failure_details.len())
.sum(),
Report::Dmarc { report, .. } => report.records().len(),
}
}
}
impl ReportCommands {
@@ -51,7 +110,7 @@ impl ReportCommands {
page_size,
} => {
let stdout = Term::buffered_stdout();
let mut query = form_urlencoded::Serializer::new("/api/report/list?".to_string());
let mut query = form_urlencoded::Serializer::new("/api/queue/reports".to_string());
if let Some(domain) = &domain {
query.append_pair("domain", domain);
@@ -61,8 +120,9 @@ impl ReportCommands {
}
let ids = client
.http_request::<Vec<String>, String>(Method::GET, &query.finish(), None)
.await;
.http_request::<List<String>, String>(Method::GET, &query.finish(), None)
.await
.items;
let ids_len = ids.len();
let page_size = page_size.map(|p| std::cmp::max(p, 1)).unwrap_or(20);
let pages_total = (ids_len as f64 / page_size as f64).ceil() as usize;
@@ -70,30 +130,29 @@ impl ReportCommands {
// Build table
let mut table = Table::new();
table.add_row(Row::new(
["ID", "Domain", "Type", "From Date", "To Date", "Size"]
["ID", "Domain", "Type", "From Date", "To Date", "Records"]
.iter()
.map(|p| Cell::new(p).with_style(Attr::Bold))
.collect(),
));
for (report, id) in client
.http_request::<Vec<Option<Report>>, String>(
Method::GET,
&format!("/api/report/status?ids={}", chunk.join(",")),
None,
)
.await
.into_iter()
.zip(chunk)
{
for id in chunk {
let report = client
.try_http_request::<Report, String>(
Method::GET,
&format!("/api/queue/reports/{id}"),
None,
)
.await;
if let Some(report) = report {
table.add_row(Row::new(vec![
Cell::new(id),
Cell::new(&report.domain),
Cell::new(report.type_.name()),
Cell::new(&report.range_from.to_rfc822()),
Cell::new(&report.range_to.to_rfc822()),
Cell::new(report.domain()),
Cell::new(report.type_()),
Cell::new(&report.range_from().to_rfc822()),
Cell::new(&report.range_to().to_rfc822()),
Cell::new(
&SpecificSize::new(report.size as u32, Byte)
&SpecificSize::new(report.num_records() as u32, Byte)
.unwrap()
.to_string(),
),
@@ -114,42 +173,41 @@ impl ReportCommands {
eprintln!("\n{ids_len} queued message(s) found.")
}
ReportCommands::Status { ids } => {
for (report, id) in client
.http_request::<Vec<Option<Report>>, String>(
Method::GET,
&format!("/api/report/status?ids={}", ids.join(",")),
None,
)
.await
.into_iter()
.zip(&ids)
{
for id in ids {
let report = client
.try_http_request::<Report, String>(
Method::GET,
&format!("/api/queue/reports/{id}"),
None,
)
.await;
let mut table = Table::new();
table.add_row(Row::new(vec![
Cell::new("ID").with_style(Attr::Bold),
Cell::new(id),
Cell::new(&id),
]));
if let Some(report) = report {
table.add_row(Row::new(vec![
Cell::new("Domain Name").with_style(Attr::Bold),
Cell::new(&report.domain),
Cell::new(report.domain()),
]));
table.add_row(Row::new(vec![
Cell::new("Type").with_style(Attr::Bold),
Cell::new(report.type_.name()),
Cell::new(report.type_()),
]));
table.add_row(Row::new(vec![
Cell::new("From Date").with_style(Attr::Bold),
Cell::new(&report.range_from.to_rfc822()),
Cell::new(&report.range_from().to_rfc822()),
]));
table.add_row(Row::new(vec![
Cell::new("To Date").with_style(Attr::Bold),
Cell::new(&report.range_to.to_rfc822()),
Cell::new(&report.range_to().to_rfc822()),
]));
table.add_row(Row::new(vec![
Cell::new("Size").with_style(Attr::Bold),
Cell::new("Records").with_style(Attr::Bold),
Cell::new(
&SpecificSize::new(report.size as u32, Byte)
&SpecificSize::new(report.num_records() as u32, Byte)
.unwrap()
.to_string(),
),
@@ -157,7 +215,7 @@ impl ReportCommands {
} else {
table.add_row(Row::new(vec![Cell::new_align(
"-- Not found --",
Alignment::CENTER,
format::Alignment::CENTER,
)
.with_hspan(2)]));
}
@@ -170,17 +228,16 @@ impl ReportCommands {
ReportCommands::Cancel { ids } => {
let mut success_count = 0;
let mut failed_list = vec![];
for (success, id) in client
.http_request::<Vec<bool>, String>(
Method::GET,
&format!("/api/report/cancel?ids={}", ids.join(",")),
None,
)
.await
.into_iter()
.zip(ids)
{
if success {
for id in ids {
let success = client
.try_http_request::<bool, String>(
Method::DELETE,
&format!("/api/queue/reports/{id}"),
None,
)
.await;
if success.unwrap_or_default() {
success_count += 1;
} else {
failed_list.push(id);
@@ -206,11 +263,4 @@ impl ReportFormat {
ReportFormat::Tls => "tls",
}
}
fn name(&self) -> &'static str {
match self {
ReportFormat::Dmarc => "DMARC",
ReportFormat::Tls => "TLS",
}
}
}