Skip to content

Commit 625887d

Browse files
committed
fix: Split SMTP jobs already in chat::create_send_msg_jobs() (#5115)
a27e84a "fix: Delete received outgoing messages from SMTP queue" can break sending messages sent as several SMTP messages because they have a lot of recipients: `pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;` We should not cancel sending if it is such a message and we received BCC-self because it does not mean the other part was sent successfully. For this, split such messages into separate jobs in the `smtp` table so that only a job containing BCC-self is canceled from `receive_imf_inner()`. Although this doesn't solve the initial problem with timed-out SMTP requests for such messages completely, this enables fine-grained SMTP retries so we don't need to resend all SMTP messages if only some of them failed to be sent.
1 parent b7c34b7 commit 625887d

File tree

7 files changed

+177
-93
lines changed

7 files changed

+177
-93
lines changed

src/chat.rs

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use crate::chatlist::Chatlist;
1919
use crate::color::str_to_color;
2020
use crate::config::Config;
2121
use crate::constants::{
22-
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
23-
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
22+
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
23+
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
2424
};
2525
use crate::contact::{self, Contact, ContactAddress, ContactId, Origin};
2626
use crate::context::Context;
@@ -2662,17 +2662,20 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
26622662

26632663
/// Tries to send a message synchronously.
26642664
///
2665-
/// Creates a new message in `smtp` table, then drectly opens an SMTP connection and sends the
2666-
/// message. If this fails, the message remains in the database to be sent later.
2665+
/// Creates jobs in the `smtp` table, then drectly opens an SMTP connection and sends the
2666+
/// message. If this fails, the jobs remain in the database for later sending.
26672667
pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
2668-
if let Some(rowid) = prepare_send_msg(context, chat_id, msg).await? {
2669-
let mut smtp = crate::smtp::Smtp::new();
2668+
let rowids = prepare_send_msg(context, chat_id, msg).await?;
2669+
if rowids.is_empty() {
2670+
return Ok(msg.id);
2671+
}
2672+
let mut smtp = crate::smtp::Smtp::new();
2673+
for rowid in rowids {
26702674
send_msg_to_smtp(context, &mut smtp, rowid)
26712675
.await
26722676
.context("failed to send message, queued for later sending")?;
2673-
2674-
context.emit_msgs_changed(msg.chat_id, msg.id);
26752677
}
2678+
context.emit_msgs_changed(msg.chat_id, msg.id);
26762679
Ok(msg.id)
26772680
}
26782681

@@ -2682,7 +2685,7 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
26822685
msg.text = strip_rtlo_characters(&msg.text);
26832686
}
26842687

2685-
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
2688+
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
26862689
context.emit_msgs_changed(msg.chat_id, msg.id);
26872690

26882691
if msg.param.exists(Param::SetLatitude) {
@@ -2695,12 +2698,12 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
26952698
Ok(msg.id)
26962699
}
26972700

2698-
/// Returns rowid from `smtp` table.
2701+
/// Returns row ids of the `smtp` table.
26992702
async fn prepare_send_msg(
27002703
context: &Context,
27012704
chat_id: ChatId,
27022705
msg: &mut Message,
2703-
) -> Result<Option<i64>> {
2706+
) -> Result<Vec<i64>> {
27042707
// prepare_msg() leaves the message state to OutPreparing, we
27052708
// only have to change the state to OutPending in this case.
27062709
// Otherwise we still have to prepare the message, which will set
@@ -2716,20 +2719,16 @@ async fn prepare_send_msg(
27162719
);
27172720
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
27182721
}
2719-
let row_id = create_send_msg_job(context, msg).await?;
2720-
Ok(row_id)
2722+
create_send_msg_jobs(context, msg).await
27212723
}
27222724

