Skip to main content

rspamd_client/protocol/
scan.rs

1use crate::error::RspamdError;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5/// Rspamd scan reply structure
6#[derive(Debug, Serialize, Deserialize)]
7pub struct RspamdScanReply {
8    /// If message has been skipped
9    #[serde(default)]
10    pub is_skipped: bool,
11    /// Scan score
12    #[serde(default)]
13    pub score: f64,
14    /// Required score (legacy)
15    #[serde(default)]
16    pub required_score: f64,
17    /// Action to take
18    #[serde(default)]
19    pub action: String,
20    /// Action thresholds
21    #[serde(default)]
22    pub thresholds: HashMap<String, f64>,
23    /// Symbols detected
24    #[serde(default)]
25    pub symbols: HashMap<String, Symbol>,
26    /// Messages
27    #[serde(default)]
28    pub messages: HashMap<String, String>,
29    /// URLs
30    #[serde(default)]
31    pub urls: Vec<String>,
32    /// Emails
33    #[serde(default)]
34    pub emails: Vec<String>,
35    /// Message id
36    #[serde(rename = "message-id", default)]
37    pub message_id: String,
38    /// Real time of scan
39    #[serde(default)]
40    pub time_real: f64,
41    /// Milter actions block
42    #[serde(default)]
43    pub milter: Option<Milter>,
44    #[serde(default)]
45    /// Filename
46    pub filename: String,
47    #[serde(default)]
48    pub scan_time: f64,
49    /// Rewritten message body (only present when body_block flag is set and message was rewritten)
50    /// This field is not part of the JSON response but is extracted from the response body
51    #[serde(skip)]
52    pub rewritten_body: Option<Vec<u8>>,
53}
54
55/// Symbol structure
56#[derive(Debug, Serialize, Deserialize)]
57pub struct Symbol {
58    #[serde(default)]
59    pub name: String,
60    #[serde(default)]
61    pub score: f64,
62    #[serde(default)]
63    pub metric_score: f64,
64    #[serde(default)]
65    pub description: Option<String>,
66    #[serde(default)]
67    pub options: Option<Vec<String>>,
68}
69
70/// Milter actions block
71#[derive(Debug, Serialize, Deserialize)]
72pub struct Milter {
73    #[serde(default)]
74    pub add_headers: HashMap<String, MailHeader>,
75    #[serde(default)]
76    pub remove_headers: HashMap<String, i32>,
77}
78
79/// Milter header action
80#[derive(Debug, Serialize, Deserialize)]
81pub struct MailHeader {
82    #[serde(default)]
83    pub value: String,
84    #[serde(default)]
85    pub order: i32,
86}
87
88/// Reply for controller commands (learn spam/ham, fuzzy add/del)
89#[derive(Debug, Serialize, Deserialize)]
90pub struct RspamdLearnReply {
91    #[serde(default)]
92    pub success: bool,
93    #[serde(default)]
94    pub error: Option<String>,
95}
96
97/// Parse a scan reply body, honouring the `Message-Offset` header used by the
98/// `body_block` feature: when present and in bounds, the body carries JSON up
99/// to the offset followed by the rewritten message.
100pub fn parse_scan_reply(
101    message_offset: Option<&str>,
102    body: &[u8],
103) -> Result<RspamdScanReply, RspamdError> {
104    if let Some(offset_str) = message_offset {
105        let offset = offset_str
106            .parse::<usize>()
107            .map_err(|e| RspamdError::HttpError(format!("Invalid Message-Offset value: {}", e)))?;
108
109        if offset < body.len() {
110            // Split body into JSON part and rewritten body part
111            let mut response = serde_json::from_slice::<RspamdScanReply>(&body[..offset])?;
112            response.rewritten_body = Some(body[offset..].to_vec());
113            return Ok(response);
114        }
115        // Offset is out of bounds, parse entire body as JSON
116    }
117    Ok(serde_json::from_slice::<RspamdScanReply>(body)?)
118}