kumo_log_types/
lib.rs

1use crate::rfc5965::ARFReport;
2use bounce_classify::BounceClass;
3use chrono::{DateTime, Utc};
4use kumo_address::host::HostAddress;
5use kumo_address::socket::SocketAddress;
6use rfc5321::Response;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::borrow::Cow;
10use std::collections::HashMap;
11use std::net::SocketAddr;
12use uuid::Uuid;
13
14pub mod rfc3464;
15pub mod rfc5965;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ResolvedAddress {
19    pub name: String,
20    pub addr: HostAddress,
21}
22
23impl std::fmt::Display for ResolvedAddress {
24    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
25        let addr = format!("{}", self.addr);
26        if addr == self.name {
27            // likely: unix domain socket path
28            write!(fmt, "{addr}")
29        } else {
30            write!(fmt, "{}/{addr}", self.name)
31        }
32    }
33}
34
35#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
36pub enum RecordType {
37    /// Recorded by a receiving listener
38    Reception,
39    /// Recorded by the delivery side, most likely as a
40    /// result of attempting a delivery to a remote host
41    Delivery,
42    Bounce,
43    TransientFailure,
44    /// Recorded when a message is expiring from the queue
45    Expiration,
46    /// Administratively failed
47    AdminBounce,
48    /// Contains information about an OOB bounce
49    OOB,
50    /// Contains a feedback report
51    Feedback,
52
53    /// SMTP Listener responded with a 4xx or 5xx
54    Rejection,
55
56    /// Administratively rebound from one queue to another
57    AdminRebind,
58
59    /// Moved from the special deferred injection queue
60    /// and into some other queue
61    DeferredInjectionRebind,
62
63    /// Explains why a message was put into the scheduled queue
64    Delayed,
65
66    /// Special for matching anything in the logging config
67    Any,
68}
69
70impl RecordType {
71    /// Returns true if it makes sense to run the corresponding record
72    /// through the bounce classifier module.
73    /// The rule of thumb for that is if the response came from the
74    /// destination when attempting delivery, but we also include
75    /// administrative bounces and message expirations.
76    pub const fn is_bounce_classifiable(&self) -> bool {
77        match self {
78            Self::Any
79            | Self::Reception
80            | Self::Delivery
81            | Self::DeferredInjectionRebind
82            | Self::AdminRebind
83            | Self::Delayed => false,
84            Self::Bounce
85            | Self::TransientFailure
86            | Self::Expiration
87            | Self::AdminBounce
88            | Self::OOB
89            | Self::Feedback
90            | Self::Rejection => true,
91        }
92    }
93}
94
95#[derive(Serialize, Deserialize, Debug, Clone)]
96pub struct JsonLogRecord {
97    /// What kind of record this is
98    #[serde(rename = "type")]
99    pub kind: RecordType,
100    /// The message id
101    pub id: String,
102    /// The envelope sender
103    pub sender: String,
104    /// The envelope recipient
105    pub recipient: String,
106    /// Which named queue the message was associated with
107    pub queue: String,
108    /// Which MX site the message was being delivered to
109    pub site: String,
110    /// The size of the message, in bytes
111    pub size: u64,
112    /// The response from/to the peer
113    pub response: Response,
114    /// The address of the peer, and our sense of its
115    /// hostname or EHLO domain
116    pub peer_address: Option<ResolvedAddress>,
117    /// The time at which we are logging this event
118    #[serde(with = "chrono::serde::ts_seconds")]
119    pub timestamp: DateTime<Utc>,
120    /// The time at which the message was initially received and created
121    #[serde(with = "chrono::serde::ts_seconds")]
122    pub created: DateTime<Utc>,
123    /// The number of delivery attempts that have been made.
124    /// Note that this may be approximate after a restart; use the
125    /// number of logged events to determine the true number
126    pub num_attempts: u16,
127
128    pub bounce_classification: BounceClass,
129
130    pub egress_pool: Option<String>,
131    pub egress_source: Option<String>,
132    pub source_address: Option<MaybeProxiedSourceAddress>,
133
134    pub feedback_report: Option<Box<ARFReport>>,
135
136    pub meta: HashMap<String, Value>,
137    pub headers: HashMap<String, Value>,
138
139    /// The protocol used to deliver, or attempt to deliver, this message
140    pub delivery_protocol: Option<String>,
141
142    /// The protocol used to receive this message
143    pub reception_protocol: Option<String>,
144
145    /// The id of the node on which the event occurred
146    pub nodeid: Uuid,
147
148    /// The TLS Cipher used, if applicable
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub tls_cipher: Option<String>,
151
152    /// The TLS protocol version used, if applicable
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub tls_protocol_version: Option<String>,
155
156    /// The Subject Name from the peer TLS certificate, if applicable
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub tls_peer_subject_name: Option<Vec<String>>,
159
160    /// The provider name, if any.
161    /// This is a way of grouping destination sites operated
162    /// by the same provider.
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub provider_name: Option<String>,
165
166    /// Uuid identifying a connection/session for either inbound
167    /// or outbound (depending on the type of the record).
168    /// This is useful when correlating a series of messages to
169    /// the same connection for either ingress or egress
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub session_id: Option<Uuid>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct MaybeProxiedSourceAddress {
176    pub address: SocketAddress,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub server: Option<SocketAddr>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub protocol: Option<Cow<'static, str>>,
181}
182
183#[cfg(all(test, target_pointer_width = "64"))]
184#[test]
185fn sizes() {
186    assert_eq!(std::mem::size_of::<JsonLogRecord>(), 712);
187}