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 SpoolCompactV1Request {
194    /// Name of the spool to compact, matching a `kumo.define_spool` name.
195    pub name: String,
196}
197
198#[derive(Serialize, Deserialize, Debug, ToSchema)]
199pub struct SuspendV1Request {
200    /// The campaign name to match. If omitted, any campaign will match.
201    #[serde(default)]
202    #[schema(example = "campaign_name")]
203    pub campaign: Option<String>,
204    /// The tenant name to match. If omitted, any tenant will match.
205    #[serde(default)]
206    #[schema(example = "tenant_name")]
207    pub tenant: Option<String>,
208    /// The domain name to match. If omitted, any domain will match.
209    #[serde(default)]
210    #[schema(example = "example.com")]
211    pub domain: Option<String>,
212
213    /// The reason for the suspension
214    #[schema(example = "pause while working on resolving a block with the destination postmaster")]
215    pub reason: String,
216
217    /// Specifies how long this suspension remains active.
218    #[serde(
219        default,
220        with = "duration_serde",
221        skip_serializing_if = "Option::is_none"
222    )]
223    pub duration: Option<Duration>,
224
225    /// instead of specifying the duration, you can set an explicit
226    /// expiration timestamp
227    #[serde(default, skip_serializing_if = "Option::is_none")]
228    pub expires: Option<DateTime<Utc>>,
229
230    /// If present, queue_names takes precedence over `campaign`,
231    /// `tenant`, and `domain` and specifies the exact set of
232    /// scheduled queue names to which the suspension applies.
233    #[serde(default, skip_serializing_if = "Vec::is_empty")]
234    #[schema(example=json!(["campaign_name:tenant_name@example.com"]))]
235    pub queue_names: Vec<String>,
236}
237
238impl SuspendV1Request {
239    pub fn duration(&self) -> Duration {
240        match &self.expires {
241            Some(exp) => (*exp - Utc::now()).to_std().unwrap_or(Duration::ZERO),
242            None => self.duration.unwrap_or_else(default_duration),
243        }
244    }
245}
246
247#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
248pub struct SuspendV1Response {
249    /// The id of the suspension. This can be used later to cancel
250    /// the suspension.
251    pub id: Uuid,
252}
253
254#[derive(Serialize, Deserialize, Debug, ToSchema)]
255pub struct SuspendV1CancelRequest {
256    /// The id of the suspension to cancel
257    pub id: Uuid,
258}
259
260#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
261pub struct InjectV1Response {
262    /// The number of messages that were injected successfully
263    pub success_count: usize,
264    /// The number of messages that failed to inject
265    pub fail_count: usize,
266
267    /// The list of failed recipients
268    #[schema(format = "email")]
269    pub failed_recipients: Vec<String>,
270
271    /// The list of error messages
272    pub errors: Vec<String>,
273}
274
275#[derive(Serialize, Deserialize, Debug, ToSchema)]
276pub struct SuspendV1ListEntry {
277    /// The id of the suspension. This can be used later to cancel
278    /// the suspension.
279    pub id: Uuid,
280
281    /// The campaign name to match. If omitted, any campaign will match.
282    #[serde(default)]
283    #[schema(example = "campaign_name")]
284    pub campaign: Option<String>,
285    /// The tenant name to match. If omitted, any tenant will match.
286    #[serde(default)]
287    #[schema(example = "tenant_name")]
288    pub tenant: Option<String>,
289    /// The domain name to match. If omitted, any domain will match.
290    #[serde(default)]
291    #[schema(example = "example.com")]
292    pub domain: Option<String>,
293
294    /// The reason for the suspension
295    #[schema(example = "pause while working on resolving a deliverability issue")]
296    pub reason: String,
297
298    #[serde(with = "duration_serde")]
299    /// Specifies how long this suspension remains active.
300    pub duration: Duration,
301}
302
303#[derive(Serialize, Deserialize, Debug, ToSchema)]
304pub struct SuspendReadyQueueV1Request {
305    /// The name of the ready queue that should be suspended
306    #[schema(
307        example = "source_name->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client"
308    )]
309    pub name: String,
310    /// The reason for the suspension
311    #[schema(example = "pause while working on resolving a block with the destination postmaster")]
312    pub reason: String,
313    /// Specifies how long this suspension remains active.
314    #[serde(
315        default,
316        with = "duration_serde",
317        skip_serializing_if = "Option::is_none"
318    )]
319    pub duration: Option<Duration>,
320
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub expires: Option<DateTime<Utc>>,
323}
324
325impl SuspendReadyQueueV1Request {
326    pub fn duration(&self) -> Duration {
327        if let Some(expires) = &self.expires {
328            let duration = expires.signed_duration_since(Utc::now());
329            duration.to_std().unwrap_or(Duration::ZERO)
330        } else {
331            self.duration.unwrap_or_else(default_duration)
332        }
333    }
334}
335
336#[derive(Serialize, Deserialize, Debug, ToSchema, PartialEq, Eq)]
337pub struct SuspendReadyQueueV1ListEntry {
338    /// The id for the suspension. Can be used to cancel the suspension.
339    pub id: Uuid,
340    /// The name of the ready queue that is suspended
341    #[schema(
342        example = "source_name->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client"
343    )]
344    pub name: String,
345    /// The reason for the suspension
346    #[schema(example = "pause while working on resolving a block with the destination postmaster")]
347    pub reason: String,
348
349    /// how long until this suspension expires and is automatically removed
350    #[serde(with = "duration_serde")]
351    pub duration: Duration,
352
353    /// The time at which the suspension will expire
354    pub expires: DateTime<Utc>,
355}
356
357#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
358pub struct InspectMessageV1Request {
359    /// The spool identifier for the message whose information
360    /// is being requested
361    pub id: SpoolId,
362    /// If true, return the message body in addition to the
363    /// metadata
364    #[serde(default)]
365    pub want_body: bool,
366}
367
368pub trait ApplyToUrl {
369    fn apply_to_url(&self, url: &mut Url);
370}
371
372impl ApplyToUrl for InspectMessageV1Request {
373    fn apply_to_url(&self, url: &mut Url) {
374        let mut query = url.query_pairs_mut();
375        query.append_pair("id", &self.id.to_string());
376        if self.want_body {
377            query.append_pair("want_body", "true");
378        }
379    }
380}
381
382#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
383pub struct InspectMessageV1Response {
384    /// The spool identifier of the message
385    pub id: SpoolId,
386    /// The message information
387    pub message: MessageInformation,
388}
389
390#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
391pub struct InspectQueueV1Request {
392    /// The name of the scheduled queue
393    #[schema(example = "campaign_name:tenant_name@example.com")]
394    pub queue_name: String,
395    /// If true, return the message body in addition to the
396    /// metadata
397    #[serde(default)]
398    pub want_body: bool,
399
400    /// Return up to `limit` messages in the queue sample.
401    /// Depending on the strategy configured for the queue,
402    /// messages may not be directly reachable via this endpoint.
403    /// If no limit is provided, all messages in the queue will
404    /// be sampled.
405    #[serde(default)]
406    pub limit: Option<usize>,
407}
408
409impl ApplyToUrl for InspectQueueV1Request {
410    fn apply_to_url(&self, url: &mut Url) {
411        let mut query = url.query_pairs_mut();
412        query.append_pair("queue_name", &self.queue_name.to_string());
413        if self.want_body {
414            query.append_pair("want_body", "true");
415        }
416        if let Some(limit) = self.limit {
417            query.append_pair("limit", &limit.to_string());
418        }
419    }
420}
421
422#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
423pub struct InspectQueueV1Response {
424    #[schema(example = "campaign_name:tenant_name@example.com")]
425    pub queue_name: String,
426    pub messages: Vec<InspectMessageV1Response>,
427    pub num_scheduled: usize,
428    #[schema(value_type=Object)]
429    pub queue_config: serde_json::Value,
430    pub delayed_metric: usize,
431    pub now: DateTime<Utc>,
432    pub last_changed: DateTime<Utc>,
433}
434
435#[serde_as]
436#[derive(Serialize, Deserialize, Debug, ToSchema)]
437pub struct MessageInformation {
438    /// The envelope sender
439    #[schema(example = "sender@sender.example.com")]
440    pub sender: String,
441    /// The envelope-to address.
442    /// May be either an individual string or an array of strings
443    /// for multi-recipient messages.
444    #[schema(example = "recipient@example.com", format = "email")]
445    #[serde_as(as = "OneOrMany<_, PreferOne>")] // FIXME: json schema
446    pub recipient: Vec<String>,
447    /// The message metadata
448    #[schema(value_type=Object, example=json!({
449        "received_from": "10.0.0.1:3488"
450    }))]
451    pub meta: serde_json::Value,
452    /// If `want_body` was set in the original request,
453    /// holds the message body
454    #[serde(default)]
455    #[schema(example = "From: user@example.com\nSubject: Hello\n\nHello there")]
456    pub data: Option<String>,
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub due: Option<DateTime<Utc>>,
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub num_attempts: Option<u16>,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    #[schema(value_type=Object)]
463    pub scheduling: Option<serde_json::Value>,
464}
465
466#[derive(Serialize, Deserialize, Debug, ToSchema)]
467pub struct TraceSmtpV1Request {
468    #[serde(default)]
469    #[schema(value_type=Option<Vec<String>>)]
470    pub source_addr: Option<CidrSet>,
471
472    #[serde(default, skip_serializing_if = "is_false")]
473    pub terse: bool,
474}
475
476fn is_false(b: &bool) -> bool {
477    !b
478}
479
480#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
481pub struct TraceSmtpV1Event {
482    pub conn_meta: serde_json::Value,
483    pub payload: TraceSmtpV1Payload,
484    pub when: DateTime<Utc>,
485}
486
487#[serde_as]
488#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq)]
489pub enum TraceSmtpV1Payload {
490    Connected,
491    Closed,
492    Read(String),
493    Write(String),
494    Diagnostic {
495        level: String,
496        message: String,
497    },
498    Callback {
499        name: String,
500        result: Option<serde_json::Value>,
501        error: Option<String>,
502    },
503    MessageDisposition {
504        relay: bool,
505        log_arf: serde_json::Value,
506        log_oob: serde_json::Value,
507        queue: String,
508        meta: serde_json::Value,
509        #[schema(format = "email")]
510        sender: String,
511        #[serde_as(as = "OneOrMany<_, PreferOne>")] // FIXME: json schema
512        #[schema(format = "email")]
513        recipient: Vec<String>,
514        id: SpoolId,
515        #[serde(default)]
516        was_arf_or_oob: Option<bool>,
517        #[serde(default)]
518        will_enqueue: Option<bool>,
519    },
520    /// Like `Read`, but abbreviated by `terse`
521    AbbreviatedRead {
522        /// The "first" or more relevant line(s)
523        snippet: String,
524        /// Total size of data being read
525        len: usize,
526    },
527}
528
529#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
530pub struct TraceSmtpClientV1Event {
531    pub conn_meta: serde_json::Value,
532    pub payload: TraceSmtpClientV1Payload,
533    pub when: DateTime<Utc>,
534}
535
536#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq)]
537pub enum TraceSmtpClientV1Payload {
538    BeginSession,
539    Connected,
540    Closed,
541    Read(String),
542    Write(String),
543    Diagnostic {
544        level: String,
545        message: String,
546    },
547    MessageObtained,
548    /// Like `Write`, but abbreviated by `terse`
549    AbbreviatedWrite {
550        /// The "first" or more relevant line(s)
551        snippet: String,
552        /// Total size of data being read
553        len: usize,
554    },
555}
556
557#[derive(Serialize, Deserialize, Debug, Default, ToSchema)]
558pub struct TraceSmtpClientV1Request {
559    /// The campaign name to match. If omitted, any campaign will match.
560    #[serde(default)]
561    #[schema(example = "campaign_name")]
562    pub campaign: Vec<String>,
563
564    /// The tenant to match. If omitted, any tenant will match.
565    #[serde(default)]
566    #[schema(example = "tenant_name")]
567    pub tenant: Vec<String>,
568
569    /// The domain name to match. If omitted, any domain will match.
570    #[serde(default)]
571    #[schema(example = "example.com")]
572    pub domain: Vec<String>,
573
574    /// The routing_domain name to match. If omitted, any routing_domain will match.
575    #[serde(default)]
576    #[schema(example = "routing_domain.com")]
577    pub routing_domain: Vec<String>,
578
579    /// The egress pool name to match. If omitted, any egress pool will match.
580    #[serde(default)]
581    #[schema(example = "pool_name")]
582    pub egress_pool: Vec<String>,
583
584    /// The egress source name to match. If omitted, any egress source will match.
585    #[serde(default)]
586    #[schema(example = "source_name")]
587    pub egress_source: Vec<String>,
588
589    /// The envelope sender to match. If omitted, any will match.
590    #[serde(default)]
591    #[schema(format = "email")]
592    pub mail_from: Vec<String>,
593
594    /// The envelope recipient to match. If omitted, any will match.
595    #[serde(default)]
596    #[schema(format = "email")]
597    pub rcpt_to: Vec<String>,
598
599    /// The source address to match. If omitted, any will match.
600    #[serde(default)]
601    #[schema(value_type=Option<Vec<String>>, example="10.0.0.1/16")]
602    pub source_addr: Option<CidrSet>,
603
604    /// The mx hostname to match. If omitted, any will match.
605    #[serde(default)]
606    #[schema(format = "mx1.example.com")]
607    pub mx_host: Vec<String>,
608
609    /// The ready queue name to match. If omitted, any will match.
610    #[serde(default)]
611    #[schema(
612        example = "source_name->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client"
613    )]
614    pub ready_queue: Vec<String>,
615
616    /// The mx ip address to match. If omitted, any will match.
617    #[serde(default)]
618    #[schema(value_type=Option<Vec<String>>, example="10.0.0.1/16")]
619    pub mx_addr: Option<CidrSet>,
620
621    /// Use a more terse representation of the data, focusing on the first
622    /// line of larger writes
623    #[serde(default, skip_serializing_if = "is_false")]
624    pub terse: bool,
625}
626
627#[derive(Serialize, Deserialize, Debug, ToSchema, IntoParams)]
628pub struct ReadyQueueStateRequest {
629    /// Which queues to request. If empty, request all queue states.
630    #[serde(default)]
631    #[schema(example=json!(["campaign_name:tenant_name@example.com"]))]
632    pub queues: Vec<String>,
633}
634
635impl ApplyToUrl for ReadyQueueStateRequest {
636    fn apply_to_url(&self, url: &mut Url) {
637        let mut query = url.query_pairs_mut();
638        if !self.queues.is_empty() {
639            query.append_pair("queues", &self.queues.join(","));
640        }
641    }
642}
643
644#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
645pub struct QueueState {
646    #[schema(example = "TooManyLeases for queue")]
647    pub context: String,
648    pub since: DateTime<Utc>,
649}
650
651#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
652pub struct ReadyQueueStateResponse {
653    pub states_by_ready_queue: HashMap<String, HashMap<String, QueueState>>,
654}
655
656/// Phase of a dispatcher task within a ready queue.
657///
658/// {{since('dev')}}
659#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
660pub enum DispatcherPhase {
661    Starting,
662    AcquiringLease { label: String },
663    Idle,
664    AccumulatingBatch { have: u32, want: u32 },
665    ConnectionRateThrottled,
666    MessageRateThrottled,
667    AttemptingConnection,
668    DeliveringMessage,
669    Closing,
670}
671
672/// Query parameters for the inspect-ready-q endpoint.
673///
674/// {{since('dev')}}
675#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
676pub struct InspectReadyQV1Request {
677    /// The name of the ready queue to inspect.
678    #[schema(example = "unspecified->gmail.com@smtp_client")]
679    pub queue_name: String,
680    /// When true, the response includes the list of scheduled queue
681    /// names that have promoted messages into this ready queue.
682    /// Walking the set is bounded but proportional to the number of
683    /// live scheduled queues, so the field is opt-in.
684    #[serde(default)]
685    pub include_scheduled_queues: bool,
686}
687
688impl ApplyToUrl for InspectReadyQV1Request {
689    fn apply_to_url(&self, url: &mut Url) {
690        let mut query = url.query_pairs_mut();
691        query.append_pair("queue_name", &self.queue_name);
692        if self.include_scheduled_queues {
693            query.append_pair("include_scheduled_queues", "true");
694        }
695    }
696}
697
698/// Snapshot of the operational state of a ready queue.
699///
700/// {{since('dev')}}
701#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
702pub struct ReadyQueueStateSnapshot {
703    pub ready_count: usize,
704    pub connection_count: usize,
705    pub connection_rate_throttled: Option<QueueState>,
706    pub connection_limited: Option<QueueState>,
707    pub suspended: Option<SuspendReadyQueueV1ListEntry>,
708    /// Effective progress watchdog timeout for this queue, honoring
709    /// the per-egress-path config or the protocol-derived default.
710    #[serde(with = "duration_serde")]
711    pub watchdog_threshold: Duration,
712}
713
714/// Per-dispatcher summary returned by the inspect-ready-q endpoint.
715///
716/// {{since('dev')}}
717#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
718pub struct DispatcherSummary {
719    pub session_id: Uuid,
720    pub started_at: DateTime<Utc>,
721    #[serde(with = "duration_serde")]
722    pub age: Duration,
723    pub phase: DispatcherPhase,
724    pub detail: Option<String>,
725    #[serde(with = "duration_serde")]
726    pub time_in_current_phase: Duration,
727    pub messages_delivered: u64,
728    pub messages_transfailed: u64,
729    pub messages_failed: u64,
730    pub delivered_this_connection: u64,
731    pub overall_rate_per_sec: f64,
732}
733
734/// Response body for the inspect-ready-q endpoint.
735///
736/// {{since('dev')}}
737#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
738pub struct InspectReadyQV1Response {
739    pub queue_name: String,
740    /// MX resolution result for the destination. `None` for
741    /// protocols that don't use MX (e.g. Lua-protocol queues) or
742    /// when MX resolution wasn't applicable.
743    pub mx: Option<crate::egress_path::MxResolution>,
744    pub egress_source: String,
745    pub egress_pool: String,
746    /// Protocol identifier as it appears in the ready queue name.
747    #[schema(example = "smtp_client")]
748    pub protocol: String,
749    pub state: ReadyQueueStateSnapshot,
750    /// Snapshot of the egress path configuration in effect for this
751    /// queue.
752    #[schema(value_type = Object)]
753    pub path_config: crate::egress_path::EgressPathConfig,
754    /// Steady-state throughput ceilings implied by `path_config`.
755    /// Each axis is tagged with the configuration term that
756    /// produced it, so operators can tell whether a plateau in the
757    /// observed rate is the result of shaping configuration or the
758    /// remote side.
759    pub constraints: crate::egress_path::EffectiveConstraints,
760    /// Scheduled queue names that currently feed this ready queue.
761    /// Populated only when the request's `include_scheduled_queues`
762    /// flag is true; otherwise None.
763    #[serde(default, skip_serializing_if = "Option::is_none")]
764    pub scheduled_queue_names: Option<Vec<String>>,
765    pub dispatchers: Vec<DispatcherSummary>,
766    pub now: DateTime<Utc>,
767}
768
769/// Request body for the abort-ready-q-conn endpoint.
770///
771/// {{since('dev')}}
772#[derive(Serialize, Deserialize, Debug, ToSchema)]
773pub struct AbortReadyQConnV1Request {
774    pub queue_name: String,
775    pub session_id: Uuid,
776}
777
778/// Query parameters for the resolve-egress-path endpoint.
779///
780/// {{since('dev')}}
781#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
782pub struct ResolveEgressPathV1Request {
783    /// Destination domain. Drives the MX lookup and is passed
784    /// through to the `get_egress_path_config` event callback.
785    #[schema(example = "gmail.com")]
786    pub domain: String,
787
788    /// Egress source name. Defaults to "unspecified" if omitted.
789    #[serde(default)]
790    pub source: Option<String>,
791}
792
793impl ApplyToUrl for ResolveEgressPathV1Request {
794    fn apply_to_url(&self, url: &mut Url) {
795        let mut query = url.query_pairs_mut();
796        query.append_pair("domain", &self.domain);
797        if let Some(source) = &self.source {
798            query.append_pair("source", source);
799        }
800    }
801}
802
803/// Response body for the resolve-egress-path endpoint.
804///
805/// {{since('dev')}}
806#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
807pub struct ResolveEgressPathV1Response {
808    pub domain: String,
809    pub source: String,
810    /// MX resolution result. `None` when MX lookup wasn't applicable
811    /// (e.g. non-SMTP protocols) or failed (e.g. internal sentinel
812    /// domains, network errors).
813    pub mx: Option<crate::egress_path::MxResolution>,
814    /// The ready-queue name that this domain/source pair would
815    /// resolve to. Lets the caller pivot to inspect-ready-q for
816    /// live runtime detail when the queue exists.
817    pub queue_name: String,
818    /// Snapshot of the resolved scheduled-queue configuration for
819    /// this domain. Carried as an untyped JSON object because
820    /// `QueueConfig` contains protocol variants that don't all
821    /// round-trip cleanly through the OpenAPI schema.
822    #[schema(value_type = Object)]
823    pub queue_config: serde_json::Value,
824    #[schema(value_type = Object)]
825    pub path_config: crate::egress_path::EgressPathConfig,
826    pub constraints: crate::egress_path::EffectiveConstraints,
827}
828
829#[derive(Serialize, Clone, Deserialize, Debug, PartialEq, ToSchema)]
830pub struct MachineInfoV1 {
831    /// The NodeID of the system
832    #[schema(example = "9745bb48-14d7-48f2-a1fb-7df8d5844217")]
833    pub node_id: String,
834    /// The hostname of the system, as reported by `gethostname(2)`
835    #[schema(example = "mta1.example.com")]
836    pub hostname: String,
837    /// The MAC address of the primary, non-loopback, network interface
838    #[schema(example = "02:02:02:02:02:02")]
839    pub mac_address: String,
840    /// The number of available CPUs as reported by
841    /// <https://docs.rs/num_cpus/latest/num_cpus/fn.get.html>
842    #[schema(example = 64)]
843    pub num_cores: usize,
844    /// The kernel version
845    #[schema(example = "6.8.0-1016-aws")]
846    pub kernel_version: Option<String>,
847    /// Identifies the running platform
848    #[schema(example = "linux/x86_64")]
849    pub platform: String,
850    /// The OS distribution
851    #[schema(example = "ubuntu")]
852    pub distribution: String,
853    /// The OS version (which often includes the distribution)
854    #[schema(example = "Linux (Ubuntu 24.04)")]
855    pub os_version: String,
856    /// Total physical memory installed in the instance
857    #[schema(example = 1003929600)]
858    pub total_memory_bytes: u64,
859    /// If we detected that we're running in a container, the name
860    /// of the container runtime
861    pub container_runtime: Option<String>,
862    /// Identifies the CPU.  If you have a mixture of different CPUs,
863    /// this will be a comma separated list of the different CPUs
864    #[schema(example = "Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz")]
865    pub cpu_brand: String,
866    /// Additional metadata hash(es) that can identify the running machine.
867    /// For example, when running in AWS, the instance-id will be
868    /// included.
869    #[schema(
870        example = "aws_instance_id=i-09aebefac97cf0000,machine_uid=ec22130d1de33cf52413457ac040000"
871    )]
872    pub fingerprint: String,
873    /// The date/time at which the process was last started
874    pub online_since: DateTime<Utc>,
875    /// Which process is running. eg: `kumod` vs `tsa-daemon` vs. `proxy-server`.
876    #[schema(example = "kumod")]
877    pub process_kind: String,
878    /// The version of KumoMTA that is running
879    #[schema(example = "2026.02.24-2d1a3174")]
880    pub version: String,
881}