Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
bcf2a34579 fix: fix compilation on iOS 2024-08-02 14:45:47 +00:00
152 changed files with 6257 additions and 11099 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
@@ -249,7 +249,7 @@ jobs:
- name: Run python tests
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
@@ -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
@@ -314,6 +314,6 @@ jobs:
- name: Run deltachat-rpc-client tests
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
working-directory: deltachat-rpc-client
run: tox -e py

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

@@ -33,7 +33,7 @@ jobs:
working-directory: deltachat-jsonrpc/typescript
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
- name: make sure websocket server version still builds
working-directory: deltachat-jsonrpc
run: cargo build --bin deltachat-jsonrpc-server --features webserver

View File

@@ -1,22 +0,0 @@
name: Test Nix flake
on:
pull_request:
push:
branches:
- main
jobs:
format:
name: check flake formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix fmt
# Check that formatting does not change anything.
- run: git diff --exit-code

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

@@ -64,5 +64,5 @@ jobs:
working-directory: node
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"

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,581 +1,5 @@
# Changelog
## [1.149.0] - 2024-11-05
### Build system
- Update tokio to 1.41 and Android NDK to r27.
- `nix flake update android`.
### Fixes
- cargo: Update iroh to 0.28.1.
This fixes the problem with iroh not sending the `Host:` header and not being able to connect to relays behind nginx reverse proxy.
## [1.148.7] - 2024-11-03
### API-Changes
- Add API to reset contact encryption.
### Features / Changes
- Emit chatlist events only if message still exists.
### Fixes
- send_msg_to_smtp: Do not fail if the message does not exist anymore.
- Do not percent-encode dot when passing to autoconfig server.
- Save contact name from SecureJoin QR to `authname`, not to `name` ([#6115](https://github.com/deltachat/deltachat-core-rust/pull/6115)).
- Always exit fake IDLE after at most 60 seconds.
- Concat NDNs ([#6129](https://github.com/deltachat/deltachat-core-rust/pull/6129)).
### Refactor
- Remove `has_decrypted_pgp_armor()`.
### Miscellaneous Tasks
- Update dependencies.
## [1.148.6] - 2024-10-31
### API-Changes
- Add Message::new_text() ([#6123](https://github.com/deltachat/deltachat-core-rust/pull/6123)).
- Add `MessageSearchResult.chat_id` ([#6120](https://github.com/deltachat/deltachat-core-rust/pull/6120)).
### Features / Changes
- Enable Webxdc realtime by default ([#6125](https://github.com/deltachat/deltachat-core-rust/pull/6125)).
### Fixes
- Save full text to mime_headers for long outgoing messages ([#6091](https://github.com/deltachat/deltachat-core-rust/pull/6091)).
- Show root SMTP connection failure in connectivity view ([#6121](https://github.com/deltachat/deltachat-core-rust/pull/6121)).
- Skip IDLE if we got unsolicited FETCH ([#6130](https://github.com/deltachat/deltachat-core-rust/pull/6130)).
### Miscellaneous Tasks
- Silence another rust-analyzer false-positive ([#6124](https://github.com/deltachat/deltachat-core-rust/pull/6124)).
- cargo: Upgrade iroh to 0.26.0.
### Refactor
- Directly use connectives ([#6128](https://github.com/deltachat/deltachat-core-rust/pull/6128)).
- Use Message::new_text() more ([#6127](https://github.com/deltachat/deltachat-core-rust/pull/6127)).
## [1.148.5] - 2024-10-27
### Fixes
- Set Config::NotifyAboutWrongPw before saving configuration ([#5896](https://github.com/deltachat/deltachat-core-rust/pull/5896)).
- Do not take write lock for maybe_network_lost() and set_push_device_token().
- Do not lock the account manager for the whole duration of background_fetch.
### Features / Changes
- Auto-restore 1:1 chat protection after receiving old unverified message.
### CI
- Take `CHATMAIL_DOMAIN` from variables instead of secrets.
### Other
- Revert "build: nix flake update fenix" to fix `nix build .#deltachat-rpc-server-armeabi-v7a-android`.
### Refactor
- Receive_imf::add_parts: Remove excessive `from_id == ContactId::SELF` checks.
- Factor out `add_gossip_peer_from_header()`.
## [1.148.4] - 2024-10-24
### Features / Changes
- Jsonrpc: add `private_tag` to `Account::Configured` Object ([#6107](https://github.com/deltachat/deltachat-core-rust/pull/6107)).
### Fixes
- Normalize proxy URLs before saving into proxy_url.
- Do not wait for connections in maybe_add_gossip_peers().
## [1.148.3] - 2024-10-24
### Fixes
- Fix reception of realtime advertisements.
### Features / Changes
- Allow sending realtime messages up to 128 KB in size.
### API-Changes
- deltachat-rpc-client: Add EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED.
### Documentation
- Fix DC_QR_PROXY docs ([#6099](https://github.com/deltachat/deltachat-core-rust/pull/6099)).
### Refactor
- Generate topic inside create_iroh_header().
### Tests
- Test that realtime advertisements work after chatting.
## [1.148.2] - 2024-10-23
### Fixes
- Never initialize Iroh if realtime is disabled.
### Features / Changes
- Add more logging for iroh initialization and peer addition.
### Build system
- `nix flake update nixpkgs`.
- `nix flake update fenix`.
## [1.148.1] - 2024-10-23
### Build system
- Revert "build: nix flake update"
This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
## [1.148.0] - 2024-10-22
### API-Changes
- Create QR codes from any data ([#6090](https://github.com/deltachat/deltachat-core-rust/pull/6090)).
- Add delta chat logo to QR codes ([#6093](https://github.com/deltachat/deltachat-core-rust/pull/6093)).
- Add realtime advertisement received event ([#6043](https://github.com/deltachat/deltachat-core-rust/pull/6043)).
- Notify adding reactions ([#6072](https://github.com/deltachat/deltachat-core-rust/pull/6072))
- Internal profile names ([#6088](https://github.com/deltachat/deltachat-core-rust/pull/6088)).
### Features / Changes
- IMAP COMPRESS support.
- Sort received outgoing message down if it's fresher than all non fresh messages.
- Prioritize cached results if DNS resolver returns many results.
- Add in-memory cache for DNS.
- deltachat-repl: Built-in QR code printer.
- Log the logic for (not) doing AEAP.
- Log when late Autocrypt header is ignored.
- Add more context to `send_msg` errors.
### Fixes
- Replace old draft with a new one atomically.
- ChatId::maybe_delete_draft: Don't delete message if it's not a draft anymore ([#6053](https://github.com/deltachat/deltachat-core-rust/pull/6053)).
- Call update_connection_history for proxified connections.
- sql: Set PRAGMA query_only to avoid writing on read-only connections.
- sql: Run `PRAGMA incremental_vacuum` on a write connection.
- Increase MAX_SECONDS_TO_LEND_FROM_FUTURE to 30.
### Build system
- Nix flake update.
- Resolve warning about default-features, and make it possible to disable vendoring ([#6079](https://github.com/deltachat/deltachat-core-rust/pull/6079)).
- Silence a rust-analyzer false-positive ([#6077](https://github.com/deltachat/deltachat-core-rust/pull/6077)).
### CI
- Update Rust to 1.82.0.
### Documentation
- Set_protection_for_timestamp_sort does not send messages.
- Document MimeFactory.req_mdn.
- Fix `too_long_first_doc_paragraph` clippy lint.
### Refactor
- Update_msg_state: Don't avoid downgrading OutMdnRcvd to OutDelivered.
- Fix elided_named_lifetimes warning.
- set_protection_for_timestamp_sort: Do not log bubbled up errors.
- Fix clippy::needless_lifetimes warnings.
- Use `HeaderDef` constant for Chat-Disposition-Notification-To.
- Resultify get_self_fingerprint().
- sql: Move write mutex into connection pool.
### Tests
- test_qr_setup_contact_svg: Stop testing for no display name.
- Always gossip if gossip_period is set to 0.
- test_aeap_flow_verified: Wait for "member added" before sending messages ([#6057](https://github.com/deltachat/deltachat-core-rust/pull/6057)).
- Make test_verified_group_member_added_recovery more reliable.
- test_aeap_flow_verified: Do not start ac1new.
- Fix `test_securejoin_after_contact_resetup` flakiness.
- Message from old setup preserves contact verification, but breaks 1:1 protection.
## [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
@@ -4481,10 +3905,14 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac
- new qr-code type `DC_QR_WEBRTC` #1779
- new `dc_chatlist_get_summary2()` api #1771
- tweak smtp-timeout for larger mails #1782
- optimize read-receipts #1765
- Allow http scheme for DCACCOUNT URLs #1770
- improve tests #1769
- bug fixes #1766 #1772 #1773 #1775 #1776 #1777
@@ -5203,29 +4631,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[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
[1.148.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.147.1..v1.148.0
[1.148.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.0..v1.148.1
[1.148.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.1..v1.148.2
[1.148.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.2..v1.148.3
[1.148.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.3..v1.148.4
[1.148.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.4..v1.148.5
[1.148.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.5..v1.148.6
[1.148.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.6..v1.148.7
[1.149.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.7..v1.149.0

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.

2870
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.149.0"
version = "1.142.1"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -41,31 +41,30 @@ 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 = "7", default-features=false, features = ["std"] }
bytes = "1"
brotli = { version = "6", default-features=false, features = ["std"] }
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.10"
image = { version = "0.25.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.28.1", default-features = false, features = ["net"] }
iroh-net = { version = "0.28.1", default-features = false }
kamadak-exif = "0.6.0"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
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 }
mailparse = "0.15"
@@ -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.14.0", default-features = false }
pin-project = "1"
parking_lot = "0.12"
pgp = { version = "0.13", default-features = false }
qrcodegen = "1.7.0"
quick-xml = "0.37"
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.10.0"
rustls = { version = "0.23.14", 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,28 +159,28 @@ 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.31"
futures-lite = "2.4.0"
deltachat-jsonrpc = { path = "deltachat-jsonrpc" }
deltachat = { path = "." }
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.20.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"
tokio = "1"
tokio = "1.38.0"
tokio-util = "0.7.11"
tracing-subscriber = "0.3"
yerpc = "0.6.2"
@@ -195,7 +189,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,12 +0,0 @@
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 24.015419,1.2870249 c -12.549421,0 -22.7283936,10.1789711 -22.7283936,22.7283931 0,12.549422 10.1789726,22.728395 22.7283936,22.728395 14.337742,-0.342877 9.614352,-4.702705 23.697556,0.969161 -7.545453,-13.001555 -1.082973,-13.32964 -0.969161,-23.697556 0,-12.549422 -10.178973,-22.7283931 -22.728395,-22.7283931 z" />
<path
style="fill:#000000;fill-opacity:1;stroke:none"
d="M 23.982249,5.3106163 C 13.645822,5.4364005 5.2618355,13.92999 5.2618355,24.275753 c 0,10.345764 8.3839865,18.635301 18.7204135,18.509516 9.827724,-0.03951 7.516769,-5.489695 18.380082,-0.443187 -5.950849,-9.296115 0.201753,-10.533667 0.340336,-18.521947 0,-10.345766 -8.383989,-18.6353031 -18.720418,-18.5095187 z" />
<g
style="fill:#ffffff"
transform="scale(1.1342891,0.88160947)">
<path
d="m 21.360141,23.513382 q -1.218487,-1.364705 -3.387392,-3.265543 -2.388233,-2.095797 -3.216804,-3.289913 -0.828571,-1.218486 -0.828571,-2.6563 0,-2.144536 1.998318,-3.363022 1.998317,-1.2428565 5.215121,-1.2428565 3.216804,0 5.605037,1.0966375 2.412603,1.096638 2.412603,3.021846 0,0.92605 -0.584873,1.535293 -0.584874,0.609243 -1.364705,0.609243 -1.121008,0 -2.631931,-1.681511 -1.535292,-1.705881 -2.60756,-2.388233 -1.047898,-0.706722 -2.461343,-0.706722 -1.803359,0 -2.973106,0.804201 -1.145377,0.804201 -1.145377,2.047057 0,1.169747 0.950419,2.193275 0.950419,1.023529 4.898315,3.728568 4.215963,2.899998 5.946213,4.532769 1.75462,1.632772 2.851258,3.972265 1.096638,2.339494 1.096638,4.947055 0,4.581508 -3.241174,8.090749 -3.216804,3.484871 -7.530245,3.484871 -3.923526,0 -6.628566,-2.802519 -2.705039,-2.802518 -2.705039,-7.481506 0,-4.508399 2.973106,-7.530245 2.997477,-3.021846 7.359658,-3.655459 z m 1.072268,1.121008 q -6.994112,1.145377 -6.994112,9.601672 0,4.36218 1.730251,6.774783 1.75462,2.412603 4.069744,2.412603 2.412603,0 3.972265,-2.315124 1.559663,-2.339493 1.559663,-6.311759 0,-5.751255 -4.337811,-10.162175 z" />
</g>

Binary file not shown.

View File

@@ -15,8 +15,7 @@
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.149.0"
version = "1.142.1"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -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,
@@ -506,11 +508,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
* This is an experimental option not compatible to other MUAs
* and older Delta Chat versions.
* 1 = enable.
* 0 = disable (default).
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
@@ -527,16 +524,14 @@ 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`.
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
* however, are not handled by the core otherwise.
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop.
* 1 = WebXDC realtime API is enabled (default).
* 0 = WebXDC realtime API is disabled and behaves as noop (default).
* 1 = WebXDC realtime API is enabled.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -871,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
@@ -1554,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.
*
@@ -2484,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
@@ -2538,12 +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_PROXY with dc_lot_t::text1=address:
* ask the user if they want to use the given proxy.
* if so, call dc_set_config_from_qr() and restart I/O.
* On success, dc_get_config(context, "proxy_url")
* will contain the new proxy in normalized form as the first element.
*
* - 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;
@@ -2618,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,
@@ -2799,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.
*
@@ -2903,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().
@@ -2943,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
@@ -5711,14 +5708,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_CERTCK_STRICT 1
/**
* Accept certificates that are expired, self-signed
* or not valid for the server hostname.
*/
#define DC_CERTCK_ACCEPT_INVALID 2
/**
* For API compatibility only: Treat this as DC_CERTCK_ACCEPT_INVALID on reading.
* Must not be written.
* Accept invalid certificates, including self-signed ones
* or having incorrect hostname.
*/
#define DC_CERTCK_ACCEPT_INVALID_CERTIFICATES 3
@@ -6064,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.
@@ -6296,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`
@@ -6331,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()
@@ -4868,7 +4880,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
}
let accounts = &*accounts;
block_on(async move { accounts.read().await.maybe_network_lost().await });
block_on(async move { accounts.write().await.maybe_network_lost().await });
}
#[no_mangle]
@@ -4882,12 +4894,12 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
}
let accounts = &*accounts;
let background_fetch_future = {
let lock = block_on(accounts.read());
lock.background_fetch(Duration::from_secs(timeout_in_seconds))
};
// At this point account manager is not locked anymore.
block_on(background_fetch_future);
block_on(async move {
let accounts = accounts.read().await;
accounts
.background_fetch(Duration::from_secs(timeout_in_seconds))
.await;
});
1
}
@@ -4905,7 +4917,7 @@ pub unsafe extern "C" fn dc_accounts_set_push_device_token(
let token = to_string_lossy(token);
block_on(async move {
let accounts = accounts.read().await;
let mut accounts = accounts.write().await;
if let Err(err) = accounts.set_push_device_token(&token).await {
accounts.emit_event(EventType::Error(format!(
"Failed to set notify token: {err:#}."

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.149.0"
version = "1.142.1"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -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.13", 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

@@ -254,12 +254,11 @@ impl CommandApi {
/// Process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
let future = {
let lock = self.accounts.read().await;
lock.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
};
// At this point account manager is not locked anymore.
future.await;
self.accounts
.write()
.await
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
.await;
Ok(())
}
@@ -322,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))
}
@@ -1135,11 +1134,9 @@ impl CommandApi {
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
let msg_id = MsgId::new(msg_id);
let message_object = MessageObject::from_msg_id(&ctx, msg_id)
MessageObject::from_msg_id(&ctx, msg_id)
.await
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))?
.with_context(|| format!("Message {msg_id} does not exist for account {account_id}"))?;
Ok(message_object)
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
@@ -1163,10 +1160,7 @@ impl CommandApi {
messages.insert(
message_id,
match message_result {
Ok(Some(message)) => MessageLoadResult::Message(message),
Ok(None) => MessageLoadResult::LoadingError {
error: "Message does not exist".to_string(),
},
Ok(message) => MessageLoadResult::Message(message),
Err(error) => MessageLoadResult::LoadingError {
error: format!("{error:#}"),
},
@@ -1424,15 +1418,6 @@ impl CommandApi {
Ok(())
}
/// Resets contact encryption.
async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
contact_id.reset_encryption(&ctx).await?;
Ok(())
}
async fn change_contact_name(
&self,
account_id: u32,
@@ -1567,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
// ---------------------------------------------
@@ -1638,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
@@ -1657,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
@@ -1961,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)
}
@@ -2004,7 +2034,9 @@ impl CommandApi {
async fn get_draft(&self, account_id: u32, chat_id: u32) -> Result<Option<MessageObject>> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
Ok(MessageObject::from_msg_id(&ctx, draft.get_id()).await?)
Ok(Some(
MessageObject::from_msg_id(&ctx, draft.get_id()).await?,
))
} else {
Ok(None)
}
@@ -2130,7 +2162,8 @@ impl CommandApi {
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new_text(text);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(text);
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
@@ -2173,9 +2206,7 @@ impl CommandApi {
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
let message = MessageObject::from_msg_id(&ctx, msg_id)
.await?
.context("Just sent message does not exist")?;
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
Ok((msg_id.to_u32(), message))
}

View File

@@ -17,9 +17,6 @@ pub enum Account {
// size: u32,
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
color: String,
/// Optional tag as "Work", "Family".
/// Meant to help profile owner to differ between profiles with similar names.
private_tag: Option<String>,
},
#[serde(rename_all = "camelCase")]
Unconfigured { id: u32 },
@@ -34,14 +31,12 @@ impl Account {
let color = color_int_to_hex_string(
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
);
let private_tag = ctx.get_config(Config::PrivateTag).await?;
Ok(Account::Configured {
id,
display_name,
addr,
profile_image,
color,
private_tag,
})
} else {
Ok(Account::Unconfigured { id })

View File

@@ -88,17 +88,11 @@ pub(crate) async fn get_chat_list_item_by_id(
let (last_updated, message_type) = match last_msgid {
Some(id) => {
if let Some(last_message) =
deltachat::message::Message::load_from_db_optional(ctx, id).await?
{
(
Some(last_message.get_timestamp() * 1000),
Some(last_message.get_viewtype().into()),
)
} else {
// Message may be deleted by the time we try to load it.
(None, None)
}
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
(
Some(last_message.get_timestamp() * 1000),
Some(last_message.get_viewtype().into()),
)
}
None => (None, None),
};

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(),
},
@@ -409,9 +382,6 @@ impl From<CoreEventType> for EventType {
},
CoreEventType::ChatlistChanged => ChatlistChanged,
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
}

View File

@@ -112,10 +112,8 @@ enum MessageQuote {
}
impl MessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Option<Self>> {
let Some(message) = Message::load_from_db_optional(context, msg_id).await? else {
return Ok(None);
};
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let sender_contact = Contact::get_by_id(context, message.get_from_id())
.await
@@ -185,7 +183,7 @@ impl MessageObject {
.map(Into::into)
.collect();
let message_object = MessageObject {
Ok(MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
from_id: message.get_from_id().to_u32(),
@@ -246,8 +244,7 @@ impl MessageObject {
reactions,
vcard_contact: vcard_contacts.first().cloned(),
};
Ok(Some(message_object))
})
}
}
@@ -493,7 +490,6 @@ pub struct MessageSearchResult {
author_name: String,
author_color: String,
author_id: u32,
chat_id: u32,
chat_profile_image: Option<String>,
chat_color: String,
chat_name: String,
@@ -533,7 +529,6 @@ impl MessageSearchResult {
author_name,
author_color: color_int_to_hex_string(sender.get_color()),
author_id: sender.id.to_u32(),
chat_id: chat.id.to_u32(),
chat_name: chat.get_name().to_owned(),
chat_color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
@@ -610,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)));
@@ -642,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

@@ -6,161 +6,85 @@ use typescript_type_def::TypeDef;
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "kind")]
pub enum QrObject {
/// Ask the user whether to verify the contact.
///
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
AskVerifyContact {
/// ID of the contact.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user whether to join the group.
AskVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// ID of the contact.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
FprOk {
/// Contact ID.
contact_id: u32,
},
/// Scanned fingerprint does not match the last seen fingerprint.
FprMismatch {
/// Contact ID.
contact_id: Option<u32>,
},
/// The scanned QR code contains a fingerprint but no e-mail address.
FprWithoutAddr {
/// Key fingerprint.
fingerprint: String,
},
/// Ask the user if they want to create an account on the given domain.
Account {
/// Server domain name.
domain: String,
},
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
Backup {
ticket: String,
},
Backup2 {
/// Authentication token.
auth_token: String,
/// Iroh node address.
node_addr: String,
},
/// Ask the user if they want to use the given service for video chats.
WebrtcInstance {
domain: String,
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.
/// Ask the user if they want to start chatting.
Addr {
/// Contact ID.
contact_id: u32,
/// Draft message.
draft: Option<String>,
},
/// URL scanned.
///
/// Ask the user if they want to open a browser or copy the URL to clipboard.
Url { url: String },
/// Text scanned.
///
/// Ask the user if they want to copy the text to clipboard.
Text { text: String },
/// Ask the user if they want to withdraw their own QR code.
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to withdraw their own group invite QR code.
WithdrawVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to revive their own QR code.
ReviveVerifyContact {
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to revive their own group invite QR code.
ReviveVerifyGroup {
/// Contact ID.
grpname: String,
/// Group ID.
grpid: String,
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// `dclogin:` scheme parameters.
///
/// Ask the user if they want to login with the email address.
Login { address: String },
Login {
address: String,
},
}
impl From<Qr> for QrObject {
@@ -210,11 +134,15 @@ 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,
} => QrObject::Backup2 {
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
auth_token,
},
Qr::WebrtcInstance {
@@ -224,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

@@ -1,6 +1,4 @@
#![recursion_limit = "256"]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
pub mod api;
pub use yerpc;
@@ -85,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.149.0"
"version": "1.142.1"
}

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.149.0"
version = "1.142.1"
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"]}
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" => {
@@ -1004,7 +1000,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
if !arg1.is_empty() {
let mut draft = Message::new_text(arg1.to_string());
let mut draft = Message::new(Viewtype::Text);
draft.set_text(arg1.to_string());
sel_chat
.as_ref()
.unwrap()
@@ -1027,7 +1024,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
!arg1.is_empty(),
"Please specify text to add as device message."
);
let mut msg = Message::new_text(arg1.to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text(arg1.to_string());
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"listmedia" => {
@@ -1249,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.149.0"
version = "1.142.1"
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

@@ -63,7 +63,6 @@ class EventType(str, Enum):
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
class ChatId(IntEnum):

View File

@@ -36,10 +36,6 @@ class Contact:
"""Delete contact."""
self._rpc.delete_contact(self.account.id, self.id)
def reset_encryption(self) -> None:
"""Reset contact encryption."""
self._rpc.reset_contact_encryption(self.account.id, self.id)
def set_name(self, name: str) -> None:
"""Change the name of this contact."""
self._rpc.change_contact_name(self.account.id, self.id, name)

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

@@ -7,17 +7,15 @@ If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import logging
import os
import sys
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()
@@ -25,7 +23,9 @@ def path_to_webxdc(request):
def log(msg):
logging.info(msg)
print()
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
print()
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
@@ -106,15 +106,13 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
assert snapshot.text == "ping2"
log("sending realtime data ac1 -> ac2")
# Test that 128 KB of data can be sent in a single message.
data = os.urandom(128000)
ac1_webxdc_msg.send_webxdc_realtime_data(data)
ac1_webxdc_msg.send_webxdc_realtime_data(b"foo")
log("ac2: waiting for realtime data")
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert event.data == list(data)
assert event.data == list(b"foo")
break
@@ -209,28 +207,3 @@ def test_no_reordering(acfactory, path_to_webxdc):
if event.data[0] == i:
break
pytest.fail("Reordering detected")
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
"""Test that realtime advertisement is assigned to the correct message after chatting."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
assert ac2_webxdc_msg.get_snapshot().text == "WebXDC"
ac1_ac2_chat.send_text("Hello!")
ac2_hello_msg = ac2.wait_for_incoming_msg()
ac2_hello_msg_snapshot = ac2_hello_msg.get_snapshot()
assert ac2_hello_msg_snapshot.text == "Hello!"
ac2_hello_msg_snapshot.chat.accept()
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
while 1:
event = ac1.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
assert event.msg_id == ac1_webxdc_msg.id
break

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
@@ -57,8 +55,8 @@ def test_acfactory(acfactory) -> None:
if event.progress == 1000: # Success
break
else:
logging.info(event)
logging.info("Successful configuration")
print(event)
print("Successful configuration")
def test_configure_starttls(acfactory) -> None:
@@ -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)
@@ -246,7 +212,6 @@ def test_contact(acfactory) -> None:
assert repr(alice_contact_bob)
alice_contact_bob.block()
alice_contact_bob.unblock()
alice_contact_bob.reset_encryption()
alice_contact_bob.set_name("new name")
alice_contact_bob.get_encryption_info()
snapshot = alice_contact_bob.get_snapshot()
@@ -434,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
@@ -656,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.149.0"
version = "1.142.1"
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 = { workspace = true, default-features = false }
deltachat = { workspace = true, default-features = false }
anyhow = { workspace = true }
futures-lite = { workspace = true }

View File

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

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,15 +10,15 @@ ignore = [
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
# Unmaintained ansi_term
"RUSTSEC-2021-0139",
# Unmaintained encoding
"RUSTSEC-2021-0153",
# Unmaintained proc-macro-error
# <https://rustsec.org/advisories/RUSTSEC-2024-0370>
"RUSTSEC-2024-0370",
# Unmaintained instant
"RUSTSEC-2024-0384",
# 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]
@@ -26,46 +27,93 @@ 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" },
{ name = "fiat-crypto", version = "0.1.20" },
{ name = "futures-lite", version = "1.13.0" },
{ name = "getrandom", version = "<0.2" },
{ 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 = "num_enum_derive", version = "0.5.11" },
{ name = "num_enum", version = "0.5.11" },
{ name = "proc-macro-crate", version = "1.3.1" },
{ 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 = "toml_edit", version = "0.19.15" },
{ 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" },
]

111
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": 1731356359,
"narHash": "sha256-vYqJnu6jotmWpPT4DgzHVdvNIZcKZCIUqS8QaptsZA0=",
"lastModified": 1712088936,
"narHash": "sha256-mVjeSWQiR/t4UZ9fUawY9OEPAhY1R3meYG+0oh8DUBs=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "c028ead7e88edb2e94cd7c90ee37593f63ae494a",
"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": 1731393059,
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
"lastModified": 1714112748,
"narHash": "sha256-jq6Cpf/pQH85p+uTwPPrGG8Ky/zUOTwMJ7mcqc5M4So=",
"owner": "nix-community",
"repo": "fenix",
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
"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": {
@@ -116,11 +135,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1730207686,
"narHash": "sha256-SCHiL+1f7q9TAnxpasriP6fMarWE5H43t25F5/9e28I=",
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "776e68c1d014c3adde193a18db9d738458cd2ba4",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"type": "github"
},
"original": {
@@ -131,11 +150,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1711703276,
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
"type": "github"
},
"original": {
@@ -147,11 +166,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1713895582,
"narHash": "sha256-cfh1hi+6muQMbi9acOlju3V1gl8BEaZBXBR9jQfQi4U=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "572af610f6151fd41c212f897c71f7056e3fb518",
"type": "github"
},
"original": {
@@ -163,9 +182,10 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 0,
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
"path": "/nix/store/zq2axpgzd5kykk1v446rkffj3bxa2m2h-source",
"lastModified": 1711668574,
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
"type": "path"
},
"original": {
@@ -175,11 +195,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1714076141,
"narHash": "sha256-Drmja/f5MRHZCskS6mvzFqxEaZMeciScCTFxWVLqWEY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "7bb2ccd8cdc44c91edba16c48d2c8f331fb3d856",
"type": "github"
},
"original": {
@@ -193,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"
@@ -202,11 +222,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1731342671,
"narHash": "sha256-36eYDHoPzjavnpmEpc2MXdzMk557S0YooGms07mDuKk=",
"lastModified": 1714031783,
"narHash": "sha256-xS/niQsq1CQPOe4M4jvVPO2cnXS/EIeRG5gIopUbk+Q=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "fc98e0657abf3ce07eed513e38274c89bbb2f8ad",
"rev": "56bee2ddafa6177b19c631eedc88d43366553223",
"type": "github"
},
"original": {
@@ -245,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

@@ -18,9 +18,9 @@
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
inherit (sdkPkgs) ndk-27-0-11902837 cmdline-tools-latest;
inherit (sdkPkgs) ndk-24-0-8215888 cmdline-tools-latest;
});
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.0.11902837";
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/24.0.8215888";
rustSrc = nix-filter.lib {
root = ./.;
@@ -257,21 +257,13 @@
androidAttrs = {
armeabi-v7a = {
cc = "armv7a-linux-androideabi21-clang";
cc = "armv7a-linux-androideabi19-clang";
rustTarget = "armv7-linux-androideabi";
};
arm64-v8a = {
cc = "aarch64-linux-android21-clang";
rustTarget = "aarch64-linux-android";
};
x86 = {
cc = "i686-linux-android21-clang";
rustTarget = "i686-linux-android";
};
x86_64 = {
cc = "x86_64-linux-android21-clang";
rustTarget = "x86_64-linux-android";
};
};
mkAndroidRustPackage = arch: packageName:
@@ -533,30 +525,28 @@
};
};
devShells.default =
let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
};
in
pkgs.mkShell {
buildInputs = with pkgs; [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
];
devShells.default = let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
};
in pkgs.mkShell {
buildInputs = with pkgs; [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
];
};
}
);
}

View File

@@ -8,8 +8,6 @@
//! is assumed to be set to "no".
//!
//! For received messages, DelSp parameter is honoured.
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
/// Wraps line to 72 characters using format=flowed soft breaks.
///

View File

@@ -1,7 +1,6 @@
// Generated!
module.exports = {
DC_CERTCK_ACCEPT_INVALID: 2,
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 3,
DC_CERTCK_AUTO: 0,
DC_CERTCK_STRICT: 1,
@@ -51,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,
@@ -69,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,
@@ -137,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

@@ -1,7 +1,6 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID = 2,
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
@@ -51,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,
@@ -69,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,
@@ -137,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,
@@ -324,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',
@@ -347,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.149.0"
"version": "1.142.1"
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.149.0"
version = "1.142.1"
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)
@@ -2209,19 +2192,6 @@ def test_configure_error_msgs_wrong_pw(acfactory):
# Password is wrong so it definitely has to say something about "password"
assert "password" in ev.data2
ac1.stop_io()
ac1.set_config("mail_pw", "abc") # Wrong mail pw
ac1.configure()
while True:
ev = ac1._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
print(f"Configuration progress: {ev.data1}")
if ev.data1 == 0:
break
assert "password" in ev.data2
# Account will continue to work with the old password, so if it becomes wrong, a notification
# must be shown.
assert ac1.get_config("notify_about_wrong_pw") == "1"
def test_configure_error_msgs_invalid_server(acfactory):
ac2 = acfactory.get_unconfigured_account()

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()
@@ -705,7 +705,7 @@ class TestOfflineChat:
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
qr = ac1.get_setup_contact_qr()
assert qr.startswith("https://i.delta.chat")
assert qr.startswith("OPENPGP4FPR:")
res = ac2.check_qr(qr)
assert res.is_ask_verifycontact()
assert not res.is_ask_verifygroup()

View File

@@ -1 +1 @@
2024-11-05
2024-07-30

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

@@ -5,8 +5,7 @@ use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use futures::future::join_all;
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
@@ -302,48 +301,20 @@ impl Accounts {
///
/// This is an auxiliary function and not part of public API.
/// Use [Accounts::background_fetch] instead.
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
async fn background_fetch_without_timeout(&self) {
async fn background_fetch_and_log_error(account: Context) {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
}
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Starting background fetch for {} accounts.",
accounts.len()
)),
});
let mut futures_unordered: FuturesUnordered<_> = accounts
.into_iter()
.map(background_fetch_and_log_error)
.collect();
while futures_unordered.next().await.is_some() {}
}
/// Auxiliary function for [Accounts::background_fetch].
async fn background_fetch_with_timeout(
accounts: Vec<Context>,
events: Events,
timeout: std::time::Duration,
) {
if let Err(_err) = tokio::time::timeout(
timeout,
Self::background_fetch_no_timeout(accounts, events.clone()),
join_all(
self.accounts
.values()
.cloned()
.map(background_fetch_and_log_error),
)
.await
{
events.emit(Event {
id: 0,
typ: EventType::Warning("Background fetch timed out.".to_string()),
});
}
events.emit(Event {
id: 0,
typ: EventType::AccountsBackgroundFetchDone,
});
.await;
}
/// Performs a background fetch for all accounts in parallel with a timeout.
@@ -351,13 +322,15 @@ impl Accounts {
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
/// process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
///
/// Returns a future that resolves when background fetch is done,
/// but does not capture `&self`.
pub fn background_fetch(&self, timeout: std::time::Duration) -> impl Future<Output = ()> {
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
let events = self.events.clone();
Self::background_fetch_with_timeout(accounts, events, timeout)
pub async fn background_fetch(&self, timeout: std::time::Duration) {
if let Err(_err) =
tokio::time::timeout(timeout, self.background_fetch_without_timeout()).await
{
self.emit_event(EventType::Warning(
"Background fetch timed out.".to_string(),
));
}
self.emit_event(EventType::AccountsBackgroundFetchDone);
}
/// Emits a single event.
@@ -371,7 +344,7 @@ impl Accounts {
}
/// Sets notification token for Apple Push Notification service.
pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
pub async fn set_push_device_token(&mut self, token: &str) -> Result<()> {
self.push_subscriber.set_device_token(token).await;
Ok(())
}

View File

@@ -260,6 +260,7 @@ fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str>
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use tokio::fs;
use tokio::io::AsyncReadExt;
@@ -519,13 +520,8 @@ Authentication-Results: dkim=";
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
}
// Test that Autocrypt works with mailing list.
//
// Previous versions of Delta Chat ignored Autocrypt based on the List-Post header.
// This is not needed: comparing of the From address to Autocrypt header address is enough.
// If the mailing list is not rewriting the From header, Autocrypt should be applied.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> {
async fn test_autocrypt_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -537,18 +533,28 @@ Authentication-Results: dkim=";
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_none());
// Do the same without the mailing list header, this time the peerstate should be accepted
let sent = alice
.send_text(alice_bob_chat.id, "hellooo without mailing list")
.await;
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_some());
// Bob can now write encrypted to Alice:
// This also means that Bob can now write encrypted to Alice:
let mut sent = bob
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
.await;
assert!(sent.load_from_db().await.get_showpadlock());
// But if Bob writes to a mailing list, Alice doesn't show a padlock
// since she can't verify the signature without accepting Bob's key:
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await;
assert!(rcvd.get_showpadlock());
assert!(!rcvd.get_showpadlock());
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
Ok(())

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)]

File diff suppressed because it is too large Load Diff

View File

@@ -394,32 +394,25 @@ impl Chatlist {
&chat_loaded
};
let lastmsg = if let Some(lastmsg_id) = lastmsg_id {
// Message may be deleted by the time we try to load it,
// so use `load_from_db_optional` instead of `load_from_db`.
Message::load_from_db_optional(context, lastmsg_id)
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
let lastmsg = Message::load_from_db(context, lastmsg_id)
.await
.context("Loading message failed")?
} else {
None
};
let lastcontact = if let Some(lastmsg) = &lastmsg {
.context("loading message failed")?;
if lastmsg.from_id == ContactId::SELF {
None
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
.await
.context("loading contact failed")?;
Some(lastcontact)
(Some(lastmsg), Some(lastcontact))
}
Chattype::Single => None,
Chattype::Single => (Some(lastmsg), None),
}
}
} else {
None
(None, None)
};
if chat.id.is_archived_link() {
@@ -483,6 +476,7 @@ mod tests {
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
send_text_msg, ProtectionStatus,
};
use crate::message::Viewtype;
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
@@ -516,7 +510,8 @@ mod tests {
// Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
// 2s here.
for chat_id in &[chat_id1, chat_id3, chat_id2] {
let mut msg = Message::new_text("hello".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hello".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
}
@@ -760,7 +755,8 @@ mod tests {
.await
.unwrap();
let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foo:\nbar \r\n test".to_string());
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
@@ -768,25 +764,6 @@ mod tests {
assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
}
/// Tests that summary does not fail to load
/// if the draft was deleted after loading the chatlist.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_deleted_draft() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let mut msg = Message::new_text("Foobar".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
chat_id.set_draft(&t, None).await.unwrap();
let summary_res = chats.get_summary(&t, 0, None).await;
assert!(summary_res.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;

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,
@@ -396,12 +331,6 @@ pub enum Config {
/// Make all outgoing messages with Autocrypt header "multipart/signed".
SignUnencrypted,
/// Enable header protection for `Autocrypt` header.
///
/// This is an experimental setting not compatible to other MUAs
/// and older Delta Chat versions (core version <= v1.149.0).
ProtectAutocrypt,
/// Let the core save all events to the database.
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]
@@ -439,7 +368,6 @@ pub enum Config {
WebxdcIntegration,
/// Enable webxdc realtime features.
#[strum(props(default = "1"))]
WebxdcRealtimeEnabled,
}
@@ -454,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
@@ -467,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));
@@ -496,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)
@@ -560,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.
@@ -600,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.
@@ -678,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
@@ -809,7 +688,7 @@ impl Context {
{
return Ok(());
}
self.scheduler.interrupt_inbox().await;
self.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -868,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
@@ -882,7 +759,7 @@ impl Context {
self.set_config_internal(Config::ConfiguredAddr, Some(primary_new))
.await?;
self.emit_event(EventType::ConnectivityChanged);
Ok(())
}
@@ -1076,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;
@@ -1119,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);
@@ -1194,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;
@@ -1218,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,31 +11,28 @@
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;
use futures::FutureExt;
use futures_lite::FutureExt as _;
use percent_encoding::utf8_percent_encode;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use server_params::{expand_param_vector, ServerParams};
use tokio::task;
use crate::config::{self, Config};
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
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::message::Message;
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;
@@ -81,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;
@@ -110,19 +110,25 @@ 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 = configure(self, &param).await?;
let success = configure(self, &mut param).await;
self.set_config_internal(Config::NotifyAboutWrongPw, None)
.await?;
on_configure_completed(self, param, old_addr).await?;
success?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, configured_param, old_addr).await?;
Ok(())
}
}
async fn on_configure_completed(
context: &Context,
param: ConfiguredLoginParam,
param: LoginParam,
old_addr: Option<String>,
) -> Result<()> {
if let Some(provider) = param.provider {
@@ -143,7 +149,8 @@ async fn on_configure_completed(
}
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new_text(provider.after_login_hint.to_string());
let mut msg = Message::new(Viewtype::Text);
msg.text = provider.after_login_hint.to_string();
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
.await
.is_err()
@@ -156,9 +163,9 @@ async fn on_configure_completed(
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
if let Some(old_addr) = old_addr {
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new_text(
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
);
let mut msg = Message::new(Viewtype::Text);
msg.text =
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await;
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")
@@ -171,28 +178,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);
@@ -201,7 +201,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?;
@@ -212,10 +212,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
@@ -227,48 +228,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;
}
@@ -285,6 +304,7 @@ async fn get_configured_param(
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
strict_tls: None,
})
}
if !servers
@@ -297,158 +317,156 @@ 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 configuring = true;
let mut imap_session = match imap.connect(ctx, configuring).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?;
ctx.set_config(Config::ShowEmails, None).await?;
ctx.set_config(Config::E2eeEnabled, Some("1")).await?;
} else {
ctx.set_config(Config::IsChatmail, Some("0")).await?;
}
let create_mvbox = !is_chatmail;
let create_mvbox = ctx.should_watch_mvbox().await?;
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
@@ -469,7 +487,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?;
@@ -487,7 +506,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.
@@ -496,25 +515,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>> {
// Make sure to not encode `.` as `%2E` here.
// Some servers like murena.io on 2024-11-01 produce incorrect autoconfig XML
// when address is encoded.
// E.g.
// <https://autoconfig.murena.io/mail/config-v1.1.xml?emailaddress=foobar%40example%2Eorg>
// produced XML file with `<username>foobar@example%2Eorg</username>`
// resulting in failure to log in.
let param_addr_urlencoded =
utf8_percent_encode(&param.addr, NON_ALPHANUMERIC_WITHOUT_DOT).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
{
@@ -529,7 +539,7 @@ async fn get_autoconfig(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
&param_domain, &param_addr_urlencoded
),
&param.addr,
param,
)
.await
{
@@ -565,7 +575,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
{
@@ -575,19 +585,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)]
@@ -611,9 +742,9 @@ pub enum Error {
#[cfg(test)]
mod tests {
use super::*;
#![allow(clippy::indexing_slicing)]
use crate::config::Config;
use crate::login_param::EnteredServerLoginParam;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -625,24 +756,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,
@@ -272,6 +274,8 @@ pub(crate) async fn moz_autoconfigure(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]

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()
@@ -215,6 +216,8 @@ pub(crate) async fn outlk_autodiscover(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]

View File

@@ -22,6 +22,9 @@ 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 {
@@ -122,6 +125,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
@@ -135,6 +146,7 @@ pub(crate) fn expand_param_vector(
// The order of expansion is important.
//
// Ports are expanded the last, so they are changed the first.
.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 +166,7 @@ mod tests {
port: 0,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -167,6 +180,7 @@ mod tests {
port: 993,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
}],
);
@@ -177,6 +191,7 @@ mod tests {
port: 123,
socket: Socket::Automatic,
username: "foobar".to_string(),
strict_tls: None,
}],
"foobar@example.net",
"example.net",
@@ -191,6 +206,7 @@ mod tests {
port: 123,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
},
ServerParams {
protocol: Protocol::Smtp,
@@ -198,10 +214,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 +227,7 @@ mod tests {
port: 123,
socket: Socket::Plain,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -221,6 +240,7 @@ mod tests {
port: 123,
socket: Socket::Plain,
username: "foobar".to_string(),
strict_tls: Some(true)
}],
);
@@ -232,6 +252,7 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -245,6 +266,7 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Imap,
@@ -252,6 +274,7 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Imap,
@@ -259,6 +282,7 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
}
],
);
@@ -272,6 +296,7 @@ mod tests {
port: 0,
socket: Socket::Automatic,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -285,6 +310,7 @@ mod tests {
port: 465,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Smtp,
@@ -292,6 +318,7 @@ mod tests {
port: 587,
socket: Socket::Starttls,
username: "foobar".to_string(),
strict_tls: Some(true)
},
],
);
@@ -311,6 +338,7 @@ mod tests {
port: 0,
socket: Socket::Automatic,
username: "".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -324,6 +352,7 @@ mod tests {
port: 993,
socket: Socket::Ssl,
username: "foobar@example.net".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Imap,
@@ -331,6 +360,7 @@ mod tests {
port: 143,
socket: Socket::Starttls,
username: "foobar@example.net".to_string(),
strict_tls: Some(true)
},
],
);

View File

@@ -4,16 +4,12 @@
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC};
use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
/// Set of characters to percent-encode in email addresses and names.
pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
#[derive(
Debug,
Default,
@@ -183,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;
@@ -215,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};
@@ -143,43 +144,6 @@ impl ContactId {
.await?;
Ok(())
}
/// Returns contact adress.
pub async fn addr(&self, context: &Context) -> Result<String> {
let addr = context
.sql
.query_row("SELECT addr FROM contacts WHERE id=?", (self,), |row| {
let addr: String = row.get(0)?;
Ok(addr)
})
.await?;
Ok(addr)
}
/// Resets encryption with the contact.
///
/// Effect is similar to receiving a message without Autocrypt header
/// from the contact, but this action is triggered manually by the user.
///
/// For example, this will result in sending the next message
/// to 1:1 chat unencrypted, but will not remove existing verified keys.
pub async fn reset_encryption(self, context: &Context) -> Result<()> {
let now = time();
let addr = self.addr(context).await?;
if let Some(mut peerstate) = Peerstate::from_addr(context, &addr).await? {
peerstate.degrade_encryption(now);
peerstate.save_to_db(&context.sql).await?;
}
// Reset 1:1 chat protection.
if let Some(chat_id) = ChatId::lookup_by_contact(context, self).await? {
chat_id
.set_protection(context, ProtectionStatus::Unprotected, now, Some(self))
.await?;
}
Ok(())
}
}
impl fmt::Display for ContactId {
@@ -462,12 +426,9 @@ pub enum Origin {
/// To: of incoming messages of unknown sender
IncomingUnknownTo = 0x40,
/// Address scanned but not verified.
/// address scanned but not verified
UnhandledQrScan = 0x80,
/// Address scanned from a SecureJoin QR code, but not verified yet.
UnhandledSecurejoinQrScan = 0x81,
/// Reply-To: of incoming message of known sender
/// Contacts with at least this origin value are shown in the contact list.
IncomingReplyTo = 0x100,
@@ -1230,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())
@@ -1252,18 +1210,18 @@ impl Contact {
let fingerprint_self = load_self_public_key(context)
.await?
.dc_fingerprint()
.fingerprint()
.to_string();
let fingerprint_other_verified = peerstate
.peek_key(true)
.map(|k| k.dc_fingerprint().to_string())
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
let fingerprint_other_unverified = peerstate
.peek_key(false)
.map(|k| k.dc_fingerprint().to_string())
.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,
@@ -1277,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)
@@ -1444,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
@@ -2726,8 +2673,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
@@ -2751,8 +2696,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(())
}
@@ -2930,7 +2873,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(),
"{}",
@@ -3189,59 +3132,4 @@ Until the false-positive is fixed:
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reset_encryption() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
assert_eq!(msg.get_showpadlock(), false);
let msg = tcm.send_recv(bob, alice, "Hi!").await;
assert_eq!(msg.get_showpadlock(), true);
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reset_verified_encryption() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.execute_securejoin(bob, alice).await;
let msg = tcm.send_recv(bob, alice, "Encrypted").await;
assert_eq!(msg.get_showpadlock(), true);
let alice_bob_chat_id = msg.chat_id;
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
// Check that the contact is still verified after resetting encryption.
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_contact_id).await?;
assert_eq!(alice_bob_contact.is_verified(alice).await?, true);
// 1:1 chat and profile is no longer verified.
assert_eq!(alice_bob_contact.is_profile_verified(alice).await?, false);
let info_msg = alice.get_last_msg_in(alice_bob_chat_id).await;
assert_eq!(
info_msg.text,
"bob@example.net sent a message from another device."
);
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
}
}

View File

@@ -10,7 +10,6 @@ use std::time::Duration;
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use pgp::types::PublicKeyTrait;
use pgp::SignedPublicKey;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, OnceCell, RwLock};
@@ -28,8 +27,8 @@ 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::message::{self, Message, MessageState, MsgId};
use crate::login_param::LoginParam;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
use crate::peerstate::Peerstate;
@@ -472,14 +471,6 @@ impl Context {
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
}
// The next line is mainly for iOS:
// iOS starts a separate process for receiving notifications and if the user concurrently
// starts the app, the UI process opens the database but waits with calling start_io()
// until the notifications process finishes.
// Now, some configs may have changed, so, we need to invalidate the cache.
self.sql.config_cache.write().await.clear();
self.scheduler.start(self.clone()).await;
}
@@ -524,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(());
@@ -536,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),
);
@@ -755,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?;
@@ -766,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")
@@ -790,7 +748,7 @@ impl Context {
.count("SELECT COUNT(*) FROM acpeerstates;", ())
.await?;
let fingerprint_str = match load_self_public_key(self).await {
Ok(key) => key.dc_fingerprint().hex(),
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {err}>"),
};
@@ -847,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 {
@@ -999,12 +945,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"protect_autocrypt",
self.get_config_int(Config::ProtectAutocrypt)
.await?
.to_string(),
);
res.insert(
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),
@@ -1186,14 +1126,15 @@ impl Context {
EncryptPreference::Mutual,
&public_key,
);
let fingerprint = public_key.dc_fingerprint();
let fingerprint = public_key.fingerprint();
peerstate.set_verified(public_key, fingerprint, "".to_string())?;
peerstate.save_to_db(&self.sql).await?;
chat_id
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
.await?;
let mut msg = Message::new_text(self.get_self_report().await?);
let mut msg = Message::new(Viewtype::Text);
msg.text = self.get_self_report().await?;
chat_id.set_draft(self, Some(&mut msg)).await?;
@@ -1318,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() {
@@ -1750,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",
@@ -1792,10 +1725,12 @@ mod tests {
assert!(res.is_empty());
// Add messages to chat with Bob.
let mut msg1 = Message::new_text("foobar".to_string());
let mut msg1 = Message::new(Viewtype::Text);
msg1.set_text("foobar".to_string());
send_msg(&alice, chat.id, &mut msg1).await?;
let mut msg2 = Message::new_text("barbaz".to_string());
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_text("barbaz".to_string());
send_msg(&alice, chat.id, &mut msg2).await?;
alice.send_text(chat.id, "Δ-Chat").await;
@@ -1898,7 +1833,8 @@ mod tests {
.await;
// Add 999 messages
let mut msg = Message::new_text("foobar".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foobar".to_string());
for _ in 0..999 {
send_msg(&alice, chat.id, &mut msg).await?;
}
@@ -2072,41 +2008,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
let alice = TestContext::new_alice().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
// Change the config circumventing the cache
// This simulates what the notification plugin on iOS might do
// because it runs in a different process
alice
.sql
.execute(
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
(),
)
.await?;
// Alice's Delta Chat doesn't know about it yet:
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("2".to_string())
);
// Starting IO will fail of course because no server settings are configured,
// but it should invalidate the caches:
alice.start_io().await;
assert_eq!(
alice.get_config(Config::ShowEmails).await?,
Some("0".to_string())
);
Ok(())
}
}

View File

@@ -1,36 +1,125 @@
//! End-to-end decryption support.
use std::collections::HashSet;
use std::str::FromStr;
use anyhow::Result;
use deltachat_contact_tools::addr_cmp;
use mailparse::ParsedMail;
use crate::aheader::Aheader;
use crate::authres::handle_authres;
use crate::authres::{self, DkimResults};
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::peerstate::Peerstate;
use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
///
/// If successful and the message is encrypted, returns decrypted body.
/// If successful and the message is encrypted, returns decrypted body and a set of valid
/// signature fingerprints.
///
/// If the message is wrongly signed, HashSet will be empty.
pub fn try_decrypt(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
) -> Result<Option<::pgp::composed::Message>> {
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
};
let data = encrypted_data_part.get_body_raw()?;
let msg = pgp::pk_decrypt(data, private_keyring)?;
decrypt_part(
encrypted_data_part,
private_keyring,
public_keyring_for_validate,
)
}
Ok(Some(msg))
pub(crate) async fn prepare_decryption(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
message_time: i64,
) -> Result<DecryptionInfo> {
if mail.headers.get_header(HeaderDef::ListPost).is_some() {
if mail.headers.get_header(HeaderDef::Autocrypt).is_some() {
info!(
context,
"Ignoring autocrypt header since this is a mailing list message. \
NOTE: For privacy reasons, the mailing list software should remove Autocrypt headers."
);
}
return Ok(DecryptionInfo {
from: from.to_string(),
autocrypt_header: None,
peerstate: None,
message_time,
dkim_results: DkimResults { dkim_passed: false },
});
}
let autocrypt_header = if context.is_self_addr(from).await? {
None
} else if let Some(aheader_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
match Aheader::from_str(&aheader_value) {
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
} else {
None
};
let dkim_results = handle_authres(context, mail, from).await?;
let allow_aeap = get_encrypted_mime(mail).is_some();
let peerstate = get_autocrypt_peerstate(
context,
from,
autocrypt_header.as_ref(),
message_time,
allow_aeap,
)
.await?;
Ok(DecryptionInfo {
from: from.to_string(),
autocrypt_header,
peerstate,
message_time,
dkim_results,
})
}
#[derive(Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
pub from: String,
pub autocrypt_header: Option<Aheader>,
/// The peerstate that will be used to validate the signatures
pub peerstate: Option<Peerstate>,
/// The timestamp when the message was sent.
/// If this is older than the peerstate's last_seen, this probably
/// means out-of-order message arrival, We don't modify the
/// peerstate in this case.
pub message_time: i64,
pub(crate) dkim_results: authres::DkimResults,
}
/// Returns a reference to the encrypted payload of a message.
pub(crate) fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
@@ -115,6 +204,37 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail
}
}
/// Returns Ok(None) if nothing encrypted was found.
fn decrypt_part(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let data = mail.get_body_raw()?;
if has_decrypted_pgp_armor(&data) {
let (plain, ret_valid_signatures) =
pgp::pk_decrypt(data, private_keyring, public_keyring_for_validate)?;
return Ok(Some((plain, ret_valid_signatures)));
}
Ok(None)
}
#[allow(clippy::indexing_slicing)]
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
if let Some(index) = input.iter().position(|b| *b > b' ') {
if input.len() - index > 26 {
let start = index;
let end = start + 27;
return &input[start..end] == b"-----BEGIN PGP MESSAGE-----";
}
}
false
}
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
///
/// Returns the signed part and the set of key
@@ -182,7 +302,7 @@ pub(crate) async fn get_autocrypt_peerstate(
// if the fingerprint is verified.
peerstate = Peerstate::from_verified_fingerprint_or_addr(
context,
&header.public_key.dc_fingerprint(),
&header.public_key.fingerprint(),
from,
)
.await?;
@@ -193,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!(
@@ -226,6 +346,24 @@ mod tests {
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
#[test]
fn test_has_decrypted_pgp_armor() {
let data = b" -----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b" \n-----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b" -----BEGIN PGP MESSAGE---";
assert_eq!(has_decrypted_pgp_armor(data), false);
let data = b" -----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b"blas";
assert_eq!(has_decrypted_pgp_armor(data), false);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mixed_up_mime() -> Result<()> {
// "Mixed Up" mail as received when sending an encrypted

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(
@@ -318,7 +301,8 @@ mod tests {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
let mut msg = Message::new_text("Hi Bob".to_owned());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Hi Bob".to_owned());
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Done);
@@ -328,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

@@ -303,12 +303,12 @@ Sent with my Delta Chat Messenger: https://delta.chat";
last_seen_autocrypt: 14,
prefer_encrypt,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: Some(pub_key.clone()),
gossip_timestamp: 15,
gossip_key_fingerprint: Some(pub_key.dc_fingerprint()),
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.dc_fingerprint()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,

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.
@@ -223,9 +219,8 @@ impl ChatId {
self.inner_set_ephemeral_timer(context, timer).await?;
if self.is_promoted(context).await? {
let mut msg = Message::new_text(
stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await,
);
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
if let Err(err) = send_msg(context, self, &mut msg).await {
error!(
@@ -514,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),
)
@@ -539,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
)
@@ -1363,7 +1356,8 @@ mod tests {
chat.id
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
.await?;
let mut msg = Message::new_text("hi".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi".to_string());
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
.await
.is_err());
@@ -1393,7 +1387,8 @@ mod tests {
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
let mut poi_msg = Message::new_text("Here".to_string());
let mut poi_msg = Message::new(Viewtype::Text);
poi_msg.text = "Here".to_string();
poi_msg.set_location(10.0, 20.0);
let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await;
@@ -1415,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

@@ -144,12 +144,12 @@ impl HtmlMsgParser {
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().eq_ignore_ascii_case("flowed")
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().eq_ignore_ascii_case("yes")
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
},
@@ -283,6 +283,7 @@ mod tests {
<meta name="color-scheme" content="light dark" />
</head><body>
This message does not have Content-Type nor Subject.<br/>
<br/>
</body></html>
"#
);
@@ -301,6 +302,7 @@ This message does not have Content-Type nor Subject.<br/>
<meta name="color-scheme" content="light dark" />
</head><body>
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
<br/>
</body></html>
"#
);
@@ -323,6 +325,7 @@ This line ends with a space and will be merged with the next one due to format=f
<br/>
This line does not end with a space<br/>
and will be wrapped as usual.<br/>
<br/>
</body></html>
"#
);
@@ -344,6 +347,7 @@ mime-modified should not be set set as there is no html and no special stuff;<br
although not being a delta-message.<br/>
test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x27; :)<br/>
<br/>
<br/>
</body></html>
"#
);
@@ -521,7 +525,8 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// alice sends a message with html-part to bob
let chat_id = alice.create_chat(&bob).await.id;
let mut msg = Message::new_text("plain text".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("plain text".to_string());
msg.set_html(Some("<b>html</b> text".to_string()));
assert!(msg.mime_modified);
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();

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::message::{self, Message, MessageState, MessengerMessage, MsgId};
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,19 +73,13 @@ 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,
authentication_failed_once: 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,
authentication_failed_once: false,
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)
}
@@ -290,11 +295,11 @@ impl Imap {
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
/// instead if you are going to actually use connection rather than trying connection
/// parameters.
pub(crate) async fn connect(
&mut self,
context: &Context,
configuring: bool,
) -> Result<Session> {
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)
@@ -336,124 +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.authentication_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;
warn!(context, "IMAP failed to login: {err:#}.");
first_error.get_or_insert(format_err!("{message} ({err:#})"));
// If it looks like the password is wrong, send a notification:
let _lock = context.wrong_pw_warning_mutex.lock().await;
if err.to_string().to_lowercase().contains("authentication") {
if self.authentication_failed_once
&& !configuring
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
let mut msg = Message::new_text(message);
if let Err(e) = chat::add_device_msg_with_importance(
context,
None,
Some(&mut msg),
true,
)
.await
{
warn!(context, "Failed to add device message: {e:#}.");
} else {
context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
.log_err(context)
.ok();
}
} else {
self.authentication_failed_once = true;
}
} else {
self.authentication_failed_once = false;
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, "{:#}", 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.
@@ -461,8 +433,7 @@ impl Imap {
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
/// determined.
pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
let configuring = false;
let mut session = match self.connect(context, configuring).await {
let mut session = match self.connect(context).await {
Ok(session) => session,
Err(err) => {
self.connectivity.set_err(context, &err).await;
@@ -475,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?;
}
@@ -1067,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
@@ -1133,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,
@@ -1202,8 +1129,6 @@ impl Session {
.await
.context("failed to fetch flags")?;
let mut got_unsolicited_fetch = false;
while let Some(fetch) = list
.try_next()
.await
@@ -1213,7 +1138,6 @@ impl Session {
uid
} else {
info!(context, "FETCH result contains no UID, skipping");
got_unsolicited_fetch = true;
continue;
};
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
@@ -1236,15 +1160,6 @@ impl Session {
warn!(context, "FETCH result contains no MODSEQ");
}
}
drop(list);
if got_unsolicited_fetch {
// We got unsolicited FETCH, which means some flags
// have been modified while our request was in progress.
// We may or may not have these new flags as a part of the response,
// so better skip next IDLE and do another round of flag synchronization.
self.new_mail = true;
}
set_modseq(context, folder, highest_modseq)
.await
@@ -1604,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>).
///
@@ -1639,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)
@@ -1730,21 +1644,17 @@ impl Imap {
}
impl Session {
/// Return whether the server sent an unsolicited EXISTS or FETCH response.
///
/// Return whether the server sent an unsolicited EXISTS response.
/// Drains all responses from `session.unsolicited_responses` in the process.
///
/// If this returns `true`, this means that new emails arrived
/// or flags have been changed.
/// In this case we may want to skip next IDLE and do a round
/// of fetching new messages and synchronizing seen flags.
fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
/// If this returns `true`, this means that new emails arrived and you should
/// fetch again, even if you just fetched.
fn server_sent_unsolicited_exists(&self, context: &Context) -> Result<bool> {
use async_imap::imap_proto::Response;
use async_imap::imap_proto::ResponseCode;
use UnsolicitedResponse::*;
let folder = self.selected_folder.as_deref().unwrap_or_default();
let mut should_refetch = false;
let mut unsolicited_exists = false;
while let Ok(response) = self.unsolicited_responses.try_recv() {
match response {
Exists(_) => {
@@ -1752,38 +1662,28 @@ impl Session {
context,
"Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
);
should_refetch = true;
unsolicited_exists = true;
}
// We are not interested in the following responses and they are are
// sent quite frequently, so, we ignore them without logging them
Expunge(_) | Recent(_) => {}
Other(ref response_data) => {
match response_data.parsed() {
Response::Fetch { .. } => {
info!(
context,
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
);
should_refetch = true;
}
Other(response_data)
if matches!(
response_data.parsed(),
Response::Fetch { .. }
| Response::Done {
code: Some(ResponseCode::CopyUid(_, _, _)),
..
}
) => {}
// We are not interested in the following responses and they are are
// sent quite frequently, so, we ignore them without logging them.
Response::Done {
code: Some(ResponseCode::CopyUid(_, _, _)),
..
} => {}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
}
}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
}
}
Ok(should_refetch)
Ok(unsolicited_exists)
}
}
@@ -1911,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.eq_ignore_ascii_case("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);
}
@@ -1938,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,21 @@
use std::net::SocketAddr;
use std::ops::{Deref, DerefMut};
use anyhow::{Context as _, Result};
use anyhow::{bail, format_err, 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_tcp_inner, connect_tls_inner};
use crate::provider::Socket;
use crate::socks::Socks5Config;
use fast_socks5::client::Socks5Stream;
#[derive(Debug)]
pub(crate) struct Client {
@@ -37,20 +36,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 +57,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 +71,87 @@ 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 {
if let Some(socks5_config) = socks5_config {
let client = match security {
ConnectionSecurity::Tls => {
Client::connect_secure_proxy(context, host, port, strict_tls, proxy_config)
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)
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
let mut first_error = None;
let load_cache =
strict_tls && (security == Socket::Ssl || security == Socket::Starttls);
for resolved_addr in lookup_host_with_cache(context, host, port, load_cache).await? {
let res = match security {
Socket::Automatic => bail!("IMAP port security is not configured"),
Socket::Ssl => Client::connect_secure(resolved_addr, host, strict_tls).await,
Socket::Starttls => {
Client::connect_starttls(resolved_addr, host, strict_tls).await
}
Socket::Plain => Client::connect_insecure(resolved_addr).await,
};
match res {
Ok(client) => {
let ip_addr = resolved_addr.ip().to_string();
if load_cache {
update_connect_timestamp(context, host, &ip_addr).await?;
}
return Ok(client);
}
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_secure(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
let tls_stream = connect_tls_inner(addr, hostname, 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);
@@ -242,7 +191,7 @@ impl Client {
let buffered_tcp_stream = client.into_inner();
let tcp_stream = buffered_tcp_stream.into_inner();
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
let tls_stream = wrap_tls(strict_tls, host, "imap", tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
@@ -252,17 +201,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 +222,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 +239,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 +261,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

@@ -9,6 +9,7 @@ use tokio::time::timeout;
use super::session::Session;
use super::Imap;
use crate::context::Context;
use crate::imap::FolderMeaning;
use crate::net::TIMEOUT;
use crate::tools::{self, time_elapsed};
@@ -31,7 +32,7 @@ impl Session {
self.select_with_uidvalidity(context, folder).await?;
if self.drain_unsolicited_responses(context)? {
if self.server_sent_unsolicited_exists(context)? {
self.new_mail = true;
}
@@ -108,16 +109,37 @@ impl Imap {
pub(crate) async fn fake_idle(
&mut self,
context: &Context,
session: &mut Session,
watch_folder: String,
folder_meaning: FolderMeaning,
) -> Result<()> {
let fake_idle_start_time = tools::Time::now();
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
// Wait for 60 seconds or until we are interrupted.
match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
Err(_) => info!(context, "Fake IDLE finished."),
Ok(_) => info!(context, "Fake IDLE interrupted."),
// Loop until we are interrupted or until we fetch something.
loop {
match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
Err(_) => {
// Let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
let res = self
.fetch_new_messages(context, session, &watch_folder, folder_meaning, false)
.await?;
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break;
}
}
Ok(_) => {
info!(context, "Fake IDLE interrupted.");
break;
}
}
}
info!(

View File

@@ -66,11 +66,21 @@ impl Imap {
&& folder_meaning != FolderMeaning::Drafts
&& folder_meaning != FolderMeaning::Trash
{
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
.await
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
// Drain leftover unsolicited EXISTS messages
session.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
.await
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !session.server_sent_unsolicited_exists(context)? {
break;
}
}
}
}

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::PublicKeyTrait;
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?;
@@ -750,7 +632,7 @@ where
true => "private",
};
let id = id.map_or("default".into(), |i| i.to_string());
let fp = key.dc_fingerprint().hex();
let fp = DcKey::fingerprint(key).hex();
format!("{kind}-key-{addr}-{id}-{fp}.asc")
};
let path = dir.join(&file_name);
@@ -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;
@@ -878,7 +746,7 @@ mod tests {
.unwrap()
.strip_suffix(".asc")
.unwrap();
assert_eq!(fingerprint, key.dc_fingerprint().hex());
assert_eq!(fingerprint, DcKey::fingerprint(&key).hex());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{blobdir}/{filename}");
let bytes = tokio::fs::read(&filename).await.unwrap();
@@ -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;
use crate::qr::Qr;
use crate::message::{Message, Viewtype};
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.");
@@ -200,7 +210,8 @@ impl BackupProvider {
info!(context, "Received backup reception acknowledgement.");
context.emit_event(EventType::ImexProgress(1000));
let mut msg = Message::new_text(backup_transfer_msg_body(&context).await);
let mut msg = Message::new(Viewtype::Text);
msg.text = backup_transfer_msg_body(&context).await;
add_device_msg(&context, None, Some(&mut msg)).await?;
Ok(())
@@ -221,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;
@@ -255,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;
}
@@ -289,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,
@@ -296,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?;
@@ -318,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(())
}
@@ -336,39 +349,207 @@ 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;
use crate::chat::{get_chat_msgs, send_msg, ChatItem};
use crate::message::Viewtype;
use crate::test_utils::TestContextManager;
use super::*;
@@ -382,7 +563,8 @@ mod tests {
// Write a message in the self chat
let self_chat = ctx0.get_self_chat().await;
let mut msg = Message::new_text("hi there".to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi there".to_string());
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
// Send an attachment in the self chat

View File

@@ -11,8 +11,7 @@ use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{PublicKeyTrait, SecretKeyTrait};
use rand::thread_rng;
use pgp::types::{KeyTrait, SecretKeyTrait};
use tokio::runtime::Handle;
use crate::config::Config;
@@ -27,7 +26,7 @@ use crate::tools::{self, time_elapsed};
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
Ok(<Self as Deserializable>::from_bytes(Cursor::new(bytes))?)
@@ -94,8 +93,8 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
/// The fingerprint for the key.
fn dc_fingerprint(&self) -> Fingerprint {
PublicKeyTrait::fingerprint(self).into()
fn fingerprint(&self) -> Fingerprint {
Fingerprint::new(KeyTrait::fingerprint(self))
}
fn is_private() -> bool;
@@ -234,8 +233,7 @@ impl DcSecretKey for SignedSecretKey {
fn split_public_key(&self) -> Result<SignedPublicKey> {
self.verify()?;
let unsigned_pubkey = SecretKeyTrait::public_key(self);
let mut rng = thread_rng();
let signed_pubkey = unsigned_pubkey.sign(&mut rng, self, || "".into())?;
let signed_pubkey = unsigned_pubkey.sign(self, || "".into())?;
Ok(signed_pubkey)
}
}
@@ -246,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();
@@ -268,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(
@@ -286,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)?,
})
@@ -335,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")?;
@@ -369,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(())
}
@@ -397,12 +410,6 @@ impl Fingerprint {
}
}
impl From<pgp::types::Fingerprint> for Fingerprint {
fn from(fingerprint: pgp::types::Fingerprint) -> Fingerprint {
Self::new(fingerprint.as_bytes().into())
}
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Fingerprint")

View File

@@ -16,8 +16,7 @@
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,
@@ -85,6 +84,7 @@ mod scheduler;
pub mod securejoin;
mod simplify;
mod smtp;
mod socks;
pub mod stock_str;
mod sync;
mod timesmearing;

View File

@@ -290,7 +290,8 @@ pub async fn send_locations_to_chat(
)
.await?;
if 0 != seconds && !is_sending_locations_before {
let mut msg = Message::new_text(stock_str::msg_location_enabled(context).await);
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::msg_location_enabled(context).await;
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
chat::send_msg(context, chat_id, &mut msg)
.await
@@ -880,6 +881,8 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::message::MessageState;

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