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
347pub trait ApplyToUrl {
348    fn apply_to_url(&self, url: &mut Url);
349}
350
351impl ApplyToUrl for InspectMessageV1Request {
352    fn apply_to_url(&self, url: &mut Url) {
353        let mut query = url.query_pairs_mut();
354        query.append_pair("id", &self.id.to_string());
355        if self.want_body {
356            query.append_pair("want_body", "true");
357        }
358    }
359}
360
361#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
362pub struct InspectMessageV1Response {
363    /// The spool identifier of the message
364    pub id: SpoolId,
365    /// The message information
366    pub message: MessageInformation,
367}
368
369#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
370pub struct InspectQueueV1Request {
371    /// The name of the scheduled queue
372    #[schema(example = "campaign_name:tenant_name@example.com")]
373    pub queue_name: String,
374    /// If true, return the message body in addition to the
375    /// metadata
376    #[serde(default)]
377    pub want_body: bool,
378
379    /// Return up to `limit` messages in the queue sample.
380    /// Depending on the strategy configured for the queue,
381    /// messages may not be directly reachable via this endpoint.
382    /// If no limit is provided, all messages in the queue will
383    /// be sampled.
384    #[serde(default)]
385    pub limit: Option<usize>,
386}
387
388impl ApplyToUrl for InspectQueueV1Request {
389    fn apply_to_url(&self, url: &mut Url) {
390        let mut query = url.query_pairs_mut();
391        query.append_pair("queue_name", &self.queue_name.to_string());
392        if self.want_body {
393            query.append_pair("want_body", "true");
394        }
395        if let Some(limit) = self.limit {
396            query.append_pair("limit", &limit.to_string());
397        }
398    }
399}
400
401#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
402pub struct InspectQueueV1Response {
403    #[schema(example = "campaign_name:tenant_name@example.com")]
404    pub queue_name: String,
405    pub messages: Vec<InspectMessageV1Response>,
406    pub num_scheduled: usize,
407    #[schema(value_type=Object)]
408    pub queue_config: serde_json::Value,
409    pub delayed_metric: usize,
410    pub now: DateTime<Utc>,
411    pub last_changed: DateTime<Utc>,
412}
413
414#[serde_as]
415#[derive(Serialize, Deserialize, Debug, ToSchema)]
416pub struct MessageInformation {
417    /// The envelope sender
418    #[schema(example = "sender@sender.example.com")]
419    pub sender: String,
420    /// The envelope-to address.
421    /// May be either an individual string or an array of strings
422    /// for multi-recipient messages.
423    #[schema(example = "recipient@example.com", format = "email")]
424    #[serde_as(as = "OneOrMany<_, PreferOne>")] // FIXME: json schema
425    pub recipient: Vec<String>,
426    /// The message metadata
427    #[schema(value_type=Object, example=json!({
428        "received_from": "10.0.0.1:3488"
429    }))]
430    pub meta: serde_json::Value,
431    /// If `want_body` was set in the original request,
432    /// holds the message body
433    #[serde(default)]
434    #[schema(example = "From: user@example.com\nSubject: Hello\n\nHello there")]
435    pub data: Option<String>,
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub due: Option<DateTime<Utc>>,
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub num_attempts: Option<u16>,
440    #[serde(skip_serializing_if = "Option::is_none")]
441    #[schema(value_type=Object)]
442    pub scheduling: Option<serde_json::Value>,
443}
444
445#[derive(Serialize, Deserialize, Debug, ToSchema)]
446pub struct TraceSmtpV1Request {
447    #[serde(default)]
448    #[schema(value_type=Option<Vec<String>>)]
449    pub source_addr: Option<CidrSet>,
450
451    #[serde(default, skip_serializing_if = "is_false")]
452    pub terse: bool,
453}
454
455fn is_false(b: &bool) -> bool {
456    !b
457}
458
459#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
460pub struct TraceSmtpV1Event {
461    pub conn_meta: serde_json::Value,
462    pub payload: TraceSmtpV1Payload,
463    pub when: DateTime<Utc>,
464}
465
466#[serde_as]
467#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq)]
468pub enum TraceSmtpV1Payload {
469    Connected,
470    Closed,
471    Read(String),
472    Write(String),
473    Diagnostic {
474        level: String,
475        message: String,
476    },
477    Callback {
478        name: String,
479        result: Option<serde_json::Value>,
480        error: Option<String>,
481    },
482    MessageDisposition {
483        relay: bool,
484        log_arf: serde_json::Value,
485        log_oob: serde_json::Value,
486        queue: String,
487        meta: serde_json::Value,
488        #[schema(format = "email")]
489        sender: String,
490        #[serde_as(as = "OneOrMany<_, PreferOne>")] // FIXME: json schema
491        #[schema(format = "email")]
492        recipient: Vec<String>,
493        id: SpoolId,
494        #[serde(default)]
495        was_arf_or_oob: Option<bool>,
496        #[serde(default)]
497        will_enqueue: Option<bool>,
498    },
499    /// Like `Read`, but abbreviated by `terse`
500    AbbreviatedRead {
501        /// The "first" or more relevant line(s)
502        snippet: String,
503        /// Total size of data being read
504        len: usize,
505    },
506}
507
508#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
509pub struct TraceSmtpClientV1Event {
510    pub conn_meta: serde_json::Value,
511    pub payload: TraceSmtpClientV1Payload,
512    pub when: DateTime<Utc>,
513}
514
515#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq)]
516pub enum TraceSmtpClientV1Payload {
517    BeginSession,
518    Connected,
519    Closed,
520    Read(String),
521    Write(String),
522    Diagnostic {
523        level: String,
524        message: String,
525    },
526    MessageObtained,
527    /// Like `Write`, but abbreviated by `terse`
528    AbbreviatedWrite {
529        /// The "first" or more relevant line(s)
530        snippet: String,
531        /// Total size of data being read
532        len: usize,
533    },
534}
535
536#[derive(Serialize, Deserialize, Debug, ToSchema)]
537pub struct TraceSmtpClientV1Request {
538    /// The campaign name to match. If omitted, any campaign will match.
539    #[serde(default)]
540    #[schema(example = "campaign_name")]
541    pub campaign: Vec<String>,
542
543    /// The tenant to match. If omitted, any tenant will match.
544    #[serde(default)]
545    #[schema(example = "tenant_name")]
546    pub tenant: Vec<String>,
547
548    /// The domain name to match. If omitted, any domain will match.
549    #[serde(default)]
550    #[schema(example = "example.com")]
551    pub domain: Vec<String>,
552
553    /// The routing_domain name to match. If omitted, any routing_domain will match.
554    #[serde(default)]
555    #[schema(example = "routing_domain.com")]
556    pub routing_domain: Vec<String>,
557
558    /// The egress pool name to match. If omitted, any egress pool will match.
559    #[serde(default)]
560    #[schema(example = "pool_name")]
561    pub egress_pool: Vec<String>,
562
563    /// The egress source name to match. If omitted, any egress source will match.
564    #[serde(default)]
565    #[schema(example = "source_name")]
566    pub egress_source: Vec<String>,
567
568    /// The envelope sender to match. If omitted, any will match.
569    #[serde(default)]
570    #[schema(format = "email")]
571    pub mail_from: Vec<String>,
572
573    /// The envelope recipient to match. If omitted, any will match.
574    #[serde(default)]
575    #[schema(format = "email")]
576    pub rcpt_to: Vec<String>,
577
578    /// The source address to match. If omitted, any will match.
579    #[serde(default)]
580    #[schema(value_type=Option<Vec<String>>, example="10.0.0.1/16")]
581    pub source_addr: Option<CidrSet>,
582
583    /// The mx hostname to match. If omitted, any will match.
584    #[serde(default)]
585    #[schema(format = "mx1.example.com")]
586    pub mx_host: Vec<String>,
587
588    /// The ready queue name to match. If omitted, any will match.
589    #[serde(default)]
590    #[schema(
591        example = "source_name->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client"
592    )]
593    pub ready_queue: Vec<String>,
594
595    /// The mx ip address to match. If omitted, any will match.
596    #[serde(default)]
597    #[schema(value_type=Option<Vec<String>>, example="10.0.0.1/16")]
598    pub mx_addr: Option<CidrSet>,
599
600    /// Use a more terse representation of the data, focusing on the first
601    /// line of larger writes
602    #[serde(default, skip_serializing_if = "is_false")]
603    pub terse: bool,
604}
605
606#[derive(Serialize, Deserialize, Debug, ToSchema, IntoParams)]
607pub struct ReadyQueueStateRequest {
608    /// Which queues to request. If empty, request all queue states.
609    #[serde(default)]
610    #[schema(example=json!(["campaign_name:tenant_name@example.com"]))]
611    pub queues: Vec<String>,
612}
613
614impl ApplyToUrl for ReadyQueueStateRequest {
615    fn apply_to_url(&self, url: &mut Url) {
616        let mut query = url.query_pairs_mut();
617        if !self.queues.is_empty() {
618            query.append_pair("queues", &self.queues.join(","));
619        }
620    }
621}
622
623#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
624pub struct QueueState {
625    #[schema(example = "TooManyLeases for queue")]
626    pub context: String,
627    pub since: DateTime<Utc>,
628}
629
630#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
631pub struct ReadyQueueStateResponse {
632    pub states_by_ready_queue: HashMap<String, HashMap<String, QueueState>>,
633}
634
635#[derive(Serialize, Clone, Deserialize, Debug, PartialEq, ToSchema)]
636pub struct MachineInfoV1 {
637    /// The NodeID of the system
638    #[schema(example = "9745bb48-14d7-48f2-a1fb-7df8d5844217")]
639    pub node_id: String,
640    /// The hostname of the system, as reported by `gethostname(2)`
641    #[schema(example = "mta1.example.com")]
642    pub hostname: String,
643    /// The MAC address of the primary, non-loopback, network interface
644    #[schema(example = "02:02:02:02:02:02")]
645    pub mac_address: String,
646    /// The number of available CPUs as reported by
647    /// <https://docs.rs/num_cpus/latest/num_cpus/fn.get.html>
648    #[schema(example = 64)]
649    pub num_cores: usize,
650    /// The kernel version
651    #[schema(example = "6.8.0-1016-aws")]
652    pub kernel_version: Option<String>,
653    /// Identifies the running platform
654    #[schema(example = "linux/x86_64")]
655    pub platform: String,
656    /// The OS distribution
657    #[schema(example = "ubuntu")]
658    pub distribution: String,
659    /// The OS version (which often includes the distribution)
660    #[schema(example = "Linux (Ubuntu 24.04)")]
661    pub os_version: String,
662    /// Total physical memory installed in the instance
663    #[schema(example = 1003929600)]
664    pub total_memory_bytes: u64,
665    /// If we detected that we're running in a container, the name
666    /// of the container runtime
667    pub container_runtime: Option<String>,
668    /// Identifies the CPU.  If you have a mixture of different CPUs,
669    /// this will be a comma separated list of the different CPUs
670    #[schema(example = "Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz")]
671    pub cpu_brand: String,
672    /// Additional metadata hash(es) that can identify the running machine.
673    /// For example, when running in AWS, the instance-id will be
674    /// included.
675    #[schema(
676        example = "aws_instance_id=i-09aebefac97cf0000,machine_uid=ec22130d1de33cf52413457ac040000"
677    )]
678    pub fingerprint: String,
679    /// The date/time at which the process was last started
680    pub online_since: DateTime<Utc>,
681    /// Which process is running. eg: `kumod` vs `tsa-daemon` vs. `proxy-server`.
682    #[schema(example = "kumod")]
683    pub process_kind: String,
684    /// The version of KumoMTA that is running
685    #[schema(example = "2026.02.24-2d1a3174")]
686    pub version: String,
687}