diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f1f689..54d396b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # SpamAssassin Milter changelog +## unreleased + +* Add `--reply-text` option to allow customising the reply text when rejecting + spam. + ## 0.1.2 (2020-06-07) * Bump milter dependency to version 0.2.1. diff --git a/spamassassin-milter.8 b/spamassassin-milter.8 index 738a415..a2f02b7 100644 --- a/spamassassin-milter.8 +++ b/spamassassin-milter.8 @@ -7,6 +7,7 @@ spamassassin-milter \- milter for spam filtering with SpamAssassin .RB [ \-B ] .RB [ \-H ] .RB [ \-n ] +[\fB\-R\fR \fIMSG\fR] .RB [ \-r ] [\fB\-s\fR \fIBYTES\fR] [\fB\-t\fR \fINETS\fR] @@ -114,6 +115,12 @@ Reject messages flagged as spam at the SMTP level. Rejection results in a permanent SMTP error being returned to the client, and the message is not delivered. .TP +.BR \-R ", " \-\-reply-text " \fIMSG\fR" +Reply with SMTP reply text +.I MSG +when rejecting a message flagged as spam. +For multiline replies, use an ASCII newline character as the line separator. +.TP .BR \-t ", " \-\-trusted-networks " \fINETS\fR" Trust connections coming from the IP networks or addresses .IR NETS . diff --git a/src/client.rs b/src/client.rs index 23d041d..5a5ceb3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -266,8 +266,8 @@ fn reject_spam(id: &str, actions: &impl SetErrorReply, config: &Config) -> milte Status::Accept } else { // These reply codes are the most appropriate according to RFCs 5321 and - // 3463. The text is kept generic and makes no mention of SpamAssassin. - actions.set_error_reply("550", Some("5.7.1"), vec!["Spam message refused"])?; + // 3463. The default text is generic and does not mention SpamAssassin. + actions.set_error_reply("550", Some("5.7.1"), config.reply_text().lines().collect())?; verbose!(config, "{}: rejected message flagged as spam", id); Status::Reject diff --git a/src/config.rs b/src/config.rs index 22fba02..426a105 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,6 +13,7 @@ pub struct ConfigBuilder { max_message_size: usize, dry_run: bool, reject_spam: bool, + reply_text: String, preserve_headers: bool, preserve_body: bool, verbose: bool, @@ -64,6 +65,11 @@ impl ConfigBuilder { self } + pub fn reply_text(&mut self, value: String) -> &mut Self { + self.reply_text = value; + self + } + pub fn preserve_headers(&mut self, value: bool) -> &mut Self { self.preserve_headers = value; self @@ -94,6 +100,7 @@ impl ConfigBuilder { max_message_size: self.max_message_size, dry_run: self.dry_run, reject_spam: self.reject_spam, + reply_text: self.reply_text, preserve_headers: self.preserve_headers, preserve_body: self.preserve_body, verbose: self.verbose, @@ -112,6 +119,8 @@ impl Default for ConfigBuilder { max_message_size: 512_000, // same as in `spamc` dry_run: Default::default(), reject_spam: Default::default(), + // Generic default reply text that makes no mention of SpamAssassin. + reply_text: String::from("Spam message refused"), preserve_headers: Default::default(), preserve_body: Default::default(), verbose: Default::default(), @@ -130,6 +139,7 @@ pub struct Config { max_message_size: usize, dry_run: bool, reject_spam: bool, + reply_text: String, preserve_headers: bool, preserve_body: bool, verbose: bool, @@ -172,6 +182,10 @@ impl Config { self.reject_spam } + pub fn reply_text(&self) -> &str { + &self.reply_text + } + pub fn preserve_headers(&self) -> bool { self.preserve_headers } diff --git a/src/main.rs b/src/main.rs index d03719a..9397088 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ const ARG_MILTER_DEBUG_LEVEL: &str = "MILTER_DEBUG_LEVEL"; const ARG_PRESERVE_BODY: &str = "PRESERVE_BODY"; const ARG_PRESERVE_HEADERS: &str = "PRESERVE_HEADERS"; const ARG_REJECT_SPAM: &str = "REJECT_SPAM"; +const ARG_REPLY_TEXT: &str = "REPLY_TEXT"; const ARG_TRUSTED_NETWORKS: &str = "TRUSTED_NETWORKS"; const ARG_VERBOSE: &str = "VERBOSE"; const ARG_SOCKET: &str = "SOCKET"; @@ -52,6 +53,12 @@ fn main() { .long("reject-spam") .conflicts_with_all(&[ARG_PRESERVE_BODY, ARG_PRESERVE_HEADERS]) .help("Reject messages flagged as spam")) + .arg(Arg::with_name(ARG_REPLY_TEXT) + .short("R") + .long("reply-text") + .value_name("MSG") + .requires(ARG_REJECT_SPAM) + .help("Reply text when rejecting messages")) .arg(Arg::with_name(ARG_TRUSTED_NETWORKS) .short("t") .long("trusted-networks") @@ -147,6 +154,10 @@ fn build_config(matches: &ArgMatches<'_>) -> Result { config.verbose(true); } + if let Some(msg) = matches.value_of(ARG_REPLY_TEXT) { + config.reply_text(msg.to_owned()); + } + if let Some(level) = matches.value_of(ARG_MILTER_DEBUG_LEVEL) { config.milter_debug_level(level.parse().unwrap()); } diff --git a/tests/reject_spam.lua b/tests/reject_spam.lua index cf3bf85..62c22b5 100644 --- a/tests/reject_spam.lua +++ b/tests/reject_spam.lua @@ -63,7 +63,7 @@ local err = mt.eom(conn) assert(err == nil, err) assert(mt.getreply(conn) == SMFIR_REPLYCODE) -assert(mt.eom_check(conn, MT_SMTPREPLY, "550", "5.7.1", "Spam message refused")) +assert(mt.eom_check(conn, MT_SMTPREPLY, "550", "5.7.1", "Not allowed!")) local err = mt.disconnect(conn) assert(err == nil, err) diff --git a/tests/reject_spam.rs b/tests/reject_spam.rs index 0e3412a..809e805 100644 --- a/tests/reject_spam.rs +++ b/tests/reject_spam.rs @@ -6,8 +6,10 @@ use spamassassin_milter::*; #[test] fn reject_spam() { let mut builder = Config::builder(); - builder.reject_spam(true); - builder.spamc_args(vec![format!("--port={}", SPAMD_PORT)]); + builder + .reject_spam(true) + .reply_text("Not allowed!".into()) + .spamc_args(vec![format!("--port={}", SPAMD_PORT)]); let config = builder.build(); let server = spawn_mock_spamd_server(SPAMD_PORT, |spam| {