Skip to content

Latest commit

 

History

History
266 lines (201 loc) · 8.72 KB

File metadata and controls

266 lines (201 loc) · 8.72 KB

Rspamd Client for Rust

This crate provides an HTTP client for interacting with the Rspamd service in Rust. It supports both synchronous and asynchronous operations using the attohttpc and reqwest libraries, respectively.

Features

  • Sync/Async: Choose between synchronous (attohttpc) or asynchronous (reqwest) client
  • Mutually Exclusive: Async and sync features are mutually exclusive by design
  • Connection Pooling: Persistent clients reuse TCP/TLS connections across scans (async)
  • Encryption: Native HTTPCrypt encryption support
  • Compression: ZSTD compression for requests and responses
  • Local File Scanning: Scan files on the same host without transferring body (File header)
  • Body Rewriting: Receive rewritten message bodies (body_block flag)
  • Envelope Data: Configure sender, recipients, IP, HELO, hostname, and custom headers
  • Proxy Support: HTTP proxy configuration
  • TLS: Custom TLS settings

Installation

Add the following to your Cargo.toml:

[dependencies]
rspamd-client = { version = "0.7", features = ["async"] }

Enable either the sync or async feature (but not both):

  • async (default): Uses reqwest and tokio
  • sync: Uses attohttpc

Usage

Asynchronous Client

Create one RspamdAsyncClient and reuse it for all scans: the underlying reqwest client pools TCP (and TLS) connections, so subsequent scans skip connection setup entirely. The client is cheap to clone and Send + Sync, so it can be stored in a static or cached keyed by Config (which implements Clone, Eq and Hash).

use rspamd_client::{Config, EnvelopeData, RspamdAsyncClient};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::builder()
        .base_url("http://localhost:11333".to_string())
        .build();
    let client = RspamdAsyncClient::new(config)?;

    let envelope = EnvelopeData::builder()
        .from("sender@example.com".to_string())
        .rcpt(vec!["recipient@example.com".to_string()])
        .ip("127.0.0.1".to_string())
        .build();

    let email = "From: user@example.com\nTo: recipient@example.com\nSubject: Test\n\nThis is a test email.";

    // Reuses the pooled connection on every call
    let response = client.scan(email, envelope).await?;
    println!("Score: {}, Action: {}", response.score, response.action);
    Ok(())
}

For a single scan, the deprecated one-shot scan_async(&config, email, envelope) wrapper still works, but it opens a new connection on every call.

Synchronous Client

use rspamd_client::{Config, EnvelopeData, RspamdSyncClient};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::builder()
        .base_url("http://localhost:11333".to_string())
        .build();
    let client = RspamdSyncClient::new(config)?;

    let envelope = EnvelopeData::builder()
        .from("sender@example.com".to_string())
        .build();

    let email = "From: user@example.com\nTo: recipient@example.com\nSubject: Test\n\nThis is a test email.";

    let response = client.scan(email, envelope)?;
    println!("Score: {}, Action: {}", response.score, response.action);
    Ok(())
}

Note: attohttpc does not pool connections, so the sync client reuses configuration and encryption state across calls, but each request still opens a fresh connection.

Learning and Fuzzy Storage

Both clients expose the remaining commands (these target the controller endpoint, so point base_url at it and set password):

let reply = client.learn_spam(email, Default::default()).await?;
let reply = client.learn_ham(email, Default::default()).await?;
// Flag/Weight go via EnvelopeData::additional_headers
let reply = client.fuzzy_add(email, envelope).await?;
let reply = client.fuzzy_del(email, envelope).await?;
assert!(reply.success);

Advanced Features

Local File Scanning (File Header)

When the client and Rspamd server are on the same host, you can scan files without transferring the body by using the file_path option. This is a significant performance optimization:

