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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
use chrono::{DateTime, Utc};
use cidr_map::CidrSet;
use serde::{Deserialize, Serialize};
use spool::SpoolId;
use std::collections::HashMap;
use std::time::Duration;
use url::Url;
use utoipa::{IntoParams, ToResponse, ToSchema};
use uuid::Uuid;

pub mod egress_path;
pub mod rebind;
pub mod shaping;
pub mod tsa;

/// Describes which messages should be bounced.
/// The criteria apply to the scheduled queue associated
/// with a given message.
#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct BounceV1Request {
    /// The campaign name to match. If omitted, any campaign will match.
    #[serde(default)]
    pub campaign: Option<String>,

    /// The tenant to match. If omitted, any tenant will match.
    #[serde(default)]
    pub tenant: Option<String>,

    /// The domain name to match. If omitted, any domain will match.
    #[serde(default)]
    #[schema(example = "example.com")]
    pub domain: Option<String>,

    /// The routing_domain name to match. If omitted, any routing_domain will match.
    #[serde(default)]
    pub routing_domain: Option<String>,

    /// Reason to log in the delivery log. Each matching message will be bounced
    /// with an AdminBounce record unless you suppress logging.
    /// The reason will also be shown in the list of currently active admin
    /// bounces.
    #[schema(example = "Cleaning up a bad send")]
    pub reason: String,

    /// Defaults to "5m". Specifies how long this bounce directive remains active.
    /// While active, newly injected messages that match the bounce criteria
    /// will also be bounced.
    #[serde(
        default,
        with = "duration_serde",
        skip_serializing_if = "Option::is_none"
    )]
    #[schema(example = "20m")]
    pub duration: Option<Duration>,

    /// If true, do not generate AdminBounce delivery logs for matching
    /// messages.
    #[serde(default)]
    pub suppress_logging: bool,
}

impl BounceV1Request {
    pub fn duration(&self) -> Duration {
        self.duration.unwrap_or_else(default_duration)
    }
}

fn default_duration() -> Duration {
    Duration::from_secs(300)
}

#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
pub struct BounceV1Response {
    /// The id of the bounce rule that was registered.
    /// This can be used later to delete the rule if desired.
    #[schema(example = "552016f1-08e7-4e90-9da3-fd5c25acd069")]
    pub id: Uuid,
    /// Deprecated: this field is no longer populated, as bounces
    /// are now always asynchronous. In earlier versions the following
    /// applies:
    ///
    /// A map of queue name to number of bounced messages that
    /// were processed as part of the initial sweep.
    /// Additional bounces may be generated if/when other messages
    /// that match the rule are discovered, but those obviously
    /// cannot be reported in the context of the initial request.
    #[schema(deprecated, example=json!({
        "gmail.com": 200,
        "yahoo.com": 100
    }))]
    pub bounced: HashMap<String, usize>,
    /// Deprecated: this field is no longer populated, as bounces are
    /// now always asynchronous. In earlier versions the following applies:
    ///
    /// The sum of the number of bounced messages reported by
    /// the `bounced` field.
    #[schema(deprecated, example = 300)]
    pub total_bounced: usize,
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct SetDiagnosticFilterRequest {
    /// The diagnostic filter spec to use
    #[schema(example = "kumod=trace")]
    pub filter: String,
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct BounceV1ListEntry {
    /// The id of this bounce rule. Corresponds to the `id` field
    /// returned by the originating request that set up the bounce,
    /// and can be used to identify this particular entry if you
    /// wish to delete it later.
    #[schema(example = "552016f1-08e7-4e90-9da3-fd5c25acd069")]
    pub id: Uuid,

    /// The campaign field of the original request, if any.
    #[serde(default)]
    pub campaign: Option<String>,
    /// The tenant field of the original request, if any.
    #[serde(default)]
    pub tenant: Option<String>,
    /// The domain field of the original request, if any.
    #[serde(default)]
    pub domain: Option<String>,
    /// The routing_domain field of the original request, if any.
    #[serde(default)]
    pub routing_domain: Option<String>,

    /// The reason field of the original request
    pub reason: String,

    /// The time remaining until this entry expires and is automatically
    /// removed.
    #[serde(with = "duration_serde")]
    pub duration: Duration,

    /// A map of queue name to number of bounced messages that
    /// were processed by this entry since it was created.
    #[schema(example=json!({
        "gmail.com": 200,
        "yahoo.com": 100
    }))]
    pub bounced: HashMap<String, usize>,
    /// The sum of the number of bounced messages reported by
    /// the `bounced` field.
    pub total_bounced: usize,
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct BounceV1CancelRequest {
    pub id: Uuid,
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct SuspendV1Request {
    /// The campaign name to match. If omitted, any campaign will match.
    #[serde(default)]
    pub campaign: Option<String>,
    /// The tenant name to match. If omitted, any tenant will match.
    #[serde(default)]
    pub tenant: Option<String>,
    /// The domain name to match. If omitted, any domain will match.
    #[serde(default)]
    pub domain: Option<String>,

    /// The reason for the suspension
    #[schema(example = "pause while working on resolving a block with the destination postmaster")]
    pub reason: String,

    /// Specifies how long this suspension remains active.
    #[serde(
        default,
        with = "duration_serde",
        skip_serializing_if = "Option::is_none"
    )]
    pub duration: Option<Duration>,

    /// instead of specifying the duration, you can set an explicit
    /// expiration timestamp
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expires: Option<DateTime<Utc>>,
}

