Compare commits

..

9 Commits

Author SHA1 Message Date
jikstra
41a87a0626 Use sql migration over direct tables.sql modification 2022-12-01 15:15:04 +01:00
jikstra
d5faf75f56 Fix test_encryption_modus() 2022-12-01 13:50:20 +01:00
jikstra
e3ff786eb7 Add {get, set}_chat_encryption_mods() methdos to jsonrpc 2022-11-29 23:37:30 +01:00
jikstra
540cc8b205 Fix warning of unused result 2022-11-29 23:34:46 +01:00
jikstra
7533f863d1 - Don't pass EncryptionModus by reference
- rename get_encryption_modus() to encryption_modus()
2022-11-18 03:06:40 +01:00
jikstra
bc4eea0c28 cargo fmt 2022-11-18 02:44:19 +01:00
jikstra
d8844a0524 Propagate encryption_mode from Chat to Message 2022-11-18 02:44:02 +01:00
jikstra
a714a4c2da - Add encryption_modus field for msgs table and expose getters/setters on
Message
- Mind the encryption modus in RenderedEmail
2022-11-18 01:26:08 +01:00
jikstra
cb03e93570 Start working on encryption_modus 2022-11-17 22:32:16 +01:00
94 changed files with 3183 additions and 2897 deletions

View File

@@ -17,7 +17,7 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
@@ -26,12 +26,15 @@ jobs:
- run: rustup component add rustfmt
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v1
- run: cargo fmt --all -- --check
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -51,7 +54,7 @@ jobs:
RUSTDOCFLAGS: -Dwarnings
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Install rust stable toolchain
uses: actions-rs/toolchain@v1
with:
@@ -62,7 +65,10 @@ jobs:
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v1
- name: Rustdoc
run: cargo doc --document-private-items --no-deps
uses: actions-rs/cargo@v1
with:
command: doc
args: --document-private-items --no-deps
build_and_test:
name: Build and test
@@ -71,19 +77,19 @@ jobs:
include:
# Currently used Rust version, same as in `rust-toolchain` file.
- os: ubuntu-latest
rust: 1.65.0
rust: 1.61.0
python: 3.9
- os: windows-latest
rust: 1.65.0
rust: 1.61.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.61.0
# Minimum Supported Rust Version = 1.57.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.61.0
rust: 1.57.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:
@@ -99,13 +105,21 @@ jobs:
uses: swatinem/rust-cache@v1
- name: check
run: cargo check --all --bins --examples --tests --features repl --benches
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests --features repl --benches
- name: tests
run: cargo test --all
uses: actions-rs/cargo@v1
with:
command: test
args: --all
- name: test cargo vendor
run: cargo vendor
uses: actions-rs/cargo@v1
with:
command: vendor
- name: install python
if: ${{ matrix.python }}
@@ -119,7 +133,10 @@ jobs:
- name: build C library
if: ${{ matrix.python }}
run: cargo build -p deltachat_ffi --features jsonrpc
uses: actions-rs/cargo@v1
with:
command: build
args: -p deltachat_ffi --features jsonrpc
- name: run python tests
if: ${{ matrix.python }}
@@ -130,21 +147,6 @@ jobs:
working-directory: python
run: tox -e lint,mypy,doc,py3
- name: build deltachat-rpc-server
if: ${{ matrix.python }}
run: cargo build -p deltachat-rpc-server
- name: add deltachat-rpc-server to path
if: ${{ matrix.python }}
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
- name: run deltachat-rpc-client tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
working-directory: deltachat-rpc-client
run: tox -e py3
- name: install pypy
if: ${{ matrix.python }}
uses: actions/setup-python@v4

View File

@@ -15,8 +15,8 @@ jobs:
- name: install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: get tag
@@ -46,12 +46,12 @@ jobs:
shell: bash
run: |
cd deltachat-jsonrpc/typescript
npm run build
npm run build:tsc
npm pack .
ls -lah
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v1
with:
name: deltachat-jsonrpc-client.tgz
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}

View File

@@ -14,9 +14,9 @@ jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v3
uses: actions/setup-node@v1
with:
node-version: 16.x
- uses: actions-rs/toolchain@v1

View File

@@ -15,7 +15,7 @@ jobs:
id: getid
run: |
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
echo ::set-output name=prid::$PULLREQUEST_ID
- name: Renaming
run: |
# create empty file to copy it over the outdated deliverable on download.delta.chat

View File

@@ -9,10 +9,10 @@ jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v1
- name: Use Node.js 16.x
uses: actions/setup-node@v3
uses: actions/setup-node@v1
with:
node-version: 16.x

View File

@@ -16,8 +16,8 @@ jobs:
os: [ubuntu-18.04, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: System info
@@ -29,7 +29,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -37,7 +37,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: |
~/.cargo/registry/
@@ -58,7 +58,7 @@ jobs:
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
- name: Upload Prebuild
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v1
with:
name: ${{ matrix.os }}
path: node/${{ matrix.os }}.tar.gz
@@ -71,7 +71,7 @@ jobs:
- name: install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
@@ -134,7 +134,7 @@ jobs:
ls -lah
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v1
with:
name: deltachat-node.tgz
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}

View File

