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