mirror of
https://github.com/chatmail/core.git
synced 2026-06-26 01:26:36 +03:00
Compare commits
2 Commits
v2.22.0
...
cli-displa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18044c2fef | ||
|
|
f5dea1d252 |
8
.github/workflows/deltachat-rpc-server.yml
vendored
8
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/nix.yml
vendored
6
.github/workflows/nix.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/repl.yml
vendored
2
.github/workflows/repl.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/upload-docs.yml
vendored
4
.github/workflows/upload-docs.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -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
|
||||
|
||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -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
17
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.22.0"
|
||||
version = "2.20.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.22.0"
|
||||
"version": "2.20.0"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.22.0"
|
||||
"version": "2.20.0"
|
||||
}
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-10-17
|
||||
2025-10-13
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
30
src/calls.rs
30
src/calls.rs
@@ -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(),
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
14
src/net.rs
14
src/net.rs
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user