From 7ebc708acab9c91db41652cfbfe2814a3a27569d Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 09:21:46 +0100 Subject: [PATCH 01/15] unselect implemented rfc3691 --- src/imap/command/anystate.rs | 7 +++++-- src/imap/command/examined.rs | 11 +++++++---- src/imap/command/selected.rs | 13 +++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index 42fe645..f326852 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -6,8 +6,11 @@ use crate::imap::flow; use crate::imap::response::Response; pub(crate) fn capability(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { - let capabilities: NonEmptyVec = - (vec![Capability::Imap4Rev1, Capability::Idle]).try_into()?; + let capabilities: NonEmptyVec = (vec![ + Capability::Imap4Rev1, + Capability::try_from("UNSELECT").unwrap(), + ]) + .try_into()?; let res = Response::build() .tag(tag) .message("Server capabilities") diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 7de94f4..8876297 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -28,7 +28,7 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl // Specific to the EXAMINE state (specialization of the SELECTED state) // ~3 commands -> close, fetch, search + NOOP - CommandBody::Close => ctx.close().await, + CommandBody::Close => ctx.close("CLOSE").await, CommandBody::Fetch { sequence_set, macro_or_item_names, @@ -44,10 +44,13 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl Response::build() .to_req(ctx.req) .message("Forbidden command: can't write in read-only mode (EXAMINE)") - .bad()?, + .no()?, flow::Transition::None, )), + // UNSELECT extension (rfc3691) + CommandBody::Unselect => ctx.close("UNSELECT").await, + // In examined mode, we fallback to authenticated when needed _ => { authenticated::dispatch(authenticated::AuthenticatedContext { @@ -64,11 +67,11 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl impl<'a> ExaminedContext<'a> { /// CLOSE in examined state is not the same as in selected state /// (in selected state it also does an EXPUNGE, here it doesn't) - async fn close(self) -> Result<(Response<'static>, flow::Transition)> { + async fn close(self, kind: &str) -> Result<(Response<'static>, flow::Transition)> { Ok(( Response::build() .to_req(self.req) - .message("CLOSE completed") + .message(format!("{} completed", kind)) .ok()?, flow::Transition::Unselect, )) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 220a952..0653226 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -59,6 +59,9 @@ pub async fn dispatch<'a>( uid, } => ctx.copy(sequence_set, mailbox, uid).await, + // UNSELECT extension (rfc3691) + CommandBody::Unselect => ctx.unselect().await, + // In selected mode, we fallback to authenticated when needed _ => { authenticated::dispatch(authenticated::AuthenticatedContext { @@ -84,6 +87,16 @@ impl<'a> SelectedContext<'a> { )) } + async fn unselect(self) -> Result<(Response<'static>, flow::Transition)> { + Ok(( + Response::build() + .to_req(self.req) + .message("UNSELECT completed") + .ok()?, + flow::Transition::Unselect, + )) + } + pub async fn fetch( self, sequence_set: &SequenceSet, From 9ce8e18fb81c4ef0bf146c5d28981d8f2a6fddd9 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 09:47:52 +0100 Subject: [PATCH 02/15] Common module in test created --- Cargo.toml | 4 +- tests/common.rs | 43 +++++++++ ..._features.rs => rfc3501_imap4rev1_base.rs} | 90 ++++++------------- 3 files changed, 72 insertions(+), 65 deletions(-) create mode 100644 tests/common.rs rename tests/{imap_features.rs => rfc3501_imap4rev1_base.rs} (78%) diff --git a/Cargo.toml b/Cargo.toml index 2a55524..4e39a66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,6 @@ imap-types = { git = "https://github.com/duesee/imap-codec", branch = "v2" } imap-codec = { git = "https://github.com/duesee/imap-codec", branch = "v2" } [[test]] -name = "imap_features" -path = "tests/imap_features.rs" +name = "rfc3501_imap4rev1_base" +path = "tests/rfc3501_imap4rev1_base.rs" harness = false diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..0d99ce8 --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,43 @@ +use anyhow::{bail, Context, Result}; +use std::net::{Shutdown, TcpStream}; +use std::process::Command; +use std::{thread, time}; + +static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); + +pub fn aerogramme_provider_daemon_dev(mut fx: impl FnMut(&mut TcpStream, &mut TcpStream) -> Result<()>) -> Result<()> { + let mut daemon = Command::new(env!("CARGO_BIN_EXE_aerogramme")) + .arg("--dev") + .arg("provider") + .arg("daemon") + .spawn()?; + + let mut max_retry = 20; + let mut imap_socket = loop { + max_retry -= 1; + match (TcpStream::connect("[::1]:1143"), max_retry) { + (Err(e), 0) => bail!("no more retry, last error is: {}", e), + (Err(e), _) => { + println!("unable to connect: {} ; will retry in 1 sec", e); + } + (Ok(v), _) => break v, + } + thread::sleep(SMALL_DELAY); + }; + + let mut lmtp_socket = TcpStream::connect("[::1]:1025").context("lmtp socket must be connected")?; + + println!("-- ready to test imap features --"); + let result = fx(&mut imap_socket, &mut lmtp_socket); + println!("-- test teardown --"); + + imap_socket + .shutdown(Shutdown::Both) + .context("closing imap socket at the end of the test")?; + lmtp_socket + .shutdown(Shutdown::Both) + .context("closing lmtp socket at the end of the test")?; + daemon.kill().context("daemon should be killed")?; + + result.context("all tests passed") +} diff --git a/tests/imap_features.rs b/tests/rfc3501_imap4rev1_base.rs similarity index 78% rename from tests/imap_features.rs rename to tests/rfc3501_imap4rev1_base.rs index 9e8d587..f416e51 100644 --- a/tests/imap_features.rs +++ b/tests/rfc3501_imap4rev1_base.rs @@ -1,9 +1,10 @@ use anyhow::{bail, Context, Result}; use std::io::{Read, Write}; -use std::net::{Shutdown, TcpStream}; -use std::process::Command; +use std::net::TcpStream; use std::{thread, time}; +mod common; + static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); static EMAIL1: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r From: Bob Robert \r @@ -57,66 +58,30 @@ Hello world!\r "; fn main() { - let mut daemon = Command::new(env!("CARGO_BIN_EXE_aerogramme")) - .arg("--dev") - .arg("provider") - .arg("daemon") - .spawn() - .expect("daemon should be started"); - - let mut max_retry = 20; - let mut imap_socket = loop { - max_retry -= 1; - match (TcpStream::connect("[::1]:1143"), max_retry) { - (Err(e), 0) => panic!("no more retry, last error is: {}", e), - (Err(e), _) => { - println!("unable to connect: {} ; will retry in 1 sec", e); - } - (Ok(v), _) => break v, - } - thread::sleep(SMALL_DELAY); - }; - - let mut lmtp_socket = TcpStream::connect("[::1]:1025").expect("lmtp socket must be connected"); - - println!("-- ready to test imap features --"); - let result = generic_test(&mut imap_socket, &mut lmtp_socket); - println!("-- test teardown --"); - - imap_socket - .shutdown(Shutdown::Both) - .expect("closing imap socket at the end of the test"); - lmtp_socket - .shutdown(Shutdown::Both) - .expect("closing lmtp socket at the end of the test"); - daemon.kill().expect("daemon should be killed"); - - result.expect("all tests passed"); -} - -fn generic_test(imap_socket: &mut TcpStream, lmtp_socket: &mut TcpStream) -> Result<()> { - connect(imap_socket).context("server says hello")?; - capability(imap_socket).context("check server capabilities")?; - login(imap_socket).context("login test")?; - create_mailbox(imap_socket).context("created mailbox archive")?; - // UNSUBSCRIBE IS NOT IMPLEMENTED YET - //unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?; - select_inbox(imap_socket).context("select inbox")?; - check(imap_socket).context("check must run")?; - status_mailbox(imap_socket).context("status of archive from inbox")?; - lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; - lmtp_deliver_email(lmtp_socket, EMAIL1).context("mail delivered successfully")?; - noop_exists(imap_socket).context("noop loop must detect a new email")?; - fetch_rfc822(imap_socket, EMAIL1).context("fetch rfc822 message")?; - copy_email(imap_socket).context("copy message to the archive mailbox")?; - append_email(imap_socket, EMAIL2).context("insert email in INBOX")?; - // SEARCH IS NOT IMPLEMENTED YET - //search(imap_socket).expect("search should return something"); - add_flags_email(imap_socket).context("should add delete and important flags to the email")?; - expunge(imap_socket).context("expunge emails")?; - rename_mailbox(imap_socket).context("archive mailbox is renamed my-archives")?; - delete_mailbox(imap_socket).context("my-archives mailbox is deleted")?; - Ok(()) + common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { + connect(imap_socket).context("server says hello")?; + capability(imap_socket).context("check server capabilities")?; + login(imap_socket).context("login test")?; + create_mailbox(imap_socket).context("created mailbox archive")?; + // UNSUBSCRIBE IS NOT IMPLEMENTED YET + //unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?; + select_inbox(imap_socket).context("select inbox")?; + check(imap_socket).context("check must run")?; + status_mailbox(imap_socket).context("status of archive from inbox")?; + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, EMAIL1).context("mail delivered successfully")?; + noop_exists(imap_socket).context("noop loop must detect a new email")?; + fetch_rfc822(imap_socket, EMAIL1).context("fetch rfc822 message")?; + copy_email(imap_socket).context("copy message to the archive mailbox")?; + append_email(imap_socket, EMAIL2).context("insert email in INBOX")?; + // SEARCH IS NOT IMPLEMENTED YET + //search(imap_socket).expect("search should return something"); + add_flags_email(imap_socket).context("should add delete and important flags to the email")?; + expunge(imap_socket).context("expunge emails")?; + rename_mailbox(imap_socket).context("archive mailbox is renamed my-archives")?; + delete_mailbox(imap_socket).context("my-archives mailbox is deleted")?; + Ok(()) + }).expect("test fully run"); } fn connect(imap: &mut TcpStream) -> Result<()> { @@ -135,7 +100,6 @@ fn capability(imap: &mut TcpStream) -> Result<()> { let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?; let srv_msg = std::str::from_utf8(read)?; assert!(srv_msg.contains("IMAP4REV1")); - assert!(srv_msg.contains("IDLE")); Ok(()) } From a059585cb423d527763ef0131773d6620ebcafd5 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 10:28:10 +0100 Subject: [PATCH 03/15] add test for the unselect extension --- Cargo.toml | 5 + tests/common.rs | 29 ++++- tests/rfc3501_imap4rev1_base.rs | 129 +++++++++----------- tests/rfc3691_imapext_unselect.rs | 190 ++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 75 deletions(-) create mode 100644 tests/rfc3691_imapext_unselect.rs diff --git a/Cargo.toml b/Cargo.toml index 4e39a66..93f2a3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,3 +71,8 @@ imap-codec = { git = "https://github.com/duesee/imap-codec", branch = "v2" } name = "rfc3501_imap4rev1_base" path = "tests/rfc3501_imap4rev1_base.rs" harness = false + +[[test]] +name = "rfc3691_imapext_unselect" +path = "tests/rfc3691_imapext_unselect.rs" +harness = false diff --git a/tests/common.rs b/tests/common.rs index 0d99ce8..f89f26c 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,11 +1,14 @@ use anyhow::{bail, Context, Result}; +use std::io::Read; use std::net::{Shutdown, TcpStream}; use std::process::Command; use std::{thread, time}; static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); -pub fn aerogramme_provider_daemon_dev(mut fx: impl FnMut(&mut TcpStream, &mut TcpStream) -> Result<()>) -> Result<()> { +pub fn aerogramme_provider_daemon_dev( + mut fx: impl FnMut(&mut TcpStream, &mut TcpStream) -> Result<()>, +) -> Result<()> { let mut daemon = Command::new(env!("CARGO_BIN_EXE_aerogramme")) .arg("--dev") .arg("provider") @@ -25,7 +28,8 @@ pub fn aerogramme_provider_daemon_dev(mut fx: impl FnMut(&mut TcpStream, &mut Tc thread::sleep(SMALL_DELAY); }; - let mut lmtp_socket = TcpStream::connect("[::1]:1025").context("lmtp socket must be connected")?; + let mut lmtp_socket = + TcpStream::connect("[::1]:1025").context("lmtp socket must be connected")?; println!("-- ready to test imap features --"); let result = fx(&mut imap_socket, &mut lmtp_socket); @@ -41,3 +45,24 @@ pub fn aerogramme_provider_daemon_dev(mut fx: impl FnMut(&mut TcpStream, &mut Tc result.context("all tests passed") } + +pub fn read_lines<'a, F: Read>( + reader: &mut F, + buffer: &'a mut [u8], + stop_marker: Option<&[u8]>, +) -> Result<&'a [u8]> { + let mut nbytes = 0; + loop { + nbytes += reader.read(&mut buffer[nbytes..])?; + //println!("partial read: {}", std::str::from_utf8(&buffer[..nbytes])?); + let pre_condition = match stop_marker { + None => true, + Some(mark) => buffer[..nbytes].windows(mark.len()).any(|w| w == mark), + }; + if pre_condition && &buffer[nbytes - 2..nbytes] == &b"\r\n"[..] { + break; + } + } + println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?); + Ok(&buffer[..nbytes]) +} diff --git a/tests/rfc3501_imap4rev1_base.rs b/tests/rfc3501_imap4rev1_base.rs index f416e51..c0985dd 100644 --- a/tests/rfc3501_imap4rev1_base.rs +++ b/tests/rfc3501_imap4rev1_base.rs @@ -1,61 +1,12 @@ use anyhow::{bail, Context, Result}; -use std::io::{Read, Write}; +use std::io::Write; use std::net::TcpStream; use std::{thread, time}; mod common; +use crate::common::read_lines; static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); -static EMAIL1: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r -From: Bob Robert \r -To: Alice Malice \r -CC: =?ISO-8859-1?Q?Andr=E9?= Pirard \r -Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r - =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\r -X-Unknown: something something\r -Bad entry\r - on multiple lines\r -Message-ID: \r -MIME-Version: 1.0\r -Content-Type: multipart/alternative;\r - boundary=\"b1_e376dc71bafc953c0b0fdeb9983a9956\"\r -Content-Transfer-Encoding: 7bit\r -\r -This is a multi-part message in MIME format.\r -\r ---b1_e376dc71bafc953c0b0fdeb9983a9956\r -Content-Type: text/plain; charset=utf-8\r -Content-Transfer-Encoding: quoted-printable\r -\r -GZ\r -OoOoO\r -oOoOoOoOo\r -oOoOoOoOoOoOoOoOo\r -oOoOoOoOoOoOoOoOoOoOoOo\r -oOoOoOoOoOoOoOoOoOoOoOoOoOoOo\r -OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\r -\r ---b1_e376dc71bafc953c0b0fdeb9983a9956\r -Content-Type: text/html; charset=us-ascii\r -\r -
GZ
\r -OoOoO
\r -oOoOoOoOo
\r -oOoOoOoOoOoOoOoOo
\r -oOoOoOoOoOoOoOoOoOoOoOo
\r -oOoOoOoOoOoOoOoOoOoOoOoOoOoOo
\r -OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO
\r -
\r -\r ---b1_e376dc71bafc953c0b0fdeb9983a9956--\r -"; - -static EMAIL2: &[u8] = b"From: alice@example.com\r -To: alice@example.tld\r -Subject: Test\r -\r -Hello world!\r -"; fn main() { common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { @@ -76,12 +27,14 @@ fn main() { append_email(imap_socket, EMAIL2).context("insert email in INBOX")?; // SEARCH IS NOT IMPLEMENTED YET //search(imap_socket).expect("search should return something"); - add_flags_email(imap_socket).context("should add delete and important flags to the email")?; + add_flags_email(imap_socket) + .context("should add delete and important flags to the email")?; expunge(imap_socket).context("expunge emails")?; rename_mailbox(imap_socket).context("archive mailbox is renamed my-archives")?; delete_mailbox(imap_socket).context("my-archives mailbox is deleted")?; Ok(()) - }).expect("test fully run"); + }) + .expect("test fully run"); } fn connect(imap: &mut TcpStream) -> Result<()> { @@ -336,23 +289,53 @@ fn delete_mailbox(imap: &mut TcpStream) -> Result<()> { Ok(()) } -fn read_lines<'a, F: Read>( - reader: &mut F, - buffer: &'a mut [u8], - stop_marker: Option<&[u8]>, -) -> Result<&'a [u8]> { - let mut nbytes = 0; - loop { - nbytes += reader.read(&mut buffer[nbytes..])?; - //println!("partial read: {}", std::str::from_utf8(&buffer[..nbytes])?); - let pre_condition = match stop_marker { - None => true, - Some(mark) => buffer[..nbytes].windows(mark.len()).any(|w| w == mark), - }; - if pre_condition && &buffer[nbytes - 2..nbytes] == &b"\r\n"[..] { - break; - } - } - println!("read: {}", std::str::from_utf8(&buffer[..nbytes])?); - Ok(&buffer[..nbytes]) -} +static EMAIL1: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r +From: Bob Robert \r +To: Alice Malice \r +CC: =?ISO-8859-1?Q?Andr=E9?= Pirard \r +Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r + =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\r +X-Unknown: something something\r +Bad entry\r + on multiple lines\r +Message-ID: \r +MIME-Version: 1.0\r +Content-Type: multipart/alternative;\r + boundary=\"b1_e376dc71bafc953c0b0fdeb9983a9956\"\r +Content-Transfer-Encoding: 7bit\r +\r +This is a multi-part message in MIME format.\r +\r +--b1_e376dc71bafc953c0b0fdeb9983a9956\r +Content-Type: text/plain; charset=utf-8\r +Content-Transfer-Encoding: quoted-printable\r +\r +GZ\r +OoOoO\r +oOoOoOoOo\r +oOoOoOoOoOoOoOoOo\r +oOoOoOoOoOoOoOoOoOoOoOo\r +oOoOoOoOoOoOoOoOoOoOoOoOoOoOo\r +OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\r +\r +--b1_e376dc71bafc953c0b0fdeb9983a9956\r +Content-Type: text/html; charset=us-ascii\r +\r +
GZ
\r +OoOoO
\r +oOoOoOoOo
\r +oOoOoOoOoOoOoOoOo
\r +oOoOoOoOoOoOoOoOoOoOoOo
\r +oOoOoOoOoOoOoOoOoOoOoOoOoOoOo
\r +OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO
\r +
\r +\r +--b1_e376dc71bafc953c0b0fdeb9983a9956--\r +"; + +static EMAIL2: &[u8] = b"From: alice@example.com\r +To: alice@example.tld\r +Subject: Test\r +\r +Hello world!\r +"; diff --git a/tests/rfc3691_imapext_unselect.rs b/tests/rfc3691_imapext_unselect.rs new file mode 100644 index 0000000..28e4bc8 --- /dev/null +++ b/tests/rfc3691_imapext_unselect.rs @@ -0,0 +1,190 @@ +use anyhow::{bail, Context, Result}; +use std::io::Write; +use std::net::TcpStream; +use std::{thread, time}; + +mod common; +use crate::common::read_lines; + +static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); +static EMAIL: &[u8] = b"From: alice@example.com\r +To: alice@example.tld\r +Subject: Test\r +\r +Hello world!\r +"; + +fn main() { + common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, EMAIL).context("mail delivered successfully")?; + + connect(imap_socket).context("server says hello")?; + capability(imap_socket).context("check server capabilities")?; + login(imap_socket).context("login test")?; + select_inbox(imap_socket).context("select inbox")?; + noop_exists(imap_socket).context("noop loop must detect a new email")?; + add_flags_email(imap_socket).context("add delete flags to the email")?; + unselect(imap_socket) + .context("unselect inbox while preserving email with the \\Delete flag")?; + select_inbox_one_msg(imap_socket).context("select inbox again")?; + fetch_rfc822(imap_socket, EMAIL).context("message is still present")?; + close(imap_socket).context("close inbox and expunge message")?; + select_inbox_zero_msg(imap_socket).context("select inbox again and check it's empty")?; + + Ok(()) + }) + .expect("test fully run"); +} + +fn connect(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..4], &b"* OK"[..]); + + Ok(()) +} + +fn capability(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"5 capability\r\n"[..])?; + + let mut buffer: [u8; 1500] = [0; 1500]; + let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains("IMAP4REV1")); + assert!(srv_msg.contains("UNSELECT")); + + Ok(()) +} + +fn login(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + imap.write(&b"10 login alice hunter2\r\n"[..])?; + + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"10 OK"[..]); + + Ok(()) +} + +fn select_inbox_one_msg(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + imap.write(&b"22 select inbox\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"22 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains("* 1 EXISTS")); + + Ok(()) +} + +fn select_inbox_zero_msg(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + imap.write(&b"21 select inbox\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"21 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains("* 0 EXISTS")); + + Ok(()) +} + +fn select_inbox(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + imap.write(&b"20 select inbox\r\n"[..])?; + let _read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?; + + Ok(()) +} + +fn noop_exists(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + let mut max_retry = 20; + loop { + max_retry -= 1; + imap.write(&b"30 NOOP\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"30 OK NOOP"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + + match (max_retry, srv_msg.contains("* 1 EXISTS")) { + (_, true) => break, + (0, _) => bail!("no more retry"), + _ => (), + } + + thread::sleep(SMALL_DELAY); + } + + Ok(()) +} + +fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + let _read = read_lines(lmtp, &mut buffer, None)?; + assert_eq!(&buffer[..4], &b"220 "[..]); + + lmtp.write(&b"LHLO example.tld\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 "[..]))?; + + Ok(()) +} + +fn lmtp_deliver_email(lmtp: &mut TcpStream, email: &[u8]) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + lmtp.write(&b"MAIL FROM:\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; + + lmtp.write(&b"RCPT TO:\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.1.5"[..]))?; + + lmtp.write(&b"DATA\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"354 "[..]))?; + + lmtp.write(email)?; + lmtp.write(&b"\r\n.\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; + + Ok(()) +} + +fn fetch_rfc822(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> { + let mut buffer: [u8; 65535] = [0; 65535]; + imap.write(&b"40 fetch 1 rfc822\r\n"[..])?; + + let read = read_lines(imap, &mut buffer, Some(&b"40 OK FETCH"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + let orig_email = std::str::from_utf8(ref_mail)?; + assert!(srv_msg.contains(orig_email)); + + Ok(()) +} + +fn add_flags_email(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + imap.write(&b"50 store 1 +FLAGS (\\Deleted)\r\n"[..])?; + let _read = read_lines(imap, &mut buffer, Some(&b"50 OK STORE"[..]))?; + + Ok(()) +} + +fn close(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"60 close\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let _read = read_lines(imap, &mut buffer, Some(&b"60 OK"[..]))?; + + Ok(()) +} + +fn unselect(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"70 unselect\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let _read = read_lines(imap, &mut buffer, Some(&b"70 OK"[..]))?; + + Ok(()) +} From 6d37924399dd5d04f5be2506e3e044dd165f6399 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 12:29:19 +0100 Subject: [PATCH 04/15] rework capability --- src/imap/capability.rs | 30 ++++++++++++++++++++++++++++++ src/imap/command/anonymous.rs | 8 +++++++- src/imap/command/anystate.rs | 14 +++++--------- src/imap/command/authenticated.rs | 6 +++++- src/imap/command/examined.rs | 8 +++++++- src/imap/command/selected.rs | 8 +++++++- src/imap/mod.rs | 12 +++++++++--- src/imap/session.rs | 13 +++++++++++-- 8 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 src/imap/capability.rs diff --git a/src/imap/capability.rs b/src/imap/capability.rs new file mode 100644 index 0000000..b98e8f8 --- /dev/null +++ b/src/imap/capability.rs @@ -0,0 +1,30 @@ +use imap_codec::imap_types::core::NonEmptyVec; +use imap_codec::imap_types::response::Capability; + +#[derive(Debug, Clone)] +pub struct ServerCapability { + r#move: bool, + unselect: bool, +} + +impl Default for ServerCapability { + fn default() -> Self { + Self { + r#move: true, + unselect: true, + } + } +} + +impl ServerCapability { + pub fn to_vec(&self) -> NonEmptyVec> { + let mut acc = vec![Capability::Imap4Rev1]; + if self.r#move { + acc.push(Capability::Move); + } + if self.unselect { + acc.push(Capability::try_from("UNSELECT").unwrap()); + } + acc.try_into().unwrap() + } +} diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index fbd10e9..fda78c2 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -1,10 +1,12 @@ use anyhow::Result; use imap_codec::imap_types::command::{Command, CommandBody}; use imap_codec::imap_types::core::AString; +use imap_codec::imap_types::response::Code; use imap_codec::imap_types::secret::Secret; use crate::imap::command::anystate; use crate::imap::flow; +use crate::imap::capability::ServerCapability; use crate::imap::response::Response; use crate::login::ArcLoginProvider; use crate::mail::user::User; @@ -13,6 +15,7 @@ use crate::mail::user::User; pub struct AnonymousContext<'a> { pub req: &'a Command<'static>, + pub server_capabilities: &'a ServerCapability, pub login_provider: &'a ArcLoginProvider, } @@ -20,7 +23,9 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, f match &ctx.req.body { // Any State CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability( + ctx.req.tag.clone(), + ctx.server_capabilities), CommandBody::Logout => anystate::logout(), // Specific to anonymous context (3 commands) @@ -69,6 +74,7 @@ impl<'a> AnonymousContext<'a> { Ok(( Response::build() .to_req(self.req) + .code(Code::Capability(self.server_capabilities.to_vec())) .message("Completed") .ok()?, flow::Transition::Authenticate(user), diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index f326852..7d7c0b2 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -1,20 +1,16 @@ use anyhow::Result; -use imap_codec::imap_types::core::{NonEmptyVec, Tag}; -use imap_codec::imap_types::response::{Capability, Data}; +use imap_codec::imap_types::core::Tag; +use imap_codec::imap_types::response::Data; use crate::imap::flow; +use crate::imap::capability::ServerCapability; use crate::imap::response::Response; -pub(crate) fn capability(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { - let capabilities: NonEmptyVec = (vec![ - Capability::Imap4Rev1, - Capability::try_from("UNSELECT").unwrap(), - ]) - .try_into()?; +pub(crate) fn capability(tag: Tag<'static>, cap: &ServerCapability) -> Result<(Response<'static>, flow::Transition)> { let res = Response::build() .tag(tag) .message("Server capabilities") - .data(Data::Capability(capabilities)) + .data(Data::Capability(cap.to_vec())) .ok()?; Ok((res, flow::Transition::None)) diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 1bb4c6d..a6a5203 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -14,6 +14,7 @@ use crate::imap::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; +use crate::imap::capability::ServerCapability; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::*; @@ -22,6 +23,7 @@ use crate::mail::IMF; pub struct AuthenticatedContext<'a> { pub req: &'a Command<'static>, + pub server_capabilities: &'a ServerCapability, pub user: &'a Arc, } @@ -31,7 +33,9 @@ pub async fn dispatch<'a>( match &ctx.req.body { // Any state CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability( + ctx.req.tag.clone(), + ctx.server_capabilities), CommandBody::Logout => anystate::logout(), // Specific to this state (11 commands) diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 8876297..7cccf7b 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -11,19 +11,24 @@ use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; +use crate::imap::capability::ServerCapability; use crate::mail::user::User; pub struct ExaminedContext<'a> { pub req: &'a Command<'static>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, + pub server_capabilities: &'a ServerCapability, } pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> { match &ctx.req.body { // Any State // noop is specific to this state - CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability( + ctx.req.tag.clone(), + ctx.server_capabilities, + ), CommandBody::Logout => anystate::logout(), // Specific to the EXAMINE state (specialization of the SELECTED state) @@ -55,6 +60,7 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl _ => { authenticated::dispatch(authenticated::AuthenticatedContext { req: ctx.req, + server_capabilities: ctx.server_capabilities, user: ctx.user, }) .await diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 0653226..681b509 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -14,6 +14,7 @@ use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; +use crate::imap::capability::ServerCapability; use crate::mail::user::User; @@ -21,6 +22,7 @@ pub struct SelectedContext<'a> { pub req: &'a Command<'static>, pub user: &'a Arc, pub mailbox: &'a mut MailboxView, + pub server_capabilities: &'a ServerCapability, } pub async fn dispatch<'a>( @@ -29,7 +31,10 @@ pub async fn dispatch<'a>( match &ctx.req.body { // Any State // noop is specific to this state - CommandBody::Capability => anystate::capability(ctx.req.tag.clone()), + CommandBody::Capability => anystate::capability( + ctx.req.tag.clone(), + ctx.server_capabilities, + ), CommandBody::Logout => anystate::logout(), // Specific to this state (7 commands + NOOP) @@ -66,6 +71,7 @@ pub async fn dispatch<'a>( _ => { authenticated::dispatch(authenticated::AuthenticatedContext { req: ctx.req, + server_capabilities: ctx.server_capabilities, user: ctx.user, }) .await diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 31eeaa8..693c99a 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -3,6 +3,7 @@ mod flow; mod mailbox_view; mod response; mod session; +mod capability; use std::net::SocketAddr; @@ -17,12 +18,14 @@ use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions}; use imap_flow::stream::AnyStream; use crate::config::ImapConfig; +use crate::imap::capability::ServerCapability; use crate::login::ArcLoginProvider; /// Server is a thin wrapper to register our Services in BàL pub struct Server { bind_addr: SocketAddr, login_provider: ArcLoginProvider, + capabilities: ServerCapability, } struct ClientContext { @@ -30,12 +33,14 @@ struct ClientContext { addr: SocketAddr, login_provider: ArcLoginProvider, must_exit: watch::Receiver, + server_capabilities: ServerCapability, } pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Server { Server { bind_addr: config.bind_addr, login_provider: login, + capabilities: ServerCapability::default(), } } @@ -66,6 +71,7 @@ impl Server { addr: remote_addr.clone(), login_provider: self.login_provider.clone(), must_exit: must_exit.clone(), + server_capabilities: self.capabilities.clone(), }; let conn = tokio::spawn(client_wrapper(client)); connections.push(conn); @@ -96,21 +102,21 @@ async fn client(mut ctx: ClientContext) -> Result<()> { let (mut server, _) = ServerFlow::send_greeting( ctx.stream, ServerFlowOptions::default(), - Greeting::ok(None, "Aerogramme").unwrap(), + Greeting::ok(Some(Code::Capability(ctx.server_capabilities.to_vec())), "Aerogramme").unwrap(), ) .await?; use crate::imap::response::{Body, Response as MyResponse}; use crate::imap::session::Instance; use imap_codec::imap_types::command::Command; - use imap_codec::imap_types::response::{Response, Status}; + use imap_codec::imap_types::response::{Response, Code, Status}; use tokio::sync::mpsc; let (cmd_tx, mut cmd_rx) = mpsc::channel::>(10); let (resp_tx, mut resp_rx) = mpsc::unbounded_channel::>(); let bckgrnd = tokio::spawn(async move { - let mut session = Instance::new(ctx.login_provider); + let mut session = Instance::new(ctx.login_provider, ctx.server_capabilities); loop { let cmd = match cmd_rx.recv().await { None => break, diff --git a/src/imap/session.rs b/src/imap/session.rs index 5c67f8e..c4f062f 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,19 +1,22 @@ use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::flow; use crate::imap::response::Response; +use crate::imap::capability::ServerCapability; use crate::login::ArcLoginProvider; use imap_codec::imap_types::command::Command; //----- pub struct Instance { pub login_provider: ArcLoginProvider, + pub server_capabilities: ServerCapability, pub state: flow::State, } impl Instance { - pub fn new(login_provider: ArcLoginProvider) -> Self { + pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self { Self { login_provider, state: flow::State::NotAuthenticated, + server_capabilities: cap, } } @@ -25,16 +28,21 @@ impl Instance { let ctx = anonymous::AnonymousContext { req: &cmd, login_provider: &self.login_provider, + server_capabilities: &self.server_capabilities, }; anonymous::dispatch(ctx).await } flow::State::Authenticated(ref user) => { - let ctx = authenticated::AuthenticatedContext { req: &cmd, user }; + let ctx = authenticated::AuthenticatedContext { + req: &cmd, + server_capabilities: &self.server_capabilities, + user }; authenticated::dispatch(ctx).await } flow::State::Selected(ref user, ref mut mailbox) => { let ctx = selected::SelectedContext { req: &cmd, + server_capabilities: &self.server_capabilities, user, mailbox, }; @@ -43,6 +51,7 @@ impl Instance { flow::State::Examined(ref user, ref mut mailbox) => { let ctx = examined::ExaminedContext { req: &cmd, + server_capabilities: &self.server_capabilities, user, mailbox, }; From ef257e286a5377031bfde9645629825d3223b3d4 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 15:00:05 +0100 Subject: [PATCH 05/15] implement move --- src/imap/command/anonymous.rs | 8 ++-- src/imap/command/anystate.rs | 7 +++- src/imap/command/authenticated.rs | 8 ++-- src/imap/command/examined.rs | 9 ++-- src/imap/command/selected.rs | 68 ++++++++++++++++++++++++++++--- src/imap/mailbox_view.rs | 31 ++++++++++++++ src/imap/mod.rs | 10 +++-- src/imap/session.rs | 9 ++-- 8 files changed, 123 insertions(+), 27 deletions(-) diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs index fda78c2..0582b06 100644 --- a/src/imap/command/anonymous.rs +++ b/src/imap/command/anonymous.rs @@ -4,9 +4,9 @@ use imap_codec::imap_types::core::AString; use imap_codec::imap_types::response::Code; use imap_codec::imap_types::secret::Secret; +use crate::imap::capability::ServerCapability; use crate::imap::command::anystate; use crate::imap::flow; -use crate::imap::capability::ServerCapability; use crate::imap::response::Response; use crate::login::ArcLoginProvider; use crate::mail::user::User; @@ -23,9 +23,9 @@ pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, f match &ctx.req.body { // Any State CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to anonymous context (3 commands) diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs index 7d7c0b2..718ba3f 100644 --- a/src/imap/command/anystate.rs +++ b/src/imap/command/anystate.rs @@ -2,11 +2,14 @@ use anyhow::Result; use imap_codec::imap_types::core::Tag; use imap_codec::imap_types::response::Data; -use crate::imap::flow; use crate::imap::capability::ServerCapability; +use crate::imap::flow; use crate::imap::response::Response; -pub(crate) fn capability(tag: Tag<'static>, cap: &ServerCapability) -> Result<(Response<'static>, flow::Transition)> { +pub(crate) fn capability( + tag: Tag<'static>, + cap: &ServerCapability, +) -> Result<(Response<'static>, flow::Transition)> { let res = Response::build() .tag(tag) .message("Server capabilities") diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index a6a5203..2970b63 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -10,11 +10,11 @@ use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::imap_types::response::{Code, CodeOther, Data}; use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; +use crate::imap::capability::ServerCapability; use crate::imap::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::mail::mailbox::Mailbox; use crate::mail::uidindex::*; @@ -33,9 +33,9 @@ pub async fn dispatch<'a>( match &ctx.req.body { // Any state CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to this state (11 commands) diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index 7cccf7b..bddd0f9 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -7,11 +7,11 @@ use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; +use crate::imap::capability::ServerCapability; use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::mail::user::User; pub struct ExaminedContext<'a> { @@ -25,10 +25,9 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl match &ctx.req.body { // Any State // noop is specific to this state - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities, - ), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to the EXAMINE state (specialization of the SELECTED state) diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 681b509..28ebbe8 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -10,11 +10,11 @@ use imap_codec::imap_types::response::{Code, CodeOther}; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; +use crate::imap::capability::ServerCapability; use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::mail::user::User; @@ -31,10 +31,9 @@ pub async fn dispatch<'a>( match &ctx.req.body { // Any State // noop is specific to this state - CommandBody::Capability => anystate::capability( - ctx.req.tag.clone(), - ctx.server_capabilities, - ), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } CommandBody::Logout => anystate::logout(), // Specific to this state (7 commands + NOOP) @@ -63,6 +62,11 @@ pub async fn dispatch<'a>( mailbox, uid, } => ctx.copy(sequence_set, mailbox, uid).await, + CommandBody::Move { + sequence_set, + mailbox, + uid, + } => ctx.r#move(sequence_set, mailbox, uid).await, // UNSELECT extension (rfc3691) CommandBody::Unselect => ctx.unselect().await, @@ -245,4 +249,58 @@ impl<'a> SelectedContext<'a> { flow::Transition::None, )) } + + async fn r#move( + self, + sequence_set: &SequenceSet, + mailbox: &MailboxCodec<'a>, + uid: &bool, + ) -> Result<(Response<'static>, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; + + let mb_opt = self.user.open_mailbox(&name).await?; + let mb = match mb_opt { + Some(mb) => mb, + None => { + return Ok(( + Response::build() + .to_req(self.req) + .message("Destination mailbox does not exist") + .code(Code::TryCreate) + .no()?, + flow::Transition::None, + )) + } + }; + + let (uidval, uid_map, data) = self.mailbox.r#move(sequence_set, mb, uid).await?; + + // compute code + let copyuid_str = format!( + "{} {} {}", + uidval, + uid_map + .iter() + .map(|(sid, _)| format!("{}", sid)) + .collect::>() + .join(","), + uid_map + .iter() + .map(|(_, tuid)| format!("{}", tuid)) + .collect::>() + .join(",") + ); + + Ok(( + Response::build() + .to_req(self.req) + .message("COPY completed") + .code(Code::Other(CodeOther::unvalidated( + format!("COPYUID {}", copyuid_str).into_bytes(), + ))) + .set_body(data) + .ok()?, + flow::Transition::None, + )) + } } diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 7434512..206b905 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -600,6 +600,37 @@ impl MailboxView { Ok((to_state.uidvalidity, ret)) } + pub async fn r#move( + &mut self, + sequence_set: &SequenceSet, + to: Arc, + is_uid_copy: &bool, + ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>, Vec>)> { + let mails = self.get_mail_ids(sequence_set, *is_uid_copy)?; + + let mut new_uuids = vec![]; + for mi in mails.iter() { + let copy_action = to.copy_from(&self.mailbox, mi.uuid).await?; + new_uuids.push(copy_action); + self.mailbox.delete(mi.uuid).await? + } + + let mut ret = vec![]; + let to_state = to.current_uid_index().await; + for (mi, new_uuid) in mails.iter().zip(new_uuids.iter()) { + let dest_uid = to_state + .table + .get(new_uuid) + .ok_or(anyhow!("moved mail not in destination mailbox"))? + .0; + ret.push((mi.uid, dest_uid)); + } + + let update = self.update().await?; + + Ok((to_state.uidvalidity, ret, update)) + } + /// Looks up state changes in the mailbox and produces a set of IMAP /// responses describing the new state. pub async fn fetch<'b>( diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 693c99a..0b5555a 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -1,9 +1,9 @@ +mod capability; mod command; mod flow; mod mailbox_view; mod response; mod session; -mod capability; use std::net::SocketAddr; @@ -102,14 +102,18 @@ async fn client(mut ctx: ClientContext) -> Result<()> { let (mut server, _) = ServerFlow::send_greeting( ctx.stream, ServerFlowOptions::default(), - Greeting::ok(Some(Code::Capability(ctx.server_capabilities.to_vec())), "Aerogramme").unwrap(), + Greeting::ok( + Some(Code::Capability(ctx.server_capabilities.to_vec())), + "Aerogramme", + ) + .unwrap(), ) .await?; use crate::imap::response::{Body, Response as MyResponse}; use crate::imap::session::Instance; use imap_codec::imap_types::command::Command; - use imap_codec::imap_types::response::{Response, Code, Status}; + use imap_codec::imap_types::response::{Code, Response, Status}; use tokio::sync::mpsc; let (cmd_tx, mut cmd_rx) = mpsc::channel::>(10); diff --git a/src/imap/session.rs b/src/imap/session.rs index c4f062f..55026b9 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,7 +1,7 @@ +use crate::imap::capability::ServerCapability; use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::flow; use crate::imap::response::Response; -use crate::imap::capability::ServerCapability; use crate::login::ArcLoginProvider; use imap_codec::imap_types::command::Command; @@ -33,10 +33,11 @@ impl Instance { anonymous::dispatch(ctx).await } flow::State::Authenticated(ref user) => { - let ctx = authenticated::AuthenticatedContext { - req: &cmd, + let ctx = authenticated::AuthenticatedContext { + req: &cmd, server_capabilities: &self.server_capabilities, - user }; + user, + }; authenticated::dispatch(ctx).await } flow::State::Selected(ref user, ref mut mailbox) => { From b91c64920d7454d50b60ad3abb58fad9d09f0511 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 15:21:51 +0100 Subject: [PATCH 06/15] add test for imap move --- Cargo.toml | 5 + tests/rfc6851_imapext_move.rs | 190 ++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 tests/rfc6851_imapext_move.rs diff --git a/Cargo.toml b/Cargo.toml index 93f2a3a..1cc143e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,3 +76,8 @@ harness = false name = "rfc3691_imapext_unselect" path = "tests/rfc3691_imapext_unselect.rs" harness = false + +[[test]] +name = "rfc6851_imapext_move" +path = "tests/rfc6851_imapext_move.rs" +harness = false diff --git a/tests/rfc6851_imapext_move.rs b/tests/rfc6851_imapext_move.rs new file mode 100644 index 0000000..d379f8d --- /dev/null +++ b/tests/rfc6851_imapext_move.rs @@ -0,0 +1,190 @@ +use anyhow::{bail, Context, Result}; +use std::io::Write; +use std::net::TcpStream; +use std::{thread, time}; + +mod common; +use crate::common::read_lines; + +static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); +static EMAIL: &[u8] = b"From: alice@example.com\r +To: alice@example.tld\r +Subject: Test\r +\r +Hello world!\r +"; + +enum Mailbox { + Inbox, + Archive, +} + +fn main() { + common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, EMAIL).context("mail delivered successfully")?; + + connect(imap_socket).context("server says hello")?; + capability(imap_socket).context("check server capabilities")?; + login(imap_socket).context("login test")?; + create_mailbox(imap_socket).context("created mailbox archive")?; + select(imap_socket, Mailbox::Inbox).context("select inbox")?; + noop_exists(imap_socket).context("noop loop must detect a new email")?; + + r#move(imap_socket).context("message from inbox moved to archive")?; + + unselect(imap_socket) + .context("unselect inbox while preserving email with the \\Delete flag")?; + select(imap_socket, Mailbox::Archive).context("select archive")?; + fetch_rfc822(imap_socket, EMAIL).context("check mail exists")?; + logout(imap_socket).context("must quit")?; + + Ok(()) + }) + .expect("test fully run"); +} + +fn connect(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..4], &b"* OK"[..]); + + Ok(()) +} + +fn create_mailbox(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + imap.write(&b"15 create archive\r\n"[..])?; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..12], &b"15 OK CREATE"[..]); + + Ok(()) +} + +fn capability(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"5 capability\r\n"[..])?; + + let mut buffer: [u8; 1500] = [0; 1500]; + let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains("IMAP4REV1")); + assert!(srv_msg.contains("UNSELECT")); + + Ok(()) +} + +fn login(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + imap.write(&b"10 login alice hunter2\r\n"[..])?; + + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"10 OK"[..]); + + Ok(()) +} + +fn select(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + match mbx { + Mailbox::Inbox => imap.write(&b"20 select inbox\r\n"[..])?, + Mailbox::Archive => imap.write(&b"20 select archive\r\n"[..])?, + }; + let _read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?; + + Ok(()) +} + +fn noop_exists(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + let mut max_retry = 20; + loop { + max_retry -= 1; + imap.write(&b"30 NOOP\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"30 OK NOOP"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + + match (max_retry, srv_msg.contains("* 1 EXISTS")) { + (_, true) => break, + (0, _) => bail!("no more retry"), + _ => (), + } + + thread::sleep(SMALL_DELAY); + } + + Ok(()) +} + +fn r#move(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + imap.write(&b"35 move 1 archive\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"35 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains("* 1 EXPUNGE")); + + Ok(()) +} + +fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + let _read = read_lines(lmtp, &mut buffer, None)?; + assert_eq!(&buffer[..4], &b"220 "[..]); + + lmtp.write(&b"LHLO example.tld\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 "[..]))?; + + Ok(()) +} + +fn lmtp_deliver_email(lmtp: &mut TcpStream, email: &[u8]) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + lmtp.write(&b"MAIL FROM:\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; + + lmtp.write(&b"RCPT TO:\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.1.5"[..]))?; + + lmtp.write(&b"DATA\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"354 "[..]))?; + + lmtp.write(email)?; + lmtp.write(&b"\r\n.\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; + + Ok(()) +} + +fn fetch_rfc822(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> { + let mut buffer: [u8; 65535] = [0; 65535]; + imap.write(&b"40 fetch 1 rfc822\r\n"[..])?; + + let read = read_lines(imap, &mut buffer, Some(&b"40 OK FETCH"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + let orig_email = std::str::from_utf8(ref_mail)?; + assert!(srv_msg.contains(orig_email)); + + Ok(()) +} + +fn logout(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"99 logout\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"* BYE"[..]); + Ok(()) +} + +fn unselect(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"70 unselect\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let _read = read_lines(imap, &mut buffer, Some(&b"70 OK"[..]))?; + + Ok(()) +} From 74686ebb778b740ccfccfbf61ccd24628f60e9d0 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 16:52:31 +0100 Subject: [PATCH 07/15] append ignore dates instead of failing --- Cargo.toml | 2 +- src/imap/capability.rs | 74 ++++++++++++++++++++++++++++++- src/imap/command/authenticated.rs | 5 ++- src/imap/mod.rs | 4 +- src/mail/incoming.rs | 16 +++---- 5 files changed, 89 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1cc143e..557be55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ aws-sdk-s3 = "1.9.0" eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" } smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } -imap-codec = { version = "1.0.0", features = ["quirk_crlf_relaxed", "bounded-static"] } +imap-codec = { version = "1.0.0", features = ["quirk_crlf_relaxed", "bounded-static", "ext_condstore_qresync"] } imap-flow = { git = "https://github.com/duesee/imap-flow.git", rev = "e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" } [dev-dependencies] diff --git a/src/imap/capability.rs b/src/imap/capability.rs index b98e8f8..f6b5a51 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -1,10 +1,24 @@ use imap_codec::imap_types::core::NonEmptyVec; use imap_codec::imap_types::response::Capability; +fn capability_unselect() -> Capability<'static> { + Capability::try_from("UNSELECT").unwrap() +} + +fn capability_condstore() -> Capability<'static> { + Capability::try_from("CONDSTORE").unwrap() +} + +fn capability_qresync() -> Capability<'static> { + Capability::try_from("QRESYNC").unwrap() +} + #[derive(Debug, Clone)] pub struct ServerCapability { r#move: bool, unselect: bool, + condstore: bool, + qresync: bool, } impl Default for ServerCapability { @@ -12,6 +26,8 @@ impl Default for ServerCapability { Self { r#move: true, unselect: true, + condstore: false, + qresync: false, } } } @@ -23,8 +39,64 @@ impl ServerCapability { acc.push(Capability::Move); } if self.unselect { - acc.push(Capability::try_from("UNSELECT").unwrap()); + acc.push(capability_unselect()); + } + if self.condstore { + acc.push(capability_condstore()); + } + if self.qresync { + acc.push(capability_qresync()); } acc.try_into().unwrap() } + + pub fn support(&self, cap: &Capability<'static>) -> bool { + match cap { + Capability::Imap4Rev1 => true, + Capability::Move => self.r#move, + x if *x == capability_condstore() => self.condstore, + x if *x == capability_qresync() => self.qresync, + x if *x == capability_unselect() => self.unselect, + _ => false, + } + } +} + +pub struct ClientCapability { + condstore: bool, + qresync: bool, +} + +impl Default for ClientCapability { + fn default() -> Self { + Self { + condstore: false, + qresync: false, + } + } +} + +impl ClientCapability { + pub fn try_enable( + &mut self, + srv: &ServerCapability, + caps: &[Capability<'static>], + ) -> Vec> { + let mut enabled = vec![]; + for cap in caps { + match cap { + x if *x == capability_condstore() && srv.condstore && !self.condstore => { + self.condstore = true; + enabled.push(x.clone()); + } + x if *x == capability_qresync() && srv.qresync && !self.qresync => { + self.qresync = true; + enabled.push(x.clone()); + } + _ => (), + } + } + + enabled + } } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index 2970b63..fbf29f9 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -305,6 +305,9 @@ impl<'a> AuthenticatedContext<'a> { StatusDataItemName::DeletedStorage => { bail!("quota not implemented, can't return freed storage after EXPUNGE will be run"); }, + StatusDataItemName::HighestModSeq => { + bail!("highestmodseq not yet implemented"); + } }); } @@ -524,7 +527,7 @@ impl<'a> AuthenticatedContext<'a> { }; if date.is_some() { - bail!("Cannot set date when appending message"); + tracing::warn!("Cannot set date when appending message"); } let msg = diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 0b5555a..aac1fd3 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -89,7 +89,7 @@ async fn client_wrapper(ctx: ClientContext) { let addr = ctx.addr.clone(); match client(ctx).await { Ok(()) => { - tracing::info!("closing successful session for {:?}", addr); + tracing::debug!("closing successful session for {:?}", addr); } Err(e) => { tracing::error!("closing errored session for {:?}: {}", addr, e); @@ -127,7 +127,9 @@ async fn client(mut ctx: ClientContext) -> Result<()> { Some(cmd_recv) => cmd_recv, }; + tracing::debug!(cmd=?cmd, sock=%ctx.addr, "command"); let maybe_response = session.command(cmd).await; + tracing::debug!(cmd=?maybe_response.completion, sock=%ctx.addr, "response"); match resp_tx.send(maybe_response) { Err(_) => break, diff --git a/src/mail/incoming.rs b/src/mail/incoming.rs index 04d2ef1..781d8dc 100644 --- a/src/mail/incoming.rs +++ b/src/mail/incoming.rs @@ -9,7 +9,7 @@ use base64::Engine; use futures::{future::BoxFuture, FutureExt}; //use tokio::io::AsyncReadExt; use tokio::sync::watch; -use tracing::{error, info, warn}; +use tracing::{debug, error, info, warn}; use crate::cryptoblob; use crate::login::{Credentials, PublicCredentials}; @@ -62,7 +62,7 @@ async fn incoming_mail_watch_process_internal( loop { let maybe_updated_incoming_key = if *lock_held.borrow() { - info!("incoming lock held"); + debug!("incoming lock held"); let wait_new_mail = async { loop { @@ -83,7 +83,7 @@ async fn incoming_mail_watch_process_internal( _ = rx_inbox_id.changed() => None, } } else { - info!("incoming lock not held"); + debug!("incoming lock not held"); tokio::select! { _ = lock_held.changed() => None, _ = rx_inbox_id.changed() => None, @@ -93,11 +93,11 @@ async fn incoming_mail_watch_process_internal( let user = match Weak::upgrade(&user) { Some(user) => user, None => { - info!("User no longer available, exiting incoming loop."); + debug!("User no longer available, exiting incoming loop."); break; } }; - info!("User still available"); + debug!("User still available"); // If INBOX no longer is same mailbox, open new mailbox let inbox_id = *rx_inbox_id.borrow(); @@ -235,7 +235,7 @@ async fn k2v_lock_loop_internal( let watch_lock_loop: BoxFuture> = async { let mut ct = row_ref.clone(); loop { - info!("k2v watch lock loop iter: ct = {:?}", ct); + debug!("k2v watch lock loop iter: ct = {:?}", ct); match storage.row_poll(&ct).await { Err(e) => { error!( @@ -263,7 +263,7 @@ async fn k2v_lock_loop_internal( } let new_ct = cv.row_ref; - info!( + debug!( "k2v watch lock loop: changed, old ct = {:?}, new ct = {:?}, v = {:?}", ct, new_ct, lock_state ); @@ -378,7 +378,7 @@ async fn k2v_lock_loop_internal( let _ = futures::try_join!(watch_lock_loop, lock_notify_loop, take_lock_loop); - info!("lock loop exited, releasing"); + debug!("lock loop exited, releasing"); if !held_tx.is_closed() { warn!("weird..."); From 7de1c66d86dbfe8e8ef4ed1183e5a9b6a3a636bf Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 18:25:37 +0100 Subject: [PATCH 08/15] Thunderbird is now able to correctly list msg info --- src/imap/mailbox_view.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index 206b905..861d27c 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -1273,7 +1273,11 @@ fn get_message_section<'a>( part.as_ref().map(|p| p.0.as_ref()).unwrap_or(&[]), |part_msg| { let mut ret = vec![]; - for f in &part_msg.mime().kv { + let mime = match &part_msg { + AnyPart::Msg(msg) => msg.child.mime(), + other => other.mime(), + }; + for f in mime.kv.iter() { let (k, v) = match f { header::Field::Good(header::Kv2(k, v)) => (k, v), _ => continue, @@ -1303,7 +1307,7 @@ fn get_message_section<'a>( let bytes = match &part { AnyPart::Txt(p) => p.mime.fields.raw, AnyPart::Bin(p) => p.mime.fields.raw, - AnyPart::Msg(p) => p.mime.fields.raw, + AnyPart::Msg(p) => p.child.mime().raw, AnyPart::Mult(p) => p.mime.fields.raw, }; Ok(bytes.to_vec().into()) From a6a0e1994db2b43f559e5c2e8ae5c58342e5136f Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 20:53:07 +0100 Subject: [PATCH 09/15] ENABLE is now supported --- src/imap/capability.rs | 93 +++++++++++++------------------ src/imap/command/authenticated.rs | 25 ++++++++- src/imap/command/examined.rs | 4 +- src/imap/command/selected.rs | 4 +- src/imap/session.rs | 8 ++- 5 files changed, 75 insertions(+), 59 deletions(-) diff --git a/src/imap/capability.rs b/src/imap/capability.rs index f6b5a51..3929558 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -1,5 +1,7 @@ use imap_codec::imap_types::core::NonEmptyVec; use imap_codec::imap_types::response::Capability; +use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind}; +use std::collections::HashSet; fn capability_unselect() -> Capability<'static> { Capability::try_from("UNSELECT").unwrap() @@ -14,84 +16,67 @@ fn capability_qresync() -> Capability<'static> { } #[derive(Debug, Clone)] -pub struct ServerCapability { - r#move: bool, - unselect: bool, - condstore: bool, - qresync: bool, -} +pub struct ServerCapability(HashSet>); impl Default for ServerCapability { fn default() -> Self { - Self { - r#move: true, - unselect: true, - condstore: false, - qresync: false, - } + Self(HashSet::from([ + Capability::Imap4Rev1, + Capability::Move, + capability_unselect(), + //capability_condstore(), + //capability_qresync(), + ])) } } impl ServerCapability { pub fn to_vec(&self) -> NonEmptyVec> { - let mut acc = vec![Capability::Imap4Rev1]; - if self.r#move { - acc.push(Capability::Move); - } - if self.unselect { - acc.push(capability_unselect()); - } - if self.condstore { - acc.push(capability_condstore()); - } - if self.qresync { - acc.push(capability_qresync()); - } - acc.try_into().unwrap() + self.0.iter().map(|v| v.clone()).collect::>().try_into().unwrap() } + #[allow(dead_code)] pub fn support(&self, cap: &Capability<'static>) -> bool { - match cap { - Capability::Imap4Rev1 => true, - Capability::Move => self.r#move, - x if *x == capability_condstore() => self.condstore, - x if *x == capability_qresync() => self.qresync, - x if *x == capability_unselect() => self.unselect, - _ => false, - } + self.0.contains(cap) } } +enum ClientStatus { + NotSupportedByServer, + Disabled, + Enabled, +} + pub struct ClientCapability { - condstore: bool, - qresync: bool, -} - -impl Default for ClientCapability { - fn default() -> Self { - Self { - condstore: false, - qresync: false, - } - } + condstore: ClientStatus, + utf8kind: Option, } impl ClientCapability { + pub fn new(sc: &ServerCapability) -> Self { + Self { + condstore: match sc.0.contains(&capability_condstore()) { + true => ClientStatus::Disabled, + _ => ClientStatus::NotSupportedByServer, + }, + utf8kind: None, + } + } + pub fn try_enable( &mut self, - srv: &ServerCapability, - caps: &[Capability<'static>], - ) -> Vec> { + caps: &[CapabilityEnable<'static>], + ) -> Vec> { let mut enabled = vec![]; for cap in caps { match cap { - x if *x == capability_condstore() && srv.condstore && !self.condstore => { - self.condstore = true; - enabled.push(x.clone()); + CapabilityEnable::CondStore if matches!(self.condstore, ClientStatus::Disabled) => { + self.condstore = ClientStatus::Enabled; + enabled.push(cap.clone()); } - x if *x == capability_qresync() && srv.qresync && !self.qresync => { - self.qresync = true; - enabled.push(x.clone()); + CapabilityEnable::Utf8(kind) if Some(kind) != self.utf8kind.as_ref() => { + self.utf8kind = Some(kind.clone()); + enabled.push(cap.clone()); } _ => (), } diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index fbf29f9..ee7c8f3 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -3,14 +3,15 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::{Atom, Literal, QuotedChar}; +use imap_codec::imap_types::core::{Atom, Literal, QuotedChar, NonEmptyVec}; use imap_codec::imap_types::datetime::DateTime; use imap_codec::imap_types::flag::{Flag, FlagNameAttribute}; use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::imap_types::response::{Code, CodeOther, Data}; use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; +use imap_codec::imap_types::extensions::enable::CapabilityEnable; -use crate::imap::capability::ServerCapability; +use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::command::{anystate, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; @@ -24,6 +25,7 @@ use crate::mail::IMF; pub struct AuthenticatedContext<'a> { pub req: &'a Command<'static>, pub server_capabilities: &'a ServerCapability, + pub client_capabilities: &'a mut ClientCapability, pub user: &'a Arc, } @@ -65,6 +67,11 @@ pub async fn dispatch<'a>( message, } => ctx.append(mailbox, flags, date, message).await, + // rfc5161 ENABLE + CommandBody::Enable { capabilities } => { + ctx.enable(capabilities) + }, + // Collect other commands _ => anystate::wrong_state(ctx.req.tag.clone()), } @@ -511,6 +518,20 @@ impl<'a> AuthenticatedContext<'a> { } } + fn enable(self, cap_enable: &NonEmptyVec>) -> Result<(Response<'static>, flow::Transition)> { + let mut response_builder = Response::build().to_req(self.req); + let capabilities = self.client_capabilities.try_enable(cap_enable.as_ref()); + if capabilities.len() > 0 { + response_builder = response_builder.data(Data::Enabled { capabilities }); + } + Ok(( + response_builder + .message("ENABLE completed") + .ok()?, + flow::Transition::None, + )) + } + pub(crate) async fn append_internal( self, mailbox: &MailboxCodec<'a>, diff --git a/src/imap/command/examined.rs b/src/imap/command/examined.rs index bddd0f9..0d688c0 100644 --- a/src/imap/command/examined.rs +++ b/src/imap/command/examined.rs @@ -7,7 +7,7 @@ use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; -use crate::imap::capability::ServerCapability; +use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::command::{anystate, authenticated}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; @@ -19,6 +19,7 @@ pub struct ExaminedContext<'a> { pub user: &'a Arc, pub mailbox: &'a mut MailboxView, pub server_capabilities: &'a ServerCapability, + pub client_capabilities: &'a mut ClientCapability, } pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, flow::Transition)> { @@ -60,6 +61,7 @@ pub async fn dispatch(ctx: ExaminedContext<'_>) -> Result<(Response<'static>, fl authenticated::dispatch(authenticated::AuthenticatedContext { req: ctx.req, server_capabilities: ctx.server_capabilities, + client_capabilities: ctx.client_capabilities, user: ctx.user, }) .await diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs index 28ebbe8..c8cc680 100644 --- a/src/imap/command/selected.rs +++ b/src/imap/command/selected.rs @@ -10,7 +10,7 @@ use imap_codec::imap_types::response::{Code, CodeOther}; use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::sequence::SequenceSet; -use crate::imap::capability::ServerCapability; +use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::flow; use crate::imap::mailbox_view::MailboxView; @@ -23,6 +23,7 @@ pub struct SelectedContext<'a> { pub user: &'a Arc, pub mailbox: &'a mut MailboxView, pub server_capabilities: &'a ServerCapability, + pub client_capabilities: &'a mut ClientCapability, } pub async fn dispatch<'a>( @@ -76,6 +77,7 @@ pub async fn dispatch<'a>( authenticated::dispatch(authenticated::AuthenticatedContext { req: ctx.req, server_capabilities: ctx.server_capabilities, + client_capabilities: ctx.client_capabilities, user: ctx.user, }) .await diff --git a/src/imap/session.rs b/src/imap/session.rs index 55026b9..6b26478 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,4 +1,4 @@ -use crate::imap::capability::ServerCapability; +use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::command::{anonymous, authenticated, examined, selected}; use crate::imap::flow; use crate::imap::response::Response; @@ -9,14 +9,17 @@ use imap_codec::imap_types::command::Command; pub struct Instance { pub login_provider: ArcLoginProvider, pub server_capabilities: ServerCapability, + pub client_capabilities: ClientCapability, pub state: flow::State, } impl Instance { pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self { + let client_cap = ClientCapability::new(&cap); Self { login_provider, state: flow::State::NotAuthenticated, server_capabilities: cap, + client_capabilities: client_cap, } } @@ -36,6 +39,7 @@ impl Instance { let ctx = authenticated::AuthenticatedContext { req: &cmd, server_capabilities: &self.server_capabilities, + client_capabilities: &mut self.client_capabilities, user, }; authenticated::dispatch(ctx).await @@ -44,6 +48,7 @@ impl Instance { let ctx = selected::SelectedContext { req: &cmd, server_capabilities: &self.server_capabilities, + client_capabilities: &mut self.client_capabilities, user, mailbox, }; @@ -53,6 +58,7 @@ impl Instance { let ctx = examined::ExaminedContext { req: &cmd, server_capabilities: &self.server_capabilities, + client_capabilities: &mut self.client_capabilities, user, mailbox, }; From 8180baae0084faf76d49e6e03f8405487723c46f Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 20:53:25 +0100 Subject: [PATCH 10/15] format code --- src/imap/capability.rs | 9 +++++++-- src/imap/command/authenticated.rs | 17 ++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/imap/capability.rs b/src/imap/capability.rs index 3929558..9a641a7 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -1,6 +1,6 @@ use imap_codec::imap_types::core::NonEmptyVec; -use imap_codec::imap_types::response::Capability; use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind}; +use imap_codec::imap_types::response::Capability; use std::collections::HashSet; fn capability_unselect() -> Capability<'static> { @@ -32,7 +32,12 @@ impl Default for ServerCapability { impl ServerCapability { pub fn to_vec(&self) -> NonEmptyVec> { - self.0.iter().map(|v| v.clone()).collect::>().try_into().unwrap() + self.0 + .iter() + .map(|v| v.clone()) + .collect::>() + .try_into() + .unwrap() } #[allow(dead_code)] diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs index ee7c8f3..1481a80 100644 --- a/src/imap/command/authenticated.rs +++ b/src/imap/command/authenticated.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::{Atom, Literal, QuotedChar, NonEmptyVec}; +use imap_codec::imap_types::core::{Atom, Literal, NonEmptyVec, QuotedChar}; use imap_codec::imap_types::datetime::DateTime; +use imap_codec::imap_types::extensions::enable::CapabilityEnable; use imap_codec::imap_types::flag::{Flag, FlagNameAttribute}; use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; use imap_codec::imap_types::response::{Code, CodeOther, Data}; use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; -use imap_codec::imap_types::extensions::enable::CapabilityEnable; use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::command::{anystate, MailboxName}; @@ -68,9 +68,7 @@ pub async fn dispatch<'a>( } => ctx.append(mailbox, flags, date, message).await, // rfc5161 ENABLE - CommandBody::Enable { capabilities } => { - ctx.enable(capabilities) - }, + CommandBody::Enable { capabilities } => ctx.enable(capabilities), // Collect other commands _ => anystate::wrong_state(ctx.req.tag.clone()), @@ -518,16 +516,17 @@ impl<'a> AuthenticatedContext<'a> { } } - fn enable(self, cap_enable: &NonEmptyVec>) -> Result<(Response<'static>, flow::Transition)> { + fn enable( + self, + cap_enable: &NonEmptyVec>, + ) -> Result<(Response<'static>, flow::Transition)> { let mut response_builder = Response::build().to_req(self.req); let capabilities = self.client_capabilities.try_enable(cap_enable.as_ref()); if capabilities.len() > 0 { response_builder = response_builder.data(Data::Enabled { capabilities }); } Ok(( - response_builder - .message("ENABLE completed") - .ok()?, + response_builder.message("ENABLE completed").ok()?, flow::Transition::None, )) } From 3a10fb9faab6232030dcd4bf55b1b410e45d2282 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 3 Jan 2024 21:29:36 +0100 Subject: [PATCH 11/15] advertise literal support --- src/imap/capability.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/imap/capability.rs b/src/imap/capability.rs index 9a641a7..631c3e2 100644 --- a/src/imap/capability.rs +++ b/src/imap/capability.rs @@ -23,6 +23,7 @@ impl Default for ServerCapability { Self(HashSet::from([ Capability::Imap4Rev1, Capability::Move, + Capability::LiteralPlus, capability_unselect(), //capability_condstore(), //capability_qresync(), From a0a7dd0ed6afac48c5d5c0b149efb8288d03e115 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 4 Jan 2024 11:23:26 +0100 Subject: [PATCH 12/15] BDD pattern is clarified --- tests/common/constants.rs | 54 +++++ tests/common/fragments.rs | 366 +++++++++++++++++++++++++++++ tests/{common.rs => common/mod.rs} | 28 ++- tests/rfc3501_imap4rev1_base.rs | 338 ++------------------------ tests/rfc3691_imapext_unselect.rs | 188 +-------------- tests/rfc6851_imapext_move.rs | 188 ++------------- 6 files changed, 487 insertions(+), 675 deletions(-) create mode 100644 tests/common/constants.rs create mode 100644 tests/common/fragments.rs rename tests/{common.rs => common/mod.rs} (68%) diff --git a/tests/common/constants.rs b/tests/common/constants.rs new file mode 100644 index 0000000..c11a04d --- /dev/null +++ b/tests/common/constants.rs @@ -0,0 +1,54 @@ +use std::time; + +pub static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); + +pub static EMAIL1: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r +From: Bob Robert \r +To: Alice Malice \r +CC: =?ISO-8859-1?Q?Andr=E9?= Pirard \r +Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r + =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\r +X-Unknown: something something\r +Bad entry\r + on multiple lines\r +Message-ID: \r +MIME-Version: 1.0\r +Content-Type: multipart/alternative;\r + boundary=\"b1_e376dc71bafc953c0b0fdeb9983a9956\"\r +Content-Transfer-Encoding: 7bit\r +\r +This is a multi-part message in MIME format.\r +\r +--b1_e376dc71bafc953c0b0fdeb9983a9956\r +Content-Type: text/plain; charset=utf-8\r +Content-Transfer-Encoding: quoted-printable\r +\r +GZ\r +OoOoO\r +oOoOoOoOo\r +oOoOoOoOoOoOoOoOo\r +oOoOoOoOoOoOoOoOoOoOoOo\r +oOoOoOoOoOoOoOoOoOoOoOoOoOoOo\r +OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\r +\r +--b1_e376dc71bafc953c0b0fdeb9983a9956\r +Content-Type: text/html; charset=us-ascii\r +\r +
GZ
\r +OoOoO
\r +oOoOoOoOo
\r +oOoOoOoOoOoOoOoOo
\r +oOoOoOoOoOoOoOoOoOoOoOo
\r +oOoOoOoOoOoOoOoOoOoOoOoOoOoOo
\r +OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO
\r +
\r +\r +--b1_e376dc71bafc953c0b0fdeb9983a9956--\r +"; + +pub static EMAIL2: &[u8] = b"From: alice@example.com\r +To: alice@example.tld\r +Subject: Test\r +\r +Hello world!\r +"; diff --git a/tests/common/fragments.rs b/tests/common/fragments.rs new file mode 100644 index 0000000..530f1f7 --- /dev/null +++ b/tests/common/fragments.rs @@ -0,0 +1,366 @@ +use anyhow::{bail, Result}; +use std::io::Write; +use std::net::TcpStream; +use std::thread; + +use crate::common::constants::*; +use crate::common::*; + +/// These fragments are not a generic IMAP client +/// but specialized to our specific tests. They can't take +/// arbitrary values, only enum for which the code is known +/// to be correct. The idea is that the generated message is more +/// or less hardcoded by the developer, so its clear what's expected, +/// and not generated by a library. + +pub fn connect(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..4], &b"* OK"[..]); + + Ok(()) +} + +pub enum Account { + Alice, +} + +pub enum Extension { + None, + Unselect, + Move, + CondStore, +} + +pub enum Mailbox { + Inbox, + Archive, + Drafts, +} + +pub enum Flag { + Deleted, + Important +} + +pub enum Email { + Basic, + Multipart, +} + +pub enum Selection { + FirstId, + SecondId, +} + +pub fn capability(imap: &mut TcpStream, ext: Extension) -> Result<()> { + imap.write(&b"5 capability\r\n"[..])?; + + let maybe_ext = match ext { + Extension::None => None, + Extension::Unselect => Some("UNSELECT"), + Extension::Move => Some("MOVE"), + Extension::CondStore => Some("CONDSTORE"), + }; + + let mut buffer: [u8; 1500] = [0; 1500]; + let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains("IMAP4REV1")); + if let Some(ext) = maybe_ext { + assert!(srv_msg.contains(ext)); + } + + Ok(()) +} + +pub fn login(imap: &mut TcpStream, account: Account) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + assert!(matches!(account, Account::Alice)); + imap.write(&b"10 login alice hunter2\r\n"[..])?; + + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"10 OK"[..]); + + Ok(()) +} + +pub fn create_mailbox(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + let mbx_str = match mbx { + Mailbox::Inbox => "INBOX", + Mailbox::Archive => "Archive", + Mailbox::Drafts => "Drafts", + }; + + let cmd = format!("15 create {}\r\n", mbx_str); + imap.write(cmd.as_bytes())?; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..12], &b"15 OK CREATE"[..]); + + Ok(()) +} + +pub fn select(imap: &mut TcpStream, mbx: Mailbox, maybe_exists: Option) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + let mbx_str = match mbx { + Mailbox::Inbox => "INBOX", + Mailbox::Archive => "Archive", + Mailbox::Drafts => "Drafts", + }; + imap.write(format!("20 select {}\r\n", mbx_str).as_bytes())?; + + let read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + if let Some(exists) = maybe_exists { + let expected = format!("* {} EXISTS", exists); + assert!(srv_msg.contains(&expected)); + } + + Ok(()) +} + +pub fn unselect(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"70 unselect\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let _read = read_lines(imap, &mut buffer, Some(&b"70 OK"[..]))?; + + Ok(()) +} + +pub fn check(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + imap.write(&b"21 check\r\n"[..])?; + let _read = read_lines(imap, &mut buffer, Some(&b"21 OK"[..]))?; + + Ok(()) +} + +pub fn status_mailbox(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> { + assert!(matches!(mbx, Mailbox::Archive)); + imap.write(&b"25 STATUS Archive (UIDNEXT MESSAGES)\r\n"[..])?; + let mut buffer: [u8; 6000] = [0; 6000]; + let _read = read_lines(imap, &mut buffer, Some(&b"25 OK"[..]))?; + + Ok(()) +} + +pub fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + let _read = read_lines(lmtp, &mut buffer, None)?; + assert_eq!(&buffer[..4], &b"220 "[..]); + + lmtp.write(&b"LHLO example.tld\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 "[..]))?; + + Ok(()) +} + +pub fn lmtp_deliver_email(lmtp: &mut TcpStream, email_type: Email) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + let email = match email_type { + Email::Basic => EMAIL2, + Email::Multipart => EMAIL1, + }; + lmtp.write(&b"MAIL FROM:\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; + + lmtp.write(&b"RCPT TO:\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.1.5"[..]))?; + + lmtp.write(&b"DATA\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"354 "[..]))?; + + lmtp.write(email)?; + lmtp.write(&b"\r\n.\r\n"[..])?; + let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; + + Ok(()) +} + +pub fn noop_exists(imap: &mut TcpStream) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + let mut max_retry = 20; + loop { + max_retry -= 1; + imap.write(&b"30 NOOP\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"30 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + + match (max_retry, srv_msg.lines().count()) { + (_, cnt) if cnt > 1 => break, + (0, _) => bail!("no more retry"), + _ => (), + } + + thread::sleep(SMALL_DELAY); + } + + Ok(()) +} + +pub fn fetch_rfc822(imap: &mut TcpStream, selection: Selection, r#ref: Email) -> Result<()> { + let mut buffer: [u8; 65535] = [0; 65535]; + + assert!(matches!(selection, Selection::FirstId)); + imap.write(&b"40 fetch 1 rfc822\r\n"[..])?; + + let read = read_lines(imap, &mut buffer, Some(&b"40 OK FETCH"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + + let ref_mail = match r#ref { + Email::Basic => EMAIL2, + Email::Multipart => EMAIL1, + }; + let orig_email = std::str::from_utf8(ref_mail)?; + assert!(srv_msg.contains(orig_email)); + + Ok(()) +} + +pub fn copy(imap: &mut TcpStream, selection: Selection, to: Mailbox) -> Result<()> { + let mut buffer: [u8; 65535] = [0; 65535]; + assert!(matches!(selection, Selection::FirstId)); + assert!(matches!(to, Mailbox::Archive)); + + imap.write(&b"45 copy 1 Archive\r\n"[..])?; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"45 OK"[..]); + + Ok(()) +} + +pub fn append_email(imap: &mut TcpStream, content: Email) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + + let ref_mail = match content { + Email::Multipart => EMAIL1, + Email::Basic => EMAIL2, + }; + + let append_cmd = format!("47 append inbox (\\Seen) {{{}}}\r\n", ref_mail.len()); + println!("append cmd: {}", append_cmd); + imap.write(append_cmd.as_bytes())?; + + // wait for continuation + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(read[0], b'+'); + + // write our stuff + imap.write(ref_mail)?; + imap.write(&b"\r\n"[..])?; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"47 OK"[..]); + + // we check that noop detects the change + noop_exists(imap)?; + + Ok(()) +} + + + +pub fn add_flags_email(imap: &mut TcpStream, selection: Selection, flag: Flag) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + assert!(matches!(selection, Selection::FirstId)); + assert!(matches!(flag, Flag::Deleted)); + imap.write(&b"50 store 1 +FLAGS (\\Deleted)\r\n"[..])?; + let _read = read_lines(imap, &mut buffer, Some(&b"50 OK STORE"[..]))?; + + Ok(()) +} + +#[allow(dead_code)] +/// Not yet implemented +pub fn search(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"55 search text \"OoOoO\"\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let _read = read_lines(imap, &mut buffer, Some(&b"55 OK SEARCH"[..]))?; + Ok(()) +} + +pub fn expunge(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"60 expunge\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let _read = read_lines(imap, &mut buffer, Some(&b"60 OK EXPUNGE"[..]))?; + + Ok(()) +} + +pub fn rename_mailbox(imap: &mut TcpStream, from: Mailbox, to: Mailbox) -> Result<()> { + assert!(matches!(from, Mailbox::Archive)); + assert!(matches!(to, Mailbox::Drafts)); + + imap.write(&b"70 rename Archive Drafts\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"70 OK"[..]); + + imap.write(&b"71 list \"\" *\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"71 OK LIST"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(!srv_msg.contains(" Archive\r\n")); + assert!(srv_msg.contains(" INBOX\r\n")); + assert!(srv_msg.contains(" Drafts\r\n")); + + Ok(()) +} + +pub fn delete_mailbox(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> { + let mbx_str = match mbx { + Mailbox::Inbox => "INBOX", + Mailbox::Archive => "Archive", + Mailbox::Drafts => "Drafts", + }; + let cmd = format!("80 delete {}\r\n", mbx_str); + + imap.write(cmd.as_bytes())?; + let mut buffer: [u8; 1500] = [0; 1500]; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"80 OK"[..]); + + imap.write(&b"81 list \"\" *\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"81 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains(" INBOX\r\n")); + assert!(!srv_msg.contains(format!(" {}\r\n", mbx_str).as_str())); + + Ok(()) +} + +pub fn close(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"60 close\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let _read = read_lines(imap, &mut buffer, Some(&b"60 OK"[..]))?; + + Ok(()) +} + +pub fn r#move(imap: &mut TcpStream, selection: Selection, to: Mailbox) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + assert!(matches!(to, Mailbox::Archive)); + assert!(matches!(selection, Selection::FirstId)); + + imap.write(&b"35 move 1 Archive\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"35 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + assert!(srv_msg.contains("* 1 EXPUNGE")); + + Ok(()) +} + +pub fn logout(imap: &mut TcpStream) -> Result<()> { + imap.write(&b"99 logout\r\n"[..])?; + let mut buffer: [u8; 1500] = [0; 1500]; + let read = read_lines(imap, &mut buffer, None)?; + assert_eq!(&read[..5], &b"* BYE"[..]); + Ok(()) +} diff --git a/tests/common.rs b/tests/common/mod.rs similarity index 68% rename from tests/common.rs rename to tests/common/mod.rs index f89f26c..d0f4ed8 100644 --- a/tests/common.rs +++ b/tests/common/mod.rs @@ -1,33 +1,55 @@ +#![allow(dead_code)] +pub mod constants; +pub mod fragments; + use anyhow::{bail, Context, Result}; use std::io::Read; use std::net::{Shutdown, TcpStream}; use std::process::Command; -use std::{thread, time}; +use std::thread; -static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); +use constants::SMALL_DELAY; pub fn aerogramme_provider_daemon_dev( mut fx: impl FnMut(&mut TcpStream, &mut TcpStream) -> Result<()>, ) -> Result<()> { + // Check port is not used (= free) before starting the test + let mut max_retry = 20; + loop { + max_retry -= 1; + match (TcpStream::connect("[::1]:1143"), max_retry) { + (Ok(_), 0) => bail!("something is listening on [::1]:1143 and prevent the test from starting"), + (Ok(_), _) => println!("something is listening on [::1]:1143, maybe a previous daemon quitting, retrying soon..."), + (Err(_), _) => { + println!("test ready to start, [::1]:1143 is free!"); + break + } + } + thread::sleep(SMALL_DELAY); + } + + // Start daemon let mut daemon = Command::new(env!("CARGO_BIN_EXE_aerogramme")) .arg("--dev") .arg("provider") .arg("daemon") .spawn()?; + // Check that our daemon is correctly listening on the free port let mut max_retry = 20; let mut imap_socket = loop { max_retry -= 1; match (TcpStream::connect("[::1]:1143"), max_retry) { (Err(e), 0) => bail!("no more retry, last error is: {}", e), (Err(e), _) => { - println!("unable to connect: {} ; will retry in 1 sec", e); + println!("unable to connect: {} ; will retry soon...", e); } (Ok(v), _) => break v, } thread::sleep(SMALL_DELAY); }; + // Assuming now it's safe to open a LMTP socket let mut lmtp_socket = TcpStream::connect("[::1]:1025").context("lmtp socket must be connected")?; diff --git a/tests/rfc3501_imap4rev1_base.rs b/tests/rfc3501_imap4rev1_base.rs index c0985dd..690913a 100644 --- a/tests/rfc3501_imap4rev1_base.rs +++ b/tests/rfc3501_imap4rev1_base.rs @@ -1,341 +1,33 @@ -use anyhow::{bail, Context, Result}; -use std::io::Write; -use std::net::TcpStream; -use std::{thread, time}; +use anyhow::Context; mod common; -use crate::common::read_lines; - -static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); +use crate::common::fragments::*; fn main() { common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { connect(imap_socket).context("server says hello")?; - capability(imap_socket).context("check server capabilities")?; - login(imap_socket).context("login test")?; - create_mailbox(imap_socket).context("created mailbox archive")?; + capability(imap_socket, Extension::None).context("check server capabilities")?; + login(imap_socket, Account::Alice).context("login test")?; + create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?; // UNSUBSCRIBE IS NOT IMPLEMENTED YET //unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?; - select_inbox(imap_socket).context("select inbox")?; + select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; check(imap_socket).context("check must run")?; - status_mailbox(imap_socket).context("status of archive from inbox")?; + status_mailbox(imap_socket, Mailbox::Archive).context("status of archive from inbox")?; lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; - lmtp_deliver_email(lmtp_socket, EMAIL1).context("mail delivered successfully")?; + lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?; noop_exists(imap_socket).context("noop loop must detect a new email")?; - fetch_rfc822(imap_socket, EMAIL1).context("fetch rfc822 message")?; - copy_email(imap_socket).context("copy message to the archive mailbox")?; - append_email(imap_socket, EMAIL2).context("insert email in INBOX")?; + fetch_rfc822(imap_socket, Selection::FirstId, Email::Multipart).context("fetch rfc822 message, should be our first message")?; + copy(imap_socket, Selection::FirstId, Mailbox::Archive).context("copy message to the archive mailbox")?; + append_email(imap_socket, Email::Basic).context("insert email in INBOX")?; // SEARCH IS NOT IMPLEMENTED YET //search(imap_socket).expect("search should return something"); - add_flags_email(imap_socket) - .context("should add delete and important flags to the email")?; + add_flags_email(imap_socket, Selection::FirstId, Flag::Deleted) + .context("should add delete flag to the email")?; expunge(imap_socket).context("expunge emails")?; - rename_mailbox(imap_socket).context("archive mailbox is renamed my-archives")?; - delete_mailbox(imap_socket).context("my-archives mailbox is deleted")?; + rename_mailbox(imap_socket, Mailbox::Archive, Mailbox::Drafts).context("Archive mailbox is renamed Drafts")?; + delete_mailbox(imap_socket, Mailbox::Drafts).context("Drafts mailbox is deleted")?; Ok(()) }) .expect("test fully run"); } - -fn connect(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..4], &b"* OK"[..]); - - Ok(()) -} - -fn capability(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"5 capability\r\n"[..])?; - - let mut buffer: [u8; 1500] = [0; 1500]; - let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - assert!(srv_msg.contains("IMAP4REV1")); - - Ok(()) -} - -fn login(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - imap.write(&b"10 login alice hunter2\r\n"[..])?; - - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..5], &b"10 OK"[..]); - - Ok(()) -} - -fn create_mailbox(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - imap.write(&b"15 create archive\r\n"[..])?; - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..12], &b"15 OK CREATE"[..]); - - Ok(()) -} - -#[allow(dead_code)] -fn unsubscribe_mailbox(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 6000] = [0; 6000]; - - imap.write(&b"16 lsub \"\" *\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"16 OK LSUB"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - assert!(srv_msg.contains(" INBOX\r\n")); - assert!(srv_msg.contains(" archive\r\n")); - - imap.write(&b"17 unsubscribe archive\r\n"[..])?; - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..5], &b"17 OK"[..]); - - imap.write(&b"18 lsub \"\" *\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"18 OK LSUB"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - assert!(srv_msg.contains(" INBOX\r\n")); - assert!(!srv_msg.contains(" archive\r\n")); - - Ok(()) -} - -fn select_inbox(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 6000] = [0; 6000]; - - imap.write(&b"20 select inbox\r\n"[..])?; - let _read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?; - - Ok(()) -} - -fn check(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - imap.write(&b"21 check\r\n"[..])?; - let _read = read_lines(imap, &mut buffer, Some(&b"21 OK"[..]))?; - - Ok(()) -} - -fn status_mailbox(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"25 STATUS archive (UIDNEXT MESSAGES)\r\n"[..])?; - let mut buffer: [u8; 6000] = [0; 6000]; - let _read = read_lines(imap, &mut buffer, Some(&b"25 OK"[..]))?; - - Ok(()) -} - -fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - let _read = read_lines(lmtp, &mut buffer, None)?; - assert_eq!(&buffer[..4], &b"220 "[..]); - - lmtp.write(&b"LHLO example.tld\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 "[..]))?; - - Ok(()) -} - -fn lmtp_deliver_email(lmtp: &mut TcpStream, email: &[u8]) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - lmtp.write(&b"MAIL FROM:\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; - - lmtp.write(&b"RCPT TO:\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.1.5"[..]))?; - - lmtp.write(&b"DATA\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"354 "[..]))?; - - lmtp.write(email)?; - lmtp.write(&b"\r\n.\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; - - Ok(()) -} - -fn noop_exists(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 6000] = [0; 6000]; - - let mut max_retry = 20; - loop { - max_retry -= 1; - imap.write(&b"30 NOOP\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"30 OK NOOP"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - - match (max_retry, srv_msg.contains("* 1 EXISTS")) { - (_, true) => break, - (0, _) => bail!("no more retry"), - _ => (), - } - - thread::sleep(SMALL_DELAY); - } - - Ok(()) -} - -fn fetch_rfc822(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> { - let mut buffer: [u8; 65535] = [0; 65535]; - imap.write(&b"40 fetch 1 rfc822\r\n"[..])?; - - let read = read_lines(imap, &mut buffer, Some(&b"40 OK FETCH"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - let orig_email = std::str::from_utf8(ref_mail)?; - assert!(srv_msg.contains(orig_email)); - - Ok(()) -} - -fn copy_email(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 65535] = [0; 65535]; - imap.write(&b"45 copy 1 archive\r\n"[..])?; - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..5], &b"45 OK"[..]); - - Ok(()) -} - -fn append_email(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> { - let mut buffer: [u8; 6000] = [0; 6000]; - assert_ne!(ref_mail.len(), 0); - let append_cmd = format!("47 append inbox (\\Seen) {{{}}}\r\n", ref_mail.len()); - println!("append cmd: {}", append_cmd); - imap.write(append_cmd.as_bytes())?; - - // wait for continuation - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(read[0], b'+'); - - // write our stuff - imap.write(ref_mail)?; - imap.write(&b"\r\n"[..])?; - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..5], &b"47 OK"[..]); - - // noop to force a sync - imap.write(&b"48 NOOP\r\n"[..])?; - let _read = read_lines(imap, &mut buffer, Some(&b"48 OK NOOP"[..]))?; - - // check it is stored successfully - imap.write(&b"49 fetch 2 rfc822.size\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"49 OK"[..]))?; - let expected = format!("* 2 FETCH (RFC822.SIZE {})", ref_mail.len()); - let expbytes = expected.as_bytes(); - assert_eq!(&read[..expbytes.len()], expbytes); - - Ok(()) -} - -fn add_flags_email(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - imap.write(&b"50 store 1 +FLAGS (\\Deleted \\Important)\r\n"[..])?; - let _read = read_lines(imap, &mut buffer, Some(&b"50 OK STORE"[..]))?; - - Ok(()) -} - -#[allow(dead_code)] -/// Not yet implemented -fn search(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"55 search text \"OoOoO\"\r\n"[..])?; - let mut buffer: [u8; 1500] = [0; 1500]; - let _read = read_lines(imap, &mut buffer, Some(&b"55 OK SEARCH"[..]))?; - Ok(()) -} - -fn expunge(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"60 expunge\r\n"[..])?; - let mut buffer: [u8; 1500] = [0; 1500]; - let _read = read_lines(imap, &mut buffer, Some(&b"60 OK EXPUNGE"[..]))?; - - Ok(()) -} - -fn rename_mailbox(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"70 rename archive my-archives\r\n"[..])?; - let mut buffer: [u8; 1500] = [0; 1500]; - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..5], &b"70 OK"[..]); - - imap.write(&b"71 list \"\" *\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"71 OK LIST"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - assert!(!srv_msg.contains(" archive\r\n")); - assert!(srv_msg.contains(" INBOX\r\n")); - assert!(srv_msg.contains(" my-archives\r\n")); - - Ok(()) -} - -fn delete_mailbox(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"80 delete my-archives\r\n"[..])?; - let mut buffer: [u8; 1500] = [0; 1500]; - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..5], &b"80 OK"[..]); - - imap.write(&b"81 list \"\" *\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"81 OK LIST"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - assert!(!srv_msg.contains(" archive\r\n")); - assert!(!srv_msg.contains(" my-archives\r\n")); - assert!(srv_msg.contains(" INBOX\r\n")); - - Ok(()) -} - -static EMAIL1: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r -From: Bob Robert \r -To: Alice Malice \r -CC: =?ISO-8859-1?Q?Andr=E9?= Pirard \r -Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r - =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\r -X-Unknown: something something\r -Bad entry\r - on multiple lines\r -Message-ID: \r -MIME-Version: 1.0\r -Content-Type: multipart/alternative;\r - boundary=\"b1_e376dc71bafc953c0b0fdeb9983a9956\"\r -Content-Transfer-Encoding: 7bit\r -\r -This is a multi-part message in MIME format.\r -\r ---b1_e376dc71bafc953c0b0fdeb9983a9956\r -Content-Type: text/plain; charset=utf-8\r -Content-Transfer-Encoding: quoted-printable\r -\r -GZ\r -OoOoO\r -oOoOoOoOo\r -oOoOoOoOoOoOoOoOo\r -oOoOoOoOoOoOoOoOoOoOoOo\r -oOoOoOoOoOoOoOoOoOoOoOoOoOoOo\r -OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\r -\r ---b1_e376dc71bafc953c0b0fdeb9983a9956\r -Content-Type: text/html; charset=us-ascii\r -\r -
GZ
\r -OoOoO
\r -oOoOoOoOo
\r -oOoOoOoOoOoOoOoOo
\r -oOoOoOoOoOoOoOoOoOoOoOo
\r -oOoOoOoOoOoOoOoOoOoOoOoOoOoOo
\r -OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO
\r -
\r -\r ---b1_e376dc71bafc953c0b0fdeb9983a9956--\r -"; - -static EMAIL2: &[u8] = b"From: alice@example.com\r -To: alice@example.tld\r -Subject: Test\r -\r -Hello world!\r -"; diff --git a/tests/rfc3691_imapext_unselect.rs b/tests/rfc3691_imapext_unselect.rs index 28e4bc8..4c5a45d 100644 --- a/tests/rfc3691_imapext_unselect.rs +++ b/tests/rfc3691_imapext_unselect.rs @@ -1,190 +1,28 @@ -use anyhow::{bail, Context, Result}; -use std::io::Write; -use std::net::TcpStream; -use std::{thread, time}; +use anyhow::Context; mod common; -use crate::common::read_lines; - -static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); -static EMAIL: &[u8] = b"From: alice@example.com\r -To: alice@example.tld\r -Subject: Test\r -\r -Hello world!\r -"; +use crate::common::fragments::*; fn main() { common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { - lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; - lmtp_deliver_email(lmtp_socket, EMAIL).context("mail delivered successfully")?; - connect(imap_socket).context("server says hello")?; - capability(imap_socket).context("check server capabilities")?; - login(imap_socket).context("login test")?; - select_inbox(imap_socket).context("select inbox")?; + + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; + + capability(imap_socket, Extension::Unselect).context("check server capabilities")?; + login(imap_socket, Account::Alice).context("login test")?; + select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; noop_exists(imap_socket).context("noop loop must detect a new email")?; - add_flags_email(imap_socket).context("add delete flags to the email")?; + add_flags_email(imap_socket, Selection::FirstId, Flag::Deleted).context("add delete flags to the email")?; unselect(imap_socket) .context("unselect inbox while preserving email with the \\Delete flag")?; - select_inbox_one_msg(imap_socket).context("select inbox again")?; - fetch_rfc822(imap_socket, EMAIL).context("message is still present")?; + select(imap_socket, Mailbox::Inbox, Some(1)).context("select inbox again")?; + fetch_rfc822(imap_socket, Selection::FirstId, Email::Basic).context("message is still present")?; close(imap_socket).context("close inbox and expunge message")?; - select_inbox_zero_msg(imap_socket).context("select inbox again and check it's empty")?; + select(imap_socket, Mailbox::Inbox, Some(0)).context("select inbox again and check it's empty")?; Ok(()) }) .expect("test fully run"); } - -fn connect(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..4], &b"* OK"[..]); - - Ok(()) -} - -fn capability(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"5 capability\r\n"[..])?; - - let mut buffer: [u8; 1500] = [0; 1500]; - let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - assert!(srv_msg.contains("IMAP4REV1")); - assert!(srv_msg.contains("UNSELECT")); - - Ok(()) -} - -fn login(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - imap.write(&b"10 login alice hunter2\r\n"[..])?; - - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..5], &b"10 OK"[..]); - - Ok(()) -} - -fn select_inbox_one_msg(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 6000] = [0; 6000]; - - imap.write(&b"22 select inbox\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"22 OK"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - assert!(srv_msg.contains("* 1 EXISTS")); - - Ok(()) -} - -fn select_inbox_zero_msg(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 6000] = [0; 6000]; - - imap.write(&b"21 select inbox\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"21 OK"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - assert!(srv_msg.contains("* 0 EXISTS")); - - Ok(()) -} - -fn select_inbox(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 6000] = [0; 6000]; - - imap.write(&b"20 select inbox\r\n"[..])?; - let _read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?; - - Ok(()) -} - -fn noop_exists(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 6000] = [0; 6000]; - - let mut max_retry = 20; - loop { - max_retry -= 1; - imap.write(&b"30 NOOP\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"30 OK NOOP"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - - match (max_retry, srv_msg.contains("* 1 EXISTS")) { - (_, true) => break, - (0, _) => bail!("no more retry"), - _ => (), - } - - thread::sleep(SMALL_DELAY); - } - - Ok(()) -} - -fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - let _read = read_lines(lmtp, &mut buffer, None)?; - assert_eq!(&buffer[..4], &b"220 "[..]); - - lmtp.write(&b"LHLO example.tld\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 "[..]))?; - - Ok(()) -} - -fn lmtp_deliver_email(lmtp: &mut TcpStream, email: &[u8]) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - lmtp.write(&b"MAIL FROM:\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; - - lmtp.write(&b"RCPT TO:\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.1.5"[..]))?; - - lmtp.write(&b"DATA\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"354 "[..]))?; - - lmtp.write(email)?; - lmtp.write(&b"\r\n.\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; - - Ok(()) -} - -fn fetch_rfc822(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> { - let mut buffer: [u8; 65535] = [0; 65535]; - imap.write(&b"40 fetch 1 rfc822\r\n"[..])?; - - let read = read_lines(imap, &mut buffer, Some(&b"40 OK FETCH"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - let orig_email = std::str::from_utf8(ref_mail)?; - assert!(srv_msg.contains(orig_email)); - - Ok(()) -} - -fn add_flags_email(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - imap.write(&b"50 store 1 +FLAGS (\\Deleted)\r\n"[..])?; - let _read = read_lines(imap, &mut buffer, Some(&b"50 OK STORE"[..]))?; - - Ok(()) -} - -fn close(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"60 close\r\n"[..])?; - let mut buffer: [u8; 1500] = [0; 1500]; - let _read = read_lines(imap, &mut buffer, Some(&b"60 OK"[..]))?; - - Ok(()) -} - -fn unselect(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"70 unselect\r\n"[..])?; - let mut buffer: [u8; 1500] = [0; 1500]; - let _read = read_lines(imap, &mut buffer, Some(&b"70 OK"[..]))?; - - Ok(()) -} diff --git a/tests/rfc6851_imapext_move.rs b/tests/rfc6851_imapext_move.rs index d379f8d..a535bce 100644 --- a/tests/rfc6851_imapext_move.rs +++ b/tests/rfc6851_imapext_move.rs @@ -1,190 +1,30 @@ -use anyhow::{bail, Context, Result}; -use std::io::Write; -use std::net::TcpStream; -use std::{thread, time}; +use anyhow::Context; mod common; -use crate::common::read_lines; - -static SMALL_DELAY: time::Duration = time::Duration::from_millis(200); -static EMAIL: &[u8] = b"From: alice@example.com\r -To: alice@example.tld\r -Subject: Test\r -\r -Hello world!\r -"; - -enum Mailbox { - Inbox, - Archive, -} +use common::fragments::*; fn main() { common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { - lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; - lmtp_deliver_email(lmtp_socket, EMAIL).context("mail delivered successfully")?; - connect(imap_socket).context("server says hello")?; - capability(imap_socket).context("check server capabilities")?; - login(imap_socket).context("login test")?; - create_mailbox(imap_socket).context("created mailbox archive")?; - select(imap_socket, Mailbox::Inbox).context("select inbox")?; - noop_exists(imap_socket).context("noop loop must detect a new email")?; - r#move(imap_socket).context("message from inbox moved to archive")?; + capability(imap_socket, Extension::Move).context("check server capabilities")?; + login(imap_socket, Account::Alice).context("login test")?; + create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?; + select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; + + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; + + noop_exists(imap_socket).context("noop loop must detect a new email")?; + r#move(imap_socket, Selection::FirstId, Mailbox::Archive).context("message from inbox moved to archive")?; unselect(imap_socket) .context("unselect inbox while preserving email with the \\Delete flag")?; - select(imap_socket, Mailbox::Archive).context("select archive")?; - fetch_rfc822(imap_socket, EMAIL).context("check mail exists")?; + select(imap_socket, Mailbox::Archive, Some(1)).context("select archive")?; + fetch_rfc822(imap_socket, Selection::FirstId, Email::Basic).context("check mail exists")?; logout(imap_socket).context("must quit")?; Ok(()) }) .expect("test fully run"); } - -fn connect(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..4], &b"* OK"[..]); - - Ok(()) -} - -fn create_mailbox(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - imap.write(&b"15 create archive\r\n"[..])?; - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..12], &b"15 OK CREATE"[..]); - - Ok(()) -} - -fn capability(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"5 capability\r\n"[..])?; - - let mut buffer: [u8; 1500] = [0; 1500]; - let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - assert!(srv_msg.contains("IMAP4REV1")); - assert!(srv_msg.contains("UNSELECT")); - - Ok(()) -} - -fn login(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - imap.write(&b"10 login alice hunter2\r\n"[..])?; - - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..5], &b"10 OK"[..]); - - Ok(()) -} - -fn select(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> { - let mut buffer: [u8; 6000] = [0; 6000]; - - match mbx { - Mailbox::Inbox => imap.write(&b"20 select inbox\r\n"[..])?, - Mailbox::Archive => imap.write(&b"20 select archive\r\n"[..])?, - }; - let _read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?; - - Ok(()) -} - -fn noop_exists(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 6000] = [0; 6000]; - - let mut max_retry = 20; - loop { - max_retry -= 1; - imap.write(&b"30 NOOP\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"30 OK NOOP"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - - match (max_retry, srv_msg.contains("* 1 EXISTS")) { - (_, true) => break, - (0, _) => bail!("no more retry"), - _ => (), - } - - thread::sleep(SMALL_DELAY); - } - - Ok(()) -} - -fn r#move(imap: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - imap.write(&b"35 move 1 archive\r\n"[..])?; - let read = read_lines(imap, &mut buffer, Some(&b"35 OK"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - assert!(srv_msg.contains("* 1 EXPUNGE")); - - Ok(()) -} - -fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - let _read = read_lines(lmtp, &mut buffer, None)?; - assert_eq!(&buffer[..4], &b"220 "[..]); - - lmtp.write(&b"LHLO example.tld\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 "[..]))?; - - Ok(()) -} - -fn lmtp_deliver_email(lmtp: &mut TcpStream, email: &[u8]) -> Result<()> { - let mut buffer: [u8; 1500] = [0; 1500]; - - lmtp.write(&b"MAIL FROM:\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; - - lmtp.write(&b"RCPT TO:\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.1.5"[..]))?; - - lmtp.write(&b"DATA\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"354 "[..]))?; - - lmtp.write(email)?; - lmtp.write(&b"\r\n.\r\n"[..])?; - let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?; - - Ok(()) -} - -fn fetch_rfc822(imap: &mut TcpStream, ref_mail: &[u8]) -> Result<()> { - let mut buffer: [u8; 65535] = [0; 65535]; - imap.write(&b"40 fetch 1 rfc822\r\n"[..])?; - - let read = read_lines(imap, &mut buffer, Some(&b"40 OK FETCH"[..]))?; - let srv_msg = std::str::from_utf8(read)?; - let orig_email = std::str::from_utf8(ref_mail)?; - assert!(srv_msg.contains(orig_email)); - - Ok(()) -} - -fn logout(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"99 logout\r\n"[..])?; - let mut buffer: [u8; 1500] = [0; 1500]; - let read = read_lines(imap, &mut buffer, None)?; - assert_eq!(&read[..5], &b"* BYE"[..]); - Ok(()) -} - -fn unselect(imap: &mut TcpStream) -> Result<()> { - imap.write(&b"70 unselect\r\n"[..])?; - let mut buffer: [u8; 1500] = [0; 1500]; - let _read = read_lines(imap, &mut buffer, Some(&b"70 OK"[..]))?; - - Ok(()) -} From a93967a6f8f1c881c64879e61edc46d64c4c662e Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 4 Jan 2024 11:51:14 +0100 Subject: [PATCH 13/15] create a single behavior test with all files --- Cargo.toml | 14 +---- tests/behavior.rs | 107 ++++++++++++++++++++++++++++++++++++++ tests/common/fragments.rs | 31 ++++++++++- 3 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 tests/behavior.rs diff --git a/Cargo.toml b/Cargo.toml index 557be55..b110a6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,16 +68,6 @@ imap-types = { git = "https://github.com/duesee/imap-codec", branch = "v2" } imap-codec = { git = "https://github.com/duesee/imap-codec", branch = "v2" } [[test]] -name = "rfc3501_imap4rev1_base" -path = "tests/rfc3501_imap4rev1_base.rs" -harness = false - -[[test]] -name = "rfc3691_imapext_unselect" -path = "tests/rfc3691_imapext_unselect.rs" -harness = false - -[[test]] -name = "rfc6851_imapext_move" -path = "tests/rfc6851_imapext_move.rs" +name = "behavior" +path = "tests/behavior.rs" harness = false diff --git a/tests/behavior.rs b/tests/behavior.rs new file mode 100644 index 0000000..e62832b --- /dev/null +++ b/tests/behavior.rs @@ -0,0 +1,107 @@ +use anyhow::Context; + +mod common; +use crate::common::fragments::*; + +fn main() { + rfc3501_imap4rev1_base(); + rfc3691_imapext_unselect(); + rfc5161_imapext_enable(); + rfc6851_imapext_move(); +} + +fn rfc3501_imap4rev1_base() { + println!("rfc3501_imap4rev1_base"); + common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { + connect(imap_socket).context("server says hello")?; + capability(imap_socket, Extension::None).context("check server capabilities")?; + login(imap_socket, Account::Alice).context("login test")?; + create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?; + // UNSUBSCRIBE IS NOT IMPLEMENTED YET + //unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?; + select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; + check(imap_socket).context("check must run")?; + status_mailbox(imap_socket, Mailbox::Archive).context("status of archive from inbox")?; + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?; + noop_exists(imap_socket).context("noop loop must detect a new email")?; + fetch_rfc822(imap_socket, Selection::FirstId, Email::Multipart).context("fetch rfc822 message, should be our first message")?; + copy(imap_socket, Selection::FirstId, Mailbox::Archive).context("copy message to the archive mailbox")?; + append_email(imap_socket, Email::Basic).context("insert email in INBOX")?; + // SEARCH IS NOT IMPLEMENTED YET + //search(imap_socket).expect("search should return something"); + add_flags_email(imap_socket, Selection::FirstId, Flag::Deleted) + .context("should add delete flag to the email")?; + expunge(imap_socket).context("expunge emails")?; + rename_mailbox(imap_socket, Mailbox::Archive, Mailbox::Drafts).context("Archive mailbox is renamed Drafts")?; + delete_mailbox(imap_socket, Mailbox::Drafts).context("Drafts mailbox is deleted")?; + Ok(()) + }) + .expect("test fully run"); +} + +fn rfc3691_imapext_unselect() { + println!("rfc3691_imapext_unselect"); + common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { + connect(imap_socket).context("server says hello")?; + + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; + + capability(imap_socket, Extension::Unselect).context("check server capabilities")?; + login(imap_socket, Account::Alice).context("login test")?; + select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; + noop_exists(imap_socket).context("noop loop must detect a new email")?; + add_flags_email(imap_socket, Selection::FirstId, Flag::Deleted).context("add delete flags to the email")?; + unselect(imap_socket) + .context("unselect inbox while preserving email with the \\Delete flag")?; + select(imap_socket, Mailbox::Inbox, Some(1)).context("select inbox again")?; + fetch_rfc822(imap_socket, Selection::FirstId, Email::Basic).context("message is still present")?; + close(imap_socket).context("close inbox and expunge message")?; + select(imap_socket, Mailbox::Inbox, Some(0)).context("select inbox again and check it's empty")?; + + Ok(()) + }) + .expect("test fully run"); +} + +fn rfc5161_imapext_enable() { + println!("rfc5161_imapext_enable"); + common::aerogramme_provider_daemon_dev(|imap_socket, _lmtp_socket| { + connect(imap_socket).context("server says hello")?; + login(imap_socket, Account::Alice).context("login test")?; + enable(imap_socket, Enable::Utf8Accept, Some(Enable::Utf8Accept))?; + enable(imap_socket, Enable::Utf8Accept, None)?; + logout(imap_socket)?; + + Ok(()) + }) + .expect("test fully run"); +} + +fn rfc6851_imapext_move() { + println!("rfc6851_imapext_move"); + common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { + connect(imap_socket).context("server says hello")?; + + capability(imap_socket, Extension::Move).context("check server capabilities")?; + login(imap_socket, Account::Alice).context("login test")?; + create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?; + select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; + + lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; + lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; + + noop_exists(imap_socket).context("noop loop must detect a new email")?; + r#move(imap_socket, Selection::FirstId, Mailbox::Archive).context("message from inbox moved to archive")?; + + unselect(imap_socket) + .context("unselect inbox while preserving email with the \\Delete flag")?; + select(imap_socket, Mailbox::Archive, Some(1)).context("select archive")?; + fetch_rfc822(imap_socket, Selection::FirstId, Email::Basic).context("check mail exists")?; + logout(imap_socket).context("must quit")?; + + Ok(()) + }) + .expect("test fully run"); +} diff --git a/tests/common/fragments.rs b/tests/common/fragments.rs index 530f1f7..c8d5ef1 100644 --- a/tests/common/fragments.rs +++ b/tests/common/fragments.rs @@ -11,7 +11,11 @@ use crate::common::*; /// arbitrary values, only enum for which the code is known /// to be correct. The idea is that the generated message is more /// or less hardcoded by the developer, so its clear what's expected, -/// and not generated by a library. +/// and not generated by a library. Also don't use vector of enum, +/// as it again introduce some kind of genericity we try so hard to avoid: +/// instead add a dedicated enum, for example "All" or anything relaevent that would +/// describe your list and then hardcode it in your fragment. +/// DON'T. TRY. TO. BE. GENERIC. HERE. pub fn connect(imap: &mut TcpStream) -> Result<()> { let mut buffer: [u8; 1500] = [0; 1500]; @@ -33,6 +37,12 @@ pub enum Extension { CondStore, } +pub enum Enable { + Utf8Accept, + CondStore, + All, +} + pub enum Mailbox { Inbox, Archive, @@ -357,6 +367,25 @@ pub fn r#move(imap: &mut TcpStream, selection: Selection, to: Mailbox) -> Result Ok(()) } +pub fn enable(imap: &mut TcpStream, ask: Enable, done: Option) -> Result<()> { + let mut buffer: [u8; 6000] = [0; 6000]; + assert!(matches!(ask, Enable::Utf8Accept)); + + imap.write(&b"36 enable UTF8=ACCEPT\r\n"[..])?; + let read = read_lines(imap, &mut buffer, Some(&b"36 OK"[..]))?; + let srv_msg = std::str::from_utf8(read)?; + match done { + None => assert_eq!(srv_msg.lines().count(), 1), + Some(Enable::Utf8Accept) => { + assert_eq!(srv_msg.lines().count(), 2); + assert!(srv_msg.contains("* ENABLED UTF8=ACCEPT")); + }, + _ => unimplemented!(), + } + + Ok(()) +} + pub fn logout(imap: &mut TcpStream) -> Result<()> { imap.write(&b"99 logout\r\n"[..])?; let mut buffer: [u8; 1500] = [0; 1500]; From 3f5d7fa766c268923c0c2c6e1dd50f4693f51a44 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 4 Jan 2024 11:53:49 +0100 Subject: [PATCH 14/15] remove old tests --- tests/rfc3501_imap4rev1_base.rs | 33 ------------------------------- tests/rfc3691_imapext_unselect.rs | 28 -------------------------- tests/rfc6851_imapext_move.rs | 30 ---------------------------- 3 files changed, 91 deletions(-) delete mode 100644 tests/rfc3501_imap4rev1_base.rs delete mode 100644 tests/rfc3691_imapext_unselect.rs delete mode 100644 tests/rfc6851_imapext_move.rs diff --git a/tests/rfc3501_imap4rev1_base.rs b/tests/rfc3501_imap4rev1_base.rs deleted file mode 100644 index 690913a..0000000 --- a/tests/rfc3501_imap4rev1_base.rs +++ /dev/null @@ -1,33 +0,0 @@ -use anyhow::Context; - -mod common; -use crate::common::fragments::*; - -fn main() { - common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { - connect(imap_socket).context("server says hello")?; - capability(imap_socket, Extension::None).context("check server capabilities")?; - login(imap_socket, Account::Alice).context("login test")?; - create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?; - // UNSUBSCRIBE IS NOT IMPLEMENTED YET - //unsubscribe_mailbox(imap_socket).context("unsubscribe from archive")?; - select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; - check(imap_socket).context("check must run")?; - status_mailbox(imap_socket, Mailbox::Archive).context("status of archive from inbox")?; - lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; - lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?; - noop_exists(imap_socket).context("noop loop must detect a new email")?; - fetch_rfc822(imap_socket, Selection::FirstId, Email::Multipart).context("fetch rfc822 message, should be our first message")?; - copy(imap_socket, Selection::FirstId, Mailbox::Archive).context("copy message to the archive mailbox")?; - append_email(imap_socket, Email::Basic).context("insert email in INBOX")?; - // SEARCH IS NOT IMPLEMENTED YET - //search(imap_socket).expect("search should return something"); - add_flags_email(imap_socket, Selection::FirstId, Flag::Deleted) - .context("should add delete flag to the email")?; - expunge(imap_socket).context("expunge emails")?; - rename_mailbox(imap_socket, Mailbox::Archive, Mailbox::Drafts).context("Archive mailbox is renamed Drafts")?; - delete_mailbox(imap_socket, Mailbox::Drafts).context("Drafts mailbox is deleted")?; - Ok(()) - }) - .expect("test fully run"); -} diff --git a/tests/rfc3691_imapext_unselect.rs b/tests/rfc3691_imapext_unselect.rs deleted file mode 100644 index 4c5a45d..0000000 --- a/tests/rfc3691_imapext_unselect.rs +++ /dev/null @@ -1,28 +0,0 @@ -use anyhow::Context; - -mod common; -use crate::common::fragments::*; - -fn main() { - common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { - connect(imap_socket).context("server says hello")?; - - lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; - lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; - - capability(imap_socket, Extension::Unselect).context("check server capabilities")?; - login(imap_socket, Account::Alice).context("login test")?; - select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; - noop_exists(imap_socket).context("noop loop must detect a new email")?; - add_flags_email(imap_socket, Selection::FirstId, Flag::Deleted).context("add delete flags to the email")?; - unselect(imap_socket) - .context("unselect inbox while preserving email with the \\Delete flag")?; - select(imap_socket, Mailbox::Inbox, Some(1)).context("select inbox again")?; - fetch_rfc822(imap_socket, Selection::FirstId, Email::Basic).context("message is still present")?; - close(imap_socket).context("close inbox and expunge message")?; - select(imap_socket, Mailbox::Inbox, Some(0)).context("select inbox again and check it's empty")?; - - Ok(()) - }) - .expect("test fully run"); -} diff --git a/tests/rfc6851_imapext_move.rs b/tests/rfc6851_imapext_move.rs deleted file mode 100644 index a535bce..0000000 --- a/tests/rfc6851_imapext_move.rs +++ /dev/null @@ -1,30 +0,0 @@ -use anyhow::Context; - -mod common; -use common::fragments::*; - -fn main() { - common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| { - connect(imap_socket).context("server says hello")?; - - capability(imap_socket, Extension::Move).context("check server capabilities")?; - login(imap_socket, Account::Alice).context("login test")?; - create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?; - select(imap_socket, Mailbox::Inbox, None).context("select inbox")?; - - lmtp_handshake(lmtp_socket).context("handshake lmtp done")?; - lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?; - - noop_exists(imap_socket).context("noop loop must detect a new email")?; - r#move(imap_socket, Selection::FirstId, Mailbox::Archive).context("message from inbox moved to archive")?; - - unselect(imap_socket) - .context("unselect inbox while preserving email with the \\Delete flag")?; - select(imap_socket, Mailbox::Archive, Some(1)).context("select archive")?; - fetch_rfc822(imap_socket, Selection::FirstId, Email::Basic).context("check mail exists")?; - logout(imap_socket).context("must quit")?; - - Ok(()) - }) - .expect("test fully run"); -} From 7ae9966675c85b34f1a99d81062b44b74385a15b Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 4 Jan 2024 12:09:16 +0100 Subject: [PATCH 15/15] test enable --- tests/behavior.rs | 13 +++++++++++++ tests/common/fragments.rs | 13 ++++++++++++- tests/common/mod.rs | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/behavior.rs b/tests/behavior.rs index e62832b..82fdc53 100644 --- a/tests/behavior.rs +++ b/tests/behavior.rs @@ -8,6 +8,7 @@ fn main() { rfc3691_imapext_unselect(); rfc5161_imapext_enable(); rfc6851_imapext_move(); + rfc7888_imapext_literal(); } fn rfc3501_imap4rev1_base() { @@ -105,3 +106,15 @@ fn rfc6851_imapext_move() { }) .expect("test fully run"); } + +fn rfc7888_imapext_literal() { + println!("rfc7888_imapext_literal"); + common::aerogramme_provider_daemon_dev(|imap_socket, _lmtp_socket| { + connect(imap_socket).context("server says hello")?; + + capability(imap_socket, Extension::LiteralPlus).context("check server capabilities")?; + login_with_literal(imap_socket, Account::Alice).context("use literal to connect Alice")?; + + Ok(()) + }).expect("test fully run"); +} diff --git a/tests/common/fragments.rs b/tests/common/fragments.rs index c8d5ef1..3ed14cc 100644 --- a/tests/common/fragments.rs +++ b/tests/common/fragments.rs @@ -35,6 +35,7 @@ pub enum Extension { Unselect, Move, CondStore, + LiteralPlus, } pub enum Enable { @@ -72,9 +73,10 @@ pub fn capability(imap: &mut TcpStream, ext: Extension) -> Result<()> { Extension::Unselect => Some("UNSELECT"), Extension::Move => Some("MOVE"), Extension::CondStore => Some("CONDSTORE"), + Extension::LiteralPlus => Some("LITERAL+"), }; - let mut buffer: [u8; 1500] = [0; 1500]; + let mut buffer: [u8; 6000] = [0; 6000]; let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?; let srv_msg = std::str::from_utf8(read)?; assert!(srv_msg.contains("IMAP4REV1")); @@ -97,6 +99,15 @@ pub fn login(imap: &mut TcpStream, account: Account) -> Result<()> { Ok(()) } +pub fn login_with_literal(imap: &mut TcpStream, account: Account) -> Result<()> { + let mut buffer: [u8; 1500] = [0; 1500]; + + assert!(matches!(account, Account::Alice)); + imap.write(&b"10 login {5+}\r\nalice {7+}\r\nhunter2\r\n"[..])?; + let _read = read_lines(imap, &mut buffer, Some(&b"10 OK"[..]))?; + Ok(()) +} + pub fn create_mailbox(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> { let mut buffer: [u8; 1500] = [0; 1500]; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d0f4ed8..810fd79 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -81,7 +81,7 @@ pub fn read_lines<'a, F: Read>( None => true, Some(mark) => buffer[..nbytes].windows(mark.len()).any(|w| w == mark), }; - if pre_condition && &buffer[nbytes - 2..nbytes] == &b"\r\n"[..] { + if pre_condition && nbytes >= 2 && &buffer[nbytes - 2..nbytes] == &b"\r\n"[..] { break; } }