diff --git a/src/dc_strencode.rs b/src/dc_strencode.rs index dcf589a84..23fe37e68 100644 --- a/src/dc_strencode.rs +++ b/src/dc_strencode.rs @@ -1,8 +1,4 @@ use itertools::Itertools; -use std::borrow::Cow; - -use charset::Charset; -use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, CONTROLS}; /// Encode non-ascii-strings as `=?UTF-8?Q?Bj=c3=b6rn_Petersen?=`. /// Belongs to RFC 2047: https://tools.ietf.org/html/rfc2047 diff --git a/src/job.rs b/src/job.rs index 87bea8054..63b8e1063 100644 --- a/src/job.rs +++ b/src/job.rs @@ -18,7 +18,7 @@ use crate::location; use crate::login_param::LoginParam; use crate::message::MsgId; use crate::message::{self, Message, MessageState}; -use crate::mimefactory::{vec_contains_lowercase, Loaded, MimeFactory}; +use crate::mimefactory::{vec_contains_lowercase, MimeFactory, RenderedEmail}; use crate::param::*; use crate::sql; @@ -604,22 +604,21 @@ fn set_delivered(context: &Context, msg_id: MsgId) { /* special case for DC_JOB_SEND_MSG_TO_SMTP */ #[allow(non_snake_case)] pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> { - let mut mimefactory = MimeFactory::load_msg(context, msg_id)?; - mimefactory.msg.try_calc_and_set_dimensions(context).ok(); + let mut msg = Message::load_from_db(context, msg_id)?; + msg.try_calc_and_set_dimensions(context).ok(); + + let mut mimefactory = MimeFactory::from_msg(context, &msg)?; /* create message */ - if let Err(msg) = mimefactory.render() { - let e = msg.to_string(); - message::set_msg_failed(context, msg_id, Some(e)); - return Err(msg); - } - if 0 != mimefactory - .msg - .param - .get_int(Param::GuaranteeE2ee) - .unwrap_or_default() - && !mimefactory.out_encrypted - { + let needs_encryption = msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default(); + + let mimefactory = MimeFactory::from_msg(context, &msg)?; + let mut rendered_msg = mimefactory.render().map_err(|err| { + message::set_msg_failed(context, msg_id, Some(err.to_string())); + err + })?; + + if 0 != needs_encryption && !rendered_msg.is_encrypted { /* unrecoverable */ message::set_msg_failed( context, @@ -629,19 +628,17 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> { bail!( "e2e encryption unavailable {} - {:?}", msg_id, - mimefactory.msg.param.get_int(Param::GuaranteeE2ee), + needs_encryption ); } + if context.get_config_bool(Config::BccSelf) - && !vec_contains_lowercase(&mimefactory.recipients_addr, &mimefactory.from_addr) + && !vec_contains_lowercase(&rendered_msg.recipients, &rendered_msg.from) { - mimefactory.recipients_names.push("".to_string()); - mimefactory - .recipients_addr - .push(mimefactory.from_addr.to_string()); + rendered_msg.recipients.push(rendered_msg.from.clone()); } - if mimefactory.recipients_addr.is_empty() { + if rendered_msg.recipients.is_empty() { // may happen eg. for groups with only SELF and bcc_self disabled info!( context, @@ -651,36 +648,27 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> { return Ok(()); } - if mimefactory.out_gossiped { - chat::set_gossiped_timestamp(context, mimefactory.msg.chat_id, time()); + if rendered_msg.is_gossiped { + chat::set_gossiped_timestamp(context, msg.chat_id, time()); } - if 0 != mimefactory.out_last_added_location_id { - if let Err(err) = location::set_kml_sent_timestamp(context, mimefactory.msg.chat_id, time()) - { + if 0 != rendered_msg.last_added_location_id { + if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()) { error!(context, "Failed to set kml sent_timestamp: {:?}", err); } - if !mimefactory.msg.hidden { - if let Err(err) = location::set_msg_location_id( - context, - mimefactory.msg.id, - mimefactory.out_last_added_location_id, - ) { + if !msg.hidden { + if let Err(err) = + location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id) + { error!(context, "Failed to set msg_location_id: {:?}", err); } } } - if mimefactory.out_encrypted - && mimefactory - .msg - .param - .get_int(Param::GuaranteeE2ee) - .unwrap_or_default() - == 0 - { - mimefactory.msg.param.set_int(Param::GuaranteeE2ee, 1); - mimefactory.msg.save_param_to_disk(context); + if rendered_msg.is_encrypted && needs_encryption == 0 { + msg.param.set_int(Param::GuaranteeE2ee, 1); + msg.save_param_to_disk(context); } - add_smtp_job(context, Action::SendMsgToSmtp, &mut mimefactory)?; + + add_smtp_job(context, Action::SendMsgToSmtp, &rendered_msg)?; Ok(()) } @@ -866,33 +854,40 @@ fn suspend_smtp_thread(context: &Context, suspend: bool) { } fn send_mdn(context: &Context, msg_id: MsgId) -> Result<(), Error> { - let mut mimefactory = MimeFactory::load_mdn(context, msg_id)?; - mimefactory.render()?; - add_smtp_job(context, Action::SendMdn, &mut mimefactory)?; + let msg = Message::load_from_db(context, msg_id)?; + let mimefactory = MimeFactory::from_mdn(context, &msg)?; + let rendered_msg = mimefactory.render()?; + + add_smtp_job(context, Action::SendMdn, &rendered_msg)?; Ok(()) } #[allow(non_snake_case)] -fn add_smtp_job(context: &Context, action: Action, mimefactory: &MimeFactory) -> Result<(), Error> { +fn add_smtp_job( + context: &Context, + action: Action, + rendered_msg: &RenderedEmail, +) -> Result<(), Error> { ensure!( - !mimefactory.recipients_addr.is_empty(), + !rendered_msg.recipients.is_empty(), "no recipients for smtp job set" ); let mut param = Params::new(); - let bytes = &mimefactory.out; - let blob = BlobObject::create(context, &mimefactory.rfc724_mid, bytes)?; - let recipients = mimefactory.recipients_addr.join("\x1e"); + let bytes = &rendered_msg.message; + let blob = BlobObject::create(context, &rendered_msg.rfc724_mid, bytes)?; + + let recipients = rendered_msg.recipients.join("\x1e"); param.set(Param::File, blob.as_name()); param.set(Param::Recipients, &recipients); + job_add( context, action, - (if mimefactory.loaded == Loaded::Message { - mimefactory.msg.id.to_u32() as i32 - } else { - 0 - }) as libc::c_int, + rendered_msg + .foreign_id + .map(|v| v.to_u32() as i32) + .unwrap_or_default(), param, 0, ); diff --git a/src/mimefactory.rs b/src/mimefactory.rs index fb88292ee..77b2412de 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1,5 +1,4 @@ use chrono::TimeZone; -use lettre::Envelope; use lettre_email::{Address, Header, MimeMessage, MimeMultipartType, PartBuilder}; use crate::chat::{self, Chat}; @@ -21,38 +20,53 @@ use crate::stock::StockMessage; #[derive(Clone, Copy, Eq, PartialEq)] pub enum Loaded { - Nothing, Message, - MDN, // TODO: invent more descriptive name + MDN, } +/// Helper to construct mime messages. #[derive(Clone)] -pub struct MimeFactory<'a> { +pub struct MimeFactory<'a, 'b> { pub from_addr: String, pub from_displayname: String, pub selfstatus: String, pub recipients_names: Vec, pub recipients_addr: Vec, pub timestamp: i64, - pub rfc724_mid: String, pub loaded: Loaded, - pub msg: Message, + pub msg: &'b Message, pub chat: Option, pub increation: bool, pub in_reply_to: String, pub references: String, pub req_mdn: bool, - pub out: Vec, - pub envelope: Option, - pub out_encrypted: bool, - pub out_gossiped: bool, - pub out_last_added_location_id: u32, pub context: &'a Context, + last_added_location_id: u32, } -impl<'a> MimeFactory<'a> { - fn from_message(context: &'a Context, msg: Message) -> Self { - MimeFactory { +/// Result of rendering a message, ready to be submitted to a send job. +#[derive(Debug, Clone)] +pub struct RenderedEmail { + pub message: Vec, + // pub envelope: Envelope, + pub is_encrypted: bool, + pub is_gossiped: bool, + pub last_added_location_id: u32, + /// None for MDN, the message id otherwise + pub foreign_id: Option, + + pub from: String, + pub recipients: Vec, + + /// Message ID (Message in the sense of Email) + pub rfc724_mid: String, +} + +impl<'a, 'b> MimeFactory<'a, 'b> { + pub fn from_msg(context: &'a Context, msg: &'b Message) -> Result, Error> { + let chat = Chat::load_from_db(context, msg.chat_id)?; + + let mut factory = MimeFactory { from_addr: context .get_config(Config::ConfiguredAddr) .unwrap_or_default(), @@ -62,31 +76,103 @@ impl<'a> MimeFactory<'a> { .unwrap_or_else(|| context.stock_str(StockMessage::StatusLine).to_string()), recipients_names: Vec::with_capacity(5), recipients_addr: Vec::with_capacity(5), - timestamp: 0, - rfc724_mid: String::default(), - loaded: Loaded::Nothing, + timestamp: msg.timestamp_sort, + loaded: Loaded::Message, msg, - chat: None, - increation: false, + chat: Some(chat), + increation: msg.is_increation(), in_reply_to: String::default(), references: String::default(), req_mdn: false, - out: Vec::new(), - envelope: None, - out_encrypted: false, - out_gossiped: false, - out_last_added_location_id: 0, + last_added_location_id: 0, context, + }; + + // just set the chat above + let chat = factory.chat.as_ref().unwrap(); + + if chat.is_self_talk() { + factory + .recipients_names + .push(factory.from_displayname.to_string()); + factory.recipients_addr.push(factory.from_addr.to_string()); + } else { + context.sql.query_map( + "SELECT c.authname, c.addr \ + FROM chats_contacts cc \ + LEFT JOIN contacts c ON cc.contact_id=c.id \ + WHERE cc.chat_id=? AND cc.contact_id>9;", + params![factory.msg.chat_id as i32], + |row| { + let authname: String = row.get(0)?; + let addr: String = row.get(1)?; + Ok((authname, addr)) + }, + |rows| { + for row in rows { + let (authname, addr) = row?; + if !vec_contains_lowercase(&factory.recipients_addr, &addr) { + factory.recipients_addr.push(addr); + factory.recipients_names.push(authname); + } + } + Ok(()) + }, + )?; + + let command = factory.msg.param.get_cmd(); + let msg = &factory.msg; + + /* for added members, the list is just fine */ + if command == SystemMessage::MemberRemovedFromGroup { + let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default(); + + let self_addr = context + .get_config(Config::ConfiguredAddr) + .unwrap_or_default(); + + if !email_to_remove.is_empty() && !addr_cmp(email_to_remove, self_addr) { + if !vec_contains_lowercase(&factory.recipients_addr, &email_to_remove) { + factory.recipients_names.push("".to_string()); + factory.recipients_addr.push(email_to_remove.to_string()); + } + } + } + if command != SystemMessage::AutocryptSetupMessage + && command != SystemMessage::SecurejoinMessage + && context.get_config_bool(Config::MdnsEnabled) + { + factory.req_mdn = true; + } } + let row = context.sql.query_row( + "SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", + params![msg.id], + |row| { + let in_reply_to: String = row.get(0)?; + let references: String = row.get(1)?; + + Ok((in_reply_to, references)) + }, + ); + + match row { + Ok((in_reply_to, references)) => { + factory.in_reply_to = in_reply_to; + factory.references = references; + } + Err(err) => { + error!( + context, + "mimefactory: failed to load mime_in_reply_to: {:?}", err + ); + } + } + + Ok(factory) } - pub fn finalize_mime_message(&mut self, encrypted: bool, gossiped: bool) -> Result<(), Error> { - self.out_encrypted = encrypted; - self.out_gossiped = encrypted && gossiped; - Ok(()) - } - - pub fn load_mdn(context: &'a Context, msg_id: MsgId) -> Result { + pub fn from_mdn(context: &'a Context, msg: &'b Message) -> Result { // MDNs not enabled - check this is late, in the job. the // user may have changed its choice while offline ... ensure!( @@ -94,33 +180,36 @@ impl<'a> MimeFactory<'a> { "MDNs meanwhile disabled" ); - let msg = Message::load_from_db(context, msg_id)?; - let mut factory = MimeFactory::from_message(context, msg); - let contact = Contact::load_from_db(factory.context, factory.msg.from_id)?; + let contact = Contact::load_from_db(context, msg.from_id)?; // Do not send MDNs trash etc.; chats.blocked is already checked by the caller // in dc_markseen_msgs() ensure!(!contact.is_blocked(), "Contact blocked"); - ensure!( - factory.msg.chat_id > DC_CHAT_ID_LAST_SPECIAL, - "Invalid chat id" - ); + ensure!(msg.chat_id > DC_CHAT_ID_LAST_SPECIAL, "Invalid chat id"); - factory - .recipients_names - .push(contact.get_authname().to_string()); - factory.recipients_addr.push(contact.get_addr().to_string()); - factory.timestamp = dc_create_smeared_timestamp(factory.context); - factory.rfc724_mid = dc_create_outgoing_rfc724_mid(None, &factory.from_addr); - factory.loaded = Loaded::MDN; - - Ok(factory) + Ok(MimeFactory { + context, + from_addr: context + .get_config(Config::ConfiguredAddr) + .unwrap_or_default(), + from_displayname: context.get_config(Config::Displayname).unwrap_or_default(), + selfstatus: context + .get_config(Config::Selfstatus) + .unwrap_or_else(|| context.stock_str(StockMessage::StatusLine).to_string()), + recipients_names: vec![contact.get_authname().to_string()], + recipients_addr: vec![contact.get_addr().to_string()], + timestamp: dc_create_smeared_timestamp(context), + loaded: Loaded::MDN, + msg, + chat: None, + increation: false, + in_reply_to: String::default(), + references: String::default(), + req_mdn: false, + last_added_location_id: 0, + }) } - /******************************************************************************* - * Render a basic email - ******************************************************************************/ - fn peerstates_for_recipients(&self) -> Result, &str)>, Error> { let self_addr = self .context @@ -165,7 +254,6 @@ impl<'a> MimeFactory<'a> { Ok(false) } Loaded::MDN => Ok(false), - Loaded::Nothing => bail!("No message loaded"), } } @@ -180,7 +268,6 @@ impl<'a> MimeFactory<'a> { } } Loaded::MDN => Ok(PeerstateVerifiedStatus::Unverified), - Loaded::Nothing => bail!("No message loaded"), } } @@ -199,7 +286,6 @@ impl<'a> MimeFactory<'a> { } } Loaded::MDN => Ok(DC_FP_NO_AUTOCRYPT_HEADER), - Loaded::Nothing => bail!("No message loaded"), } } @@ -225,7 +311,6 @@ impl<'a> MimeFactory<'a> { Ok(false) } Loaded::MDN => Ok(false), - Loaded::Nothing => bail!("No message loaded"), } } @@ -248,7 +333,6 @@ impl<'a> MimeFactory<'a> { Ok(None) } Loaded::MDN => Ok(None), - Loaded::Nothing => bail!("No message loaded"), } } @@ -288,18 +372,10 @@ impl<'a> MimeFactory<'a> { let e = self.context.stock_str(StockMessage::ReadRcpt); Ok(format!("Chat: {}", e).to_string()) } - Loaded::Nothing => bail!("No message loaded"), } } - pub fn render(&mut self) -> Result<(), Error> { - // TODO: take self - - ensure!( - self.loaded != Loaded::Nothing && self.out.is_empty(), - "Invalid use of mimefactory-object." - ); - + pub fn render(mut self) -> Result { let e2ee_guranteed = self.is_e2ee_guranteed()?; let mut encrypt_helper = EncryptHelper::new(self.context)?; @@ -389,7 +465,6 @@ impl<'a> MimeFactory<'a> { self.render_message(&mut protected_headers, &mut unprotected_headers, &grpimage)? } Loaded::MDN => self.render_mdn(&mut protected_headers, &mut unprotected_headers)?, - Loaded::Nothing => bail!("No message loaded"), }; if force_plaintext != DC_FP_NO_AUTOCRYPT_HEADER { @@ -403,8 +478,9 @@ impl<'a> MimeFactory<'a> { let peerstates = self.peerstates_for_recipients()?; let should_encrypt = encrypt_helper.should_encrypt(self.context, e2ee_guranteed, &peerstates)?; + let is_encrypted = should_encrypt && force_plaintext == 0; - let mut outer_message = if should_encrypt && force_plaintext == 0 { + let mut outer_message = if is_encrypted { for header in protected_headers.into_iter() { message = message.header(header); } @@ -452,9 +528,6 @@ impl<'a> MimeFactory<'a> { ) .header(("Subject".to_string(), "...".to_string())); - let gossiped = do_gossip && !peerstates.is_empty(); - self.finalize_mime_message(true, gossiped)?; - outer_message } else { // In the unencrypted case, we add all headers to the outer message. @@ -464,8 +537,6 @@ impl<'a> MimeFactory<'a> { for header in unprotected_headers.into_iter() { message = message.header(header); } - self.finalize_mime_message(false, false)?; - message }; @@ -473,11 +544,36 @@ impl<'a> MimeFactory<'a> { .header(Header::new_with_value("To".into(), to).unwrap()) .header(Header::new_with_value("From".into(), vec![from]).unwrap()); - // TODO - // self.envelope = Some(Envelope::new(Some(from), to).expect("setting from")); - self.out = outer_message.build().as_string().into_bytes(); + let is_gossiped = is_encrypted && do_gossip && !peerstates.is_empty(); - Ok(()) + let MimeFactory { + recipients_addr, + from_addr, + last_added_location_id, + msg, + loaded, + .. + } = self; + + let rfc724_mid = match loaded { + Loaded::Message => msg.rfc724_mid.clone(), + Loaded::MDN => dc_create_outgoing_rfc724_mid(None, &from_addr), + }; + + Ok(RenderedEmail { + message: outer_message.build().as_string().into_bytes(), + // envelope: Envelope::new, + is_encrypted, + is_gossiped, + last_added_location_id, + foreign_id: match loaded { + Loaded::Message => Some(msg.id), + Loaded::MDN => None, + }, + recipients: recipients_addr, + from: from_addr, + rfc724_mid, + }) } fn render_message( @@ -738,7 +834,7 @@ impl<'a> MimeFactory<'a> { is_multipart = true; if !self.msg.param.exists(Param::SetLatitude) { // otherwise, the independent location is already filed - self.out_last_added_location_id = last_added_location_id; + self.last_added_location_id = last_added_location_id; } } Err(err) => { @@ -761,100 +857,6 @@ impl<'a> MimeFactory<'a> { ) -> Result { unimplemented!() } - - pub fn load_msg(context: &Context, msg_id: MsgId) -> Result, Error> { - let msg = Message::load_from_db(context, msg_id)?; - let chat = Chat::load_from_db(context, msg.chat_id)?; - let mut factory = MimeFactory::from_message(context, msg); - factory.chat = Some(chat); - - // just set the chat above - let chat = factory.chat.as_ref().unwrap(); - - if chat.is_self_talk() { - factory - .recipients_names - .push(factory.from_displayname.to_string()); - factory.recipients_addr.push(factory.from_addr.to_string()); - } else { - context.sql.query_map( - "SELECT c.authname, c.addr \ - FROM chats_contacts cc \ - LEFT JOIN contacts c ON cc.contact_id=c.id \ - WHERE cc.chat_id=? AND cc.contact_id>9;", - params![factory.msg.chat_id as i32], - |row| { - let authname: String = row.get(0)?; - let addr: String = row.get(1)?; - Ok((authname, addr)) - }, - |rows| { - for row in rows { - let (authname, addr) = row?; - if !vec_contains_lowercase(&factory.recipients_addr, &addr) { - factory.recipients_addr.push(addr); - factory.recipients_names.push(authname); - } - } - Ok(()) - }, - )?; - - let command = factory.msg.param.get_cmd(); - let msg = &factory.msg; - - /* for added members, the list is just fine */ - if command == SystemMessage::MemberRemovedFromGroup { - let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default(); - - let self_addr = context - .get_config(Config::ConfiguredAddr) - .unwrap_or_default(); - - if !email_to_remove.is_empty() && !addr_cmp(email_to_remove, self_addr) { - if !vec_contains_lowercase(&factory.recipients_addr, &email_to_remove) { - factory.recipients_names.push("".to_string()); - factory.recipients_addr.push(email_to_remove.to_string()); - } - } - } - if command != SystemMessage::AutocryptSetupMessage - && command != SystemMessage::SecurejoinMessage - && context.get_config_bool(Config::MdnsEnabled) - { - factory.req_mdn = true; - } - } - let row = context.sql.query_row( - "SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", - params![factory.msg.id], - |row| { - let in_reply_to: String = row.get(0)?; - let references: String = row.get(1)?; - - Ok((in_reply_to, references)) - }, - ); - match row { - Ok((in_reply_to, references)) => { - factory.in_reply_to = in_reply_to; - factory.references = references; - } - Err(err) => { - error!( - context, - "mimefactory: failed to load mime_in_reply_to: {:?}", err - ); - } - } - - factory.loaded = Loaded::Message; - factory.timestamp = factory.msg.timestamp_sort; - factory.rfc724_mid = factory.msg.rfc724_mid.clone(); - factory.increation = factory.msg.is_increation(); - - Ok(factory) - } } fn build_body_file(