Compare commits

..

2 Commits

Author SHA1 Message Date
link2xt
6dd2f16df1 fix: explicitly close the database on account removal 2024-07-29 22:42:39 +00:00
link2xt
0cdf9b8f9c debug: remove try_many_times
Attempt to reproduce <https://github.com/deltachat/deltachat-core-rust/issues/5809>
in CI.
2024-07-29 20:34:22 +00:00
127 changed files with 5385 additions and 8957 deletions

View File

@@ -7,10 +7,3 @@ updates:
commit-message:
prefix: "chore(cargo)"
open-pull-requests-limit: 50
# Keep GitHub Actions up to date.
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.82.0
RUSTUP_TOOLCHAIN: 1.80.0
steps:
- uses: actions/checkout@v4
with:
@@ -59,7 +59,7 @@ jobs:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: EmbarkStudios/cargo-deny-action@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
arguments: --all-features --workspace
command: check
@@ -95,11 +95,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.82.0
rust: 1.80.0
- os: windows-latest
rust: 1.82.0
rust: 1.80.0
- os: macos-latest
rust: 1.82.0
rust: 1.80.0
# Minimum Supported Rust Version = 1.77.0
- os: ubuntu-latest
@@ -211,9 +211,9 @@ jobs:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.13
python: 3.12
- os: macos-latest
python: 3.13
python: 3.12
# PyPy tests
- os: ubuntu-latest
@@ -263,11 +263,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.13
python: 3.12
- os: macos-latest
python: 3.13
python: 3.12
- os: windows-latest
python: 3.13
python: 3.12
# PyPy tests
- os: ubuntu-latest

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.2.0
uses: dependabot/fetch-metadata@v1.1.1
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve a PR

View File

@@ -31,7 +31,7 @@ jobs:
mv docs js
- name: Upload
uses: horochx/deploy-via-scp@1.1.0
uses: horochx/deploy-via-scp@v1.0.1
with:
user: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}

View File

@@ -74,7 +74,7 @@ jobs:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v2
with:
node-version: '18'
- name: npm install

View File

