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#[derive(Serialize, Deserialize, Debug, ToSchema)]
31#[serde(deny_unknown_fields)]
32pub struct BounceV1Request {
33 #[serde(default)]
35 #[schema(example = "campaign_name")]
36 pub campaign: Option<String>,
37
38 #[serde(default)]
40 #[schema(example = "tenant_name")]
41 pub tenant: Option<String>,
42
43 #[serde(default)]
45 #[schema(example = "example.com")]
46 pub domain: Option<String>,
47
48 #[serde(default)]
51 #[schema(example = "routing_domain.com")]
52 pub routing_domain: Option<String>,
53
54 #[schema(example = "Cleaning up a bad send")]
59 pub reason: String,
60
61 #[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 #[serde(default)]
75 #[schema(default = false)]
76 pub suppress_logging: bool,
77
78 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub expires: Option<DateTime<Utc>>,
82
83 #[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 #[schema(example = "552016f1-08e7-4e90-9da3-fd5c25acd069")]
109 pub id: Uuid,
110 #[schema(deprecated, example=json!({
120 "gmail.com": 200,
121 "yahoo.com": 100
122 }))]
123 pub bounced: HashMap<String, usize>,
124 #[schema(deprecated, example = 300)]
130 pub total_bounced: usize,
131}
132
133#[derive(Serialize, Deserialize, Debug, ToSchema)]
134pub struct SetDiagnosticFilterRequest {
135 #[schema(example = "kumod=trace")]
137 pub filter: String,
138}
139
140#[derive(Serialize, Deserialize, Debug, ToSchema)]
141pub struct BounceV1ListEntry {
142 #[schema(example = "552016f1-08e7-4e90-9da3-fd5c25acd069")]
147 pub id: Uuid,
148
149 #[serde(default)]
151 #[schema(example = "campaign_name")]
152 pub campaign: Option<String>,
153 #[serde(default)]
155 #[schema(example = "tenant_name")]
156 pub tenant: Option<String>,
157 #[serde(default)]
159 #[schema(example = "example.com")]
160 pub domain: Option<String>,
161 #[serde(default)]
163 #[schema(example = "routing_domain.com")]
164 pub routing_domain: Option<String>,
165
166 #[schema(example = "cleaning up a bad send")]
168 pub reason: String,
169
170 #[serde(with = "duration_serde")]
173 pub duration: Duration,
174
175 #[schema(example=json!({
178 "gmail.com": 200,
179 "yahoo.com": 100
180 }))]
181 pub bounced: HashMap<String, usize>,
182 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 pub name: String,
196}
197
198#[derive(Serialize, Deserialize, Debug, ToSchema)]
199pub struct SuspendV1Request {
200 #[serde(default)]
202 #[schema(example = "campaign_name")]
203 pub campaign: Option<String>,
204 #[serde(default)]
206 #[schema(example = "tenant_name")]
207 pub tenant: Option<String>,
208 #[serde(default)]
210 #[schema(example = "example.com")]
211 pub domain: Option<String>,
212
213 #[schema(example = "pause while working on resolving a block with the destination postmaster")]
215 pub reason: String,
216
217 #[serde(
219 default,
220 with = "duration_serde",
221 skip_serializing_if = "Option::is_none"
222 )]
223 pub duration: Option<Duration>,
224
225 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub expires: Option<DateTime<Utc>>,
229
230 #[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 pub id: Uuid,
252}
253
254#[derive(Serialize, Deserialize, Debug, ToSchema)]
255pub struct SuspendV1CancelRequest {
256 pub id: Uuid,
258}
259
260#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
261pub struct InjectV1Response {
262 pub success_count: usize,
264 pub fail_count: usize,
266
267 #[schema(format = "email")]
269 pub failed_recipients: Vec<String>,
270
271 pub errors: Vec<String>,
273}
274
275#[derive(Serialize, Deserialize, Debug, ToSchema)]
276pub struct SuspendV1ListEntry {
277 pub id: Uuid,
280
281 #[serde(default)]
283 #[schema(example = "campaign_name")]
284 pub campaign: Option<String>,
285 #[serde(default)]
287 #[schema(example = "tenant_name")]
288 pub tenant: Option<String>,
289 #[serde(default)]
291 #[schema(example = "example.com")]
292 pub domain: Option<String>,
293
294 #[schema(example = "pause while working on resolving a deliverability issue")]
296 pub reason: String,
297
298 #[serde(with = "duration_serde")]
299 pub duration: Duration,
301}
302
303#[derive(Serialize, Deserialize, Debug, ToSchema)]
304pub struct SuspendReadyQueueV1Request {
305 #[schema(
307 example = "source_name->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client"
308 )]
309 pub name: String,
310 #[schema(example = "pause while working on resolving a block with the destination postmaster")]
312 pub reason: String,
313 #[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 pub id: Uuid,
340 #[schema(
342 example = "source_name->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client"
343 )]
344 pub name: String,
345 #[schema(example = "pause while working on resolving a block with the destination postmaster")]
347 pub reason: String,
348
349 #[serde(with = "duration_serde")]
351 pub duration: Duration,
352
353 pub expires: DateTime<Utc>,
355}
356
357#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
358pub struct InspectMessageV1Request {
359 pub id: SpoolId,
362 #[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 pub id: SpoolId,
386 pub message: MessageInformation,
388}
389
390#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
391pub struct InspectQueueV1Request {
392 #[schema(example = "campaign_name:tenant_name@example.com")]
394 pub queue_name: String,
395 #[serde(default)]
398 pub want_body: bool,
399
400 #[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 #[schema(example = "sender@sender.example.com")]
440 pub sender: String,
441 #[schema(example = "recipient@example.com", format = "email")]
445 #[serde_as(as = "OneOrMany<_, PreferOne>")] pub recipient: Vec<String>,
447 #[schema(value_type=Object, example=json!({
449 "received_from": "10.0.0.1:3488"
450 }))]
451 pub meta: serde_json::Value,
452 #[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>")] #[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 AbbreviatedRead {
522 snippet: String,
524 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 AbbreviatedWrite {
550 snippet: String,
552 len: usize,
554 },
555}
556
557#[derive(Serialize, Deserialize, Debug, Default, ToSchema)]
558pub struct TraceSmtpClientV1Request {
559 #[serde(default)]
561 #[schema(example = "campaign_name")]
562 pub campaign: Vec<String>,
563
564 #[serde(default)]
566 #[schema(example = "tenant_name")]
567 pub tenant: Vec<String>,
568
569 #[serde(default)]
571 #[schema(example = "example.com")]
572 pub domain: Vec<String>,
573
574 #[serde(default)]
576 #[schema(example = "routing_domain.com")]
577 pub routing_domain: Vec<String>,
578
579 #[serde(default)]
581 #[schema(example = "pool_name")]
582 pub egress_pool: Vec<String>,
583
584 #[serde(default)]
586 #[schema(example = "source_name")]
587 pub egress_source: Vec<String>,
588
589 #[serde(default)]
591 #[schema(format = "email")]
592 pub mail_from: Vec<String>,
593
594 #[serde(default)]
596 #[schema(format = "email")]
597 pub rcpt_to: Vec<String>,
598
599 #[serde(default)]
601 #[schema(value_type=Option<Vec<String>>, example="10.0.0.1/16")]
602 pub source_addr: Option<CidrSet>,
603
604 #[serde(default)]
606 #[schema(format = "mx1.example.com")]
607 pub mx_host: Vec<String>,
608
609 #[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 #[serde(default)]
618 #[schema(value_type=Option<Vec<String>>, example="10.0.0.1/16")]
619 pub mx_addr: Option<CidrSet>,
620
621 #[serde(default, skip_serializing_if = "is_false")]
624 pub terse: bool,
625}
626
627#[derive(Serialize, Deserialize, Debug, ToSchema, IntoParams)]
628pub struct ReadyQueueStateRequest {
629 #[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#[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#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
676pub struct InspectReadyQV1Request {
677 #[schema(example = "unspecified->gmail.com@smtp_client")]
679 pub queue_name: String,
680 #[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#[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 #[serde(with = "duration_serde")]
711 pub watchdog_threshold: Duration,
712}
713
714#[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#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
738pub struct InspectReadyQV1Response {
739 pub queue_name: String,
740 pub mx: Option<crate::egress_path::MxResolution>,
744 pub egress_source: String,
745 pub egress_pool: String,
746 #[schema(example = "smtp_client")]
748 pub protocol: String,
749 pub state: ReadyQueueStateSnapshot,
750 #[schema(value_type = Object)]
753 pub path_config: crate::egress_path::EgressPathConfig,
754 pub constraints: crate::egress_path::EffectiveConstraints,
760 #[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#[derive(Serialize, Deserialize, Debug, ToSchema)]
773pub struct AbortReadyQConnV1Request {
774 pub queue_name: String,
775 pub session_id: Uuid,
776}
777
778#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
782pub struct ResolveEgressPathV1Request {
783 #[schema(example = "gmail.com")]
786 pub domain: String,
787
788 #[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#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
807pub struct ResolveEgressPathV1Response {
808 pub domain: String,
809 pub source: String,
810 pub mx: Option<crate::egress_path::MxResolution>,
814 pub queue_name: String,
818 #[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 #[schema(example = "9745bb48-14d7-48f2-a1fb-7df8d5844217")]
833 pub node_id: String,
834 #[schema(example = "mta1.example.com")]
836 pub hostname: String,
837 #[schema(example = "02:02:02:02:02:02")]
839 pub mac_address: String,
840 #[schema(example = 64)]
843 pub num_cores: usize,
844 #[schema(example = "6.8.0-1016-aws")]
846 pub kernel_version: Option<String>,
847 #[schema(example = "linux/x86_64")]
849 pub platform: String,
850 #[schema(example = "ubuntu")]
852 pub distribution: String,
853 #[schema(example = "Linux (Ubuntu 24.04)")]
855 pub os_version: String,
856 #[schema(example = 1003929600)]
858 pub total_memory_bytes: u64,
859 pub container_runtime: Option<String>,
862 #[schema(example = "Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz")]
865 pub cpu_brand: String,
866 #[schema(
870 example = "aws_instance_id=i-09aebefac97cf0000,machine_uid=ec22130d1de33cf52413457ac040000"
871 )]
872 pub fingerprint: String,
873 pub online_since: DateTime<Utc>,
875 #[schema(example = "kumod")]
877 pub process_kind: String,
878 #[schema(example = "2026.02.24-2d1a3174")]
880 pub version: String,
881}