mirror of
https://github.com/chatmail/core.git
synced 2026-05-16 05:16:39 +03:00
Merge remote-tracking branch 'origin/master' into iroh-share
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -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
833
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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 }
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
43
python/src/deltachat/reactions.py
Normal file
43
python/src/deltachat/reactions.py
Normal 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))
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
31
src/chat.rs
31
src/chat.rs
@@ -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());
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
172
src/peerstate.rs
172
src/peerstate.rs
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
42
src/sql.rs
42
src/sql.rs
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
148
src/stock_str.rs
148
src/stock_str.rs
@@ -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(×tamp_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
|
||||||
|
|||||||
@@ -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 { "" },
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
93
test-data/message/thunderbird_with_autocrypt.eml
Normal file
93
test-data/message/thunderbird_with_autocrypt.eml
Normal 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--
|
||||||
Reference in New Issue
Block a user