Add more tests, revise README
This commit is contained in:
parent
4ed2133618
commit
de915de167
31 changed files with 847 additions and 252 deletions
7
CHANGELOG.md
Normal file
7
CHANGELOG.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# SpamAssassin Milter changelog
|
||||
|
||||
**UNRELEASED**
|
||||
|
||||
## 0.1.0 (2020-??-??)
|
||||
|
||||
Initial release.
|
|
@ -18,10 +18,6 @@ libc = "0.2"
|
|||
milter = "0.2"
|
||||
once_cell = "1"
|
||||
|
||||
[badges]
|
||||
gitlab = { repository = "glts/spamassassin-milter" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
# Temporary hack to make the build work on docs.rs, see
|
||||
# https://github.com/rust-lang/docs.rs/issues/191.
|
||||
[package.metadata.docs.rs]
|
||||
|
|
230
README.md
230
README.md
|
@ -1,124 +1,196 @@
|
|||
# SpamAssassin Milter
|
||||
|
||||
This milter applies mail filtering through SpamAssassin server using the spamc
|
||||
client.
|
||||
SpamAssassin Milter is a milter application that filters email through
|
||||
SpamAssassin server using the `spamc` client. It is a light-weight component
|
||||
that serves to integrate [Apache SpamAssassin] with a milter-capable MTA (mail
|
||||
server) such as [Postfix]. Its task is thus helping combat spam on email sites.
|
||||
|
||||
This program is intended as a replacement for [spamass-milt].
|
||||
SpamAssassin Milter operates as a milter hooked into the MTA’s SMTP protocol
|
||||
handler. It passes incoming messages to SpamAssassin for analysis, and then
|
||||
interprets the response from SpamAssassin and applies suggested changes to the
|
||||
message.
|
||||
|
||||
Status: **EXPERIMENTAL/IN DEVELOPMENT**
|
||||
By default, the following modifications are made:
|
||||
|
||||
* Always: Add SpamAssassin headers to the message (headers starting with
|
||||
`X-Spam-`)
|
||||
* Spam only: Rewrite headers `Subject` `From` `To`, if requested
|
||||
* Spam only: Replace message body (and rewrite related headers `MIME-Version`
|
||||
`Content-Type`, if requested)
|
||||
|
||||
Alternatively, messages flagged as spam may be rejected at the SMTP level with
|
||||
an SMTP error reply code.
|
||||
|
||||
Both SpamAssassin and SpamAssassin Milter provide various configuration options
|
||||
to alter the default behaviour. See below for a discussion of several
|
||||
configuration and integration approaches.
|
||||
|
||||
This application can be used as a replacement for [spamass-milt]; it has a
|
||||
reduced feature set, but it should be satisfactory for a personal mail server
|
||||
setup. SpamAssassin Milter has been used in such a setup together with Postfix,
|
||||
SpamAssassin, and for delivery [Dovecot] with LMTP and the [Sieve] plugin.
|
||||
|
||||
[Apache SpamAssassin]: https://spamassassin.apache.org
|
||||
[Postfix]: http://www.postfix.org
|
||||
[spamass-milt]: https://savannah.nongnu.org/projects/spamass-milt/
|
||||
|
||||
## Operation
|
||||
|
||||
SpamAssassin Milter filters email messages through the SpamAssassin server. By
|
||||
default it applies the following modifications to messages:
|
||||
|
||||
* Always: Rewrite SpamAssassin headers
|
||||
* Spam only: Rewrite ‘rewrite’ headers Subject From To if necessary
|
||||
* Spam only: Replace message body (and rewrite related headers MIME-Version
|
||||
Content-Type if necessary)
|
||||
|
||||
TODO describe --reject-spam
|
||||
[Dovecot]: https://dovecot.org
|
||||
[Sieve]: https://doc.dovecot.org/configuration_manual/sieve/
|
||||
|
||||
## Building
|
||||
|
||||
This project uses Cargo as usual. If your distribution does not install a
|
||||
pkg-config metadata file for the milter library used by SpamAssassin Milter, you
|
||||
can use the provided milter.pc file. Put it on the pkg-config path as follows:
|
||||
This project is a Rust package. Build it with Cargo as usual.
|
||||
|
||||
As a milter, this package requires the libmilter C library to be available. Be
|
||||
sure to install the libmilter shared library and header files.
|
||||
|
||||
If your distribution does not install a pkg-config metadata file for libmilter,
|
||||
you can use the provided `milter.pc` file. Put it on the pkg-config path as
|
||||
follows:
|
||||
|
||||
```
|
||||
PKG_CONFIG_PATH=. cargo build
|
||||
```
|
||||
|
||||
The integration tests rely on the `miltertest` utility. Make sure `miltertest`
|
||||
is available and can be executed when running the integration tests.
|
||||
|
||||
Note that until recently `miltertest` had a bad bug that prevents most
|
||||
integration tests in this package from completing. Make sure you use an
|
||||
up-to-date version of `miltertest`.
|
||||
|
||||
## Usage
|
||||
|
||||
**This program is in development.** (I do already use it to filter mail on my
|
||||
own domain, though.)
|
||||
Once installed, SpamAssassin Milter can be by invoked as `spamassassin-milter`.
|
||||
`spamassassin-milter` takes one mandatory argument, namely the listening socket
|
||||
of the milter (the socket to which the MTA will connect). The socket spec can be
|
||||
in one of the formats <code>inet:<em>port</em>@<em>host</em></code> or
|
||||
<code>inet6:<em>port</em>@<em>host</em></code> (IPv6), or
|
||||
<code>unix:<em>path</em></code>, for a TCP or UNIX domain socket, respectively.
|
||||
|
||||
The easiest way of setting up SpamAssassin Milter is to edit the provided
|
||||
systemd service file spamassassin-milter.service, and install it in
|
||||
/etc/systemd/system.
|
||||
|
||||
There is one required argument, the milter listening socket.
|
||||
|
||||
TODO
|
||||
|
||||
### ‘dry-run’
|
||||
|
||||
For first-time users, before deploying SpamAssassin Milter on your mail server,
|
||||
you may wish to run it with the ‘dry-run’ option. Combined with the verbose
|
||||
logging option, this gives good insight in the log about the changes that
|
||||
SpamAssassin Milter would apply.
|
||||
For example, the following invocation starts SpamAssassin Milter on port 3000:
|
||||
|
||||
```
|
||||
/usr/sbin/spamassassin-milter --dry-run --verbose inet:3001@localhost
|
||||
spamassassin-milter inet:3000@localhost
|
||||
```
|
||||
|
||||
The available options and flags can be glimpsed by passing the `-h` flag:
|
||||
|
||||
```
|
||||
spamassassin-milter[8604]: AD3F03F1F3: rewriting header: add header "X-Spam-Checker-Version" [dry-run, not done]
|
||||
spamassassin-milter[8604]: AD3F03F1F3: rewriting header: add header "X-Spam-Level" [dry-run, not done]
|
||||
spamassassin-milter[8604]: AD3F03F1F3: rewriting header: add header "X-Spam-Status" [dry-run, not done]
|
||||
spamassassin-milter[8604]: AD3F03F1F3: finished processing
|
||||
spamassassin-milter[8604]: 112D03F1F3: rewriting header: replace header "X-Spam-Checker-Version" [dry-run, not done]
|
||||
spamassassin-milter[8604]: 112D03F1F3: rewriting header: replace header "X-Spam-Status" [dry-run, not done]
|
||||
spamassassin-milter[8604]: 112D03F1F3: finished processing
|
||||
spamassassin-milter -h
|
||||
```
|
||||
|
||||
More detailed information can be found in the provided man page
|
||||
*spamassassin-milter*(8).
|
||||
|
||||
Setting up SpamAssassin Milter as a system service is easiest by using the
|
||||
provided systemd service file: Edit `spamassassin-milter.service` with the
|
||||
desired port, install it in `/etc/systemd/system`, then enable and start the
|
||||
service.
|
||||
|
||||
## Configuration
|
||||
|
||||
SpamAssassin Milter is designed as a light-weight ‘glue’ application with just a
|
||||
few configuration options; this is intentional, as the SpamAssassin components
|
||||
are themselves already highly configurable.
|
||||
|
||||
SpamAssassin Milter is configured by setting command-line options. All options
|
||||
have reasonable defaults that work well with a stock installation of
|
||||
SpamAssassin server (`spamd`) and client (`spamc`). You can get started with
|
||||
just picking a socket and things should just work. Some integration options are
|
||||
discussed in subsequent sections.
|
||||
|
||||
First-time users may wish to run SpamAssassin Milter with the `--dry-run` option
|
||||
before ‘going live’. Combined with the `--verbose` option, this gives accurate
|
||||
insight into the changes that SpamAssassin Milter would apply.
|
||||
|
||||
```
|
||||
spamassassin-milter --dry-run --verbose inet:3000@localhost
|
||||
```
|
||||
|
||||
## Integration with SpamAssassin
|
||||
|
||||
Integration with SpamAssassin has two components: the SpamAssassin server,
|
||||
called spamd, running as a daemonised service, and the SpamAssassin client
|
||||
spamc. SpamAssassin Milter uses spamc to communicate with the SpamAssassin
|
||||
server.
|
||||
SpamAssassin Milter must be integrated with two SpamAssassin components: the
|
||||
SpamAssassin server itself, called `spamd`, which does the actual work, and the
|
||||
SpamAssassin client `spamc`, which serves as an intermediary between the milter
|
||||
and the server.
|
||||
|
||||
### SpamAssassin configuration
|
||||
|
||||
The main SpamAssassin configuration file is /etc/spamassassin/local.conf.
|
||||
The main SpamAssassin configuration file is `/etc/spamassassin/local.conf`. See
|
||||
`perldoc Mail::SpamAssassin::Conf` for detailed information.
|
||||
|
||||
By default, SpamAssassin creates ‘reports’ for messages it recognises as spam.
|
||||
These reports replace the message body, that is, the message body is rewritten
|
||||
to present a report instead of the original message, and the original message is
|
||||
relegated to a MIME attachment. SpamAssassin Milter by default applies reports.
|
||||
|
||||
If the reports behaviour is not desired, they may be disabled by setting the
|
||||
parameter `report_safe 0` in the SpamAssassin configuration.
|
||||
Reports are controlled with the `report_safe` configuration parameter. Disable
|
||||
reports as follows:
|
||||
|
||||
TODO describe reports and disabling reports with report_safe 0
|
||||
```
|
||||
report_safe 0
|
||||
```
|
||||
|
||||
### spamc configuration
|
||||
In addition, body rewriting can also be suppressed on the SpamAssassin Milter
|
||||
side with the `--preserve-body` flag.
|
||||
|
||||
To integrate with spamc, it is recommended to set spamc configuration options in
|
||||
the spamc configuration file /etc/spamassassin/spamc.conf.
|
||||
SpamAssassin may also rewrite the Subject and other headers, for example, adding
|
||||
a prefix ‘\*\*\*Spam\*\*\*’. This is not done by default, but may be enabled
|
||||
with a setting like the following:
|
||||
|
||||
For example, the following is the content of a minimal spamc.conf file:
|
||||
```
|
||||
rewrite_header Subject ***SPAM***
|
||||
```
|
||||
|
||||
SpamAssassin Milter by default applies header rewriting. Header rewriting can be
|
||||
suppressed on the SpamAssassin Milter side with the `--preserve-headers` flag.
|
||||
|
||||
### `spamc` configuration
|
||||
|
||||
`spamc` can be configured by passing it command-line options, or preferably, by
|
||||
setting the command-line options in the configuration file
|
||||
`/etc/spamassassin/spamc.conf`.
|
||||
|
||||
By default, `spamc` will try to reach SpamAssassin server on the dedicated port
|
||||
783, so that a stock installation of SpamAssassin should work with SpamAssassin
|
||||
Milter without further configuration.
|
||||
|
||||
If SpamAssassin server listens on a different port or on a UNIX domain socket
|
||||
instead, set the `--socket` option as appropriate in `spamc.conf`:
|
||||
|
||||
```
|
||||
--socket=/run/spamassassin/spamd.sock
|
||||
```
|
||||
|
||||
This configuration instructs spamc to connect to spamd on a UNIX domain socket
|
||||
at /run/spamassassin/spamd.sock.
|
||||
|
||||
When reports are disabled, the following is a reasonable configuration:
|
||||
When reports are disabled, it is recommended to use the `--headers` option.
|
||||
|
||||
```
|
||||
--socket=/run/spamassassin/spamd.sock
|
||||
--headers
|
||||
```
|
||||
|
||||
The `--headers` option is just a shortcut that tells spamd not to transmit the
|
||||
body back to spamc. This option obviously only makes sense and should only be
|
||||
used when SpamAssassin reports are disabled.
|
||||
This option is just a shortcut that causes `spamd` not to transmit the message
|
||||
body back to `spamc`. (This option obviously only makes sense and should only be
|
||||
used when SpamAssassin reports are disabled.)
|
||||
|
||||
Finally, a pitfall of `spamc` deserves highlighting: `spamc` by default tries to
|
||||
resist failure to an extent that it will not indicate failure even if it cannot
|
||||
connect to SpamAssassin server at all (apart from warnings logged to syslog)! If
|
||||
it cannot connect to the server, it simply echoes what it received, and so masks
|
||||
the error condition. This mechanism is labelled ‘safe fallback’ and should be
|
||||
disabled once the system is set up and working well. Set the following flag:
|
||||
|
||||
```
|
||||
--no-safe-fallback
|
||||
```
|
||||
|
||||
## Integration with Postfix
|
||||
|
||||
To integrate with Postfix, ensure the SpamAssassin Milter service is up and
|
||||
running, and then configure its listening socket in /etc/postfix/main.cf as
|
||||
running, and then configure its listening socket in `/etc/postfix/main.cf` as
|
||||
follows:
|
||||
|
||||
```
|
||||
smtpd_milters = inet:localhost:3001
|
||||
smtpd_milters = inet:localhost:3000
|
||||
non_smtpd_milters = $smtpd_milters
|
||||
```
|
||||
|
||||
|
@ -126,7 +198,33 @@ After reloading the Postfix configuration, mail will be processed by
|
|||
SpamAssassin Milter.
|
||||
|
||||
By default, SpamAssassin Milter will accept, that is, won’t check messages
|
||||
coming from connections from localhost, and messages from authenticated senders.
|
||||
coming from local connections (for example, mail sent locally from the
|
||||
command-line), and messages from authenticated senders (for example, mail
|
||||
submitted via a SASL-authenticated channel).
|
||||
|
||||
## Integration with mail delivery
|
||||
|
||||
A further component that can be useful with SpamAssassin Milter is a
|
||||
[Sieve]-capable mail delivery agent. A Sieve script can for example look at the
|
||||
`X-Spam-` SpamAssassin headers of the incoming message, and take action based on
|
||||
those.
|
||||
|
||||
As an example, in case [Dovecot] does mail delivery with [LMTP], enable the
|
||||
Sieve plugin for the LMTP protocol, then setup a global Sieve script that files
|
||||
messages flagged as spam into the ‘Junk’ folder:
|
||||
|
||||
```
|
||||
require "fileinto";
|
||||
|
||||
if header :contains "X-Spam-Flag" "YES" {
|
||||
fileinto "Junk";
|
||||
stop;
|
||||
}
|
||||
```
|
||||
|
||||
[Dovecot]: https://dovecot.org
|
||||
[LMTP]: https://doc.dovecot.org/configuration_manual/protocols/lmtp_server/
|
||||
[Sieve]: http://sieve.info
|
||||
|
||||
## Licence
|
||||
|
||||
|
|
|
@ -92,13 +92,13 @@ default, 512000.
|
|||
.TP
|
||||
.BR \-B ", " \-\-preserve-body
|
||||
Suppress rewriting of spam message body.
|
||||
If this option is not used, the message body of messages flagged as spam (as
|
||||
well as the values of related headers
|
||||
If this option is not used, the message body of messages flagged as spam is
|
||||
replaced with the body received from SpamAssassin (as are the values of related
|
||||
headers
|
||||
.B MIME-Version
|
||||
and
|
||||
.BR Content-Type ,
|
||||
if necessary)
|
||||
is replaced with the body received from SpamAssassin.
|
||||
if necessary).
|
||||
.TP
|
||||
.BR \-H ", " \-\-preserve-headers
|
||||
Suppress rewriting of headers
|
||||
|
@ -107,8 +107,8 @@ Suppress rewriting of headers
|
|||
and
|
||||
.B To
|
||||
of spam messages.
|
||||
If this option is not used, these headers of messages flagged as spam will be
|
||||
replaced with the values received from SpamAssassin, if necessary.
|
||||
If this option is not used, these headers of messages flagged as spam will have
|
||||
their values replaced with the values received from SpamAssassin, if necessary.
|
||||
.TP
|
||||
.BR \-r ", " \-\-reject-spam
|
||||
Reject messages flagged as spam at the SMTP level.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Description=SpamAssassin Milter
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/sbin/spamassassin-milter inet:3001@localhost
|
||||
ExecStart=/usr/sbin/spamassassin-milter inet:3000@localhost
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
@ -124,7 +124,7 @@ fn handle_data(mut ctx: Context<Connection>) -> milter::Result<Status> {
|
|||
let id = queue_id(&ctx.api)?;
|
||||
|
||||
if let Err(e) = client.connect() {
|
||||
eprintln!("{}: cannot connect to spamc: {}", id, e);
|
||||
eprintln!("{}: failed to start spamc: {}", id, e);
|
||||
return Ok(Status::Tempfail);
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,8 @@ impl Spamc {
|
|||
|
||||
impl Process for Spamc {
|
||||
fn connect(&mut self) -> Result<()> {
|
||||
// `Command::spawn` always succeeds when spamc can be invoked, even if
|
||||
// logically the command is invalid, eg if it uses non-existing options.
|
||||
let child = Command::new(Spamc::SPAMC_PROGRAM)
|
||||
.args(self.spamc_args)
|
||||
.stdin(Stdio::piped())
|
||||
|
@ -52,12 +54,13 @@ impl Process for Spamc {
|
|||
}
|
||||
|
||||
fn finish(&mut self) -> Result<Vec<u8>> {
|
||||
let output = self.spamc.take().expect("spamc process not started").wait_with_output()?;
|
||||
let spamc = self.spamc.take().expect("spamc process not started");
|
||||
let output = spamc.wait_with_output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(output.stdout)
|
||||
} else {
|
||||
Err(Error::IoError)
|
||||
Err(Error::IoError(String::from("spamc returned error exit code")))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +71,7 @@ impl Process for Spamc {
|
|||
|
||||
impl Drop for Spamc {
|
||||
fn drop(&mut self) {
|
||||
// Kill the child: a child process will continue to run even after its
|
||||
// Kill spamc: a child process will continue to run even after its
|
||||
// `Child` handle has gone out of scope.
|
||||
if let Some(spamc) = self.spamc.as_mut() {
|
||||
let _ = spamc.kill();
|
||||
|
@ -80,7 +83,7 @@ pub struct Client {
|
|||
process: Box<dyn Process>,
|
||||
sender: String,
|
||||
recipients: Vec<String>,
|
||||
headers: HeaderMap<'static>, // 'static keys in canonical form
|
||||
headers: HeaderMap<'static>, // `'static` keys in canonical form
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
|
@ -99,28 +102,34 @@ impl Client {
|
|||
self.bytes
|
||||
}
|
||||
|
||||
pub fn add_recipient(&mut self, r: String) {
|
||||
self.recipients.push(r);
|
||||
pub fn add_recipient(&mut self, rcpt: String) {
|
||||
self.recipients.push(rcpt);
|
||||
}
|
||||
|
||||
pub fn connect(&mut self) -> Result<()> {
|
||||
self.process.connect()
|
||||
}
|
||||
|
||||
// Implementation note: the send operations can all fail with an IO error
|
||||
// and so return a Result that is then simply unwrapped with `?` in the
|
||||
// `callbacks` module. That is, in normal circumstances we don’t expect this
|
||||
// to occur, only in abnormal circumstances such as when wrong flags where
|
||||
// passed to spamc!
|
||||
|
||||
pub fn send_envelope_sender(&mut self) -> Result<()> {
|
||||
let buf = format!("X-Envelope-From: {}\r\n", self.sender);
|
||||
|
||||
let writer = self.process.writer();
|
||||
self.bytes += self.process.writer().write(buf.as_bytes())?;
|
||||
|
||||
Ok(self.bytes += writer.write(buf.as_bytes())?)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_envelope_recipients(&mut self) -> Result<()> {
|
||||
let buf = format!("X-Envelope-To: {}\r\n", self.recipients.join(",\r\n\t"));
|
||||
|
||||
let writer = self.process.writer();
|
||||
self.bytes += self.process.writer().write(buf.as_bytes())?;
|
||||
|
||||
Ok(self.bytes += writer.write(buf.as_bytes())?)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_forged_received_header(
|
||||
|
@ -133,7 +142,6 @@ impl Client {
|
|||
queue_id: &str,
|
||||
date_time: &str,
|
||||
) -> Result<()> {
|
||||
let protocol = if tls { "ESMTPS" } else { "ESMTP" };
|
||||
let buf = format!(
|
||||
"Received: from {} ({})\r\n\
|
||||
\tby {} ({}) with {} id {};\r\n\
|
||||
|
@ -143,15 +151,15 @@ impl Client {
|
|||
client_name_addr,
|
||||
my_hostname,
|
||||
mta,
|
||||
protocol,
|
||||
if tls { "ESMTPS" } else { "ESMTP" },
|
||||
queue_id,
|
||||
date_time,
|
||||
self.sender
|
||||
);
|
||||
|
||||
let writer = self.process.writer();
|
||||
self.bytes += self.process.writer().write(buf.as_bytes())?;
|
||||
|
||||
Ok(self.bytes += writer.write(buf.as_bytes())?)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_header(&mut self, name: &str, value: &str) -> Result<()> {
|
||||
|
@ -165,7 +173,7 @@ impl Client {
|
|||
}
|
||||
|
||||
let value = email::ensure_crlf(value);
|
||||
let buf = format!("{}: {}\r\n", name, &value);
|
||||
let buf = format!("{}: {}\r\n", name, value);
|
||||
|
||||
if let Some(canonical_name) = email::REWRITE_HEADERS.get(name)
|
||||
.or_else(|| email::REPORT_HEADERS.get(name))
|
||||
|
@ -173,21 +181,21 @@ impl Client {
|
|||
self.headers.insert_if_absent(canonical_name, value);
|
||||
}
|
||||
|
||||
let writer = self.process.writer();
|
||||
self.bytes += self.process.writer().write(buf.as_bytes())?;
|
||||
|
||||
Ok(self.bytes += writer.write(buf.as_bytes())?)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_eoh(&mut self) -> Result<()> {
|
||||
let writer = self.process.writer();
|
||||
self.bytes += self.process.writer().write(b"\r\n")?;
|
||||
|
||||
Ok(self.bytes += writer.write(b"\r\n")?)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_body_chunk(&mut self, bytes: &[u8]) -> Result<()> {
|
||||
let writer = self.process.writer();
|
||||
self.bytes += self.process.writer().write(bytes)?;
|
||||
|
||||
Ok(self.bytes += writer.write(bytes)?)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process(
|
||||
|
@ -199,7 +207,7 @@ impl Client {
|
|||
let output = match self.process.finish() {
|
||||
Ok(output) => output,
|
||||
Err(e) => {
|
||||
eprintln!("{}: unable to wait for spamc: {}", id, e);
|
||||
eprintln!("{}: failed to wait for spamc to exit: {}", id, e);
|
||||
return Ok(Status::Tempfail);
|
||||
}
|
||||
};
|
||||
|
@ -207,7 +215,7 @@ impl Client {
|
|||
let email = match Email::parse(&output) {
|
||||
Ok(email) => email,
|
||||
Err(e) => {
|
||||
eprintln!("{}: unable to parse response from spamc: {}", id, e);
|
||||
eprintln!("{}: {}", id, e);
|
||||
return Ok(Status::Tempfail);
|
||||
}
|
||||
};
|
||||
|
@ -245,8 +253,9 @@ fn reject_spam(id: &str, actions: &impl SetErrorReply, config: &Config) -> milte
|
|||
verbose!(config, "{}: rejected message flagged as spam [dry-run, not done]", id);
|
||||
Status::Accept
|
||||
} else {
|
||||
// TODO Message text needed? For example "Rejected spam."?
|
||||
actions.set_error_reply("550", Some("5.7.1"), vec![])?;
|
||||
// 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"])?;
|
||||
|
||||
verbose!(config, "{}: rejected message flagged as spam", id);
|
||||
Status::Reject
|
||||
|
@ -410,7 +419,7 @@ mod tests {
|
|||
fn client_process_invalid_response() {
|
||||
let spamc = MockSpamc::with_output(b"invalid message response".to_vec());
|
||||
let actions = MockActionContext::new();
|
||||
let config = Config::builder().build();
|
||||
let config = Default::default();
|
||||
|
||||
let client = Client::new(spamc, String::from("sender"));
|
||||
let status = client.process("id", &actions, &config).unwrap();
|
||||
|
@ -432,7 +441,11 @@ mod tests {
|
|||
assert_eq!(status, Status::Reject);
|
||||
assert_eq!(
|
||||
actions.called.borrow().first(),
|
||||
Some(&Action::SetErrorReply("550".into(), Some("5.7.1".into()), vec![]))
|
||||
Some(&Action::SetErrorReply(
|
||||
"550".into(),
|
||||
Some("5.7.1".into()),
|
||||
vec!["Spam message refused".into()]
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -442,7 +455,7 @@ mod tests {
|
|||
b"X-Spam-Flag: YES\r\nX-Spam-Level: *****\r\n\r\nReport".to_vec(),
|
||||
);
|
||||
let actions = MockActionContext::new();
|
||||
let config = Config::builder().build();
|
||||
let config = Default::default();
|
||||
|
||||
let mut client = Client::new(spamc, String::from("sender"));
|
||||
client.send_header("x-spam-level", "*").unwrap();
|
||||
|
|
|
@ -2,10 +2,8 @@ use ipnet::IpNet;
|
|||
use once_cell::sync::OnceCell;
|
||||
use std::{collections::HashSet, net::IpAddr};
|
||||
|
||||
/// A builder for [`Config`] objects.
|
||||
///
|
||||
/// [`Config`]: struct.Config.html
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// A builder for SpamAssassin Milter configuration objects.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConfigBuilder {
|
||||
has_trusted_networks: bool,
|
||||
trusted_networks: HashSet<IpNet>,
|
||||
|
@ -87,8 +85,25 @@ impl ConfigBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for ConfigBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
has_trusted_networks: Default::default(),
|
||||
trusted_networks: Default::default(),
|
||||
auth_untrusted: Default::default(),
|
||||
spamc_args: Default::default(),
|
||||
max_message_size: 512_000,
|
||||
dry_run: Default::default(),
|
||||
reject_spam: Default::default(),
|
||||
preserve_headers: Default::default(),
|
||||
preserve_body: Default::default(),
|
||||
verbose: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A configuration object for SpamAssassin Milter.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
has_trusted_networks: bool,
|
||||
trusted_networks: HashSet<IpNet>,
|
||||
|
@ -103,14 +118,8 @@ pub struct Config {
|
|||
}
|
||||
|
||||
impl Config {
|
||||
/// Returns a new `ConfigBuilder` with default settings.
|
||||
///
|
||||
/// [`ConfigBuilder`]: struct.ConfigBuilder.html
|
||||
pub fn builder() -> ConfigBuilder {
|
||||
ConfigBuilder {
|
||||
max_message_size: 512_000,
|
||||
..Default::default()
|
||||
}
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn has_trusted_networks(&self) -> bool {
|
||||
|
@ -154,6 +163,12 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
ConfigBuilder::default().build()
|
||||
}
|
||||
}
|
||||
|
||||
static CONFIG: OnceCell<Config> = OnceCell::new();
|
||||
|
||||
pub fn init(config: Config) {
|
||||
|
|
24
src/email.rs
24
src/email.rs
|
@ -154,10 +154,10 @@ impl<'a> HeaderRewriter<'a> {
|
|||
pub fn new(original: HeaderMap<'a>, config: &'a Config) -> Self {
|
||||
Self {
|
||||
original,
|
||||
processed: Default::default(),
|
||||
spam_assassin_mods: Default::default(),
|
||||
rewrite_mods: Default::default(),
|
||||
report_mods: Default::default(),
|
||||
processed: HeaderSet::new(),
|
||||
spam_assassin_mods: vec![],
|
||||
rewrite_mods: vec![],
|
||||
report_mods: vec![],
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
@ -340,7 +340,10 @@ mod tests {
|
|||
assert_eq!(header_lines(b"x\r\n\t"), vec![b"x\r\n\t" as &[_]]);
|
||||
assert_eq!(header_lines(b"x\r\n\ty"), vec![b"x\r\n\ty" as &[_]]);
|
||||
assert_eq!(header_lines(b"x\r\n\ty\r\n"), vec![b"x\r\n\ty" as &[_]]);
|
||||
assert_eq!(header_lines(b"x\r\n\ty\r\n\tz\r\nq"), vec![b"x\r\n\ty\r\n\tz" as &[_], b"q" as &[_]]);
|
||||
assert_eq!(
|
||||
header_lines(b"x\r\n\ty\r\n\tz\r\nq"),
|
||||
vec![b"x\r\n\ty\r\n\tz" as &[_], b"q" as &[_]]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -350,7 +353,10 @@ mod tests {
|
|||
assert_eq!(parse_header_line(b"\t : whitespace name"), Err(Error::ParseEmail));
|
||||
assert_eq!(parse_header_line(b"name:value"), Ok(Header { name: "name", value: "value" }));
|
||||
assert_eq!(parse_header_line(b"name: value"), Ok(Header { name: "name", value: "value" }));
|
||||
assert_eq!(parse_header_line(b"name:\r\n\tvalue"), Ok(Header { name: "name", value: "\r\n\tvalue" }));
|
||||
assert_eq!(
|
||||
parse_header_line(b"name:\r\n\tvalue"),
|
||||
Ok(Header { name: "name", value: "\r\n\tvalue" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -388,7 +394,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn header_rewriter_flags_spam() {
|
||||
let config = Config::builder().build();
|
||||
let config = Default::default();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-spam-flag", String::from("no"));
|
||||
|
||||
|
@ -400,7 +406,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn header_rewriter_processes_first_occurrence_only() {
|
||||
let config = Config::builder().build();
|
||||
let config = Default::default();
|
||||
let headers = HeaderMap::new();
|
||||
|
||||
let mut rewriter = HeaderRewriter::new(headers, &config);
|
||||
|
@ -420,7 +426,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn header_rewriter_replaces_different_values() {
|
||||
let config = Config::builder().build();
|
||||
let config = Default::default();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-spam-level", String::from("***"));
|
||||
headers.insert("x-spam-report", String::from("original"));
|
||||
|
|
22
src/error.rs
22
src/error.rs
|
@ -6,40 +6,34 @@ use std::{
|
|||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum Error {
|
||||
ParseEmail,
|
||||
IoError,
|
||||
// Milter(MilterError),
|
||||
// For our purposes it is enough to record just the error message of I/O
|
||||
// errors, no need to keep the full `io::Error` around.
|
||||
IoError(String),
|
||||
}
|
||||
|
||||
// #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
// pub enum MilterError {
|
||||
// Main,
|
||||
// Socket,
|
||||
// }
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
use Error::*;
|
||||
|
||||
match self {
|
||||
ParseEmail => write!(f, "could not parse email response from spamc"),
|
||||
_ => write!(f, "TODO"),
|
||||
ParseEmail => write!(f, "failed to parse email response"),
|
||||
IoError(msg) => msg.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
// TODO
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(_: io::Error) -> Self {
|
||||
Error::IoError
|
||||
fn from(error: io::Error) -> Self {
|
||||
Error::IoError(format!("{}", error)) // just record the error message
|
||||
}
|
||||
}
|
||||
|
||||
|
|
26
src/lib.rs
26
src/lib.rs
|
@ -42,13 +42,25 @@ pub const VERSION: &str = "0.0.2";
|
|||
///
|
||||
/// If execution of the milter fails, an error variant of type `milter::Error`
|
||||
/// is returned.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use spamassassin_milter::Config;
|
||||
/// use std::process;
|
||||
///
|
||||
/// let socket = "inet:3000@localhost";
|
||||
/// let config = Config::builder().build();
|
||||
///
|
||||
/// if let Err(e) = spamassassin_milter::run(socket, config) {
|
||||
/// eprintln!("failed to run spamassassin-milter: {}", e);
|
||||
/// process::exit(1);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn run(socket: &str, config: Config) -> milter::Result<()> {
|
||||
config::init(config);
|
||||
|
||||
eprintln!("{} {} starting", MILTER_NAME, VERSION);
|
||||
|
||||
// TODO should return our Result type?
|
||||
let result = Milter::new(socket)
|
||||
Milter::new(socket)
|
||||
.name(MILTER_NAME)
|
||||
.on_negotiate(negotiate_callback)
|
||||
.on_connect(connect_callback)
|
||||
|
@ -62,9 +74,5 @@ pub fn run(socket: &str, config: Config) -> milter::Result<()> {
|
|||
.on_eom(eom_callback)
|
||||
.on_abort(abort_callback)
|
||||
.on_close(close_callback)
|
||||
.run();
|
||||
|
||||
eprintln!("{} {} shut down", MILTER_NAME, VERSION);
|
||||
|
||||
result
|
||||
.run()
|
||||
}
|
||||
|
|
26
src/main.rs
26
src/main.rs
|
@ -14,8 +14,10 @@ const ARG_SOCKET: &str = "SOCKET";
|
|||
const ARG_SPAMC_ARGS: &str = "SPAMC_ARGS";
|
||||
|
||||
fn main() {
|
||||
let matches = App::new(spamassassin_milter::MILTER_NAME)
|
||||
.version(spamassassin_milter::VERSION)
|
||||
use spamassassin_milter::{MILTER_NAME, VERSION};
|
||||
|
||||
let matches = App::new(MILTER_NAME)
|
||||
.version(VERSION)
|
||||
.arg(Arg::with_name(ARG_AUTH_UNTRUSTED)
|
||||
.short("a")
|
||||
.long("auth-untrusted")
|
||||
|
@ -40,6 +42,7 @@ fn main() {
|
|||
.arg(Arg::with_name(ARG_REJECT_SPAM)
|
||||
.short("r")
|
||||
.long("reject-spam")
|
||||
.conflicts_with_all(&[ARG_PRESERVE_BODY, ARG_PRESERVE_HEADERS])
|
||||
.help("Reject messages flagged as spam"))
|
||||
.arg(Arg::with_name(ARG_TRUSTED_NETWORKS)
|
||||
.short("t")
|
||||
|
@ -52,8 +55,8 @@ fn main() {
|
|||
.long("verbose")
|
||||
.help("Enable verbose operation logging"))
|
||||
.arg(Arg::with_name(ARG_SOCKET)
|
||||
.help("Listening socket of the milter")
|
||||
.required(true))
|
||||
.required(true)
|
||||
.help("Listening socket of the milter"))
|
||||
.arg(Arg::with_name(ARG_SPAMC_ARGS)
|
||||
.last(true)
|
||||
.multiple(true)
|
||||
|
@ -69,9 +72,16 @@ fn main() {
|
|||
}
|
||||
};
|
||||
|
||||
if let Err(e) = spamassassin_milter::run(socket, config) {
|
||||
eprintln!("error: {}", e);
|
||||
process::exit(1);
|
||||
eprintln!("{} {} starting", MILTER_NAME, VERSION);
|
||||
|
||||
match spamassassin_milter::run(socket, config) {
|
||||
Ok(_) => {
|
||||
eprintln!("{} {} shut down", MILTER_NAME, VERSION);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{} {} terminated with error: {}", MILTER_NAME, VERSION, e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,7 +134,7 @@ fn build_config(matches: &ArgMatches<'_>) -> Result<Config, String> {
|
|||
}
|
||||
|
||||
if let Some(s) = matches.values_of(ARG_SPAMC_ARGS) {
|
||||
config.set_spamc_args(s.map(|arg| arg.to_owned()).collect::<Vec<_>>());
|
||||
config.set_spamc_args(s.map(|arg| arg.to_owned()).collect());
|
||||
};
|
||||
|
||||
Ok(config.build())
|
||||
|
|
23
tests/authenticated_sender.lua
Normal file
23
tests/authenticated_sender.lua
Normal file
|
@ -0,0 +1,23 @@
|
|||
-- An authenticated sender is accepted, the message is not processed.
|
||||
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.helo(conn, "mail.gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
-- `{auth_authen}` holds the SASL login name, if any.
|
||||
local err = mt.macro(conn, SMFIC_MAIL, "{auth_authen}", "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
|
||||
local err = mt.mailfrom(conn, "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_ACCEPT)
|
||||
|
||||
local err = mt.disconnect(conn)
|
||||
assert(err == nil, err)
|
16
tests/authenticated_sender.rs
Normal file
16
tests/authenticated_sender.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
mod common;
|
||||
|
||||
pub use common::*; // `pub` only to silence unused code warnings
|
||||
use spamassassin_milter::*;
|
||||
|
||||
#[test]
|
||||
fn authenticated_sender() {
|
||||
let config = Config::builder().build();
|
||||
|
||||
let miltertest = spawn_miltertest_runner(file!());
|
||||
|
||||
run("inet:3333@localhost", config).expect("milter execution failed");
|
||||
|
||||
let exit_code = miltertest.join().expect("panic in miltertest runner");
|
||||
assert!(exit_code.success(), "miltertest returned error exit code");
|
||||
}
|
|
@ -8,29 +8,78 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
const MILTERTEST: &str = "miltertest";
|
||||
pub const SPAMD_PORT: u16 = 3783; // mock port
|
||||
|
||||
pub fn spawn_echo_server(port: u16) -> JoinHandle<()> {
|
||||
pub type HamOrSpam = Result<String, String>;
|
||||
|
||||
/// A mock `spamd` server that echoes what it is sent, after applying
|
||||
/// transformation `f` to the message content and mock-classifying it as ham or
|
||||
/// spam.
|
||||
pub fn spawn_mock_spamd_server<F>(port: u16, f: F) -> JoinHandle<()>
|
||||
where
|
||||
F: Fn(String) -> HamOrSpam + Send + 'static,
|
||||
{
|
||||
let socket_addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), port);
|
||||
|
||||
thread::spawn(move || {
|
||||
let listener = TcpListener::bind(socket_addr).unwrap();
|
||||
|
||||
// TODO do not run for ever?
|
||||
for stream in listener.incoming() {
|
||||
let mut s = stream.unwrap();
|
||||
|
||||
let mut v = Vec::new();
|
||||
|
||||
s.read_to_end(&mut v).unwrap();
|
||||
|
||||
s.write_all(&v).unwrap();
|
||||
|
||||
s.shutdown(Shutdown::Write).unwrap();
|
||||
// This server handles only a single connection, so that we can `join`
|
||||
// this thread in the tests and detect panics.
|
||||
// TODO What if the connection never comes? Timeout?
|
||||
match listener.accept() {
|
||||
Ok((mut stream, _)) => {
|
||||
let mut buf = Vec::new();
|
||||
stream.read_to_end(&mut buf).unwrap();
|
||||
stream.write_all(process_message(buf, &f).as_bytes()).unwrap();
|
||||
stream.shutdown(Shutdown::Write).unwrap();
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("mock spamd server could not open connection: {}", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The client/server protocol is here ‘reverse-engineered’ in a very rudimentary
|
||||
// fashion, but hopefully sufficient for testing purposes. Both client and
|
||||
// server send a protocol header containing a content length indication and
|
||||
// terminated with "\r\n\r\n". The payload is the email message (with CRLF line
|
||||
// endings).
|
||||
|
||||
const SPAMD_PROTOCOL_OK: &str = "SPAMD/1.1 0 EX_OK";
|
||||
|
||||
fn process_message<F>(buf: Vec<u8>, f: &F) -> String
|
||||
where
|
||||
F: Fn(String) -> HamOrSpam,
|
||||
{
|
||||
let mut s = String::from_utf8(buf).unwrap();
|
||||
|
||||
// Crude handling of the spamc client protocol: strip off everything before
|
||||
// and including the first "\r\n\r\n".
|
||||
let i = s.find("\r\n\r\n").expect("spamc protocol header missing");
|
||||
s.replace_range(..i + 4, "");
|
||||
|
||||
match f(s) {
|
||||
// Again very basic handling of the spamd server protocol: add a forged
|
||||
// protocol header terminated with "\r\n\r\n".
|
||||
Ok(ham) => format!(
|
||||
"{}\r\nContent-length: {}\r\nSpam: False ; 4.0 / 5.0\r\n\r\n{}",
|
||||
SPAMD_PROTOCOL_OK,
|
||||
ham.len(),
|
||||
ham
|
||||
),
|
||||
Err(spam) => format!(
|
||||
"{}\r\nContent-length: {}\r\nSpam: True ; 6.0 / 5.0\r\n\r\n{}",
|
||||
SPAMD_PROTOCOL_OK,
|
||||
spam.len(),
|
||||
spam
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const MILTERTEST: &str = "miltertest";
|
||||
|
||||
pub fn spawn_miltertest_runner(test_file_name: &str) -> JoinHandle<ExitStatus> {
|
||||
let _timeout_thread = thread::spawn(|| {
|
||||
thread::sleep(Duration::from_secs(15));
|
||||
|
|
|
@ -1,31 +1,29 @@
|
|||
conn = mt.connect("inet:3783@localhost")
|
||||
-- ‘Happy path’ processing of an ordinary ham (not spam) message.
|
||||
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, "client.example.com", "123.123.123.123")
|
||||
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.helo(conn, "mail.example.com")
|
||||
local err = mt.helo(conn, "mail.gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
-- local err = mt.macro(conn, SMFIC_MAIL, "{auth_authen}", "my-auth-authen")
|
||||
-- assert(err == nil, err)
|
||||
|
||||
local err = mt.mailfrom(conn, "from@example.com")
|
||||
local err = mt.mailfrom(conn, "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.rcptto(conn, "to@example.com")
|
||||
local err = mt.rcptto(conn, "to@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
-- SMFIC_DATA not exported by miltertest:
|
||||
SMFIC_DATA = string.byte("T")
|
||||
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
|
||||
local err = mt.macro(conn, SMFIC_DATA,
|
||||
"i", "B45003F07A",
|
||||
"i", "1234567ABC",
|
||||
"j", "localhost",
|
||||
"_", "client.example.com [123.123.123.123]",
|
||||
"_", "client.gluet.ch [123.123.123.123]",
|
||||
"{tls_version}", "TLSv1.2",
|
||||
"v", "Postfix 3.3.0")
|
||||
assert(err == nil, err)
|
||||
|
@ -34,24 +32,19 @@ local err = mt.data(conn)
|
|||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.header(conn, "From", "from@example.com")
|
||||
local err = mt.header(conn, "From", "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.header(conn, "To", "to@example.com")
|
||||
local err = mt.header(conn, "To", "to@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.header(conn, "Subject", "Test message")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.header(conn, "Message-Id", "<1234567890@example.com>")
|
||||
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
-- TODO format current date?
|
||||
local err = mt.header(conn, "Date", "Tue, 21 Jan 2020 20:19:22 +0100")
|
||||
local err = mt.header(conn, "Date", os.date("%a, %d %b %Y %H:%M:%S %Z"))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
mod common;
|
||||
|
||||
pub use common::*; // `pub` only to silence unused code warnings
|
||||
use spamassassin_milter::*;
|
||||
|
||||
#[test]
|
||||
fn ham_flow() {
|
||||
let port = 4444;
|
||||
|
||||
let mut builder = Config::builder();
|
||||
builder.set_spamc_args(vec![format!("--port={}", port)]);
|
||||
builder.set_spamc_args(vec![format!("--port={}", SPAMD_PORT)]);
|
||||
let config = builder.build();
|
||||
|
||||
let _server = common::spawn_echo_server(port);
|
||||
let miltertest = common::spawn_miltertest_runner(file!());
|
||||
let server = spawn_mock_spamd_server(SPAMD_PORT, Ok);
|
||||
let miltertest = spawn_miltertest_runner(file!());
|
||||
|
||||
run("inet:3783@localhost", config).expect("spamassassin-milter failed");
|
||||
run("inet:3333@localhost", config).expect("milter execution failed");
|
||||
|
||||
let exit_code = miltertest.join().expect("panic in miltertest runner");
|
||||
assert!(exit_code.success(), "miltertest returned error exit code");
|
||||
|
||||
server.join().expect("panic in mock spamd server");
|
||||
}
|
||||
|
|
66
tests/live.lua
Normal file
66
tests/live.lua
Normal file
|
@ -0,0 +1,66 @@
|
|||
-- Live test against an actual SpamAssassin server.
|
||||
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.helo(conn, "mail.gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.mailfrom(conn, "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.rcptto(conn, "to@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
|
||||
local err = mt.macro(conn, SMFIC_DATA,
|
||||
"i", "1234567ABC",
|
||||
"j", "localhost",
|
||||
"_", "client.gluet.ch [123.123.123.123]",
|
||||
"{tls_version}", "TLSv1.2",
|
||||
"v", "Postfix 3.3.0")
|
||||
assert(err == nil, err)
|
||||
|
||||
local err = mt.data(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.header(conn, "From", "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "To", "to@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Subject", "Test message")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Date", os.date("%a, %d %b %Y %H:%M:%S %Z"))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.eoh(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
-- This is the magic GTUBE value, which makes this message certain spam.
|
||||
-- See https://spamassassin.apache.org/gtube/.
|
||||
local err = mt.bodystring(conn, "XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.eom(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_ACCEPT)
|
||||
|
||||
local err = mt.disconnect(conn)
|
||||
assert(err == nil, err)
|
22
tests/live.rs
Normal file
22
tests/live.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
mod common;
|
||||
|
||||
pub use common::*; // `pub` only to silence unused code warnings
|
||||
use spamassassin_milter::*;
|
||||
|
||||
/// Runs a ‘live’ test against a real SpamAssassin server instance. This test is
|
||||
/// run on demand, as SpamAssassin will actually analyse the input, and do DNS
|
||||
/// queries etc.
|
||||
#[test]
|
||||
#[ignore] // use option `--include-ignored` to run
|
||||
fn live() {
|
||||
// Without `spamc_args` set, `spamc` will try to connect to the default
|
||||
// `spamd` port 783 (see also /etc/services).
|
||||
let config = Config::builder().build();
|
||||
|
||||
let miltertest = spawn_miltertest_runner(file!());
|
||||
|
||||
run("inet:3333@localhost", config).expect("milter execution failed");
|
||||
|
||||
let exit_code = miltertest.join().expect("panic in miltertest runner");
|
||||
assert!(exit_code.success(), "miltertest returned error exit code");
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
-- Connection from loopback IP address.
|
||||
-- 1) A connection from the loopback IP address is accepted.
|
||||
|
||||
conn = mt.connect("inet:3030@localhost")
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, nil, "127.0.0.1")
|
||||
|
@ -10,9 +10,10 @@ assert(mt.getreply(conn) == SMFIR_ACCEPT)
|
|||
local err = mt.disconnect(conn)
|
||||
assert(err == nil, err)
|
||||
|
||||
-- Connection from ‘unknown’ IP address (for example, UNIX domain socket).
|
||||
-- 2) A connection from an ‘unknown’ IP address (for example, from a UNIX
|
||||
-- domain socket) is also accepted.
|
||||
|
||||
conn = mt.connect("inet:3030@localhost")
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, nil, "unspec")
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
mod common;
|
||||
|
||||
pub use common::*; // `pub` only to silence unused code warnings
|
||||
use spamassassin_milter::*;
|
||||
|
||||
#[test]
|
||||
fn loopback_connection() {
|
||||
let config = Config::builder()
|
||||
// TODO spamc_args for echo server
|
||||
.build();
|
||||
let config = Config::builder().build();
|
||||
|
||||
let _server = common::spawn_echo_server(4444);
|
||||
let miltertest = common::spawn_miltertest_runner(file!());
|
||||
let miltertest = spawn_miltertest_runner(file!());
|
||||
|
||||
run("inet:3030@localhost", config).expect("spamassassin-milter failed");
|
||||
run("inet:3333@localhost", config).expect("milter execution failed");
|
||||
|
||||
let exit_code = miltertest.join().expect("panic in miltertest runner");
|
||||
assert!(exit_code.success(), "miltertest returned error exit code");
|
||||
|
||||
// TODO detect panic in server?
|
||||
// server.join().expect("panic in server");
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
-- Connection from loopback IP address.
|
||||
|
||||
conn = mt.connect("inet:3030@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, nil, "127.0.0.1")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE) -- connection not accepted
|
||||
|
||||
local err = mt.disconnect(conn)
|
||||
assert(err == nil, err)
|
||||
|
||||
-- Connection from ‘unknown’ IP address (for example, UNIX domain socket).
|
||||
|
||||
conn = mt.connect("inet:3030@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, nil, "unspec")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE) -- connection not accepted
|
||||
|
||||
local err = mt.disconnect(conn)
|
||||
assert(err == nil, err)
|
|
@ -1,18 +0,0 @@
|
|||
mod common;
|
||||
|
||||
use spamassassin_milter::*;
|
||||
|
||||
#[test]
|
||||
fn loopback_connection_untrusted() {
|
||||
let mut builder = Config::builder();
|
||||
builder.set_has_trusted_networks(true); // empty trusted networks, none trusted
|
||||
let config = builder.build();
|
||||
|
||||
let _server = common::spawn_echo_server(4444);
|
||||
let miltertest = common::spawn_miltertest_runner(file!());
|
||||
|
||||
run("inet:3030@localhost", config).expect("spamassassin-milter failed");
|
||||
|
||||
let exit_code = miltertest.join().expect("panic in miltertest runner");
|
||||
assert!(exit_code.success(), "miltertest returned error exit code");
|
||||
}
|
79
tests/skip_large_message.lua
Normal file
79
tests/skip_large_message.lua
Normal file
|
@ -0,0 +1,79 @@
|
|||
-- Transfer message body chunks until more than some max message size bytes
|
||||
-- have been written, then skip the rest of the message (oversized message
|
||||
-- won’t be processed by SpamAssassin anyway, so it is futile to send the whole
|
||||
-- message).
|
||||
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.helo(conn, "mail.gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.mailfrom(conn, "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.rcptto(conn, "to@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
|
||||
local err = mt.macro(conn, SMFIC_DATA,
|
||||
"i", "1234567ABC",
|
||||
"j", "localhost",
|
||||
"_", "client.gluet.ch [123.123.123.123]",
|
||||
"{tls_version}", "TLSv1.2",
|
||||
"v", "Postfix 3.3.0")
|
||||
assert(err == nil, err)
|
||||
|
||||
local err = mt.data(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.header(conn, "From", "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "To", "to@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Subject", "Test message")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Date", os.date("%a, %d %b %Y %H:%M:%S %Z"))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.eoh(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
-- At this point still below the size limit …
|
||||
local err = mt.bodystring(conn, "Test message body")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
-- … after sending the following, we’re past the limit and skip.
|
||||
local err = mt.bodystring(conn, [[
|
||||
................................................................................
|
||||
................................................................................
|
||||
................................................................................
|
||||
................................................................................
|
||||
................................................................................
|
||||
................................................................................
|
||||
]])
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_SKIP)
|
||||
|
||||
local err = mt.eom(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_ACCEPT)
|
||||
|
||||
local err = mt.disconnect(conn)
|
||||
assert(err == nil, err)
|
22
tests/skip_large_message.rs
Normal file
22
tests/skip_large_message.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
mod common;
|
||||
|
||||
pub use common::*; // `pub` only to silence unused code warnings
|
||||
use spamassassin_milter::*;
|
||||
|
||||
#[test]
|
||||
fn skip_large_message() {
|
||||
let mut builder = Config::builder();
|
||||
builder.set_max_message_size(512);
|
||||
builder.set_spamc_args(vec![format!("--port={}", SPAMD_PORT)]);
|
||||
let config = builder.build();
|
||||
|
||||
let server = spawn_mock_spamd_server(SPAMD_PORT, Ok);
|
||||
let miltertest = spawn_miltertest_runner(file!());
|
||||
|
||||
run("inet:3333@localhost", config).expect("milter execution failed");
|
||||
|
||||
let exit_code = miltertest.join().expect("panic in miltertest runner");
|
||||
assert!(exit_code.success(), "miltertest returned error exit code");
|
||||
|
||||
server.join().expect("panic in mock spamd server");
|
||||
}
|
71
tests/spam_flow.lua
Normal file
71
tests/spam_flow.lua
Normal file
|
@ -0,0 +1,71 @@
|
|||
-- ‘Happy path’ processing of a spam message.
|
||||
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.helo(conn, "mail.gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.mailfrom(conn, "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.rcptto(conn, "to@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
|
||||
local err = mt.macro(conn, SMFIC_DATA,
|
||||
"i", "1234567ABC",
|
||||
"j", "localhost",
|
||||
"_", "client.gluet.ch [123.123.123.123]",
|
||||
"{tls_version}", "TLSv1.2",
|
||||
"v", "Postfix 3.3.0")
|
||||
assert(err == nil, err)
|
||||
|
||||
local err = mt.data(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.header(conn, "From", "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "To", "to@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Subject", "Test message")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Date", os.date("%a, %d %b %Y %H:%M:%S %Z"))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "X-Spam-Checker-Version", "BogusChecker 1.0.0")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.eoh(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.bodystring(conn, "You have just won a billion dollars!!!!")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.eom(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_ACCEPT)
|
||||
|
||||
assert(mt.eom_check(conn, MT_HDRCHANGE, "X-Spam-Checker-Version", "MyChecker 1.0.0"))
|
||||
assert(mt.eom_check(conn, MT_HDRADD, "X-Spam-Custom", "Custom-Value"))
|
||||
assert(mt.eom_check(conn, MT_BODYCHANGE))
|
||||
|
||||
local err = mt.disconnect(conn)
|
||||
assert(err == nil, err)
|
30
tests/spam_flow.rs
Normal file
30
tests/spam_flow.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
mod common;
|
||||
|
||||
pub use common::*; // `pub` only to silence unused code warnings
|
||||
use spamassassin_milter::*;
|
||||
|
||||
#[test]
|
||||
fn spam_flow() {
|
||||
let mut builder = Config::builder();
|
||||
builder.set_spamc_args(vec![format!("--port={}", SPAMD_PORT)]);
|
||||
let config = builder.build();
|
||||
|
||||
let server = spawn_mock_spamd_server(SPAMD_PORT, |s| {
|
||||
Err(
|
||||
s.replacen("Subject: Test message\r\n", "Subject: [SPAM] Test message\r\n", 1)
|
||||
.replacen("\r\n\r\n", "\r\n\
|
||||
X-Spam-Checker-Version: MyChecker 1.0.0\r\n\
|
||||
X-Spam-Flag: YES\r\n\
|
||||
X-Spam-Custom: Custom-Value\r\n\
|
||||
\r\n", 1)
|
||||
)
|
||||
});
|
||||
let miltertest = spawn_miltertest_runner(file!());
|
||||
|
||||
run("inet:3333@localhost", config).expect("milter execution failed");
|
||||
|
||||
let exit_code = miltertest.join().expect("panic in miltertest runner");
|
||||
assert!(exit_code.success(), "miltertest returned error exit code");
|
||||
|
||||
server.join().expect("panic in mock spamd server");
|
||||
}
|
68
tests/spamc_connection_error.lua
Normal file
68
tests/spamc_connection_error.lua
Normal file
|
@ -0,0 +1,68 @@
|
|||
-- When no spamd server is available, spamc fails to connect.
|
||||
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.helo(conn, "mail.gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.mailfrom(conn, "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.rcptto(conn, "to@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
SMFIC_DATA = string.byte("T") -- SMFIC_DATA not exported by miltertest
|
||||
local err = mt.macro(conn, SMFIC_DATA,
|
||||
"i", "1234567ABC",
|
||||
"j", "localhost",
|
||||
"_", "client.gluet.ch [123.123.123.123]",
|
||||
"{tls_version}", "TLSv1.2",
|
||||
"v", "Postfix 3.3.0")
|
||||
assert(err == nil, err)
|
||||
|
||||
-- If spamc cannot connect to spamd, it will retry several times. That is why
|
||||
-- this test can proceed all the way to the `eom` stage – where the failure
|
||||
-- finally surfaces.
|
||||
|
||||
local err = mt.data(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.header(conn, "From", "from@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "To", "to@gluet.ch")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Subject", "Test message")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Message-ID", string.format("<%06d@gluet.ch>", math.random(999999)))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
local err = mt.header(conn, "Date", os.date("%a, %d %b %Y %H:%M:%S %Z"))
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.eoh(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.bodystring(conn, "Test message body")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_CONTINUE)
|
||||
|
||||
local err = mt.eom(conn)
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_TEMPFAIL)
|
||||
|
||||
local err = mt.disconnect(conn)
|
||||
assert(err == nil, err)
|
24
tests/spamc_connection_error.rs
Normal file
24
tests/spamc_connection_error.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
mod common;
|
||||
|
||||
pub use common::*; // `pub` only to silence unused code warnings
|
||||
use spamassassin_milter::*;
|
||||
|
||||
#[test]
|
||||
fn spamc_connection_error() {
|
||||
let mut builder = Config::builder();
|
||||
// spamc always ‘works’ even if it cannot actually reach spamd!
|
||||
// `--no-safe-fallback` prevents this masking of connection errors.
|
||||
// TODO Also try with non-existing flag, behaviour is different?
|
||||
builder.set_spamc_args(vec![
|
||||
String::from("--no-safe-fallback"),
|
||||
format!("--port={}", SPAMD_PORT),
|
||||
]);
|
||||
let config = builder.build();
|
||||
|
||||
let miltertest = spawn_miltertest_runner(file!());
|
||||
|
||||
run("inet:3333@localhost", config).expect("milter execution failed");
|
||||
|
||||
let exit_code = miltertest.join().expect("panic in miltertest runner");
|
||||
assert!(exit_code.success(), "miltertest returned error exit code");
|
||||
}
|
11
tests/trusted_network_connection.lua
Normal file
11
tests/trusted_network_connection.lua
Normal file
|
@ -0,0 +1,11 @@
|
|||
-- A connection from a trusted network is accepted.
|
||||
|
||||
conn = mt.connect("inet:3333@localhost")
|
||||
assert(conn, "could not open connection")
|
||||
|
||||
local err = mt.conninfo(conn, "client.gluet.ch", "123.123.123.123")
|
||||
assert(err == nil, err)
|
||||
assert(mt.getreply(conn) == SMFIR_ACCEPT)
|
||||
|
||||
local err = mt.disconnect(conn)
|
||||
assert(err == nil, err)
|
18
tests/trusted_network_connection.rs
Normal file
18
tests/trusted_network_connection.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
mod common;
|
||||
|
||||
pub use common::*; // `pub` only to silence unused code warnings
|
||||
use spamassassin_milter::*;
|
||||
|
||||
#[test]
|
||||
fn trusted_network_connection() {
|
||||
let mut builder = Config::builder();
|
||||
builder.add_trusted_network("123.120.0.0/14".parse().unwrap());
|
||||
let config = builder.build();
|
||||
|
||||
let miltertest = spawn_miltertest_runner(file!());
|
||||
|
||||
run("inet:3333@localhost", config).expect("milter execution failed");
|
||||
|
||||
let exit_code = miltertest.join().expect("panic in miltertest runner");
|
||||
assert!(exit_code.success(), "miltertest returned error exit code");
|
||||
}
|
Loading…
Reference in a new issue