Compare commits

..

2 Commits

44 changed files with 411 additions and 372 deletions

View File

@@ -43,7 +43,6 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Run rustfmt - name: Run rustfmt
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
- name: Run clippy - name: Run clippy
@@ -97,7 +96,6 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Rustdoc - name: Rustdoc
run: cargo doc --document-private-items --no-deps run: cargo doc --document-private-items --no-deps
@@ -143,10 +141,9 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Install nextest - name: Install nextest
uses: taiki-e/install-action@cca35edeb1d01366c2843b68fc3ca441446d73d3 uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458
with: with:
tool: nextest tool: nextest
@@ -180,7 +177,6 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Build C library - name: Build C library
run: cargo build -p deltachat_ffi run: cargo build -p deltachat_ffi
@@ -209,7 +205,6 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Build deltachat-rpc-server - name: Build deltachat-rpc-server
run: cargo build -p deltachat-rpc-server run: cargo build -p deltachat-rpc-server

View File

@@ -34,7 +34,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server binaries - name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -58,7 +58,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server wheels - name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
@@ -82,7 +82,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server binaries - name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }} run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -106,7 +106,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server wheels - name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
@@ -157,7 +157,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server binaries - name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -181,7 +181,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server wheels - name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
@@ -208,7 +208,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Download Linux aarch64 binary - name: Download Linux aarch64 binary
uses: actions/download-artifact@v7 uses: actions/download-artifact@v7

View File

@@ -25,10 +25,7 @@ jobs:
with: with:
node-version: 18.x node-version: 18.x
- name: Add Rust cache - name: Add Rust cache
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: npm install - name: npm install
working-directory: deltachat-jsonrpc/typescript working-directory: deltachat-jsonrpc/typescript
run: npm install run: npm install

View File

@@ -25,7 +25,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- run: nix fmt flake.nix -- --check - run: nix fmt flake.nix -- --check
build: build:
@@ -84,7 +84,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- run: nix build .#${{ matrix.installable }} - run: nix build .#${{ matrix.installable }}
build-macos: build-macos:
@@ -105,5 +105,5 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- run: nix build .#${{ matrix.installable }} - run: nix build .#${{ matrix.installable }}

View File

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

View File

@@ -41,7 +41,7 @@ jobs:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number. fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build Python documentation - name: Build Python documentation
run: nix build .#python-docs run: nix build .#python-docs
- name: Upload to py.delta.chat - name: Upload to py.delta.chat
@@ -63,7 +63,7 @@ jobs:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number. fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build C documentation - name: Build C documentation
run: nix build .#docs run: nix build .#docs
- name: Upload to c.delta.chat - name: Upload to c.delta.chat

88
Cargo.lock generated
View File

