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