Skip to main content

guerrillamail_client/
models.rs

1//! Wire models returned by GuerrillaMail API calls used by [`Client`](crate::Client).
2
3use serde::Deserialize;
4use serde::Deserializer;
5use std::fmt;
6
7/// An email message header returned by GuerrillaMail.
8#[derive(Debug, Clone, Deserialize)]
9pub struct Message {
10    /// Unique message ID.
11    pub mail_id: String,
12    /// Sender email address.
13    pub mail_from: String,
14    /// Email subject line.
15    pub mail_subject: String,
16    /// Short excerpt of the email body.
17    pub mail_excerpt: String,
18    /// Unix timestamp in seconds (string) of when the email was received.
19    pub mail_timestamp: String,
20}
21
22/// Attachment metadata returned by GuerrillaMail.
23#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
24pub struct Attachment {
25    /// Original filename.
26    #[serde(default, rename = "f")]
27    pub filename: String,
28    /// MIME type or server-provided hint (may be a fallback, not always a strict MIME type).
29    #[serde(default, rename = "t")]
30    pub content_type_or_hint: Option<String>,
31    /// Attachment part ID used for download.
32    #[serde(default, rename = "p")]
33    pub part_id: String,
34}
35
36#[derive(Deserialize)]
37#[serde(untagged)]
38enum StrOrNumU32 {
39    Str(String),
40    Num(u64),
41}
42
43fn de_u32_str_or_num_opt<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
44where
45    D: Deserializer<'de>,
46{
47    let value = Option::<StrOrNumU32>::deserialize(deserializer)?;
48    match value {
49        None => Ok(None),
50        Some(StrOrNumU32::Str(raw)) => raw
51            .trim()
52            .parse::<u32>()
53            .map(Some)
54            .map_err(serde::de::Error::custom),
55        Some(StrOrNumU32::Num(num)) => u32::try_from(num)
56            .map(Some)
57            .map_err(serde::de::Error::custom),
58    }
59}
60
61/// Full email details including body content.
62#[derive(Clone, Deserialize)]
63pub struct EmailDetails {
64    /// Unique message ID.
65    pub mail_id: String,
66    /// Sender email address.
67    pub mail_from: String,
68    /// Email subject line.
69    pub mail_subject: String,
70    /// Full HTML body of the email.
71    pub mail_body: String,
72    /// Unix timestamp in seconds (string) of when the email was received.
73    pub mail_timestamp: String,
74    /// Attachment metadata entries (if any); see [`Attachment`].
75    #[serde(default, rename = "att_info")]
76    pub attachments: Vec<Attachment>,
77    /// Attachment count (if provided by API).
78    #[serde(default, rename = "att", deserialize_with = "de_u32_str_or_num_opt")]
79    pub attachment_count: Option<u32>,
80    /// Session token sometimes returned by the API.
81    #[serde(default)]
82    pub sid_token: Option<String>,
83}
84
85impl fmt::Debug for EmailDetails {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.debug_struct("EmailDetails")
88            .field("mail_id", &self.mail_id)
89            .field("mail_from", &self.mail_from)
90            .field("mail_subject", &self.mail_subject)
91            .field("mail_body", &self.mail_body)
92            .field("mail_timestamp", &self.mail_timestamp)
93            .field("attachments", &self.attachments)
94            .field("attachment_count", &self.attachment_count)
95            .field("sid_token", &self.sid_token.as_ref().map(|_| "<redacted>"))
96            .finish()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use serde_json::json;
104
105    #[test]
106    fn email_details_deserialize_without_attachments() {
107        let value = json!({
108            "mail_id": "123",
109            "mail_from": "sender@example.com",
110            "mail_subject": "Hello",
111            "mail_body": "<p>Body</p>",
112            "mail_timestamp": "1700000000"
113        });
114
115        let details: EmailDetails = serde_json::from_value(value).unwrap();
116        assert_eq!(details.mail_id, "123");
117        assert!(details.attachments.is_empty());
118        assert!(details.attachment_count.is_none());
119        assert!(details.sid_token.is_none());
120    }
121
122    #[test]
123    fn email_details_deserialize_with_attachments() {
124        let value = json!({
125            "mail_id": "123",
126            "mail_from": "sender@example.com",
127            "mail_subject": "Hello",
128            "mail_body": "<p>Body</p>",
129            "mail_timestamp": "1700000000",
130            "att": 1,
131            "att_info": [
132                { "f": "file.txt", "t": "text/plain", "p": "99" }
133            ],
134            "sid_token": "sid123"
135        });
136
137        let details: EmailDetails = serde_json::from_value(value).unwrap();
138        assert_eq!(details.attachment_count, Some(1));
139        assert_eq!(
140            details.attachments,
141            vec![Attachment {
142                filename: "file.txt".to_string(),
143                content_type_or_hint: Some("text/plain".to_string()),
144                part_id: "99".to_string(),
145            }]
146        );
147        assert_eq!(details.sid_token.as_deref(), Some("sid123"));
148    }
149
150    #[test]
151    fn email_details_deserialize_attachment_count_string() {
152        let value = json!({
153            "mail_id": "123",
154            "mail_from": "sender@example.com",
155            "mail_subject": "Hello",
156            "mail_body": "<p>Body</p>",
157            "mail_timestamp": "1700000000",
158            "att": "1"
159        });
160
161        let details: EmailDetails = serde_json::from_value(value).unwrap();
162        assert_eq!(details.attachment_count, Some(1));
163    }
164
165    #[test]
166    fn email_details_deserialize_attachment_count_missing() {
167        let value = json!({
168            "mail_id": "123",
169            "mail_from": "sender@example.com",
170            "mail_subject": "Hello",
171            "mail_body": "<p>Body</p>",
172            "mail_timestamp": "1700000000"
173        });
174
175        let details: EmailDetails = serde_json::from_value(value).unwrap();
176        assert!(details.attachment_count.is_none());
177    }
178}