@@ -2608,25 +2608,6 @@ dependencies = [
"libm", "libm",
] ]
[[package]]
name = "hybrid-array"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9"
dependencies = [
"typenum",
]
[[package]]
name = "hybrid-array"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d15931895091dea5c47afa5b3c9a01ba634b311919fd4d41388fa0e3d76af"
dependencies = [
"typenum",
"zeroize",
]
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.9.0" version = "1.9.0"
@@ -3276,16 +3257,6 @@ dependencies = [
"cpufeatures 0.2.17", "cpufeatures 0.2.17",
] ]
[[package]]
name = "kem"
version = "0.3.0-pre.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f"
dependencies = [
"rand_core 0.6.4",
"zeroize",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -3499,35 +3470,6 @@ dependencies = [
"windows-sys 0.61.1", "windows-sys 0.61.1",
] ]
[[package]]
name = "ml-dsa"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac4a46643af2001eafebcc37031fc459eb72d45057aac5d7a15b00046a2ad6db"
dependencies = [
"const-oid",
"hybrid-array 0.3.1",
"num-traits",
"pkcs8",
"rand_core 0.6.4",
"sha3",
"signature",
"zeroize",
]
[[package]]
name = "ml-kem"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f"
dependencies = [
"hybrid-array 0.2.3",
"kem",
"rand_core 0.6.4",
"sha3",
"zeroize",
]
[[package]] [[package]]
name = "moka" name = "moka"
version = "0.12.10" version = "0.12.10"
@@ -4263,8 +4205,6 @@ dependencies = [
"k256", "k256",
"log", "log",
"md-5", "md-5",
"ml-dsa",
"ml-kem",
"nom 8.0.0", "nom 8.0.0",
"num-bigint-dig", "num-bigint-dig",
"num-traits", "num-traits",
@@ -4283,7 +4223,6 @@ dependencies = [
"sha2", "sha2",
"sha3", "sha3",
"signature", "signature",
"slh-dsa",
"smallvec", "smallvec",
"snafu", "snafu",
"twofish", "twofish",
@@ -5748,25 +5687,6 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "slh-dsa"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd2f20f4049197e03db1104a6452f4d9e96665d79f880198dce4a7026ba5f267"
dependencies = [
"const-oid",
"digest",
"hmac",
"hybrid-array 0.3.1",
"pkcs8",
"rand_core 0.6.4",
"sha2",
"sha3",
"signature",
"typenum",
"zerocopy",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@@ -7505,9 +7425,9 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"zerocopy-derive", "zerocopy-derive",
@@ -7515,9 +7435,9 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.7.35" version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -78,7 +78,7 @@ num-derive = "0.4"
num-traits = { workspace = true } num-traits = { workspace = true }
parking_lot = "0.12.4" parking_lot = "0.12.4"
percent-encoding = "2.3" percent-encoding = "2.3"
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false } pgp = { version = "0.19.0", default-features = false }
pin-project = "1" pin-project = "1"
qrcodegen = "1.7.0" qrcodegen = "1.7.0"
quick-xml = { version = "0.39", features = ["escape-html"] } quick-xml = { version = "0.39", features = ["escape-html"] }

View File

@@ -275,7 +275,7 @@ pub unsafe extern "C" fn dc_get_config(
.strdup() .strdup()
} else { } else {
match config::Config::from_str(&key) match config::Config::from_str(&key)
.with_context(|| format!("Invalid key {key:?}")) .with_context(|| format!("Invalid key {:?}", &key))
.log_err(ctx) .log_err(ctx)
{ {
Ok(key) => ctx Ok(key) => ctx

View File

@@ -122,7 +122,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
let name_f = entry.file_name(); let name_f = entry.file_name();
let name = name_f.to_string_lossy(); let name = name_f.to_string_lossy();
if name.ends_with(".eml") { if name.ends_with(".eml") {
let path_plus_name = format!("{real_spec}/{name}"); let path_plus_name = format!("{}/{}", &real_spec, name);
println!("Import: {path_plus_name}"); println!("Import: {path_plus_name}");
if poke_eml_file(context, Path::new(&path_plus_name)) if poke_eml_file(context, Path::new(&path_plus_name))
.await .await
@@ -133,11 +133,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
} }
} }
} else { } else {
eprintln!("Import: Cannot open directory {real_spec:?}."); eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
return false; return false;
} }
} }
println!("Import: {read_cnt} items read from {real_spec:?}."); println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
if read_cnt > 0 { if read_cnt > 0 {
context.emit_msgs_changed_without_ids(); context.emit_msgs_changed_without_ids();
} }
@@ -179,7 +179,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
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 { "" },
contact_name, &contact_name,
contact_id, contact_id,
msgtext, msgtext,
if msg.has_html() { "[HAS-HTML]" } else { "" }, if msg.has_html() { "[HAS-HTML]" } else { "" },
@@ -221,7 +221,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
}, },
statestr, statestr,
downloadstate, downloadstate,
temp2, &temp2,
); );
} }
@@ -561,7 +561,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.map_or_else(String::new, |prefix| format!("{prefix}: ")), .map_or_else(String::new, |prefix| format!("{prefix}: ")),
summary.text, summary.text,
statestr, statestr,
timestr, &timestr,
if chat.is_sending_locations() { if chat.is_sending_locations() {
"📍" "📍"
} else { } else {

View File

@@ -432,7 +432,7 @@ async fn handle_cmd(
{ {
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}"); println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
} else { } else {
println!("OAuth2 not available for {addr}."); println!("OAuth2 not available for {}.", &addr);
} }
} else { } else {
println!("oauth2: set addr first."); println!("oauth2: set addr first.");

View File

@@ -29,7 +29,7 @@ $ pip install .
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`. 1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
2. Install tox `pip install -U tox` 2. Install tox `pip install -U tox`
3. Run `CHATMAIL_DOMAIN=ci-chatmail.testrun.org PATH="../target/debug:$PATH" tox`. 3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output. Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.

View File

@@ -43,12 +43,7 @@ ignore = [
# hickory-proto 0.25.2 quadratic complexity issue. # hickory-proto 0.25.2 quadratic complexity issue.
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02. # Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
# <https://rustsec.org/advisories/RUSTSEC-2026-0119> # <https://rustsec.org/advisories/RUSTSEC-2026-0119>
"RUSTSEC-2026-0119", "RUSTSEC-2026-0119"
# Timing side channel in ml-dsa dependency of rPGP.
# We enable PQC for encryption rather than signatures.
# <https://rustsec.org/advisories/RUSTSEC-2025-0144>
"RUSTSEC-2025-0144",
] ]
[bans] [bans]
@@ -67,7 +62,6 @@ skip = [
{ name = "getrandom", version = "0.2.12" }, { name = "getrandom", version = "0.2.12" },
{ name = "heck", version = "0.4.1" }, { name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" }, { name = "http", version = "0.2.12" },
{ name = "hybrid-array", version = "0.2.3" },
{ name = "linux-raw-sys", version = "0.4.14" }, { name = "linux-raw-sys", version = "0.4.14" },
{ name = "lru", version = "0.12.5" }, { name = "lru", version = "0.12.5" },
{ name = "netlink-packet-route", version = "0.17.1" }, { name = "netlink-packet-route", version = "0.17.1" },

View File

@@ -1,9 +1,3 @@
#!/bin/sh #!/bin/sh
# Run clippy for all Rust code in the project. # Run clippy for all Rust code in the project.
# cargo clippy --workspace --all-targets --all-features -- -D warnings
# To check, run
# scripts/clippy.sh
#
# To automatically fix warnings, run
# scripts/clippy.sh --fix --allow-dirty
cargo clippy --workspace --all-targets --all-features "$@" -- -D warnings

View File

@@ -794,7 +794,7 @@ impl Config {
.with_push_subscriber(push_subscriber.clone()) .with_push_subscriber(push_subscriber.clone())
.build() .build()
.await .await
.with_context(|| format!("failed to create context from file {dbfile:?}"))?; .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
// Try to open without a passphrase, // Try to open without a passphrase,
// but do not return an error if account is passphare-protected. // but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?; ctx.open("".to_string()).await?;

View File

@@ -2529,7 +2529,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
// running numbers, etc. // running numbers, etc.
let filename: String = match viewtype_orig { let filename: String = match viewtype_orig {
Viewtype::Voice => format!( Viewtype::Voice => format!(
"voice-messsage_{}.{suffix}", "voice-messsage_{}.{}",
chrono::Utc chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0) .timestamp_opt(msg.timestamp_sort, 0)
.single() .single()
@@ -2537,9 +2537,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|| "YY-mm-dd_hh:mm:ss".to_string(), || "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string() |ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
), ),
&suffix
), ),
Viewtype::Image | Viewtype::Gif => format!( Viewtype::Image | Viewtype::Gif => format!(
"image_{}.{suffix}", "image_{}.{}",
chrono::Utc chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0) .timestamp_opt(msg.timestamp_sort, 0)
.single() .single()
@@ -2547,9 +2548,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|| "YY-mm-dd_hh:mm:ss".to_string(), || "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(), |ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
), ),
&suffix,
), ),
Viewtype::Video => format!( Viewtype::Video => format!(
"video_{}.{suffix}", "video_{}.{}",
chrono::Utc chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0) .timestamp_opt(msg.timestamp_sort, 0)
.single() .single()
@@ -2557,6 +2559,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|| "YY-mm-dd_hh:mm:ss".to_string(), || "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string() |ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
), ),
&suffix
), ),
_ => filename, _ => filename,
}; };
@@ -2951,7 +2954,6 @@ WHERE id=?
) )
.await?; .await?;
let chunk_size = context.get_max_smtp_rcpt_to().await?;
let trans_fn = |t: &mut rusqlite::Transaction| { let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new(); let mut row_ids = Vec::<i64>::new();
@@ -2965,12 +2967,12 @@ WHERE id=?
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)", VALUES (?1, ?2, ?3, ?4)",
)?; )?;
for recipients_chunk in recipients.chunks(chunk_size) { if !recipients.is_empty() {
let recipients_chunk = recipients_chunk.join(" "); let all_recipients = recipients.join(" ");
if let Some(pre_msg) = &rendered_pre_msg { if let Some(pre_msg) = &rendered_pre_msg {
let row_id = stmt.execute(( let row_id = stmt.execute((
&pre_msg.rfc724_mid, &pre_msg.rfc724_mid,
&recipients_chunk, &all_recipients,
&pre_msg.message, &pre_msg.message,
msg.id, msg.id,
))?; ))?;
@@ -2978,7 +2980,7 @@ WHERE id=?
} }
let row_id = stmt.execute(( let row_id = stmt.execute((
&rendered_msg.rfc724_mid, &rendered_msg.rfc724_mid,
&recipients_chunk, &all_recipients,
&rendered_msg.message, &rendered_msg.message,
msg.id, msg.id,
))?; ))?;

View File

@@ -707,7 +707,8 @@ async fn get_autoconfig(
ctx, ctx,
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense // the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense
&format!( &format!(
"https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}" "https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
&param_domain, &param_addr_urlencoded
), ),
&param.addr, &param.addr,
) )
@@ -720,7 +721,7 @@ async fn get_autoconfig(
// Outlook uses always SSL but different domains (this comment describes the next two steps) // Outlook uses always SSL but different domains (this comment describes the next two steps)
if let Ok(res) = outlk_autodiscover( if let Ok(res) = outlk_autodiscover(
ctx, ctx,
format!("https://{param_domain}/autodiscover/autodiscover.xml"), format!("https://{}/autodiscover/autodiscover.xml", &param_domain),
) )
.await .await
{ {
@@ -730,7 +731,10 @@ async fn get_autoconfig(
if let Ok(res) = outlk_autodiscover( if let Ok(res) = outlk_autodiscover(
ctx, ctx,
format!("https://autodiscover.{param_domain}/autodiscover/autodiscover.xml",), format!(
"https://autodiscover.{}/autodiscover/autodiscover.xml",
&param_domain
),
) )
.await .await
{ {
@@ -741,7 +745,7 @@ async fn get_autoconfig(
// always SSL for Thunderbird's database // always SSL for Thunderbird's database
if let Ok(res) = moz_autoconfigure( if let Ok(res) = moz_autoconfigure(
ctx, ctx,
&format!("https://autoconfig.thunderbird.net/v1.1/{param_domain}"), &format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain),
&param.addr, &param.addr,
) )
.await .await

View File

@@ -204,9 +204,6 @@ pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
// `max_smtp_rcpt_to` in the provider db. // `max_smtp_rcpt_to` in the provider db.
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50; pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
/// Same as `DEFAULT_MAX_SMTP_RCPT_TO`, but for chatmail relays.
pub(crate) const DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO: usize = 999;
/// How far the last quota check needs to be in the past to be checked by the background function (in seconds). /// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours

View File

@@ -16,7 +16,7 @@ use tokio::sync::{Mutex, Notify, RwLock};
use crate::chat::{ChatId, get_chat_cnt}; use crate::chat::{ChatId, get_chat_cnt};
use crate::config::Config; use crate::config::Config;
use crate::constants::{self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR}; use crate::constants::{DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR};
use crate::contact::{Contact, ContactId}; use crate::contact::{Contact, ContactId};
use crate::debug_logging::DebugLogging; use crate::debug_logging::DebugLogging;
use crate::events::{Event, EventEmitter, EventType, Events}; use crate::events::{Event, EventEmitter, EventType, Events};
@@ -587,23 +587,6 @@ impl Context {
self.get_config_bool(Config::IsChatmail).await self.get_config_bool(Config::IsChatmail).await
} }
/// Returns maximum number of recipients the provider allows to send a single email to.
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<usize> {
let is_chatmail = self.is_chatmail().await?;
let val = self
.get_configured_provider()
.await?
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or_else(
|| match is_chatmail {
true => constants::DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO,
false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
},
usize::from,
);
Ok(val)
}
/// Does a single round of fetching from IMAP and returns. /// Does a single round of fetching from IMAP and returns.
/// ///
/// Can be used even if I/O is currently stopped. /// Can be used even if I/O is currently stopped.

View File

@@ -304,7 +304,7 @@ mod tests {
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
This message does not have Content-Type nor Subject.<br/> This message does not have Content-Type nor Subject.<br/>
</body></html> </body></html>
"# "#
@@ -322,7 +322,7 @@ This message does not have Content-Type nor Subject.<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/> message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
</body></html> </body></html>
"# "#
@@ -341,7 +341,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
This line ends with a space and will be merged with the next one due to format=flowed.<br/> This line ends with a space and will be merged with the next one due to format=flowed.<br/>
<br/> <br/>
This line does not end with a space<br/> This line does not end with a space<br/>
@@ -362,7 +362,7 @@ and will be wrapped as usual.<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
mime-modified should not be set set as there is no html and no special stuff;<br/> mime-modified should not be set set as there is no html and no special stuff;<br/>
although not being a delta-message.<br/> although not being a delta-message.<br/>
test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x27; :)<br/> test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x27; :)<br/>

View File

@@ -1045,12 +1045,15 @@ impl Session {
if target.is_empty() { if target.is_empty() {
self.delete_message_batch(context, &uid_set, rowid_set) self.delete_message_batch(context, &uid_set, rowid_set)
.await .await
.with_context(|| format!("cannot delete batch of messages {uid_set:?}"))?; .with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
} else { } else {
self.move_message_batch(context, &uid_set, rowid_set, &target) self.move_message_batch(context, &uid_set, rowid_set, &target)
.await .await
.with_context(|| { .with_context(|| {
format!("cannot move batch of messages {uid_set:?} to folder {target:?}",) format!(
"cannot move batch of messages {:?} to folder {:?}",
&uid_set, target
)
})?; })?;
} }
} }
@@ -1284,10 +1287,9 @@ impl Session {
for (request_uids, set) in build_sequence_sets(&request_uids)? { for (request_uids, set) in build_sequence_sets(&request_uids)? {
info!(context, "Starting UID FETCH of message set \"{}\".", set); info!(context, "Starting UID FETCH of message set \"{}\".", set);
let mut fetch_responses = self let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
.uid_fetch(&set, BODY_FULL) format!("fetching messages {} from folder \"{}\"", &set, folder)
.await })?;
.with_context(|| format!("fetching messages {set} from folder {folder:?}"))?;
// Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here // Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here
// when we want to process other messages first. // when we want to process other messages first.
@@ -1501,7 +1503,7 @@ impl Session {
.get_metadata( .get_metadata(
mailbox, mailbox,
options, options,
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)", "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn /shared/vendor/deltachat/maxsmtprecipients)",
) )
.await?; .await?;
for m in metadata { for m in metadata {
@@ -1537,6 +1539,21 @@ impl Session {
} }
} }
} }
"/shared/vendor/deltachat/maxsmtprecipients" => {
if let Some(value) = m.value.and_then(|v| v.parse::<u32>().ok()) {
let transport_id = self.transport_id();
context
.sql
.execute(
"UPDATE transports \
SET max_smtp_rcpt_to=? WHERE id=?",
(value, transport_id),
)
.await
.log_err(context)
.ok();
}
}
_ => {} _ => {}
} }
} }

View File

@@ -427,16 +427,18 @@ mod tests {
let self_chat = ctx1.get_self_chat().await; let self_chat = ctx1.get_self_chat().await;
let msgs = get_chat_msgs(&ctx1, self_chat.id).await.unwrap(); let msgs = get_chat_msgs(&ctx1, self_chat.id).await.unwrap();
assert_eq!(msgs.len(), 2); assert_eq!(msgs.len(), 2);
let ChatItem::Message { msg_id } = msgs.first().unwrap() else { let msgid = match msgs.first().unwrap() {
panic!("wrong chat item"); ChatItem::Message { msg_id } => msg_id,
_ => panic!("wrong chat item"),
}; };
let msg = Message::load_from_db(&ctx1, *msg_id).await.unwrap(); let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
let text = msg.get_text(); let text = msg.get_text();
assert_eq!(text, "hi there"); assert_eq!(text, "hi there");
let ChatItem::Message { msg_id } = msgs.get(1).unwrap() else { let msgid = match msgs.get(1).unwrap() {
panic!("wrong chat item"); ChatItem::Message { msg_id } => msg_id,
_ => panic!("wrong chat item"),
}; };
let msg = Message::load_from_db(&ctx1, *msg_id).await.unwrap(); let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
let path = msg.get_file(&ctx1).unwrap(); let path = msg.get_file(&ctx1).unwrap();
assert_eq!( assert_eq!(

View File

@@ -570,11 +570,9 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul
pub struct Fingerprint(Vec<u8>); pub struct Fingerprint(Vec<u8>);
impl Fingerprint { impl Fingerprint {
/// Creates new fingerprint. /// Creates new 160-bit (20 bytes) fingerprint.
///
/// It is 160-bit (20 bytes) for v4 keys and 32 bytes for v6 keys.
pub fn new(v: Vec<u8>) -> Fingerprint { pub fn new(v: Vec<u8>) -> Fingerprint {
debug_assert!(v.len() == 20 || v.len() == 32); debug_assert_eq!(v.len(), 20);
Fingerprint(v) Fingerprint(v)
} }
@@ -627,10 +625,7 @@ impl std::str::FromStr for Fingerprint {
.filter(|&c| c.is_ascii_hexdigit()) .filter(|&c| c.is_ascii_hexdigit())
.collect(); .collect();
let v: Vec<u8> = hex::decode(&hex_repr)?; let v: Vec<u8> = hex::decode(&hex_repr)?;
ensure!( ensure!(v.len() == 20, "wrong fingerprint length: {hex_repr}");
v.len() == 20 || v.len() == 32,
"wrong fingerprint length: {hex_repr}"
);
let fp = Fingerprint::new(v); let fp = Fingerprint::new(v);
Ok(fp) Ok(fp)
} }

View File

@@ -222,7 +222,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
} else { } else {
msg.timestamp_sort msg.timestamp_sort
}); });
ret += &format!("Received: {s}"); ret += &format!("Received: {}", &s);
ret += "\n"; ret += "\n";
} }
@@ -301,7 +301,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
ret += "Type: "; ret += "Type: ";
ret += &format!("{}", msg.viewtype); ret += &format!("{}", msg.viewtype);
ret += "\n"; ret += "\n";
ret += &format!("Mimetype: {}\n", msg.get_filemime().unwrap_or_default()); ret += &format!("Mimetype: {}\n", &msg.get_filemime().unwrap_or_default());
} }
let w = msg.param.get_int(Param::Width).unwrap_or_default(); let w = msg.param.get_int(Param::Width).unwrap_or_default();
let h = msg.param.get_int(Param::Height).unwrap_or_default(); let h = msg.param.get_int(Param::Height).unwrap_or_default();

View File

@@ -1939,13 +1939,32 @@ pub(crate) fn render_outer_message(
/// Takes the encrypted part, wraps it in a MimePart, /// Takes the encrypted part, wraps it in a MimePart,
/// and sets the appropriate Content-Type for the outer message /// and sets the appropriate Content-Type for the outer message
pub(crate) fn wrap_encrypted_part(encrypted: String) -> MimePart<'static> { pub(crate) fn wrap_encrypted_part(encrypted: String) -> MimePart<'static> {
// XXX: additional newline is needed
// to pass filtermail at
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>:
let encrypted = encrypted + "\n";
MimePart::new( MimePart::new(
"multipart/encrypted; protocol=\"application/pgp-encrypted\"", "multipart/encrypted; protocol=\"application/pgp-encrypted\"",
vec![ vec![
// Autocrypt part 1 // Autocrypt part 1
MimePart::new("application/pgp-encrypted", "Version: 1\r\n"), MimePart::new("application/pgp-encrypted", "Version: 1\r\n").header(
"Content-Description",
mail_builder::headers::raw::Raw::new("PGP/MIME version identification"),
),
// Autocrypt part 2 // Autocrypt part 2
MimePart::new("application/octet-stream", encrypted), MimePart::new(
"application/octet-stream; name=\"encrypted.asc\"",
encrypted,
)
.header(
"Content-Description",
mail_builder::headers::raw::Raw::new("OpenPGP encrypted message"),
)
.header(
"Content-Disposition",
mail_builder::headers::raw::Raw::new("inline; filename=\"encrypted.asc\";"),
),
], ],
) )
} }

View File

@@ -356,7 +356,7 @@ impl MimeMessage {
let decrypted_msg; // Decrypted signed OpenPGP message. let decrypted_msg; // Decrypted signed OpenPGP message.
let expected_sender_fingerprint: Option<String>; let expected_sender_fingerprint: Option<String>;
let (mail, is_encrypted) = match Box::pin(decrypt::decrypt(context, &mail)).await { let (mail, is_encrypted) = match decrypt::decrypt(context, &mail).await {
Ok(Some((mut msg, expected_sender_fp))) => { Ok(Some((mut msg, expected_sender_fp))) => {
mail_raw = msg.as_data_vec().unwrap_or_default(); mail_raw = msg.as_data_vec().unwrap_or_default();

View File

@@ -109,8 +109,8 @@ pub(crate) async fn connect_tcp_inner(
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> { ) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
let tcp_stream = timeout(TIMEOUT, TcpStream::connect(addr)) let tcp_stream = timeout(TIMEOUT, TcpStream::connect(addr))
.await .await
.with_context(|| format!("Connection to {addr} timed out"))? .context("Connection timeout")?
.with_context(|| format!("Connection to {addr} failed"))?; .context("Connection failure")?;
// Disable Nagle's algorithm. // Disable Nagle's algorithm.
tcp_stream.set_nodelay(true)?; tcp_stream.set_nodelay(true)?;
@@ -180,7 +180,7 @@ where
delay_set.spawn(tokio::time::sleep(delay)); delay_set.spawn(tokio::time::sleep(delay));
} }
let mut all_errors = Vec::new(); let mut first_error = None;
let res = loop { let res = loop {
if let Some(fut) = futures.next() { if let Some(fut) = futures.next() {
@@ -200,7 +200,7 @@ where
} }
Ok(Err(err)) => { Ok(Err(err)) => {
// Some connection attempt failed. // Some connection attempt failed.
all_errors.push(err); first_error.get_or_insert(err);
} }
Err(err) => { Err(err) => {
break Err(err); break Err(err);
@@ -211,11 +211,9 @@ where
// Out of connection attempts. // Out of connection attempts.
// //
// Break out of the loop and return error. // Break out of the loop and return error.
break if all_errors.is_empty() { break Err(
Err(format_err!("No connection attempts were made")) first_error.unwrap_or_else(|| format_err!("No connection attempts were made"))
} else { );
Err(format_err!("All connection attempts failed: {}", all_errors.into_iter().map(|err| format!("{err:#}")).collect::<Vec<String>>().join("; ")))
};
} }
} }
}, },

View File

@@ -847,41 +847,4 @@ mod tests {
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err()); assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err()); assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
} }
/// Test PQC support.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pqc() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let pqc = &tcm.pqc().await;
let pqc_received_message = tcm.send_recv_accept(alice, pqc, "Hi!").await;
let pqc_chat_id = pqc_received_message.chat_id;
let pqc_sent = pqc.send_text(pqc_chat_id, "Hello back!").await;
let alice_rcvd = alice.recv_msg(&pqc_sent).await;
assert_eq!(alice_rcvd.text, "Hello back!");
Ok(())
}
/// Tests securejoin with inviter using PQC key.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_securejoin_pqc_inviter() {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let pqc = &tcm.pqc().await;
tcm.execute_securejoin(pqc, alice).await;
}
/// Tests securejoin with joiner using PQC key.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_securejoin_pqc_joiner() {
let mut tcm = TestContextManager::new();
let pqc = &tcm.pqc().await;
let bob = &tcm.bob().await;
tcm.execute_securejoin(bob, pqc).await;
}
} }

View File

@@ -39,7 +39,7 @@ impl PlainText {
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
"# "#
.to_string(); .to_string();
@@ -132,7 +132,7 @@ http://link-at-start-of-line.org
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
line 1<br/> line 1<br/>
line 2<br/> line 2<br/>
line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a> and <a href="http://link-end-of-line.com/file?foo=bar%20">http://link-end-of-line.com/file?foo=bar%20</a><br/> line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a> and <a href="http://link-end-of-line.com/file?foo=bar%20">http://link-end-of-line.com/file?foo=bar%20</a><br/>
@@ -156,7 +156,7 @@ line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
Foo<br/> Foo<br/>
bar<br/> bar<br/>
</body></html> </body></html>
@@ -178,7 +178,7 @@ bar<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
line with &lt;<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.link/?foo=_bar</a>&gt; here!<br/> line with &lt;<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.link/?foo=_bar</a>&gt; here!<br/>
</body></html> </body></html>
"# "#
@@ -199,7 +199,7 @@ line with &lt;<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.l
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
line with nohttp://no.link here<br/> line with nohttp://no.link here<br/>
</body></html> </body></html>
"# "#
@@ -220,7 +220,7 @@ line with nohttp://no.link here<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:another@one.de">another@one.de</a><br/> just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:another@one.de">another@one.de</a><br/>
</body></html> </body></html>
"# "#
@@ -241,7 +241,7 @@ just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:an
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
line still line<br/> line still line<br/>
<em>&gt;quote </em><br/> <em>&gt;quote </em><br/>
<em>&gt;still quote</em><br/> <em>&gt;still quote</em><br/>
@@ -265,7 +265,7 @@ line still line<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
linestill line<br/> linestill line<br/>
<em>&gt;quote </em><br/> <em>&gt;quote </em><br/>
<em>&gt;still quote</em><br/> <em>&gt;still quote</em><br/>
@@ -289,7 +289,7 @@ linestill line<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
line <br/> line <br/>
still line<br/> still line<br/>
<em>&gt;quote </em><br/> <em>&gt;quote </em><br/>
@@ -314,7 +314,7 @@ still line<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body dir="auto" style="unicode-bidi: plaintext"> </head><body>
def foo():<br/> def foo():<br/>
&nbsp;&nbsp;&nbsp;&nbsp;pass<br/> &nbsp;&nbsp;&nbsp;&nbsp;pass<br/>
<br/> <br/>

View File

@@ -332,7 +332,7 @@ fn inner_generate_secure_join_qr_code(
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?; d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?;
d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?; d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?;
d.attr("r", HALF_LOGO_SIZE)?; d.attr("r", HALF_LOGO_SIZE)?;
d.attr("style", format!("fill:{color}")) d.attr("style", format!("fill:{}", &color))
})?; })?;
let avatar_font_size = LOGO_SIZE * 0.65; let avatar_font_size = LOGO_SIZE * 0.65;

View File

@@ -2344,6 +2344,8 @@ INSERT INTO msgs
/// Checks for "Chat-Edit" and "Chat-Delete" headers, /// Checks for "Chat-Edit" and "Chat-Delete" headers,
/// and edits/deletes existing messages accordingly. /// and edits/deletes existing messages accordingly.
///
/// Returns `true` if this message is an edit/deletion request.
async fn handle_edit_delete( async fn handle_edit_delete(
context: &Context, context: &Context,
mime_parser: &MimeMessage, mime_parser: &MimeMessage,
@@ -3558,7 +3560,12 @@ async fn create_or_lookup_mailinglist_or_broadcast(
mime_parser.timestamp_sent, mime_parser.timestamp_sent,
) )
.await .await
.with_context(|| format!("failed to create mailinglist '{name}' for grpid={listid}",))?; .with_context(|| {
format!(
"failed to create mailinglist '{}' for grpid={}",
&name, &listid
)
})?;
if chattype == Chattype::InBroadcast { if chattype == Chattype::InBroadcast {
chat::add_to_chat_contacts_table( chat::add_to_chat_contacts_table(

View File

@@ -704,10 +704,10 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
assert_eq!(msg.state, MessageState::OutFailed); assert_eq!(msg.state, MessageState::OutFailed);
let msgs = chat::get_chat_msgs(&t, msg.chat_id).await?; let msgs = chat::get_chat_msgs(&t, msg.chat_id).await?;
assert!(matches!( let ChatItem::Message { msg_id } = *msgs.last().unwrap() else {
*msgs.last().unwrap(), panic!("Wrong item type");
ChatItem::Message { msg_id } if msg_id == msg.id };
)); assert_eq!(msg_id, msg.id);
Ok(()) Ok(())
} }
@@ -1598,7 +1598,9 @@ async fn test_in_reply_to() {
// Load the first message from the same chat. // Load the first message from the same chat.
let msgs = chat::get_chat_msgs(&t, msg.chat_id).await.unwrap(); let msgs = chat::get_chat_msgs(&t, msg.chat_id).await.unwrap();
let ChatItem::Message { msg_id } = msgs.first().unwrap() else { let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() {
msg_id
} else {
panic!("Wrong item type"); panic!("Wrong item type");
}; };

View File

@@ -218,7 +218,7 @@ pub(crate) fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStor
impl fmt::Debug for ConnectivityStore { impl fmt::Debug for ConnectivityStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(guard) = self.0.try_lock() { if let Some(guard) = self.0.try_lock() {
write!(f, "ConnectivityStore {:?}", *guard) write!(f, "ConnectivityStore {:?}", &*guard)
} else { } else {
write!(f, "ConnectivityStore [LOCKED]") write!(f, "ConnectivityStore [LOCKED]")
} }

View File

@@ -73,7 +73,8 @@ fn shorten_name(name: &str, length: usize) -> String {
// We use _ rather than ... to avoid dots at the end of the URL, which would confuse linkifiers // We use _ rather than ... to avoid dots at the end of the URL, which would confuse linkifiers
format!( format!(
"{}_", "{}_",
name.chars() &name
.chars()
.take(length.saturating_sub(1)) .take(length.saturating_sub(1))
.collect::<String>() .collect::<String>()
) )

View File

@@ -992,7 +992,7 @@ async fn test_wrong_auth_token() -> Result<()> {
tcm.send_recv(alice, bob, "hi").await; tcm.send_recv(alice, bob, "hi").await;
let alice_qr = get_securejoin_qr(alice, None).await?; let alice_qr = get_securejoin_qr(alice, None).await?;
println!("{alice_qr}"); println!("{}", &alice_qr);
let invalid_alice_qr = alice_qr.replace("&s=", "&s=INVALIDAUTHTOKEN&someotherkey="); let invalid_alice_qr = alice_qr.replace("&s=", "&s=INVALIDAUTHTOKEN&someotherkey=");
join_securejoin(bob, &invalid_alice_qr).await?; join_securejoin(bob, &invalid_alice_qr).await?;

View File

@@ -2,6 +2,8 @@
mod connect; mod connect;
pub mod send; pub mod send;
#[cfg(test)]
mod chunking_tests;
use anyhow::{Context as _, Error, Result, bail, format_err}; use anyhow::{Context as _, Error, Result, bail, format_err};
use async_smtp::response::{Category, Code, Detail}; use async_smtp::response::{Category, Code, Detail};
@@ -10,6 +12,7 @@ use tokio::task;
use crate::chat::{ChatId, add_info_msg_with_cmd}; use crate::chat::{ChatId, add_info_msg_with_cmd};
use crate::config::Config; use crate::config::Config;
use crate::constants;
use crate::contact::{Contact, ContactId}; use crate::contact::{Contact, ContactId};
use crate::context::Context; use crate::context::Context;
use crate::events::EventType; use crate::events::EventType;
@@ -34,6 +37,9 @@ pub(crate) struct Smtp {
/// Email address we are sending from. /// Email address we are sending from.
from: Option<EmailAddress>, from: Option<EmailAddress>,
/// Transport used for the current connection.
transport_id: Option<u32>,
/// Timestamp of last successful send/receive network interaction /// Timestamp of last successful send/receive network interaction
/// (eg connect or send succeeded). On initialization and disconnect /// (eg connect or send succeeded). On initialization and disconnect
/// it is set to None. /// it is set to None.
@@ -60,6 +66,7 @@ impl Smtp {
task::spawn(async move { transport.quit().await }); task::spawn(async move { transport.quit().await });
} }
self.last_success = None; self.last_success = None;
self.transport_id = None;
} }
/// Return true if smtp was connected but is not known to /// Return true if smtp was connected but is not known to
@@ -89,9 +96,10 @@ impl Smtp {
} }
self.connectivity.set_connecting(context); self.connectivity.set_connecting(context);
let (_transport_id, lp) = ConfiguredLoginParam::load(context) let (transport_id, lp) = ConfiguredLoginParam::load(context)
.await? .await?
.context("Not configured")?; .context("Not configured")?;
self.transport_id = Some(transport_id);
let proxy_config = ProxyConfig::load(context).await?; let proxy_config = ProxyConfig::load(context).await?;
self.connect( self.connect(
context, context,
@@ -165,6 +173,7 @@ impl Smtp {
} }
} }
#[derive(Debug)]
pub(crate) enum SendResult { pub(crate) enum SendResult {
/// Message was sent successfully. /// Message was sent successfully.
Success, Success,
@@ -176,13 +185,36 @@ pub(crate) enum SendResult {
Retry, Retry,
} }
pub(crate) trait SmtpSender: Send {
fn send_chunk<'a>(
&'a mut self,
context: &'a Context,
recipients: &'a [async_smtp::EmailAddress],
body: &'a str,
) -> futures::future::BoxFuture<'a, SendResult>;
}
struct RealSmtpSender<'a> {
smtp: &'a mut Smtp,
}
impl SmtpSender for RealSmtpSender<'_> {
fn send_chunk<'a>(
&'a mut self,
context: &'a Context,
recipients: &'a [async_smtp::EmailAddress],
body: &'a str,
) -> futures::future::BoxFuture<'a, SendResult> {
Box::pin(smtp_send(context, recipients, body, self.smtp))
}
}
/// Tries to send a message. /// Tries to send a message.
pub(crate) async fn smtp_send( pub(crate) async fn smtp_send(
context: &Context, context: &Context,
recipients: &[async_smtp::EmailAddress], recipients: &[async_smtp::EmailAddress],
message: &str, message: &str,
smtp: &mut Smtp, smtp: &mut Smtp,
msg_id: Option<MsgId>,
) -> SendResult { ) -> SendResult {
if recipients.is_empty() { if recipients.is_empty() {
return SendResult::Success; return SendResult::Success;
@@ -310,25 +342,6 @@ pub(crate) async fn smtp_send(
Ok(()) => SendResult::Success, Ok(()) => SendResult::Success,
}; };
if let SendResult::Failure(err) = &status
&& let Some(msg_id) = msg_id
{
// We couldn't send the message, so mark it as failed
match Message::load_from_db(context, msg_id).await {
Ok(mut msg) => {
if let Err(err) = message::set_msg_failed(context, &mut msg, &err.to_string()).await
{
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
}
}
Err(err) => {
error!(
context,
"Failed to load {msg_id} to mark it as failed: {err:#}."
);
}
}
}
status status
} }
@@ -406,7 +419,40 @@ pub(crate) async fn send_msg_to_smtp(
) )
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let status = smtp_send(context, &recipients_list, body.as_str(), smtp, Some(msg_id)).await; let transport_id = smtp
.transport_id
.context("SMTP not connected to a transport")?;
let chunk_size = max_smtp_rcpt_to(context, transport_id).await?;
let mut sender = RealSmtpSender { smtp };
let (status, start_idx) = send_smtp_chunks(
context,
&recipients_list,
body.as_str(),
chunk_size,
&mut sender,
)
.await;
let unsent_saved = start_idx < recipients_list.len();
if let Some(unsent) = recipients_list.get(start_idx..)
&& !unsent.is_empty()
{
let unsent_str: String = unsent
.iter()
.map(|a| a.as_ref())
.collect::<Vec<&str>>()
.join(" ");
context
.sql
.execute(
"UPDATE smtp SET recipients=? WHERE id=?",
(unsent_str, rowid),
)
.await
.log_err(context)
.ok();
}
match status { match status {
SendResult::Retry => {} SendResult::Retry => {}
@@ -455,11 +501,16 @@ pub(crate) async fn send_msg_to_smtp(
.await?; .await?;
}; };
} }
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
message::set_msg_failed(context, &mut msg, &err.to_string()).await?;
}
if !unsent_saved {
context context
.sql .sql
.execute("DELETE FROM smtp WHERE id=?", (rowid,)) .execute("DELETE FROM smtp WHERE id=?", (rowid,))
.await?; .await?;
} }
}
}; };
match status { match status {
@@ -470,9 +521,38 @@ pub(crate) async fn send_msg_to_smtp(
} }
Ok(()) Ok(())
} }
SendResult::Failure(err) => Err(format_err!("{err}")), SendResult::Failure(err) => {
if unsent_saved {
Err(format_err!("Retry"))
} else {
Err(format_err!("{err}"))
} }
} }
}
}
async fn max_smtp_rcpt_to(context: &Context, transport_id: u32) -> Result<usize> {
let limit = context
.sql
.query_row_optional(
"SELECT max_smtp_rcpt_to FROM transports WHERE id=?",
(transport_id,),
|row| row.get::<_, u32>(0),
)
.await?
.unwrap_or(0);
if limit > 0 {
return Ok(limit as usize);
}
let val = context
.get_configured_provider()
.await?
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
Ok(val)
}
pub(crate) async fn msg_has_pending_smtp_job( pub(crate) async fn msg_has_pending_smtp_job(
context: &Context, context: &Context,
@@ -600,7 +680,7 @@ async fn send_mdn_rfc724_mid(
}) })
.collect(); .collect();
match smtp_send(context, &recipients, &body, smtp, None).await { match smtp_send(context, &recipients, &body, smtp).await {
SendResult::Success => { SendResult::Success => {
if !recipients.is_empty() { if !recipients.is_empty() {
info!(context, "Successfully sent MDN for {rfc724_mid}."); info!(context, "Successfully sent MDN for {rfc724_mid}.");
@@ -722,3 +802,22 @@ pub(crate) async fn add_self_recipients(
} }
Ok(()) Ok(())
} }
#[allow(clippy::arithmetic_side_effects)]
pub(crate) async fn send_smtp_chunks(
context: &Context,
recipients: &[async_smtp::EmailAddress],
body: &str,
chunk_size: usize,
sender: &mut (dyn SmtpSender + Send),
) -> (SendResult, usize) {
for (i, chunk) in recipients.chunks(chunk_size).enumerate() {
let status = sender.send_chunk(context, chunk, body).await;
match status {
SendResult::Success => continue,
SendResult::Failure(_) => return (status, (i + 1) * chunk_size),
SendResult::Retry => return (status, i * chunk_size),
}
}
(SendResult::Success, recipients.len())
}

102
src/smtp/chunking_tests.rs Normal file
View File

@@ -0,0 +1,102 @@
use crate::smtp::{send_smtp_chunks, SendResult, SmtpSender};
use crate::test_utils::TestContextManager;
use crate::context::Context;
use anyhow::Result;
use futures::future::{BoxFuture, FutureExt};
/// Result the mock should return on the designated call.
enum MockFailure {
Transient,
Permanent,
}
struct MockSmtpSender {
call_count: usize,
fail_on_call: Option<(usize, MockFailure)>,
}
impl SmtpSender for MockSmtpSender {
fn send_chunk<'a>(
&'a mut self,
_context: &'a Context,
_recipients: &'a [async_smtp::EmailAddress],
_body: &'a str,
) -> BoxFuture<'a, SendResult> {
self.call_count += 1;
let count = self.call_count;
let fail_on = self.fail_on_call.as_ref().map(|(n, _)| *n);
let is_permanent = matches!(
self.fail_on_call,
Some((_, MockFailure::Permanent))
);
async move {
if fail_on == Some(count) {
if is_permanent {
return SendResult::Failure(
anyhow::format_err!("permanent error"),
);
}
return SendResult::Retry;
}
SendResult::Success
}
.boxed()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_smtp_chunks() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let recipients: Vec<_> = ["r1@ex.org", "r2@ex.org", "r3@ex.org", "r4@ex.org", "r5@ex.org"]
.iter()
.map(|a| async_smtp::EmailAddress::new(a.to_string()).unwrap())
.collect();
// All chunks succeed.
let mut sender = MockSmtpSender { call_count: 0, fail_on_call: None };
let (status, processed) =
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
assert!(matches!(status, SendResult::Success));
assert_eq!(processed, 5);
assert_eq!(sender.call_count, 3); // chunks: [2, 2, 1]
// Second chunk gets a transient error, only first chunk's recipients are processed.
let mut sender =
MockSmtpSender { call_count: 0, fail_on_call: Some((2, MockFailure::Transient)) };
let (status, processed) =
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
assert!(matches!(status, SendResult::Retry));
assert_eq!(processed, 2);
assert_eq!(sender.call_count, 2);
// Last chunk gets a transient error, first two chunks' recipients are processed.
let mut sender =
MockSmtpSender { call_count: 0, fail_on_call: Some((3, MockFailure::Transient)) };
let (status, processed) =
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
assert!(matches!(status, SendResult::Retry));
assert_eq!(processed, 4);
assert_eq!(sender.call_count, 3);
// Second chunk gets a permanent error; processed includes the failed chunk.
let mut sender =
MockSmtpSender { call_count: 0, fail_on_call: Some((2, MockFailure::Permanent)) };
let (status, processed) =
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
assert!(matches!(status, SendResult::Failure(_)));
assert_eq!(processed, 4);
assert_eq!(sender.call_count, 2);
// Last chunk gets a permanent error; processed includes the failed chunk.
let mut sender =
MockSmtpSender { call_count: 0, fail_on_call: Some((3, MockFailure::Permanent)) };
let (status, processed) =
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
assert!(matches!(status, SendResult::Failure(_)));
assert_eq!(processed, 6); // capped at (i+1)*chunk_size, may exceed len
assert_eq!(sender.call_count, 3);
Ok(())
}

View File

@@ -2378,13 +2378,22 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
sql.execute_migration( sql.execute_migration(
" "
UPDATE msgs SET state=26 WHERE state=28; -- Change OutMdnRcvd to OutDelivered. UPDATE msgs SET state=26 WHERE state=28; -- Change OutMdnRcvd to OutDelivered.
UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed. UPDATE msgs SET state=19 WHERE state=24; -- Change OutPreparing to OutFailed.
", ",
migration_version, migration_version,
) )
.await?; .await?;
} }
inc_and_check(&mut migration_version, 153)?;
if dbversion < migration_version {
sql.execute_migration(
"ALTER TABLE transports ADD COLUMN max_smtp_rcpt_to INTEGER NOT NULL DEFAULT 0",
migration_version,
)
.await?;
}
let new_version = sql let new_version = sql
.get_raw_config_int(VERSION_CFG) .get_raw_config_int(VERSION_CFG)
.await? .await?

View File

@@ -137,17 +137,6 @@ impl TestContextManager {
.await .await
} }
/// Returns a new "device" with a preconfigured v6 PQC key.
pub async fn pqc(&mut self) -> TestContext {
TestContext::builder()
.with_key_pair(pqc_keypair())
.with_address("pqc@example.org".to_string())
.with_id_offset(7000)
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.await
}
/// Creates a new unconfigured test account. /// Creates a new unconfigured test account.
pub async fn unconfigured(&mut self) -> TestContext { pub async fn unconfigured(&mut self) -> TestContext {
TestContext::builder() TestContext::builder()
@@ -315,9 +304,6 @@ impl TestContextManager {
pub struct TestContextBuilder { pub struct TestContextBuilder {
key_pair: Option<SignedSecretKey>, key_pair: Option<SignedSecretKey>,
/// Email address.
address: Option<String>,
/// Log sink if set. /// Log sink if set.
/// ///
/// If log sink is not set, /// If log sink is not set,
@@ -342,7 +328,6 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(alice_keypair())`. /// This is a shortcut for `.with_key_pair(alice_keypair())`.
pub fn configure_alice(self) -> Self { pub fn configure_alice(self) -> Self {
self.with_key_pair(alice_keypair()) self.with_key_pair(alice_keypair())
.with_address("alice@example.org".to_string())
} }
/// Configures as bob@example.net with fixed secret key. /// Configures as bob@example.net with fixed secret key.
@@ -350,7 +335,6 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(bob_keypair())`. /// This is a shortcut for `.with_key_pair(bob_keypair())`.
pub fn configure_bob(self) -> Self { pub fn configure_bob(self) -> Self {
self.with_key_pair(bob_keypair()) self.with_key_pair(bob_keypair())
.with_address("bob@example.net".to_string())
} }
/// Configures as charlie@example.net with fixed secret key. /// Configures as charlie@example.net with fixed secret key.
@@ -358,7 +342,6 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(charlie_keypair())`. /// This is a shortcut for `.with_key_pair(charlie_keypair())`.
pub fn configure_charlie(self) -> Self { pub fn configure_charlie(self) -> Self {
self.with_key_pair(charlie_keypair()) self.with_key_pair(charlie_keypair())
.with_address("charlie@example.net".to_string())
} }
/// Configures as dom@example.net with fixed secret key. /// Configures as dom@example.net with fixed secret key.
@@ -366,7 +349,6 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(dom_keypair())`. /// This is a shortcut for `.with_key_pair(dom_keypair())`.
pub fn configure_dom(self) -> Self { pub fn configure_dom(self) -> Self {
self.with_key_pair(dom_keypair()) self.with_key_pair(dom_keypair())
.with_address("dom@example.net".to_string())
} }
/// Configures as elena@example.net with fixed secret key. /// Configures as elena@example.net with fixed secret key.
@@ -374,7 +356,6 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(elena_keypair())`. /// This is a shortcut for `.with_key_pair(elena_keypair())`.
pub fn configure_elena(self) -> Self { pub fn configure_elena(self) -> Self {
self.with_key_pair(elena_keypair()) self.with_key_pair(elena_keypair())
.with_address("elena@example.net".to_string())
} }
/// Configures as fiona@example.net with fixed secret key. /// Configures as fiona@example.net with fixed secret key.
@@ -382,7 +363,6 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(fiona_keypair())`. /// This is a shortcut for `.with_key_pair(fiona_keypair())`.
pub fn configure_fiona(self) -> Self { pub fn configure_fiona(self) -> Self {
self.with_key_pair(fiona_keypair()) self.with_key_pair(fiona_keypair())
.with_address("fiona@example.net".to_string())
} }
/// Configures the new [`TestContext`] with the provided [`SignedSecretKey`]. /// Configures the new [`TestContext`] with the provided [`SignedSecretKey`].
@@ -394,12 +374,6 @@ impl TestContextBuilder {
self self
} }
/// Sets email address.
pub fn with_address(mut self, address: String) -> Self {
self.address = Some(address);
self
}
/// Attaches a [`LogSink`] to this [`TestContext`]. /// Attaches a [`LogSink`] to this [`TestContext`].
/// ///
/// This is useful when using multiple [`TestContext`] instances in one test: it allows /// This is useful when using multiple [`TestContext`] instances in one test: it allows
@@ -422,7 +396,16 @@ impl TestContextBuilder {
/// Builds the [`TestContext`]. /// Builds the [`TestContext`].
pub async fn build(self, used_names: Option<&mut BTreeSet<String>>) -> TestContext { pub async fn build(self, used_names: Option<&mut BTreeSet<String>>) -> TestContext {
if let Some(key_pair) = self.key_pair { if let Some(key_pair) = self.key_pair {
let addr = self.address.expect("Address is not set").clone(); let userid = {
let public_key = key_pair.to_public_key();
let id_bstr = public_key.details.users.first().unwrap().id.id();
String::from_utf8(id_bstr.to_vec()).unwrap()
};
let addr = mailparse::addrparse(&userid)
.unwrap()
.extract_single_info()
.unwrap()
.addr;
let name = EmailAddress::new(&addr).unwrap().local; let name = EmailAddress::new(&addr).unwrap().local;
let mut unused_name = name.clone(); let mut unused_name = name.clone();
@@ -1437,13 +1420,6 @@ pub fn fiona_keypair() -> SignedSecretKey {
key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap() key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap()
} }
/// Loads a pre-generated v6 PQC keypair from disk.
///
/// Like [alice_keypair] but a different key and identity.
pub fn pqc_keypair() -> SignedSecretKey {
key::SignedSecretKey::from_asc(include_str!("../test-data/key/pqc-secret.asc")).unwrap()
}
/// Utility to help wait for and retrieve events. /// Utility to help wait for and retrieve events.
/// ///
/// This buffers the events in order they are emitted. This allows consuming events in /// This buffers the events in order they are emitted. This allows consuming events in
@@ -1581,7 +1557,9 @@ pub(crate) async fn get_chat_msg(
asserted_msgs_count, asserted_msgs_count,
msgs.len() msgs.len()
); );
let ChatItem::Message { msg_id } = msgs[index] else { let msg_id = if let ChatItem::Message { msg_id } = msgs[index] {
msg_id
} else {
panic!("Wrong item type"); panic!("Wrong item type");
}; };
Message::load_from_db(&t.ctx, msg_id).await.unwrap() Message::load_from_db(&t.ctx, msg_id).await.unwrap()
@@ -1707,7 +1685,7 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
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 { "" },
contact_name, &contact_name,
contact_id, contact_id,
msgtext, msgtext,
if msg.get_from_id() == ContactId::SELF { if msg.get_from_id() == ContactId::SELF {

View File

@@ -161,7 +161,7 @@ async fn check_that_transition_worked(
2, 2,
"Group {} has members {:?}, but should have members {:?} and {:?}", "Group {} has members {:?}, but should have members {:?} and {:?}",
group, group,
members, &members,
alice_contact_id, alice_contact_id,
ContactId::SELF ContactId::SELF
); );

View File

@@ -62,13 +62,13 @@ pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<'_, str> {
if let Some(index) = buf.get(..end_pos).and_then(|s| s.rfind([' ', '\n'])) { if let Some(index) = buf.get(..end_pos).and_then(|s| s.rfind([' ', '\n'])) {
Cow::Owned(format!( Cow::Owned(format!(
"{}{}", "{}{}",
buf.get(..=index).unwrap_or_default(), &buf.get(..=index).unwrap_or_default(),
DC_ELLIPSIS DC_ELLIPSIS
)) ))
} else { } else {
Cow::Owned(format!( Cow::Owned(format!(
"{}{}", "{}{}",
buf.get(..end_pos).unwrap_or_default(), &buf.get(..end_pos).unwrap_or_default(),
DC_ELLIPSIS DC_ELLIPSIS
)) ))
} }

View File

@@ -247,12 +247,12 @@ proptest! {
assert!( assert!(
l <= approx_chars + el_len, l <= approx_chars + el_len,
"buf: '{}' - res: '{}' - len {}, approx {}", "buf: '{}' - res: '{}' - len {}, approx {}",
buf, res, res.len(), approx_chars &buf, &res, res.len(), approx_chars
); );
if buf.chars().count() > approx_chars + el_len { if buf.chars().count() > approx_chars + el_len {
let l = res.len(); let l = res.len();
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {res}"); assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
} }
} }
} }

View File

@@ -116,7 +116,7 @@ pub(crate) struct ConnectionCandidate {
impl fmt::Display for ConnectionCandidate { impl fmt::Display for ConnectionCandidate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.host, self.port, self.security)?; write!(f, "{}:{}:{}", &self.host, self.port, self.security)?;
Ok(()) Ok(())
} }
} }
@@ -131,7 +131,7 @@ pub(crate) struct ConfiguredServerLoginParam {
impl fmt::Display for ConfiguredServerLoginParam { impl fmt::Display for ConfiguredServerLoginParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.connection, self.user)?; write!(f, "{}:{}", self.connection, &self.user)?;
Ok(()) Ok(())
} }
} }

View File

@@ -1,39 +0,0 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
xUsGaf8NSRsAAAAgYy+GaofURMeV0+bcZZGY2ZdAamU+LG69ONjd3haVU3cAhm6G
IT/UEgFgVdPEhiXER9cfPLiCgkiw/L5mrAZfuLfCqgYfGwgAAABLBQJp/w1JIiEG
hys0q6D+DFWPnwQoWtuX0mL6ovH2kCjWmDufAFmB0+QCGwMCHgkECwkIBwYVCg4J
CAwBFg0nCQIIAgcCCQEIAQcBAAAAAEGQEO9Py9Q7njj1WXhtn1wMJSLBdHBE+qQu
RaCaiWkY5l4EWLlVRPAjX2bBSGq6n3+M+H6oFpOHETAX8IcFSxc260UD+PM0jQpV
H6ReNy7PBCQKx8RrBmn/DUkjAAAEwPmkVcPy1ye0/7D9nDQCkENUGry97iLkpcw/
tLJfzL5gJAdzrPkDkyukHxrO7kiUx+mzpiGZRZeyRgBd5YQ+mTgGrptxXLFHcKFR
79Fjg1UjgHEFjxCkCHUfnNcGZVM3p5skESnNgzsgFGiODfKhM4ew3AFgkUc5LNZj
Zgpgt4ETIhylbLUY89ccfNpKnQeJl3cv8lvA/yqhoUutJXwZQ/qYKFnEIGEBTFto
hLZn0KauF9KYOYvOV4yjeZQBlxSPNAWj9SqSNcalpTUFzwoQVSsqWwiys1PEzGAu
twQVKsZ3e/hlZAyR4eGMiYEmCEy7qjuaOJsqHQuW7hdOHWdVRUpRHOtfj3QAzdc0
CehVbyCRJVwnTSKiT3AYsdACH8U7mhI5/VxeSHNRIDN1Y6g6N5sx6Wur/HuKGFwx
L4urdPdpJJgyLXR8GUkL/yeqUhogu4mbVAmULbq2BCIKFNpMyGdhnDugN6Sp5MWc
GOxCW7CASuBYPHW/rto0C4M/3gCtN2sPtRAhOsXNBBhMqLlzzCgawulCiGtNjHUK
HsVhghgYwKRBT7vLSKDNsCVizzoZxNQq8yUEXpFIRsTGt3wYoigZn4wOSpmQbxGe
P3Uc2GWuuukCBNEP5oW4+TCFaNw5mvZgZwl5n4K34poxVgpqBIM2m2fEu8oyLPJZ
bBxnbty3MUAdLpxv+0otGSHJF4xa3lsEyUdr6+JZZXohNXKoyjeJMGo6qPkvCADI
upMnDSYZeLU5bVstHWS6otuRMEcjdLBkYfqfzBhkzbptscaUXzsaK4cd/iQzAA1r
A0ygvcA78Vo363cElNAJh3lntrZZGpBYnzcU/zLACKAVJCYPy3Cj8Al8x+gHP0Yr
ZSOYdZA1q9s2Kuqk7upCpcYDZ+uXGZs3ubA0TYCcO3FKhAwLhzJad5WApBFETYt2
3KJEwgEjQaCs7sNNiwaKxhLC2VJhUckgluGs5iUu9ck5jdU+N9MqTmloF/u2Gok8
QEqF9+DBhPg/fJoI9sN8sIyLrksEUQsm59mvJbVWOpxtbwpWZ+J4cat4azHE0khy
rolL6lZqDJYW4xVeoAVl5iccicjE6mJLemoxf6iJdohi5cN5JXyZtgtdsbIesJib
BLPJVahmv5W1Q4RmrwEp5Ua4xra5Mcac4PeINTOkGMErIhdvnuxEH/Cxd8VKhNlU
vdty4MOyUOkRRPhOMNKUyTkwS9yjprK3QbhEJgrJygHCGpQ0jwp9PrtKqNnOONSX
t4ZORuiAYHDFz3DPlJhLLzNoAJse0RAyolkPThoMl2JlY5ci8pVHb+Ed/kaeFxnE
UJJIOZvDFfNCFCM5CCXG/2pi/icA7nHPDFVBeYPMz1B5vrgmdDNMMFVQNMBtrroT
4pi1N5U4+EAv1vah40akQ/iFcfZjt4sE/jG0M0NCWqHBDPO7e0ae+2IqnIJmsHjL
N1ak3egY00CnRHdrPCkOkhooFIHA1hYxIwyP07qfkhUBNwSqZ4AfF8UW/nuLjeaj
ajEWz3zGLvQpfHSobEGPQKk+eIA1fOVeAuJAUAmJz5YO1Dk4OfczeQqQiOhtv+qe
PYaZQfBFJVamGocDHomPQkP/IvAJhuO9xWPapqbdRwGfVRJZgGsAy89mT1w0PU1C
u6VpIoyZB2J9LZkw9qb9sRRJAr2gpWGBD4CCmPZ8d17ZGDcIr8o+eI+bo5eKf+1j
6NhsjM7AmIccStNxZYWE4ZucvYYbPvT3ns/TNa7BH2DBqfGK84PawosGGBsIAAAA
LAUCaf8NSQIbDCIhBocrNKug/gxVj58EKFrbl9Ji+qLx9pAo1pg7nwBZgdPkAAAA
ADrcEIqnwTwJoiZAxzK+w7uQFHzsYMWIj8x+DKsn7D1silKINHDnFSrlSKRtbAW6
x9+HrN/nvR7bOnXZvZhz7lQ3Lp3YUdzEcqRMj8BWW8IXdm0C
-----END PGP PRIVATE KEY BLOCK-----