impl SuspendV1Request {
    pub fn duration(&self) -> Duration {
        match &self.expires {
            Some(exp) => (*exp - Utc::now()).to_std().unwrap_or(Duration::ZERO),
            None => self.duration.unwrap_or_else(default_duration),
        }
    }
}

#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
pub struct SuspendV1Response {
    /// The id of the suspension. This can be used later to cancel
    /// the suspension.
    pub id: Uuid,
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct SuspendV1CancelRequest {
    /// The id of the suspension to cancel
    pub id: Uuid,
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct SuspendV1ListEntry {
    /// The id of the suspension. This can be used later to cancel
    /// the suspension.
    pub id: Uuid,

    /// The campaign name to match. If omitted, any campaign will match.
    #[serde(default)]
    pub campaign: Option<String>,
    /// The tenant name to match. If omitted, any tenant will match.
    #[serde(default)]
    pub tenant: Option<String>,
    /// The domain name to match. If omitted, any domain will match.
    #[serde(default)]
    pub domain: Option<String>,

    /// The reason for the suspension
    #[schema(example = "pause while working on resolving a deliverability issue")]
    pub reason: String,

    #[serde(with = "duration_serde")]
    /// Specifies how long this suspension remains active.
    pub duration: Duration,
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct SuspendReadyQueueV1Request {
    /// The name of the ready queue that should be suspended
    pub name: String,
    /// The reason for the suspension
    #[schema(example = "pause while working on resolving a block with the destination postmaster")]
    pub reason: String,
    /// Specifies how long this suspension remains active.
    #[serde(
        default,
        with = "duration_serde",
        skip_serializing_if = "Option::is_none"
    )]
    pub duration: Option<Duration>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expires: Option<DateTime<Utc>>,
}

impl SuspendReadyQueueV1Request {
    pub fn duration(&self) -> Duration {
        if let Some(expires) = &self.expires {
            let duration = expires.signed_duration_since(Utc::now());
            duration.to_std().unwrap_or(Duration::ZERO)
        } else {
            self.duration.unwrap_or_else(default_duration)
        }
    }
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct SuspendReadyQueueV1ListEntry {
    /// The id for the suspension. Can be used to cancel the suspension.
    pub id: Uuid,
    /// The name of the ready queue that is suspended
    pub name: String,
    /// The reason for the suspension
    #[schema(example = "pause while working on resolving a block with the destination postmaster")]
    pub reason: String,

    /// how long until this suspension expires and is automatically removed
    #[serde(with = "duration_serde")]
    pub duration: Duration,

    /// The time at which the suspension will expire
    pub expires: DateTime<Utc>,
}

#[derive(Serialize, Deserialize, Debug, IntoParams)]
pub struct InspectMessageV1Request {
    /// The spool identifier for the message whose information
    /// is being requested
    pub id: SpoolId,
    /// If true, return the message body in addition to the
    /// metadata
    #[serde(default)]
    pub want_body: bool,
}

impl InspectMessageV1Request {
    pub fn apply_to_url(&self, url: &mut Url) {
        let mut query = url.query_pairs_mut();
        query.append_pair("id", &self.id.to_string());
        if self.want_body {
            query.append_pair("want_body", "true");
        }
    }
}

#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
pub struct InspectMessageV1Response {
    /// The spool identifier of the message
    pub id: SpoolId,
    /// The message information
    pub message: MessageInformation,
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct MessageInformation {
    /// The envelope sender
    #[schema(example = "sender@sender.example.com")]
    pub sender: String,
    /// The envelope-to address
    #[schema(example = "recipient@example.com")]
    pub recipient: String,
    /// The message metadata
    #[schema(example=json!({
        "received_from": "10.0.0.1:3488"
    }))]
    pub meta: serde_json::Value,
    /// If `want_body` was set in the original request,
    /// holds the message body
    #[serde(default)]
    pub data: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct TraceSmtpV1Request {
    #[serde(default)]
    pub source_addr: Option<CidrSet>,
}

