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
This commit is contained in:
Franz Heinzmann (Frando)
2020-06-14 01:02:09 +02:00
parent 1350495574
commit be4d91fcb3
8 changed files with 141 additions and 15 deletions

View File

@@ -371,6 +371,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
listmsgs <query>\n\
msginfo <msg-id>\n\
openfile <msg-id>\n\
download <msg-id>\n\
listfresh\n\
forward <msg-id> <chat-id>\n\
markseen <msg-id>\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 <msg-id> 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;

View File

@@ -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",

View File

@@ -34,6 +34,7 @@ pub enum HeaderDef {
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
ChatUploadUrl,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,

View File

@@ -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<Action> 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))

View File

@@ -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<PathBuf>,
) -> 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;

View File

@@ -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

View File

@@ -352,16 +352,28 @@ async fn symm_encrypt_to_message(passphrase: &str, plain: &[u8]) -> Result<Messa
.await
}
/// Symmetric decryption.
/// Symmetric decryption from armored text.
pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
passphrase: &str,
ctext: T,
) -> Result<Vec<u8>> {
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<T: std::io::Read + std::io::Seek>(
passphrase: &str,
cbytes: T,
) -> Result<Vec<u8>> {
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<Vec<u8>> {
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::<pgp::errors::Result<Vec<_>>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");

View File

@@ -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<PathBuf>,
) -> 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<Vec<u8>> {
let (passphrase, url) = parse_upload_url(url)?;
info!(context, "downloading file from {}", &url);
pub async fn download_file(
context: &Context,
url: impl AsRef<str>,
passphrase: String,
) -> Result<Vec<u8>> {
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);