2723-
/// Constructs a job for sending a message and inserts into `smtp` table.
2725+
/// Constructs jobs for sending a message and inserts them into the `smtp` table.
27242726
///
2725-
/// Returns rowid if job was created or `None` if SMTP job is not needed, e.g. when sending to a
2727+
/// Returns row ids if jobs were created or an empty `Vec` otherwise, e.g. when sending to a
27262728
/// group with only self and no BCC-to-self configured.
27272729
///
2728-
/// The caller has to interrupt SMTP loop or otherwise process a new row.
2729-
pub(crate) async fn create_send_msg_job(
2730-
context: &Context,
2731-
msg: &mut Message,
2732-
) -> Result<Option<i64>> {
2730+
/// The caller has to interrupt SMTP loop or otherwise process new rows.
2731+
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
27332732
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
27342733

27352734
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
@@ -2748,7 +2747,7 @@ pub(crate) async fn create_send_msg_job(
27482747
let lowercase_from = from.to_lowercase();
27492748

27502749
// Send BCC to self if it is enabled and we are not going to
2751-
// delete it immediately.
2750+
// delete it immediately. `from` must be the last addr, see `receive_imf_inner()` why.
27522751
if context.get_config_bool(Config::BccSelf).await?
27532752
&& context.get_config_delete_server_after().await? != Some(0)
27542753
&& !recipients
@@ -2766,7 +2765,7 @@ pub(crate) async fn create_send_msg_job(
27662765
);
27672766
msg.id.set_delivered(context).await?;
27682767
msg.state = MessageState::OutDelivered;
2769-
return Ok(None);
2768+
return Ok(Vec::new());
27702769
}
27712770

27722771
let rendered_msg = match mimefactory.render(context).await {
@@ -2826,27 +2825,32 @@ pub(crate) async fn create_send_msg_job(
28262825
msg.update_param(context).await?;
28272826
}
28282827

2829-
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
2830-
2831-
let recipients = recipients.join(" ");
2832-
28332828
msg.subject = rendered_msg.subject.clone();
28342829
msg.update_subject(context).await?;
2835-
2836-
let row_id = context
2837-
.sql
2838-
.insert(
2839-
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
2840-
VALUES (?1, ?2, ?3, ?4)",
2841-
(
2842-
&rendered_msg.rfc724_mid,
2843-
recipients,
2844-
&rendered_msg.message,
2845-
msg.id,
2846-
),
2847-
)
2848-
.await?;
2849-
Ok(Some(row_id))
2830+
let chunk_size = context
2831+
.get_configured_provider()
2832+
.await?
2833+
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
2834+
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
2835+
let trans_fn = |t: &mut rusqlite::Transaction| {
2836+
let mut row_ids = Vec::<i64>::new();
2837+
for recipients_chunk in recipients.chunks(chunk_size) {
2838+
let recipients_chunk = recipients_chunk.join(" ");
2839+
let row_id = t.execute(
2840+
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
2841+
VALUES (?1, ?2, ?3, ?4)",
2842+
(
2843+
&rendered_msg.rfc724_mid,
2844+
recipients_chunk,
2845+
&rendered_msg.message,
2846+
msg.id,
2847+
),
2848+
)?;
2849+
row_ids.push(row_id.try_into()?);
2850+
}
2851+
Ok(row_ids)
2852+
};
2853+
context.sql.transaction(trans_fn).await
28502854
}
28512855

28522856
/// Sends a text message to the given chat.
@@ -4002,7 +4006,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
40024006
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
40034007
.await?;
40044008
curr_timestamp += 1;
4005-
if create_send_msg_job(context, &mut msg).await?.is_some() {
4009+
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
40064010
context.scheduler.interrupt_smtp().await;
40074011
}
40084012
}
@@ -4059,7 +4063,7 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
40594063
chat_id: msg.chat_id,
40604064
msg_id: msg.id,
40614065
});
4062-
if create_send_msg_job(context, &mut msg).await?.is_some() {
4066+
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
40634067
context.scheduler.interrupt_smtp().await;
40644068
}
40654069
}

src/constants.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
209209
// this value can be increased if the folder configuration is changed and must be redone on next program start
210210
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
211211

212+
// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
213+
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting
214+
// `max_smtp_rcpt_to` in the provider db.
215+
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
216+
212217
#[cfg(test)]
213218
mod tests {
214219
use num_traits::FromPrimitive;

src/message.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1665,7 +1665,10 @@ pub(crate) async fn update_msg_state(
16651665
) -> Result<()> {
16661666
context
16671667
.sql
1668-
.execute("UPDATE msgs SET state=? WHERE id=?;", (state, msg_id))
1668+
.execute(
1669+
"UPDATE msgs SET state=?1 WHERE id=?2 AND (?1!=?3 OR state<?3)",
1670+
(state, msg_id, MessageState::OutDelivered),
1671+
)
16691672
.await?;
16701673
Ok(())
16711674
}

src/receive_imf.rs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,36 @@ pub(crate) async fn receive_imf_inner(
193193
);
194194
let incoming = !context.is_self_addr(&mime_parser.from.addr).await?;
195195

196-
// For the case if we missed a successful SMTP response.
197-
if !incoming {
196+
// For the case if we missed a successful SMTP response. Be optimistic that the message is
197+
// delivered also.
198+
let delivered = !incoming && {
199+
let self_addr = context.get_primary_self_addr().await?;
198200
context
199201
.sql
200-
.execute("DELETE FROM smtp WHERE rfc724_mid=?", (rfc724_mid_orig,))
202+
.execute(
203+
"DELETE FROM smtp \
204+
WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))",
205+
(rfc724_mid_orig, &self_addr),
206+
)
201207
.await?;
208+
!context
209+
.sql
210+
.exists(
211+
"SELECT COUNT(*) FROM smtp WHERE rfc724_mid=?",
212+
(rfc724_mid_orig,),
213+
)
214+
.await?
215+
};
216+
217+
async fn on_msg_in_db(
218+
context: &Context,
219+
msg_id: MsgId,
220+
delivered: bool,
221+
) -> Result<Option<ReceivedMsg>> {
222+
if delivered {
223+
msg_id.set_delivered(context).await?;
224+
}
225+
Ok(None)
202226
}
203227

