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