Merge remote-tracking branch 'origin/master' into iroh-share

This commit is contained in:
dignifiedquire
2022-12-02 16:51:41 +01:00
43 changed files with 1155 additions and 892 deletions

View File

@@ -2,6 +2,23 @@
## Unreleased ## Unreleased
### Changes
- Refactor: Remove the remaining AsRef<str> #3669
- Small speedup #3780
### API-Changes
- Add Python API to send reactions #3762
- jsonrpc: add message errors to MessageObject #3788
### Fixes
- Make sure malformed messsages will never block receiving further messages anymore #3771
- strip leading/trailing whitespace from "Chat-Group-Name{,-Changed}:" headers content #3650
- Assume all Thunderbird users prefer encryption #3774
- refactor peerstate handling to ensure no duplicate peerstates #3776
## 1.102.0
### Changes ### Changes
- If an email has multiple From addresses, handle this as if there was - If an email has multiple From addresses, handle this as if there was

833
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deltachat" name = "deltachat"
version = "1.101.0" version = "1.102.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"] authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
@@ -57,7 +57,7 @@ quick-xml = "0.23"
r2d2 = "0.8" r2d2 = "0.8"
r2d2_sqlite = "0.20" r2d2_sqlite = "0.20"
rand = "0.8" rand = "0.8"
regex = "1.6" regex = "1.7"
rusqlite = { version = "0.27", features = ["sqlcipher"] } rusqlite = { version = "0.27", features = ["sqlcipher"] }
rust-hsluv = "0.1" rust-hsluv = "0.1"
rustyline = { version = "10", optional = true } rustyline = { version = "10", optional = true }
@@ -74,14 +74,14 @@ toml = "0.5"
url = "2" url = "2"
uuid = { version = "1", features = ["serde", "v4"] } uuid = { version = "1", features = ["serde", "v4"] }
fast-socks5 = "0.8" fast-socks5 = "0.8"
humansize = "1" humansize = "2"
qrcodegen = "1.7.0" qrcodegen = "1.7.0"
tagger = "4.3.3" tagger = "4.3.3"
textwrap = "0.16.0" textwrap = "0.16.0"
async-channel = "1.6.1" async-channel = "1.6.1"
futures-lite = "1.12.0" futures-lite = "1.12.0"
tokio-stream = { version = "0.1.11", features = ["fs"] } tokio-stream = { version = "0.1.11", features = ["fs"] }
reqwest = { version = "0.11.12", features = ["json"] } reqwest = { version = "0.11.13", features = ["json"] }
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] } async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
iroh-share = { git = "https://github.com/n0-computer/iroh", tag = "v0.1.1" } iroh-share = { git = "https://github.com/n0-computer/iroh", tag = "v0.1.1" }
iroh-resolver = { git = "https://github.com/n0-computer/iroh", tag = "v0.1.1", default-features = false } iroh-resolver = { git = "https://github.com/n0-computer/iroh", tag = "v0.1.1", default-features = false }

View File

@@ -120,6 +120,15 @@ $ cargo test -- --ignored
- `vendored`: When using Openssl for TLS, this bundles a vendored version. - `vendored`: When using Openssl for TLS, this bundles a vendored version.
- `nightly`: Enable nightly only performance and security related features. - `nightly`: Enable nightly only performance and security related features.
## Update Provider Data
To add the updates from the
[provider-db](https://github.com/deltachat/provider-db) to the core, run:
```
./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
```
## Language bindings and frontend projects ## Language bindings and frontend projects
Language bindings are available for: Language bindings are available for:

View File

@@ -38,11 +38,64 @@ Hello {i}",
context context
} }
/// Receive 100 emails that remove charlie@example.com and add
/// him back
async fn recv_groupmembership_emails(context: Context) -> Context {
for i in 0..50 {
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
Chat-Group-Member-Added: charlie@example.com
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
.unwrap();
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
Chat-Group-Member-Removed: charlie@example.com
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
.unwrap();
}
context
}
async fn create_context() -> Context { async fn create_context() -> Context {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite"); let dbfile = dir.path().join("db.sqlite");
let id = 100; let id = 100;
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new()) let context = Context::new(dbfile.as_path(), id, Events::new(), StockStrings::new())
.await .await
.unwrap(); .unwrap();
@@ -52,7 +105,7 @@ async fn create_context() -> Context {
if backup.exists() { if backup.exists() {
println!("Importing backup"); println!("Importing backup");
imex(&context, ImexMode::ImportBackup, &backup, None) imex(&context, ImexMode::ImportBackup, backup.as_path(), None)
.await .await
.unwrap(); .unwrap();
} }
@@ -83,6 +136,20 @@ fn criterion_benchmark(c: &mut Criterion) {
} }
}); });
}); });
group.bench_function(
"Receive 100 Chat-Group-Member-{Added|Removed} messages",
|b| {
let rt = tokio::runtime::Runtime::new().unwrap();
let context = rt.block_on(create_context());
b.to_async(&rt).iter(|| {
let ctx = context.clone();
async move {
recv_groupmembership_emails(black_box(ctx)).await;
}
});
},
);
group.finish(); group.finish();
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deltachat_ffi" name = "deltachat_ffi"
version = "1.101.0" version = "1.102.0"
description = "Deltachat FFI" description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"] authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018" edition = "2018"

View File

@@ -1660,7 +1660,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
let ctx = &*context; let ctx = &*context;
block_on(async move { block_on(async move {
chat::set_chat_profile_image(ctx, ChatId::new(chat_id), to_string_lossy(image)) chat::set_chat_profile_image(ctx, ChatId::new(chat_id), &to_string_lossy(image))
.await .await
.map(|_| 1) .map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set profile image") .unwrap_or_log_default(ctx, "Failed to set profile image")

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deltachat-jsonrpc" name = "deltachat-jsonrpc"
version = "1.101.0" version = "1.102.0"
description = "DeltaChat JSON-RPC API" description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"] authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021" edition = "2021"
@@ -23,7 +23,7 @@ async-channel = { version = "1.6.1" }
futures = { version = "0.3.25" } futures = { version = "0.3.25" }
serde_json = "1.0.87" serde_json = "1.0.87"
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] } yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.3", features = ["json_value"] } typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.21.2" } tokio = { version = "1.21.2" }
sanitize-filename = "0.4" sanitize-filename = "0.4"
walkdir = "2.3.2" walkdir = "2.3.2"

View File

@@ -733,7 +733,7 @@ impl CommandApi {
image_path: Option<String>, image_path: Option<String>,
) -> Result<()> { ) -> Result<()> {
let ctx = self.get_context(account_id).await?; let ctx = self.get_context(account_id).await?;
chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), image_path.unwrap_or_default()) chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), &image_path.unwrap_or_default())
.await .await
} }

View File

@@ -34,6 +34,9 @@ pub struct MessageObject {
view_type: MessageViewtype, view_type: MessageViewtype,
state: u32, state: u32,
/// An error text, if there is one.
error: Option<String>,
timestamp: i64, timestamp: i64,
sort_timestamp: i64, sort_timestamp: i64,
received_timestamp: i64, received_timestamp: i64,
@@ -167,6 +170,7 @@ impl MessageObject {
.get_state() .get_state()
.to_u32() .to_u32()
.ok_or_else(|| anyhow!("state conversion to number failed"))?, .ok_or_else(|| anyhow!("state conversion to number failed"))?,
error: message.error(),
timestamp: message.get_timestamp(), timestamp: message.get_timestamp(),
sort_timestamp: message.get_sort_timestamp(), sort_timestamp: message.get_sort_timestamp(),

View File

@@ -48,5 +48,5 @@
}, },
"type": "module", "type": "module",
"types": "dist/deltachat.d.ts", "types": "dist/deltachat.d.ts",
"version": "1.101.0" "version": "1.102.0"
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deltachat-rpc-server" name = "deltachat-rpc-server"
version = "1.101.0" version = "1.102.0"
description = "DeltaChat JSON-RPC server" description = "DeltaChat JSON-RPC server"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"] authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021" edition = "2021"

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit" "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
}, },
"types": "node/dist/index.d.ts", "types": "node/dist/index.d.ts",
"version": "1.101.0" "version": "1.102.0"
} }

View File

@@ -156,6 +156,7 @@ def extract_defines(flags):
| DC_KEY_GEN | DC_KEY_GEN
| DC_IMEX | DC_IMEX
| DC_CONNECTIVITY | DC_CONNECTIVITY
| DC_DOWNLOAD
) # End of prefix matching ) # End of prefix matching
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048 _[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
) # Close the capturing group, this contains ) # Close the capturing group, this contains

View File

@@ -192,6 +192,12 @@ class FFIEventTracker:
return self.account.get_message_by_id(ev.data2) return self.account.get_message_by_id(ev.data2)
return None return None
def wait_next_reactions_changed(self):
"""wait for and return next reactions-changed message"""
ev = self.get_matching("DC_EVENT_REACTIONS_CHANGED")
assert ev.data1 > 0
return self.account.get_message_by_id(ev.data2)
def wait_msg_delivered(self, msg): def wait_msg_delivered(self, msg):
ev = self.get_matching("DC_EVENT_MSG_DELIVERED") ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
assert ev.data1 == msg.chat.id assert ev.data1 == msg.chat.id
@@ -296,6 +302,10 @@ class EventThread(threading.Thread):
"ac_incoming_message", "ac_incoming_message",
dict(message=msg), dict(message=msg),
) )
elif name == "DC_EVENT_REACTIONS_CHANGED":
assert ffi_event.data1 > 0
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_reactions_changed", dict(message=msg)
elif name == "DC_EVENT_MSG_DELIVERED": elif name == "DC_EVENT_MSG_DELIVERED":
msg = account.get_message_by_id(ffi_event.data2) msg = account.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", dict(message=msg) yield "ac_message_delivered", dict(message=msg)

View File

@@ -49,6 +49,10 @@ class PerAccount:
def ac_outgoing_message(self, message): def ac_outgoing_message(self, message):
"""Called on each outgoing message (both system and "normal").""" """Called on each outgoing message (both system and "normal")."""
@account_hookspec
def ac_reactions_changed(self, message):
"""Called when message reactions changed."""
@account_hookspec @account_hookspec
def ac_message_delivered(self, message): def ac_message_delivered(self, message):
"""Called when an outgoing message has been delivered to SMTP. """Called when an outgoing message has been delivered to SMTP.

View File

@@ -9,6 +9,7 @@ from typing import Optional, Union
from . import const, props from . import const, props
from .capi import ffi, lib from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer
from .reactions import Reactions
class Message(object): class Message(object):
@@ -161,6 +162,17 @@ class Message(object):
) )
) )
def send_reaction(self, reaction: str):
"""Send a reaction to message and return the resulting Message instance."""
msg_id = lib.dc_send_reaction(self.account._dc_context, self.id, as_dc_charpointer(reaction))
if msg_id == 0:
raise ValueError("reaction could not be send")
return Message.from_db(self.account, msg_id)
def get_reactions(self) -> Reactions:
"""Get :class:`deltachat.reactions.Reactions` to the message."""
return Reactions.from_msg(self)
def is_system_message(self): def is_system_message(self):
"""return True if this message is a system/info message.""" """return True if this message is a system/info message."""
return bool(lib.dc_msg_is_info(self._dc_msg)) return bool(lib.dc_msg_is_info(self._dc_msg))
@@ -449,6 +461,17 @@ class Message(object):
"""mark this message as seen.""" """mark this message as seen."""
self.account.mark_seen_messages([self.id]) self.account.mark_seen_messages([self.id])
#
# Message download state
#
@property
def download_state(self):
assert self.id > 0
# load message from db to get a fresh/current state
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
return lib.dc_msg_get_download_state(dc_msg)
# some code for handling DC_MSG_* view types # some code for handling DC_MSG_* view types

View File

@@ -0,0 +1,43 @@
""" The Reactions object. """
from .capi import ffi, lib
from .cutil import from_dc_charpointer, iter_array
class Reactions(object):
"""Reactions object.
You obtain instances of it through :class:`deltachat.message.Message`.
"""
def __init__(self, account, dc_reactions):
assert isinstance(account._dc_context, ffi.CData)
assert isinstance(dc_reactions, ffi.CData)
assert dc_reactions != ffi.NULL
self.account = account
self._dc_reactions = dc_reactions
def __repr__(self):
return "<Reactions dc_reactions={}>".format(self._dc_reactions)
@classmethod
def from_msg(cls, msg):
assert msg.id > 0
return cls(
msg.account,
ffi.gc(lib.dc_get_msg_reactions(msg.account._dc_context, msg.id), lib.dc_reactions_unref),
)
def get_contacts(self) -> list:
"""Get list of contacts reacted to the message.
:returns: list of :class:`deltachat.contact.Contact` objects for this reaction.
"""
from .contact import Contact
dc_array = ffi.gc(lib.dc_reactions_get_contacts(self._dc_reactions), lib.dc_array_unref)
return list(iter_array(dc_array, lambda x: Contact(self.account, x)))
def get_by_contact(self, contact) -> str:
"""Get a string containing space-separated reactions of a single :class:`deltachat.contact.Contact`."""
return from_dc_charpointer(lib.dc_reactions_get_by_contact_id(self._dc_reactions, contact.id))

View File

