mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
23 Commits
fix_node_p
...
1.84.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f34ca8962 | ||
|
|
7def6e70ba | ||
|
|
82c190a0c5 | ||
|
|
ba74a40b6d | ||
|
|
64abe54b15 | ||
|
|
ba0f5ee81d | ||
|
|
eebd219414 | ||
|
|
27fd0dbaec | ||
|
|
b7af53c7d9 | ||
|
|
c762834e05 | ||
|
|
0725fe38f8 | ||
|
|
73341394ee | ||
|
|
9549aca48b | ||
|
|
9c41f0fdb9 | ||
|
|
1d522edb01 | ||
|
|
b7d2828f60 | ||
|
|
deece15648 | ||
|
|
d06683489f | ||
|
|
307063ade0 | ||
|
|
ed00adbecc | ||
|
|
2aaa850e25 | ||
|
|
8859da42c6 | ||
|
|
2968c2919c |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -77,10 +77,10 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version, same as in `rust-toolchain` file.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.60.0
|
||||
rust: 1.61.0
|
||||
python: 3.9
|
||||
- os: windows-latest
|
||||
rust: 1.60.0
|
||||
rust: 1.61.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.56.0
|
||||
|
||||
48
.github/workflows/node-package.yml
vendored
48
.github/workflows/node-package.yml
vendored
@@ -4,11 +4,12 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- '!py-*'
|
||||
|
||||
|
||||
jobs:
|
||||
prebuild:
|
||||
name: 'Tests & Prebuild'
|
||||
name: 'prebuild'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -50,20 +51,6 @@ jobs:
|
||||
cd node
|
||||
npm install --verbose
|
||||
|
||||
- name: Test
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
npm run test
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
- name: Run tests on Windows, except lint
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
npm run test:mocha
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
- name: Build Prebuild
|
||||
run: |
|
||||
cd node
|
||||
@@ -97,7 +84,7 @@ jobs:
|
||||
run: |
|
||||
tag=${{ steps.tag.outputs.tag }}
|
||||
if [ -z "$tag" ]; then
|
||||
node -e "console.log('::set-output name=prid::' + '${{ github.ref }}'.split('/')[2])"
|
||||
node -e "console.log('DELTACHAT_NODE_TAR_GZ=deltachat-node-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No preview will be uploaded this time, but the $tag release"
|
||||
fi
|
||||
@@ -108,6 +95,7 @@ jobs:
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
echo $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Download ubuntu prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
@@ -122,11 +110,12 @@ jobs:
|
||||
name: windows-latest
|
||||
- shell: bash
|
||||
run: |
|
||||
mkdir prebuilds
|
||||
tar -xvzf ubuntu-18.04/ubuntu-18.04.tar.gz -C prebuilds
|
||||
tar -xvzf macos-latest/macos-latest.tar.gz -C prebuilds
|
||||
tar -xvzf windows-latest/windows-latest.tar.gz -C prebuilds
|
||||
tree prebuilds
|
||||
mkdir node/prebuilds
|
||||
tar -xvzf ubuntu-18.04/ubuntu-18.04.tar.gz -C node/prebuilds
|
||||
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
||||
tree node/prebuilds
|
||||
rm -rf ubuntu-18.04 macos-latest windows-latest
|
||||
- name: install dependencies without running scripts
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
@@ -136,14 +125,15 @@ jobs:
|
||||
- name: package
|
||||
shell: bash
|
||||
run: |
|
||||
mv node/README.md README.md
|
||||
npm pack .
|
||||
ls -lah
|
||||
mv $(find deltachat-node-*) deltachat-node-${{ steps.prepare.outputs.prid }}.tar.gz
|
||||
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: deltachat-node.tgz
|
||||
path: deltachat-node-${{ steps.prepare.outputs.prid }}.tar.gz
|
||||
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||
# Upload to download.delta.chat/node/preview/
|
||||
- name: Upload deltachat-node preview to download.delta.chat/node/preview/
|
||||
id: upload-preview
|
||||
@@ -151,13 +141,17 @@ jobs:
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-node-${{ steps.prepare.outputs.prid }}.tar.gz "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
||||
if [[ -z "$DELTACHAT_NODE_TAR_GZ" ]]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
||||
continue-on-error: true
|
||||
- name: "Post links to details"
|
||||
if: steps.upload-preview.outcome == 'success'
|
||||
run: node ./node/scripts/postLinksToDetails.js
|
||||
env:
|
||||
URL: preview/deltachat-node-${{ steps.prepare.outputs.prid }}
|
||||
URL: preview/${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Upload to download.delta.chat/node/
|
||||
- name: Upload deltachat-node build to download.delta.chat/node/
|
||||
@@ -167,4 +161,6 @@ jobs:
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-node-${{ steps.prepare.outputs.prid }}.tar.gz "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"
|
||||
$DELTACHAT_NODE_TAG_TAR_GZ=deltachat-node-${{ steps.tag.outputs.tag }}.tar.gz
|
||||
mv $DELTACHAT_NODE_TAR_GZ $DELTACHAT_NODE_TAG_TAR_GZ
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAG_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"
|
||||
|
||||
67
.github/workflows/node-tests.yml
vendored
Normal file
67
.github/workflows/node-tests.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: 'node.js'
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- staging
|
||||
- trying
|
||||
|
||||
jobs:
|
||||
prebuild:
|
||||
name: 'tests'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04, macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
rustup -vV
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
~/.npm
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
|
||||
|
||||
- name: Install dependencies & build
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd node
|
||||
npm install --verbose
|
||||
|
||||
- name: Test
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
npm run test
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
- name: Run tests on Windows, except lint
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
npm run test:mocha
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,5 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.84.0
|
||||
|
||||
### Changes
|
||||
- refactorings #3354 #3347 #3353 #3346
|
||||
|
||||
### Fixes
|
||||
- do not unnecessarily SELECT folders if there are no operations planned on
|
||||
them #3333
|
||||
- trim chat encryption info #3350
|
||||
- fix failure to decrypt first message to self after key synchronization
|
||||
via Autocrypt Setup Message #3352
|
||||
- Keep pgp key when you change your own email address #3351
|
||||
- Do not ignore Sent and Spam folders on Gmail #3369
|
||||
- handle decryption errors explicitly and don't get confused by encrypted mail attachments #3374
|
||||
|
||||
|
||||
## 1.83.0
|
||||
|
||||
### Fixes
|
||||
- fix node prebuild & package ci #3337
|
||||
|
||||
## 1.82.0
|
||||
|
||||
### API-Changes
|
||||
- re-add removed DC_MSG_ID_MARKER1 as in use on iOS #3330
|
||||
|
||||
### Changes
|
||||
- refactorings #3328
|
||||
|
||||
### Fixes
|
||||
- fix node package ci #3331
|
||||
- fix race condition in ongoing process (import/export, configuration) allocation #3322
|
||||
|
||||
|
||||
## 1.81.0
|
||||
|
||||
### API-Changes
|
||||
@@ -24,12 +60,8 @@
|
||||
- node: throw error when getting context with an invalid account id
|
||||
- node: throw error when instanciating a wrapper class on `null` (Context, Message, Chat, ChatList and so on)
|
||||
- use same contact-color if email address differ only in upper-/lowercase #3327
|
||||
- fix race condition in ongoing process (import/export, configuration) allocation
|
||||
- repair encrypted mails "mixed up" by Google Workspace "Append footer" function #3315
|
||||
|
||||
### Removed
|
||||
- node: remove unmaintained coverage scripts
|
||||
|
||||
|
||||
## 1.80.0
|
||||
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1067,7 +1067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.81.0"
|
||||
version = "1.84.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1146,7 +1146,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.81.0"
|
||||
version = "1.84.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.81.0"
|
||||
version = "1.84.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.81.0"
|
||||
version = "1.84.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -458,7 +458,37 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
}
|
||||
|
||||
let event = &*event;
|
||||
event.as_id()
|
||||
match event.typ {
|
||||
EventType::Info(_) => 100,
|
||||
EventType::SmtpConnected(_) => 101,
|
||||
EventType::ImapConnected(_) => 102,
|
||||
EventType::SmtpMessageSent(_) => 103,
|
||||
EventType::ImapMessageDeleted(_) => 104,
|
||||
EventType::ImapMessageMoved(_) => 105,
|
||||
EventType::NewBlobFile(_) => 150,
|
||||
EventType::DeletedBlobFile(_) => 151,
|
||||
EventType::Warning(_) => 300,
|
||||
EventType::Error(_) => 400,
|
||||
EventType::ErrorSelfNotInGroup(_) => 410,
|
||||
EventType::MsgsChanged { .. } => 2000,
|
||||
EventType::IncomingMsg { .. } => 2005,
|
||||
EventType::MsgsNoticed { .. } => 2008,
|
||||
EventType::MsgDelivered { .. } => 2010,
|
||||
EventType::MsgFailed { .. } => 2012,
|
||||
EventType::MsgRead { .. } => 2015,
|
||||
EventType::ChatModified(_) => 2020,
|
||||
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
||||
EventType::ContactsChanged(_) => 2030,
|
||||
EventType::LocationChanged(_) => 2035,
|
||||
EventType::ConfigureProgress { .. } => 2041,
|
||||
EventType::ImexProgress(_) => 2051,
|
||||
EventType::ImexFileWritten(_) => 2052,
|
||||
EventType::SecurejoinInviterProgress { .. } => 2060,
|
||||
EventType::SecurejoinJoinerProgress { .. } => 2061,
|
||||
EventType::ConnectivityChanged => 2100,
|
||||
EventType::SelfavatarChanged => 2110,
|
||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
99
draft/aeap-mvp.md
Normal file
99
draft/aeap-mvp.md
Normal file
@@ -0,0 +1,99 @@
|
||||
AEAP MVP
|
||||
========
|
||||
|
||||
Changes to the UIs
|
||||
------------------
|
||||
|
||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
||||
|
||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
||||
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- DONE: We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- DONE: If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
|
||||
|
||||
- When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
|
||||
|
||||
- When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
|
||||
AND there is a `Chat-Version` header\
|
||||
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
|
||||
|
||||
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
|
||||
|
||||
### Notes:
|
||||
|
||||
- We treat protected and non-protected chats the same
|
||||
- We leave the aeap transition statement away since it seems not to be needed, makes things harder on the sending side, wastes some network traffic, and is worse for privacy (since more pepole know what old addresses you had).
|
||||
- As soon as we encrypt read receipts, sending a read receipt will be enough to tell a lot of people that you transitioned
|
||||
- AEAP will make the problem of inconsistent group state worse, both because it doesn't work if the message is unencrypted (even if the design allowed it, it would be problematic security-wise) and because some chat partners may have gotten the transition and some not. We should do something against this at some point in the future, like asking the user whether they want to add/remove the members to restore consistent group state.
|
||||
|
||||
#### Downsides of this design:
|
||||
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
|
||||
|
||||
#### Upsides:
|
||||
- With this approach, it's easy to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||
- Faster transation: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.
|
||||
|
||||
### Alternatives and old discussions/plans:
|
||||
|
||||
- Change the contact instead of rewriting the group member lists. This seems to call for more trouble since we will end up with multiple contacts having the same email address.
|
||||
|
||||
- If needed, we could add a header a) indicating that the sender did an address transition or b) listing all the secondary (old) addresses. For now, there is no big enough benefit to warrant introducing another header and its processing on the receiver side (including all the neccessary checks and handling of error cases). Instead, we only check for the `Chat-Version` header to prevent accidental transitions when an MUA user sends a message from another email address with the same key.
|
||||
|
||||
- The condition for a transition temporarily was:
|
||||
|
||||
> When receiving a message: If we are going to assign a message to a chat, but the sender is not a member of this chat\
|
||||
> AND the signing key is the same as the direct (non-gossiped) key of one of the chat members\
|
||||
> AND ...
|
||||
|
||||
However, this would mean that in 1:1 messages can't trigger a transition, since we don't assign private messages to the parent chat, but always to the 1:1 chat with the sender.
|
||||
|
||||
<details>
|
||||
<summary>Some previous state of the discussion, which temporarily lived in an issue description</summary>
|
||||
Summarizing the discussions from https://github.com/deltachat/deltachat-core-rust/pull/2896, mostly quoting @hpk42:
|
||||
|
||||
1. (DONE) At the time of configure we push the current primary to become a secondary.
|
||||
|
||||
2. When a message is sent out to a chat, and the message is encrypted, and we have secondary addresses, then we
|
||||
a) add a protected "AEAP-Replacement" header that contains all secondary addresses
|
||||
b) if any of the secondary addresses is in the chat's member list, we remove it and leave a system message that we did so
|
||||
3. When an encrypted message with a replacement header is received, replace the e-mail address of all secondary contacts (if they exist) with the new primary and drop a sysmessage in all chats the secondary is member off. This might (in edge cases) result in chats that have two or more contacts with the same e-mail address. We might ignore this for a first release and just log a warning. Let's maybe not get hung up on this case before everything else works.
|
||||
|
||||
Notes:
|
||||
- for now we will send out aeap replacement headers forever, there is no termination condition other than lack of secondary addresses. I think that's fine for now. Later on we might introduce options to remove secondary addresses but i wouldn't do this for a first release/PR.
|
||||
- the design is resilient against changing e-mail providers from A to B to C and then back to A, with partially updated chats and diverging views from recipients/contacts on this transition. In the end, you will have a primary and some secondaries, and when you start sending out messages everybody will eventually synchronize when they receive the current state of primaries/secondaries.
|
||||
- of course on incoming message for need to check for each stated secondary address in the replacement header that it uses the same signature as the signature we verified as valid with the incoming message **--> Also we have to somehow make sure that the signing key was not just gossiped from some random other person in some group.**
|
||||
- there are no extra flags/columns in the database needed (i hope)
|
||||
|
||||
#### Downsides of the chosen approach:
|
||||
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
|
||||
- There will be multiple contacts with the same address in the database. We will have to do something against this at some point.
|
||||
|
||||
The most obvious alternative would be to create a new contact with the new address and replace the old contact in the groups.
|
||||
|
||||
#### Upsides:
|
||||
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||
- (Also, less important: Slightly faster transation: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
|
||||
|
||||
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
||||
|
||||
_end of the previous state of the discussion_
|
||||
|
||||
</details>
|
||||
|
||||
Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
@@ -1,33 +0,0 @@
|
||||
AEAP MVP
|
||||
========
|
||||
|
||||
Changes to the UIs
|
||||
------------------
|
||||
|
||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
||||
|
||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
||||
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- No changes for 1:1 chats, there simply is a new one
|
||||
|
||||
- When we send a message to a group, and the primary address is not a member of a group, but a secondary address is:
|
||||
|
||||
Add Chat-Group-Member-Removed=<old address> and Chat-Group-Member-Added=<new address> headers to this message
|
||||
|
||||
- On the receiving side, make sure that we accept this (even in verified groups) if the message is signed and the key stayed the same
|
||||
|
||||
Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
@@ -17,7 +17,7 @@ const STATUS_DATA = {
|
||||
state: 'success',
|
||||
description: '⏩ Click on "Details" to download →',
|
||||
context: 'Download the node-bindings.tar.gz',
|
||||
target_url: base_url + file_url + '.tar.gz',
|
||||
target_url: base_url + file_url,
|
||||
}
|
||||
|
||||
const http = require('https')
|
||||
|
||||
@@ -326,7 +326,7 @@ static void call_js_event_handler(napi_env env, napi_value js_callback, void* _c
|
||||
|
||||
if (status != napi_ok) {
|
||||
TRACE("Unable to call event_handler callback2");
|
||||
napi_extended_error_info* error_result;
|
||||
const napi_extended_error_info* error_result;
|
||||
NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result));
|
||||
}
|
||||
}
|
||||
@@ -3195,7 +3195,7 @@ static void call_accounts_js_event_handler(napi_env env, napi_value js_callback,
|
||||
|
||||
if (status != napi_ok) {
|
||||
TRACE("Unable to call event_handler callback2");
|
||||
napi_extended_error_info* error_result;
|
||||
const napi_extended_error_info* error_result;
|
||||
NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,5 +61,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.81.0"
|
||||
}
|
||||
"version": "1.84.0"
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class Chat(object):
|
||||
def is_group(self) -> bool:
|
||||
""" return true if this chat is a group chat.
|
||||
|
||||
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
|
||||
:returns: True if chat is a group-chat, false otherwise
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
|
||||
@@ -689,7 +689,7 @@ def test_gossip_encryption_preference(acfactory, lp):
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "first message"
|
||||
assert not msg.is_encrypted()
|
||||
res = "End-to-end encryption preferred:\n{}\n".format(ac2.get_config('addr'))
|
||||
res = "End-to-end encryption preferred:\n{}".format(ac2.get_config('addr'))
|
||||
assert msg.chat.get_encryption_info() == res
|
||||
lp.sec("ac2 learns that ac3 prefers encryption")
|
||||
ac2.create_chat(ac3)
|
||||
@@ -701,7 +701,7 @@ def test_gossip_encryption_preference(acfactory, lp):
|
||||
lp.sec("ac3 does not know that ac1 prefers encryption")
|
||||
ac1.create_chat(ac3)
|
||||
chat = ac3.create_chat(ac1)
|
||||
res = "No encryption:\n{}\n".format(ac1.get_config('addr'))
|
||||
res = "No encryption:\n{}".format(ac1.get_config('addr'))
|
||||
assert chat.get_encryption_info() == res
|
||||
msg = chat.send_text("not encrypted")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
@@ -712,7 +712,7 @@ def test_gossip_encryption_preference(acfactory, lp):
|
||||
group_chat = ac1.create_group_chat("hello")
|
||||
group_chat.add_contact(ac2)
|
||||
encryption_info = group_chat.get_encryption_info()
|
||||
res = "End-to-end encryption preferred:\n{}\n".format(ac2.get_config("addr"))
|
||||
res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr"))
|
||||
assert encryption_info == res
|
||||
msg = group_chat.send_text("hi")
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.60.0
|
||||
1.61.0
|
||||
|
||||
@@ -8,7 +8,7 @@ set -e -x
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.60.0
|
||||
RUST_VERSION=1.61.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
|
||||
@@ -8,7 +8,7 @@ set -e -x
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.60.0
|
||||
RUST_VERSION=1.61.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
|
||||
@@ -141,9 +141,10 @@ impl Accounts {
|
||||
|
||||
/// Remove an account.
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
let ctx = self.accounts.remove(&id);
|
||||
ensure!(ctx.is_some(), "no account with this id: {}", id);
|
||||
let ctx = ctx.unwrap();
|
||||
let ctx = self
|
||||
.accounts
|
||||
.remove(&id)
|
||||
.with_context(|| format!("no account with id {}", id))?;
|
||||
ctx.stop_io().await;
|
||||
drop(ctx);
|
||||
|
||||
|
||||
@@ -289,7 +289,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
/// Returns the filename of the blob.
|
||||
pub fn as_file_name(&self) -> &str {
|
||||
self.name.rsplit('/').next().unwrap()
|
||||
self.name.rsplit('/').next().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The path relative in the blob directory.
|
||||
@@ -1083,8 +1083,7 @@ mod tests {
|
||||
assert_eq!(img.width() as u32, compressed_width);
|
||||
assert_eq!(img.height() as u32, compressed_height);
|
||||
|
||||
bob.recv_msg(&sent).await;
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file = bob_msg.get_file(&bob).unwrap();
|
||||
|
||||
100
src/chat.rs
100
src/chat.rs
@@ -41,7 +41,7 @@ use crate::webxdc::WEBXDC_SUFFIX;
|
||||
use crate::{location, sql};
|
||||
|
||||
/// An chat item, such as a message or a marker.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum ChatItem {
|
||||
Message {
|
||||
msg_id: MsgId,
|
||||
@@ -786,7 +786,7 @@ impl ChatId {
|
||||
);
|
||||
let row = sql
|
||||
.query_row_optional(
|
||||
query,
|
||||
&query,
|
||||
paramsv![
|
||||
self,
|
||||
MessageState::OutPreparing,
|
||||
@@ -895,7 +895,7 @@ impl ChatId {
|
||||
ret += &ret_mutual;
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
Ok(ret.trim().to_string())
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
@@ -3061,7 +3061,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
let ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
&format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
@@ -3676,12 +3676,11 @@ mod tests {
|
||||
|
||||
// create group and sync it to the second device
|
||||
let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?;
|
||||
send_text_msg(&a1, a1_chat_id, "ho!".to_string()).await?;
|
||||
a1.send_text(a1_chat_id, "ho!").await;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?;
|
||||
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_chat_id = a2_msg.chat_id;
|
||||
let a2_chat = Chat::load_from_db(&a2, a2_chat_id).await?;
|
||||
|
||||
@@ -3700,8 +3699,7 @@ mod tests {
|
||||
add_contact_to_chat(&a1, a1_chat_id, bob).await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
|
||||
assert!(a1_msg.is_system_message());
|
||||
assert!(a2_msg.is_system_message());
|
||||
@@ -3714,8 +3712,7 @@ mod tests {
|
||||
set_chat_name(&a1, a1_chat_id, "bar").await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
|
||||
assert!(a1_msg.is_system_message());
|
||||
assert!(a2_msg.is_system_message());
|
||||
@@ -3728,8 +3725,7 @@ mod tests {
|
||||
remove_contact_from_chat(&a1, a1_chat_id, bob).await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
|
||||
assert!(a1_msg.is_system_message());
|
||||
assert!(a2_msg.is_system_message());
|
||||
@@ -3792,10 +3788,9 @@ mod tests {
|
||||
bob.recv_msg(&add3).await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
bob.recv_msg(&add2).await;
|
||||
let bob_chat_id = bob.recv_msg(&add2).await.chat_id;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 4);
|
||||
|
||||
bob.recv_msg(&remove2).await;
|
||||
@@ -3864,12 +3859,11 @@ mod tests {
|
||||
|
||||
// Alice sends first message to group.
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let bob_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
|
||||
// Bob leaves the group.
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
@@ -4937,8 +4931,7 @@ mod tests {
|
||||
let mime = sent_msg.payload();
|
||||
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
|
||||
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, bob_chat.id);
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
assert_eq!(msg.get_filename(), Some(filename.to_string()));
|
||||
@@ -5000,18 +4993,16 @@ mod tests {
|
||||
|
||||
// send sticker to bob
|
||||
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
// forward said sticker to alice
|
||||
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
|
||||
let forwarded_msg = bob.pop_sent_msg().await;
|
||||
alice.recv_msg(&forwarded_msg).await;
|
||||
|
||||
// retrieve forwarded sticker which should not have forwarded-flag
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
let msg = alice.recv_msg(&forwarded_msg).await;
|
||||
// forwarded sticker should not have forwarded-flag
|
||||
assert!(!msg.is_forwarded());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5025,15 +5016,12 @@ mod tests {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("Hi Bob".to_owned()));
|
||||
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
|
||||
|
||||
let forwarded_msg = bob.pop_sent_msg().await;
|
||||
alice.recv_msg(&forwarded_msg).await;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
let msg = alice.recv_msg(&forwarded_msg).await;
|
||||
assert!(msg.get_text().unwrap() == "Hi Bob");
|
||||
assert!(msg.is_forwarded());
|
||||
Ok(())
|
||||
@@ -5048,23 +5036,19 @@ mod tests {
|
||||
|
||||
// Alice sends a message to Bob.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
// Bob quotes received message and sends a reply to Alice.
|
||||
let mut reply = Message::new(Viewtype::Text);
|
||||
reply.set_text(Some("Reply".to_owned()));
|
||||
reply.set_quote(&bob, Some(&received_msg)).await?;
|
||||
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
|
||||
alice.recv_msg(&sent_reply).await;
|
||||
let received_reply = alice.get_last_msg().await;
|
||||
let received_reply = alice.recv_msg(&sent_reply).await;
|
||||
|
||||
// Alice forwards a reply.
|
||||
forward_msgs(&alice, &[received_reply.id], alice_chat.get_id()).await?;
|
||||
let forwarded_msg = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&forwarded_msg).await;
|
||||
|
||||
let alice_forwarded_msg = alice.get_last_msg().await;
|
||||
let alice_forwarded_msg = bob.recv_msg(&forwarded_msg).await;
|
||||
assert!(alice_forwarded_msg.quoted_message(&alice).await?.is_none());
|
||||
assert_eq!(
|
||||
alice_forwarded_msg.quoted_text(),
|
||||
@@ -5096,8 +5080,7 @@ mod tests {
|
||||
let sent_group_msg = alice
|
||||
.send_text(alice_group_chat_id, "Hi Bob and Claire")
|
||||
.await;
|
||||
bob.recv_msg(&sent_group_msg).await;
|
||||
let bob_group_chat_id = bob.get_last_msg().await.chat_id;
|
||||
let bob_group_chat_id = bob.recv_msg(&sent_group_msg).await.chat_id;
|
||||
|
||||
// Alice deletes a message on her device.
|
||||
// This is needed to make assignment of further messages received in this group
|
||||
@@ -5107,15 +5090,13 @@ mod tests {
|
||||
|
||||
// Alice sends a message to Bob.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(received_msg.get_text(), Some("Hi Bob".to_string()));
|
||||
assert_eq!(received_msg.chat_id, bob_chat.id);
|
||||
|
||||
// Alice sends another message to Bob, this has first message as a parent.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hello Bob").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(received_msg.get_text(), Some("Hello Bob".to_string()));
|
||||
assert_eq!(received_msg.chat_id, bob_chat.id);
|
||||
|
||||
@@ -5152,8 +5133,7 @@ mod tests {
|
||||
// Bob forwards that message to Claire -
|
||||
// Claire should not get information about Alice for the original Group
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let orig_msg = bob.get_last_msg().await;
|
||||
let orig_msg = bob.recv_msg(&sent_msg).await;
|
||||
let claire_id = Contact::create(&bob, "claire", "claire@foo").await?;
|
||||
let single_id = ChatId::create_for_contact(&bob, claire_id).await?;
|
||||
let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?;
|
||||
@@ -5200,15 +5180,16 @@ mod tests {
|
||||
|
||||
// Bob receives all messages
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(msg.get_text().unwrap(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0).await?.len(), 1);
|
||||
bob.recv_msg(&sent2).await;
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0).await?.len(), 2);
|
||||
bob.recv_msg(&sent3).await;
|
||||
let received = bob.recv_msg_opt(&sent3).await;
|
||||
// No message should actually be added since we already know this message:
|
||||
assert!(received.is_none());
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0).await?.len(), 2);
|
||||
|
||||
@@ -5216,8 +5197,7 @@ mod tests {
|
||||
let claire = TestContext::new().await;
|
||||
claire.configure_addr("claire@example.org").await;
|
||||
claire.recv_msg(&sent2).await;
|
||||
claire.recv_msg(&sent3).await;
|
||||
let msg = claire.get_last_msg().await;
|
||||
let msg = claire.recv_msg(&sent3).await;
|
||||
assert_eq!(msg.get_text().unwrap(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&claire, msg.chat_id, 0).await?.len(), 2);
|
||||
@@ -5240,8 +5220,7 @@ mod tests {
|
||||
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
assert!(resend_msgs(&bob, &[msg.id]).await.is_err());
|
||||
|
||||
Ok(())
|
||||
@@ -5262,8 +5241,7 @@ mod tests {
|
||||
|
||||
// Bob now can send an encrypted message
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
|
||||
msg.chat_id.accept(&bob).await?;
|
||||
@@ -5347,8 +5325,7 @@ mod tests {
|
||||
|
||||
let chat_bob = bob.create_chat(&alice).await;
|
||||
send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
let msg = alice.get_last_msg().await;
|
||||
let msg = alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// test broadcast list
|
||||
@@ -5368,8 +5345,7 @@ mod tests {
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.get_text(), Some("ola!".to_string()));
|
||||
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
@@ -5429,7 +5405,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
bob@example.net\n"
|
||||
bob@example.net"
|
||||
);
|
||||
|
||||
add_contact_to_chat(&alice, chat_id, contact_fiona).await?;
|
||||
@@ -5437,7 +5413,7 @@ mod tests {
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
bob@example.net\n\
|
||||
fiona@example.net\n"
|
||||
fiona@example.net"
|
||||
);
|
||||
|
||||
let direct_chat = bob.create_chat(&alice).await;
|
||||
@@ -5450,7 +5426,7 @@ mod tests {
|
||||
fiona@example.net\n\
|
||||
\n\
|
||||
End-to-end encryption preferred:\n\
|
||||
bob@example.net\n"
|
||||
bob@example.net"
|
||||
);
|
||||
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
@@ -5463,7 +5439,7 @@ mod tests {
|
||||
fiona@example.net\n\
|
||||
\n\
|
||||
End-to-end encryption available:\n\
|
||||
bob@example.net\n"
|
||||
bob@example.net"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::blob::BlobObject;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input};
|
||||
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
|
||||
use crate::events::EventType;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
@@ -355,6 +355,8 @@ 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<()> {
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
|
||||
// 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
|
||||
@@ -368,6 +370,17 @@ impl Context {
|
||||
self.set_config(Config::ConfiguredAddr, Some(primary_new))
|
||||
.await?;
|
||||
|
||||
if let Some(old_addr) = old_addr {
|
||||
let old_addr = EmailAddress::new(&old_addr)?;
|
||||
let old_keypair = crate::key::load_keypair(self, &old_addr).await?;
|
||||
|
||||
if let Some(mut old_keypair) = old_keypair {
|
||||
old_keypair.addr = EmailAddress::new(primary_new)?;
|
||||
crate::key::store_self_keypair(self, &old_keypair, crate::key::KeyPairUse::Default)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -419,7 +432,9 @@ mod tests {
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::constants;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
#[test]
|
||||
@@ -499,14 +514,14 @@ mod tests {
|
||||
assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);
|
||||
|
||||
// Test adding a new (primary) self address
|
||||
// The address is trimmed during by `LoginParam::from_database()`,
|
||||
// The address is trimmed during configure by `LoginParam::from_database()`,
|
||||
// so `set_primary_self_addr()` doesn't have to trim it.
|
||||
alice.set_primary_self_addr(" Alice@alice.com ").await?;
|
||||
assert!(alice.is_self_addr(" aliCe@example.org").await?);
|
||||
alice.set_primary_self_addr("Alice@alice.com").await?;
|
||||
assert!(alice.is_self_addr("aliCe@example.org").await?);
|
||||
assert!(alice.is_self_addr("alice@alice.com").await?);
|
||||
assert_eq!(
|
||||
alice.get_all_self_addrs().await?,
|
||||
vec![" Alice@alice.com ", "Alice@Example.Org"]
|
||||
vec!["Alice@alice.com", "Alice@Example.Org"]
|
||||
);
|
||||
|
||||
// Check that the entry is not duplicated
|
||||
@@ -537,4 +552,68 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_change_primary_self_addr() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Alice sends a message to Bob
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_text(alice_bob_chat.id, "Hi").await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
bob_msg.chat_id.accept(&bob).await?;
|
||||
assert_eq!(bob_msg.text.unwrap(), "Hi");
|
||||
|
||||
// Alice changes her self address and reconfigures
|
||||
// (ensure_secret_key_exists() is called during configure)
|
||||
alice
|
||||
.set_primary_self_addr("alice@someotherdomain.xyz")
|
||||
.await?;
|
||||
crate::e2ee::ensure_secret_key_exists(&alice).await?;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_primary_self_addr().await?,
|
||||
"alice@someotherdomain.xyz"
|
||||
);
|
||||
|
||||
// Bob sends a message to Alice, encrypting to her previous key
|
||||
let sent = bob.send_text(bob_msg.chat_id, "hi back").await;
|
||||
|
||||
// Alice set up message forwarding so that she still receives
|
||||
// the message with her new address
|
||||
let alice_msg = alice.recv_msg(&sent).await;
|
||||
assert_eq!(alice_msg.text, Some("hi back".to_string()));
|
||||
assert_eq!(alice_msg.get_showpadlock(), true);
|
||||
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
|
||||
|
||||
// Even if Bob sends a message to Alice without In-Reply-To,
|
||||
// it's still assigned to the 1:1 chat with Bob and not to
|
||||
// a group (without secondary addresses, an ad-hoc group
|
||||
// would be created)
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: bob@example.net
|
||||
To: alice@example.org
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <456@example.com>
|
||||
|
||||
Message w/out In-Reply-To
|
||||
",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
alice_msg.text,
|
||||
Some("Message w/out In-Reply-To".to_string())
|
||||
);
|
||||
assert_eq!(alice_msg.get_showpadlock(), false);
|
||||
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,7 +699,7 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
&format!(
|
||||
"SELECT c.id FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
|
||||
WHERE c.addr NOT IN ({})
|
||||
@@ -754,7 +754,7 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
&format!(
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr NOT IN ({})
|
||||
AND id>?
|
||||
|
||||
@@ -47,16 +47,9 @@ pub struct ReceivedMsg {
|
||||
pub chat_id: ChatId,
|
||||
pub state: MessageState,
|
||||
pub sort_timestamp: i64,
|
||||
// Feel free to add more fields here
|
||||
}
|
||||
|
||||
/// Information about added message parts.
|
||||
struct AddedParts {
|
||||
/// Common info about received messages.
|
||||
pub received_msg: ReceivedMsg,
|
||||
|
||||
/// IDs of inserted rows in messages table.
|
||||
pub created_db_entries: Vec<MsgId>,
|
||||
pub msg_ids: Vec<MsgId>,
|
||||
|
||||
/// Whether IMAP messages should be immediately deleted.
|
||||
pub needs_delete_job: bool,
|
||||
@@ -186,7 +179,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp));
|
||||
|
||||
// Add parts
|
||||
let added_parts = add_parts(
|
||||
let received_msg = add_parts(
|
||||
context,
|
||||
&mut mime_parser,
|
||||
imf_raw,
|
||||
@@ -211,7 +204,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
// Update gossiped timestamp for the chat if someone else or our other device sent
|
||||
// Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves
|
||||
// and waste traffic.
|
||||
let chat_id = added_parts.received_msg.chat_id;
|
||||
let chat_id = received_msg.chat_id;
|
||||
if !chat_id.is_special()
|
||||
&& mime_parser
|
||||
.recipients
|
||||
@@ -229,7 +222,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
}
|
||||
}
|
||||
|
||||
let insert_msg_id = if let Some(msg_id) = added_parts.created_db_entries.last() {
|
||||
let insert_msg_id = if let Some(msg_id) = received_msg.msg_ids.last() {
|
||||
*msg_id
|
||||
} else {
|
||||
MsgId::new_unset()
|
||||
@@ -310,8 +303,8 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
// Get user-configured server deletion
|
||||
let delete_server_after = context.get_config_delete_server_after().await?;
|
||||
|
||||
if !added_parts.created_db_entries.is_empty() {
|
||||
if added_parts.needs_delete_job
|
||||
if !received_msg.msg_ids.is_empty() {
|
||||
if received_msg.needs_delete_job
|
||||
|| (delete_server_after == Some(0) && is_partial_download.is_none())
|
||||
{
|
||||
context
|
||||
@@ -330,12 +323,12 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
if replace_partial_download {
|
||||
context.emit_msgs_changed(chat_id, MsgId::new(0));
|
||||
} else if !chat_id.is_trash() {
|
||||
let fresh = added_parts.received_msg.state == MessageState::InFresh;
|
||||
for msg_id in added_parts.created_db_entries {
|
||||
let fresh = received_msg.state == MessageState::InFresh;
|
||||
for msg_id in &received_msg.msg_ids {
|
||||
if incoming && fresh {
|
||||
context.emit_incoming_msg(chat_id, msg_id);
|
||||
context.emit_incoming_msg(chat_id, *msg_id);
|
||||
} else {
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
context.emit_msgs_changed(chat_id, *msg_id);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -344,7 +337,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
.handle_reports(context, from_id, sent_timestamp, &mime_parser.parts)
|
||||
.await;
|
||||
|
||||
Ok(Some(added_parts.received_msg))
|
||||
Ok(Some(received_msg))
|
||||
}
|
||||
|
||||
/// Converts "From" field to contact id.
|
||||
@@ -408,7 +401,7 @@ async fn add_parts(
|
||||
is_partial_download: Option<u32>,
|
||||
fetching_existing_messages: bool,
|
||||
prevent_rename: bool,
|
||||
) -> Result<AddedParts> {
|
||||
) -> Result<ReceivedMsg> {
|
||||
let mut chat_id = None;
|
||||
let mut chat_id_blocked = Blocked::Not;
|
||||
|
||||
@@ -1181,15 +1174,11 @@ INSERT INTO msgs
|
||||
}
|
||||
}
|
||||
|
||||
let received_msg = ReceivedMsg {
|
||||
Ok(ReceivedMsg {
|
||||
chat_id,
|
||||
state,
|
||||
sort_timestamp,
|
||||
};
|
||||
|
||||
Ok(AddedParts {
|
||||
received_msg,
|
||||
created_db_entries,
|
||||
msg_ids: created_db_entries,
|
||||
needs_delete_job,
|
||||
})
|
||||
}
|
||||
@@ -1441,7 +1430,9 @@ async fn create_or_lookup_group(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let grpname = mime_parser.get_header(HeaderDef::ChatGroupName).unwrap();
|
||||
let grpname = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
.context("Chat-Group-Name vanished")?;
|
||||
let new_chat_id = ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
@@ -2008,7 +1999,7 @@ async fn check_verified_properties(
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
&format!(
|
||||
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
|
||||
sql::repeat_vars(to_ids.len())
|
||||
@@ -3747,8 +3738,7 @@ YEAAAAAA!.
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.get_text(), Some("hi!".to_string()));
|
||||
assert!(!msg.get_showpadlock());
|
||||
let mime = message::get_mime_headers(&bob, msg.id).await?;
|
||||
@@ -3767,25 +3757,23 @@ YEAAAAAA!.
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.get_text(), Some("hi!".to_string()));
|
||||
assert!(!msg.get_showpadlock());
|
||||
let mime = message::get_mime_headers(&bob, msg.id).await?;
|
||||
let mime_str = String::from_utf8_lossy(&mime);
|
||||
assert!(mime_str.contains("Received:"));
|
||||
assert!(mime_str.contains("Message-ID:"));
|
||||
assert!(mime_str.contains("From:"));
|
||||
|
||||
// another one, from bob to alice, that gets encrypted
|
||||
let chat_bob = bob.create_chat(&alice).await;
|
||||
chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
let msg = alice.get_last_msg().await;
|
||||
let msg = alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.get_text(), Some("ho!".to_string()));
|
||||
assert!(msg.get_showpadlock());
|
||||
let mime = message::get_mime_headers(&alice, msg.id).await?;
|
||||
let mime_str = String::from_utf8_lossy(&mime);
|
||||
assert!(mime_str.contains("Received:"));
|
||||
assert!(mime_str.contains("Message-ID:"));
|
||||
assert!(mime_str.contains("From:"));
|
||||
Ok(())
|
||||
}
|
||||
@@ -4470,16 +4458,14 @@ Second thread."#;
|
||||
let alice_first_reply = alice
|
||||
.send_text(alice_first_msg.chat_id, "First reply")
|
||||
.await;
|
||||
bob.recv_msg(&alice_first_reply).await;
|
||||
let bob_first_reply = bob.get_last_msg().await;
|
||||
let bob_first_reply = bob.recv_msg(&alice_first_reply).await;
|
||||
assert_eq!(bob_first_reply.chat_id, bob_first_msg.chat_id);
|
||||
|
||||
alice_second_msg.chat_id.accept(&alice).await?;
|
||||
let alice_second_reply = alice
|
||||
.send_text(alice_second_msg.chat_id, "Second reply")
|
||||
.await;
|
||||
bob.recv_msg(&alice_second_reply).await;
|
||||
let bob_second_reply = bob.get_last_msg().await;
|
||||
let bob_second_reply = bob.recv_msg(&alice_second_reply).await;
|
||||
assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id);
|
||||
|
||||
// Alice adds Fiona to both ad hoc groups.
|
||||
@@ -4494,13 +4480,11 @@ Second thread."#;
|
||||
|
||||
chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?;
|
||||
let alice_first_invite = alice.pop_sent_msg().await;
|
||||
fiona.recv_msg(&alice_first_invite).await;
|
||||
let fiona_first_invite = fiona.get_last_msg().await;
|
||||
let fiona_first_invite = fiona.recv_msg(&alice_first_invite).await;
|
||||
|
||||
chat::add_contact_to_chat(&alice, alice_second_msg.chat_id, alice_fiona_contact_id).await?;
|
||||
let alice_second_invite = alice.pop_sent_msg().await;
|
||||
fiona.recv_msg(&alice_second_invite).await;
|
||||
let fiona_second_invite = fiona.get_last_msg().await;
|
||||
let fiona_second_invite = fiona.recv_msg(&alice_second_invite).await;
|
||||
|
||||
// Fiona was added to two separate chats and should see two separate chats, even though they
|
||||
// don't have different group IDs to distinguish them.
|
||||
@@ -4782,8 +4766,7 @@ Reply from different address
|
||||
let sent = alice.send_msg(alice_chat.id, &mut msg_alice).await;
|
||||
println!("{}", sent.payload());
|
||||
|
||||
bob.recv_msg(&sent).await;
|
||||
let msg_bob = bob.get_last_msg().await;
|
||||
let msg_bob = bob.recv_msg(&sent).await;
|
||||
|
||||
async fn check_message(msg: &Message, t: &TestContext, content: &str) {
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||
@@ -4823,9 +4806,7 @@ Reply from different address
|
||||
|
||||
alice1.recv_msg(&sent).await;
|
||||
alice2.recv_msg(&sent).await;
|
||||
bob2.recv_msg(&sent).await;
|
||||
|
||||
let alice1_msg = alice1.get_last_msg().await;
|
||||
let alice1_msg = bob2.recv_msg(&sent).await;
|
||||
assert_eq!(alice1_msg.text.unwrap(), "Hello!");
|
||||
let alice1_chat = chat::Chat::load_from_db(&alice1, alice1_msg.chat_id).await?;
|
||||
assert!(alice1_chat.is_contact_request());
|
||||
|
||||
21
src/e2ee.rs
21
src/e2ee.rs
@@ -171,7 +171,6 @@ pub async fn try_decrypt(
|
||||
}
|
||||
|
||||
// Possibly perform decryption
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
@@ -185,17 +184,11 @@ pub async fn try_decrypt(
|
||||
}
|
||||
}
|
||||
|
||||
let (out_mail, signatures) = match decrypt_if_autocrypt_message(
|
||||
context,
|
||||
mail,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some((out_mail, signatures)) => (Some(out_mail), signatures),
|
||||
None => (None, Default::default()),
|
||||
};
|
||||
let (out_mail, signatures) =
|
||||
match decrypt_if_autocrypt_message(context, mail, public_keyring_for_validate).await? {
|
||||
Some((out_mail, signatures)) => (Some(out_mail), signatures),
|
||||
None => (None, Default::default()),
|
||||
};
|
||||
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// If message is not encrypted and it is not a read receipt, degrade encryption.
|
||||
@@ -293,7 +286,6 @@ fn get_attachment_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMai
|
||||
async fn decrypt_if_autocrypt_message(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail)
|
||||
@@ -307,6 +299,9 @@ async fn decrypt_if_autocrypt_message(
|
||||
Some(res) => res,
|
||||
};
|
||||
info!(context, "Detected Autocrypt-mime message");
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
|
||||
.await
|
||||
.context("failed to get own keyring")?;
|
||||
|
||||
decrypt_part(
|
||||
encrypted_data_part,
|
||||
|
||||
@@ -76,7 +76,7 @@ use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::stock_str;
|
||||
use std::cmp::max;
|
||||
|
||||
@@ -303,15 +303,11 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
) -> Result<()> {
|
||||
let msg_ids: Vec<&dyn crate::ToSql> = msg_ids
|
||||
.iter()
|
||||
.map(|msg_id| msg_id as &dyn crate::ToSql)
|
||||
.collect();
|
||||
let now = time();
|
||||
let count = context
|
||||
.sql
|
||||
.execute(
|
||||
format!(
|
||||
&format!(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? + ephemeral_timer
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ? + ephemeral_timer) AND ephemeral_timer > 0
|
||||
AND id IN ({})",
|
||||
@@ -320,7 +316,7 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
rusqlite::params_from_iter(
|
||||
std::iter::once(&now as &dyn crate::ToSql)
|
||||
.chain(std::iter::once(&now as &dyn crate::ToSql))
|
||||
.chain(msg_ids),
|
||||
.chain(params_iter(msg_ids)),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
//! # Events specification.
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
use async_std::channel::{self, Receiver, Sender, TrySendError};
|
||||
use async_std::path::PathBuf;
|
||||
use strum::EnumProperty;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::contact::ContactId;
|
||||
@@ -92,8 +89,6 @@ impl async_std::stream::Stream for EventEmitter {
|
||||
/// context emits them in relation to various operations happening, a lot of these are again
|
||||
/// documented in `deltachat.h`.
|
||||
///
|
||||
/// This struct [`Deref`]s to the [`EventType`].
|
||||
///
|
||||
/// [`Context`]: crate::context::Context
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Event {
|
||||
@@ -110,68 +105,39 @@ pub struct Event {
|
||||
pub typ: EventType,
|
||||
}
|
||||
|
||||
impl Deref for Event {
|
||||
type Target = EventType;
|
||||
|
||||
fn deref(&self) -> &EventType {
|
||||
&self.typ
|
||||
}
|
||||
}
|
||||
|
||||
impl EventType {
|
||||
/// Returns the corresponding Event ID.
|
||||
///
|
||||
/// These are the IDs used in the `DC_EVENT_*` constants in `deltachat.h`.
|
||||
pub fn as_id(&self) -> i32 {
|
||||
self.get_str("id")
|
||||
.expect("missing id")
|
||||
.parse()
|
||||
.expect("invalid id")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EventType {
|
||||
/// The library-user may write an informational string to the log.
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
#[strum(props(id = "100"))]
|
||||
Info(String),
|
||||
|
||||
/// Emitted when SMTP connection is established and login was successful.
|
||||
#[strum(props(id = "101"))]
|
||||
SmtpConnected(String),
|
||||
|
||||
/// Emitted when IMAP connection is established and login was successful.
|
||||
#[strum(props(id = "102"))]
|
||||
ImapConnected(String),
|
||||
|
||||
/// Emitted when a message was successfully sent to the SMTP server.
|
||||
#[strum(props(id = "103"))]
|
||||
SmtpMessageSent(String),
|
||||
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
#[strum(props(id = "104"))]
|
||||
ImapMessageDeleted(String),
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
#[strum(props(id = "105"))]
|
||||
ImapMessageMoved(String),
|
||||
|
||||
/// Emitted when an new file in the $BLOBDIR was created
|
||||
#[strum(props(id = "150"))]
|
||||
NewBlobFile(String),
|
||||
|
||||
/// Emitted when an file in the $BLOBDIR was deleted
|
||||
#[strum(props(id = "151"))]
|
||||
DeletedBlobFile(String),
|
||||
|
||||
/// The library-user should write a warning string to the log.
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
#[strum(props(id = "300"))]
|
||||
Warning(String),
|
||||
|
||||
/// The library-user should report an error to the end-user.
|
||||
@@ -184,7 +150,6 @@ pub enum EventType {
|
||||
/// it might be better to delay showing these events until the function has really
|
||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||
/// in a messasge box then.
|
||||
#[strum(props(id = "400"))]
|
||||
Error(String),
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
@@ -192,7 +157,6 @@ pub enum EventType {
|
||||
/// dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
|
||||
/// dc_send_text_msg() or another sending function.
|
||||
#[strum(props(id = "410"))]
|
||||
ErrorSelfNotInGroup(String),
|
||||
|
||||
/// Messages or chats changed. One or more messages or chats changed for various
|
||||
@@ -203,35 +167,44 @@ pub enum EventType {
|
||||
///
|
||||
/// `chat_id` is set if only a single chat is affected by the changes, otherwise 0.
|
||||
/// `msg_id` is set if only a single message is affected by the changes, otherwise 0.
|
||||
#[strum(props(id = "2000"))]
|
||||
MsgsChanged { chat_id: ChatId, msg_id: MsgId },
|
||||
MsgsChanged {
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
||||
#[strum(props(id = "2005"))]
|
||||
IncomingMsg { chat_id: ChatId, msg_id: MsgId },
|
||||
IncomingMsg {
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Messages were seen or noticed.
|
||||
/// chat id is always set.
|
||||
#[strum(props(id = "2008"))]
|
||||
MsgsNoticed(ChatId),
|
||||
|
||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
|
||||
#[strum(props(id = "2010"))]
|
||||
MsgDelivered { chat_id: ChatId, msg_id: MsgId },
|
||||
MsgDelivered {
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
|
||||
#[strum(props(id = "2012"))]
|
||||
MsgFailed { chat_id: ChatId, msg_id: MsgId },
|
||||
MsgFailed {
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
|
||||
#[strum(props(id = "2015"))]
|
||||
MsgRead { chat_id: ChatId, msg_id: MsgId },
|
||||
MsgRead {
|
||||
chat_id: ChatId,
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
@@ -240,11 +213,9 @@ pub enum EventType {
|
||||
///
|
||||
/// This event does not include ephemeral timer modification, which
|
||||
/// is a separate event.
|
||||
#[strum(props(id = "2020"))]
|
||||
ChatModified(ChatId),
|
||||
|
||||
/// Chat ephemeral timer changed.
|
||||
#[strum(props(id = "2021"))]
|
||||
ChatEphemeralTimerModified {
|
||||
chat_id: ChatId,
|
||||
timer: EphemeralTimer,
|
||||
@@ -253,7 +224,6 @@ pub enum EventType {
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
///
|
||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
#[strum(props(id = "2030"))]
|
||||
ContactsChanged(Option<ContactId>),
|
||||
|
||||
/// Location of one or more contact has changed.
|
||||
@@ -261,11 +231,9 @@ pub enum EventType {
|
||||
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||
/// If the locations of several contacts have been changed,
|
||||
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
|
||||
#[strum(props(id = "2035"))]
|
||||
LocationChanged(Option<ContactId>),
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
#[strum(props(id = "2041"))]
|
||||
ConfigureProgress {
|
||||
/// Progress.
|
||||
///
|
||||
@@ -280,7 +248,6 @@ pub enum EventType {
|
||||
///
|
||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
/// @param data2 0
|
||||
#[strum(props(id = "2051"))]
|
||||
ImexProgress(usize),
|
||||
|
||||
/// A file has been exported. A file has been written by imex().
|
||||
@@ -290,7 +257,6 @@ pub enum EventType {
|
||||
/// services.
|
||||
///
|
||||
/// @param data2 0
|
||||
#[strum(props(id = "2052"))]
|
||||
ImexFileWritten(PathBuf),
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the inviter
|
||||
@@ -305,7 +271,6 @@ pub enum EventType {
|
||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
#[strum(props(id = "2060"))]
|
||||
SecurejoinInviterProgress {
|
||||
contact_id: ContactId,
|
||||
progress: usize,
|
||||
@@ -319,7 +284,6 @@ pub enum EventType {
|
||||
/// @param data2 (int) Progress as:
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
#[strum(props(id = "2061"))]
|
||||
SecurejoinJoinerProgress {
|
||||
contact_id: ContactId,
|
||||
progress: usize,
|
||||
@@ -329,13 +293,10 @@ pub enum EventType {
|
||||
/// This means that you should refresh the connectivity view
|
||||
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
|
||||
/// dc_get_connectivity_html() for details.
|
||||
#[strum(props(id = "2100"))]
|
||||
ConnectivityChanged,
|
||||
|
||||
#[strum(props(id = "2110"))]
|
||||
SelfavatarChanged,
|
||||
|
||||
#[strum(props(id = "2120"))]
|
||||
WebxdcStatusUpdate {
|
||||
msg_id: MsgId,
|
||||
status_update_serial: StatusUpdateSerial,
|
||||
|
||||
19
src/html.rs
19
src/html.rs
@@ -11,7 +11,7 @@ use futures::future::FutureExt;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use lettre_email::mime::{self, Mime};
|
||||
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
@@ -134,7 +134,7 @@ impl HtmlMsgParser {
|
||||
if raw.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mail = mailparse::parse_mail(&raw).unwrap();
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
self.collect_texts_recursive(context, &mail).await
|
||||
}
|
||||
MimeMultipartType::Single => {
|
||||
@@ -190,7 +190,7 @@ impl HtmlMsgParser {
|
||||
if raw.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mail = mailparse::parse_mail(&raw).unwrap();
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
self.cid_to_data_recursive(context, &mail).await
|
||||
}
|
||||
MimeMultipartType::Single => {
|
||||
@@ -467,8 +467,8 @@ test some special html-characters as < > and & but also " and &#x
|
||||
// bob: check that bob also got the html-part of the forwarded message
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg_in(chat.get_id()).await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(chat.id, msg.chat_id);
|
||||
assert_ne!(msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert!(msg.is_forwarded());
|
||||
@@ -503,9 +503,8 @@ test some special html-characters as < > and & but also " and &#x
|
||||
// receive the message on another device
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(alice.get_config_int(Config::ShowEmails).await.unwrap(), 0); // set to "1" above, make sure it is another db
|
||||
alice.recv_msg(&msg).await;
|
||||
let chat = alice.get_self_chat().await;
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
let msg = alice.recv_msg(&msg).await;
|
||||
assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
|
||||
assert_eq!(msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert!(msg.get_showpadlock());
|
||||
@@ -539,8 +538,8 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
// let bob receive the message
|
||||
let chat_id = bob.create_chat(&alice).await.id;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg_in(chat_id).await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.chat_id, chat_id);
|
||||
assert_eq!(msg.get_text(), Some("plain text".to_string()));
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.mime_modified);
|
||||
|
||||
25
src/imap.rs
25
src/imap.rs
@@ -125,6 +125,14 @@ enum FolderMeaning {
|
||||
Sent,
|
||||
Drafts,
|
||||
Other,
|
||||
|
||||
/// Virtual folders.
|
||||
///
|
||||
/// On Gmail there are virtual folders marked as \\All, \\Important and \\Flagged.
|
||||
/// Delta Chat ignores these folders because the same messages can be fetched
|
||||
/// from the real folder and the result of moving and deleting messages via
|
||||
/// virtual folder is unclear.
|
||||
Virtual,
|
||||
}
|
||||
|
||||
impl FolderMeaning {
|
||||
@@ -135,6 +143,7 @@ impl FolderMeaning {
|
||||
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
|
||||
FolderMeaning::Drafts => None,
|
||||
FolderMeaning::Other => None,
|
||||
FolderMeaning::Virtual => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -910,7 +919,7 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
format!(
|
||||
&format!(
|
||||
"DELETE FROM imap WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
@@ -947,7 +956,7 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
format!(
|
||||
&format!(
|
||||
"DELETE FROM imap WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
@@ -989,7 +998,7 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
format!(
|
||||
&format!(
|
||||
"UPDATE imap SET target='' WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
@@ -1030,9 +1039,14 @@ impl Imap {
|
||||
.await?;
|
||||
|
||||
self.prepare(context).await?;
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
|
||||
for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
// Select folder inside the loop to avoid selecting it if there are no pending
|
||||
// MOVE/DELETE operations. This does not result in multiple SELECT commands
|
||||
// being sent because `select_folder()` does nothing if the folder is already
|
||||
// selected.
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
|
||||
// Empty target folder name means messages should be deleted.
|
||||
if target.is_empty() {
|
||||
self.delete_message_batch(context, &uid_set, rowid_set)
|
||||
@@ -1101,7 +1115,7 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
format!(
|
||||
&format!(
|
||||
"DELETE FROM imap_markseen WHERE id IN ({})",
|
||||
sql::repeat_vars(rowid_set.len())
|
||||
),
|
||||
@@ -1883,6 +1897,7 @@ fn get_folder_meaning(folder_name: &Name) -> FolderMeaning {
|
||||
"\\Sent" => return FolderMeaning::Sent,
|
||||
"\\Spam" | "\\Junk" => return FolderMeaning::Spam,
|
||||
"\\Drafts" => return FolderMeaning::Drafts,
|
||||
"\\All" | "\\Important" | "\\Flagged" => return FolderMeaning::Virtual,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,16 +35,14 @@ impl Imap {
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
|
||||
for folder in folders {
|
||||
// Gmail labels are not folders and should be skipped. For example,
|
||||
// emails appear in the inbox and under "All Mail" as soon as it is
|
||||
// received. The code used to wrongly conclude that the email had
|
||||
// already been moved and left it in the inbox.
|
||||
let folder_name = folder.name();
|
||||
if folder_name.starts_with("[Gmail]") {
|
||||
let folder_meaning = get_folder_meaning(&folder);
|
||||
if folder_meaning == FolderMeaning::Virtual {
|
||||
// Gmail has virtual folders that should be skipped. For example,
|
||||
// emails appear in the inbox and under "All Mail" as soon as it is
|
||||
// received. The code used to wrongly conclude that the email had
|
||||
// already been moved and left it in the inbox.
|
||||
continue;
|
||||
}
|
||||
|
||||
let folder_meaning = get_folder_meaning(&folder);
|
||||
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
|
||||
|
||||
if let Some(config) = folder_meaning.to_config() {
|
||||
|
||||
45
src/imex.rs
45
src/imex.rs
@@ -987,4 +987,49 @@ mod tests {
|
||||
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
|
||||
assert!(headers.get(HEADER_SETUPCODE).is_none());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_key_transfer() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let alice_clone = alice.clone();
|
||||
let key_transfer_task = async_std::task::spawn(async move {
|
||||
let ctx = alice_clone;
|
||||
initiate_key_transfer(&ctx).await
|
||||
});
|
||||
|
||||
// Wait for the message to be added to the queue.
|
||||
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let setup_code = key_transfer_task.await?;
|
||||
|
||||
// Alice sets up a second device.
|
||||
let alice2 = TestContext::new().await;
|
||||
alice2.set_name("alice2");
|
||||
alice2.configure_addr("alice@example.org").await;
|
||||
alice2.recv_msg(&sent).await;
|
||||
let msg = alice2.get_last_msg().await;
|
||||
|
||||
// Send a message that cannot be decrypted because the keys are
|
||||
// not synchronized yet.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_ne!(
|
||||
alice.get_last_msg().await.get_text(),
|
||||
Some("Test".to_string())
|
||||
);
|
||||
|
||||
// Transfer the key.
|
||||
continue_key_transfer(&alice2, msg.id, &setup_code).await?;
|
||||
|
||||
// Alice sends a message to self from the new device.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
alice.get_last_msg().await.get_text(),
|
||||
Some("Test".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
105
src/key.rs
105
src/key.rs
@@ -4,7 +4,7 @@ use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
@@ -82,7 +82,7 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
|
||||
|
||||
/// The fingerprint for the key.
|
||||
fn fingerprint(&self) -> Fingerprint {
|
||||
Fingerprint::new(KeyTrait::fingerprint(self)).expect("Invalid fingerprint from rpgp")
|
||||
Fingerprint::new(KeyTrait::fingerprint(self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,29 +204,8 @@ 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 context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT public_key, private_key
|
||||
FROM keypairs
|
||||
WHERE addr=?1
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
|row| {
|
||||
let pub_bytes: Vec<u8> = row.get(0)?;
|
||||
let sec_bytes: Vec<u8> = row.get(1)?;
|
||||
Ok((pub_bytes, sec_bytes))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some((pub_bytes, sec_bytes)) => Ok(KeyPair {
|
||||
addr,
|
||||
public: SignedPublicKey::from_slice(&pub_bytes)?,
|
||||
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||
}),
|
||||
match load_keypair(context, &addr).await? {
|
||||
Some(key_pair) => Ok(key_pair),
|
||||
None => {
|
||||
let start = std::time::SystemTime::now();
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
|
||||
@@ -246,6 +225,39 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn load_keypair(
|
||||
context: &Context,
|
||||
addr: &EmailAddress,
|
||||
) -> Result<Option<KeyPair>> {
|
||||
let res = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT public_key, private_key
|
||||
FROM keypairs
|
||||
WHERE addr=?1
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
|row| {
|
||||
let pub_bytes: Vec<u8> = row.get(0)?;
|
||||
let sec_bytes: Vec<u8> = row.get(1)?;
|
||||
Ok((pub_bytes, sec_bytes))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
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)?,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Use of a [KeyPair] for encryption or decryption.
|
||||
///
|
||||
/// This is used by [store_self_keypair] to know what kind of key is
|
||||
@@ -275,23 +287,20 @@ pub async fn store_self_keypair(
|
||||
keypair: &KeyPair,
|
||||
default: KeyPairUse,
|
||||
) -> Result<()> {
|
||||
// Everything should really be one transaction, more refactoring
|
||||
// is needed for that.
|
||||
let mut conn = context.sql.get_conn().await?;
|
||||
let transaction = conn.transaction()?;
|
||||
|
||||
let public_key = DcKey::to_bytes(&keypair.public);
|
||||
let secret_key = DcKey::to_bytes(&keypair.secret);
|
||||
context
|
||||
.sql
|
||||
transaction
|
||||
.execute(
|
||||
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
|
||||
paramsv![public_key, secret_key],
|
||||
)
|
||||
.await
|
||||
.context("failed to remove old use of key")?;
|
||||
if default == KeyPairUse::Default {
|
||||
context
|
||||
.sql
|
||||
transaction
|
||||
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
|
||||
.await
|
||||
.context("failed to clear default")?;
|
||||
}
|
||||
let is_default = match default {
|
||||
@@ -302,16 +311,16 @@ pub async fn store_self_keypair(
|
||||
let addr = keypair.addr.to_string();
|
||||
let t = time();
|
||||
|
||||
context
|
||||
.sql
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
|
||||
VALUES (?,?,?,?,?);",
|
||||
paramsv![addr, is_default, public_key, secret_key, t],
|
||||
)
|
||||
.await
|
||||
.context("failed to insert keypair")?;
|
||||
|
||||
transaction.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -320,11 +329,9 @@ pub async fn store_self_keypair(
|
||||
pub struct Fingerprint(Vec<u8>);
|
||||
|
||||
impl Fingerprint {
|
||||
pub fn new(v: Vec<u8>) -> Result<Fingerprint> {
|
||||
match v.len() {
|
||||
20 => Ok(Fingerprint(v)),
|
||||
_ => Err(format_err!("Wrong fingerprint length")),
|
||||
}
|
||||
pub fn new(v: Vec<u8>) -> Fingerprint {
|
||||
debug_assert_eq!(v.len(), 20);
|
||||
Fingerprint(v)
|
||||
}
|
||||
|
||||
/// Make a hex string from the fingerprint.
|
||||
@@ -364,14 +371,15 @@ impl fmt::Display for Fingerprint {
|
||||
impl std::str::FromStr for Fingerprint {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
let hex_repr: String = input
|
||||
.to_uppercase()
|
||||
.chars()
|
||||
.filter(|&c| ('0'..='9').contains(&c) || ('A'..='F').contains(&c))
|
||||
.collect();
|
||||
let v: Vec<u8> = hex::decode(hex_repr)?;
|
||||
let fp = Fingerprint::new(v)?;
|
||||
let v: Vec<u8> = hex::decode(&hex_repr)?;
|
||||
ensure!(v.len() == 20, "wrong fingerprint length: {}", hex_repr);
|
||||
let fp = Fingerprint::new(v);
|
||||
Ok(fp)
|
||||
}
|
||||
}
|
||||
@@ -589,8 +597,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
fn test_fingerprint_from_str() {
|
||||
let res = Fingerprint::new(vec![
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
])
|
||||
.unwrap();
|
||||
]);
|
||||
|
||||
let fp: Fingerprint = "0102030405060708090A0B0c0d0e0F1011121314".parse().unwrap();
|
||||
assert_eq!(fp, res);
|
||||
@@ -607,8 +614,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
fn test_fingerprint_hex() {
|
||||
let fp = Fingerprint::new(vec![
|
||||
1, 2, 4, 8, 16, 32, 64, 128, 255, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
])
|
||||
.unwrap();
|
||||
]);
|
||||
assert_eq!(fp.hex(), "0102040810204080FF0A0B0C0D0E0F1011121314");
|
||||
}
|
||||
|
||||
@@ -616,8 +622,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
fn test_fingerprint_to_string() {
|
||||
let fp = Fingerprint::new(vec![
|
||||
1, 2, 4, 8, 16, 32, 64, 128, 255, 1, 2, 4, 8, 16, 32, 64, 128, 255, 19, 20,
|
||||
])
|
||||
.unwrap();
|
||||
]);
|
||||
assert_eq!(
|
||||
fp.to_string(),
|
||||
"0102 0408 1020 4080 FF01\n0204 0810 2040 80FF 1314"
|
||||
|
||||
@@ -153,12 +153,12 @@ impl Kml {
|
||||
) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
if tag == "document" {
|
||||
if let Some(addr) = event.attributes().find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "addr")
|
||||
.unwrap_or_default()
|
||||
}) {
|
||||
self.addr = addr.unwrap().unescape_and_decode_value(reader).ok();
|
||||
if let Some(addr) = event
|
||||
.attributes()
|
||||
.filter_map(|a| a.ok())
|
||||
.find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "addr")
|
||||
{
|
||||
self.addr = addr.unescape_and_decode_value(reader).ok();
|
||||
}
|
||||
} else if tag == "placemark" {
|
||||
self.tag = KmlTag::PLACEMARK;
|
||||
|
||||
@@ -210,7 +210,7 @@ impl LoginParam {
|
||||
let key = format!("{}smtp_certificate_checks", prefix);
|
||||
let smtp_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap_or_default()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
@@ -1283,7 +1283,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
let msgs = context
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
&format!(
|
||||
"SELECT
|
||||
m.id AS id,
|
||||
m.chat_id AS chat_id,
|
||||
@@ -2090,8 +2090,8 @@ mod tests {
|
||||
.first()
|
||||
.unwrap();
|
||||
let contact = Contact::load_from_db(&bob, contact_id).await.unwrap();
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg_in(chat.id).await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
assert_eq!(msg.text, Some("bla blubb".to_string()));
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
@@ -2116,13 +2116,11 @@ mod tests {
|
||||
|
||||
// alice sends to bob,
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
bob.recv_msg(&alice.send_msg(alice_chat.id, &mut msg).await)
|
||||
.await;
|
||||
let msg1 = bob.get_last_msg().await;
|
||||
let sent1 = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg1 = bob.recv_msg(&sent1).await;
|
||||
let bob_chat_id = msg1.chat_id;
|
||||
bob.recv_msg(&alice.send_msg(alice_chat.id, &mut msg).await)
|
||||
.await;
|
||||
let msg2 = bob.get_last_msg().await;
|
||||
let sent2 = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg2 = bob.recv_msg(&sent2).await;
|
||||
assert_eq!(msg1.chat_id, msg2.chat_id);
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
@@ -2143,14 +2141,12 @@ mod tests {
|
||||
|
||||
// bob sends to alice,
|
||||
// alice knows bob and messages appear in normal chat
|
||||
alice
|
||||
let msg1 = alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let msg1 = alice.get_last_msg().await;
|
||||
alice
|
||||
let msg2 = alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let msg2 = alice.get_last_msg().await;
|
||||
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0)?, alice_chat.id);
|
||||
@@ -2226,8 +2222,7 @@ mod tests {
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutFailed).await;
|
||||
|
||||
// check incoming message states on receiver side
|
||||
bob.recv_msg(&payload).await;
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
let bob_msg = bob.recv_msg(&payload).await;
|
||||
assert_eq!(bob_chat.id, bob_msg.chat_id);
|
||||
assert_state(&bob, bob_msg.id, MessageState::InFresh).await;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::future::Future;
|
||||
use std::io::Cursor;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use lettre_email::mime::{self, Mime};
|
||||
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||
@@ -265,22 +265,15 @@ impl MimeMessage {
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
|
||||
(decrypted_mail, signatures, true)
|
||||
(Ok(decrypted_mail), signatures, true)
|
||||
} else {
|
||||
// Message was not encrypted
|
||||
(mail, signatures, false)
|
||||
(Ok(mail), signatures, false)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// continue with the current, still encrypted, mime tree.
|
||||
// unencrypted parts will be replaced by an error message
|
||||
// that is added as "the message" to the chat then.
|
||||
//
|
||||
// if we just return here, the header is missing
|
||||
// and the caller cannot display the message
|
||||
// and try to assign the message to a chat
|
||||
warn!(context, "decryption failed: {}", err);
|
||||
(mail, Default::default(), true)
|
||||
(Err(err), Default::default(), true)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -291,7 +284,7 @@ impl MimeMessage {
|
||||
list_post,
|
||||
from,
|
||||
chat_disposition_notification_to,
|
||||
decrypting_failed: false,
|
||||
decrypting_failed: mail.is_err(),
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signatures,
|
||||
@@ -318,9 +311,24 @@ impl MimeMessage {
|
||||
.create_stub_from_partial_download(context, org_bytes)
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
parser.parse_mime_recursive(context, &mail, false).await?;
|
||||
}
|
||||
None => match mail {
|
||||
Ok(mail) => {
|
||||
parser.parse_mime_recursive(context, &mail, false).await?;
|
||||
}
|
||||
Err(err) => {
|
||||
let msg_body = stock_str::cant_decrypt_msg_body(context).await;
|
||||
let txt = format!("[{}]", msg_body);
|
||||
|
||||
let part = Part {
|
||||
typ: Viewtype::Text,
|
||||
msg_raw: Some(txt.clone()),
|
||||
msg: txt,
|
||||
error: Some(format!("Decrypting failed: {}", err)),
|
||||
..Default::default()
|
||||
};
|
||||
parser.parts.push(part);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
parser.maybe_remove_bad_parts();
|
||||
@@ -715,7 +723,7 @@ impl MimeMessage {
|
||||
if raw.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mail = mailparse::parse_mail(&raw).unwrap();
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
|
||||
self.parse_mime_recursive(context, &mail, is_related).await
|
||||
}
|
||||
@@ -779,25 +787,6 @@ impl MimeMessage {
|
||||
self.is_mime_modified = true;
|
||||
}
|
||||
}
|
||||
(mime::MULTIPART, "encrypted") => {
|
||||
// we currently do not try to decrypt non-autocrypt messages
|
||||
// at all. If we see an encrypted part, we set
|
||||
// decrypting_failed.
|
||||
let msg_body = stock_str::cant_decrypt_msg_body(context).await;
|
||||
let txt = format!("[{}]", msg_body);
|
||||
|
||||
let part = Part {
|
||||
typ: Viewtype::Text,
|
||||
msg_raw: Some(txt.clone()),
|
||||
msg: txt,
|
||||
error: Some("Decryption failed".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
self.parts.push(part);
|
||||
|
||||
any_part_added = true;
|
||||
self.decrypting_failed = true;
|
||||
}
|
||||
(mime::MULTIPART, "signed") => {
|
||||
/* RFC 1847: "The multipart/signed content type
|
||||
contains exactly two body parts. The first body
|
||||
|
||||
@@ -146,8 +146,10 @@ pub async fn dc_get_oauth2_access_token(
|
||||
value = &redirect_uri;
|
||||
} else if value == "$CODE" {
|
||||
value = code;
|
||||
} else if value == "$REFRESH_TOKEN" && refresh_token.is_some() {
|
||||
value = refresh_token.as_ref().unwrap();
|
||||
} else if value == "$REFRESH_TOKEN" {
|
||||
if let Some(refresh_token) = refresh_token.as_ref() {
|
||||
value = refresh_token;
|
||||
}
|
||||
}
|
||||
|
||||
post_param.insert(key, value);
|
||||
@@ -162,16 +164,18 @@ pub async fn dc_get_oauth2_access_token(
|
||||
|
||||
let client = surf::Client::new();
|
||||
let parsed: Result<Response, _> = client.recv_json(req).await;
|
||||
if parsed.is_err() {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to parse OAuth2 JSON response from {}: error: {:?}", token_url, parsed
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
let response = match parsed {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to parse OAuth2 JSON response from {}: error: {}", token_url, err
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
// update refresh_token if given, typically on the first round, but we update it later as well.
|
||||
let response = parsed.unwrap();
|
||||
if let Some(ref token) = response.refresh_token {
|
||||
context
|
||||
.sql
|
||||
@@ -286,12 +290,13 @@ impl Oauth2 {
|
||||
// }
|
||||
let response: Result<HashMap<String, serde_json::Value>, surf::Error> =
|
||||
surf::get(userinfo_url).recv_json().await;
|
||||
if response.is_err() {
|
||||
warn!(context, "Error getting userinfo: {:?}", response);
|
||||
return None;
|
||||
}
|
||||
|
||||
let parsed = response.unwrap();
|
||||
let parsed = match response {
|
||||
Ok(parsed) => parsed,
|
||||
Err(err) => {
|
||||
warn!(context, "Error getting userinfo: {}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
|
||||
// but serde_json::Value.to_string() does not!
|
||||
if let Some(addr) = parsed.get("email") {
|
||||
|
||||
@@ -267,6 +267,11 @@ impl Peerstate {
|
||||
context: &Context,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
if context.is_self_addr(&self.addr).await? {
|
||||
// Do not try to search all the chats with self.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.fingerprint_changed {
|
||||
if let Some(contact_id) = context
|
||||
.sql
|
||||
|
||||
12
src/pgp.rs
12
src/pgp.rs
@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashSet};
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
|
||||
@@ -98,9 +98,7 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
|
||||
let mut bytes = Vec::with_capacity(buf.len());
|
||||
|
||||
dearmor.read_to_end(&mut bytes)?;
|
||||
ensure!(dearmor.typ.is_some(), "Failed to parse type");
|
||||
|
||||
let typ = dearmor.typ.unwrap();
|
||||
let typ = dearmor.typ.context("failed to parse type")?;
|
||||
|
||||
// normalize headers
|
||||
let headers = dearmor
|
||||
@@ -183,10 +181,12 @@ pub(crate) fn create_keypair(
|
||||
.can_encrypt(true)
|
||||
.passphrase(None)
|
||||
.build()
|
||||
.unwrap(),
|
||||
.map_err(|err| {
|
||||
PgpKeygenError::new("failed to build subkey parameters", format_err!("{}", err))
|
||||
})?,
|
||||
)
|
||||
.build()
|
||||
.map_err(|err| PgpKeygenError::new("invalid key params", format_err!(err)))?;
|
||||
.map_err(|err| PgpKeygenError::new("invalid key params", format_err!("{}", err)))?;
|
||||
let key = key_params
|
||||
.generate()
|
||||
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
|
||||
|
||||
@@ -137,27 +137,13 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum JoinError {
|
||||
#[error("Failed to send handshake message: {0}")]
|
||||
SendMessage(#[from] SendMsgError),
|
||||
|
||||
// Note that this can currently only occur if there is a bug in the QR/Lot code as this
|
||||
// is supposed to create a contact for us.
|
||||
#[error("Unknown contact (this is a bug): {0}")]
|
||||
UnknownContact(#[source] anyhow::Error),
|
||||
|
||||
#[error("Other")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
|
||||
///
|
||||
/// This is the start of the process for the joiner. See the module and ffi documentation
|
||||
/// for more details.
|
||||
///
|
||||
/// The function returns immediately and the handshake will run in background.
|
||||
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
|
||||
securejoin(context, qr).await.map_err(|err| {
|
||||
warn!(context, "Fatal joiner error: {:#}", err);
|
||||
// The user just scanned this QR code so has context on what failed.
|
||||
@@ -166,7 +152,7 @@ pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId, J
|
||||
})
|
||||
}
|
||||
|
||||
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
|
||||
/*========================================================
|
||||
==== Bob - the joiner's side =====
|
||||
==== Step 2 in "Setup verified contact" protocol =====
|
||||
@@ -180,14 +166,6 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
bob::start_protocol(context, invite).await
|
||||
}
|
||||
|
||||
/// Error when failing to send a protocol handshake message.
|
||||
///
|
||||
/// Wrapping the [anyhow::Error] means we can "impl From" more easily on errors from this
|
||||
/// function.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Failed sending handshake message")]
|
||||
pub struct SendMsgError(#[from] anyhow::Error);
|
||||
|
||||
/// Send handshake message from Alice's device;
|
||||
/// Bob's handshake messages are sent in `BobState::send_handshake_message()`.
|
||||
async fn send_alice_handshake_msg(
|
||||
@@ -195,7 +173,7 @@ async fn send_alice_handshake_msg(
|
||||
contact_id: ContactId,
|
||||
step: &str,
|
||||
fingerprint: Option<Fingerprint>,
|
||||
) -> Result<(), SendMsgError> {
|
||||
) -> Result<()> {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(format!("Secure-Join: {}", step)),
|
||||
@@ -245,8 +223,11 @@ async fn fingerprint_equals_sender(
|
||||
};
|
||||
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.public_key_fingerprint.is_some()
|
||||
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
|
||||
if peerstate
|
||||
.public_key_fingerprint
|
||||
.as_ref()
|
||||
.filter(|&fp| fp == fingerprint)
|
||||
.is_some()
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
@@ -350,7 +331,8 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
&format!("{}-auth-required", &step[..2]),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.context("failed sending auth-required handshake message")?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
"vg-auth-required" | "vc-auth-required" => {
|
||||
@@ -478,7 +460,8 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
"vc-contact-confirm",
|
||||
Some(fingerprint),
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! This are some helper functions around [`BobState`] which augment the state changes with
|
||||
//! the required user interactions.
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus};
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
@@ -16,7 +16,7 @@ use crate::{chat, stock_str};
|
||||
|
||||
use super::bobstate::{BobHandshakeStage, BobState};
|
||||
use super::qrinvite::QrInvite;
|
||||
use super::{HandshakeMessage, JoinError};
|
||||
use super::HandshakeMessage;
|
||||
|
||||
/// Starts the securejoin protocol with the QR `invite`.
|
||||
///
|
||||
@@ -30,10 +30,7 @@ use super::{HandshakeMessage, JoinError};
|
||||
///
|
||||
/// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1
|
||||
/// chat with Alice, for a SecureJoin QR this is the group chat.
|
||||
pub(super) async fn start_protocol(
|
||||
context: &Context,
|
||||
invite: QrInvite,
|
||||
) -> Result<ChatId, JoinError> {
|
||||
pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Result<ChatId> {
|
||||
// A 1:1 chat is needed to send messages to Alice. When joining a group this chat is
|
||||
// hidden, if a user starts sending messages in it it will be unhidden in
|
||||
// dc_receive_imf.
|
||||
@@ -43,7 +40,7 @@ pub(super) async fn start_protocol(
|
||||
};
|
||||
let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
|
||||
.await
|
||||
.map_err(JoinError::UnknownContact)?;
|
||||
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
|
||||
|
||||
// Now start the protocol and initialise the state
|
||||
let (state, stage, aborted_states) =
|
||||
|
||||
@@ -22,9 +22,7 @@ use crate::param::Param;
|
||||
use crate::sql::Sql;
|
||||
|
||||
use super::qrinvite::QrInvite;
|
||||
use super::{
|
||||
encrypted_and_signed, fingerprint_equals_sender, mark_peer_as_verified, JoinError, SendMsgError,
|
||||
};
|
||||
use super::{encrypted_and_signed, fingerprint_equals_sender, mark_peer_as_verified};
|
||||
|
||||
/// The stage of the [`BobState`] securejoin handshake protocol state machine.
|
||||
///
|
||||
@@ -94,7 +92,7 @@ impl BobState {
|
||||
context: &Context,
|
||||
invite: QrInvite,
|
||||
chat_id: ChatId,
|
||||
) -> Result<(Self, BobHandshakeStage, Vec<Self>), JoinError> {
|
||||
) -> Result<(Self, BobHandshakeStage, Vec<Self>)> {
|
||||
let (stage, next) =
|
||||
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await?
|
||||
{
|
||||
@@ -404,11 +402,7 @@ impl BobState {
|
||||
/// Sends the requested handshake message to Alice.
|
||||
///
|
||||
/// This takes care of adding the required headers for the step.
|
||||
async fn send_handshake_message(
|
||||
&self,
|
||||
context: &Context,
|
||||
step: BobHandshakeMsg,
|
||||
) -> Result<(), SendMsgError> {
|
||||
async fn send_handshake_message(&self, context: &Context, step: BobHandshakeMsg) -> Result<()> {
|
||||
send_handshake_message(context, &self.invite, self.chat_id, step).await
|
||||
}
|
||||
}
|
||||
@@ -422,7 +416,7 @@ async fn send_handshake_message(
|
||||
invite: &QrInvite,
|
||||
chat_id: ChatId,
|
||||
step: BobHandshakeMsg,
|
||||
) -> Result<(), SendMsgError> {
|
||||
) -> Result<()> {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(step.body_text(invite)),
|
||||
|
||||
@@ -593,7 +593,7 @@ async fn send_mdn_msg_id(
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(q, rusqlite::params_from_iter(additional_msg_ids))
|
||||
.execute(&q, rusqlite::params_from_iter(additional_msg_ids))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
56
src/sql.rs
56
src/sql.rs
@@ -331,24 +331,16 @@ impl Sql {
|
||||
}
|
||||
|
||||
/// Execute the given query, returning the number of affected rows.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
query: impl AsRef<str>,
|
||||
params: impl rusqlite::Params,
|
||||
) -> Result<usize> {
|
||||
pub async fn execute(&self, query: &str, params: impl rusqlite::Params) -> Result<usize> {
|
||||
let conn = self.get_conn().await?;
|
||||
let res = conn.execute(query.as_ref(), params)?;
|
||||
let res = conn.execute(query, params)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Executes the given query, returning the last inserted row ID.
|
||||
pub async fn insert(
|
||||
&self,
|
||||
query: impl AsRef<str>,
|
||||
params: impl rusqlite::Params,
|
||||
) -> Result<i64> {
|
||||
pub async fn insert(&self, query: &str, params: impl rusqlite::Params) -> Result<i64> {
|
||||
let conn = self.get_conn().await?;
|
||||
conn.execute(query.as_ref(), params)?;
|
||||
conn.execute(query, params)?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
@@ -357,7 +349,7 @@ impl Sql {
|
||||
/// result of that function.
|
||||
pub async fn query_map<T, F, G, H>(
|
||||
&self,
|
||||
sql: impl AsRef<str>,
|
||||
sql: &str,
|
||||
params: impl rusqlite::Params,
|
||||
f: F,
|
||||
mut g: G,
|
||||
@@ -366,8 +358,6 @@ impl Sql {
|
||||
F: FnMut(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||
G: FnMut(rusqlite::MappedRows<F>) -> Result<H>,
|
||||
{
|
||||
let sql = sql.as_ref();
|
||||
|
||||
let conn = self.get_conn().await?;
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
let res = stmt.query_map(params, f)?;
|
||||
@@ -385,11 +375,7 @@ impl Sql {
|
||||
}
|
||||
|
||||
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
|
||||
pub async fn count(
|
||||
&self,
|
||||
query: impl AsRef<str>,
|
||||
params: impl rusqlite::Params,
|
||||
) -> anyhow::Result<usize> {
|
||||
pub async fn count(&self, query: &str, params: impl rusqlite::Params) -> anyhow::Result<usize> {
|
||||
let count: isize = self.query_row(query, params, |row| row.get(0)).await?;
|
||||
Ok(usize::try_from(count)?)
|
||||
}
|
||||
@@ -404,7 +390,7 @@ impl Sql {
|
||||
/// Execute a query which is expected to return one row.
|
||||
pub async fn query_row<T, F>(
|
||||
&self,
|
||||
query: impl AsRef<str>,
|
||||
query: &str,
|
||||
params: impl rusqlite::Params,
|
||||
f: F,
|
||||
) -> Result<T>
|
||||
@@ -412,7 +398,7 @@ impl Sql {
|
||||
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||
{
|
||||
let conn = self.get_conn().await?;
|
||||
let res = conn.query_row(query.as_ref(), params, f)?;
|
||||
let res = conn.query_row(query, params, f)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -474,7 +460,7 @@ impl Sql {
|
||||
/// Execute a query which is expected to return zero or one row.
|
||||
pub async fn query_row_optional<T, F>(
|
||||
&self,
|
||||
sql: impl AsRef<str>,
|
||||
sql: &str,
|
||||
params: impl rusqlite::Params,
|
||||
f: F,
|
||||
) -> anyhow::Result<Option<T>>
|
||||
@@ -716,13 +702,15 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
Ok(mut dir_handle) => {
|
||||
/* avoid deletion of files that are just created to build a message object */
|
||||
let diff = std::time::Duration::from_secs(60 * 60);
|
||||
let keep_files_newer_than = std::time::SystemTime::now().checked_sub(diff).unwrap();
|
||||
let keep_files_newer_than = std::time::SystemTime::now()
|
||||
.checked_sub(diff)
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
|
||||
|
||||
while let Some(entry) = dir_handle.next().await {
|
||||
if entry.is_err() {
|
||||
break;
|
||||
}
|
||||
let entry = entry.unwrap();
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => break,
|
||||
};
|
||||
let name_f = entry.file_name();
|
||||
let name_s = name_f.to_string_lossy();
|
||||
|
||||
@@ -738,11 +726,13 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
|
||||
if let Ok(stats) = async_std::fs::metadata(entry.path()).await {
|
||||
let recently_created =
|
||||
stats.created().is_ok() && stats.created().unwrap() > keep_files_newer_than;
|
||||
let recently_modified = stats.modified().is_ok()
|
||||
&& stats.modified().unwrap() > keep_files_newer_than;
|
||||
let recently_accessed = stats.accessed().is_ok()
|
||||
&& stats.accessed().unwrap() > keep_files_newer_than;
|
||||
stats.created().map_or(false, |t| t > keep_files_newer_than);
|
||||
let recently_modified = stats
|
||||
.modified()
|
||||
.map_or(false, |t| t > keep_files_newer_than);
|
||||
let recently_accessed = stats
|
||||
.accessed()
|
||||
.map_or(false, |t| t > keep_files_newer_than);
|
||||
|
||||
if recently_created || recently_modified || recently_accessed {
|
||||
info!(
|
||||
|
||||
@@ -199,7 +199,7 @@ impl Context {
|
||||
pub(crate) async fn delete_sync_ids(&self, ids: String) -> Result<()> {
|
||||
self.sql
|
||||
.execute(
|
||||
format!("DELETE FROM multi_device_sync WHERE id IN ({});", ids),
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({});", ids),
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Utilities to help writing tests.
|
||||
//!
|
||||
//! This private module is only compiled for test runs.
|
||||
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use std::panic;
|
||||
@@ -370,17 +370,41 @@ impl TestContext {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Receive a message.
|
||||
///
|
||||
/// Receives a message using the `dc_receive_imf()` pipeline.
|
||||
pub async fn recv_msg(&self, msg: &SentMessage) {
|
||||
let received_msg =
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n"
|
||||
.to_owned()
|
||||
+ msg.payload();
|
||||
dc_receive_imf(&self.ctx, received_msg.as_bytes(), false)
|
||||
/// Receive a message using the `dc_receive_imf()` pipeline. Panics if it's not shown
|
||||
/// in the chat as exactly one message.
|
||||
pub async fn recv_msg(&self, msg: &SentMessage) -> Message {
|
||||
let received = self.recv_msg_opt(msg).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
received.msg_ids.len(),
|
||||
1,
|
||||
"recv_msg() can currently only receive messages with exactly one part"
|
||||
);
|
||||
let msg = Message::load_from_db(self, received.msg_ids[0])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat_msgs = chat::get_chat_msgs(self, received.chat_id, 0)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
chat_msgs.contains(&ChatItem::Message { msg_id: msg.id }),
|
||||
"received message is not shown in chat, maybe it's hidden (you may have \
|
||||
to call set_config(Config::ShowEmails, Some(\"2\")).await)"
|
||||
);
|
||||
|
||||
msg
|
||||
}
|
||||
|
||||
/// Receive a message using the `dc_receive_imf()` pipeline. This is similar
|
||||
/// to `recv_msg()`, but doesn't assume that the message is shown in the chat.
|
||||
pub async fn recv_msg_opt(
|
||||
&self,
|
||||
msg: &SentMessage,
|
||||
) -> Option<crate::dc_receive_imf::ReceivedMsg> {
|
||||
dc_receive_imf(self, msg.payload().as_bytes(), false)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Gets the most recent message of a chat.
|
||||
|
||||
@@ -1121,8 +1121,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Bob receives all messages
|
||||
bob.recv_msg(sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid);
|
||||
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
||||
@@ -1219,8 +1218,7 @@ mod tests {
|
||||
assert_eq!(alice_instance.chat_id, alice_chat_id);
|
||||
|
||||
// bob receives the instance together with the initial updates in a single message
|
||||
bob.recv_msg(&sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
let bob_instance = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
||||
assert_eq!(bob_instance.get_filename(), Some("minimal.xdc".to_string()));
|
||||
assert!(sent1.payload().contains("Content-Type: application/json"));
|
||||
@@ -1537,8 +1535,7 @@ sth_for_the = "future""#
|
||||
assert_eq!(info.summary, "sum: 2".to_string());
|
||||
|
||||
// Bob receives the updates
|
||||
bob.recv_msg(sent_instance).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
let bob_instance = bob.recv_msg(sent_instance).await;
|
||||
bob.recv_msg(sent_update1).await;
|
||||
bob.recv_msg(sent_update2).await;
|
||||
let info = Message::load_from_db(&bob, bob_instance.id)
|
||||
@@ -1549,8 +1546,7 @@ sth_for_the = "future""#
|
||||
|
||||
// Alice has a second device and also receives the updates there
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
alice2.recv_msg(sent_instance).await;
|
||||
let alice2_instance = alice2.get_last_msg().await;
|
||||
let alice2_instance = alice2.recv_msg(sent_instance).await;
|
||||
alice2.recv_msg(sent_update1).await;
|
||||
alice2.recv_msg(sent_update2).await;
|
||||
let info = Message::load_from_db(&alice2, alice2_instance.id)
|
||||
@@ -1591,8 +1587,7 @@ sth_for_the = "future""#
|
||||
assert_eq!(info.summary, "".to_string());
|
||||
|
||||
// Bob receives the updates
|
||||
bob.recv_msg(sent_instance).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
let bob_instance = bob.recv_msg(sent_instance).await;
|
||||
bob.recv_msg(sent_update1).await;
|
||||
let info = Message::load_from_db(&bob, bob_instance.id)
|
||||
.await?
|
||||
@@ -1643,8 +1638,7 @@ sth_for_the = "future""#
|
||||
);
|
||||
|
||||
// Bob receives all messages
|
||||
bob.recv_msg(sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
bob.recv_msg(sent2).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
||||
@@ -1664,8 +1658,7 @@ sth_for_the = "future""#
|
||||
|
||||
// Alice has a second device and also receives the info message there
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
alice2.recv_msg(sent1).await;
|
||||
let alice2_instance = alice2.get_last_msg().await;
|
||||
let alice2_instance = alice2.recv_msg(sent1).await;
|
||||
let alice2_chat_id = alice2_instance.chat_id;
|
||||
alice2.recv_msg(sent2).await;
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 2);
|
||||
@@ -1721,8 +1714,7 @@ sth_for_the = "future""#
|
||||
assert!(update_msg.get_showpadlock());
|
||||
|
||||
// Bob receives instance+update
|
||||
bob.recv_msg(sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
bob.recv_msg(sent2).await;
|
||||
assert!(bob_instance.get_showpadlock());
|
||||
|
||||
@@ -1786,14 +1778,12 @@ sth_for_the = "future""#
|
||||
|
||||
// Bob receives that instance
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent1).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
let bob_instance = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_instance.get_text(), Some("user added text".to_string()));
|
||||
|
||||
// Alice's second device receives the instance as well
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
alice2.recv_msg(&sent1).await;
|
||||
let alice2_instance = alice2.get_last_msg().await;
|
||||
let alice2_instance = alice2.recv_msg(&sent1).await;
|
||||
assert_eq!(
|
||||
alice2_instance.get_text(),
|
||||
Some("user added text".to_string())
|
||||
|
||||
Reference in New Issue
Block a user