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