From be4d91fcb3b906d32f7fdccf5c06e6c4970b8047 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sun, 14 Jun 2020 01:02:09 +0200 Subject: [PATCH] Download & decrypt uploaded attachements. - Parse the "Chat-Upload-Url" header - Add message::schedule_download function to schedule download job - In the job download, decrypt, save --- examples/repl/cmdline.rs | 11 ++++++ examples/repl/main.rs | 3 +- src/headerdef.rs | 1 + src/job.rs | 18 ++++++++-- src/message.rs | 27 +++++++++++++++ src/mimeparser.rs | 7 ++++ src/pgp.rs | 16 +++++++-- src/upload.rs | 73 ++++++++++++++++++++++++++++++++++------ 8 files changed, 141 insertions(+), 15 deletions(-) diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index d1dde5fc9..3b134fb02 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -371,6 +371,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu listmsgs \n\ msginfo \n\ openfile \n\ + download \n\ listfresh\n\ forward \n\ markseen \n\ @@ -900,6 +901,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu let filepath = filepath.unwrap(); open::that(filepath)?; } + "download" => { + ensure!(!arg1.is_empty(), "Argument missing."); + let id = MsgId::new(arg1.parse()?); + let path = if !arg2.is_empty() { + Some(arg2.into()) + } else { + None + }; + message::schedule_download(&context, id, path).await?; + } "listfresh" => { let msglist = context.get_fresh_msgs().await; diff --git a/examples/repl/main.rs b/examples/repl/main.rs index a14157286..88a889911 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -186,10 +186,11 @@ const CHAT_COMMANDS: [&str; 26] = [ "unpin", "delchat", ]; -const MESSAGE_COMMANDS: [&str; 9] = [ +const MESSAGE_COMMANDS: [&str; 10] = [ "listmsgs", "msginfo", "openfile", + "download", "listfresh", "forward", "markseen", diff --git a/src/headerdef.rs b/src/headerdef.rs index 10beaf4c8..db9d081e3 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -34,6 +34,7 @@ pub enum HeaderDef { ChatContent, ChatDuration, ChatDispositionNotificationTo, + ChatUploadUrl, Autocrypt, AutocryptSetupMessage, SecureJoin, diff --git a/src/job.rs b/src/job.rs index a8e635c2b..cd2cbe2f3 100644 --- a/src/job.rs +++ b/src/job.rs @@ -32,7 +32,7 @@ use crate::message::{self, Message, MessageState}; use crate::mimefactory::MimeFactory; use crate::param::*; use crate::smtp::Smtp; -use crate::upload::{generate_upload_url, upload_file}; +use crate::upload::{download_message_file, generate_upload_url, upload_file}; use crate::{scheduler::InterruptInfo, sql}; // results in ~3 weeks for the last backoff timespan @@ -109,6 +109,8 @@ pub enum Action { MaybeSendLocationsEnded = 5007, SendMdn = 5010, SendMsgToSmtp = 5901, // ... high priority + + DownloadMessageFile = 7000, } impl Default for Action { @@ -135,6 +137,9 @@ impl From for Thread { MaybeSendLocationsEnded => Thread::Smtp, SendMdn => Thread::Smtp, SendMsgToSmtp => Thread::Smtp, + + // TODO: Where does downloading fit in the thread architecture? + DownloadMessageFile => Thread::Imap, } } } @@ -668,6 +673,13 @@ impl Job { } } } + + pub(crate) async fn download_message_file(&mut self, context: &Context) -> Status { + let msg_id = MsgId::new(self.foreign_id); + let download_path = job_try!(self.param.get_upload_path(context)); + job_try!(download_message_file(context, msg_id, download_path).await); + Status::Finished(Ok(())) + } } /// Delete all pending jobs with the given action. @@ -1009,6 +1021,7 @@ async fn perform_job_action( sql::housekeeping(context).await; Status::Finished(Ok(())) } + Action::DownloadMessageFile => job.download_message_file(context).await, }; info!( @@ -1065,7 +1078,8 @@ pub async fn add(context: &Context, job: Job) { | Action::OldDeleteMsgOnImap | Action::DeleteMsgOnImap | Action::MarkseenMsgOnImap - | Action::MoveMsg => { + | Action::MoveMsg + | Action::DownloadMessageFile => { info!(context, "interrupt: imap"); context .interrupt_inbox(InterruptInfo::new(false, None)) diff --git a/src/message.rs b/src/message.rs index 1c77b4017..886151aa8 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1545,6 +1545,33 @@ pub async fn update_server_uid( } } +/// Schedule attachement download for a message. +pub async fn schedule_download( + context: &Context, + msg_id: MsgId, + path: Option, +) -> Result<(), Error> { + let msg = Message::load_from_db(context, msg_id).await?; + if let Some(_upload_url) = msg.param.get_upload_url() { + // TODO: Check if message was already downloaded. + let mut params = Params::new(); + if let Some(path) = path { + params.set_upload_path(path); + } + job::add( + context, + job::Job::new(Action::DownloadMessageFile, msg_id.to_u32(), params, 0), + ) + .await; + } else { + warn!( + context, + "Tried to schedule download for message {} which has no uploads", msg_id + ); + } + Ok(()) +} + #[allow(dead_code)] pub async fn dc_empty_server(context: &Context, flags: u32) { job::kill_action(context, Action::EmptyServer).await; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 17883e137..dc7eef587 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -334,6 +334,13 @@ impl MimeMessage { } } + let upload_url = self.get(HeaderDef::ChatUploadUrl).map(|v| v.to_string()); + if let Some(upload_url) = upload_url { + for part in self.parts.iter_mut() { + part.param.set_upload_url(upload_url.clone()); + } + } + self.parse_attachments(); // See if an MDN is requested from the other side diff --git a/src/pgp.rs b/src/pgp.rs index 486736c89..bc61514ef 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -352,16 +352,28 @@ async fn symm_encrypt_to_message(passphrase: &str, plain: &[u8]) -> Result( passphrase: &str, ctext: T, ) -> Result> { let (enc_msg, _) = Message::from_armor_single(ctext)?; + symm_decrypt_from_message(enc_msg, passphrase).await +} +/// Symmetric decryption from bytes. +pub async fn symm_decrypt_bytes( + passphrase: &str, + cbytes: T, +) -> Result> { + let enc_msg = Message::from_bytes(cbytes)?; + symm_decrypt_from_message(enc_msg, passphrase).await +} + +async fn symm_decrypt_from_message(message: Message, passphrase: &str) -> Result> { let passphrase = passphrase.to_string(); async_std::task::spawn_blocking(move || { - let decryptor = enc_msg.decrypt_with_password(|| passphrase)?; + let decryptor = message.decrypt_with_password(|| passphrase)?; let msgs = decryptor.collect::>>()?; ensure!(!msgs.is_empty(), "No valid messages found"); diff --git a/src/upload.rs b/src/upload.rs index 18f58bf55..8e4f1f252 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -1,6 +1,9 @@ +use crate::blob::BlobObject; +// use crate::constants::Viewtype; use crate::context::Context; -use crate::error::{bail, Result}; -use crate::pgp::{symm_decrypt, symm_encrypt_bytes}; +use crate::error::{bail, format_err, Result}; +use crate::message::{Message, MsgId}; +use crate::pgp::{symm_decrypt_bytes, symm_encrypt_bytes}; use async_std::fs; use async_std::path::PathBuf; use rand::Rng; @@ -31,18 +34,68 @@ pub async fn upload_file( } } +pub async fn download_message_file( + context: &Context, + msg_id: MsgId, + download_path: Option, +) -> Result<()> { + let mut message = Message::load_from_db(context, msg_id).await?; + let upload_url = message + .param + .get_upload_url() + .ok_or_else(|| format_err!("Message has no upload URL"))?; + + let (passphrase, url) = parse_upload_url(upload_url)?; + + let filename: String = url + .path_segments() + .ok_or_else(|| format_err!("Invalid upload URL"))? + .last() + .ok_or_else(|| format_err!("Invalid upload URL"))? + .to_string(); + + let data = download_file(context, url, passphrase).await?; + let saved_path = if let Some(download_path) = download_path { + fs::write(&download_path, data).await?; + download_path.to_string_lossy().to_string() + } else { + let blob = BlobObject::create(context, filename.clone(), &data) + .await + .map_err(|err| { + format_err!( + "Could not add blob for file download {}, error {}", + filename, + err + ) + })?; + blob.as_name().to_string() + }; + info!(context, "saved download to: {:?}", saved_path); + + // TODO: Support getting the mime type. + let filemime = None; + + message.set_file(saved_path, filemime); + message.save_param_to_disk(context).await; + + Ok(()) +} + /// Download and decrypt a file from a HTTP endpoint. -/// TODO: Use this. -#[allow(dead_code)] -pub async fn download_file(context: &Context, url: String) -> Result> { - let (passphrase, url) = parse_upload_url(url)?; - info!(context, "downloading file from {}", &url); +pub async fn download_file( + context: &Context, + url: impl AsRef, + passphrase: String, +) -> Result> { + info!(context, "downloading file from {}", &url.as_ref()); let response = surf::get(url).recv_bytes().await; if let Err(err) = response { bail!("Download failed: {}", err); } - let reader = Cursor::new(response.unwrap()); - let decrypted = symm_decrypt(&passphrase, reader).await?; + let bytes = response.unwrap(); + info!(context, "download complete, len: {}", bytes.len()); + let reader = Cursor::new(bytes); + let decrypted = symm_decrypt_bytes(&passphrase, reader).await?; Ok(decrypted) } @@ -65,7 +118,7 @@ pub fn generate_upload_url(_context: &Context, mut endpoint: String) -> String { // equals at least 32 random bytes. const PASSPHRASE_LEN: usize = 52; - if endpoint.chars().last() == Some('/') { + if endpoint.ends_with('/') { endpoint.pop(); } let passphrase = generate_token_string(PASSPHRASE_LEN);