#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
pub struct TraceSmtpV1Event {
    pub conn_meta: serde_json::Value,
    pub payload: TraceSmtpV1Payload,
    pub when: DateTime<Utc>,
}

#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq)]
pub enum TraceSmtpV1Payload {
    Connected,
    Closed,
    Read(String),
    Write(String),
    Diagnostic {
        level: String,
        message: String,
    },
    Callback {
        name: String,
        result: Option<serde_json::Value>,
        error: Option<String>,
    },
    MessageDisposition {
        relay: bool,
        log_arf: bool,
        log_oob: bool,
        queue: String,
        meta: serde_json::Value,
        sender: String,
        recipient: String,
        id: SpoolId,
    },
}

#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
pub struct TraceSmtpClientV1Event {
    pub conn_meta: serde_json::Value,
    pub payload: TraceSmtpClientV1Payload,
    pub when: DateTime<Utc>,
}

#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq)]
pub enum TraceSmtpClientV1Payload {
    BeginSession,
    Connected,
    Closed,
    Read(String),
    Write(String),
    Diagnostic { level: String, message: String },
    MessageObtained,
}

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct TraceSmtpClientV1Request {
    /// The campaign name to match. If omitted, any campaign will match.
    #[serde(default)]
    pub campaign: Vec<String>,

    /// The tenant to match. If omitted, any tenant will match.
    #[serde(default)]
    pub tenant: Vec<String>,

    /// The domain name to match. If omitted, any domain will match.
    #[serde(default)]
    #[schema(example = "example.com")]
    pub domain: Vec<String>,

    /// The routing_domain name to match. If omitted, any routing_domain will match.
    #[serde(default)]
    pub routing_domain: Vec<String>,

    /// The egress pool name to match. If omitted, any egress pool will match.
    #[serde(default)]
    pub egress_pool: Vec<String>,

    /// The egress source name to match. If omitted, any egress source will match.
    #[serde(default)]
    pub egress_source: Vec<String>,

    /// The envelope sender to match. If omitted, any will match.
    #[serde(default)]
    pub mail_from: Vec<String>,

    /// The envelope recipient to match. If omitted, any will match.
    #[serde(default)]
    pub rcpt_to: Vec<String>,

    /// The source address to match. If omitted, any will match.
    #[serde(default)]
    pub source_addr: Option<CidrSet>,

    /// The mx hostname to match. If omitted, any will match.
    #[serde(default)]
    pub mx_host: Vec<String>,

    /// The ready queue name to match. If omitted, any will match.
    #[serde(default)]
    pub ready_queue: Vec<String>,

    /// The mx ip address to match. If omitted, any will match.
    #[serde(default)]
    pub mx_addr: Option<CidrSet>,
}