kumo_api_types/
lib.rs

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