Merge branch 'master' into flub/send-backup

This commit is contained in:
Floris Bruynooghe
2023-03-02 11:35:17 +01:00
72 changed files with 2256 additions and 1383 deletions

View File

@@ -476,10 +476,13 @@ impl Config {
struct AccountConfig {
/// Unique id.
pub id: u32,
/// Root directory for all data for this account.
///
/// The path is relative to the account manager directory.
pub dir: std::path::PathBuf,
/// Universally unique account identifier.
pub uuid: Uuid,
}

View File

@@ -276,7 +276,7 @@ impl ChatId {
grpname,
grpid,
create_blocked,
create_smeared_timestamp(context).await,
create_smeared_timestamp(context),
create_protected,
param.unwrap_or_default(),
],
@@ -482,7 +482,7 @@ impl ChatId {
self,
&msg_text,
cmd,
create_smeared_timestamp(context).await,
create_smeared_timestamp(context),
None,
None,
None,
@@ -1881,7 +1881,10 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
/// [`Deref`]: std::ops::Deref
#[derive(Debug)]
pub(crate) struct ChatIdBlocked {
/// Chat ID.
pub id: ChatId,
/// Whether the chat is blocked, unblocked or a contact request.
pub blocked: Blocked,
}
@@ -1953,7 +1956,6 @@ impl ChatIdBlocked {
_ => (),
}
let created_timestamp = create_smeared_timestamp(context).await;
let chat_id = context
.sql
.transaction(move |transaction| {
@@ -1966,7 +1968,7 @@ impl ChatIdBlocked {
chat_name,
params.to_string(),
create_blocked as u8,
created_timestamp,
create_smeared_timestamp(context)
],
)?;
let chat_id = ChatId::new(
@@ -2114,7 +2116,7 @@ async fn prepare_msg_common(
context,
msg,
update_msg_id,
create_smeared_timestamp(context).await,
create_smeared_timestamp(context),
)
.await?;
msg.chat_id = chat_id;
@@ -2839,7 +2841,7 @@ pub async fn create_group_chat(
Chattype::Group,
chat_name,
grpid,
create_smeared_timestamp(context).await,
create_smeared_timestamp(context),
],
)
.await?;
@@ -2897,7 +2899,7 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
Chattype::Broadcast,
chat_name,
grpid,
create_smeared_timestamp(context).await,
create_smeared_timestamp(context),
],
)
.await?;
@@ -3358,7 +3360,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
}
curr_timestamp = create_smeared_timestamps(context, msg_ids.len()).await;
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
let ids = context
.sql
.query_map(
@@ -3560,7 +3562,7 @@ pub async fn add_device_msg_with_importance(
msg.try_calc_and_set_dimensions(context).await.ok();
prepare_msg_blob(context, msg).await?;
let timestamp_sent = create_smeared_timestamp(context).await;
let timestamp_sent = create_smeared_timestamp(context);
// makes sure, the added message is the last one,
// even if the date is wrong (useful esp. when warning about bad dates)
@@ -4088,7 +4090,6 @@ mod tests {
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let add1 = alice.pop_sent_msg().await;
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
@@ -4107,29 +4108,18 @@ mod tests {
remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
let remove2 = alice.pop_sent_msg().await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
// Bob receives the add and deletion messages out of order
let bob = TestContext::new_bob().await;
bob.recv_msg(&add1).await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
bob.recv_msg(&add3).await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let bob_chat_id = bob.recv_msg(&add2).await.chat_id;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 4);
bob.recv_msg(&remove2).await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
bob.recv_msg(&remove1).await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
Ok(())

View File

@@ -300,6 +300,9 @@ pub enum Config {
/// See `crate::authres::update_authservid_candidates`.
AuthservIdCandidates,
/// Make all outgoing messages with Autocrypt header "multipart/signed".
SignUnencrypted,
/// Let the core save all events to the database.
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]

View File

@@ -646,10 +646,14 @@ async fn try_smtp_one_param(
}
}
/// Failure to connect and login with email client configuration.
#[derive(Debug, thiserror::Error)]
#[error("Trying {config}…\nError: {msg}")]
pub struct ConfigurationError {
/// Tried configuration description.
config: String,
/// Error message.
msg: String,
}

View File

@@ -190,11 +190,11 @@ pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
/// How many existing messages shall be fetched after configuration.
pub const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
// max. width/height of an avatar
pub const BALANCED_AVATAR_SIZE: u32 = 256;
pub const WORSE_AVATAR_SIZE: u32 = 128;
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256;
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
// max. width/height of images
pub const BALANCED_IMAGE_SIZE: u32 = 1280;

View File

@@ -4,6 +4,7 @@ use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
@@ -27,6 +28,7 @@ use crate::quota::QuotaInfo;
use crate::scheduler::Scheduler;
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
use crate::tools::{duration_to_str, time};
/// Builder for the [`Context`].
@@ -189,7 +191,7 @@ pub struct InnerContext {
/// Blob directory path
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
pub(crate) last_smeared_timestamp: RwLock<i64>,
pub(crate) smeared_timestamp: SmearedTimestamp,
/// The global "ongoing" process state.
///
/// This is a global mutex-like state for operations which should be modal in the
@@ -211,6 +213,12 @@ pub struct InnerContext {
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock<Option<QuotaInfo>>,
/// Set to true if quota update is requested.
pub(crate) quota_update_request: AtomicBool,
/// IMAP UID resync request.
pub(crate) resync_request: AtomicBool,
/// Server ID response if ID capability is supported
/// and the server returned non-NIL on the inbox connection.
/// <https://datatracker.ietf.org/doc/html/rfc2971>
@@ -369,7 +377,7 @@ impl Context {
blobdir,
running_state: RwLock::new(Default::default()),
sql: Sql::new(dbfile),
last_smeared_timestamp: RwLock::new(0),
smeared_timestamp: SmearedTimestamp::new(),
generating_key_mutex: Mutex::new(()),
oauth2_mutex: Mutex::new(()),
wrong_pw_warning_mutex: Mutex::new(()),
@@ -378,6 +386,8 @@ impl Context {
scheduler: RwLock::new(None),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
quota: RwLock::new(None),
quota_update_request: AtomicBool::new(false),
resync_request: AtomicBool::new(false),
server_id: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
@@ -789,6 +799,12 @@ impl Context {
.await?
.unwrap_or_default(),
);
res.insert(
"sign_unencrypted",
self.get_config_int(Config::SignUnencrypted)
.await?
.to_string(),
);
res.insert(
"debug_logging",

View File

@@ -13,7 +13,6 @@ use crate::imap::{Imap, ImapActionResult};
use crate::job::{self, Action, Job, Status};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::param::Params;
use crate::tools::time;
use crate::{job_try, stock_str, EventType};
@@ -86,11 +85,7 @@ impl MsgId {
DownloadState::Available | DownloadState::Failure => {
self.update_download_state(context, DownloadState::InProgress)
.await?;
job::add(
context,
Job::new(Action::DownloadMsg, self.to_u32(), Params::new(), 0),
)
.await?;
job::add(context, Job::new(Action::DownloadMsg, self.to_u32())).await?;
}
}
Ok(())

View File

@@ -124,6 +124,19 @@ impl EncryptHelper {
Ok(ctext)
}
/// Signs the passed-in `mail` using the private key from `context`.
/// Returns the payload and the signature.
pub async fn sign(
self,
context: &Context,
mail: lettre_email::PartBuilder,
) -> Result<(lettre_email::MimeMessage, String)> {
let sign_key = SignedSecretKey::load_self(context).await?;
let mime_message = mail.build();
let signature = pgp::pk_calc_signature(mime_message.as_string().as_bytes(), &sign_key)?;
Ok((mime_message, signature))
}
}
/// Ensures a private key exists for the configured user.

View File

@@ -650,7 +650,7 @@ mod tests {
use crate::download::DownloadState;
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use crate::tools::MAX_SECONDS_TO_LEND_FROM_FUTURE;
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
use crate::{
chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus},
tools::IsNoneOrEmpty,

View File

@@ -116,6 +116,8 @@ impl async_imap::Authenticator for OAuth2 {
#[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
pub enum FolderMeaning {
Unknown,
/// Spam folder.
Spam,
Inbox,
Mvbox,
@@ -149,8 +151,11 @@ impl FolderMeaning {
#[derive(Debug)]
struct ImapConfig {
/// Email address.
pub addr: String,
pub lp: ServerLoginParam,
/// SOCKS 5 configuration.
pub socks5_config: Option<Socks5Config>,
pub strict_tls: bool,
}
@@ -899,6 +904,24 @@ impl Imap {
info!(context, "Done fetching existing messages.");
Ok(())
}
/// Synchronizes UIDs for all folders.
pub(crate) async fn resync_folders(&mut self, context: &Context) -> Result<()> {
self.prepare(context).await?;
let all_folders = self
.list_folders(context)
.await
.context("listing folders for resync")?;
for folder in all_folders {
let folder_meaning = get_folder_meaning(&folder);
if folder_meaning != FolderMeaning::Virtual {
self.resync_folder_uids(context, folder.name(), folder_meaning)
.await?;
}
}
Ok(())
}
}
impl Session {

View File

@@ -11,9 +11,9 @@ use tokio::io::BufWriter;
use super::capabilities::Capabilities;
use super::session::Session;
use crate::context::Context;
use crate::login_param::build_tls;
use crate::net::connect_tcp;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::socks::Socks5Config;
/// IMAP write and read timeout.
@@ -95,8 +95,7 @@ impl Client {
strict_tls: bool,
) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
let tls = build_tls(strict_tls);
let tls_stream = tls.connect(hostname, tcp_stream).await?;
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = ImapClient::new(session_stream);
@@ -142,9 +141,7 @@ impl Client {
.context("STARTTLS command failed")?;
let tcp_stream = client.into_inner();
let tls = build_tls(strict_tls);
let tls_stream = tls
.connect(hostname, tcp_stream)
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
@@ -165,8 +162,7 @@ impl Client {
let socks5_stream = socks5_config
.connect(context, domain, port, IMAP_TIMEOUT, strict_tls)
.await?;
let tls = build_tls(strict_tls);
let tls_stream = tls.connect(domain, socks5_stream).await?;
let tls_stream = wrap_tls(strict_tls, domain, socks5_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = ImapClient::new(session_stream);
@@ -221,9 +217,7 @@ impl Client {
.context("STARTTLS command failed")?;
let socks5_stream = client.into_inner();
let tls = build_tls(strict_tls);
let tls_stream = tls
.connect(hostname, socks5_stream)
let tls_stream = wrap_tls(strict_tls, hostname, socks5_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);

View File

@@ -1,3 +1,5 @@
//! # IMAP folder selection module.
use anyhow::Context as _;
use super::session::Session as ImapSession;

View File

@@ -760,7 +760,7 @@ async fn export_database(context: &Context, dest: &Path, passphrase: String) ->
sql::housekeeping(context).await.ok_or_log(context);
context
.sql
.call(|conn| {
.call_write(|conn| {
conn.execute("VACUUM;", params![])
.map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
.ok();

View File

@@ -6,14 +6,14 @@
#![allow(missing_docs)]
use std::fmt;
use std::sync::atomic::Ordering;
use anyhow::{Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use rand::{thread_rng, Rng};
use crate::context::Context;
use crate::imap::{get_folder_meaning, FolderMeaning, Imap};
use crate::param::Params;
use crate::imap::Imap;
use crate::scheduler::InterruptInfo;
use crate::tools::time;
@@ -25,7 +25,6 @@ const JOB_RETRIES: u32 = 17;
pub enum Status {
Finished(Result<()>),
RetryNow,
RetryLater,
}
#[macro_export]
@@ -58,18 +57,11 @@ macro_rules! job_try {
)]
#[repr(u32)]
pub enum Action {
// this is user initiated so it should have a fairly high priority
UpdateRecentQuota = 140,
// This job will download partially downloaded messages completely
// and is added when download_full() is called.
// Most messages are downloaded automatically on fetch
// and do not go through this job.
DownloadMsg = 250,
// UID synchronization is high-priority to make sure correct UIDs
// are used by message moving/deletion.
ResyncFolders = 300,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -80,7 +72,6 @@ pub struct Job {
pub desired_timestamp: i64,
pub added_timestamp: i64,
pub tries: u32,
pub param: Params,
}
impl fmt::Display for Job {
@@ -90,24 +81,19 @@ impl fmt::Display for Job {
}
impl Job {
pub fn new(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Self {
pub fn new(action: Action, foreign_id: u32) -> Self {
let timestamp = time();
Self {
job_id: 0,
action,
foreign_id,
desired_timestamp: timestamp + delay_seconds,
desired_timestamp: timestamp,
added_timestamp: timestamp,
tries: 0,
param,
}
}
pub fn delay_seconds(&self) -> i64 {
self.desired_timestamp - self.added_timestamp
}
/// Deletes the job from the database.
async fn delete(self, context: &Context) -> Result<()> {
if self.job_id != 0 {
@@ -130,23 +116,21 @@ impl Job {
context
.sql
.execute(
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
"UPDATE jobs SET desired_timestamp=?, tries=? WHERE id=?;",
paramsv![
self.desired_timestamp,
i64::from(self.tries),
self.param.to_string(),
self.job_id as i32,
],
)
.await?;
} else {
context.sql.execute(
"INSERT INTO jobs (added_timestamp, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?);",
"INSERT INTO jobs (added_timestamp, action, foreign_id, desired_timestamp) VALUES (?,?,?,?);",
paramsv![
self.added_timestamp,
self.action,
self.foreign_id,
self.param.to_string(),
self.desired_timestamp
]
).await?;
@@ -154,63 +138,6 @@ impl Job {
Ok(())
}
/// Synchronizes UIDs for all folders.
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!(context, "could not connect: {:#}", err);
return Status::RetryLater;
}
let all_folders = match imap.list_folders(context).await {
Ok(v) => v,
Err(e) => {
warn!(context, "Listing folders for resync failed: {:#}", e);
return Status::RetryLater;
}
};
let mut any_failed = false;
for folder in all_folders {
let folder_meaning = get_folder_meaning(&folder);
if folder_meaning == FolderMeaning::Virtual {
continue;
}
if let Err(e) = imap
.resync_folder_uids(context, folder.name(), folder_meaning)
.await
{
warn!(context, "{:#}", e);
any_failed = true;
}
}
if any_failed {
Status::RetryLater
} else {
Status::Finished(Ok(()))
}
}
}
/// Delete all pending jobs with the given action.
pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
context
.sql
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
.await?;
Ok(())
}
pub async fn action_exists(context: &Context, action: Action) -> Result<bool> {
let exists = context
.sql
.exists(
"SELECT COUNT(*) FROM jobs WHERE action=?;",
paramsv![action],
)
.await?;
Ok(exists)
}
pub(crate) enum Connection<'a> {
@@ -234,13 +161,13 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
};
match try_res {
Status::RetryNow | Status::RetryLater => {
Status::RetryNow => {
let tries = job.tries + 1;
if tries < JOB_RETRIES {
info!(context, "increase job {} tries to {}", job, tries);
job.tries = tries;
let time_offset = get_backoff_time_offset(tries, job.action);
let time_offset = get_backoff_time_offset(tries);
job.desired_timestamp = time() + time_offset;
info!(
context,
@@ -288,11 +215,6 @@ async fn perform_job_action(
info!(context, "begin immediate try {} of job {}", tries, job);
let try_res = match job.action {
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
Action::UpdateRecentQuota => match context.update_recent_quota(connection.inbox()).await {
Ok(status) => status,
Err(err) => Status::Finished(Err(err)),
},
Action::DownloadMsg => job.download_msg(context, connection.inbox()).await,
};
@@ -301,50 +223,34 @@ async fn perform_job_action(
try_res
}
fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
match action {
// Just try every 10s to update the quota
// If all retries are exhausted, a new job will be created when the quota information is needed
Action::UpdateRecentQuota => 10,
_ => {
// Exponential backoff
let n = 2_i32.pow(tries - 1) * 60;
let mut rng = thread_rng();
let r: i32 = rng.gen();
let mut seconds = r % (n + 1);
if seconds < 1 {
seconds = 1;
}
i64::from(seconds)
}
fn get_backoff_time_offset(tries: u32) -> i64 {
// Exponential backoff
let n = 2_i32.pow(tries - 1) * 60;
let mut rng = thread_rng();
let r: i32 = rng.gen();
let mut seconds = r % (n + 1);
if seconds < 1 {
seconds = 1;
}
i64::from(seconds)
}
pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
kill_action(context, Action::ResyncFolders).await?;
add(
context,
Job::new(Action::ResyncFolders, 0, Params::new(), 0),
)
.await?;
context.resync_request.store(true, Ordering::Relaxed);
context
.interrupt_inbox(InterruptInfo {
probe_network: false,
})
.await;
Ok(())
}
/// Adds a job to the database, scheduling it.
pub async fn add(context: &Context, job: Job) -> Result<()> {
let action = job.action;
let delay_seconds = job.delay_seconds();
job.save(context).await.context("failed to save job")?;
if delay_seconds == 0 {
match action {
Action::ResyncFolders | Action::UpdateRecentQuota | Action::DownloadMsg => {
info!(context, "interrupt: imap");
context.interrupt_inbox(InterruptInfo::new(false)).await;
}
}
}
info!(context, "interrupt: imap");
context.interrupt_inbox(InterruptInfo::new(false)).await;
Ok(())
}
@@ -396,7 +302,6 @@ LIMIT 1;
desired_timestamp: row.get("desired_timestamp")?,
added_timestamp: row.get("added_timestamp")?,
tries: row.get("tries")?,
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
};
Ok(job)
@@ -436,8 +341,8 @@ mod tests {
.sql
.execute(
"INSERT INTO jobs
(added_timestamp, action, foreign_id, param, desired_timestamp)
VALUES (?, ?, ?, ?, ?);",
(added_timestamp, action, foreign_id, desired_timestamp)
VALUES (?, ?, ?, ?);",
paramsv![
now,
if valid {
@@ -446,7 +351,6 @@ mod tests {
-1
},
foreign_id,
Params::new().to_string(),
now
],
)

View File

@@ -92,6 +92,7 @@ mod smtp;
mod socks;
pub mod stock_str;
mod sync;
mod timesmearing;
mod token;
mod update_helper;
pub mod webxdc;

View File

@@ -603,7 +603,7 @@ pub(crate) async fn save(
context
.sql
.call(|conn| {
.call_write(|conn| {
let mut stmt_test = conn
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
let mut stmt_insert = conn.prepare_cached(stmt_insert)?;

View File

@@ -3,8 +3,6 @@
use std::fmt;
use anyhow::{ensure, Result};
use async_native_tls::Certificate;
use once_cell::sync::Lazy;
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::provider::{get_provider_by_id, Provider};
@@ -306,28 +304,6 @@ fn unset_empty(s: &str) -> &str {
}
}
// this certificate is missing on older android devices (eg. lg with android6 from 2017)
// certificate downloaded from https://letsencrypt.org/certificates/
static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
Certificate::from_der(include_bytes!(
"../assets/root-certificates/letsencrypt/isrgrootx1.der"
))
.unwrap()
});
pub fn build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
let tls_builder =
async_native_tls::TlsConnector::new().add_root_certificate(LETSENCRYPT_ROOT.clone());
if strict_tls {
tls_builder
} else {
tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -378,13 +354,4 @@ mod tests {
assert_eq!(param, loaded);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_build_tls() -> Result<()> {
// we are using some additional root certificates.
// make sure, they do not break construction of TlsConnector
let _ = build_tls(true);
let _ = build_tls(false);
Ok(())
}
}

View File

@@ -1771,13 +1771,8 @@ async fn ndn_maybe_add_info_msg(
// Tell the user which of the recipients failed if we know that (because in
// a group, this might otherwise be unclear)
let text = stock_str::failed_sending_to(context, contact.get_display_name()).await;
chat::add_info_msg(
context,
chat_id,
&text,
create_smeared_timestamp(context).await,
)
.await?;
chat::add_info_msg(context, chat_id, &text, create_smeared_timestamp(context))
.await?;
context.emit_event(EventType::ChatModified(chat_id));
}
}

View File

@@ -250,7 +250,7 @@ impl<'a> MimeFactory<'a> {
.get_config(Config::Selfstatus)
.await?
.unwrap_or_default();
let timestamp = create_smeared_timestamp(context).await;
let timestamp = create_smeared_timestamp(context);
let res = MimeFactory::<'a> {
from_addr,
@@ -779,10 +779,36 @@ impl<'a> MimeFactory<'a> {
};
// Store protected headers in the outer message.
headers
let message = headers
.protected
.into_iter()
.fold(message, |message, header| message.header(header))
.fold(message, |message, header| message.header(header));
if self.should_skip_autocrypt()
|| !context.get_config_bool(Config::SignUnencrypted).await?
{
message
} else {
let (payload, signature) = encrypt_helper.sign(context, message).await?;
PartBuilder::new()
.header((
"Content-Type".to_string(),
"multipart/signed; protocol=\"application/pgp-signature\"".to_string(),
))
.child(payload)
.child(
PartBuilder::new()
.content_type(
&"application/pgp-signature; name=\"signature.asc\""
.parse::<mime::Mime>()
.unwrap(),
)
.header(("Content-Description", "OpenPGP digital signature"))
.header(("Content-Disposition", "attachment; filename=\"signature\";"))
.body(signature)
.build(),
)
}
};
// Store the unprotected headers on the outer message.
@@ -2140,6 +2166,96 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_unencrypted_signed() {
// create chat with bob, set selfavatar
let t = TestContext::new_alice().await;
t.set_config(Config::SignUnencrypted, Some("1"))
.await
.unwrap();
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
let file = t.dir.path().join("avatar.png");
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await.unwrap();
t.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await
.unwrap();
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("this is the text!".to_string()));
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
let part = payload.next().unwrap();
assert_eq!(part.match_indices("multipart/signed").count(), 1);
assert_eq!(part.match_indices("Subject:").count(), 0);
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(part.match_indices("multipart/mixed").count(), 1);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 0);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(part.match_indices("text/plain").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1);
assert_eq!(part.match_indices("Subject:").count(), 0);
let body = payload.next().unwrap();
assert_eq!(body.match_indices("this is the text!").count(), 1);
let bob = TestContext::new_bob().await;
bob.recv_msg(&sent_msg).await;
let alice_id = Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.unwrap()
.unwrap();
let alice_contact = Contact::load_from_db(&bob.ctx, alice_id).await.unwrap();
assert!(alice_contact
.get_profile_image(&bob.ctx)
.await
.unwrap()
.is_some());
// if another message is sent, that one must not contain the avatar
// and no artificial multipart/mixed nesting
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
let part = payload.next().unwrap();
assert_eq!(part.match_indices("multipart/signed").count(), 1);
assert_eq!(part.match_indices("Subject:").count(), 0);
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(part.match_indices("text/plain").count(), 1);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 0);
assert_eq!(part.match_indices("multipart/mixed").count(), 0);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let body = payload.next().unwrap();
assert_eq!(body.match_indices("this is the text!").count(), 1);
assert_eq!(body.match_indices("text/plain").count(), 0);
assert_eq!(body.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(body.match_indices("Subject:").count(), 0);
bob.recv_msg(&sent_msg).await;
let alice_contact = Contact::load_from_db(&bob.ctx, alice_id).await.unwrap();
assert!(alice_contact
.get_profile_image(&bob.ctx)
.await
.unwrap()
.is_some());
}
/// Test that removed member address does not go into the `To:` field.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_remove_member_bcc() -> Result<()> {

View File

@@ -224,8 +224,32 @@ impl MimeMessage {
// Parse hidden headers.
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
let (part, mimetype) =
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" {
if let Some(part) = mail.subparts.first() {
// We don't remove "subject" from `headers` because currently just signed
// messages are shown as unencrypted anyway.
MimeMessage::merge_headers(
context,
&mut headers,
&mut recipients,
&mut from,
&mut list_post,
&mut chat_disposition_notification_to,
&part.headers,
);
(part, part.ctype.mimetype.parse::<Mime>()?)
} else {
// If it's a partially fetched message, there are no subparts.
(&mail, mimetype)
}
} else {
// Currently we do not sign unencrypted messages by default.
(&mail, mimetype)
};
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" {
if let Some(part) = mail.subparts.first() {
if let Some(part) = part.subparts.first() {
for field in &part.headers {
let key = field.get_key().to_lowercase();
@@ -256,26 +280,27 @@ impl MimeMessage {
hop_info += &decryption_info.dkim_results.to_string();
let public_keyring = keyring_from_peerstate(decryption_info.peerstate.as_ref());
let (mail, mut signatures, encrypted) =
match try_decrypt(context, &mail, &private_keyring, &public_keyring) {
Ok(Some((raw, signatures))) => {
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}
(Ok(decrypted_mail), signatures, true)
let (mail, mut signatures, encrypted) = match tokio::task::block_in_place(|| {
try_decrypt(context, &mail, &private_keyring, &public_keyring)
}) {
Ok(Some((raw, signatures))) => {
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}
Ok(None) => (Ok(mail), HashSet::new(), false),
Err(err) => {
warn!(context, "decryption failed: {:#}", err);
(Err(err), HashSet::new(), false)
}
};
(Ok(decrypted_mail), signatures, true)
}
Ok(None) => (Ok(mail), HashSet::new(), false),
Err(err) => {
warn!(context, "decryption failed: {:#}", err);
(Err(err), HashSet::new(), false)
}
};
let mail = mail.as_ref().map(|mail| {
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
.unwrap_or((mail, Default::default()));

View File

@@ -13,6 +13,7 @@ use crate::context::Context;
use crate::tools::time;
pub(crate) mod session;
pub(crate) mod tls;
async fn connect_tcp_inner(addr: SocketAddr, timeout_val: Duration) -> Result<TcpStream> {
let tcp_stream = timeout(timeout_val, TcpStream::connect(addr))

52
src/net/tls.rs Normal file
View File

@@ -0,0 +1,52 @@
//! TLS support.
use anyhow::Result;
use async_native_tls::{Certificate, Protocol, TlsConnector, TlsStream};
use once_cell::sync::Lazy;
use tokio::io::{AsyncRead, AsyncWrite};
// this certificate is missing on older android devices (eg. lg with android6 from 2017)
// certificate downloaded from https://letsencrypt.org/certificates/
static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
Certificate::from_der(include_bytes!(
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
))
.unwrap()
});
pub fn build_tls(strict_tls: bool) -> TlsConnector {
let tls_builder = TlsConnector::new()
.min_protocol_version(Some(Protocol::Tlsv12))
.add_root_certificate(LETSENCRYPT_ROOT.clone());
if strict_tls {
tls_builder
} else {
tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
}
}
pub async fn wrap_tls<T: AsyncRead + AsyncWrite + Unpin>(
strict_tls: bool,
hostname: &str,
stream: T,
) -> Result<TlsStream<T>> {
let tls = build_tls(strict_tls);
let tls_stream = tls.connect(hostname, stream).await?;
Ok(tls_stream)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_tls() {
// we are using some additional root certificates.
// make sure, they do not break construction of TlsConnector
let _ = build_tls(true);
let _ = build_tls(false);
}
}

View File

@@ -262,6 +262,20 @@ pub async fn pk_encrypt(
.await?
}
/// Signs `plain` text using `private_key_for_signing`.
pub fn pk_calc_signature(
plain: &[u8],
private_key_for_signing: &SignedSecretKey,
) -> Result<String> {
let msg = Message::new_literal_bytes("", plain).sign(
private_key_for_signing,
|| "".into(),
Default::default(),
)?;
let signature = msg.into_signature().to_armored_string(None)?;
Ok(signature)
}
/// Decrypts the message with keys from the private key keyring.
///
/// Receiver private keys are provided in

View File

@@ -1952,4 +1952,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 2, 21).unwrap());
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 2, 20).unwrap());

View File

@@ -1,208 +0,0 @@
#!/usr/bin/env python3
# if the yaml import fails, run "pip install pyyaml"
import sys
import yaml
import datetime
from pathlib import Path
out_all = ""
out_domains = ""
out_ids = ""
domains_set = set()
def camel(name):
words = name.split("_")
return "".join(w.capitalize() for i, w in enumerate(words))
def cleanstr(s):
s = s.strip()
s = s.replace("\n", " ")
s = s.replace("\\", "\\\\")
s = s.replace("\"", "\\\"")
return s
def file2id(f):
return f.stem
def file2varname(f):
f = file2id(f)
f = f.replace(".", "_")
f = f.replace("-", "_")
return "P_" + f.upper()
def file2url(f):
f = file2id(f)
f = f.replace(".", "-")
return "https://providers.delta.chat/" + f
def process_opt(data):
if not "opt" in data:
return "Default::default()"
opt = "ProviderOptions {\n"
opt_data = data.get("opt", "")
for key in opt_data:
value = str(opt_data[key])
if key == "max_smtp_rcpt_to":
value = "Some(" + value + ")"
if value in {"True", "False"}:
value = value.lower()
opt += " " + key + ": " + value + ",\n"
opt += " ..Default::default()\n"
opt += " }"
return opt
def process_config_defaults(data):
if not "config_defaults" in data:
return "None"
defaults = "Some(vec![\n"
config_defaults = data.get("config_defaults", "")
for key in config_defaults:
value = str(config_defaults[key])
defaults += " ConfigDefault { key: Config::" + camel(key) + ", value: \"" + value + "\" },\n"
defaults += " ])"
return defaults
def process_data(data, file):
status = data.get("status", "")
if status != "OK" and status != "PREPARATION" and status != "BROKEN":
raise TypeError("bad status")
comment = ""
domains = ""
if not "domains" in data:
raise TypeError("no domains found")
for domain in data["domains"]:
domain = cleanstr(domain)
if domain == "" or domain.lower() != domain:
raise TypeError("bad domain: " + domain)
global domains_set
if domain in domains_set:
raise TypeError("domain used twice: " + domain)
domains_set.add(domain)
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
comment += domain + ", "
ids = ""
ids += " (\"" + file2id(file) + "\", &*" + file2varname(file) + "),\n"
server = ""
has_imap = False
has_smtp = False
if "server" in data:
for s in data["server"]:
hostname = cleanstr(s.get("hostname", ""))
port = int(s.get("port", ""))
if hostname == "" or hostname.lower() != hostname or port <= 0:
raise TypeError("bad hostname or port")
protocol = s.get("type", "").upper()
if protocol == "IMAP":
has_imap = True
elif protocol == "SMTP":
has_smtp = True
else:
raise TypeError("bad protocol")
socket = s.get("socket", "").upper()
if socket != "STARTTLS" and socket != "SSL" and socket != "PLAIN":
raise TypeError("bad socket")
username_pattern = s.get("username_pattern", "EMAIL").upper()
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
raise TypeError("bad username pattern")
server += (" Server { protocol: " + protocol.capitalize() + ", socket: " + socket.capitalize() + ", hostname: \""
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern.capitalize() + " },\n")
opt = process_opt(data)
config_defaults = process_config_defaults(data)
oauth2 = data.get("oauth2", "")
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
provider = ""
before_login_hint = cleanstr(data.get("before_login_hint", ""))
after_login_hint = cleanstr(data.get("after_login_hint", ""))
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
provider += "static " + file2varname(file) + ": Lazy<Provider> = Lazy::new(|| Provider {\n"
provider += " id: \"" + file2id(file) + "\",\n"
provider += " status: Status::" + status.capitalize() + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " opt: " + opt + ",\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += "});\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")
if status != "OK" and before_login_hint == "":
raise TypeError("status PREPARATION or BROKEN requires before_login_hint: " + file)
# finally, add the provider
global out_all, out_domains, out_ids
out_all += "// " + file.name + ": " + comment.strip(", ") + "\n"
# also add provider with no special things to do -
# eg. _not_ supporting oauth2 is also an information and we can skip the mx-lookup in this case
out_all += provider
out_domains += domains
out_ids += ids
def process_file(file):
print("processing file: {}".format(file), file=sys.stderr)
with open(file) as f:
# load_all() loads "---"-separated yamls -
# by coincidence, this is also the frontmatter separator :)
data = next(yaml.load_all(f, Loader=yaml.SafeLoader))
process_data(data, file)
def process_dir(dir):
print("processing directory: {}".format(dir), file=sys.stderr)
files = sorted(f for f in dir.iterdir() if f.suffix == '.md')
for f in files:
process_file(f)
if __name__ == "__main__":
if len(sys.argv) < 2:
raise SystemExit("usage: update.py DIR_WITH_MD_FILES > data.rs")
out_all = ("// file generated by src/provider/update.py\n\n"
"use crate::provider::Protocol::*;\n"
"use crate::provider::Socket::*;\n"
"use crate::provider::UsernamePattern::*;\n"
"use crate::provider::{\n"
" Config, ConfigDefault, Oauth2Authorizer, Provider, ProviderOptions, Server, Status,\n"
"};\n"
"use std::collections::HashMap;\n\n"
"use once_cell::sync::Lazy;\n\n")
process_dir(Path(sys.argv[1]))
out_all += "pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| [\n"
out_all += out_domains;
out_all += "].iter().copied().collect());\n\n"
out_all += "pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| [\n"
out_all += out_ids;
out_all += "].iter().copied().collect());\n\n"
now = datetime.datetime.utcnow()
out_all += "pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "\
"Lazy::new(|| chrono::NaiveDate::from_ymd_opt("+str(now.year)+", "+str(now.month)+", "+str(now.day)+").unwrap());\n"
print(out_all)

View File

@@ -515,11 +515,15 @@ fn decode_backup(qr: &str) -> Result<Qr> {
#[derive(Debug, Deserialize)]
struct CreateAccountSuccessResponse {
/// Email address.
email: String,
/// Password.
password: String,
}
#[derive(Debug, Deserialize)]
struct CreateAccountErrorResponse {
/// Reason for the failure to create account returned by the server.
reason: String,
}

View File

@@ -1,6 +1,7 @@
//! # Support for IMAP QUOTA extension.
use std::collections::BTreeMap;
use std::sync::atomic::Ordering;
use anyhow::{anyhow, Context as _, Result};
use async_imap::types::{Quota, QuotaResource};
@@ -11,11 +12,10 @@ use crate::context::Context;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::session::Session as ImapSession;
use crate::imap::Imap;
use crate::job::{Action, Status};
use crate::message::{Message, Viewtype};
use crate::param::Params;
use crate::scheduler::InterruptInfo;
use crate::tools::time;
use crate::{job, stock_str, EventType};
use crate::{stock_str, EventType};
/// warn about a nearly full mailbox after this usage percentage is reached.
/// quota icon is "yellow".
@@ -112,12 +112,10 @@ pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> b
impl Context {
// Adds a job to update `quota.recent`
pub(crate) async fn schedule_quota_update(&self) -> Result<()> {
if !job::action_exists(self, Action::UpdateRecentQuota).await? {
job::add(
self,
job::Job::new(Action::UpdateRecentQuota, 0, Params::new(), 0),
)
.await?;
let requested = self.quota_update_request.swap(true, Ordering::Relaxed);
if !requested {
// Quota update was not requested before.
self.interrupt_inbox(InterruptInfo::new(false)).await;
}
Ok(())
}
@@ -132,10 +130,10 @@ impl Context {
/// and new space is allocated as needed.
///
/// Called in response to `Action::UpdateRecentQuota`.
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Result<Status> {
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Result<()> {
if let Err(err) = imap.prepare(self).await {
warn!(self, "could not connect: {:#}", err);
return Ok(Status::RetryNow);
return Ok(());
}
let session = imap.session.as_mut().context("no session")?;
@@ -166,13 +164,16 @@ impl Context {
}
}
// Clear the request to update quota.
self.quota_update_request.store(false, Ordering::Relaxed);
*self.quota.write().await = Some(QuotaInfo {
recent: quota,
modified: time(),
});
self.emit_event(EventType::ConnectivityChanged);
Ok(Status::Finished(Ok(())))
Ok(())
}
}

View File

@@ -203,7 +203,7 @@ pub(crate) async fn receive_imf_inner(
)
.await?;
let rcvd_timestamp = smeared_time(context).await;
let rcvd_timestamp = smeared_time(context);
// Sender timestamp is allowed to be a bit in the future due to
// unsynchronized clocks, but not too much.
@@ -1149,7 +1149,10 @@ async fn add_parts(
// also change `MsgId::trash()` and `delete_expired_messages()`
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
let row_id = context.sql.insert(
let row_id = context
.sql
.call_write(|conn| {
let mut stmt = conn.prepare_cached(
r#"
INSERT INTO msgs
(
@@ -1179,47 +1182,51 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
bytes=excluded.bytes, mime_headers=excluded.mime_headers, mime_in_reply_to=excluded.mime_in_reply_to,
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
"#,
paramsv![
replace_msg_id,
rfc724_mid,
if trash { DC_CHAT_ID_TRASH } else { chat_id },
if trash { ContactId::UNDEFINED } else { from_id },
if trash { ContactId::UNDEFINED } else { to_id },
sort_timestamp,
sent_timestamp,
rcvd_timestamp,
typ,
state,
is_dc_message,
if trash { "" } else { msg },
if trash { "" } else { &subject },
// txt_raw might contain invalid utf8
if trash { "" } else { &txt_raw },
if trash {
"".to_string()
} else {
param.to_string()
},
part.bytes as isize,
if (save_mime_headers || mime_modified) && !trash {
mime_headers.clone()
} else {
Vec::new()
},
mime_in_reply_to,
mime_references,
mime_modified,
part.error.as_deref().unwrap_or_default(),
ephemeral_timer,
ephemeral_timestamp,
if is_partial_download.is_some() {
DownloadState::Available
} else {
DownloadState::Done
},
mime_parser.hop_info
]).await?;
"#)?;
stmt.execute(params![
replace_msg_id,
rfc724_mid,
if trash { DC_CHAT_ID_TRASH } else { chat_id },
if trash { ContactId::UNDEFINED } else { from_id },
if trash { ContactId::UNDEFINED } else { to_id },
sort_timestamp,
sent_timestamp,
rcvd_timestamp,
typ,
state,
is_dc_message,
if trash { "" } else { msg },
if trash { "" } else { &subject },
// txt_raw might contain invalid utf8
if trash { "" } else { &txt_raw },
if trash {
"".to_string()
} else {
param.to_string()
},
part.bytes as isize,
if (save_mime_headers || mime_modified) && !trash {
mime_headers.clone()
} else {
Vec::new()
},
mime_in_reply_to,
mime_references,
mime_modified,
part.error.as_deref().unwrap_or_default(),
ephemeral_timer,
ephemeral_timestamp,
if is_partial_download.is_some() {
DownloadState::Available
} else {
DownloadState::Done
},
mime_parser.hop_info
])?;
let row_id = conn.last_insert_rowid();
Ok(row_id)
})
.await?;
// We only replace placeholder with a first part,
// afterwards insert additional parts.
@@ -1373,7 +1380,7 @@ async fn calc_sort_timestamp(
}
}
Ok(min(sort_timestamp, smeared_time(context).await))
Ok(min(sort_timestamp, smeared_time(context)))
}
async fn lookup_chat_by_reply(

View File

@@ -2133,6 +2133,76 @@ Original signature updated",
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ignore_old_status_updates() -> Result<()> {
let t = TestContext::new_alice().await;
let bob_id = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("bob@example.net")?,
Origin::AddressBook,
)
.await?
.0;
receive_imf(
&t,
b"From: Bob <bob@example.net>
To: Alice <alice@example.org>
Message-ID: <2@example.org>
Date: Wed, 22 Feb 2023 20:00:00 +0000
body
--
sig wednesday",
false,
)
.await?;
let chat_id = t.get_last_msg().await.chat_id;
let bob = Contact::load_from_db(&t, bob_id).await?;
assert_eq!(bob.get_status(), "sig wednesday");
assert_eq!(get_chat_msgs(&t, chat_id).await?.len(), 1);
receive_imf(
&t,
b"From: Bob <bob@example.net>
To: Alice <alice@example.org>
Message-ID: <1@example.org>
Date: Tue, 21 Feb 2023 20:00:00 +0000
body
--
sig tuesday",
false,
)
.await?;
let bob = Contact::load_from_db(&t, bob_id).await?;
assert_eq!(bob.get_status(), "sig wednesday");
assert_eq!(get_chat_msgs(&t, chat_id).await?.len(), 2);
receive_imf(
&t,
b"From: Bob <bob@example.net>
To: Alice <alice@example.org>
Message-ID: <3@example.org>
Date: Thu, 23 Feb 2023 20:00:00 +0000
body
--
sig thursday",
false,
)
.await?;
let bob = Contact::load_from_db(&t, bob_id).await?;
assert_eq!(bob.get_status(), "sig thursday");
assert_eq!(get_chat_msgs(&t, chat_id).await?.len(), 3);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_assignment_private_classical_reply() {
for outgoing_is_classical in &[true, false] {

View File

@@ -1,4 +1,5 @@
use std::iter::{self, once};
use std::sync::atomic::Ordering;
use anyhow::{bail, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
@@ -128,6 +129,21 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
info = Default::default();
}
None => {
let quota_requested = ctx.quota_update_request.swap(false, Ordering::Relaxed);
if quota_requested {
if let Err(err) = ctx.update_recent_quota(&mut connection).await {
warn!(ctx, "Failed to update quota: {:#}.", err);
}
}
let resync_requested = ctx.resync_request.swap(false, Ordering::Relaxed);
if resync_requested {
if let Err(err) = connection.resync_folders(&ctx).await {
warn!(ctx, "Failed to resync folders: {:#}.", err);
ctx.resync_request.store(true, Ordering::Relaxed);
}
}
maybe_add_time_based_warnings(&ctx).await;
match ctx.get_config_i64(Config::LastHousekeeping).await {

View File

@@ -13,12 +13,13 @@ use tokio::task;
use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::events::EventType;
use crate::login_param::{build_tls, CertificateChecks, LoginParam, ServerLoginParam};
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::Message;
use crate::message::{self, MsgId};
use crate::mimefactory::MimeFactory;
use crate::net::connect_tcp;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::oauth2::get_oauth2_access_token;
use crate::provider::Socket;
use crate::socks::Socks5Config;
@@ -119,8 +120,7 @@ impl Smtp {
let socks5_stream = socks5_config
.connect(context, hostname, port, SMTP_TIMEOUT, strict_tls)
.await?;
let tls = build_tls(strict_tls);
let tls_stream = tls.connect(hostname, socks5_stream).await?;
let tls_stream = wrap_tls(strict_tls, hostname, socks5_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = smtp::SmtpClient::new().smtp_utf8(true);
@@ -144,9 +144,7 @@ impl Smtp {
let client = smtp::SmtpClient::new().smtp_utf8(true);
let transport = SmtpTransport::new(client, socks5_stream).await?;
let tcp_stream = transport.starttls().await?;
let tls = build_tls(strict_tls);
let tls_stream = tls
.connect(hostname, tcp_stream)
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
@@ -181,8 +179,7 @@ impl Smtp {
strict_tls: bool,
) -> Result<SmtpTransport<Box<dyn SessionStream>>> {
let tcp_stream = connect_tcp(context, hostname, port, SMTP_TIMEOUT, false).await?;
let tls = build_tls(strict_tls);
let tls_stream = tls.connect(hostname, tcp_stream).await?;
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = smtp::SmtpClient::new().smtp_utf8(true);
@@ -203,9 +200,7 @@ impl Smtp {
let client = smtp::SmtpClient::new().smtp_utf8(true);
let transport = SmtpTransport::new(client, tcp_stream).await?;
let tcp_stream = transport.starttls().await?;
let tls = build_tls(strict_tls);
let tls_stream = tls
.connect(hostname, tcp_stream)
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);

View File

@@ -5,8 +5,8 @@ use std::convert::TryFrom;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context as _, Result};
use rusqlite::{self, config::DbConfig, Connection, OpenFlags, TransactionBehavior};
use tokio::sync::RwLock;
use rusqlite::{self, config::DbConfig, Connection, OpenFlags};
use tokio::sync::{Mutex, MutexGuard, RwLock};
use crate::blob::BlobObject;
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
@@ -56,6 +56,11 @@ pub struct Sql {
/// Database file path
pub(crate) dbfile: PathBuf,
/// Write transaction mutex.
///
/// See [`Self::write_lock`].
write_mtx: Mutex<()>,
/// SQL connection pool.
pool: RwLock<Option<Pool>>,
@@ -72,6 +77,7 @@ impl Sql {
pub fn new(dbfile: PathBuf) -> Sql {
Self {
dbfile,
write_mtx: Mutex::new(()),
pool: Default::default(),
is_encrypted: Default::default(),
config_cache: Default::default(),
@@ -130,7 +136,7 @@ impl Sql {
.with_context(|| format!("path {path:?} is not valid unicode"))?
.to_string();
let res = self
.call(move |conn| {
.call_write(move |conn| {
// Check that backup passphrase is correct before resetting our database.
conn.execute(
"ATTACH DATABASE ? AS backup KEY ?",
@@ -299,10 +305,40 @@ impl Sql {
}
}
/// Allocates a connection and calls given function with the connection.
/// Locks the write transactions mutex.
/// We do not make all transactions
/// [IMMEDIATE](https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions)
/// for more parallelism -- at least read transactions can be made DEFERRED to run in parallel
/// w/o any drawbacks. But if we make write transactions DEFERRED also w/o any external locking,
/// then they are upgraded from read to write ones on the first write statement. This has some
/// drawbacks:
/// - If there are other write transactions, we block the thread and the db connection until
/// upgraded. Also if some reader comes then, it has to get next, less used connection with a
/// worse per-connection page cache.
/// - If a transaction is blocked for more than busy_timeout, it fails with SQLITE_BUSY.
/// - Configuring busy_timeout is not the best way to manage transaction timeouts, we would
/// prefer it to be integrated with Rust/tokio asyncs. Moreover, SQLite implements waiting
/// using sleeps.
/// - If upon a successful upgrade to a write transaction the db has been modified by another
/// one, the transaction has to be rolled back and retried. It is an extra work in terms of
/// CPU/battery.
/// - Maybe minor, but we lose some fairness in servicing write transactions, i.e. we service
/// them in the order of the first write statement, not in the order they come.
/// The only pro of making write transactions DEFERRED w/o the external locking is some
/// parallelism between them. Also we have an option to make write transactions IMMEDIATE, also
/// w/o the external locking. But then the most of cons above are still valid. Instead, if we
/// perform all write transactions under an async mutex, the only cons is losing some
/// parallelism for write transactions.
pub async fn write_lock(&self) -> MutexGuard<'_, ()> {
self.write_mtx.lock().await
}
/// Allocates a connection and calls `function` with the connection. If `function` does write
/// queries, either a lock must be taken first using `write_lock()` or `call_write()` used
/// instead.
///
/// Returns the result of the function.
pub async fn call<'a, F, R>(&'a self, function: F) -> Result<R>
async fn call<'a, F, R>(&'a self, function: F) -> Result<R>
where
F: 'a + FnOnce(&mut Connection) -> Result<R> + Send,
R: Send + 'static,
@@ -314,13 +350,26 @@ impl Sql {
Ok(res)
}
/// Execute the given query, returning the number of affected rows.
/// Allocates a connection and calls given function, assuming it does write queries, with the
/// connection.
///
/// Returns the result of the function.
pub async fn call_write<'a, F, R>(&'a self, function: F) -> Result<R>
where
F: 'a + FnOnce(&mut Connection) -> Result<R> + Send,
R: Send + 'static,
{
let _lock = self.write_lock().await;
self.call(function).await
}
/// Execute `query` assuming it is a write query, returning the number of affected rows.
pub async fn execute(
&self,
query: &str,
params: impl rusqlite::Params + Send,
) -> Result<usize> {
self.call(move |conn| {
self.call_write(move |conn| {
let res = conn.execute(query, params)?;
Ok(res)
})
@@ -329,7 +378,7 @@ impl Sql {
/// Executes the given query, returning the last inserted row ID.
pub async fn insert(&self, query: &str, params: impl rusqlite::Params + Send) -> Result<i64> {
self.call(move |conn| {
self.call_write(move |conn| {
conn.execute(query, params)?;
Ok(conn.last_insert_rowid())
})
@@ -390,23 +439,17 @@ impl Sql {
.await
}
/// Execute the function inside a transaction.
/// Execute the function inside a transaction assuming that it does write queries.
///
/// If the function returns an error, the transaction will be rolled back. If it does not return an
/// error, the transaction will be committed.
///
/// Transactions started use IMMEDIATE behavior
/// rather than default DEFERRED behavior
/// to avoid "database is busy" errors
/// which may happen when DEFERRED transaction
/// is attempted to be promoted to a write transaction.
pub async fn transaction<G, H>(&self, callback: G) -> Result<H>
where
H: Send + 'static,
G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
{
self.call(move |conn| {
let mut transaction = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
self.call_write(move |conn| {
let mut transaction = conn.transaction()?;
let ret = callback(&mut transaction);
match ret {
@@ -617,7 +660,7 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
conn.execute_batch(
"PRAGMA cipher_memory_security = OFF; -- Too slow on Android
PRAGMA secure_delete=on;
PRAGMA busy_timeout = 60000; -- 60 seconds
PRAGMA busy_timeout = 0; -- fail immediately
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
PRAGMA foreign_keys=on;
",
@@ -983,7 +1026,7 @@ mod tests {
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
t.sql.close().await;
housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed
housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail
t.sql.open(&t, "".to_string()).await.unwrap();
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();

193
src/timesmearing.rs Normal file
View File

@@ -0,0 +1,193 @@
//! # Time smearing.
//!
//! As e-mails typically only use a second-based-resolution for timestamps,
//! the order of two mails sent withing one second is unclear.
//! This is bad e.g. when forwarding some messages from a chat -
//! these messages will appear at the recipient easily out of order.
//!
//! We work around this issue by not sending out two mails with the same timestamp.
//! For this purpose, in short, we track the last timestamp used in `last_smeared_timestamp`
//! when another timestamp is needed in the same second, we use `last_smeared_timestamp+1`
//! after some moments without messages sent out,
//! `last_smeared_timestamp` is again in sync with the normal time.
//!
//! However, we do not do all this for the far future,
//! but at max `MAX_SECONDS_TO_LEND_FROM_FUTURE`
use std::cmp::{max, min};
use std::sync::atomic::{AtomicI64, Ordering};
pub(crate) const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
/// Smeared timestamp generator.
#[derive(Debug)]
pub struct SmearedTimestamp {
/// Next timestamp available for allocation.
smeared_timestamp: AtomicI64,
}
impl SmearedTimestamp {
/// Creates a new smeared timestamp generator.
pub fn new() -> Self {
Self {
smeared_timestamp: AtomicI64::new(0),
}
}
/// Allocates `count` unique timestamps.
///
/// Returns the first allocated timestamp.
pub fn create_n(&self, now: i64, count: i64) -> i64 {
let mut prev = self.smeared_timestamp.load(Ordering::Relaxed);
loop {
// Advance the timestamp if it is in the past,
// but keep `count - 1` timestamps from the past if possible.
let t = max(prev, now - count + 1);
// Rewind the time back if there is no room
// to allocate `count` timestamps without going too far into the future.
// Not going too far into the future
// is more important than generating unique timestamps.
let first = min(t, now + MAX_SECONDS_TO_LEND_FROM_FUTURE - count + 1);
// Allocate `count` timestamps by advancing the current timestamp.
let next = first + count;
if let Err(x) = self.smeared_timestamp.compare_exchange_weak(
prev,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
prev = x;
} else {
return first;
}
}
}
/// Creates a single timestamp.
pub fn create(&self, now: i64) -> i64 {
self.create_n(now, 1)
}
/// Returns the current smeared timestamp.
pub fn current(&self) -> i64 {
self.smeared_timestamp.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use std::time::SystemTime;
use super::*;
use crate::test_utils::TestContext;
use crate::tools::{create_smeared_timestamp, create_smeared_timestamps, smeared_time, time};
#[test]
fn test_smeared_timestamp() {
let smeared_timestamp = SmearedTimestamp::new();
let now = time();
assert_eq!(smeared_timestamp.current(), 0);
for i in 0..MAX_SECONDS_TO_LEND_FROM_FUTURE {
assert_eq!(smeared_timestamp.create(now), now + i);
}
assert_eq!(
smeared_timestamp.create(now),
now + MAX_SECONDS_TO_LEND_FROM_FUTURE
);
assert_eq!(
smeared_timestamp.create(now),
now + MAX_SECONDS_TO_LEND_FROM_FUTURE
);
// System time rewinds back by 1000 seconds.
let now = now - 1000;
assert_eq!(
smeared_timestamp.create(now),
now + MAX_SECONDS_TO_LEND_FROM_FUTURE
);
assert_eq!(
smeared_timestamp.create(now),
now + MAX_SECONDS_TO_LEND_FROM_FUTURE
);
assert_eq!(
smeared_timestamp.create(now + 1),
now + MAX_SECONDS_TO_LEND_FROM_FUTURE + 1
);
assert_eq!(smeared_timestamp.create(now + 100), now + 100);
assert_eq!(smeared_timestamp.create(now + 100), now + 101);
assert_eq!(smeared_timestamp.create(now + 100), now + 102);
}
#[test]
fn test_create_n_smeared_timestamps() {
let smeared_timestamp = SmearedTimestamp::new();
let now = time();
// Create a single timestamp to initialize the generator.
assert_eq!(smeared_timestamp.create(now), now);
// Wait a minute.
let now = now + 60;
// Simulate forwarding 7 messages.
let forwarded_messages = 7;
// We have not sent anything for a minute,
// so we can take the current timestamp and take 6 timestamps from the past.
assert_eq!(smeared_timestamp.create_n(now, forwarded_messages), now - 6);
assert_eq!(smeared_timestamp.current(), now + 1);
// Wait 4 seconds.
// Now we have 3 free timestamps in the past.
let now = now + 4;
assert_eq!(smeared_timestamp.current(), now - 3);
// Forward another 7 messages.
// We can only lend 3 timestamps from the past.
assert_eq!(smeared_timestamp.create_n(now, forwarded_messages), now - 3);
// We had to borrow 3 timestamps from the future
// because there were not enough timestamps in the past.
assert_eq!(smeared_timestamp.current(), now + 4);
// Forward another 7 messages.
// We cannot use more than 5 timestamps from the future,
// so we use 5 timestamps from the future,
// the current timestamp and one timestamp from the past.
assert_eq!(smeared_timestamp.create_n(now, forwarded_messages), now - 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_smeared_timestamp() {
let t = TestContext::new().await;
assert_ne!(create_smeared_timestamp(&t), create_smeared_timestamp(&t));
assert!(
create_smeared_timestamp(&t)
>= SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_smeared_timestamps() {
let t = TestContext::new().await;
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
let start = create_smeared_timestamps(&t, count as usize);
let next = smeared_time(&t);
assert!((start + count - 1) < next);
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
let start = create_smeared_timestamps(&t, count as usize);
let next = smeared_time(&t);
assert!((start + count - 1) < next);
}
}

View File

@@ -3,7 +3,6 @@
#![allow(missing_docs)]
use core::cmp::{max, min};
use std::borrow::Cow;
use std::fmt;
use std::io::Cursor;
@@ -140,63 +139,27 @@ pub(crate) fn gm2local_offset() -> i64 {
i64::from(lt.offset().local_minus_utc())
}
// timesmearing
// - as e-mails typically only use a second-based-resolution for timestamps,
// the order of two mails sent withing one second is unclear.
// this is bad eg. when forwarding some messages from a chat -
// these messages will appear at the recipient easily out of order.
// - we work around this issue by not sending out two mails with the same timestamp.
// - for this purpose, in short, we track the last timestamp used in `last_smeared_timestamp`
// when another timestamp is needed in the same second, we use `last_smeared_timestamp+1`
// - after some moments without messages sent out,
// `last_smeared_timestamp` is again in sync with the normal time.
// - however, we do not do all this for the far future,
// but at max `MAX_SECONDS_TO_LEND_FROM_FUTURE`
pub(crate) const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
/// Returns the current smeared timestamp,
///
/// The returned timestamp MUST NOT be sent out.
pub(crate) async fn smeared_time(context: &Context) -> i64 {
let mut now = time();
let ts = *context.last_smeared_timestamp.read().await;
if ts >= now {
now = ts + 1;
}
now
pub(crate) fn smeared_time(context: &Context) -> i64 {
let now = time();
let ts = context.smeared_timestamp.current();
std::cmp::max(ts, now)
}
/// Returns a timestamp that is guaranteed to be unique.
pub(crate) async fn create_smeared_timestamp(context: &Context) -> i64 {
pub(crate) fn create_smeared_timestamp(context: &Context) -> i64 {
let now = time();
let mut ret = now;
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await;
if ret <= *last_smeared_timestamp {
ret = *last_smeared_timestamp + 1;
if ret - now > MAX_SECONDS_TO_LEND_FROM_FUTURE {
ret = now + MAX_SECONDS_TO_LEND_FROM_FUTURE
}
}
*last_smeared_timestamp = ret;
ret
context.smeared_timestamp.create(now)
}
// creates `count` timestamps that are guaranteed to be unique.
// the frist created timestamps is returned directly,
// the first created timestamps is returned directly,
// get the other timestamps just by adding 1..count-1
pub(crate) async fn create_smeared_timestamps(context: &Context, count: usize) -> i64 {
pub(crate) fn create_smeared_timestamps(context: &Context, count: usize) -> i64 {
let now = time();
let count = count as i64;
let mut start = now + min(count, MAX_SECONDS_TO_LEND_FROM_FUTURE) - count;
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await;
start = max(*last_smeared_timestamp + 1, start);
*last_smeared_timestamp = start + count - 1;
start
context.smeared_timestamp.create_n(now, count as i64)
}
// if the system time is not plausible, once a day, add a device message.
@@ -592,6 +555,8 @@ pub(crate) fn improve_single_line_input(input: &str) -> String {
}
pub(crate) trait IsNoneOrEmpty<T> {
/// Returns true if an Option does not contain a string
/// or contains an empty string.
fn is_none_or_empty(&self) -> bool;
}
impl<T> IsNoneOrEmpty<T> for Option<T>
@@ -1069,36 +1034,6 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
assert!(!file_exist!(context, &fn0));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_smeared_timestamp() {
let t = TestContext::new().await;
assert_ne!(
create_smeared_timestamp(&t).await,
create_smeared_timestamp(&t).await
);
assert!(
create_smeared_timestamp(&t).await
>= SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_smeared_timestamps() {
let t = TestContext::new().await;
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
let start = create_smeared_timestamps(&t, count as usize).await;
let next = smeared_time(&t).await;
assert!((start + count - 1) < next);
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
let start = create_smeared_timestamps(&t, count as usize).await;
let next = smeared_time(&t).await;
assert!((start + count - 1) < next);
}
#[test]
fn test_duration_to_str() {
assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s");

View File

@@ -2,8 +2,8 @@
use anyhow::Result;
use crate::chat::{Chat, ChatId};
use crate::contact::{Contact, ContactId};
use crate::chat::ChatId;
use crate::contact::ContactId;
use crate::context::Context;
use crate::param::{Param, Params};
@@ -17,12 +17,26 @@ impl Context {
scope: Param,
new_timestamp: i64,
) -> Result<bool> {
let mut contact = Contact::load_from_db(self, contact_id).await?;
if contact.param.update_timestamp(scope, new_timestamp)? {
contact.update_param(self).await?;
return Ok(true);
}
Ok(false)
self.sql
.transaction(|transaction| {
let mut param: Params = transaction.query_row(
"SELECT param FROM contacts WHERE id=?",
[contact_id],
|row| {
let param: String = row.get(0)?;
Ok(param.parse().unwrap_or_default())
},
)?;
let update = param.update_timestamp(scope, new_timestamp)?;
if update {
transaction.execute(
"UPDATE contacts SET param=? WHERE id=?",
params![param.to_string(), contact_id],
)?;
}
Ok(update)
})
.await
}
}
@@ -35,12 +49,24 @@ impl ChatId {
scope: Param,
new_timestamp: i64,
) -> Result<bool> {
let mut chat = Chat::load_from_db(context, *self).await?;
if chat.param.update_timestamp(scope, new_timestamp)? {
chat.update_param(context).await?;
return Ok(true);
}
Ok(false)
context
.sql
.transaction(|transaction| {
let mut param: Params =
transaction.query_row("SELECT param FROM chats WHERE id=?", [self], |row| {
let param: String = row.get(0)?;
Ok(param.parse().unwrap_or_default())
})?;
let update = param.update_timestamp(scope, new_timestamp)?;
if update {
transaction.execute(
"UPDATE chats SET param=? WHERE id=?",
params![param.to_string(), self],
)?;
}
Ok(update)
})
.await
}
}
@@ -60,6 +86,7 @@ impl Params {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::Chat;
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use crate::tools::time;

View File

@@ -408,7 +408,7 @@ impl Context {
.create_status_update_record(
&mut instance,
update_str,
create_smeared_timestamp(self).await,
create_smeared_timestamp(self),
send_now,
ContactId::SELF,
)
@@ -431,6 +431,7 @@ impl Context {
async fn pop_smtp_status_update(
&self,
) -> Result<Option<(MsgId, StatusUpdateSerial, StatusUpdateSerial, String)>> {
let _lock = self.sql.write_lock().await;
let res = self
.sql
.query_row_optional(
@@ -670,8 +671,10 @@ impl Message {
Ok(archive)
}
/// Return file form inside an archive.
/// Return file from inside an archive.
/// Currently, this works only if the message is an webxdc instance.
///
/// `name` is the filename within the archive, e.g. `index.html`.
pub async fn get_webxdc_blob(&self, context: &Context, name: &str) -> Result<Vec<u8>> {
ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance.");