diff --git a/Cargo.lock b/Cargo.lock index 4313294..4e94149 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,6 +577,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1273,6 +1282,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1382,7 +1397,10 @@ checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ "anstyle", "difflib", + "float-cmp", + "normalize-line-endings", "predicates-core", + "regex", ] [[package]] @@ -1927,6 +1945,7 @@ dependencies = [ "insta", "itertools", "lz4_flex", + "predicates", "rand", "regex", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 6108bb8..c6c5eab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ uuid = { version = "1.11.0", features = ["v4"] } assert_cmd = "2.0.16" hex-literal = "0.4.1" insta = "1.41.1" +predicates = "3.1.2" similar-asserts = "1.6.0" tempfile = "3" diff --git a/src/mails.rs b/src/mails.rs index 0ffcbf0..2d9ea9f 100644 --- a/src/mails.rs +++ b/src/mails.rs @@ -57,6 +57,7 @@ impl Mail { client: &Client, session: &Session, folder: &Folder, + ignore_new_mails: bool, ) -> impl Stream>> { let group_keys = Arc::clone(&session.group_keys); let folder_id = folder.id.clone(); @@ -69,18 +70,36 @@ impl Mail { let group_keys = Arc::clone(&group_keys); let folder_id = folder_id.clone(); async move { - let mail = Self::decode(m, &group_keys, folder_id)?; - Ok(Arc::new(mail)) + Self::decode(m, &group_keys, folder_id) + } + }) + .try_filter_map(move |mail| async move { + match mail { + Some(mail) => Ok(Some(Arc::new(mail))), + None if ignore_new_mails => Ok(None), + None => bail!("Folder contains new mail that has not been decoded before. Use the official app and view the folder to decode the data, or pass --ignore-new-mails to skip new emails."), } }) } - fn decode(resp: MailReponse, group_keys: &GroupKeys, folder_id: String) -> Result { + /// Decode [`MailReponse`]. + /// + /// Returns [`None`] if no encryption key is set. This usually happens when the mail was NOT + /// processed via the official app yet. + fn decode( + resp: MailReponse, + group_keys: &GroupKeys, + folder_id: String, + ) -> Result> { + let Some(key) = resp.owner_enc_session_key else { + return Ok(None); + }; + let session_key = decrypt_key( group_keys .get(&resp.owner_group) .context("getting owner group key")?, - resp.owner_enc_session_key, + key, ) .context("decrypting session key")?; @@ -100,7 +119,7 @@ impl Mail { } }; - Ok(Self { + Ok(Some(Self { folder_id, mail_id: resp.id[1].clone(), archive_id, @@ -111,7 +130,7 @@ impl Mail { subject, sender, attachments: resp.attachments, - }) + })) } pub(crate) fn ui_url(&self) -> String { diff --git a/src/main.rs b/src/main.rs index d4510b1..60bfc92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,8 @@ use tracing::{debug, info}; #[cfg(test)] use assert_cmd as _; #[cfg(test)] +use predicates as _; +#[cfg(test)] use similar_asserts as _; #[cfg(test)] use tempfile as _; @@ -79,6 +81,13 @@ struct DownloadCLIConfig { /// Target path. #[clap(long, action)] path: PathBuf, + + /// Ignore new mails that cannot be decrypted (yet). + /// + /// Use the official app to view and respective folder. This will convert the mail data to a + /// format that we can read. + #[clap(long, action)] + ignore_new_mails: bool, } /// Command @@ -149,7 +158,7 @@ async fn exec_cmd(client: &Client, session: &Session, cmd: Command) -> Result<() .context("folder not found")?; debug!(mails = folder.mails.as_str(), "download mails from folder"); - Mail::list(client, session, &folder) + Mail::list(client, session, &folder, cfg.ignore_new_mails) .map(|mail| { let cfg = &cfg; diff --git a/src/proto/messages.rs b/src/proto/messages.rs index 964eb48..88242a2 100644 --- a/src/proto/messages.rs +++ b/src/proto/messages.rs @@ -156,7 +156,7 @@ pub(crate) struct MailReponse { pub(crate) _format: Format<0>, #[serde(rename = "_ownerEncSessionKey")] - pub(crate) owner_enc_session_key: EncryptedKey, + pub(crate) owner_enc_session_key: Option, #[serde(rename = "_ownerGroup")] pub(crate) owner_group: String, diff --git a/tests/cli.rs b/tests/cli.rs index fb360de..3b98a61 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -115,4 +115,34 @@ mod integration { similar_asserts::assert_eq!(actual_content, expected_content); } } + + #[test] + fn test_new_mail_without_flag() { + let path = TempDir::new().unwrap(); + + let mut cmd = cmd(); + cmd.arg("-vv") + .arg("download") + .arg("--folder=Inbox") + .arg("--path") + .arg(path.path()) + .assert() + .failure() + .stderr(predicates::str::contains("--ignore-new-mails")); + } + + #[test] + fn test_new_mail_with_flag() { + let path = TempDir::new().unwrap(); + + let mut cmd = cmd(); + cmd.arg("-vv") + .arg("download") + .arg("--folder=Inbox") + .arg("--path") + .arg(path.path()) + .arg("--ignore-new-mails") + .assert() + .success(); + } }