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, ToSchema)]
255pub struct SuspendV1ListEntry {
256 pub id: Uuid,
259
260 #[serde(default)]
262 #[schema(example = "campaign_name")]
263 pub campaign: Option<String>,
264 #[serde(default)]
266 #[schema(example = "tenant_name")]
267 pub tenant: Option<String>,
268 #[serde(default)]
270 #[schema(example = "example.com")]
271 pub domain: Option<String>,
272
273 #[schema(example = "pause while working on resolving a deliverability issue")]
275 pub reason: String,
276
277 #[serde(with = "duration_serde")]
278 pub duration: Duration,
280}
281
282#[derive(Serialize, Deserialize, Debug, ToSchema)]
283pub struct SuspendReadyQueueV1Request {
284 #[schema(
286 example = "source_name->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client"
287 )]
288 pub name: String,
289 #[schema(example = "pause while working on resolving a block with the destination postmaster")]
291 pub reason: String,
292 #[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 pub id: Uuid,
319 #[schema(
321 example = "source_name->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client"
322 )]
323 pub name: String,
324 #[schema(example = "pause while working on resolving a block with the destination postmaster")]
326 pub reason: String,
327
328 #[serde(with = "duration_serde")]
330 pub duration: Duration,
331
332 pub expires: DateTime<Utc>,
334}
335
336#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
337pub struct InspectMessageV1Request {
338 pub id: SpoolId,
341 #[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 pub id: SpoolId,
365 pub message: MessageInformation,
367}
368
369#[derive(Serialize, Deserialize, Debug, IntoParams, ToSchema)]
370pub struct InspectQueueV1Request {
371 #[schema(example = "campaign_name:tenant_name@example.com")]
373 pub queue_name: String,
374 #[serde(default)]
377 pub want_body: bool,
378
379 #[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 #[schema(example = "sender@sender.example.com")]
419 pub sender: String,
420 #[schema(example = "recipient@example.com", format = "email")]
424 #[serde_as(as = "OneOrMany<_, PreferOne>")] pub recipient: Vec<String>,
426 #[schema(value_type=Object, example=json!({
428 "received_from": "10.0.0.1:3488"
429 }))]
430 pub meta: serde_json::Value,
431 #[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>")] #[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 AbbreviatedRead {
501 snippet: String,
503 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 AbbreviatedWrite {
529 snippet: String,
531 len: usize,
533 },
534}
535
536#[derive(Serialize, Deserialize, Debug, ToSchema)]
537pub struct TraceSmtpClientV1Request {
538 #[serde(default)]
540 #[schema(example = "campaign_name")]
541 pub campaign: Vec<String>,
542
543 #[serde(default)]
545 #[schema(example = "tenant_name")]
546 pub tenant: Vec<String>,
547
548 #[serde(default)]
550 #[schema(example = "example.com")]
551 pub domain: Vec<String>,
552
553 #[serde(default)]
555 #[schema(example = "routing_domain.com")]
556 pub routing_domain: Vec<String>,
557
558 #[serde(default)]
560 #[schema(example = "pool_name")]
561 pub egress_pool: Vec<String>,
562
563 #[serde(default)]
565 #[schema(example = "source_name")]
566 pub egress_source: Vec<String>,
567
568 #[serde(default)]
570 #[schema(format = "email")]
571 pub mail_from: Vec<String>,
572
573 #[serde(default)]
575 #[schema(format = "email")]
576 pub rcpt_to: Vec<String>,
577
578 #[serde(default)]
580 #[schema(value_type=Option<Vec<String>>, example="10.0.0.1/16")]
581 pub source_addr: Option<CidrSet>,
582
583 #[serde(default)]
585 #[schema(format = "mx1.example.com")]
586 pub mx_host: Vec<String>,
587
588 #[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 #[serde(default)]
597 #[schema(value_type=Option<Vec<String>>, example="10.0.0.1/16")]
598 pub mx_addr: Option<CidrSet>,
599
600 #[serde(default, skip_serializing_if = "is_false")]
603 pub terse: bool,
604}
605
606#[derive(Serialize, Deserialize, Debug, ToSchema, IntoParams)]
607pub struct ReadyQueueStateRequest {
608 #[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 #[schema(example = "9745bb48-14d7-48f2-a1fb-7df8d5844217")]
639 pub node_id: String,
640 #[schema(example = "mta1.example.com")]
642 pub hostname: String,
643 #[schema(example = "02:02:02:02:02:02")]
645 pub mac_address: String,
646 #[schema(example = 64)]
649 pub num_cores: usize,
650 #[schema(example = "6.8.0-1016-aws")]
652 pub kernel_version: Option<String>,
653 #[schema(example = "linux/x86_64")]
655 pub platform: String,
656 #[schema(example = "ubuntu")]
658 pub distribution: String,
659 #[schema(example = "Linux (Ubuntu 24.04)")]
661 pub os_version: String,
662 #[schema(example = 1003929600)]
664 pub total_memory_bytes: u64,
665 pub container_runtime: Option<String>,
668 #[schema(example = "Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz")]
671 pub cpu_brand: String,
672 #[schema(
676 example = "aws_instance_id=i-09aebefac97cf0000,machine_uid=ec22130d1de33cf52413457ac040000"
677 )]
678 pub fingerprint: String,
679 pub online_since: DateTime<Utc>,
681 #[schema(example = "kumod")]
683 pub process_kind: String,
684 #[schema(example = "2026.02.24-2d1a3174")]
686 pub version: String,
687}