1use core::{fmt, num::NonZeroU32};
54
55use alloc::{string::String, string::ToString, vec::Vec};
56
57use imap_codec::{
58 CommandCodec,
59 fragmentizer::Fragmentizer,
60 imap_types::{
61 command::{Command, CommandBody},
62 core::TagGenerator,
63 datetime::DateTime,
64 extensions::binary::LiteralOrLiteral8,
65 flag::Flag,
66 mailbox::Mailbox,
67 response::{Code, StatusKind, Tagged},
68 },
69};
70use log::trace;
71use thiserror::Error;
72
73use crate::{coroutine::*, imap_try, rfc3501::mailbox::encode_inplace, send::*};
74
75#[derive(Clone, Debug, Error)]
77pub enum ImapAppendUidError {
78 #[error("IMAP APPEND failed: NO {0}")]
80 No(String),
81 #[error("IMAP APPEND failed: BAD {0}")]
83 Bad(String),
84 #[error("IMAP APPEND failed: BYE {0}")]
86 Bye(String),
87 #[error("IMAP APPEND failed: server did not return a tagged response")]
89 MissingTagged,
90 #[error("IMAP APPEND failed: {0}")]
92 Send(#[from] ImapSendError),
93}
94
95#[derive(Clone, Debug, Default, Eq, PartialEq)]
97pub struct ImapAppendUidOptions {
98 pub flags: Vec<Flag<'static>>,
100 pub date: Option<DateTime>,
103}
104
105pub struct ImapAppendUid {
107 state: State,
108}
109
110impl ImapAppendUid {
111 pub fn new(
114 mut mailbox: Mailbox<'static>,
115 message: LiteralOrLiteral8<'static>,
116 opts: ImapAppendUidOptions,
117 ) -> Self {
118 encode_inplace(&mut mailbox);
119
120 let command = Command {
121 tag: TagGenerator::new().generate(),
122 body: CommandBody::Append {
123 mailbox,
124 flags: opts.flags,
125 date: opts.date,
126 message,
127 },
128 };
129
130 trace!("send IMAP command {command:?}");
131
132 let state = State::Send(ImapSend::new(CommandCodec::new(), command));
133
134 Self { state }
135 }
136}
137
138impl ImapCoroutine for ImapAppendUid {
139 type Yield = ImapYield;
140 type Return = Result<Option<(NonZeroU32, NonZeroU32)>, ImapAppendUidError>;
141
142 fn resume(
143 &mut self,
144 fragmentizer: &mut Fragmentizer,
145 arg: Option<&[u8]>,
146 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
147 match &mut self.state {
148 State::Send(send) => {
149 let out = imap_try!(send, fragmentizer, arg);
150
151 if let Some(bye) = out.bye {
152 let err = ImapAppendUidError::Bye(bye.text.to_string());
153 return ImapCoroutineState::Complete(Err(err));
154 }
155
156 let Some(Tagged { body, .. }) = out.tagged else {
157 let err = ImapAppendUidError::MissingTagged;
158 return ImapCoroutineState::Complete(Err(err));
159 };
160
161 match body.kind {
162 StatusKind::Ok => {
163 let pair = if let Some(Code::AppendUid { uid_validity, uid }) = body.code {
164 Some((uid_validity, uid))
165 } else {
166 None
167 };
168 ImapCoroutineState::Complete(Ok(pair))
169 }
170 StatusKind::No => {
171 let err = ImapAppendUidError::No(body.text.to_string());
172 ImapCoroutineState::Complete(Err(err))
173 }
174 StatusKind::Bad => {
175 let err = ImapAppendUidError::Bad(body.text.to_string());
176 ImapCoroutineState::Complete(Err(err))
177 }
178 }
179 }
180 }
181 }
182}
183
184enum State {
185 Send(ImapSend<CommandCodec>),
186}
187
188impl fmt::Display for State {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 match self {
191 Self::Send(_) => f.write_str("send append"),
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use core::str;
199
200 use alloc::{borrow::ToOwned, format, vec::Vec};
201
202 use imap_codec::imap_types::core::Literal;
203
204 use crate::rfc4315::appenduid::*;
205
206 #[test]
207 fn success_with_appenduid_returns_pair() {
208 let message = LiteralOrLiteral8::Literal(Literal::unvalidated_non_sync(b"x"));
209 let mut append = ImapAppendUid::new(
210 "INBOX".try_into().expect("valid mailbox"),
211 message,
212 ImapAppendUidOptions::default(),
213 );
214 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
215
216 let bytes = expect_wants_write(&mut append, &mut frag, None);
217 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
218
219 expect_wants_read(&mut append, &mut frag);
220
221 let reply = format!("{tag} OK [APPENDUID 1700000000 7] APPEND completed\r\n");
222 let pair = expect_complete_ok(&mut append, &mut frag, reply.as_bytes())
223 .expect("APPENDUID returned");
224 assert_eq!(1700000000, pair.0.get());
225 assert_eq!(7, pair.1.get());
226 }
227
228 #[test]
229 fn success_without_appenduid_returns_none() {
230 let message = LiteralOrLiteral8::Literal(Literal::unvalidated_non_sync(b"x"));
231 let mut append = ImapAppendUid::new(
232 "INBOX".try_into().expect("valid mailbox"),
233 message,
234 ImapAppendUidOptions::default(),
235 );
236 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
237
238 let bytes = expect_wants_write(&mut append, &mut frag, None);
239 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
240
241 expect_wants_read(&mut append, &mut frag);
242
243 let reply = format!("{tag} OK APPEND completed\r\n");
244 let pair = expect_complete_ok(&mut append, &mut frag, reply.as_bytes());
245 assert!(pair.is_none());
246 }
247
248 #[test]
249 fn tagged_no_returns_no_error() {
250 let message = LiteralOrLiteral8::Literal(Literal::unvalidated_non_sync(b"x"));
251 let mut append = ImapAppendUid::new(
252 "INBOX".try_into().expect("valid mailbox"),
253 message,
254 ImapAppendUidOptions::default(),
255 );
256 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
257
258 let bytes = expect_wants_write(&mut append, &mut frag, None);
259 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
260
261 expect_wants_read(&mut append, &mut frag);
262
263 let reply = format!("{tag} NO mailbox is read-only\r\n");
264 let err = expect_complete_err(&mut append, &mut frag, reply.as_bytes());
265 let ImapAppendUidError::No(text) = err else {
266 panic!("expected ImapAppendUidError::No, got {err:?}");
267 };
268 assert_eq!(text, "mailbox is read-only");
269 }
270
271 #[test]
272 fn bye_returns_bye_error() {
273 let message = LiteralOrLiteral8::Literal(Literal::unvalidated_non_sync(b"x"));
274 let mut append = ImapAppendUid::new(
275 "INBOX".try_into().expect("valid mailbox"),
276 message,
277 ImapAppendUidOptions::default(),
278 );
279 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
280
281 let _ = expect_wants_write(&mut append, &mut frag, None);
282 expect_wants_read(&mut append, &mut frag);
283
284 let err = expect_complete_err(&mut append, &mut frag, b"* BYE going down\r\n");
285 let ImapAppendUidError::Bye(text) = err else {
286 panic!("expected ImapAppendUidError::Bye, got {err:?}");
287 };
288 assert_eq!(text, "going down");
289 }
290
291 fn expect_wants_write(
292 cor: &mut ImapAppendUid,
293 frag: &mut Fragmentizer,
294 arg: Option<&[u8]>,
295 ) -> Vec<u8> {
296 match cor.resume(frag, arg) {
297 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
298 state => panic!("expected WantsWrite, got {state:?}"),
299 }
300 }
301
302 fn expect_wants_read(cor: &mut ImapAppendUid, frag: &mut Fragmentizer) {
303 match cor.resume(frag, None) {
304 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
305 state => panic!("expected WantsRead, got {state:?}"),
306 }
307 }
308
309 fn expect_complete_ok(
310 cor: &mut ImapAppendUid,
311 frag: &mut Fragmentizer,
312 reply: &[u8],
313 ) -> Option<(NonZeroU32, NonZeroU32)> {
314 match cor.resume(frag, Some(reply)) {
315 ImapCoroutineState::Complete(Ok(value)) => value,
316 state => panic!("expected Complete(Ok), got {state:?}"),
317 }
318 }
319
320 fn expect_complete_err(
321 cor: &mut ImapAppendUid,
322 frag: &mut Fragmentizer,
323 reply: &[u8],
324 ) -> ImapAppendUidError {
325 match cor.resume(frag, Some(reply)) {
326 ImapCoroutineState::Complete(Err(err)) => err,
327 state => panic!("expected Complete(Err), got {state:?}"),
328 }
329 }
330
331 fn first_word(line: &str) -> &str {
332 line.split_whitespace()
333 .next()
334 .expect("first whitespace-separated token")
335 }
336}