@@ -16,8 +16,8 @@ jobs:
os: [ubuntu-18.04, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: System info
@@ -29,7 +29,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -37,7 +37,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: |
~/.cargo/registry/

View File

@@ -11,7 +11,7 @@ jobs:
name: Build REPL example
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@master
- name: Install Rust
uses: actions-rs/toolchain@v1
@@ -20,10 +20,13 @@ jobs:
override: true
- name: build
run: cargo build --example repl --features repl,vendored
uses: actions-rs/cargo@v1
with:
command: build
args: --example repl --features repl,vendored
- name: Upload binary
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: repl.exe
path: 'target/debug/examples/repl.exe'

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: Build the documentation with cargo
run: |

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: Build the documentation with cargo
run: |

2
.gitignore vendored
View File

@@ -12,8 +12,8 @@ include
*.db
*.db-blobs
.tox
python/.eggs
python/.tox
*.egg-info
__pycache__
python/src/deltachat/capi*.so

View File

@@ -6,57 +6,9 @@
### API-Changes
### Fixes
## 1.103.0
### Changes
- Disable Autocrypt & Authres-checking for mailing lists,
because they don't work well with mailing lists #3765
- Refactor: Remove the remaining AsRef<str> #3669
- Add more logging to `fetch_many_msgs` and refactor it #3811
- Small speedup #3780
- Log the reason when the message cannot be sent to the chat #3810
- Add IMAP server ID line to the context info only when it is known #3814
- Remove autogenerated typescript files #3815
- Move functions that require an IMAP session from `Imap` to `Session`
to reduce the number of code paths where IMAP session may not exist.
Drop connection on error instead of trying to disconnect,
potentially preventing IMAP task from getting stuck. #3812
### API-Changes
- Add Python API to send reactions #3762
- jsonrpc: add message errors to MessageObject #3788
- jsonrpc: Add async Python client #3734
### Fixes
- Make sure malformed messsages will never block receiving further messages anymore #3771
- strip leading/trailing whitespace from "Chat-Group-Name{,-Changed}:" headers content #3650
- Assume all Thunderbird users prefer encryption #3774
- refactor peerstate handling to ensure no duplicate peerstates #3776
- Fetch messages in order of their INTERNALDATE (fixes reactions for Gmail f.e.) #3789
- python: do not pass NULL to ffi.gc if the context can't be created #3818
- Add read/write timeouts to IMAP sockets #3820
- Add connection timeout to IMAP sockets #3828
- Disable read timeout during IMAP IDLE #3826
- Bots automatically accept mailing lists #3831
## 1.102.0
### Changes
- If an email has multiple From addresses, handle this as if there was
no From address, to prevent from forgery attacks. Also, improve
handling of emails with invalid From addresses in general #3667
### API-Changes
### Fixes
- fix detection of "All mail", "Trash", "Junk" etc folders. #3760
- fetch messages sequentially to fix reactions on partially downloaded messages #3688
- Fix a bug where one malformed message blocked receiving any further messages #3769
## 1.101.0

441
Cargo.lock generated
View File

@@ -23,6 +23,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "aes"
version = "0.8.2"
@@ -69,12 +75,6 @@ dependencies = [
"libc",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "ansi_term"
version = "0.12.1"
@@ -98,9 +98,9 @@ checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
[[package]]
name = "async-channel"
version = "1.8.0"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28"
dependencies = [
"concurrent-queue",
"event-listener",
@@ -123,7 +123,7 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.6.0"
source = "git+https://github.com/async-email/async-imap?branch=master#85ff7a3d9d71a3715354fabf2fc1a8d047b5710e"
source = "git+https://github.com/async-email/async-imap?branch=master#8755b666fcd8991ed4d09864b67aa88a1eb6934f"
dependencies = [
"async-channel",
"async-native-tls",
@@ -224,7 +224,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"hermit-abi",
"libc",
"winapi",
]
@@ -242,7 +242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43"
dependencies = [
"async-trait",
"axum-core 0.2.9",
"axum-core",
"base64 0.13.1",
"bitflags",
"bytes",
@@ -250,8 +250,8 @@ dependencies = [
"http",
"http-body",
"hyper",
"itoa",
"matchit 0.5.0",
"itoa 1.0.3",
"matchit",
"memchr",
"mime",
"percent-encoding",
@@ -269,42 +269,6 @@ dependencies = [
"tower-service",
]
[[package]]
name = "axum"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08b108ad2665fa3f6e6a517c3d80ec3e77d224c47d605167aefaa5d7ef97fa48"
dependencies = [
"async-trait",
"axum-core 0.3.0",
"base64 0.13.1",
"bitflags",
"bytes",
"futures-util",
"http",
"http-body",
"hyper",
"itoa",
"matchit 0.7.0",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sha-1",
"sync_wrapper",
"tokio",
"tokio-tungstenite",
"tower",
"tower-http",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.2.9"
@@ -321,23 +285,6 @@ dependencies = [
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79b8558f5a0581152dc94dcd289132a1d377494bdeafcd41869b3258e3e2ad92"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"mime",
"rustversion",
"tower-layer",
"tower-service",
]
[[package]]
name = "backtrace"
version = "0.3.66"
@@ -348,7 +295,7 @@ dependencies = [
"cc",
"cfg-if",
"libc",
"miniz_oxide 0.5.3",
"miniz_oxide",
"object",
"rustc-demangle",
]
@@ -426,6 +373,18 @@ dependencies = [
"cipher",
]
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "buf_redux"
version = "0.8.4"
@@ -476,6 +435,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]]
name = "cache-padded"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
[[package]]
name = "cast"
version = "0.3.0"
@@ -537,33 +502,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "ciborium"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369"
[[package]]
name = "ciborium-ll"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "cipher"
version = "0.4.3"
@@ -576,23 +514,13 @@ dependencies = [
[[package]]
name = "clap"
version = "3.2.23"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"bitflags",
"clap_lex",
"indexmap",
"textwrap",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
"textwrap 0.11.0",
"unicode-width",
]
[[package]]
@@ -614,11 +542,11 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "concurrent-queue"
version = "2.0.0"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b"
checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c"
dependencies = [
"crossbeam-utils",
"cache-padded",
]
[[package]]
@@ -675,16 +603,15 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.4.0"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f"
dependencies = [
"anes",
"atty",
"cast",
"ciborium",
"clap",
"criterion-plot",
"csv",
"futures",
"itertools",
"lazy_static",
@@ -694,6 +621,7 @@ dependencies = [
"rayon",
"regex",
"serde",
"serde_cbor",
"serde_derive",
"serde_json",
"tinytemplate",
@@ -703,9 +631,9 @@ dependencies = [
[[package]]
name = "criterion-plot"
version = "0.5.0"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876"
dependencies = [
"cast",
"itertools",
@@ -776,6 +704,28 @@ dependencies = [
"typenum",
]
[[package]]
name = "csv"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
dependencies = [
"bstr",
"csv-core",
"itoa 0.4.8",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
dependencies = [
"memchr",
]
[[package]]
name = "curve25519-dalek"
version = "3.2.0"
@@ -865,9 +815,18 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]]
name = "deflate"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f"
dependencies = [
"adler32",
]
[[package]]
name = "deltachat"
version = "1.103.0"
version = "1.101.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -926,27 +885,26 @@ dependencies = [
"strum_macros",
"tagger",
"tempfile",
"textwrap",
"textwrap 0.16.0",
"thiserror",
"tokio",
"tokio-io-timeout",
"tokio-stream",
"tokio-tar",
"toml",
"trust-dns-resolver",
"url",
"uuid 1.2.2",
"uuid 1.2.1",
]
[[package]]
name = "deltachat-jsonrpc"
version = "1.103.0"
version = "1.101.0"
dependencies = [
"anyhow",
"async-channel",
"axum 0.6.1",
"axum",
"deltachat",
"env_logger 0.10.0",
"env_logger 0.9.1",
"futures",
"log",
"num-traits",
@@ -962,11 +920,11 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.103.0"
version = "1.101.0"
dependencies = [
"anyhow",
"deltachat-jsonrpc",
"env_logger 0.10.0",
"env_logger 0.9.1",
"futures-lite",
"log",
"serde",
@@ -985,7 +943,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.103.0"
version = "1.101.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1282,12 +1240,12 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.10.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272"
dependencies = [
"atty",
"humantime 2.1.0",
"is-terminal",
"log",
"regex",
"termcolor",
@@ -1389,7 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11dcc7e4d79a8c89b9ab4c6f5c30b1fc4a83c420792da3542fd31179ed5f517"
dependencies = [
"cfg-if",
"rustix 0.35.7",
"rustix",
"windows-sys 0.36.1",
]
@@ -1412,7 +1370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
dependencies = [
"crc32fast",
"miniz_oxide 0.5.3",
"miniz_oxide",
]
[[package]]
@@ -1664,15 +1622,6 @@ dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.4.3"
@@ -1698,7 +1647,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [
"bytes",
"fnv",
"itoa",
"itoa 1.0.3",
]
[[package]]
@@ -1747,12 +1696,9 @@ dependencies = [
[[package]]
name = "humansize"
version = "2.1.2"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756"
dependencies = [
"libm",
]
checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
[[package]]
name = "humantime"
@@ -1784,7 +1730,7 @@ dependencies = [
"http-body",
"httparse",
"httpdate",
"itoa",
"itoa 1.0.3",
"pin-project-lite",
"socket2",
"tokio",
@@ -1848,9 +1794,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.5"
version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945"
checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c"
dependencies = [
"bytemuck",
"byteorder",
@@ -1905,16 +1851,6 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24c3f4eff5495aee4c0399d7b6a0dc2b6e81be84242ffbfcf253ebacccc1d0cb"
[[package]]
name = "io-lifetimes"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
dependencies = [
"libc",
"windows-sys 0.42.0",
]
[[package]]
name = "ipconfig"
version = "0.3.0"
@@ -1933,18 +1869,6 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
[[package]]
name = "is-terminal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330"
dependencies = [
"hermit-abi 0.2.6",
"io-lifetimes 1.0.3",
"rustix 0.36.4",
"windows-sys 0.42.0",
]
[[package]]
name = "itertools"
version = "0.10.3"
@@ -1954,6 +1878,12 @@ dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoa"
version = "1.0.3"
@@ -1962,9 +1892,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
[[package]]
name = "jpeg-decoder"
version = "0.3.0"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b"
[[package]]
name = "js-sys"
@@ -2059,12 +1989,6 @@ version = "0.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d"
[[package]]
name = "linux-raw-sys"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f"
[[package]]
name = "lock_api"
version = "0.4.7"
@@ -2122,12 +2046,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"
[[package]]
name = "matchit"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40"
[[package]]
name = "md-5"
version = "0.10.5"
@@ -2173,15 +2091,6 @@ dependencies = [
"adler",
]
[[package]]
name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.5"
@@ -2331,11 +2240,11 @@ dependencies = [
[[package]]
name = "num_cpus"
version = "1.14.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [
"hermit-abi 0.1.19",
"hermit-abi",
"libc",
]
@@ -2421,12 +2330,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "os_str_bytes"
version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]]
name = "os_type"
version = "2.4.0"
@@ -2638,14 +2541,14 @@ dependencies = [
[[package]]
name = "png"
version = "0.17.7"
version = "0.17.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba"
dependencies = [
"bitflags",
"crc32fast",
"flate2",
"miniz_oxide 0.6.2",
"deflate",
"miniz_oxide",
]
[[package]]
@@ -2913,15 +2816,21 @@ dependencies = [
[[package]]
name = "regex"
version = "1.7.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "regex-syntax"
version = "0.6.27"
@@ -2939,9 +2848,9 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.11.13"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
dependencies = [
"base64 0.13.1",
"bytes",
@@ -3049,26 +2958,12 @@ checksum = "d51cc38aa10f6bbb377ed28197aa052aa4e2b762c22be9d3153d01822587e787"
dependencies = [
"bitflags",
"errno",
"io-lifetimes 0.7.2",
"io-lifetimes",
"libc",
"linux-raw-sys 0.0.46",
"linux-raw-sys",
"windows-sys 0.36.1",
]
[[package]]
name = "rustix"
version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23"
dependencies = [
"bitflags",
"errno",
"io-lifetimes 1.0.3",
"libc",
"linux-raw-sys 0.1.3",
"windows-sys 0.42.0",
]
[[package]]
name = "rustversion"
version = "1.0.9"
@@ -3179,18 +3074,28 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.148"
version = "1.0.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.148"
name = "serde_cbor"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
dependencies = [
"half",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
dependencies = [
"proc-macro2",
"quote",
@@ -3199,24 +3104,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.89"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
dependencies = [
"itoa",
"itoa 1.0.3",
"ryu",
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "184c643044780f7ceb59104cef98a5a6f12cb2288a7bc701ab93a362b49fd47d"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -3224,16 +3120,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"itoa 1.0.3",
"ryu",
"serde",
]
[[package]]
name = "sha-1"
version = "0.10.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -3408,9 +3304,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.105"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
dependencies = [
"proc-macro2",
"quote",
@@ -3437,9 +3333,9 @@ dependencies = [
[[package]]
name = "tagger"
version = "4.3.4"
version = "4.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aaa6f5d645d1dae4cd0286e9f8bf15b75a31656348e5e106eb1a940abd34b63"
checksum = "77dd78fc7dd20ba3a13620ec231cef9e73ea5c7ba162f6c4e05b1d521e04b221"
[[package]]
name = "tempfile"
@@ -3464,6 +3360,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.16.0"
@@ -3533,9 +3438,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.22.0"
version = "1.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
dependencies = [
"autocfg",
"bytes",
@@ -3551,16 +3456,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "tokio-io-timeout"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
dependencies = [
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-macros"
version = "1.8.0"
@@ -3680,9 +3575,9 @@ dependencies = [
[[package]]
name = "tower-layer"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62"
[[package]]
name = "tower-service"
@@ -3810,9 +3705,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "typescript-type-def"
version = "0.5.5"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "947b91d2fe9ec02a6749b8b645541f16f527e2ea88a60b3f774eca26fd657325"
checksum = "1781793d51f116db5bb614f42b42aef2f3fdebe8b2f5f7d00254ed6bb14b0c69"
dependencies = [
"serde_json",
"typescript-type-def-derive",
@@ -3820,9 +3715,9 @@ dependencies = [
[[package]]
name = "typescript-type-def-derive"
version = "0.5.5"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c1bfe689e4067733530495b04959b00f05cd95f038bed59af4fc70b3e26240"
checksum = "3ac1df09a36962cc2b9404e7fd78cc58c060ecb216c24d666de932e16ff6e539"
dependencies = [
"darling 0.13.4",
"ident_case",
@@ -3915,9 +3810,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.2.2"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
dependencies = [
"getrandom 0.2.7",
"serde",
@@ -4253,7 +4148,7 @@ dependencies = [
"async-channel",
"async-mutex",
"async-trait",
"axum 0.5.17",
"axum",
"futures",
"futures-util",
"log",

View File

@@ -1,10 +1,10 @@
[package]
name = "deltachat"
version = "1.103.0"
version = "1.101.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
rust-version = "1.61"
rust-version = "1.57"
[profile.dev]
debug = 0
@@ -39,14 +39,14 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch =
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.24.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
log = {version = "0.4.16", optional = true }
mailparse = "0.13"
native-tls = "0.2"
num_cpus = "1.14"
num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.16.0"
@@ -57,7 +57,7 @@ quick-xml = "0.23"
r2d2 = "0.8"
r2d2_sqlite = "0.20"
rand = "0.8"
regex = "1.7"
regex = "1.6"
rusqlite = { version = "0.27", features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustyline = { version = "10", optional = true }
@@ -74,20 +74,19 @@ toml = "0.5"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
fast-socks5 = "0.8"
humansize = "2"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "4.3.4"
tagger = "4.3.3"
textwrap = "0.16.0"
async-channel = "1.8.0"
async-channel = "1.6.1"
futures-lite = "1.12.0"
tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-io-timeout = "1.2.0"
reqwest = { version = "0.11.13", features = ["json"] }
reqwest = { version = "0.11.12", features = ["json"] }
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"
criterion = { version = "0.4.0", features = ["async_tokio"] }
criterion = { version = "0.3.6", features = ["async_tokio"] }
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"

View File

@@ -120,15 +120,6 @@ $ cargo test -- --ignored
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
- `nightly`: Enable nightly only performance and security related features.
## Update Provider Data
To add the updates from the
[provider-db](https://github.com/deltachat/provider-db) to the core, run:
```
./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
```
## Language bindings and frontend projects
Language bindings are available for:

View File

@@ -38,64 +38,11 @@ Hello {i}",
context
}
/// Receive 100 emails that remove charlie@example.com and add
/// him back
async fn recv_groupmembership_emails(context: Context) -> Context {
for i in 0..50 {
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
Chat-Group-Member-Added: charlie@example.com
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
.unwrap();
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
Chat-Group-Member-Removed: charlie@example.com
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
.unwrap();
}
context
}
async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new(dbfile.as_path(), id, Events::new(), StockStrings::new())
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
.await
.unwrap();
@@ -105,7 +52,7 @@ async fn create_context() -> Context {
if backup.exists() {
println!("Importing backup");
imex(&context, ImexMode::ImportBackup, backup.as_path(), None)
imex(&context, ImexMode::ImportBackup, &backup, None)
.await
.unwrap();
}
@@ -136,20 +83,6 @@ fn criterion_benchmark(c: &mut Criterion) {
}
});
});
group.bench_function(
"Receive 100 Chat-Group-Member-{Added|Removed} messages",
|b| {
let rt = tokio::runtime::Runtime::new().unwrap();
let context = rt.block_on(create_context());
b.to_async(&rt).iter(|| {
let ctx = context.clone();
async move {
recv_groupmembership_emails(black_box(ctx)).await;
}
});
},
);
group.finish();
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.103.0"
version = "1.101.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"

View File

@@ -1659,7 +1659,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
let ctx = &*context;
block_on(async move {
chat::set_chat_profile_image(ctx, ChatId::new(chat_id), &to_string_lossy(image))
chat::set_chat_profile_image(ctx, ChatId::new(chat_id), to_string_lossy(image))
.await
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set profile image")

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.103.0"
version = "1.101.0"
description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
@@ -19,21 +19,21 @@ num-traits = "0.2"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.8.0" }
async-channel = { version = "1.6.1" }
futures = { version = "0.3.25" }
serde_json = "1.0.89"
serde_json = "1.0.87"
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.22.0" }
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
tokio = { version = "1.21.2" }
sanitize-filename = "0.4"
walkdir = "2.3.2"
# optional dependencies
axum = { version = "0.6.1", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
axum = { version = "0.5.17", optional = true, features = ["ws"] }
env_logger = { version = "0.9.1", optional = true }
[dev-dependencies]
tokio = { version = "1.22.0", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.21.2", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -47,7 +47,7 @@ use types::provider_info::ProviderInfo;
use types::webxdc::WebxdcMessageInfo;
use self::types::{
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
chat::{BasicChat, JSONRPCChatVisibility, JSONRPCEncryptionModus, MuteDuration},
location::JsonrpcLocation,
message::{
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
@@ -528,6 +528,8 @@ impl CommandApi {
ChatId::new(chat_id).get_encryption_info(&ctx).await
}
/// Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
/// The QR code is compatible to the OPENPGP4FPR format
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
@@ -557,6 +559,32 @@ impl CommandApi {
))
}
async fn set_chat_encryption_modus(
&self,
account_id: u32,
chat_id: u32,
encryption_modus: JSONRPCEncryptionModus
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let chat = ChatId::new(chat_id);
Ok(chat.set_encryption_modus(&ctx, encryption_modus.into_core_type()).await?)
}
async fn get_chat_encryption_modus(
&self,
account_id: u32,
chat_id: u32
) -> Result<Option<JSONRPCEncryptionModus>> {
let ctx = self.get_context(account_id).await?;
let chat = ChatId::new(chat_id);
Ok(
match chat.encryption_modus(&ctx).await? {
Some(encryption_modus) => Some(JSONRPCEncryptionModus::from_core_type(encryption_modus)),
None => None,
}
)
}
/// Continue a Setup-Contact or Verified-Group-Invite protocol
/// started on another device with `get_chat_securejoin_qr_code_svg()`.
/// This function is typically called when `check_qr()` returns
@@ -733,7 +761,7 @@ impl CommandApi {
image_path: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), &image_path.unwrap_or_default())
chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), image_path.unwrap_or_default())
.await
}

View File

@@ -2,7 +2,7 @@ use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, Result};
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId};
use deltachat::chat::{Chat, ChatId, EncryptionModus};
use deltachat::constants::Chattype;
use deltachat::contact::{Contact, ContactId};
use deltachat::context::Context;
@@ -211,3 +211,33 @@ impl JSONRPCChatVisibility {
}
}
}
#[derive(Clone, Serialize, Deserialize, TypeDef)]
#[serde(rename = "EncryptionModus")]
pub enum JSONRPCEncryptionModus {
Opportunistic = 0,
ForcePlaintext = 1,
ForceEncrypted = 2,
ForceVerified = 3,
}
impl JSONRPCEncryptionModus {
pub fn into_core_type(self) -> EncryptionModus {
match self {
JSONRPCEncryptionModus::Opportunistic => EncryptionModus::Opportunistic,
JSONRPCEncryptionModus::ForcePlaintext => EncryptionModus::ForcePlaintext,
JSONRPCEncryptionModus::ForceEncrypted => EncryptionModus::ForceEncrypted,
JSONRPCEncryptionModus::ForceVerified => EncryptionModus::ForceVerified
}
}
pub fn from_core_type(core_encryption_modus: EncryptionModus) -> Self {
match core_encryption_modus {
EncryptionModus::Opportunistic => JSONRPCEncryptionModus::Opportunistic,
EncryptionModus::ForcePlaintext => JSONRPCEncryptionModus::ForcePlaintext,
EncryptionModus::ForceEncrypted => JSONRPCEncryptionModus::ForceEncrypted,
EncryptionModus::ForceVerified => JSONRPCEncryptionModus::ForceVerified
}
}
}

View File

@@ -34,9 +34,6 @@ pub struct MessageObject {
view_type: MessageViewtype,
state: u32,
/// An error text, if there is one.
error: Option<String>,
timestamp: i64,
sort_timestamp: i64,
received_timestamp: i64,
@@ -170,7 +167,6 @@ impl MessageObject {
.get_state()
.to_u32()
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
error: message.error(),
timestamp: message.get_timestamp(),
sort_timestamp: message.get_sort_timestamp(),

View File

@@ -6,4 +6,3 @@ yarn.lock
package-lock.json
docs
accounts
generated

View File

@@ -0,0 +1,958 @@
// AUTO-GENERATED by yerpc-derive
import * as T from "./types.js"
import * as RPC from "./jsonrpc.js"
type RequestMethod = (method: string, params?: RPC.Params) => Promise<unknown>;
type NotificationMethod = (method: string, params?: RPC.Params) => void;
interface Transport {
request: RequestMethod,
notification: NotificationMethod
}
export class RawClient {
constructor(private _transport: Transport) {}
/**
* Check if an email address is valid.
*/
public checkEmailValidity(email: string): Promise<boolean> {
return (this._transport.request('check_email_validity', [email] as RPC.Params)) as Promise<boolean>;
}
/**
* Get general system info.
*/
public getSystemInfo(): Promise<Record<string,string>> {
return (this._transport.request('get_system_info', [] as RPC.Params)) as Promise<Record<string,string>>;
}
public addAccount(): Promise<T.U32> {
return (this._transport.request('add_account', [] as RPC.Params)) as Promise<T.U32>;
}
public removeAccount(accountId: T.U32): Promise<null> {
return (this._transport.request('remove_account', [accountId] as RPC.Params)) as Promise<null>;
}
public getAllAccountIds(): Promise<(T.U32)[]> {
return (this._transport.request('get_all_account_ids', [] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Select account id for internally selected state.
* TODO: Likely this is deprecated as all methods take an account id now.
*/
public selectAccount(id: T.U32): Promise<null> {
return (this._transport.request('select_account', [id] as RPC.Params)) as Promise<null>;
}
/**
* Get the selected account id of the internal state..
* TODO: Likely this is deprecated as all methods take an account id now.
*/
public getSelectedAccountId(): Promise<(T.U32|null)> {
return (this._transport.request('get_selected_account_id', [] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* Get a list of all configured accounts.
*/
public getAllAccounts(): Promise<(T.Account)[]> {
return (this._transport.request('get_all_accounts', [] as RPC.Params)) as Promise<(T.Account)[]>;
}
public startIoForAllAccounts(): Promise<null> {
return (this._transport.request('start_io_for_all_accounts', [] as RPC.Params)) as Promise<null>;
}
public stopIoForAllAccounts(): Promise<null> {
return (this._transport.request('stop_io_for_all_accounts', [] as RPC.Params)) as Promise<null>;
}
public startIo(id: T.U32): Promise<null> {
return (this._transport.request('start_io', [id] as RPC.Params)) as Promise<null>;
}
public stopIo(id: T.U32): Promise<null> {
return (this._transport.request('stop_io', [id] as RPC.Params)) as Promise<null>;
}
/**
* Get top-level info for an account.
*/
public getAccountInfo(accountId: T.U32): Promise<T.Account> {
return (this._transport.request('get_account_info', [accountId] as RPC.Params)) as Promise<T.Account>;
}
/**
* Get the combined filesize of an account in bytes
*/
public getAccountFileSize(accountId: T.U32): Promise<T.U64> {
return (this._transport.request('get_account_file_size', [accountId] as RPC.Params)) as Promise<T.U64>;
}
/**
* Returns provider for the given domain.
*
* This function looks up domain in offline database.
*
* For compatibility, email address can be passed to this function
* instead of the domain.
*/
public getProviderInfo(accountId: T.U32, email: string): Promise<(T.ProviderInfo|null)> {
return (this._transport.request('get_provider_info', [accountId, email] as RPC.Params)) as Promise<(T.ProviderInfo|null)>;
}
/**
* Checks if the context is already configured.
*/
public isConfigured(accountId: T.U32): Promise<boolean> {
return (this._transport.request('is_configured', [accountId] as RPC.Params)) as Promise<boolean>;
}
/**
* Get system info for an account.
*/
public getInfo(accountId: T.U32): Promise<Record<string,string>> {
return (this._transport.request('get_info', [accountId] as RPC.Params)) as Promise<Record<string,string>>;
}
public setConfig(accountId: T.U32, key: string, value: (string|null)): Promise<null> {
return (this._transport.request('set_config', [accountId, key, value] as RPC.Params)) as Promise<null>;
}
public batchSetConfig(accountId: T.U32, config: Record<string,(string|null)>): Promise<null> {
return (this._transport.request('batch_set_config', [accountId, config] as RPC.Params)) as Promise<null>;
}
/**
* Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
* Before this function is called, `checkQr()` should confirm the type of the
* QR code is `account` or `webrtcInstance`.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
*/
public setConfigFromQr(accountId: T.U32, qrContent: string): Promise<null> {
return (this._transport.request('set_config_from_qr', [accountId, qrContent] as RPC.Params)) as Promise<null>;
}
public checkQr(accountId: T.U32, qrContent: string): Promise<T.Qr> {
return (this._transport.request('check_qr', [accountId, qrContent] as RPC.Params)) as Promise<T.Qr>;
}
public getConfig(accountId: T.U32, key: string): Promise<(string|null)> {
return (this._transport.request('get_config', [accountId, key] as RPC.Params)) as Promise<(string|null)>;
}
public batchGetConfig(accountId: T.U32, keys: (string)[]): Promise<Record<string,(string|null)>> {
return (this._transport.request('batch_get_config', [accountId, keys] as RPC.Params)) as Promise<Record<string,(string|null)>>;
}
public setStockStrings(strings: Record<T.U32,string>): Promise<null> {
return (this._transport.request('set_stock_strings', [strings] as RPC.Params)) as Promise<null>;
}
/**
* Configures this account with the currently set parameters.
* Setup the credential config before calling this.
*/
public configure(accountId: T.U32): Promise<null> {
return (this._transport.request('configure', [accountId] as RPC.Params)) as Promise<null>;
}
/**
* Signal an ongoing process to stop.
*/
public stopOngoingProcess(accountId: T.U32): Promise<null> {
return (this._transport.request('stop_ongoing_process', [accountId] as RPC.Params)) as Promise<null>;
}
public exportSelfKeys(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('export_self_keys', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
}
public importSelfKeys(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('import_self_keys', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
}
/**
* Returns the message IDs of all _fresh_ messages of any chat.
* Typically used for implementing notification summaries
* or badge counters e.g. on the app icon.
* The list is already sorted and starts with the most recent fresh message.
*
* Messages belonging to muted chats or to the contact requests are not returned;
* these messages should not be notified
* and also badge counters should not include these messages.
*
* To get the number of fresh messages for a single chat, muted or not,
* use `get_fresh_msg_cnt()`.
*/
public getFreshMsgs(accountId: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('get_fresh_msgs', [accountId] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Get the number of _fresh_ messages in a chat.
* Typically used to implement a badge with a number in the chatlist.
*
* If the specified chat is muted,
* the UI should show the badge counter "less obtrusive",
* e.g. using "gray" instead of "red" color.
*/
public getFreshMsgCnt(accountId: T.U32, chatId: T.U32): Promise<T.Usize> {
return (this._transport.request('get_fresh_msg_cnt', [accountId, chatId] as RPC.Params)) as Promise<T.Usize>;
}
/**
* Estimate the number of messages that will be deleted
* by the set_config()-options `delete_device_after` or `delete_server_after`.
* This is typically used to show the estimated impact to the user
* before actually enabling deletion of old messages.
*/
public estimateAutoDeletionCount(accountId: T.U32, fromServer: boolean, seconds: T.I64): Promise<T.Usize> {
return (this._transport.request('estimate_auto_deletion_count', [accountId, fromServer, seconds] as RPC.Params)) as Promise<T.Usize>;
}
public initiateAutocryptKeyTransfer(accountId: T.U32): Promise<string> {
return (this._transport.request('initiate_autocrypt_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
}
public continueAutocryptKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
return (this._transport.request('continue_autocrypt_key_transfer', [accountId, messageId, setupCode] as RPC.Params)) as Promise<null>;
}
public getChatlistEntries(accountId: T.U32, listFlags: (T.U32|null), queryString: (string|null), queryContactId: (T.U32|null)): Promise<(T.ChatListEntry)[]> {
return (this._transport.request('get_chatlist_entries', [accountId, listFlags, queryString, queryContactId] as RPC.Params)) as Promise<(T.ChatListEntry)[]>;
}
public getChatlistItemsByEntries(accountId: T.U32, entries: (T.ChatListEntry)[]): Promise<Record<T.U32,T.ChatListItemFetchResult>> {
return (this._transport.request('get_chatlist_items_by_entries', [accountId, entries] as RPC.Params)) as Promise<Record<T.U32,T.ChatListItemFetchResult>>;
}
public getFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
return (this._transport.request('get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
}
/**
* get basic info about a chat,
* use chatlist_get_full_chat_by_id() instead if you need more information
*/
public getBasicChatInfo(accountId: T.U32, chatId: T.U32): Promise<T.BasicChat> {
return (this._transport.request('get_basic_chat_info', [accountId, chatId] as RPC.Params)) as Promise<T.BasicChat>;
}
public acceptChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('accept_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
public blockChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('block_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Delete a chat.
*
* Messages are deleted from the device and the chat database entry is deleted.
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
*
* Things that are _not done_ implicitly:
*
* - Messages are **not deleted from the server**.
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
* and the user may create the chat again.
* - **Groups are not left** - this would
* be unexpected as (1) deleting a normal chat also does not prevent new mails
* from arriving, (2) leaving a group requires sending a message to
* all group members - especially for groups not used for a longer time, this is
* really unexpected when deletion results in contacting all members again,
* (3) only leaving groups is also a valid usecase.
*
* To leave a chat explicitly, use leave_group()
*/
public deleteChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('delete_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Get encryption info for a chat.
* Get a multi-line encryption info, containing encryption preferences of all members.
* Can be used to find out why messages sent to group are not encrypted.
*
* returns Multi-line text
*/
public getChatEncryptionInfo(accountId: T.U32, chatId: T.U32): Promise<string> {
return (this._transport.request('get_chat_encryption_info', [accountId, chatId] as RPC.Params)) as Promise<string>;
}
/**
* Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
*
* The scanning device will pass the scanned content to `checkQr()` then;
* if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
* an out-of-band-verification can be joined using `secure_join()`
*
* chat_id: If set to a group-chat-id,
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If not set, the Setup-Contact protocol is offered in the QR code.
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* return format: `[code, svg]`
*/
public getChatSecurejoinQrCodeSvg(accountId: T.U32, chatId: (T.U32|null)): Promise<[string,string]> {
return (this._transport.request('get_chat_securejoin_qr_code_svg', [accountId, chatId] as RPC.Params)) as Promise<[string,string]>;
}
public setChatEncryptionModus(accountId: T.U32, chatId: T.U32, encryptionModus: T.EncryptionModus): Promise<null> {
return (this._transport.request('set_chat_encryption_modus', [accountId, chatId, encryptionModus] as RPC.Params)) as Promise<null>;
}
public getChatEncryptionModus(accountId: T.U32, chatId: T.U32): Promise<(T.EncryptionModus|null)> {
return (this._transport.request('get_chat_encryption_modus', [accountId, chatId] as RPC.Params)) as Promise<(T.EncryptionModus|null)>;
}
/**
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with `get_chat_securejoin_qr_code_svg()`.
* This function is typically called when `check_qr()` returns
* type=AskVerifyContact or type=AskVerifyGroup.
*
* The function returns immediately and the handshake runs in background,
* sending and receiving several messages.
* During the handshake, info messages are added to the chat,
* showing progress, success or errors.
*
* Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
*
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* **qr**: The text of the scanned QR code. Typically, the same string as given
* to `check_qr()`.
*
* **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
* A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
*
*/
public secureJoin(accountId: T.U32, qr: string): Promise<T.U32> {
return (this._transport.request('secure_join', [accountId, qr] as RPC.Params)) as Promise<T.U32>;
}
public leaveGroup(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('leave_group', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Remove a member from a group.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public removeContactFromChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('remove_contact_from_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
}
/**
* Add a member to a group.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* If the group has group protection enabled, only verified contacts can be added to the group.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public addContactToChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('add_contact_to_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
}
/**
* Get the contact IDs belonging to a chat.
*
* - for normal chats, the function always returns exactly one contact,
* DC_CONTACT_ID_SELF is returned only for SELF-chats.
*
* - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
* explicitly as it may happen that oneself gets removed from a still existing
* group
*
* - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
*
* - for mailing lists, the behavior is not documented currently, we will decide on that later.
* for now, the UI should not show the list for mailing lists.
* (we do not know all members and there is not always a global mailing list address,
* so we could return only SELF or the known members; this is not decided yet)
*/
public getChatContacts(accountId: T.U32, chatId: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('get_chat_contacts', [accountId, chatId] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Create a new group chat.
*
* After creation,
* the group has one member with the ID DC_CONTACT_ID_SELF
* and is in _unpromoted_ state.
* This means, you can add or remove members, change the name,
* the group image and so on without messages being sent to all group members.
*
* This changes as soon as the first message is sent to the group members
* and the group becomes _promoted_.
* After that, all changes are synced with all group members
* by sending status message.
*
* To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
* This may be useful if you want to show some help for just created groups.
*
* @param protect If set to 1 the function creates group with protection initially enabled.
* Only verified members are allowed in these groups
* and end-to-end-encryption is always enabled.
*/
public createGroupChat(accountId: T.U32, name: string, protect: boolean): Promise<T.U32> {
return (this._transport.request('create_group_chat', [accountId, name, protect] as RPC.Params)) as Promise<T.U32>;
}
/**
* Create a new broadcast list.
*
* Broadcast lists are similar to groups on the sending device,
* however, recipients get the messages in normal one-to-one chats
* and will not be aware of other members.
*
* Replies to broadcasts go only to the sender
* and not to all broadcast recipients.
* Moreover, replies will not appear in the broadcast list
* but in the one-to-one chat with the person answering.
*
* The name and the image of the broadcast list is set automatically
* and is visible to the sender only.
* Not asking for these data allows more focused creation
* and we bypass the question who will get which data.
* Also, many users will have at most one broadcast list
* so, a generic name and image is sufficient at the first place.
*
* Later on, however, the name can be changed using dc_set_chat_name().
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
* All in all, this is also what other messengers are doing here.
*/
public createBroadcastList(accountId: T.U32): Promise<T.U32> {
return (this._transport.request('create_broadcast_list', [accountId] as RPC.Params)) as Promise<T.U32>;
}
/**
* Set group name.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public setChatName(accountId: T.U32, chatId: T.U32, newName: string): Promise<null> {
return (this._transport.request('set_chat_name', [accountId, chatId, newName] as RPC.Params)) as Promise<null>;
}
/**
* Set group profile image.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* To find out the profile image of a chat, use dc_chat_get_profile_image()
*
* @param image_path Full path of the image to use as the group image. The image will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* If you pass null here, the group image is deleted (for promoted groups, all members are informed about
* this change anyway).
*/
public setChatProfileImage(accountId: T.U32, chatId: T.U32, imagePath: (string|null)): Promise<null> {
return (this._transport.request('set_chat_profile_image', [accountId, chatId, imagePath] as RPC.Params)) as Promise<null>;
}
public setChatVisibility(accountId: T.U32, chatId: T.U32, visibility: T.ChatVisibility): Promise<null> {
return (this._transport.request('set_chat_visibility', [accountId, chatId, visibility] as RPC.Params)) as Promise<null>;
}
public setChatEphemeralTimer(accountId: T.U32, chatId: T.U32, timer: T.U32): Promise<null> {
return (this._transport.request('set_chat_ephemeral_timer', [accountId, chatId, timer] as RPC.Params)) as Promise<null>;
}
public getChatEphemeralTimer(accountId: T.U32, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('get_chat_ephemeral_timer', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
}
public addDeviceMessage(accountId: T.U32, label: string, text: string): Promise<T.U32> {
return (this._transport.request('add_device_message', [accountId, label, text] as RPC.Params)) as Promise<T.U32>;
}
/**
* Mark all messages in a chat as _noticed_.
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
* but are still waiting for being marked as "seen" using markseen_msgs()
* (IMAP/MDNs is not done for noticed messages).
*
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
* See also markseen_msgs().
*/
public marknoticedChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('marknoticed_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
public getFirstUnreadMessageOfChat(accountId: T.U32, chatId: T.U32): Promise<(T.U32|null)> {
return (this._transport.request('get_first_unread_message_of_chat', [accountId, chatId] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* Set mute duration of a chat.
*
* The UI can then call is_chat_muted() when receiving a new message
* to decide whether it should trigger an notification.
*
* Muted chats should not sound or vibrate
* and should not show a visual notification in the system area.
* Moreover, muted chats should be excluded from global badge counter
* (get_fresh_msgs() skips muted chats therefore)
* and the in-app, per-chat badge counter should use a less obtrusive color.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*/
public setChatMuteDuration(accountId: T.U32, chatId: T.U32, duration: T.MuteDuration): Promise<null> {
return (this._transport.request('set_chat_mute_duration', [accountId, chatId, duration] as RPC.Params)) as Promise<null>;
}
/**
* Check whether the chat is currently muted (can be changed by set_chat_mute_duration()).
*
* This is available as a standalone function outside of fullchat, because it might be only needed for notification
*/
public isChatMuted(accountId: T.U32, chatId: T.U32): Promise<boolean> {
return (this._transport.request('is_chat_muted', [accountId, chatId] as RPC.Params)) as Promise<boolean>;
}
/**
* Mark messages as presented to the user.
* Typically, UIs call this function on scrolling through the message list,
* when the messages are presented at least for a little moment.
* The concrete action depends on the type of the chat and on the users settings
* (dc_msgs_presented() may be a better name therefore, but well. :)
*
* - For normal chats, the IMAP state is updated, MDN is sent
* (if set_config()-options `mdns_enabled` is set)
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
*
* - For contact requests, no IMAP or MDNs is done
* and the internal state is not changed therefore.
* See also marknoticed_chat().
*
* Moreover, timer is started for incoming ephemeral messages.
* This also happens for contact requests chats.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*/
public markseenMsgs(accountId: T.U32, msgIds: (T.U32)[]): Promise<null> {
return (this._transport.request('markseen_msgs', [accountId, msgIds] as RPC.Params)) as Promise<null>;
}
public getMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
}
public getMessageListItems(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.MessageListItem)[]> {
return (this._transport.request('get_message_list_items', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.MessageListItem)[]>;
}
public getMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
return (this._transport.request('get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
}
public getMessageHtml(accountId: T.U32, messageId: T.U32): Promise<(string|null)> {
return (this._transport.request('get_message_html', [accountId, messageId] as RPC.Params)) as Promise<(string|null)>;
}
public getMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return (this._transport.request('get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
}
/**
* Fetch info desktop needs for creating a notification for a message
*/
public getMessageNotificationInfo(accountId: T.U32, messageId: T.U32): Promise<T.MessageNotificationInfo> {
return (this._transport.request('get_message_notification_info', [accountId, messageId] as RPC.Params)) as Promise<T.MessageNotificationInfo>;
}
/**
* Delete messages. The messages are deleted on the current device and
* on the IMAP server.
*/
public deleteMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<null> {
return (this._transport.request('delete_messages', [accountId, messageIds] as RPC.Params)) as Promise<null>;
}
/**
* Get an informational text for a single message. The text is multiline and may
* contain e.g. the raw text of the message.
*
* The max. text returned is typically longer (about 100000 characters) than the
* max. text returned by dc_msg_get_text() (about 30000 characters).
*/
public getMessageInfo(accountId: T.U32, messageId: T.U32): Promise<string> {
return (this._transport.request('get_message_info', [accountId, messageId] as RPC.Params)) as Promise<string>;
}
/**
* Asks the core to start downloading a message fully.
* This function is typically called when the user hits the "Download" button
* that is shown by the UI in case `download_state` is `'Available'` or `'Failure'`
*
* On success, the @ref DC_MSG "view type of the message" may change
* or the message may be replaced completely by one or more messages with other message IDs.
* That may happen e.g. in cases where the message was encrypted
* and the type could not be determined without fully downloading.
* Downloaded content can be accessed as usual after download.
*
* To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted.
*/
public downloadFullMessage(accountId: T.U32, messageId: T.U32): Promise<null> {
return (this._transport.request('download_full_message', [accountId, messageId] as RPC.Params)) as Promise<null>;
}
/**
* Search messages containing the given query string.
* Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
*
* Global chat results are typically displayed using dc_msg_get_summary(), chat
* search results may just hilite the corresponding messages and present a
* prev/next button.
*
* For global search, result is limited to 1000 messages,
* this allows incremental search done fast.
* So, when getting exactly 1000 results, the result may be truncated;
* the UIs may display sth. as "1000+ messages found" in this case.
* Chat search (if a chat_id is set) is not limited.
*/
public searchMessages(accountId: T.U32, query: string, chatId: (T.U32|null)): Promise<(T.U32)[]> {
return (this._transport.request('search_messages', [accountId, query, chatId] as RPC.Params)) as Promise<(T.U32)[]>;
}
public messageIdsToSearchResults(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.MessageSearchResult>> {
return (this._transport.request('message_ids_to_search_results', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.MessageSearchResult>>;
}
/**
* Get a single contact options by ID.
*/
public getContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
return (this._transport.request('get_contact', [accountId, contactId] as RPC.Params)) as Promise<T.Contact>;
}
/**
* Add a single contact as a result of an explicit user action.
*
* Returns contact id of the created or existing contact
*/
public createContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
return (this._transport.request('create_contact', [accountId, email, name] as RPC.Params)) as Promise<T.U32>;
}
/**
* Returns contact id of the created or existing DM chat with that contact
*/
public createChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
return (this._transport.request('create_chat_by_contact_id', [accountId, contactId] as RPC.Params)) as Promise<T.U32>;
}
public blockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('block_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public unblockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('unblock_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public getBlockedContacts(accountId: T.U32): Promise<(T.Contact)[]> {
return (this._transport.request('get_blocked_contacts', [accountId] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public getContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
return (this._transport.request('get_contact_ids', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Get a list of contacts.
* (formerly called getContacts2 in desktop)
*/
public getContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
return (this._transport.request('get_contacts', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public getContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
return (this._transport.request('get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
}
public deleteContact(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('delete_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public changeContactName(accountId: T.U32, contactId: T.U32, name: string): Promise<null> {
return (this._transport.request('change_contact_name', [accountId, contactId, name] as RPC.Params)) as Promise<null>;
}
/**
* Get encryption info for a contact.
* Get a multi-line encryption info, containing your fingerprint and the
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
*/
public getContactEncryptionInfo(accountId: T.U32, contactId: T.U32): Promise<string> {
return (this._transport.request('get_contact_encryption_info', [accountId, contactId] as RPC.Params)) as Promise<string>;
}
/**
* Check if an e-mail address belongs to a known and unblocked contact.
* To get a list of all known and unblocked contacts, use contacts_get_contacts().
*
* To validate an e-mail address independently of the contact database
* use check_email_validity().
*/
public lookupContactIdByAddr(accountId: T.U32, addr: string): Promise<(T.U32|null)> {
return (this._transport.request('lookup_contact_id_by_addr', [accountId, addr] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* Returns all message IDs of the given types in a chat.
* Typically used to show a gallery.
*
* The list is already sorted and starts with the oldest message.
* Clients should not try to re-sort the list as this would be an expensive action
* and would result in inconsistencies between clients.
*
* Setting `chat_id` to `None` (`null` in typescript) means get messages with media
* from any chat of the currently used account.
*/
public getChatMedia(accountId: T.U32, chatId: (T.U32|null), messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
return (this._transport.request('get_chat_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Search next/previous message based on a given message and a list of types.
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
* one combined call for getting chat::get_next_media for both directions
* the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
*/
public getNeighboringChatMedia(accountId: T.U32, msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
return (this._transport.request('get_neighboring_chat_media', [accountId, msgId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<[(T.U32|null),(T.U32|null)]>;
}
public exportBackup(accountId: T.U32, destination: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('export_backup', [accountId, destination, passphrase] as RPC.Params)) as Promise<null>;
}
public importBackup(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('import_backup', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
}
/**
* Indicate that the network likely has come back.
* or just that the network conditions might have changed
*/
public maybeNetwork(): Promise<null> {
return (this._transport.request('maybe_network', [] as RPC.Params)) as Promise<null>;
}
/**
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
* One of:
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
*
* We don't use exact values but ranges here so that we can split up
* states into multiple states in the future.
*
* Meant as a rough overview that can be shown
* e.g. in the title of the main screen.
*
* If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*/
public getConnectivity(accountId: T.U32): Promise<T.U32> {
return (this._transport.request('get_connectivity', [accountId] as RPC.Params)) as Promise<T.U32>;
}
/**
* Get an overview of the current connectivity, and possibly more statistics.
* Meant to give the user more insight about the current status than
* the basic connectivity info returned by get_connectivity(); show this
* e.g., if the user taps on said basic connectivity info.
*
* If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*
* This comes as an HTML from the core so that we can easily improve it
* and the improvement instantly reaches all UIs.
*/
public getConnectivityHtml(accountId: T.U32): Promise<string> {
return (this._transport.request('get_connectivity_html', [accountId] as RPC.Params)) as Promise<string>;
}
public getLocations(accountId: T.U32, chatId: (T.U32|null), contactId: (T.U32|null), timestampBegin: T.I64, timestampEnd: T.I64): Promise<(T.Location)[]> {
return (this._transport.request('get_locations', [accountId, chatId, contactId, timestampBegin, timestampEnd] as RPC.Params)) as Promise<(T.Location)[]>;
}
public sendWebxdcStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return (this._transport.request('send_webxdc_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
}
public getWebxdcStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
return (this._transport.request('get_webxdc_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
}
/**
* Get info from a webxdc message
*/
public getWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
return (this._transport.request('get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
}
/**
* Forward messages to another chat.
*
* All types of messages can be forwarded,
* however, they will be flagged as such (dc_msg_is_forwarded() is set).
*
* Original sender, info-state and webxdc updates are not forwarded on purpose.
*/
public forwardMessages(accountId: T.U32, messageIds: (T.U32)[], chatId: T.U32): Promise<null> {
return (this._transport.request('forward_messages', [accountId, messageIds, chatId] as RPC.Params)) as Promise<null>;
}
public sendSticker(accountId: T.U32, chatId: T.U32, stickerPath: string): Promise<T.U32> {
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.U32>;
}
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*/
public sendReaction(accountId: T.U32, messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
return (this._transport.request('send_reaction', [accountId, messageId, reaction] as RPC.Params)) as Promise<T.U32>;
}
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Get draft for a chat, if any.
*/
public getDraft(accountId: T.U32, chatId: T.U32): Promise<(T.Message|null)> {
return (this._transport.request('get_draft', [accountId, chatId] as RPC.Params)) as Promise<(T.Message|null)>;
}
public sendVideochatInvitation(accountId: T.U32, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
}
public miscGetStickerFolder(accountId: T.U32): Promise<string> {
return (this._transport.request('misc_get_sticker_folder', [accountId] as RPC.Params)) as Promise<string>;
}
/**
* save a sticker to a collection/folder in the account's sticker folder
*/
public miscSaveSticker(accountId: T.U32, msgId: T.U32, collection: string): Promise<null> {
return (this._transport.request('misc_save_sticker', [accountId, msgId, collection] as RPC.Params)) as Promise<null>;
}
/**
* for desktop, get stickers from stickers folder,
* grouped by the collection/folder they are in.
*/
public miscGetStickers(accountId: T.U32): Promise<Record<string,(string)[]>> {
return (this._transport.request('misc_get_stickers', [accountId] as RPC.Params)) as Promise<Record<string,(string)[]>>;
}
/**
* Returns the messageid of the sent message
*/
public miscSendTextMessage(accountId: T.U32, chatId: T.U32, text: string): Promise<T.U32> {
return (this._transport.request('misc_send_text_message', [accountId, chatId, text] as RPC.Params)) as Promise<T.U32>;
}
public miscSendMsg(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), location: ([T.F64,T.F64]|null), quotedMessageId: (T.U32|null)): Promise<[T.U32,T.Message]> {
return (this._transport.request('misc_send_msg', [accountId, chatId, text, file, location, quotedMessageId] as RPC.Params)) as Promise<[T.U32,T.Message]>;
}
public miscSetDraft(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), quotedMessageId: (T.U32|null)): Promise<null> {
return (this._transport.request('misc_set_draft', [accountId, chatId, text, file, quotedMessageId] as RPC.Params)) as Promise<null>;
}
}

View File

@@ -0,0 +1,199 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
DC_CHAT_ID_ALLDONE_HINT = 7,
DC_CHAT_ID_ARCHIVED_LINK = 6,
DC_CHAT_ID_LAST_SPECIAL = 9,
DC_CHAT_ID_TRASH = 3,
DC_CHAT_TYPE_BROADCAST = 160,
DC_CHAT_TYPE_GROUP = 120,
DC_CHAT_TYPE_MAILINGLIST = 140,
DC_CHAT_TYPE_SINGLE = 100,
DC_CHAT_TYPE_UNDEFINED = 0,
DC_CONNECTIVITY_CONNECTED = 4000,
DC_CONNECTIVITY_CONNECTING = 2000,
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
DC_CONNECTIVITY_WORKING = 3000,
DC_CONTACT_ID_DEVICE = 5,
DC_CONTACT_ID_INFO = 2,
DC_CONTACT_ID_LAST_SPECIAL = 9,
DC_CONTACT_ID_SELF = 1,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
DC_GCL_ARCHIVED_ONLY = 1,
DC_GCL_FOR_FORWARDING = 8,
DC_GCL_NO_SPECIALS = 2,
DC_GCL_VERIFIED_ONLY = 1,
DC_GCM_ADDDAYMARKER = 1,
DC_GCM_INFO_ONLY = 2,
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,
DC_MEDIA_QUALITY_WORSE = 1,
DC_MSG_ID_DAYMARKER = 9,
DC_MSG_ID_LAST_SPECIAL = 9,
DC_MSG_ID_MARKER1 = 1,
DC_PROVIDER_STATUS_BROKEN = 3,
DC_PROVIDER_STATUS_OK = 1,
DC_PROVIDER_STATUS_PREPARATION = 2,
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
DC_SHOW_EMAILS_ALL = 2,
DC_SHOW_EMAILS_OFF = 0,
DC_SOCKET_AUTO = 0,
DC_SOCKET_PLAIN = 3,
DC_SOCKET_SSL = 1,
DC_SOCKET_STARTTLS = 2,
DC_STATE_IN_FRESH = 10,
DC_STATE_IN_NOTICED = 13,
DC_STATE_IN_SEEN = 16,
DC_STATE_OUT_DELIVERED = 26,
DC_STATE_OUT_DRAFT = 19,
DC_STATE_OUT_FAILED = 24,
DC_STATE_OUT_MDN_RCVD = 28,
DC_STATE_OUT_PENDING = 20,
DC_STATE_OUT_PREPARING = 18,
DC_STATE_UNDEFINED = 0,
DC_STR_AC_SETUP_MSG_BODY = 43,
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
DC_STR_ADD_MEMBER_BY_OTHER = 129,
DC_STR_ADD_MEMBER_BY_YOU = 128,
DC_STR_AEAP_ADDR_CHANGED = 122,
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
DC_STR_CONTACT_NOT_VERIFIED = 36,
DC_STR_CONTACT_SETUP_CHANGED = 37,
DC_STR_CONTACT_VERIFIED = 35,
DC_STR_DEVICE_MESSAGES = 68,
DC_STR_DEVICE_MESSAGES_HINT = 70,
DC_STR_DOWNLOAD_AVAILABILITY = 100,
DC_STR_DRAFT = 3,
DC_STR_E2E_AVAILABLE = 25,
DC_STR_E2E_PREFERRED = 34,
DC_STR_ENCRYPTEDMSG = 24,
DC_STR_ENCR_NONE = 28,
DC_STR_ENCR_TRANSP = 27,
DC_STR_EPHEMERAL_DAY = 79,
DC_STR_EPHEMERAL_DAYS = 95,
DC_STR_EPHEMERAL_DISABLED = 75,
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
DC_STR_EPHEMERAL_HOUR = 78,
DC_STR_EPHEMERAL_HOURS = 94,
DC_STR_EPHEMERAL_MINUTE = 77,
DC_STR_EPHEMERAL_MINUTES = 93,
DC_STR_EPHEMERAL_SECONDS = 76,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
DC_STR_EPHEMERAL_WEEK = 80,
DC_STR_EPHEMERAL_WEEKS = 96,
DC_STR_ERROR = 112,
DC_STR_ERROR_NO_NETWORK = 87,
DC_STR_FAILED_SENDING_TO = 74,
DC_STR_FILE = 12,
DC_STR_FINGERPRINTS = 30,
DC_STR_FORWARDED = 97,
DC_STR_GIF = 23,
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
DC_STR_GROUP_LEFT_BY_OTHER = 133,
DC_STR_GROUP_LEFT_BY_YOU = 132,
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
DC_STR_MESSAGES = 114,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
DC_STR_MSGADDMEMBER = 17,
DC_STR_MSGDELMEMBER = 18,
DC_STR_MSGGROUPLEFT = 19,
DC_STR_MSGGRPIMGCHANGED = 16,
DC_STR_MSGGRPIMGDELETED = 33,
DC_STR_MSGGRPNAME = 15,
DC_STR_MSGLOCATIONDISABLED = 65,
DC_STR_MSGLOCATIONENABLED = 64,
DC_STR_NOMESSAGES = 1,
DC_STR_NOT_CONNECTED = 121,
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
DC_STR_ONE_MOMENT = 106,
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
DC_STR_SECURE_JOIN_REPLIES = 118,
DC_STR_SECURE_JOIN_STARTED = 117,
DC_STR_SELF = 2,
DC_STR_SELF_DELETED_MSG_BODY = 91,
DC_STR_SENDING = 110,
DC_STR_SERVER_TURNED_OFF = 92,
DC_STR_SETUP_CONTACT_QR_DESC = 119,
DC_STR_STICKER = 67,
DC_STR_STORAGE_ON_DOMAIN = 105,
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
DC_STR_SYNC_MSG_BODY = 102,
DC_STR_SYNC_MSG_SUBJECT = 101,
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
DC_STR_UPDATING = 109,
DC_STR_VIDEO = 10,
DC_STR_VIDEOCHAT_INVITATION = 82,
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
DC_STR_VOICEMESSAGE = 7,
DC_STR_WELCOME_MESSAGE = 71,
DC_TEXT1_DRAFT = 1,
DC_TEXT1_SELF = 3,
DC_TEXT1_USERNAME = 2,
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
DC_VIDEOCHATTYPE_JITSI = 2,
DC_VIDEOCHATTYPE_UNKNOWN = 0,
}

View File

@@ -0,0 +1,214 @@
// AUTO-GENERATED by typescript-type-def
export type U32=number;
export type Usize=number;
export type Event=(({
/**
* 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.
*/
"type":"Info";}&{"msg":string;})|({
/**
* Emitted when SMTP connection is established and login was successful.
*/
"type":"SmtpConnected";}&{"msg":string;})|({
/**
* Emitted when IMAP connection is established and login was successful.
*/
"type":"ImapConnected";}&{"msg":string;})|({
/**
* Emitted when a message was successfully sent to the SMTP server.
*/
"type":"SmtpMessageSent";}&{"msg":string;})|({
/**
* Emitted when an IMAP message has been marked as deleted
*/
"type":"ImapMessageDeleted";}&{"msg":string;})|({
/**
* Emitted when an IMAP message has been moved
*/
"type":"ImapMessageMoved";}&{"msg":string;})|({
/**
* Emitted when an new file in the $BLOBDIR was created
*/
"type":"NewBlobFile";}&{"file":string;})|({
/**
* Emitted when an file in the $BLOBDIR was deleted
*/
"type":"DeletedBlobFile";}&{"file":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.
*/
"type":"Warning";}&{"msg":string;})|({
/**
* The library-user should report an error to the end-user.
*
* As most things are asynchronous, things may go wrong at any time and the user
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
*
* However, for ongoing processes (eg. configure())
* or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
* 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.
*/
"type":"Error";}&{"msg":string;})|({
/**
* An action cannot be performed because the user is not in the group.
* Reported eg. after a call to
* setChatName(), setChatProfileImage(),
* addContactToChat(), removeContactFromChat(),
* and messages sending functions.
*/
"type":"ErrorSelfNotInGroup";}&{"msg":string;})|({
/**
* Messages or chats changed. One or more messages or chats changed for various
* reasons in the database:
* - Messages sent, received or removed
* - Chats created, deleted or archived
* - A draft has been set
*
* `chatId` is set if only a single chat is affected by the changes, otherwise 0.
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
*/
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
/**
* Reactions for the message changed.
*/
"type":"ReactionsChanged";}&{"chatId":U32;"msgId":U32;"contactId":U32;})|({
/**
* 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.
*/
"type":"IncomingMsg";}&{"chatId":U32;"msgId":U32;})|({
/**
* Downloading a bunch of messages just finished. This is an experimental
* event to allow the UI to only show one notification per message bunch,
* instead of cluttering the user with many notifications.
*
* msg_ids contains the message ids.
*/
"type":"IncomingMsgBunch";}&{"msgIds":(U32)[];})|({
/**
* Messages were seen or noticed.
* chat id is always set.
*/
"type":"MsgsNoticed";}&{"chatId":U32;})|({
/**
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
* DC_STATE_OUT_DELIVERED, see `Message.state`.
*/
"type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({
/**
* 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 `Message.state`.
*/
"type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({
/**
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_MDN_RCVD, see `Message.state`.
*/
"type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({
/**
* 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.
* See setChatName(), setChatProfileImage(), addContactToChat()
* and removeContactFromChat().
*
* This event does not include ephemeral timer modification, which
* is a separate event.
*/
"type":"ChatModified";}&{"chatId":U32;})|({
/**
* Chat ephemeral timer changed.
*/
"type":"ChatEphemeralTimerModified";}&{"chatId":U32;"timer":U32;})|({
/**
* 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.
*/
"type":"ContactsChanged";}&{"contactId":(U32|null);})|({
/**
* Location of one or more contact has changed.
*
* @param data1 (u32) contact_id of the contact for which the location has changed.
* If the locations of several contacts have been changed,
* this parameter is set to `None`.
*/
"type":"LocationChanged";}&{"contactId":(U32|null);})|({
/**
* Inform about the configuration progress started by configure().
*/
"type":"ConfigureProgress";}&{
/**
* Progress.
*
* 0=error, 1-999=progress in permille, 1000=success and done
*/
"progress":Usize;
/**
* Progress comment or error, something to display to the user.
*/
"comment":(string|null);})|({
/**
* Inform about the import/export progress started by imex().
*
* @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
* @param data2 0
*/
"type":"ImexProgress";}&{"progress":Usize;})|({
/**
* A file has been exported. A file has been written by imex().
* This event may be sent multiple times by a single call to imex().
*
* A typical purpose for a handler of this event may be to make the file public to some system
* services.
*
* @param data2 0
*/
"type":"ImexFileWritten";}&{"path":string;})|({
/**
* Progress information of a secure-join handshake from the view of the inviter
* (Alice, the person who shows the QR code).
*
* These events are typically sent after a joiner has scanned the QR code
* generated by getChatSecurejoinQrCodeSvg().
*
* @param data1 (int) ID of the contact that wants to join.
* @param data2 (int) Progress as:
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
* 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.
*/
"type":"SecurejoinInviterProgress";}&{"contactId":U32;"progress":Usize;})|({
/**
* Progress information of a secure-join handshake from the view of the joiner
* (Bob, the person who scans the QR code).
* The events are typically sent while secureJoin(), which
* may take some time, is executed.
* @param data1 (int) ID of the inviting contact.
* @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)
*/
"type":"SecurejoinJoinerProgress";}&{"contactId":U32;"progress":Usize;})|{
/**
* The connectivity to the server changed.
* This means that you should refresh the connectivity view
* and possibly the connectivtiy HTML; see getConnectivity() and
* getConnectivityHtml() for details.
*/
"type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({
/**
* Inform that a message containing a webxdc instance has been deleted
*/
"type":"WebxdcInstanceDeleted";}&{"msgId":U32;}));

View File

@@ -0,0 +1,10 @@
// AUTO-GENERATED by typescript-type-def
export type JSONValue=(null|boolean|number|string|(JSONValue)[]|{[key:string]:JSONValue;});
export type Params=((JSONValue)[]|Record<string,JSONValue>);
export type U32=number;
export type Request={"jsonrpc":"2.0";"method":string;"params"?:Params;"id"?:U32;};
export type I32=number;
export type Error={"code":I32;"message":string;"data"?:JSONValue;};
export type Response={"jsonrpc":"2.0";"id":(U32|null);"result"?:JSONValue;"error"?:Error;};
export type Message=(Request|Response);

View File

@@ -0,0 +1,199 @@
// AUTO-GENERATED by typescript-type-def
export type U32=number;
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
export type U64=number;
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
export type Usize=number;
export type I64=number;
export type ChatListEntry=[U32,U32];
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
/**
* true when chat is a broadcastlist
*/
"isBroadcast":boolean;
/**
* contact id if this is a dm chat (for view profile entry in context menu)
*/
"dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;
/**
* the contact's last seen timestamp
*/
"lastSeen":I64;"wasSeenRecently":boolean;};
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":(string|null);};
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
export type BasicChat=
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
export type EncryptionModus=("Opportunistic"|"ForcePlaintext"|"ForceEncrypted"|"ForceVerified");
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
/**
* Day marker, separating messages that correspond to different
* days according to local time.
*/
"kind":"dayMarker";}&{
/**
* Marker timestamp, for day markers, in unix milliseconds
*/
"timestamp":I64;}));
export type Viewtype=("Unknown"|
/**
* Text message.
*/
"Text"|
/**
* Image message.
* If the image is an animated GIF, the type `Viewtype.Gif` should be used.
*/
"Image"|
/**
* Animated GIF message.
*/
"Gif"|
/**
* Message containing a sticker, similar to image.
* If possible, the ui should display the image without borders in a transparent way.
* A click on a sticker will offer to install the sticker set in some future.
*/
"Sticker"|
/**
* Message containing an Audio file.
*/
"Audio"|
/**
* A voice message that was directly recorded by the user.
* For all other audio messages, the type `Viewtype.Audio` should be used.
*/
"Voice"|
/**
* Video messages.
*/
"Video"|
/**
* Message containing any file, eg. a PDF.
*/
"File"|
/**
* Message is an invitation to a videochat.
*/
"VideochatInvitation"|
/**
* Message is an webxdc instance.
*/
"Webxdc");
export type MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;"viewType":Viewtype;}));
export type SystemMessageType=("Unknown"|"GroupNameChanged"|"GroupImageChanged"|"MemberAddedToGroup"|"MemberRemovedFromGroup"|"AutocryptSetupMessage"|"SecurejoinMessage"|"LocationStreamingEnabled"|"LocationOnly"|
/**
* Chat ephemeral message timer is changed.
*/
"EphemeralTimerChanged"|"ChatProtectionEnabled"|"ChatProtectionDisabled"|
/**
* Self-sent-message that contains only json used for multi-device-sync;
* if possible, we attach that to other messages as for locations.
*/
"MultiDeviceSync"|"WebxdcStatusUpdate"|
/**
* Webxdc info added with `info` set in `send_webxdc_status_update()`.
*/
"WebxdcInfoMessage");
export type I32=number;
export type WebxdcMessageInfo={
/**
* The name of the app.
*
* Defaults to the filename if not set in the manifest.
*/
"name":string;
/**
* App icon file name.
* Defaults to an standard icon if nothing is set in the manifest.
*
* To get the file, use dc_msg_get_webxdc_blob(). (not yet in jsonrpc, use rust api or cffi for it)
*
* App icons should should be square,
* the implementations will add round corners etc. as needed.
*/
"icon":string;
/**
* if the Webxdc represents a document, then this is the name of the document
*/
"document":(string|null);
/**
* short string describing the state of the app,
* sth. as "2 votes", "Highscore: 123",
* can be changed by the apps
*/
"summary":(string|null);
/**
* URL where the source code of the Webxdc and other information can be found;
* defaults to an empty string.
* Implementations may offer an menu or a button to open this URL.
*/
"sourceCodeUrl":(string|null);
/**
* True if full internet access should be granted to the app.
*/
"internetAccess":boolean;};
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
/**
* Structure representing all reactions to a particular message.
*/
export type Reactions=
/**
* Structure representing all reactions to a particular message.
*/
{
/**
* Map from a contact to it's reaction to message.
*/
"reactionsByContact":Record<U32,(string)[]>;
/**
* Unique reactions and their count
*/
"reactions":Record<string,U32>;};
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;
/**
* when is_info is true this describes what type of system message it is
*/
"systemMessageType":SystemMessageType;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);};
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
/**
* also known as summary_text1
*/
"summaryPrefix":(string|null);
/**
* also known as summary_text2
*/
"summaryText":string;};
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
export type F64=number;
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,U32,EncryptionModus,null,U32,U32,(EncryptionModus|null),U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,null,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,string,null,U32,Record<string,(string)[]>,U32,U32,string,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];

View File

@@ -48,5 +48,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.103.0"
"version": "1.101.0"
}

View File

@@ -1,41 +0,0 @@
# Delta Chat RPC python client
RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
and provides asynchronous interface to it.
## Getting started
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
Install it anywhere in your `PATH`.
## Testing
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
2. Run `PATH="../target/debug:$PATH" tox`.
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
## Using in REPL
Setup a development environment:
```
$ tox --devenv env
$ . env/bin/activate
```
It is recommended to use IPython, because it supports using `await` directly
from the REPL.
```
$ pip install ipython
$ PATH="../target/debug:$PATH" ipython
...
In [1]: from deltachat_rpc_client import *
In [2]: rpc = Rpc()
In [3]: await rpc.start()
In [4]: dc = DeltaChat(rpc)
In [5]: system_info = await dc.get_system_info()
In [6]: system_info["level"]
Out[6]: 'awesome'
In [7]: await rpc.close()
```

View File

@@ -1,54 +0,0 @@
#!/usr/bin/env python3
import asyncio
import logging
import sys
import deltachat_rpc_client as dc
async def main():
async with dc.Rpc() as rpc:
deltachat = dc.DeltaChat(rpc)
system_info = await deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
await account.set_config("bot", "1")
if not await account.is_configured():
logging.info("Account is not configured, configuring")
await account.set_config("addr", sys.argv[1])
await account.set_config("mail_pw", sys.argv[2])
await account.configure()
logging.info("Configured")
else:
logging.info("Account is already configured")
await deltachat.start_io()
async def process_messages():
for message in await account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot()
if not snapshot.is_info:
await snapshot.chat.send_text(snapshot.text)
await snapshot.message.mark_seen()
# Process old messages.
await process_messages()
while True:
event = await account.wait_for_event()
if event["type"] == "Info":
logging.info("%s", event["msg"])
elif event["type"] == "Warning":
logging.warning("%s", event["msg"])
elif event["type"] == "Error":
logging.error("%s", event["msg"])
elif event["type"] == "IncomingMsg":
logging.info("Got an incoming message")
await process_messages()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())

View File

@@ -1,29 +0,0 @@
[build-system]
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
description = "Python client for Delta Chat core JSON-RPC interface"
dependencies = [
"aiohttp",
"aiodns"
]
dynamic = [
"version"
]
[tool.setuptools]
# We declare the package not-zip-safe so that our type hints are also available
# when checking client code that uses our (installed) package.
# Ref:
# https://mypy.readthedocs.io/en/stable/installed_packages.html?highlight=zip#using-installed-packages-with-mypy-pep-561
zip-safe = false
[tool.setuptools.package-data]
deltachat_rpc_client = [
"py.typed"
]
[project.entry-points.pytest11]
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"

View File

@@ -1,5 +0,0 @@
from .account import Account
from .contact import Contact
from .deltachat import DeltaChat
from .message import Message
from .rpc import Rpc

View File

@@ -1,79 +0,0 @@
from typing import List, Optional
from .chat import Chat
from .contact import Contact
from .message import Message
class Account:
def __init__(self, rpc, account_id) -> None:
self._rpc = rpc
self.account_id = account_id
def __repr__(self) -> str:
return f"<Account id={self.account_id}>"
async def wait_for_event(self) -> dict:
"""Wait until the next event and return it."""
return await self._rpc.wait_for_event(self.account_id)
async def remove(self) -> None:
"""Remove the account."""
await self._rpc.remove_account(self.account_id)
async def start_io(self) -> None:
"""Start the account I/O."""
await self._rpc.start_io(self.account_id)
async def stop_io(self) -> None:
"""Stop the account I/O."""
await self._rpc.stop_io(self.account_id)
async def get_info(self) -> dict:
return await self._rpc.get_info(self.account_id)
async def get_file_size(self) -> int:
return await self._rpc.get_account_file_size(self.account_id)
async def is_configured(self) -> bool:
"""Return True for configured accounts."""
return await self._rpc.is_configured(self.account_id)
async def set_config(self, key: str, value: Optional[str] = None) -> None:
"""Set the configuration value key pair."""
await self._rpc.set_config(self.account_id, key, value)
async def get_config(self, key: str) -> Optional[str]:
"""Get the configuration value."""
return await self._rpc.get_config(self.account_id, key)
async def configure(self) -> None:
"""Configure an account."""
await self._rpc.configure(self.account_id)
async def create_contact(self, address: str, name: Optional[str] = None) -> Contact:
"""Create a contact with the given address and, optionally, a name."""
return Contact(
self._rpc,
self.account_id,
await self._rpc.create_contact(self.account_id, address, name),
)
async def secure_join(self, qrdata: str) -> Chat:
chat_id = await self._rpc.secure_join(self.account_id, qrdata)
return Chat(self._rpc, self.account_id, chat_id)
async def get_fresh_messages(self) -> List[Message]:
"""Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications.
If you are writing a bot, use get_fresh_messages_in_arrival_order instead,
to process oldest messages first.
"""
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.account_id)
return [Message(self._rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids]
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.account_id))
return [Message(self._rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids]

View File

@@ -1,41 +0,0 @@
from typing import TYPE_CHECKING
from .rpc import Rpc
if TYPE_CHECKING:
from .message import Message
class Chat:
def __init__(self, rpc: Rpc, account_id: int, chat_id: int) -> None:
self._rpc = rpc
self.account_id = account_id
self.chat_id = chat_id
async def block(self) -> None:
"""Block the chat."""
await self._rpc.block_chat(self.account_id, self.chat_id)
async def accept(self) -> None:
"""Accept the contact request."""
await self._rpc.accept_chat(self.account_id, self.chat_id)
async def delete(self) -> None:
await self._rpc.delete_chat(self.account_id, self.chat_id)
async def get_encryption_info(self) -> str:
return await self._rpc.get_chat_encryption_info(self.account_id, self.chat_id)
async def send_text(self, text: str) -> "Message":
from .message import Message
msg_id = await self._rpc.misc_send_text_message(
self.account_id, self.chat_id, text
)
return Message(self._rpc, self.account_id, msg_id)
async def leave(self) -> None:
await self._rpc.leave_group(self.account_id, self.chat_id)
async def get_fresh_message_count(self) -> int:
return await self._rpc.get_fresh_msg_cnt(self.account_id, self.chat_id)

View File

@@ -1,52 +0,0 @@
from typing import TYPE_CHECKING
from .rpc import Rpc
if TYPE_CHECKING:
from .chat import Chat
class Contact:
"""
Contact API.
Essentially a wrapper for RPC, account ID and a contact ID.
"""
def __init__(self, rpc: Rpc, account_id: int, contact_id: int) -> None:
self._rpc = rpc
self.account_id = account_id
self.contact_id = contact_id
async def block(self) -> None:
"""Block contact."""
await self._rpc.block_contact(self.account_id, self.contact_id)
async def unblock(self) -> None:
"""Unblock contact."""
await self._rpc.unblock_contact(self.account_id, self.contact_id)
async def delete(self) -> None:
"""Delete contact."""
await self._rpc.delete_contact(self.account_id, self.contact_id)
async def change_name(self, name: str) -> None:
await self._rpc.change_contact_name(self.account_id, self.contact_id, name)
async def get_encryption_info(self) -> str:
return await self._rpc.get_contact_encryption_info(
self.account_id, self.contact_id
)
async def get_dictionary(self) -> dict:
"""Return a dictionary with a snapshot of all contact properties."""
return await self._rpc.get_contact(self.account_id, self.contact_id)
async def create_chat(self) -> "Chat":
from .chat import Chat
return Chat(
self._rpc,
self.account_id,
await self._rpc.create_chat_by_contact_id(self.account_id, self.contact_id),
)

View File

@@ -1,34 +0,0 @@
from typing import List
from .account import Account
from .rpc import Rpc
class DeltaChat:
"""
Delta Chat account manager.
This is the root of the object oriented API.
"""
def __init__(self, rpc: Rpc) -> None:
self.rpc = rpc
async def add_account(self) -> Account:
account_id = await self.rpc.add_account()
return Account(self.rpc, account_id)
async def get_all_accounts(self) -> List[Account]:
account_ids = await self.rpc.get_all_account_ids()
return [Account(self.rpc, account_id) for account_id in account_ids]
async def start_io(self) -> None:
await self.rpc.start_io_for_all_accounts()
async def stop_io(self) -> None:
await self.rpc.stop_io_for_all_accounts()
async def maybe_network(self) -> None:
await self.rpc.maybe_network()
async def get_system_info(self) -> dict:
return await self.rpc.get_system_info()

View File

@@ -1,42 +0,0 @@
from dataclasses import dataclass
from typing import Optional
from .chat import Chat
from .contact import Contact
from .rpc import Rpc
class Message:
def __init__(self, rpc: Rpc, account_id: int, msg_id: int) -> None:
self._rpc = rpc
self.account_id = account_id
self.msg_id = msg_id
async def send_reaction(self, reactions: str) -> "Message":
msg_id = await self._rpc.send_reaction(self.account_id, self.msg_id, reactions)
return Message(self._rpc, self.account_id, msg_id)
async def get_snapshot(self) -> "MessageSnapshot":
message_object = await self._rpc.get_message(self.account_id, self.msg_id)
return MessageSnapshot(
message=self,
chat=Chat(self._rpc, self.account_id, message_object["chatId"]),
sender=Contact(self._rpc, self.account_id, message_object["fromId"]),
text=message_object["text"],
error=message_object.get("error"),
is_info=message_object["isInfo"],
)
async def mark_seen(self) -> None:
"""Mark the message as seen."""
await self._rpc.markseen_msgs(self.account_id, [self.msg_id])
@dataclass
class MessageSnapshot:
message: Message
chat: Chat
sender: Contact
text: str
error: Optional[str]
is_info: bool

View File

@@ -1 +0,0 @@
# PEP 561 marker file. See https://peps.python.org/pep-0561/

View File

@@ -1,52 +0,0 @@
import asyncio
import json
import os
from typing import AsyncGenerator, List
import aiohttp
import pytest_asyncio
from .account import Account
from .deltachat import DeltaChat
from .rpc import Rpc
async def get_temp_credentials() -> dict:
url = os.getenv("DCC_NEW_TMP_EMAIL")
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
async with aiohttp.ClientSession() as session:
async with session.post(url) as response:
return json.loads(await response.text())
class ACFactory:
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
async def new_configured_account(self) -> Account:
credentials = await get_temp_credentials()
account = await self.deltachat.add_account()
assert not await account.is_configured()
await account.set_config("addr", credentials["email"])
await account.set_config("mail_pw", credentials["password"])
await account.configure()
assert await account.is_configured()
return account
async def get_online_accounts(self, num: int) -> List[Account]:
accounts = [await self.new_configured_account() for _ in range(num)]
await self.deltachat.start_io()
return accounts
@pytest_asyncio.fixture
async def rpc(tmp_path) -> AsyncGenerator:
env = {**os.environ, "DC_ACCOUNTS_PATH": str(tmp_path / "accounts")}
rpc_server = Rpc(env=env)
async with rpc_server as rpc:
yield rpc
@pytest_asyncio.fixture
async def acfactory(rpc) -> AsyncGenerator:
yield ACFactory(DeltaChat(rpc))

View File

@@ -1,92 +0,0 @@
import asyncio
import json
from typing import Any, AsyncGenerator, Dict, Optional
class JsonRpcError(Exception):
pass
class Rpc:
def __init__(self, *args, **kwargs):
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
self._args = args
self._kwargs = kwargs
async def start(self) -> None:
self.process = await asyncio.create_subprocess_exec(
"deltachat-rpc-server",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
*self._args,
**self._kwargs
)
self.event_queues: Dict[int, asyncio.Queue] = {}
self.id = 0
self.reader_task = asyncio.create_task(self.reader_loop())
# Map from request ID to `asyncio.Future` returning the response.
self.request_events: Dict[int, asyncio.Future] = {}
async def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.process.terminate()
await self.reader_task
async def __aenter__(self):
await self.start()
return self
async def __aexit__(self, exc_type, exc, tb):
await self.close()
async def reader_loop(self) -> None:
while True:
line = await self.process.stdout.readline()
if not line: # EOF
break
response = json.loads(line)
if "id" in response:
fut = self.request_events.pop(response["id"])
fut.set_result(response)
elif response["method"] == "event":
# An event notification.
params = response["params"]
account_id = params["contextId"]
if account_id not in self.event_queues:
self.event_queues[account_id] = asyncio.Queue()
await self.event_queues[account_id].put(params["event"])
else:
print(response)
async def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""
if account_id in self.event_queues:
return await self.event_queues[account_id].get()
return None
def __getattr__(self, attr: str):
async def method(*args, **kwargs) -> Any:
self.id += 1
request_id = self.id
assert not (args and kwargs), "Mixing positional and keyword arguments"
request = {
"jsonrpc": "2.0",
"method": attr,
"params": kwargs or args,
"id": self.id,
}
data = (json.dumps(request) + "\n").encode()
self.process.stdin.write(data)
loop = asyncio.get_running_loop()
fut = loop.create_future()
self.request_events[request_id] = fut
response = await fut
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:
return response["result"]
return method

View File

@@ -1,60 +0,0 @@
import pytest
@pytest.mark.asyncio
async def test_system_info(rpc) -> None:
system_info = await rpc.get_system_info()
assert "arch" in system_info
assert "deltachat_core_version" in system_info
@pytest.mark.asyncio
async def test_email_address_validity(rpc) -> None:
valid_addresses = [
"email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
]
invalid_addresses = ["email@", "example.com", "emai221"]
for addr in valid_addresses:
assert await rpc.check_email_validity(addr)
for addr in invalid_addresses:
assert not await rpc.check_email_validity(addr)
@pytest.mark.asyncio
async def test_acfactory(acfactory) -> None:
account = await acfactory.new_configured_account()
while True:
event = await account.wait_for_event()
if event["type"] == "ConfigureProgress":
# Progress 0 indicates error.
assert event["progress"] != 0
if event["progress"] == 1000:
# Success.
break
else:
print(event)
print("Successful configuration")
@pytest.mark.asyncio
async def test_object_account(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
alice_contact_bob = await alice.create_contact(await bob.get_config("addr"), "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!")
while True:
event = await bob.wait_for_event()
if event["type"] == "IncomingMsg":
chat_id = event["chatId"]
msg_id = event["msgId"]
break
rpc = acfactory.deltachat.rpc
message = await rpc.get_message(bob.account_id, msg_id)
assert message["chatId"] == chat_id
assert message["text"] == "Hello!"

View File

@@ -1,19 +0,0 @@
[tox]
isolated_build = true
envlist =
py3
[testenv]
commands =
pytest {posargs}
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
passenv =
DCC_NEW_TMP_EMAIL
deps =
pytest
pytest-async
pytest-asyncio
aiohttp
aiodns

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.103.0"
version = "1.101.0"
description = "DeltaChat JSON-RPC server"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
@@ -17,10 +17,10 @@ name = "deltachat-rpc-server"
deltachat-jsonrpc = { path = "../deltachat-jsonrpc" }
anyhow = "1"
env_logger = { version = "0.10.0" }
env_logger = { version = "0.9.1" }
futures-lite = "1.12.0"
log = "0.4"
serde_json = "1.0.89"
serde_json = "1.0.85"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.22.0", features = ["io-std"] }
tokio = { version = "1.21.2", features = ["io-std"] }
yerpc = { version = "0.3.1", features = ["anyhow_expose"] }

View File

@@ -4,7 +4,7 @@ This document gives a quick overview about the Webxdc specification,
It is meant for both, developing Webxdc apps
and developing Webxdc implementations.
The [Webxdc guidebook](https://docs.webxdc.org/) shows more detailed information
The [Webxdc guidebook](https://deltachat.github.io/webxdc_docs/) shows more detailed information
when developing Webxdc apps.

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.103.0"
"version": "1.101.0"
}

View File

@@ -156,7 +156,6 @@ def extract_defines(flags):
| DC_KEY_GEN
| DC_IMEX
| DC_CONNECTIVITY
| DC_DOWNLOAD
) # End of prefix matching
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
) # Close the capturing group, this contains

View File

@@ -82,13 +82,12 @@ class Account(object):
if hasattr(db_path, "encode"):
db_path = db_path.encode("utf8")
ptr = lib.dc_context_new_closed(db_path) if closed else lib.dc_context_new(ffi.NULL, db_path, ffi.NULL)
if ptr == ffi.NULL:
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
self._dc_context = ffi.gc(
ptr,
lib.dc_context_new_closed(db_path) if closed else lib.dc_context_new(ffi.NULL, db_path, ffi.NULL),
lib.dc_context_unref,
)
if self._dc_context == ffi.NULL:
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
self._shutdown_event = Event()
self._event_thread = EventThread(self)

View File

@@ -192,12 +192,6 @@ class FFIEventTracker:
return self.account.get_message_by_id(ev.data2)
return None
def wait_next_reactions_changed(self):
"""wait for and return next reactions-changed message"""
ev = self.get_matching("DC_EVENT_REACTIONS_CHANGED")
assert ev.data1 > 0
return self.account.get_message_by_id(ev.data2)
def wait_msg_delivered(self, msg):
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
assert ev.data1 == msg.chat.id
@@ -302,10 +296,6 @@ class EventThread(threading.Thread):
"ac_incoming_message",
dict(message=msg),
)
elif name == "DC_EVENT_REACTIONS_CHANGED":
assert ffi_event.data1 > 0
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_reactions_changed", dict(message=msg)
elif name == "DC_EVENT_MSG_DELIVERED":
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", dict(message=msg)

View File

@@ -49,10 +49,6 @@ class PerAccount:
def ac_outgoing_message(self, message):
"""Called on each outgoing message (both system and "normal")."""
@account_hookspec
def ac_reactions_changed(self, message):
"""Called when message reactions changed."""
@account_hookspec
def ac_message_delivered(self, message):
"""Called when an outgoing message has been delivered to SMTP.

View File

@@ -9,7 +9,6 @@ from typing import Optional, Union
from . import const, props
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer
from .reactions import Reactions
class Message(object):
@@ -162,17 +161,6 @@ class Message(object):
)
)
def send_reaction(self, reaction: str):
"""Send a reaction to message and return the resulting Message instance."""
msg_id = lib.dc_send_reaction(self.account._dc_context, self.id, as_dc_charpointer(reaction))
if msg_id == 0:
raise ValueError("reaction could not be send")
return Message.from_db(self.account, msg_id)
def get_reactions(self) -> Reactions:
"""Get :class:`deltachat.reactions.Reactions` to the message."""
return Reactions.from_msg(self)
def is_system_message(self):
"""return True if this message is a system/info message."""
return bool(lib.dc_msg_is_info(self._dc_msg))
@@ -461,17 +449,6 @@ class Message(object):
"""mark this message as seen."""
self.account.mark_seen_messages([self.id])
#
# Message download state
#
@property
def download_state(self):
assert self.id > 0
# load message from db to get a fresh/current state
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
return lib.dc_msg_get_download_state(dc_msg)
# some code for handling DC_MSG_* view types

View File

@@ -1,43 +0,0 @@
""" The Reactions object. """
from .capi import ffi, lib
from .cutil import from_dc_charpointer, iter_array
class Reactions(object):
"""Reactions object.
You obtain instances of it through :class:`deltachat.message.Message`.
"""
def __init__(self, account, dc_reactions):
assert isinstance(account._dc_context, ffi.CData)
assert isinstance(dc_reactions, ffi.CData)
assert dc_reactions != ffi.NULL
self.account = account
self._dc_reactions = dc_reactions
def __repr__(self):
return "<Reactions dc_reactions={}>".format(self._dc_reactions)
@classmethod
def from_msg(cls, msg):
assert msg.id > 0
return cls(
msg.account,
ffi.gc(lib.dc_get_msg_reactions(msg.account._dc_context, msg.id), lib.dc_reactions_unref),
)
def get_contacts(self) -> list:
"""Get list of contacts reacted to the message.
:returns: list of :class:`deltachat.contact.Contact` objects for this reaction.
"""
from .contact import Contact
dc_array = ffi.gc(lib.dc_reactions_get_contacts(self._dc_reactions), lib.dc_array_unref)
return list(iter_array(dc_array, lambda x: Contact(self.account, x)))
def get_by_contact(self, contact) -> str:
"""Get a string containing space-separated reactions of a single :class:`deltachat.contact.Contact`."""
return from_dc_charpointer(lib.dc_reactions_get_by_contact_id(self._dc_reactions, contact.id))

View File

@@ -1,7 +1,6 @@
import os
import queue
import sys
import time
from datetime import datetime, timezone
import pytest
@@ -406,29 +405,6 @@ def test_forward_own_message(acfactory, lp):
assert msg_in.is_forwarded()
def test_long_group_name(acfactory, lp):
"""See bug https://github.com/deltachat/deltachat-core-rust/issues/3650 "Space added before long
group names after MIME serialization/deserialization".
When the mailadm bot creates a group with botadmin, the bot creates is as
"pytest-supportuser-282@x.testrun.org support group" (for example). But in the botadmin's
account object, the group chat is called " pytest-supportuser-282@x.testrun.org support group"
(with an additional space character in the beginning).
"""
ac1, ac2 = acfactory.get_online_accounts(2)
lp.sec("ac1: creating group chat and sending a message")
group_name = "pytest-supportuser-282@x.testrun.org support group"
group = ac1.create_group_chat(group_name)
group.add_contact(ac2)
group.send_text("message")
# wait for other account to receive
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.chat.get_name() == group_name
def test_send_self_message(acfactory, lp):
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
@@ -925,34 +901,6 @@ def test_dont_show_emails(acfactory, lp):
ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Spam",
"""
From: unknown.address@junk.org, unkwnown.add@junk.org
Subject: subj
To: {}
Message-ID: <spam.message2@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown & malformed message in Spam
""".format(
ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Spam",
"""
From: alice@example.org
Subject: subj
To: {}
Message-ID: <spam.message3@junk.org>
Content-Type: text/plain; charset=utf-8
Actually interesting message in Spam
""".format(
ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Junk",
"""
@@ -978,9 +926,7 @@ def test_dont_show_emails(acfactory, lp):
ac1._evtracker.wait_idle_inbox_ready()
assert msg.text == "subj message in Sent"
chat_msgs = msg.chat.get_messages()
assert len(chat_msgs) == 2
assert any(msg.text == "subj Actually interesting message in Spam" for msg in chat_msgs)
assert len(msg.chat.get_messages()) == 1
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
ac1.direct_imap.select_folder("Spam")
@@ -996,7 +942,7 @@ def test_dont_show_emails(acfactory, lp):
msg2 = ac1._evtracker.wait_next_messages_changed()
assert msg2.text == "subj message in Drafts that is moved to Sent later"
assert len(msg.chat.get_messages()) == 3
assert len(msg.chat.get_messages()) == 2
def test_no_old_msg_is_fresh(acfactory, lp):
@@ -1291,103 +1237,6 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_in
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
If the Inbox contains X small messages followed by Y large messages followed by Z small
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
with online test as follows:
- Bob enables download limit and goes offline.
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
- Bob goes online
- Bob first processes a reaction message and throws it away because there is no corresponding
message, then processes a partially downloaded message.
- As a result, Bob does not see a reaction
"""
download_limit = 32768
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
chat = ac1.create_chat(ac2)
ac2.set_config("download_limit", str(download_limit))
ac2.stop_io()
reactions_queue = queue.Queue()
class InPlugin:
@account_hookimpl
def ac_reactions_changed(self, message):
reactions_queue.put(message)
ac2.add_account_plugin(InPlugin())
lp.sec("sending small+large messages from ac1 to ac2")
msgs = []
msgs.append(chat.send_text("hi"))
path = tmpdir.join("large")
with open(path, "wb") as fout:
fout.write(os.urandom(download_limit + 1))
msgs.append(chat.send_file(path.strpath))
lp.sec("sending a reaction to the large message from ac1 to ac2")
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
for m in msgs:
ac1._evtracker.wait_msg_delivered(m)
ac2.start_io()
lp.sec("wait for ac2 to receive a reaction")
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.get_sender_contact().addr == ac1_addr
assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE
assert reactions_queue.get() == msg2
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
assert len(contacts) == 1
assert contacts[0].addr == ac1_addr
assert reactions.get_by_contact(contacts[0]) == react_str
def test_reactions_for_a_reordering_move(acfactory, lp):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
lp.sec("sending message + reaction from ac1 to ac2")
msg1 = chat1.send_text("hi")
ac1._evtracker.wait_msg_delivered(msg1)
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(2)
react_str = "\N{THUMBS UP SIGN}"
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
lp.sec("moving messages to ac2's DeltaChat folder in the reverse order")
ac2.direct_imap.connect()
for uid in sorted([m.uid for m in ac2.direct_imap.get_all_messages()], reverse=True):
ac2.direct_imap.conn.move(uid, "DeltaChat")
lp.sec("receiving messages by ac2")
ac2.start_io()
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.text == msg1.text
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
assert len(contacts) == 1
assert contacts[0].addr == ac1.get_config("addr")
assert reactions.get_by_contact(contacts[0]) == react_str
def test_import_export_online_all(acfactory, tmpdir, data, lp):
(ac1,) = acfactory.get_online_accounts(1)

View File

@@ -27,13 +27,6 @@ deps =
pdbpp
requests
[testenv:.pkg]
passenv =
DCC_RS_DEV
DCC_RS_TARGET
CARGO_TARGET_DIR
RUSTC_WRAPPER
[testenv:auditwheels]
skipsdist = True
deps = auditwheel

View File

@@ -1 +1 @@
1.65.0
1.61.0

View File

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

View File

@@ -34,9 +34,10 @@ pub(crate) async fn handle_authres(
let from_domain = match EmailAddress::new(from) {
Ok(email) => email.domain,
Err(e) => {
warn!(context, "invalid email {:#}", e);
// This email is invalid, but don't return an error, we still want to
// add a stub to the database so that it's not downloaded again
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
return Ok(DkimResults::default());
}
};
@@ -45,7 +46,7 @@ pub(crate) async fn handle_authres(
compute_dkim_results(context, authres, &from_domain, message_time).await
}
#[derive(Debug)]
#[derive(Default, Debug)]
pub(crate) struct DkimResults {
/// Whether DKIM passed for this particular e-mail.
pub dkim_passed: bool,
@@ -358,7 +359,6 @@ mod tests {
use crate::aheader::EncryptPreference;
use crate::e2ee;
use crate::message;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::securejoin::get_securejoin_qr;
@@ -574,7 +574,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
let from = &mimeparser::get_from(&mail.headers)[0].addr;
let res = handle_authres(&t, &mail, from, time()).await?;
assert!(res.allow_keychange);
@@ -586,7 +586,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
let from = &mimeparser::get_from(&mail.headers)[0].addr;
let res = handle_authres(&t, &mail, from, time()).await?;
if !res.allow_keychange {
@@ -637,10 +637,9 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
// return an Err because this would prevent the message from being added
// to the database and downloaded again and again
let bytes = b"From: invalid@from.com
Authentication-Results: dkim=";
let bytes = b"Authentication-Results: dkim=";
let mail = mailparse::parse_mail(bytes).unwrap();
handle_authres(&t, &mail, "invalid@rom.com", time())
handle_authres(&t, &mail, "invalidfrom.com", time())
.await
.unwrap();
}
@@ -682,7 +681,7 @@ Authentication-Results: dkim=";
.await;
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
let received = alice.recv_msg(&sent).await;
@@ -718,7 +717,7 @@ Authentication-Results: dkim=";
loop {
if let Some(mut sent) = bob2.pop_sent_msg_opt(Duration::ZERO).await {
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
alice.recv_msg(&sent).await;
} else if let Some(sent) = alice.pop_sent_msg_opt(Duration::ZERO).await {
bob2.recv_msg(&sent).await;
@@ -751,86 +750,4 @@ Authentication-Results: dkim=";
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_autocrypt_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_bob_chat = alice.create_chat(&bob).await;
let bob_alice_chat = bob.create_chat(&alice).await;
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_none());
// Do the same without the mailing list header, this time the peerstate should be accepted
let sent = alice
.send_text(alice_bob_chat.id, "hellooo without mailing list")
.await;
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_some());
// This also means that Bob can now write encrypted to Alice:
let mut sent = bob
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
.await;
assert!(sent.load_from_db().await.get_showpadlock());
// But if Bob writes to a mailing list, Alice doesn't show a padlock
// since she can't verify the signature without accepting Bob's key:
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await;
assert!(!rcvd.get_showpadlock());
assert_eq!(&rcvd.text.unwrap(), "hellooo in the mailinglist again");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Assume Bob received an email from something@example.net with
// correct DKIM -> `set_dkim_works()` was called
set_dkim_works_timestamp(&bob, "example.org", time()).await?;
// And Bob knows his server's authserv-id
bob.set_config(Config::AuthservIdCandidates, Some("example.net"))
.await?;
let alice_bob_chat = alice.create_chat(&bob).await;
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
sent.payload
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
let rcvd = bob.recv_msg(&sent).await;
assert!(rcvd.error.is_none());
// Do the same without the mailing list header, this time the failed
// authres isn't ignored
let mut sent = alice
.send_text(alice_bob_chat.id, "hellooo without mailing list")
.await;
sent.payload
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
let rcvd = bob.recv_msg(&sent).await;
// Disallowing keychanges is disabled for now:
// assert!(rcvd.error.unwrap().contains("DKIM failed"));
// The message info should contain a warning:
assert!(message::get_msg_info(&bob, rcvd.id)
.await
.unwrap()
.contains("KEYCHANGES NOT ALLOWED"));
Ok(())
}
}

View File

@@ -2,7 +2,6 @@
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::{Duration, SystemTime};
@@ -76,50 +75,35 @@ pub enum ProtectionStatus {
Protected = 1,
}
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum EncryptionModus {
Opportunistic = 0,
ForcePlaintext = 1,
ForceEncrypted = 2,
ForceVerified = 3,
}
impl Default for ProtectionStatus {
fn default() -> Self {
ProtectionStatus::Unprotected
}
}
/// The reason why messages cannot be sent to the chat.
///
/// The reason is mainly for logging and displaying in debug REPL, thus not translated.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CantSendReason {
/// Special chat.
SpecialChat,
/// The chat is a device chat.
DeviceChat,
/// The chat is a contact request, it needs to be accepted before sending a message.
ContactRequest,
/// Mailing list without known List-Post header.
ReadOnlyMailingList,
/// Not a member of the chat.
NotAMember,
}
impl fmt::Display for CantSendReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SpecialChat => write!(f, "the chat is a special chat"),
Self::DeviceChat => write!(f, "the chat is a device chat"),
Self::ContactRequest => write!(
f,
"contact request chat should be accepted before sending messages"
),
Self::ReadOnlyMailingList => {
write!(f, "mailing list does not have a know post address")
}
Self::NotAMember => write!(f, "not a member of the chat"),
}
}
}
/// Chat ID, including reserved IDs.
///
/// Some chat IDs are reserved to identify special chat types. This
@@ -684,11 +668,8 @@ impl ChatId {
}
let chat = Chat::load_from_db(context, self).await?;
if let Some(cant_send_reason) = chat.why_cant_send(context).await? {
bail!(
"Can't set a draft because chat is not writeable: {}",
cant_send_reason
);
if !chat.can_send(context).await? {
bail!("Can't set a draft: Can't send");
}
// set back draft information to allow identifying the draft later on -
@@ -940,6 +921,38 @@ impl ChatId {
Ok(ret.trim().to_string())
}
/// This sets a protection modus for the chat and enforces that messages are only send if they
/// meet the encryption modus (ForcePlaintext, Opportunistic, ForceEncrypted, ForceVerified)
pub async fn set_encryption_modus(
self,
context: &Context,
modus: EncryptionModus,
) -> Result<()> {
context
.sql
.execute(
"UPDATE chats SET encryption_modus=? WHERE id=?;",
paramsv![modus, self],
)
.await?;
Ok(())
}
/// This sets a protection modus for the chat and enforces that messages are only send if they
/// meet the encryption modus (ForcePlaintext, Opportunistic, ForceEncrypted, ForceVerified)
pub async fn encryption_modus(self, context: &Context) -> Result<Option<EncryptionModus>> {
let encryption_modus: Option<EncryptionModus> = context
.sql
.query_get_value(
"SELECT encryption_modus FROM chats WHERE id=?;",
paramsv![self],
)
.await?;
Ok(encryption_modus)
}
/// Bad evil escape hatch.
///
/// Avoid using this, eventually types should be cleaned up enough
@@ -1124,33 +1137,14 @@ impl Chat {
self.typ == Chattype::Mailinglist
}
/// Returns None if user can send messages to this chat.
///
/// Otherwise returns a reason useful for logging.
pub(crate) async fn why_cant_send(&self, context: &Context) -> Result<Option<CantSendReason>> {
use CantSendReason::*;
let reason = if self.id.is_special() {
Some(SpecialChat)
} else if self.is_device_talk() {
Some(DeviceChat)
} else if self.is_contact_request() {
Some(ContactRequest)
} else if self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty() {
Some(ReadOnlyMailingList)
} else if !self.is_self_in_chat(context).await? {
Some(NotAMember)
} else {
None
};
Ok(reason)
}
/// Returns true if can send to the chat.
///
/// This function can be used by the UI to decide whether to display the input box.
/// Returns true if user can send messages to this chat.
pub async fn can_send(&self, context: &Context) -> Result<bool> {
Ok(self.why_cant_send(context).await?.is_none())
let cannot_send = self.id.is_special()
|| self.is_device_talk()
|| self.is_contact_request()
|| (self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty())
|| !self.is_self_in_chat(context).await?;
Ok(!cannot_send)
}
/// Checks if the user is part of a chat
@@ -1316,13 +1310,18 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
if let Some(reason) = self.why_cant_send(context).await? {
if self.typ == Chattype::Group && reason == CantSendReason::NotAMember {
if !self.can_send(context).await? {
if self.typ == Chattype::Group
&& !is_contact_in_chat(context, self.id, ContactId::SELF).await?
{
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot send message; self not in group.".into(),
));
bail!("Cannot set message; self not in group.");
} else {
error!(context, "Cannot send to chat type #{}.", self.typ,);
bail!("Cannot send to chat type #{}", self.typ);
}
bail!("Cannot send message to {}: {}", self.id, reason);
}
let from = context.get_primary_self_addr().await?;
@@ -1937,9 +1936,7 @@ async fn prepare_msg_common(
change_state_to: MessageState,
) -> Result<MsgId> {
let mut chat = Chat::load_from_db(context, chat_id).await?;
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
}
ensure!(chat.can_send(context).await?, "cannot send to {}", chat_id);
// check current MessageState for drafts (to keep msg_id) ...
let update_msg_id = if msg.state == MessageState::OutDraft {
@@ -2002,6 +1999,14 @@ pub async fn is_contact_in_chat(
// the caller can get it from msg.chat_id. Forwards would need to
// be fixed for this somehow too.
pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
// Propagate same encryption_mode of chat to message in case messages doesn't yet have an
// encryption_mode
if let None = msg.encryption_modus(&context).await? {
if let Some(encryption_mode) = chat_id.encryption_modus(&context).await? {
msg.set_encryption_modus(&context, encryption_mode).await?;
}
}
if chat_id.is_unset() {
let forwards = msg.param.get(Param::PrepForwards);
if let Some(forwards) = forwards {
@@ -2246,7 +2251,7 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.param.set(Param::WebrtcRoom, &instance);
msg.text = Some(
stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
stock_str::videochat_invite_msg_body(context, Message::parse_webrtc_instance(&instance).1)
.await,
);
send_msg(context, chat_id, &mut msg).await
@@ -2621,7 +2626,7 @@ pub async fn create_group_chat(
let chat_id = ChatId::new(u32::try_from(row_id)?);
if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?;
}
context.emit_msgs_changed_without_ids();
@@ -2682,25 +2687,19 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
Ok(chat_id)
}
/// Adds contacts to the `chats_contacts` table.
/// Adds a contact to the `chats_contacts` table.
pub(crate) async fn add_to_chat_contacts_table(
context: &Context,
chat_id: ChatId,
contact_ids: &[ContactId],
contact_id: ContactId,
) -> Result<()> {
context
.sql
.transaction(move |transaction| {
for contact_id in contact_ids {
transaction.execute(
"INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
paramsv![chat_id, contact_id],
)?;
}
Ok(())
})
.execute(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
paramsv![chat_id, contact_id],
)
.await?;
Ok(())
}
@@ -2802,7 +2801,7 @@ pub(crate) async fn add_contact_to_chat_ex(
if is_contact_in_chat(context, chat_id, contact_id).await? {
return Ok(false);
}
add_to_chat_contacts_table(context, chat_id, &[contact_id]).await?;
add_to_chat_contacts_table(context, chat_id, contact_id).await?;
}
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
@@ -3069,7 +3068,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
pub async fn set_chat_profile_image(
context: &Context,
chat_id: ChatId,
new_image: &str, // XXX use PathBuf
new_image: impl AsRef<str>, // XXX use PathBuf
) -> Result<()> {
ensure!(!chat_id.is_special(), "Invalid chat ID");
let mut chat = Chat::load_from_db(context, chat_id).await?;
@@ -3087,12 +3086,13 @@ pub async fn set_chat_profile_image(
let mut msg = Message::new(Viewtype::Text);
msg.param
.set_int(Param::Cmd, SystemMessage::GroupImageChanged as i32);
if new_image.is_empty() {
if new_image.as_ref().is_empty() {
chat.param.remove(Param::ProfileImage);
msg.param.remove(Param::Arg);
msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await);
} else {
let mut image_blob = BlobObject::new_from_path(context, Path::new(new_image)).await?;
let mut image_blob =
BlobObject::new_from_path(context, Path::new(new_image.as_ref())).await?;
image_blob.recode_to_avatar_size(context).await?;
chat.param.set(Param::ProfileImage, image_blob.as_name());
msg.param.set(Param::Arg, image_blob.as_name());
@@ -3117,9 +3117,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
chat_id.unarchive_if_not_muted(context).await?;
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
}
ensure!(chat.can_send(context).await?, "cannot send to {}", chat_id);
curr_timestamp = create_smeared_timestamps(context, msg_ids.len()).await;
let ids = context
.sql
@@ -4093,7 +4091,6 @@ mod tests {
assert!(chat.is_device_talk());
assert!(!chat.is_self_talk());
assert!(!chat.can_send(&t).await?);
assert!(chat.why_cant_send(&t).await? == Some(CantSendReason::DeviceChat));
assert_eq!(chat.name, stock_str::device_messages(&t).await);
assert!(chat.get_profile_image(&t).await?.is_some());
@@ -5715,4 +5712,27 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encryption_modus() -> Result<()> {
let t = TestContext::new_alice().await;
let contact_fiona = Contact::create(&t, "", "fiona@example.net").await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "Group").await?;
assert_eq!(
chat_id.encryption_modus(&t).await?,
Some(EncryptionModus::Opportunistic)
);
chat_id
.set_encryption_modus(&t, EncryptionModus::ForceEncrypted)
.await?;
assert_eq!(
chat_id.encryption_modus(&t).await?,
Some(EncryptionModus::ForceEncrypted)
);
Ok(())
}
}

View File

@@ -194,20 +194,20 @@ pub enum Config {
impl Context {
pub async fn config_exists(&self, key: Config) -> Result<bool> {
Ok(self.sql.get_raw_config(key.as_ref()).await?.is_some())
Ok(self.sql.get_raw_config(key).await?.is_some())
}
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(key.as_ref()).await?;
let rel_path = self.sql.get_raw_config(key).await?;
rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(key.as_ref()).await?,
_ => self.sql.get_raw_config(key).await?,
};
if value.is_some() {
@@ -297,30 +297,26 @@ impl Context {
Some(value) => {
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
blob.recode_to_avatar_size(self).await?;
self.sql
.set_raw_config(key.as_ref(), Some(blob.as_name()))
.await?;
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
}
None => {
self.sql.set_raw_config(key.as_ref(), None).await?;
self.sql.set_raw_config(key, None).await?;
}
}
self.emit_event(EventType::SelfavatarChanged);
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(key.as_ref(), value).await;
let ret = self.sql.set_raw_config(key, value).await;
// Interrupt ephemeral loop to delete old messages immediately.
self.interrupt_ephemeral_task().await;
ret?
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql
.set_raw_config(key.as_ref(), value.as_deref())
.await?;
self.sql.set_raw_config(key, value.as_deref()).await?;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
self.sql.set_raw_config(key, value).await?;
}
}
Ok(())

View File

@@ -87,7 +87,7 @@ impl Context {
self,
// We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}:
&format!("{:#}", err),
format!("{:#}", err),
)
.await
)
@@ -153,7 +153,7 @@ async fn on_configure_completed(
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new(Viewtype::Text);
msg.text =
Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
Some(stock_str::aeap_explanation_and_link(context, old_addr, new_addr).await);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.ok_or_log_msg(context, "Cannot add AEAP explanation");
@@ -442,6 +442,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let create_mvbox = ctx.should_watch_mvbox().await?;
// Send client ID as soon as possible before doing anything else.
imap.determine_capabilities(ctx).await?;
imap.configure_folders(ctx, create_mvbox).await?;
imap.select_with_uidvalidity(ctx, "INBOX")

View File

@@ -1578,6 +1578,7 @@ mod tests {
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
use crate::chatlist::Chatlist;
use crate::message::Message;
use crate::receive_imf::receive_imf;
use crate::test_utils::{self, TestContext, TestContextManager};
@@ -2327,7 +2328,7 @@ CCCB 5AA9 F6E1 141C 9431
let sent_msg = alice1.pop_sent_msg().await;
// Message is not encrypted.
let message = sent_msg.load_from_db().await;
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
assert!(!message.get_showpadlock());
// Alice's second devices receives a copy of outgoing message.
@@ -2354,7 +2355,7 @@ CCCB 5AA9 F6E1 141C 9431
let sent_msg = alice1.pop_sent_msg().await;
// Second message is encrypted.
let message = sent_msg.load_from_db().await;
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
assert!(message.get_showpadlock());
// Alice's second devices receives a copy of second outgoing message.
@@ -2409,7 +2410,7 @@ CCCB 5AA9 F6E1 141C 9431
let sent_msg = alice1.pop_sent_msg().await;
// The message is encrypted.
let message = sent_msg.load_from_db().await;
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
assert!(message.get_showpadlock());
// Alice's second device receives a copy of the outgoing message.

View File

@@ -614,9 +614,9 @@ impl Context {
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());
if let Some(server_id) = &*self.server_id.read().await {
res.insert("imap_server_id", format!("{:?}", server_id));
}
let server_id = self.server_id.read().await;
res.insert("imap_server_id", format!("{:?}", server_id));
drop(server_id);
res.insert("secondary_addrs", secondary_addrs);
res.insert(

View File

@@ -4,13 +4,13 @@ use std::collections::HashSet;
use anyhow::{Context as _, Result};
use mailparse::ParsedMail;
use mailparse::SingleInfo;
use crate::aheader::{Aheader, EncryptPreference};
use crate::aheader::Aheader;
use crate::authres;
use crate::authres::handle_authres;
use crate::authres::{self, DkimResults};
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::log::LogExt;
@@ -56,44 +56,22 @@ pub async fn try_decrypt(
.await
}
pub(crate) async fn prepare_decryption(
pub async fn prepare_decryption(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
from: &[SingleInfo],
message_time: i64,
is_thunderbird: bool,
) -> Result<DecryptionInfo> {
if mail.headers.get_header(HeaderDef::ListPost).is_some() {
if mail.headers.get_header(HeaderDef::Autocrypt).is_some() {
info!(
context,
"Ignoring autocrypt header since this is a mailing list message. \
NOTE: For privacy reasons, the mailing list software should remove Autocrypt headers."
);
}
return Ok(DecryptionInfo {
from: from.to_string(),
autocrypt_header: None,
peerstate: None,
message_time,
dkim_results: DkimResults {
dkim_passed: false,
dkim_should_work: false,
allow_keychange: true,
},
});
}
let from = if let Some(f) = from.first() {
&f.addr
} else {
return Ok(DecryptionInfo::default());
};
let mut autocrypt_header = Aheader::from_headers(from, &mail.headers)
let autocrypt_header = Aheader::from_headers(from, &mail.headers)
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
.flatten();
if is_thunderbird {
if let Some(autocrypt_header) = &mut autocrypt_header {
autocrypt_header.prefer_encrypt = EncryptPreference::Mutual;
}
}
let dkim_results = handle_authres(context, mail, from, message_time).await?;
let peerstate = get_autocrypt_peerstate(
@@ -115,7 +93,7 @@ pub(crate) async fn prepare_decryption(
})
}
#[derive(Debug)]
#[derive(Default, Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
@@ -328,7 +306,7 @@ pub(crate) async fn get_autocrypt_peerstate(
if addr_cmp(&peerstate.addr, from) {
if allow_change {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql).await?;
peerstate.save_to_db(&context.sql, false).await?;
} else {
info!(
context,
@@ -344,7 +322,7 @@ pub(crate) async fn get_autocrypt_peerstate(
// to the database.
} else {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql).await?;
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
} else {

View File

@@ -428,7 +428,9 @@ mod tests {
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
let sent2_rfc724_mid = sent2.load_from_db().await.rfc724_mid;
let sent2_rfc724_mid = Message::load_from_db(&alice, sent2.sender_msg_id)
.await?
.rfc724_mid;
// not downloading the status update results in an placeholder
receive_imf_inner(

View File

@@ -147,6 +147,7 @@ mod tests {
use crate::chat;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::peerstate::ToSave;
use crate::test_utils::{bob_keypair, TestContext};
use super::*;
@@ -296,6 +297,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
vec![(Some(peerstate), addr)]

View File

@@ -226,13 +226,13 @@ pub(crate) async fn stock_ephemeral_timer_changed(
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
Timer::Enabled { duration } => match duration {
0..=59 => {
stock_str::msg_ephemeral_timer_enabled(context, &timer.to_string(), from_id).await
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await
}
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
61..=3599 => {
stock_str::msg_ephemeral_timer_minutes(
context,
&format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
from_id,
)
.await
@@ -241,7 +241,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
3601..=86399 => {
stock_str::msg_ephemeral_timer_hours(
context,
&format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
from_id,
)
.await
@@ -250,7 +250,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
86401..=604_799 => {
stock_str::msg_ephemeral_timer_days(
context,
&format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
from_id,
)
.await
@@ -259,7 +259,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
_ => {
stock_str::msg_ephemeral_timer_weeks(
context,
&format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
from_id,
)
.await

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
//! # IMAP capabilities
//!
//! IMAP server capabilities are determined with a `CAPABILITY` command.
use std::collections::HashMap;
#[derive(Debug)]
pub(crate) struct Capabilities {
/// True if the server has IDLE capability as defined in
/// <https://tools.ietf.org/html/rfc2177>
pub can_idle: bool,
/// True if the server has MOVE capability as defined in
/// <https://tools.ietf.org/html/rfc6851>
pub can_move: bool,
/// True if the server has QUOTA capability as defined in
/// <https://tools.ietf.org/html/rfc2087>
pub can_check_quota: bool,
/// True if the server has CONDSTORE capability as defined in
/// <https://tools.ietf.org/html/rfc7162>
pub can_condstore: bool,
/// Server ID if the server supports ID capability.
pub server_id: Option<HashMap<String, String>>,
}

View File

@@ -6,21 +6,17 @@ use std::{
use anyhow::{Context as _, Result};
use async_imap::Client as ImapClient;
use async_imap::Session as ImapSession;
use async_smtp::ServerAddress;
use tokio::net::{self, TcpStream};
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
use super::capabilities::Capabilities;
use super::session::Session;
use crate::login_param::{build_tls, Socks5Config};
use super::session::SessionStream;
/// IMAP write and read timeout in seconds.
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30);
const IMAP_TIMEOUT: u64 = 30;
#[derive(Debug)]
pub(crate) struct Client {
@@ -42,54 +38,27 @@ impl DerefMut for Client {
}
}
/// Determine server capabilities.
///
/// If server supports ID capability, send our client ID.
async fn determine_capabilities(
session: &mut ImapSession<Box<dyn SessionStream>>,
) -> Result<Capabilities> {
let caps = session
.capabilities()
.await
.context("CAPABILITY command error")?;
let server_id = if caps.has_str("ID") {
session.id([("name", Some("Delta Chat"))]).await?
} else {
None
};
let capabilities = Capabilities {
can_idle: caps.has_str("IDLE"),
can_move: caps.has_str("MOVE"),
can_check_quota: caps.has_str("QUOTA"),
can_condstore: caps.has_str("CONDSTORE"),
server_id,
};
Ok(capabilities)
}
impl Client {
pub(crate) async fn login(self, username: &str, password: &str) -> Result<Session> {
pub async fn login(self, username: &str, password: &str) -> Result<Session> {
let Client { inner, .. } = self;
let mut session = inner
let session = inner
.login(username, password)
.await
.map_err(|(err, _client)| err)?;
let capabilities = determine_capabilities(&mut session).await?;
Ok(Session::new(session, capabilities))
Ok(Session { inner: session })
}
pub(crate) async fn authenticate(
pub async fn authenticate(
self,
auth_type: &str,
authenticator: impl async_imap::Authenticator,
) -> Result<Session> {
let Client { inner, .. } = self;
let mut session = inner
let session = inner
.authenticate(auth_type, authenticator)
.await
.map_err(|(err, _client)| err)?;
let capabilities = determine_capabilities(&mut session).await?;
Ok(Session::new(session, capabilities))
Ok(Session { inner: session })
}
pub async fn connect_secure(
@@ -97,15 +66,9 @@ impl Client {
domain: &str,
strict_tls: bool,
) -> Result<Self> {
let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect(addr)).await??;
let mut timeout_stream = TimeoutStream::new(tcp_stream);
timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT));
timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT));
let timeout_stream = Box::pin(timeout_stream);
let stream = TcpStream::connect(addr).await?;
let tls = build_tls(strict_tls);
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(domain, timeout_stream).await?);
let tls_stream: Box<dyn SessionStream> = Box::new(tls.connect(domain, stream).await?);
let mut client = ImapClient::new(tls_stream);
let _greeting = client
@@ -120,12 +83,7 @@ impl Client {
}
pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> Result<Self> {
let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect(addr)).await??;
let mut timeout_stream = TimeoutStream::new(tcp_stream);
timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT));
timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT));
let timeout_stream = Box::pin(timeout_stream);
let stream: Box<dyn SessionStream> = Box::new(timeout_stream);
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
let mut client = ImapClient::new(stream);
let _greeting = client
@@ -146,7 +104,7 @@ impl Client {
) -> Result<Self> {
let socks5_stream: Box<dyn SessionStream> = Box::new(
socks5_config
.connect(target_addr, Some(IMAP_TIMEOUT))
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
.await?,
);
@@ -172,7 +130,7 @@ impl Client {
) -> Result<Self> {
let socks5_stream: Box<dyn SessionStream> = Box::new(
socks5_config
.connect(target_addr, Some(IMAP_TIMEOUT))
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
.await?,
);
@@ -188,7 +146,7 @@ impl Client {
})
}
pub async fn secure(self, domain: &str, strict_tls: bool) -> Result<Self> {
pub async fn secure(self, domain: &str, strict_tls: bool) -> Result<Client> {
if self.is_secure {
Ok(self)
} else {

View File

@@ -1,114 +1,113 @@
use super::Imap;
use anyhow::{bail, Context as _, Result};
use async_channel::Receiver;
use async_imap::extensions::idle::IdleResponse;
use futures_lite::FutureExt;
use std::time::{Duration, SystemTime};
use super::session::Session;
use crate::imap::client::IMAP_TIMEOUT;
use crate::{context::Context, scheduler::InterruptInfo};
const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
use super::session::Session;
impl Imap {
pub fn can_idle(&self) -> bool {
self.config.can_idle
}
impl Session {
pub async fn idle(
mut self,
&mut self,
context: &Context,
idle_interrupt_receiver: Receiver<InterruptInfo>,
watch_folder: Option<String>,
) -> Result<(Self, InterruptInfo)> {
) -> Result<InterruptInfo> {
use futures::future::FutureExt;
if !self.can_idle() {
bail!("IMAP server does not have IDLE capability");
}
let mut info = Default::default();
self.prepare(context).await?;
self.select_folder(context, watch_folder.as_deref()).await?;
let timeout = Duration::from_secs(23 * 60);
let mut info = Default::default();
if self.server_sent_unsolicited_exists(context)? {
return Ok((self, info));
return Ok(info);
}
if let Ok(info) = idle_interrupt_receiver.try_recv() {
info!(context, "skip idle, got interrupt {:?}", info);
return Ok((self, info));
if let Some(session) = self.session.take() {
if let Ok(info) = self.idle_interrupt.try_recv() {
info!(context, "skip idle, got interrupt {:?}", info);
self.session = Some(session);
return Ok(info);
}
let mut handle = session.idle();
if let Err(err) = handle.init().await {
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
}
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
enum Event {
IdleResponse(IdleResponse),
Interrupt(InterruptInfo),
}
let folder_name = watch_folder.as_deref().unwrap_or("None");
info!(
context,
"{}: Idle entering wait-on-remote state", folder_name
);
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let info = self.idle_interrupt.recv().await;
// cancel imap idle connection properly
drop(interrupt);
Ok(Event::Interrupt(info.unwrap_or_default()))
});
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(
context,
"{}: Idle-wait timeout or interruption", folder_name
);
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(
context,
"{}: Idle wait was interrupted manually", folder_name
);
}
Ok(Event::Interrupt(i)) => {
info!(
context,
"{}: Idle wait was interrupted: {:?}", folder_name, &i
);
info = i;
}
Err(err) => {
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
}
}
let session = tokio::time::timeout(Duration::from_secs(15), handle.done())
.await
.with_context(|| format!("{}: IMAP IDLE protocol timed out", folder_name))?
.with_context(|| format!("{}: IMAP IDLE failed", folder_name))?;
self.session = Some(Session { inner: session });
} else {
warn!(context, "Attempted to idle without a session");
}
let mut handle = self.inner.idle();
if let Err(err) = handle.init().await {
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
}
// At this point IDLE command was sent and we received a "+ idling" response. We will now
// read from the stream without getting any data for up to `IDLE_TIMEOUT`. If we don't
// disable read timeout, we would get a timeout after `IMAP_TIMEOUT`, which is a lot
// shorter than `IDLE_TIMEOUT`.
handle.as_mut().set_read_timeout(None);
let (idle_wait, interrupt) = handle.wait_with_timeout(IDLE_TIMEOUT);
enum Event {
IdleResponse(IdleResponse),
Interrupt(InterruptInfo),
}
let folder_name = watch_folder.as_deref().unwrap_or("None");
info!(
context,
"{}: Idle entering wait-on-remote state", folder_name
);
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let info = idle_interrupt_receiver.recv().await;
// cancel imap idle connection properly
drop(interrupt);
Ok(Event::Interrupt(info.unwrap_or_default()))
});
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(
context,
"{}: Idle-wait timeout or interruption", folder_name
);
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(
context,
"{}: Idle wait was interrupted manually", folder_name
);
}
Ok(Event::Interrupt(i)) => {
info!(
context,
"{}: Idle wait was interrupted: {:?}", folder_name, &i
);
info = i;
}
Err(err) => {
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
}
}
let mut session = tokio::time::timeout(Duration::from_secs(15), handle.done())
.await
.with_context(|| format!("{}: IMAP IDLE protocol timed out", folder_name))?
.with_context(|| format!("{}: IMAP IDLE failed", folder_name))?;
session.as_mut().set_read_timeout(Some(IMAP_TIMEOUT));
self.inner = session;
Ok((self, info))
Ok(info)
}
}
impl Imap {
pub(crate) async fn fake_idle(
&mut self,
context: &Context,
@@ -124,11 +123,7 @@ impl Imap {
watch_folder
} else {
info!(context, "IMAP-fake-IDLE: no folder, waiting for interrupt");
return self
.idle_interrupt_receiver
.recv()
.await
.unwrap_or_default();
return self.idle_interrupt.recv().await.unwrap_or_default();
};
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
@@ -147,7 +142,7 @@ impl Imap {
.tick()
.map(|_| Event::Tick)
.race(
self.idle_interrupt_receiver
self.idle_interrupt
.recv()
.map(|probe_network| Event::Interrupt(probe_network.unwrap_or_default())),
)
@@ -161,11 +156,9 @@ impl Imap {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if let Some(session) = &self.session {
if session.can_idle() {
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false);
}
if self.config.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false);
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
@@ -184,7 +177,7 @@ impl Imap {
}
Err(err) => {
error!(context, "could not fetch from folder: {:#}", err);
self.trigger_reconnect(context);
self.trigger_reconnect(context).await;
}
}
}

View File

@@ -63,18 +63,16 @@ impl Imap {
// Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
let session = self.session.as_mut().context("no session")?;
// Drain leftover unsolicited EXISTS messages
session.server_sent_unsolicited_exists(context)?;
self.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, folder.name(), is_spam_folder)
.await
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
let session = self.session.as_mut().context("no session")?;
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !session.server_sent_unsolicited_exists(context)? {
if !self.server_sent_unsolicited_exists(context)? {
break;
}
}

View File

@@ -1,4 +1,4 @@
use super::session::Session as ImapSession;
use super::Imap;
use crate::context::Context;
use anyhow::Context as _;
@@ -7,6 +7,9 @@ type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IMAP Could not obtain imap-session object.")]
NoSession,
#[error("IMAP Connection Lost or no connection established")]
ConnectionLost,
@@ -26,38 +29,55 @@ impl From<anyhow::Error> for Error {
}
}
impl ImapSession {
/// Issues a CLOSE command if selected folder needs expunge,
/// i.e. if Delta Chat marked a message there as deleted previously.
impl Imap {
/// Issues a CLOSE command to expunge selected folder.
///
/// CLOSE is considerably faster than an EXPUNGE, see
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if let Some(folder) = &self.selected_folder {
if self.selected_folder_needs_expunge {
info!(context, "Expunge messages in \"{}\".", folder);
pub(super) async fn close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if let Some(ref folder) = self.config.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
self.close().await.context("IMAP close/expunge failed")?;
info!(context, "close/expunge succeeded");
self.selected_folder = None;
self.selected_folder_needs_expunge = false;
let session = self.session.as_mut().context("no session")?;
if let Err(err) = session.close().await.context("IMAP close/expunge failed") {
self.trigger_reconnect(context).await;
return Err(err);
}
info!(context, "close/expunge succeeded");
}
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
Ok(())
}
/// Issues a CLOSE command if selected folder needs expunge.
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if self.config.selected_folder_needs_expunge {
self.close_folder(context).await?;
}
Ok(())
}
/// Selects a folder, possibly updating uid_validity and, if needed,
/// expunging the folder to remove delete-marked messages.
/// select a folder, possibly update uid_validity and, if needed,
/// expunge the folder to remove delete-marked messages.
/// Returns whether a new folder was selected.
pub(super) async fn select_folder(
&mut self,
context: &Context,
folder: Option<&str>,
) -> Result<NewlySelected> {
if self.session.is_none() {
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
self.trigger_reconnect(context).await;
return Err(Error::NoSession);
}
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(folder) = folder {
if let Some(selected_folder) = &self.selected_folder {
if let Some(ref selected_folder) = self.config.selected_folder {
if folder == selected_folder {
return Ok(NewlySelected::No);
}
@@ -69,30 +89,42 @@ impl ImapSession {
// select new folder
if let Some(folder) = folder {
let res = if self.can_condstore() {
self.select_condstore(folder).await
if let Some(ref mut session) = &mut self.session {
let res = if self.config.can_condstore {
session.select_condstore(folder).await
} else {
session.select(folder).await
};
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// says that if the server reports select failure we are in
// authenticated (not-select) state.
match res {
Ok(mailbox) => {
self.config.selected_folder = Some(folder.to_string());
self.config.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::ConnectionLost) => {
self.trigger_reconnect(context).await;
self.config.selected_folder = None;
Err(Error::ConnectionLost)
}
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.to_string()))
}
Err(async_imap::error::Error::No(response)) => {
Err(Error::NoFolder(folder.to_string(), response))
}
Err(err) => {
self.config.selected_folder = None;
self.trigger_reconnect(context).await;
Err(Error::Other(err.to_string()))
}
}
} else {
self.select(folder).await
};
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// says that if the server reports select failure we are in
// authenticated (not-select) state.
match res {
Ok(mailbox) => {
self.selected_folder = Some(folder.to_string());
self.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::ConnectionLost) => Err(Error::ConnectionLost),
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.to_string()))
}
Err(async_imap::error::Error::No(response)) => {
Err(Error::NoFolder(folder.to_string(), response))
}
Err(err) => Err(Error::Other(err.to_string())),
Err(Error::NoSession)
}
} else {
Ok(NewlySelected::No)
@@ -109,7 +141,8 @@ impl ImapSession {
Ok(newly_selected) => Ok(newly_selected),
Err(err) => match err {
Error::NoFolder(..) => {
self.create(folder).await.with_context(|| {
let session = self.session.as_mut().context("no IMAP session")?;
session.create(folder).await.with_context(|| {
format!("Couldn't select folder ('{}'), then create() failed", err)
})?;
@@ -120,7 +153,6 @@ impl ImapSession {
}
}
}
#[derive(PartialEq, Debug, Copy, Clone, Eq)]
pub(super) enum NewlySelected {
/// The folder was newly selected during this call to select_folder().

View File

@@ -1,59 +1,24 @@
use std::ops::{Deref, DerefMut};
use std::pin::Pin;
use std::time::Duration;
use async_imap::types::Mailbox;
use async_imap::Session as ImapSession;
use async_native_tls::TlsStream;
use fast_socks5::client::Socks5Stream;
use tokio::net::TcpStream;
use tokio_io_timeout::TimeoutStream;
use super::capabilities::Capabilities;
#[derive(Debug)]
pub(crate) struct Session {
pub(super) inner: ImapSession<Box<dyn SessionStream>>,
pub capabilities: Capabilities,
/// Selected folder name.
pub selected_folder: Option<String>,
/// Mailbox structure returned by IMAP server.
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
}
pub(crate) trait SessionStream:
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync + std::fmt::Debug
{
/// Change the read timeout on the session stream.
fn set_read_timeout(&mut self, timeout: Option<Duration>);
}
impl SessionStream for TlsStream<Box<dyn SessionStream>> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
}
impl SessionStream for TlsStream<Pin<Box<TimeoutStream<TcpStream>>>> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
}
impl SessionStream for Pin<Box<TimeoutStream<TcpStream>>> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.as_mut().set_read_timeout_pinned(timeout);
}
}
impl SessionStream for Socks5Stream<TcpStream> {
fn set_read_timeout(&mut self, _timeout: Option<Duration>) {
// FIXME: build SOCKS streams on top of TimeoutStream, not directly TcpStream,
// so we can set a read timeout for them.
}
}
impl SessionStream for TlsStream<Box<dyn SessionStream>> {}
impl SessionStream for TlsStream<TcpStream> {}
impl SessionStream for TcpStream {}
impl SessionStream for Socks5Stream<TcpStream> {}
impl Deref for Session {
type Target = ImapSession<Box<dyn SessionStream>>;
@@ -70,32 +35,8 @@ impl DerefMut for Session {
}
impl Session {
pub(crate) fn new(
inner: ImapSession<Box<dyn SessionStream>>,
capabilities: Capabilities,
) -> Self {
Self {
inner,
capabilities,
selected_folder: None,
selected_mailbox: None,
selected_folder_needs_expunge: false,
}
}
pub fn can_idle(&self) -> bool {
self.capabilities.can_idle
}
pub fn can_move(&self) -> bool {
self.capabilities.can_move
}
pub fn can_check_quota(&self) -> bool {
self.capabilities.can_check_quota
}
pub fn can_condstore(&self) -> bool {
self.capabilities.can_condstore
pub fn idle(self) -> async_imap::extensions::idle::Handle<Box<dyn SessionStream>> {
let Session { inner } = self;
inner.idle()
}
}

View File

@@ -169,7 +169,7 @@ impl LoginParam {
async fn from_database(context: &Context, prefix: &str) -> Result<Self> {
let sql = &context.sql;
let key = &format!("{}addr", prefix);
let key = format!("{}addr", prefix);
let addr = sql
.get_raw_config(key)
.await?
@@ -177,26 +177,26 @@ impl LoginParam {
.trim()
.to_string();
let key = &format!("{}mail_server", prefix);
let key = format!("{}mail_server", prefix);
let mail_server = sql.get_raw_config(key).await?.unwrap_or_default();
let key = &format!("{}mail_port", prefix);
let key = format!("{}mail_port", prefix);
let mail_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
let key = &format!("{}mail_user", prefix);
let key = format!("{}mail_user", prefix);
let mail_user = sql.get_raw_config(key).await?.unwrap_or_default();
let key = &format!("{}mail_pw", prefix);
let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_raw_config(key).await?.unwrap_or_default();
let key = &format!("{}mail_security", prefix);
let key = format!("{}mail_security", prefix);
let mail_security = sql
.get_raw_config_int(key)
.await?
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let key = &format!("{}imap_certificate_checks", prefix);
let key = format!("{}imap_certificate_checks", prefix);
let imap_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
@@ -204,26 +204,26 @@ impl LoginParam {
Default::default()
};
let key = &format!("{}send_server", prefix);
let key = format!("{}send_server", prefix);
let send_server = sql.get_raw_config(key).await?.unwrap_or_default();
let key = &format!("{}send_port", prefix);
let key = format!("{}send_port", prefix);
let send_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
let key = &format!("{}send_user", prefix);
let key = format!("{}send_user", prefix);
let send_user = sql.get_raw_config(key).await?.unwrap_or_default();
let key = &format!("{}send_pw", prefix);
let key = format!("{}send_pw", prefix);
let send_pw = sql.get_raw_config(key).await?.unwrap_or_default();
let key = &format!("{}send_security", prefix);
let key = format!("{}send_security", prefix);
let send_security = sql
.get_raw_config_int(key)
.await?
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let key = &format!("{}smtp_certificate_checks", prefix);
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_or_default()
@@ -231,11 +231,11 @@ impl LoginParam {
Default::default()
};
let key = &format!("{}server_flags", prefix);
let key = format!("{}server_flags", prefix);
let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default();
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
let key = &format!("{}provider", prefix);
let key = format!("{}provider", prefix);
let provider = sql
.get_raw_config(key)
.await?
@@ -275,50 +275,50 @@ impl LoginParam {
context.set_primary_self_addr(&self.addr).await?;
let key = &format!("{}mail_server", prefix);
let key = format!("{}mail_server", prefix);
sql.set_raw_config(key, Some(&self.imap.server)).await?;
let key = &format!("{}mail_port", prefix);
let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(key, i32::from(self.imap.port))
.await?;
let key = &format!("{}mail_user", prefix);
let key = format!("{}mail_user", prefix);
sql.set_raw_config(key, Some(&self.imap.user)).await?;
let key = &format!("{}mail_pw", prefix);
let key = format!("{}mail_pw", prefix);
sql.set_raw_config(key, Some(&self.imap.password)).await?;
let key = &format!("{}mail_security", prefix);
let key = format!("{}mail_security", prefix);
sql.set_raw_config_int(key, self.imap.security as i32)
.await?;
let key = &format!("{}imap_certificate_checks", prefix);
let key = format!("{}imap_certificate_checks", prefix);
sql.set_raw_config_int(key, self.imap.certificate_checks as i32)
.await?;
let key = &format!("{}send_server", prefix);
let key = format!("{}send_server", prefix);
sql.set_raw_config(key, Some(&self.smtp.server)).await?;
let key = &format!("{}send_port", prefix);
let key = format!("{}send_port", prefix);
sql.set_raw_config_int(key, i32::from(self.smtp.port))
.await?;
let key = &format!("{}send_user", prefix);
let key = format!("{}send_user", prefix);
sql.set_raw_config(key, Some(&self.smtp.user)).await?;
let key = &format!("{}send_pw", prefix);
let key = format!("{}send_pw", prefix);
sql.set_raw_config(key, Some(&self.smtp.password)).await?;
let key = &format!("{}send_security", prefix);
let key = format!("{}send_security", prefix);
sql.set_raw_config_int(key, self.smtp.security as i32)
.await?;
let key = &format!("{}smtp_certificate_checks", prefix);
let key = format!("{}smtp_certificate_checks", prefix);
sql.set_raw_config_int(key, self.smtp.certificate_checks as i32)
.await?;
// The OAuth2 flag is either set for both IMAP and SMTP or not at all.
let key = &format!("{}server_flags", prefix);
let key = format!("{}server_flags", prefix);
let server_flags = match self.imap.oauth2 {
true => DC_LP_AUTH_OAUTH2,
false => DC_LP_AUTH_NORMAL,
@@ -326,7 +326,7 @@ impl LoginParam {
sql.set_raw_config_int(key, server_flags).await?;
if let Some(provider) = self.provider {
let key = &format!("{}provider", prefix);
let key = format!("{}provider", prefix);
sql.set_raw_config(key, Some(provider.id)).await?;
}

View File

@@ -8,6 +8,7 @@ use deltachat_derive::{FromSql, ToSql};
use rusqlite::types::ValueRef;
use serde::{Deserialize, Serialize};
use crate::chat::EncryptionModus;
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::{
@@ -302,6 +303,7 @@ impl Message {
" m.hidden AS hidden,",
" m.location_id AS location,",
" c.blocked AS blocked",
" m.encryption_modus as encryption_modus",
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
" WHERE m.id=?;"
),
@@ -848,10 +850,39 @@ impl Message {
}
/// Force the message to be sent in plain text.
/// Deprecated: use Message::set_encryption_modus(EncryptionModus::ForcePlaintext)
pub fn force_plaintext(&mut self) {
self.param.set_int(Param::ForcePlaintext, 1);
}
pub async fn set_encryption_modus(
&mut self,
context: &Context,
encryption_modus: EncryptionModus,
) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs SET encryption_modus=? WHERE id=?;",
paramsv![encryption_modus, self.id],
)
.await?;
Ok(())
}
pub async fn encryption_modus(&self, context: &Context) -> Result<Option<EncryptionModus>> {
let encryption_modus: Option<EncryptionModus> = context
.sql
.query_get_value(
"SELECT encryption_modus FROM msgs WHERE id=?;",
paramsv![self.id],
)
.await?;
Ok(encryption_modus)
}
pub async fn update_param(&self, context: &Context) -> Result<()> {
context
.sql

View File

@@ -9,6 +9,7 @@ use tokio::fs;
use crate::blob::BlobObject;
use crate::chat::Chat;
use crate::chat::EncryptionModus;
use crate::config::Config;
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
use crate::contact::Contact;
@@ -324,7 +325,7 @@ impl<'a> MimeFactory<'a> {
}
}
fn should_force_plaintext(&self) -> bool {
fn should_force_plaintext(&self, encryption_modus: &EncryptionModus) -> bool {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
@@ -333,6 +334,8 @@ impl<'a> MimeFactory<'a> {
// encryption may disclose recipients;
// this is probably a worse issue than not opportunistically (!) encrypting
true
} else if encryption_modus == &EncryptionModus::ForcePlaintext {
true
} else {
self.msg
.param
@@ -429,7 +432,7 @@ impl<'a> MimeFactory<'a> {
}
}
let self_name = &match context.get_config(Config::Displayname).await? {
let self_name = match context.get_config(Config::Displayname).await? {
Some(name) => name,
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
};
@@ -602,7 +605,11 @@ impl<'a> MimeFactory<'a> {
let min_verified = self.min_verified();
let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext();
let encryption_modus = match self.msg.encryption_modus(context).await? {
Some(encryption_modus) => encryption_modus,
None => EncryptionModus::Opportunistic,
};
let force_plaintext = self.should_force_plaintext(&encryption_modus);
let skip_autocrypt = self.should_skip_autocrypt();
let e2ee_guaranteed = self.is_e2ee_guaranteed();
let encrypt_helper = EncryptHelper::new(context).await?;
@@ -644,6 +651,34 @@ impl<'a> MimeFactory<'a> {
encrypt_helper.should_encrypt(context, e2ee_guaranteed, &peerstates)?;
let is_encrypted = should_encrypt && !force_plaintext;
// Ensure we fulfill encryption_modus
match encryption_modus {
EncryptionModus::Opportunistic => {}
EncryptionModus::ForcePlaintext => {
ensure!(
!is_encrypted,
"EncryptionModus is ForcePlaintext but message is encrypted"
);
}
EncryptionModus::ForceEncrypted => {
ensure!(
is_encrypted,
"EncryptionModus is ForceEncrypted but message is unencrypted"
);
}
EncryptionModus::ForceVerified => {
let chat_is_protected = if let Loaded::Message { chat } = &self.loaded {
chat.is_protected()
} else {
false
};
ensure!(
is_encrypted && chat_is_protected,
"EncryptionModus is ForceVerified but chat is not protected"
);
}
};
let message = if parts.is_empty() {
// Single part, render as regular message.
main_part
@@ -1259,7 +1294,7 @@ impl<'a> MimeFactory<'a> {
.truncated_text(32)
.to_string()
};
let p2 = stock_str::read_rcpt_mail_body(context, &p1).await;
let p2 = stock_str::read_rcpt_mail_body(context, p1).await;
let message_text = format!("{}\r\n", format_flowed(&p2));
message = message.child(
PartBuilder::new()
@@ -1733,11 +1768,11 @@ mod tests {
}
async fn get_subject(
t: &TestContext,
sent: crate::test_utils::SentMessage<'_>,
sent: crate::test_utils::SentMessage,
) -> Result<String> {
let parsed_subject = t.parse_msg(&sent).await.get_subject().unwrap();
let sent_msg = sent.load_from_db().await;
let sent_msg = Message::load_from_db(t, sent.sender_msg_id).await?;
assert_eq!(parsed_subject, sent_msg.subject);
Ok(parsed_subject)

View File

@@ -21,6 +21,7 @@ use crate::events::EventType;
use crate::format_flowed::unformat_flowed;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::Fingerprint;
use crate::location;
use crate::message::{self, Viewtype};
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
@@ -28,7 +29,6 @@ use crate::simplify::{simplify, SimplifiedText};
use crate::stock_str;
use crate::sync::SyncItems;
use crate::tools::{get_filemeta, parse_receive_headers, truncate_by_lines};
use crate::{location, tools};
/// A parsed MIME message.
///
@@ -46,7 +46,7 @@ pub struct MimeMessage {
/// Addresses are normalized and lowercased:
pub recipients: Vec<SingleInfo>,
pub from: SingleInfo,
pub from: Vec<SingleInfo>,
/// Whether the From address was repeated in the signed part
/// (and we know that the signer intended to send from this address)
pub from_is_signed: bool,
@@ -166,7 +166,7 @@ impl MimeMessage {
///
/// If `partial` is set, it contains the full message size in bytes
/// and `body` contains the header only.
pub(crate) async fn from_bytes_with_partial(
pub async fn from_bytes_with_partial(
context: &Context,
body: &[u8],
partial: Option<u32>,
@@ -216,21 +216,11 @@ impl MimeMessage {
headers.remove("secure-join-fingerprint");
headers.remove("chat-verified");
let is_thunderbird = headers
.get("user-agent")
.map_or(false, |user_agent| user_agent.contains("Thunderbird"));
if is_thunderbird {
info!(context, "Detected Thunderbird");
}
let from = from.context("No from in message")?;
let mut decryption_info =
prepare_decryption(context, &mail, &from.addr, message_time, is_thunderbird).await?;
// Memory location for a possible decrypted message.
let mut mail_raw = Vec::new();
let mut gossiped_addr = Default::default();
let mut from_is_signed = false;
let mut decryption_info = prepare_decryption(context, &mail, &from, message_time).await?;
hop_info += "\n\n";
hop_info += &decryption_info.dkim_results.to_string();
@@ -265,7 +255,7 @@ impl MimeMessage {
// Signature was checked for original From, so we
// do not allow overriding it.
let mut signed_from = None;
let mut signed_from = Vec::new();
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
@@ -280,21 +270,23 @@ impl MimeMessage {
&mut chat_disposition_notification_to,
&decrypted_mail.headers,
);
if let Some(signed_from) = signed_from {
if addr_cmp(&signed_from.addr, &from.addr) {
from_is_signed = true;
} else {
// There is a From: header in the encrypted &
// signed part, but it doesn't match the outer one.
// This _might_ be because the sender's mail server
// replaced the sending address, e.g. in a mailing list.
// Or it's because someone is doing some replay attack
// - OTOH, I can't come up with an attack scenario
// where this would be useful.
warn!(
context,
"From header in signed part does't match the outer one"
);
if let Some(signed_from) = signed_from.first() {
if let Some(from) = from.first() {
if addr_cmp(&signed_from.addr, &from.addr) {
from_is_signed = true;
} else {
// There is a From: header in the encrypted &
// signed part, but it doesn't match the outer one.
// This _might_ be because the sender's mail server
// replaced the sending address, e.g. in a mailing list.
// Or it's because someone is doing some replay attack
// - OTOH, I can't come up with an attack scenario
// where this would be useful.
warn!(
context,
"From header in signed part does't match the outer one"
);
}
}
}
@@ -310,7 +302,7 @@ impl MimeMessage {
// && decryption_info.dkim_results.allow_keychange
{
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql).await?;
peerstate.save_to_db(&context.sql, false).await?;
}
}
(Ok(mail), HashSet::new(), false)
@@ -590,18 +582,21 @@ impl MimeMessage {
// See if an MDN is requested from the other side
if !self.decrypting_failed && !self.parts.is_empty() {
if let Some(ref dn_to) = self.chat_disposition_notification_to {
// Check that the message is not outgoing.
let from = &self.from.addr;
if !context.is_self_addr(from).await? {
if from.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
if let Some(from) = self.from.get(0) {
// Check that the message is not outgoing.
if !context.is_self_addr(&from.addr).await? {
if from.addr.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring",
from.addr,
dn_to.addr
);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring", from, dn_to.addr
);
}
}
}
@@ -1231,7 +1226,7 @@ impl MimeMessage {
context: &Context,
headers: &mut HashMap<String, String>,
recipients: &mut Vec<SingleInfo>,
from: &mut Option<SingleInfo>,
from: &mut Vec<SingleInfo>,
list_post: &mut Option<String>,
chat_disposition_notification_to: &mut Option<SingleInfo>,
fields: &[mailparse::MailHeader<'_>],
@@ -1260,7 +1255,7 @@ impl MimeMessage {
*recipients = recipients_new;
}
let from_new = get_from(fields);
if from_new.is_some() {
if !from_new.is_empty() {
*from = from_new;
}
let list_post_new = get_list_post(fields);
@@ -1586,11 +1581,11 @@ async fn update_gossip_peerstates(
let peerstate;
if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
p.apply_gossip(&header, message_time);
p.save_to_db(&context.sql).await?;
p.save_to_db(&context.sql, false).await?;
peerstate = p;
} else {
let p = Peerstate::from_gossip(&header, message_time);
p.save_to_db(&context.sql).await?;
p.save_to_db(&context.sql, true).await?;
peerstate = p;
};
peerstate
@@ -1805,9 +1800,8 @@ pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
}
/// Returned addresses are normalized and lowercased.
pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
let all = get_all_addresses_from_header(headers, |header_key| header_key == "from");
tools::single_value(all)
pub(crate) fn get_from(headers: &[MailHeader]) -> Vec<SingleInfo> {
get_all_addresses_from_header(headers, |header_key| header_key == "from")
}
/// Returned addresses are normalized and lowercased.
@@ -1883,35 +1877,35 @@ mod tests {
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi")
.await
.unwrap();
let contact = mimemsg.from;
let contact = mimemsg.from.first().unwrap();
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, None);
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi")
.await
.unwrap();
let contact = mimemsg.from;
let contact = mimemsg.from.first().unwrap();
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, None);
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: <g@c.de>\n\nhi")
.await
.unwrap();
let contact = mimemsg.from;
let contact = mimemsg.from.first().unwrap();
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, None);
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C <g@c.de>\n\nhi")
.await
.unwrap();
let contact = mimemsg.from;
let contact = mimemsg.from.first().unwrap();
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, Some("Goetz C".to_string()));
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" <g@c.de>\n\nhi")
.await
.unwrap();
let contact = mimemsg.from;
let contact = mimemsg.from.first().unwrap();
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, Some("Goetz C".to_string()));
@@ -1919,7 +1913,7 @@ mod tests {
MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C <g@c.de>\n\nhi")
.await
.unwrap();
let contact = mimemsg.from;
let contact = mimemsg.from.first().unwrap();
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, Some("Götz C".to_string()));
@@ -1929,7 +1923,7 @@ mod tests {
MimeMessage::from_bytes(&ctx, b"From: \"=?utf-8?q?G=C3=B6tz?= C\" <g@c.de>\n\nhi")
.await
.unwrap();
let contact = mimemsg.from;
let contact = mimemsg.from.first().unwrap();
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, Some("Götz C".to_string()));
}
@@ -2161,9 +2155,14 @@ mod tests {
test1\n\
";
let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await;
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert!(mimeparser.is_err());
let of = &mimeparser.from[0];
assert_eq!(of.addr, "hello@one.org");
assert!(mimeparser.chat_disposition_notification_to.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2202,7 +2201,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mimeparser_with_context() {
let context = TestContext::new().await;
let raw = b"From: hello@example.org\n\
let raw = b"From: hello\n\
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
Subject: outer-subject\n\
Secure-Join-Group: no\n\

View File

@@ -51,6 +51,7 @@ pub enum Param {
ErroneousE2ee = b'e',
/// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
/// Deprecated: Use EncryptionModus::ForcePlaintext
ForcePlaintext = b'u',
/// For Messages: do not include Autocrypt header.

View File

@@ -4,7 +4,7 @@ use std::collections::HashSet;
use std::fmt;
use crate::aheader::{Aheader, EncryptPreference};
use crate::chat::{self, Chat};
use crate::chat::{self, is_contact_in_chat, Chat};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::{addr_cmp, Contact, Origin};
@@ -46,6 +46,7 @@ pub struct Peerstate {
pub gossip_key_fingerprint: Option<Fingerprint>,
pub verified_key: Option<SignedPublicKey>,
pub verified_key_fingerprint: Option<Fingerprint>,
pub to_save: Option<ToSave>,
pub fingerprint_changed: bool,
}
@@ -62,6 +63,7 @@ impl PartialEq for Peerstate {
&& self.gossip_key_fingerprint == other.gossip_key_fingerprint
&& self.verified_key == other.verified_key
&& self.verified_key_fingerprint == other.verified_key_fingerprint
&& self.to_save == other.to_save
&& self.fingerprint_changed == other.fingerprint_changed
}
}
@@ -82,11 +84,19 @@ impl fmt::Debug for Peerstate {
.field("gossip_key_fingerprint", &self.gossip_key_fingerprint)
.field("verified_key", &self.verified_key)
.field("verified_key_fingerprint", &self.verified_key_fingerprint)
.field("to_save", &self.to_save)
.field("fingerprint_changed", &self.fingerprint_changed)
.finish()
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum ToSave {
Timestamps = 0x01,
All = 0x02,
}
impl Peerstate {
pub fn from_header(header: &Aheader, message_time: i64) -> Self {
Peerstate {
@@ -101,6 +111,7 @@ impl Peerstate {
gossip_timestamp: 0,
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
}
}
@@ -126,6 +137,7 @@ impl Peerstate {
gossip_timestamp: message_time,
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
}
}
@@ -217,6 +229,7 @@ impl Peerstate {
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
to_save: None,
fingerprint_changed: false,
};
@@ -235,10 +248,14 @@ impl Peerstate {
let old_public_fingerprint = self.public_key_fingerprint.take();
self.public_key_fingerprint = Some(public_key.fingerprint());
if old_public_fingerprint.is_some()
&& old_public_fingerprint != self.public_key_fingerprint
if old_public_fingerprint.is_none()
|| self.public_key_fingerprint.is_none()
|| old_public_fingerprint != self.public_key_fingerprint
{
self.fingerprint_changed = true;
self.to_save = Some(ToSave::All);
if old_public_fingerprint.is_some() {
self.fingerprint_changed = true;
}
}
}
@@ -250,6 +267,8 @@ impl Peerstate {
|| self.gossip_key_fingerprint.is_none()
|| old_gossip_fingerprint != self.gossip_key_fingerprint
{
self.to_save = Some(ToSave::All);
// Warn about gossip key change only if there is no public key obtained from
// Autocrypt header, which overrides gossip key.
if old_gossip_fingerprint.is_some() && self.public_key_fingerprint.is_none() {
@@ -262,6 +281,7 @@ impl Peerstate {
pub fn degrade_encryption(&mut self, message_time: i64) {
self.prefer_encrypt = EncryptPreference::Reset;
self.last_seen = message_time;
self.to_save = Some(ToSave::All);
}
pub fn apply_header(&mut self, header: &Aheader, message_time: i64) {
@@ -272,16 +292,19 @@ impl Peerstate {
if message_time > self.last_seen {
self.last_seen = message_time;
self.last_seen_autocrypt = message_time;
self.to_save = Some(ToSave::Timestamps);
if (header.prefer_encrypt == EncryptPreference::Mutual
|| header.prefer_encrypt == EncryptPreference::NoPreference)
&& header.prefer_encrypt != self.prefer_encrypt
{
self.prefer_encrypt = header.prefer_encrypt;
self.to_save = Some(ToSave::All)
}
if self.public_key.as_ref() != Some(&header.public_key) {
self.public_key = Some(header.public_key.clone());
self.recalc_fingerprint();
self.to_save = Some(ToSave::All);
}
}
}
@@ -293,9 +316,11 @@ impl Peerstate {
if message_time > self.gossip_timestamp {
self.gossip_timestamp = message_time;
self.to_save = Some(ToSave::Timestamps);
if self.gossip_key.as_ref() != Some(&gossip_header.public_key) {
self.gossip_key = Some(gossip_header.public_key.clone());
self.recalc_fingerprint();
self.to_save = Some(ToSave::All)
}
// This is non-standard.
@@ -314,6 +339,7 @@ impl Peerstate {
&& gossip_header.prefer_encrypt == EncryptPreference::Mutual
{
self.prefer_encrypt = EncryptPreference::Mutual;
self.to_save = Some(ToSave::All);
}
};
}
@@ -369,6 +395,7 @@ impl Peerstate {
if self.public_key_fingerprint.is_some()
&& self.public_key_fingerprint.as_ref().unwrap() == fingerprint
{
self.to_save = Some(ToSave::All);
self.verified_key = self.public_key.clone();
self.verified_key_fingerprint = self.public_key_fingerprint.clone();
true
@@ -380,6 +407,7 @@ impl Peerstate {
if self.gossip_key_fingerprint.is_some()
&& self.gossip_key_fingerprint.as_ref().unwrap() == fingerprint
{
self.to_save = Some(ToSave::All);
self.verified_key = self.gossip_key.clone();
self.verified_key_fingerprint = self.gossip_key_fingerprint.clone();
true
@@ -393,48 +421,66 @@ impl Peerstate {
}
}
pub async fn save_to_db(&self, sql: &Sql) -> Result<()> {
sql.execute(
"INSERT INTO acpeerstates (
last_seen,
last_seen_autocrypt,
prefer_encrypted,
public_key,
gossip_timestamp,
gossip_key,
public_key_fingerprint,
gossip_key_fingerprint,
verified_key,
verified_key_fingerprint,
addr)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT (addr)
DO UPDATE SET
last_seen = excluded.last_seen,
last_seen_autocrypt = excluded.last_seen_autocrypt,
prefer_encrypted = excluded.prefer_encrypted,
public_key = excluded.public_key,
gossip_timestamp = excluded.gossip_timestamp,
gossip_key = excluded.gossip_key,
public_key_fingerprint = excluded.public_key_fingerprint,
gossip_key_fingerprint = excluded.gossip_key_fingerprint,
verified_key = excluded.verified_key,
verified_key_fingerprint = excluded.verified_key_fingerprint",
paramsv![
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()),
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.addr,
],
)
.await?;
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> Result<()> {
if self.to_save == Some(ToSave::All) || create {
sql.execute(
if create {
"INSERT INTO acpeerstates ( \
last_seen, \
last_seen_autocrypt, \
prefer_encrypted, \
public_key, \
gossip_timestamp, \
gossip_key, \
public_key_fingerprint, \
gossip_key_fingerprint, \
verified_key, \
verified_key_fingerprint, \
addr \
) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
} else {
"UPDATE acpeerstates \
SET last_seen=?, \
last_seen_autocrypt=?, \
prefer_encrypted=?, \
public_key=?, \
gossip_timestamp=?, \
gossip_key=?, \
public_key_fingerprint=?, \
gossip_key_fingerprint=?, \
verified_key=?, \
verified_key_fingerprint=? \
WHERE addr=?"
},
paramsv![
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()),
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.addr,
],
)
.await?;
} else if self.to_save == Some(ToSave::Timestamps) {
sql.execute(
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
WHERE addr=?;",
paramsv![
self.last_seen,
self.last_seen_autocrypt,
self.gossip_timestamp,
self.addr
],
)
.await?;
}
Ok(())
}
@@ -474,7 +520,7 @@ impl Peerstate {
let chats = Chatlist::try_load(context, 0, None, Some(contact_id)).await?;
let msg = match &change {
PeerstateChange::FingerprintChange => {
stock_str::contact_setup_changed(context, &self.addr).await
stock_str::contact_setup_changed(context, self.addr.clone()).await
}
PeerstateChange::Aeap(new_addr) => {
let old_contact = Contact::load_from_db(context, contact_id).await?;
@@ -523,7 +569,9 @@ impl Peerstate {
let (new_contact_id, _) =
Contact::add_or_lookup(context, "", new_addr, Origin::IncomingUnknownFrom)
.await?;
chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id]).await?;
if !is_contact_in_chat(context, *chat_id, new_contact_id).await? {
chat::add_to_chat_contacts_table(context, *chat_id, new_contact_id).await?;
}
context.emit_event(EventType::ChatModified(*chat_id));
}
@@ -569,9 +617,10 @@ pub async fn maybe_do_aeap_transition(
mime_parser: &crate::mimeparser::MimeMessage,
) -> Result<()> {
if let Some(peerstate) = &mut info.peerstate {
// If the from addr is different from the peerstate address we know,
// we may want to do an AEAP transition.
if !addr_cmp(&peerstate.addr, &mime_parser.from.addr)
if let Some(from) = mime_parser.from.first() {
// If the from addr is different from the peerstate address we know,
// we may want to do an AEAP transition.
if !addr_cmp(&peerstate.addr, &from.addr)
// Check if it's a chat message; we do this to avoid
// some accidental transitions if someone writes from multiple
// addresses with an MUA.
@@ -587,24 +636,31 @@ pub async fn maybe_do_aeap_transition(
// to the attacker's address, allowing for easier phishing.
&& mime_parser.from_is_signed
&& info.message_time > peerstate.last_seen
{
// Add info messages to chats with this (verified) contact
//
peerstate
.handle_setup_change(
context,
info.message_time,
PeerstateChange::Aeap(info.from.clone()),
)
.await?;
{
// Add info messages to chats with this (verified) contact
//
peerstate
.handle_setup_change(
context,
info.message_time,
PeerstateChange::Aeap(info.from.clone()),
)
.await?;
peerstate.addr = info.from.clone();
let header = info.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(header, info.message_time);
peerstate.addr = info.from.clone();
let header = info.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(header, info.message_time);
peerstate.to_save = Some(ToSave::All);
peerstate.save_to_db(&context.sql).await?;
// We don't know whether a peerstate with this address already existed, or a
// new one should be created, so just try both create=false and create=true,
// and if this fails, create=true, one will succeed (this is a very cold path,
// so performance doesn't really matter).
peerstate.save_to_db(&context.sql, true).await?;
peerstate.save_to_db(&context.sql, false).await?;
}
}
}
@@ -656,7 +712,7 @@ mod tests {
let pub_key = alice_keypair().public;
let peerstate = Peerstate {
let mut peerstate = Peerstate {
addr: addr.into(),
last_seen: 10,
last_seen_autocrypt: 11,
@@ -668,11 +724,12 @@ mod tests {
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
assert!(
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save to db"
);
@@ -681,6 +738,8 @@ mod tests {
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
// clear to_save, as that is not persissted
peerstate.to_save = None;
assert_eq!(peerstate, peerstate_new);
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
.await
@@ -707,15 +766,16 @@ mod tests {
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
assert!(
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save"
);
assert!(
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"double-call with create failed"
);
}
@@ -727,7 +787,7 @@ mod tests {
let pub_key = alice_keypair().public;
let peerstate = Peerstate {
let mut peerstate = Peerstate {
addr: addr.into(),
last_seen: 10,
last_seen_autocrypt: 11,
@@ -739,11 +799,12 @@ mod tests {
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
assert!(
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save"
);
@@ -751,6 +812,8 @@ mod tests {
.await
.expect("failed to load peerstate from db");
// clear to_save, as that is not persissted
peerstate.to_save = None;
assert_eq!(Some(peerstate), peerstate_new);
}
@@ -801,6 +864,7 @@ mod tests {
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
to_save: None,
fingerprint_changed: false,
};
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::NoPreference);

View File

@@ -641,6 +641,7 @@ mod tests {
use crate::aheader::EncryptPreference;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::key::DcKey;
use crate::peerstate::ToSave;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{alice_keypair, TestContext};
use anyhow::Result;
@@ -893,10 +894,11 @@ mod tests {
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
assert!(
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save peerstate"
);

View File

@@ -8,7 +8,6 @@ use crate::chat::add_device_msg_with_importance;
use crate::config::Config;
use crate::context::Context;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::session::Session as ImapSession;
use crate::imap::Imap;
use crate::job::{Action, Status};
use crate::message::{Message, Viewtype};
@@ -50,12 +49,12 @@ pub struct QuotaInfo {
}
async fn get_unique_quota_roots_and_usage(
session: &mut ImapSession,
folders: Vec<String>,
imap: &mut Imap,
) -> Result<BTreeMap<String, Vec<QuotaResource>>> {
let mut unique_quota_roots: BTreeMap<String, Vec<QuotaResource>> = BTreeMap::new();
for folder in folders {
let (quota_roots, quotas) = &session.get_quota_root(&folder).await?;
let (quota_roots, quotas) = &imap.get_quota_roots(&folder).await?;
// if there are new quota roots found in this imap folder, add them to the list
for qr_entries in quota_roots {
for quota_root_name in &qr_entries.quota_root_names {
@@ -136,10 +135,9 @@ impl Context {
return Ok(Status::RetryNow);
}
let session = imap.session.as_mut().context("no session")?;
let quota = if session.can_check_quota() {
let quota = if imap.can_check_quota() {
let folders = get_watched_folders(self).await?;
get_unique_quota_roots_and_usage(session, folders).await
get_unique_quota_roots_and_usage(folders, imap).await
} else {
Err(anyhow!(stock_str::not_supported_by_provider(self).await))
};

View File

@@ -13,6 +13,7 @@ use regex::Regex;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::contact;
use crate::contact::{
may_be_valid_addr, normalize_name, Contact, ContactId, Origin, VerifiedStatus,
};
@@ -21,14 +22,14 @@ use crate::download::DownloadState;
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
use crate::imap::markseen_on_imap_table;
use crate::location;
use crate::log::LogExt;
use crate::message::{
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
};
use crate::mimeparser::{
parse_message_ids, AvatarAction, MailinglistType, MimeMessage, SystemMessage,
parse_message_id, parse_message_ids, AvatarAction, MailinglistType, MimeMessage, SystemMessage,
};
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
@@ -36,8 +37,7 @@ use crate::reaction::{set_msg_reaction, Reaction};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::sql;
use crate::stock_str;
use crate::tools::{extract_grpid_from_rfc724_mid, smeared_time};
use crate::{contact, imap};
use crate::tools::{create_id, extract_grpid_from_rfc724_mid, smeared_time};
/// This is the struct that is returned after receiving one email (aka MIME message).
///
@@ -66,19 +66,25 @@ pub async fn receive_imf(
seen: bool,
) -> Result<Option<ReceivedMsg>> {
let mail = parse_mail(imf_raw).context("can't parse mail")?;
let rfc724_mid = imap::prefetch_get_or_create_message_id(&mail.headers);
let rfc724_mid = mail
.headers
.get_header_value(HeaderDef::MessageId)
.and_then(|msgid| parse_message_id(&msgid).ok())
.unwrap_or_else(create_id);
receive_imf_inner(context, &rfc724_mid, imf_raw, seen, None, false).await
}
/// Receive a message and add it to the database.
///
/// Returns an error on database failure or if the message is broken,
/// e.g. has nonstandard MIME structure.
/// Returns an error on recoverable errors, e.g. database errors. In this case,
/// message parsing should be retried later.
///
/// If possible, creates a database entry to prevent the message from being
/// downloaded again, sets `chat_id=DC_CHAT_ID_TRASH` and returns `Ok(Some(…))`.
/// If the message is so wrong that we didn't even create a database entry,
/// returns `Ok(None)`.
/// If message itself is wrong, logs
/// the error and returns success:
/// - If possible, creates a database entry to prevent the message from being
/// downloaded again, sets `chat_id=DC_CHAT_ID_TRASH` and returns `Ok(Some(…))`
/// - If the message is so wrong that we didn't even create a database entry,
/// returns `Ok(None)`
///
/// If `is_partial_download` is set, it contains the full message size in bytes.
/// Do not confuse that with `replace_partial_download` that will be set when the full message is loaded later.
@@ -101,28 +107,7 @@ pub(crate) async fn receive_imf_inner(
match MimeMessage::from_bytes_with_partial(context, imf_raw, is_partial_download).await {
Err(err) => {
warn!(context, "receive_imf: can't parse MIME: {}", err);
let msg_ids;
if !rfc724_mid.starts_with(GENERATED_PREFIX) {
let row_id = context
.sql
.execute(
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
paramsv![rfc724_mid, DC_CHAT_ID_TRASH],
)
.await?;
msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
} else {
return Ok(None);
// We don't have an rfc724_mid, there's no point in adding a trash entry
}
return Ok(Some(ReceivedMsg {
chat_id: DC_CHAT_ID_TRASH,
state: MessageState::Undefined,
sort_timestamp: 0,
msg_ids,
needs_delete_job: false,
}));
return Ok(None);
}
Ok(mime_parser) => mime_parser,
};
@@ -156,6 +141,7 @@ pub(crate) async fn receive_imf_inner(
None
};
// the function returns the number of created messages in the database
let prevent_rename =
mime_parser.is_mailinglist_message() || mime_parser.get_header(HeaderDef::Sender).is_some();
@@ -182,6 +168,7 @@ pub(crate) async fn receive_imf_inner(
} else {
Origin::IncomingUnknownTo
},
prevent_rename,
)
.await?;
@@ -366,25 +353,28 @@ pub(crate) async fn receive_imf_inner(
/// * `prevent_rename`: passed through to `add_or_lookup_contacts_by_address_list()`
pub async fn from_field_to_contact_id(
context: &Context,
from: &SingleInfo,
from_address_list: &[SingleInfo],
prevent_rename: bool,
) -> Result<(ContactId, bool, Origin)> {
let display_name = if prevent_rename {
Some("")
} else {
from.display_name.as_deref()
};
let from_id = add_or_lookup_contact_by_addr(
let from_ids = add_or_lookup_contacts_by_address_list(
context,
display_name,
&from.addr,
from_address_list,
Origin::IncomingUnknownFrom,
prevent_rename,
)
.await?;
if from_id == ContactId::SELF {
if from_ids.contains(&ContactId::SELF) {
Ok((ContactId::SELF, false, Origin::OutgoingBcc))
} else {
} else if !from_ids.is_empty() {
if from_ids.len() > 1 {
warn!(
context,
"mail has more than one From address, only using first: {:?}", from_address_list
);
}
let from_id = from_ids.get(0).cloned().unwrap_or_default();
let mut from_id_blocked = false;
let mut incoming_origin = Origin::Unknown;
if let Ok(contact) = Contact::load_from_db(context, from_id).await {
@@ -392,6 +382,13 @@ pub async fn from_field_to_contact_id(
incoming_origin = contact.origin;
}
Ok((from_id, from_id_blocked, incoming_origin))
} else {
warn!(
context,
"mail has an empty From header: {:?}", from_address_list
);
Ok((ContactId::UNDEFINED, false, Origin::Unknown))
}
}
@@ -573,10 +570,9 @@ async fn add_parts(
if chat.is_protected() {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.repl_msg_by_error(&s);
} else {
} else if let Some(from) = mime_parser.from.first() {
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
// to the sender's name, indicating to the user that he/she is not part of the group.
let from = &mime_parser.from;
let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
for part in mime_parser.parts.iter_mut() {
part.param.set(Param::OverrideSenderDisplayname, name);
@@ -641,9 +637,11 @@ async fn add_parts(
// if contact renaming is prevented (for mailinglists and bots),
// we use name from From:-header as override name
if prevent_rename {
if let Some(name) = &mime_parser.from.display_name {
for part in mime_parser.parts.iter_mut() {
part.param.set(Param::OverrideSenderDisplayname, name);
if let Some(from) = mime_parser.from.first() {
if let Some(name) = &from.display_name {
for part in mime_parser.parts.iter_mut() {
part.param.set(Param::OverrideSenderDisplayname, name);
}
}
}
}
@@ -1510,10 +1508,7 @@ async fn create_or_lookup_group(
let grpname = mime_parser
.get_header(HeaderDef::ChatGroupName)
.context("Chat-Group-Name vanished")?
// W/a for "Space added before long group names after MIME serialization/deserialization
// #3650" issue. DC itself never creates group names with leading/trailing whitespace.
.trim();
.context("Chat-Group-Name vanished")?;
let new_chat_id = ChatId::create_multiuser_record(
context,
Chattype::Group,
@@ -1530,13 +1525,19 @@ async fn create_or_lookup_group(
chat_id_blocked = create_blocked;
// Create initial member list.
let mut members = vec![ContactId::SELF];
if !from_id.is_special() {
members.push(from_id);
chat::add_to_chat_contacts_table(context, new_chat_id, ContactId::SELF).await?;
if !from_id.is_special() && !chat::is_contact_in_chat(context, new_chat_id, from_id).await?
{
chat::add_to_chat_contacts_table(context, new_chat_id, from_id).await?;
}
for &to_id in to_ids.iter() {
info!(context, "adding to={:?} to chat id={}", to_id, new_chat_id);
if to_id != ContactId::SELF
&& !chat::is_contact_in_chat(context, new_chat_id, to_id).await?
{
chat::add_to_chat_contacts_table(context, new_chat_id, to_id).await?;
}
}
members.extend(to_ids);
members.dedup();
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
// once, we have protected-chats explained in UI, we can uncomment the following lines.
// ("verified groups" did not add a message anyway)
@@ -1615,15 +1616,9 @@ async fn apply_group_changes(
{
better_msg = Some(stock_str::msg_add_member(context, &added_member, from_id).await);
recreate_member_list = true;
} else if let Some(old_name) = mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
// See create_or_lookup_group() for explanation
.map(|s| s.trim())
{
} else if let Some(old_name) = mime_parser.get_header(HeaderDef::ChatGroupNameChanged) {
if let Some(grpname) = mime_parser
.get_header(HeaderDef::ChatGroupName)
// See create_or_lookup_group() for explanation
.map(|grpname| grpname.trim())
.filter(|grpname| grpname.len() < 200)
{
if chat_id
@@ -1692,7 +1687,6 @@ async fn apply_group_changes(
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
.await?
{
let mut members_to_add = vec![];
if removed_id.is_some()
|| !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
{
@@ -1707,23 +1701,26 @@ async fn apply_group_changes(
)
.await?;
members_to_add.push(ContactId::SELF);
if removed_id != Some(ContactId::SELF) {
chat::add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?;
}
}
if !from_id.is_special() {
members_to_add.push(from_id);
if !from_id.is_special()
&& from_id != ContactId::SELF
&& !chat::is_contact_in_chat(context, chat_id, from_id).await?
&& removed_id != Some(from_id)
{
chat::add_to_chat_contacts_table(context, chat_id, from_id).await?;
}
members_to_add.extend(to_ids);
if let Some(removed_id) = removed_id {
members_to_add.retain(|id| *id != removed_id);
for &to_id in to_ids.iter() {
if to_id != ContactId::SELF
&& !chat::is_contact_in_chat(context, chat_id, to_id).await?
&& removed_id != Some(to_id)
{
info!(context, "adding to={:?} to chat id={}", to_id, chat_id);
chat::add_to_chat_contacts_table(context, chat_id, to_id).await?;
}
}
members_to_add.dedup();
info!(
context,
"adding {:?} to chat id={}", members_to_add, chat_id
);
chat::add_to_chat_contacts_table(context, chat_id, &members_to_add).await?;
send_event_chat_modified = true;
}
}
@@ -1804,8 +1801,10 @@ async fn create_or_lookup_mailinglist(
// a usable name for these lists is in the `From` header
// and we can detect these lists by a unique `ListId`-suffix.
if listid.ends_with(".list-id.mcsv.net") {
if let Some(display_name) = &mime_parser.from.display_name {
name = display_name.clone();
if let Some(from) = mime_parser.from.first() {
if let Some(display_name) = &from.display_name {
name = display_name.clone();
}
}
}
@@ -1825,15 +1824,18 @@ async fn create_or_lookup_mailinglist(
//
// this pattern is similar to mailchimp above, however,
// with weaker conditions and does not overwrite existing names.
if name.is_empty()
&& (mime_parser.from.addr.contains("noreply")
|| mime_parser.from.addr.contains("no-reply")
|| mime_parser.from.addr.starts_with("notifications@")
|| mime_parser.from.addr.starts_with("newsletter@")
|| listid.ends_with(".xt.local"))
{
if let Some(display_name) = &mime_parser.from.display_name {
name = display_name.clone();
if name.is_empty() {
if let Some(from) = mime_parser.from.first() {
if from.addr.contains("noreply")
|| from.addr.contains("no-reply")
|| from.addr.starts_with("notifications@")
|| from.addr.starts_with("newsletter@")
|| listid.ends_with(".xt.local")
{
if let Some(display_name) = &from.display_name {
name = display_name.clone();
}
}
}
}
@@ -1858,18 +1860,12 @@ async fn create_or_lookup_mailinglist(
p.to_string()
});
let is_bot = context.get_config(Config::Bot).await?.is_some();
let blocked = if is_bot {
Blocked::Not
} else {
Blocked::Request
};
let chat_id = ChatId::create_multiuser_record(
context,
Chattype::Mailinglist,
&listid,
&name,
blocked,
Blocked::Request,
ProtectionStatus::Unprotected,
param,
)
@@ -1881,8 +1877,8 @@ async fn create_or_lookup_mailinglist(
)
})?;
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
Ok(Some((chat_id, blocked)))
chat::add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?;
Ok(Some((chat_id, Blocked::Request)))
} else {
info!(context, "creating list forbidden by caller");
Ok(None)
@@ -2011,7 +2007,9 @@ async fn create_adhoc_group(
None,
)
.await?;
chat::add_to_chat_contacts_table(context, new_chat_id, member_ids).await?;
for &member_id in member_ids.iter() {
chat::add_to_chat_contacts_table(context, new_chat_id, member_id).await?;
}
context.emit_event(EventType::ChatModified(new_chat_id));
@@ -2125,7 +2123,7 @@ async fn check_verified_properties(
&fp,
PeerstateVerifiedStatus::BidirectVerified,
);
peerstate.save_to_db(&context.sql).await?;
peerstate.save_to_db(&context.sql, false).await?;
is_verified = true;
}
}
@@ -2235,6 +2233,7 @@ async fn add_or_lookup_contacts_by_address_list(
context: &Context,
address_list: &[SingleInfo],
origin: Origin,
prevent_rename: bool,
) -> Result<Vec<ContactId>> {
let mut contact_ids = HashSet::new();
for info in address_list.iter() {
@@ -2242,7 +2241,11 @@ async fn add_or_lookup_contacts_by_address_list(
if !may_be_valid_addr(addr) {
continue;
}
let display_name = info.display_name.as_deref();
let display_name = if prevent_rename {
Some("")
} else {
info.display_name.as_deref()
};
contact_ids
.insert(add_or_lookup_contact_by_addr(context, display_name, addr, origin).await?);
}
@@ -2273,7 +2276,6 @@ mod tests {
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::get_chat_contacts;
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
use crate::chatlist::Chatlist;
@@ -2286,7 +2288,7 @@ mod tests {
async fn test_grpid_simple() {
let context = TestContext::new().await;
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: hello@example.org\n\
From: hello\n\
Subject: outer-subject\n\
In-Reply-To: <lqkjwelq123@123123>\n\
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
@@ -2300,25 +2302,11 @@ mod tests {
assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_bad_from() {
let context = TestContext::new().await;
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: hello\n\
Subject: outer-subject\n\
In-Reply-To: <lqkjwelq123@123123>\n\
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
\n\
hello\x00";
let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await;
assert!(mimeparser.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_grpid_from_multiple() {
let context = TestContext::new().await;
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: hello@example.org\n\
From: hello\n\
Subject: outer-subject\n\
In-Reply-To: <Gr.HcxyMARjyJy.9-qweqwe@asd.net>\n\
References: <qweqweqwe>, <Gr.HcxyMARjyJy.9-uvzWPTLtV@nau.ca>\n\
@@ -2605,55 +2593,8 @@ mod tests {
.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
// Check that the message is not shown to the user:
assert!(chats.is_empty());
// Check that the message was added to the db:
assert!(message::rfc724_mid_exists(context, "3924@example.com")
.await
.unwrap()
.is_some());
}
/// If there is no Message-Id header, we generate a random id.
/// But there is no point in adding a trash entry in the database
/// if the email is malformed (e.g. because `From` is missing)
/// with this random id we just generated.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_message_id_header() {
let t = TestContext::new_alice().await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert!(chats.get_msg_id(0).is_err());
let received = receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
To: bob@example.com\n\
Subject: foo\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
false,
)
.await
.unwrap();
dbg!(&received);
assert!(received.is_none());
assert!(!t
.sql
.exists(
"SELECT COUNT(*) FROM msgs WHERE chat_id=?;",
paramsv![DC_CHAT_ID_TRASH],
)
.await
.unwrap());
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
// Check that the message is not shown to the user:
assert!(chats.is_empty());
// Check that the message was added to the database:
assert!(chats.get_msg_id(0).is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -3692,27 +3633,6 @@ Hello mailinglist!\r\n"
assert_eq!(chat.name, "Test1");
}
/// Tests that bots automatically accept mailing lists.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mailing_list_bot() {
let t = TestContext::new_alice().await;
t.set_config(Config::Bot, Some("1")).await.unwrap();
receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_chat_message.eml"),
false,
)
.await
.unwrap();
let msg = t.get_last_msg().await;
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
assert_eq!(chat.blocked, Blocked::Not);
// Bot should see the message as fresh and process it.
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_show_tokens_in_contacts_list() {
check_dont_show_in_contacts_list(
@@ -5177,10 +5097,13 @@ Reply from different address
chat::add_to_chat_contacts_table(
&bob,
group_id,
&[
bob.add_or_lookup_contact(&alice1).await.id,
Contact::create(&bob, "", "charlie@example.org").await?,
],
bob.add_or_lookup_contact(&alice1).await.id,
)
.await?;
chat::add_to_chat_contacts_table(
&bob,
group_id,
Contact::create(&bob, "", "charlie@example.org").await?,
)
.await?;
@@ -5261,7 +5184,7 @@ Reply from different address
chat::add_to_chat_contacts_table(
&bob,
group_id,
&[bob.add_or_lookup_contact(&alice).await.id],
bob.add_or_lookup_contact(&alice).await.id,
)
.await?;
@@ -5314,20 +5237,4 @@ Reply from different address
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_thunderbird_autocrypt() -> Result<()> {
let t = TestContext::new_bob().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
let raw = include_bytes!("../test-data/message/thunderbird_with_autocrypt.eml");
receive_imf(&t, raw, false).await?;
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
.await?
.unwrap();
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
Ok(())
}
}

View File

@@ -166,12 +166,6 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
.await;
}
/// Implement a single iteration of IMAP loop.
///
/// This function performs all IMAP operations on a single folder, selecting it if necessary and
/// handling all the errors. In case of an error, it is logged, but not propagated upwards. If
/// critical operation fails such as fetching new messages fails, connection is reset via
/// `trigger_reconnect`, so a fresh one can be opened.
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config) -> InterruptInfo {
let folder = match ctx.get_config(folder_config).await {
Ok(folder) => folder,
@@ -193,25 +187,18 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
};
// connect and fake idle if unable to connect
if let Err(err) = connection
.prepare(ctx)
.await
.context("prepare IMAP connection")
{
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
if let Err(err) = connection.prepare(ctx).await {
warn!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
if folder_config == Config::ConfiguredInboxFolder {
if let Some(session) = connection.session.as_mut() {
session
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap")
.ok_or_log(ctx);
} else {
warn!(ctx, "No session even though we just prepared it");
if let Err(err) = connection
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap failed")
{
warn!(ctx, "{:#}", err);
}
}
@@ -221,7 +208,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
.await
.context("fetch_move_delete")
{
connection.trigger_reconnect(ctx);
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
@@ -230,10 +217,12 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
// called right before `fetch_move_delete` because it is not well optimized and would
// otherwise slow down message fetching.
delete_expired_imap_messages(ctx)
if let Err(err) = delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages")
.ok_or_log(ctx);
{
warn!(ctx, "{:#}", err);
}
// Scan additional folders only after finishing fetching the watched folder.
//
@@ -259,7 +248,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
.await
.context("fetch_move_delete after scan_folders")
{
connection.trigger_reconnect(ctx);
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
@@ -277,38 +266,18 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
connection.connectivity.set_connected(ctx).await;
if let Some(session) = connection.session.take() {
if !session.can_idle() {
info!(
ctx,
"IMAP session does not support IDLE, going to fake idle."
);
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
// idle
if !connection.can_idle() {
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
info!(ctx, "IMAP session supports IDLE, using it.");
match session
.idle(
ctx,
connection.idle_interrupt_receiver.clone(),
Some(watch_folder),
)
.await
.context("idle")
{
Ok((session, info)) => {
connection.session = Some(session);
info
}
Err(err) => {
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
InterruptInfo::new(false)
}
match connection.idle(ctx, Some(watch_folder)).await {
Ok(v) => v,
Err(err) => {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
InterruptInfo::new(false)
}
} else {
warn!(ctx, "No IMAP session, going to fake idle.");
connection.fake_idle(ctx, Some(watch_folder)).await
}
}

View File

@@ -12,7 +12,7 @@ use crate::tools::time;
use crate::{config::Config, scheduler::Scheduler, stock_str, tools};
use crate::{context::Context, log::LogExt};
use anyhow::{anyhow, Result};
use humansize::{format_size, BINARY};
use humansize::{file_size_opts, FileSize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
pub enum Connectivity {
@@ -448,7 +448,7 @@ impl Context {
// [======67%===== ]
// =============================================================================================
let domain = &tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain;
let domain = tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain;
let storage_on_domain = stock_str::storage_on_domain(self, domain).await;
ret += &format!("<h3>{}</h3><ul>", storage_on_domain);
let quota = self.quota.read().await;
@@ -473,8 +473,8 @@ impl Context {
let messages = stock_str::messages(self).await;
let part_of_total_used = stock_str::part_of_total_used(
self,
&resource.usage.to_string(),
&resource.limit.to_string(),
resource.usage.to_string(),
resource.limit.to_string(),
)
.await;
ret += &match &resource.name {
@@ -495,8 +495,12 @@ impl Context {
// - the string is not longer than the other strings that way (minus title, plus units) -
// additional linebreaks on small displays are unlikely therefore
// - most times, this is the only item anyway
let usage = &format_size(resource.usage * 1024, BINARY);
let limit = &format_size(resource.limit * 1024, BINARY);
let usage = (resource.usage * 1024)
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
let limit = (resource.limit * 1024)
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
stock_str::part_of_total_used(self, usage, limit).await
}
};

View File

@@ -18,7 +18,7 @@ use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToSave};
use crate::qr::check_qr;
use crate::stock_str;
use crate::token;
@@ -640,7 +640,11 @@ async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) ->
PeerstateVerifiedStatus::BidirectVerified,
) {
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
peerstate.to_save = Some(ToSave::All);
peerstate
.save_to_db(&context.sql, false)
.await
.unwrap_or_default();
return Ok(());
}
}
@@ -928,9 +932,10 @@ mod tests {
gossip_key_fingerprint: Some(alice_pubkey.fingerprint()),
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
peerstate.save_to_db(&bob.ctx.sql).await?;
peerstate.save_to_db(&bob.ctx.sql, true).await?;
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
let qr = get_securejoin_qr(&alice.ctx, None).await?;

View File

@@ -60,7 +60,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// TODO: how does this group become usable?
let group_chat_id = state.joining_chat_id(context).await?;
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
chat::add_to_chat_contacts_table(context, group_chat_id, &[invite.contact_id()])
chat::add_to_chat_contacts_table(context, group_chat_id, invite.contact_id())
.await?;
}
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;

View File

@@ -280,7 +280,7 @@ impl Sql {
for addr in &addrs {
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
peerstate.recalc_fingerprint();
peerstate.save_to_db(self).await?;
peerstate.save_to_db(self, false).await?;
}
}
}
@@ -432,7 +432,7 @@ impl Sql {
pub async fn transaction<G, H>(&self, callback: G) -> Result<H>
where
H: Send + 'static,
G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
{
let mut conn = self.get_conn().await?;
tokio::task::block_in_place(move || {
@@ -528,7 +528,9 @@ impl Sql {
///
/// Setting `None` deletes the value. On failure an error message
/// will already have been logged.
pub async fn set_raw_config(&self, key: &str, value: Option<&str>) -> Result<()> {
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> {
let key = key.as_ref();
let mut lock = self.config_cache.write().await;
if let Some(value) = value {
let exists = self
@@ -562,9 +564,9 @@ impl Sql {
}
/// Get configuration options from the database.
pub async fn get_raw_config(&self, key: &str) -> Result<Option<String>> {
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Result<Option<String>> {
let lock = self.config_cache.read().await;
let cached = lock.get(key).cloned();
let cached = lock.get(key.as_ref()).cloned();
drop(lock);
if let Some(c) = cached {
@@ -573,42 +575,48 @@ impl Sql {
let mut lock = self.config_cache.write().await;
let value = self
.query_get_value("SELECT value FROM config WHERE keyname=?;", paramsv![key])
.query_get_value(
"SELECT value FROM config WHERE keyname=?;",
paramsv![key.as_ref()],
)
.await
.context(format!("failed to fetch raw config: {}", key))?;
lock.insert(key.to_string(), value.clone());
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
lock.insert(key.as_ref().to_string(), value.clone());
drop(lock);
Ok(value)
}
pub async fn set_raw_config_int(&self, key: &str, value: i32) -> Result<()> {
pub async fn set_raw_config_int(&self, key: impl AsRef<str>, value: i32) -> Result<()> {
self.set_raw_config(key, Some(&format!("{}", value))).await
}
pub async fn get_raw_config_int(&self, key: &str) -> Result<Option<i32>> {
pub async fn get_raw_config_int(&self, key: impl AsRef<str>) -> Result<Option<i32>> {
self.get_raw_config(key)
.await
.map(|s| s.and_then(|s| s.parse().ok()))
}
pub async fn get_raw_config_bool(&self, key: &str) -> Result<bool> {
pub async fn get_raw_config_bool(&self, key: impl AsRef<str>) -> Result<bool> {
// Not the most obvious way to encode bool as string, but it is matter
// of backward compatibility.
let res = self.get_raw_config_int(key).await?;
Ok(res.unwrap_or_default() > 0)
}
pub async fn set_raw_config_bool(&self, key: &str, value: bool) -> Result<()> {
pub async fn set_raw_config_bool<T>(&self, key: T, value: bool) -> Result<()>
where
T: AsRef<str>,
{
let value = if value { Some("1") } else { None };
self.set_raw_config(key, value).await
}
pub async fn set_raw_config_int64(&self, key: &str, value: i64) -> Result<()> {
pub async fn set_raw_config_int64(&self, key: impl AsRef<str>, value: i64) -> Result<()> {
self.set_raw_config(key, Some(&format!("{}", value))).await
}
pub async fn get_raw_config_int64(&self, key: &str) -> Result<Option<i64>> {
pub async fn get_raw_config_int64(&self, key: impl AsRef<str>) -> Result<Option<i64>> {
self.get_raw_config(key)
.await
.map(|s| s.and_then(|r| r.parse().ok()))
@@ -720,7 +728,7 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|row| row.get::<_, String>(0),
|rows| {
for row in rows {
maybe_add_file(&mut files_in_use, &row?);
maybe_add_file(&mut files_in_use, row?);
}
Ok(())
},
@@ -811,8 +819,8 @@ fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, nam
files_in_use.contains(name_to_check)
}
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: &str) {
if let Some(file) = file.strip_prefix("$BLOBDIR/") {
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: impl AsRef<str>) {
if let Some(file) = file.as_ref().strip_prefix("$BLOBDIR/") {
files_in_use.insert(file.to_string());
}
}

View File

@@ -10,7 +10,7 @@ use crate::provider::get_provider_by_domain;
use crate::sql::Sql;
use crate::tools::EmailAddress;
const DBVERSION: i32 = 68;
const DBVERSION: i32 = 69;
const VERSION_CFG: &str = "dbversion";
const TABLES: &str = include_str!("./tables.sql");
@@ -320,11 +320,11 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
if dbversion < 67 {
for prefix in &["", "configured_"] {
if let Some(server_flags) = sql
.get_raw_config_int(&format!("{}server_flags", prefix))
.get_raw_config_int(format!("{}server_flags", prefix))
.await?
{
let imap_socket_flags = server_flags & 0x700;
let key = &format!("{}mail_security", prefix);
let key = format!("{}mail_security", prefix);
match imap_socket_flags {
0x100 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
0x200 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
@@ -332,7 +332,7 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
_ => sql.set_raw_config_int(key, 0).await?,
}
let smtp_socket_flags = server_flags & 0x70000;
let key = &format!("{}send_security", prefix);
let key = format!("{}send_security", prefix);
match smtp_socket_flags {
0x10000 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
0x20000 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
@@ -618,48 +618,13 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
}
if dbversion < 94 {
sql.execute_migration(
// Create new `acpeerstates` table, same as before but with unique constraint.
//
// This allows to use `UPSERT` to update existing or insert a new peerstate
// depending on whether one exists already.
"CREATE TABLE new_acpeerstates (
id INTEGER PRIMARY KEY,
addr TEXT DEFAULT '' COLLATE NOCASE,
last_seen INTEGER DEFAULT 0,
last_seen_autocrypt INTEGER DEFAULT 0,
public_key,
prefer_encrypted INTEGER DEFAULT 0,
gossip_timestamp INTEGER DEFAULT 0,
gossip_key,
public_key_fingerprint TEXT DEFAULT '',
gossip_key_fingerprint TEXT DEFAULT '',
verified_key,
verified_key_fingerprint TEXT DEFAULT '',
UNIQUE (addr) -- Only one peerstate per address
);
INSERT OR IGNORE INTO new_acpeerstates SELECT * FROM acpeerstates;
DROP TABLE acpeerstates;
ALTER TABLE new_acpeerstates RENAME TO acpeerstates;
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
",
r#"
ALTER TABLE chats ADD COLUMN encryption_modus INTEGER DEFAULT 0;
ALTER TABLE msgs ADD COLUMN encryption_modus INTEGER DEFAULT 0;"#,
94,
)
.await?;
}
if dbversion < 95 {
sql.execute_migration(
"CREATE TABLE new_chats_contacts (chat_id INTEGER, contact_id INTEGER, UNIQUE(chat_id, contact_id));\
INSERT OR IGNORE INTO new_chats_contacts SELECT * FROM chats_contacts;\
DROP TABLE chats_contacts;\
ALTER TABLE new_chats_contacts RENAME TO chats_contacts;\
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);\
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);",
95
).await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)

View File

@@ -17,7 +17,7 @@ use crate::context::Context;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::tools::timestamp_to_str;
use humansize::{format_size, BINARY};
use humansize::{file_size_opts, FileSize};
#[derive(Debug, Clone)]
pub struct StockStrings {
@@ -466,33 +466,33 @@ async fn translated(context: &Context, id: StockMessage) -> String {
/// Helper trait only meant to be implemented for [`String`].
trait StockStringMods: AsRef<str> + Sized {
/// Substitutes the first replacement value if one is present.
fn replace1(&self, replacement: &str) -> String {
fn replace1(&self, replacement: impl AsRef<str>) -> String {
self.as_ref()
.replacen("%1$s", replacement, 1)
.replacen("%1$d", replacement, 1)
.replacen("%1$@", replacement, 1)
.replacen("%1$s", replacement.as_ref(), 1)
.replacen("%1$d", replacement.as_ref(), 1)
.replacen("%1$@", replacement.as_ref(), 1)
}
/// Substitutes the second replacement value if one is present.
///
/// Be aware you probably should have also called [`StockStringMods::replace1`] if
/// you are calling this.
fn replace2(&self, replacement: &str) -> String {
fn replace2(&self, replacement: impl AsRef<str>) -> String {
self.as_ref()
.replacen("%2$s", replacement, 1)
.replacen("%2$d", replacement, 1)
.replacen("%2$@", replacement, 1)
.replacen("%2$s", replacement.as_ref(), 1)
.replacen("%2$d", replacement.as_ref(), 1)
.replacen("%2$@", replacement.as_ref(), 1)
}
/// Substitutes the third replacement value if one is present.
///
/// Be aware you probably should have also called [`StockStringMods::replace1`] and
/// [`StockStringMods::replace2`] if you are calling this.
fn replace3(&self, replacement: &str) -> String {
fn replace3(&self, replacement: impl AsRef<str>) -> String {
self.as_ref()
.replacen("%3$s", replacement, 1)
.replacen("%3$d", replacement, 1)
.replacen("%3$@", replacement, 1)
.replacen("%3$s", replacement.as_ref(), 1)
.replacen("%3$d", replacement.as_ref(), 1)
.replacen("%3$@", replacement.as_ref(), 1)
}
}
@@ -551,8 +551,8 @@ pub(crate) async fn file(context: &Context) -> String {
/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
pub(crate) async fn msg_grp_name(
context: &Context,
from_group: &str,
to_group: &str,
from_group: impl AsRef<str>,
to_group: impl AsRef<str>,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
@@ -565,7 +565,7 @@ pub(crate) async fn msg_grp_name(
.await
.replace1(from_group)
.replace2(to_group)
.replace3(&by_contact.get_stock_name(context).await)
.replace3(by_contact.get_stock_name(context).await)
}
}
@@ -575,7 +575,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
} else {
translated(context, StockMessage::MsgGrpImgChangedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(by_contact.get_stock_name(context).await)
}
}
@@ -585,11 +585,11 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
/// contacts to combine with the display name.
pub(crate) async fn msg_add_member(
context: &Context,
added_member_addr: &str,
added_member_addr: impl AsRef<str>,
by_contact: ContactId,
) -> String {
let addr = added_member_addr;
let who = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
let addr = added_member_addr.as_ref();
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await
.map(|contact| contact.get_name_n_addr())
@@ -604,7 +604,7 @@ pub(crate) async fn msg_add_member(
translated(context, StockMessage::MsgAddMemberBy)
.await
.replace1(who)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(by_contact.get_stock_name(context).await)
}
}
@@ -614,11 +614,11 @@ pub(crate) async fn msg_add_member(
/// the contacts to combine with the display name.
pub(crate) async fn msg_del_member(
context: &Context,
removed_member_addr: &str,
removed_member_addr: impl AsRef<str>,
by_contact: ContactId,
) -> String {
let addr = removed_member_addr;
let who = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
let addr = removed_member_addr.as_ref();
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await
.map(|contact| contact.get_name_n_addr())
@@ -633,7 +633,7 @@ pub(crate) async fn msg_del_member(
translated(context, StockMessage::MsgDelMemberBy)
.await
.replace1(who)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(by_contact.get_stock_name(context).await)
}
}
@@ -644,7 +644,7 @@ pub(crate) async fn msg_group_left(context: &Context, by_contact: ContactId) ->
} else {
translated(context, StockMessage::MsgGroupLeftBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(by_contact.get_stock_name(context).await)
}
}
@@ -684,7 +684,7 @@ pub(crate) async fn read_rcpt(context: &Context) -> String {
}
/// Stock string: `This is a return receipt for the message "%1$s".`.
pub(crate) async fn read_rcpt_mail_body(context: &Context, message: &str) -> String {
pub(crate) async fn read_rcpt_mail_body(context: &Context, message: impl AsRef<str>) -> String {
translated(context, StockMessage::ReadRcptMailBody)
.await
.replace1(message)
@@ -697,7 +697,7 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId
} else {
translated(context, StockMessage::MsgGrpImgDeletedBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(by_contact.get_stock_name(context).await)
}
}
@@ -714,7 +714,7 @@ pub(crate) async fn secure_join_started(
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinStarted)
.await
.replace1(&contact.get_name_n_addr())
.replace1(contact.get_name_n_addr())
.replace2(contact.get_display_name())
} else {
format!(
@@ -741,7 +741,7 @@ pub(crate) async fn setup_contact_qr_description(
display_name: &str,
addr: &str,
) -> String {
let name = &if display_name == addr {
let name = if display_name == addr {
addr.to_owned()
} else {
format!("{} ({})", display_name, addr)
@@ -760,7 +760,7 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C
/// Stock string: `%1$s verified.`.
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
let addr = &contact.get_name_n_addr();
let addr = contact.get_name_n_addr();
translated(context, StockMessage::ContactVerified)
.await
.replace1(addr)
@@ -768,14 +768,17 @@ pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> St
/// Stock string: `Cannot verify %1$s`.
pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
let addr = &contact.get_name_n_addr();
let addr = contact.get_name_n_addr();
translated(context, StockMessage::ContactNotVerified)
.await
.replace1(addr)
}
/// Stock string: `Changed setup for %1$s`.
pub(crate) async fn contact_setup_changed(context: &Context, contact_addr: &str) -> String {
pub(crate) async fn contact_setup_changed(
context: &Context,
contact_addr: impl AsRef<str>,
) -> String {
translated(context, StockMessage::ContactSetupChanged)
.await
.replace1(contact_addr)
@@ -807,7 +810,7 @@ pub(crate) async fn sync_msg_body(context: &Context) -> String {
}
/// Stock string: `Cannot login as \"%1$s\". Please check...`.
pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
pub(crate) async fn cannot_login(context: &Context, user: impl AsRef<str>) -> String {
translated(context, StockMessage::CannotLogin)
.await
.replace1(user)
@@ -825,7 +828,7 @@ pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactI
} else {
translated(context, StockMessage::MsgLocationEnabledBy)
.await
.replace1(&contact.get_stock_name(context).await)
.replace1(contact.get_stock_name(context).await)
}
}
@@ -871,14 +874,17 @@ pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
/// Stock string: `Message from %1$s`.
// TODO: This can compute `self_name` itself instead of asking the caller to do this.
pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
pub(crate) async fn subject_for_new_contact(
context: &Context,
self_name: impl AsRef<str>,
) -> String {
translated(context, StockMessage::SubjectForNewContact)
.await
.replace1(self_name)
}
/// Stock string: `Failed to send message to %1$s.`.
pub(crate) async fn failed_sending_to(context: &Context, name: &str) -> String {
pub(crate) async fn failed_sending_to(context: &Context, name: impl AsRef<str>) -> String {
translated(context, StockMessage::FailedSendingTo)
.await
.replace1(name)
@@ -894,14 +900,14 @@ pub(crate) async fn msg_ephemeral_timer_disabled(
} else {
translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to %1$s s.`.
pub(crate) async fn msg_ephemeral_timer_enabled(
context: &Context,
timer: &str,
timer: impl AsRef<str>,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
@@ -912,7 +918,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
.await
.replace1(timer)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(by_contact.get_stock_name(context).await)
}
}
@@ -923,7 +929,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: Co
} else {
translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(by_contact.get_stock_name(context).await)
}
}
@@ -934,7 +940,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont
} else {
translated(context, StockMessage::MsgEphemeralTimerHourBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(by_contact.get_stock_name(context).await)
}
}
@@ -945,7 +951,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta
} else {
translated(context, StockMessage::MsgEphemeralTimerDayBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(by_contact.get_stock_name(context).await)
}
}
@@ -956,7 +962,7 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont
} else {
translated(context, StockMessage::MsgEphemeralTimerWeekBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(by_contact.get_stock_name(context).await)
}
}
@@ -966,14 +972,14 @@ pub(crate) async fn videochat_invitation(context: &Context) -> String {
}
/// Stock string: `You are invited to a video chat, click %1$s to join.`.
pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> String {
pub(crate) async fn videochat_invite_msg_body(context: &Context, url: impl AsRef<str>) -> String {
translated(context, StockMessage::VideochatInviteMsgBody)
.await
.replace1(url)
}
/// Stock string: `Error:\n\n“%1$s”`.
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
pub(crate) async fn configuration_failed(context: &Context, details: impl AsRef<str>) -> String {
translated(context, StockMessage::ConfigurationFailed)
.await
.replace1(details)
@@ -981,7 +987,7 @@ pub(crate) async fn configuration_failed(context: &Context, details: &str) -> St
/// Stock string: `⚠️ Date or time of your device seem to be inaccurate (%1$s)...`.
// TODO: This could compute now itself.
pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
pub(crate) async fn bad_time_msg_body(context: &Context, now: impl AsRef<str>) -> String {
translated(context, StockMessage::BadTimeMsgBody)
.await
.replace1(now)
@@ -1004,7 +1010,7 @@ pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId)
} else {
translated(context, StockMessage::ProtectionEnabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(by_contact.get_stock_name(context).await)
}
}
@@ -1015,7 +1021,7 @@ pub(crate) async fn protection_disabled(context: &Context, by_contact: ContactId
} else {
translated(context, StockMessage::ProtectionDisabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
.replace1(by_contact.get_stock_name(context).await)
}
}
@@ -1037,7 +1043,7 @@ pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
/// Stock string: `Message deletion timer is set to %1$s minutes.`.
pub(crate) async fn msg_ephemeral_timer_minutes(
context: &Context,
minutes: &str,
minutes: impl AsRef<str>,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
@@ -1048,14 +1054,14 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
.await
.replace1(minutes)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to %1$s hours.`.
pub(crate) async fn msg_ephemeral_timer_hours(
context: &Context,
hours: &str,
hours: impl AsRef<str>,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
@@ -1066,14 +1072,14 @@ pub(crate) async fn msg_ephemeral_timer_hours(
translated(context, StockMessage::MsgEphemeralTimerHoursBy)
.await
.replace1(hours)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to %1$s days.`.
pub(crate) async fn msg_ephemeral_timer_days(
context: &Context,
days: &str,
days: impl AsRef<str>,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
@@ -1084,14 +1090,14 @@ pub(crate) async fn msg_ephemeral_timer_days(
translated(context, StockMessage::MsgEphemeralTimerDaysBy)
.await
.replace1(days)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Message deletion timer is set to %1$s weeks.`.
pub(crate) async fn msg_ephemeral_timer_weeks(
context: &Context,
weeks: &str,
weeks: impl AsRef<str>,
by_contact: ContactId,
) -> String {
if by_contact == ContactId::SELF {
@@ -1102,7 +1108,7 @@ pub(crate) async fn msg_ephemeral_timer_weeks(
translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
.await
.replace1(weeks)
.replace2(&by_contact.get_stock_name(context).await)
.replace2(by_contact.get_stock_name(context).await)
}
}
@@ -1115,13 +1121,15 @@ pub(crate) async fn forwarded(context: &Context) -> String {
pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
translated(context, StockMessage::QuotaExceedingMsgBody)
.await
.replace1(&format!("{}", highest_usage))
.replace1(format!("{}", highest_usage))
.replace("%%", "%")
}
/// Stock string: `%1$s message` with placeholder replaced by human-readable size.
pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
let size = &format_size(org_bytes, BINARY);
let size = org_bytes
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
translated(context, StockMessage::PartialDownloadMsgBody)
.await
.replace1(size)
@@ -1131,7 +1139,7 @@ pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32)
pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
translated(context, StockMessage::DownloadAvailability)
.await
.replace1(&timestamp_to_str(timestamp))
.replace1(timestamp_to_str(timestamp))
}
/// Stock string: `Incoming Messages`.
@@ -1146,7 +1154,7 @@ pub(crate) async fn outgoing_messages(context: &Context) -> String {
/// Stock string: `Storage on %1$s`.
/// `%1$s` will be replaced by the domain of the configured email-address.
pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
pub(crate) async fn storage_on_domain(context: &Context, domain: impl AsRef<str>) -> String {
translated(context, StockMessage::StorageOnDomain)
.await
.replace1(domain)
@@ -1184,7 +1192,7 @@ pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
/// Stock string: `Error: %1$s…`.
/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
pub(crate) async fn error(context: &Context, error: &str) -> String {
pub(crate) async fn error(context: &Context, error: impl AsRef<str>) -> String {
translated(context, StockMessage::Error)
.await
.replace1(error)
@@ -1202,7 +1210,11 @@ pub(crate) async fn messages(context: &Context) -> String {
}
/// Stock string: `%1$s of %2$s used`.
pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
pub(crate) async fn part_of_total_used(
context: &Context,
part: impl AsRef<str>,
total: impl AsRef<str>,
) -> String {
translated(context, StockMessage::PartOfTotallUsed)
.await
.replace1(part)
@@ -1218,9 +1230,9 @@ pub(crate) async fn broadcast_list(context: &Context) -> String {
/// Stock string: `%1$s changed their address from %2$s to %3$s`.
pub(crate) async fn aeap_addr_changed(
context: &Context,
contact_name: &str,
old_addr: &str,
new_addr: &str,
contact_name: impl AsRef<str>,
old_addr: impl AsRef<str>,
new_addr: impl AsRef<str>,
) -> String {
translated(context, StockMessage::AeapAddrChanged)
.await
@@ -1231,8 +1243,8 @@ pub(crate) async fn aeap_addr_changed(
pub(crate) async fn aeap_explanation_and_link(
context: &Context,
old_addr: &str,
new_addr: &str,
old_addr: impl AsRef<str>,
new_addr: impl AsRef<str>,
) -> String {
translated(context, StockMessage::AeapExplanationAndLink)
.await

View File

@@ -380,13 +380,13 @@ impl TestContext {
/// table. Messages are returned in the order they have been sent.
///
/// Panics if there is no message or on any error.
pub async fn pop_sent_msg(&self) -> SentMessage<'_> {
pub async fn pop_sent_msg(&self) -> SentMessage {
self.pop_sent_msg_opt(Duration::from_secs(3))
.await
.expect("no sent message found in jobs table")
}
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage<'_>> {
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage> {
let start = Instant::now();
let (rowid, msg_id, payload, recipients) = loop {
let row = self
@@ -428,7 +428,6 @@ impl TestContext {
Some(SentMessage {
payload,
sender_msg_id: msg_id,
sender_context: &self.ctx,
recipients,
})
}
@@ -440,7 +439,7 @@ impl TestContext {
/// peerstates will be updated. Later receiving the message using [recv_msg] is
/// unlikely to be affected as the peerstate would be processed again in exactly the
/// same way.
pub async fn parse_msg(&self, msg: &SentMessage<'_>) -> MimeMessage {
pub async fn parse_msg(&self, msg: &SentMessage) -> MimeMessage {
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes())
.await
.unwrap()
@@ -448,7 +447,7 @@ impl TestContext {
/// Receive a message using the `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 {
pub async fn recv_msg(&self, msg: &SentMessage) -> Message {
let received = self
.recv_msg_opt(msg)
.await
@@ -477,10 +476,7 @@ impl TestContext {
/// Receive a message using the `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::receive_imf::ReceivedMsg> {
pub async fn recv_msg_opt(&self, msg: &SentMessage) -> Option<crate::receive_imf::ReceivedMsg> {
receive_imf(self, msg.payload().as_bytes(), false)
.await
.unwrap()
@@ -587,7 +583,7 @@ impl TestContext {
/// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call
/// [`TestContext::recv_msg`] with the returned [`SentMessage`] if it wants to receive
/// the message.
pub async fn send_text(&self, chat_id: ChatId, txt: &str) -> SentMessage<'_> {
pub async fn send_text(&self, chat_id: ChatId, txt: &str) -> SentMessage {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(txt.to_string()));
self.send_msg(chat_id, &mut msg).await
@@ -598,7 +594,7 @@ impl TestContext {
/// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call
/// [`TestContext::recv_msg`] with the returned [`SentMessage`] if it wants to receive
/// the message.
pub async fn send_msg(&self, chat_id: ChatId, msg: &mut Message) -> SentMessage<'_> {
pub async fn send_msg(&self, chat_id: ChatId, msg: &mut Message) -> SentMessage {
chat::prepare_msg(self, chat_id, msg).await.unwrap();
let msg_id = chat::send_msg(self, chat_id, msg).await.unwrap();
let res = self.pop_sent_msg().await;
@@ -746,14 +742,13 @@ impl Drop for LogSink {
/// This is a raw message, probably in the shape DC was planning to send it but not having
/// passed through a SMTP-IMAP pipeline.
#[derive(Debug, Clone)]
pub struct SentMessage<'a> {
pub struct SentMessage {
pub payload: String,
recipients: String,
pub sender_msg_id: MsgId,
sender_context: &'a Context,
}
impl SentMessage<'_> {
impl SentMessage {
/// A recipient the message was destined for.
///
/// If there are multiple recipients this is just a random one, so is not very useful.
@@ -770,12 +765,6 @@ impl SentMessage<'_> {
pub fn payload(&self) -> &str {
&self.payload
}
pub async fn load_from_db(&self) -> Message {
Message::load_from_db(self.sender_context, self.sender_msg_id)
.await
.unwrap()
}
}
/// Load a pre-generated keypair for alice@example.org from disk.
@@ -990,7 +979,7 @@ fn print_event(event: &Event) {
/// Logs an individual message to stdout.
///
/// This includes a bunch of the message meta-data as well.
async fn log_msg(context: &Context, prefix: &str, msg: &Message) {
async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let contact = match Contact::get_by_id(context, msg.get_from_id()).await {
Ok(contact) => contact,
Err(e) => {
@@ -1012,7 +1001,7 @@ async fn log_msg(context: &Context, prefix: &str, msg: &Message) {
let msgtext = msg.get_text();
println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}",
prefix,
prefix.as_ref(),
msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" },
if msg.has_location() { "📍" } else { "" },

View File

@@ -341,8 +341,9 @@ async fn mark_as_verified(this: &TestContext, other: &TestContext) {
peerstate.verified_key = peerstate.public_key.clone();
peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone();
peerstate.to_save = Some(peerstate::ToSave::All);
peerstate.save_to_db(&this.sql).await.unwrap();
peerstate.save_to_db(&this.sql, false).await.unwrap();
}
async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option<Message> {

View File

@@ -206,7 +206,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
msg.text = Some(
stock_str::bad_time_msg_body(
context,
&Local
Local
.timestamp(now, 0)
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
@@ -319,8 +319,8 @@ pub(crate) fn extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
}
// the returned suffix is lower-case
pub fn get_filesuffix_lc(path_filename: &str) -> Option<String> {
Path::new(path_filename)
pub fn get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
Path::new(path_filename.as_ref())
.extension()
.map(|p| p.to_string_lossy().to_lowercase())
}
@@ -670,18 +670,6 @@ pub(crate) fn parse_receive_headers(headers: &Headers) -> String {
.join("\n")
}
/// If `collection` contains exactly one element, return this element.
/// Otherwise, return None.
pub(crate) fn single_value<T>(collection: impl IntoIterator<Item = T>) -> Option<T> {
let mut iter = collection.into_iter();
if let Some(value) = iter.next() {
if iter.next().is_none() {
return Some(value);
}
}
None
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]

View File

@@ -360,9 +360,7 @@ impl Context {
}
let chat = Chat::load_from_db(self, instance.chat_id).await?;
if let Some(reason) = chat.why_cant_send(self).await? {
bail!("cannot send to {}: {}", chat.id, reason);
}
ensure!(chat.can_send(self).await?, "cannot send to {}", chat.id);
let send_now = !matches!(
instance.state,
@@ -1076,7 +1074,7 @@ mod tests {
)
.await?;
let sent1 = alice.send_msg(chat.id, &mut alice_instance).await;
let alice_instance = sent1.load_from_db().await;
let alice_instance = Message::load_from_db(&alice, sent1.sender_msg_id).await?;
alice
.send_webxdc_status_update(
alice_instance.id,
@@ -1378,7 +1376,7 @@ mod tests {
alice.flush_status_updates().await?;
expect_status_update_event(&alice, alice_instance.id).await?;
let sent2 = &alice.pop_sent_msg().await;
let alice_update = sent2.load_from_db().await;
let alice_update = Message::load_from_db(&alice, sent2.sender_msg_id).await?;
assert!(alice_update.hidden);
assert_eq!(alice_update.viewtype, Viewtype::Text);
assert_eq!(alice_update.get_filename(), None);
@@ -2197,7 +2195,7 @@ sth_for_the = "future""#
.await?;
alice.flush_status_updates().await?;
let sent2 = &alice.pop_sent_msg().await;
let update_msg = sent2.load_from_db().await;
let update_msg = Message::load_from_db(&alice, sent2.sender_msg_id).await?;
assert!(alice_instance.get_showpadlock());
assert!(update_msg.get_showpadlock());
@@ -2217,7 +2215,7 @@ sth_for_the = "future""#
.await?;
bob.flush_status_updates().await?;
let sent3 = bob.pop_sent_msg().await;
let update_msg = sent3.load_from_db().await;
let update_msg = Message::load_from_db(&bob, sent3.sender_msg_id).await?;
assert!(!update_msg.get_showpadlock());
Ok(())

View File

@@ -1,93 +0,0 @@
From - Thu, 24 Nov 2022 19:06:16 GMT
X-Mozilla-Status: 0001
X-Mozilla-Status2: 00800000
Message-ID: <0bb9ffe1-2596-d997-95b4-1fef8cc4808e@example.org>
Date: Thu, 24 Nov 2022 20:05:57 +0100
MIME-Version: 1.0
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
Thunderbird/102.4.2
From: Alice <alice@example.org>
To: bob@example.net
Content-Language: en-US
Autocrypt: addr=alice@example.org; keydata=
xjMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5DN
GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp
7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4M
CyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDr
RuI8A/8tEEXAA844BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp
01JrRe6Xqy22HQMBCAfCeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsM
AAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIy
VfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg==
Subject: ...
Content-Type: multipart/encrypted;
protocol="application/pgp-encrypted";
boundary="------------EOdOT2kJUL5hgCilmIhYyVZg"
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
--------------EOdOT2kJUL5hgCilmIhYyVZg
Content-Type: application/pgp-encrypted
Content-Description: PGP/MIME version identification
Version: 1
--------------EOdOT2kJUL5hgCilmIhYyVZg
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc"
-----BEGIN PGP MESSAGE-----
wV4D5tq63hTeebASAQdA1dVUsUjGZCOIfCnYtVdmOvKs/BNovI3sG8w1IH4ymTMwAZzgwVbGS5KL
+e1VTD5mUTeVSEYe1cd3VozH4KbNJa1tBlcO0nzGwCPpsTVDMoxIwcBMA+PY3JvEjuMiAQf/d2yj
t0+GyaptwX26bgSqo6vj21W8mcWS5vXOi8wjGwRbPaKKjS4kq1xDOz04eHrE8HUPD8otcXoI8CLz
etJpRbFs0XJP4Cozbsr72dgoWhozRg/iSpBndxWOddTl7Yqo8m/fyhU5uzKZ41m2T8mha6KkKWD8
QecGdOgieYBucNBjHwWc71p9G6jTnzfy4S4GtGS2gwOSMxpwO7HxpKzsHI4POqFSQbxrl/YRwWSC
f5WqyYcerasIiR/fnOIw8lnvCeQ5rB90eGEDR70YFGt0t4rFBjfGrSPUiWYaTaC1Zvpd+t5sy7zy
FpsS2/aTkwP/UpGqmtFaD/brSouRf9hijNLI0QFTaVmSoI3BKzF8B4zwvtEbOLZjyDb+Va/fZJ3w
nYd2Q/5PPPL+pE4pWKN+jl0TZNzAaqBgvggXomgUqQ7QiksUzym+yuFKrJX0RF2awdrgjQIxjnda
Qp3UFphnFTyYUJpIU9iewjOfVxgPzv7PyuCHYwoP3kh7MJZ6bgbDmOkeFSnjEDJpdf1m9xC9LlBL
beC8scmPs6kx9GARBYSHvyPQ025gN3+XEHh4OrTxHZ91U3IlTfd2kACwOOAXEuhItSHmcNOV0K4M
nI2PH6gW8HgBkWlAPm40K4jUyo3nl1usDiI6ouvYqvW7YUc2hTtPTej1l2/mS57tTt+PFurKs555
5R9DD/xg9Nx7OuQKy5bIdlXM20UmwuZTOhRJ5kpHFRzLxaHDbSzW+orhRW4llJSevBSAH3cLOjIQ
gh87j+MxG9j0TD2K2A0rcUcxdrnflw+mxcDVaL4payeqmOa+bJyhlftTqH+vqq5DhR68rX5VW+z7
riqH3o8VbvO2y0XSpYHf1jowkfJj3vr8pynAUIv1dbylUSF5wtrHvzWOprw4bNrdtwQNRNy+JcVF
dUKeNmHaL6XOe4LUWpiI11beRyCpAG52khMCEAO3Q6+4e24cEipbu6suSOtv3OpYDZeHjwNrQIhi
rJg7i9TpMqwOeCvFWK+9UZ+P2n6h9g0/JO2+I82BFGUjVa5IvCTNOgv01GqxWY9ecdtaJjTc+dF2
OAcRoKwvmtMJlxKEEgveui3BvPA4tuNdSrcoZBrQeo0ZHWVugXPvEZnwfZMcqwwPA+a/sUbZFg0P
Pr0AR0ZHpytnQE9OXE8wEUgT8H1yofQ+5QoZdgMpeAb8zGs+RuviLxcDkb9NtXUAiQ49ooWuFP3L
K9wMlaoWFTq7R+n5JVuSEYRCHC0l0bCV1/+awalT7XltXVCupI4lWzjYs52FZGGzuHG7S50Eufad
m4CQTPVgVaVn8WW2dmpMR8Gj8WbbZdyv21wMGOWjfgT0u3oiDnddGrFOoMNnZHch6rN3FRppoh7h
0U0fi8xxU1+EhUKq+fSIxZNr2iWN2if3Pipbxi9tyK9M41Y6aVF3HWjD58/OEql3aZjJZ1bqpXcE
qsPeFoXX78+7mTDvL75olMk2s/mg4mLqAAWQvTuoiOmj+SgMIFuTtFR+4r/TIFNdamz6AQ3RcmWG
ZcdRii+V27dtMA836vlAwxXRmJyE1LCL1kvUTq+J+AVsZi3xmBLFNlKPTlxswu7vSBrP1DlYOaBq
AgA0lKnkQdeXyDk/VdbTml7ywMW1g6HkFSqKGW/IIAObmBumBcIyHE6dWEHumRQomlJssIlEFSe+
XEQ0rwedLetJXi5A0AXT1we1wvaKCEg0Pb0ZUxygwNPDrj6MmdodH7gDfyx0mW/7mEMCtIJb5MB+
TRGPEa/vqdJb8uGtNXUy9UlwMhJ3tYoT7NXY4+IlNjbDH/yleMdwtWP2H2WH8oC+ysXPYXjlT8eU
poxRfJzPMVUn5SA3cvdGXDJWdX8U91j5sf9wuoYE5RBVrrJif3D3l0FpMrlWWoGw7wtZbMC2FaeT
QvdMS5c54IoXBtBTM+/AsTAw7WEE1QSmaQGHnh6xLL5Ns8olsWeKOMlVXdO9jSDbjOGBLr7mWukW
YzLXkH3TtJPQcbVN79af3YPhaHdMYITVKIwfg+vxZlLFHWLJQnkTl+9Qi7u2gKqkNeU7Zqs4E3CR
9K4dHrJMyAZLZ2HA1XQEj0/tMnbTpAzZhj02JRcFobLXK9SQfw7dzGZwMRky8cHcBHoK14P5RIEV
hr+38HSBM6wXtge5gL6DomAACvuORQO4X9x/CTjRt/J8uN3lKK5p+wi3ULeb319CEWiCiqmC1M+C
TADUhPUhUmTinSAVkTEn+BdbH/97dVaJnvd6HtLmdSlw4xqdWUfVL9Qd7+/5L6iwlOzGLKRv97c/
gCRw+hzXyAom+5C18slSwanMuyPgIyrrFy/kp9Romk9SQr/c0CUF2am99t8G5qvVi/TiJGHyKEXD
aUYd4V7lqNlHMiiasvFHeq8blwmFr7rGEvbZzLNplc6sRUVlYhY2unRfyWsq9mqk3NDRW12Fa0J2
YxQJlnXHQhNE8EyM/zsD9jCVNwsRZJ9/e5KS+ignmu6gKIR+ItDTwRfNI+NG/YmTgENUTyuO+vQC
CUKS3PCwpP+OEC966ARl7OCMdfn1hEyiAxsZnp1RmFngR6FM+mlGgfUoWNoHvnR1/YyQ4F4dadiA
QINwuSm5faw75F1EeL8Qi+LHKuqt05Pi/V9GJ6TzIkIsEbyyJ5sKHrp4QsU4C1p7ZhPjddz8De8k
6ZdwMIeXxi27WKtsFLcr8JKOBe0imIilKdMBOPS31pc1iJe4472WbWM0aBwdEYmnz9+xfOqnjHtO
0XTMjff7pzV6Y7t/u8J/zm3JS3ykote9HNRQvhZZNeVClVWd0fYFzat5ESnTojZTwHcc/BFTPnhz
VgLyw1KEIy2r3ZyGHu1b8GSYivzl33MOK/NVBQPZUIEfdcQ5vhkAvj+Yx340IYykRFEChwioprXD
LrIbTou7TNT5fTFA+beidHFsL+OE002/LMs6C3erSUW5C/LNjAQMS7cAV2yCyjX+/2GBmmDqnC4r
Ja2x5yik+fbOUPh3kk/md1YvrodlX/JkQeoWRrrVJsX2dr3BgivPJavaN0Jz1eHyxAYKNqlrfd1T
YWEDIisWerTxAVY/rEruZ6+OqLqOtZtn+4SOajOq8KFusglaMZqoYuM+LhPZck9PlZXwRqX08Vlv
8jX5V75BFWRhFd5/LYbnQHI6ZW80Wb2sBNngLL2QJT9yXGCDJb5qCdFwGd3i655pvRJXabeyCtDD
7I2PJcYRDd4stdq07BHyHJmye6vas8mG5QUygyWyUQv78za0m4gLMrRZBgoBDcVpWJUc+cPXzzfG
7PvLZu/Y0SaD5hqTp0LBB1PFxTpzdVeJ21gzVNQ6D4XGLTtdv4K4fOEYoeKEuzGoBaUDtIqz47gd
5rwfQ3ps2slkxfbtQcdKEACKvsCwzqHlgwsxD8QNOFzXYLiiiJBX22fIRoiJeSDMKSZyuFtpykCm
7bOpybPSHv3E7EIr8sIOr9MOe/R5HSthU2IgW1L5Ynr2t9HUnCA8CenkzIQjg0h5sruxcGWCYLx7
q0f1AQs4Z7SebVbq1SCWVJNX/vc1bVjnjYfri7RX5WMmjJkuSnuIoP6a42cqJcAg7m0STB0elFAy
oO4vW9/JEmFUqLyQmWnoLJHX3IKtWa9CPvE=
=OA6b
-----END PGP MESSAGE-----
--------------EOdOT2kJUL5hgCilmIhYyVZg--