Compare commits

..

2 Commits

Author SHA1 Message Date
missytake
18044c2fef fix ruff 2025-10-13 16:12:20 +02:00
missytake
f5dea1d252 feat: allow setting displayname + selfavatar via CLI 2025-10-13 16:12:18 +02:00
42 changed files with 135 additions and 532 deletions

View File

@@ -34,7 +34,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -58,7 +58,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -109,7 +109,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -136,7 +136,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v5

View File

@@ -25,7 +25,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix fmt flake.nix -- --check
build:
@@ -84,7 +84,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -104,5 +104,5 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix build .#${{ matrix.installable }}

View File

@@ -18,7 +18,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary

View File

@@ -36,7 +36,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -55,7 +55,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat

View File

@@ -25,7 +25,7 @@ jobs:
run: uvx zizmor --format sarif . > results.sarif
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
category: zizmor

View File

@@ -1,50 +1,5 @@
# Changelog
## [2.22.0] - 2025-10-17
### Fixes
- Do not notify about incoming calls for contact requests and blocked contacts.
### Tests
- Accept the chat with the caller before accepting calls.
## [2.21.0] - 2025-10-16
### Build system
- nix: Remove unused dependencies.
### Features / Changes
- TLS 1.3 session resumption.
- REPL: Add send-sync command.
- Set `User-Agent` for tile.openstreetmap.org requests.
- Cache tile.openstreetmap.org tiles for 7 days.
### Fixes
- Remove Exif with non-fatal errors from images.
- jsonrpc: Use Core's logic for computing VcardContact.color ([#7294](https://github.com/chatmail/core/pull/7294)).
### Miscellaneous Tasks
- deps: Bump cachix/install-nix-action from 31.7.0 to 31.8.0.
- cargo: Bump async_zip from 0.0.17 to 0.0.18 ([#7257](https://github.com/chatmail/core/pull/7257)).
- deps: Bump github/codeql-action from 3 to 4 ([#7304](https://github.com/chatmail/core/pull/7304)).
### Refactor
- Use rustls reexported from tokio_rustls.
- Pass ALPN around as &str.
- mimeparser: Store only one signature fingerprint.
### Tests
- Test expiration of ephemeral messages with unknown viewtype.
- Test expiration of non-ephemeral message with unknown viewtype.
## [2.20.0] - 2025-10-13
This release fixes a bug that resulted in ephemeral loop getting stuck in infinite loop
@@ -6966,5 +6921,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.18.0]: https://github.com/chatmail/core/compare/v2.17.0..v2.18.0
[2.19.0]: https://github.com/chatmail/core/compare/v2.18.0..v2.19.0
[2.20.0]: https://github.com/chatmail/core/compare/v2.19.0..v2.20.0
[2.21.0]: https://github.com/chatmail/core/compare/v2.20.0..v2.21.0
[2.22.0]: https://github.com/chatmail/core/compare/v2.21.0..v2.22.0

17
Cargo.lock generated
View File

@@ -346,15 +346,15 @@ dependencies = [
[[package]]
name = "async_zip"
version = "0.0.18"
version = "0.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6"
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
dependencies = [
"async-compression",
"crc32fast",
"futures-lite",
"pin-project",
"thiserror 2.0.17",
"thiserror 1.0.69",
"tokio",
"tokio-util",
]
@@ -1289,7 +1289,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.22.0"
version = "2.20.0"
dependencies = [
"anyhow",
"async-broadcast",
@@ -1346,6 +1346,7 @@ dependencies = [
"ratelimit",
"regex",
"rusqlite",
"rustls",
"rustls-pki-types",
"sanitize-filename",
"sdp",
@@ -1398,7 +1399,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.22.0"
version = "2.20.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1420,7 +1421,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.22.0"
version = "2.20.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1436,7 +1437,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.22.0"
version = "2.20.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1465,7 +1466,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.22.0"
version = "2.20.0"
dependencies = [
"anyhow",
"deltachat",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.22.0"
version = "2.20.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"
@@ -47,7 +47,7 @@ async-channel = { workspace = true }
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
blake3 = "1.8.2"
brotli = { version = "8", default-features=false, features = ["std"] }
@@ -87,6 +87,7 @@ rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rustls-pki-types = "1.12.0"
rustls = { version = "0.23.22", default-features = false }
sanitize-filename = { workspace = true }
sdp = "0.8.0"
serde_json = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.22.0"
version = "2.20.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.22.0"
version = "2.20.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use deltachat::color;
use deltachat::context::Context;
use deltachat::key::{DcKey, SignedPublicKey};
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -130,13 +130,7 @@ pub struct VcardContact {
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
let display_name = vc.display_name().to_string();
let is_self = false;
let fpr = vc.key.as_deref().and_then(|k| {
SignedPublicKey::from_base64(k)
.ok()
.map(|k| k.dc_fingerprint())
});
let color = deltachat::contact::get_color(is_self, &vc.addr, &fpr);
let color = color::str_to_color(&vc.addr.to_lowercase());
Self {
addr: vc.addr,
display_name,

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.22.0"
"version": "2.20.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.22.0"
version = "2.20.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"

View File

@@ -358,7 +358,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
dellocations\n\
getlocations [<contact-id>]\n\
send <text>\n\
send-sync <text>\n\
sendempty\n\
sendimage <file> [<text>]\n\
sendsticker <file> [<text>]\n\
@@ -909,23 +908,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
}
"send-sync" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No message text given.");
// Send message over a dedicated SMTP connection
// and measure time.
//
// This can be used to benchmark SMTP connection establishment.
let time_start = std::time::Instant::now();
let msg = format!("{arg1} {arg2}");
let mut msg = Message::new_text(msg);
chat::send_msg_sync(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
let time_needed = time_start.elapsed();
println!("Sent message in {time_needed:?}.");
}
"sendempty" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;

View File

@@ -179,7 +179,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 39] = [
const CHAT_COMMANDS: [&str; 38] = [
"listchats",
"listarchived",
"start-realtime",
@@ -199,7 +199,6 @@ const CHAT_COMMANDS: [&str; 39] = [
"dellocations",
"getlocations",
"send",
"send-sync",
"sendempty",
"sendimage",
"sendsticker",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.22.0"
version = "2.20.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -92,6 +92,12 @@ def _run_cli(
)
parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
parser.add_argument(
"--displayname", action="store", help="the profile's display name", default=os.getenv("DELTACHAT_DISPLAYNAME"),
)
parser.add_argument(
"--avatar", action="store", help="filename of the profile's avatar", default=os.getenv("DELTACHAT_AVATAR"),
)
args = parser.parse_args(argv[1:])
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
@@ -108,7 +114,12 @@ def _run_cli(
configure_thread = Thread(
target=client.configure,
daemon=True,
kwargs={"email": args.email, "password": args.password},
kwargs={
"email": args.email,
"password": args.password,
"displayname": args.displayname,
"selfavatar": args.avatar,
},
)
configure_thread.start()
client.run_forever()

View File

@@ -9,7 +9,6 @@ def test_calls(acfactory) -> None:
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info)
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
@@ -68,7 +67,6 @@ a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
alice, bob = acfactory.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.place_outgoing_call(place_call_info)
@@ -86,24 +84,3 @@ def test_ice_servers(acfactory) -> None:
ice_servers = alice.ice_servers()
assert len(ice_servers) == 1
def test_no_contact_request_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer")
alice_chat_bob.send_text("Hello!")
# Notification for "Hello!" message should arrive
# without the call ringing.
while True:
event = bob.wait_for_event()
# There should be no incoming call notification.
assert event.kind != EventType.INCOMING_CALL
if event.kind == EventType.INCOMING_MSG:
msg = bob.get_message_by_id(event.msg_id)
assert msg.get_snapshot().text == "Hello!"
break

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.22.0"
version = "2.20.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.22.0"
"version": "2.20.0"
}

View File

@@ -98,6 +98,9 @@
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
};
@@ -480,6 +483,12 @@
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.22.0"
version = "2.20.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"

View File

@@ -1 +1 @@
2025-10-17
2025-10-13

View File

@@ -537,11 +537,7 @@ fn file_hash(src: &Path) -> Result<blake3::Hash> {
fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(file);
let exif = exif::Reader::new()
.continue_on_error(true)
.read_from_container(&mut bufreader)
.or_else(|e| e.distill_partial_result(|_errors| {}))
.ok();
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
}

View File

@@ -334,28 +334,6 @@ async fn test_recode_image_2() {
assert_correct_rotation(&img_rotated);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_bad_exif() {
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
// detected and removed.
let bytes = include_bytes!("../../test-data/image/1000x1000-bad-exif.jpg");
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
bytes,
extension: "jpg",
has_exif: true,
original_width: 1000,
original_height: 1000,
compressed_width: 1000,
compressed_height: 1000,
..Default::default()
}
.test()
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_balanced_png() {
let bytes = include_bytes!("../../test-data/image/screenshot.png");
@@ -440,7 +418,7 @@ async fn test_recode_image_balanced_png() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_with_exif() {
let bytes = include_bytes!("../../test-data/image/logo-exif.png");
let bytes = include_bytes!("../../test-data/image/logo.png");
SendImageCheckMediaquality {
viewtype: Viewtype::Sticker,
bytes,

View File

@@ -2,9 +2,8 @@
//!
//! Internally, calls are bound a user-visible message initializing the call.
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
use crate::chat::ChatIdBlocked;
use crate::chat::{Chat, ChatId, send_msg};
use crate::constants::{Blocked, Chattype};
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
@@ -346,27 +345,12 @@ impl Context {
false
}
};
if let Some(chat_id_blocked) =
ChatIdBlocked::lookup_by_contact(self, from_id).await?
{
match chat_id_blocked.blocked {
Blocked::Not => {
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
});
}
Blocked::Yes | Blocked::Request => {
// Do not notify about incoming calls
// from contact requests and blocked contacts.
//
// User can still access the call and accept it
// via the chat in case of contact requests.
}
}
}
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
});
let wait = call.remaining_ring_seconds();
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),

View File

@@ -45,12 +45,6 @@ async fn setup_call() -> Result<CallSetup> {
// Alice creates a chat with Bob and places an outgoing call there.
// Alice's other device sees the same message as an outgoing call.
let alice_chat = alice.create_chat(&bob).await;
// Create chat on Bob's side
// so incoming call causes a notification.
bob.create_chat(&alice).await;
bob2.create_chat(&alice).await;
let test_msg_id = alice
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
.await?;

View File

@@ -36,7 +36,7 @@ use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time, to_lowercase};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time};
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -1574,10 +1574,19 @@ impl Contact {
Ok(None)
}
/// Returns a color for the contact.
/// See [`self::get_color`].
/// Get a color for the contact.
/// The color is calculated from the contact's fingerprint (for key-contacts)
/// or email address (for address-contacts) and can be used
/// for an fallback avatar with white initials
/// as well as for headlines in bubbles of group chats.
pub fn get_color(&self) -> u32 {
get_color(self.id == ContactId::SELF, &self.addr, &self.fingerprint())
if let Some(fingerprint) = self.fingerprint() {
str_to_color(&fingerprint.hex())
} else if self.id == ContactId::SELF {
0x808080
} else {
str_to_color(&self.addr.to_lowercase())
}
}
/// Gets the contact's status.
@@ -1673,21 +1682,6 @@ impl Contact {
}
}
/// Returns a color for a contact having given attributes.
///
/// The color is calculated from contact's fingerprint (for key-contacts) or email address (for
/// address-contacts; should be lowercased to avoid allocation) and can be used for an fallback
/// avatar with white initials as well as for headlines in bubbles of group chats.
pub fn get_color(is_self: bool, addr: &str, fingerprint: &Option<Fingerprint>) -> u32 {
if let Some(fingerprint) = fingerprint {
str_to_color(&fingerprint.hex())
} else if is_self {
0x808080
} else {
str_to_color(&to_lowercase(addr))
}
}
// Updates the names of the chats which use the contact name.
//
// This is one of the few duplicated data, however, getting the chat list is easier this way.

View File

@@ -30,7 +30,6 @@ use crate::log::{info, warn};
use crate::logged_debug_assert;
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::net::tls::TlsSessionStore;
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
use crate::push::PushSubscriber;
@@ -298,9 +297,6 @@ pub struct InnerContext {
/// True if account has subscribed to push notifications via IMAP.
pub(crate) push_subscribed: AtomicBool,
/// TLS session resumption cache.
pub(crate) tls_session_store: TlsSessionStore,
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
@@ -479,7 +475,6 @@ impl Context {
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
tls_session_store: TlsSessionStore::new(),
iroh: Arc::new(RwLock::new(None)),
self_fingerprint: OnceLock::new(),
connectivities: parking_lot::Mutex::new(Vec::new()),

View File

@@ -826,68 +826,3 @@ async fn test_ephemeral_timer_non_member() -> Result<()> {
Ok(())
}
/// Tests that expiration of a disappearing message
/// with unknown viewtype does not make `delete_expired_messages` fail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_disappearing_unknown_viewtype() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat = alice.create_chat(bob).await;
let duration = 60;
chat.id
.set_ephemeral_timer(alice, Timer::Enabled { duration })
.await?;
let mut msg = Message::new_text("Expiring message".to_string());
let _alice_sent_message = alice.send_msg(chat.id, &mut msg).await;
// Set message viewtype to unassigned
// type 70 that was previously used for videochat invitations.
alice
.sql
.execute("UPDATE msgs SET type=70 WHERE id=?", (msg.id,))
.await?;
SystemTime::shift(Duration::from_secs(100));
// This should not fail.
delete_expired_messages(alice, time()).await?;
Ok(())
}
/// Tests that deletion of a message with unknown viewtype
/// triggered by `delete_device_after`
/// does not make `delete_expired_messages` fail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_device_after_unknown_viewtype() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat = alice.create_chat(bob).await;
alice
.set_config(Config::DeleteDeviceAfter, Some("600"))
.await?;
let mut msg = Message::new_text("Some message".to_string());
let _alice_sent_message = alice.send_msg(chat.id, &mut msg).await;
// Set message viewtype to unassigned
// type 70 that was previously used for videochat invitations.
alice
.sql
.execute("UPDATE msgs SET type=70 WHERE id=?", (msg.id,))
.await?;
SystemTime::shift(Duration::from_secs(1000));
// This should not fail.
delete_expired_messages(alice, time()).await?;
Ok(())
}

View File

@@ -37,12 +37,12 @@ impl DerefMut for Client {
}
/// Converts port number to ALPN list.
fn alpn(port: u16) -> &'static str {
fn alpn(port: u16) -> &'static [&'static str] {
if port == 993 {
// Do not request ALPN on standard port.
""
&[]
} else {
"imap"
&["imap"]
}
}
@@ -210,15 +210,7 @@ impl Client {
let account_id = context.get_id();
let events = context.events.clone();
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
let tls_stream = wrap_tls(
strict_tls,
hostname,
addr.port(),
alpn(addr.port()),
logging_stream,
&context.tls_session_store,
)
.await?;
let tls_stream = wrap_tls(strict_tls, hostname, alpn(addr.port()), logging_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -270,16 +262,9 @@ impl Client {
let buffered_tcp_stream = client.into_inner();
let tcp_stream = buffered_tcp_stream.into_inner();
let tls_stream = wrap_tls(
strict_tls,
host,
addr.port(),
"",
tcp_stream,
&context.tls_session_store,
)
.await
.context("STARTTLS upgrade failed")?;
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);
@@ -296,15 +281,7 @@ impl Client {
let proxy_stream = proxy_config
.connect(context, domain, port, strict_tls)
.await?;
let tls_stream = wrap_tls(
strict_tls,
domain,
port,
alpn(port),
proxy_stream,
&context.tls_session_store,
)
.await?;
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -357,16 +334,9 @@ impl Client {
let buffered_proxy_stream = client.into_inner();
let proxy_stream = buffered_proxy_stream.into_inner();
let tls_stream = wrap_tls(
strict_tls,
hostname,
port,
"",
proxy_stream,
&context.tls_session_store,
)
.await
.context("STARTTLS upgrade failed")?;
let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);

View File

@@ -25,7 +25,7 @@ use crate::tools::{self, time_elapsed};
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub trait DcKey: Serialize + Deserializable + Clone {
pub(crate) trait DcKey: Serialize + Deserializable + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
let res = <Self as Deserializable>::from_bytes(Cursor::new(bytes));
@@ -112,10 +112,7 @@ pub trait DcKey: Serialize + Deserializable + Clone {
/// The fingerprint for the key.
fn dc_fingerprint(&self) -> Fingerprint;
/// Whether the key is private (or public).
fn is_private() -> bool;
/// Returns the OpenPGP Key ID.
fn key_id(&self) -> KeyId;
}

View File

@@ -87,12 +87,12 @@ pub(crate) struct MimeMessage {
pub chat_disposition_notification_to: Option<SingleInfo>,
pub decrypting_failed: bool,
/// Valid signature fingerprint if a message is an
/// Set of valid signature fingerprints if a message is an
/// Autocrypt encrypted and signed message.
///
/// If a message is not encrypted or the signature is not valid,
/// this is `None`.
pub signature: Option<Fingerprint>,
/// this set is empty.
pub signatures: HashSet<Fingerprint>,
/// The addresses for which there was a gossip header
/// and their respective gossiped keys.
@@ -589,7 +589,7 @@ impl MimeMessage {
decrypting_failed: mail.is_err(),
// only non-empty if it was a valid autocrypt message
signature: signatures.into_iter().last(),
signatures,
autocrypt_fingerprint,
gossiped_keys,
is_forwarded: false,
@@ -966,7 +966,7 @@ impl MimeMessage {
/// This means the message was both encrypted and signed with a
/// valid signature.
pub fn was_encrypted(&self) -> bool {
self.signature.is_some()
!self.signatures.is_empty()
}
/// Returns whether the email contains a `chat-version` header.

View File

@@ -12,7 +12,6 @@ use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::net::session::SessionStream;
use crate::net::tls::TlsSessionStore;
use crate::sql::Sql;
use crate::tools::time;
@@ -128,19 +127,10 @@ pub(crate) async fn connect_tls_inner(
addr: SocketAddr,
host: &str,
strict_tls: bool,
alpn: &str,
tls_session_store: &TlsSessionStore,
alpn: &[&str],
) -> Result<impl SessionStream + 'static> {
let tcp_stream = connect_tcp_inner(addr).await?;
let tls_stream = wrap_tls(
strict_tls,
host,
addr.port(),
alpn,
tcp_stream,
tls_session_store,
)
.await?;
let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?;
Ok(tls_stream)
}

View File

@@ -16,10 +16,6 @@ use crate::net::session::SessionStream;
use crate::net::tls::wrap_rustls;
use crate::tools::time;
/// User-Agent for HTTP requests if a resource usage policy requires it.
/// By default we do not set User-Agent.
const USER_AGENT: &str = "chatmail/2 (+https://github.com/chatmail/core/)";
/// HTTP(S) GET response.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
@@ -80,13 +76,11 @@ where
let proxy_stream = proxy_config
.connect(context, host, port, load_cache)
.await?;
let tls_stream =
wrap_rustls(host, port, "", proxy_stream, &context.tls_session_store).await?;
let tls_stream = wrap_rustls(host, &[], proxy_stream).await?;
Box::new(tls_stream)
} else {
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
let tls_stream =
wrap_rustls(host, port, "", tcp_stream, &context.tls_session_store).await?;
let tls_stream = wrap_rustls(host, &[], tcp_stream).await?;
Box::new(tls_stream)
}
}
@@ -108,13 +102,6 @@ fn http_url_cache_timestamps(url: &str, mimetype: Option<&str>) -> (i64, i64) {
let stale = if url.ends_with(".xdc") {
// WebXDCs are never stale, they just expire.
expires
} else if url.starts_with("https://tile.openstreetmap.org/")
|| url.starts_with("https://vector.openstreetmap.org/")
{
// Policy at <https://operations.osmfoundation.org/policies/tiles/>
// requires that we cache tiles for at least 7 days.
// Do not revalidate earlier than that.
now + 3600 * 24 * 7
} else if mimetype.is_some_and(|s| s.starts_with("image/")) {
// Cache images for 1 day.
//
@@ -256,22 +243,8 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
.context("URL has no authority")?
.clone();
let req = hyper::Request::builder().uri(parsed_url);
// OSM usage policy requires
// that User-Agent is set for HTTP GET requests
// to tile servers:
// <https://operations.osmfoundation.org/policies/tiles/>
// Same for vectory tiles
// at <https://operations.osmfoundation.org/policies/vector/>.
let req =
if authority == "tile.openstreetmap.org" || authority == "vector.openstreetmap.org" {
req.header("User-Agent", USER_AGENT)
} else {
req
};
let req = req
let req = hyper::Request::builder()
.uri(parsed_url)
.header(hyper::header::HOST, authority.as_str())
.body(http_body_util::Empty::<Bytes>::new())?;
let response = sender.send_request(req).await?;

View File

@@ -429,14 +429,7 @@ impl ProxyConfig {
load_cache,
)
.await?;
let tls_stream = wrap_rustls(
&https_config.host,
https_config.port,
"",
tcp_stream,
&context.tls_session_store,
)
.await?;
let tls_stream = wrap_rustls(&https_config.host, &[], tcp_stream).await?;
let auth = if let Some((username, password)) = &https_config.user_password {
Some((username.as_str(), password.as_str()))
} else {

View File

@@ -1,38 +1,27 @@
//! TLS support.
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
use anyhow::Result;
use crate::net::session::SessionStream;
use tokio_rustls::rustls::client::ClientSessionStore;
pub async fn wrap_tls<'a>(
strict_tls: bool,
hostname: &str,
port: u16,
alpn: &str,
alpn: &[&str],
stream: impl SessionStream + 'static,
tls_session_store: &TlsSessionStore,
) -> Result<impl SessionStream + 'a> {
if strict_tls {
let tls_stream = wrap_rustls(hostname, port, alpn, stream, tls_session_store).await?;
let tls_stream = wrap_rustls(hostname, alpn, stream).await?;
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
Ok(boxed_stream)
} else {
// We use native_tls because it accepts 1024-bit RSA keys.
// Rustls does not support them even if
// certificate checks are disabled: <https://github.com/rustls/rustls/issues/234>.
let alpns = if alpn.is_empty() {
Box::from([])
} else {
Box::from([alpn])
};
let tls = async_native_tls::TlsConnector::new()
.min_protocol_version(Some(async_native_tls::Protocol::Tlsv12))
.request_alpns(&alpns)
.request_alpns(alpn)
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true);
let tls_stream = tls.connect(hostname, stream).await?;
@@ -41,82 +30,18 @@ pub async fn wrap_tls<'a>(
}
}
/// Map to store TLS session tickets.
///
/// Tickets are separated by port and ALPN
/// to avoid trying to use Postfix ticket for Dovecot and vice versa.
/// Doing so would not be a security issue,
/// but wastes the ticket and the opportunity to resume TLS session unnecessarily.
/// Rustls takes care of separating tickets that belong to different domain names.
#[derive(Debug)]
pub(crate) struct TlsSessionStore {
sessions: Mutex<HashMap<(u16, String), Arc<dyn ClientSessionStore>>>,
}
// This is the default for TLS session store
// as of Rustls version 0.23.16,
// but we want to create multiple caches
// to separate them by port and ALPN.
const TLS_CACHE_SIZE: usize = 256;
impl TlsSessionStore {
/// Creates a new TLS session store.
///
/// One such store should be created per profile
/// to keep TLS sessions independent.
pub fn new() -> Self {
Self {
sessions: Default::default(),
}
}
/// Returns session store for given port and ALPN.
///
/// Rustls additionally separates sessions by hostname.
pub fn get(&self, port: u16, alpn: &str) -> Arc<dyn ClientSessionStore> {
Arc::clone(
self.sessions
.lock()
.entry((port, alpn.to_string()))
.or_insert_with(|| {
Arc::new(tokio_rustls::rustls::client::ClientSessionMemoryCache::new(
TLS_CACHE_SIZE,
))
}),
)
}
}
pub async fn wrap_rustls<'a>(
hostname: &str,
port: u16,
alpn: &str,
alpn: &[&str],
stream: impl SessionStream + 'a,
tls_session_store: &TlsSessionStore,
) -> Result<impl SessionStream + 'a> {
let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty();
let mut root_cert_store = rustls::RootCertStore::empty();
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let mut config = tokio_rustls::rustls::ClientConfig::builder()
let mut config = rustls::ClientConfig::builder()
.with_root_certificates(root_cert_store)
.with_no_client_auth();
config.alpn_protocols = if alpn.is_empty() {
vec![]
} else {
vec![alpn.as_bytes().to_vec()]
};
// Enable TLS 1.3 session resumption
// as defined in <https://www.rfc-editor.org/rfc/rfc8446#section-2.2>.
//
// Obsolete TLS 1.2 mechanisms defined in RFC 5246
// and RFC 5077 have worse security
// and are not worth increasing
// attack surface: <https://words.filippo.io/we-need-to-talk-about-session-tickets/>.
let resumption_store = tls_session_store.get(port, alpn);
let resumption = tokio_rustls::rustls::client::Resumption::store(resumption_store)
.tls12_resumption(tokio_rustls::rustls::client::Tls12Resumption::Disabled);
config.resumption = resumption;
config.alpn_protocols = alpn.iter().map(|s| s.as_bytes().to_vec()).collect();
let tls = tokio_rustls::TlsConnector::from(Arc::new(config));
let name = rustls_pki_types::ServerName::try_from(hostname)?.to_owned();

View File

@@ -642,7 +642,7 @@ pub(crate) async fn receive_imf_inner(
// For example, GitHub sends messages from `notifications@github.com`,
// but uses display name of the user whose action generated the notification
// as the display name.
let fingerprint = mime_parser.signature.as_ref();
let fingerprint = mime_parser.signatures.iter().next();
let (from_id, _from_id_blocked, incoming_origin) = match from_field_to_contact_id(
context,
&mime_parser.from,
@@ -3662,10 +3662,7 @@ async fn has_verified_encryption(
));
}
let signed_with_verified_key = mimeparser
.signature
.as_ref()
.is_some_and(|signature| *signature == fingerprint);
let signed_with_verified_key = mimeparser.signatures.contains(&fingerprint);
if signed_with_verified_key {
Ok(Verified)
} else {

View File

@@ -623,19 +623,17 @@ fn encrypted_and_signed(
mimeparser: &MimeMessage,
expected_fingerprint: &Fingerprint,
) -> bool {
if let Some(signature) = mimeparser.signature.as_ref() {
if signature == expected_fingerprint {
true
} else {
warn!(
context,
"Message does not match expected fingerprint {expected_fingerprint}.",
);
false
}
} else {
if !mimeparser.was_encrypted() {
warn!(context, "Message not encrypted.",);
false
} else if !mimeparser.signatures.contains(expected_fingerprint) {
warn!(
context,
"Message does not match expected fingerprint {}.", expected_fingerprint,
);
false
} else {
true
}
}

View File

@@ -12,20 +12,20 @@ use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionBufStream;
use crate::net::tls::{TlsSessionStore, wrap_tls};
use crate::net::tls::wrap_tls;
use crate::net::{
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
};
use crate::oauth2::get_oauth2_access_token;
use crate::tools::time;
/// Converts port number to ALPN.
fn alpn(port: u16) -> &'static str {
/// Converts port number to ALPN list.
fn alpn(port: u16) -> &'static [&'static str] {
if port == 465 {
// Do not request ALPN on standard port.
""
&[]
} else {
"smtp"
&["smtp"]
}
}
@@ -109,12 +109,8 @@ async fn connection_attempt(
"Attempting SMTP connection to {host} ({resolved_addr})."
);
let res = match security {
ConnectionSecurity::Tls => {
connect_secure(resolved_addr, host, strict_tls, &context.tls_session_store).await
}
ConnectionSecurity::Starttls => {
connect_starttls(resolved_addr, host, strict_tls, &context.tls_session_store).await
}
ConnectionSecurity::Tls => connect_secure(resolved_addr, host, strict_tls).await,
ConnectionSecurity::Starttls => connect_starttls(resolved_addr, host, strict_tls).await,
ConnectionSecurity::Plain => connect_insecure(resolved_addr).await,
};
match res {
@@ -230,15 +226,7 @@ async fn connect_secure_proxy(
let proxy_stream = proxy_config
.connect(context, hostname, port, strict_tls)
.await?;
let tls_stream = wrap_tls(
strict_tls,
hostname,
port,
alpn(port),
proxy_stream,
&context.tls_session_store,
)
.await?;
let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), proxy_stream).await?;
let mut buffered_stream = BufStream::new(tls_stream);
skip_smtp_greeting(&mut buffered_stream).await?;
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
@@ -261,16 +249,9 @@ async fn connect_starttls_proxy(
skip_smtp_greeting(&mut buffered_stream).await?;
let transport = new_smtp_transport(buffered_stream).await?;
let tcp_stream = transport.starttls().await?.into_inner();
let tls_stream = wrap_tls(
strict_tls,
hostname,
port,
"",
tcp_stream,
&context.tls_session_store,
)
.await
.context("STARTTLS upgrade failed")?;
let tls_stream = wrap_tls(strict_tls, hostname, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufStream::new(tls_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
Ok(session_stream)
@@ -293,16 +274,8 @@ async fn connect_secure(
addr: SocketAddr,
hostname: &str,
strict_tls: bool,
tls_session_store: &TlsSessionStore,
) -> Result<Box<dyn SessionBufStream>> {
let tls_stream = connect_tls_inner(
addr,
hostname,
strict_tls,
alpn(addr.port()),
tls_session_store,
)
.await?;
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
let mut buffered_stream = BufStream::new(tls_stream);
skip_smtp_greeting(&mut buffered_stream).await?;
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
@@ -313,7 +286,6 @@ async fn connect_starttls(
addr: SocketAddr,
host: &str,
strict_tls: bool,
tls_session_store: &TlsSessionStore,
) -> Result<Box<dyn SessionBufStream>> {
let tcp_stream = connect_tcp_inner(addr).await?;
@@ -322,16 +294,9 @@ async fn connect_starttls(
skip_smtp_greeting(&mut buffered_stream).await?;
let transport = new_smtp_transport(buffered_stream).await?;
let tcp_stream = transport.starttls().await?.into_inner();
let tls_stream = wrap_tls(
strict_tls,
host,
addr.port(),
"",
tcp_stream,
tls_session_store,
)
.await
.context("STARTTLS upgrade failed")?;
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufStream::new(tls_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);

View File

@@ -753,14 +753,6 @@ pub(crate) fn buf_decompress(buf: &[u8]) -> Result<Vec<u8>> {
Ok(mem::take(decompressor.get_mut()))
}
/// Returns the given `&str` if already lowercased to avoid allocation, otherwise lowercases it.
pub(crate) fn to_lowercase(s: &str) -> Cow<'_, str> {
match s.chars().all(char::is_lowercase) {
true => Cow::Borrowed(s),
false => Cow::Owned(s.to_lowercase()),
}
}
/// Increments `*t` and checks that it equals to `expected` after that.
pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
t: &mut T,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB