mirror of
https://github.com/chatmail/core.git
synced 2026-05-12 11:26:29 +03:00
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:
@@ -371,6 +371,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
listmsgs <query>\n\
|
listmsgs <query>\n\
|
||||||
msginfo <msg-id>\n\
|
msginfo <msg-id>\n\
|
||||||
openfile <msg-id>\n\
|
openfile <msg-id>\n\
|
||||||
|
download <msg-id>\n\
|
||||||
listfresh\n\
|
listfresh\n\
|
||||||
forward <msg-id> <chat-id>\n\
|
forward <msg-id> <chat-id>\n\
|
||||||
markseen <msg-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();
|
let filepath = filepath.unwrap();
|
||||||
open::that(filepath)?;
|
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" => {
|
"listfresh" => {
|
||||||
let msglist = context.get_fresh_msgs().await;
|
let msglist = context.get_fresh_msgs().await;
|
||||||
|
|
||||||
|
|||||||
@@ -186,10 +186,11 @@ const CHAT_COMMANDS: [&str; 26] = [
|
|||||||
"unpin",
|
"unpin",
|
||||||
"delchat",
|
"delchat",
|
||||||
];
|
];
|
||||||
const MESSAGE_COMMANDS: [&str; 9] = [
|
const MESSAGE_COMMANDS: [&str; 10] = [
|
||||||
"listmsgs",
|
"listmsgs",
|
||||||
"msginfo",
|
"msginfo",
|
||||||
"openfile",
|
"openfile",
|
||||||
|
"download",
|
||||||
"listfresh",
|
"listfresh",
|
||||||
"forward",
|
"forward",
|
||||||
"markseen",
|
"markseen",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pub enum HeaderDef {
|
|||||||
ChatContent,
|
ChatContent,
|
||||||
ChatDuration,
|
ChatDuration,
|
||||||
ChatDispositionNotificationTo,
|
ChatDispositionNotificationTo,
|
||||||
|
ChatUploadUrl,
|
||||||
Autocrypt,
|
Autocrypt,
|
||||||
AutocryptSetupMessage,
|
AutocryptSetupMessage,
|
||||||
SecureJoin,
|
SecureJoin,
|
||||||
|
|||||||
18
src/job.rs
18
src/job.rs
@@ -32,7 +32,7 @@ use crate::message::{self, Message, MessageState};
|
|||||||
use crate::mimefactory::MimeFactory;
|
use crate::mimefactory::MimeFactory;
|
||||||
use crate::param::*;
|
use crate::param::*;
|
||||||
use crate::smtp::Smtp;
|
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};
|
use crate::{scheduler::InterruptInfo, sql};
|
||||||
|
|
||||||
// results in ~3 weeks for the last backoff timespan
|
// results in ~3 weeks for the last backoff timespan
|
||||||
@@ -109,6 +109,8 @@ pub enum Action {
|
|||||||
MaybeSendLocationsEnded = 5007,
|
MaybeSendLocationsEnded = 5007,
|
||||||
SendMdn = 5010,
|
SendMdn = 5010,
|
||||||
SendMsgToSmtp = 5901, // ... high priority
|
SendMsgToSmtp = 5901, // ... high priority
|
||||||
|
|
||||||
|
DownloadMessageFile = 7000,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Action {
|
impl Default for Action {
|
||||||
@@ -135,6 +137,9 @@ impl From<Action> for Thread {
|
|||||||
MaybeSendLocationsEnded => Thread::Smtp,
|
MaybeSendLocationsEnded => Thread::Smtp,
|
||||||
SendMdn => Thread::Smtp,
|
SendMdn => Thread::Smtp,
|
||||||
SendMsgToSmtp => 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.
|
/// Delete all pending jobs with the given action.
|
||||||
@@ -1009,6 +1021,7 @@ async fn perform_job_action(
|
|||||||
sql::housekeeping(context).await;
|
sql::housekeeping(context).await;
|
||||||
Status::Finished(Ok(()))
|
Status::Finished(Ok(()))
|
||||||
}
|
}
|
||||||
|
Action::DownloadMessageFile => job.download_message_file(context).await,
|
||||||
};
|
};
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
@@ -1065,7 +1078,8 @@ pub async fn add(context: &Context, job: Job) {
|
|||||||
| Action::OldDeleteMsgOnImap
|
| Action::OldDeleteMsgOnImap
|
||||||
| Action::DeleteMsgOnImap
|
| Action::DeleteMsgOnImap
|
||||||
| Action::MarkseenMsgOnImap
|
| Action::MarkseenMsgOnImap
|
||||||
| Action::MoveMsg => {
|
| Action::MoveMsg
|
||||||
|
| Action::DownloadMessageFile => {
|
||||||
info!(context, "interrupt: imap");
|
info!(context, "interrupt: imap");
|
||||||
context
|
context
|
||||||
.interrupt_inbox(InterruptInfo::new(false, None))
|
.interrupt_inbox(InterruptInfo::new(false, None))
|
||||||
|
|||||||
@@ -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)]
|
#[allow(dead_code)]
|
||||||
pub async fn dc_empty_server(context: &Context, flags: u32) {
|
pub async fn dc_empty_server(context: &Context, flags: u32) {
|
||||||
job::kill_action(context, Action::EmptyServer).await;
|
job::kill_action(context, Action::EmptyServer).await;
|
||||||
|
|||||||
@@ -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();
|
self.parse_attachments();
|
||||||
|
|
||||||
// See if an MDN is requested from the other side
|
// See if an MDN is requested from the other side
|
||||||
|
|||||||
16
src/pgp.rs
16
src/pgp.rs
@@ -352,16 +352,28 @@ async fn symm_encrypt_to_message(passphrase: &str, plain: &[u8]) -> Result<Messa
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Symmetric decryption.
|
/// Symmetric decryption from armored text.
|
||||||
pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
|
pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
|
||||||
passphrase: &str,
|
passphrase: &str,
|
||||||
ctext: T,
|
ctext: T,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
let (enc_msg, _) = Message::from_armor_single(ctext)?;
|
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();
|
let passphrase = passphrase.to_string();
|
||||||
async_std::task::spawn_blocking(move || {
|
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<_>>>()?;
|
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
|
||||||
ensure!(!msgs.is_empty(), "No valid messages found");
|
ensure!(!msgs.is_empty(), "No valid messages found");
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
use crate::blob::BlobObject;
|
||||||
|
// use crate::constants::Viewtype;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error::{bail, Result};
|
use crate::error::{bail, format_err, Result};
|
||||||
use crate::pgp::{symm_decrypt, symm_encrypt_bytes};
|
use crate::message::{Message, MsgId};
|
||||||
|
use crate::pgp::{symm_decrypt_bytes, symm_encrypt_bytes};
|
||||||
use async_std::fs;
|
use async_std::fs;
|
||||||
use async_std::path::PathBuf;
|
use async_std::path::PathBuf;
|
||||||
use rand::Rng;
|
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.
|
/// Download and decrypt a file from a HTTP endpoint.
|
||||||
/// TODO: Use this.
|
pub async fn download_file(
|
||||||
#[allow(dead_code)]
|
context: &Context,
|
||||||
pub async fn download_file(context: &Context, url: String) -> Result<Vec<u8>> {
|
url: impl AsRef<str>,
|
||||||
let (passphrase, url) = parse_upload_url(url)?;
|
passphrase: String,
|
||||||
info!(context, "downloading file from {}", &url);
|
) -> Result<Vec<u8>> {
|
||||||
|
info!(context, "downloading file from {}", &url.as_ref());
|
||||||
let response = surf::get(url).recv_bytes().await;
|
let response = surf::get(url).recv_bytes().await;
|
||||||
if let Err(err) = response {
|
if let Err(err) = response {
|
||||||
bail!("Download failed: {}", err);
|
bail!("Download failed: {}", err);
|
||||||
}
|
}
|
||||||
let reader = Cursor::new(response.unwrap());
|
let bytes = response.unwrap();
|
||||||
let decrypted = symm_decrypt(&passphrase, reader).await?;
|
info!(context, "download complete, len: {}", bytes.len());
|
||||||
|
let reader = Cursor::new(bytes);
|
||||||
|
let decrypted = symm_decrypt_bytes(&passphrase, reader).await?;
|
||||||
Ok(decrypted)
|
Ok(decrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +118,7 @@ pub fn generate_upload_url(_context: &Context, mut endpoint: String) -> String {
|
|||||||
// equals at least 32 random bytes.
|
// equals at least 32 random bytes.
|
||||||
const PASSPHRASE_LEN: usize = 52;
|
const PASSPHRASE_LEN: usize = 52;
|
||||||
|
|
||||||
if endpoint.chars().last() == Some('/') {
|
if endpoint.ends_with('/') {
|
||||||
endpoint.pop();
|
endpoint.pop();
|
||||||
}
|
}
|
||||||
let passphrase = generate_token_string(PASSPHRASE_LEN);
|
let passphrase = generate_token_string(PASSPHRASE_LEN);
|
||||||
|
|||||||
Reference in New Issue
Block a user