204228
// check, if the mail is already in our database.
@@ -210,7 +234,7 @@ pub(crate) async fn receive_imf_inner(
210234
context,
211235
"Got a partial download and message is already in DB."
212236
);
213-
return Ok(None);
237+
return on_msg_in_db(context, old_msg_id, delivered).await;
214238
}
215239
let msg = Message::load_from_db(context, old_msg_id).await?;
216240
replace_msg_id = Some(old_msg_id);
@@ -232,9 +256,12 @@ pub(crate) async fn receive_imf_inner(
232256
};
233257
replace_chat_id = None;
234258
}
235-
if replace_msg_id.is_some() && replace_chat_id.is_none() {
259+
260+
if replace_chat_id.is_some() {
261+
// Need to update chat id in the db.
262+
} else if let Some(msg_id) = replace_msg_id {
236263
info!(context, "Message is already downloaded.");
237-
return Ok(None);
264+
return on_msg_in_db(context, msg_id, delivered).await;
238265
};
239266

240267
let prevent_rename =

src/smtp.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,13 @@ pub(crate) async fn send_msg_to_smtp(
595595
match status {
596596
SendResult::Retry => Err(format_err!("Retry")),
597597
SendResult::Success => {
598-
msg_id.set_delivered(context).await?;
598+
if !context
599+
.sql
600+
.exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,))
601+
.await?
602+
{
603+
msg_id.set_delivered(context).await?;
604+
}
599605
Ok(())
600606
}
601607
SendResult::Failure(err) => Err(format_err!("{}", err)),

src/smtp/send.rs

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ use crate::events::EventType;
99

1010
pub type Result<T> = std::result::Result<T, Error>;
1111

12-
// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is split to chunks.
13-
// this does not affect MIME'e `To:` header.
14-
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
15-
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
16-
1712
#[derive(Debug, thiserror::Error)]
1813
pub enum Error {
1914
#[error("Envelope error: {}", _0)]
@@ -43,40 +38,30 @@ impl Smtp {
4338
}
4439

4540
let message_len_bytes = message.len();
41+
let recipients_display = recipients
42+
.iter()
43+
.map(|x| x.as_ref())
44+
.collect::<Vec<&str>>()
45+
.join(",");
4646

47-
let chunk_size = context
48-
.get_configured_provider()
49-
.await?
50-
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
51-
.map_or(DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
52-
53-
for recipients_chunk in recipients.chunks(chunk_size) {
54-
let recipients_display = recipients_chunk
55-
.iter()
56-
.map(|x| x.as_ref())
57-
.collect::<Vec<&str>>()
58-
.join(",");
59-
60-
let envelope = Envelope::new(self.from.clone(), recipients_chunk.to_vec())
61-
.map_err(Error::Envelope)?;
62-
let mail = SendableEmail::new(envelope, message);
47+
let envelope =
48+
Envelope::new(self.from.clone(), recipients.to_vec()).map_err(Error::Envelope)?;
49+
let mail = SendableEmail::new(envelope, message);
6350

64-
if let Some(ref mut transport) = self.transport {
65-
transport.send(mail).await.map_err(Error::SmtpSend)?;
51+
if let Some(ref mut transport) = self.transport {
52+
transport.send(mail).await.map_err(Error::SmtpSend)?;
6653

67-
let info_msg = format!(
68-
"Message len={message_len_bytes} was SMTP-sent to {recipients_display}"
69-
);
70-
info!(context, "{info_msg}.");
71-
context.emit_event(EventType::SmtpMessageSent(info_msg));
72-
self.last_success = Some(std::time::SystemTime::now());
73-
} else {
74-
warn!(
75-
context,
76-
"uh? SMTP has no transport, failed to send to {}", recipients_display
77-
);
78-
return Err(Error::NoTransport);
79-
}
54+
let info_msg =
55+
format!("Message len={message_len_bytes} was SMTP-sent to {recipients_display}");
56+
info!(context, "{info_msg}.");
57+
context.emit_event(EventType::SmtpMessageSent(info_msg));
58+
self.last_success = Some(std::time::SystemTime::now());
59+
} else {
60+
warn!(
61+
context,
62+
"uh? SMTP has no transport, failed to send to {}", recipients_display
63+
);
64+
return Err(Error::NoTransport);
8065
}
8166
Ok(())
8267
}

0 commit comments

Comments
 (0)