Compare commits

...

15 Commits

Author SHA1 Message Date
Alexander Krotov
7ea90143ff Load only one job at a time
As a result of job, other jobs can be added or deleted. To avoid
processing deleted jobs and to process other jobs in the correct order,
we should reload them after performing each job.

However, we don't use "LIMIT 1" in SQL queries, because we want to be
able to skip over invalid jobs, and checking if job is invalid can only
be done in Rust.
2020-01-18 21:49:10 +03:00
Alexander Krotov
7abe095b5b Mark two messages as seen in test_send_and_receive_message_markseen
This may or may not send a combined MDN out.  We don't test for it,
but the test ensures that *if combined MDNs are sent in this case*,
then we receive them correctly.
2020-01-18 21:49:10 +03:00
Alexander Krotov
61a8e10226 Log it when MDNs are combined 2020-01-18 21:49:10 +03:00
Alexander Krotov
6005ea3b68 Aggregate SendMdn jobs 2020-01-18 21:49:10 +03:00
Alexander Krotov
e6f8df87f1 Make it possible to add X-Additional-Message-IDs to MDNs 2020-01-18 21:49:10 +03:00
Alexander Krotov
9ee49e4697 Parse additional message IDs in MDNs 2020-01-18 21:49:10 +03:00
Alexander Krotov
645e773d3e Construct list of recipients in SendMdn
There is always one recipient for MDNs, so it is easier to only handle
this case.
2020-01-18 21:42:51 +03:00
Alexander Krotov
a7d88973be Move check for blocked contact from mimefactory to SendMdn job 2020-01-18 21:42:51 +03:00
Alexander Krotov
a5c8e9e72e Store contact ID in SendMdn job foreign_id
This change makes it possible to find all pending MDNs for the contact
with an SQL query.
2020-01-18 21:42:51 +03:00
Alexander Krotov
e7f4898e90 Move check for enabled MDNs from message rendering to MDN job 2020-01-18 21:42:51 +03:00
Alexander Krotov
57d9bafde3 Move foreign_id handling out of mimefactory 2020-01-18 21:42:51 +03:00
Alexander Krotov
5ba879ce95 Make a new job type for MDN sending
Now MDN is generated on every try instead of being stored in a blob.
2020-01-18 21:42:51 +03:00
Alexander Krotov
171fa32380 Remove no-op jobs SendMdnOld and SendMsgToSmtpOld 2020-01-18 21:42:51 +03:00
Alexander Krotov
4bb62a5fbd Implement Display for MessageState 2020-01-18 21:42:51 +03:00
Alexander Krotov
2bed9d57cc Test MDN parsing 2020-01-18 21:42:51 +03:00
7 changed files with 453 additions and 99 deletions

View File

