Store incoming reports in the data store
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user