kumo_api_types/
lib.rs

1use chrono::{DateTime, Utc};
2use cidr_map::CidrSet;
3use serde::{Deserialize, Serialize};
4use spool::SpoolId;
5use std::collections::HashMap;
6use std::time::Duration;
7use url::Url;
8use utoipa::{IntoParams, ToResponse, ToSchema};
9use uuid::Uuid;
10
11pub mod egress_path;
12pub mod rebind;
13pub mod shaping;
14pub mod tsa;
15
16/// Describes which messages should be bounced.
17/// The criteria apply to the scheduled queue associated
18/// with a given message.
19#[derive(Serialize, Deserialize, Debug, ToSchema)]
20#[serde(deny_unknown_fields)]
21pub struct BounceV1Request {
22    /// The campaign name to match. If omitted, any campaign will match.
23    #[serde(default)]
24    pub campaign: Option<String>,
25
26    /// The tenant to match. If omitted, any tenant will match.
27    #[serde(default)]
28    pub tenant: Option<String>,
29
30    /// The domain name to match. If omitted, any domain will match.
31    #[serde(default)]
32    #[schema(example = "example.com")]
33    pub domain: Option<String>,
34
35    /// The routing_domain name to match. If omitted, any routing_domain will match.
36    #[serde(default)]
37    pub routing_domain: Option<String>,
38
39    /// Reason to log in the delivery log. Each matching message will be bounced
40    /// with an AdminBounce record unless you suppress logging.
41    /// The reason will also be shown in the list of currently active admin
42    /// bounces.
43    #[schema(example = "Cleaning up a bad send")]
44    pub reason: String,
45
46    /// Defaults to "5m". Specifies how long this bounce directive remains active.
47    /// While active, newly injected messages that match the bounce criteria
48    /// will also be bounced.
49    #[serde(
50        default,
51        with = "duration_serde",
52        skip_serializing_if = "Option::is_none"
53    )]
54    #[schema(example = "20m")]
55    pub duration: Option<Duration>,
56
57    /// If true, do not generate AdminBounce delivery logs for matching
58    /// messages.
59    #[serde(default)]
60    pub suppress_logging: bool,
61
62    /// instead of specifying the duration, you can set an explicit
63    /// expiration timestamp
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub expires: Option<DateTime<Utc>>,
66}
67
68impl BounceV1Request {
69    pub fn duration(&self) -> Duration {
70        match &self.expires {
71            Some(exp) => (*exp - Utc::now()).to_std().unwrap_or(Duration::ZERO),
72            None => self.duration.unwrap_or_else(default_duration),
73        }
74    }
75}
76
77fn default_duration() -> Duration {
78    Duration::from_secs(300)
79}
80
81#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
82pub struct BounceV1Response {
83    /// The id of the bounce rule that was registered.
84    /// This can be used later to delete the rule if desired.
85    #[schema(example = "552016f1-08e7-4e90-9da3-fd5c25acd069")]
86    pub id: Uuid,
87    /// Deprecated: this field is no longer populated, as bounces
88    /// are now always asynchronous. In earlier versions the following
89    /// applies:
90    ///
91    /// A map of queue name to number of bounced messages that
92    /// were processed as part of the initial sweep.
93    /// Additional bounces may be generated if/when other messages
94    /// that match the rule are discovered, but those obviously
95    /// cannot be reported in the context of the initial request.
96    #[schema(deprecated, example=json!({
97        "gmail.com": 200,
98        "yahoo.com": 100
99    }))]
100    pub bounced: HashMap<String, usize>,
101    /// Deprecated: this field is no longer populated, as bounces are
102    /// now always asynchronous. In earlier versions the following applies:
103    ///
104    /// The sum of the number of bounced messages reported by
105    /// the `bounced` field.
106    #[schema(deprecated, example = 300)]
107    pub total_bounced: usize,
108}
109
110#[derive(Serialize, Deserialize, Debug, ToSchema)]
111pub struct SetDiagnosticFilterRequest {
112    /// The diagnostic filter spec to use
113    #[schema(example = "kumod=trace")]
114    pub filter: String,
115}
116
117#[derive(Serialize, Deserialize, Debug, ToSchema)]
118pub struct BounceV1ListEntry {
119    /// The id of this bounce rule. Corresponds to the `id` field
120    /// returned by the originating request that set up the bounce,
121    /// and can be used to identify this particular entry if you
122    /// wish to delete it later.
123    #[schema(example = "552016f1-08e7-4e90-9da3-fd5c25acd069")]
124    pub id: Uuid,
125
126    /// The campaign field of the original request, if any.
127    #[serde(default)]
128    pub campaign: Option<String>,
129    /// The tenant field of the original request, if any.
130    #[serde(default)]
131    pub tenant: Option<String>,
132    /// The domain field of the original request, if any.
133    #[serde(default)]
134    pub domain: Option<String>,
135    /// The routing_domain field of the original request, if any.
136    #[serde(default)]
137    pub routing_domain: Option<String>,
138
139    /// The reason field of the original request
140    pub reason: String,
141
142    /// The time remaining until this entry expires and is automatically
143    /// removed.
144    #[serde(with = "duration_serde")]
145    pub duration: Duration,
146
147    /// A map of queue name to number of bounced messages that
148    /// were processed by this entry since it was created.
149    #[schema(example=json!({
150        "gmail.com": 200,
151        "yahoo.com": 100
152    }))]
153    pub bounced: HashMap<String, usize>,
154    /// The sum of the number of bounced messages reported by
155    /// the `bounced` field.
156    pub total_bounced: usize,
157}
158
159#[derive(Serialize, Deserialize, Debug, ToSchema)]
160pub struct BounceV1CancelRequest {
161    pub id: Uuid,
162}
163
164#[derive(Serialize, Deserialize, Debug, ToSchema)]
165pub struct SuspendV1Request {
166    /// The campaign name to match. If omitted, any campaign will match.
167    #[serde(default)]
168    pub campaign: Option<String>,
169    /// The tenant name to match. If omitted, any tenant will match.
170    #[serde(default)]
171    pub tenant: Option<String>,
172    /// The domain name to match. If omitted, any domain will match.
173    #[serde(default)]
174    pub domain: Option<String>,
175
176    /// The reason for the suspension
177    #[schema(example = "pause while working on resolving a block with the destination postmaster")]
178    pub reason: String,
179
180    /// Specifies how long this suspension remains active.
181    #[serde(
182        default,
183        with = "duration_serde",
184        skip_serializing_if = "Option::is_none"
185    )]
186    pub duration: Option<Duration>,
187
188    /// instead of specifying the duration, you can set an explicit
189    /// expiration timestamp
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub expires: Option<DateTime<Utc>>,
192}
193
194impl SuspendV1Request {
195    pub fn duration(&self) -> Duration {
196        match &self.expires {
197            Some(exp) => (*exp - Utc::now()).to_std().unwrap_or(Duration::ZERO),
198            None => self.duration.unwrap_or_else(default_duration),
199        }
200    }
201}
202
203#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
204pub struct SuspendV1Response {
205    /// The id of the suspension. This can be used later to cancel
206    /// the suspension.
207    pub id: Uuid,
208}
209
210#[derive(Serialize, Deserialize, Debug, ToSchema)]
211pub struct SuspendV1CancelRequest {
212    /// The id of the suspension to cancel
213    pub id: Uuid,
214}
215
216#[derive(Serialize, Deserialize, Debug, ToSchema)]
217pub struct SuspendV1ListEntry {
218    /// The id of the suspension. This can be used later to cancel
219    /// the suspension.
220    pub id: Uuid,
221
222    /// The campaign name to match. If omitted, any campaign will match.
223    #[serde(default)]
224    pub campaign: Option<String>,
225    /// The tenant name to match. If omitted, any tenant will match.
226    #[serde(default)]
227    pub tenant: Option<String>,
228    /// The domain name to match. If omitted, any domain will match.
229    #[serde(default)]
230    pub domain: Option<String>,
231
232    /// The reason for the suspension
233    #[schema(example = "pause while working on resolving a deliverability issue")]
234    pub reason: String,
235
236    #[serde(with = "duration_serde")]
237    /// Specifies how long this suspension remains active.
238    pub duration: Duration,
239}
240
241#[derive(Serialize, Deserialize, Debug, ToSchema)]
242pub struct SuspendReadyQueueV1Request {
243    /// The name of the ready queue that should be suspended
244    pub name: String,
245    /// The reason for the suspension
246    #[schema(example = "pause while working on resolving a block with the destination postmaster")]
247    pub reason: String,
248    /// Specifies how long this suspension remains active.
249    #[serde(
250        default,
251        with = "duration_serde",
252        skip_serializing_if = "Option::is_none"
253    )]
254    pub duration: Option<Duration>,
255
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub expires: Option<DateTime<Utc>>,
258}
259
260impl SuspendReadyQueueV1Request {
261    pub fn duration(&self) -> Duration {
262        if let Some(expires) = &self.expires {
263            let duration = expires.signed_duration_since(Utc::now());
264            duration.to_std().unwrap_or(Duration::ZERO)
265        } else {
266            self.duration.unwrap_or_else(default_duration)
267        }
268    }
269}
270
271#[derive(Serialize, Deserialize, Debug, ToSchema)]
272pub struct SuspendReadyQueueV1ListEntry {
273    /// The id for the suspension. Can be used to cancel the suspension.
274    pub id: Uuid,
275    /// The name of the ready queue that is suspended
276    pub name: String,
277    /// The reason for the suspension
278    #[schema(example = "pause while working on resolving a block with the destination postmaster")]
279    pub reason: String,
280
281    /// how long until this suspension expires and is automatically removed
282    #[serde(with = "duration_serde")]
283    pub duration: Duration,
284
285    /// The time at which the suspension will expire
286    pub expires: DateTime<Utc>,
287}
288
289#[derive(Serialize, Deserialize, Debug, IntoParams)]
290pub struct InspectMessageV1Request {
291    /// The spool identifier for the message whose information
292    /// is being requested
293    pub id: SpoolId,
294    /// If true, return the message body in addition to the
295    /// metadata
296    #[serde(default)]
297    pub want_body: bool,
298}
299
300impl InspectMessageV1Request {
301    pub fn apply_to_url(&self, url: &mut Url) {
302        let mut query = url.query_pairs_mut();
303        query.append_pair("id", &self.id.to_string());
304        if self.want_body {
305            query.append_pair("want_body", "true");
306        }
307    }
308}
309
310#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
311pub struct InspectMessageV1Response {
312    /// The spool identifier of the message
313    pub id: SpoolId,
314    /// The message information
315    pub message: MessageInformation,
316}
317
318#[derive(Serialize, Deserialize, Debug, IntoParams)]
319pub struct InspectQueueV1Request {
320    /// The name of the scheduled queue
321    pub queue_name: String,
322    /// If true, return the message body in addition to the
323    /// metadata
324    #[serde(default)]
325    pub want_body: bool,
326
327    /// Return up to `limit` messages in the queue sample.
328    /// Depending on the strategy configured for the queue,
329    /// messages may not be directly reachable via this endpoint.
330    /// If no limit is provided, all messages in the queue will
331    /// be sampled.
332    #[serde(default)]
333    pub limit: Option<usize>,
334}
335
336impl InspectQueueV1Request {
337    pub fn apply_to_url(&self, url: &mut Url) {
338        let mut query = url.query_pairs_mut();
339        query.append_pair("queue_name", &self.queue_name.to_string());
340        if self.want_body {
341            query.append_pair("want_body", "true");
342        }
343        if let Some(limit) = self.limit {
344            query.append_pair("limit", &limit.to_string());
345        }
346    }
347}
348
349#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
350pub struct InspectQueueV1Response {
351    pub queue_name: String,
352    pub messages: Vec<InspectMessageV1Response>,
353    pub num_scheduled: usize,
354    pub queue_config: serde_json::Value,
355    pub delayed_metric: usize,
356    pub now: DateTime<Utc>,
357    pub last_changed: DateTime<Utc>,
358}
359
360#[derive(Serialize, Deserialize, Debug, ToSchema)]
361pub struct MessageInformation {
362    /// The envelope sender
363    #[schema(example = "sender@sender.example.com")]
364    pub sender: String,
365    /// The envelope-to address
366    #[schema(example = "recipient@example.com")]
367    pub recipient: String,
368    /// The message metadata
369    #[schema(example=json!({
370        "received_from": "10.0.0.1:3488"
371    }))]
372    pub meta: serde_json::Value,
373    /// If `want_body` was set in the original request,
374    /// holds the message body
375    #[serde(default)]
376    pub data: Option<String>,
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub due: Option<DateTime<Utc>>,
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub num_attempts: Option<u16>,
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub scheduling: Option<serde_json::Value>,
383}
384
385#[derive(Serialize, Deserialize, Debug, ToSchema)]
386pub struct TraceSmtpV1Request {
387    #[serde(default)]
388    pub source_addr: Option<CidrSet>,
389
390    #[serde(default, skip_serializing_if = "is_false")]
391    pub terse: bool,
392}
393
394fn is_false(b: &bool) -> bool {
395    !b
396}
397
398#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
399pub struct TraceSmtpV1Event {
400    pub conn_meta: serde_json::Value,
401    pub payload: TraceSmtpV1Payload,
402    pub when: DateTime<Utc>,
403}
404
405#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq)]
406pub enum TraceSmtpV1Payload {
407    Connected,
408    Closed,
409    Read(String),
410    Write(String),
411    Diagnostic {
412        level: String,
413        message: String,
414    },
415    Callback {
416        name: String,
417        result: Option<serde_json::Value>,
418        error: Option<String>,
419    },
420    MessageDisposition {
421        relay: bool,
422        log_arf: bool,
423        log_oob: bool,
424        queue: String,
425        meta: serde_json::Value,
426        sender: String,
427        recipient: String,
428        id: SpoolId,
429    },
430    /// Like `Read`, but abbreviated by `terse`
431    AbbreviatedRead {
432        /// The "first" or more relevant line(s)
433        snippet: String,
434        /// Total size of data being read
435        len: usize,
436    },
437}
438
439#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
440pub struct TraceSmtpClientV1Event {
441    pub conn_meta: serde_json::Value,
442    pub payload: TraceSmtpClientV1Payload,
443    pub when: DateTime<Utc>,
444}
445
446#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq)]
447pub enum TraceSmtpClientV1Payload {
448    BeginSession,
449    Connected,
450    Closed,
451    Read(String),
452    Write(String),
453    Diagnostic {
454        level: String,
455        message: String,
456    },
457    MessageObtained,
458    /// Like `Write`, but abbreviated by `terse`
459    AbbreviatedWrite {
460        /// The "first" or more relevant line(s)
461        snippet: String,
462        /// Total size of data being read
463        len: usize,
464    },
465}
466
467#[derive(Serialize, Deserialize, Debug, ToSchema)]
468pub struct TraceSmtpClientV1Request {
469    /// The campaign name to match. If omitted, any campaign will match.
470    #[serde(default)]
471    pub campaign: Vec<String>,
472
473    /// The tenant to match. If omitted, any tenant will match.
474    #[serde(default)]
475    pub tenant: Vec<String>,
476
477    /// The domain name to match. If omitted, any domain will match.
478    #[serde(default)]
479    #[schema(example = "example.com")]
480    pub domain: Vec<String>,
481
482    /// The routing_domain name to match. If omitted, any routing_domain will match.
483    #[serde(default)]
484    pub routing_domain: Vec<String>,
485
486    /// The egress pool name to match. If omitted, any egress pool will match.
487    #[serde(default)]
488    pub egress_pool: Vec<String>,
489
490    /// The egress source name to match. If omitted, any egress source will match.
491    #[serde(default)]
492    pub egress_source: Vec<String>,
493
494    /// The envelope sender to match. If omitted, any will match.
495    #[serde(default)]
496    pub mail_from: Vec<String>,
497
498    /// The envelope recipient to match. If omitted, any will match.
499    #[serde(default)]
500    pub rcpt_to: Vec<String>,
501
502    /// The source address to match. If omitted, any will match.
503    #[serde(default)]
504    pub source_addr: Option<CidrSet>,
505
506    /// The mx hostname to match. If omitted, any will match.
507    #[serde(default)]
508    pub mx_host: Vec<String>,
509
510    /// The ready queue name to match. If omitted, any will match.
511    #[serde(default)]
512    pub ready_queue: Vec<String>,
513
514    /// The mx ip address to match. If omitted, any will match.
515    #[serde(default)]
516    pub mx_addr: Option<CidrSet>,
517
518    /// Use a more terse representation of the data, focusing on the first
519    /// line of larger writes
520    #[serde(default, skip_serializing_if = "is_false")]
521    pub terse: bool,
522}
523
524#[derive(Serialize, Deserialize, Debug, ToSchema, IntoParams)]
525pub struct ReadyQueueStateRequest {
526    /// Which queues to request. If empty, request all queue states.
527    #[serde(default)]
528    pub queues: Vec<String>,
529}
530
531#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
532pub struct QueueState {
533    pub context: String,
534    pub since: DateTime<Utc>,
535}
536
537#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
538pub struct ReadyQueueStateResponse {
539    pub states_by_ready_queue: HashMap<String, HashMap<String, QueueState>>,
540}