@@ -678,14 +678,26 @@ class TestOnlineAccount:
chat2b.mark_noticed()
assert chat2b.count_fresh_messages() == 0
lp.sec("mark message as seen on ac2, wait for changes on ac1")
ac2.mark_seen_messages([msg_in])
ac2._evlogger.consume_events()
lp.sec("sending a second message from ac1 to ac2")
msg_out2 = chat.send_text("message2")
lp.sec("wait for ac2 to receive second message")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
assert ev[2] == msg_out2.id
msg_in2 = ac2.get_message_by_id(msg_out2.id)
lp.sec("mark messages as seen on ac2, wait for changes on ac1")
ac2.mark_seen_messages([msg_in, msg_in2])
lp.step("1")
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_READ")
assert ev[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev[2] > const.DC_MSG_ID_LAST_SPECIAL
for i in range(2):
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_READ")
assert ev[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev[2] > const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
assert msg_out.is_out_mdn_received()
assert msg_out2.is_out_mdn_received()
lp.sec("check that a second call to mark_seen does not create change or smtp job")
ac2._evlogger.consume_events()

View File

@@ -10,6 +10,10 @@ pub enum HeaderDef {
Cc,
Disposition,
OriginalMessageId,
/// Delta Chat extension for message IDs in combined MDNs
XAdditionalMessageIds,
ListId,
References,
InReplyTo,

View File

@@ -6,6 +6,7 @@
use std::{fmt, time};
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use rand::{thread_rng, Rng};
use async_std::task;
@@ -15,6 +16,7 @@ use crate::chat;
use crate::config::Config;
use crate::configure::*;
use crate::constants::*;
use crate::contact::Contact;
use crate::context::{Context, PerformJobsNeeded};
use crate::dc_tools::*;
use crate::error::{Error, Result};
@@ -88,9 +90,7 @@ pub enum Action {
// Jobs in the SMTP-thread, range from DC_SMTP_THREAD..DC_SMTP_THREAD+999
MaybeSendLocations = 5005, // low priority ...
MaybeSendLocationsEnded = 5007,
SendMdnOld = 5010,
SendMdn = 5011,
SendMsgToSmtpOld = 5900,
SendMdn = 5010,
SendMsgToSmtp = 5901, // ... high priority
}
@@ -118,9 +118,7 @@ impl From<Action> for Thread {
MaybeSendLocations => Thread::Smtp,
MaybeSendLocationsEnded => Thread::Smtp,
SendMdnOld => Thread::Smtp,
SendMdn => Thread::Smtp,
SendMsgToSmtpOld => Thread::Smtp,
SendMsgToSmtp => Thread::Smtp,
}
}
@@ -257,6 +255,130 @@ impl Job {
}
}
/// Get `SendMdn` jobs with foreign_id equal to `contact_id` excluding the `job_id` job.
fn get_additional_mdn_jobs(
&self,
context: &Context,
contact_id: u32,
) -> sql::Result<(Vec<u32>, Vec<String>)> {
// Extract message IDs from job parameters
let res: Vec<(u32, MsgId)> = context.sql.query_map(
"SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?",
params![contact_id, self.job_id],
|row| {
let job_id: u32 = row.get(0)?;
let params_str: String = row.get(1)?;
let params: Params = params_str.parse().unwrap_or_default();
Ok((job_id, params))
},
|jobs| {
let res = jobs
.filter_map(|row| {
let (job_id, params) = row.ok()?;
let msg_id = params.get_msg_id()?;
Some((job_id, msg_id))
})
.collect();
Ok(res)
},
)?;
// Load corresponding RFC724 message IDs
let mut job_ids = Vec::new();
let mut rfc724_mids = Vec::new();
for (job_id, msg_id) in res {
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id) {
job_ids.push(job_id);
rfc724_mids.push(rfc724_mid);
}
}
Ok((job_ids, rfc724_mids))
}
#[allow(non_snake_case)]
fn SendMdn(&mut self, context: &Context) -> Status {
if !context.get_config_bool(Config::MdnsEnabled) {
// User has disabled MDNs after job scheduling but before
// execution.
return Status::Finished(Err(format_err!("MDNs are disabled")));
}
let contact_id = self.foreign_id;
let contact = job_try!(Contact::load_from_db(context, contact_id));
if contact.is_blocked() {
return Status::Finished(Err(format_err!("Contact is blocked")));
}
let msg_id = if let Some(msg_id) = self.param.get_msg_id() {
msg_id
} else {
return Status::Finished(Err(format_err!(
"SendMdn job has invalid parameters: {}",
self.param
)));
};
// Try to aggregate other SendMdn jobs and send a combined MDN.
let (additional_job_ids, additional_rfc724_mids) = self
.get_additional_mdn_jobs(context, contact_id)
.unwrap_or_default();
if !additional_rfc724_mids.is_empty() {
info!(
context,
"SendMdn job: aggregating {} additional MDNs",
additional_rfc724_mids.len()
)
}
let msg = job_try!(Message::load_from_db(context, msg_id));
let mimefactory = job_try!(MimeFactory::from_mdn(context, &msg, additional_rfc724_mids));
let rendered_msg = job_try!(mimefactory.render());
let body = rendered_msg.message;
let addr = contact.get_addr();
let recipient = job_try!(async_smtp::EmailAddress::new(addr.to_string())
.map_err(|err| format_err!("invalid recipient: {} {:?}", addr, err)));
let recipients = vec![recipient];
/* connect to SMTP server, if not yet done */
if !context.smtp.lock().unwrap().is_connected() {
let loginparam = LoginParam::from_database(context, "configured_");
if let Err(err) = context.smtp.lock().unwrap().connect(context, &loginparam) {
warn!(context, "SMTP connection failure: {:?}", err);
return Status::RetryLater;
}
}
let mut smtp = context.smtp.lock().unwrap();
match task::block_on(smtp.send(context, recipients, body, self.job_id)) {
Err(crate::smtp::send::Error::SendError(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {}", err);
smtp.disconnect();
self.pending_error = Some(err.to_string());
Status::RetryLater
}
Err(crate::smtp::send::Error::EnvelopeError(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect();
warn!(context, "SMTP job is invalid: {}", err);
Status::Finished(Err(Error::SmtpError(err)))
}
Err(crate::smtp::send::Error::NoTransport) => {
// Should never happen.
// It does not even make sense to disconnect here.
error!(context, "SMTP job failed because SMTP has no transport");
Status::Finished(Err(format_err!("SMTP has not transport")))
}
Ok(()) => {
// Remove additional SendMdn jobs we have aggretated into this one.
job_try!(job_kill_ids(context, &additional_job_ids));
Status::Finished(Ok(()))
}
}
}
#[allow(non_snake_case)]
fn MoveMsg(&mut self, context: &Context) -> Status {
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
@@ -363,7 +485,7 @@ impl Job {
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default()
&& context.get_config_bool(Config::MdnsEnabled)
{
if let Err(err) = send_mdn(context, msg.id) {
if let Err(err) = send_mdn(context, &msg) {
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
return Status::Finished(Err(err));
}
@@ -422,6 +544,19 @@ pub fn job_kill_action(context: &Context, action: Action) -> bool {
.is_ok()
}
/// Remove jobs with specified IDs.
pub fn job_kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
sql::execute(
context,
&context.sql,
format!(
"DELETE FROM jobs WHERE id IN({})",
job_ids.iter().map(|_| "?").join(",")
),
job_ids,
)
}
pub fn perform_inbox_fetch(context: &Context) {
let use_network = context.get_config_bool(Config::InboxWatch);
@@ -731,7 +866,7 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
msg.save_param_to_disk(context);
}
add_smtp_job(context, Action::SendMsgToSmtp, &rendered_msg)?;
add_smtp_job(context, Action::SendMsgToSmtp, msg.id, &rendered_msg)?;
Ok(())
}
@@ -756,9 +891,7 @@ pub fn perform_sentbox_jobs(context: &Context) {
}
fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
let jobs: Vec<Job> = load_jobs(context, thread, probe_network);
for mut job in jobs {
while let Some(mut job) = load_next_job(context, thread, probe_network) {
info!(context, "{}-job {} started...", thread, job);
// some configuration jobs are "exclusive":
@@ -797,7 +930,7 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context),
Action::MarkseenMdnOnImap => job.MarkseenMdnOnImap(context),
Action::MoveMsg => job.MoveMsg(context),
Action::SendMdn => job.SendMsgToSmtp(context),
Action::SendMdn => job.SendMdn(context),
Action::ConfigureImap => JobConfigureImap(context),
Action::ImexImap => match JobImexImap(context, &job) {
Ok(()) => Status::Finished(Ok(())),
@@ -814,8 +947,6 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
sql::housekeeping(context);
Status::Finished(Ok(()))
}
Action::SendMdnOld => Status::Finished(Ok(())),
Action::SendMsgToSmtpOld => Status::Finished(Ok(())),
};
info!(
@@ -946,17 +1077,21 @@ fn suspend_smtp_thread(context: &Context, suspend: bool) {
}
}
fn send_mdn(context: &Context, msg_id: MsgId) -> Result<()> {
let msg = Message::load_from_db(context, msg_id)?;
let mimefactory = MimeFactory::from_mdn(context, &msg)?;
let rendered_msg = mimefactory.render()?;
fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
let mut param = Params::new();
param.set(Param::MessageId, msg.id.to_u32().to_string());
add_smtp_job(context, Action::SendMdn, &rendered_msg)?;
job_add(context, Action::SendMdn, msg.from_id as i32, param, 0);
Ok(())
}
fn add_smtp_job(context: &Context, action: Action, rendered_msg: &RenderedEmail) -> Result<()> {
fn add_smtp_job(
context: &Context,
action: Action,
msg_id: MsgId,
rendered_msg: &RenderedEmail,
) -> Result<()> {
ensure!(
!rendered_msg.recipients.is_empty(),
"no recipients for smtp job set"
@@ -969,16 +1104,7 @@ fn add_smtp_job(context: &Context, action: Action, rendered_msg: &RenderedEmail)
param.set(Param::File, blob.as_name());
param.set(Param::Recipients, &recipients);
job_add(
context,
action,
rendered_msg
.foreign_id
.map(|v| v.to_u32() as i32)
.unwrap_or_default(),
param,
0,
);
job_add(context, action, msg_id.to_u32() as i32, param, 0);
Ok(())
}
@@ -1039,7 +1165,7 @@ pub fn interrupt_smtp_idle(context: &Context) {
/// IMAP jobs. The `probe_network` parameter decides how to query
/// jobs, this is tricky and probably wrong currently. Look at the
/// SQL queries for details.
fn load_jobs(context: &Context, thread: Thread, probe_network: bool) -> Vec<Job> {
fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Option<Job> {
let query = if !probe_network {
// processing for first-try and after backoff-timeouts:
// process jobs in the order they were added.
@@ -1081,14 +1207,13 @@ fn load_jobs(context: &Context, thread: Thread, probe_network: bool) -> Vec<Job>
Ok(job)
},
|jobs| {
let mut ret: Vec<Job> = Vec::new();
for job in jobs {
match job {
Ok(j) => ret.push(j),
Ok(j) => return Ok(Some(j)),
Err(e) => warn!(context, "Bad job from the database: {}", e),
}
}
Ok(ret)
Ok(None)
},
)
.unwrap_or_default()
@@ -1121,15 +1246,17 @@ mod tests {
}
#[test]
fn test_load_jobs() {
fn test_load_next_job() {
// We want to ensure that loading jobs skips over jobs which
// fails to load from the database instead of failing to load
// all jobs.
let t = dummy_context();
insert_job(&t.ctx, 0);
insert_job(&t.ctx, -1); // This can not be loaded into Job struct.
let jobs = load_next_job(&t.ctx, Thread::from(Action::MoveMsg), false);
assert!(jobs.is_none());
insert_job(&t.ctx, 1);
let jobs = load_jobs(&t.ctx, Thread::from(Action::MoveMsg), false);
assert_eq!(jobs.len(), 2);
let jobs = load_next_job(&t.ctx, Thread::from(Action::MoveMsg), false);
assert!(jobs.is_some());
}
}

View File

@@ -610,7 +610,7 @@ impl Message {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[repr(i32)]
pub enum MessageState {
Undefined = 0,
@@ -661,6 +661,27 @@ impl Default for MessageState {
}
}
impl std::fmt::Display for MessageState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Undefined => "Undefined",
Self::InFresh => "Fresh",
Self::InNoticed => "Noticed",
Self::InSeen => "Seen",
Self::OutPreparing => "Preparing",
Self::OutDraft => "Draft",
Self::OutPending => "Pending",
Self::OutFailed => "Failed",
Self::OutDelivered => "Delivered",
Self::OutMdnRcvd => "Read",
}
)
}
}
impl From<MessageState> for LotState {
fn from(s: MessageState) -> Self {
use MessageState::*;
@@ -815,19 +836,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
}
}
ret += "State: ";
use MessageState::*;
match msg.state {
InFresh => ret += "Fresh",
InNoticed => ret += "Noticed",
InSeen => ret += "Seen",
OutDelivered => ret += "Delivered",
OutFailed => ret += "Failed",
OutMdnRcvd => ret += "Read",
OutPending => ret += "Pending",
OutPreparing => ret += "Preparing",
_ => ret += &format!("{}", msg.state),
}
ret += &format!("State: {}", msg.state);
if msg.has_location() {
ret += ", Location sent";

View File

@@ -11,7 +11,6 @@ use crate::dc_tools::*;
use crate::e2ee::*;
use crate::error::Error;
use crate::location;
use crate::message::MsgId;
use crate::message::{self, Message};
use crate::mimeparser::SystemMessage;
use crate::param::*;
@@ -21,7 +20,7 @@ use crate::stock::StockMessage;
#[derive(Debug, Clone)]
pub enum Loaded {
Message { chat: Chat },
MDN,
MDN { additional_msg_ids: Vec<String> },
}
/// Helper to construct mime messages.
@@ -53,8 +52,6 @@ pub struct RenderedEmail {
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<MsgId>,
pub from: String,
pub recipients: Vec<String>,
@@ -161,21 +158,15 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Ok(factory)
}
pub fn from_mdn(context: &'a Context, msg: &'b Message) -> Result<Self, Error> {
// MDNs not enabled - check this is late, in the job. the
// user may have changed its choice while offline ...
ensure!(
context.get_config_bool(Config::MdnsEnabled),
"MDNs meanwhile disabled"
);
pub fn from_mdn(
context: &'a Context,
msg: &'b Message,
additional_msg_ids: Vec<String>,
) -> Result<Self, Error> {
ensure!(msg.chat_id > DC_CHAT_ID_LAST_SPECIAL, "Invalid chat 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!(msg.chat_id > DC_CHAT_ID_LAST_SPECIAL, "Invalid chat id");
Ok(MimeFactory {
context,
from_addr: context
@@ -190,7 +181,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
contact.get_addr().to_string(),
)],
timestamp: dc_create_smeared_timestamp(context),
loaded: Loaded::MDN,
loaded: Loaded::MDN { additional_msg_ids },
msg,
in_reply_to: String::default(),
references: String::default(),
@@ -243,7 +234,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
false
}
Loaded::MDN => false,
Loaded::MDN { .. } => false,
}
}
@@ -256,7 +247,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
PeerstateVerifiedStatus::Unverified
}
}
Loaded::MDN => PeerstateVerifiedStatus::Unverified,
Loaded::MDN { .. } => PeerstateVerifiedStatus::Unverified,
}
}
@@ -272,7 +263,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.unwrap_or_default()
}
}
Loaded::MDN => ForcePlaintext::NoAutocryptHeader as i32,
Loaded::MDN { .. } => ForcePlaintext::NoAutocryptHeader as i32,
}
}
@@ -287,7 +278,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
self.msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
}
Loaded::MDN => false,
Loaded::MDN { .. } => false,
}
}
@@ -317,7 +308,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
None
}
Loaded::MDN => None,
Loaded::MDN { .. } => None,
}
}
@@ -347,7 +338,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
format!("Chat: {}", raw_subject)
}
}
Loaded::MDN => self.context.stock_str(StockMessage::ReadRcpt).into_owned(),
Loaded::MDN { .. } => self.context.stock_str(StockMessage::ReadRcpt).into_owned(),
}
}
@@ -433,7 +424,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Loaded::Message { .. } => {
self.render_message(&mut protected_headers, &mut unprotected_headers, &grpimage)?
}
Loaded::MDN => self.render_mdn()?,
Loaded::MDN { .. } => self.render_mdn()?,
};
if force_plaintext != ForcePlaintext::NoAutocryptHeader as i32 {
@@ -451,7 +442,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
Loaded::MDN => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
Loaded::MDN { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
};
// we could also store the message-id in the protected headers
@@ -563,8 +554,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
recipients,
from_addr,
last_added_location_id,
msg,
loaded,
..
} = self;
@@ -574,10 +563,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
is_encrypted,
is_gossiped,
last_added_location_id,
foreign_id: match loaded {
Loaded::Message { .. } => Some(msg.id),
Loaded::MDN => None,
},
recipients: recipients.into_iter().map(|(_, addr)| addr).collect(),
from: from_addr,
rfc724_mid,
@@ -593,7 +578,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let context = self.context;
let chat = match &self.loaded {
Loaded::Message { chat } => chat,
Loaded::MDN => bail!("Attempt to render MDN as a message"),
Loaded::MDN { .. } => bail!("Attempt to render MDN as a message"),
};
let command = self.msg.param.get_cmd();
let mut placeholdertext = None;
@@ -912,6 +897,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
// are forwarded for any reasons (eg. gmail always forwards to IMAP), we have no chance to decrypt them;
// this issue is fixed with 0.9.4
let additional_msg_ids = match &self.loaded {
Loaded::Message { .. } => bail!("Attempt to render a message as MDN"),
Loaded::MDN {
additional_msg_ids, ..
} => additional_msg_ids,
};
let mut message = PartBuilder::new().header((
"Content-Type".to_string(),
"multipart/report; report-type=disposition-notification".to_string(),
@@ -953,10 +945,22 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
version, self.from_addr, self.from_addr, self.msg.rfc724_mid
);
let extension_fields = if additional_msg_ids.is_empty() {
"".to_string()
} else {
"X-Additional-Message-IDs: ".to_string()
+ &additional_msg_ids
.iter()
.map(|mid| render_rfc724_mid(&mid))
.collect::<Vec<String>>()
.join(" ")
+ "\r\n"
};
message = message.child(
PartBuilder::new()
.content_type(&"message/disposition-notification".parse().unwrap())
.body(message_text2)
.body(message_text2 + &extension_fields)
.build(),
);

View File

@@ -49,7 +49,7 @@ pub struct MimeMessage<'a> {
pub message_kml: Option<location::Kml>,
pub user_avatar: AvatarAction,
pub group_avatar: AvatarAction,
reports: Vec<Report>,
pub(crate) reports: Vec<Report>,
}
#[derive(Debug, PartialEq)]
@@ -742,8 +742,17 @@ impl<'a> MimeMessage<'a> {
.flatten()
.and_then(|v| parse_message_id(&v))
{
let additional_message_ids = report_fields
.get_first_value(&HeaderDef::XAdditionalMessageIds.get_headername())
.ok()
.flatten()
.map_or_else(Vec::new, |v| {
v.split(' ').filter_map(parse_message_id).collect()
});
return Ok(Some(Report {
original_message_id,
additional_message_ids,
}));
}
}
@@ -770,14 +779,18 @@ impl<'a> MimeMessage<'a> {
let mut mdn_recognized = false;
for report in &self.reports {
if let Some((chat_id, msg_id)) = message::mdn_from_ext(
self.context,
from_id,
&report.original_message_id,
sent_timestamp,
) {
self.context.call_cb(Event::MsgRead { chat_id, msg_id });
mdn_recognized = true;
for original_message_id in
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
{
if let Some((chat_id, msg_id)) = message::mdn_from_ext(
self.context,
from_id,
original_message_id,
sent_timestamp,
) {
self.context.call_cb(Event::MsgRead { chat_id, msg_id });
mdn_recognized = true;
}
}
}
@@ -852,8 +865,11 @@ fn update_gossip_peerstates(
}
#[derive(Debug)]
struct Report {
pub(crate) struct Report {
/// Original-Message-ID header
original_message_id: String,
/// X-Additional-Message-IDs
additional_message_ids: Vec<String>,
}
fn parse_message_id(field: &str) -> Option<String> {
@@ -1262,4 +1278,176 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\
// and only goes into message_kml.
assert_eq!(mimeparser.parts.len(), 1);
}
#[test]
fn test_parse_mdn() {
let context = dummy_context();
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
Message-ID: <bar@example.org>\n\
To: Alice <alice@example.org>\n\
From: Bob <bob@example.org>\n\
Content-Type: multipart/report; report-type=disposition-notification;\n\t\
boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
Content-Type: text/plain; charset=utf-8\n\
\n\
The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\
\n\
This is no guarantee the content was read.\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
Content-Type: message/disposition-notification\n\
\n\
Reporting-UA: Delta Chat 1.0.0-beta.22\n\
Original-Recipient: rfc822;bob@example.org\n\
Final-Recipient: rfc822;bob@example.org\n\
Original-Message-ID: <foo@example.org>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
assert_eq!(
message.get_subject(),
Some("Chat: Message opened".to_string())
);
assert_eq!(message.parts.len(), 0);
assert_eq!(message.reports.len(), 1);
}
/// Test parsing multiple MDNs combined in a single message.
///
/// RFC 6522 specifically allows MDNs to be nested inside
/// multipart MIME messages.
#[test]
fn test_parse_multiple_mdns() {
let context = dummy_context();
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
Message-ID: <foo@example.org>\n\
To: Alice <alice@example.org>\n\
From: Bob <bob@example.org>\n\
Content-Type: multipart/parallel; boundary=outer\n\
\n\
This is a multipart MDN.\n\
\n\
--outer\n\
Content-Type: multipart/report; report-type=disposition-notification;\n\t\
boundary=kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
Content-Type: text/plain; charset=utf-8\n\
\n\
The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\
\n\
This is no guarantee the content was read.\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
Content-Type: message/disposition-notification\n\
\n\
Reporting-UA: Delta Chat 1.0.0-beta.22\n\
Original-Recipient: rfc822;bob@example.org\n\
Final-Recipient: rfc822;bob@example.org\n\
Original-Message-ID: <bar@example.org>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
--outer\n\
Content-Type: multipart/report; report-type=disposition-notification;\n\t\
boundary=zuOJlsTfZAukyawEPVdIgqWjaM9w2W\n\
\n\
\n\
--zuOJlsTfZAukyawEPVdIgqWjaM9w2W\n\
Content-Type: text/plain; charset=utf-8\n\
\n\
The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\
\n\
This is no guarantee the content was read.\n\
\n\
\n\
--zuOJlsTfZAukyawEPVdIgqWjaM9w2W\n\
Content-Type: message/disposition-notification\n\
\n\
Reporting-UA: Delta Chat 1.0.0-beta.22\n\
Original-Recipient: rfc822;bob@example.org\n\
Final-Recipient: rfc822;bob@example.org\n\
Original-Message-ID: <baz@example.org>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n\
\n\
--zuOJlsTfZAukyawEPVdIgqWjaM9w2W--\n\
--outer--\n\
";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
assert_eq!(
message.get_subject(),
Some("Chat: Message opened".to_string())
);
assert_eq!(message.parts.len(), 0);
assert_eq!(message.reports.len(), 2);
}
#[test]
fn test_parse_mdn_with_additional_message_ids() {
let context = dummy_context();
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
Message-ID: <bar@example.org>\n\
To: Alice <alice@example.org>\n\
From: Bob <bob@example.org>\n\
Content-Type: multipart/report; report-type=disposition-notification;\n\t\
boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
Content-Type: text/plain; charset=utf-8\n\
\n\
The \"Encrypted message\" message you sent was displayed on the screen of the recipient.\n\
\n\
This is no guarantee the content was read.\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
Content-Type: message/disposition-notification\n\
\n\
Reporting-UA: Delta Chat 1.0.0-beta.22\n\
Original-Recipient: rfc822;bob@example.org\n\
Final-Recipient: rfc822;bob@example.org\n\
Original-Message-ID: <foo@example.org>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
X-Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
assert_eq!(
message.get_subject(),
Some("Chat: Message opened".to_string())
);
assert_eq!(message.parts.len(), 0);
assert_eq!(message.reports.len(), 1);
assert_eq!(message.reports[0].original_message_id, "foo@example.org");
assert_eq!(
&message.reports[0].additional_message_ids,
&["foo@example.com", "foo@example.net"]
);
}
}

View File

@@ -8,6 +8,7 @@ use num_traits::FromPrimitive;
use crate::blob::{BlobError, BlobObject};
use crate::context::Context;
use crate::error;
use crate::message::MsgId;
use crate::mimeparser::SystemMessage;
/// Available param keys.
@@ -116,6 +117,9 @@ pub enum Param {
/// For QR
GroupName = b'g',
/// For MDN-sending job
MessageId = b'I',
}
/// Possible values for `Param::ForcePlaintext`.
@@ -312,6 +316,12 @@ impl Params {
Ok(Some(path))
}
pub fn get_msg_id(&self) -> Option<MsgId> {
self.get(Param::MessageId)
.and_then(|x| x.parse::<u32>().ok())
.map(MsgId::new)
}
/// Set the given paramter to the passed in `i32`.
pub fn set_int(&mut self, key: Param, value: i32) -> &mut Self {
self.set(key, format!("{}", value));