1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//! Response containing modified data
//!
//! Only to an end-of-body the milter can respond with change requests.
//! These are modification actions.

pub mod body;
pub mod headers;
pub mod quarantine;
pub mod recipients;

use enum_dispatch::enum_dispatch;

use super::{
    actions::{Action, Continue},
    ServerMessage,
};

use crate::encoding::Writable;
use crate::{actions::Abort, optneg::Capability};
use bytes::BytesMut;

use body::ReplaceBody;
use headers::{AddHeader, ChangeHeader, InsertHeader};
use quarantine::Quarantine;
use recipients::{AddRecipient, DeleteRecipient};

/// A container for multiple modification requests towards the milter client.
///
/// ```
/// use miltr_common::modifications::{ModificationResponse, headers::AddHeader};
///
/// let mut builder = ModificationResponse::builder();
/// builder.push(AddHeader::new(
///      "Test Add Header".as_bytes(),
///      "Add Header Value".as_bytes(),
///   ));
/// let response = builder.contin();
/// ```
///
/// # Note on Capabilities
/// While all [`ModificationAction`] can be pushed into this response,
/// they might not all be sent.
/// During option negotiation, client and server agree on supported
/// [`Capability`].
#[derive(Debug)]
pub struct ModificationResponse {
    modifications: Vec<ModificationAction>,
    final_action: Action,
}

impl ModificationResponse {
    /// Create a builder to assemble a modification response.
    #[must_use]
    pub fn builder() -> ModificationResponseBuilder {
        ModificationResponseBuilder {
            modifications: Vec::default(),
        }
    }

    /// Create an empty `ModificationResponse` just to continue
    #[must_use]
    pub fn empty_continue() -> Self {
        Self {
            modifications: Vec::new(),
            final_action: Continue.into(),
        }
    }

    /// Filter modification actions in `self`, keep only those which have been
    /// allowed by the specified `capabilities`.
    pub fn filter_mods_by_caps(&mut self, capabilities: Capability) {
        self.modifications
            .retain(|m| Self::mod_matches_caps(m, capabilities));
    }

    /// Returns true, if a single modification action matches the set `capabilities`
    fn mod_matches_caps(modification: &ModificationAction, capabilities: Capability) -> bool {
        match modification {
            ModificationAction::AddHeader(_) => capabilities.contains(Capability::SMFIF_ADDHDRS),
            ModificationAction::ReplaceBody(_) => capabilities.contains(Capability::SMFIF_CHGBODY),
            ModificationAction::AddRecipient(_) => capabilities.contains(Capability::SMFIF_ADDRCPT),
            ModificationAction::DeleteRecipient(_) => {
                capabilities.contains(Capability::SMFIF_DELRCPT)
            }
            ModificationAction::ChangeHeader(_) | ModificationAction::InsertHeader(_) => {
                capabilities.contains(Capability::SMFIF_CHGHDRS)
            }
            ModificationAction::Quarantine(_) => {
                capabilities.contains(Capability::SMFIF_QUARANTINE)
            }
        }
    }

    /// Get the received modification actions
    #[must_use]
    pub fn modifications(&self) -> &[ModificationAction] {
        self.modifications.as_ref()
    }

    /// Get the final action to be done to the mail
    #[must_use]
    pub fn final_action(&self) -> &Action {
        &self.final_action
    }
}

impl From<ModificationResponse> for Vec<ServerMessage> {
    fn from(value: ModificationResponse) -> Self {
        let mut resp: Vec<ServerMessage> = Vec::with_capacity(value.modifications.len() + 1);
        resp.extend(
            value
                .modifications
                .into_iter()
                .map(ServerMessage::ModificationAction),
        );
        resp.push(ServerMessage::Action(value.final_action));
        resp
    }
}

/// Gather up Modification actions to send to the milter client
#[derive(Debug, Clone)]
pub struct ModificationResponseBuilder {
    modifications: Vec<ModificationAction>,
}

impl ModificationResponseBuilder {
    /// Push another modification action onto the builder
    pub fn push<M: Into<ModificationAction>>(&mut self, mod_action: M) {
        self.modifications.push(mod_action.into());
    }

    /// Send the `Abort` command to the milter client
    #[must_use]
    pub fn abort(self) -> ModificationResponse {
        self.build(Abort)
    }

    /// Send a `Continue` command to the milter client with all set
    /// modification responses.
    #[must_use]
    pub fn contin(self) -> ModificationResponse {
        self.build(Continue)
    }

    /// Finalize into a [`ModificationResponse`] with a final action
    #[must_use]
    pub fn build<A: Into<Action>>(self, final_action: A) -> ModificationResponse {
        ModificationResponse {
            modifications: self.modifications,
            final_action: final_action.into(),
        }
    }
}

/// The container of possible milter modification actions
#[enum_dispatch]
#[derive(Debug, Clone)]
pub enum ModificationAction {
    /// Add recipient
    AddRecipient,
    /// Delete recipient
    DeleteRecipient,
    // /* add recipient (incl. ESMTP args) */
    // currently not supported, feel free to implement
    // SmfirAddrcptPar,
    // /* 421: shutdown (internal to MTA) */
    // Not implemented in Milter
    // SmfirShutdown,
    /// Replace mail body
    ReplaceBody,
    // /* change envelope sender (from) */
    // currently not supported, feel free to implement
    // SmfirChgfrom,
    // /* cause a connection failure */
    // currently not supported, feel free to implement. But why would you
    // need the connection to fail? Please, at least try to reason why you
    // need this
    // SmfirConnFail,
    /// Add an arbitrary header
    AddHeader,
    /// Insert the header at a specific place
    InsertHeader,
    // /* set list of symbols (macros) */
    // SmfirSetsymlist,
    /// Change an existing header
    ChangeHeader,
    // /* progress */
    // currently not supported, feel free to implement. May be a bit complicated
    // and config needed to handle timeouts and when to send and stuff
    // SmfirProgress,
    /// Quarantine this mail
    Quarantine,
}