Skip to main content

io_email/message/imap/
add.rs

1//! IMAP message-add coroutine wrapping APPEND (RFC 3501 ยง6.3.11).
2//!
3//! Two Message-ID safety nets:
4//!
5//! 1. A missing Message-ID: header is replaced by a synthetic
6//!    <uuid@io-email.invalid> so the message stays addressable.
7//! 2. When UIDPLUS (RFC 4315) is missing, falls back to SELECT +
8//!    UID SEARCH HEADER Message-ID and takes the highest UID.
9//!
10//! # Example
11//!
12//! ```rust,ignore
13//! use io_email::message::imap::add::ImapMessageAdd;
14//!
15//! let uid = client.run(ImapMessageAdd::new("INBOX", &flags, raw)?)?;
16//! ```
17
18use alloc::{
19    format,
20    string::{String, ToString},
21    vec::Vec,
22};
23use core::mem;
24
25use io_imap::{
26    codec::fragmentizer::Fragmentizer,
27    coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
28    rfc3501::{
29        append::{ImapMessageAppend, ImapMessageAppendError, ImapMessageAppendOptions},
30        search::{ImapMessageSearch, ImapMessageSearchError, ImapMessageSearchOptions},
31        select::{ImapMailboxSelect, ImapMailboxSelectError, ImapMailboxSelectOptions},
32    },
33    types::{
34        core::{AString, Literal, Vec1},
35        extensions::binary::LiteralOrLiteral8,
36        search::SearchKey,
37    },
38};
39use log::trace;
40use mail_parser::MessageParser;
41use thiserror::Error;
42use uuid::Uuid;
43
44use crate::{
45    flag::types::Flag,
46    imap::convert::{InvalidMailboxName, flag_from, parse_mailbox},
47};
48
49/// Errors produced by [`ImapMessageAdd`].
50#[derive(Debug, Error)]
51pub enum ImapMessageAddError {
52    #[error(transparent)]
53    Append(#[from] ImapMessageAppendError),
54    #[error(transparent)]
55    Select(#[from] ImapMailboxSelectError),
56    #[error(transparent)]
57    Search(#[from] ImapMessageSearchError),
58    #[error("invalid IMAP mailbox `{0}`")]
59    InvalidMailbox(String),
60    #[error("invalid message content: {0}")]
61    InvalidContent(String),
62    #[error("fallback UID search returned no match (server omitted UIDPLUS)")]
63    UidLookupFailed,
64    #[error("coroutine was resumed after completion")]
65    ResumedAfterDone,
66}
67
68impl From<InvalidMailboxName> for ImapMessageAddError {
69    fn from(err: InvalidMailboxName) -> Self {
70        Self::InvalidMailbox(err.0)
71    }
72}
73
74/// I/O-free coroutine appending a raw RFC 5322 message and resolving
75/// the appended UID.
76pub struct ImapMessageAdd {
77    state: State,
78    mailbox: String,
79    message_id: String,
80}
81
82impl ImapMessageAdd {
83    /// `flags` apply to the appended message; pass an empty slice for
84    /// none. `raw` must be a valid RFC 5322 message.
85    pub fn new(
86        mailbox: &str,
87        flags: &[Flag],
88        mut raw: Vec<u8>,
89    ) -> Result<Self, ImapMessageAddError> {
90        trace!("prepare IMAP message add");
91
92        // NOTE: extract or synthesize Message-ID up front; needed as a
93        // fallback to recover the UID when UIDPLUS is missing.
94        let parsed_id = MessageParser::default()
95            .parse_headers(&raw)
96            .and_then(|m| m.message_id().map(ToString::to_string))
97            .filter(|s| !s.is_empty());
98
99        let message_id = match parsed_id {
100            Some(id) => id,
101            None => {
102                let generated = format!("{}@io-email.invalid", Uuid::new_v4());
103                trace!("appended message had no Message-ID; injected <{generated}>");
104                let header = format!("Message-ID: <{generated}>\r\n");
105                raw.splice(0..0, header.bytes());
106                generated
107            }
108        };
109
110        let mbox = parse_mailbox(mailbox)?;
111        let imap_flags: Vec<_> = flags.iter().map(flag_from).collect();
112        let literal = Literal::try_from(raw)
113            .map_err(|err| ImapMessageAddError::InvalidContent(err.to_string()))?;
114        let message = LiteralOrLiteral8::Literal(literal);
115        let append = ImapMessageAppend::new(
116            mbox,
117            message,
118            ImapMessageAppendOptions {
119                flags: imap_flags,
120                date: None,
121            },
122        );
123
124        Ok(Self {
125            state: State::Appending(append),
126            mailbox: mailbox.to_string(),
127            message_id,
128        })
129    }
130}
131
132enum State {
133    Appending(ImapMessageAppend),
134    SelectingForLookup { select: ImapMailboxSelect },
135    Searching(ImapMessageSearch),
136    Done,
137}
138
139impl ImapCoroutine for ImapMessageAdd {
140    type Yield = ImapYield;
141    type Return = Result<String, ImapMessageAddError>;
142
143    fn resume(
144        &mut self,
145        fragmentizer: &mut Fragmentizer,
146        mut bytes: Option<&[u8]>,
147    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
148        loop {
149            match mem::replace(&mut self.state, State::Done) {
150                State::Appending(mut append) => match append.resume(fragmentizer, bytes.take()) {
151                    ImapCoroutineState::Yielded(yielded) => {
152                        self.state = State::Appending(append);
153                        return ImapCoroutineState::Yielded(yielded);
154                    }
155                    ImapCoroutineState::Complete(Ok((_exists, appenduid))) => {
156                        if let Some((_, uid)) = appenduid {
157                            return ImapCoroutineState::Complete(Ok(uid.to_string()));
158                        }
159                        let mbox = match parse_mailbox(&self.mailbox) {
160                            Ok(m) => m,
161                            Err(err) => return ImapCoroutineState::Complete(Err(err.into())),
162                        };
163                        self.state = State::SelectingForLookup {
164                            select: ImapMailboxSelect::new(
165                                mbox,
166                                ImapMailboxSelectOptions::default(),
167                            ),
168                        };
169                    }
170                    ImapCoroutineState::Complete(Err(err)) => {
171                        return ImapCoroutineState::Complete(Err(err.into()));
172                    }
173                },
174                State::SelectingForLookup { mut select } => {
175                    match select.resume(fragmentizer, bytes.take()) {
176                        ImapCoroutineState::Yielded(yielded) => {
177                            self.state = State::SelectingForLookup { select };
178                            return ImapCoroutineState::Yielded(yielded);
179                        }
180                        ImapCoroutineState::Complete(Ok(_)) => {
181                            let Ok(field) = AString::try_from("Message-ID") else {
182                                return ImapCoroutineState::Complete(Err(
183                                    ImapMessageAddError::UidLookupFailed,
184                                ));
185                            };
186                            let Ok(value) = AString::try_from(self.message_id.clone()) else {
187                                return ImapCoroutineState::Complete(Err(
188                                    ImapMessageAddError::UidLookupFailed,
189                                ));
190                            };
191                            let criteria = Vec1::from(SearchKey::Header(field, value));
192                            self.state = State::Searching(ImapMessageSearch::new(
193                                criteria,
194                                ImapMessageSearchOptions { uid: true },
195                            ));
196                        }
197                        ImapCoroutineState::Complete(Err(err)) => {
198                            return ImapCoroutineState::Complete(Err(err.into()));
199                        }
200                    }
201                }
202                State::Searching(mut search) => match search.resume(fragmentizer, bytes.take()) {
203                    ImapCoroutineState::Yielded(yielded) => {
204                        self.state = State::Searching(search);
205                        return ImapCoroutineState::Yielded(yielded);
206                    }
207                    ImapCoroutineState::Complete(Ok(uids)) => match uids.into_iter().max() {
208                        Some(uid) => return ImapCoroutineState::Complete(Ok(uid.to_string())),
209                        None => {
210                            return ImapCoroutineState::Complete(Err(
211                                ImapMessageAddError::UidLookupFailed,
212                            ));
213                        }
214                    },
215                    ImapCoroutineState::Complete(Err(err)) => {
216                        return ImapCoroutineState::Complete(Err(err.into()));
217                    }
218                },
219                State::Done => {
220                    return ImapCoroutineState::Complete(Err(
221                        ImapMessageAddError::ResumedAfterDone,
222                    ));
223                }
224            }
225        }
226    }
227}