mirror of
https://github.com/mailscope/kumomta.git
synced 2026-08-19 19:08:17 +00:00
Add kumo.configure_bounce_classifier
and connect it to the logger
This commit is contained in:
Generated
+1
@@ -1281,6 +1281,7 @@ dependencies = [
|
||||
"axum-client-ip",
|
||||
"axum-server",
|
||||
"base64 0.13.1",
|
||||
"bounce-classify",
|
||||
"caps",
|
||||
"chrono",
|
||||
"cidr",
|
||||
|
||||
@@ -173,9 +173,18 @@ mod test {
|
||||
("552 4.2.2 mailbox is stuffed", BounceClass::QuotaIssues),
|
||||
("552 4.2.2 mailbox is stuffed", BounceClass::QuotaIssues),
|
||||
("352 5.2.2 mailbox is stuffed", BounceClass::Uncategorized),
|
||||
("525 4.7.13 user account is disabled", BounceClass::InactiveMailbox),
|
||||
("551 4.7.17 mailbox owner has changed", BounceClass::InvalidRecipient),
|
||||
("551 4.7.18 domain owner has changed", BounceClass::BadDomain),
|
||||
(
|
||||
"525 4.7.13 user account is disabled",
|
||||
BounceClass::InactiveMailbox,
|
||||
),
|
||||
(
|
||||
"551 4.7.17 mailbox owner has changed",
|
||||
BounceClass::InvalidRecipient,
|
||||
),
|
||||
(
|
||||
"551 4.7.18 domain owner has changed",
|
||||
BounceClass::BadDomain,
|
||||
),
|
||||
];
|
||||
|
||||
for &(input, output) in corpus {
|
||||
|
||||
@@ -12,6 +12,7 @@ axum = "0.6"
|
||||
axum-client-ip = "0.4"
|
||||
axum-server = {version="0.4", features=["tls-rustls"]}
|
||||
base64 = "0.13"
|
||||
bounce-classify = {path="../bounce-classify"}
|
||||
caps = "0.5"
|
||||
chrono = {version="0.4", features=["serde"]}
|
||||
cidr = {version="0.2", features=["serde"]}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::mx::ResolvedAddress;
|
||||
use anyhow::Context;
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_channel::{Receiver, Sender};
|
||||
use bounce_classify::{BounceClass, BounceClassifier, BounceClassifierBuilder};
|
||||
use chrono::{DateTime, Utc};
|
||||
use message::Message;
|
||||
use once_cell::sync::OnceCell;
|
||||
@@ -14,6 +15,39 @@ use std::thread::JoinHandle;
|
||||
use zstd::stream::write::{AutoFinishEncoder, Encoder};
|
||||
|
||||
static LOGGER: OnceCell<Logger> = OnceCell::new();
|
||||
static CLASSIFY: OnceCell<BounceClassifier> = OnceCell::new();
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct ClassifierParams {
|
||||
pub files: Vec<String>,
|
||||
}
|
||||
|
||||
impl ClassifierParams {
|
||||
pub fn register(&self) -> anyhow::Result<()> {
|
||||
let mut builder = BounceClassifierBuilder::new();
|
||||
for file_name in &self.files {
|
||||
if file_name.ends_with(".json") {
|
||||
builder
|
||||
.merge_json_file(file_name)
|
||||
.map_err(|err| anyhow!("{err}"))?;
|
||||
} else if file_name.ends_with(".toml") {
|
||||
builder
|
||||
.merge_toml_file(file_name)
|
||||
.map_err(|err| anyhow!("{err}"))?;
|
||||
} else {
|
||||
anyhow::bail!("{file_name}: classifier files must have either .toml or .json filename extension");
|
||||
}
|
||||
}
|
||||
|
||||
let classifier = builder.build().map_err(|err| anyhow!("{err}"))?;
|
||||
|
||||
CLASSIFY
|
||||
.set(classifier)
|
||||
.map_err(|_| anyhow::anyhow!("classifieer already initialized"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct LogFileParams {
|
||||
@@ -98,8 +132,11 @@ impl Logger {
|
||||
fn do_record(
|
||||
params: &LogFileParams,
|
||||
file: &mut Option<OpenedFile>,
|
||||
record: JsonLogRecord,
|
||||
mut record: JsonLogRecord,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(classifier) = CLASSIFY.get() {
|
||||
record.bounce_classification = classifier.classify_response(&record.response);
|
||||
}
|
||||
if file.is_none() {
|
||||
let now = Utc::now();
|
||||
let name = params.log_dir.join(now.format("%Y%m%d-%H%M%S").to_string());
|
||||
@@ -216,6 +253,8 @@ pub struct JsonLogRecord {
|
||||
/// number of logged events to determine the true number
|
||||
pub num_attempts: u16,
|
||||
|
||||
pub bounce_classification: BounceClass,
|
||||
|
||||
pub egress_pool: Option<String>,
|
||||
pub egress_source: Option<String>,
|
||||
}
|
||||
@@ -253,6 +292,7 @@ pub async fn log_disposition(
|
||||
num_attempts: msg.get_num_attempts(),
|
||||
egress_pool: egress_pool.map(|s| s.to_string()),
|
||||
egress_source: egress_source.map(|s| s.to_string()),
|
||||
bounce_classification: BounceClass::Uncategorized,
|
||||
};
|
||||
if let Err(err) = logger.log(record).await {
|
||||
tracing::error!("failed to log: {err:#}");
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::egress_path::EgressPathConfig;
|
||||
use crate::egress_source::{EgressPool, EgressSource};
|
||||
use crate::http_server::HttpListenerParams;
|
||||
use crate::lifecycle::LifeCycle;
|
||||
use crate::logging::LogFileParams;
|
||||
use crate::logging::{ClassifierParams, LogFileParams};
|
||||
use crate::queue::QueueConfig;
|
||||
use crate::smtp_server::{EsmtpListenerParams, RejectError};
|
||||
use config::get_or_create_module;
|
||||
@@ -22,6 +22,16 @@ pub fn register(lua: &Lua) -> anyhow::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
kumo_mod.set(
|
||||
"configure_bounce_classifier",
|
||||
lua.create_function(move |lua, params: Value| {
|
||||
let params: ClassifierParams = lua.from_value(params)?;
|
||||
params
|
||||
.register()
|
||||
.map_err(|err| mlua::Error::external(format!("{err:#}")))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
kumo_mod.set(
|
||||
"configure_local_logs",
|
||||
lua.create_function(move |lua, params: Value| {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# `kumo.configure_bounce_classifier {PARAMS}`
|
||||
|
||||
Configures the bounce classifier. The purpose of the classifier
|
||||
is to attempt to digest complex and wide-ranging responses into
|
||||
a smaller set of categories to help inform the sender how best
|
||||
to respond and react to a delivery failure.
|
||||
|
||||
This function should be called only from inside your [init](../events/init.md)
|
||||
event handler.
|
||||
|
||||
```admonish
|
||||
The precise set of classifications are not yet finalized so are
|
||||
not reproduced here. They can be found in the `BounceClass` enum
|
||||
in `kumomta/crates/bounce-classify/src/lib.rs`
|
||||
```
|
||||
|
||||
The classifier must be configured with a set of rules files
|
||||
that provide mappings from a set of regular expressions to
|
||||
the available clasification codes.
|
||||
|
||||
`kumo.configure_bounce_classifier` will compile the merged
|
||||
set of files and rules into an efficient regexset that can
|
||||
quickly match the rule to the classification code.
|
||||
|
||||
Once the classifier has been configured via this function,
|
||||
the logging functions will automatically call into it to
|
||||
populate the `bounce_classification` field.
|
||||
|
||||
```lua
|
||||
kumo.on('init', function()
|
||||
kumo.configure_local_logs {
|
||||
log_dir = '/var/log/kumo-logs',
|
||||
}
|
||||
kumo.configure_bounce_classifier {
|
||||
files = {
|
||||
'/etc/kumo/iana.toml',
|
||||
},
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
The `iana.toml` file is provided with rules that map from
|
||||
[IANA defined status
|
||||
codes](https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml)
|
||||
to an appropriate bounce class.
|
||||
|
||||
You may create and maintain your own classifications and add then to the list
|
||||
of files.
|
||||
|
||||
Here's an excerpt of the `iana.toml`:
|
||||
|
||||
```toml
|
||||
# This file contains rules that match SMTP ENHANCEDSTATUSCODES
|
||||
# codes as defined in the IANA registry:
|
||||
# https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml
|
||||
# to bounce classifications.
|
||||
[rules]
|
||||
InvalidRecipient = [
|
||||
"^(451|550) [45]\\.1\\.[1234] ",
|
||||
"^45[02] [45]\\.2\\.4 ", # Mailing list expansion
|
||||
"^5\\d{2} [45]\\.7\\.17 ", # RRVS: Mailbox owner has changed
|
||||
]
|
||||
BadDomain = [
|
||||
"^(451|550) [45]\\.1\\.10 ", # NULL MX
|
||||
"^5\\d{2} [45]\\.7\\.18 ", # RRVS: domain owner has changed
|
||||
]
|
||||
InactiveMailbox = [
|
||||
"^(451|550) [45]\\.1\\.[6] ",
|
||||
"^[45]\\d{2} [45]\\.2\\.1 ",
|
||||
"^525 [45]\\.7\\.13 ", # User account disabled
|
||||
]
|
||||
InvalidSender = [
|
||||
"^(451|550) [45]\\.1\\.[78] ",
|
||||
"^\\d{3} [45]\\.7\\.27 ", # Send address has NULL MX
|
||||
]
|
||||
QuotaIssues = [
|
||||
"^552 [45]\\.2\\.2 ",
|
||||
"^552 [45]\\.2\\.3 ",
|
||||
"^452 [45]\\.3\\.1 ", # Mail System Full
|
||||
"^55[24] [45]\\.3\\.4 ", # Message too large for system
|
||||
]
|
||||
```
|
||||
Reference in New Issue
Block a user