use rspamd_client::{Config, EnvelopeData, RspamdAsyncClient};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::builder()
        .base_url("http://localhost:11333".to_string())
        .build();
    let client = RspamdAsyncClient::new(config)?;

    let envelope = EnvelopeData::builder()
        .from("sender@example.com".to_string())
        .file_path("/var/mail/message.eml".to_string())  // Rspamd reads file directly
        .build();

    // Empty body - file is read by Rspamd from disk
    let response = client.scan("", envelope).await?;
    println!("Scanned file with score: {}", response.score);
    Ok(())
}

Body Block (Rewritten Message)

Request the rewritten message body from Rspamd when modifications are applied (e.g., subject rewriting, header changes):

use rspamd_client::{Config, EnvelopeData, RspamdAsyncClient};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::builder()
        .base_url("http://localhost:11333".to_string())
        .build();
    let client = RspamdAsyncClient::new(config)?;

    let envelope = EnvelopeData::builder()
        .from("sender@example.com".to_string())
        .body_block(true)  // Request rewritten body if modified
        .build();

    let email = "From: user@example.com\nTo: recipient@example.com\nSubject: Test\n\nBody content.";

    let response = client.scan(email, envelope).await?;

    if let Some(rewritten_body) = response.rewritten_body {
        println!("Message was rewritten, new body size: {} bytes", rewritten_body.len());
        // Use rewritten_body instead of original
    }
    Ok(())
}

Encryption (HTTPCrypt)

Use native Rspamd HTTPCrypt encryption:

let config = Config::builder()
    .base_url("http://localhost:11333".to_string())
    .encryption_key("k4nz984k36xmcynm1hr9kdbn6jhcxf4ggbrb1quay7f88rpm9kay".to_string())
    .build();

The encryption key must be in Rspamd base32 format and match the server's public key.

Compression

ZSTD compression is enabled by default. To disable:

let config = Config::builder()
    .base_url("http://localhost:11333".to_string())
    .zstd(false)
    .build();

Proxy Configuration

use rspamd_client::{Config, ProxyConfig};

let proxy = ProxyConfig {
    proxy_url: "http://proxy.example.com:8080".to_string(),
    username: Some("user".to_string()),
    password: Some("pass".to_string()),
};

let config = Config::builder()
    .base_url("http://localhost:11333".to_string())
    .proxy_config(proxy)
    .build();

Configuration

Config Options

  • base_url: Rspamd server URL (required)
  • password: Optional authentication password
  • timeout: Request timeout as a std::time::Duration (default: 30 seconds)
  • retries: Number of retry attempts (default: 1)
  • zstd: Enable ZSTD compression (default: true)
  • encryption_key: HTTPCrypt encryption key (optional)
  • proxy_config: HTTP proxy settings (optional)
  • tls_settings: Custom TLS configuration (optional)

EnvelopeData Options

  • from: Sender email address
  • rcpt: List of recipient email addresses
  • ip: Sender IP address
  • user: Authenticated username
  • helo: SMTP HELO string
  • hostname: Resolved hostname
  • file_path: Local file path for scanning (instead of body transfer)
  • body_block: Request rewritten body in response
  • additional_headers: Custom HTTP headers

Response Structure

pub struct RspamdScanReply {
    pub score: f64,                              // Spam score
    pub action: String,                          // Action to take (e.g., "reject", "add header")
    pub symbols: HashMap<String, Symbol>,        // Detected symbols
    pub messages: HashMap<String, String>,       // Messages from Rspamd
    pub urls: Vec<String>,                       // Extracted URLs
    pub emails: Vec<String>,                     // Extracted emails
    pub message_id: String,                      // Message ID
    pub time_real: f64,                          // Scan time
    pub milter: Option<Milter>,                  // Milter actions (headers to add/remove)
    pub rewritten_body: Option<Vec<u8>>,         // Rewritten message body (if body_block enabled)
    // ... other fields
}

License

This project is licensed under the Apache 2.0 License.

Contributing

Contributions are welcome! Please open a pull request or issue on GitHub.

Links