kumo_log_types/
lib.rs

1use crate::rfc5965::ARFReport;
2use bounce_classify::BounceClass;
3use chrono::{DateTime, Utc};
4use kumo_address::host_or_socket::HostOrSocketAddress;
5use kumo_address::socket::SocketAddress;
6use rfc5321::Response;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use serde_with::formats::PreferOne;
10use serde_with::{serde_as, OneOrMany};
11use std::borrow::Cow;
12use std::collections::HashMap;
13use std::net::SocketAddr;
14use uuid::Uuid;
15
16pub mod rfc3464;
17pub mod rfc5965;
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20pub struct ResolvedAddress {
21    pub name: String,
22    pub addr: HostOrSocketAddress,
23    /// Whether the address was resolved via a DNSSEC-validated (secure) chain.
24    /// Used to decide DANE eligibility for the connection. Defaults to false
25    /// for addresses that did not originate from a secure DNS lookup (literals,
26    /// inbound peers, etc).
27    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
28    pub is_secure: bool,
29}
30
31impl std::fmt::Display for ResolvedAddress {
32    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
33        let addr = format!("{}", self.addr);
34        if addr == self.name {
35            // likely: unix domain socket path
36            write!(fmt, "{addr}")
37        } else {
38            write!(fmt, "{}/{addr}", self.name)
39        }
40    }
41}
42
43#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
44pub enum RecordType {
45    /// Recorded by a receiving listener
46    Reception,
47    /// Recorded by the delivery side, most likely as a
48    /// result of attempting a delivery to a remote host
49    Delivery,
50    Bounce,
51    TransientFailure,
52    /// Recorded when a message is expiring from the queue
53    Expiration,
54    /// Administratively failed
55    AdminBounce,
56    /// Contains information about an OOB bounce
57    OOB,
58    /// Contains a feedback report
59    Feedback,
60
61    /// SMTP Listener responded with a 4xx or 5xx
62    Rejection,
63
64    /// Administratively rebound from one queue to another
65    AdminRebind,
66
67    /// Moved from the special deferred injection queue
68    /// and into some other queue
69    DeferredInjectionRebind,
70
71    /// Explains why a message was put into the scheduled queue
72    Delayed,
73
74    /// Message was transferred to another kumomta node,
75    /// which is now responsible for delivery.
76    XferOut,
77
78    /// Message was received from another kumomta node,
79    /// and we are now responsible for delivery.
80    XferIn,
81
82    /// Special for matching anything in the logging config
83    Any,
84}
85
86impl RecordType {
87    /// Returns true if it makes sense to run the corresponding record
88    /// through the bounce classifier module.
89    /// The rule of thumb for that is if the response came from the
90    /// destination when attempting delivery, but we also include
91    /// administrative bounces and message expirations.
92    pub const fn is_bounce_classifiable(&self) -> bool {
93        match self {
94            Self::Any
95            | Self::Reception
96            | Self::Delivery
97            | Self::DeferredInjectionRebind
98            | Self::AdminRebind
99            | Self::XferOut
100            | Self::XferIn
101            | Self::Delayed => false,
102            Self::Bounce
103            | Self::TransientFailure
104            | Self::Expiration
105            | Self::AdminBounce
106            | Self::OOB
107            | Self::Feedback
108            | Self::Rejection => true,
109        }
110    }
111}
112
113/// Unfortunately, when we defined the `timestamp` and `created` fields
114/// in the log structure, we made the decision to log as the integer
115/// unix timestamp format, which causes us to discard the sub-second
116/// information that we otherwise have available.
117///
118/// We'd like to now include the full info in the serialized log
119/// record, without bloating the in-memory representation, or otherwise
120/// explicitly duplicating data to arrange for serde to emit it for us.
121///
122/// That's where this macro comes in; it allows us to serialize those
123/// fields via a proxy type that effectively causes serde to emit two
124/// different serializations of the same value.
125///
126/// Usage is: `ts_serializer(MODULE_NAME, STRUCT_NAME, SECONDS_FIELD, FULL_FIELD)`
127///
128/// The MODULE_NAME and STRUCT_NAME are not especially important and
129/// are really present just for namespacing.
130///
131/// The SECONDS_FIELD defines the name of the field to be emitted
132/// as the unix timestamp (in seconds).
133///
134/// The FULL_FIELD defines the name of the field to be emitted
135/// as the full RFC 3339 datetime.
136///
137/// The macro defines a module and struct that can be used as a proxy
138/// for serialization.
139///
140/// To actually use it, you need to annotate the field in the struct;
141///
142/// ```norun
143/// #[serde(flatten, with = "ts_serializer")]
144/// pub timestamp: DateTime<Utc>,
145/// ```
146///
147/// It is important that `flatten` is used to avoid serde emitting
148/// a nested/child struct, and the `with` attribute is what points
149/// the serialization to the defined proxy module; it must
150/// reference the MODULE_NAME you defined.
151macro_rules! ts_serializer {
152    ($module:ident, $name:ident, $seconds:ident, $full:ident) => {
153        mod $module {
154            use super::*;
155            use serde::{Deserializer, Serializer};
156
157            #[derive(Serialize, Deserialize, Copy, Clone, Eq, Hash, Default, Debug, PartialEq)]
158            struct $name {
159                #[serde(with = "chrono::serde::ts_seconds")]
160                pub $seconds: DateTime<Utc>,
161
162                /// Optional for backwards compatibility: we don't
163                /// expect $full to be present, but we'll take it
164                /// if it is!
165                #[serde(default)]
166                pub $full: Option<DateTime<Utc>>,
167            }
168
169            impl std::ops::Deref for $name {
170                type Target = DateTime<Utc>;
171                fn deref(&self) -> &DateTime<Utc> {
172                    self.$full.as_ref().unwrap_or(&self.$seconds)
173                }
174            }
175
176            impl<T: chrono::TimeZone> From<DateTime<T>> for $name
177            where
178                DateTime<Utc>: From<DateTime<T>>,
179            {
180                fn from(value: DateTime<T>) -> $name {
181                    let timestamp: DateTime<Utc> = value.into();
182                    $name {
183                        $seconds: timestamp,
184                        $full: Some(timestamp),
185                    }
186                }
187            }
188
189            impl From<$name> for DateTime<Utc> {
190                fn from(value: $name) -> DateTime<Utc> {
191                    *value
192                }
193            }
194
195            pub fn serialize<S>(d: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
196            where
197                S: Serializer,
198            {
199                let proxy: $name = (*d).into();
200                proxy.serialize(s)
201            }
202
203            pub fn deserialize<'a, D>(d: D) -> Result<DateTime<Utc>, D::Error>
204            where
205                D: Deserializer<'a>,
206            {
207                $name::deserialize(d).map(|p| p.into())
208            }
209        }
210    };
211}
212
213ts_serializer!(ts_serializer, TimestampSerializer, timestamp, event_time);
214ts_serializer!(ct_serializer, CreationTimeSerializer, created, created_time);
215
216#[serde_as]
217#[derive(Serialize, Deserialize, Debug, Clone)]
218pub struct JsonLogRecord {
219    /// What kind of record this is
220    #[serde(rename = "type")]
221    pub kind: RecordType,
222    /// The message id
223    pub id: String,
224    /// The envelope sender
225    pub sender: String,
226    /// The envelope recipient
227    #[serde_as(as = "OneOrMany<_, PreferOne>")]
228    pub recipient: Vec<String>,
229    /// Which named queue the message was associated with
230    pub queue: String,
231    /// Which MX site the message was being delivered to
232    pub site: String,
233    /// The size of the message, in bytes
234    pub size: u64,
235    /// The response from/to the peer
236    pub response: Response,
237    /// The address of the peer, and our sense of its
238    /// hostname or EHLO domain
239    pub peer_address: Option<ResolvedAddress>,
240    /// The time at which we are logging this event
241    #[serde(flatten, with = "ts_serializer")]
242    pub timestamp: DateTime<Utc>,
243    /// The time at which the message was initially received and created
244    #[serde(flatten, with = "ct_serializer")]
245    pub created: DateTime<Utc>,
246    /// The number of delivery attempts that have been made.
247    /// Note that this may be approximate after a restart; use the
248    /// number of logged events to determine the true number
249    pub num_attempts: u16,
250
251    pub bounce_classification: BounceClass,
252
253    pub egress_pool: Option<String>,
254    pub egress_source: Option<String>,
255    pub source_address: Option<MaybeProxiedSourceAddress>,
256
257    pub feedback_report: Option<Box<ARFReport>>,
258
259    pub meta: HashMap<String, Value>,
260    pub headers: HashMap<String, Value>,
261
262    /// The protocol used to deliver, or attempt to deliver, this message
263    pub delivery_protocol: Option<String>,
264
265    /// The protocol used to receive this message
266    pub reception_protocol: Option<String>,
267
268    /// The id of the node on which the event occurred
269    pub nodeid: Uuid,
270
271    /// The TLS Cipher used, if applicable
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub tls_cipher: Option<String>,
274
275    /// The TLS protocol version used, if applicable
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub tls_protocol_version: Option<String>,
278
279    /// The Subject Name from the peer TLS certificate, if applicable
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub tls_peer_subject_name: Option<Vec<String>>,
282
283    /// The provider name, if any.
284    /// This is a way of grouping destination sites operated
285    /// by the same provider.
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub provider_name: Option<String>,
288
289    /// Uuid identifying a connection/session for either inbound
290    /// or outbound (depending on the type of the record).
291    /// This is useful when correlating a series of messages to
292    /// the same connection for either ingress or egress
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub session_id: Option<Uuid>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct MaybeProxiedSourceAddress {
299    pub address: SocketAddress,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub server: Option<SocketAddr>,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub protocol: Option<Cow<'static, str>>,
304}
305
306#[cfg(all(test, target_pointer_width = "64"))]
307#[test]
308fn sizes() {
309    assert_eq!(std::mem::size_of::<JsonLogRecord>(), 720);
310}