io_email/message/imap/
add.rs1use 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#[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
74pub struct ImapMessageAdd {
77 state: State,
78 mailbox: String,
79 message_id: String,
80}
81
82impl ImapMessageAdd {
83 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 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}