Updated spam filter autolearn settings

This commit is contained in:
mdecimus
2025-12-14 15:57:03 +01:00
parent 89f0229191
commit e1c1e9a6d4
8 changed files with 47 additions and 23 deletions

View File

@@ -87,6 +87,8 @@ pub struct ClassifierConfig {
pub min_spam_samples: u64,
pub auto_learn_reply_ham: bool,
pub auto_learn_card_is_ham: bool,
pub auto_learn_spam_score: f32,
pub auto_learn_ham_score: f32,
pub hold_samples_for: u64,
pub train_frequency: Option<u64>,
}
@@ -482,6 +484,12 @@ impl ClassifierConfig {
auto_learn_reply_ham: config
.property_or_default("spam-filter.trusted-reply.learn", "true")
.unwrap_or(true),
auto_learn_spam_score: config
.property_or_default("spam-filter.classifier.auto-learn.spam-score", "8.0")
.unwrap_or(8.0),
auto_learn_ham_score: config
.property_or_default("spam-filter.classifier.auto-learn.ham-score", "-8.0")
.unwrap_or(-8.0),
hold_samples_for: config
.property_or_default::<Duration>("spam-filter.classifier.samples.hold-for", "180d")
.unwrap_or(Duration::from_secs(180 * 24 * 60 * 60))

View File

@@ -31,7 +31,9 @@ use jmap_proto::{
use jmap_tools::{JsonPointerItem, Key, Map, Value};
use std::future::Future;
use store::{
ValueKey, roaring::RoaringBitmap, write::{AlignedBytes, Archive, BatchBuilder, assert::AssertValue}
ValueKey,
roaring::RoaringBitmap,
write::{AlignedBytes, Archive, BatchBuilder, assert::AssertValue},
};
use trc::AddContext;
use types::{
@@ -526,12 +528,12 @@ impl MailboxSet for Server {
// Role of internal folders cannot be modified
if update.as_ref().is_some_and(|(document_id, _)| {
*document_id == INBOX_ID || *document_id == TRASH_ID
*document_id == INBOX_ID || *document_id == TRASH_ID || *document_id == JUNK_ID
}) {
return Ok(Err(SetError::invalid_properties()
.with_property(MailboxProperty::Role)
.with_description(
"You are not allowed to change the role of Inbox or Trash folders.",
"You are not allowed to change the role of Inbox, Junk or Trash folders.",
)));
}
}

View File

@@ -425,7 +425,7 @@ impl<T: SessionStream> Session<T> {
}
// Run SPAM filter
let mut train_as_spam = false;
let mut train_spam = None;
if self.server.core.spam.enabled
&& self
.server
@@ -446,7 +446,7 @@ impl<T: SessionStream> Session<T> {
SpamFilterAction::Allow(score) => {
// Add headers
headers.extend_from_slice(score.headers.as_bytes());
train_as_spam = score.spam_trap;
train_spam = score.train_spam;
// Add scores for local recipients
for (is_spam, recipient) in
@@ -685,7 +685,7 @@ impl<T: SessionStream> Session<T> {
{
MessageSource::Unauthenticated {
dmarc_pass: dmarc_pass || message.message.return_path.starts_with("dmarc-"),
train_as_spam,
train_spam,
}
}
@@ -693,7 +693,7 @@ impl<T: SessionStream> Session<T> {
{
MessageSource::Unauthenticated {
dmarc_pass,
train_as_spam,
train_spam,
}
}
} else {

View File

@@ -45,7 +45,7 @@ pub enum MessageSource {
Authenticated,
Unauthenticated {
dmarc_pass: bool,
train_as_spam: bool,
train_spam: Option<bool>,
},
Dsn,
Report,

View File

@@ -306,30 +306,30 @@ impl MessageWrapper {
MessageSource::Authenticated => (
FROM_AUTHENTICATED,
trc::QueueEvent::QueueMessageAuthenticated,
false,
None,
),
MessageSource::Unauthenticated {
dmarc_pass: true,
train_as_spam,
train_spam,
} => (
FROM_UNAUTHENTICATED_DMARC,
trc::QueueEvent::QueueMessage,
train_as_spam,
train_spam,
),
MessageSource::Unauthenticated {
dmarc_pass: false,
train_as_spam,
train_spam,
} => (
FROM_UNAUTHENTICATED,
trc::QueueEvent::QueueMessage,
train_as_spam,
train_spam,
),
MessageSource::Dsn => (FROM_DSN, trc::QueueEvent::QueueDsn, false),
MessageSource::Report => (FROM_REPORT, trc::QueueEvent::QueueReport, false),
MessageSource::Dsn => (FROM_DSN, trc::QueueEvent::QueueDsn, None),
MessageSource::Report => (FROM_REPORT, trc::QueueEvent::QueueReport, None),
MessageSource::Autogenerated => (
FROM_AUTOGENERATED,
trc::QueueEvent::QueueAutogenerated,
false,
None,
),
};
self.message.flags |= flags;
@@ -439,7 +439,9 @@ impl MessageWrapper {
);
}
if train_spam && let Some(config) = &server.core.spam.classifier {
if let Some(is_spam) = train_spam
&& let Some(config) = &server.core.spam.classifier
{
let hold_period = now + config.hold_samples_for;
batch
@@ -455,12 +457,12 @@ impl MessageWrapper {
hash: self.message.blob_hash.clone(),
until: hold_period,
},
vec![1, 1],
vec![u8::from(is_spam), 1],
);
trc::event!(
Spam(SpamEvent::TrainSampleAdded),
Details = "spam",
Details = if is_spam { "spam" } else { "ham" },
Expires = trc::Value::Timestamp(hold_period),
SpanId = self.span_id,
);

View File

@@ -36,7 +36,6 @@ impl SpamFilterAnalyzeClassify for Server {
match store.key_exists(addr.address.as_str()).await {
Ok(true) => {
ctx.result.add_tag("SPAM_TRAP");
ctx.result.spam_trap = true;
return true;
}
Ok(false) => (),

View File

@@ -44,7 +44,7 @@ pub trait SpamFilterAnalyzeScore: Sync + Send {
pub struct SpamFilterScore {
pub results: Vec<bool>,
pub headers: String,
pub spam_trap: bool,
pub train_spam: Option<bool>,
pub score: f32,
}
@@ -163,10 +163,24 @@ impl SpamFilterAnalyzeScore for Server {
);
}
// Autolearn
let mut train_spam = None;
let config = self.core.spam.classifier.as_ref().unwrap();
if config.auto_learn_spam_score > 0.0 && final_score >= config.auto_learn_spam_score {
if !ctx.result.has_tag("PROB_SPAM_HIGH") {
train_spam = Some(true);
}
} else if config.auto_learn_ham_score < 0.0
&& final_score <= config.auto_learn_ham_score
&& !ctx.result.has_tag("PROB_HAM_HIGH")
{
train_spam = Some(false);
}
SpamFilterAction::Allow(SpamFilterScore {
results: user_results,
headers,
spam_trap: ctx.result.spam_trap,
train_spam,
score: final_score,
})
}

View File

@@ -106,7 +106,6 @@ pub struct SpamFilterResult {
pub rbl_url_checks: usize,
pub rbl_email_checks: usize,
pub llm_result: Option<(String, String)>,
pub spam_trap: bool,
}
pub struct SpamFilterContext<'x> {