@@ -405,6 +405,29 @@ def test_forward_own_message(acfactory, lp):
assert msg_in.is_forwarded() assert msg_in.is_forwarded()
def test_long_group_name(acfactory, lp):
"""See bug https://github.com/deltachat/deltachat-core-rust/issues/3650 "Space added before long
group names after MIME serialization/deserialization".
When the mailadm bot creates a group with botadmin, the bot creates is as
"pytest-supportuser-282@x.testrun.org support group" (for example). But in the botadmin's
account object, the group chat is called " pytest-supportuser-282@x.testrun.org support group"
(with an additional space character in the beginning).
"""
ac1, ac2 = acfactory.get_online_accounts(2)
lp.sec("ac1: creating group chat and sending a message")
group_name = "pytest-supportuser-282@x.testrun.org support group"
group = ac1.create_group_chat(group_name)
group.add_contact(ac2)
group.send_text("message")
# wait for other account to receive
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.chat.get_name() == group_name
def test_send_self_message(acfactory, lp): def test_send_self_message(acfactory, lp):
ac1 = acfactory.new_online_configuring_account(mvbox_move=True) ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online() acfactory.bring_accounts_online()
@@ -1267,6 +1290,66 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_in assert m == msg_in
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
If the Inbox contains X small messages followed by Y large messages followed by Z small
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
with online test as follows:
- Bob enables download limit and goes offline.
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
- Bob goes online
- Bob first processes a reaction message and throws it away because there is no corresponding
message, then processes a partially downloaded message.
- As a result, Bob does not see a reaction
"""
download_limit = 32768
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
chat = ac1.create_chat(ac2)
ac2.set_config("download_limit", str(download_limit))
ac2.stop_io()
reactions_queue = queue.Queue()
class InPlugin:
@account_hookimpl
def ac_reactions_changed(self, message):
reactions_queue.put(message)
ac2.add_account_plugin(InPlugin())
lp.sec("sending small+large messages from ac1 to ac2")
msgs = []
msgs.append(chat.send_text("hi"))
path = tmpdir.join("large")
with open(path, "wb") as fout:
fout.write(os.urandom(download_limit + 1))
msgs.append(chat.send_file(path.strpath))
lp.sec("sending a reaction to the large message from ac1 to ac2")
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
for m in msgs:
ac1._evtracker.wait_msg_delivered(m)
ac2.start_io()
lp.sec("wait for ac2 to receive a reaction")
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.get_sender_contact().addr == ac1_addr
assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE
assert reactions_queue.get() == msg2
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
assert len(contacts) == 1
assert contacts[0].addr == ac1_addr
assert reactions.get_by_contact(contacts[0]) == react_str
def test_import_export_online_all(acfactory, tmpdir, data, lp): def test_import_export_online_all(acfactory, tmpdir, data, lp):
(ac1,) = acfactory.get_online_accounts(1) (ac1,) = acfactory.get_online_accounts(1)

View File

@@ -13,8 +13,6 @@ use once_cell::sync::Lazy;
use crate::config::Config; use crate::config::Config;
use crate::context::Context; use crate::context::Context;
use crate::headerdef::HeaderDef; use crate::headerdef::HeaderDef;
use crate::mimeparser;
use crate::mimeparser::ParserErrorExt;
use crate::tools::time; use crate::tools::time;
use crate::tools::EmailAddress; use crate::tools::EmailAddress;
@@ -32,23 +30,19 @@ pub(crate) async fn handle_authres(
mail: &ParsedMail<'_>, mail: &ParsedMail<'_>,
from: &str, from: &str,
message_time: i64, message_time: i64,
) -> mimeparser::ParserResult<DkimResults> { ) -> Result<DkimResults> {
let from_domain = match EmailAddress::new(from) { let from_domain = match EmailAddress::new(from) {
Ok(email) => email.domain, Ok(email) => email.domain,
Err(e) => { Err(e) => {
// This email is invalid, but don't return an error, we still want to // This email is invalid, but don't return an error, we still want to
// add a stub to the database so that it's not downloaded again // add a stub to the database so that it's not downloaded again
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e)).map_err_malformed(); return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
} }
}; };
let authres = parse_authres_headers(&mail.get_headers(), &from_domain); let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
update_authservid_candidates(context, &authres) update_authservid_candidates(context, &authres).await?;
.await compute_dkim_results(context, authres, &from_domain, message_time).await
.map_err_sql()?;
compute_dkim_results(context, authres, &from_domain, message_time)
.await
.map_err_sql()
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]

View File

@@ -2188,7 +2188,7 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
let mut msg = Message::new(Viewtype::VideochatInvitation); let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.param.set(Param::WebrtcRoom, &instance); msg.param.set(Param::WebrtcRoom, &instance);
msg.text = Some( msg.text = Some(
stock_str::videochat_invite_msg_body(context, Message::parse_webrtc_instance(&instance).1) stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
.await, .await,
); );
send_msg(context, chat_id, &mut msg).await send_msg(context, chat_id, &mut msg).await
@@ -2563,7 +2563,7 @@ pub async fn create_group_chat(
let chat_id = ChatId::new(u32::try_from(row_id)?); let chat_id = ChatId::new(u32::try_from(row_id)?);
if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? { if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?; add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
} }
context.emit_msgs_changed_without_ids(); context.emit_msgs_changed_without_ids();
@@ -2624,19 +2624,25 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
Ok(chat_id) Ok(chat_id)
} }
/// Adds a contact to the `chats_contacts` table. /// Adds contacts to the `chats_contacts` table.
pub(crate) async fn add_to_chat_contacts_table( pub(crate) async fn add_to_chat_contacts_table(
context: &Context, context: &Context,
chat_id: ChatId, chat_id: ChatId,
contact_id: ContactId, contact_ids: &[ContactId],
) -> Result<()> { ) -> Result<()> {
context context
.sql .sql
.execute( .transaction(move |transaction| {
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)", for contact_id in contact_ids {
paramsv![chat_id, contact_id], transaction.execute(
) "INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
paramsv![chat_id, contact_id],
)?;
}
Ok(())
})
.await?; .await?;
Ok(()) Ok(())
} }
@@ -2738,7 +2744,7 @@ pub(crate) async fn add_contact_to_chat_ex(
if is_contact_in_chat(context, chat_id, contact_id).await? { if is_contact_in_chat(context, chat_id, contact_id).await? {
return Ok(false); return Ok(false);
} }
add_to_chat_contacts_table(context, chat_id, contact_id).await?; add_to_chat_contacts_table(context, chat_id, &[contact_id]).await?;
} }
if chat.typ == Chattype::Group && chat.is_promoted() { if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text; msg.viewtype = Viewtype::Text;
@@ -3005,7 +3011,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
pub async fn set_chat_profile_image( pub async fn set_chat_profile_image(
context: &Context, context: &Context,
chat_id: ChatId, chat_id: ChatId,
new_image: impl AsRef<str>, // XXX use PathBuf new_image: &str, // XXX use PathBuf
) -> Result<()> { ) -> Result<()> {
ensure!(!chat_id.is_special(), "Invalid chat ID"); ensure!(!chat_id.is_special(), "Invalid chat ID");
let mut chat = Chat::load_from_db(context, chat_id).await?; let mut chat = Chat::load_from_db(context, chat_id).await?;
@@ -3023,13 +3029,12 @@ pub async fn set_chat_profile_image(
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.param msg.param
.set_int(Param::Cmd, SystemMessage::GroupImageChanged as i32); .set_int(Param::Cmd, SystemMessage::GroupImageChanged as i32);
if new_image.as_ref().is_empty() { if new_image.is_empty() {
chat.param.remove(Param::ProfileImage); chat.param.remove(Param::ProfileImage);
msg.param.remove(Param::Arg); msg.param.remove(Param::Arg);
msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await); msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await);
} else { } else {
let mut image_blob = let mut image_blob = BlobObject::new_from_path(context, Path::new(new_image)).await?;
BlobObject::new_from_path(context, Path::new(new_image.as_ref())).await?;
image_blob.recode_to_avatar_size(context).await?; image_blob.recode_to_avatar_size(context).await?;
chat.param.set(Param::ProfileImage, image_blob.as_name()); chat.param.set(Param::ProfileImage, image_blob.as_name());
msg.param.set(Param::Arg, image_blob.as_name()); msg.param.set(Param::Arg, image_blob.as_name());

View File

@@ -194,20 +194,20 @@ pub enum Config {
impl Context { impl Context {
pub async fn config_exists(&self, key: Config) -> Result<bool> { pub async fn config_exists(&self, key: Config) -> Result<bool> {
Ok(self.sql.get_raw_config(key).await?.is_some()) Ok(self.sql.get_raw_config(key.as_ref()).await?.is_some())
} }
/// Get a configuration key. Returns `None` if no value is set, and no default value found. /// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Result<Option<String>> { pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
let value = match key { let value = match key {
Config::Selfavatar => { Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(key).await?; let rel_path = self.sql.get_raw_config(key.as_ref()).await?;
rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned()) rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned())
} }
Config::SysVersion => Some((*DC_VERSION_STR).clone()), Config::SysVersion => Some((*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)), Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()), Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(key).await?, _ => self.sql.get_raw_config(key.as_ref()).await?,
}; };
if value.is_some() { if value.is_some() {
@@ -297,26 +297,30 @@ impl Context {
Some(value) => { Some(value) => {
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?; let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
blob.recode_to_avatar_size(self).await?; blob.recode_to_avatar_size(self).await?;
self.sql.set_raw_config(key, Some(blob.as_name())).await?; self.sql
.set_raw_config(key.as_ref(), Some(blob.as_name()))
.await?;
} }
None => { None => {
self.sql.set_raw_config(key, None).await?; self.sql.set_raw_config(key.as_ref(), None).await?;
} }
} }
self.emit_event(EventType::SelfavatarChanged); self.emit_event(EventType::SelfavatarChanged);
} }
Config::DeleteDeviceAfter => { Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(key, value).await; let ret = self.sql.set_raw_config(key.as_ref(), value).await;
// Interrupt ephemeral loop to delete old messages immediately. // Interrupt ephemeral loop to delete old messages immediately.
self.interrupt_ephemeral_task().await; self.interrupt_ephemeral_task().await;
ret? ret?
} }
Config::Displayname => { Config::Displayname => {
let value = value.map(improve_single_line_input); let value = value.map(improve_single_line_input);
self.sql.set_raw_config(key, value.as_deref()).await?; self.sql
.set_raw_config(key.as_ref(), value.as_deref())
.await?;
} }
_ => { _ => {
self.sql.set_raw_config(key, value).await?; self.sql.set_raw_config(key.as_ref(), value).await?;
} }
} }
Ok(()) Ok(())

View File

@@ -87,7 +87,7 @@ impl Context {
self, self,
// We are using Anyhow's .context() and to show the // We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}: // inner error, too, we need the {:#}:
format!("{:#}", err), &format!("{:#}", err),
) )
.await .await
) )
@@ -153,7 +153,7 @@ async fn on_configure_completed(
if !addr_cmp(&new_addr, &old_addr) { if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.text = msg.text =
Some(stock_str::aeap_explanation_and_link(context, old_addr, new_addr).await); Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
chat::add_device_msg(context, None, Some(&mut msg)) chat::add_device_msg(context, None, Some(&mut msg))
.await .await
.ok_or_log_msg(context, "Cannot add AEAP explanation"); .ok_or_log_msg(context, "Cannot add AEAP explanation");

View File

@@ -5,7 +5,7 @@ use std::collections::HashSet;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use mailparse::ParsedMail; use mailparse::ParsedMail;
use crate::aheader::Aheader; use crate::aheader::{Aheader, EncryptPreference};
use crate::authres; use crate::authres;
use crate::authres::handle_authres; use crate::authres::handle_authres;
use crate::contact::addr_cmp; use crate::contact::addr_cmp;
@@ -13,7 +13,6 @@ use crate::context::Context;
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring; use crate::keyring::Keyring;
use crate::log::LogExt; use crate::log::LogExt;
use crate::mimeparser::{self, ParserErrorExt};
use crate::peerstate::Peerstate; use crate::peerstate::Peerstate;
use crate::pgp; use crate::pgp;
@@ -61,11 +60,18 @@ pub(crate) async fn prepare_decryption(
mail: &ParsedMail<'_>, mail: &ParsedMail<'_>,
from: &str, from: &str,
message_time: i64, message_time: i64,
) -> mimeparser::ParserResult<DecryptionInfo> { is_thunderbird: bool,
let autocrypt_header = Aheader::from_headers(from, &mail.headers) ) -> Result<DecryptionInfo> {
let mut autocrypt_header = Aheader::from_headers(from, &mail.headers)
.ok_or_log_msg(context, "Failed to parse Autocrypt header") .ok_or_log_msg(context, "Failed to parse Autocrypt header")
.flatten(); .flatten();
if is_thunderbird {
if let Some(autocrypt_header) = &mut autocrypt_header {
autocrypt_header.prefer_encrypt = EncryptPreference::Mutual;
}
}
let dkim_results = handle_authres(context, mail, from, message_time).await?; let dkim_results = handle_authres(context, mail, from, message_time).await?;
let peerstate = get_autocrypt_peerstate( let peerstate = get_autocrypt_peerstate(
@@ -76,8 +82,7 @@ pub(crate) async fn prepare_decryption(
// Disallowing keychanges is disabled for now: // Disallowing keychanges is disabled for now:
true, // dkim_results.allow_keychange, true, // dkim_results.allow_keychange,
) )
.await .await?;
.map_err_sql()?;
Ok(DecryptionInfo { Ok(DecryptionInfo {
from: from.to_string(), from: from.to_string(),
@@ -301,7 +306,7 @@ pub(crate) async fn get_autocrypt_peerstate(
if addr_cmp(&peerstate.addr, from) { if addr_cmp(&peerstate.addr, from) {
if allow_change { if allow_change {
peerstate.apply_header(header, message_time); peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?; peerstate.save_to_db(&context.sql).await?;
} else { } else {
info!( info!(
context, context,
@@ -317,7 +322,7 @@ pub(crate) async fn get_autocrypt_peerstate(
// to the database. // to the database.
} else { } else {
let p = Peerstate::from_header(header, message_time); let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?; p.save_to_db(&context.sql).await?;
peerstate = Some(p); peerstate = Some(p);
} }
} else { } else {

View File

@@ -147,7 +147,6 @@ mod tests {
use crate::chat; use crate::chat;
use crate::message::{Message, Viewtype}; use crate::message::{Message, Viewtype};
use crate::param::Param; use crate::param::Param;
use crate::peerstate::ToSave;
use crate::test_utils::{bob_keypair, TestContext}; use crate::test_utils::{bob_keypair, TestContext};
use super::*; use super::*;
@@ -297,7 +296,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
gossip_key_fingerprint: Some(pub_key.fingerprint()), gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()), verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()), verified_key_fingerprint: Some(pub_key.fingerprint()),
to_save: Some(ToSave::All),
fingerprint_changed: false, fingerprint_changed: false,
}; };
vec![(Some(peerstate), addr)] vec![(Some(peerstate), addr)]

View File

@@ -226,13 +226,13 @@ pub(crate) async fn stock_ephemeral_timer_changed(
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await, Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
Timer::Enabled { duration } => match duration { Timer::Enabled { duration } => match duration {
0..=59 => { 0..=59 => {
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await stock_str::msg_ephemeral_timer_enabled(context, &timer.to_string(), from_id).await
} }
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await, 60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
61..=3599 => { 61..=3599 => {
stock_str::msg_ephemeral_timer_minutes( stock_str::msg_ephemeral_timer_minutes(
context, context,
format!("{}", (f64::from(duration) / 6.0).round() / 10.0), &format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
from_id, from_id,
) )
.await .await
@@ -241,7 +241,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
3601..=86399 => { 3601..=86399 => {
stock_str::msg_ephemeral_timer_hours( stock_str::msg_ephemeral_timer_hours(
context, context,
format!("{}", (f64::from(duration) / 360.0).round() / 10.0), &format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
from_id, from_id,
) )
.await .await
@@ -250,7 +250,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
86401..=604_799 => { 86401..=604_799 => {
stock_str::msg_ephemeral_timer_days( stock_str::msg_ephemeral_timer_days(
context, context,
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0), &format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
from_id, from_id,
) )
.await .await
@@ -259,7 +259,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
_ => { _ => {
stock_str::msg_ephemeral_timer_weeks( stock_str::msg_ephemeral_timer_weeks(
context, context,
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0), &format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
from_id, from_id,
) )
.await .await

View File

@@ -1364,7 +1364,9 @@ impl Imap {
/// Fetches a list of messages by server UID. /// Fetches a list of messages by server UID.
/// ///
/// Returns the last uid fetch successfully and the info about each downloaded message. /// Returns the last UID fetched successfully and the info about each downloaded message.
/// If the message is incorrect or there is a failure to write a message to the database,
/// it is skipped and the error is logged.
pub(crate) async fn fetch_many_msgs( pub(crate) async fn fetch_many_msgs(
&mut self, &mut self,
context: &Context, context: &Context,
@@ -1474,12 +1476,12 @@ impl Imap {
if let Some(m) = received_msg { if let Some(m) = received_msg {
received_msgs.push(m); received_msgs.push(m);
} }
last_uid = Some(server_uid)
} }
Err(err) => { Err(err) => {
warn!(context, "receive_imf error: {:#}", err); warn!(context, "receive_imf error: {:#}", err);
} }
}; };
last_uid = Some(server_uid)
} }
} }

View File

@@ -169,7 +169,7 @@ impl LoginParam {
async fn from_database(context: &Context, prefix: &str) -> Result<Self> { async fn from_database(context: &Context, prefix: &str) -> Result<Self> {
let sql = &context.sql; let sql = &context.sql;
let key = format!("{}addr", prefix); let key = &format!("{}addr", prefix);
let addr = sql let addr = sql
.get_raw_config(key) .get_raw_config(key)
.await? .await?
@@ -177,26 +177,26 @@ impl LoginParam {
.trim() .trim()
.to_string(); .to_string();
let key = format!("{}mail_server", prefix); let key = &format!("{}mail_server", prefix);
let mail_server = sql.get_raw_config(key).await?.unwrap_or_default(); let mail_server = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}mail_port", prefix); let key = &format!("{}mail_port", prefix);
let mail_port = sql.get_raw_config_int(key).await?.unwrap_or_default(); let mail_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
let key = format!("{}mail_user", prefix); let key = &format!("{}mail_user", prefix);
let mail_user = sql.get_raw_config(key).await?.unwrap_or_default(); let mail_user = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}mail_pw", prefix); let key = &format!("{}mail_pw", prefix);
let mail_pw = sql.get_raw_config(key).await?.unwrap_or_default(); let mail_pw = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}mail_security", prefix); let key = &format!("{}mail_security", prefix);
let mail_security = sql let mail_security = sql
.get_raw_config_int(key) .get_raw_config_int(key)
.await? .await?
.and_then(num_traits::FromPrimitive::from_i32) .and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default(); .unwrap_or_default();
let key = format!("{}imap_certificate_checks", prefix); let key = &format!("{}imap_certificate_checks", prefix);
let imap_certificate_checks = let imap_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? { if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap() num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
@@ -204,26 +204,26 @@ impl LoginParam {
Default::default() Default::default()
}; };
let key = format!("{}send_server", prefix); let key = &format!("{}send_server", prefix);
let send_server = sql.get_raw_config(key).await?.unwrap_or_default(); let send_server = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}send_port", prefix); let key = &format!("{}send_port", prefix);
let send_port = sql.get_raw_config_int(key).await?.unwrap_or_default(); let send_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
let key = format!("{}send_user", prefix); let key = &format!("{}send_user", prefix);
let send_user = sql.get_raw_config(key).await?.unwrap_or_default(); let send_user = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}send_pw", prefix); let key = &format!("{}send_pw", prefix);
let send_pw = sql.get_raw_config(key).await?.unwrap_or_default(); let send_pw = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}send_security", prefix); let key = &format!("{}send_security", prefix);
let send_security = sql let send_security = sql
.get_raw_config_int(key) .get_raw_config_int(key)
.await? .await?
.and_then(num_traits::FromPrimitive::from_i32) .and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default(); .unwrap_or_default();
let key = format!("{}smtp_certificate_checks", prefix); let key = &format!("{}smtp_certificate_checks", prefix);
let smtp_certificate_checks = let smtp_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? { if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap_or_default() num_traits::FromPrimitive::from_i32(certificate_checks).unwrap_or_default()
@@ -231,11 +231,11 @@ impl LoginParam {
Default::default() Default::default()
}; };
let key = format!("{}server_flags", prefix); let key = &format!("{}server_flags", prefix);
let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default(); let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default();
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2); let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
let key = format!("{}provider", prefix); let key = &format!("{}provider", prefix);
let provider = sql let provider = sql
.get_raw_config(key) .get_raw_config(key)
.await? .await?
@@ -275,50 +275,50 @@ impl LoginParam {
context.set_primary_self_addr(&self.addr).await?; context.set_primary_self_addr(&self.addr).await?;
let key = format!("{}mail_server", prefix); let key = &format!("{}mail_server", prefix);
sql.set_raw_config(key, Some(&self.imap.server)).await?; sql.set_raw_config(key, Some(&self.imap.server)).await?;
let key = format!("{}mail_port", prefix); let key = &format!("{}mail_port", prefix);
sql.set_raw_config_int(key, i32::from(self.imap.port)) sql.set_raw_config_int(key, i32::from(self.imap.port))
.await?; .await?;
let key = format!("{}mail_user", prefix); let key = &format!("{}mail_user", prefix);
sql.set_raw_config(key, Some(&self.imap.user)).await?; sql.set_raw_config(key, Some(&self.imap.user)).await?;
let key = format!("{}mail_pw", prefix); let key = &format!("{}mail_pw", prefix);
sql.set_raw_config(key, Some(&self.imap.password)).await?; sql.set_raw_config(key, Some(&self.imap.password)).await?;
let key = format!("{}mail_security", prefix); let key = &format!("{}mail_security", prefix);
sql.set_raw_config_int(key, self.imap.security as i32) sql.set_raw_config_int(key, self.imap.security as i32)
.await?; .await?;
let key = format!("{}imap_certificate_checks", prefix); let key = &format!("{}imap_certificate_checks", prefix);
sql.set_raw_config_int(key, self.imap.certificate_checks as i32) sql.set_raw_config_int(key, self.imap.certificate_checks as i32)
.await?; .await?;
let key = format!("{}send_server", prefix); let key = &format!("{}send_server", prefix);
sql.set_raw_config(key, Some(&self.smtp.server)).await?; sql.set_raw_config(key, Some(&self.smtp.server)).await?;
let key = format!("{}send_port", prefix); let key = &format!("{}send_port", prefix);
sql.set_raw_config_int(key, i32::from(self.smtp.port)) sql.set_raw_config_int(key, i32::from(self.smtp.port))
.await?; .await?;
let key = format!("{}send_user", prefix); let key = &format!("{}send_user", prefix);
sql.set_raw_config(key, Some(&self.smtp.user)).await?; sql.set_raw_config(key, Some(&self.smtp.user)).await?;
let key = format!("{}send_pw", prefix); let key = &format!("{}send_pw", prefix);
sql.set_raw_config(key, Some(&self.smtp.password)).await?; sql.set_raw_config(key, Some(&self.smtp.password)).await?;
let key = format!("{}send_security", prefix); let key = &format!("{}send_security", prefix);
sql.set_raw_config_int(key, self.smtp.security as i32) sql.set_raw_config_int(key, self.smtp.security as i32)
.await?; .await?;
let key = format!("{}smtp_certificate_checks", prefix); let key = &format!("{}smtp_certificate_checks", prefix);
sql.set_raw_config_int(key, self.smtp.certificate_checks as i32) sql.set_raw_config_int(key, self.smtp.certificate_checks as i32)
.await?; .await?;
// The OAuth2 flag is either set for both IMAP and SMTP or not at all. // The OAuth2 flag is either set for both IMAP and SMTP or not at all.
let key = format!("{}server_flags", prefix); let key = &format!("{}server_flags", prefix);
let server_flags = match self.imap.oauth2 { let server_flags = match self.imap.oauth2 {
true => DC_LP_AUTH_OAUTH2, true => DC_LP_AUTH_OAUTH2,
false => DC_LP_AUTH_NORMAL, false => DC_LP_AUTH_NORMAL,
@@ -326,7 +326,7 @@ impl LoginParam {
sql.set_raw_config_int(key, server_flags).await?; sql.set_raw_config_int(key, server_flags).await?;
if let Some(provider) = self.provider { if let Some(provider) = self.provider {
let key = format!("{}provider", prefix); let key = &format!("{}provider", prefix);
sql.set_raw_config(key, Some(provider.id)).await?; sql.set_raw_config(key, Some(provider.id)).await?;
} }

View File

@@ -429,7 +429,7 @@ impl<'a> MimeFactory<'a> {
} }
} }
let self_name = match context.get_config(Config::Displayname).await? { let self_name = &match context.get_config(Config::Displayname).await? {
Some(name) => name, Some(name) => name,
None => context.get_config(Config::Addr).await?.unwrap_or_default(), None => context.get_config(Config::Addr).await?.unwrap_or_default(),
}; };
@@ -1261,7 +1261,7 @@ impl<'a> MimeFactory<'a> {
.truncated_text(32) .truncated_text(32)
.to_string() .to_string()
}; };
let p2 = stock_str::read_rcpt_mail_body(context, p1).await; let p2 = stock_str::read_rcpt_mail_body(context, &p1).await;
let message_text = format!("{}\r\n", format_flowed(&p2)); let message_text = format!("{}\r\n", format_flowed(&p2));
message = message.child( message = message.child(
PartBuilder::new() PartBuilder::new()

View File

@@ -157,38 +157,9 @@ impl Default for SystemMessage {
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup"; const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
#[derive(Debug, thiserror::Error)]
pub(crate) enum ParserError {
#[error("{}", _0)]
Malformed(anyhow::Error),
#[error("{:#}", _0)]
Sql(anyhow::Error),
}
pub(crate) type ParserResult<T> = std::result::Result<T, ParserError>;
pub(crate) trait ParserErrorExt<T, E>
where
Self: std::marker::Sized,
{
fn map_err_malformed(self) -> ParserResult<T>;
fn map_err_sql(self) -> ParserResult<T>;
}
impl<T, E: Into<anyhow::Error>> ParserErrorExt<T, E> for Result<T, E> {
fn map_err_malformed(self) -> ParserResult<T> {
self.map_err(|e| ParserError::Malformed(e.into()))
}
fn map_err_sql(self) -> ParserResult<T> {
self.map_err(|e| ParserError::Sql(e.into()))
}
}
impl MimeMessage { impl MimeMessage {
pub async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> { pub async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
Ok(MimeMessage::from_bytes_with_partial(context, body, None).await?) MimeMessage::from_bytes_with_partial(context, body, None).await
} }
/// Parse a mime message. /// Parse a mime message.
@@ -199,8 +170,8 @@ impl MimeMessage {
context: &Context, context: &Context,
body: &[u8], body: &[u8],
partial: Option<u32>, partial: Option<u32>,
) -> ParserResult<Self> { ) -> Result<Self> {
let mail = mailparse::parse_mail(body).map_err_malformed()?; let mail = mailparse::parse_mail(body)?;
let message_time = mail let message_time = mail
.headers .headers
@@ -227,7 +198,7 @@ impl MimeMessage {
); );
// Parse hidden headers. // Parse hidden headers.
let mimetype = mail.ctype.mimetype.parse::<Mime>().map_err_malformed()?; let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" { if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" {
if let Some(part) = mail.subparts.first() { if let Some(part) = mail.subparts.first() {
for field in &part.headers { for field in &part.headers {
@@ -245,9 +216,16 @@ impl MimeMessage {
headers.remove("secure-join-fingerprint"); headers.remove("secure-join-fingerprint");
headers.remove("chat-verified"); headers.remove("chat-verified");
let from = from.context("No from in message").map_err_malformed()?; let is_thunderbird = headers
.get("user-agent")
.map_or(false, |user_agent| user_agent.contains("Thunderbird"));
if is_thunderbird {
info!(context, "Detected Thunderbird");
}
let from = from.context("No from in message")?;
let mut decryption_info = let mut decryption_info =
prepare_decryption(context, &mail, &from.addr, message_time).await?; prepare_decryption(context, &mail, &from.addr, message_time, is_thunderbird).await?;
// Memory location for a possible decrypted message. // Memory location for a possible decrypted message.
let mut mail_raw = Vec::new(); let mut mail_raw = Vec::new();
@@ -265,7 +243,7 @@ impl MimeMessage {
// autocrypt message. // autocrypt message.
mail_raw = raw; mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw).map_err_malformed()?; let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "decrypted message mime-body:"); info!(context, "decrypted message mime-body:");
println!("{}", String::from_utf8_lossy(&mail_raw)); println!("{}", String::from_utf8_lossy(&mail_raw));
@@ -279,8 +257,7 @@ impl MimeMessage {
decrypted_mail.headers.get_all_values("Autocrypt-Gossip"); decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossiped_addr = gossiped_addr =
update_gossip_peerstates(context, message_time, &mail, gossip_headers) update_gossip_peerstates(context, message_time, &mail, gossip_headers)
.await .await?;
.map_err_sql()?;
} }
// let known protected headers from the decrypted // let known protected headers from the decrypted
@@ -333,10 +310,7 @@ impl MimeMessage {
// && decryption_info.dkim_results.allow_keychange // && decryption_info.dkim_results.allow_keychange
{ {
peerstate.degrade_encryption(message_time); peerstate.degrade_encryption(message_time);
peerstate peerstate.save_to_db(&context.sql).await?;
.save_to_db(&context.sql, false)
.await
.map_err_sql()?;
} }
} }
(Ok(mail), HashSet::new(), false) (Ok(mail), HashSet::new(), false)
@@ -380,15 +354,11 @@ impl MimeMessage {
Some(org_bytes) => { Some(org_bytes) => {
parser parser
.create_stub_from_partial_download(context, org_bytes) .create_stub_from_partial_download(context, org_bytes)
.await .await?;
.map_err_sql()?;
} }
None => match mail { None => match mail {
Ok(mail) => { Ok(mail) => {
parser parser.parse_mime_recursive(context, &mail, false).await?;
.parse_mime_recursive(context, &mail, false)
.await
.map_err_malformed()?;
} }
Err(err) => { Err(err) => {
let msg_body = stock_str::cant_decrypt_msg_body(context).await; let msg_body = stock_str::cant_decrypt_msg_body(context).await;
@@ -409,7 +379,7 @@ impl MimeMessage {
parser.maybe_remove_bad_parts(); parser.maybe_remove_bad_parts();
parser.maybe_remove_inline_mailinglist_footer(); parser.maybe_remove_inline_mailinglist_footer();
parser.heuristically_parse_ndn(context).await; parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context).await.map_err_malformed()?; parser.parse_headers(context).await?;
// Disallowing keychanges is disabled for now // Disallowing keychanges is disabled for now
// if !decryption_info.dkim_results.allow_keychange { // if !decryption_info.dkim_results.allow_keychange {
@@ -427,14 +397,11 @@ impl MimeMessage {
parser.decoded_data = mail_raw; parser.decoded_data = mail_raw;
} }
crate::peerstate::maybe_do_aeap_transition(context, &mut decryption_info, &parser) crate::peerstate::maybe_do_aeap_transition(context, &mut decryption_info, &parser).await?;
.await
.map_err_sql()?;
if let Some(peerstate) = decryption_info.peerstate { if let Some(peerstate) = decryption_info.peerstate {
peerstate peerstate
.handle_fingerprint_change(context, message_time) .handle_fingerprint_change(context, message_time)
.await .await?;
.map_err_sql()?;
} }
Ok(parser) Ok(parser)
@@ -1619,11 +1586,11 @@ async fn update_gossip_peerstates(
let peerstate; let peerstate;
if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? { if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
p.apply_gossip(&header, message_time); p.apply_gossip(&header, message_time);
p.save_to_db(&context.sql, false).await?; p.save_to_db(&context.sql).await?;
peerstate = p; peerstate = p;
} else { } else {
let p = Peerstate::from_gossip(&header, message_time); let p = Peerstate::from_gossip(&header, message_time);
p.save_to_db(&context.sql, true).await?; p.save_to_db(&context.sql).await?;
peerstate = p; peerstate = p;
}; };
peerstate peerstate
@@ -2196,7 +2163,7 @@ mod tests {
let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await; let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await;
assert!(matches!(mimeparser, Err(ParserError::Malformed(_)))); assert!(mimeparser.is_err());
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -4,7 +4,7 @@ use std::collections::HashSet;
use std::fmt; use std::fmt;
use crate::aheader::{Aheader, EncryptPreference}; use crate::aheader::{Aheader, EncryptPreference};
use crate::chat::{self, is_contact_in_chat, Chat}; use crate::chat::{self, Chat};
use crate::chatlist::Chatlist; use crate::chatlist::Chatlist;
use crate::constants::Chattype; use crate::constants::Chattype;
use crate::contact::{addr_cmp, Contact, Origin}; use crate::contact::{addr_cmp, Contact, Origin};
@@ -46,7 +46,6 @@ pub struct Peerstate {
pub gossip_key_fingerprint: Option<Fingerprint>, pub gossip_key_fingerprint: Option<Fingerprint>,
pub verified_key: Option<SignedPublicKey>, pub verified_key: Option<SignedPublicKey>,
pub verified_key_fingerprint: Option<Fingerprint>, pub verified_key_fingerprint: Option<Fingerprint>,
pub to_save: Option<ToSave>,
pub fingerprint_changed: bool, pub fingerprint_changed: bool,
} }
@@ -63,7 +62,6 @@ impl PartialEq for Peerstate {
&& self.gossip_key_fingerprint == other.gossip_key_fingerprint && self.gossip_key_fingerprint == other.gossip_key_fingerprint
&& self.verified_key == other.verified_key && self.verified_key == other.verified_key
&& self.verified_key_fingerprint == other.verified_key_fingerprint && self.verified_key_fingerprint == other.verified_key_fingerprint
&& self.to_save == other.to_save
&& self.fingerprint_changed == other.fingerprint_changed && self.fingerprint_changed == other.fingerprint_changed
} }
} }
@@ -84,19 +82,11 @@ impl fmt::Debug for Peerstate {
.field("gossip_key_fingerprint", &self.gossip_key_fingerprint) .field("gossip_key_fingerprint", &self.gossip_key_fingerprint)
.field("verified_key", &self.verified_key) .field("verified_key", &self.verified_key)
.field("verified_key_fingerprint", &self.verified_key_fingerprint) .field("verified_key_fingerprint", &self.verified_key_fingerprint)
.field("to_save", &self.to_save)
.field("fingerprint_changed", &self.fingerprint_changed) .field("fingerprint_changed", &self.fingerprint_changed)
.finish() .finish()
} }
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum ToSave {
Timestamps = 0x01,
All = 0x02,
}
impl Peerstate { impl Peerstate {
pub fn from_header(header: &Aheader, message_time: i64) -> Self { pub fn from_header(header: &Aheader, message_time: i64) -> Self {
Peerstate { Peerstate {
@@ -111,7 +101,6 @@ impl Peerstate {
gossip_timestamp: 0, gossip_timestamp: 0,
verified_key: None, verified_key: None,
verified_key_fingerprint: None, verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false, fingerprint_changed: false,
} }
} }
@@ -137,7 +126,6 @@ impl Peerstate {
gossip_timestamp: message_time, gossip_timestamp: message_time,
verified_key: None, verified_key: None,
verified_key_fingerprint: None, verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false, fingerprint_changed: false,
} }
} }
@@ -229,7 +217,6 @@ impl Peerstate {
.map(|s| s.parse::<Fingerprint>()) .map(|s| s.parse::<Fingerprint>())
.transpose() .transpose()
.unwrap_or_default(), .unwrap_or_default(),
to_save: None,
fingerprint_changed: false, fingerprint_changed: false,
}; };
@@ -248,14 +235,10 @@ impl Peerstate {
let old_public_fingerprint = self.public_key_fingerprint.take(); let old_public_fingerprint = self.public_key_fingerprint.take();
self.public_key_fingerprint = Some(public_key.fingerprint()); self.public_key_fingerprint = Some(public_key.fingerprint());
if old_public_fingerprint.is_none() if old_public_fingerprint.is_some()
|| self.public_key_fingerprint.is_none() && old_public_fingerprint != self.public_key_fingerprint
|| old_public_fingerprint != self.public_key_fingerprint
{ {
self.to_save = Some(ToSave::All); self.fingerprint_changed = true;
if old_public_fingerprint.is_some() {
self.fingerprint_changed = true;
}
} }
} }
@@ -267,8 +250,6 @@ impl Peerstate {
|| self.gossip_key_fingerprint.is_none() || self.gossip_key_fingerprint.is_none()
|| old_gossip_fingerprint != self.gossip_key_fingerprint || old_gossip_fingerprint != self.gossip_key_fingerprint
{ {
self.to_save = Some(ToSave::All);
// Warn about gossip key change only if there is no public key obtained from // Warn about gossip key change only if there is no public key obtained from
// Autocrypt header, which overrides gossip key. // Autocrypt header, which overrides gossip key.
if old_gossip_fingerprint.is_some() && self.public_key_fingerprint.is_none() { if old_gossip_fingerprint.is_some() && self.public_key_fingerprint.is_none() {
@@ -281,7 +262,6 @@ impl Peerstate {
pub fn degrade_encryption(&mut self, message_time: i64) { pub fn degrade_encryption(&mut self, message_time: i64) {
self.prefer_encrypt = EncryptPreference::Reset; self.prefer_encrypt = EncryptPreference::Reset;
self.last_seen = message_time; self.last_seen = message_time;
self.to_save = Some(ToSave::All);
} }
pub fn apply_header(&mut self, header: &Aheader, message_time: i64) { pub fn apply_header(&mut self, header: &Aheader, message_time: i64) {
@@ -292,19 +272,16 @@ impl Peerstate {
if message_time > self.last_seen { if message_time > self.last_seen {
self.last_seen = message_time; self.last_seen = message_time;
self.last_seen_autocrypt = message_time; self.last_seen_autocrypt = message_time;
self.to_save = Some(ToSave::Timestamps);
if (header.prefer_encrypt == EncryptPreference::Mutual if (header.prefer_encrypt == EncryptPreference::Mutual
|| header.prefer_encrypt == EncryptPreference::NoPreference) || header.prefer_encrypt == EncryptPreference::NoPreference)
&& header.prefer_encrypt != self.prefer_encrypt && header.prefer_encrypt != self.prefer_encrypt
{ {
self.prefer_encrypt = header.prefer_encrypt; self.prefer_encrypt = header.prefer_encrypt;
self.to_save = Some(ToSave::All)
} }
if self.public_key.as_ref() != Some(&header.public_key) { if self.public_key.as_ref() != Some(&header.public_key) {
self.public_key = Some(header.public_key.clone()); self.public_key = Some(header.public_key.clone());
self.recalc_fingerprint(); self.recalc_fingerprint();
self.to_save = Some(ToSave::All);
} }
} }
} }
@@ -316,11 +293,9 @@ impl Peerstate {
if message_time > self.gossip_timestamp { if message_time > self.gossip_timestamp {
self.gossip_timestamp = message_time; self.gossip_timestamp = message_time;
self.to_save = Some(ToSave::Timestamps);
if self.gossip_key.as_ref() != Some(&gossip_header.public_key) { if self.gossip_key.as_ref() != Some(&gossip_header.public_key) {
self.gossip_key = Some(gossip_header.public_key.clone()); self.gossip_key = Some(gossip_header.public_key.clone());
self.recalc_fingerprint(); self.recalc_fingerprint();
self.to_save = Some(ToSave::All)
} }
// This is non-standard. // This is non-standard.
@@ -339,7 +314,6 @@ impl Peerstate {
&& gossip_header.prefer_encrypt == EncryptPreference::Mutual && gossip_header.prefer_encrypt == EncryptPreference::Mutual
{ {
self.prefer_encrypt = EncryptPreference::Mutual; self.prefer_encrypt = EncryptPreference::Mutual;
self.to_save = Some(ToSave::All);
} }
}; };
} }
@@ -395,7 +369,6 @@ impl Peerstate {
if self.public_key_fingerprint.is_some() if self.public_key_fingerprint.is_some()
&& self.public_key_fingerprint.as_ref().unwrap() == fingerprint && self.public_key_fingerprint.as_ref().unwrap() == fingerprint
{ {
self.to_save = Some(ToSave::All);
self.verified_key = self.public_key.clone(); self.verified_key = self.public_key.clone();
self.verified_key_fingerprint = self.public_key_fingerprint.clone(); self.verified_key_fingerprint = self.public_key_fingerprint.clone();
true true
@@ -407,7 +380,6 @@ impl Peerstate {
if self.gossip_key_fingerprint.is_some() if self.gossip_key_fingerprint.is_some()
&& self.gossip_key_fingerprint.as_ref().unwrap() == fingerprint && self.gossip_key_fingerprint.as_ref().unwrap() == fingerprint
{ {
self.to_save = Some(ToSave::All);
self.verified_key = self.gossip_key.clone(); self.verified_key = self.gossip_key.clone();
self.verified_key_fingerprint = self.gossip_key_fingerprint.clone(); self.verified_key_fingerprint = self.gossip_key_fingerprint.clone();
true true
@@ -421,66 +393,48 @@ impl Peerstate {
} }
} }
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> Result<()> { pub async fn save_to_db(&self, sql: &Sql) -> Result<()> {
if self.to_save == Some(ToSave::All) || create { sql.execute(
sql.execute( "INSERT INTO acpeerstates (
if create { last_seen,
"INSERT INTO acpeerstates ( \ last_seen_autocrypt,
last_seen, \ prefer_encrypted,
last_seen_autocrypt, \ public_key,
prefer_encrypted, \ gossip_timestamp,
public_key, \ gossip_key,
gossip_timestamp, \ public_key_fingerprint,
gossip_key, \ gossip_key_fingerprint,
public_key_fingerprint, \ verified_key,
gossip_key_fingerprint, \ verified_key_fingerprint,
verified_key, \ addr)
verified_key_fingerprint, \ VALUES (?,?,?,?,?,?,?,?,?,?,?)
addr \ ON CONFLICT (addr)
) VALUES(?,?,?,?,?,?,?,?,?,?,?)" DO UPDATE SET
} else { last_seen = excluded.last_seen,
"UPDATE acpeerstates \ last_seen_autocrypt = excluded.last_seen_autocrypt,
SET last_seen=?, \ prefer_encrypted = excluded.prefer_encrypted,
last_seen_autocrypt=?, \ public_key = excluded.public_key,
prefer_encrypted=?, \ gossip_timestamp = excluded.gossip_timestamp,
public_key=?, \ gossip_key = excluded.gossip_key,
gossip_timestamp=?, \ public_key_fingerprint = excluded.public_key_fingerprint,
gossip_key=?, \ gossip_key_fingerprint = excluded.gossip_key_fingerprint,
public_key_fingerprint=?, \ verified_key = excluded.verified_key,
gossip_key_fingerprint=?, \ verified_key_fingerprint = excluded.verified_key_fingerprint",
verified_key=?, \ paramsv![
verified_key_fingerprint=? \ self.last_seen,
WHERE addr=?" self.last_seen_autocrypt,
}, self.prefer_encrypt as i64,
paramsv![ self.public_key.as_ref().map(|k| k.to_bytes()),
self.last_seen, self.gossip_timestamp,
self.last_seen_autocrypt, self.gossip_key.as_ref().map(|k| k.to_bytes()),
self.prefer_encrypt as i64, self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.public_key.as_ref().map(|k| k.to_bytes()), self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_timestamp, self.verified_key.as_ref().map(|k| k.to_bytes()),
self.gossip_key.as_ref().map(|k| k.to_bytes()), self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()), self.addr,
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()), ],
self.verified_key.as_ref().map(|k| k.to_bytes()), )
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()), .await?;
self.addr,
],
)
.await?;
} else if self.to_save == Some(ToSave::Timestamps) {
sql.execute(
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
WHERE addr=?;",
paramsv![
self.last_seen,
self.last_seen_autocrypt,
self.gossip_timestamp,
self.addr
],
)
.await?;
}
Ok(()) Ok(())
} }
@@ -520,7 +474,7 @@ impl Peerstate {
let chats = Chatlist::try_load(context, 0, None, Some(contact_id)).await?; let chats = Chatlist::try_load(context, 0, None, Some(contact_id)).await?;
let msg = match &change { let msg = match &change {
PeerstateChange::FingerprintChange => { PeerstateChange::FingerprintChange => {
stock_str::contact_setup_changed(context, self.addr.clone()).await stock_str::contact_setup_changed(context, &self.addr).await
} }
PeerstateChange::Aeap(new_addr) => { PeerstateChange::Aeap(new_addr) => {
let old_contact = Contact::load_from_db(context, contact_id).await?; let old_contact = Contact::load_from_db(context, contact_id).await?;
@@ -569,9 +523,7 @@ impl Peerstate {
let (new_contact_id, _) = let (new_contact_id, _) =
Contact::add_or_lookup(context, "", new_addr, Origin::IncomingUnknownFrom) Contact::add_or_lookup(context, "", new_addr, Origin::IncomingUnknownFrom)
.await?; .await?;
if !is_contact_in_chat(context, *chat_id, new_contact_id).await? { chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id]).await?;
chat::add_to_chat_contacts_table(context, *chat_id, new_contact_id).await?;
}
context.emit_event(EventType::ChatModified(*chat_id)); context.emit_event(EventType::ChatModified(*chat_id));
} }
@@ -651,14 +603,8 @@ pub async fn maybe_do_aeap_transition(
"Internal error: Tried to do an AEAP transition without an autocrypt header??", "Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?; )?;
peerstate.apply_header(header, info.message_time); peerstate.apply_header(header, info.message_time);
peerstate.to_save = Some(ToSave::All);
// We don't know whether a peerstate with this address already existed, or a peerstate.save_to_db(&context.sql).await?;
// new one should be created, so just try both create=false and create=true,
// and if this fails, create=true, one will succeed (this is a very cold path,
// so performance doesn't really matter).
peerstate.save_to_db(&context.sql, true).await?;
peerstate.save_to_db(&context.sql, false).await?;
} }
} }
@@ -710,7 +656,7 @@ mod tests {
let pub_key = alice_keypair().public; let pub_key = alice_keypair().public;
let mut peerstate = Peerstate { let peerstate = Peerstate {
addr: addr.into(), addr: addr.into(),
last_seen: 10, last_seen: 10,
last_seen_autocrypt: 11, last_seen_autocrypt: 11,
@@ -722,12 +668,11 @@ mod tests {
gossip_key_fingerprint: Some(pub_key.fingerprint()), gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()), verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()), verified_key_fingerprint: Some(pub_key.fingerprint()),
to_save: Some(ToSave::All),
fingerprint_changed: false, fingerprint_changed: false,
}; };
assert!( assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(), peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
"failed to save to db" "failed to save to db"
); );
@@ -736,8 +681,6 @@ mod tests {
.expect("failed to load peerstate from db") .expect("failed to load peerstate from db")
.expect("no peerstate found in the database"); .expect("no peerstate found in the database");
// clear to_save, as that is not persissted
peerstate.to_save = None;
assert_eq!(peerstate, peerstate_new); assert_eq!(peerstate, peerstate_new);
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint()) let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
.await .await
@@ -764,16 +707,15 @@ mod tests {
gossip_key_fingerprint: None, gossip_key_fingerprint: None,
verified_key: None, verified_key: None,
verified_key_fingerprint: None, verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false, fingerprint_changed: false,
}; };
assert!( assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(), peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
"failed to save" "failed to save"
); );
assert!( assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(), peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
"double-call with create failed" "double-call with create failed"
); );
} }
@@ -785,7 +727,7 @@ mod tests {
let pub_key = alice_keypair().public; let pub_key = alice_keypair().public;
let mut peerstate = Peerstate { let peerstate = Peerstate {
addr: addr.into(), addr: addr.into(),
last_seen: 10, last_seen: 10,
last_seen_autocrypt: 11, last_seen_autocrypt: 11,
@@ -797,12 +739,11 @@ mod tests {
gossip_key_fingerprint: None, gossip_key_fingerprint: None,
verified_key: None, verified_key: None,
verified_key_fingerprint: None, verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false, fingerprint_changed: false,
}; };
assert!( assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(), peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
"failed to save" "failed to save"
); );
@@ -810,8 +751,6 @@ mod tests {
.await .await
.expect("failed to load peerstate from db"); .expect("failed to load peerstate from db");
// clear to_save, as that is not persissted
peerstate.to_save = None;
assert_eq!(Some(peerstate), peerstate_new); assert_eq!(Some(peerstate), peerstate_new);
} }
@@ -862,7 +801,6 @@ mod tests {
gossip_key_fingerprint: None, gossip_key_fingerprint: None,
verified_key: None, verified_key: None,
verified_key_fingerprint: None, verified_key_fingerprint: None,
to_save: None,
fingerprint_changed: false, fingerprint_changed: false,
}; };
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::NoPreference); assert_eq!(peerstate.prefer_encrypt, EncryptPreference::NoPreference);

View File

@@ -659,7 +659,6 @@ mod tests {
use crate::aheader::EncryptPreference; use crate::aheader::EncryptPreference;
use crate::chat::{create_group_chat, ProtectionStatus}; use crate::chat::{create_group_chat, ProtectionStatus};
use crate::key::DcKey; use crate::key::DcKey;
use crate::peerstate::ToSave;
use crate::securejoin::get_securejoin_qr; use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{alice_keypair, TestContext}; use crate::test_utils::{alice_keypair, TestContext};
use anyhow::Result; use anyhow::Result;
@@ -912,11 +911,10 @@ mod tests {
gossip_key_fingerprint: None, gossip_key_fingerprint: None,
verified_key: None, verified_key: None,
verified_key_fingerprint: None, verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false, fingerprint_changed: false,
}; };
assert!( assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(), peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
"failed to save peerstate" "failed to save peerstate"
); );

View File

@@ -28,7 +28,7 @@ use crate::message::{
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype, self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
}; };
use crate::mimeparser::{ use crate::mimeparser::{
parse_message_ids, AvatarAction, MailinglistType, MimeMessage, ParserError, SystemMessage, parse_message_ids, AvatarAction, MailinglistType, MimeMessage, SystemMessage,
}; };
use crate::param::{Param, Params}; use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus}; use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
@@ -72,15 +72,13 @@ pub async fn receive_imf(
/// Receive a message and add it to the database. /// Receive a message and add it to the database.
/// ///
/// Returns an error on recoverable errors, e.g. database errors. In this case, /// Returns an error on database failure or if the message is broken,
/// message parsing should be retried later. /// e.g. has nonstandard MIME structure.
/// ///
/// If message itself is wrong, logs /// If possible, creates a database entry to prevent the message from being
/// the error and returns success: /// downloaded again, sets `chat_id=DC_CHAT_ID_TRASH` and returns `Ok(Some(…))`.
/// - If possible, creates a database entry to prevent the message from being /// If the message is so wrong that we didn't even create a database entry,
/// downloaded again, sets `chat_id=DC_CHAT_ID_TRASH` and returns `Ok(Some(…))` /// returns `Ok(None)`.
/// - If the message is so wrong that we didn't even create a database entry,
/// returns `Ok(None)`
/// ///
/// If `is_partial_download` is set, it contains the full message size in bytes. /// If `is_partial_download` is set, it contains the full message size in bytes.
/// Do not confuse that with `replace_partial_download` that will be set when the full message is loaded later. /// Do not confuse that with `replace_partial_download` that will be set when the full message is loaded later.
@@ -101,9 +99,8 @@ pub(crate) async fn receive_imf_inner(
let mut mime_parser = let mut mime_parser =
match MimeMessage::from_bytes_with_partial(context, imf_raw, is_partial_download).await { match MimeMessage::from_bytes_with_partial(context, imf_raw, is_partial_download).await {
Err(ParserError::Malformed(err)) => { Err(err) => {
warn!(context, "receive_imf: can't parse MIME: {}", err); warn!(context, "receive_imf: can't parse MIME: {}", err);
let msg_ids; let msg_ids;
if !rfc724_mid.starts_with(GENERATED_PREFIX) { if !rfc724_mid.starts_with(GENERATED_PREFIX) {
let row_id = context let row_id = context
@@ -127,7 +124,6 @@ pub(crate) async fn receive_imf_inner(
needs_delete_job: false, needs_delete_job: false,
})); }));
} }
Err(ParserError::Sql(err)) => return Err(err),
Ok(mime_parser) => mime_parser, Ok(mime_parser) => mime_parser,
}; };
@@ -1514,7 +1510,10 @@ async fn create_or_lookup_group(
let grpname = mime_parser let grpname = mime_parser
.get_header(HeaderDef::ChatGroupName) .get_header(HeaderDef::ChatGroupName)
.context("Chat-Group-Name vanished")?; .context("Chat-Group-Name vanished")?
// W/a for "Space added before long group names after MIME serialization/deserialization
// #3650" issue. DC itself never creates group names with leading/trailing whitespace.
.trim();
let new_chat_id = ChatId::create_multiuser_record( let new_chat_id = ChatId::create_multiuser_record(
context, context,
Chattype::Group, Chattype::Group,
@@ -1531,19 +1530,13 @@ async fn create_or_lookup_group(
chat_id_blocked = create_blocked; chat_id_blocked = create_blocked;
// Create initial member list. // Create initial member list.
chat::add_to_chat_contacts_table(context, new_chat_id, ContactId::SELF).await?; let mut members = vec![ContactId::SELF];
if !from_id.is_special() && !chat::is_contact_in_chat(context, new_chat_id, from_id).await? if !from_id.is_special() {
{ members.push(from_id);
chat::add_to_chat_contacts_table(context, new_chat_id, from_id).await?;
}
for &to_id in to_ids.iter() {
info!(context, "adding to={:?} to chat id={}", to_id, new_chat_id);
if to_id != ContactId::SELF
&& !chat::is_contact_in_chat(context, new_chat_id, to_id).await?
{
chat::add_to_chat_contacts_table(context, new_chat_id, to_id).await?;
}
} }
members.extend(to_ids);
members.dedup();
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
// once, we have protected-chats explained in UI, we can uncomment the following lines. // once, we have protected-chats explained in UI, we can uncomment the following lines.
// ("verified groups" did not add a message anyway) // ("verified groups" did not add a message anyway)
@@ -1622,9 +1615,15 @@ async fn apply_group_changes(
{ {
better_msg = Some(stock_str::msg_add_member(context, &added_member, from_id).await); better_msg = Some(stock_str::msg_add_member(context, &added_member, from_id).await);
recreate_member_list = true; recreate_member_list = true;
} else if let Some(old_name) = mime_parser.get_header(HeaderDef::ChatGroupNameChanged) { } else if let Some(old_name) = mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
// See create_or_lookup_group() for explanation
.map(|s| s.trim())
{
if let Some(grpname) = mime_parser if let Some(grpname) = mime_parser
.get_header(HeaderDef::ChatGroupName) .get_header(HeaderDef::ChatGroupName)
// See create_or_lookup_group() for explanation
.map(|grpname| grpname.trim())
.filter(|grpname| grpname.len() < 200) .filter(|grpname| grpname.len() < 200)
{ {
if chat_id if chat_id
@@ -1693,6 +1692,7 @@ async fn apply_group_changes(
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp) .update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
.await? .await?
{ {
let mut members_to_add = vec![];
if removed_id.is_some() if removed_id.is_some()
|| !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await? || !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
{ {
@@ -1707,26 +1707,23 @@ async fn apply_group_changes(
) )
.await?; .await?;
if removed_id != Some(ContactId::SELF) { members_to_add.push(ContactId::SELF);
chat::add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?;
}
} }
if !from_id.is_special()
&& from_id != ContactId::SELF if !from_id.is_special() {
&& !chat::is_contact_in_chat(context, chat_id, from_id).await? members_to_add.push(from_id);
&& removed_id != Some(from_id)
{
chat::add_to_chat_contacts_table(context, chat_id, from_id).await?;
} }
for &to_id in to_ids.iter() { members_to_add.extend(to_ids);
if to_id != ContactId::SELF if let Some(removed_id) = removed_id {
&& !chat::is_contact_in_chat(context, chat_id, to_id).await? members_to_add.retain(|id| *id != removed_id);
&& removed_id != Some(to_id)
{
info!(context, "adding to={:?} to chat id={}", to_id, chat_id);
chat::add_to_chat_contacts_table(context, chat_id, to_id).await?;
}
} }
members_to_add.dedup();
info!(
context,
"adding {:?} to chat id={}", members_to_add, chat_id
);
chat::add_to_chat_contacts_table(context, chat_id, &members_to_add).await?;
send_event_chat_modified = true; send_event_chat_modified = true;
} }
} }
@@ -1878,7 +1875,7 @@ async fn create_or_lookup_mailinglist(
) )
})?; })?;
chat::add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?; chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
Ok(Some((chat_id, Blocked::Request))) Ok(Some((chat_id, Blocked::Request)))
} else { } else {
info!(context, "creating list forbidden by caller"); info!(context, "creating list forbidden by caller");
@@ -2008,9 +2005,7 @@ async fn create_adhoc_group(
None, None,
) )
.await?; .await?;
for &member_id in member_ids.iter() { chat::add_to_chat_contacts_table(context, new_chat_id, member_ids).await?;
chat::add_to_chat_contacts_table(context, new_chat_id, member_id).await?;
}
context.emit_event(EventType::ChatModified(new_chat_id)); context.emit_event(EventType::ChatModified(new_chat_id));
@@ -2124,7 +2119,7 @@ async fn check_verified_properties(
&fp, &fp,
PeerstateVerifiedStatus::BidirectVerified, PeerstateVerifiedStatus::BidirectVerified,
); );
peerstate.save_to_db(&context.sql, false).await?; peerstate.save_to_db(&context.sql).await?;
is_verified = true; is_verified = true;
} }
} }
@@ -2272,6 +2267,7 @@ mod tests {
use super::*; use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::get_chat_contacts; use crate::chat::get_chat_contacts;
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility}; use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
use crate::chatlist::Chatlist; use crate::chatlist::Chatlist;
@@ -2309,7 +2305,7 @@ mod tests {
\n\ \n\
hello\x00"; hello\x00";
let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await; let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await;
assert!(matches!(mimeparser, Err(ParserError::Malformed(_)))); assert!(mimeparser.is_err());
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -5154,13 +5150,10 @@ Reply from different address
chat::add_to_chat_contacts_table( chat::add_to_chat_contacts_table(
&bob, &bob,
group_id, group_id,
bob.add_or_lookup_contact(&alice1).await.id, &[
) bob.add_or_lookup_contact(&alice1).await.id,
.await?; Contact::create(&bob, "", "charlie@example.org").await?,
chat::add_to_chat_contacts_table( ],
&bob,
group_id,
Contact::create(&bob, "", "charlie@example.org").await?,
) )
.await?; .await?;
@@ -5241,7 +5234,7 @@ Reply from different address
chat::add_to_chat_contacts_table( chat::add_to_chat_contacts_table(
&bob, &bob,
group_id, group_id,
bob.add_or_lookup_contact(&alice).await.id, &[bob.add_or_lookup_contact(&alice).await.id],
) )
.await?; .await?;
@@ -5294,4 +5287,20 @@ Reply from different address
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_thunderbird_autocrypt() -> Result<()> {
let t = TestContext::new_bob().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
let raw = include_bytes!("../test-data/message/thunderbird_with_autocrypt.eml");
receive_imf(&t, raw, false).await?;
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
.await?
.unwrap();
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
Ok(())
}
} }

View File

@@ -12,7 +12,7 @@ use crate::tools::time;
use crate::{config::Config, scheduler::Scheduler, stock_str, tools}; use crate::{config::Config, scheduler::Scheduler, stock_str, tools};
use crate::{context::Context, log::LogExt}; use crate::{context::Context, log::LogExt};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use humansize::{file_size_opts, FileSize}; use humansize::{format_size, BINARY};
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
pub enum Connectivity { pub enum Connectivity {
@@ -448,7 +448,7 @@ impl Context {
// [======67%===== ] // [======67%===== ]
// ============================================================================================= // =============================================================================================
let domain = tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain; let domain = &tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain;
let storage_on_domain = stock_str::storage_on_domain(self, domain).await; let storage_on_domain = stock_str::storage_on_domain(self, domain).await;
ret += &format!("<h3>{}</h3><ul>", storage_on_domain); ret += &format!("<h3>{}</h3><ul>", storage_on_domain);
let quota = self.quota.read().await; let quota = self.quota.read().await;
@@ -473,8 +473,8 @@ impl Context {
let messages = stock_str::messages(self).await; let messages = stock_str::messages(self).await;
let part_of_total_used = stock_str::part_of_total_used( let part_of_total_used = stock_str::part_of_total_used(
self, self,
resource.usage.to_string(), &resource.usage.to_string(),
resource.limit.to_string(), &resource.limit.to_string(),
) )
.await; .await;
ret += &match &resource.name { ret += &match &resource.name {
@@ -495,12 +495,8 @@ impl Context {
// - the string is not longer than the other strings that way (minus title, plus units) - // - the string is not longer than the other strings that way (minus title, plus units) -
// additional linebreaks on small displays are unlikely therefore // additional linebreaks on small displays are unlikely therefore
// - most times, this is the only item anyway // - most times, this is the only item anyway
let usage = (resource.usage * 1024) let usage = &format_size(resource.usage * 1024, BINARY);
.file_size(file_size_opts::BINARY) let limit = &format_size(resource.limit * 1024, BINARY);
.unwrap_or_default();
let limit = (resource.limit * 1024)
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
stock_str::part_of_total_used(self, usage, limit).await stock_str::part_of_total_used(self, usage, limit).await
} }
}; };

View File

@@ -18,7 +18,7 @@ use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::message::{Message, Viewtype}; use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param; use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToSave}; use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::qr::check_qr; use crate::qr::check_qr;
use crate::stock_str; use crate::stock_str;
use crate::token; use crate::token;
@@ -640,11 +640,7 @@ async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) ->
PeerstateVerifiedStatus::BidirectVerified, PeerstateVerifiedStatus::BidirectVerified,
) { ) {
peerstate.prefer_encrypt = EncryptPreference::Mutual; peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.to_save = Some(ToSave::All); peerstate.save_to_db(&context.sql).await.unwrap_or_default();
peerstate
.save_to_db(&context.sql, false)
.await
.unwrap_or_default();
return Ok(()); return Ok(());
} }
} }
@@ -932,10 +928,9 @@ mod tests {
gossip_key_fingerprint: Some(alice_pubkey.fingerprint()), gossip_key_fingerprint: Some(alice_pubkey.fingerprint()),
verified_key: None, verified_key: None,
verified_key_fingerprint: None, verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false, fingerprint_changed: false,
}; };
peerstate.save_to_db(&bob.ctx.sql, true).await?; peerstate.save_to_db(&bob.ctx.sql).await?;
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact // Step 1: Generate QR-code, ChatId(0) indicates setup-contact
let qr = get_securejoin_qr(&alice.ctx, None).await?; let qr = get_securejoin_qr(&alice.ctx, None).await?;

View File

@@ -60,7 +60,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// TODO: how does this group become usable? // TODO: how does this group become usable?
let group_chat_id = state.joining_chat_id(context).await?; let group_chat_id = state.joining_chat_id(context).await?;
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? { if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
chat::add_to_chat_contacts_table(context, group_chat_id, invite.contact_id()) chat::add_to_chat_contacts_table(context, group_chat_id, &[invite.contact_id()])
.await?; .await?;
} }
let msg = stock_str::secure_join_started(context, invite.contact_id()).await; let msg = stock_str::secure_join_started(context, invite.contact_id()).await;

View File

@@ -280,7 +280,7 @@ impl Sql {
for addr in &addrs { for addr in &addrs {
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? { if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
peerstate.recalc_fingerprint(); peerstate.recalc_fingerprint();
peerstate.save_to_db(self, false).await?; peerstate.save_to_db(self).await?;
} }
} }
} }
@@ -432,7 +432,7 @@ impl Sql {
pub async fn transaction<G, H>(&self, callback: G) -> Result<H> pub async fn transaction<G, H>(&self, callback: G) -> Result<H>
where where
H: Send + 'static, H: Send + 'static,
G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>, G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
{ {
let mut conn = self.get_conn().await?; let mut conn = self.get_conn().await?;
tokio::task::block_in_place(move || { tokio::task::block_in_place(move || {
@@ -528,9 +528,7 @@ impl Sql {
/// ///
/// Setting `None` deletes the value. On failure an error message /// Setting `None` deletes the value. On failure an error message
/// will already have been logged. /// will already have been logged.
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> { pub async fn set_raw_config(&self, key: &str, value: Option<&str>) -> Result<()> {
let key = key.as_ref();
let mut lock = self.config_cache.write().await; let mut lock = self.config_cache.write().await;
if let Some(value) = value { if let Some(value) = value {
let exists = self let exists = self
@@ -564,9 +562,9 @@ impl Sql {
} }
/// Get configuration options from the database. /// Get configuration options from the database.
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Result<Option<String>> { pub async fn get_raw_config(&self, key: &str) -> Result<Option<String>> {
let lock = self.config_cache.read().await; let lock = self.config_cache.read().await;
let cached = lock.get(key.as_ref()).cloned(); let cached = lock.get(key).cloned();
drop(lock); drop(lock);
if let Some(c) = cached { if let Some(c) = cached {
@@ -575,48 +573,42 @@ impl Sql {
let mut lock = self.config_cache.write().await; let mut lock = self.config_cache.write().await;
let value = self let value = self
.query_get_value( .query_get_value("SELECT value FROM config WHERE keyname=?;", paramsv![key])
"SELECT value FROM config WHERE keyname=?;",
paramsv![key.as_ref()],
)
.await .await
.context(format!("failed to fetch raw config: {}", key.as_ref()))?; .context(format!("failed to fetch raw config: {}", key))?;
lock.insert(key.as_ref().to_string(), value.clone()); lock.insert(key.to_string(), value.clone());
drop(lock); drop(lock);
Ok(value) Ok(value)
} }
pub async fn set_raw_config_int(&self, key: impl AsRef<str>, value: i32) -> Result<()> { pub async fn set_raw_config_int(&self, key: &str, value: i32) -> Result<()> {
self.set_raw_config(key, Some(&format!("{}", value))).await self.set_raw_config(key, Some(&format!("{}", value))).await
} }
pub async fn get_raw_config_int(&self, key: impl AsRef<str>) -> Result<Option<i32>> { pub async fn get_raw_config_int(&self, key: &str) -> Result<Option<i32>> {
self.get_raw_config(key) self.get_raw_config(key)
.await .await
.map(|s| s.and_then(|s| s.parse().ok())) .map(|s| s.and_then(|s| s.parse().ok()))
} }
pub async fn get_raw_config_bool(&self, key: impl AsRef<str>) -> Result<bool> { pub async fn get_raw_config_bool(&self, key: &str) -> Result<bool> {
// Not the most obvious way to encode bool as string, but it is matter // Not the most obvious way to encode bool as string, but it is matter
// of backward compatibility. // of backward compatibility.
let res = self.get_raw_config_int(key).await?; let res = self.get_raw_config_int(key).await?;
Ok(res.unwrap_or_default() > 0) Ok(res.unwrap_or_default() > 0)
} }
pub async fn set_raw_config_bool<T>(&self, key: T, value: bool) -> Result<()> pub async fn set_raw_config_bool(&self, key: &str, value: bool) -> Result<()> {
where
T: AsRef<str>,
{
let value = if value { Some("1") } else { None }; let value = if value { Some("1") } else { None };
self.set_raw_config(key, value).await self.set_raw_config(key, value).await
} }
pub async fn set_raw_config_int64(&self, key: impl AsRef<str>, value: i64) -> Result<()> { pub async fn set_raw_config_int64(&self, key: &str, value: i64) -> Result<()> {
self.set_raw_config(key, Some(&format!("{}", value))).await self.set_raw_config(key, Some(&format!("{}", value))).await
} }
pub async fn get_raw_config_int64(&self, key: impl AsRef<str>) -> Result<Option<i64>> { pub async fn get_raw_config_int64(&self, key: &str) -> Result<Option<i64>> {
self.get_raw_config(key) self.get_raw_config(key)
.await .await
.map(|s| s.and_then(|r| r.parse().ok())) .map(|s| s.and_then(|r| r.parse().ok()))
@@ -728,7 +720,7 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|row| row.get::<_, String>(0), |row| row.get::<_, String>(0),
|rows| { |rows| {
for row in rows { for row in rows {
maybe_add_file(&mut files_in_use, row?); maybe_add_file(&mut files_in_use, &row?);
} }
Ok(()) Ok(())
}, },
@@ -819,8 +811,8 @@ fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, nam
files_in_use.contains(name_to_check) files_in_use.contains(name_to_check)
} }
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: impl AsRef<str>) { fn maybe_add_file(files_in_use: &mut HashSet<String>, file: &str) {
if let Some(file) = file.as_ref().strip_prefix("$BLOBDIR/") { if let Some(file) = file.strip_prefix("$BLOBDIR/") {
files_in_use.insert(file.to_string()); files_in_use.insert(file.to_string());
} }
} }

View File

@@ -320,11 +320,11 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
if dbversion < 67 { if dbversion < 67 {
for prefix in &["", "configured_"] { for prefix in &["", "configured_"] {
if let Some(server_flags) = sql if let Some(server_flags) = sql
.get_raw_config_int(format!("{}server_flags", prefix)) .get_raw_config_int(&format!("{}server_flags", prefix))
.await? .await?
{ {
let imap_socket_flags = server_flags & 0x700; let imap_socket_flags = server_flags & 0x700;
let key = format!("{}mail_security", prefix); let key = &format!("{}mail_security", prefix);
match imap_socket_flags { match imap_socket_flags {
0x100 => sql.set_raw_config_int(key, 2).await?, // STARTTLS 0x100 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
0x200 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS 0x200 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
@@ -332,7 +332,7 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
_ => sql.set_raw_config_int(key, 0).await?, _ => sql.set_raw_config_int(key, 0).await?,
} }
let smtp_socket_flags = server_flags & 0x70000; let smtp_socket_flags = server_flags & 0x70000;
let key = format!("{}send_security", prefix); let key = &format!("{}send_security", prefix);
match smtp_socket_flags { match smtp_socket_flags {
0x10000 => sql.set_raw_config_int(key, 2).await?, // STARTTLS 0x10000 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
0x20000 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS 0x20000 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
@@ -616,6 +616,50 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
) )
.await?; .await?;
} }
if dbversion < 94 {
sql.execute_migration(
// Create new `acpeerstates` table, same as before but with unique constraint.
//
// This allows to use `UPSERT` to update existing or insert a new peerstate
// depending on whether one exists already.
"CREATE TABLE new_acpeerstates (
id INTEGER PRIMARY KEY,
addr TEXT DEFAULT '' COLLATE NOCASE,
last_seen INTEGER DEFAULT 0,
last_seen_autocrypt INTEGER DEFAULT 0,
public_key,
prefer_encrypted INTEGER DEFAULT 0,
gossip_timestamp INTEGER DEFAULT 0,
gossip_key,
public_key_fingerprint TEXT DEFAULT '',
gossip_key_fingerprint TEXT DEFAULT '',
verified_key,
verified_key_fingerprint TEXT DEFAULT '',
UNIQUE (addr) -- Only one peerstate per address
);
INSERT OR IGNORE INTO new_acpeerstates SELECT * FROM acpeerstates;
DROP TABLE acpeerstates;
ALTER TABLE new_acpeerstates RENAME TO acpeerstates;
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
",
94,
)
.await?;
}
if dbversion < 95 {
sql.execute_migration(
"CREATE TABLE new_chats_contacts (chat_id INTEGER, contact_id INTEGER, UNIQUE(chat_id, contact_id));\
INSERT OR IGNORE INTO new_chats_contacts SELECT * FROM chats_contacts;\
DROP TABLE chats_contacts;\
ALTER TABLE new_chats_contacts RENAME TO chats_contacts;\
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);\
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);",
95
).await?;
}
let new_version = sql let new_version = sql
.get_raw_config_int(VERSION_CFG) .get_raw_config_int(VERSION_CFG)

View File

@@ -17,7 +17,7 @@ use crate::context::Context;
use crate::message::{Message, Viewtype}; use crate::message::{Message, Viewtype};
use crate::param::Param; use crate::param::Param;
use crate::tools::timestamp_to_str; use crate::tools::timestamp_to_str;
use humansize::{file_size_opts, FileSize}; use humansize::{format_size, BINARY};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StockStrings { pub struct StockStrings {
@@ -466,33 +466,33 @@ async fn translated(context: &Context, id: StockMessage) -> String {
/// Helper trait only meant to be implemented for [`String`]. /// Helper trait only meant to be implemented for [`String`].
trait StockStringMods: AsRef<str> + Sized { trait StockStringMods: AsRef<str> + Sized {
/// Substitutes the first replacement value if one is present. /// Substitutes the first replacement value if one is present.
fn replace1(&self, replacement: impl AsRef<str>) -> String { fn replace1(&self, replacement: &str) -> String {
self.as_ref() self.as_ref()
.replacen("%1$s", replacement.as_ref(), 1) .replacen("%1$s", replacement, 1)
.replacen("%1$d", replacement.as_ref(), 1) .replacen("%1$d", replacement, 1)
.replacen("%1$@", replacement.as_ref(), 1) .replacen("%1$@", replacement, 1)
} }
/// Substitutes the second replacement value if one is present. /// Substitutes the second replacement value if one is present.
/// ///
/// Be aware you probably should have also called [`StockStringMods::replace1`] if /// Be aware you probably should have also called [`StockStringMods::replace1`] if
/// you are calling this. /// you are calling this.
fn replace2(&self, replacement: impl AsRef<str>) -> String { fn replace2(&self, replacement: &str) -> String {
self.as_ref() self.as_ref()
.replacen("%2$s", replacement.as_ref(), 1) .replacen("%2$s", replacement, 1)
.replacen("%2$d", replacement.as_ref(), 1) .replacen("%2$d", replacement, 1)
.replacen("%2$@", replacement.as_ref(), 1) .replacen("%2$@", replacement, 1)
} }
/// Substitutes the third replacement value if one is present. /// Substitutes the third replacement value if one is present.
/// ///
/// Be aware you probably should have also called [`StockStringMods::replace1`] and /// Be aware you probably should have also called [`StockStringMods::replace1`] and
/// [`StockStringMods::replace2`] if you are calling this. /// [`StockStringMods::replace2`] if you are calling this.
fn replace3(&self, replacement: impl AsRef<str>) -> String { fn replace3(&self, replacement: &str) -> String {
self.as_ref() self.as_ref()
.replacen("%3$s", replacement.as_ref(), 1) .replacen("%3$s", replacement, 1)
.replacen("%3$d", replacement.as_ref(), 1) .replacen("%3$d", replacement, 1)
.replacen("%3$@", replacement.as_ref(), 1) .replacen("%3$@", replacement, 1)
} }
} }
@@ -551,8 +551,8 @@ pub(crate) async fn file(context: &Context) -> String {
/// Stock string: `Group name changed from "%1$s" to "%2$s".`. /// Stock string: `Group name changed from "%1$s" to "%2$s".`.
pub(crate) async fn msg_grp_name( pub(crate) async fn msg_grp_name(
context: &Context, context: &Context,
from_group: impl AsRef<str>, from_group: &str,
to_group: impl AsRef<str>, to_group: &str,
by_contact: ContactId, by_contact: ContactId,
) -> String { ) -> String {
if by_contact == ContactId::SELF { if by_contact == ContactId::SELF {
@@ -565,7 +565,7 @@ pub(crate) async fn msg_grp_name(
.await .await
.replace1(from_group) .replace1(from_group)
.replace2(to_group) .replace2(to_group)
.replace3(by_contact.get_stock_name(context).await) .replace3(&by_contact.get_stock_name(context).await)
} }
} }
@@ -575,7 +575,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
} else { } else {
translated(context, StockMessage::MsgGrpImgChangedBy) translated(context, StockMessage::MsgGrpImgChangedBy)
.await .await
.replace1(by_contact.get_stock_name(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -585,11 +585,11 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
/// contacts to combine with the display name. /// contacts to combine with the display name.
pub(crate) async fn msg_add_member( pub(crate) async fn msg_add_member(
context: &Context, context: &Context,
added_member_addr: impl AsRef<str>, added_member_addr: &str,
by_contact: ContactId, by_contact: ContactId,
) -> String { ) -> String {
let addr = added_member_addr.as_ref(); let addr = added_member_addr;
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { let who = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await .await
.map(|contact| contact.get_name_n_addr()) .map(|contact| contact.get_name_n_addr())
@@ -604,7 +604,7 @@ pub(crate) async fn msg_add_member(
translated(context, StockMessage::MsgAddMemberBy) translated(context, StockMessage::MsgAddMemberBy)
.await .await
.replace1(who) .replace1(who)
.replace2(by_contact.get_stock_name(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
@@ -614,11 +614,11 @@ pub(crate) async fn msg_add_member(
/// the contacts to combine with the display name. /// the contacts to combine with the display name.
pub(crate) async fn msg_del_member( pub(crate) async fn msg_del_member(
context: &Context, context: &Context,
removed_member_addr: impl AsRef<str>, removed_member_addr: &str,
by_contact: ContactId, by_contact: ContactId,
) -> String { ) -> String {
let addr = removed_member_addr.as_ref(); let addr = removed_member_addr;
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { let who = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await .await
.map(|contact| contact.get_name_n_addr()) .map(|contact| contact.get_name_n_addr())
@@ -633,7 +633,7 @@ pub(crate) async fn msg_del_member(
translated(context, StockMessage::MsgDelMemberBy) translated(context, StockMessage::MsgDelMemberBy)
.await .await
.replace1(who) .replace1(who)
.replace2(by_contact.get_stock_name(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
@@ -644,7 +644,7 @@ pub(crate) async fn msg_group_left(context: &Context, by_contact: ContactId) ->
} else { } else {
translated(context, StockMessage::MsgGroupLeftBy) translated(context, StockMessage::MsgGroupLeftBy)
.await .await
.replace1(by_contact.get_stock_name(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -684,7 +684,7 @@ pub(crate) async fn read_rcpt(context: &Context) -> String {
} }
/// Stock string: `This is a return receipt for the message "%1$s".`. /// Stock string: `This is a return receipt for the message "%1$s".`.
pub(crate) async fn read_rcpt_mail_body(context: &Context, message: impl AsRef<str>) -> String { pub(crate) async fn read_rcpt_mail_body(context: &Context, message: &str) -> String {
translated(context, StockMessage::ReadRcptMailBody) translated(context, StockMessage::ReadRcptMailBody)
.await .await
.replace1(message) .replace1(message)
@@ -697,7 +697,7 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId
} else { } else {
translated(context, StockMessage::MsgGrpImgDeletedBy) translated(context, StockMessage::MsgGrpImgDeletedBy)
.await .await
.replace1(by_contact.get_stock_name(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -714,7 +714,7 @@ pub(crate) async fn secure_join_started(
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await { if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinStarted) translated(context, StockMessage::SecureJoinStarted)
.await .await
.replace1(contact.get_name_n_addr()) .replace1(&contact.get_name_n_addr())
.replace2(contact.get_display_name()) .replace2(contact.get_display_name())
} else { } else {
format!( format!(
@@ -741,7 +741,7 @@ pub(crate) async fn setup_contact_qr_description(
display_name: &str, display_name: &str,
addr: &str, addr: &str,
) -> String { ) -> String {
let name = if display_name == addr { let name = &if display_name == addr {
addr.to_owned() addr.to_owned()
} else { } else {
format!("{} ({})", display_name, addr) format!("{} ({})", display_name, addr)
@@ -760,7 +760,7 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C
/// Stock string: `%1$s verified.`. /// Stock string: `%1$s verified.`.
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String { pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
let addr = contact.get_name_n_addr(); let addr = &contact.get_name_n_addr();
translated(context, StockMessage::ContactVerified) translated(context, StockMessage::ContactVerified)
.await .await
.replace1(addr) .replace1(addr)
@@ -768,17 +768,14 @@ pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> St
/// Stock string: `Cannot verify %1$s`. /// Stock string: `Cannot verify %1$s`.
pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String { pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
let addr = contact.get_name_n_addr(); let addr = &contact.get_name_n_addr();
translated(context, StockMessage::ContactNotVerified) translated(context, StockMessage::ContactNotVerified)
.await .await
.replace1(addr) .replace1(addr)
} }
/// Stock string: `Changed setup for %1$s`. /// Stock string: `Changed setup for %1$s`.
pub(crate) async fn contact_setup_changed( pub(crate) async fn contact_setup_changed(context: &Context, contact_addr: &str) -> String {
context: &Context,
contact_addr: impl AsRef<str>,
) -> String {
translated(context, StockMessage::ContactSetupChanged) translated(context, StockMessage::ContactSetupChanged)
.await .await
.replace1(contact_addr) .replace1(contact_addr)
@@ -810,7 +807,7 @@ pub(crate) async fn sync_msg_body(context: &Context) -> String {
} }
/// Stock string: `Cannot login as \"%1$s\". Please check...`. /// Stock string: `Cannot login as \"%1$s\". Please check...`.
pub(crate) async fn cannot_login(context: &Context, user: impl AsRef<str>) -> String { pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
translated(context, StockMessage::CannotLogin) translated(context, StockMessage::CannotLogin)
.await .await
.replace1(user) .replace1(user)
@@ -828,7 +825,7 @@ pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactI
} else { } else {
translated(context, StockMessage::MsgLocationEnabledBy) translated(context, StockMessage::MsgLocationEnabledBy)
.await .await
.replace1(contact.get_stock_name(context).await) .replace1(&contact.get_stock_name(context).await)
} }
} }
@@ -874,17 +871,14 @@ pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
/// Stock string: `Message from %1$s`. /// Stock string: `Message from %1$s`.
// TODO: This can compute `self_name` itself instead of asking the caller to do this. // TODO: This can compute `self_name` itself instead of asking the caller to do this.
pub(crate) async fn subject_for_new_contact( pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
context: &Context,
self_name: impl AsRef<str>,
) -> String {
translated(context, StockMessage::SubjectForNewContact) translated(context, StockMessage::SubjectForNewContact)
.await .await
.replace1(self_name) .replace1(self_name)
} }
/// Stock string: `Failed to send message to %1$s.`. /// Stock string: `Failed to send message to %1$s.`.
pub(crate) async fn failed_sending_to(context: &Context, name: impl AsRef<str>) -> String { pub(crate) async fn failed_sending_to(context: &Context, name: &str) -> String {
translated(context, StockMessage::FailedSendingTo) translated(context, StockMessage::FailedSendingTo)
.await .await
.replace1(name) .replace1(name)
@@ -900,14 +894,14 @@ pub(crate) async fn msg_ephemeral_timer_disabled(
} else { } else {
translated(context, StockMessage::MsgEphemeralTimerDisabledBy) translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
.await .await
.replace1(by_contact.get_stock_name(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
/// Stock string: `Message deletion timer is set to %1$s s.`. /// Stock string: `Message deletion timer is set to %1$s s.`.
pub(crate) async fn msg_ephemeral_timer_enabled( pub(crate) async fn msg_ephemeral_timer_enabled(
context: &Context, context: &Context,
timer: impl AsRef<str>, timer: &str,
by_contact: ContactId, by_contact: ContactId,
) -> String { ) -> String {
if by_contact == ContactId::SELF { if by_contact == ContactId::SELF {
@@ -918,7 +912,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
translated(context, StockMessage::MsgEphemeralTimerEnabledBy) translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
.await .await
.replace1(timer) .replace1(timer)
.replace2(by_contact.get_stock_name(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
@@ -929,7 +923,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: Co
} else { } else {
translated(context, StockMessage::MsgEphemeralTimerMinuteBy) translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
.await .await
.replace1(by_contact.get_stock_name(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -940,7 +934,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont
} else { } else {
translated(context, StockMessage::MsgEphemeralTimerHourBy) translated(context, StockMessage::MsgEphemeralTimerHourBy)
.await .await
.replace1(by_contact.get_stock_name(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -951,7 +945,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta
} else { } else {
translated(context, StockMessage::MsgEphemeralTimerDayBy) translated(context, StockMessage::MsgEphemeralTimerDayBy)
.await .await
.replace1(by_contact.get_stock_name(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -962,7 +956,7 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont
} else { } else {
translated(context, StockMessage::MsgEphemeralTimerWeekBy) translated(context, StockMessage::MsgEphemeralTimerWeekBy)
.await .await
.replace1(by_contact.get_stock_name(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -972,14 +966,14 @@ pub(crate) async fn videochat_invitation(context: &Context) -> String {
} }
/// Stock string: `You are invited to a video chat, click %1$s to join.`. /// Stock string: `You are invited to a video chat, click %1$s to join.`.
pub(crate) async fn videochat_invite_msg_body(context: &Context, url: impl AsRef<str>) -> String { pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> String {
translated(context, StockMessage::VideochatInviteMsgBody) translated(context, StockMessage::VideochatInviteMsgBody)
.await .await
.replace1(url) .replace1(url)
} }
/// Stock string: `Error:\n\n“%1$s”`. /// Stock string: `Error:\n\n“%1$s”`.
pub(crate) async fn configuration_failed(context: &Context, details: impl AsRef<str>) -> String { pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
translated(context, StockMessage::ConfigurationFailed) translated(context, StockMessage::ConfigurationFailed)
.await .await
.replace1(details) .replace1(details)
@@ -987,7 +981,7 @@ pub(crate) async fn configuration_failed(context: &Context, details: impl AsRef<
/// Stock string: `⚠️ Date or time of your device seem to be inaccurate (%1$s)...`. /// Stock string: `⚠️ Date or time of your device seem to be inaccurate (%1$s)...`.
// TODO: This could compute now itself. // TODO: This could compute now itself.
pub(crate) async fn bad_time_msg_body(context: &Context, now: impl AsRef<str>) -> String { pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
translated(context, StockMessage::BadTimeMsgBody) translated(context, StockMessage::BadTimeMsgBody)
.await .await
.replace1(now) .replace1(now)
@@ -1010,7 +1004,7 @@ pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId)
} else { } else {
translated(context, StockMessage::ProtectionEnabledBy) translated(context, StockMessage::ProtectionEnabledBy)
.await .await
.replace1(by_contact.get_stock_name(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1021,7 +1015,7 @@ pub(crate) async fn protection_disabled(context: &Context, by_contact: ContactId
} else { } else {
translated(context, StockMessage::ProtectionDisabledBy) translated(context, StockMessage::ProtectionDisabledBy)
.await .await
.replace1(by_contact.get_stock_name(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1043,7 +1037,7 @@ pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
/// Stock string: `Message deletion timer is set to %1$s minutes.`. /// Stock string: `Message deletion timer is set to %1$s minutes.`.
pub(crate) async fn msg_ephemeral_timer_minutes( pub(crate) async fn msg_ephemeral_timer_minutes(
context: &Context, context: &Context,
minutes: impl AsRef<str>, minutes: &str,
by_contact: ContactId, by_contact: ContactId,
) -> String { ) -> String {
if by_contact == ContactId::SELF { if by_contact == ContactId::SELF {
@@ -1054,14 +1048,14 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
translated(context, StockMessage::MsgEphemeralTimerMinutesBy) translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
.await .await
.replace1(minutes) .replace1(minutes)
.replace2(by_contact.get_stock_name(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
/// Stock string: `Message deletion timer is set to %1$s hours.`. /// Stock string: `Message deletion timer is set to %1$s hours.`.
pub(crate) async fn msg_ephemeral_timer_hours( pub(crate) async fn msg_ephemeral_timer_hours(
context: &Context, context: &Context,
hours: impl AsRef<str>, hours: &str,
by_contact: ContactId, by_contact: ContactId,
) -> String { ) -> String {
if by_contact == ContactId::SELF { if by_contact == ContactId::SELF {
@@ -1072,14 +1066,14 @@ pub(crate) async fn msg_ephemeral_timer_hours(
translated(context, StockMessage::MsgEphemeralTimerHoursBy) translated(context, StockMessage::MsgEphemeralTimerHoursBy)
.await .await
.replace1(hours) .replace1(hours)
.replace2(by_contact.get_stock_name(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
/// Stock string: `Message deletion timer is set to %1$s days.`. /// Stock string: `Message deletion timer is set to %1$s days.`.
pub(crate) async fn msg_ephemeral_timer_days( pub(crate) async fn msg_ephemeral_timer_days(
context: &Context, context: &Context,
days: impl AsRef<str>, days: &str,
by_contact: ContactId, by_contact: ContactId,
) -> String { ) -> String {
if by_contact == ContactId::SELF { if by_contact == ContactId::SELF {
@@ -1090,14 +1084,14 @@ pub(crate) async fn msg_ephemeral_timer_days(
translated(context, StockMessage::MsgEphemeralTimerDaysBy) translated(context, StockMessage::MsgEphemeralTimerDaysBy)
.await .await
.replace1(days) .replace1(days)
.replace2(by_contact.get_stock_name(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
/// Stock string: `Message deletion timer is set to %1$s weeks.`. /// Stock string: `Message deletion timer is set to %1$s weeks.`.
pub(crate) async fn msg_ephemeral_timer_weeks( pub(crate) async fn msg_ephemeral_timer_weeks(
context: &Context, context: &Context,
weeks: impl AsRef<str>, weeks: &str,
by_contact: ContactId, by_contact: ContactId,
) -> String { ) -> String {
if by_contact == ContactId::SELF { if by_contact == ContactId::SELF {
@@ -1108,7 +1102,7 @@ pub(crate) async fn msg_ephemeral_timer_weeks(
translated(context, StockMessage::MsgEphemeralTimerWeeksBy) translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
.await .await
.replace1(weeks) .replace1(weeks)
.replace2(by_contact.get_stock_name(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1121,15 +1115,13 @@ pub(crate) async fn forwarded(context: &Context) -> String {
pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String { pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
translated(context, StockMessage::QuotaExceedingMsgBody) translated(context, StockMessage::QuotaExceedingMsgBody)
.await .await
.replace1(format!("{}", highest_usage)) .replace1(&format!("{}", highest_usage))
.replace("%%", "%") .replace("%%", "%")
} }
/// Stock string: `%1$s message` with placeholder replaced by human-readable size. /// Stock string: `%1$s message` with placeholder replaced by human-readable size.
pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String { pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
let size = org_bytes let size = &format_size(org_bytes, BINARY);
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
translated(context, StockMessage::PartialDownloadMsgBody) translated(context, StockMessage::PartialDownloadMsgBody)
.await .await
.replace1(size) .replace1(size)
@@ -1139,7 +1131,7 @@ pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32)
pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String { pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
translated(context, StockMessage::DownloadAvailability) translated(context, StockMessage::DownloadAvailability)
.await .await
.replace1(timestamp_to_str(timestamp)) .replace1(&timestamp_to_str(timestamp))
} }
/// Stock string: `Incoming Messages`. /// Stock string: `Incoming Messages`.
@@ -1154,7 +1146,7 @@ pub(crate) async fn outgoing_messages(context: &Context) -> String {
/// Stock string: `Storage on %1$s`. /// Stock string: `Storage on %1$s`.
/// `%1$s` will be replaced by the domain of the configured email-address. /// `%1$s` will be replaced by the domain of the configured email-address.
pub(crate) async fn storage_on_domain(context: &Context, domain: impl AsRef<str>) -> String { pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
translated(context, StockMessage::StorageOnDomain) translated(context, StockMessage::StorageOnDomain)
.await .await
.replace1(domain) .replace1(domain)
@@ -1192,7 +1184,7 @@ pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
/// Stock string: `Error: %1$s…`. /// Stock string: `Error: %1$s…`.
/// `%1$s` will be replaced by a possibly more detailed, typically english, error description. /// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
pub(crate) async fn error(context: &Context, error: impl AsRef<str>) -> String { pub(crate) async fn error(context: &Context, error: &str) -> String {
translated(context, StockMessage::Error) translated(context, StockMessage::Error)
.await .await
.replace1(error) .replace1(error)
@@ -1210,11 +1202,7 @@ pub(crate) async fn messages(context: &Context) -> String {
} }
/// Stock string: `%1$s of %2$s used`. /// Stock string: `%1$s of %2$s used`.
pub(crate) async fn part_of_total_used( pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
context: &Context,
part: impl AsRef<str>,
total: impl AsRef<str>,
) -> String {
translated(context, StockMessage::PartOfTotallUsed) translated(context, StockMessage::PartOfTotallUsed)
.await .await
.replace1(part) .replace1(part)
@@ -1230,9 +1218,9 @@ pub(crate) async fn broadcast_list(context: &Context) -> String {
/// Stock string: `%1$s changed their address from %2$s to %3$s`. /// Stock string: `%1$s changed their address from %2$s to %3$s`.
pub(crate) async fn aeap_addr_changed( pub(crate) async fn aeap_addr_changed(
context: &Context, context: &Context,
contact_name: impl AsRef<str>, contact_name: &str,
old_addr: impl AsRef<str>, old_addr: &str,
new_addr: impl AsRef<str>, new_addr: &str,
) -> String { ) -> String {
translated(context, StockMessage::AeapAddrChanged) translated(context, StockMessage::AeapAddrChanged)
.await .await
@@ -1243,8 +1231,8 @@ pub(crate) async fn aeap_addr_changed(
pub(crate) async fn aeap_explanation_and_link( pub(crate) async fn aeap_explanation_and_link(
context: &Context, context: &Context,
old_addr: impl AsRef<str>, old_addr: &str,
new_addr: impl AsRef<str>, new_addr: &str,
) -> String { ) -> String {
translated(context, StockMessage::AeapExplanationAndLink) translated(context, StockMessage::AeapExplanationAndLink)
.await .await

View File

@@ -979,7 +979,7 @@ fn print_event(event: &Event) {
/// Logs an individual message to stdout. /// Logs an individual message to stdout.
/// ///
/// This includes a bunch of the message meta-data as well. /// This includes a bunch of the message meta-data as well.
async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) { async fn log_msg(context: &Context, prefix: &str, msg: &Message) {
let contact = match Contact::get_by_id(context, msg.get_from_id()).await { let contact = match Contact::get_by_id(context, msg.get_from_id()).await {
Ok(contact) => contact, Ok(contact) => contact,
Err(e) => { Err(e) => {
@@ -1001,7 +1001,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let msgtext = msg.get_text(); let msgtext = msg.get_text();
println!( println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}", "{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}",
prefix.as_ref(), prefix,
msg.get_id(), msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" }, if msg.get_showpadlock() { "🔒" } else { "" },
if msg.has_location() { "📍" } else { "" }, if msg.has_location() { "📍" } else { "" },

View File

@@ -341,9 +341,8 @@ async fn mark_as_verified(this: &TestContext, other: &TestContext) {
peerstate.verified_key = peerstate.public_key.clone(); peerstate.verified_key = peerstate.public_key.clone();
peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone(); peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone();
peerstate.to_save = Some(peerstate::ToSave::All);
peerstate.save_to_db(&this.sql, false).await.unwrap(); peerstate.save_to_db(&this.sql).await.unwrap();
} }
async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option<Message> { async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option<Message> {

View File

@@ -206,7 +206,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
msg.text = Some( msg.text = Some(
stock_str::bad_time_msg_body( stock_str::bad_time_msg_body(
context, context,
Local &Local
.timestamp_opt(now, 0) .timestamp_opt(now, 0)
.unwrap() .unwrap()
.format("%Y-%m-%d %H:%M:%S") .format("%Y-%m-%d %H:%M:%S")
@@ -324,8 +324,8 @@ pub(crate) fn extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
} }
// the returned suffix is lower-case // the returned suffix is lower-case
pub fn get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> { pub fn get_filesuffix_lc(path_filename: &str) -> Option<String> {
Path::new(path_filename.as_ref()) Path::new(path_filename)
.extension() .extension()
.map(|p| p.to_string_lossy().to_lowercase()) .map(|p| p.to_string_lossy().to_lowercase())
} }

View File

@@ -0,0 +1,93 @@
From - Thu, 24 Nov 2022 19:06:16 GMT
X-Mozilla-Status: 0001
X-Mozilla-Status2: 00800000
Message-ID: <0bb9ffe1-2596-d997-95b4-1fef8cc4808e@example.org>
Date: Thu, 24 Nov 2022 20:05:57 +0100
MIME-Version: 1.0
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
Thunderbird/102.4.2
From: Alice <alice@example.org>
To: bob@example.net
Content-Language: en-US
Autocrypt: addr=alice@example.org; keydata=
xjMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5DN
GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp
7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4M
CyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDr
RuI8A/8tEEXAA844BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp
01JrRe6Xqy22HQMBCAfCeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsM
AAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIy
VfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg==
Subject: ...
Content-Type: multipart/encrypted;
protocol="application/pgp-encrypted";
boundary="------------EOdOT2kJUL5hgCilmIhYyVZg"
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
--------------EOdOT2kJUL5hgCilmIhYyVZg
Content-Type: application/pgp-encrypted
Content-Description: PGP/MIME version identification
Version: 1
--------------EOdOT2kJUL5hgCilmIhYyVZg
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc"
-----BEGIN PGP MESSAGE-----
wV4D5tq63hTeebASAQdA1dVUsUjGZCOIfCnYtVdmOvKs/BNovI3sG8w1IH4ymTMwAZzgwVbGS5KL
+e1VTD5mUTeVSEYe1cd3VozH4KbNJa1tBlcO0nzGwCPpsTVDMoxIwcBMA+PY3JvEjuMiAQf/d2yj
t0+GyaptwX26bgSqo6vj21W8mcWS5vXOi8wjGwRbPaKKjS4kq1xDOz04eHrE8HUPD8otcXoI8CLz
etJpRbFs0XJP4Cozbsr72dgoWhozRg/iSpBndxWOddTl7Yqo8m/fyhU5uzKZ41m2T8mha6KkKWD8
QecGdOgieYBucNBjHwWc71p9G6jTnzfy4S4GtGS2gwOSMxpwO7HxpKzsHI4POqFSQbxrl/YRwWSC
f5WqyYcerasIiR/fnOIw8lnvCeQ5rB90eGEDR70YFGt0t4rFBjfGrSPUiWYaTaC1Zvpd+t5sy7zy
FpsS2/aTkwP/UpGqmtFaD/brSouRf9hijNLI0QFTaVmSoI3BKzF8B4zwvtEbOLZjyDb+Va/fZJ3w
nYd2Q/5PPPL+pE4pWKN+jl0TZNzAaqBgvggXomgUqQ7QiksUzym+yuFKrJX0RF2awdrgjQIxjnda
Qp3UFphnFTyYUJpIU9iewjOfVxgPzv7PyuCHYwoP3kh7MJZ6bgbDmOkeFSnjEDJpdf1m9xC9LlBL
beC8scmPs6kx9GARBYSHvyPQ025gN3+XEHh4OrTxHZ91U3IlTfd2kACwOOAXEuhItSHmcNOV0K4M
nI2PH6gW8HgBkWlAPm40K4jUyo3nl1usDiI6ouvYqvW7YUc2hTtPTej1l2/mS57tTt+PFurKs555
5R9DD/xg9Nx7OuQKy5bIdlXM20UmwuZTOhRJ5kpHFRzLxaHDbSzW+orhRW4llJSevBSAH3cLOjIQ
gh87j+MxG9j0TD2K2A0rcUcxdrnflw+mxcDVaL4payeqmOa+bJyhlftTqH+vqq5DhR68rX5VW+z7
riqH3o8VbvO2y0XSpYHf1jowkfJj3vr8pynAUIv1dbylUSF5wtrHvzWOprw4bNrdtwQNRNy+JcVF
dUKeNmHaL6XOe4LUWpiI11beRyCpAG52khMCEAO3Q6+4e24cEipbu6suSOtv3OpYDZeHjwNrQIhi
rJg7i9TpMqwOeCvFWK+9UZ+P2n6h9g0/JO2+I82BFGUjVa5IvCTNOgv01GqxWY9ecdtaJjTc+dF2
OAcRoKwvmtMJlxKEEgveui3BvPA4tuNdSrcoZBrQeo0ZHWVugXPvEZnwfZMcqwwPA+a/sUbZFg0P
Pr0AR0ZHpytnQE9OXE8wEUgT8H1yofQ+5QoZdgMpeAb8zGs+RuviLxcDkb9NtXUAiQ49ooWuFP3L
K9wMlaoWFTq7R+n5JVuSEYRCHC0l0bCV1/+awalT7XltXVCupI4lWzjYs52FZGGzuHG7S50Eufad
m4CQTPVgVaVn8WW2dmpMR8Gj8WbbZdyv21wMGOWjfgT0u3oiDnddGrFOoMNnZHch6rN3FRppoh7h
0U0fi8xxU1+EhUKq+fSIxZNr2iWN2if3Pipbxi9tyK9M41Y6aVF3HWjD58/OEql3aZjJZ1bqpXcE
qsPeFoXX78+7mTDvL75olMk2s/mg4mLqAAWQvTuoiOmj+SgMIFuTtFR+4r/TIFNdamz6AQ3RcmWG
ZcdRii+V27dtMA836vlAwxXRmJyE1LCL1kvUTq+J+AVsZi3xmBLFNlKPTlxswu7vSBrP1DlYOaBq
AgA0lKnkQdeXyDk/VdbTml7ywMW1g6HkFSqKGW/IIAObmBumBcIyHE6dWEHumRQomlJssIlEFSe+
XEQ0rwedLetJXi5A0AXT1we1wvaKCEg0Pb0ZUxygwNPDrj6MmdodH7gDfyx0mW/7mEMCtIJb5MB+
TRGPEa/vqdJb8uGtNXUy9UlwMhJ3tYoT7NXY4+IlNjbDH/yleMdwtWP2H2WH8oC+ysXPYXjlT8eU
poxRfJzPMVUn5SA3cvdGXDJWdX8U91j5sf9wuoYE5RBVrrJif3D3l0FpMrlWWoGw7wtZbMC2FaeT
QvdMS5c54IoXBtBTM+/AsTAw7WEE1QSmaQGHnh6xLL5Ns8olsWeKOMlVXdO9jSDbjOGBLr7mWukW
YzLXkH3TtJPQcbVN79af3YPhaHdMYITVKIwfg+vxZlLFHWLJQnkTl+9Qi7u2gKqkNeU7Zqs4E3CR
9K4dHrJMyAZLZ2HA1XQEj0/tMnbTpAzZhj02JRcFobLXK9SQfw7dzGZwMRky8cHcBHoK14P5RIEV
hr+38HSBM6wXtge5gL6DomAACvuORQO4X9x/CTjRt/J8uN3lKK5p+wi3ULeb319CEWiCiqmC1M+C
TADUhPUhUmTinSAVkTEn+BdbH/97dVaJnvd6HtLmdSlw4xqdWUfVL9Qd7+/5L6iwlOzGLKRv97c/
gCRw+hzXyAom+5C18slSwanMuyPgIyrrFy/kp9Romk9SQr/c0CUF2am99t8G5qvVi/TiJGHyKEXD
aUYd4V7lqNlHMiiasvFHeq8blwmFr7rGEvbZzLNplc6sRUVlYhY2unRfyWsq9mqk3NDRW12Fa0J2
YxQJlnXHQhNE8EyM/zsD9jCVNwsRZJ9/e5KS+ignmu6gKIR+ItDTwRfNI+NG/YmTgENUTyuO+vQC
CUKS3PCwpP+OEC966ARl7OCMdfn1hEyiAxsZnp1RmFngR6FM+mlGgfUoWNoHvnR1/YyQ4F4dadiA
QINwuSm5faw75F1EeL8Qi+LHKuqt05Pi/V9GJ6TzIkIsEbyyJ5sKHrp4QsU4C1p7ZhPjddz8De8k
6ZdwMIeXxi27WKtsFLcr8JKOBe0imIilKdMBOPS31pc1iJe4472WbWM0aBwdEYmnz9+xfOqnjHtO
0XTMjff7pzV6Y7t/u8J/zm3JS3ykote9HNRQvhZZNeVClVWd0fYFzat5ESnTojZTwHcc/BFTPnhz
VgLyw1KEIy2r3ZyGHu1b8GSYivzl33MOK/NVBQPZUIEfdcQ5vhkAvj+Yx340IYykRFEChwioprXD
LrIbTou7TNT5fTFA+beidHFsL+OE002/LMs6C3erSUW5C/LNjAQMS7cAV2yCyjX+/2GBmmDqnC4r
Ja2x5yik+fbOUPh3kk/md1YvrodlX/JkQeoWRrrVJsX2dr3BgivPJavaN0Jz1eHyxAYKNqlrfd1T
YWEDIisWerTxAVY/rEruZ6+OqLqOtZtn+4SOajOq8KFusglaMZqoYuM+LhPZck9PlZXwRqX08Vlv
8jX5V75BFWRhFd5/LYbnQHI6ZW80Wb2sBNngLL2QJT9yXGCDJb5qCdFwGd3i655pvRJXabeyCtDD
7I2PJcYRDd4stdq07BHyHJmye6vas8mG5QUygyWyUQv78za0m4gLMrRZBgoBDcVpWJUc+cPXzzfG
7PvLZu/Y0SaD5hqTp0LBB1PFxTpzdVeJ21gzVNQ6D4XGLTtdv4K4fOEYoeKEuzGoBaUDtIqz47gd
5rwfQ3ps2slkxfbtQcdKEACKvsCwzqHlgwsxD8QNOFzXYLiiiJBX22fIRoiJeSDMKSZyuFtpykCm
7bOpybPSHv3E7EIr8sIOr9MOe/R5HSthU2IgW1L5Ynr2t9HUnCA8CenkzIQjg0h5sruxcGWCYLx7
q0f1AQs4Z7SebVbq1SCWVJNX/vc1bVjnjYfri7RX5WMmjJkuSnuIoP6a42cqJcAg7m0STB0elFAy
oO4vW9/JEmFUqLyQmWnoLJHX3IKtWa9CPvE=
=OA6b
-----END PGP MESSAGE-----
--------------EOdOT2kJUL5hgCilmIhYyVZg--