@@ -1,400 +1,5 @@
# Changelog
## [1.147.1] - 2024-10-13
### Build system
- Build Python 3.13 wheels.
- deltachat-rpc-client: Add classifiers for all supported Python versions.
### CI
- Update to Python 3.13.
### Documentation
- CONTRIBUTING.md: Add a note on deleting/changing db columns.
### Fixes
- Reset quota on configured address change ([#5908](https://github.com/deltachat/deltachat-core-rust/pull/5908)).
- Do not emit progress 1000 when configuration is cancelled.
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/deltachat/deltachat-core-rust/pull/5338)).
- Readd tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)).
### Miscellaneous Tasks
- cargo: Bump futures-* from 0.3.30 to 0.3.31.
- cargo: Upgrade async_zip to 0.0.17 ([#6035](https://github.com/deltachat/deltachat-core-rust/pull/6035)).
### Refactor
- MsgId::update_download_state: Don't fail if the message doesn't exist anymore.
## [1.147.0] - 2024-10-05
### API-Changes
- [**breaking**] Remove deprecated get_next_media() APIs.
### Features / Changes
- Reuse existing connections in background_fetch() if I/O is started.
- MsgId::get_info(): Report original filename as well.
- More context for the "Cannot establish guaranteed..." info message ([#6022](https://github.com/deltachat/deltachat-core-rust/pull/6022)).
- deltachat-repl: Add `fetch` command to test `background_fetch()`.
- deltachat-repl: Print send-backup QR code to the terminal.
### Fixes
- Do not attempt to reference info messages.
- query_row_optional: Do not treat rows with NULL as missing rows.
- Skip unconfigured folders in `background_fetch()`.
- Break out of accept() loop if there is an error transferring backup.
- Make it possible to cancel ongoing backup transfer.
- Make backup reception cancellable by stopping ongoing process.
- Smooth progress bar for backup transfer.
- Emit progress 0 if get_backup() fails.
### Documentation
- CONTRIBUTING.md: Add more SQL advices.
## [1.146.0] - 2024-10-03
### Fixes
- download_msg: Do not fail if the message does not exist anymore.
- Better log message for failed QR scan.
### Features / Changes
- Assign message to ad-hoc group with matching name and members ([#5385](https://github.com/deltachat/deltachat-core-rust/pull/5385)).
- Use Rustls instead of native TLS for HTTPS requests.
### Miscellaneous Tasks
- cargo: Bump anyhow from 1.0.86 to 1.0.89.
- cargo: Bump tokio-stream from 0.1.15 to 0.1.16.
- cargo: Bump thiserror from 1.0.63 to 1.0.64.
- cargo: Bump bytes from 1.7.1 to 1.7.2.
- cargo: Bump libc from 0.2.158 to 0.2.159.
- cargo: Bump tempfile from 3.10.1 to 3.13.0.
- cargo: Bump pretty_assertions from 1.4.0 to 1.4.1.
- cargo: Bump hyper-util from 0.1.7 to 0.1.9.
- cargo: Bump rustls-pki-types from 1.8.0 to 1.9.0.
- cargo: Bump quick-xml from 0.36.1 to 0.36.2.
- cargo: Bump serde from 1.0.209 to 1.0.210.
- cargo: Bump syn from 2.0.77 to 2.0.79.
### Refactor
- Move group name calculation out of create_adhoc_group().
- Merge build_tls() function into wrap_tls().
## [1.145.0] - 2024-09-26
### Fixes
- Avoid changing `delete_server_after` default for existing configurations.
### Miscellaneous Tasks
- Sort dependency list.
### Refactor
- Do not wrap shadowsocks::ProxyClientStream.
## [1.144.0] - 2024-09-21
### API-Changes
- [**breaking**] Make QR code type for proxy not specific to SOCKS5 ([#5980](https://github.com/deltachat/deltachat-core-rust/pull/5980)).
`DC_QR_SOCKS5_PROXY` is replaced with `DC_QR_PROXY`.
### Features / Changes
- Make resending OutPending messages possible ([#5817](https://github.com/deltachat/deltachat-core-rust/pull/5817)).
- Don't SMTP-send messages to self-chat if BccSelf is disabled.
- HTTP(S) tunneling.
- Don't put displayname into From/To/Sender if it equals to address ([#5983](https://github.com/deltachat/deltachat-core-rust/pull/5983)).
- Use IMAP APPEND command to upload sync messages ([#5845](https://github.com/deltachat/deltachat-core-rust/pull/5845)).
- Generate 144-bit group IDs.
- smtp: More verbose SMTP connection establishment errors.
- Log unexpected message state when resending fails.
### Fixes
- Save QR code token regardless of whether the group exists ([#5954](https://github.com/deltachat/deltachat-core-rust/pull/5954)).
- Shorten message text in locally sent messages too ([#2281](https://github.com/deltachat/deltachat-core-rust/pull/2281)).
### Documentation
- CONTRIBUTING.md: Document how to format SQL statements.
### Miscellaneous Tasks
- Update provider database.
- cargo: Update iroh to 0.25.
- cargo: Update lazy_static to 1.5.0.
- deps: Bump async-imap from 0.10.0 to 0.10.1.
### Refactor
- Do not store deprecated `addr` and `is_default` into `keypairs`.
- Remove `addr` from KeyPair.
- Use `KeyPair::new()` in `create_keypair()`.
## [1.143.0] - 2024-09-12
### Features / Changes
- Automatic reconfiguration, e.g. switching to implicit TLS if STARTTLS port stops working.
- Always use preloaded DNS results.
- Add "Auto-Submitted: auto-replied" header to appropriate SecureJoin messages.
- Parallelize IMAP and SMTP connection attempts ([#5915](https://github.com/deltachat/deltachat-core-rust/pull/5915)).
- securejoin: Ignore invalid *-request-with-auth messages silently.
- ChatId::create_for_contact_with_blocked: Don't emit events on no op.
- Delete messages from a chatmail server immediately by default ([#5805](https://github.com/deltachat/deltachat-core-rust/pull/5805)) ([#5840](https://github.com/deltachat/deltachat-core-rust/pull/5840)).
- Shadowsocks support.
- Recognize t.me SOCKS5 proxy QR codes ([#5895](https://github.com/deltachat/deltachat-core-rust/pull/5895))
- Remove old iroh 0.4 and support for old `DCBACKUP` QR codes.
### Fixes
- http: Set I/O timeout to 1 minute rather than whole request timeout.
- Add Auto-Submitted header in a single place.
- Do not allow quotes with "... wrote:" headers in chat messages.
- Don't sync QR code token before populating the group ([#5935](https://github.com/deltachat/deltachat-core-rust/pull/5935)).
### Documentation
- Document that `bcc_self` is enabled by default.
### CI
- Update Rust to 1.81.0.
### Miscellaneous Tasks
- Update provider database.
- cargo: Update iroh to 0.23.0.
- cargo: Reduce number of duplicate dependencies.
- cargo: Replace unmaintained ansi_term with nu-ansi-term.
- Replace `reqwest` with direct usage of `hyper`.
### Refactor
- login_param: Use Config:: constants to avoid typos in key names.
- Make Context::config_exists() crate-public.
- Get_config_bool_opt(): Return None if only default value exists.
### Tests
- Test that alternative port 443 works.
- Alice is (non-)bot on Bob's side after QR contact setup.
## [1.142.12] - 2024-09-02
### Fixes
- Display Config::MdnsEnabled as true by default ([#5948](https://github.com/deltachat/deltachat-core-rust/pull/5948)).
## [1.142.11] - 2024-08-30
### Fixes
- Set backward verification when observing vc-contact-confirm or `vg-member-added` ([#5930](https://github.com/deltachat/deltachat-core-rust/pull/5930)).
## [1.142.10] - 2024-08-26
### Fixes
- Only include one From: header in securejoin messages ([#5917](https://github.com/deltachat/deltachat-core-rust/pull/5917)).
## [1.142.9] - 2024-08-24
### Fixes
- Fix reading of multiline SMTP greetings ([#5911](https://github.com/deltachat/deltachat-core-rust/pull/5911)).
### Features / Changes
- Update preloaded DNS cache.
## [1.142.8] - 2024-08-21
### Fixes
- Do not panic on unknown CertificateChecks values.
## [1.142.7] - 2024-08-17
### Fixes
- Do not save "Automatic" into configured_imap_certificate_checks. **This fixes regression introduced in core 1.142.4. Versions 1.142.4..1.142.6 should not be used in releases.**
- Create a group unblocked for bot even if 1:1 chat is blocked ([#5514](https://github.com/deltachat/deltachat-core-rust/pull/5514)).
- Update rpgp from 0.13.1 to 0.13.2 to fix "unable to decrypt" errors when sending messages to old Delta Chat clients and using Ed25519 keys to encrypt.
- Do not request ALPN on standard ports and when using STARTTLS.
### Features / Changes
- jsonrpc: Add ContactObject::e2ee_avail.
### Tests
- Protected group for bot is auto-accepted.
## [1.142.6] - 2024-08-15
### Fixes
- Default to strict TLS checks if not configured.
### Miscellaneous Tasks
- deltachat-rpc-client: Fix ruff 0.6.0 warnings.
## [1.142.5] - 2024-08-14
### Fixes
- Still try to create "INBOX.DeltaChat" if couldn't create "DeltaChat" ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)).
- `store_seen_flags_on_imap`: Skip to next messages if couldn't select folder ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)).
- Increase timeout for QR generation to 60s ([#5882](https://github.com/deltachat/deltachat-core-rust/pull/5882)).
### Documentation
- Document new `mdns_enabled` behavior (bots do not send MDNs by default).
### CI
- Configure Dependabot to update GitHub Actions.
### Miscellaneous Tasks
- cargo: Bump regex from 1.10.5 to 1.10.6.
- cargo: Bump serde from 1.0.204 to 1.0.205.
- deps: Bump horochx/deploy-via-scp from 1.0.1 to 1.1.0.
- deps: Bump dependabot/fetch-metadata from 1.1.1 to 2.2.0.
- deps: Bump actions/setup-node from 2 to 4.
- Update provider database.
## [1.142.4] - 2024-08-09
### Build system
- Downgrade Tokio to 1.38 to fix Android compilation.
- Use `--locked` with `cargo install`.
### Features / Changes
- Add Config::FixIsChatmail.
- Always move outgoing auto-generated messages to the mvbox.
- Disable requesting MDNs for bots by default.
- Allow using OAuth 2 with SOCKS5.
- Allow autoconfig when SOCKS5 is enabled.
- Update provider database.
- cargo: Update iroh from 0.21 to 0.22 ([#5860](https://github.com/deltachat/deltachat-core-rust/pull/5860)).
### CI
- Update Rust to 1.80.1.
- Update EmbarkStudios/cargo-deny-action.
### Documentation
- Point to active Header Protection draft
### Refactor
- Derive `Default` for `CertificateChecks`.
- Merge imap_certificate_checks and smtp_certificate_checks.
- Remove param_addr_urlencoded argument from get_autoconfig().
- Pass address to moz_autoconfigure() instead of LoginParam.
## [1.142.3] - 2024-08-04
### Build system
- cargo: Update rusqlite and libsqlite3-sys.
- Fix cargo warnings about default-features
- Do not disable "vendored" feature in the workspace.
- cargo: Bump quick-xml from 0.35.0 to 0.36.1.
- cargo: Bump uuid from 1.9.1 to 1.10.0.
- cargo: Bump tokio from 1.38.0 to 1.39.2.
- cargo: Bump env_logger from 0.11.3 to 0.11.5.
- Remove sha2 dependency.
- Remove `backtrace` dependency.
- Remove direct "quinn" dependency.
## [1.142.2] - 2024-08-02
### Features / Changes
- Try only the full email address if username is unspecified.
- Sort DNS results by successful connection timestamp ([#5818](https://github.com/deltachat/deltachat-core-rust/pull/5818)).
### Fixes
- Await the tasks after aborting them.
- Do not reset is_chatmail config on failed reconfiguration.
- Fix compilation on iOS.
- Reset configured_provider on reconfiguration.
### Refactor
- Don't update message state to `OutMdnRcvd` anymore.
### Build system
- Use workspace dependencies to make cargo-deny 0.15.1 happy.
- cargo: Update bytemuck from 0.14.3 to 0.16.3.
- cargo: Bump toml from 0.8.14 to 0.8.15.
- cargo: Bump serde_json from 1.0.120 to 1.0.122.
- cargo: Bump human-panic from 2.0.0 to 2.0.1.
- cargo: Bump thiserror from 1.0.61 to 1.0.63.
- cargo: Bump syn from 2.0.68 to 2.0.72.
- cargo: Bump quoted_printable from 0.5.0 to 0.5.1.
- cargo: Bump serde from 1.0.203 to 1.0.204.
## [1.142.1] - 2024-07-30
### Features / Changes
- Do not reveal sender's language in read receipts ([#5802](https://github.com/deltachat/deltachat-core-rust/pull/5802)).
- Try next DNS resolution result if TLS setup fails.
- Report first error instead of the last on connection failure.
### Fixes
- smtp: Use DNS cache for implicit TLS connections.
- Imex::import_backup: Unpack all blobs before importing a db ([#4307](https://github.com/deltachat/deltachat-core-rust/pull/4307)).
- Import_backup_stream: Fix progress stucking at 0.
- Sql::import: Detach backup db if any step of the import fails.
- Imex::import_backup: Ignore errors from delete_and_reset_all_device_msgs().
- Explicitly close the database on account removal.
### Miscellaneous Tasks
- cargo: Update time from 0.3.34 to 0.3.36.
- cargo: Update iroh from 0.20.0 to 0.21.0.
### Refactor
- Add net/dns submodule.
- Pass single ALPN around instead of ALPN list.
- Replace {IMAP,SMTP,HTTP}_TIMEOUT with a single constant.
- smtp: Unify SMTP connection setup between TLS and STARTTLS.
- imap: Unify IMAP connection setup in Client::connect().
- Move DNS resolution into IMAP and SMTP connect code.
### CI
- Update Rust to 1.80.0.
## [1.142.0] - 2024-07-23
### API-Changes
@@ -4990,21 +4595,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.141.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.0...v1.141.1
[1.141.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.1...v1.141.2
[1.142.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.2...v1.142.0
[1.142.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.0...v1.142.1
[1.142.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.1...v1.142.2
[1.142.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.2...v1.142.3
[1.142.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.3...v1.142.4
[1.142.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.4...v1.142.5
[1.142.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.5...v1.142.6
[1.142.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.6...v1.142.7
[1.142.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.7...v1.142.8
[1.142.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.8...v1.142.9
[1.142.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.9..v1.142.10
[1.142.11]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.10..v1.142.11
[1.142.12]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.11..v1.142.12
[1.143.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.12..v1.143.0
[1.144.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.143.0..v1.144.0
[1.145.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.144.0..v1.145.0
[1.146.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.145.0..v1.146.0
[1.147.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.146.0..v1.147.0
[1.147.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.147.0..v1.147.1

View File

@@ -27,7 +27,7 @@ add_custom_command(
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --features jsonrpc
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
)

View File

@@ -32,66 +32,6 @@ on the contributing page: <https://github.com/deltachat/deltachat-core-rust/cont
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
### SQL
Multi-line SQL statements should be formatted using string literals,
for example
```
sql.execute(
"CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT DEFAULT '' NOT NULL -- message text
) STRICT",
)
.await?;
```
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
or [`indoc!](https://docs.rs/indoc).
Do not escape newlines like this:
```
sql.execute(
"CREATE TABLE messages ( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
text TEXT DEFAULT '' NOT NULL \
) STRICT",
)
.await?;
```
Escaping newlines
is prone to errors like this if space before backslash is missing:
```
"SELECT foo\
FROM bar"
```
Literal above results in `SELECT fooFROM bar` string.
This style also does not allow using `--` comments.
---
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
to make SQLite check column types.
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
This avoids reuse of the row IDs and can avoid dangerous bugs
like forwarding wrong message because the message was deleted
and another message took its row ID.
Declare all new columns as `NOT NULL`
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
Use `HAVING COUNT(*) > 0` clause
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
an older version. Also don't change the column type, consider adding a new column with another name
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
keyword doesn't help here.
### Commit messages
Commit messages follow the [Conventional Commits] notation.
We use [git-cliff] to generate the changelog from commit messages before the release.

2448
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.147.1"
version = "1.142.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -34,37 +34,36 @@ strip = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
deltachat-time = { path = "./deltachat-time" }
deltachat-contact-tools = { workspace = true }
deltachat-contact-tools = { path = "./deltachat-contact-tools" }
format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.1"
async-channel = { workspace = true }
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = { workspace = true }
brotli = { version = "6", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.9"
fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
futures-lite = { workspace = true }
hex = "0.4.0"
hickory-resolver = "=0.25.0-alpha.2"
http-body-util = "0.1.2"
hickory-resolver = "0.24"
humansize = "2"
hyper = "1"
hyper-util = "0.1.9"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.25.0", default-features = false, features = ["net"] }
iroh-net = { version = "0.25.0", default-features = false }
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"}
iroh-net = { version = "0.21.0", default-features = false }
iroh-gossip = { version = "0.21.0", default-features = false, features = ["net"] }
quinn = "0.10.0"
kamadak-exif = "0.5.3"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = { workspace = true }
@@ -74,53 +73,48 @@ num_cpus = "1.16"
num-derive = "0.4"
num-traits = { workspace = true }
once_cell = { workspace = true }
parking_lot = "0.12"
percent-encoding = "2.3"
pgp = { version = "0.13.2", default-features = false }
pin-project = "1"
parking_lot = "0.12"
pgp = { version = "0.13", default-features = false }
qrcodegen = "1.7.0"
quick-xml = "0.36"
quick-xml = "0.35"
quoted_printable = "0.5"
rand = { workspace = true }
regex = { workspace = true }
reqwest = { version = "0.12.5", features = ["json"] }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.9.0"
rustls = { version = "0.23.13", default-features = false }
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
shadowsocks = { version = "1.21.0", default-features = false, features = ["aead-cipher-2022"] }
sha2 = "0.10"
smallvec = "1.13.2"
strum = "0.26"
strum_macros = "0.26"
tagger = "4.3.4"
textwrap = "0.16.1"
thiserror = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
tokio-rustls = { version = "0.26.0", default-features = false }
tokio-stream = { version = "0.1.16", features = ["fs"] }
tokio-stream = { version = "0.1.15", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.6"
[dev-dependencies]
ansi_term = { workspace = true }
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.5.1", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
pretty_assertions = "1.4.1"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = { workspace = true }
testdir = "0.9.0"
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
pretty_assertions = "1.3.0"
[workspace]
members = [
@@ -165,35 +159,25 @@ harness = false
[workspace.dependencies]
anyhow = "1"
ansi_term = "0.12.1"
async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.38", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
futures = "0.3.30"
futures-lite = "2.3.0"
libc = "0.2"
log = "0.4"
nu-ansi-term = "0.46"
num-traits = "0.2"
once_cell = "1.18.0"
rand = "0.8"
regex = "1.10"
rusqlite = "0.32"
rusqlite = "0.31"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.13.0"
serde = "1.0"
tempfile = "3.10.1"
thiserror = "1"
# 1.38 is the latest version before `mio` dependency update
# that broke compilation with Android NDK r23c and r24.
# Version 1.39.0 cannot be compiled using these NDKs,
# see issue <https://github.com/tokio-rs/tokio/issues/6748>
# for details.
tokio = "~1.38.1"
tokio = "1.38.0"
tokio-util = "0.7.11"
tracing-subscriber = "0.3"
yerpc = "0.6.2"
@@ -202,7 +186,9 @@ yerpc = "0.6.2"
default = ["vendored"]
internals = []
vendored = [
"rusqlite/bundled-sqlcipher-vendored-openssl"
"async-native-tls/vendored",
"rusqlite/bundled-sqlcipher-vendored-openssl",
"reqwest/native-tls-vendored"
]
[lints.rust]

View File

@@ -30,13 +30,13 @@ $ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`:
```
$ cargo run --locked -p deltachat-repl -- ~/deltachat-db
$ cargo run -p deltachat-repl -- ~/deltachat-db
```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
Optionally, install `deltachat-repl` binary with
```
$ cargo install --locked --path deltachat-repl/
$ cargo install --path deltachat-repl/
```
and run as
```

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.147.1"
version = "1.142.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -14,8 +14,8 @@ name = "deltachat"
crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { workspace = true, default-features = false }
deltachat-jsonrpc = { workspace = true, optional = true }
deltachat = { path = "../", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
libc = { workspace = true }
human-panic = { version = "2", default-features = false }
num-traits = { workspace = true }
@@ -29,6 +29,6 @@ yerpc = { workspace = true, features = ["anyhow_expose"] }
[features]
default = ["vendored"]
vendored = ["deltachat/vendored", "deltachat-jsonrpc/vendored"]
vendored = ["deltachat/vendored"]
jsonrpc = ["dep:deltachat-jsonrpc"]

View File

@@ -403,10 +403,13 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `proxy_enabled` = Proxy enabled. Disabled by default.
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
* - `socks5_enabled` = SOCKS5 enabled
* - `socks5_host` = SOCKS5 proxy server host
* - `socks5_port` = SOCKS5 proxy server port
* - `socks5_user` = SOCKS5 proxy username
* - `socks5_password` = SOCKS5 proxy password
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = deprecated option, should be set to the same value as `imap_certificate_checks` but ignored by the new core
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
* - `selfstatus` = Own status to display, e.g. in e-mail footers, defaults to empty
* - `selfavatar` = File containing avatar. Will immediately be copied to the
@@ -417,10 +420,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* and also recoded to a reasonable size.
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts
* default=send and request read receipts, only send but not reuqest if `bot` is set
* - `bcc_self` = 0=do not send a copy of outgoing messages to self,
* 1=send a copy of outgoing messages to self (default).
* 1=send and request read receipts (default)
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
* 1=send a copy of outgoing messages to self.
* Sending messages to self is needed for a proper multi-account setup,
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
@@ -522,8 +524,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* In contrast to `dc_set_chat_mute_duration()`,
* fresh message and badge counters are not changed by this setting,
* but should be tuned down where appropriate.
* - `private_tag` = Optional tag as "Work", "Family".
* Meant to help profile owner to differ between profiles with similar names.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
@@ -866,10 +866,13 @@ void dc_maybe_network (dc_context_t* context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param addr The e-mail address of the user. This must match the
* configured_addr setting of the context as well as the UID of the key.
* @param public_data Ignored, actual public key is extracted from secret_data.
* @param secret_data ASCII armored secret key.
* @return 1 on success, 0 on failure.
*/
int dc_preconfigure_keypair (dc_context_t* context, const char *secret_data);
int dc_preconfigure_keypair (dc_context_t* context, const char *addr, const char *public_data, const char *secret_data);
// handle chatlists
@@ -1549,6 +1552,30 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t chat_id, int msg_type, int msg_type2, int msg_type3);
/**
* Search next/previous message based on a given message and a list of types.
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_id The ID of the current message from which the next or previous message should be searched.
* @param dir 1=get the next message, -1=get the previous one.
* @param msg_type The message type to search for.
* If 0, the message type from curr_msg_id is used.
* @param msg_type2 Alternative message type to search for. 0 to skip.
* @param msg_type3 Alternative message type to search for. 0 to skip.
* @return Returns the message ID that should be played next.
* The returned message is in the same chat as the given one
* and has one of the given types.
* Typically, this result is passed again to dc_get_next_media()
* later on the next swipe.
* If there is not next/previous message, the function returns 0.
*/
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
/**
* Set chat visibility to pinned, archived or normal.
*
@@ -2479,7 +2506,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_BACKUP 251
#define DC_QR_BACKUP2 252
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
@@ -2533,10 +2559,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask the user if they want to use the given service for video chats;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_SOCKS5_PROXY with dc_lot_t::text1=host, dc_lot_t::text2=port:
* ask the user if they want to use the given proxy and overwrite the previous one, if any.
* if so, call dc_set_config_from_qr() and restart I/O.
*
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
* e-mail address scanned, optionally, a draft message could be set in
* dc_lot_t::text1 in which case dc_lot_t::text1_meaning will be DC_TEXT1_DRAFT;
@@ -2611,7 +2633,6 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
* Get QR code image from the QR code text generated by dc_get_securejoin_qr().
* See dc_get_securejoin_qr() for details about the contained QR code.
*
* @deprecated 2024-10 use dc_create_qr_svg(dc_get_securejoin_qr()) instead.
* @memberof dc_context_t
* @param context The context object.
* @param chat_id group-chat-id for secure-join or 0 for setup-contact,
@@ -2792,22 +2813,6 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
void dc_delete_all_locations (dc_context_t* context);
// misc
/**
* Create a QR code from any input data.
*
* The QR code is returned as a square SVG image.
*
* @memberof dc_context_t
* @param payload The content for the QR code.
* @return SVG image with the QR code.
* On errors, an empty string is returned.
* The returned string must be released using dc_str_unref() after usage.
*/
char* dc_create_qr_svg (const char* payload);
/**
* Get last error string.
*
@@ -2896,7 +2901,6 @@ char* dc_backup_provider_get_qr (const dc_backup_provider_t* backup_provider);
* This works like dc_backup_provider_qr() but returns the text of a rendered
* SVG image containing the QR code.
*
* @deprecated 2024-10 use dc_create_qr_svg(dc_backup_provider_get_qr()) instead.
* @memberof dc_backup_provider_t
* @param backup_provider The backup provider object as created by
* dc_backup_provider_new().
@@ -2936,7 +2940,7 @@ void dc_backup_provider_unref (dc_backup_provider_t* backup_provider);
* Gets a backup offered by a dc_backup_provider_t object on another device.
*
* This function is called on a device that scanned the QR code offered by
* dc_backup_provider_get_qr(). Typically this is a
* dc_backup_sender_qr() or dc_backup_sender_qr_svg(). Typically this is a
* different device than that which provides the backup.
*
* This call will block while the backup is being transferred and only
@@ -6051,21 +6055,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_REACTIONS_CHANGED 2001
/**
* A reaction to one's own sent message received.
* Typically, the UI will show a notification for that.
*
* In addition to this event, DC_EVENT_REACTIONS_CHANGED is emitted.
*
* @param data1 (int) contact_id ID of the contact sending this reaction.
* @param data2 (int) msg_id + (char*) reaction.
* ID of the message for which a reaction was received in dc_event_get_data2_int(),
* and the reaction as dc_event_get_data2_str().
* string must be passed to dc_str_unref() afterwards.
*/
#define DC_EVENT_INCOMING_REACTION 2002
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
@@ -6283,7 +6272,7 @@ void dc_event_unref(dc_event_t* event);
/**
* Webxdc status update received.
* webxdc status update received.
* To get the received status update, use dc_get_webxdc_status_updates() with
* `serial` set to the last known update
* (in case of special bots, `status_update_serial` from `data2`
@@ -6318,15 +6307,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_WEBXDC_REALTIME_DATA 2150
/**
* Advertisement for ephemeral peer channel communication received.
* This can be used by bots to initiate peer-to-peer communication from their side.
* @param data1 (int) msg_id
* @param data2 0
*/
#define DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT 2151
/**
* Tells that the Background fetch was completed (or timed out).
*

View File

@@ -30,7 +30,7 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::preconfigure_keypair;
use deltachat::message::MsgId;
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
@@ -541,7 +541,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::ErrorSelfNotInGroup(_) => 410,
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingReaction { .. } => 2002,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::MsgsNoticed { .. } => 2008,
@@ -564,14 +563,10 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::WebxdcStatusUpdate { .. } => 2120,
EventType::WebxdcInstanceDeleted { .. } => 2121,
EventType::WebxdcRealtimeData { .. } => 2150,
EventType::WebxdcRealtimeAdvertisementReceived { .. } => 2151,
EventType::AccountsBackgroundFetchDone => 2200,
EventType::ChatlistChanged => 2300,
EventType::ChatlistItemChanged { .. } => 2301,
EventType::EventChannelOverflow { .. } => 2400,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -602,7 +597,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone => 0,
EventType::ChatlistChanged => 0,
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
@@ -627,15 +621,11 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
}
EventType::WebxdcRealtimeData { msg_id, .. }
| EventType::WebxdcStatusUpdate { msg_id, .. }
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::ChatlistItemChanged { chat_id } => {
chat_id.unwrap_or_default().to_u32() as libc::c_int
}
EventType::EventChannelOverflow { n } => *n as libc::c_int,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -676,11 +666,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ChatlistItemChanged { .. }
| EventType::ConfigSynced { .. }
| EventType::ChatModified(_)
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::EventChannelOverflow { .. } => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingReaction { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
@@ -694,9 +682,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
..
} => status_update_serial.to_u32() as libc::c_int,
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -748,7 +733,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::IncomingMsgBunch { .. }
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
@@ -770,14 +754,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len());
ptr as *mut libc::c_char
}
EventType::IncomingReaction { reaction, .. } => reaction
.as_str()
.to_c_string()
.unwrap_or_default()
.into_raw(),
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -859,6 +835,8 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
#[no_mangle]
pub unsafe extern "C" fn dc_preconfigure_keypair(
context: *mut dc_context_t,
addr: *const libc::c_char,
_public_data: *const libc::c_char,
secret_data: *const libc::c_char,
) -> i32 {
if context.is_null() {
@@ -866,8 +844,9 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
return 0;
}
let ctx = &*context;
let addr = to_string_lossy(addr);
let secret_data = to_string_lossy(secret_data);
block_on(preconfigure_keypair(ctx, &secret_data))
block_on(preconfigure_keypair(ctx, &addr, &secret_data))
.context("Failed to save keypair")
.log_err(ctx)
.is_ok() as libc::c_int
@@ -1467,6 +1446,48 @@ pub unsafe extern "C" fn dc_get_chat_media(
})
}
#[no_mangle]
#[allow(deprecated)]
pub unsafe extern "C" fn dc_get_next_media(
context: *mut dc_context_t,
msg_id: u32,
dir: libc::c_int,
msg_type: libc::c_int,
or_msg_type2: libc::c_int,
or_msg_type3: libc::c_int,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_next_media()");
return 0;
}
let direction = if dir < 0 {
chat::Direction::Backward
} else {
chat::Direction::Forward
};
let ctx = &*context;
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {msg_type}"));
let or_msg_type2 =
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {or_msg_type2}"));
let or_msg_type3 =
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {or_msg_type3}"));
block_on(async move {
chat::get_next_media(
ctx,
MsgId::new(msg_id),
direction,
msg_type,
or_msg_type2,
or_msg_type3,
)
.await
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
.unwrap_or(0)
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t,
@@ -2594,18 +2615,6 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
if payload.is_null() {
eprintln!("ignoring careless call to dc_create_qr_svg()");
return "".strdup();
}
create_qr_svg(&to_string_lossy(payload))
.unwrap_or_else(|_| "".to_string())
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
@@ -4528,16 +4537,19 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
let addr = to_string_lossy(addr);
let ctx = &*context;
let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled))
.context("Can't get config")
.log_err(ctx);
let socks5_enabled = block_on(async move {
ctx.get_config_bool(config::Config::Socks5Enabled)
.await
.context("Can't get config")
.log_err(ctx)
});
match proxy_enabled {
Ok(proxy_enabled) => {
match socks5_enabled {
Ok(socks5_enabled) => {
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
proxy_enabled,
socks5_enabled,
))
.log_err(ctx)
.unwrap_or_default()

View File

@@ -34,34 +34,34 @@ pub enum Meaning {
}
impl Lot {
pub fn get_text1(&self) -> Option<Cow<str>> {
pub fn get_text1(&self) -> Option<&str> {
match self {
Self::Summary(summary) => match &summary.prefix {
None => None,
Some(SummaryPrefix::Draft(text)) => Some(Cow::Borrowed(text)),
Some(SummaryPrefix::Username(username)) => Some(Cow::Borrowed(username)),
Some(SummaryPrefix::Me(text)) => Some(Cow::Borrowed(text)),
Some(SummaryPrefix::Draft(text)) => Some(text),
Some(SummaryPrefix::Username(username)) => Some(username),
Some(SummaryPrefix::Me(text)) => Some(text),
},
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => None,
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::AskVerifyGroup { grpname, .. } => Some(grpname),
Qr::FprOk { .. } => None,
Qr::FprMismatch { .. } => None,
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
Qr::Account { domain } => Some(domain),
Qr::Backup { .. } => None,
Qr::Backup2 { .. } => None,
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
Qr::Url { url } => Some(Cow::Borrowed(url)),
Qr::Text { text } => Some(Cow::Borrowed(text)),
Qr::WebrtcInstance { domain, .. } => Some(domain),
Qr::Addr { draft, .. } => draft.as_deref(),
Qr::Url { url } => Some(url),
Qr::Text { text } => Some(text),
Qr::WithdrawVerifyContact { .. } => None,
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
Qr::ReviveVerifyContact { .. } => None,
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
Qr::Login { address, .. } => Some(address),
},
Self::Error(err) => Some(Cow::Borrowed(err)),
Self::Error(err) => Some(err),
}
}
@@ -102,9 +102,9 @@ impl Lot {
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
Qr::Account { .. } => LotState::QrAccount,
Qr::Backup { .. } => LotState::QrBackup,
Qr::Backup2 { .. } => LotState::QrBackup2,
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
Qr::Proxy { .. } => LotState::QrProxy,
Qr::Addr { .. } => LotState::QrAddr,
Qr::Url { .. } => LotState::QrUrl,
Qr::Text { .. } => LotState::QrText,
@@ -128,9 +128,9 @@ impl Lot {
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::Backup { .. } => Default::default(),
Qr::Backup2 { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Proxy { .. } => Default::default(),
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(),
Qr::Text { .. } => Default::default(),
@@ -185,9 +185,6 @@ pub enum LotState {
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,
/// text1=address, text2=protocol
QrProxy = 271,
/// id=contact
QrAddr = 320,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.147.1"
version = "1.142.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -14,8 +14,8 @@ required-features = ["webserver"]
[dependencies]
anyhow = { workspace = true }
deltachat = { workspace = true }
deltachat-contact-tools = { workspace = true }
deltachat = { path = ".." }
deltachat-contact-tools = { path = "../deltachat-contact-tools" }
num-traits = { workspace = true }
schemars = "0.8.21"
serde = { workspace = true, features = ["derive"] }
@@ -25,7 +25,7 @@ async-channel = { workspace = true }
futures = { workspace = true }
serde_json = { workspace = true }
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.12", features = ["json_value"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { workspace = true }
sanitize-filename = { workspace = true }
walkdir = "2.5.0"
@@ -33,7 +33,7 @@ base64 = { workspace = true }
# optional dependencies
axum = { version = "0.7", optional = true, features = ["ws"] }
env_logger = { version = "0.11.5", optional = true }
env_logger = { version = "0.11.3", optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }

View File

@@ -321,12 +321,12 @@ impl CommandApi {
) -> Result<Option<ProviderInfo>> {
let ctx = self.get_context(account_id).await?;
let proxy_enabled = ctx
.get_config_bool(deltachat::config::Config::ProxyEnabled)
let socks5_enabled = ctx
.get_config_bool(deltachat::config::Config::Socks5Enabled)
.await?;
let provider_info =
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_enabled).await;
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), socks5_enabled).await;
Ok(ProviderInfo::from_dc_type(provider_info))
}
@@ -1552,6 +1552,55 @@ impl CommandApi {
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
}
/// Search next/previous message based on a given message and a list of types.
/// Typically used to implement the "next" and "previous" buttons
/// in a gallery or in a media player.
///
/// one combined call for getting chat::get_next_media for both directions
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
///
/// Deprecated 2023-10-03, use `get_chat_media` method
/// and navigate the returned array instead.
#[allow(deprecated)]
async fn get_neighboring_chat_media(
&self,
account_id: u32,
msg_id: u32,
message_type: MessageViewtype,
or_message_type2: Option<MessageViewtype>,
or_message_type3: Option<MessageViewtype>,
) -> Result<(Option<u32>, Option<u32>)> {
let ctx = self.get_context(account_id).await?;
let msg_type: Viewtype = message_type.into();
let msg_type2: Viewtype = or_message_type2.map(|v| v.into()).unwrap_or_default();
let msg_type3: Viewtype = or_message_type3.map(|v| v.into()).unwrap_or_default();
let prev = chat::get_next_media(
&ctx,
MsgId::new(msg_id),
chat::Direction::Backward,
msg_type,
msg_type2,
msg_type3,
)
.await?
.map(|id| id.to_u32());
let next = chat::get_next_media(
&ctx,
MsgId::new(msg_id),
chat::Direction::Forward,
msg_type,
msg_type2,
msg_type3,
)
.await?
.map(|id| id.to_u32());
Ok((prev, next))
}
// ---------------------------------------------
// backup
// ---------------------------------------------
@@ -1623,10 +1672,10 @@ impl CommandApi {
///
/// This call will block until the QR code is ready,
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 60 seconds to avoid deadlocks.
/// but will fail after 10 seconds to avoid deadlocks.
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
let qr = tokio::time::timeout(
Duration::from_secs(60),
Duration::from_secs(10),
self.inner_get_backup_qr(account_id),
)
.await
@@ -1642,13 +1691,13 @@ impl CommandApi {
///
/// This call will block until the QR code is ready,
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 60 seconds to avoid deadlocks.
/// but will fail after 10 seconds to avoid deadlocks.
///
/// Returns the QR code rendered as an SVG image.
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let qr = tokio::time::timeout(
Duration::from_secs(60),
Duration::from_secs(10),
self.inner_get_backup_qr(account_id),
)
.await
@@ -1946,13 +1995,9 @@ impl CommandApi {
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut message = data
.create_message(&ctx)
.await
.context("Failed to create message")?;
let mut message = data.create_message(&ctx).await?;
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
.await
.context("Failed to send created message")?
.await?
.to_u32();
Ok(msg_id)
}

View File

@@ -19,7 +19,6 @@ pub struct ContactObject {
profile_image: Option<String>, // BLOBS
name_and_addr: String,
is_blocked: bool,
e2ee_avail: bool,
/// True if the contact can be added to verified groups.
///
@@ -80,7 +79,6 @@ impl ContactObject {
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
e2ee_avail: contact.e2ee_avail(context).await?,
is_verified,
is_profile_verified,
verifier_id,

View File

@@ -98,14 +98,6 @@ pub enum EventType {
contact_id: u32,
},
/// Incoming reaction, should be notified.
#[serde(rename_all = "camelCase")]
IncomingReaction {
contact_id: u32,
msg_id: u32,
reaction: String,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -252,11 +244,6 @@ pub enum EventType {
#[serde(rename_all = "camelCase")]
WebxdcRealtimeData { msg_id: u32, data: Vec<u8> },
/// Advertisement received over an ephemeral peer channel.
/// This can be used by bots to initiate peer-to-peer communication from their side.
#[serde(rename_all = "camelCase")]
WebxdcRealtimeAdvertisementReceived { msg_id: u32 },
/// Inform that a message containing a webxdc instance has been deleted
#[serde(rename_all = "camelCase")]
WebxdcInstanceDeleted { msg_id: u32 },
@@ -310,15 +297,6 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
contact_id: contact_id.to_u32(),
},
CoreEventType::IncomingReaction {
contact_id,
msg_id,
reaction,
} => IncomingReaction {
contact_id: contact_id.to_u32(),
msg_id: msg_id.to_u32(),
reaction: reaction.as_str().to_string(),
},
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
@@ -395,11 +373,6 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
data,
},
CoreEventType::WebxdcRealtimeAdvertisementReceived { msg_id } => {
WebxdcRealtimeAdvertisementReceived {
msg_id: msg_id.to_u32(),
}
}
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
},

View File

@@ -605,13 +605,16 @@ impl MessageData {
message.set_location(latitude, longitude);
}
if let Some(id) = self.quoted_message_id {
let quoted_message = Message::load_from_db(context, MsgId::new(id))
.await
.context("Failed to load quoted message")?;
message
.set_quote(context, Some(&quoted_message))
.await
.context("Failed to set quote")?;
.set_quote(
context,
Some(
&Message::load_from_db(context, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
} else if let Some(text) = self.quoted_text {
let protect = false;
message.set_quote_text(Some((text, protect)));
@@ -637,7 +640,7 @@ pub struct MessageInfo {
error: Option<String>,
rfc724_mid: String,
server_urls: Vec<String>,
hop_info: String,
hop_info: Option<String>,
}
impl MessageInfo {

View File

@@ -32,6 +32,9 @@ pub enum QrObject {
Account {
domain: String,
},
Backup {
ticket: String,
},
Backup2 {
auth_token: String,
@@ -41,11 +44,6 @@ pub enum QrObject {
domain: String,
instance_pattern: String,
},
Proxy {
url: String,
host: String,
port: u16,
},
Addr {
contact_id: u32,
draft: Option<String>,
@@ -136,6 +134,9 @@ impl From<Qr> for QrObject {
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::Backup { ticket } => QrObject::Backup {
ticket: ticket.to_string(),
},
Qr::Backup2 {
ref node_addr,
auth_token,
@@ -151,7 +152,6 @@ impl From<Qr> for QrObject {
domain,
instance_pattern,
},
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }

View File

@@ -83,7 +83,7 @@ mod tests {
assert_eq!(result, response.to_owned());
}
{
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#;
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
session.handle_incoming(request).await;
let result = receiver.recv().await?;

View File

@@ -58,5 +58,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.147.1"
"version": "1.142.0"
}

View File

@@ -86,7 +86,10 @@ describe("online tests", function () {
null
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
await dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello");
const { chatId: chatIdOnAccountB } = await eventPromise;
@@ -116,7 +119,10 @@ describe("online tests", function () {
null
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
// wait for message from A
console.log("wait for message from A");
@@ -137,7 +143,10 @@ describe("online tests", function () {
);
expect(message.text).equal("Hello2");
// Send message back from B to A
const eventPromise2 = waitForEvent(dc, "IncomingMsg", accountId1);
const eventPromise2 = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId1),
waitForEvent(dc, "IncomingMsg", accountId1),
]);
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
// Check if answer arrives at A and if it is encrypted
await eventPromise2;

View File

@@ -1,17 +1,16 @@
[package]
name = "deltachat-repl"
version = "1.147.1"
version = "1.142.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
[dependencies]
ansi_term = { workspace = true }
anyhow = { workspace = true }
deltachat = { workspace = true, features = ["internals"]}
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = { workspace = true }
nu-ansi-term = { workspace = true }
qr2term = "0.3.3"
rusqlite = { workspace = true }
rustyline = "14"
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }

View File

@@ -22,7 +22,6 @@ use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::qr_code_generator::create_qr_svg;
use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
@@ -356,7 +355,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
configure\n\
connect\n\
disconnect\n\
fetch\n\
connectivity\n\
maybenetwork\n\
housekeeping\n\
@@ -426,7 +424,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
checkqr <qr-content>\n\
joinqr <qr-content>\n\
setqr <qr-content>\n\
createqrsvg <qr-content>\n\
providerinfo <addr>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
@@ -489,9 +486,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"send-backup" => {
let provider = BackupProvider::prepare(&context).await?;
let qr = format_backup(&provider.qr())?;
println!("QR code: {}", qr);
qr2term::print_qr(qr.as_str())?;
let qr = provider.qr();
println!("QR code: {}", format_backup(&qr)?);
provider.await?;
}
"receive-backup" => {
@@ -1251,19 +1247,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Err(err) => println!("Cannot set config from QR code: {err:?}"),
}
}
"createqrsvg" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let svg = create_qr_svg(arg1)?;
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
fs::write(&file, svg).await?;
println!("{file:#?} written.");
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
let proxy_enabled = context
.get_config_bool(config::Config::ProxyEnabled)
let socks5_enabled = context
.get_config_bool(config::Config::Socks5Enabled)
.await?;
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
Some(info) => {
println!("Information for provider belonging to {arg1}:");
println!("status: {}", info.status as u32);

View File

@@ -9,7 +9,10 @@
extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::process::Command;
use ansi_term::Color;
use anyhow::{bail, Error};
use deltachat::chat::ChatId;
use deltachat::config;
@@ -19,7 +22,6 @@ use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::EventType;
use log::{error, info, warn};
use nu_ansi_term::Color;
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
@@ -166,7 +168,7 @@ const IMEX_COMMANDS: [&str; 13] = [
"stop",
];
const DB_COMMANDS: [&str; 11] = [
const DB_COMMANDS: [&str; 10] = [
"info",
"set",
"get",
@@ -174,7 +176,6 @@ const DB_COMMANDS: [&str; 11] = [
"configure",
"connect",
"disconnect",
"fetch",
"connectivity",
"maybenetwork",
"housekeeping",
@@ -240,13 +241,12 @@ const CONTACT_COMMANDS: [&str; 9] = [
"unblock",
"listblocked",
];
const MISC_COMMANDS: [&str; 12] = [
const MISC_COMMANDS: [&str; 11] = [
"getqr",
"getqrsvg",
"getbadqr",
"checkqr",
"joinqr",
"createqrsvg",
"fileinfo",
"clear",
"exit",
@@ -417,9 +417,6 @@ async fn handle_cmd(
"disconnect" => {
ctx.stop_io().await;
}
"fetch" => {
ctx.background_fetch().await?;
}
"configure" => {
ctx.configure().await?;
}
@@ -449,7 +446,12 @@ async fn handle_cmd(
qr.replace_range(12..22, "0000000000")
}
println!("{qr}");
qr2term::print_qr(qr.as_str())?;
let output = Command::new("qrencode")
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.output()
.expect("failed to execute process");
io::stdout().write_all(&output.stdout).unwrap();
io::stderr().write_all(&output.stderr).unwrap();
}
}
"getqrsvg" => {

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.147.1"
version = "1.142.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -13,13 +13,10 @@ classifiers = [
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
]

View File

@@ -9,19 +9,18 @@ import io
import pathlib
import ssl
from contextlib import contextmanager
from typing import TYPE_CHECKING
from imap_tools import (
AND,
Header,
MailBox,
MailBoxTls,
MailMessage,
MailMessageFlags,
errors,
)
if TYPE_CHECKING:
from . import Account
from . import Account, const
FLAGS = b"FLAGS"
FETCH = b"FETCH"
@@ -36,15 +35,28 @@ class DirectImap:
self.connect()
def connect(self):
# Assume the testing server supports TLS on port 993.
host = self.account.get_config("configured_mail_server")
port = 993
port = int(self.account.get_config("configured_mail_port"))
security = int(self.account.get_config("configured_mail_security"))
user = self.account.get_config("addr")
host = user.rsplit("@")[-1]
pw = self.account.get_config("mail_pw")
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
if security == const.SocketSecurity.PLAIN:
ssl_context = None
else:
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
if security == const.SocketSecurity.STARTTLS:
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
self.conn = MailBox(host, port, ssl_context=ssl_context)
self.conn.login(user, pw)
self.select_folder("INBOX")

View File

@@ -114,13 +114,13 @@ class ACFactory:
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
@pytest.fixture
@pytest.fixture()
def rpc(tmp_path) -> AsyncGenerator:
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
yield rpc_server
@pytest.fixture
@pytest.fixture()
def acfactory(rpc) -> AsyncGenerator:
return ACFactory(DeltaChat(rpc))

View File

@@ -12,11 +12,10 @@ import threading
import time
import pytest
from deltachat_rpc_client import EventType
@pytest.fixture
@pytest.fixture()
def path_to_webxdc(request):
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc")
assert p.exists()

View File

@@ -1,8 +1,6 @@
import logging
import time
import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
@@ -45,6 +43,13 @@ def test_qr_setup_contact_svg(acfactory) -> None:
_qr_code, svg = alice.get_qr_code_svg()
# Test that email address is in SVG
# when we have no display name.
# Check only the domain name, because
# long address may be split over multiple lines
# and not matched.
assert domain in svg
alice.set_config("displayname", "Alice")
# Test that display name is used
@@ -55,21 +60,14 @@ def test_qr_setup_contact_svg(acfactory) -> None:
@pytest.mark.parametrize("protect", [True, False])
def test_qr_securejoin(acfactory, protect, tmp_path):
alice, bob, fiona = acfactory.get_online_accounts(3)
def test_qr_securejoin(acfactory, protect):
alice, bob = acfactory.get_online_accounts(2)
# Setup second device for Alice
# to test observing securejoin protocol.
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group", protect=protect)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=protect)
assert alice_chat.get_basic_snapshot().is_protected == protect
logging.info("Bob joins the group")
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
bob.secure_join(qr_code)
@@ -98,21 +96,6 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
# Start second Alice device.
# Alice observes securejoin protocol and verifies Bob on second device.
alice2.start_io()
alice2.wait_for_securejoin_inviter_success()
alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
assert alice2_contact_bob_snapshot.is_verified
# The QR code token is synced, so alice2 must be able to handle join requests.
logging.info("Fiona joins the group via alice2")
alice.stop_io()
fiona.secure_join(qr_code)
alice2.wait_for_securejoin_inviter_success()
fiona.wait_for_securejoin_joiner_success()
def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
@@ -326,6 +309,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_ac2)
ac3_chat.add_contact(ac3_contact_ac2)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
@@ -335,8 +319,6 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "removed" in snapshot.text
ac3_chat.add_contact(ac3_contact_ac2)
event = ac2.wait_for_incoming_msg_event()
msg_id = event.msg_id
chat_id = event.chat_id
@@ -460,10 +442,7 @@ def test_qr_new_group_unblocked(acfactory):
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2 = acfactory.get_online_accounts(2)
# ac1new is only used to get a new address.
ac1new = acfactory.new_preconfigured_account()
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
@@ -472,7 +451,6 @@ def test_aeap_flow_verified(acfactory):
logging.info("ac2: start QR-code based join-group protocol")
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac2.wait_for_securejoin_joiner_success()
logging.info("sending first message")
msg_out = chat.send_text("old address").get_snapshot()
@@ -570,7 +548,6 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 waits for member added message and creates a QR code.
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
ac1_qr_code = snapshot.chat.get_qr_code()
# ac2 verifies ac1
@@ -585,29 +562,17 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 resetups the account.
ac1 = acfactory.resetup_account(ac1)
# Loop sending message from ac1 to ac2
# until ac2 accepts new ac1 key.
#
# This may not happen immediately because resetup of ac1
# rewinds "smeared timestamp" so Date: header for messages
# sent by new ac1 are in the past compared to the last Date:
# header sent by old ac1.
while True:
# ac1 sends a message to ac2.
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
ac1_chat_ac2.send_text("Hello!")
# ac1 sends a message to ac2.
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
ac1_chat_ac2.send_text("Hello!")
# ac2 receives a message.
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello!"
logging.info("ac2 received Hello!")
# ac2 receives a message.
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello!"
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
if not ac2_contact_ac1.get_snapshot().is_verified:
break
time.sleep(1)
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
assert not ac2_contact_ac1.get_snapshot().is_verified
# ac1 goes offline.
ac1.remove()
@@ -669,8 +634,7 @@ def test_withdraw_securejoin_qr(acfactory):
logging.info("Bob scanned withdrawn QR code")
while True:
event = alice.wait_for_event()
if (
event.kind == EventType.WARNING
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
):
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
break
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))

View File

@@ -3,13 +3,11 @@ import concurrent.futures
import json
import logging
import os
import socket
import subprocess
import time
from unittest.mock import MagicMock
import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.direct_imap import DirectImap
@@ -71,38 +69,6 @@ def test_configure_starttls(acfactory) -> None:
assert account.is_configured()
def test_configure_ip(acfactory) -> None:
account = acfactory.new_preconfigured_account()
domain = account.get_config("addr").rsplit("@")[-1]
ip_address = socket.gethostbyname(domain)
# This should fail TLS check.
account.set_config("mail_server", ip_address)
with pytest.raises(JsonRpcError):
account.configure()
def test_configure_alternative_port(acfactory) -> None:
"""Test that configuration with alternative port 443 works."""
account = acfactory.new_preconfigured_account()
account.set_config("mail_port", "443")
account.set_config("send_port", "443")
account.configure()
def test_configure_username(acfactory) -> None:
account = acfactory.new_preconfigured_account()
addr = account.get_config("addr")
account.set_config("mail_user", addr)
account.configure()
assert account.get_config("configured_mail_user") == addr
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@@ -433,7 +399,7 @@ def test_provider_info(rpc) -> None:
assert provider_info["id"] == "gmail"
# Disable MX record resolution.
rpc.set_config(account_id, "proxy_enabled", "1")
rpc.set_config(account_id, "socks5_enabled", "1")
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
@@ -655,24 +621,3 @@ def test_get_http_response(acfactory):
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
assert http_response["mimetype"] == "text/html"
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
# Certificate checks should be configured (not None)
assert configured_certificate_checks
# 0 is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic)
# and configuration failed to use strict TLS checks
# so it switched strict TLS checks off.
#
# New versions of Delta Chat are not disabling TLS checks
# unless users explicitly disables them
# or provider database says provider has invalid certificates.
#
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
# This test is a regression test to prevent this happening again.
assert configured_certificate_checks != "0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.147.1"
version = "1.142.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -10,8 +10,8 @@ keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
categories = ["cryptography", "std", "email"]
[dependencies]
deltachat-jsonrpc = { workspace = true }
deltachat = { workspace = true }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
deltachat = { path = "..", default-features = false }
anyhow = { workspace = true }
futures-lite = { workspace = true }

View File

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

View File

@@ -1,6 +1,7 @@
[advisories]
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
# Timing attack on RSA.
# Delta Chat does not use RSA for new keys
@@ -9,8 +10,15 @@ ignore = [
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
# Unmaintained ansi_term
"RUSTSEC-2021-0139",
# Unmaintained encoding
"RUSTSEC-2021-0153",
# Problem in curve25519-dalek 3.2.0 used by iroh 0.4.
# curve25519-dalek 4.1.3 has the problem fixed.
"RUSTSEC-2024-0344",
]
[bans]
@@ -19,10 +27,28 @@ ignore = [
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "asn1-rs-derive", version = "0.4.0" },
{ name = "asn1-rs-impl", version = "0.1.0" },
{ name = "asn1-rs", version = "0.5.2" },
{ name = "async-channel", version = "1.9.0" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "base64", version = "0.21.7" },
{ name = "bitflags", version = "1.3.2" },
{ name = "block-buffer", version = "<0.10" },
{ name = "convert_case", version = "0.4.0" },
{ name = "curve25519-dalek", version = "3.2.0" },
{ name = "darling_core", version = "<0.14" },
{ name = "darling_macro", version = "<0.14" },
{ name = "darling", version = "<0.14" },
{ name = "der_derive", version = "0.6.1" },
{ name = "derive_more", version = "0.99.17" },
{ name = "der-parser", version = "8.2.0" },
{ name = "der", version = "0.6.1" },
{ name = "digest", version = "<0.10" },
{ name = "dlopen2", version = "0.4.1" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "event-listener", version = "2.5.3" },
{ name = "event-listener", version = "4.0.3" },
{ name = "fastrand", version = "1.9.0" },
@@ -31,31 +57,63 @@ skip = [
{ name = "h2", version = "0.3.26" },
{ name = "http-body", version = "0.4.6" },
{ name = "http", version = "0.2.12" },
{ name = "hyper-rustls", version = "0.24.2" },
{ name = "hyper", version = "0.14.28" },
{ name = "idna", version = "0.4.0" },
{ name = "netlink-packet-core", version = "0.5.0" },
{ name = "netlink-packet-route", version = "0.15.0" },
{ name = "nix", version = "0.26.4" },
{ name = "oid-registry", version = "0.6.1" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pem", version = "1.1.1" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "rcgen", version = "<0.12.1" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "reqwest", version = "0.11.27" },
{ name = "ring", version = "0.16.20" },
{ name = "rustls-pemfile", version = "1.0.4" },
{ name = "rustls", version = "0.21.11" },
{ name = "rustls-webpki", version = "0.101.7" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "ssh-encoding", version = "0.1.0" },
{ name = "ssh-key", version = "0.5.1" },
{ name = "strsim", version = "0.10.0" },
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "synstructure", version = "0.12.6" },
{ name = "syn", version = "1.0.109" },
{ name = "system-configuration-sys", version = "0.5.0" },
{ name = "system-configuration", version = "0.5.1" },
{ name = "time", version = "<0.3" },
{ name = "tokio-rustls", version = "0.24.1" },
{ name = "toml_edit", version = "0.21.1" },
{ name = "untrusted", version = "0.7.1" },
{ name = "wasi", version = "<0.11" },
{ name = "webpki-roots", version ="0.25.4" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
{ name = "windows_aarch64_msvc", version = "<0.52" },
{ name = "windows-core", version = "<0.54.0" },
{ name = "windows_i686_gnu", version = "<0.52" },
{ name = "windows_i686_msvc", version = "<0.52" },
{ name = "windows-sys", version = "<0.59" },
{ name = "windows-sys", version = "<0.52" },
{ name = "windows-targets", version = "<0.52" },
{ name = "windows", version = "0.32.0" },
{ name = "windows", version = "<0.54.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
{ name = "winnow", version = "0.5.40" },
{ name = "winreg", version = "0.50.0" },
{ name = "x509-parser", version = "<0.16.0" },
]

109
flake.lock generated
View File

@@ -3,15 +3,15 @@
"android": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1729369131,
"narHash": "sha256-PtfScp+nQd1PsT5rf0Qgjdbsh4Iag6R1ivYMWLizyIc=",
"lastModified": 1712088936,
"narHash": "sha256-mVjeSWQiR/t4UZ9fUawY9OEPAhY1R3meYG+0oh8DUBs=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "82bffbf3f06bdccf44fc62a9bd4f152ac80a55b0",
"rev": "2d8181caef279f19c4a33dc694723f89ffc195d4",
"type": "github"
},
"original": {
@@ -22,17 +22,18 @@
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"android",
"nixpkgs"
]
},
"locked": {
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"lastModified": 1711099426,
"narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=",
"owner": "numtide",
"repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8",
"type": "github"
},
"original": {
@@ -47,11 +48,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1729375822,
"narHash": "sha256-bRo4xVwUhvJ4Gz+OhWMREFMdBOYSw4Yi1Apj01ebbug=",
"lastModified": 1714112748,
"narHash": "sha256-jq6Cpf/pQH85p+uTwPPrGG8Ky/zUOTwMJ7mcqc5M4So=",
"owner": "nix-community",
"repo": "fenix",
"rev": "2853e7d9b5c52a148a9fb824bfe4f9f433f557ab",
"rev": "3ae4b908a795b6a3824d401a0702e11a7157d7e1",
"type": "github"
},
"original": {
@@ -65,11 +66,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -83,11 +84,29 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
@@ -101,11 +120,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1721727458,
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
"lastModified": 1713520724,
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"owner": "nix-community",
"repo": "naersk",
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"type": "github"
},
"original": {
@@ -131,11 +150,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1729256560,
"narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
"lastModified": 1711703276,
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0",
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
"type": "github"
},
"original": {
@@ -147,11 +166,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1729070438,
"narHash": "sha256-KOTTUfPkugH52avUvXGxvWy8ibKKj4genodIYUED+Kc=",
"lastModified": 1713895582,
"narHash": "sha256-cfh1hi+6muQMbi9acOlju3V1gl8BEaZBXBR9jQfQi4U=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5785b6bb5eaae44e627d541023034e1601455827",
"rev": "572af610f6151fd41c212f897c71f7056e3fb518",
"type": "github"
},
"original": {
@@ -163,12 +182,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1729265718,
"narHash": "sha256-4HQI+6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ccc0c2126893dd20963580b6478d1a10a4512185",
"type": "github"
"lastModified": 1711668574,
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
"type": "path"
},
"original": {
"id": "nixpkgs",
@@ -177,11 +195,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1729256560,
"narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
"lastModified": 1714076141,
"narHash": "sha256-Drmja/f5MRHZCskS6mvzFqxEaZMeciScCTFxWVLqWEY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0",
"rev": "7bb2ccd8cdc44c91edba16c48d2c8f331fb3d856",
"type": "github"
},
"original": {
@@ -195,7 +213,7 @@
"inputs": {
"android": "android",
"fenix": "fenix",
"flake-utils": "flake-utils_2",
"flake-utils": "flake-utils_3",
"naersk": "naersk",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_4"
@@ -204,11 +222,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1729255720,
"narHash": "sha256-yODOuZxBkS0UfqMa6nmbqNbVfIbsu0tYLbV5vZzmsqI=",
"lastModified": 1714031783,
"narHash": "sha256-xS/niQsq1CQPOe4M4jvVPO2cnXS/EIeRG5gIopUbk+Q=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "72b214fbfbe6f7b95a7877b962783bd42062cc0a",
"rev": "56bee2ddafa6177b19c631eedc88d43366553223",
"type": "github"
},
"original": {
@@ -247,6 +265,21 @@
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -50,7 +50,6 @@ module.exports = {
DC_EVENT_IMEX_PROGRESS: 2051,
DC_EVENT_INCOMING_MSG: 2005,
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
DC_EVENT_INCOMING_REACTION: 2002,
DC_EVENT_INFO: 100,
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,
@@ -68,7 +67,6 @@ module.exports = {
DC_EVENT_SMTP_MESSAGE_SENT: 103,
DC_EVENT_WARNING: 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT: 2151,
DC_EVENT_WEBXDC_REALTIME_DATA: 2150,
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
DC_GCL_ADD_ALLDONE_HINT: 4,
@@ -136,7 +134,6 @@ module.exports = {
DC_QR_FPR_OK: 210,
DC_QR_FPR_WITHOUT_ADDR: 230,
DC_QR_LOGIN: 520,
DC_QR_PROXY: 271,
DC_QR_REVIVE_VERIFYCONTACT: 510,
DC_QR_REVIVE_VERIFYGROUP: 512,
DC_QR_TEXT: 330,

View File

@@ -16,7 +16,6 @@ module.exports = {
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2002: 'DC_EVENT_INCOMING_REACTION',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
@@ -39,7 +38,6 @@ module.exports = {
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',

View File

@@ -50,7 +50,6 @@ export enum C {
DC_EVENT_IMEX_PROGRESS = 2051,
DC_EVENT_INCOMING_MSG = 2005,
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
DC_EVENT_INCOMING_REACTION = 2002,
DC_EVENT_INFO = 100,
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
@@ -68,7 +67,6 @@ export enum C {
DC_EVENT_SMTP_MESSAGE_SENT = 103,
DC_EVENT_WARNING = 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT = 2151,
DC_EVENT_WEBXDC_REALTIME_DATA = 2150,
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
DC_GCL_ADD_ALLDONE_HINT = 4,
@@ -136,7 +134,6 @@ export enum C {
DC_QR_FPR_OK = 210,
DC_QR_FPR_WITHOUT_ADDR = 230,
DC_QR_LOGIN = 520,
DC_QR_PROXY = 271,
DC_QR_REVIVE_VERIFYCONTACT = 510,
DC_QR_REVIVE_VERIFYGROUP = 512,
DC_QR_TEXT = 330,
@@ -323,7 +320,6 @@ export const EventId2EventName: { [key: number]: string } = {
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2002: 'DC_EVENT_INCOMING_REACTION',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
@@ -346,7 +342,6 @@ export const EventId2EventName: { [key: number]: string } = {
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',

View File

@@ -475,6 +475,47 @@ export class Context extends EventEmitter {
return binding.dcn_get_msg_html(this.dcn_context, Number(messageId))
}
getNextMediaMessage(
messageId: number,
msgType1: number,
msgType2: number,
msgType3: number
) {
debug(
`getNextMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
)
return this._getNextMedia(messageId, 1, msgType1, msgType2, msgType3)
}
getPreviousMediaMessage(
messageId: number,
msgType1: number,
msgType2: number,
msgType3: number
) {
debug(
`getPreviousMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
)
return this._getNextMedia(messageId, -1, msgType1, msgType2, msgType3)
}
_getNextMedia(
messageId: number,
dir: number,
msgType1: number,
msgType2: number,
msgType3: number
): number {
return binding.dcn_get_next_media(
this.dcn_context,
Number(messageId),
dir,
msgType1 || 0,
msgType2 || 0,
msgType3 || 0
)
}
getSecurejoinQrCode(chatId: number): string {
debug(`getSecurejoinQrCode ${chatId}`)
return binding.dcn_get_securejoin_qr(this.dcn_context, Number(chatId))

View File

@@ -1053,6 +1053,27 @@ NAPI_METHOD(dcn_get_msg_html) {
NAPI_RETURN_AND_UNREF_STRING(msg_html);
}
NAPI_METHOD(dcn_get_next_media) {
NAPI_ARGV(6);
NAPI_DCN_CONTEXT();
NAPI_ARGV_UINT32(msg_id, 1);
NAPI_ARGV_INT32(dir, 2);
NAPI_ARGV_INT32(msg_type1, 3);
NAPI_ARGV_INT32(msg_type2, 4);
NAPI_ARGV_INT32(msg_type3, 5);
//TRACE("calling..");
uint32_t next_id = dc_get_next_media(dcn_context->dc_context,
msg_id,
dir,
msg_type1,
msg_type2,
msg_type3);
//TRACE("result %d", next_id);
NAPI_RETURN_UINT32(next_id);
}
NAPI_METHOD(dcn_set_chat_visibility) {
NAPI_ARGV(3);
NAPI_DCN_CONTEXT();
@@ -3422,6 +3443,7 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_get_msg_cnt);
NAPI_EXPORT_FUNCTION(dcn_get_msg_info);
NAPI_EXPORT_FUNCTION(dcn_get_msg_html);
NAPI_EXPORT_FUNCTION(dcn_get_next_media);
NAPI_EXPORT_FUNCTION(dcn_set_chat_visibility);
NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr);
NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr_svg);

View File

@@ -271,7 +271,7 @@ describe('Basic offline Tests', function () {
'sync_msgs',
'sentbox_watch',
'show_emails',
'proxy_enabled',
'socks5_enabled',
'sqlite_version',
'uptime',
'used_account_settings',

View File

@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.147.1"
"version": "1.142.0"
}

View File

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

View File

@@ -194,13 +194,15 @@ class Account:
assert res != ffi.NULL, f"config value not found for: {name!r}"
return from_dc_charpointer(res)
def _preconfigure_keypair(self, secret: str) -> None:
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
"""
res = lib.dc_preconfigure_keypair(
self._dc_context,
as_dc_charpointer(addr),
ffi.NULL,
as_dc_charpointer(secret),
)
if res == 0:

View File

@@ -308,7 +308,7 @@ class Chat:
msg = as_dc_charpointer(text)
msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg)
if msg_id == 0:
raise ValueError("The message could not be sent. Does the chat exist?")
raise ValueError("message could not be send, does chat exist?")
return Message.from_db(self.account, msg_id)
def send_file(self, path, mime_type="application/octet-stream"):

View File

@@ -8,19 +8,19 @@ import io
import pathlib
import ssl
from contextlib import contextmanager
from typing import List, TYPE_CHECKING
from typing import List
from imap_tools import (
AND,
Header,
MailBox,
MailBoxTls,
MailMessage,
MailMessageFlags,
errors,
)
if TYPE_CHECKING:
from deltachat import Account
from deltachat import Account, const
FLAGS = b"FLAGS"
FETCH = b"FETCH"
@@ -28,7 +28,7 @@ ALL = "1:*"
class DirectImap:
def __init__(self, account: "Account") -> None:
def __init__(self, account: Account) -> None:
self.account = account
self.logid = account.get_config("displayname") or id(account)
self._idling = False
@@ -36,13 +36,27 @@ class DirectImap:
def connect(self):
host = self.account.get_config("configured_mail_server")
port = 993
port = int(self.account.get_config("configured_mail_port"))
security = int(self.account.get_config("configured_mail_security"))
user = self.account.get_config("addr")
host = user.rsplit("@")[-1]
pw = self.account.get_config("mail_pw")
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
if security == const.DC_SOCKET_PLAIN:
ssl_context = None
else:
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
if security == const.DC_SOCKET_STARTTLS:
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
elif security == const.DC_SOCKET_PLAIN or security == const.DC_SOCKET_SSL:
self.conn = MailBox(host, port, ssl_context=ssl_context)
self.conn.login(user, pw)
self.select_folder("INBOX")

View File

@@ -462,7 +462,7 @@ class ACFactory:
def remove_preconfigured_keys(self) -> None:
self._preconfigured_keys = []
def _preconfigure_key(self, account):
def _preconfigure_key(self, account, addr):
# Only set a preconfigured key if we haven't used it yet for another account.
try:
keyname = self._preconfigured_keys.pop(0)
@@ -471,9 +471,9 @@ class ACFactory:
else:
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
if fname_sec:
account._preconfigure_keypair(fname_sec)
account._preconfigure_keypair(addr, fname_sec)
return True
print("WARN: could not use preconfigured keys")
print(f"WARN: could not use preconfigured keys for {addr!r}")
def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Account:
# do a pseudo-configured account
@@ -492,7 +492,7 @@ class ACFactory:
"configured": "1",
},
)
self._preconfigure_key(ac)
self._preconfigure_key(ac, addr)
self._acsetup.init_logging(ac)
return ac
@@ -525,10 +525,9 @@ class ACFactory:
configdict.setdefault("mvbox_move", False)
configdict.setdefault("sentbox_watch", False)
configdict.setdefault("sync_msgs", False)
configdict.setdefault("delete_server_after", 0)
ac.update_config(configdict)
self._acsetup._account2config[ac] = configdict
self._preconfigure_key(ac)
self._preconfigure_key(ac, configdict["addr"])
return ac
def wait_configured(self, account) -> None:

View File

@@ -484,24 +484,6 @@ def test_move_works_on_self_sent(acfactory):
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
def test_move_sync_msgs(acfactory):
ac1 = acfactory.new_online_configuring_account(bcc_self=True, sync_msgs=True, fix_is_chatmail=True)
acfactory.bring_accounts_online()
ac1.direct_imap.select_folder("DeltaChat")
# Sync messages may also be sent during the configuration.
mvbox_msg_cnt = len(ac1.direct_imap.get_all_messages())
ac1.set_config("displayname", "Alice")
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
ac1.set_config("displayname", "Bob")
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
ac1.direct_imap.select_folder("Inbox")
assert len(ac1.direct_imap.get_all_messages()) == 0
ac1.direct_imap.select_folder("DeltaChat")
assert len(ac1.direct_imap.get_all_messages()) == mvbox_msg_cnt + 2
def test_forward_messages(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
@@ -628,7 +610,7 @@ def test_long_group_name(acfactory, lp):
def test_send_self_message(acfactory, lp):
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, bcc_self=True)
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
lp.sec("ac1: create self chat")
chat = ac1.get_self_contact().create_chat()
@@ -2084,11 +2066,12 @@ def test_send_receive_locations(acfactory, lp):
def test_immediate_autodelete(acfactory, lp):
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account()
acfactory.bring_accounts_online()
# "1" means delete immediately, while "0" means do not delete
ac2.set_config("delete_server_after", "1")
acfactory.bring_accounts_online()
lp.sec("ac1: create chat with ac2")
chat1 = ac1.create_chat(ac2)
ac2.create_chat(ac1)

View File

@@ -67,7 +67,7 @@ class TestOfflineAccountBasic:
ac = acfactory.get_unconfigured_account()
alice_secret = data.read_path("key/alice-secret.asc")
assert alice_secret
ac._preconfigure_keypair(alice_secret)
ac._preconfigure_keypair("alice@example.org", alice_secret)
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()

View File

@@ -1 +1 @@
2024-10-13
2024-07-23

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.82.0
RUST_VERSION=1.80.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -3,4 +3,4 @@ set -euo pipefail
tox -c deltachat-rpc-client -e py --devenv venv
venv/bin/pip install --upgrade pip
cargo install --locked --path deltachat-rpc-server/ --root "$PWD/venv" --debug
cargo install --path deltachat-rpc-server/ --root "$PWD/venv" --debug

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cargo install --locked --path deltachat-rpc-server/ --root "$PWD/venv" --debug
cargo install --path deltachat-rpc-server/ --root "$PWD/venv" --debug
PATH="$PWD/venv/bin:$PATH" tox -c deltachat-rpc-client

View File

@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,py313,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"

View File

@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=77cbf92a8565fdf1bcaba10fa93c1455c750a1e9
REV=828e5ddc7e6609b582fbd7f063cc3f60b580ce96
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"

View File

@@ -1,7 +1,6 @@
//! # Account manager module.
use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
@@ -166,25 +165,13 @@ impl Accounts {
.remove(&id)
.with_context(|| format!("no account with id {id}"))?;
ctx.stop_io().await;
// Explicitly close the database
// to make sure the database file is closed
// and can be removed on Windows.
// If some spawned task tries to use the database afterwards,
// it will fail.
//
// Previously `stop_io()` aborted the tasks without awaiting them
// and this resulted in keeping `Context` clones inside
// `Future`s that were not dropped. This bug is fixed now,
// but explicitly closing the database ensures that file is freed
// even if not all `Context` references are dropped.
ctx.sql.close().await;
drop(ctx);
if let Some(cfg) = self.config.get_account(id) {
let account_path = self.dir.join(cfg.dir);
try_many_times(|| fs::remove_dir_all(&account_path))
fs::remove_dir_all(&account_path)
.await
.context("failed to remove account data")?;
}
@@ -220,10 +207,10 @@ impl Accounts {
fs::create_dir_all(self.dir.join(&account_config.dir))
.await
.context("failed to create dir")?;
try_many_times(|| fs::rename(&dbfile, &new_dbfile))
fs::rename(&dbfile, &new_dbfile)
.await
.context("failed to rename dbfile")?;
try_many_times(|| fs::rename(&blobdir, &new_blobdir))
fs::rename(&blobdir, &new_blobdir)
.await
.context("failed to rename blobdir")?;
if walfile.exists() {
@@ -248,7 +235,7 @@ impl Accounts {
}
Err(err) => {
let account_path = std::path::PathBuf::from(&account_config.dir);
try_many_times(|| fs::remove_dir_all(&account_path))
fs::remove_dir_all(&account_path)
.await
.context("failed to remove account data")?;
self.config.remove_account(account_config.id).await?;
@@ -633,37 +620,6 @@ impl Config {
}
}
/// Spend up to 1 minute trying to do the operation.
///
/// Even if Delta Chat itself does not hold the file lock,
/// there may be other processes such as antivirus,
/// or the filesystem may be network-mounted.
///
/// Without this workaround removing account may fail on Windows with an error
/// "The process cannot access the file because it is being used by another process. (os error 32)".
async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
where
F: Fn() -> Fut,
Fut: Future<Output = std::result::Result<(), T>>,
{
let mut counter = 0;
loop {
counter += 1;
if let Err(err) = f().await {
if counter > 60 {
return Err(err);
}
// Wait 1 second and try again.
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
} else {
break;
}
}
Ok(())
}
/// Configuration of a single account.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct AccountConfig {

View File

@@ -253,16 +253,16 @@ impl<'a> BlobObject<'a> {
///
/// The extension part will always be lowercased.
fn sanitise_name(name: &str) -> (String, String) {
let mut name = name;
let mut name = name.to_string();
for part in name.rsplit('/') {
if !part.is_empty() {
name = part;
name = part.to_string();
break;
}
}
for part in name.rsplit('\\') {
if !part.is_empty() {
name = part;
name = part.to_string();
break;
}
}
@@ -272,39 +272,32 @@ impl<'a> BlobObject<'a> {
replacement: "",
};
let name = sanitize_filename::sanitize_with_options(name, opts);
// Let's take a tricky filename,
let clean = sanitize_filename::sanitize_with_options(name, opts);
// Let's take the tricky filename
// "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example.
// Assume that the extension is 32 chars maximum.
let ext: String = name
.chars()
// Split it into "file" and "with_lots_of_characters_behind_point_and_double_ending.tar.gz":
let mut iter = clean.splitn(2, '.');
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
// stem == "file"
let ext_chars = iter.next().unwrap_or_default().chars();
let ext: String = ext_chars
.rev()
.take_while(|c| !c.is_whitespace())
.take(33)
.take(32)
.collect::<Vec<_>>()
.iter()
.rev()
.collect();
// ext == "nd_point_and_double_ending.tar.gz"
// ext == "d_point_and_double_ending.tar.gz"
// Split it into "nd_point_and_double_ending" and "tar.gz":
let mut iter = ext.splitn(2, '.');
iter.next();
let ext = iter.next().unwrap_or_default();
let ext = if ext.is_empty() {
String::new()
if ext.is_empty() {
(stem, "".to_string())
} else {
format!(".{ext}")
// ".tar.gz"
};
let stem = name
.strip_suffix(&ext)
.unwrap_or_default()
.chars()
.take(64)
.collect();
(stem, ext.to_lowercase())
(stem, format!(".{ext}").to_lowercase())
// Return ("file", ".d_point_and_double_ending.tar.gz")
// which is not perfect but acceptable.
}
}
/// Checks whether a name is a valid blob name.
@@ -622,7 +615,7 @@ fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
0
}
impl fmt::Display for BlobObject<'_> {
impl<'a> fmt::Display for BlobObject<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "$BLOBDIR/{}", self.name)
}
@@ -673,6 +666,10 @@ impl<'a> BlobDirContents<'a> {
pub(crate) fn iter(&self) -> BlobDirIter<'_> {
BlobDirIter::new(self.context, self.inner.iter())
}
pub(crate) fn len(&self) -> usize {
self.inner.len()
}
}
/// A iterator over all the [`BlobObject`]s in the blobdir.
@@ -970,19 +967,6 @@ mod tests {
assert!(!stem.contains(':'));
assert!(!stem.contains('*'));
assert!(!stem.contains('?'));
let (stem, ext) = BlobObject::sanitise_name(
"file.with_lots_of_characters_behind_point_and_double_ending.tar.gz",
);
assert_eq!(
stem,
"file.with_lots_of_characters_behind_point_and_double_ending"
);
assert_eq!(ext, ".tar.gz");
let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz");
assert_eq!(stem, "a. tar");
assert_eq!(ext, ".tar.gz");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -46,8 +46,8 @@ use crate::stock_str;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
create_smeared_timestamps, get_abs_path, gm2local_offset, smeared_time, time,
truncate_msg_text, IsNoneOrEmpty, SystemTime,
create_smeared_timestamps, get_abs_path, gm2local_offset, smeared_time, time, IsNoneOrEmpty,
SystemTime,
};
use crate::webxdc::StatusUpdateSerial;
@@ -279,10 +279,9 @@ impl ChatId {
) -> Result<Self> {
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
Some(chat) => {
if create_blocked != Blocked::Not || chat.blocked == Blocked::Not {
return Ok(chat.id);
if create_blocked == Blocked::Not && chat.blocked != Blocked::Not {
chat.id.set_blocked(context, Blocked::Not).await?;
}
chat.id.set_blocked(context, Blocked::Not).await?;
chat.id
}
None => {
@@ -578,7 +577,7 @@ impl ChatId {
Ok(())
}
/// Sets protection and adds a message.
/// Sets protection and sends or adds a message.
///
/// `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
@@ -589,16 +588,20 @@ impl ChatId {
timestamp_sort: i64,
contact_id: Option<ContactId>,
) -> Result<()> {
let protection_status_modified = self
.inner_set_protection(context, protect)
.await
.with_context(|| format!("Cannot set protection for {self}"))?;
if protection_status_modified {
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
.await?;
chatlist_events::emit_chatlist_item_changed(context, self);
match self.inner_set_protection(context, protect).await {
Ok(protection_status_modified) => {
if protection_status_modified {
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
.await?;
chatlist_events::emit_chatlist_item_changed(context, self);
}
Ok(())
}
Err(e) => {
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
Err(e)
}
}
Ok(())
}
/// Sets protection and sends or adds a message.
@@ -612,9 +615,8 @@ impl ChatId {
contact_id: Option<ContactId>,
) -> Result<()> {
let sort_to_bottom = true;
let (received, incoming) = (false, false);
let ts = self
.calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, received, incoming)
.calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, false)
.await?
// Always sort protection messages below `SystemMessage::SecurejoinWait{,Timeout}` ones
// in case of race conditions.
@@ -866,14 +868,13 @@ impl ChatId {
///
/// Returns `true`, if message was deleted, `false` otherwise.
async fn maybe_delete_draft(self, context: &Context) -> Result<bool> {
Ok(context
.sql
.execute(
"DELETE FROM msgs WHERE chat_id=? AND state=?",
(self, MessageState::OutDraft),
)
.await?
> 0)
match self.get_draft_msg_id(context).await? {
Some(msg_id) => {
msg_id.delete_from_db(context).await?;
Ok(true)
}
None => Ok(false),
}
}
/// Set provided message as draft message for specified chat.
@@ -945,18 +946,12 @@ impl ChatId {
}
}
// insert new draft
self.maybe_delete_draft(context).await?;
let row_id = context
.sql
.transaction(|transaction| {
// Delete existing draft if it exists.
transaction.execute(
"DELETE FROM msgs WHERE chat_id=? AND state=?",
(self, MessageState::OutDraft),
)?;
// Insert new draft.
transaction.execute(
"INSERT INTO msgs (
.insert(
"INSERT INTO msgs (
chat_id,
from_id,
timestamp,
@@ -968,22 +963,19 @@ impl ChatId {
hidden,
mime_in_reply_to)
VALUES (?,?,?,?,?,?,?,?,?,?);",
(
self,
ContactId::SELF,
time(),
msg.viewtype,
MessageState::OutDraft,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
1,
msg.in_reply_to.as_deref().unwrap_or_default(),
),
)?;
Ok(transaction.last_insert_rowid())
})
(
self,
ContactId::SELF,
time(),
msg.viewtype,
MessageState::OutDraft,
&msg.text,
message::normalize_text(&msg.text),
msg.param.to_string(),
1,
msg.in_reply_to.as_deref().unwrap_or_default(),
),
)
.await?;
msg.id = MsgId::new(row_id.try_into()?);
Ok(true)
@@ -1049,13 +1041,7 @@ impl ChatId {
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
let timestamp = context
.sql
.query_get_value(
"SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=?
HAVING COUNT(*) > 0",
(self,),
)
.query_get_value("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", (self,))
.await?;
Ok(timestamp)
}
@@ -1241,7 +1227,6 @@ impl ChatId {
AND ((state BETWEEN {} AND {}) OR (state >= {})) \
AND NOT hidden \
AND download_state={} \
AND from_id != {} \
ORDER BY timestamp DESC, id DESC \
LIMIT 1;",
MessageState::InFresh as u32,
@@ -1250,9 +1235,6 @@ impl ChatId {
// Do not reply to not fully downloaded messages. Such a message could be a group chat
// message that we assigned to 1:1 chat.
DownloadState::Done as u32,
// Do not reference info messages, they are not actually sent out
// and have Message-IDs unknown to other chat members.
ContactId::INFO.to_u32(),
);
sql.query_row_optional(&query, (self,), f).await
}
@@ -1264,7 +1246,7 @@ impl ChatId {
) -> Result<Option<(String, String, String)>> {
self.parent_query(
context,
"rfc724_mid, mime_in_reply_to, IFNULL(mime_references, '')",
"rfc724_mid, mime_in_reply_to, mime_references",
state_out_min,
|row: &rusqlite::Row| {
let rfc724_mid: String = row.get(0)?;
@@ -1399,14 +1381,12 @@ impl ChatId {
/// corresponding event in case of a system message (usually the current system time).
/// `always_sort_to_bottom` makes this ajust the returned timestamp up so that the message goes
/// to the chat bottom.
/// `received` -- whether the message is received. Otherwise being sent.
/// `incoming` -- whether the message is incoming.
pub(crate) async fn calc_sort_timestamp(
self,
context: &Context,
message_timestamp: i64,
always_sort_to_bottom: bool,
received: bool,
incoming: bool,
) -> Result<i64> {
let mut sort_timestamp = cmp::min(message_timestamp, smeared_time(context));
@@ -1420,45 +1400,26 @@ impl ChatId {
context
.sql
.query_get_value(
"SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=? AND state!=?
HAVING COUNT(*) > 0",
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state!=?",
(self, MessageState::OutDraft),
)
.await?
} else if received {
// Received messages shouldn't mingle with just sent ones and appear somewhere in the
// middle of the chat, so we go after the newest non fresh message.
//
// But if a received outgoing message is older than some seen message, better sort the
// received message purely by timestamp. We could place it just before that seen
// message, but anyway the user may not notice it.
//
// NB: Received outgoing messages may break sorting of fresh incoming ones, but this
// shouldn't happen frequently. Seen incoming messages don't really break sorting of
// fresh ones, they rather mean that older incoming messages are actually seen as well.
} else if incoming {
// get newest non fresh message for this chat.
// If a user hasn't been online for some time, the Inbox is fetched first and then the
// Sentbox. In order for Inbox and Sent messages to be allowed to mingle, outgoing
// messages are purely sorted by their sent timestamp. NB: The Inbox must be fetched
// first otherwise Inbox messages would be always below old Sentbox messages. We could
// take in the query below only incoming messages, but then new incoming messages would
// mingle with just sent outgoing ones and apear somewhere in the middle of the chat.
context
.sql
.query_row_optional(
"SELECT MAX(timestamp), MAX(IIF(state=?,timestamp_sent,0))
FROM msgs
WHERE chat_id=? AND hidden=0 AND state>?
HAVING COUNT(*) > 0",
(MessageState::InSeen, self, MessageState::InFresh),
|row| {
let ts: i64 = row.get(0)?;
let ts_sent_seen: i64 = row.get(1)?;
Ok((ts, ts_sent_seen))
},
.query_get_value(
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0 AND state>?",
(self, MessageState::InFresh),
)
.await?
.and_then(|(ts, ts_sent_seen)| {
match incoming || ts_sent_seen <= message_timestamp {
true => Some(ts),
false => None,
}
})
} else {
None
};
@@ -1973,13 +1934,11 @@ impl Chat {
msg.param.set_int(Param::AttachGroupImage, 1);
self.param.remove(Param::Unpromoted);
self.update_param(context).await?;
// TODO: Remove this compat code needed because Core <= v1.143:
// - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also
// send them when the group is promoted.
// - doesn't sync QR code tokens for unpromoted groups and the group might be created
// before an upgrade.
// send_sync_msg() is called (usually) a moment later at send_msg_to_smtp()
// when the group creation message is actually sent through SMTP --
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
context
.sync_qr_code_tokens(Some(self.grpid.as_str()))
.sync_qr_code_tokens(Some(self.id))
.await
.log_err(context)
.ok();
@@ -2112,8 +2071,6 @@ impl Chat {
msg.from_id = ContactId::SELF;
msg.rfc724_mid = new_rfc724_mid;
msg.timestamp_sort = timestamp;
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
let mime_modified = new_mime_headers.is_some() | was_truncated;
// add message to the database
if let Some(update_msg_id) = update_msg_id {
@@ -2135,14 +2092,14 @@ impl Chat {
msg.timestamp_sort,
msg.viewtype,
msg.state,
msg_text,
message::normalize_text(&msg_text),
msg.text,
message::normalize_text(&msg.text),
&msg.subject,
msg.param.to_string(),
msg.hidden,
msg.in_reply_to.as_deref().unwrap_or_default(),
new_references,
mime_modified,
new_mime_headers.is_some(),
new_mime_headers.unwrap_or_default(),
location_id as i32,
ephemeral_timer,
@@ -2186,14 +2143,14 @@ impl Chat {
msg.timestamp_sort,
msg.viewtype,
msg.state,
msg_text,
message::normalize_text(&msg_text),
msg.text,
message::normalize_text(&msg.text),
&msg.subject,
msg.param.to_string(),
msg.hidden,
msg.in_reply_to.as_deref().unwrap_or_default(),
new_references,
mime_modified,
new_mime_headers.is_some(),
new_mime_headers.unwrap_or_default(),
location_id as i32,
ephemeral_timer,
@@ -2283,7 +2240,7 @@ pub(crate) async fn sync(context: &Context, id: SyncId, action: SyncAction) -> R
context
.add_sync_item(SyncData::AlterChat { id, action })
.await?;
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -2941,15 +2898,13 @@ async fn prepare_send_msg(
);
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
}
let row_ids = create_send_msg_jobs(context, msg)
.await
.context("Failed to create send jobs")?;
Ok(row_ids)
create_send_msg_jobs(context, msg).await
}
/// Constructs jobs for sending a message and inserts them into the appropriate table.
/// Constructs jobs for sending a message and inserts them into the `smtp` table.
///
/// Returns row ids if `smtp` table jobs were created or an empty `Vec` otherwise.
/// Returns row ids if jobs were created or an empty `Vec` otherwise, e.g. when sending to a
/// group with only self and no BCC-to-self configured.
///
/// The caller has to interrupt SMTP loop or otherwise process new rows.
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
@@ -3043,6 +2998,12 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
}
}
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
if let Err(err) = context.delete_sync_ids(sync_ids).await {
error!(context, "Failed to delete sync ids: {err:#}.");
}
}
if attach_selfavatar {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
@@ -3059,30 +3020,19 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
let chunk_size = context.get_max_smtp_rcpt_to().await?;
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new();
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
t.execute(
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
(),
for recipients_chunk in recipients.chunks(chunk_size) {
let recipients_chunk = recipients_chunk.join(" ");
let row_id = t.execute(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
VALUES (?1, ?2, ?3, ?4)",
(
&rendered_msg.rfc724_mid,
recipients_chunk,
&rendered_msg.message,
msg.id,
),
)?;
t.execute(
"INSERT INTO imap_send (mime, msg_id) VALUES (?, ?)",
(&rendered_msg.message, msg.id),
)?;
} else {
for recipients_chunk in recipients.chunks(chunk_size) {
let recipients_chunk = recipients_chunk.join(" ");
let row_id = t.execute(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
VALUES (?1, ?2, ?3, ?4)",
(
&rendered_msg.rfc724_mid,
recipients_chunk,
&rendered_msg.message,
msg.id,
),
)?;
row_ids.push(row_id.try_into()?);
}
row_ids.push(row_id.try_into()?);
}
Ok(row_ids)
};
@@ -3457,6 +3407,65 @@ pub async fn get_chat_media(
Ok(list)
}
/// Indicates the direction over which to iterate.
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(i32)]
pub enum Direction {
/// Search forward.
Forward = 1,
/// Search backward.
Backward = -1,
}
/// Searches next/previous message based on the given message and list of types.
///
/// Deprecated since 2023-10-03.
#[deprecated(note = "use `get_chat_media` instead")]
pub async fn get_next_media(
context: &Context,
curr_msg_id: MsgId,
direction: Direction,
msg_type: Viewtype,
msg_type2: Viewtype,
msg_type3: Viewtype,
) -> Result<Option<MsgId>> {
let mut ret: Option<MsgId> = None;
if let Ok(msg) = Message::load_from_db(context, curr_msg_id).await {
let list: Vec<MsgId> = get_chat_media(
context,
Some(msg.chat_id),
if msg_type != Viewtype::Unknown {
msg_type
} else {
msg.viewtype
},
msg_type2,
msg_type3,
)
.await?;
for (i, msg_id) in list.iter().enumerate() {
if curr_msg_id == *msg_id {
match direction {
Direction::Forward => {
if i + 1 < list.len() {
ret = list.get(i + 1).copied();
}
}
Direction::Backward => {
if i >= 1 {
ret = list.get(i - 1).copied();
}
}
}
break;
}
}
}
Ok(ret)
}
/// Returns a vector of contact IDs for given chat ID.
pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
// Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a
@@ -3717,13 +3726,17 @@ pub(crate) async fn add_contact_to_chat_ex(
bail!("can not add contact because the account is not part of the group/broadcast");
}
let sync_qr_code_tokens;
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
chat.param.remove(Param::Unpromoted);
chat.update_param(context).await?;
sync_qr_code_tokens = true;
} else {
sync_qr_code_tokens = false;
if context
.sync_qr_code_tokens(Some(chat_id))
.await
.log_err(context)
.is_ok()
{
context.scheduler.interrupt_smtp().await;
}
}
if context.is_self_addr(contact.get_addr()).await? {
@@ -3767,20 +3780,6 @@ pub(crate) async fn add_contact_to_chat_ex(
return Err(e);
}
sync = Nosync;
// TODO: Remove this compat code needed because Core <= v1.143:
// - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also send
// them when the group is promoted.
// - doesn't sync QR code tokens for unpromoted groups and the group might be created before
// an upgrade.
if sync_qr_code_tokens
&& context
.sync_qr_code_tokens(Some(chat.grpid.as_str()))
.await
.log_err(context)
.is_ok()
{
context.scheduler.interrupt_inbox().await;
}
}
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
@@ -4255,14 +4254,10 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
msg.update_param(context).await?;
}
match msg.get_state() {
// `get_state()` may return an outdated `OutPending`, so update anyway.
MessageState::OutPending
| MessageState::OutFailed
| MessageState::OutDelivered
| MessageState::OutMdnRcvd => {
MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {
message::update_msg_state(context, msg.id, MessageState::OutPending).await?
}
msg_state => bail!("Unexpected message state {msg_state}"),
_ => bail!("unexpected message state"),
}
context.emit_event(EventType::MsgsChanged {
chat_id: msg.chat_id,
@@ -4382,10 +4377,7 @@ pub async fn add_device_msg_with_importance(
if let Some(last_msg_time) = context
.sql
.query_get_value(
"SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=?
HAVING COUNT(*) > 0",
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
(chat_id,),
)
.await?
@@ -4711,7 +4703,6 @@ mod tests {
use super::*;
use crate::chatlist::get_archived_cnt;
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::headerdef::HeaderDef;
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::{sync, TestContext, TestContextManager};
@@ -4867,37 +4858,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_only_one_draft_per_chat() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let msgs: Vec<message::Message> = (1..=1000)
.map(|i| {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(i.to_string());
msg
})
.collect();
let mut tasks = Vec::new();
for mut msg in msgs {
let ctx = t.clone();
let task = tokio::spawn(async move {
let ctx = ctx;
chat_id.set_draft(&ctx, Some(&mut msg)).await
});
tasks.push(task);
}
futures::future::join_all(tasks.into_iter()).await;
assert!(chat_id.get_draft(&t).await?.is_some());
chat_id.set_draft(&t, None).await?;
assert!(chat_id.get_draft(&t).await?.is_none());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -6309,10 +6269,11 @@ mod tests {
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
let sent_msg = alice.pop_sent_msg().await;
let msg = sent_msg.payload();
assert_eq!(msg.match_indices("Message-ID: <").count(), 2);
assert_eq!(msg.match_indices("References: <").count(), 1);
let msg = msg.replace("Message-ID: <", "Message-ID: <X.X");
assert_eq!(msg.match_indices("References: <").count(), 1);
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 2);
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 0);
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
@@ -6329,7 +6290,7 @@ mod tests {
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
let msg = sent_msg.payload();
let msg = msg.replace("Message-ID: <", "Message-ID: <X.X");
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
let msg = msg.replace("Chat-", "XXXX-");
assert_eq!(msg.match_indices("Chat-").count(), 0);
@@ -6879,29 +6840,8 @@ mod tests {
)
.await?;
let sent2 = alice.pop_sent_msg().await;
let resent_msg_id = sent1.sender_msg_id;
resend_msgs(&alice, &[resent_msg_id]).await?;
assert_eq!(
resent_msg_id.get_state(&alice).await?,
MessageState::OutPending
);
resend_msgs(&alice, &[resent_msg_id]).await?;
// Message can be re-sent multiple times.
assert_eq!(
resent_msg_id.get_state(&alice).await?,
MessageState::OutPending
);
alice.pop_sent_msg().await;
// There's still one more pending SMTP job.
assert_eq!(
resent_msg_id.get_state(&alice).await?,
MessageState::OutPending
);
resend_msgs(&alice, &[sent1.sender_msg_id]).await?;
let sent3 = alice.pop_sent_msg().await;
assert_eq!(
resent_msg_id.get_state(&alice).await?,
MessageState::OutDelivered
);
// Bob receives all messages
let bob = TestContext::new_bob().await;
@@ -7698,29 +7638,4 @@ mod tests {
Ok(())
}
/// Tests that info message is ignored when constructing `In-Reply-To`.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_info_not_referenced() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_received_message = tcm.send_recv_accept(alice, bob, "Hi!").await;
let bob_chat_id = bob_received_message.chat_id;
add_info_msg(bob, bob_chat_id, "Some info", create_smeared_timestamp(bob)).await?;
// Bob sends a message.
// This message should reference Alice's "Hi!" message and not the info message.
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
let mime_message = alice.parse_msg(&sent).await;
let in_reply_to = mime_message.get_header(HeaderDef::InReplyTo).unwrap();
assert_eq!(
in_reply_to,
format!("<{}>", bob_received_message.rfc724_mid)
);
Ok(())
}
}

View File

@@ -13,7 +13,7 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use tokio::fs;
use crate::blob::BlobObject;
use crate::constants;
use crate::constants::{self, DC_VERSION_STR};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
@@ -59,10 +59,7 @@ pub enum Config {
/// IMAP server security (e.g. TLS, STARTTLS).
MailSecurity,
/// How to check TLS certificates.
///
/// "IMAP" in the name is for compatibility,
/// this actually applies to both IMAP and SMTP connections.
/// How to check IMAP server TLS certificates.
ImapCertificateChecks,
/// SMTP server hostname.
@@ -80,9 +77,7 @@ pub enum Config {
/// SMTP server security (e.g. TLS, STARTTLS).
SendSecurity,
/// Deprecated option for backwards compatibilty.
///
/// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
/// How to check SMTP server TLS certificates.
SmtpCertificateChecks,
/// Whether to use OAuth 2.
@@ -91,44 +86,21 @@ pub enum Config {
/// Should not be extended in the future, create new config keys instead.
ServerFlags,
/// True if proxy is enabled.
///
/// Can be used to disable proxy without erasing known URLs.
ProxyEnabled,
/// Proxy URL.
///
/// Supported URLs schemes are `http://` (HTTP), `https://` (HTTPS),
/// `socks5://` (SOCKS5) and `ss://` (Shadowsocks).
///
/// May contain multiple URLs separated by newline, in which case the first one is used.
ProxyUrl,
/// True if SOCKS5 is enabled.
///
/// Can be used to disable SOCKS5 without erasing SOCKS5 configuration.
///
/// Deprecated in favor of `ProxyEnabled`.
Socks5Enabled,
/// SOCKS5 proxy server hostname or address.
///
/// Deprecated in favor of `ProxyUrl`.
Socks5Host,
/// SOCKS5 proxy server port.
///
/// Deprecated in favor of `ProxyUrl`.
Socks5Port,
/// SOCKS5 proxy server username.
///
/// Deprecated in favor of `ProxyUrl`.
Socks5User,
/// SOCKS5 proxy server password.
///
/// Deprecated in favor of `ProxyUrl`.
Socks5Password,
/// Own name to use in the `From:` field when sending messages.
@@ -159,8 +131,7 @@ pub enum Config {
#[strum(props(default = "0"))]
SentboxWatch,
/// True if chat messages should be moved to a separate folder. Auto-sent messages like sync
/// ones are moved there anyway.
/// True if chat messages should be moved to a separate folder.
#[strum(props(default = "1"))]
MvboxMove,
@@ -197,12 +168,12 @@ pub enum Config {
/// Timer in seconds after which the message is deleted from the
/// server.
///
/// 0 means messages are never deleted by Delta Chat.
/// Equals to 0 by default, which means the message is never
/// deleted.
///
/// Value 1 is treated as "delete at once": messages are deleted
/// immediately, without moving to DeltaChat folder.
///
/// Default is 1 for chatmail accounts before a backup export, 0 otherwise.
#[strum(props(default = "0"))]
DeleteServerAfter,
/// Timer in seconds after which the message is deleted from the
@@ -223,74 +194,45 @@ pub enum Config {
/// The primary email address. Also see `SecondaryAddrs`.
ConfiguredAddr,
/// List of configured IMAP servers as a JSON array.
ConfiguredImapServers,
/// Configured IMAP server hostname.
///
/// This is replaced by `configured_imap_servers` for new configurations.
ConfiguredMailServer,
/// Configured IMAP server port.
///
/// This is replaced by `configured_imap_servers` for new configurations.
ConfiguredMailPort,
/// Configured IMAP server security (e.g. TLS, STARTTLS).
///
/// This is replaced by `configured_imap_servers` for new configurations.
ConfiguredMailSecurity,
/// Configured IMAP server username.
///
/// This is set if user has configured username manually.
ConfiguredMailUser,
/// Configured IMAP server password.
ConfiguredMailPw,
/// Configured TLS certificate checks.
/// This option is saved on successful configuration
/// and should not be modified manually.
///
/// This actually applies to both IMAP and SMTP connections,
/// but has "IMAP" in the name for backwards compatibility.
/// Configured IMAP server port.
ConfiguredMailPort,
/// Configured IMAP server security (e.g. TLS, STARTTLS).
ConfiguredMailSecurity,
/// How to check IMAP server TLS certificates.
ConfiguredImapCertificateChecks,
/// List of configured SMTP servers as a JSON array.
ConfiguredSmtpServers,
/// Configured SMTP server hostname.
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendServer,
/// Configured SMTP server port.
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendPort,
/// Configured SMTP server security (e.g. TLS, STARTTLS).
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendSecurity,
/// Configured SMTP server username.
///
/// This is set if user has configured username manually.
ConfiguredSendUser,
/// Configured SMTP server password.
ConfiguredSendPw,
/// Deprecated, stored for backwards compatibility.
///
/// ConfiguredImapCertificateChecks is actually used.
/// Configured SMTP server port.
ConfiguredSendPort,
/// How to check SMTP server TLS certificates.
ConfiguredSmtpCertificateChecks,
/// Whether OAuth 2 is used with configured provider.
ConfiguredServerFlags,
/// Configured SMTP server security (e.g. TLS, STARTTLS).
ConfiguredSendSecurity,
/// Configured folder for incoming messages.
ConfiguredInboxFolder,
@@ -315,16 +257,9 @@ pub enum Config {
/// True if account is a chatmail account.
IsChatmail,
/// True if `IsChatmail` mustn't be autoconfigured. For tests.
FixIsChatmail,
/// True if account is muted.
IsMuted,
/// Optional tag as "Work", "Family".
/// Meant to help profile owner to differ between profiles with similar names.
PrivateTag,
/// All secondary self addresses separated by spaces
/// (`addr1@example.org addr2@example.org addr3@example.org`)
SecondaryAddrs,
@@ -447,6 +382,9 @@ impl Config {
/// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which
/// mustn't be controlled by other devices.
pub(crate) fn is_synced(&self) -> bool {
// NB: We don't restart IO from the synchronisation code, so `MvboxMove` isn't effective
// immediately if `ConfiguredMvboxFolder` is unset, but only after a reconnect (see
// `Imap::prepare()`).
matches!(
self,
Self::Displayname
@@ -460,21 +398,21 @@ impl Config {
/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
matches!(self, Config::OnlyFetchMvbox | Config::SentboxWatch)
matches!(
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
)
}
}
impl Context {
/// Returns true if configuration value is set in the db for the given key.
///
/// NB: Don't use this to check if the key is configured because this doesn't look into
/// environment. The proper use of this function is e.g. checking a key before setting it.
pub(crate) async fn config_exists(&self, key: Config) -> Result<bool> {
/// Returns true if configuration value is set for the given key.
pub async fn config_exists(&self, key: Config) -> Result<bool> {
Ok(self.sql.get_raw_config(key.as_ref()).await?.is_some())
}
/// Get a config key value. Returns `None` if no value is set.
pub(crate) async fn get_config_opt(&self, key: Config) -> Result<Option<String>> {
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
let env_key = format!("DELTACHAT_{}", key.as_ref().to_uppercase());
if let Ok(value) = env::var(env_key) {
return Ok(Some(value));
@@ -489,43 +427,24 @@ impl Context {
.into_owned()
})
}
Config::SysVersion => Some((*constants::DC_VERSION_STR).clone()),
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(key.as_ref()).await?,
};
Ok(value)
}
/// Get a config key value if set, or a default value. Returns `None` if no value exists.
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
let value = self.get_config_opt(key).await?;
if value.is_some() {
return Ok(value);
}
// Default values
let val = match key {
Config::ConfiguredInboxFolder => Some("INBOX"),
Config::DeleteServerAfter => match Box::pin(self.is_chatmail()).await? {
false => Some("0"),
true => Some("1"),
},
_ => key.get_str("default"),
};
Ok(val.map(|s| s.to_string()))
match key {
Config::ConfiguredInboxFolder => Ok(Some("INBOX".to_owned())),
_ => Ok(key.get_str("default").map(|s| s.to_string())),
}
}
/// Returns Some(T) if a value for the given key is set and was successfully parsed.
/// Returns None if could not parse.
pub(crate) async fn get_config_opt_parsed<T: FromStr>(&self, key: Config) -> Result<Option<T>> {
self.get_config_opt(key)
.await
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()))
}
/// Returns Some(T) if a value for the given key exists (incl. default value) and was
/// successfully parsed.
/// Returns Some(T) if a value for the given key exists and was successfully parsed.
/// Returns None if could not parse.
pub async fn get_config_parsed<T: FromStr>(&self, key: Config) -> Result<Option<T>> {
self.get_config(key)
@@ -553,28 +472,20 @@ impl Context {
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
}
/// Returns boolean configuration value (if set) for the given key.
pub(crate) async fn get_config_bool_opt(&self, key: Config) -> Result<Option<bool>> {
Ok(self
.get_config_opt_parsed::<i32>(key)
.await?
.map(|x| x != 0))
/// Returns boolean configuration value (if any) for the given key.
pub async fn get_config_bool_opt(&self, key: Config) -> Result<Option<bool>> {
Ok(self.get_config_parsed::<i32>(key).await?.map(|x| x != 0))
}
/// Returns boolean configuration value for the given key.
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
Ok(self
.get_config_parsed::<i32>(key)
.await?
.map(|x| x != 0)
.unwrap_or_default())
Ok(self.get_config_bool_opt(key).await?.unwrap_or_default())
}
/// Returns true if movebox ("DeltaChat" folder) should be watched.
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| self.get_config_bool(Config::OnlyFetchMvbox).await?
|| !self.get_config_bool(Config::IsChatmail).await?)
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
}
/// Returns true if sentbox ("Sent" folder) should be watched.
@@ -593,40 +504,16 @@ impl Context {
&& !self.get_config_bool(Config::Bot).await?)
}
/// Returns whether sync messages should be uploaded to the mvbox.
pub(crate) async fn should_move_sync_msgs(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| !self.get_config_bool(Config::IsChatmail).await?)
}
/// Returns whether MDNs should be requested.
pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
match self.get_config_bool_opt(Config::MdnsEnabled).await? {
Some(val) => Ok(val),
None => Ok(!self.get_config_bool(Config::Bot).await?),
}
}
/// Returns whether MDNs should be sent.
pub(crate) async fn should_send_mdns(&self) -> Result<bool> {
self.get_config_bool(Config::MdnsEnabled).await
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
/// at once, `Some(x)` means delete after `x` seconds.
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
let val = match self
.get_config_parsed::<i64>(Config::DeleteServerAfter)
.await?
.unwrap_or(0)
{
0 => None,
1 => Some(0),
x => Some(x),
};
Ok(val)
match self.get_config_int(Config::DeleteServerAfter).await? {
0 => Ok(None),
1 => Ok(Some(0)),
x => Ok(Some(i64::from(x))),
}
}
/// Gets the configured provider, as saved in the `configured_provider` value.
@@ -671,7 +558,6 @@ impl Context {
fn check_config(key: Config, value: Option<&str>) -> Result<()> {
match key {
Config::Socks5Enabled
| Config::ProxyEnabled
| Config::BccSelf
| Config::E2eeEnabled
| Config::MdnsEnabled
@@ -802,7 +688,7 @@ impl Context {
{
return Ok(());
}
self.scheduler.interrupt_inbox().await;
self.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -861,8 +747,6 @@ impl Context {
///
/// This should only be used by test code and during configure.
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
self.quota.write().await.take();
// add old primary address (if exists) to secondary addresses
let mut secondary_addrs = self.get_all_self_addrs().await?;
// never store a primary address also as a secondary
@@ -875,7 +759,7 @@ impl Context {
self.set_config_internal(Config::ConfiguredAddr, Some(primary_new))
.await?;
self.emit_event(EventType::ConnectivityChanged);
Ok(())
}
@@ -1069,23 +953,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdns_default_behaviour() -> Result<()> {
let t = &TestContext::new_alice().await;
assert!(t.should_request_mdns().await?);
assert!(t.should_send_mdns().await?);
assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
// The setting should be displayed correctly.
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
t.set_config_bool(Config::Bot, true).await?;
assert!(!t.should_request_mdns().await?);
assert!(t.should_send_mdns().await?);
assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync() -> Result<()> {
let alice0 = TestContext::new_alice().await;
@@ -1112,6 +979,7 @@ mod tests {
// Reset to default. Test that it's not synced because defaults may differ across client
// versions.
alice0.set_config(Config::MdnsEnabled, None).await?;
assert_eq!(alice0.get_config_bool(Config::MdnsEnabled).await?, true);
alice0.set_config_bool(Config::MdnsEnabled, false).await?;
sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
@@ -1187,7 +1055,7 @@ mod tests {
let status = "Synced via usual message";
alice0.set_config(Config::Selfstatus, Some(status)).await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_sync_msg().await;
alice0.pop_sent_msg().await;
let status1 = "Synced via sync message";
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
tcm.send_recv(alice0, alice1, "hi Alice!").await;
@@ -1211,7 +1079,7 @@ mod tests {
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_sync_msg().await;
alice0.pop_sent_msg().await;
let file = alice1.dir.path().join("avatar.jpg");
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
tokio::fs::write(&file, bytes).await?;

View File

@@ -11,9 +11,9 @@
mod auto_mozilla;
mod auto_outlook;
pub(crate) mod server_params;
mod server_params;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use anyhow::{bail, ensure, Context as _, Result};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use deltachat_contact_tools::EmailAddress;
@@ -25,16 +25,14 @@ use tokio::task;
use crate::config::{self, Config};
use crate::context::Context;
use crate::imap::Imap;
use crate::imap::{session::Session as ImapSession, Imap};
use crate::log::LogExt;
use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
};
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{Message, Viewtype};
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::socks::Socks5Config;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::time;
@@ -80,7 +78,10 @@ impl Context {
let res = self
.inner_configure()
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
.race(cancel_channel.recv().map(|_| {
progress!(self, 0);
Ok(())
}))
.await;
self.free_ongoing().await;
@@ -109,15 +110,20 @@ impl Context {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let param = EnteredLoginParam::load(self).await?;
let mut param = LoginParam::load_candidate_params(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
let configured_param_res = configure(self, &param).await;
// Reset our knowledge about whether the server is a chatmail server.
// We will update it when we connect to IMAP.
self.set_config_internal(Config::IsChatmail, None).await?;
let success = configure(self, &mut param).await;
self.set_config_internal(Config::NotifyAboutWrongPw, None)
.await?;
on_configure_completed(self, configured_param_res?, old_addr).await?;
on_configure_completed(self, param, old_addr).await?;
success?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
Ok(())
@@ -126,7 +132,7 @@ impl Context {
async fn on_configure_completed(
context: &Context,
param: ConfiguredLoginParam,
param: LoginParam,
old_addr: Option<String>,
) -> Result<()> {
if let Some(provider) = param.provider {
@@ -176,28 +182,21 @@ async fn on_configure_completed(
Ok(())
}
/// Retrieves data from autoconfig and provider database
/// to transform user-entered login parameters into complete configuration.
async fn get_configured_param(
ctx: &Context,
param: &EnteredLoginParam,
) -> Result<ConfiguredLoginParam> {
ensure!(!param.addr.is_empty(), "Missing email address.");
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 1);
ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password.");
let socks5_config = param.socks5_config.clone();
let socks5_enabled = socks5_config.is_some();
// SMTP password is an "advanced" setting. If unset, use the same password as for IMAP.
let smtp_password = if param.smtp.password.is_empty() {
param.imap.password.clone()
} else {
param.smtp.password.clone()
};
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
let proxy_config = param.proxy_config.clone();
let proxy_enabled = proxy_config.is_some();
// Step 1: Load the parameters and check email-address and password
let mut addr = param.addr.clone();
if param.oauth2 {
// Do oauth2 only if socks5 is disabled. As soon as we have a http library that can do
// socks5 requests, this can work with socks5 too. OAuth is always set either for both
// IMAP and SMTP or not at all.
if param.imap.oauth2 && !socks5_enabled {
// the used oauth2 addr may differ, check this.
// if get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
@@ -206,7 +205,7 @@ async fn get_configured_param(
.and_then(|e| e.parse().ok())
{
info!(ctx, "Authorized address is {}", oauth2_addr);
addr = oauth2_addr;
param.addr = oauth2_addr;
ctx.sql
.set_raw_config("addr", Some(param.addr.as_str()))
.await?;
@@ -217,10 +216,11 @@ async fn get_configured_param(
let parsed = EmailAddress::new(&param.addr).context("Bad email-address")?;
let param_domain = parsed.domain;
let param_addr_urlencoded = utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
// Step 2: Autoconfig
progress!(ctx, 200);
let provider;
let param_autoconfig;
if param.imap.server.is_empty()
&& param.imap.port == 0
@@ -232,48 +232,66 @@ async fn get_configured_param(
&& param.smtp.user.is_empty()
{
// no advanced parameters entered by the user: query provider-database or do Autoconfig
info!(
ctx,
"checking internal provider-info for offline autoconfig"
);
provider = provider::get_provider_info(ctx, &param_domain, proxy_enabled).await;
if let Some(provider) = provider {
if provider.server.is_empty() {
info!(ctx, "Offline autoconfig found, but no servers defined.");
param_autoconfig = None;
} else {
info!(ctx, "Offline autoconfig found.");
let servers = provider
.server
.iter()
.map(|s| ServerParams {
protocol: s.protocol,
socket: s.socket,
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::Email => param.addr.to_string(),
UsernamePattern::Emaillocalpart => {
if let Some(at) = param.addr.find('@') {
param.addr.split_at(at).0.to_string()
} else {
param.addr.to_string()
}
}
},
})
.collect();
if let Some(provider) =
provider::get_provider_info(ctx, &param_domain, socks5_enabled).await
{
param.provider = Some(provider);
match provider.status {
provider::Status::Ok | provider::Status::Preparation => {
if provider.server.is_empty() {
info!(ctx, "offline autoconfig found, but no servers defined");
param_autoconfig = None;
} else {
info!(ctx, "offline autoconfig found");
let servers = provider
.server
.iter()
.map(|s| ServerParams {
protocol: s.protocol,
socket: s.socket,
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::Email => param.addr.to_string(),
UsernamePattern::Emaillocalpart => {
if let Some(at) = param.addr.find('@') {
param.addr.split_at(at).0.to_string()
} else {
param.addr.to_string()
}
}
},
strict_tls: Some(provider.opt.strict_tls),
})
.collect();
param_autoconfig = Some(servers)
param_autoconfig = Some(servers)
}
}
provider::Status::Broken => {
info!(ctx, "offline autoconfig found, provider is broken");
param_autoconfig = None;
}
}
} else {
// Try receiving autoconfig
info!(ctx, "No offline autoconfig found.");
param_autoconfig = get_autoconfig(ctx, param, &param_domain).await;
info!(ctx, "no offline autoconfig found");
param_autoconfig = if socks5_enabled {
// Currently we can't do http requests through socks5, to not leak
// the ip, just don't do online autoconfig
info!(ctx, "socks5 enabled, skipping autoconfig");
None
} else {
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await
}
}
} else {
provider = None;
param_autoconfig = None;
}
@@ -290,6 +308,7 @@ async fn get_configured_param(
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
strict_tls: None,
})
}
if !servers
@@ -302,149 +321,145 @@ async fn get_configured_param(
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
strict_tls: None,
})
}
// respect certificate setting from function parameters
for server in &mut servers {
let certificate_checks = match server.protocol {
Protocol::Imap => param.imap.certificate_checks,
Protocol::Smtp => param.smtp.certificate_checks,
};
server.strict_tls = match certificate_checks {
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
CertificateChecks::Strict => Some(true),
CertificateChecks::Automatic => server.strict_tls,
};
}
let servers = expand_param_vector(servers, &param.addr, &param_domain);
let configured_login_param = ConfiguredLoginParam {
addr,
imap: servers
.iter()
.filter_map(|params| {
let Ok(security) = params.socket.try_into() else {
return None;
};
if params.protocol == Protocol::Imap {
Some(ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: params.hostname.clone(),
port: params.port,
security,
},
user: params.username.clone(),
})
} else {
None
}
})
.collect(),
imap_user: param.imap.user.clone(),
imap_password: param.imap.password.clone(),
smtp: servers
.iter()
.filter_map(|params| {
let Ok(security) = params.socket.try_into() else {
return None;
};
if params.protocol == Protocol::Smtp {
Some(ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: params.hostname.clone(),
port: params.port,
security,
},
user: params.username.clone(),
})
} else {
None
}
})
.collect(),
smtp_user: param.smtp.user.clone(),
smtp_password,
proxy_config: param.proxy_config.clone(),
provider,
certificate_checks: match param.certificate_checks {
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
EnteredCertificateChecks::AcceptInvalidCertificates
| EnteredCertificateChecks::AcceptInvalidCertificates2 => {
ConfiguredCertificateChecks::AcceptInvalidCertificates
}
},
oauth2: param.oauth2,
};
Ok(configured_login_param)
}
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<ConfiguredLoginParam> {
progress!(ctx, 1);
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
let configured_param = get_configured_param(ctx, param).await?;
let strict_tls = configured_param.strict_tls();
progress!(ctx, 550);
// Spawn SMTP configuration task
// to try SMTP while connecting to IMAP.
let mut smtp = Smtp::new();
let context_smtp = ctx.clone();
let smtp_param = configured_param.smtp.clone();
let smtp_password = configured_param.smtp_password.clone();
let smtp_addr = configured_param.addr.clone();
let proxy_config = configured_param.proxy_config.clone();
let mut smtp_param = param.smtp.clone();
let smtp_addr = param.addr.clone();
let smtp_servers: Vec<ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Smtp)
.cloned()
.collect();
let provider_strict_tls = param
.provider
.map_or(socks5_config.is_some(), |provider| provider.opt.strict_tls);
let smtp_config_task = task::spawn(async move {
let mut smtp = Smtp::new();
smtp.connect(
&context_smtp,
&smtp_param,
&smtp_password,
&proxy_config,
&smtp_addr,
strict_tls,
configured_param.oauth2,
)
.await?;
let mut smtp_configured = false;
let mut errors = Vec::new();
for smtp_server in smtp_servers {
smtp_param.user.clone_from(&smtp_server.username);
smtp_param.server.clone_from(&smtp_server.hostname);
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
smtp_param.certificate_checks = match smtp_server.strict_tls {
Some(true) => CertificateChecks::Strict,
Some(false) => CertificateChecks::AcceptInvalidCertificates,
None => CertificateChecks::Automatic,
};
Ok::<(), anyhow::Error>(())
match try_smtp_one_param(
&context_smtp,
&smtp_param,
&socks5_config,
&smtp_addr,
provider_strict_tls,
&mut smtp,
)
.await
{
Ok(_) => {
smtp_configured = true;
break;
}
Err(e) => errors.push(e),
}
}
if smtp_configured {
Ok(smtp_param)
} else {
Err(errors)
}
});
progress!(ctx, 600);
// Configure IMAP
let (_s, r) = async_channel::bounded(1);
let mut imap = Imap::new(
configured_param.imap.clone(),
configured_param.imap_password.clone(),
configured_param.proxy_config.clone(),
&configured_param.addr,
strict_tls,
configured_param.oauth2,
r,
);
let mut imap_session = match imap.connect(ctx).await {
Ok(session) => session,
Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await),
let mut imap: Option<(Imap, ImapSession)> = None;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Imap)
.collect();
let imap_servers_count = imap_servers.len();
let mut errors = Vec::new();
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
param.imap.user.clone_from(&imap_server.username);
param.imap.server.clone_from(&imap_server.hostname);
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
param.imap.certificate_checks = match imap_server.strict_tls {
Some(true) => CertificateChecks::Strict,
Some(false) => CertificateChecks::AcceptInvalidCertificates,
None => CertificateChecks::Automatic,
};
match try_imap_one_param(
ctx,
&param.imap,
&param.socks5_config,
&param.addr,
provider_strict_tls,
)
.await
{
Ok(configured_imap) => {
imap = Some(configured_imap);
break;
}
Err(e) => errors.push(e),
}
progress!(
ctx,
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
let (mut imap, mut imap_session) = match imap {
Some(imap) => imap,
None => bail!(nicer_configuration_error(ctx, errors).await),
};
progress!(ctx, 850);
// Wait for SMTP configuration
smtp_config_task.await.unwrap()?;
match smtp_config_task.await.unwrap() {
Ok(smtp_param) => {
param.smtp = smtp_param;
}
Err(errors) => {
bail!(nicer_configuration_error(ctx, errors).await);
}
}
progress!(ctx, 900);
let is_chatmail = match ctx.get_config_bool(Config::FixIsChatmail).await? {
false => {
let is_chatmail = imap_session.is_chatmail();
ctx.set_config(
Config::IsChatmail,
Some(match is_chatmail {
false => "0",
true => "1",
}),
)
.await?;
is_chatmail
}
true => ctx.get_config_bool(Config::IsChatmail).await?,
};
if is_chatmail {
if imap_session.is_chatmail() {
ctx.set_config(Config::IsChatmail, Some("1")).await?;
ctx.set_config(Config::SentboxWatch, None).await?;
ctx.set_config(Config::MvboxMove, Some("0")).await?;
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
@@ -452,7 +467,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
ctx.set_config(Config::E2eeEnabled, Some("1")).await?;
}
let create_mvbox = !is_chatmail;
let create_mvbox = ctx.should_watch_mvbox().await?;
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
@@ -473,7 +489,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
}
}
configured_param.save_as_configured_params(ctx).await?;
// the trailing underscore is correct
param.save_as_configured_params(ctx).await?;
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
@@ -491,7 +508,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
ctx.sql.set_raw_config_bool("configured", true).await?;
Ok(configured_param)
Ok(())
}
/// Retrieve available autoconfigurations.
@@ -500,17 +517,16 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
/// B. If we have no configuration yet, search configuration in Thunderbird's central database
async fn get_autoconfig(
ctx: &Context,
param: &EnteredLoginParam,
param: &LoginParam,
param_domain: &str,
param_addr_urlencoded: &str,
) -> Option<Vec<ServerParams>> {
let param_addr_urlencoded = utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
if let Ok(res) = moz_autoconfigure(
ctx,
&format!(
"https://autoconfig.{param_domain}/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
),
&param.addr,
param,
)
.await
{
@@ -525,7 +541,7 @@ async fn get_autoconfig(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
&param_domain, &param_addr_urlencoded
),
&param.addr,
param,
)
.await
{
@@ -561,7 +577,7 @@ async fn get_autoconfig(
if let Ok(res) = moz_autoconfigure(
ctx,
&format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain),
&param.addr,
param,
)
.await
{
@@ -571,19 +587,140 @@ async fn get_autoconfig(
None
}
async fn nicer_configuration_error(context: &Context, e: String) -> String {
if e.to_lowercase().contains("could not resolve")
|| e.to_lowercase().contains("connection attempts")
|| e.to_lowercase()
.contains("temporary failure in name resolution")
|| e.to_lowercase().contains("name or service not known")
|| e.to_lowercase()
.contains("failed to lookup address information")
async fn try_imap_one_param(
context: &Context,
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
provider_strict_tls: bool,
) -> Result<(Imap, ImapSession), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
param.user,
param.server,
param.port,
param.security,
param.certificate_checks,
param.oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
} else {
"None".to_string()
}
);
info!(context, "Trying: {}", inf);
let (_s, r) = async_channel::bounded(1);
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
Err(err) => {
info!(context, "failure: {:#}", err);
return Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
});
}
Ok(imap) => imap,
};
match imap.connect(context).await {
Err(err) => {
info!(context, "IMAP failure: {err:#}.");
Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
})
}
Ok(session) => {
info!(context, "IMAP success: {inf}.");
Ok((imap, session))
}
}
}
async fn try_smtp_one_param(
context: &Context,
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
provider_strict_tls: bool,
smtp: &mut Smtp,
) -> Result<(), ConfigurationError> {
let inf = format!(
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
param.user,
param.server,
param.port,
param.security,
param.certificate_checks,
param.oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
} else {
"None".to_string()
}
);
info!(context, "Trying: {}", inf);
if let Err(err) = smtp
.connect(context, param, socks5_config, addr, provider_strict_tls)
.await
{
info!(context, "SMTP failure: {err:#}.");
Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
})
} else {
info!(context, "SMTP success: {inf}.");
smtp.disconnect();
Ok(())
}
}
/// Failure to connect and login with email client configuration.
#[derive(Debug, thiserror::Error)]
#[error("Trying {config}…\nError: {msg}")]
pub struct ConfigurationError {
/// Tried configuration description.
config: String,
/// Error message.
msg: String,
}
async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationError>) -> String {
let first_err = if let Some(f) = errors.first() {
f
} else {
// This means configuration failed but no errors have been captured. This should never
// happen, but if it does, the user will see classic "Error: no error".
return "no error".to_string();
};
if errors.iter().all(|e| {
e.msg.to_lowercase().contains("could not resolve")
|| e.msg.to_lowercase().contains("no dns resolution results")
|| e.msg
.to_lowercase()
.contains("temporary failure in name resolution")
|| e.msg.to_lowercase().contains("name or service not known")
|| e.msg
.to_lowercase()
.contains("failed to lookup address information")
}) {
return stock_str::error_no_network(context).await;
}
e
if errors.iter().all(|e| e.msg == first_err.msg) {
return first_err.msg.to_string();
}
errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join("\n\n")
}
#[derive(Debug, thiserror::Error)]
@@ -609,9 +746,7 @@ pub enum Error {
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::login_param::EnteredServerLoginParam;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -623,24 +758,4 @@ mod tests {
t.set_config(Config::MailPw, Some("123456")).await.unwrap();
assert!(t.configure().await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_configured_param() -> Result<()> {
let t = &TestContext::new().await;
let entered_param = EnteredLoginParam {
addr: "alice@example.org".to_string(),
imap: EnteredServerLoginParam {
user: "alice@example.net".to_string(),
password: "foobar".to_string(),
..Default::default()
},
..Default::default()
};
let configured_param = get_configured_param(t, &entered_param).await?;
assert_eq!(configured_param.imap_user, "alice@example.net");
assert_eq!(configured_param.smtp_user, "");
Ok(())
}
}

View File

@@ -9,6 +9,7 @@ use quick_xml::events::{BytesStart, Event};
use super::{Error, ServerParams};
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::net::read_url;
use crate::provider::{Protocol, Socket};
@@ -247,6 +248,7 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
hostname: server.hostname,
port: server.port,
username: server.username,
strict_tls: None,
})
})
.collect();
@@ -256,11 +258,11 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
pub(crate) async fn moz_autoconfigure(
context: &Context,
url: &str,
addr: &str,
param_in: &LoginParam,
) -> Result<Vec<ServerParams>, Error> {
let xml_raw = read_url(context, url).await?;
let res = parse_serverparams(addr, &xml_raw);
let res = parse_serverparams(&param_in.addr, &xml_raw);
if let Err(err) = &res {
warn!(
context,

View File

@@ -187,6 +187,7 @@ fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
hostname: protocol.server,
port: protocol.port,
username: String::new(),
strict_tls: None,
})
})
.collect()

View File

@@ -22,18 +22,31 @@ pub(crate) struct ServerParams {
/// Username, empty if unknown.
pub username: String,
/// Whether TLS certificates should be strictly checked or not, `None` for automatic.
pub strict_tls: Option<bool>,
}
impl ServerParams {
fn expand_usernames(self, addr: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.username.is_empty() {
vec![Self {
res.push(Self {
username: addr.to_string(),
..self.clone()
}]
});
if let Some(at) = addr.find('@') {
res.push(Self {
username: addr.split_at(at).0.to_string(),
..self
});
}
} else {
vec![self]
res.push(self)
}
res
}
fn expand_hostnames(self, param_domain: &str) -> Vec<ServerParams> {
@@ -122,6 +135,14 @@ impl ServerParams {
vec![self]
}
}
fn expand_strict_tls(self) -> Vec<ServerParams> {
vec![Self {
// Strict if not set by the user or provider database.
strict_tls: Some(self.strict_tls.unwrap_or(true)),
..self
}]
}
}
/// Expands vector of `ServerParams`, replacing placeholders with
@@ -134,7 +155,9 @@ pub(crate) fn expand_param_vector(
v.into_iter()
// The order of expansion is important.
//
// Ports are expanded the last, so they are changed the first.
// Ports are expanded the last, so they are changed the first. Username is only changed if
// default value (address with domain) didn't work for all available hosts and ports.
.flat_map(|params| params.expand_strict_tls().into_iter())
.flat_map(|params| params.expand_usernames(addr).into_iter())
.flat_map(|params| params.expand_hostnames(domain).into_iter())
.flat_map(|params| params.expand_ports().into_iter())
@@ -154,6 +177,7 @@ mod tests {
port: 0,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -167,6 +191,7 @@ mod tests {
port: 993,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
}],
);
@@ -177,6 +202,7 @@ mod tests {
port: 123,
socket: Socket::Automatic,
username: "foobar".to_string(),
strict_tls: None,
}],
"foobar@example.net",
"example.net",
@@ -191,6 +217,7 @@ mod tests {
port: 123,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
},
ServerParams {
protocol: Protocol::Smtp,
@@ -198,10 +225,12 @@ mod tests {
port: 123,
socket: Socket::Starttls,
username: "foobar".to_string(),
strict_tls: Some(true)
},
],
);
// Test that strict_tls is not expanded for plaintext connections.
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Smtp,
@@ -209,6 +238,7 @@ mod tests {
port: 123,
socket: Socket::Plain,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -221,6 +251,7 @@ mod tests {
port: 123,
socket: Socket::Plain,
username: "foobar".to_string(),
strict_tls: Some(true)
}],
);
@@ -232,6 +263,7 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -245,6 +277,7 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Imap,
@@ -252,6 +285,7 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Imap,
@@ -259,6 +293,7 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
}
],
);
@@ -272,6 +307,7 @@ mod tests {
port: 0,
socket: Socket::Automatic,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -285,6 +321,7 @@ mod tests {
port: 465,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Smtp,
@@ -292,45 +329,7 @@ mod tests {
port: 587,
socket: Socket::Starttls,
username: "foobar".to_string(),
},
],
);
// Test that email address is used as the default username.
// We do not try other usernames
// such as the local part of the address
// as this is very uncommon configuration
// and not worth doubling the number of candidates to try.
// If such configuration is used, email provider
// should provide XML autoconfig or
// be added to the provider database as an exception.
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 0,
socket: Socket::Automatic,
username: "".to_string(),
}],
"foobar@example.net",
"example.net",
);
assert_eq!(
v,
vec![
ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 993,
socket: Socket::Ssl,
username: "foobar@example.net".to_string(),
},
ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 143,
socket: Socket::Starttls,
username: "foobar@example.net".to_string(),
strict_tls: Some(true)
},
],
);

View File

@@ -179,9 +179,7 @@ pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEX
// and may be set together with the username, password etc.
// via dc_set_config() using the key "server_flags".
/// Force OAuth2 authorization.
///
/// This flag does not skip automatic configuration.
/// Force OAuth2 authorization. This flag does not skip automatic configuration.
/// Before calling configure() with DC_LP_AUTH_OAUTH2 set,
/// the user has to confirm access at the URL returned by dc_get_oauth2_url().
pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
@@ -211,7 +209,7 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
// Key for the folder configuration version (see below).
pub(crate) const DC_FOLDERS_CONFIGURED_KEY: &str = "folders_configured";
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 5;
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting

View File

@@ -30,6 +30,7 @@ use crate::context::Context;
use crate::events::EventType;
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
use crate::log::LogExt;
use crate::login_param::LoginParam;
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
@@ -1190,10 +1191,7 @@ impl Contact {
);
let contact = Contact::get_by_id(context, contact_id).await?;
let addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let loginparam = LoginParam::load_configured_params(context).await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
@@ -1222,8 +1220,8 @@ impl Contact {
.peek_key(false)
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
if addr < peerstate.addr {
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
if loginparam.addr < peerstate.addr {
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
cat_fingerprint(
&mut ret,
&peerstate.addr,
@@ -1237,7 +1235,7 @@ impl Contact {
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
}
Ok(ret)
@@ -1404,17 +1402,6 @@ impl Contact {
self.status.as_str()
}
/// Returns whether end-to-end encryption to the contact is available.
pub async fn e2ee_avail(&self, context: &Context) -> Result<bool> {
if self.id == ContactId::SELF {
return Ok(true);
}
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
return Ok(false);
};
Ok(peerstate.peek_key(false).is_some())
}
/// Returns true if the contact
/// can be added to verified chats,
/// i.e. has a verified key
@@ -1926,13 +1913,8 @@ impl RecentlySeenLoop {
.unwrap();
}
pub(crate) async fn abort(self) {
pub(crate) fn abort(self) {
self.handle.abort();
// Await aborted task to ensure the `Future` is dropped
// with all resources moved inside such as the `Context`
// reference to `InnerContext`.
self.handle.await.ok();
}
}
@@ -2686,8 +2668,6 @@ mod tests {
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
assert_eq!(encrinfo, "No encryption");
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
assert!(!contact.e2ee_avail(&alice).await?);
let bob = TestContext::new_bob().await;
let chat_alice = bob
@@ -2711,8 +2691,6 @@ bob@example.net:
CCCB 5AA9 F6E1 141C 9431
65F1 DB18 B18C BCF7 0487"
);
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
assert!(contact.e2ee_avail(&alice).await?);
Ok(())
}
@@ -2890,7 +2868,7 @@ Hi."#;
bob.recv_msg(&sent_msg).await;
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
let green = nu_ansi_term::Color::Green.normal();
let green = ansi_term::Color::Green.normal();
assert!(
contact.was_seen_recently(),
"{}",

View File

@@ -27,7 +27,7 @@ use crate::download::DownloadState;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::login_param::LoginParam;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
@@ -515,11 +515,8 @@ impl Context {
Ok(val)
}
/// Does a single round of fetching from IMAP and returns.
///
/// Can be used even if I/O is currently stopped.
/// If I/O is currently stopped, starts a new IMAP connection
/// and fetches from Inbox and DeltaChat folders.
/// Does a background fetch
/// pauses the scheduler and does one imap fetch, then unpauses and returns
pub async fn background_fetch(&self) -> Result<()> {
if !(self.is_configured().await?) {
return Ok(());
@@ -527,63 +524,35 @@ impl Context {
let address = self.get_primary_self_addr().await?;
let time_start = tools::Time::now();
info!(self, "background_fetch started fetching {address}.");
info!(self, "background_fetch started fetching {address}");
if self.scheduler.is_running().await {
self.scheduler.maybe_network().await;
let _pause_guard = self.scheduler.pause(self.clone()).await?;
// Wait until fetching is finished.
// Ideally we could wait for connectivity change events,
// but sleep loop is good enough.
// connection
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
let mut session = connection.prepare(self).await?;
// First 100 ms sleep in chunks of 10 ms.
for _ in 0..10 {
if self.all_work_done().await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
// fetch imap folders
for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] {
let (_, watch_folder) = convert_folder_meaning(self, folder_meaning).await?;
connection
.fetch_move_delete(self, &mut session, &watch_folder, folder_meaning)
.await?;
}
// If we are not finished in 100 ms, keep waking up every 100 ms.
while !self.all_work_done().await {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
} else {
// Pause the scheduler to ensure another connection does not start
// while we are fetching on a dedicated connection.
let _pause_guard = self.scheduler.pause(self.clone()).await?;
// Start a new dedicated connection.
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
let mut session = connection.prepare(self).await?;
// Fetch IMAP folders.
// Inbox is fetched before Mvbox because fetching from Inbox
// may result in moving some messages to Mvbox.
for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] {
if let Some((_folder_config, watch_folder)) =
convert_folder_meaning(self, folder_meaning).await?
{
connection
.fetch_move_delete(self, &mut session, &watch_folder, folder_meaning)
.await?;
}
}
// Update quota (to send warning if full) - but only check it once in a while.
if self
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
.await
{
if let Err(err) = self.update_recent_quota(&mut session).await {
warn!(self, "Failed to update quota: {err:#}.");
}
// update quota (to send warning if full) - but only check it once in a while
if self
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
.await
{
if let Err(err) = self.update_recent_quota(&mut session).await {
warn!(self, "Failed to update quota: {err:#}.");
}
}
info!(
self,
"background_fetch done for {address} took {:?}.",
"background_fetch done for {address} took {:?}",
time_elapsed(&time_start),
);
@@ -746,10 +715,8 @@ impl Context {
/// Returns information about the context as key-value pairs.
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let unset = "0";
let l = EnteredLoginParam::load(self).await?;
let l2 = ConfiguredLoginParam::load(self)
.await?
.map_or_else(|| "Not configured".to_string(), |param| param.to_string());
let l = LoginParam::load_candidate_params_unchecked(self).await?;
let l2 = LoginParam::load_configured_params(self).await?;
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
let displayname = self.get_config(Config::Displayname).await?;
let chats = get_chat_cnt(self).await?;
@@ -757,7 +724,7 @@ impl Context {
let request_msgs = message::get_request_msg_cnt(self).await;
let contacts = Contact::get_real_cnt(self).await?;
let is_configured = self.get_config_int(Config::Configured).await?;
let proxy_enabled = self.get_config_int(Config::ProxyEnabled).await?;
let socks5_enabled = self.get_config_int(Config::Socks5Enabled).await?;
let dbversion = self
.sql
.get_raw_config_int("dbversion")
@@ -838,31 +805,19 @@ impl Context {
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
res.insert("proxy_enabled", proxy_enabled.to_string());
res.insert("socks5_enabled", socks5_enabled.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2);
res.insert("used_account_settings", l2.to_string());
if let Some(server_id) = &*self.server_id.read().await {
res.insert("imap_server_id", format!("{server_id:?}"));
}
res.insert("is_chatmail", self.is_chatmail().await?.to_string());
res.insert(
"fix_is_chatmail",
self.get_config_bool(Config::FixIsChatmail)
.await?
.to_string(),
);
res.insert(
"is_muted",
self.get_config_bool(Config::IsMuted).await?.to_string(),
);
res.insert(
"private_tag",
self.get_config(Config::PrivateTag)
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
if let Some(metadata) = &*self.metadata.read().await {
if let Some(comment) = &metadata.comment {
@@ -1304,12 +1259,6 @@ impl Context {
///
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
/// is `None` this searches messages from all chats.
///
/// NB: Wrt the search in long messages which are shown truncated with the "Show Full Message…"
/// button, we only look at the first several kilobytes. Let's not fix this -- one can send a
/// dictionary in the message that matches any reasonable search request, but the user won't see
/// the match because they should tap on "Show Full Message…" for that. Probably such messages
/// would only clutter search results.
pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: &str) -> Result<Vec<MsgId>> {
let real_query = query.trim().to_lowercase();
if real_query.is_empty() {
@@ -1736,8 +1685,6 @@ mod tests {
"server_flags",
"skip_start_messages",
"smtp_certificate_checks",
"proxy_url", // May contain passwords, don't leak it to the logs.
"socks5_enabled", // SOCKS5 options are deprecated.
"socks5_host",
"socks5_port",
"socks5_user",

View File

@@ -313,7 +313,7 @@ pub(crate) async fn get_autocrypt_peerstate(
if let Some(ref mut peerstate) = peerstate {
if addr_cmp(&peerstate.addr, from) {
if allow_change {
peerstate.apply_header(context, header, message_time);
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql).await?;
} else {
info!(

View File

@@ -98,26 +98,19 @@ impl MsgId {
Ok(())
}
/// Updates the message download state. Returns `Ok` if the message doesn't exist anymore.
pub(crate) async fn update_download_state(
self,
context: &Context,
download_state: DownloadState,
) -> Result<()> {
if context
let msg = Message::load_from_db(context, self).await?;
context
.sql
.execute(
"UPDATE msgs SET download_state=? WHERE id=?;",
(download_state, self),
)
.await?
== 0
{
return Ok(());
}
let Some(msg) = Message::load_from_db_optional(context, self).await? else {
return Ok(());
};
.await?;
context.emit_event(EventType::MsgsChanged {
chat_id: msg.chat_id,
msg_id: self,
@@ -142,17 +135,7 @@ pub(crate) async fn download_msg(
msg_id: MsgId,
session: &mut Session,
) -> Result<()> {
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
// If partially downloaded message was already deleted
// we do not know its Message-ID anymore
// so cannot download it.
//
// Probably the message expired due to `delete_device_after`
// setting or was otherwise removed from the device,
// so we don't want it to reappear anyway.
return Ok(());
};
let msg = Message::load_from_db(context, msg_id).await?;
let row = context
.sql
.query_row_optional(
@@ -329,19 +312,11 @@ mod tests {
DownloadState::InProgress,
DownloadState::Failure,
DownloadState::Done,
DownloadState::Done,
] {
msg_id.update_download_state(&t, *s).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.download_state(), *s);
}
t.sql
.execute("DELETE FROM msgs WHERE id=?", (msg_id,))
.await?;
// Nothing to do is ok.
msg_id
.update_download_state(&t, DownloadState::Done)
.await?;
Ok(())
}

View File

@@ -69,7 +69,7 @@ use std::num::ParseIntError;
use std::str::FromStr;
use std::time::{Duration, UNIX_EPOCH};
use anyhow::{ensure, Context as _, Result};
use anyhow::{ensure, Result};
use async_channel::Receiver;
use serde::{Deserialize, Serialize};
use tokio::time::timeout;
@@ -176,13 +176,9 @@ impl ChatId {
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
let timer = context
.sql
.query_get_value(
"SELECT IFNULL(ephemeral_timer, 0) FROM chats WHERE id=?",
(self,),
)
.await?
.with_context(|| format!("Chat {self} not found"))?;
Ok(timer)
.query_get_value("SELECT ephemeral_timer FROM chats WHERE id=?;", (self,))
.await?;
Ok(timer.unwrap_or_default())
}
/// Set ephemeral timer value without sending a message.
@@ -513,8 +509,7 @@ async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<
FROM msgs
WHERE chat_id > ?
AND chat_id != ?
AND chat_id != ?
HAVING count(*) > 0
AND chat_id != ?;
"#,
(DC_CHAT_ID_TRASH, self_chat_id, device_chat_id),
)
@@ -538,8 +533,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
SELECT min(ephemeral_timestamp)
FROM msgs
WHERE ephemeral_timestamp != 0
AND chat_id != ?
HAVING count(*) > 0
AND chat_id != ?;
"#,
(DC_CHAT_ID_TRASH,), // Trash contains already deleted messages, skip them
)
@@ -1416,14 +1410,4 @@ mod tests {
Ok(())
}
/// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> {
let context = TestContext::new().await;
let chat_id = ChatId::new(12345);
assert!(chat_id.get_ephemeral_timer(&context).await.is_err());
Ok(())
}
}

View File

@@ -8,7 +8,6 @@ use crate::config::Config;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::reaction::Reaction;
use crate::webxdc::StatusUpdateSerial;
/// Event payload.
@@ -95,18 +94,6 @@ pub enum EventType {
contact_id: ContactId,
},
/// Reactions for the message changed.
IncomingReaction {
/// ID of the contact whose reaction set is changed.
contact_id: ContactId,
/// ID of the message for which reactions were changed.
msg_id: MsgId,
/// The reaction.
reaction: Reaction,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -301,13 +288,6 @@ pub enum EventType {
data: Vec<u8>,
},
/// Advertisement received over an ephemeral peer channel.
/// This can be used by bots to initiate peer-to-peer communication from their side.
WebxdcRealtimeAdvertisementReceived {
/// Message ID of the webxdc instance.
msg_id: MsgId,
},
/// Inform that a message containing a webxdc instance has been deleted.
WebxdcInstanceDeleted {
/// ID of the deleted message.

View File

@@ -11,7 +11,6 @@ pub enum HeaderDef {
Date,
From_,
To,
AutoSubmitted,
/// Carbon copy.
Cc,

View File

@@ -32,19 +32,15 @@ use crate::contact::{Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::log::LogExt;
use crate::login_param::{
prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam,
};
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::mimeparser;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
use crate::oauth2::get_oauth2_access_token;
use crate::receive_imf::{
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
};
use crate::scheduler::connectivity::ConnectivityStore;
use crate::socks::Socks5Config;
use crate::sql;
use crate::stock_str;
use crate::tools::{self, create_id, duration_to_str};
@@ -56,7 +52,7 @@ pub mod scan_folders;
pub mod select_folder;
pub(crate) mod session;
use client::{determine_capabilities, Client};
use client::Client;
use mailparse::SingleInfo;
use session::Session;
@@ -77,18 +73,12 @@ pub(crate) struct Imap {
addr: String,
/// Login parameters.
lp: Vec<ConfiguredServerLoginParam>,
/// Password.
password: String,
/// Proxy configuration.
proxy_config: Option<ProxyConfig>,
lp: ServerLoginParam,
/// SOCKS 5 configuration.
socks5_config: Option<Socks5Config>,
strict_tls: bool,
oauth2: bool,
login_failed_once: bool,
pub(crate) connectivity: ConnectivityStore,
@@ -238,29 +228,38 @@ impl Imap {
///
/// `addr` is used to renew token if OAuth2 authentication is used.
pub fn new(
lp: Vec<ConfiguredServerLoginParam>,
password: String,
proxy_config: Option<ProxyConfig>,
lp: &ServerLoginParam,
socks5_config: Option<Socks5Config>,
addr: &str,
strict_tls: bool,
oauth2: bool,
provider_strict_tls: bool,
idle_interrupt_receiver: Receiver<()>,
) -> Self {
Imap {
) -> Result<Self> {
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
bail!("Incomplete IMAP connection parameters");
}
let strict_tls = match lp.certificate_checks {
CertificateChecks::Automatic => provider_strict_tls,
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
let imap = Imap {
idle_interrupt_receiver,
addr: addr.to_string(),
lp,
password,
proxy_config,
lp: lp.clone(),
socks5_config,
strict_tls,
oauth2,
login_failed_once: false,
connectivity: Default::default(),
conn_last_try: UNIX_EPOCH,
conn_backoff_ms: 0,
// 1 connection per minute + a burst of 2.
ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
}
};
Ok(imap)
}
/// Creates new disconnected IMAP client using configured parameters.
@@ -268,18 +267,24 @@ impl Imap {
context: &Context,
idle_interrupt_receiver: Receiver<()>,
) -> Result<Self> {
let param = ConfiguredLoginParam::load(context)
.await?
.context("Not configured")?;
if !context.is_configured().await? {
bail!("IMAP Connect without configured params");
}
let param = LoginParam::load_configured_params(context).await?;
// the trailing underscore is correct
let imap = Self::new(
param.imap.clone(),
param.imap_password.clone(),
param.proxy_config.clone(),
&param.imap,
param.socks5_config.clone(),
&param.addr,
param.strict_tls(),
param.oauth2,
param
.provider
.map_or(param.socks5_config.is_some(), |provider| {
provider.opt.strict_tls
}),
idle_interrupt_receiver,
);
)?;
Ok(imap)
}
@@ -291,6 +296,10 @@ impl Imap {
/// instead if you are going to actually use connection rather than trying connection
/// parameters.
pub(crate) async fn connect(&mut self, context: &Context) -> Result<Session> {
if self.lp.server.is_empty() {
bail!("IMAP operation attempted while it is torn down");
}
let now = tools::Time::now();
let until_can_send = max(
min(self.conn_last_try, now)
@@ -332,123 +341,91 @@ impl Imap {
);
self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
let mut first_error = None;
for lp in login_params {
info!(context, "IMAP trying to connect to {}.", &lp.connection);
let connection_candidate = lp.connection.clone();
let client = match Client::connect(
context,
self.proxy_config.clone(),
self.strict_tls,
connection_candidate,
)
.await
{
Ok(client) => client,
Err(err) => {
warn!(context, "IMAP failed to connect: {err:#}.");
first_error.get_or_insert(err);
continue;
}
let connection_res = Client::connect(
context,
self.lp.server.as_ref(),
self.lp.port,
self.strict_tls,
self.socks5_config.clone(),
self.lp.security,
)
.await;
let client = connection_res?;
self.conn_backoff_ms = BACKOFF_MIN_MS;
self.ratelimit.send();
let imap_user: &str = self.lp.user.as_ref();
let imap_pw: &str = self.lp.password.as_ref();
let oauth2 = self.lp.oauth2;
let login_res = if oauth2 {
info!(context, "Logging into IMAP server with OAuth 2");
let addr: &str = self.addr.as_ref();
let token = get_oauth2_access_token(context, addr, imap_pw, true)
.await?
.context("IMAP could not get OAUTH token")?;
let auth = OAuth2 {
user: imap_user.into(),
access_token: token,
};
client.authenticate("XOAUTH2", auth).await
} else {
info!(context, "Logging into IMAP server with LOGIN");
client.login(imap_user, imap_pw).await
};
self.conn_backoff_ms = BACKOFF_MIN_MS;
self.ratelimit.send();
match login_res {
Ok(session) => {
// Store server ID in the context to display in account info.
let mut lock = context.server_id.write().await;
lock.clone_from(&session.capabilities.server_id);
let imap_user: &str = lp.user.as_ref();
let imap_pw: &str = &self.password;
self.login_failed_once = false;
context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}",
self.lp.user
)));
self.connectivity.set_connected(context).await;
info!(context, "Successfully logged into IMAP server");
Ok(session)
}
let login_res = if self.oauth2 {
info!(context, "Logging into IMAP server with OAuth 2.");
let addr: &str = self.addr.as_ref();
Err(err) => {
let imap_user = self.lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user).await;
let token = get_oauth2_access_token(context, addr, imap_pw, true)
.await?
.context("IMAP could not get OAUTH token")?;
let auth = OAuth2 {
user: imap_user.into(),
access_token: token,
};
client.authenticate("XOAUTH2", auth).await
} else {
info!(context, "Logging into IMAP server with LOGIN.");
client.login(imap_user, imap_pw).await
};
warn!(context, "{} ({:#})", message, err);
match login_res {
Ok(mut session) => {
let capabilities = determine_capabilities(&mut session).await?;
let session = if capabilities.can_compress {
info!(context, "Enabling IMAP compression.");
let compressed_session = session
.compress(|s| {
let session_stream: Box<dyn SessionStream> = Box::new(s);
session_stream
})
.await
.context("Failed to enable IMAP compression")?;
Session::new(compressed_session, capabilities)
} else {
Session::new(session, capabilities)
};
// Store server ID in the context to display in account info.
let mut lock = context.server_id.write().await;
lock.clone_from(&session.capabilities.server_id);
self.login_failed_once = false;
context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}",
lp.user
)));
self.connectivity.set_connected(context).await;
info!(context, "Successfully logged into IMAP server");
return Ok(session);
}
Err(err) => {
let imap_user = lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user).await;
let err_str = err.to_string();
warn!(context, "IMAP failed to login: {err:#}.");
first_error.get_or_insert(format_err!("{message} ({err:#})"));
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& err_str.to_lowercase().contains("authentication")
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
{
warn!(context, "{e:#}.");
}
drop(lock);
let mut msg = Message::new(Viewtype::Text);
msg.text.clone_from(&message);
if let Err(e) = chat::add_device_msg_with_importance(
context,
None,
Some(&mut msg),
true,
)
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& err.to_string().to_lowercase().contains("authentication")
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
{
warn!(context, "Failed to add device message: {e:#}.");
}
} else {
self.login_failed_once = true;
{
warn!(context, "{:#}", e);
}
drop(lock);
let mut msg = Message::new(Viewtype::Text);
msg.text.clone_from(&message);
if let Err(e) =
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
.await
{
warn!(context, "{:#}", e);
}
} else {
self.login_failed_once = true;
}
Err(format_err!("{}\n\n{:#}", message, err))
}
}
Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
}
/// Prepare for IMAP operation.
@@ -469,11 +446,7 @@ impl Imap {
.get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
.await?;
if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
let is_chatmail = match context.get_config_bool(Config::FixIsChatmail).await? {
false => session.is_chatmail(),
true => context.get_config_bool(Config::IsChatmail).await?,
};
let create_mvbox = !is_chatmail || context.get_config_bool(Config::MvboxMove).await?;
let create_mvbox = true;
self.configure_folders(context, &mut session, create_mvbox)
.await?;
}
@@ -1061,52 +1034,6 @@ impl Session {
Ok(())
}
/// Uploads sync messages from the `imap_send` table with `\Seen` flag set.
pub(crate) async fn send_sync_msgs(&mut self, context: &Context, folder: &str) -> Result<()> {
context.send_sync_msg().await?;
while let Some((id, mime, msg_id, attempts)) = context
.sql
.query_row_optional(
"SELECT id, mime, msg_id, attempts FROM imap_send ORDER BY id LIMIT 1",
(),
|row| {
let id: i64 = row.get(0)?;
let mime: String = row.get(1)?;
let msg_id: MsgId = row.get(2)?;
let attempts: i64 = row.get(3)?;
Ok((id, mime, msg_id, attempts))
},
)
.await
.context("Failed to SELECT from imap_send")?
{
let res = self
.append(folder, Some("(\\Seen)"), None, mime)
.await
.with_context(|| format!("IMAP APPEND to {folder} failed for {msg_id}"))
.log_err(context);
if res.is_ok() {
msg_id.set_delivered(context).await?;
}
const MAX_ATTEMPTS: i64 = 2;
if res.is_ok() || attempts >= MAX_ATTEMPTS - 1 {
context
.sql
.execute("DELETE FROM imap_send WHERE id=?", (id,))
.await
.context("Failed to delete from imap_send")?;
} else {
context
.sql
.execute("UPDATE imap_send SET attempts=attempts+1 WHERE id=?", (id,))
.await
.context("Failed to update imap_send.attempts")?;
res?;
}
}
Ok(())
}
/// Stores pending `\Seen` flags for messages in `imap_markseen` table.
pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
let rows = context
@@ -1127,12 +1054,18 @@ impl Session {
.await?;
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
if let Err(err) = self.select_with_uidvalidity(context, &folder).await {
warn!(context, "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}.");
} else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
self.select_with_uidvalidity(context, &folder)
.await
.context("failed to select folder")?;
if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
warn!(
context,
"Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}.");
"Cannot mark messages {} in folder {} as seen, will retry later: {}.",
uid_set,
folder,
err
);
} else {
info!(
context,
@@ -1556,7 +1489,7 @@ impl Session {
} else if !context.push_subscriber.heartbeat_subscribed().await {
let context = context.clone();
// Subscribe for heartbeat notifications.
tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
tokio::spawn(async move { context.push_subscriber.subscribe().await });
}
Ok(())
@@ -1586,8 +1519,8 @@ impl Session {
/// Attempts to configure mvbox.
///
/// Tries to find any folder examining `folders` in the order they go. If none is found, tries
/// to create any folder in the same order. This method does not use LIST command to ensure that
/// Tries to find any folder in the given list of `folders`. If none is found, tries to create
/// `folders[0]`. This method does not use LIST command to ensure that
/// configuration works even if mailbox lookup is forbidden via Access Control List (see
/// <https://datatracker.ietf.org/doc/html/rfc4314>).
///
@@ -1621,17 +1554,16 @@ impl Session {
if !create_mvbox {
return Ok(None);
}
// Some servers require namespace-style folder names like "INBOX.DeltaChat", so we try all
// the variants here.
for folder in folders {
match self.select_with_uidvalidity(context, folder).await {
Ok(_) => {
info!(context, "MVBOX-folder {} created.", folder);
return Ok(Some(folder));
}
Err(err) => {
warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
}
let Some(folder) = folders.first() else {
return Ok(None);
};
match self.select_with_uidvalidity(context, folder).await {
Ok(_) => {
info!(context, "MVBOX-folder {} created.", folder);
return Ok(Some(folder));
}
Err(err) => {
warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
}
}
Ok(None)
@@ -1879,20 +1811,6 @@ async fn needs_move_to_mvbox(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
) -> Result<bool> {
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
if !context.get_config_bool(Config::IsChatmail).await?
&& has_chat_version
&& headers
.get_header_value(HeaderDef::AutoSubmitted)
.filter(|val| val.to_ascii_lowercase() == "auto-generated")
.is_some()
{
if let Some(from) = mimeparser::get_from(headers) {
if context.is_self_addr(&from.addr).await? {
return Ok(true);
}
}
}
if !context.get_config_bool(Config::MvboxMove).await? {
return Ok(false);
}
@@ -1906,7 +1824,7 @@ async fn needs_move_to_mvbox(
return Ok(false);
}
if has_chat_version {
if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
Ok(true)
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
match parent.is_dc_message {

View File

@@ -25,10 +25,6 @@ pub(crate) struct Capabilities {
/// <https://tools.ietf.org/html/rfc5464>
pub can_metadata: bool,
/// True if the server has COMPRESS=DEFLATE capability as defined in
/// <https://tools.ietf.org/html/rfc4978>
pub can_compress: bool,
/// True if the server supports XDELTAPUSH capability.
/// This capability means setting /private/devicetoken IMAP METADATA
/// on the INBOX results in new mail notifications

View File

@@ -1,22 +1,19 @@
use std::net::SocketAddr;
use std::ops::{Deref, DerefMut};
use anyhow::{Context as _, Result};
use anyhow::{bail, Context as _, Result};
use async_imap::Client as ImapClient;
use async_imap::Session as ImapSession;
use tokio::io::BufWriter;
use super::capabilities::Capabilities;
use super::session::Session;
use crate::context::Context;
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::SessionStream;
use crate::net::tls::wrap_tls;
use crate::net::{
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
};
use crate::tools::time;
use crate::net::{connect_starttls_imap, connect_tcp, connect_tls};
use crate::provider::Socket;
use crate::socks::Socks5Config;
use fast_socks5::client::Socks5Stream;
#[derive(Debug)]
pub(crate) struct Client {
@@ -37,20 +34,10 @@ impl DerefMut for Client {
}
}
/// Converts port number to ALPN list.
fn alpn(port: u16) -> &'static [&'static str] {
if port == 993 {
// Do not request ALPN on standard port.
&[]
} else {
&["imap"]
}
}
/// Determine server capabilities.
///
/// If server supports ID capability, send our client ID.
pub(crate) async fn determine_capabilities(
async fn determine_capabilities(
session: &mut ImapSession<Box<dyn SessionStream>>,
) -> Result<Capabilities> {
let caps = session
@@ -68,7 +55,6 @@ pub(crate) async fn determine_capabilities(
can_check_quota: caps.has_str("QUOTA"),
can_condstore: caps.has_str("CONDSTORE"),
can_metadata: caps.has_str("METADATA"),
can_compress: caps.has_str("COMPRESS=DEFLATE"),
can_push: caps.has_str("XDELTAPUSH"),
is_chatmail: caps.has_str("XCHATMAIL"),
server_id,
@@ -83,126 +69,70 @@ impl Client {
}
}
pub(crate) async fn login(
self,
username: &str,
password: &str,
) -> Result<ImapSession<Box<dyn SessionStream>>> {
pub(crate) async fn login(self, username: &str, password: &str) -> Result<Session> {
let Client { inner, .. } = self;
let session = inner
let mut session = inner
.login(username, password)
.await
.map_err(|(err, _client)| err)?;
Ok(session)
let capabilities = determine_capabilities(&mut session).await?;
Ok(Session::new(session, capabilities))
}
pub(crate) async fn authenticate(
self,
auth_type: &str,
authenticator: impl async_imap::Authenticator,
) -> Result<ImapSession<Box<dyn SessionStream>>> {
) -> Result<Session> {
let Client { inner, .. } = self;
let session = inner
let mut session = inner
.authenticate(auth_type, authenticator)
.await
.map_err(|(err, _client)| err)?;
Ok(session)
}
async fn connection_attempt(
context: Context,
host: String,
security: ConnectionSecurity,
resolved_addr: SocketAddr,
strict_tls: bool,
) -> Result<Self> {
let context = &context;
let host = &host;
info!(
context,
"Attempting IMAP connection to {host} ({resolved_addr})."
);
let res = match security {
ConnectionSecurity::Tls => {
Client::connect_secure(resolved_addr, host, strict_tls).await
}
ConnectionSecurity::Starttls => {
Client::connect_starttls(resolved_addr, host, strict_tls).await
}
ConnectionSecurity::Plain => Client::connect_insecure(resolved_addr).await,
};
match res {
Ok(client) => {
let ip_addr = resolved_addr.ip().to_string();
let port = resolved_addr.port();
let save_cache = match security {
ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
ConnectionSecurity::Plain => false,
};
if save_cache {
update_connect_timestamp(context, host, &ip_addr).await?;
}
update_connection_history(context, "imap", host, port, &ip_addr, time()).await?;
Ok(client)
}
Err(err) => {
warn!(
context,
"Failed to connect to {host} ({resolved_addr}): {err:#}."
);
Err(err)
}
}
let capabilities = determine_capabilities(&mut session).await?;
Ok(Session::new(session, capabilities))
}
pub async fn connect(
context: &Context,
proxy_config: Option<ProxyConfig>,
host: &str,
port: u16,
strict_tls: bool,
candidate: ConnectionCandidate,
socks5_config: Option<Socks5Config>,
security: Socket,
) -> Result<Self> {
let host = &candidate.host;
let port = candidate.port;
let security = candidate.security;
if let Some(proxy_config) = proxy_config {
let client = match security {
ConnectionSecurity::Tls => {
Client::connect_secure_proxy(context, host, port, strict_tls, proxy_config)
.await?
if let Some(socks5_config) = socks5_config {
match security {
Socket::Automatic => bail!("IMAP port security is not configured"),
Socket::Ssl => {
Client::connect_secure_socks5(context, host, port, strict_tls, socks5_config)
.await
}
ConnectionSecurity::Starttls => {
Client::connect_starttls_proxy(context, host, port, proxy_config, strict_tls)
.await?
Socket::Starttls => {
Client::connect_starttls_socks5(context, host, port, socks5_config, strict_tls)
.await
}
ConnectionSecurity::Plain => {
Client::connect_insecure_proxy(context, host, port, proxy_config).await?
Socket::Plain => {
Client::connect_insecure_socks5(context, host, port, socks5_config).await
}
};
update_connection_history(context, "imap", host, port, host, time()).await?;
Ok(client)
}
} else {
let load_cache = match security {
ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
ConnectionSecurity::Plain => false,
};
let connection_futures =
lookup_host_with_cache(context, host, port, "imap", load_cache)
.await?
.into_iter()
.map(|resolved_addr| {
let context = context.clone();
let host = host.to_string();
Self::connection_attempt(context, host, security, resolved_addr, strict_tls)
});
run_connection_attempts(connection_futures).await
match security {
Socket::Automatic => bail!("IMAP port security is not configured"),
Socket::Ssl => Client::connect_secure(context, host, port, strict_tls).await,
Socket::Starttls => Client::connect_starttls(context, host, port, strict_tls).await,
Socket::Plain => Client::connect_insecure(context, host, port).await,
}
}
}
async fn connect_secure(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
async fn connect_secure(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
) -> Result<Self> {
let tls_stream = connect_tls(context, hostname, port, strict_tls, "imap").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);
@@ -213,8 +143,8 @@ impl Client {
Ok(client)
}
async fn connect_insecure(addr: SocketAddr) -> Result<Self> {
let tcp_stream = connect_tcp_inner(addr).await?;
async fn connect_insecure(context: &Context, hostname: &str, port: u16) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, false).await?;
let buffered_stream = BufWriter::new(tcp_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -225,26 +155,13 @@ impl Client {
Ok(client)
}
async fn connect_starttls(addr: SocketAddr, host: &str, strict_tls: bool) -> Result<Self> {
let tcp_stream = connect_tcp_inner(addr).await?;
// Run STARTTLS command and convert the client back into a stream.
let buffered_tcp_stream = BufWriter::new(tcp_stream);
let mut client = async_imap::Client::new(buffered_tcp_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")??;
client
.run_command_and_check_ok("STARTTLS", None)
.await
.context("STARTTLS command failed")?;
let buffered_tcp_stream = client.into_inner();
let tcp_stream = buffered_tcp_stream.into_inner();
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
async fn connect_starttls(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
) -> Result<Self> {
let tls_stream = connect_starttls_imap(context, hostname, port, strict_tls).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
@@ -252,17 +169,17 @@ impl Client {
Ok(client)
}
async fn connect_secure_proxy(
async fn connect_secure_socks5(
context: &Context,
domain: &str,
port: u16,
strict_tls: bool,
proxy_config: ProxyConfig,
socks5_config: Socks5Config,
) -> Result<Self> {
let proxy_stream = proxy_config
let socks5_stream = socks5_config
.connect(context, domain, port, strict_tls)
.await?;
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?;
let tls_stream = wrap_tls(strict_tls, domain, "imap", socks5_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);
@@ -273,14 +190,14 @@ impl Client {
Ok(client)
}
async fn connect_insecure_proxy(
async fn connect_insecure_socks5(
context: &Context,
domain: &str,
port: u16,
proxy_config: ProxyConfig,
socks5_config: Socks5Config,
) -> Result<Self> {
let proxy_stream = proxy_config.connect(context, domain, port, false).await?;
let buffered_stream = BufWriter::new(proxy_stream);
let socks5_stream = socks5_config.connect(context, domain, port, false).await?;
let buffered_stream = BufWriter::new(socks5_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
let _greeting = client
@@ -290,20 +207,20 @@ impl Client {
Ok(client)
}
async fn connect_starttls_proxy(
async fn connect_starttls_socks5(
context: &Context,
hostname: &str,
port: u16,
proxy_config: ProxyConfig,
socks5_config: Socks5Config,
strict_tls: bool,
) -> Result<Self> {
let proxy_stream = proxy_config
let socks5_stream = socks5_config
.connect(context, hostname, port, strict_tls)
.await?;
// Run STARTTLS command and convert the client back into a stream.
let buffered_proxy_stream = BufWriter::new(proxy_stream);
let mut client = ImapClient::new(buffered_proxy_stream);
let buffered_socks5_stream = BufWriter::new(socks5_stream);
let mut client = ImapClient::new(buffered_socks5_stream);
let _greeting = client
.read_response()
.await
@@ -312,10 +229,10 @@ impl Client {
.run_command_and_check_ok("STARTTLS", None)
.await
.context("STARTTLS command failed")?;
let buffered_proxy_stream = client.into_inner();
let proxy_stream = buffered_proxy_stream.into_inner();
let buffered_socks5_stream = client.into_inner();
let socks5_stream: Socks5Stream<_> = buffered_socks5_stream.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream)
let tls_stream = wrap_tls(strict_tls, hostname, "imap", socks5_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);

View File

@@ -24,7 +24,6 @@ const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIE
FROM \
IN-REPLY-TO REFERENCES \
CHAT-VERSION \
AUTO-SUBMITTED \
AUTOCRYPT-SETUP-MESSAGE\
)])";

View File

@@ -2,21 +2,18 @@
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use ::pgp::types::KeyTrait;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use deltachat_contact_tools::EmailAddress;
use futures::TryStreamExt;
use futures_lite::FutureExt;
use pin_project::pin_project;
use tokio::fs::{self, File};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_tar::Archive;
use crate::blob::BlobDirContents;
use crate::chat::{self, delete_and_reset_all_device_msgs};
use crate::config::Config;
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
@@ -180,7 +177,10 @@ async fn set_self_key(context: &Context, armored: &str, set_default: bool) -> Re
info!(context, "No Autocrypt-Prefer-Encrypt header.");
};
let self_addr = context.get_primary_self_addr().await?;
let addr = EmailAddress::new(&self_addr)?;
let keypair = pgp::KeyPair {
addr,
public: public_key,
secret: private_key,
};
@@ -215,7 +215,7 @@ async fn imex_inner(
path.display()
);
ensure!(context.sql.is_open().await, "Database not opened.");
context.emit_event(EventType::ImexProgress(1));
context.emit_event(EventType::ImexProgress(10));
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists
@@ -297,71 +297,12 @@ pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
.0
}
/// Reader that emits progress events as bytes are read from it.
#[pin_project]
struct ProgressReader<R> {
/// Wrapped reader.
#[pin]
inner: R,
/// Number of bytes successfully read from the internal reader.
read: usize,
/// Total size of the backup .tar file expected to be read from the reader.
/// Used to calculate the progress.
file_size: usize,
/// Last progress emitted to avoid emitting the same progress value twice.
last_progress: usize,
/// Context for emitting progress events.
context: Context,
}
impl<R> ProgressReader<R> {
fn new(r: R, context: Context, file_size: u64) -> Self {
Self {
inner: r,
read: 0,
file_size: file_size as usize,
last_progress: 1,
context,
}
}
}
impl<R> AsyncRead for ProgressReader<R>
where
R: AsyncRead,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let this = self.project();
let before = buf.filled().len();
let res = this.inner.poll_read(cx, buf);
if let std::task::Poll::Ready(Ok(())) = res {
*this.read = this.read.saturating_add(buf.filled().len() - before);
let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999);
if progress > *this.last_progress {
this.context.emit_event(EventType::ImexProgress(progress));
*this.last_progress = progress;
}
}
res
}
}
async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
context: &Context,
backup_file: R,
file_size: u64,
passphrase: String,
) -> (Result<()>,) {
let backup_file = ProgressReader::new(backup_file, context.clone(), file_size);
let mut archive = Archive::new(backup_file);
let mut entries = match archive.entries() {
@@ -369,12 +310,29 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
Err(e) => return (Err(e).context("Failed to get archive entries"),),
};
let mut blobs = Vec::new();
// We already emitted ImexProgress(10) above
let mut last_progress = 10;
const PROGRESS_MIGRATIONS: u128 = 999;
let mut total_size: u64 = 0;
let mut res: Result<()> = loop {
let mut f = match entries.try_next().await {
Ok(Some(f)) => f,
Ok(None) => break Ok(()),
Err(e) => break Err(e).context("Failed to get next entry"),
};
total_size += match f.header().entry_size() {
Ok(size) => size,
Err(e) => break Err(e).context("Failed to get entry size"),
};
let max = PROGRESS_MIGRATIONS - 1;
let progress = std::cmp::min(
max * u128::from(total_size) / std::cmp::max(u128::from(file_size), 1),
max,
);
if progress > last_progress {
context.emit_event(EventType::ImexProgress(progress as usize));
last_progress = progress;
}
let path = match f.path() {
Ok(path) => path.to_path_buf(),
@@ -415,16 +373,13 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
.await
.context("cannot import unpacked database");
}
if res.is_ok() {
res = adjust_delete_server_after(context).await;
}
fs::remove_file(unpacked_database)
.await
.context("cannot remove unpacked database")
.log_err(context)
.ok();
if res.is_ok() {
context.emit_event(EventType::ImexProgress(999));
context.emit_event(EventType::ImexProgress(PROGRESS_MIGRATIONS as usize));
res = context.sql.run_migrations(context).await;
}
if res.is_ok() {
@@ -497,14 +452,7 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
let file = File::create(&temp_path).await?;
let blobdir = BlobDirContents::new(context).await?;
let mut file_size = 0;
file_size += temp_db_path.metadata()?.len();
for blob in blobdir.iter() {
file_size += blob.to_abs_path().metadata()?.len()
}
export_backup_stream(context, &temp_db_path, blobdir, file, file_size)
export_backup_stream(context, &temp_db_path, blobdir, file)
.await
.context("Exporting backup to file failed")?;
fs::rename(temp_path, &dest_path).await?;
@@ -512,99 +460,33 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
Ok(())
}
/// Writer that emits progress events as bytes are written into it.
#[pin_project]
struct ProgressWriter<W> {
/// Wrapped writer.
#[pin]
inner: W,
/// Number of bytes successfully written into the internal writer.
written: usize,
/// Total size of the backup .tar file expected to be written into the writer.
/// Used to calculate the progress.
file_size: usize,
/// Last progress emitted to avoid emitting the same progress value twice.
last_progress: usize,
/// Context for emitting progress events.
context: Context,
}
impl<W> ProgressWriter<W> {
fn new(w: W, context: Context, file_size: u64) -> Self {
Self {
inner: w,
written: 0,
file_size: file_size as usize,
last_progress: 1,
context,
}
}
}
impl<W> AsyncWrite for ProgressWriter<W>
where
W: AsyncWrite,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<Result<usize, std::io::Error>> {
let this = self.project();
let res = this.inner.poll_write(cx, buf);
if let std::task::Poll::Ready(Ok(written)) = res {
*this.written = this.written.saturating_add(written);
let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999);
if progress > *this.last_progress {
this.context.emit_event(EventType::ImexProgress(progress));
*this.last_progress = progress;
}
}
res
}
fn poll_flush(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> {
self.project().inner.poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> {
self.project().inner.poll_shutdown(cx)
}
}
/// Exports the database and blobs into a stream.
pub(crate) async fn export_backup_stream<'a, W>(
context: &'a Context,
temp_db_path: &Path,
blobdir: BlobDirContents<'a>,
writer: W,
file_size: u64,
) -> Result<()>
where
W: tokio::io::AsyncWrite + tokio::io::AsyncWriteExt + Unpin + Send + 'static,
{
let writer = ProgressWriter::new(writer, context.clone(), file_size);
let mut builder = tokio_tar::Builder::new(writer);
builder
.append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
.await?;
for blob in blobdir.iter() {
let mut last_progress = 10;
for (i, blob) in blobdir.iter().enumerate() {
let mut file = File::open(blob.to_abs_path()).await?;
let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(blob.as_name());
builder.append_file(path_in_archive, &mut file).await?;
let progress = std::cmp::min(1000 * i / blobdir.len(), 999);
if progress > last_progress {
context.emit_event(EventType::ImexProgress(progress));
last_progress = progress;
}
}
builder.finish().await?;
@@ -795,7 +677,6 @@ async fn export_database(
.to_str()
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
adjust_delete_server_after(context).await?;
context
.sql
.set_raw_config_int("backup_time", timestamp)
@@ -825,19 +706,6 @@ async fn export_database(
.await
}
/// Sets `Config::DeleteServerAfter` to "never" if needed so that new messages are present on the
/// server after a backup restoration or available for all devices in multi-device case.
/// NB: Calling this after a backup import isn't reliable as we can crash in between, but this is a
/// problem only for old backups, new backups already have `DeleteServerAfter` set if necessary.
async fn adjust_delete_server_after(context: &Context) -> Result<()> {
if context.is_chatmail().await? && !context.config_exists(Config::DeleteServerAfter).await? {
context
.set_config(Config::DeleteServerAfter, Some("0"))
.await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::time::Duration;
@@ -1023,49 +891,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_import_chatmail_backup() -> Result<()> {
let backup_dir = tempfile::tempdir().unwrap();
let context1 = &TestContext::new_alice().await;
// Check that the setting is displayed correctly.
assert_eq!(
context1.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
context1.set_config_bool(Config::IsChatmail, true).await?;
assert_eq!(
context1.get_config(Config::DeleteServerAfter).await?,
Some("1".to_string())
);
assert_eq!(context1.get_config_delete_server_after().await?, Some(0));
imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
let _event = context1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
let context2 = &TestContext::new().await;
let backup = has_backup(context2, backup_dir.path()).await?;
imex(context2, ImexMode::ImportBackup, backup.as_ref(), None).await?;
let _event = context2
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
assert!(context2.is_configured().await?);
assert!(context2.is_chatmail().await?);
for ctx in [context1, context2] {
assert_eq!(
ctx.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
assert_eq!(ctx.get_config_delete_server_after().await?, None);
}
Ok(())
}
/// This is a regression test for
/// https://github.com/deltachat/deltachat-android/issues/2263
/// where the config cache wasn't reset properly after a backup.

View File

@@ -31,25 +31,36 @@ use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll;
use anyhow::{bail, format_err, Context as _, Result};
use futures_lite::FutureExt;
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
use futures_lite::StreamExt;
use iroh_net::relay::RelayMode;
use iroh_net::Endpoint;
use tokio::fs;
use tokio::task::JoinHandle;
use iroh_old;
use iroh_old::blobs::Collection;
use iroh_old::get::DataStream;
use iroh_old::progress::ProgressEmitter;
use iroh_old::provider::Ticket;
use tokio::fs::{self, File};
use tokio::io::{self, AsyncWriteExt, BufWriter};
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::{broadcast, Mutex};
use tokio::task::{JoinHandle, JoinSet};
use tokio_stream::wrappers::ReadDirStream;
use tokio_util::sync::CancellationToken;
use crate::chat::add_device_msg;
use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs};
use crate::context::Context;
use crate::imex::BlobDirContents;
use crate::message::{Message, Viewtype};
use crate::qr::Qr;
use crate::qr::{self, Qr};
use crate::stock_str::backup_transfer_msg_body;
use crate::tools::{create_id, time, TempPathGuard};
use crate::EventType;
use super::{export_backup_stream, export_database, import_backup_stream, DBFILE_BACKUP_NAME};
const MAX_CONCURRENT_DIALS: u8 = 16;
/// ALPN protocol identifier for the backup transfer protocol.
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
@@ -98,7 +109,7 @@ impl BackupProvider {
let endpoint = Endpoint::builder()
.alpns(vec![BACKUP_ALPN.to_vec()])
.relay_mode(relay_mode)
.bind()
.bind(0)
.await?;
let node_addr = endpoint.node_addr().await?;
@@ -109,7 +120,6 @@ impl BackupProvider {
.get_blobdir()
.parent()
.context("Context dir not found")?;
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
if fs::metadata(&dbfile).await.is_ok() {
fs::remove_file(&dbfile).await?;
@@ -125,6 +135,7 @@ impl BackupProvider {
export_database(context, &dbfile, passphrase, time())
.await
.context("Database export failed")?;
context.emit_event(EventType::ImexProgress(300));
let drop_token = CancellationToken::new();
let handle = {
@@ -178,7 +189,6 @@ impl BackupProvider {
}
info!(context, "Received valid backup authentication token.");
context.emit_event(EventType::ImexProgress(1));
let blobdir = BlobDirContents::new(&context).await?;
@@ -190,7 +200,7 @@ impl BackupProvider {
send_stream.write_all(&file_size.to_be_bytes()).await?;
export_backup_stream(&context, &dbfile, blobdir, send_stream, file_size)
export_backup_stream(&context, &dbfile, blobdir, send_stream)
.await
.context("Failed to write backup into QUIC stream")?;
info!(context, "Finished writing backup into QUIC stream.");
@@ -222,31 +232,12 @@ impl BackupProvider {
conn = endpoint.accept() => {
if let Some(conn) = conn {
let conn = match conn.accept() {
Ok(conn) => conn,
Err(err) => {
warn!(context, "Failed to accept iroh connection: {err:#}.");
continue;
}
};
// Got a new in-progress connection.
let context = context.clone();
let auth_token = auth_token.clone();
let dbfile = dbfile.clone();
if let Err(err) = Self::handle_connection(context.clone(), conn, auth_token, dbfile).race(
async {
cancel_token.recv().await.ok();
Err(format_err!("Backup transfer cancelled"))
}
).race(
async {
drop_token.cancelled().await;
Err(format_err!("Backup provider dropped"))
}
).await {
if let Err(err) = Self::handle_connection(context.clone(), conn, auth_token, dbfile).await {
warn!(context, "Error while handling backup connection: {err:#}.");
context.emit_event(EventType::ImexProgress(0));
break;
} else {
info!(context, "Backup transfer finished successfully.");
break;
@@ -256,12 +247,10 @@ impl BackupProvider {
}
},
_ = cancel_token.recv() => {
info!(context, "Backup transfer cancelled by the user, stopping accept loop.");
context.emit_event(EventType::ImexProgress(0));
break;
}
_ = drop_token.cancelled() => {
info!(context, "Backup transfer cancelled by dropping the provider, stopping accept loop.");
context.emit_event(EventType::ImexProgress(0));
break;
}
@@ -290,6 +279,33 @@ impl Future for BackupProvider {
}
}
/// Retrieves backup from a legacy backup provider using iroh 0.4.
pub async fn get_legacy_backup(context: &Context, qr: Qr) -> Result<()> {
ensure!(
matches!(qr, Qr::Backup { .. }),
"QR code for backup must be of type DCBACKUP"
);
ensure!(
!context.is_configured().await?,
"Cannot import backups to accounts in use."
);
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
let _guard = context.scheduler.pause(context.clone()).await;
info!(
context,
"Running get_backup for {}",
qr::format_backup(&qr)?
);
let res = tokio::select! {
biased;
res = get_backup_inner(context, qr) => res,
_ = cancel_token.recv() => Err(format_err!("cancelled")),
};
context.free_ongoing().await;
res
}
pub async fn get_backup2(
context: &Context,
node_addr: iroh_net::NodeAddr,
@@ -297,7 +313,7 @@ pub async fn get_backup2(
) -> Result<()> {
let relay_mode = RelayMode::Disabled;
let endpoint = Endpoint::builder().relay_mode(relay_mode).bind().await?;
let endpoint = Endpoint::builder().relay_mode(relay_mode).bind(0).await?;
let conn = endpoint.connect(node_addr, BACKUP_ALPN).await?;
let (mut send_stream, mut recv_stream) = conn.open_bi().await?;
@@ -319,13 +335,9 @@ pub async fn get_backup2(
// Send an acknowledgement, but ignore the errors.
// We have imported backup successfully already.
send_stream.write_all(b".").await.ok();
send_stream.finish().ok();
send_stream.finish().await.ok();
info!(context, "Sent backup reception acknowledgment.");
// Wait for the peer to acknowledge reception of the acknowledgement
// before closing the connection.
_ = send_stream.stopped().await;
Ok(())
}
@@ -337,33 +349,202 @@ pub async fn get_backup2(
///
/// This is a long running operation which will return only when completed.
///
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variant of it. It
/// does avoid having [`iroh_net::NodeAddr`] in the primary API however, without
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variants of it. It
/// does avoid having [`iroh_old::provider::Ticket`] in the primary API however, without
/// having to revert to untyped bytes.
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
match qr {
Qr::Backup { .. } => get_legacy_backup(context, qr).await?,
Qr::Backup2 {
node_addr,
auth_token,
} => {
let cancel_token = context.alloc_ongoing().await?;
let res = get_backup2(context, node_addr, auth_token)
.race(async {
cancel_token.recv().await.ok();
Err(format_err!("Backup reception cancelled"))
})
.await;
if res.is_err() {
context.emit_event(EventType::ImexProgress(0));
}
context.free_ongoing().await;
res?;
}
_ => bail!("QR code for backup must be of type DCBACKUP2"),
} => get_backup2(context, node_addr, auth_token).await?,
_ => bail!("QR code for backup must be of type DCBACKUP or DCBACKUP2"),
}
Ok(())
}
async fn get_backup_inner(context: &Context, qr: Qr) -> Result<()> {
let ticket = match qr {
Qr::Backup { ticket } => ticket,
_ => bail!("QR code for backup must be of type DCBACKUP"),
};
match transfer_from_provider(context, &ticket).await {
Ok(()) => {
context.sql.run_migrations(context).await?;
delete_and_reset_all_device_msgs(context).await?;
context.emit_event(ReceiveProgress::Completed.into());
Ok(())
}
Err(err) => {
// Clean up any blobs we already wrote.
let readdir = fs::read_dir(context.get_blobdir()).await?;
let mut readdir = ReadDirStream::new(readdir);
while let Some(dirent) = readdir.next().await {
if let Ok(dirent) = dirent {
fs::remove_file(dirent.path()).await.ok();
}
}
context.emit_event(ReceiveProgress::Failed.into());
Err(err)
}
}
}
async fn transfer_from_provider(context: &Context, ticket: &Ticket) -> Result<()> {
let progress = ProgressEmitter::new(0, ReceiveProgress::max_blob_progress());
spawn_progress_proxy(context.clone(), progress.subscribe());
let on_connected = || {
context.emit_event(ReceiveProgress::Connected.into());
async { Ok(()) }
};
let on_collection = |collection: &Collection| {
context.emit_event(ReceiveProgress::CollectionReceived.into());
progress.set_total(collection.total_blobs_size());
async { Ok(()) }
};
let jobs = Mutex::new(JoinSet::default());
let on_blob =
|hash, reader, name| on_blob(context, &progress, &jobs, ticket, hash, reader, name);
// Perform the transfer.
let keylog = false; // Do not enable rustls SSLKEYLOGFILE env var functionality
let stats = iroh_old::get::run_ticket(
ticket,
keylog,
MAX_CONCURRENT_DIALS,
on_connected,
on_collection,
on_blob,
)
.await?;
let mut jobs = jobs.lock().await;
while let Some(job) = jobs.join_next().await {
job.context("job failed")?;
}
drop(progress);
info!(
context,
"Backup transfer finished, transfer rate was {} Mbps.",
stats.mbits()
);
Ok(())
}
/// Get callback when a blob is received from the provider.
///
/// This writes the blobs to the blobdir. If the blob is the database it will import it to
/// the database of the current [`Context`].
async fn on_blob(
context: &Context,
progress: &ProgressEmitter,
jobs: &Mutex<JoinSet<()>>,
ticket: &Ticket,
_hash: iroh_old::Hash,
mut reader: DataStream,
name: String,
) -> Result<DataStream> {
ensure!(!name.is_empty(), "Received a nameless blob");
let path = if name.starts_with("db/") {
let context_dir = context
.get_blobdir()
.parent()
.ok_or_else(|| anyhow!("Context dir not found"))?;
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
if fs::metadata(&dbfile).await.is_ok() {
fs::remove_file(&dbfile).await?;
warn!(context, "Previous database export deleted");
}
dbfile
} else {
ensure!(name.starts_with("blob/"), "malformatted blob name");
let blobname = name.rsplit('/').next().context("malformatted blob name")?;
context.get_blobdir().join(blobname)
};
let mut wrapped_reader = progress.wrap_async_read(&mut reader);
let file = File::create(&path).await?;
let mut file = BufWriter::with_capacity(128 * 1024, file);
io::copy(&mut wrapped_reader, &mut file).await?;
file.flush().await?;
if name.starts_with("db/") {
let context = context.clone();
let token = ticket.token().to_string();
jobs.lock().await.spawn(async move {
if let Err(err) = context.sql.import(&path, token).await {
error!(context, "cannot import database: {:#?}", err);
}
if let Err(err) = fs::remove_file(&path).await {
error!(
context,
"failed to delete database import file '{}': {:#?}",
path.display(),
err,
);
}
});
}
Ok(reader)
}
/// Spawns a task proxying progress events.
///
/// This spawns a tokio task which receives events from the [`ProgressEmitter`] and sends
/// them to the context. The task finishes when the emitter is dropped.
///
/// This could be done directly in the emitter by making it less generic.
fn spawn_progress_proxy(context: Context, mut rx: broadcast::Receiver<u16>) {
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(step) => context.emit_event(ReceiveProgress::BlobProgress(step).into()),
Err(RecvError::Closed) => break,
Err(RecvError::Lagged(_)) => continue,
}
}
});
}
/// Create [`EventType::ImexProgress`] events using readable names.
///
/// Plus you get warnings if you don't use all variants.
#[derive(Debug)]
enum ReceiveProgress {
Connected,
CollectionReceived,
/// A value between 0 and 85 interpreted as a percentage.
///
/// Other values are already used by the other variants of this enum.
BlobProgress(u16),
Completed,
Failed,
}
impl ReceiveProgress {
/// The maximum value for [`ReceiveProgress::BlobProgress`].
///
/// This only exists to keep this magic value local in this type.
fn max_blob_progress() -> u16 {
85
}
}
impl From<ReceiveProgress> for EventType {
fn from(source: ReceiveProgress) -> Self {
let val = match source {
ReceiveProgress::Connected => 50,
ReceiveProgress::CollectionReceived => 100,
ReceiveProgress::BlobProgress(val) => 100 + 10 * val,
ReceiveProgress::Completed => 1000,
ReceiveProgress::Failed => 0,
};
EventType::ImexProgress(val.into())
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;

View File

@@ -244,7 +244,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let _guard = context.generating_key_mutex.lock().await;
// Check if the key appeared while we were waiting on the lock.
match load_keypair(context).await? {
match load_keypair(context, &addr).await? {
Some(key_pair) => Ok(key_pair),
None => {
let start = tools::Time::now();
@@ -266,7 +266,10 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
}
}
pub(crate) async fn load_keypair(context: &Context) -> Result<Option<KeyPair>> {
pub(crate) async fn load_keypair(
context: &Context,
addr: &EmailAddress,
) -> Result<Option<KeyPair>> {
let res = context
.sql
.query_row_optional(
@@ -284,6 +287,7 @@ pub(crate) async fn load_keypair(context: &Context) -> Result<Option<KeyPair>> {
Ok(if let Some((pub_bytes, sec_bytes)) = res {
Some(KeyPair {
addr: addr.clone(),
public: SignedPublicKey::from_slice(&pub_bytes)?,
secret: SignedSecretKey::from_slice(&sec_bytes)?,
})
@@ -333,11 +337,17 @@ pub(crate) async fn store_self_keypair(
KeyPairUse::ReadOnly => false,
};
// `addr` and `is_default` written for compatibility with older versions,
// until new cores are rolled out everywhere.
// otherwise "add second device" or "backup" may break.
// moreover, this allows downgrades to the previous version.
// writing of `addr` and `is_default` can be removed ~ 2024-08
let addr = keypair.addr.to_string();
transaction
.execute(
"INSERT OR REPLACE INTO keypairs (public_key, private_key)
VALUES (?,?)",
(&public_key, &secret_key),
"INSERT OR REPLACE INTO keypairs (public_key, private_key, addr, is_default)
VALUES (?,?,?,?)",
(&public_key, &secret_key, addr, is_default),
)
.context("Failed to insert keypair")?;
@@ -367,10 +377,15 @@ pub(crate) async fn store_self_keypair(
/// This API is used for testing purposes
/// to avoid generating the key in tests.
/// Use import/export APIs instead.
pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Result<()> {
pub async fn preconfigure_keypair(context: &Context, addr: &str, secret_data: &str) -> Result<()> {
let addr = EmailAddress::new(addr)?;
let secret = SignedSecretKey::from_asc(secret_data)?.0;
let public = secret.split_public_key()?;
let keypair = KeyPair { public, secret };
let keypair = KeyPair {
addr,
public,
secret,
};
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
Ok(())
}

View File

@@ -84,6 +84,7 @@ mod scheduler;
pub mod securejoin;
mod simplify;
mod smtp;
mod socks;
pub mod stock_str;
mod sync;
mod timesmearing;

File diff suppressed because it is too large Load Diff

View File

@@ -81,20 +81,7 @@ impl MsgId {
pub async fn get_state(self, context: &Context) -> Result<MessageState> {
let result = context
.sql
.query_row_optional(
concat!(
"SELECT m.state, mdns.msg_id",
" FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
" WHERE id=?",
" LIMIT 1",
),
(self,),
|row| {
let state: MessageState = row.get(0)?;
let mdn_msg_id: Option<MsgId> = row.get(1)?;
Ok(state.with_mdns(mdn_msg_id.is_some()))
},
)
.query_get_value("SELECT state FROM msgs WHERE id=?", (self,))
.await?
.unwrap_or_default();
Ok(result)
@@ -148,6 +135,21 @@ impl MsgId {
Ok(())
}
/// Deletes a message, corresponding MDNs and unsent SMTP messages from the database.
pub(crate) async fn delete_from_db(self, context: &Context) -> Result<()> {
context
.sql
.transaction(move |transaction| {
transaction.execute("DELETE FROM smtp WHERE msg_id=?", (self,))?;
transaction.execute("DELETE FROM msgs_mdns WHERE msg_id=?", (self,))?;
transaction.execute("DELETE FROM msgs_status_updates WHERE msg_id=?", (self,))?;
transaction.execute("DELETE FROM msgs WHERE id=?", (self,))?;
Ok(())
})
.await?;
Ok(())
}
pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
update_msg_state(context, self, MessageState::OutDelivered).await?;
let chat_id: ChatId = context
@@ -204,13 +206,11 @@ impl MsgId {
}
/// Returns information about hops of a message, used for message info
pub async fn hop_info(self, context: &Context) -> Result<String> {
let hop_info = context
pub async fn hop_info(self, context: &Context) -> Result<Option<String>> {
context
.sql
.query_get_value("SELECT IFNULL(hop_info, '') FROM msgs WHERE id=?", (self,))
.await?
.with_context(|| format!("Message {self} not found"))?;
Ok(hop_info)
.query_get_value("SELECT hop_info FROM msgs WHERE id=?", (self,))
.await
}
/// Returns detailed message information in a multi-line text form.
@@ -315,12 +315,7 @@ impl MsgId {
if let Some(path) = msg.get_file(context) {
let bytes = get_filebytes(context, &path).await?;
ret += &format!(
"\nFile: {}, name: {}, {} bytes\n",
path.display(),
msg.get_filename().unwrap_or_default(),
bytes
);
ret += &format!("\nFile: {}, {} bytes\n", path.display(), bytes);
}
if msg.viewtype != Viewtype::Text {
@@ -353,11 +348,7 @@ impl MsgId {
let hop_info = self.hop_info(context).await?;
ret += "\n\n";
if hop_info.is_empty() {
ret += "No Hop Info";
} else {
ret += &hop_info;
}
ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned());
Ok(ret)
}
@@ -528,7 +519,6 @@ impl Message {
" m.ephemeral_timestamp AS ephemeral_timestamp,",
" m.type AS type,",
" m.state AS state,",
" mdns.msg_id AS mdn_msg_id,",
" m.download_state AS download_state,",
" m.error AS error,",
" m.msgrmsg AS msgrmsg,",
@@ -539,16 +529,11 @@ impl Message {
" m.hidden AS hidden,",
" m.location_id AS location,",
" c.blocked AS blocked",
" FROM msgs m",
" LEFT JOIN chats c ON c.id=m.chat_id",
" LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
" WHERE m.id=? AND chat_id!=3",
" LIMIT 1",
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
" WHERE m.id=? AND chat_id!=3;"
),
(id,),
|row| {
let state: MessageState = row.get("state")?;
let mdn_msg_id: Option<MsgId> = row.get("mdn_msg_id")?;
let text = match row.get_ref("txt")? {
rusqlite::types::ValueRef::Text(buf) => {
match String::from_utf8(buf.to_vec()) {
@@ -583,7 +568,7 @@ impl Message {
ephemeral_timer: row.get("ephemeral_timer")?,
ephemeral_timestamp: row.get("ephemeral_timestamp")?,
viewtype: row.get("type")?,
state: state.with_mdns(mdn_msg_id.is_some()),
state: row.get("state")?,
download_state: row.get("download_state")?,
error: Some(row.get::<_, String>("error")?)
.filter(|error| !error.is_empty()),
@@ -1368,7 +1353,7 @@ pub enum MessageState {
OutDelivered = 26,
/// Outgoing message read by the recipient (two checkmarks; this
/// requires goodwill on the receiver's side). Not used in the db for new messages.
/// requires goodwill on the receiver's side)
OutMdnRcvd = 28,
}
@@ -1411,14 +1396,6 @@ impl MessageState {
OutPreparing | OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd
)
}
/// Returns adjusted message state if the message has MDNs.
pub(crate) fn with_mdns(self, has_mdns: bool) -> Self {
if self == MessageState::OutDelivered && has_mdns {
return MessageState::OutMdnRcvd;
}
self
}
}
/// Returns contacts that sent read receipts and the time of reading.
@@ -1765,17 +1742,19 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
if curr_blocked == Blocked::Not
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
&& curr_param.get_cmd() == SystemMessage::Unknown
&& context.should_send_mdns().await?
{
context
.sql
.execute(
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
(id, curr_from_id, curr_rfc724_mid),
)
.await
.context("failed to insert into smtp_mdns")?;
context.scheduler.interrupt_smtp().await;
let mdns_enabled = context.get_config_bool(Config::MdnsEnabled).await?;
if mdns_enabled {
context
.sql
.execute(
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
(id, curr_from_id, curr_rfc724_mid),
)
.await
.context("failed to insert into smtp_mdns")?;
context.scheduler.interrupt_smtp().await;
}
}
updated_chat_ids.insert(curr_chat_id);
}
@@ -1799,10 +1778,6 @@ pub(crate) async fn update_msg_state(
msg_id: MsgId,
state: MessageState,
) -> Result<()> {
ensure!(
state != MessageState::OutMdnRcvd,
"Update msgs_mdns table instead!"
);
ensure!(state != MessageState::OutFailed, "use set_msg_failed()!");
let error_subst = match state >= MessageState::OutPending {
true => ", error=''",
@@ -1811,8 +1786,8 @@ pub(crate) async fn update_msg_state(
context
.sql
.execute(
&format!("UPDATE msgs SET state=? {error_subst} WHERE id=?"),
(state, msg_id),
&format!("UPDATE msgs SET state=?1 {error_subst} WHERE id=?2 AND (?1!=?3 OR state<?3)"),
(state, msg_id, MessageState::OutDelivered),
)
.await?;
Ok(())
@@ -1900,7 +1875,6 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
/// Estimates the number of messages that will be deleted
/// by the options `delete_device_after` or `delete_server_after`.
///
/// This is typically used to show the estimated impact to the user
/// before actually enabling deletion of old messages.
///
@@ -1990,9 +1964,7 @@ pub(crate) async fn rfc724_mid_exists_ex(
.query_row_optional(
&("SELECT id, timestamp_sent, MIN(".to_string()
+ expr
+ ") FROM msgs WHERE rfc724_mid=?
HAVING COUNT(*) > 0 -- Prevent MIN(expr) from returning NULL when there are no rows.
ORDER BY timestamp_sent DESC"),
+ ") FROM msgs WHERE rfc724_mid=? ORDER BY timestamp_sent DESC"),
(rfc724_mid,),
|row| {
let msg_id: MsgId = row.get(0)?;
@@ -2356,25 +2328,6 @@ mod tests {
assert_eq!(quoted_msg.get_text(), msg2.quoted_text().unwrap());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_quote() {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.send_recv_accept(alice, bob, "Hi!").await;
let msg = tcm
.send_recv(
alice,
bob,
"On 2024-08-28, Alice wrote:\n> A quote.\nNot really.",
)
.await;
assert!(msg.quoted_text().is_none());
assert!(msg.quoted_message(bob).await.unwrap().is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unencrypted_quote_encrypted_message() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -2612,6 +2565,9 @@ mod tests {
let payload = alice.pop_sent_msg().await;
assert_state(&alice, alice_msg.id, MessageState::OutDelivered).await;
update_msg_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await?;
assert_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await;
set_msg_failed(&alice, &mut alice_msg, "badly failed").await?;
assert_state(&alice, alice_msg.id, MessageState::OutFailed).await;

View File

@@ -82,10 +82,7 @@ pub struct MimeFactory {
/// as needed.
references: String,
/// True if the message requests Message Disposition Notification
/// using `Chat-Disposition-Notification-To` header.
req_mdn: bool,
last_added_location_id: Option<u32>,
/// If the created mime-structure contains sync-items,
@@ -107,8 +104,10 @@ pub struct RenderedEmail {
pub is_gossiped: bool,
pub last_added_location_id: Option<u32>,
/// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted
/// from `multi_device_sync` once the message is actually queued for sending.
/// A comma-separated string of sync-IDs that are used by the rendered email
/// and must be deleted once the message is actually queued for sending
/// (deletion must be done by `delete_sync_ids()`).
/// If the rendered email is not queued for sending, the IDs must not be deleted.
pub sync_ids_to_delete: Option<String>,
/// Message ID (Message in the sense of Email)
@@ -118,13 +117,6 @@ pub struct RenderedEmail {
pub subject: String,
}
fn new_address_with_name(name: &str, address: String) -> Address {
match name == address {
true => Address::new_mailbox(address),
false => Address::new_mailbox_with_name(name.to_string(), address),
}
}
impl MimeFactory {
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
let chat = Chat::load_from_db(context, msg.chat_id).await?;
@@ -151,9 +143,7 @@ impl MimeFactory {
let mut req_mdn = false;
if chat.is_self_talk() {
if msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
recipients.push((from_displayname.to_string(), from_addr.to_string()));
}
recipients.push((from_displayname.to_string(), from_addr.to_string()));
} else if chat.is_mailing_list() {
let list_post = chat
.param
@@ -196,7 +186,7 @@ impl MimeFactory {
if !msg.is_system_message()
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
&& context.should_request_mdns().await?
&& context.get_config_bool(Config::MdnsEnabled).await?
{
req_mdn = true;
}
@@ -204,8 +194,7 @@ impl MimeFactory {
let (in_reply_to, references) = context
.sql
.query_row(
"SELECT mime_in_reply_to, IFNULL(mime_references, '')
FROM msgs WHERE id=?",
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
(msg.id,),
|row| {
let in_reply_to: String = row.get(0)?;
@@ -355,11 +344,7 @@ impl MimeFactory {
// beside key- and member-changes, force a periodic re-gossip.
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
// `gossip_period == 0` is a special case for testing,
// enabling gossip in every message.
// Othewise "smeared timestamps" may result in the condition
// to fail even if the clock is monotonic.
if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period {
if time() >= gossiped_timestamp + gossip_period {
Ok(true)
} else {
Ok(false)
@@ -487,7 +472,10 @@ impl MimeFactory {
pub async fn render(mut self, context: &Context) -> Result<RenderedEmail> {
let mut headers = Vec::<Header>::new();
let from = new_address_with_name(&self.from_displayname, self.from_addr.clone());
let from = Address::new_mailbox_with_name(
self.from_displayname.to_string(),
self.from_addr.clone(),
);
let undisclosed_recipients = match &self.loaded {
Loaded::Message { chat, .. } => chat.typ == Chattype::Broadcast,
@@ -522,7 +510,10 @@ impl MimeFactory {
if name.is_empty() {
to.push(Address::new_mailbox(addr.clone()));
} else {
to.push(new_address_with_name(name, addr.clone()));
to.push(Address::new_mailbox_with_name(
name.to_string(),
addr.clone(),
));
}
}
@@ -537,7 +528,8 @@ impl MimeFactory {
headers.push(from_header.clone());
if let Some(sender_displayname) = &self.sender_displayname {
let sender = new_address_with_name(sender_displayname, self.from_addr.clone());
let sender =
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
headers.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
}
headers.push(Header::new_with_value("To".into(), to.clone()).unwrap());
@@ -587,16 +579,6 @@ impl MimeFactory {
"Auto-Submitted".to_string(),
"auto-generated".to_string(),
));
} else if let Loaded::Message { msg, .. } = &self.loaded {
if msg.param.get_cmd() == SystemMessage::SecurejoinMessage {
let step = msg.param.get(Param::Arg).unwrap_or_default();
if step != "vg-request" && step != "vc-request" {
headers.push(Header::new(
"Auto-Submitted".to_string(),
"auto-replied".to_string(),
));
}
}
}
if let Loaded::Message { chat, .. } = &self.loaded {
@@ -617,9 +599,7 @@ impl MimeFactory {
// because replies to "Disposition-Notification-To" are weird in many cases
// eg. are just freetext and/or do not follow any standard.
headers.push(Header::new(
HeaderDef::ChatDispositionNotificationTo
.get_headername()
.to_string(),
"Chat-Disposition-Notification-To".into(),
self.from_addr.clone(),
));
}
@@ -746,18 +726,18 @@ impl MimeFactory {
} else if header_name == "autocrypt" {
unprotected_headers.push(header.clone());
} else if header_name == "from" {
// Unencrypted securejoin messages should _not_ include the display name:
if is_encrypted || !is_securejoin_message {
protected_headers.push(header.clone());
protected_headers.push(header.clone());
if is_encrypted && verified || is_securejoin_message {
unprotected_headers.push(
Header::new_with_value(
header.name,
vec![Address::new_mailbox(self.from_addr.clone())],
)
.unwrap(),
);
} else {
unprotected_headers.push(header);
}
unprotected_headers.push(
Header::new_with_value(
header.name,
vec![Address::new_mailbox(self.from_addr.clone())],
)
.unwrap(),
);
} else if header_name == "to" {
protected_headers.push(header.clone());
if is_encrypted {
@@ -922,11 +902,12 @@ impl MimeFactory {
.fold(message, |message, header| message.header(header.clone()));
if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? {
// Deduplicate unprotected headers that also are in the protected headers:
let protected: HashSet<&str> =
HashSet::from_iter(protected_headers.iter().map(|h| h.name.as_str()));
unprotected_headers.retain(|h| !protected.contains(&h.name.as_str()));
let protected: HashSet<Header> = HashSet::from_iter(protected_headers.into_iter());
for h in unprotected_headers.split_off(0) {
if !protected.contains(&h) {
unprotected_headers.push(h);
}
}
message
} else {
let message = message.header(get_content_type_directives_header());
@@ -1684,7 +1665,10 @@ mod tests {
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' '));
let s = format!("{}", new_address_with_name(display_name, addr.to_string()));
let s = format!(
"{}",
Address::new_mailbox_with_name(display_name.to_string(), addr.to_string())
);
println!("{s}");
@@ -1701,19 +1685,15 @@ mod tests {
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' '));
let s = format!("{}", new_address_with_name(display_name, addr.to_string()));
let s = format!(
"{}",
Address::new_mailbox_with_name(display_name.to_string(), addr.to_string())
);
// Addresses should not be unnecessarily be encoded, see <https://github.com/deltachat/deltachat-core-rust/issues/1575>:
assert_eq!(s, "a space <x@y.org>");
}
#[test]
fn test_render_email_address_duplicated_as_name() {
let addr = "x@y.org";
let s = format!("{}", new_address_with_name(addr, addr.to_string()));
assert_eq!(s, "<x@y.org>");
}
#[test]
fn test_render_rfc724_mid() {
assert_eq!(
@@ -2255,7 +2235,7 @@ mod tests {
if name.is_empty() {
Address::new_mailbox(addr.to_string())
} else {
new_address_with_name(name, addr.to_string())
Address::new_mailbox_with_name(name.to_string(), addr.to_string())
}
})
.collect();

View File

@@ -17,7 +17,7 @@ use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
use crate::chat::{add_info_msg, ChatId};
use crate::config::Config;
use crate::constants::{self, Chattype};
use crate::constants::{self, Chattype, DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::decrypt::{
@@ -28,13 +28,16 @@ use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
use crate::message::{self, get_vcard_summary, set_msg_failed, Message, MsgId, Viewtype};
use crate::message::{
self, get_vcard_summary, set_msg_failed, update_msg_state, Message, MessageState, MsgId,
Viewtype,
};
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::simplify::{simplify, SimplifiedText};
use crate::sync::SyncItems;
use crate::tools::{
create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time, truncate_msg_text,
create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time, truncate_by_lines,
validate_id,
};
use crate::{chatlist_events, location, stock_str, tools};
@@ -1179,11 +1182,22 @@ impl MimeMessage {
(simplified_txt, top_quote)
};
let (simplified_txt, was_truncated) =
truncate_msg_text(context, simplified_txt).await?;
if was_truncated {
self.is_mime_modified = was_truncated;
}
let is_bot = context.get_config_bool(Config::Bot).await?;
let simplified_txt = if is_bot {
simplified_txt
} else {
// Truncate text if it has too many lines
let (simplified_txt, was_truncated) = truncate_by_lines(
simplified_txt,
DC_DESIRED_TEXT_LINES,
DC_DESIRED_TEXT_LINE_LEN,
);
if was_truncated {
self.is_mime_modified = was_truncated;
}
simplified_txt
};
if !simplified_txt.is_empty() || simplified_quote.is_some() {
let mut part = Part {
@@ -2143,32 +2157,24 @@ async fn handle_mdn(
return Ok(());
}
let Some((msg_id, chat_id, has_mdns, is_dup)) = context
let Some((msg_id, chat_id, msg_state)) = context
.sql
.query_row_optional(
concat!(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" mdns.contact_id AS mdn_contact",
" FROM msgs m ",
" LEFT JOIN chats c ON m.chat_id=c.id",
" LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
" m.state AS state",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" WHERE rfc724_mid=? AND from_id=1",
" ORDER BY msg_id DESC, mdn_contact=? DESC",
" LIMIT 1",
" ORDER BY m.id"
),
(&rfc724_mid, from_id),
(&rfc724_mid,),
|row| {
let msg_id: MsgId = row.get("msg_id")?;
let chat_id: ChatId = row.get("chat_id")?;
let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
Ok((
msg_id,
chat_id,
mdn_contact.is_some(),
mdn_contact == Some(from_id),
))
let msg_state: MessageState = row.get("state")?;
Ok((msg_id, chat_id, msg_state))
},
)
.await?
@@ -2180,17 +2186,28 @@ async fn handle_mdn(
return Ok(());
};
if is_dup {
return Ok(());
}
context
if !context
.sql
.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
(msg_id, from_id, timestamp_sent),
.exists(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?",
(msg_id, from_id),
)
.await?;
if !has_mdns {
.await?
{
context
.sql
.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
(msg_id, from_id, timestamp_sent),
)
.await?;
}
if msg_state == MessageState::OutPreparing
|| msg_state == MessageState::OutPending
|| msg_state == MessageState::OutDelivered
{
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await?;
context.emit_event(EventType::MsgRead { chat_id, msg_id });
// note(treefit): only matters if it is the last message in chat (but probably too expensive to check, debounce also solves it)
chatlist_events::emit_chatlist_item_changed(context, chat_id);
@@ -2298,7 +2315,7 @@ mod tests {
chat,
chatlist::Chatlist,
constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
message::{MessageState, MessengerMessage},
message::MessengerMessage,
receive_imf::receive_imf,
test_utils::{TestContext, TestContextManager},
tools::time,
@@ -3598,17 +3615,6 @@ On 2020-10-25, Bob wrote:
assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
}
{
let chat = t.get_self_chat().await;
t.send_text(chat.id, &long_txt).await;
let msg = t.get_last_msg_in(chat.id).await;
assert!(msg.has_html());
assert!(
msg.text.matches("just repeated").count() <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
);
assert!(msg.text.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
}
t.set_config(Config::Bot, Some("1")).await?;
{

View File

@@ -1,23 +1,20 @@
//! # Common network utilities.
use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use std::time::Duration;
use anyhow::{format_err, Context as _, Result};
use async_native_tls::TlsStream;
use tokio::io::BufStream;
use tokio::io::BufWriter;
use tokio::net::TcpStream;
use tokio::task::JoinSet;
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::sql::Sql;
use crate::tools::time;
pub(crate) mod dns;
pub(crate) mod http;
pub(crate) mod proxy;
pub(crate) mod session;
pub(crate) mod tls;
@@ -30,82 +27,12 @@ use tls::wrap_tls;
/// This constant should be more than the largest expected RTT.
pub(crate) const TIMEOUT: Duration = Duration::from_secs(60);
/// TTL for caches in seconds.
pub(crate) const CACHE_TTL: u64 = 30 * 24 * 60 * 60;
/// Removes connection history entries after `CACHE_TTL`.
pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
let now = time();
context
.sql
.execute(
"DELETE FROM connection_history
WHERE ? > timestamp + ?",
(now, CACHE_TTL),
)
.await?;
Ok(())
}
/// Update the timestamp of the last successfull connection
/// to the given `host` and `port`
/// with the given application protocol `alpn`.
///
/// `addr` is the string representation of IP address.
/// If connection is made over a proxy which does
/// its own DNS resolution,
/// `addr` should be the same as `host`.
pub(crate) async fn update_connection_history(
context: &Context,
alpn: &str,
host: &str,
port: u16,
addr: &str,
now: i64,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO connection_history (host, port, alpn, addr, timestamp)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (host, port, alpn, addr)
DO UPDATE SET timestamp=excluded.timestamp",
(host, port, alpn, addr, now),
)
.await?;
Ok(())
}
/// Returns timestamp of the most recent successful connection
/// to the host and port for given protocol.
pub(crate) async fn load_connection_timestamp(
sql: &Sql,
alpn: &str,
host: &str,
port: u16,
addr: Option<&str>,
) -> Result<Option<i64>> {
let timestamp = sql
.query_get_value(
"SELECT timestamp FROM connection_history
WHERE host = ?
AND port = ?
AND alpn = ?
AND addr = IFNULL(?, addr)",
(host, port, alpn, addr),
)
.await?;
Ok(timestamp)
}
/// Returns a TCP connection stream with read/write timeouts set
/// and Nagle's algorithm disabled with `TCP_NODELAY`.
///
/// `TCP_NODELAY` ensures writing to the stream always results in immediate sending of the packet
/// to the network, which is important to reduce the latency of interactive protocols such as IMAP.
pub(crate) async fn connect_tcp_inner(
addr: SocketAddr,
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
async fn connect_tcp_inner(addr: SocketAddr) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
let tcp_stream = timeout(TIMEOUT, TcpStream::connect(addr))
.await
.context("connection timeout")?
@@ -123,107 +50,17 @@ pub(crate) async fn connect_tcp_inner(
/// Attempts to establish TLS connection
/// given the result of the hostname to address resolution.
pub(crate) async fn connect_tls_inner(
async fn connect_tls_inner(
addr: SocketAddr,
host: &str,
strict_tls: bool,
alpn: &[&str],
alpn: &str,
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp_inner(addr).await?;
let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?;
Ok(tls_stream)
}
/// Runs connection attempt futures.
///
/// Accepts iterator of connection attempt futures
/// and runs them until one of them succeeds
/// or all of them fail.
///
/// If all connection attempts fail, returns the first error.
///
/// This functions starts with one connection attempt and maintains
/// up to five parallel connection attempts if connecting takes time.
pub(crate) async fn run_connection_attempts<O, I, F>(mut futures: I) -> Result<O>
where
I: Iterator<Item = F>,
F: Future<Output = Result<O>> + Send + 'static,
O: Send + 'static,
{
let mut connection_attempt_set = JoinSet::new();
// Start additional connection attempts after 300 ms, 1 s, 5 s and 10 s.
// This way we can have up to 5 parallel connection attempts at the same time.
let mut delay_set = JoinSet::new();
for delay in [
Duration::from_millis(300),
Duration::from_secs(1),
Duration::from_secs(5),
Duration::from_secs(10),
] {
delay_set.spawn(tokio::time::sleep(delay));
}
let mut first_error = None;
let res = loop {
if let Some(fut) = futures.next() {
connection_attempt_set.spawn(fut);
}
tokio::select! {
biased;
res = connection_attempt_set.join_next() => {
match res {
Some(res) => {
match res.context("Failed to join task") {
Ok(Ok(conn)) => {
// Successfully connected.
break Ok(conn);
}
Ok(Err(err)) => {
// Some connection attempt failed.
first_error.get_or_insert(err);
}
Err(err) => {
break Err(err);
}
}
}
None => {
// Out of connection attempts.
//
// Break out of the loop and return error.
break Err(
first_error.unwrap_or_else(|| format_err!("No connection attempts were made"))
);
}
}
},
_ = delay_set.join_next(), if !delay_set.is_empty() => {
// Delay expired.
//
// Don't do anything other than pushing
// another connection attempt into `connection_attempt_set`.
}
}
};
// Abort remaining connection attempts and free resources
// such as OS sockets and `Context` references
// held by connection attempt tasks.
//
// `delay_set` contains just `sleep` tasks
// so no need to await futures there,
// it is enough that futures are aborted
// when the set is dropped.
connection_attempt_set.shutdown().await;
res
}
/// If `load_cache` is true, may use cached DNS results.
/// Because the cache may be poisoned with incorrect results by networks hijacking DNS requests,
/// this option should only be used when connection is authenticated,
@@ -236,9 +73,149 @@ pub(crate) async fn connect_tcp(
port: u16,
load_cache: bool,
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
let connection_futures = lookup_host_with_cache(context, host, port, "", load_cache)
.await?
.into_iter()
.map(connect_tcp_inner);
run_connection_attempts(connection_futures).await
let mut first_error = None;
for resolved_addr in lookup_host_with_cache(context, host, port, load_cache).await? {
match connect_tcp_inner(resolved_addr).await {
Ok(stream) => {
return Ok(stream);
}
Err(err) => {
warn!(
context,
"Failed to connect to {}: {:#}.", resolved_addr, err
);
first_error.get_or_insert(err);
}
}
}
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
}
pub(crate) async fn connect_tls(
context: &Context,
host: &str,
port: u16,
strict_tls: bool,
alpn: &str,
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let mut first_error = None;
for resolved_addr in lookup_host_with_cache(context, host, port, strict_tls).await? {
match connect_tls_inner(resolved_addr, host, strict_tls, alpn).await {
Ok(tls_stream) => {
if strict_tls {
dns::update_connect_timestamp(context, host, &resolved_addr.ip().to_string())
.await?;
}
return Ok(tls_stream);
}
Err(err) => {
warn!(context, "Failed to connect to {resolved_addr}: {err:#}.");
first_error.get_or_insert(err);
}
}
}
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
}
async fn connect_starttls_imap_inner(
addr: SocketAddr,
host: &str,
strict_tls: bool,
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp_inner(addr).await?;
// Run STARTTLS command and convert the client back into a stream.
let buffered_tcp_stream = BufWriter::new(tcp_stream);
let mut client = async_imap::Client::new(buffered_tcp_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")??;
client
.run_command_and_check_ok("STARTTLS", None)
.await
.context("STARTTLS command failed")?;
let buffered_tcp_stream = client.into_inner();
let tcp_stream = buffered_tcp_stream.into_inner();
let tls_stream = wrap_tls(strict_tls, host, "imap", tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
Ok(tls_stream)
}
pub(crate) async fn connect_starttls_imap(
context: &Context,
host: &str,
port: u16,
strict_tls: bool,
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let mut first_error = None;
for resolved_addr in lookup_host_with_cache(context, host, port, strict_tls).await? {
match connect_starttls_imap_inner(resolved_addr, host, strict_tls).await {
Ok(tls_stream) => {
if strict_tls {
dns::update_connect_timestamp(context, host, &resolved_addr.ip().to_string())
.await?;
}
return Ok(tls_stream);
}
Err(err) => {
warn!(context, "Failed to connect to {resolved_addr}: {err:#}.");
first_error.get_or_insert(err);
}
}
}
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
}
async fn connect_starttls_smtp_inner(
addr: SocketAddr,
host: &str,
strict_tls: bool,
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp_inner(addr).await?;
// Run STARTTLS command and convert the client back into a stream.
let client = async_smtp::SmtpClient::new().smtp_utf8(true);
let transport = async_smtp::SmtpTransport::new(client, BufStream::new(tcp_stream)).await?;
let tcp_stream = transport.starttls().await?.into_inner();
let tls_stream = wrap_tls(strict_tls, host, "smtp", tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
Ok(tls_stream)
}
pub(crate) async fn connect_starttls_smtp(
context: &Context,
host: &str,
port: u16,
strict_tls: bool,
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let mut first_error = None;
for resolved_addr in lookup_host_with_cache(context, host, port, strict_tls).await? {
match connect_starttls_smtp_inner(resolved_addr, host, strict_tls).await {
Ok(tls_stream) => {
if strict_tls {
dns::update_connect_timestamp(context, host, &resolved_addr.ip().to_string())
.await?;
}
return Ok(tls_stream);
}
Err(err) => {
warn!(context, "Failed to connect to {resolved_addr}: {err:#}.");
first_error.get_or_insert(err);
}
}
}
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,21 @@
//! # HTTP module.
use anyhow::{anyhow, bail, Context as _, Result};
use bytes::Bytes;
use http_body_util::BodyExt;
use hyper_util::rt::TokioIo;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use mime::Mime;
use serde::Serialize;
use once_cell::sync::Lazy;
use crate::context::Context;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_rustls;
use crate::net::lookup_host_with_cache;
use crate::socks::Socks5Config;
static LETSENCRYPT_ROOT: Lazy<reqwest::tls::Certificate> = Lazy::new(|| {
reqwest::tls::Certificate::from_der(include_bytes!(
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
))
.unwrap()
});
/// HTTP(S) GET response.
#[derive(Debug)]
@@ -27,94 +32,48 @@ pub struct Response {
/// Retrieves the text contents of URL using HTTP GET request.
pub async fn read_url(context: &Context, url: &str) -> Result<String> {
let response = read_url_blob(context, url).await?;
let text = String::from_utf8_lossy(&response.blob);
Ok(text.to_string())
}
async fn get_http_sender<B>(
context: &Context,
parsed_url: hyper::Uri,
) -> Result<hyper::client::conn::http1::SendRequest<B>>
where
B: hyper::body::Body + 'static + Send,
B::Data: Send,
B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
{
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
let host = parsed_url.host().context("URL has no host")?;
let proxy_config_opt = ProxyConfig::load(context).await?;
let stream: Box<dyn SessionStream> = match scheme {
"http" => {
let port = parsed_url.port_u16().unwrap_or(80);
// It is safe to use cached IP addresses
// for HTTPS URLs, but for HTTP URLs
// better resolve from scratch each time to prevent
// cache poisoning attacks from having lasting effects.
let load_cache = false;
if let Some(proxy_config) = proxy_config_opt {
let proxy_stream = proxy_config
.connect(context, host, port, load_cache)
.await?;
Box::new(proxy_stream)
} else {
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
Box::new(tcp_stream)
}
}
"https" => {
let port = parsed_url.port_u16().unwrap_or(443);
let load_cache = true;
if let Some(proxy_config) = proxy_config_opt {
let proxy_stream = proxy_config
.connect(context, host, port, load_cache)
.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, &[], tcp_stream).await?;
Box::new(tls_stream)
}
}
_ => bail!("Unknown URL scheme"),
};
let io = TokioIo::new(stream);
let (sender, conn) = hyper::client::conn::http1::handshake(io).await?;
tokio::task::spawn(conn);
Ok(sender)
Ok(read_url_inner(context, url).await?.text().await?)
}
/// Retrieves the binary contents of URL using HTTP GET request.
pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
let response = read_url_inner(context, url).await?;
let content_type = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok());
let mimetype = content_type
.as_ref()
.map(|mime| mime.essence_str().to_string());
let encoding = content_type.as_ref().and_then(|mime| {
mime.get_param(mime::CHARSET)
.map(|charset| charset.as_str().to_string())
});
let blob: Vec<u8> = response.bytes().await?.into();
Ok(Response {
blob,
mimetype,
encoding,
})
}
async fn read_url_inner(context: &Context, url: &str) -> Result<reqwest::Response> {
// It is safe to use cached IP addresses
// for HTTPS URLs, but for HTTP URLs
// better resolve from scratch each time to prevent
// cache poisoning attacks from having lasting effects.
let load_cache = url.starts_with("https://");
let client = get_client(context, load_cache).await?;
let mut url = url.to_string();
// Follow up to 10 http-redirects
for _i in 0..10 {
let parsed_url = url
.parse::<hyper::Uri>()
.with_context(|| format!("Failed to parse URL {url:?}"))?;
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
let authority = parsed_url
.authority()
.context("URL has no authority")?
.clone();
let req = hyper::Request::builder()
.uri(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.body(http_body_util::Empty::<Bytes>::new())?;
let response = sender.send_request(req).await?;
let response = client.get(&url).send().await?;
if response.status().is_redirection() {
let header = response
.headers()
let headers = response.headers();
let header = headers
.get_all("location")
.iter()
.last()
@@ -125,119 +84,72 @@ pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
continue;
}
let content_type = response
.headers()
.get("content-type")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok());
let mimetype = content_type
.as_ref()
.map(|mime| mime.essence_str().to_string());
let encoding = content_type.as_ref().and_then(|mime| {
mime.get_param(mime::CHARSET)
.map(|charset| charset.as_str().to_string())
});
let body = response.collect().await?.to_bytes();
let blob: Vec<u8> = body.to_vec();
return Ok(Response {
blob,
mimetype,
encoding,
});
return Ok(response);
}
Err(anyhow!("Followed 10 redirections"))
}
/// Sends an empty POST request to the URL.
///
/// Returns response text and whether request was successful or not.
///
/// Does not follow redirects.
pub(crate) async fn post_empty(context: &Context, url: &str) -> Result<(String, bool)> {
let parsed_url = url
.parse::<hyper::Uri>()
.with_context(|| format!("Failed to parse URL {url:?}"))?;
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
if scheme != "https" {
bail!("POST requests to non-HTTPS URLs are not allowed");
}
struct CustomResolver {
context: Context,
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
let authority = parsed_url
.authority()
.context("URL has no authority")?
.clone();
let req = hyper::Request::post(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.body(http_body_util::Empty::<Bytes>::new())?;
let response = sender.send_request(req).await?;
let response_status = response.status();
let body = response.collect().await?.to_bytes();
let text = String::from_utf8_lossy(&body);
let response_text = text.to_string();
Ok((response_text, response_status.is_success()))
/// Whether to return cached results or not.
/// If resolver can be used for URLs
/// without TLS, e.g. HTTP URLs from HTML email,
/// this must be false. If TLS is used
/// and certificate hostnames are checked,
/// it is safe to load cache.
load_cache: bool,
}
/// Posts string to the given URL.
///
/// Returns true if successful HTTP response code was returned.
///
/// Does not follow redirects.
#[allow(dead_code)]
pub(crate) async fn post_string(context: &Context, url: &str, body: String) -> Result<bool> {
let parsed_url = url
.parse::<hyper::Uri>()
.with_context(|| format!("Failed to parse URL {url:?}"))?;
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
if scheme != "https" {
bail!("POST requests to non-HTTPS URLs are not allowed");
impl CustomResolver {
fn new(context: Context, load_cache: bool) -> Self {
Self {
context,
load_cache,
}
}
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
let authority = parsed_url
.authority()
.context("URL has no authority")?
.clone();
let request = hyper::Request::post(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.body(body)?;
let response = sender.send_request(request).await?;
Ok(response.status().is_success())
}
/// Sends a POST request with x-www-form-urlencoded data.
///
/// Does not follow redirects.
pub(crate) async fn post_form<T: Serialize + ?Sized>(
context: &Context,
url: &str,
form: &T,
) -> Result<Bytes> {
let parsed_url = url
.parse::<hyper::Uri>()
.with_context(|| format!("Failed to parse URL {url:?}"))?;
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
if scheme != "https" {
bail!("POST requests to non-HTTPS URLs are not allowed");
}
impl reqwest::dns::Resolve for CustomResolver {
fn resolve(&self, hostname: reqwest::dns::Name) -> reqwest::dns::Resolving {
let context = self.context.clone();
let load_cache = self.load_cache;
Box::pin(async move {
let port = 443; // Actual port does not matter.
let encoded_body = serde_urlencoded::to_string(form).context("Failed to encode data")?;
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
let authority = parsed_url
.authority()
.context("URL has no authority")?
.clone();
let request = hyper::Request::post(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.header("content-type", "application/x-www-form-urlencoded")
.body(encoded_body)?;
let response = sender.send_request(request).await?;
let bytes = response.collect().await?.to_bytes();
Ok(bytes)
let socket_addrs =
lookup_host_with_cache(&context, hostname.as_str(), port, load_cache).await;
match socket_addrs {
Ok(socket_addrs) => {
let addrs: reqwest::dns::Addrs = Box::new(socket_addrs.into_iter());
Ok(addrs)
}
Err(err) => Err(err.into()),
}
})
}
}
pub(crate) async fn get_client(context: &Context, load_cache: bool) -> Result<reqwest::Client> {
let socks5_config = Socks5Config::from_database(&context.sql).await?;
let resolver = Arc::new(CustomResolver::new(context.clone(), load_cache));
let builder = reqwest::ClientBuilder::new()
.timeout(super::TIMEOUT)
.add_root_certificate(LETSENCRYPT_ROOT.clone())
.dns_resolver(resolver);
let builder = if let Some(socks5_config) = socks5_config {
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
builder.proxy(proxy)
} else {
// Disable usage of "system" proxy configured via environment variables.
// It is enabled by default in `reqwest`, see
// <https://docs.rs/reqwest/0.11.14/reqwest/struct.ClientBuilder.html#method.no_proxy>
// for documentation.
builder.no_proxy()
};
Ok(builder.build()?)
}

View File

@@ -1,655 +0,0 @@
//! # Proxy support.
//!
//! Delta Chat supports HTTP(S) CONNECT, SOCKS5 and Shadowsocks protocols.
use std::fmt;
use std::pin::Pin;
use anyhow::{bail, format_err, Context as _, Result};
use base64::Engine;
use bytes::{BufMut, BytesMut};
use fast_socks5::client::Socks5Stream;
use fast_socks5::util::target_addr::ToTargetAddr;
use fast_socks5::AuthenticationMethod;
use fast_socks5::Socks5Command;
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio_io_timeout::TimeoutStream;
use url::Url;
use crate::config::Config;
use crate::context::Context;
use crate::net::connect_tcp;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_rustls;
use crate::sql::Sql;
/// Default SOCKS5 port according to [RFC 1928](https://tools.ietf.org/html/rfc1928).
pub const DEFAULT_SOCKS_PORT: u16 = 1080;
#[derive(Debug, Clone)]
pub struct ShadowsocksConfig {
pub server_config: shadowsocks::config::ServerConfig,
}
impl PartialEq for ShadowsocksConfig {
fn eq(&self, other: &Self) -> bool {
self.server_config.to_url() == other.server_config.to_url()
}
}
impl Eq for ShadowsocksConfig {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpConfig {
/// HTTP proxy host.
pub host: String,
/// HTTP proxy port.
pub port: u16,
/// Username and password for basic authentication.
///
/// If set, `Proxy-Authorization` header is sent.
pub user_password: Option<(String, String)>,
}
impl HttpConfig {
fn from_url(url: Url) -> Result<Self> {
let host = url
.host_str()
.context("HTTP proxy URL has no host")?
.to_string();
let port = url
.port_or_known_default()
.context("HTTP(S) URLs are guaranteed to return Some port")?;
let user_password = if let Some(password) = url.password() {
let username = percent_encoding::percent_decode_str(url.username())
.decode_utf8()
.context("HTTP(S) proxy username is not a valid UTF-8")?
.to_string();
let password = percent_encoding::percent_decode_str(password)
.decode_utf8()
.context("HTTP(S) proxy password is not a valid UTF-8")?
.to_string();
Some((username, password))
} else {
None
};
let http_config = HttpConfig {
host,
port,
user_password,
};
Ok(http_config)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Socks5Config {
pub host: String,
pub port: u16,
pub user_password: Option<(String, String)>,
}
impl Socks5Config {
async fn connect(
&self,
context: &Context,
target_host: &str,
target_port: u16,
load_dns_cache: bool,
) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache)
.await
.context("Failed to connect to SOCKS5 proxy")?;
let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
{
Some(AuthenticationMethod::Password {
username: username.into(),
password: password.into(),
})
} else {
None
};
let mut socks_stream =
Socks5Stream::use_stream(tcp_stream, authentication_method, Default::default()).await?;
let target_addr = (target_host, target_port).to_target_addr()?;
socks_stream
.request(Socks5Command::TCPConnect, target_addr)
.await?;
Ok(socks_stream)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProxyConfig {
// HTTP proxy.
Http(HttpConfig),
// HTTPS proxy.
Https(HttpConfig),
// SOCKS5 proxy.
Socks5(Socks5Config),
// Shadowsocks proxy.
Shadowsocks(ShadowsocksConfig),
}
/// Constructs HTTP/1.1 `CONNECT` request for HTTP(S) proxy.
fn http_connect_request(host: &str, port: u16, auth: Option<(&str, &str)>) -> String {
// According to <https://datatracker.ietf.org/doc/html/rfc7230#section-5.4>
// clients MUST send `Host:` header in HTTP/1.1 requests,
// so repeat the host there.
let mut res = format!("CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n");
if let Some((username, password)) = auth {
res += "Proxy-Authorization: Basic ";
res += &base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}"));
res += "\r\n";
}
res += "\r\n";
res
}
/// Sends HTTP/1.1 `CONNECT` request over given connection
/// to establish an HTTP tunnel.
///
/// Returns the same connection back so actual data can be tunneled over it.
async fn http_tunnel<T>(mut conn: T, host: &str, port: u16, auth: Option<(&str, &str)>) -> Result<T>
where
T: AsyncReadExt + AsyncWriteExt + Unpin,
{
// Send HTTP/1.1 CONNECT request.
let request = http_connect_request(host, port, auth);
conn.write_all(request.as_bytes()).await?;
let mut buffer = BytesMut::with_capacity(4096);
let res = loop {
if !buffer.has_remaining_mut() {
bail!("CONNECT response exceeded buffer size");
}
let n = conn.read_buf(&mut buffer).await?;
if n == 0 {
bail!("Unexpected end of CONNECT response");
}
let res = &buffer[..];
if res.ends_with(b"\r\n\r\n") {
// End of response is not reached, read more.
break res;
}
};
// Normally response looks like
// `HTTP/1.1 200 Connection established\r\n\r\n`.
if !res.starts_with(b"HTTP/") {
bail!("Unexpected HTTP CONNECT response: {res:?}");
}
// HTTP-version followed by space has fixed length
// according to RFC 7230:
// <https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2>
//
// Normally status line starts with `HTTP/1.1 `.
// We only care about 3-digit status code.
let status_code = res
.get(9..12)
.context("HTTP status line does not contain a status code")?;
// Interpert status code according to
// <https://datatracker.ietf.org/doc/html/rfc7231#section-6>.
if status_code == b"407" {
Err(format_err!("Proxy Authentication Required"))
} else if status_code.starts_with(b"2") {
// Success.
Ok(conn)
} else {
Err(format_err!(
"Failed to establish HTTP CONNECT tunnel: {res:?}"
))
}
}
impl ProxyConfig {
/// Creates a new proxy configuration by parsing given proxy URL.
fn from_url(url: &str) -> Result<Self> {
let url = Url::parse(url).context("Cannot parse proxy URL")?;
match url.scheme() {
"http" => {
let http_config = HttpConfig::from_url(url)?;
Ok(Self::Http(http_config))
}
"https" => {
let https_config = HttpConfig::from_url(url)?;
Ok(Self::Https(https_config))
}
"ss" => {
let server_config = shadowsocks::config::ServerConfig::from_url(url.as_str())?;
let shadowsocks_config = ShadowsocksConfig { server_config };
Ok(Self::Shadowsocks(shadowsocks_config))
}
// Because of `curl` convention,
// `socks5` URL scheme may be expected to resolve domain names locally
// with `socks5h` URL scheme meaning that hostnames are passed to the proxy.
// Resolving hostnames locally is not supported
// in Delta Chat when using a proxy
// to prevent DNS leaks.
// Because of this we do not distinguish
// between `socks5` and `socks5h`.
"socks5" => {
let host = url
.host_str()
.context("socks5 URL has no host")?
.to_string();
let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT);
let user_password = if let Some(password) = url.password() {
let username = percent_encoding::percent_decode_str(url.username())
.decode_utf8()
.context("SOCKS5 username is not a valid UTF-8")?
.to_string();
let password = percent_encoding::percent_decode_str(password)
.decode_utf8()
.context("SOCKS5 password is not a valid UTF-8")?
.to_string();
Some((username, password))
} else {
None
};
let socks5_config = Socks5Config {
host,
port,
user_password,
};
Ok(Self::Socks5(socks5_config))
}
scheme => Err(format_err!("Unknown URL scheme {scheme:?}")),
}
}
/// Migrates legacy `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password`
/// config into `proxy_url` if `proxy_url` is unset or empty.
///
/// Unsets `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password` in any case.
async fn migrate_socks_config(sql: &Sql) -> Result<()> {
if sql.get_raw_config("proxy_url").await?.is_none() {
// Load legacy SOCKS5 settings.
if let Some(host) = sql
.get_raw_config("socks5_host")
.await?
.filter(|s| !s.is_empty())
{
let port: u16 = sql
.get_raw_config_int("socks5_port")
.await?
.unwrap_or(DEFAULT_SOCKS_PORT.into()) as u16;
let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default();
let pass = sql
.get_raw_config("socks5_password")
.await?
.unwrap_or_default();
let mut proxy_url = "socks5://".to_string();
if !pass.is_empty() {
proxy_url += &percent_encode(user.as_bytes(), NON_ALPHANUMERIC).to_string();
proxy_url += ":";
proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
proxy_url += "@";
};
proxy_url += &host;
proxy_url += ":";
proxy_url += &port.to_string();
sql.set_raw_config("proxy_url", Some(&proxy_url)).await?;
} else {
sql.set_raw_config("proxy_url", Some("")).await?;
}
let socks5_enabled = sql.get_raw_config("socks5_enabled").await?;
sql.set_raw_config("proxy_enabled", socks5_enabled.as_deref())
.await?;
}
sql.set_raw_config("socks5_enabled", None).await?;
sql.set_raw_config("socks5_host", None).await?;
sql.set_raw_config("socks5_port", None).await?;
sql.set_raw_config("socks5_user", None).await?;
sql.set_raw_config("socks5_password", None).await?;
Ok(())
}
/// Reads proxy configuration from the database.
pub async fn load(context: &Context) -> Result<Option<Self>> {
Self::migrate_socks_config(&context.sql)
.await
.context("Failed to migrate legacy SOCKS config")?;
let enabled = context.get_config_bool(Config::ProxyEnabled).await?;
if !enabled {
return Ok(None);
}
let proxy_url = context
.get_config(Config::ProxyUrl)
.await?
.unwrap_or_default();
let proxy_url = proxy_url
.split_once('\n')
.map_or(proxy_url.clone(), |(first_url, _rest)| {
first_url.to_string()
});
let proxy_config = Self::from_url(&proxy_url).context("Failed to parse proxy URL")?;
Ok(Some(proxy_config))
}
/// If `load_dns_cache` is true, loads cached DNS resolution results.
/// Use this only if the connection is going to be protected with TLS checks.
pub async fn connect(
&self,
context: &Context,
target_host: &str,
target_port: u16,
load_dns_cache: bool,
) -> Result<Box<dyn SessionStream>> {
match self {
ProxyConfig::Http(http_config) => {
let load_cache = false;
let tcp_stream = crate::net::connect_tcp(
context,
&http_config.host,
http_config.port,
load_cache,
)
.await?;
let auth = if let Some((username, password)) = &http_config.user_password {
Some((username.as_str(), password.as_str()))
} else {
None
};
let tunnel_stream = http_tunnel(tcp_stream, target_host, target_port, auth).await?;
Ok(Box::new(tunnel_stream))
}
ProxyConfig::Https(https_config) => {
let load_cache = true;
let tcp_stream = crate::net::connect_tcp(
context,
&https_config.host,
https_config.port,
load_cache,
)
.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 {
None
};
let tunnel_stream = http_tunnel(tls_stream, target_host, target_port, auth).await?;
Ok(Box::new(tunnel_stream))
}
ProxyConfig::Socks5(socks5_config) => {
let socks5_stream = socks5_config
.connect(context, target_host, target_port, load_dns_cache)
.await?;
Ok(Box::new(socks5_stream))
}
ProxyConfig::Shadowsocks(ShadowsocksConfig { server_config }) => {
let shadowsocks_context = shadowsocks::context::Context::new_shared(
shadowsocks::config::ServerType::Local,
);
let tcp_stream = {
let server_addr = server_config.addr();
let host = server_addr.host();
let port = server_addr.port();
connect_tcp(context, &host, port, load_dns_cache)
.await
.context("Failed to connect to Shadowsocks proxy")?
};
let shadowsocks_stream = shadowsocks::ProxyClientStream::from_stream(
shadowsocks_context,
tcp_stream,
server_config,
(target_host.to_string(), target_port),
);
Ok(Box::new(shadowsocks_stream))
}
}
}
}
impl fmt::Display for Socks5Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"host:{},port:{},user_password:{}",
self.host,
self.port,
if let Some(user_password) = self.user_password.clone() {
format!("user: {}, password: ***", user_password.0)
} else {
"user: None".to_string()
}
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::test_utils::TestContext;
#[test]
fn test_socks5_url() {
let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:9050").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(),
port: 9050,
user_password: None
})
);
let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(),
port: 9150,
user_password: Some(("foo".to_string(), "bar".to_string()))
})
);
let proxy_config = ProxyConfig::from_url("socks5://%66oo:b%61r@127.0.0.1:9150").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(),
port: 9150,
user_password: Some(("foo".to_string(), "bar".to_string()))
})
);
let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:80").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(),
port: 80,
user_password: None
})
);
let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(),
port: 1080,
user_password: None
})
);
let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:1080").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(),
port: 1080,
user_password: None
})
);
}
#[test]
fn test_http_url() {
let proxy_config = ProxyConfig::from_url("http://127.0.0.1").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Http(HttpConfig {
host: "127.0.0.1".to_string(),
port: 80,
user_password: None
})
);
let proxy_config = ProxyConfig::from_url("http://127.0.0.1:80").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Http(HttpConfig {
host: "127.0.0.1".to_string(),
port: 80,
user_password: None
})
);
let proxy_config = ProxyConfig::from_url("http://127.0.0.1:443").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Http(HttpConfig {
host: "127.0.0.1".to_string(),
port: 443,
user_password: None
})
);
}
#[test]
fn test_https_url() {
let proxy_config = ProxyConfig::from_url("https://127.0.0.1").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Https(HttpConfig {
host: "127.0.0.1".to_string(),
port: 443,
user_password: None
})
);
let proxy_config = ProxyConfig::from_url("https://127.0.0.1:80").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Https(HttpConfig {
host: "127.0.0.1".to_string(),
port: 80,
user_password: None
})
);
let proxy_config = ProxyConfig::from_url("https://127.0.0.1:443").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Https(HttpConfig {
host: "127.0.0.1".to_string(),
port: 443,
user_password: None
})
);
}
#[test]
fn test_http_connect_request() {
assert_eq!(http_connect_request("example.org", 143, Some(("aladdin", "opensesame"))), "CONNECT example.org:143 HTTP/1.1\r\nHost: example.org:143\r\nProxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l\r\n\r\n");
assert_eq!(
http_connect_request("example.net", 587, None),
"CONNECT example.net:587 HTTP/1.1\r\nHost: example.net:587\r\n\r\n"
);
}
#[test]
fn test_shadowsocks_url() {
// Example URL from <https://shadowsocks.org/doc/sip002.html>.
let proxy_config =
ProxyConfig::from_url("ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1")
.unwrap();
assert!(matches!(proxy_config, ProxyConfig::Shadowsocks(_)));
}
#[test]
fn test_invalid_proxy_url() {
assert!(ProxyConfig::from_url("foobar://127.0.0.1:9050").is_err());
assert!(ProxyConfig::from_url("abc").is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_socks5_migration() -> Result<()> {
let t = TestContext::new().await;
// Test that config is migrated on attempt to load even if disabled.
t.set_config(Config::Socks5Host, Some("127.0.0.1")).await?;
t.set_config(Config::Socks5Port, Some("9050")).await?;
let proxy_config = ProxyConfig::load(&t).await?;
// Even though proxy is not enabled, config should be migrated.
assert_eq!(proxy_config, None);
assert_eq!(
t.get_config(Config::ProxyUrl).await?.unwrap(),
"socks5://127.0.0.1:9050"
);
Ok(())
}
// Test SOCKS5 setting migration if proxy was never configured.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_socks5_migration_unconfigured() -> Result<()> {
let t = TestContext::new().await;
// Try to load config to trigger migration.
assert_eq!(ProxyConfig::load(&t).await?, None);
assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
assert_eq!(
t.get_config(Config::ProxyUrl).await?.unwrap(),
String::new()
);
Ok(())
}
// Test SOCKS5 setting migration if SOCKS5 host is empty.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_socks5_migration_empty() -> Result<()> {
let t = TestContext::new().await;
t.set_config(Config::Socks5Host, Some("")).await?;
// Try to load config to trigger migration.
assert_eq!(ProxyConfig::load(&t).await?, None);
assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
assert_eq!(
t.get_config(Config::ProxyUrl).await?.unwrap(),
String::new()
);
Ok(())
}
}

View File

@@ -1,3 +1,4 @@
use async_native_tls::TlsStream;
use fast_socks5::client::Socks5Stream;
use std::pin::Pin;
use std::time::Duration;
@@ -16,16 +17,11 @@ impl SessionStream for Box<dyn SessionStream> {
self.as_mut().set_read_timeout(timeout);
}
}
impl<T: SessionStream> SessionStream for async_native_tls::TlsStream<T> {
impl<T: SessionStream> SessionStream for TlsStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
}
impl<T: SessionStream> SessionStream for tokio_rustls::client::TlsStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().0.set_read_timeout(timeout);
}
}
impl<T: SessionStream> SessionStream for BufStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
@@ -48,16 +44,6 @@ impl<T: SessionStream> SessionStream for Socks5Stream<T> {
self.get_socket_mut().set_read_timeout(timeout)
}
}
impl<T: SessionStream> SessionStream for shadowsocks::ProxyClientStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout)
}
}
impl<T: SessionStream> SessionStream for async_imap::DeflateStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout)
}
}
/// Session stream with a read buffer.
pub(crate) trait SessionBufStream: SessionStream + AsyncBufRead {}

View File

@@ -1,5 +1,4 @@
//! TLS support.
use std::sync::Arc;
use anyhow::Result;
use async_native_tls::{Certificate, Protocol, TlsConnector, TlsStream};
@@ -15,42 +14,41 @@ static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
.unwrap()
});
pub async fn wrap_tls<T: AsyncRead + AsyncWrite + Unpin>(
strict_tls: bool,
hostname: &str,
alpn: &[&str],
stream: T,
) -> Result<TlsStream<T>> {
pub fn build_tls(strict_tls: bool, alpns: &[&str]) -> TlsConnector {
let tls_builder = TlsConnector::new()
.min_protocol_version(Some(Protocol::Tlsv12))
.request_alpns(alpn)
.request_alpns(alpns)
.add_root_certificate(LETSENCRYPT_ROOT.clone());
let tls = if strict_tls {
if strict_tls {
tls_builder
} else {
tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
};
}
}
pub async fn wrap_tls<T: AsyncRead + AsyncWrite + Unpin>(
strict_tls: bool,
hostname: &str,
alpn: &str,
stream: T,
) -> Result<TlsStream<T>> {
let tls = build_tls(strict_tls, &[alpn]);
let tls_stream = tls.connect(hostname, stream).await?;
Ok(tls_stream)
}
pub async fn wrap_rustls<T: AsyncRead + AsyncWrite + Unpin>(
hostname: &str,
alpn: &[&str],
stream: T,
) -> Result<tokio_rustls::client::TlsStream<T>> {
let mut root_cert_store = rustls::RootCertStore::empty();
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
#[cfg(test)]
mod tests {
use super::*;
let mut config = rustls::ClientConfig::builder()
.with_root_certificates(root_cert_store)
.with_no_client_auth();
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();
let tls_stream = tls.connect(name, stream).await?;
Ok(tls_stream)
#[test]
fn test_build_tls() {
// we are using some additional root certificates.
// make sure, they do not break construction of TlsConnector
let _ = build_tls(true, &[]);
let _ = build_tls(false, &[]);
}
}

View File

@@ -2,13 +2,12 @@
use std::collections::HashMap;
use anyhow::{Context as _, Result};
use anyhow::Result;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use crate::config::Config;
use crate::context::Context;
use crate::net::http::post_form;
use crate::net::read_url_blob;
use crate::provider;
use crate::provider::Oauth2Authorizer;
use crate::tools::time;
@@ -61,7 +60,8 @@ pub async fn get_oauth2_url(
addr: &str,
redirect_uri: &str,
) -> Result<Option<String>> {
if let Some(oauth2) = Oauth2::from_address(context, addr).await {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await {
context
.sql
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri))
@@ -81,7 +81,8 @@ pub(crate) async fn get_oauth2_access_token(
code: &str,
regenerate: bool,
) -> Result<Option<String>> {
if let Some(oauth2) = Oauth2::from_address(context, addr).await {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await {
let lock = context.oauth2_mutex.lock().await;
// read generated token
@@ -158,19 +159,25 @@ pub(crate) async fn get_oauth2_access_token(
// ... and POST
let response: Response = match post_form(context, post_url, &post_param).await {
Ok(resp) => match serde_json::from_slice(&resp) {
// All OAuth URLs are hardcoded HTTPS URLs,
// so it is safe to load DNS cache.
let load_cache = true;
let client = crate::net::http::get_client(context, load_cache).await?;
let response: Response = match client.post(post_url).form(&post_param).send().await {
Ok(resp) => match resp.json().await {
Ok(response) => response,
Err(err) => {
warn!(
context,
"Failed to parse OAuth2 JSON response from {token_url}: {err:#}."
"Failed to parse OAuth2 JSON response from {}: error: {}", token_url, err
);
return Ok(None);
}
},
Err(err) => {
warn!(context, "Error calling OAuth2 at {token_url}: {err:#}.");
warn!(context, "Error calling OAuth2 at {}: {:?}", token_url, err);
return Ok(None);
}
};
@@ -229,7 +236,8 @@ pub(crate) async fn get_oauth2_addr(
addr: &str,
code: &str,
) -> Result<Option<String>> {
let oauth2 = match Oauth2::from_address(context, addr).await {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
let oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await {
Some(o) => o,
None => return Ok(None),
};
@@ -238,20 +246,11 @@ pub(crate) async fn get_oauth2_addr(
}
if let Some(access_token) = get_oauth2_access_token(context, addr, code, false).await? {
let addr_out = match oauth2.get_addr(context, &access_token).await {
Ok(addr) => addr,
Err(err) => {
warn!(context, "Error getting addr: {err:#}.");
None
}
};
let addr_out = oauth2.get_addr(context, &access_token).await;
if addr_out.is_none() {
// regenerate
if let Some(access_token) = get_oauth2_access_token(context, addr, code, true).await? {
Ok(oauth2
.get_addr(context, &access_token)
.await
.unwrap_or_default())
Ok(oauth2.get_addr(context, &access_token).await)
} else {
Ok(None)
}
@@ -264,9 +263,8 @@ pub(crate) async fn get_oauth2_addr(
}
impl Oauth2 {
async fn from_address(context: &Context, addr: &str) -> Option<Self> {
async fn from_address(context: &Context, addr: &str, skip_mx: bool) -> Option<Self> {
let addr_normalized = normalize_addr(addr);
let skip_mx = true;
if let Some(domain) = addr_normalized
.find('@')
.map(|index| addr_normalized.split_at(index + 1).1)
@@ -284,7 +282,7 @@ impl Oauth2 {
None
}
async fn get_addr(&self, context: &Context, access_token: &str) -> Result<Option<String>> {
async fn get_addr(&self, context: &Context, access_token: &str) -> Option<String> {
let userinfo_url = self.get_userinfo.unwrap_or("");
let userinfo_url = replace_in_uri(userinfo_url, "$ACCESS_TOKEN", access_token);
@@ -296,21 +294,44 @@ impl Oauth2 {
// "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg"
// }
let response = read_url_blob(context, &userinfo_url).await?;
let parsed: HashMap<String, serde_json::Value> =
serde_json::from_slice(&response.blob).context("Error getting userinfo")?;
// All OAuth URLs are hardcoded HTTPS URLs,
// so it is safe to load DNS cache.
let load_cache = true;
let client = match crate::net::http::get_client(context, load_cache).await {
Ok(cl) => cl,
Err(err) => {
warn!(context, "failed to get HTTP client: {}", err);
return None;
}
};
let response = match client.get(userinfo_url).send().await {
Ok(response) => response,
Err(err) => {
warn!(context, "failed to get userinfo: {}", err);
return None;
}
};
let response: Result<HashMap<String, serde_json::Value>, _> = response.json().await;
let parsed = match response {
Ok(parsed) => parsed,
Err(err) => {
warn!(context, "Error getting userinfo: {}", err);
return None;
}
};
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
// but serde_json::Value.to_string() does not!
if let Some(addr) = parsed.get("email") {
if let Some(s) = addr.as_str() {
Ok(Some(s.to_string()))
Some(s.to_string())
} else {
warn!(context, "E-mail in userinfo is not a string: {}", addr);
Ok(None)
None
}
} else {
warn!(context, "E-mail missing in userinfo.");
Ok(None)
None
}
}
}
@@ -364,20 +385,38 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oauth_from_address() {
let t = TestContext::new().await;
// Delta Chat does not have working Gmail client ID anymore.
assert_eq!(Oauth2::from_address(&t, "hello@gmail.com").await, None);
assert_eq!(Oauth2::from_address(&t, "hello@googlemail.com").await, None);
assert_eq!(
Oauth2::from_address(&t, "hello@yandex.com").await,
Oauth2::from_address(&t, "hello@gmail.com", false).await,
Some(OAUTH2_GMAIL)
);
assert_eq!(
Oauth2::from_address(&t, "hello@googlemail.com", false).await,
Some(OAUTH2_GMAIL)
);
assert_eq!(
Oauth2::from_address(&t, "hello@yandex.com", false).await,
Some(OAUTH2_YANDEX)
);
assert_eq!(
Oauth2::from_address(&t, "hello@yandex.ru").await,
Oauth2::from_address(&t, "hello@yandex.ru", false).await,
Some(OAUTH2_YANDEX)
);
assert_eq!(Oauth2::from_address(&t, "hello@web.de").await, None);
assert_eq!(Oauth2::from_address(&t, "hello@web.de", false).await, None);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oauth_from_mx() {
// youtube staff seems to use "google workspace with oauth2", figures this out by MX lookup
let t = TestContext::new().await;
assert_eq!(
Oauth2::from_address(&t, "hello@youtube.com", false).await,
Some(OAUTH2_GMAIL)
);
// without MX lookup, we would not know as youtube.com is not in our provider-db
assert_eq!(
Oauth2::from_address(&t, "hello@youtube.com", true).await,
None
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -393,11 +432,11 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_oauth2_url() {
let ctx = TestContext::new().await;
let addr = "example@yandex.com";
let addr = "dignifiedquire@gmail.com";
let redirect_uri = "chat.delta:/com.b44t.messenger";
let res = get_oauth2_url(&ctx.ctx, addr, redirect_uri).await.unwrap();
assert_eq!(res, Some("https://oauth.yandex.com/authorize?client_id=c4d0b6735fc8420a816d7e1303469341&response_type=code&scope=mail%3Aimap_full%20mail%3Asmtp&force_confirm=true".into()));
assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878%2D4mvtgf6feshskf7695nfln6002mom908%2Eapps%2Egoogleusercontent%2Ecom&redirect_uri=chat%2Edelta%3A%2Fcom%2Eb44t%2Emessenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into()));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -26,16 +26,15 @@
use anyhow::{anyhow, Context as _, Result};
use email::Header;
use futures_lite::StreamExt;
use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN};
use iroh_gossip::proto::TopicId;
use iroh_gossip::net::{Gossip, JoinTopicFut, GOSSIP_ALPN};
use iroh_gossip::proto::{Event as IrohEvent, TopicId};
use iroh_net::key::{PublicKey, SecretKey};
use iroh_net::relay::{RelayMap, RelayUrl};
use iroh_net::{relay::RelayMode, Endpoint};
use iroh_net::{NodeAddr, NodeId};
use parking_lot::Mutex;
use std::collections::{BTreeSet, HashMap};
use std::env;
use tokio::sync::{oneshot, RwLock};
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use url::Url;
@@ -60,9 +59,6 @@ pub struct Iroh {
/// [Gossip] needed for iroh peer channels.
pub(crate) gossip: Gossip,
/// Sequence numbers for gossip channels.
pub(crate) sequence_numbers: Mutex<HashMap<TopicId, i32>>,
/// Topics for which an advertisement has already been sent.
pub(crate) iroh_channels: RwLock<HashMap<TopicId, ChannelState>>,
@@ -87,7 +83,7 @@ impl Iroh {
&self,
ctx: &Context,
msg_id: MsgId,
) -> Result<Option<oneshot::Receiver<()>>> {
) -> Result<Option<JoinTopicFut>> {
let topic = get_iroh_topic_for_msg(ctx, msg_id)
.await?
.with_context(|| format!("Message {msg_id} has no gossip topic"))?;
@@ -98,9 +94,14 @@ impl Iroh {
// Otherwise we would receive every message twice or more times.
let mut iroh_channels = self.iroh_channels.write().await;
if iroh_channels.contains_key(&topic) {
return Ok(None);
}
let seq = if let Some(channel_state) = iroh_channels.get(&topic) {
if channel_state.subscribe_loop.is_some() {
return Ok(None);
}
channel_state.seq_number
} else {
0
};
let peers = get_iroh_gossip_peers(ctx, msg_id).await?;
let node_ids = peers.iter().map(|p| p.node_id).collect::<Vec<_>>();
@@ -117,35 +118,33 @@ impl Iroh {
}
}
let (join_tx, join_rx) = oneshot::channel();
let (gossip_sender, gossip_receiver) = self
.gossip
.join_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
.split();
// Connect to all peers
let connect_future = self.gossip.join(topic, node_ids).await?;
let ctx = ctx.clone();
let gossip = self.gossip.clone();
let subscribe_loop = tokio::spawn(async move {
if let Err(e) = subscribe_loop(&ctx, gossip_receiver, topic, msg_id, join_tx).await {
if let Err(e) = subscribe_loop(&ctx, gossip, topic, msg_id).await {
warn!(ctx, "subscribe_loop failed: {e}")
}
});
iroh_channels.insert(topic, ChannelState::new(subscribe_loop, gossip_sender));
iroh_channels.insert(topic, ChannelState::new(seq, subscribe_loop));
Ok(Some(join_rx))
Ok(Some(connect_future))
}
/// Add gossip peers to realtime channel if it is already active.
pub async fn maybe_add_gossip_peers(&self, topic: TopicId, peers: Vec<NodeAddr>) -> Result<()> {
if self.iroh_channels.read().await.get(&topic).is_some() {
for peer in &peers {
self.endpoint.add_node_addr(peer.clone())?;
if let Some(state) = self.iroh_channels.read().await.get(&topic) {
if state.subscribe_loop.is_some() {
for peer in &peers {
self.endpoint.add_node_addr(peer.clone())?;
}
self.gossip
.join(topic, peers.into_iter().map(|peer| peer.node_id).collect())
.await?;
}
self.gossip
.join(topic, peers.into_iter().map(|peer| peer.node_id).collect())
.await?;
}
Ok(())
}
@@ -162,16 +161,11 @@ impl Iroh {
.with_context(|| format!("Message {msg_id} has no gossip topic"))?;
self.join_and_subscribe_gossip(ctx, msg_id).await?;
let seq_num = self.get_and_incr(&topic);
let mut iroh_channels = self.iroh_channels.write().await;
let state = iroh_channels
.get_mut(&topic)
.context("Just created state does not exist")?;
let seq_num = self.get_and_incr(&topic).await;
data.extend(seq_num.to_le_bytes());
data.extend(self.public_key.as_bytes());
state.sender.broadcast(data.into()).await?;
self.gossip.broadcast(topic, data.into()).await?;
if env::var("REALTIME_DEBUG").is_ok() {
info!(ctx, "Sent realtime data");
@@ -180,11 +174,13 @@ impl Iroh {
Ok(())
}
fn get_and_incr(&self, topic: &TopicId) -> i32 {
let mut sequence_numbers = self.sequence_numbers.lock();
let entry = sequence_numbers.entry(*topic).or_default();
*entry = entry.wrapping_add(1);
*entry
async fn get_and_incr(&self, topic: &TopicId) -> i32 {
let mut seq = 0;
if let Some(state) = self.iroh_channels.write().await.get_mut(topic) {
seq = state.seq_number;
state.seq_number = state.seq_number.wrapping_add(1)
}
seq
}
/// Get the iroh [NodeAddr] without direct IP addresses.
@@ -196,17 +192,12 @@ impl Iroh {
/// Leave the realtime channel for a given topic.
pub(crate) async fn leave_realtime(&self, topic: TopicId) -> Result<()> {
if let Some(channel) = self.iroh_channels.write().await.remove(&topic) {
// Dropping the last GossipTopic results in quitting the topic.
// It is split into GossipReceiver and GossipSender.
// GossipSender (`channel.sender`) is dropped automatically.
// Subscribe loop owns GossipReceiver.
// Aborting it and waiting for it to be dropped
// drops the receiver.
channel.subscribe_loop.abort();
let _ = channel.subscribe_loop.await;
if let Some(channel) = &mut self.iroh_channels.write().await.get_mut(&topic) {
if let Some(subscribe_loop) = channel.subscribe_loop.take() {
subscribe_loop.abort();
}
}
self.gossip.quit(topic).await?;
Ok(())
}
}
@@ -214,17 +205,17 @@ impl Iroh {
/// Single gossip channel state.
#[derive(Debug)]
pub(crate) struct ChannelState {
/// Sequence number for the gossip channel.
seq_number: i32,
/// The subscribe loop handle.
subscribe_loop: JoinHandle<()>,
sender: iroh_gossip::net::GossipSender,
subscribe_loop: Option<JoinHandle<()>>,
}
impl ChannelState {
fn new(subscribe_loop: JoinHandle<()>, sender: iroh_gossip::net::GossipSender) -> Self {
fn new(seq_number: i32, subscribe_loop: JoinHandle<()>) -> Self {
Self {
subscribe_loop,
sender,
seq_number,
subscribe_loop: Some(subscribe_loop),
}
}
}
@@ -253,7 +244,7 @@ impl Context {
.secret_key(secret_key)
.alpns(vec![GOSSIP_ALPN.to_vec()])
.relay_mode(relay_mode)
.bind()
.bind(0)
.await?;
// create gossip
@@ -265,11 +256,11 @@ impl Context {
// Shuts down on deltachat shutdown
tokio::spawn(endpoint_loop(context, endpoint.clone(), gossip.clone()));
tokio::spawn(gossip_direct_address_loop(endpoint.clone(), gossip.clone()));
Ok(Iroh {
endpoint,
gossip,
sequence_numbers: Mutex::new(HashMap::new()),
iroh_channels: RwLock::new(HashMap::new()),
public_key,
})
@@ -284,6 +275,15 @@ impl Context {
}
}
/// Loop to update direct addresses of the gossip.
async fn gossip_direct_address_loop(endpoint: Endpoint, gossip: Gossip) -> Result<()> {
let mut stream = endpoint.direct_addresses();
while let Some(addrs) = stream.next().await {
gossip.update_direct_addresses(&addrs)?;
}
Ok(())
}
/// Cache a peers [NodeId] for one topic.
pub(crate) async fn iroh_add_peer_for_topic(
ctx: &Context,
@@ -370,7 +370,7 @@ pub(crate) async fn get_iroh_topic_for_msg(
pub async fn send_webxdc_realtime_advertisement(
ctx: &Context,
msg_id: MsgId,
) -> Result<Option<oneshot::Receiver<()>>> {
) -> Result<Option<JoinTopicFut>> {
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
return Ok(None);
}
@@ -432,13 +432,6 @@ pub(crate) async fn create_iroh_header(
async fn endpoint_loop(context: Context, endpoint: Endpoint, gossip: Gossip) {
while let Some(conn) = endpoint.accept().await {
let conn = match conn.accept() {
Ok(conn) => conn,
Err(err) => {
warn!(context, "Failed to accept iroh connection: {err:#}.");
continue;
}
};
info!(context, "IROH_REALTIME: accepting iroh connection");
let gossip = gossip.clone();
let context = context.clone();
@@ -474,50 +467,32 @@ async fn handle_connection(
async fn subscribe_loop(
context: &Context,
mut stream: iroh_gossip::net::GossipReceiver,
gossip: Gossip,
topic: TopicId,
msg_id: MsgId,
join_tx: oneshot::Sender<()>,
) -> Result<()> {
let mut join_tx = Some(join_tx);
while let Some(event) = stream.try_next().await? {
let mut stream = gossip.subscribe(topic).await?;
loop {
let event = stream.recv().await?;
match event {
Event::Gossip(event) => match event {
GossipEvent::Joined(nodes) => {
if let Some(join_tx) = join_tx.take() {
// Try to notify that at least one peer joined,
// but ignore the error if receiver is dropped and nobody listens.
join_tx.send(()).ok();
}
for node in nodes {
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
}
GossipEvent::NeighborUp(node) => {
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
GossipEvent::NeighborDown(_node) => {}
GossipEvent::Received(message) => {
info!(context, "IROH_REALTIME: Received realtime data");
context.emit_event(EventType::WebxdcRealtimeData {
msg_id,
data: message
.content
.get(0..message.content.len() - 4 - PUBLIC_KEY_LENGTH)
.context("too few bytes in iroh message")?
.into(),
});
}
},
Event::Lagged => {
warn!(context, "Gossip lost some messages");
IrohEvent::NeighborUp(node) => {
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
IrohEvent::Received(event) => {
info!(context, "IROH_REALTIME: Received realtime data");
context.emit_event(EventType::WebxdcRealtimeData {
msg_id,
data: event
.content
.get(0..event.content.len() - 4 - PUBLIC_KEY_LENGTH)
.context("too few bytes in iroh message")?
.into(),
});
}
_ => (),
};
}
Ok(())
}
#[cfg(test)]
@@ -565,10 +540,10 @@ mod tests {
assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
let webxdc = alice.pop_sent_msg().await;
let bob_webxdc = bob.recv_msg(&webxdc).await;
assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
let bob_webdxc = bob.recv_msg(&webxdc).await;
assert_eq!(bob_webdxc.get_viewtype(), Viewtype::Webxdc);
bob_webxdc.chat_id.accept(bob).await.unwrap();
bob_webdxc.chat_id.accept(bob).await.unwrap();
// Alice advertises herself.
send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
@@ -576,17 +551,10 @@ mod tests {
.unwrap();
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
loop {
let event = bob.evtracker.recv().await.unwrap();
if let EventType::WebxdcRealtimeAdvertisementReceived { msg_id } = event.typ {
assert!(msg_id == alice_webxdc.id);
break;
}
}
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
// Bob adds alice to gossip peers.
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
let members = get_iroh_gossip_peers(bob, bob_webdxc.id)
.await
.unwrap()
.into_iter()
@@ -600,7 +568,7 @@ mod tests {
);
bob_iroh
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.join_and_subscribe_gossip(bob, bob_webdxc.id)
.await
.unwrap()
.unwrap()
@@ -628,7 +596,7 @@ mod tests {
}
// Bob sends ephemeral message
bob_iroh
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
.send_webxdc_realtime_data(bob, bob_webdxc.id, "bob -> alice".as_bytes().to_vec())
.await
.unwrap();
@@ -660,7 +628,7 @@ mod tests {
);
bob_iroh
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
.send_webxdc_realtime_data(bob, bob_webdxc.id, "bob -> alice 2".as_bytes().to_vec())
.await
.unwrap();
@@ -718,10 +686,10 @@ mod tests {
assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
let webxdc = alice.pop_sent_msg().await;
let bob_webxdc = bob.recv_msg(&webxdc).await;
assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
let bob_webdxc = bob.recv_msg(&webxdc).await;
assert_eq!(bob_webdxc.get_viewtype(), Viewtype::Webxdc);
bob_webxdc.chat_id.accept(bob).await.unwrap();
bob_webdxc.chat_id.accept(bob).await.unwrap();
// Alice advertises herself.
send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
@@ -732,7 +700,7 @@ mod tests {
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
// Bob adds alice to gossip peers.
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
let members = get_iroh_gossip_peers(bob, bob_webdxc.id)
.await
.unwrap()
.into_iter()
@@ -746,7 +714,7 @@ mod tests {
);
bob_iroh
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.join_and_subscribe_gossip(bob, bob_webdxc.id)
.await
.unwrap()
.unwrap()
@@ -773,32 +741,11 @@ mod tests {
}
}
let bob_topic = get_iroh_topic_for_msg(bob, bob_webxdc.id)
.await
.unwrap()
.unwrap();
let bob_sequence_number = bob
.iroh
.get()
.unwrap()
.sequence_numbers
.lock()
.get(&bob_topic)
.copied();
leave_webxdc_realtime(bob, bob_webxdc.id).await.unwrap();
let bob_sequence_number_after = bob
.iroh
.get()
.unwrap()
.sequence_numbers
.lock()
.get(&bob_topic)
.copied();
// Check that sequence number is persisted when leaving the channel.
assert_eq!(bob_sequence_number, bob_sequence_number_after);
// TODO: check that seq number is persisted
leave_webxdc_realtime(bob, bob_webdxc.id).await.unwrap();
bob_iroh
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.join_and_subscribe_gossip(bob, bob_webdxc.id)
.await
.unwrap()
.unwrap()
@@ -806,7 +753,7 @@ mod tests {
.unwrap();
bob_iroh
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
.send_webxdc_realtime_data(bob, bob_webdxc.id, "bob -> alice".as_bytes().to_vec())
.await
.unwrap();
@@ -836,7 +783,7 @@ mod tests {
.await
.unwrap()
.unwrap();
assert!(alice
assert!(if let Some(state) = alice
.iroh
.get()
.unwrap()
@@ -844,7 +791,11 @@ mod tests {
.read()
.await
.get(&topic)
.is_none());
{
state.subscribe_loop.is_none()
} else {
false
});
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -343,7 +343,7 @@ impl Peerstate {
}
/// Updates peerstate according to the given `Autocrypt` header.
pub fn apply_header(&mut self, context: &Context, header: &Aheader, message_time: i64) {
pub fn apply_header(&mut self, header: &Aheader, message_time: i64) {
if !addr_cmp(&self.addr, &header.addr) {
return;
}
@@ -362,13 +362,6 @@ impl Peerstate {
self.public_key = Some(header.public_key.clone());
self.recalc_fingerprint();
}
} else {
warn!(
context,
"Ignoring outdated Autocrypt header because message_time={} < last_seen={}.",
message_time,
self.last_seen
);
}
}
@@ -773,65 +766,23 @@ pub(crate) async fn maybe_do_aeap_transition(
// If the from addr is different from the peerstate address we know,
// we may want to do an AEAP transition.
if !addr_cmp(&peerstate.addr, &mime_parser.from.addr) {
// Check if it's a chat message; we do this to avoid
// some accidental transitions if someone writes from multiple
// addresses with an MUA.
if !mime_parser.has_chat_version() {
info!(
context,
"Not doing AEAP from {} to {} because the message is not a chat message.",
&peerstate.addr,
&mime_parser.from.addr
);
return Ok(());
}
// Check if the message is encrypted and signed correctly. If it's not encrypted, it's
// probably from a new contact sharing the same key.
if mime_parser.signatures.is_empty() {
info!(
context,
"Not doing AEAP from {} to {} because the message is not encrypted and signed.",
&peerstate.addr,
&mime_parser.from.addr
);
return Ok(());
}
// Check if the From: address was also in the signed part of the email.
// Without this check, an attacker could replay a message from Alice
// to Bob. Then Bob's device would do an AEAP transition from Alice's
// to the attacker's address, allowing for easier phishing.
if !mime_parser.from_is_signed {
info!(
context,
"Not doing AEAP from {} to {} because From: is not signed.",
&peerstate.addr,
&mime_parser.from.addr
);
return Ok(());
}
// DC avoids sending messages with the same timestamp, that's why messages
// with equal timestamps are ignored here unlike in `Peerstate::apply_header()`.
if info.message_time <= peerstate.last_seen {
info!(
context,
"Not doing AEAP from {} to {} because {} < {}.",
&peerstate.addr,
&mime_parser.from.addr,
info.message_time,
peerstate.last_seen
);
return Ok(());
}
info!(
context,
"Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr
);
if !addr_cmp(&peerstate.addr, &mime_parser.from.addr)
// Check if it's a chat message; we do this to avoid
// some accidental transitions if someone writes from multiple
// addresses with an MUA.
&& mime_parser.has_chat_version()
// Check if the message is encrypted and signed correctly. If it's not encrypted, it's
// probably from a new contact sharing the same key.
&& !mime_parser.signatures.is_empty()
// Check if the From: address was also in the signed part of the email.
// Without this check, an attacker could replay a message from Alice
// to Bob. Then Bob's device would do an AEAP transition from Alice's
// to the attacker's address, allowing for easier phishing.
&& mime_parser.from_is_signed
// DC avoids sending messages with the same timestamp, that's why `>` is here unlike in
// `Peerstate::apply_header()`.
&& info.message_time > peerstate.last_seen
{
let info = &mut mime_parser.decryption_info;
let peerstate = info.peerstate.as_mut().context("no peerstate??")?;
// Add info messages to chats with this (verified) contact
@@ -849,7 +800,7 @@ pub(crate) async fn maybe_do_aeap_transition(
let header = info.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(context, header, info.message_time);
peerstate.apply_header(header, info.message_time);
peerstate
.save_to_db_ex(&context.sql, Some(&old_addr))
@@ -1028,8 +979,6 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_peerstate_degrade_reordering() {
let ctx = crate::test_utils::TestContext::new().await;
let addr = "example@example.org";
let pub_key = alice_keypair().public;
let header = Aheader::new(addr.to_string(), pub_key, EncryptPreference::Mutual);
@@ -1054,7 +1003,7 @@ mod tests {
fingerprint_changed: false,
};
peerstate.apply_header(&ctx, &header, 100);
peerstate.apply_header(&header, 100);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
peerstate.degrade_encryption(300);
@@ -1062,11 +1011,11 @@ mod tests {
// This has message time 200, while encryption was degraded at timestamp 300.
// Because of reordering, header should not be applied.
peerstate.apply_header(&ctx, &header, 200);
peerstate.apply_header(&header, 200);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
// Same header will be applied in the future.
peerstate.apply_header(&ctx, &header, 300);
peerstate.apply_header(&header, 300);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
}
}

View File

@@ -14,7 +14,9 @@ use pgp::composed::{
use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::types::{CompressionAlgorithm, KeyTrait, Mpi, PublicKeyTrait, StringToKey};
use pgp::types::{
CompressionAlgorithm, KeyTrait, Mpi, PublicKeyTrait, SecretKeyTrait, StringToKey,
};
use rand::{thread_rng, CryptoRng, Rng};
use tokio::runtime::Handle;
@@ -41,7 +43,7 @@ enum SignedPublicKeyOrSubkey<'a> {
Subkey(&'a SignedPublicSubKey),
}
impl KeyTrait for SignedPublicKeyOrSubkey<'_> {
impl<'a> KeyTrait for SignedPublicKeyOrSubkey<'a> {
fn fingerprint(&self) -> Vec<u8> {
match self {
Self::Key(k) => k.fingerprint(),
@@ -64,7 +66,7 @@ impl KeyTrait for SignedPublicKeyOrSubkey<'_> {
}
}
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
impl<'a> PublicKeyTrait for SignedPublicKeyOrSubkey<'a> {
fn verify_signature(
&self,
hash: HashAlgorithm,
@@ -133,6 +135,9 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
/// keys together as they are one unit.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct KeyPair {
/// Email address.
pub addr: EmailAddress,
/// Public key.
pub public: SignedPublicKey,
@@ -140,18 +145,6 @@ pub struct KeyPair {
pub secret: SignedSecretKey,
}
impl KeyPair {
/// Creates new keypair from a secret key.
///
/// Public key is split off the secret key.
pub fn new(secret: SignedSecretKey) -> Result<Self> {
use crate::key::DcSecretKey;
let public = secret.split_public_key()?;
Ok(Self { public, secret })
}
}
/// Create a new key pair.
///
/// Both secret and public key consist of signing primary key and encryption subkey
@@ -208,12 +201,19 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
.verify()
.context("invalid secret key generated")?;
let key_pair = KeyPair::new(secret_key)?;
key_pair
.public
let public_key = secret_key
.public_key()
.sign(&secret_key, || "".into())
.context("failed to sign public key")?;
public_key
.verify()
.context("invalid public key generated")?;
Ok(key_pair)
Ok(KeyPair {
addr,
public: public_key,
secret: secret_key,
})
}
/// Select public key or subkey to use for encryption.

View File

@@ -1,6 +1,6 @@
//! [Provider database](https://providers.delta.chat/) module.
pub(crate) mod data;
mod data;
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;

View File

@@ -509,8 +509,6 @@ static P_FREENET_DE: Provider = Provider {
overview_page: "https://providers.delta.chat/freenet-de",
server: &[
Server { protocol: Imap, socket: Ssl, hostname: "mx.freenet.de", port: 993, username_pattern: Email },
Server { protocol: Imap, socket: Starttls, hostname: "mx.freenet.de", port: 143, username_pattern: Email },
Server { protocol: Smtp, socket: Ssl, hostname: "mx.freenet.de", port: 465, username_pattern: Email },
Server { protocol: Smtp, socket: Starttls, hostname: "mx.freenet.de", port: 587, username_pattern: Email },
],
opt: ProviderOptions::new(),
@@ -522,7 +520,7 @@ static P_FREENET_DE: Provider = Provider {
static P_GMAIL: Provider = Provider {
id: "gmail",
status: Status::Preparation,
before_login_hint: "For Gmail accounts, you need to have \"2-Step Verification\" enabled and create an app-password.",
before_login_hint: "For Gmail accounts, you need to create an app-password if you have \"2-Step Verification\" enabled. If this setting is not available, you need to enable \"less secure apps\".",
after_login_hint: "",
overview_page: "https://providers.delta.chat/gmail",
server: &[
@@ -534,7 +532,7 @@ static P_GMAIL: Provider = Provider {
..ProviderOptions::new()
},
config_defaults: None,
oauth2_authorizer: None,
oauth2_authorizer: Some(Oauth2Authorizer::Gmail),
};
// gmx.net.md: gmx.net, gmx.de, gmx.at, gmx.ch, gmx.org, gmx.eu, gmx.info, gmx.biz, gmx.com
@@ -876,20 +874,6 @@ static P_MEHL_CLOUD: Provider = Provider {
after_login_hint: "",
overview_page: "https://providers.delta.chat/mehl-cloud",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "mehl.cloud",
port: 443,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mehl.cloud",
port: 443,
username_pattern: Email,
},
Server {
protocol: Imap,
socket: Ssl,
@@ -940,41 +924,6 @@ static P_MEHL_STORE: Provider = Provider {
oauth2_authorizer: None,
};
// migadu.md: migadu.com
static P_MIGADU: Provider = Provider {
id: "migadu",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/migadu",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "imap.migadu.com",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "smtp.migadu.com",
port: 465,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "smtp.migadu.com",
port: 587,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: None,
oauth2_authorizer: None,
};
// nauta.cu.md: nauta.cu
static P_NAUTA_CU: Provider = Provider {
id: "nauta.cu",
@@ -1060,20 +1009,6 @@ static P_NINE_TESTRUN_ORG: Provider = Provider {
after_login_hint: "",
overview_page: "https://providers.delta.chat/nine-testrun-org",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "nine.testrun.org",
port: 443,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "nine.testrun.org",
port: 443,
username_pattern: Email,
},
Server {
protocol: Imap,
socket: Ssl,
@@ -1195,7 +1130,7 @@ static P_OUVATON_COOP: Provider = Provider {
oauth2_authorizer: None,
};
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ca, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ca, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
static P_POSTEO: Provider = Provider {
id: "posteo",
status: Status::Ok,
@@ -1325,14 +1260,14 @@ static P_RISEUP_NET: Provider = Provider {
socket: Ssl,
hostname: "mail.riseup.net",
port: 993,
username_pattern: Email,
username_pattern: Emaillocalpart,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mail.riseup.net",
port: 465,
username_pattern: Email,
username_pattern: Emaillocalpart,
},
],
opt: ProviderOptions::new(),
@@ -1366,37 +1301,6 @@ static P_SONIC: Provider = Provider {
oauth2_authorizer: None,
};
// stinpriza.net.md: stinpriza.net, stinpriza.eu, el-hoyo.net
static P_STINPRIZA_NET: Provider = Provider {
id: "stinpriza.net",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/stinpriza-net",
server: &[
Server {
protocol: Imap,
socket: Starttls,
hostname: "stinpriza.net",
port: 143,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "stinpriza.net",
port: 587,
username_pattern: Email,
},
],
opt: ProviderOptions {
strict_tls: true,
..ProviderOptions::new()
},
config_defaults: None,
oauth2_authorizer: None,
};
// systemausfall.org.md: systemausfall.org, solidaris.me
static P_SYSTEMAUSFALL_ORG: Provider = Provider {
id: "systemausfall.org",
@@ -1569,26 +1473,11 @@ static P_TUTANOTA: Provider = Provider {
// ukr.net.md: ukr.net
static P_UKR_NET: Provider = Provider {
id: "ukr.net",
status: Status::Preparation,
before_login_hint: "You must allow IMAP access to your account before you can login.",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/ukr-net",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "imap.ukr.net",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "smtp.ukr.net",
port: 465,
username_pattern: Email,
},
],
server: &[],
opt: ProviderOptions::new(),
config_defaults: None,
oauth2_authorizer: None,
@@ -1666,13 +1555,11 @@ static P_VIVALDI: Provider = Provider {
// vk.com.md: vk.com
static P_VK_COM: Provider = Provider {
id: "vk.com",
status: Status::Preparation,
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru https://account.mail.ru/user/2-step-auth/passwords/ чтобы vk.com работал с Delta Chat.",
status: Status::Broken,
before_login_hint: "К сожалению, VK Почта не поддерживает работу с Delta Chat. См. https://help.vk.mail.ru/vkmail/questions/client",
after_login_hint: "",
overview_page: "https://providers.delta.chat/vk-com",
server: &[
Server { protocol: Imap, socket: Ssl, hostname: "imap.mail.ru", port: 993, username_pattern: Email },
Server { protocol: Smtp, socket: Ssl, hostname: "smtp.mail.ru", port: 465, username_pattern: Email },
],
opt: ProviderOptions::new(),
config_defaults: None,
@@ -1870,7 +1757,7 @@ static P_ZOHO: Provider = Provider {
oauth2_authorizer: None,
};
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 533] = [
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 528] = [
("163.com", &P_163),
("aktivix.org", &P_AKTIVIX_ORG),
("aliyun.com", &P_ALIYUN),
@@ -2243,7 +2130,6 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 533] = [
("ente.quest", &P_MEHL_STORE),
("ente.cfd", &P_MEHL_STORE),
("nein.jetzt", &P_MEHL_STORE),
("migadu.com", &P_MIGADU),
("nauta.cu", &P_NAUTA_CU),
("naver.com", &P_NAVER),
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
@@ -2264,7 +2150,6 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 533] = [
("posteo.cl", &P_POSTEO),
("posteo.co", &P_POSTEO),
("posteo.co.uk", &P_POSTEO),
("posteo.com", &P_POSTEO),
("posteo.com.br", &P_POSTEO),
("posteo.cr", &P_POSTEO),
("posteo.cz", &P_POSTEO),
@@ -2327,9 +2212,6 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 533] = [
("riseup.net", &P_RISEUP_NET),
("rogers.com", &P_ROGERS_COM),
("sonic.net", &P_SONIC),
("stinpriza.net", &P_STINPRIZA_NET),
("stinpriza.eu", &P_STINPRIZA_NET),
("el-hoyo.net", &P_STINPRIZA_NET),
("systemausfall.org", &P_SYSTEMAUSFALL_ORG),
("solidaris.me", &P_SYSTEMAUSFALL_ORG),
("systemli.org", &P_SYSTEMLI_ORG),
@@ -2447,7 +2329,6 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("mailo.com", &P_MAILO_COM),
("mehl.cloud", &P_MEHL_CLOUD),
("mehl.store", &P_MEHL_STORE),
("migadu", &P_MIGADU),
("nauta.cu", &P_NAUTA_CU),
("naver", &P_NAVER),
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
@@ -2462,7 +2343,6 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("riseup.net", &P_RISEUP_NET),
("rogers.com", &P_ROGERS_COM),
("sonic", &P_SONIC),
("stinpriza.net", &P_STINPRIZA_NET),
("systemausfall.org", &P_SYSTEMAUSFALL_ORG),
("systemli.org", &P_SYSTEMLI_ORG),
("t-online", &P_T_ONLINE),
@@ -2486,4 +2366,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 9, 13).unwrap());
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 6, 24).unwrap());

View File

@@ -48,7 +48,7 @@ impl PushSubscriber {
/// Subscribes for heartbeat notifications with previously set device token.
#[cfg(target_os = "ios")]
pub(crate) async fn subscribe(&self, context: &Context) -> Result<()> {
pub(crate) async fn subscribe(&self) -> Result<()> {
use crate::net::http;
let mut state = self.inner.write().await;
@@ -61,13 +61,15 @@ impl PushSubscriber {
return Ok(());
};
if http::post_string(
context,
"https://notifications.delta.chat/register",
format!("{{\"token\":\"{token}\"}}"),
)
.await?
{
let socks5_config = None;
let response = http::get_client(socks5_config)?
.post("https://notifications.delta.chat/register")
.body(format!("{{\"token\":\"{token}\"}}"))
.send()
.await?;
let response_status = response.status();
if response_status.is_success() {
state.heartbeat_subscribed = true;
}
Ok(())
@@ -75,7 +77,7 @@ impl PushSubscriber {
/// Placeholder to skip subscribing to heartbeat notifications outside iOS.
#[cfg(not(target_os = "ios"))]
pub(crate) async fn subscribe(&self, _context: &Context) -> Result<()> {
pub(crate) async fn subscribe(&self) -> Result<()> {
let mut state = self.inner.write().await;
state.heartbeat_subscribed = true;
Ok(())

442
src/qr.rs
View File

@@ -7,11 +7,11 @@ use anyhow::{anyhow, bail, ensure, Context as _, Result};
pub use dclogin_scheme::LoginOptions;
use deltachat_contact_tools::{addr_normalize, may_be_valid_addr, ContactAddress};
use once_cell::sync::Lazy;
use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
use percent_encoding::percent_decode_str;
use serde::Deserialize;
use self::dclogin_scheme::configure_from_login_qr;
use crate::chat::ChatIdBlocked;
use crate::chat::{get_chat_id_by_grpid, ChatIdBlocked};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{Contact, ContactId, Origin};
@@ -19,11 +19,10 @@ use crate::context::Context;
use crate::events::EventType;
use crate::key::Fingerprint;
use crate::message::Message;
use crate::net::http::post_empty;
use crate::net::proxy::DEFAULT_SOCKS_PORT;
use crate::peerstate::Peerstate;
use crate::token;
use crate::tools::validate_id;
use iroh_old as iroh;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
@@ -31,13 +30,15 @@ const IDELTACHAT_NOSLASH_SCHEME: &str = "https://i.delta.chat#";
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
const DCWEBRTC_SCHEME: &str = "DCWEBRTC:";
const TG_SOCKS_SCHEME: &str = "https://t.me/socks";
const MAILTO_SCHEME: &str = "mailto:";
const MATMSG_SCHEME: &str = "MATMSG:";
const VCARD_SCHEME: &str = "BEGIN:VCARD";
const SMTP_SCHEME: &str = "SMTP:";
const HTTP_SCHEME: &str = "http://";
const HTTPS_SCHEME: &str = "https://";
const SHADOWSOCKS_SCHEME: &str = "ss://";
/// Legacy backup transfer based on iroh 0.4.
pub(crate) const DCBACKUP_SCHEME: &str = "DCBACKUP:";
/// Backup transfer based on iroh-net.
pub(crate) const DCBACKUP2_SCHEME: &str = "DCBACKUP2:";
@@ -109,6 +110,20 @@ pub enum Qr {
domain: String,
},
/// Provides a backup that can be retrieved using legacy iroh 0.4.
///
/// This contains all the data needed to connect to a device and download a backup from
/// it to configure the receiving device with the same account.
Backup {
/// Printable version of the provider information.
///
/// This is the printable version of a `sendme` ticket, which contains all the
/// information to connect to and authenticate a backup provider.
///
/// The format is somewhat opaque, but `sendme` can deserialise this.
ticket: iroh::provider::Ticket,
},
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
Backup2 {
/// Iroh node address.
@@ -127,28 +142,6 @@ pub enum Qr {
instance_pattern: String,
},
/// Ask the user if they want to use the given proxy.
///
/// Note that HTTP(S) URLs without a path
/// and query parameters are treated as HTTP(S) proxy URL.
/// UI may want to still offer to open the URL
/// in the browser if QR code contents
/// starts with `http://` or `https://`
/// and the QR code was not scanned from
/// the proxy configuration screen.
Proxy {
/// Proxy URL.
///
/// This is the URL that is going to be added.
url: String,
/// Host extracted from the URL to display in the UI.
host: String,
/// Port extracted from the URL to display in the UI.
port: u16,
},
/// Contact address is scanned.
///
/// Optionally, a draft message could be provided.
@@ -284,10 +277,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
dclogin_scheme::decode_login(qr)?
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
decode_webrtc_instance(context, qr)?
} else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) {
decode_tg_socks_proxy(context, qr)?
} else if qr.starts_with(SHADOWSOCKS_SCHEME) {
decode_shadowsocks_proxy(qr)?
} else if starts_with_ignore_case(qr, DCBACKUP_SCHEME) {
decode_backup(qr)?
} else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) {
decode_backup2(qr)?
} else if qr.starts_with(MAILTO_SCHEME) {
@@ -298,44 +289,9 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
decode_matmsg(context, qr).await?
} else if qr.starts_with(VCARD_SCHEME) {
decode_vcard(context, qr).await?
} else if let Ok(url) = url::Url::parse(qr) {
match url.scheme() {
"socks5" => Qr::Proxy {
url: qr.to_string(),
host: url.host_str().context("URL has no host")?.to_string(),
port: url.port().unwrap_or(DEFAULT_SOCKS_PORT),
},
"http" | "https" => {
// Parsing with a non-standard scheme
// is a hack to work around the `url` crate bug
// <https://github.com/servo/rust-url/issues/957>.
let url = if let Some(rest) = qr.strip_prefix("http://") {
url::Url::parse(&format!("foobarbaz://{rest}"))?
} else if let Some(rest) = qr.strip_prefix("https://") {
url::Url::parse(&format!("foobarbaz://{rest}"))?
} else {
// Should not happen.
url
};
if url.port().is_none() | (url.path() != "") | url.query().is_some() {
// URL without a port, with a path or query cannot be a proxy URL.
Qr::Url {
url: qr.to_string(),
}
} else {
Qr::Proxy {
url: qr.to_string(),
host: url.host_str().context("URL has no host")?.to_string(),
port: url
.port_or_known_default()
.context("HTTP(S) URLs are guaranteed to return Some port")?,
}
}
}
_ => Qr::Url {
url: qr.to_string(),
},
} else if qr.starts_with(HTTP_SCHEME) || qr.starts_with(HTTPS_SCHEME) {
Qr::Url {
url: qr.to_string(),
}
} else {
Qr::Text {
@@ -345,7 +301,7 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
Ok(qrcode)
}
/// Formats the text of the [`Qr::Backup2`] variant.
/// Formats the text of the [`Qr::Backup`] variant.
///
/// This is the inverse of [`check_qr`] for that variant only.
///
@@ -353,6 +309,7 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
/// into `FromStr`.
pub fn format_backup(qr: &Qr) -> Result<String> {
match qr {
Qr::Backup { ref ticket } => Ok(format!("{DCBACKUP_SCHEME}{ticket}")),
Qr::Backup2 {
ref node_addr,
ref auth_token,
@@ -539,7 +496,7 @@ async fn decode_ideltachat(context: &Context, prefix: &str, qr: &str) -> Result<
let qr = qr.replacen('&', "#", 1);
decode_openpgp(context, &qr)
.await
.with_context(|| format!("failed to decode {prefix} QR code"))
.context("failed to decode {prefix} QR code")
}
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
@@ -582,55 +539,16 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
}
}
/// scheme: `https://t.me/socks?server=foo&port=123` or `https://t.me/socks?server=1.2.3.4&port=123`
fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
let mut host: Option<String> = None;
let mut port: u16 = DEFAULT_SOCKS_PORT;
let mut user: Option<String> = None;
let mut pass: Option<String> = None;
for (key, value) in url.query_pairs() {
if key == "server" {
host = Some(value.to_string());
} else if key == "port" {
port = value.parse().unwrap_or(DEFAULT_SOCKS_PORT);
} else if key == "user" {
user = Some(value.to_string());
} else if key == "pass" {
pass = Some(value.to_string());
}
}
let Some(host) = host else {
bail!("Bad t.me/socks url: {:?}", url);
};
let mut url = "socks5://".to_string();
if let Some(pass) = pass {
url += &percent_encode(user.unwrap_or_default().as_bytes(), NON_ALPHANUMERIC).to_string();
url += ":";
url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
url += "@";
};
url += &host;
url += ":";
url += &port.to_string();
Ok(Qr::Proxy { url, host, port })
}
/// Decodes `ss://` URLs for Shadowsocks proxies.
fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
let server_config = shadowsocks::config::ServerConfig::from_url(qr)?;
let addr = server_config.addr();
let host = addr.host().to_string();
let port = addr.port();
Ok(Qr::Proxy {
url: qr.to_string(),
host,
port,
})
/// Decodes a [`DCBACKUP_SCHEME`] QR code.
///
/// The format of this scheme is `DCBACKUP:<encoded ticket>`. The encoding is the
/// [`iroh::provider::Ticket`]'s `Display` impl.
fn decode_backup(qr: &str) -> Result<Qr> {
let payload = qr
.strip_prefix(DCBACKUP_SCHEME)
.ok_or_else(|| anyhow!("invalid DCBACKUP scheme"))?;
let ticket: iroh::provider::Ticket = payload.parse().context("invalid DCBACKUP payload")?;
Ok(Qr::Backup { ticket })
}
/// Decodes a [`DCBACKUP2_SCHEME`] QR code.
@@ -676,8 +594,21 @@ async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
bail!("DCACCOUNT QR codes must use HTTPS scheme");
}
let (response_text, response_success) = post_empty(context, url_str).await?;
if response_success {
// As only HTTPS is used, it is safe to load DNS cache.
let load_cache = true;
let response = crate::net::http::get_client(context, load_cache)
.await?
.post(url_str)
.send()
.await?;
let response_status = response.status();
let response_text = response
.text()
.await
.context("Cannot create account, request failed: empty response")?;
if response_status.is_success() {
let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text)
.with_context(|| {
format!("Cannot create account, response is malformed:\n{response_text:?}")
@@ -718,23 +649,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
.set_config_internal(Config::WebrtcInstance, Some(&instance_pattern))
.await?;
}
Qr::Proxy { url, .. } => {
let old_proxy_url_value = context
.get_config(Config::ProxyUrl)
.await?
.unwrap_or_default();
let proxy_urls: Vec<&str> = std::iter::once(url.as_str())
.chain(
old_proxy_url_value
.split('\n')
.filter(|s| !s.is_empty() && *s != url),
)
.collect();
context
.set_config(Config::ProxyUrl, Some(&proxy_urls.join("\n")))
.await?;
context.set_config_bool(Config::ProxyEnabled, true).await?;
}
Qr::WithdrawVerifyContact {
invitenumber,
authcode,
@@ -765,7 +679,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
token::save(context, token::Namespace::InviteNumber, None, &invitenumber).await?;
token::save(context, token::Namespace::Auth, None, &authcode).await?;
context.sync_qr_code_tokens(None).await?;
context.scheduler.interrupt_inbox().await;
context.scheduler.interrupt_smtp().await;
}
Qr::ReviveVerifyGroup {
invitenumber,
@@ -773,16 +687,19 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
grpid,
..
} => {
let chat_id = get_chat_id_by_grpid(context, &grpid)
.await?
.map(|(chat_id, _protected, _blocked)| chat_id);
token::save(
context,
token::Namespace::InviteNumber,
Some(&grpid),
chat_id,
&invitenumber,
)
.await?;
token::save(context, token::Namespace::Auth, Some(&grpid), &authcode).await?;
context.sync_qr_code_tokens(Some(&grpid)).await?;
context.scheduler.interrupt_inbox().await;
token::save(context, token::Namespace::Auth, chat_id, &authcode).await?;
context.sync_qr_code_tokens(chat_id).await?;
context.scheduler.interrupt_smtp().await;
}
Qr::Login { address, options } => {
configure_from_login_qr(context, &address, options).await?
@@ -953,7 +870,6 @@ mod tests {
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::config::Config;
use crate::key::DcKey;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{alice_keypair, TestContext};
@@ -962,38 +878,11 @@ mod tests {
async fn test_decode_http() -> Result<()> {
let ctx = TestContext::new().await;
let qr = check_qr(&ctx.ctx, "http://www.hello.com:80").await?;
assert_eq!(
qr,
Qr::Proxy {
url: "http://www.hello.com:80".to_string(),
host: "www.hello.com".to_string(),
port: 80
}
);
// If it has no explicit port, then it is not a proxy.
let qr = check_qr(&ctx.ctx, "http://www.hello.com").await?;
assert_eq!(
qr,
Qr::Url {
url: "http://www.hello.com".to_string(),
}
);
// If it has a path, then it is not a proxy.
let qr = check_qr(&ctx.ctx, "http://www.hello.com/").await?;
assert_eq!(
qr,
Qr::Url {
url: "http://www.hello.com/".to_string(),
}
);
let qr = check_qr(&ctx.ctx, "http://www.hello.com/hello").await?;
assert_eq!(
qr,
Qr::Url {
url: "http://www.hello.com/hello".to_string(),
url: "http://www.hello.com".to_string()
}
);
@@ -1004,38 +893,11 @@ mod tests {
async fn test_decode_https() -> Result<()> {
let ctx = TestContext::new().await;
let qr = check_qr(&ctx.ctx, "https://www.hello.com:443").await?;
assert_eq!(
qr,
Qr::Proxy {
url: "https://www.hello.com:443".to_string(),
host: "www.hello.com".to_string(),
port: 443
}
);
// If it has no explicit port, then it is not a proxy.
let qr = check_qr(&ctx.ctx, "https://www.hello.com").await?;
assert_eq!(
qr,
Qr::Url {
url: "https://www.hello.com".to_string(),
}
);
// If it has a path, then it is not a proxy.
let qr = check_qr(&ctx.ctx, "https://www.hello.com/").await?;
assert_eq!(
qr,
Qr::Url {
url: "https://www.hello.com/".to_string(),
}
);
let qr = check_qr(&ctx.ctx, "https://www.hello.com/hello").await?;
assert_eq!(
qr,
Qr::Url {
url: "https://www.hello.com/hello".to_string(),
url: "https://www.hello.com".to_string()
}
);
@@ -1543,12 +1405,9 @@ mod tests {
ctx.ctx.get_config(Config::SendUser).await?,
Some("SendUser".to_owned())
);
// `sc` option is actually ignored and `ic` is used instead
// because `smtp_certificate_checks` is deprecated.
assert_eq!(
ctx.ctx.get_config(Config::SmtpCertificateChecks).await?,
Some("1".to_owned())
Some("3".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::SendSecurity).await?,
@@ -1616,69 +1475,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_tg_socks_proxy() -> Result<()> {
let t = TestContext::new().await;
let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?;
assert_eq!(
qr,
Qr::Proxy {
url: "socks5://84.53.239.95:4145".to_string(),
host: "84.53.239.95".to_string(),
port: 4145,
}
);
let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?;
assert_eq!(
qr,
Qr::Proxy {
url: "socks5://foo.bar:123".to_string(),
host: "foo.bar".to_string(),
port: 123,
}
);
let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?;
assert_eq!(
qr,
Qr::Proxy {
url: "socks5://foo.baz:1080".to_string(),
host: "foo.baz".to_string(),
port: 1080,
}
);
let qr = check_qr(
&t,
"https://t.me/socks?server=foo.baz&port=12345&user=ada&pass=ms%21%2F%24",
)
.await?;
assert_eq!(
qr,
Qr::Proxy {
url: "socks5://ada:ms%21%2F%24@foo.baz:12345".to_string(),
host: "foo.baz".to_string(),
port: 12345,
}
);
// wrong domain results in Qr:Url instead of Qr::Socks5Proxy
let qr = check_qr(&t, "https://not.me/socks?noserver=84.53.239.95&port=4145").await?;
assert_eq!(
qr,
Qr::Url {
url: "https://not.me/socks?noserver=84.53.239.95&port=4145".to_string()
}
);
let qr = check_qr(&t, "https://t.me/socks?noserver=84.53.239.95&port=4145").await;
assert!(qr.is_err());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_account_bad_scheme() {
let ctx = TestContext::new().await;
@@ -1699,7 +1495,7 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_webrtc_instance_config_from_qr() -> Result<()> {
async fn test_set_config_from_qr() -> Result<()> {
let ctx = TestContext::new().await;
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
@@ -1708,6 +1504,10 @@ mod tests {
assert!(res.is_err());
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
let res = set_config_from_qr(&ctx.ctx, "https://no.qr").await;
assert!(res.is_err());
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
assert!(res.is_ok());
assert_eq!(
@@ -1725,106 +1525,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_proxy_config_from_qr() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false);
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
assert_eq!(
t.get_config(Config::ProxyUrl).await?,
Some("socks5://foo:666".to_string())
);
// Test URL without port.
let res = set_config_from_qr(&t, "https://t.me/socks?server=1.2.3.4").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
assert_eq!(
t.get_config(Config::ProxyUrl).await?,
Some("socks5://1.2.3.4:1080\nsocks5://foo:666".to_string())
);
// make sure, user&password are set when specified in the URL
// Password is an URL-encoded "x&%$X".
let res =
set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await;
assert!(res.is_ok());
assert_eq!(
t.get_config(Config::ProxyUrl).await?,
Some(
"socks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080\nsocks5://foo:666"
.to_string()
)
);
// Scanning existing proxy brings it to the top in the list.
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
assert_eq!(
t.get_config(Config::ProxyUrl).await?,
Some(
"socks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
.to_string()
)
);
set_config_from_qr(
&t,
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
)
.await?;
assert_eq!(
t.get_config(Config::ProxyUrl).await?,
Some(
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
.to_string()
)
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_shadowsocks() -> Result<()> {
let ctx = TestContext::new().await;
let qr = check_qr(
&ctx.ctx,
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
)
.await?;
assert_eq!(
qr,
Qr::Proxy {
url: "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1".to_string(),
host: "192.168.100.1".to_string(),
port: 8888,
}
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_socks5() -> Result<()> {
let ctx = TestContext::new().await;
let qr = check_qr(&ctx.ctx, "socks5://127.0.0.1:9050").await?;
assert_eq!(
qr,
Qr::Proxy {
url: "socks5://127.0.0.1:9050".to_string(),
host: "127.0.0.1".to_string(),
port: 9050,
}
);
Ok(())
}
}

View File

@@ -8,7 +8,7 @@ use num_traits::cast::ToPrimitive;
use super::{Qr, DCLOGIN_SCHEME};
use crate::config::Config;
use crate::context::Context;
use crate::login_param::EnteredCertificateChecks;
use crate::login_param::CertificateChecks;
use crate::provider::Socket;
/// Options for `dclogin:` scheme.
@@ -39,6 +39,9 @@ pub enum LoginOptions {
/// IMAP socket security.
imap_security: Option<Socket>,
/// IMAP certificate checks.
imap_certificate_checks: Option<CertificateChecks>,
/// SMTP host.
smtp_host: Option<String>,
@@ -54,8 +57,8 @@ pub enum LoginOptions {
/// SMTP socket security.
smtp_security: Option<Socket>,
/// Certificate checks.
certificate_checks: Option<EnteredCertificateChecks>,
/// SMTP certificate checks.
smtp_certificate_checks: Option<CertificateChecks>,
},
}
@@ -72,7 +75,7 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
.unwrap_or(url_without_scheme);
let addr = payload
.split(['?', '/'])
.split(|c| c == '?' || c == '/')
.next()
.context("invalid DCLOGIN payload E3")?;
@@ -104,13 +107,14 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
imap_username: parameter_map.get("iu").map(|s| s.to_owned()),
imap_password: parameter_map.get("ipw").map(|s| s.to_owned()),
imap_security: parse_socket_security(parameter_map.get("is"))?,
imap_certificate_checks: parse_certificate_checks(parameter_map.get("ic"))?,
smtp_host: parameter_map.get("sh").map(|s| s.to_owned()),
smtp_port: parse_port(parameter_map.get("sp"))
.context("could not parse smtp port")?,
smtp_username: parameter_map.get("su").map(|s| s.to_owned()),
smtp_password: parameter_map.get("spw").map(|s| s.to_owned()),
smtp_security: parse_socket_security(parameter_map.get("ss"))?,
certificate_checks: parse_certificate_checks(parameter_map.get("ic"))?,
smtp_certificate_checks: parse_certificate_checks(parameter_map.get("sc"))?,
},
Some(Ok(v)) => LoginOptions::UnsuportedVersion(v),
Some(Err(_)) => bail!("version could not be parsed as number E6"),
@@ -146,12 +150,11 @@ fn parse_socket_security(security: Option<&String>) -> Result<Option<Socket>> {
fn parse_certificate_checks(
certificate_checks: Option<&String>,
) -> Result<Option<EnteredCertificateChecks>> {
) -> Result<Option<CertificateChecks>> {
Ok(match certificate_checks.map(|s| s.as_str()) {
Some("0") => Some(EnteredCertificateChecks::Automatic),
Some("1") => Some(EnteredCertificateChecks::Strict),
Some("2") => Some(EnteredCertificateChecks::AcceptInvalidCertificates),
Some("3") => Some(EnteredCertificateChecks::AcceptInvalidCertificates2),
Some("0") => Some(CertificateChecks::Automatic),
Some("1") => Some(CertificateChecks::Strict),
Some("3") => Some(CertificateChecks::AcceptInvalidCertificates),
Some(other) => bail!("Unknown certificatecheck level: {}", other),
None => None,
})
@@ -174,12 +177,13 @@ pub(crate) async fn configure_from_login_qr(
imap_username,
imap_password,
imap_security,
imap_certificate_checks,
smtp_host,
smtp_port,
smtp_username,
smtp_password,
smtp_security,
certificate_checks,
smtp_certificate_checks,
} => {
context
.set_config_internal(Config::MailPw, Some(&mail_pw))
@@ -212,6 +216,14 @@ pub(crate) async fn configure_from_login_qr(
.set_config_internal(Config::MailSecurity, Some(&code.to_string()))
.await?;
}
if let Some(value) = imap_certificate_checks {
let code = value
.to_u32()
.context("could not convert imap certificate checks value to number")?;
context
.set_config_internal(Config::ImapCertificateChecks, Some(&code.to_string()))
.await?;
}
if let Some(value) = smtp_host {
context
.set_config_internal(Config::SendServer, Some(&value))
@@ -240,13 +252,10 @@ pub(crate) async fn configure_from_login_qr(
.set_config_internal(Config::SendSecurity, Some(&code.to_string()))
.await?;
}
if let Some(value) = certificate_checks {
if let Some(value) = smtp_certificate_checks {
let code = value
.to_u32()
.context("could not convert certificate checks value to number")?;
context
.set_config_internal(Config::ImapCertificateChecks, Some(&code.to_string()))
.await?;
.context("could not convert smtp certificate checks value to number")?;
context
.set_config_internal(Config::SmtpCertificateChecks, Some(&code.to_string()))
.await?;
@@ -264,7 +273,7 @@ mod test {
use anyhow::bail;
use super::{decode_login, LoginOptions};
use crate::{login_param::EnteredCertificateChecks, provider::Socket, qr::Qr};
use crate::{login_param::CertificateChecks, provider::Socket, qr::Qr};
macro_rules! login_options_just_pw {
($pw: expr) => {
@@ -275,12 +284,13 @@ mod test {
imap_username: None,
imap_password: None,
imap_security: None,
imap_certificate_checks: None,
smtp_host: None,
smtp_port: None,
smtp_username: None,
smtp_password: None,
smtp_security: None,
certificate_checks: None,
smtp_certificate_checks: None,
}
};
}
@@ -382,12 +392,13 @@ mod test {
imap_username: Some("max".to_owned()),
imap_password: Some("87654".to_owned()),
imap_security: Some(Socket::Ssl),
imap_certificate_checks: Some(CertificateChecks::Strict),
smtp_host: Some("mail.host.tld".to_owned()),
smtp_port: Some(3000),
smtp_username: Some("max@host.tld".to_owned()),
smtp_password: Some("3242HS".to_owned()),
smtp_security: Some(Socket::Plain),
certificate_checks: Some(EnteredCertificateChecks::Strict),
smtp_certificate_checks: Some(CertificateChecks::AcceptInvalidCertificates),
}
);
} else {

Some files were not shown because too many files have changed in this diff Show More