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 SuspendV1Request {
194 #[serde(default)]
196 #[schema(example = "campaign_name")]
197 pub campaign: Option<String>,
198 #[serde(default)]
200 #[schema(example = "tenant_name")]
201 pub tenant: Option<String>,
202 #[serde(default)]
204 #[schema(example = "example.com")]
205 pub domain: Option<String>,
206
207 #[schema(example = "pause while working on resolving a block with the destination postmaster")]
209 pub reason: String,
210
211 #[serde(
213 default,
214 with = "duration_serde",
215 skip_serializing_if = "Option::is_none"
216 )]
217 pub duration: Option<Duration>,
218
219 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub expires: Option<DateTime<Utc>>,
223
224 #[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 pub id: Uuid,
246}
247
248#[derive(Serialize, Deserialize, Debug, ToSchema)]
249pub struct SuspendV1CancelRequest {
250 pub id: Uuid,
252}
253
254#[derive(Serialize, Deserialize, Debug, ToResponse, ToSchema)]
255pub struct InjectV1Response {
256 pub success_count: usize,
258 pub fail_count: usize,
260
261 #[schema(format = "email")]
263 pub failed_recipients: Vec<String>,
264
265 pub errors: Vec<String>,
267}
268
269#[derive(Serialize, Deserialize, Debug, ToSchema)]
270pub struct SuspendV1ListEntry {
271 pub id: Uuid,
274
275 #[serde(default)]
277 #[schema(example = "campaign_name")]
278 pub campaign: Option<String>,
279 #[serde(default)]
281 #[schema(example = "tenant_name")]
282 pub tenant: Option<String>,
283 #[serde(default)]
285 #[schema(example = "example.com")]
286 pub domain: Option<String>,
287
288 #[schema(example = "pause while working on resolving a deliverability issue")]
290 pub reason: String,
291
292 #[serde(with = "duration_serde")]
293 pub duration: Duration,
295}
296
297#[derive(Serialize, Deserialize, Debug, ToSchema)]
298pub struct SuspendReadyQueueV1Request {
299 #[schema(
301 example = "source_name->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client"
302 )]
303 pub name: String,
304 #[schema(example = "pause while working on resolving a block with the destination postmaster")]
306 pub reason: String,
307 #[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 pub id: Uuid,
334 #[schema(
336 example = "source_name->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client"
337 )]
338 pub name: String,
339 #[schema(example = "pause while working on resolving a block with the destination postmaster")]
341 pub reason: String,
342
343 #[serde(with = "duration_serde")]
345 pub duration: Duration,
346
347 pub expires: DateTime<Utc>,
349}
350
351#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
352pub struct InspectMessageV1Request {
353 pub id: SpoolId,
356 #[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 pub id: SpoolId,
380 pub message: MessageInformation,
382}
383
384#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
385pub struct InspectQueueV1Request {
386 #[schema(example = "campaign_name:tenant_name@example.com")]
388 pub queue_name: String,
389 #[serde(default)]
392 pub want_body: bool,
393
394 #[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 #[schema(example = "sender@sender.example.com")]
434 pub sender: String,
435 #[schema(example = "recipient@example.com", format = "email")]
439 #[serde_as(as = "OneOrMany<_, PreferOne>")] pub recipient: Vec<String>,
441 #[schema(value_type=Object, example=json!({
443 "received_from": "10.0.0.1:3488"
444 }))]
445 pub meta: serde_json::Value,
446 #[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>")] #[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 AbbreviatedRead {
516 snippet: String,
518 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 AbbreviatedWrite {
544 snippet: String,
546 len: usize,
548 },
549}
550
551#[derive(Serialize, Deserialize, Debug, ToSchema)]
552pub struct TraceSmtpClientV1Request {
553 #[serde(default)]
555 #[schema(example = "campaign_name")]
556 pub campaign: Vec<String>,
557
558 #[serde(default)]
560 #[schema(example = "tenant_name")]
561 pub tenant: Vec<String>,
562
563 #[serde(default)]
565 #[schema(example = "example.com")]
566 pub domain: Vec<String>,
567
568 #[serde(default)]
570 #[schema(example = "routing_domain.com")]
571 pub routing_domain: Vec<String>,
572
573 #[serde(default)]
575 #[schema(example = "pool_name")]
576 pub egress_pool: Vec<String>,
577
578 #[serde(default)]
580 #[schema(example = "source_name")]
581 pub egress_source: Vec<String>,
582
583 #[serde(default)]
585 #[schema(format = "email")]
586 pub mail_from: Vec<String>,
587
588 #[serde(default)]
590 #[schema(format = "email")]
591 pub rcpt_to: Vec<String>,
592
593 #[serde(default)]
595 #[schema(value_type=Option<Vec<String>>, example="10.0.0.1/16")]
596 pub source_addr: Option<CidrSet>,
597
598 #[serde(default)]
600 #[schema(format = "mx1.example.com")]
601 pub mx_host: Vec<String>,
602
603 #[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 #[serde(default)]
612 #[schema(value_type=Option<Vec<String>>, example="10.0.0.1/16")]
613 pub mx_addr: Option<CidrSet>,
614
615 #[serde(default, skip_serializing_if = "is_false")]
618 pub terse: bool,
619}
620
621#[derive(Serialize, Deserialize, Debug, ToSchema, IntoParams)]
622pub struct ReadyQueueStateRequest {
623 #[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 #[schema(example = "9745bb48-14d7-48f2-a1fb-7df8d5844217")]
654 pub node_id: String,
655 #[schema(example = "mta1.example.com")]
657 pub hostname: String,
658 #[schema(example = "02:02:02:02:02:02")]
660 pub mac_address: String,
661 #[schema(example = 64)]
664 pub num_cores: usize,
665 #[schema(example = "6.8.0-1016-aws")]
667 pub kernel_version: Option<String>,
668 #[schema(example = "linux/x86_64")]
670 pub platform: String,
671 #[schema(example = "ubuntu")]
673 pub distribution: String,
674 #[schema(example = "Linux (Ubuntu 24.04)")]
676 pub os_version: String,
677 #[schema(example = 1003929600)]
679 pub total_memory_bytes: u64,
680 pub container_runtime: Option<String>,
683 #[schema(example = "Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz")]
686 pub cpu_brand: String,
687 #[schema(
691 example = "aws_instance_id=i-09aebefac97cf0000,machine_uid=ec22130d1de33cf52413457ac040000"
692 )]
693 pub fingerprint: String,
694 pub online_since: DateTime<Utc>,
696 #[schema(example = "kumod")]
698 pub process_kind: String,
699 #[schema(example = "2026.02.24-2d1a3174")]
701 pub version: String,
702}