imap_types/
datetime.rs

1//! Date and time-related types.
2
3use std::fmt::{Debug, Formatter};
4
5#[cfg(feature = "bounded-static")]
6use bounded_static::{IntoBoundedStatic, ToBoundedStatic};
7use chrono::{Datelike, FixedOffset};
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11use crate::datetime::error::{DateTimeError, NaiveDateError};
12
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14#[derive(Clone, Eq, PartialEq, Hash)]
15pub struct DateTime(chrono::DateTime<FixedOffset>);
16
17impl DateTime {
18    pub fn validate(value: &chrono::DateTime<FixedOffset>) -> Result<(), DateTimeError> {
19        // Only a subset of `chrono`s `DateTime<FixedOffset>` is valid in IMAP.
20        if !(0..=9999).contains(&value.year()) {
21            return Err(DateTimeError::YearOutOfRange { got: value.year() });
22        }
23
24        if value.timestamp_subsec_nanos() != 0 {
25            return Err(DateTimeError::UnalignedNanoSeconds {
26                got: value.timestamp_subsec_nanos(),
27            });
28        }
29
30        if value.offset().local_minus_utc() % 60 != 0 {
31            return Err(DateTimeError::UnalignedOffset {
32                got: value.offset().local_minus_utc() % 60,
33            });
34        }
35
36        Ok(())
37    }
38
39    /// Constructs a date time without validation.
40    ///
41    /// # Warning: IMAP conformance
42    ///
43    /// The caller must ensure that `value` is valid according to [`Self::validate`]. Failing to do
44    /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows.
45    /// Do not call this constructor with untrusted data.
46    #[cfg(feature = "unvalidated")]
47    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
48    pub fn unvalidated(value: chrono::DateTime<FixedOffset>) -> Self {
49        Self(value)
50    }
51}
52
53impl TryFrom<chrono::DateTime<FixedOffset>> for DateTime {
54    type Error = DateTimeError;
55
56    fn try_from(value: chrono::DateTime<FixedOffset>) -> Result<Self, Self::Error> {
57        Self::validate(&value)?;
58
59        Ok(Self(value))
60    }
61}
62
63impl Debug for DateTime {
64    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65        Debug::fmt(&self.0, f)
66    }
67}
68
69impl AsRef<chrono::DateTime<FixedOffset>> for DateTime {
70    fn as_ref(&self) -> &chrono::DateTime<FixedOffset> {
71        &self.0
72    }
73}
74
75#[cfg(feature = "bounded-static")]
76impl IntoBoundedStatic for DateTime {
77    type Static = Self;
78
79    fn into_static(self) -> Self::Static {
80        self
81    }
82}
83
84#[cfg(feature = "bounded-static")]
85impl ToBoundedStatic for DateTime {
86    type Static = Self;
87
88    fn to_static(&self) -> Self::Static {
89        self.clone()
90    }
91}
92
93#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
94#[derive(Clone, Eq, PartialEq, Hash)]
95pub struct NaiveDate(chrono::NaiveDate);
96
97impl NaiveDate {
98    pub fn validate(value: &chrono::NaiveDate) -> Result<(), NaiveDateError> {
99        // Only a subset of `chrono`s `NaiveDate` is valid in IMAP.
100        if !(0..=9999).contains(&value.year()) {
101            return Err(NaiveDateError::YearOutOfRange { got: value.year() });
102        }
103
104        Ok(())
105    }
106
107    /// Constructs a naive date without validation.
108    ///
109    /// # Warning: IMAP conformance
110    ///
111    /// The caller must ensure that `value` is valid according to [`Self::validate`]. Failing to do
112    /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows.
113    /// Do not call this constructor with untrusted data.
114    #[cfg(feature = "unvalidated")]
115    #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
116    pub fn unvalidated(value: chrono::NaiveDate) -> Self {
117        Self(value)
118    }
119}
120
121impl TryFrom<chrono::NaiveDate> for NaiveDate {
122    type Error = NaiveDateError;
123
124    fn try_from(value: chrono::NaiveDate) -> Result<Self, Self::Error> {
125        Self::validate(&value)?;
126
127        Ok(Self(value))
128    }
129}
130
131impl Debug for NaiveDate {
132    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
133        Debug::fmt(&self.0, f)
134    }
135}
136
137impl AsRef<chrono::NaiveDate> for NaiveDate {
138    fn as_ref(&self) -> &chrono::NaiveDate {
139        &self.0
140    }
141}
142
143#[cfg(feature = "bounded-static")]
144impl IntoBoundedStatic for NaiveDate {
145    type Static = Self;
146
147    fn into_static(self) -> Self::Static {
148        self
149    }
150}
151
152#[cfg(feature = "bounded-static")]
153impl ToBoundedStatic for NaiveDate {
154    type Static = Self;
155
156    fn to_static(&self) -> Self::Static {
157        self.clone()
158    }
159}
160
161/// Error-related types.
162pub mod error {
163    use thiserror::Error;
164
165    #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
166    pub enum DateTimeError {
167        #[error("expected `0 <= year <= 9999`, got {got}")]
168        YearOutOfRange { got: i32 },
169        #[error("expected `nanos == 0`, got {got}")]
170        UnalignedNanoSeconds { got: u32 },
171        #[error("expected `offset % 60 == 0`, got {got}")]
172        UnalignedOffset { got: i32 },
173    }
174
175    #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
176    pub enum NaiveDateError {
177        #[error("expected `0 <= year <= 9999`, got {got}")]
178        YearOutOfRange { got: i32 },
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use chrono::{TimeZone, Timelike};
185
186    use super::*;
187
188    #[test]
189    fn test_conversion_date_time_failing() {
190        let tests = [
191            (
192                DateTime::try_from(
193                    chrono::FixedOffset::east_opt(3600)
194                        .unwrap()
195                        .from_local_datetime(&chrono::NaiveDateTime::new(
196                            chrono::NaiveDate::from_ymd_opt(-1, 2, 1).unwrap(),
197                            chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
198                        ))
199                        .unwrap(),
200                ),
201                DateTimeError::YearOutOfRange { got: -1 },
202            ),
203            (
204                DateTime::try_from(
205                    chrono::FixedOffset::east_opt(3600)
206                        .unwrap()
207                        .from_local_datetime(&chrono::NaiveDateTime::new(
208                            chrono::NaiveDate::from_ymd_opt(10000, 2, 1).unwrap(),
209                            chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
210                        ))
211                        .unwrap(),
212                ),
213                DateTimeError::YearOutOfRange { got: 10000 },
214            ),
215            (
216                DateTime::try_from(
217                    chrono::FixedOffset::east_opt(1)
218                        .unwrap()
219                        .from_local_datetime(&chrono::NaiveDateTime::new(
220                            chrono::NaiveDate::from_ymd_opt(0, 2, 1).unwrap(),
221                            chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
222                        ))
223                        .unwrap(),
224                ),
225                DateTimeError::UnalignedOffset { got: 1 },
226            ),
227            (
228                DateTime::try_from(
229                    chrono::FixedOffset::east_opt(59)
230                        .unwrap()
231                        .from_local_datetime(&chrono::NaiveDateTime::new(
232                            chrono::NaiveDate::from_ymd_opt(9999, 2, 1).unwrap(),
233                            chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
234                        ))
235                        .unwrap(),
236                ),
237                DateTimeError::UnalignedOffset { got: 59 },
238            ),
239            (
240                DateTime::try_from(
241                    chrono::FixedOffset::east_opt(60)
242                        .unwrap()
243                        .from_local_datetime(&chrono::NaiveDateTime::new(
244                            chrono::NaiveDate::from_ymd_opt(0, 2, 1).unwrap(),
245                            chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
246                        ))
247                        .unwrap()
248                        .with_nanosecond(1)
249                        .unwrap(),
250                ),
251                DateTimeError::UnalignedNanoSeconds { got: 1 },
252            ),
253        ];
254
255        for (got, expected) in tests {
256            println!("{}", got.clone().unwrap_err());
257            println!("{:?}", got.clone().unwrap_err());
258            assert_eq!(expected, got.unwrap_err());
259        }
260    }
261}