mirror of
https://github.com/chatmail/core.git
synced 2026-06-30 11:36:35 +03:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e05015028 | ||
|
|
6aa0291dfe | ||
|
|
bb9112811a | ||
|
|
9574c09613 | ||
|
|
486c9f6655 | ||
|
|
65aec0c965 | ||
|
|
e0750a04e8 |
43
.github/workflows/ci.yml
vendored
43
.github/workflows/ci.yml
vendored
@@ -5,8 +5,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- staging
|
||||
- trying
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -18,48 +16,22 @@ jobs:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.45.0
|
||||
toolchain: 1.43.1
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
run_clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.45.0
|
||||
components: clippy
|
||||
override: true
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all
|
||||
|
||||
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
rust: [1.45.0]
|
||||
experimental: [false]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: nightly
|
||||
experimental: true
|
||||
- os: windows-latest
|
||||
rust: nightly
|
||||
experimental: true
|
||||
- os: macOS-latest
|
||||
rust: nightly
|
||||
experimental: true
|
||||
rust: [nightly, 1.43.1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
@@ -88,14 +60,9 @@ jobs:
|
||||
path: target
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all --bins --examples --tests
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
args: --workspace
|
||||
|
||||
|
||||
54
ASYNC-API-TODO.txt
Normal file
54
ASYNC-API-TODO.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
Delta Chat ASYNC (friedel, bjoern, floris, friedel)
|
||||
|
||||
- smtp fake-idle/load jobs gerade noch alle fuenf sekunden , sollte alle zehn minuten (oder gar nicht)
|
||||
|
||||
APIs:
|
||||
dc_context_new # opens the database
|
||||
dc_open # FFI only
|
||||
-> drop it and move parameters to dc_context_new()
|
||||
|
||||
dc_configure # note: dc_start_jobs() is NOT allowed to run concurrently
|
||||
dc_imex NEVER goes through the job system
|
||||
dc_imex import_backup needs to ensure dc_stop_jobs()
|
||||
|
||||
dc_start_io # start smtp/imap and job handling subsystems
|
||||
dc_stop_io # stop smtp/imap and job handling subsystems
|
||||
dc_is_io_running # return 1 if smtp/imap/jobs susbystem is running
|
||||
|
||||
dc_close # FFI only
|
||||
-> can be dropped
|
||||
dc_context_unref
|
||||
|
||||
for ios share-extension:
|
||||
Int dc_direct_send() -> try send out without going through jobs system, but queue a job in db if it needs to be retried on failure
|
||||
0: message was sent
|
||||
1: message failed to go out, is queued as a job to be retried later
|
||||
2: message permanently failed?
|
||||
|
||||
EVENT handling:
|
||||
start a callback thread and call get_next_event() which is BLOCKING
|
||||
it's fine to start this callback thread later, it will see all events.
|
||||
Note that the core infinitely fills the internal queue if you never drain it.
|
||||
|
||||
FFI-get_next_event() returns NULL if the context is unrefed already?
|
||||
|
||||
sidenote: how python's callback thread does it currently:
|
||||
CB-thread runs this while loop:
|
||||
while not QUITFLAG:
|
||||
ev = context.get_next_event( )
|
||||
...
|
||||
So in order to shutdown properly one has to set QUITFLAG
|
||||
before calling dc_stop_jobs() and dc_context_unref
|
||||
|
||||
event API:
|
||||
get_data1_int
|
||||
get_data2_int
|
||||
get_data3_str
|
||||
|
||||
|
||||
- userdata likely only used for the callbacks, likely can be dropped, needs verification
|
||||
|
||||
|
||||
- iOS needs for the share app to call "try_send_smtp" wihtout a full dc_context_run and without going
|
||||
|
||||
87
CHANGELOG.md
87
CHANGELOG.md
@@ -1,92 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 1.41.0
|
||||
|
||||
- new apis to initiate video chats #1718 #1735
|
||||
|
||||
- new apis `dc_msg_get_ephemeral_timer()`
|
||||
and `dc_msg_get_ephemeral_timestamp()`
|
||||
|
||||
- improve IMAP handling #1703 #1704
|
||||
|
||||
- improve ephemeral messages #1696 #1705
|
||||
|
||||
- mark location-messages as auto-generated #1715
|
||||
|
||||
- multi-device avatar-sync #1716 #1717
|
||||
|
||||
- improve python bindings #1732 #1733 #1738 #1769
|
||||
|
||||
- Allow http scheme for DCACCOUNT urls #1770
|
||||
|
||||
- more fixes #1702 #1706 #1707 #1710 #1719 #1721
|
||||
#1723 #1734 #1740 #1744 #1748 #1760 #1766
|
||||
|
||||
- refactorings #1712 #1714 #1757
|
||||
|
||||
- update toolchains and dependencies #1726 #1736 #1737 #1742 #1743 #1746
|
||||
|
||||
|
||||
## 1.40.0
|
||||
|
||||
- introduce ephemeral messages #1540 #1680 #1683 #1684 #1691 #1692
|
||||
|
||||
- `DC_MSG_ID_DAYMARKER` gets timestamp attached #1677 #1685
|
||||
|
||||
- improve idle #1690 #1688
|
||||
|
||||
- fix message processing issues by sequential processing #1694
|
||||
|
||||
- refactorings #1670 #1673
|
||||
|
||||
|
||||
## 1.39.0
|
||||
|
||||
- fix handling of `mvbox_watch`, `sentbox_watch`, `inbox_watch` #1654 #1658
|
||||
|
||||
- fix potential panics, update dependencies #1650 #1655
|
||||
|
||||
|
||||
## 1.38.0
|
||||
|
||||
- fix sorting, esp. for multi-device
|
||||
|
||||
|
||||
## 1.37.0
|
||||
|
||||
- improve ndn heuristics #1630
|
||||
|
||||
- get oauth2 authorizer from provider-db #1641
|
||||
|
||||
- removed linebreaks and spaces from generated qr-code #1631
|
||||
|
||||
- more fixes #1633 #1635 #1636 #1637
|
||||
|
||||
|
||||
## 1.36.0
|
||||
|
||||
- parse ndn (network delivery notification) reports
|
||||
and report failed messages as such #1552 #1622 #1630
|
||||
|
||||
- add oauth2 support for gsuite domains #1626
|
||||
|
||||
- read image orientation from exif before recoding #1619
|
||||
|
||||
- improve logging #1593 #1598
|
||||
|
||||
- improve python and bot bindings #1583 #1609
|
||||
|
||||
- improve imap logout #1595
|
||||
|
||||
- fix sorting #1600 #1604
|
||||
|
||||
- fix qr code generation #1631
|
||||
|
||||
- update rustcrypto releases #1603
|
||||
|
||||
- refactorings #1617
|
||||
|
||||
|
||||
## 1.35.0
|
||||
|
||||
- enable strict-tls from a new provider-db setting #1587
|
||||
|
||||
381
Cargo.lock
generated
381
Cargo.lock
generated
@@ -2,9 +2,9 @@
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.12.2"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c"
|
||||
checksum = "a49806b9dadc843c61e7c97e72490ad7f7220ae249012fbda9ad0609457c0543"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
@@ -238,9 +238,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-std"
|
||||
version = "1.6.2"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00d68a33ebc8b57800847d00787307f84a562224a14db069b0acefe4c2abbf5d"
|
||||
checksum = "b93c583a035d21e6d6f09adf48abfc55277bf48886406df370e5db6babe3ab98"
|
||||
dependencies = [
|
||||
"async-attributes",
|
||||
"async-task",
|
||||
@@ -281,9 +281,9 @@ checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.36"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a265e3abeffdce30b2e26b7a11b222fe37c6067404001b434101457d0385eb92"
|
||||
checksum = "89cb5d814ab2a47fd66d3266e9efccb53ca4c740b7451043b8ffcf9a6208f3f8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -459,19 +459,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c98bfd7c112b6399fef97cc0614af1cd375b27a112e552ce60f94c1b5f13cb74"
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d17efb70ce4421e351d61aafd90c16a20fb5bfe339fcdc32a86816280e62ce0"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"once_cell",
|
||||
"parking",
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.5.0"
|
||||
@@ -521,6 +508,12 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b92204551573580e078dc80017f36a213eb77a0450e4ddd8cfa0f3f2d1f0178f"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.2.0"
|
||||
@@ -544,10 +537,17 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cache-padded"
|
||||
version = "1.1.0"
|
||||
name = "cargo_metadata"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24508e28c677875c380c20f4d28124fab6f8ed4ef929a1397d7b1a31e92f1005"
|
||||
checksum = "e5d1b4d380e1bab994591a24c2bdd1b054f64b60bef483a8c598c7c345bc3bbe"
|
||||
dependencies = [
|
||||
"error-chain",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast5"
|
||||
@@ -633,15 +633,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83c06aff61f2d899eb87c379df3cbf7876f14471dcab474e0b6dc90ab96c080"
|
||||
dependencies = [
|
||||
"cache-padded",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.1.5"
|
||||
@@ -659,7 +650,7 @@ dependencies = [
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"sha2 0.8.2",
|
||||
"time 0.2.16",
|
||||
]
|
||||
@@ -695,6 +686,32 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"maybe-uninit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
|
||||
dependencies = [
|
||||
"autocfg 1.0.0",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"lazy_static",
|
||||
"maybe-uninit",
|
||||
"memoffset",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.2.3"
|
||||
@@ -745,7 +762,7 @@ checksum = "5d85653f070353a16313d0046f173f70d1aadd5b42600a14de626f0dfb3473a5"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"digest 0.8.1",
|
||||
"rand_core",
|
||||
"rand_core 0.5.1",
|
||||
"subtle 2.2.3",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -785,12 +802,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72aa14c04dfae8dd7d8a2b1cb7ca2152618cd01336dbfe704b8dcbf8d41dbd69"
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "0.8.4"
|
||||
@@ -803,7 +814,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.41.0"
|
||||
version = "1.35.0"
|
||||
dependencies = [
|
||||
"ansi_term 0.12.1",
|
||||
"anyhow",
|
||||
@@ -826,6 +837,7 @@ dependencies = [
|
||||
"futures 0.3.5",
|
||||
"hex",
|
||||
"image",
|
||||
"image-meta",
|
||||
"indexmap",
|
||||
"itertools",
|
||||
"kamadak-exif",
|
||||
@@ -845,7 +857,7 @@ dependencies = [
|
||||
"quick-xml",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"regex",
|
||||
"rusqlite",
|
||||
"rustyline",
|
||||
@@ -874,7 +886,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.41.0"
|
||||
version = "1.35.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
@@ -965,9 +977,9 @@ checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.6"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b"
|
||||
checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3"
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
@@ -977,7 +989,7 @@ checksum = "978710b352437433c97b2bff193f2fb1dfd58a093f863dd95e225a19baa599a2"
|
||||
dependencies = [
|
||||
"clear_on_drop",
|
||||
"curve25519-dalek",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"sha2 0.8.2",
|
||||
]
|
||||
|
||||
@@ -997,7 +1009,7 @@ dependencies = [
|
||||
"encoded-words",
|
||||
"encoding",
|
||||
"lazy_static",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"time 0.1.43",
|
||||
"version_check 0.9.2",
|
||||
]
|
||||
@@ -1166,12 +1178,6 @@ dependencies = [
|
||||
"ascii_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a64b0126b293b050395b37b10489951590ed024c03d7df4f249d219c8ded7cbf"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.14"
|
||||
@@ -1205,6 +1211,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.1.29"
|
||||
@@ -1373,6 +1385,12 @@ version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.2.1"
|
||||
@@ -1471,19 +1489,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http-types"
|
||||
version = "2.2.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca4221cd1c7cedf275cd0ad3c9dfe58b5acc93cdd5511c7e020a102e1995fe99"
|
||||
checksum = "9a89eaaf43f3700e78c01cb8165d1bd05155065637d26ee2f49800c95e7b62ee"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
"cookie",
|
||||
"infer",
|
||||
"pin-project-lite",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"serde_urlencoded",
|
||||
"url",
|
||||
]
|
||||
@@ -1537,9 +1554,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.23.6"
|
||||
version = "0.23.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b0553fec6407d63fe2975b794dfb099f3f790bdc958823851af37b26404ab4"
|
||||
checksum = "d534e95ad8b9d5aa614322d02352b4f1bf962254adcf02ac6f2def8be18498e8"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
@@ -1551,6 +1568,17 @@ dependencies = [
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-meta"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a2da7b4225d0954c9b8ba1a0dcec85be29f496cba4d85f9390426f810e3ab0d"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"skeptic",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imap-proto"
|
||||
version = "0.10.2"
|
||||
@@ -1571,9 +1599,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "infer"
|
||||
version = "0.1.7"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6854dd77ddc4f9ba1a448f487e27843583d407648150426a30c2ea3a2c39490a"
|
||||
checksum = "d55c406a76164eb346a829ed4b97b73cb06259078eca01adeb12e8ca308d4123"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iovec"
|
||||
@@ -1598,18 +1629,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.9.0"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
|
||||
checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.6"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
|
||||
checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
@@ -1807,6 +1838,15 @@ version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8"
|
||||
dependencies = [
|
||||
"autocfg 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
@@ -1914,7 +1954,7 @@ dependencies = [
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
@@ -1954,9 +1994,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.3.0"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5b4d7360f362cfb50dde8143501e6940b22f644be75a4cc90b2d81968908138"
|
||||
checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef"
|
||||
dependencies = [
|
||||
"autocfg 1.0.0",
|
||||
"num-integer",
|
||||
@@ -2070,12 +2110,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a7fad362df89617628a7508b3e9d588ade1b0ac31aa25de168193ad999c2dd4"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.10.2"
|
||||
@@ -2151,7 +2185,7 @@ dependencies = [
|
||||
"num-bigint-dig",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"ripemd160",
|
||||
"rsa",
|
||||
"sha-1",
|
||||
@@ -2197,6 +2231,18 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01608bfa680dafb103f9207fa944facf572e4e3e708d10de19a0d0c3d36e5f18"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.17"
|
||||
@@ -2286,7 +2332,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"quick-error",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"rand_chacha",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
@@ -2294,6 +2340,15 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eef52fac62d0ea7b9b4dc7da092aa64ea7ec3d90af6679422d3d7e0e14b6ee15"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
@@ -2345,6 +2400,19 @@ dependencies = [
|
||||
"rusqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
|
||||
dependencies = [
|
||||
"fuchsia-cprng",
|
||||
"libc",
|
||||
"rand_core 0.3.1",
|
||||
"rdrand",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
@@ -2355,7 +2423,7 @@ dependencies = [
|
||||
"libc",
|
||||
"packed_simd",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
@@ -2366,9 +2434,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
|
||||
dependencies = [
|
||||
"rand_core 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
@@ -2384,7 +2467,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2393,7 +2476,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rdrand"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
|
||||
dependencies = [
|
||||
"rand_core 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2496,7 +2588,7 @@ dependencies = [
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"pem",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"sha2 0.9.0",
|
||||
"simple_asn1",
|
||||
"subtle 2.2.3",
|
||||
@@ -2588,6 +2680,15 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sanitize-filename"
|
||||
version = "0.2.1"
|
||||
@@ -2618,10 +2719,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.0"
|
||||
name = "scoped-tls-hkt"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
|
||||
checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
@@ -2659,6 +2760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
||||
dependencies = [
|
||||
"semver-parser",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2675,18 +2777,18 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.114"
|
||||
version = "1.0.112"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3"
|
||||
checksum = "736aac72d1eafe8e5962d1d1c3d99b0df526015ba40915cb3c49d042e92ec243"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.114"
|
||||
version = "1.0.112"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e"
|
||||
checksum = "bf0343ce212ac0d3d6afd9391ac8e9c9efe06b533c8d33f660f6390cc4093f57"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2704,18 +2806,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_qs"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6e32b85107a5c8062643265a90575cc6e798cec0906ea58519b42175062ba27"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"error-chain",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.6.1"
|
||||
@@ -2794,6 +2884,22 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "skeptic"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6fb8ed853fdc19ce09752d63f3a2e5b5158aeb261520cd75eb618bd60305165"
|
||||
dependencies = [
|
||||
"bytecount",
|
||||
"cargo_metadata",
|
||||
"error-chain",
|
||||
"glob",
|
||||
"pulldown-cmark",
|
||||
"serde_json",
|
||||
"tempdir",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.2"
|
||||
@@ -2808,23 +2914,23 @@ checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
|
||||
|
||||
[[package]]
|
||||
name = "smol"
|
||||
version = "0.1.18"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "620cbb3c6e34da57d3a248cda0cd01cd5848164dc062e764e65d06fe3ea7aed5"
|
||||
checksum = "afcf8beb4aa23cc616e3351e49585153b462637c8fec929163b54d88e522b3b0"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"blocking",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"scoped-tls",
|
||||
"piper",
|
||||
"scoped-tls-hkt",
|
||||
"slab",
|
||||
"socket2",
|
||||
"wepoll-sys-stjepang",
|
||||
"winapi",
|
||||
"wepoll-binding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2992,9 +3098,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.33"
|
||||
version = "1.0.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd"
|
||||
checksum = "b5304cfdf27365b7585c25d4af91b35016ed21ef88f17ced89c7093b43dba8b6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3013,6 +3119,16 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempdir"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
|
||||
dependencies = [
|
||||
"rand 0.4.6",
|
||||
"remove_dir_all",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.1.0"
|
||||
@@ -3021,7 +3137,7 @@ checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"redox_syscall",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
@@ -3113,12 +3229,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed"
|
||||
|
||||
[[package]]
|
||||
name = "tokio-io"
|
||||
version = "0.1.13"
|
||||
@@ -3152,7 +3262,7 @@ dependencies = [
|
||||
"idna",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"url",
|
||||
@@ -3223,11 +3333,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977"
|
||||
checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3281,7 +3391,7 @@ version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3318,10 +3428,15 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.0.0"
|
||||
name = "walkdir"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9571542c2ce85ce642e6b58b3364da2fb53526360dfb7c211add4f5c23105ff7"
|
||||
checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
@@ -3406,10 +3521,20 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wepoll-sys-stjepang"
|
||||
version = "1.0.2"
|
||||
name = "wepoll-binding"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "035f8ab1fcf98d41f8fd5206a97c335604162e5eb0c25c4839062093150ddc79"
|
||||
checksum = "374fff4ff9701ff8b6ad0d14bacd3156c44063632d8c136186ff5967d48999a7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"wepoll-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wepoll-sys"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9082a777aed991f6769e2b654aa0cb29f1c3d615daf009829b07b66c7aff6a24"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -3476,7 +3601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"rand_core",
|
||||
"rand_core 0.5.1",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.41.0"
|
||||
version = "1.35.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -43,7 +43,8 @@ strum = "0.18.0"
|
||||
strum_macros = "0.18.0"
|
||||
backtrace = "0.3.33"
|
||||
byteorder = "1.3.1"
|
||||
itertools = "0.9.0"
|
||||
itertools = "0.8.0"
|
||||
image-meta = "0.1.0"
|
||||
quick-xml = "0.18.1"
|
||||
escaper = "0.1.0"
|
||||
bitflags = "1.1.0"
|
||||
|
||||
@@ -123,7 +123,6 @@ Language bindings are available for:
|
||||
- [Node.js](https://www.npmjs.com/package/deltachat-node)
|
||||
- [Python](https://py.delta.chat)
|
||||
- [Go](https://github.com/hugot/go-deltachat/)
|
||||
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
|
||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||
|
||||
The following "frontend" projects make use of the Rust-library
|
||||
|
||||
@@ -16,6 +16,6 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||
ADD deps/build_python.sh /builder/build_python.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
||||
|
||||
# Install Rust
|
||||
# Install Rust nightly
|
||||
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.45.0-x86_64-unknown-linux-gnu -y
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.43.1-x86_64-unknown-linux-gnu -y
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
rustc --version
|
||||
|
||||
# remove some 300-400 MB that we don't need for automated builds
|
||||
rm -rf /root/.rustup/toolchains/1.45.0-x86_64-unknown-linux-gnu/share
|
||||
rm -rf /root/.rustup/toolchains/1.43.1-x86_64-unknown-linux-gnu/share
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.41.0"
|
||||
version = "1.35.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -19,10 +19,10 @@ fn main() {
|
||||
include_str!("deltachat.pc.in"),
|
||||
name = "deltachat",
|
||||
description = env::var("CARGO_PKG_DESCRIPTION").unwrap(),
|
||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
|
||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or("".to_string()),
|
||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||
libs_priv = libs_priv,
|
||||
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
||||
prefix = env::var("PREFIX").unwrap_or("/usr/local".to_string()),
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
|
||||
@@ -262,25 +262,17 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `selfavatar` = File containing avatar. Will immediately be copied to the
|
||||
* `blobdir`; the original image will not be needed anymore.
|
||||
* NULL to remove the avatar.
|
||||
* As for `displayname` and `selfstatus`, also the avatar is sent to the recipients.
|
||||
* To save traffic, however, the avatar is attached only as needed
|
||||
* and also recoded to a reasonable size.
|
||||
* It is planned for future versions
|
||||
* to send this image together with the next messages.
|
||||
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
|
||||
* - `mdns_enabled` = 0=do not send or request read receipts,
|
||||
* 1=send and request read receipts (default)
|
||||
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
|
||||
* 1=send a copy of outgoing messages to self.
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `inbox_watch` = 1=watch `INBOX`-folder for changes (default),
|
||||
* 0=do not watch the `INBOX`-folder,
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* 0=do not watch the `INBOX`-folder
|
||||
* - `sentbox_watch`= 1=watch `Sent`-folder for changes (default),
|
||||
* 0=do not watch the `Sent`-folder,
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* 0=do not watch the `Sent`-folder
|
||||
* - `mvbox_watch` = 1=watch `DeltaChat`-folder for changes (default),
|
||||
* 0=do not watch the `DeltaChat`-folder,
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* 0=do not watch the `DeltaChat`-folder
|
||||
* - `mvbox_move` = 1=heuristically detect chat-messages
|
||||
* and move them to the `DeltaChat`-folder,
|
||||
* 0=do not move chat-messages
|
||||
@@ -318,11 +310,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* The library uses the `media_quality` setting to use different defaults
|
||||
* for recoding images sent with type DC_MSG_IMAGE.
|
||||
* If needed, recoding other file types is up to the UI.
|
||||
* - `webrtc_instance` = webrtc instance to use for videochats in the form
|
||||
* `[basicwebrtc:]https://example.com/subdir#roomname=$ROOM`
|
||||
* if the url is prefixed by `basicwebrtc`, the server is assumed to be of the type
|
||||
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
|
||||
* If no type is prefixed, the videochat is handled completely in a browser.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -839,42 +826,6 @@ uint32_t dc_send_msg_sync (dc_context_t* context, uint32
|
||||
uint32_t dc_send_text_msg (dc_context_t* context, uint32_t chat_id, const char* text_to_send);
|
||||
|
||||
|
||||
/**
|
||||
* Send invitation to a videochat.
|
||||
*
|
||||
* This function reads the `webrtc_instance` config value,
|
||||
* may check that the server is working in some way
|
||||
* and creates a unique room for this chat, if needed doing a TOKEN roundtrip for that.
|
||||
*
|
||||
* After that, the function sends out a message that contains information to join the room:
|
||||
*
|
||||
* - To allow non-delta-clients to join the chat,
|
||||
* the message contains a text-area with some descriptive text
|
||||
* and a url that can be opened in a supported browser to join the videochat
|
||||
*
|
||||
* - delta-clients can get all information needed from
|
||||
* the message object, using eg.
|
||||
* dc_msg_get_videochat_url() and check dc_msg_get_viewtype() for #DC_MSG_VIDEOCHAT_INVITATION
|
||||
*
|
||||
* dc_send_videochat_invitation() is blocking and may take a while,
|
||||
* so the UIs will typically call the function from within a thread.
|
||||
* Moreover, UIs will typically enter the room directly without an additional click on the message,
|
||||
* for this purpose, the function returns the message-id directly.
|
||||
*
|
||||
* As for other messages sent, this function
|
||||
* sends the event #DC_EVENT_MSGS_CHANGED on succcess, the message has a delivery state, and so on.
|
||||
* The recipient will get noticed by the call as usual by #DC_EVENT_INCOMING_MSG or #DC_EVENT_MSGS_CHANGED,
|
||||
* However, UIs might some things differently, eg. play a different sound.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to start a videochat for.
|
||||
* @return The id if the message sent out
|
||||
* or 0 for errors.
|
||||
*/
|
||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Save a draft for a chat in the database.
|
||||
*
|
||||
@@ -1018,7 +969,6 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
* @param chat_id The chat ID of which the messages IDs should be queried.
|
||||
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
|
||||
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
||||
* To get the concrete time of the marker, use dc_array_get_timestamp().
|
||||
* @param marker1before An optional message ID. If set, the id DC_MSG_ID_MARKER1 will be added just
|
||||
* before the given ID in the returned array. Set this to 0 if you do not want this behaviour.
|
||||
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
@@ -1213,16 +1163,6 @@ void dc_delete_chat (dc_context_t* context, uint32_t ch
|
||||
*/
|
||||
dc_array_t* dc_get_chat_contacts (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
/**
|
||||
* Get the chat's ephemeral message timer.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param chat_id The chat ID.
|
||||
*
|
||||
* @return ephemeral timer value in seconds, 0 if the timer is disabled or if there is an error
|
||||
*/
|
||||
uint32_t dc_get_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
/**
|
||||
* Search messages containing the given query string.
|
||||
@@ -1355,21 +1295,6 @@ int dc_remove_contact_from_chat (dc_context_t* context, uint32_t ch
|
||||
*/
|
||||
int dc_set_chat_name (dc_context_t* context, uint32_t chat_id, const char* name);
|
||||
|
||||
/**
|
||||
* Set the chat's ephemeral message timer.
|
||||
*
|
||||
* This timer is applied to all messages in a chat and starts when the
|
||||
* message is read. The setting is synchronized to all clients
|
||||
* participating in a chat.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param chat_id The chat ID to set the ephemeral message timer for.
|
||||
* @param timer The timer value in seconds or 0 to disable the timer.
|
||||
*
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32_t timer);
|
||||
|
||||
/**
|
||||
* Set group profile image.
|
||||
@@ -2352,6 +2277,17 @@ int dc_array_is_independent (const dc_array_t* array, size_t in
|
||||
int dc_array_search_id (const dc_array_t* array, uint32_t needle, size_t* ret_index);
|
||||
|
||||
|
||||
/**
|
||||
* Get raw pointer to the data.
|
||||
*
|
||||
* @memberof dc_array_t
|
||||
* @param array The array object.
|
||||
* @return Raw pointer to the array. You MUST NOT free the data. You MUST NOT access the data beyond the current item count.
|
||||
* It is not possible to enlarge the array this way. Calling any other dc_array*()-function may discard the returned pointer.
|
||||
*/
|
||||
const uint32_t* dc_array_get_raw (const dc_array_t* array);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_chatlist_t
|
||||
*
|
||||
@@ -3067,32 +3003,6 @@ int dc_msg_get_duration (const dc_msg_t* msg);
|
||||
int dc_msg_get_showpadlock (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get ephemeral timer duration for message.
|
||||
*
|
||||
* To check if the timer is started and calculate remaining time,
|
||||
* use dc_msg_get_ephemeral_timestamp().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Duration in seconds, or 0 if no timer is set.
|
||||
*/
|
||||
uint32_t dc_msg_get_ephemeral_timer (const dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* Get timestamp of ephemeral message removal.
|
||||
*
|
||||
* If returned value is non-zero, you can calculate the * fraction of
|
||||
* time remaining by divinding the difference between the current timestamp
|
||||
* and this timestamp by dc_msg_get_ephemeral_timer().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Time of message removal, 0 if the timer is not started.
|
||||
*/
|
||||
int64_t dc_msg_get_ephemeral_timestamp (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get a summary for a message.
|
||||
*
|
||||
@@ -3276,57 +3186,6 @@ int dc_msg_is_setupmessage (const dc_msg_t* msg);
|
||||
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get url of a videochat invitation.
|
||||
*
|
||||
* Videochat invitations are sent out using dc_send_videochat_invitation()
|
||||
* and dc_msg_get_viewtype() returns #DC_MSG_VIDEOCHAT_INVITATION for such invitations.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return If the message contains a videochat invitation,
|
||||
* the url of the invitation is returned.
|
||||
* If the message is no videochat invitation, NULL is returned.
|
||||
* Must be released using dc_str_unref() when done.
|
||||
*/
|
||||
char* dc_msg_get_videochat_url (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get type of videochat.
|
||||
*
|
||||
* Calling this functions only makes sense for messages of type #DC_MSG_VIDEOCHAT_INVITATION,
|
||||
* in this case, if "basic webrtc" as of https://github.com/cracker0dks/basicwebrtc was used to initiate the videochat,
|
||||
* dc_msg_get_videochat_type() returns DC_VIDEOCHATTYPE_BASICWEBRTC.
|
||||
* "basic webrtc" videochat may be processed natively by the app
|
||||
* whereas for other urls just the browser is opened.
|
||||
*
|
||||
* The videochat-url can be retrieved using dc_msg_get_videochat_url().
|
||||
* To check if a message is a videochat invitation at all, check the message type for #DC_MSG_VIDEOCHAT_INVITATION.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Type of the videochat as of DC_VIDEOCHATTYPE_BASICWEBRTC or DC_VIDEOCHATTYPE_UNKNOWN.
|
||||
*
|
||||
* Example:
|
||||
* ~~~
|
||||
* if (dc_msg_get_viewtype(msg) == DC_MSG_VIDEOCHAT_INVITATION) {
|
||||
* if (dc_msg_get_videochat_type(msg) == DC_VIDEOCHATTYPE_BASICWEBRTC) {
|
||||
* // videochat invitation that we ship a client for
|
||||
* } else {
|
||||
* // use browser for videochat, just open the url
|
||||
* }
|
||||
* } else {
|
||||
* // not a videochat invitation
|
||||
* }
|
||||
* ~~~
|
||||
*/
|
||||
int dc_msg_get_videochat_type (const dc_msg_t* msg);
|
||||
|
||||
#define DC_VIDEOCHATTYPE_UNKNOWN 0
|
||||
#define DC_VIDEOCHATTYPE_BASICWEBRTC 1
|
||||
|
||||
|
||||
/**
|
||||
* Set the text of a message object.
|
||||
* This does not alter any information in the database; this may be done by dc_send_msg() later.
|
||||
@@ -3862,18 +3721,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
#define DC_MSG_FILE 60
|
||||
|
||||
|
||||
/**
|
||||
* Message indicating an incoming or outgoing videochat.
|
||||
* The message was created via dc_send_videochat_invitation() on this or a remote device.
|
||||
*
|
||||
* Typically, such messages are rendered differently by the UIs,
|
||||
* eg. contain a button to join the videochat.
|
||||
* The url for joining can be retrieved using dc_msg_get_videochat_url().
|
||||
*/
|
||||
#define DC_MSG_VIDEOCHAT_INVITATION 70
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -4231,12 +4078,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
* Network errors should be reported to users in a non-disturbing way,
|
||||
* however, as network errors may come in a sequence,
|
||||
* it is not useful to raise each an every error to the user.
|
||||
* For this purpose, data1 is set to 1 if the error is probably worth reporting.
|
||||
*
|
||||
* Moreover, if the UI detects that the device is offline,
|
||||
* it is probably more useful to report this to the user
|
||||
* instead of the string from data2.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data1 (int) 1=first/new network error, should be reported the user;
|
||||
* 0=subsequent network error, should be logged only
|
||||
* @param data2 (char*) Error string, always set, never NULL.
|
||||
*/
|
||||
#define DC_EVENT_ERROR_NETWORK 401
|
||||
@@ -4322,11 +4171,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_CHAT_MODIFIED 2020
|
||||
|
||||
/**
|
||||
* Chat ephemeral timer changed.
|
||||
*/
|
||||
#define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, verified, blocked or deleted.
|
||||
@@ -4463,7 +4307,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provider works out-of-the-box.
|
||||
* Prover works out-of-the-box.
|
||||
* This provider status is returned for provider where the login
|
||||
* works by just entering the name or the email-address.
|
||||
*
|
||||
@@ -4609,17 +4453,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_STR_UNKNOWN_SENDER_FOR_CHAT 72
|
||||
#define DC_STR_SUBJECT_FOR_NEW_CONTACT 73
|
||||
#define DC_STR_FAILED_SENDING_TO 74
|
||||
#define DC_STR_EPHEMERAL_DISABLED 75
|
||||
#define DC_STR_EPHEMERAL_SECONDS 76
|
||||
#define DC_STR_EPHEMERAL_MINUTE 77
|
||||
#define DC_STR_EPHEMERAL_HOUR 78
|
||||
#define DC_STR_EPHEMERAL_DAY 79
|
||||
#define DC_STR_EPHEMERAL_WEEK 80
|
||||
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
|
||||
#define DC_STR_VIDEOCHAT_INVITATION 82
|
||||
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
|
||||
|
||||
#define DC_STR_COUNT 83
|
||||
#define DC_STR_COUNT 74
|
||||
|
||||
/*
|
||||
* @}
|
||||
|
||||
@@ -1,56 +1,46 @@
|
||||
use crate::chat::ChatItem;
|
||||
use crate::constants::{DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
|
||||
use crate::location::Location;
|
||||
use crate::message::MsgId;
|
||||
|
||||
/* * the structure behind dc_array_t */
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum dc_array_t {
|
||||
MsgIds(Vec<MsgId>),
|
||||
Chat(Vec<ChatItem>),
|
||||
Locations(Vec<Location>),
|
||||
Uint(Vec<u32>),
|
||||
}
|
||||
|
||||
impl dc_array_t {
|
||||
pub(crate) fn get_id(&self, index: usize) -> u32 {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
dc_array_t::Uint(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
/// Constructs a new, empty `dc_array_t` holding locations with specified `capacity`.
|
||||
pub fn new_locations(capacity: usize) -> Self {
|
||||
dc_array_t::Locations(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
pub fn add_id(&mut self, item: u32) {
|
||||
if let Self::Uint(array) = self {
|
||||
array.push(item);
|
||||
} else {
|
||||
panic!("Attempt to add id to array of other type");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_location(&mut self, location: Location) {
|
||||
if let Self::Locations(array) = self {
|
||||
array.push(location)
|
||||
} else {
|
||||
panic!("Attempt to add a location to array of other type");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_id(&self, index: usize) -> u32 {
|
||||
match self {
|
||||
Self::MsgIds(array) => array[index].to_u32(),
|
||||
Self::Chat(array) => match array[index] {
|
||||
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||
ChatItem::Marker1 => DC_MSG_ID_MARKER1,
|
||||
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||
},
|
||||
Self::Locations(array) => array[index].location_id,
|
||||
Self::Uint(array) => array[index],
|
||||
Self::Uint(array) => array[index] as u32,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
|
||||
match self {
|
||||
Self::MsgIds(_) => None,
|
||||
Self::Chat(array) => array.get(index).and_then(|item| match item {
|
||||
ChatItem::Message { .. } => None,
|
||||
ChatItem::Marker1 { .. } => None,
|
||||
ChatItem::DayMarker { timestamp } => Some(*timestamp),
|
||||
}),
|
||||
Self::Locations(array) => array.get(index).map(|location| location.timestamp),
|
||||
Self::Uint(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
|
||||
match self {
|
||||
Self::MsgIds(_) => None,
|
||||
Self::Chat(_) => None,
|
||||
Self::Locations(array) => array
|
||||
.get(index)
|
||||
.and_then(|location| location.marker.as_deref()),
|
||||
Self::Uint(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_location(&self, index: usize) -> &Location {
|
||||
pub fn get_location(&self, index: usize) -> &Location {
|
||||
if let Self::Locations(array) = self {
|
||||
&array[index]
|
||||
} else {
|
||||
@@ -58,18 +48,55 @@ impl dc_array_t {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of elements in the array.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Locations(array) => array.is_empty(),
|
||||
Self::Uint(array) => array.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of elements in the array.
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
Self::MsgIds(array) => array.len(),
|
||||
Self::Chat(array) => array.len(),
|
||||
Self::Locations(array) => array.len(),
|
||||
Self::Uint(array) => array.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn search_id(&self, needle: u32) -> Option<usize> {
|
||||
(0..self.len()).find(|i| self.get_id(*i) == needle)
|
||||
pub fn clear(&mut self) {
|
||||
match self {
|
||||
Self::Locations(array) => array.clear(),
|
||||
Self::Uint(array) => array.clear(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_id(&self, needle: u32) -> Option<usize> {
|
||||
if let Self::Uint(array) = self {
|
||||
for (i, &u) in array.iter().enumerate() {
|
||||
if u == needle {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
} else {
|
||||
panic!("Attempt to search for id in array of other type");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sort_ids(&mut self) {
|
||||
if let dc_array_t::Uint(v) = self {
|
||||
v.sort();
|
||||
} else {
|
||||
panic!("Attempt to sort array of something other than uints");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> *const u32 {
|
||||
if let dc_array_t::Uint(v) = self {
|
||||
v.as_ptr()
|
||||
} else {
|
||||
panic!("Attempt to convert array of something other than uints to raw");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,18 +106,6 @@ impl From<Vec<u32>> for dc_array_t {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<MsgId>> for dc_array_t {
|
||||
fn from(array: Vec<MsgId>) -> Self {
|
||||
dc_array_t::MsgIds(array)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ChatItem>> for dc_array_t {
|
||||
fn from(array: Vec<ChatItem>) -> Self {
|
||||
dc_array_t::Chat(array)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Location>> for dc_array_t {
|
||||
fn from(array: Vec<Location>) -> Self {
|
||||
dc_array_t::Locations(array)
|
||||
@@ -103,11 +118,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_dc_array() {
|
||||
let arr: dc_array_t = Vec::<u32>::new().into();
|
||||
assert!(arr.len() == 0);
|
||||
let mut arr = dc_array_t::new(7);
|
||||
assert!(arr.is_empty());
|
||||
|
||||
let ids: Vec<u32> = (2..1002).collect();
|
||||
let arr: dc_array_t = ids.into();
|
||||
for i in 0..1000 {
|
||||
arr.add_id(i + 2);
|
||||
}
|
||||
|
||||
assert_eq!(arr.len(), 1000);
|
||||
|
||||
@@ -115,15 +131,31 @@ mod tests {
|
||||
assert_eq!(arr.get_id(i), (i + 2) as u32);
|
||||
}
|
||||
|
||||
assert_eq!(arr.search_id(10), Some(8));
|
||||
assert_eq!(arr.search_id(1), None);
|
||||
arr.clear();
|
||||
|
||||
assert!(arr.is_empty());
|
||||
|
||||
arr.add_id(13);
|
||||
arr.add_id(7);
|
||||
arr.add_id(666);
|
||||
arr.add_id(0);
|
||||
arr.add_id(5000);
|
||||
|
||||
arr.sort_ids();
|
||||
|
||||
assert_eq!(arr.get_id(0), 0);
|
||||
assert_eq!(arr.get_id(1), 7);
|
||||
assert_eq!(arr.get_id(2), 13);
|
||||
assert_eq!(arr.get_id(3), 666);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_dc_array_out_of_bounds() {
|
||||
let ids: Vec<u32> = (2..1002).collect();
|
||||
let arr: dc_array_t = ids.into();
|
||||
let mut arr = dc_array_t::new(7);
|
||||
for i in 0..1000 {
|
||||
arr.add_id(i + 2);
|
||||
}
|
||||
arr.get_id(1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![deny(clippy::all)]
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
@@ -28,7 +27,6 @@ use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::stock::StockMessage;
|
||||
@@ -128,7 +126,7 @@ pub unsafe extern "C" fn dc_set_config(
|
||||
// When ctx.set_config() fails it already logged the error.
|
||||
// TODO: Context::set_config() should not log this
|
||||
Ok(key) => block_on(async move {
|
||||
ctx.set_config(key, to_opt_string_lossy(value).as_deref())
|
||||
ctx.set_config(key, to_opt_string_lossy(value).as_ref().map(|x| x.as_str()))
|
||||
.await
|
||||
.is_ok() as libc::c_int
|
||||
}),
|
||||
@@ -287,7 +285,7 @@ pub unsafe extern "C" fn dc_start_io(context: *mut dc_context_t) {
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(ctx.start_io())
|
||||
block_on({ ctx.start_io() })
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -297,7 +295,7 @@ pub unsafe extern "C" fn dc_is_io_running(context: *mut dc_context_t) -> libc::c
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(ctx.is_io_running()) as libc::c_int
|
||||
block_on({ ctx.is_io_running() }) as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -351,8 +349,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| Event::MsgDelivered { chat_id, .. }
|
||||
| Event::MsgFailed { chat_id, .. }
|
||||
| Event::MsgRead { chat_id, .. }
|
||||
| Event::ChatModified(chat_id)
|
||||
| Event::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
| Event::ChatModified(chat_id) => chat_id.to_u32() as libc::c_int,
|
||||
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
|
||||
let id = id.unwrap_or_default();
|
||||
id as libc::c_int
|
||||
@@ -402,7 +399,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| Event::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
Event::SecurejoinInviterProgress { progress, .. }
|
||||
| Event::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||
Event::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,8 +439,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| Event::ConfigureProgress(_)
|
||||
| Event::ImexProgress(_)
|
||||
| Event::SecurejoinInviterProgress { .. }
|
||||
| Event::SecurejoinJoinerProgress { .. }
|
||||
| Event::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
| Event::SecurejoinJoinerProgress { .. } => ptr::null_mut(),
|
||||
Event::ImexFileWritten(file) => {
|
||||
let data2 = file.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
@@ -487,7 +482,7 @@ pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *
|
||||
events
|
||||
.recv_sync()
|
||||
.map(|ev| Box::into_raw(Box::new(ev)))
|
||||
.unwrap_or_else(ptr::null_mut)
|
||||
.unwrap_or_else(|| ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -559,7 +554,14 @@ pub unsafe extern "C" fn dc_get_chatlist(
|
||||
let qi = if query_id == 0 { None } else { Some(query_id) };
|
||||
|
||||
block_on(async move {
|
||||
match chatlist::Chatlist::try_load(&ctx, flags as usize, qs.as_deref(), qi).await {
|
||||
match chatlist::Chatlist::try_load(
|
||||
&ctx,
|
||||
flags as usize,
|
||||
qs.as_ref().map(|x| x.as_str()),
|
||||
qi,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(list) => {
|
||||
let ffi_list = ChatlistWrapper { context, list };
|
||||
Box::into_raw(Box::new(ffi_list))
|
||||
@@ -710,25 +712,6 @@ pub unsafe extern "C" fn dc_send_text_msg(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_videochat_invitation()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(&ctx, "Failed to send video chat invitation")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -769,9 +752,13 @@ pub unsafe extern "C" fn dc_add_device_msg(
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
chat::add_device_msg(&ctx, to_opt_string_lossy(label).as_deref(), msg)
|
||||
.await
|
||||
.unwrap_or_log_default(&ctx, "Failed to add device message")
|
||||
chat::add_device_msg(
|
||||
&ctx,
|
||||
to_opt_string_lossy(label).as_ref().map(|x| x.as_str()),
|
||||
msg,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_log_default(&ctx, "Failed to add device message")
|
||||
})
|
||||
.to_u32()
|
||||
}
|
||||
@@ -854,11 +841,14 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
let arr = dc_array_t::from(
|
||||
chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag)
|
||||
.await
|
||||
.into(),
|
||||
))
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -987,7 +977,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {}", or_msg_type3));
|
||||
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
let arr = dc_array_t::from(
|
||||
chat::get_chat_media(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
@@ -996,8 +986,11 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
or_msg_type3,
|
||||
)
|
||||
.await
|
||||
.into(),
|
||||
))
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1303,49 +1296,6 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_chat_ephemeral_timer(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_chat_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
// Timer value 0 is returned in the rare case of a database error,
|
||||
// but it is not dangerous since it is only meant to be used as a
|
||||
// default when changing the value. Such errors should not be
|
||||
// ignored when ephemeral timer value is used to construct
|
||||
// message headers.
|
||||
block_on(async move { ChatId::new(chat_id).get_ephemeral_timer(ctx).await })
|
||||
.log_err(ctx, "Failed to get ephemeral timer")
|
||||
.unwrap_or_default()
|
||||
.to_u32()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_chat_ephemeral_timer(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
timer: u32,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_set_chat_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
ChatId::new(chat_id)
|
||||
.set_ephemeral_timer(ctx, EphemeralTimer::from_u32(timer))
|
||||
.await
|
||||
.log_err(ctx, "Failed to set ephemeral timer")
|
||||
.is_ok() as libc::c_int
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_msg_info(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1879,11 +1829,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(location::send_locations_to_chat(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
seconds as i64,
|
||||
));
|
||||
block_on({ location::send_locations_to_chat(&ctx, ChatId::new(chat_id), seconds as i64) });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2041,7 +1987,7 @@ pub unsafe extern "C" fn dc_array_get_timestamp(
|
||||
return 0;
|
||||
}
|
||||
|
||||
(*array).get_timestamp(index).unwrap_or_default()
|
||||
(*array).get_location(index).timestamp
|
||||
}
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_get_chat_id(
|
||||
@@ -2088,7 +2034,7 @@ pub unsafe extern "C" fn dc_array_get_marker(
|
||||
return std::ptr::null_mut(); // NULL explicitly defined as "no markers"
|
||||
}
|
||||
|
||||
if let Some(s) = (*array).get_marker(index) {
|
||||
if let Some(s) = &(*array).get_location(index).marker {
|
||||
s.strdup()
|
||||
} else {
|
||||
std::ptr::null_mut()
|
||||
@@ -2116,6 +2062,16 @@ pub unsafe extern "C" fn dc_array_search_id(
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_get_raw(array: *const dc_array_t) -> *const u32 {
|
||||
if array.is_null() {
|
||||
eprintln!("ignoring careless call to dc_array_get_raw()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
(*array).as_ptr()
|
||||
}
|
||||
|
||||
// Return the independent-state of the location at the given index.
|
||||
// Independent locations do not belong to the track of the user.
|
||||
// Returns 1 if location belongs to the track of the user,
|
||||
@@ -2695,26 +2651,6 @@ pub unsafe extern "C" fn dc_msg_get_showpadlock(msg: *mut dc_msg_t) -> libc::c_i
|
||||
ffi_msg.message.get_showpadlock() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_ephemeral_timer(msg: *mut dc_msg_t) -> u32 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_ephemeral_timer()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_ephemeral_timestamp(msg: *mut dc_msg_t) -> i64 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_ephemeral_timestamp()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_summary(
|
||||
msg: *mut dc_msg_t,
|
||||
@@ -2839,31 +2775,6 @@ pub unsafe extern "C" fn dc_msg_is_setupmessage(msg: *mut dc_msg_t) -> libc::c_i
|
||||
ffi_msg.message.is_setupmessage().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_videochat_url(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_videochat_url()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
|
||||
ffi_msg
|
||||
.message
|
||||
.get_videochat_url()
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_videochat_type(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_videochat_type()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_videochat_type().unwrap_or_default() as i32
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
@@ -2901,7 +2812,7 @@ pub unsafe extern "C" fn dc_msg_set_file(
|
||||
let ffi_msg = &mut *msg;
|
||||
ffi_msg.message.set_file(
|
||||
to_string_lossy(file),
|
||||
to_opt_string_lossy(filemime).as_deref(),
|
||||
to_opt_string_lossy(filemime).as_ref().map(|x| x.as_str()),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -105,9 +105,8 @@ impl<T: AsRef<std::ffi::OsStr>> OsStrExt for T {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn to_c_string(&self) -> Result<CString, CStringError> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
CString::new(self.as_ref().as_bytes()).map_err(|err| {
|
||||
let std::ffi::NulError { .. } = err;
|
||||
CStringError::InteriorNullByte
|
||||
CString::new(self.as_ref().as_bytes()).map_err(|err| match err {
|
||||
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,9 +122,8 @@ fn os_str_to_c_string_unicode(
|
||||
os_str: &dyn AsRef<std::ffi::OsStr>,
|
||||
) -> Result<CString, CStringError> {
|
||||
match os_str.as_ref().to_str() {
|
||||
Some(val) => CString::new(val.as_bytes()).map_err(|err| {
|
||||
let std::ffi::NulError { .. } = err;
|
||||
CStringError::InteriorNullByte
|
||||
Some(val) => CString::new(val.as_bytes()).map_err(|err| match err {
|
||||
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
||||
}),
|
||||
None => Err(CStringError::NotUnicode),
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
|
||||
simplify/streamline mark-seen/delete/move/send-mdn job handling
|
||||
---------------------------------------------------------------
|
||||
|
||||
Idea: Introduce a new "msgwork" sql table that looks very
|
||||
much like the jobs table but has a primary key "msgid"
|
||||
and no job id and no foreign-id anymore. This opens up
|
||||
bulk-processing by looking at the whole table and combining
|
||||
flag-setting to reduce imap-roundtrips and select-folder calls.
|
||||
|
||||
Concretely, these IMAP jobs:
|
||||
|
||||
DeleteMsgOnImap
|
||||
MarkseenMsgOnImap
|
||||
MoveMsg
|
||||
|
||||
Would be replaced by a few per-message columns in the new msgwork table:
|
||||
|
||||
- needs_mark_seen: (bool) message shall be marked as seen on imap
|
||||
- needs_to_move: (bool) message should be moved to mvbox_folder
|
||||
- deletion_time: (target_time or 0) message shall be deleted at specified time
|
||||
- needs_send_mdn: (bool) MDN shall be sent
|
||||
|
||||
The various places that currently add the (replaced) jobs
|
||||
would now add/modify the respective message record in the message-work table.
|
||||
|
||||
Looking at a single message-work entry conceptually looks like this::
|
||||
|
||||
if msg.server_uid==0:
|
||||
return RetryLater # nothing can be done without server_uid
|
||||
|
||||
if msg.deletion_time > current_time:
|
||||
imap.mark_delete(msg) # might trigger early exit with a RetryLater/Failed
|
||||
clear(needs_deletion)
|
||||
clear(mark_seen)
|
||||
|
||||
if needs_mark_seen:
|
||||
imap.mark_seen(msg) # might trigger early exit with a RetryLater/Failed
|
||||
clear(needs_mark_seen)
|
||||
|
||||
if needs_send_mdn:
|
||||
schedule_smtp_send_mdn(msg)
|
||||
clear(needs_send_mdn)
|
||||
|
||||
if any_flag_set():
|
||||
retrylater
|
||||
# remove msgwork entry from table
|
||||
|
||||
|
||||
Notes/Questions:
|
||||
|
||||
- it's unclear how much we need per-message retry-time tracking/backoff
|
||||
|
||||
- drafting bulk processing algo is useful before
|
||||
going for the implementation, i.e. including select_folder calls etc.
|
||||
|
||||
- maybe it's better to not have bools for the flags but
|
||||
|
||||
0 (no change)
|
||||
1 (set the imap flag)
|
||||
2 (clear the imap flag)
|
||||
|
||||
and design such that we can cover all imap flags.
|
||||
|
||||
- It might not be neccessary to keep needs_send_mdn state in this table
|
||||
if this can be decided rather when we succeed with mark_seen/mark_delete.
|
||||
@@ -2,7 +2,7 @@ use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use async_std::path::Path;
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
@@ -183,7 +183,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
|
||||
let msgtext = msg.get_text();
|
||||
println!(
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{} [{}]",
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]",
|
||||
prefix.as_ref(),
|
||||
msg.get_id(),
|
||||
if msg.get_showpadlock() { "🔒" } else { "" },
|
||||
@@ -202,15 +202,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
"[FRESH]"
|
||||
},
|
||||
if msg.is_info() { "[INFO]" } else { "" },
|
||||
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
||||
format!(
|
||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||
msg.get_videochat_url().unwrap_or_default(),
|
||||
msg.get_videochat_type().unwrap_or_default()
|
||||
)
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
if msg.is_forwarded() {
|
||||
"[FORWARDED]"
|
||||
} else {
|
||||
@@ -224,7 +215,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
|
||||
let mut lines_out = 0;
|
||||
for &msg_id in msglist {
|
||||
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
|
||||
if msg_id.is_daymarker() {
|
||||
println!(
|
||||
"--------------------------------------------------------------------------------"
|
||||
);
|
||||
@@ -368,7 +359,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
send-garbage\n\
|
||||
sendimage <file> [<text>]\n\
|
||||
sendfile <file> [<text>]\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
listmedia\n\
|
||||
@@ -498,7 +488,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"listchats" | "listarchived" | "chats" => {
|
||||
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
|
||||
let time_start = std::time::SystemTime::now();
|
||||
let chatlist = Chatlist::try_load(
|
||||
&context,
|
||||
listflags,
|
||||
@@ -506,9 +495,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let time_needed = std::time::SystemTime::now()
|
||||
.duration_since(time_start)
|
||||
.unwrap_or_default();
|
||||
|
||||
let cnt = chatlist.len();
|
||||
if cnt > 0 {
|
||||
@@ -567,7 +553,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{} chats", cnt);
|
||||
println!("{:?} to create this list", time_needed);
|
||||
}
|
||||
"chat" => {
|
||||
if sel_chat.is_none() && arg1.is_empty() {
|
||||
@@ -584,15 +569,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let sel_chat = sel_chat.as_ref().unwrap();
|
||||
|
||||
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await;
|
||||
let msglist: Vec<MsgId> = msglist
|
||||
.into_iter()
|
||||
.map(|x| match x {
|
||||
ChatItem::Message { msg_id } => msg_id,
|
||||
ChatItem::Marker1 => MsgId::new(DC_MSG_ID_MARKER1),
|
||||
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let members = chat::get_chat_contacts(&context, sel_chat.id).await;
|
||||
let subtitle = if sel_chat.is_device_talk() {
|
||||
"device-talk".to_string()
|
||||
@@ -818,10 +794,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
}
|
||||
"listmsgs" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ const DB_COMMANDS: [&str; 9] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 27] = [
|
||||
const CHAT_COMMANDS: [&str; 26] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
@@ -178,7 +178,6 @@ const CHAT_COMMANDS: [&str; 27] = [
|
||||
"send",
|
||||
"sendimage",
|
||||
"sendfile",
|
||||
"videochat",
|
||||
"draft",
|
||||
"listmedia",
|
||||
"archive",
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
1.40.1
|
||||
0.900.0 (DRAFT)
|
||||
---------------
|
||||
|
||||
- emit "ac_member_removed" event (with 'actor' being the removed contact)
|
||||
for when a user leaves a group.
|
||||
|
||||
- fix create_contact(addr) when addr is the self-contact.
|
||||
|
||||
|
||||
1.40.0
|
||||
---------------
|
||||
|
||||
- uses latest 1.40+ Delta Chat core
|
||||
|
||||
- refactored internals to use plugin-approach
|
||||
|
||||
- introduced PerAccount and Global hooks that plugins can implement
|
||||
@@ -21,7 +10,6 @@
|
||||
- introduced two documented examples for an echo and a group-membership
|
||||
tracking plugin.
|
||||
|
||||
|
||||
0.800.0
|
||||
-------
|
||||
|
||||
|
||||
@@ -7,14 +7,76 @@ which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
|
||||
a low-level Chat/Contact/Message API to user interfaces and bots.
|
||||
|
||||
|
||||
Installing bindings from source (Updated: 20-Jan-2020)
|
||||
=========================================================
|
||||
|
||||
Install Rust and Cargo first. Deltachat needs a specific nightly
|
||||
version, the easiest is probably to first install Rust stable from
|
||||
rustup and then use this to install the correct nightly version.
|
||||
|
||||
Bootstrap Rust and Cargo by using rustup::
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh
|
||||
|
||||
Then GIT clone the deltachat-core-rust repo and get the actual
|
||||
rust- and cargo-toolchain needed by deltachat::
|
||||
|
||||
git clone https://github.com/deltachat/deltachat-core-rust
|
||||
cd deltachat-core-rust
|
||||
rustup show
|
||||
|
||||
To install the Delta Chat Python bindings make sure you have Python3 installed.
|
||||
E.g. on Debian-based systems `apt install python3 python3-pip
|
||||
python3-venv` should give you a usable python installation.
|
||||
|
||||
Ensure you are in the deltachat-core-rust/python directory, create the
|
||||
virtual environment and activate it in your shell::
|
||||
|
||||
cd python
|
||||
python3 -m venv venv # or: virtualenv venv
|
||||
source venv/bin/activate
|
||||
|
||||
You should now be able to build the python bindings using the supplied script::
|
||||
|
||||
./install_python_bindings.py
|
||||
|
||||
The installation might take a while, depending on your machine.
|
||||
The bindings will be installed in release mode but with debug symbols.
|
||||
The release mode is currently necessary because some tests generate RSA keys
|
||||
which is prohibitively slow in non-release mode.
|
||||
|
||||
After successful binding installation you can install a few more
|
||||
Python packages before running the tests::
|
||||
|
||||
python -m pip install pytest pytest-timeout pytest-rerunfailures requests
|
||||
pytest -v tests
|
||||
|
||||
|
||||
running "live" tests with temporary accounts
|
||||
---------------------------------------------
|
||||
|
||||
If you want to run "liveconfig" functional tests you can set
|
||||
``DCC_NEW_TMP_EMAIL`` to:
|
||||
|
||||
- a particular https-url that you can ask for from the delta
|
||||
chat devs. This is implemented on the server side via
|
||||
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
|
||||
|
||||
- or the path of a file that contains two lines, each describing
|
||||
via "addr=... mail_pw=..." a test account login that will
|
||||
be used for the live tests.
|
||||
|
||||
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
|
||||
e-mail accounts and run through all functional "liveconfig" tests.
|
||||
|
||||
|
||||
Installing pre-built packages (Linux-only)
|
||||
========================================================
|
||||
|
||||
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
|
||||
without any "build-from-source" steps. Otherwise you need to `compile the Delta Chat bindings
|
||||
yourself <sourceinstall>`_.
|
||||
without any "build-from-source" steps.
|
||||
|
||||
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
||||
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
||||
then create a fresh Python virtual environment and activate it in your shell::
|
||||
|
||||
virtualenv venv # or: python -m venv
|
||||
@@ -41,78 +103,6 @@ To verify it worked::
|
||||
`in contact with us <https://delta.chat/en/contribute>`_.
|
||||
|
||||
|
||||
Running tests
|
||||
=============
|
||||
|
||||
After successful binding installation you can install a few more
|
||||
Python packages before running the tests::
|
||||
|
||||
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
|
||||
pytest -v tests
|
||||
|
||||
This will run all "offline" tests and skip all functional
|
||||
end-to-end tests that require accounts on real e-mail servers.
|
||||
|
||||
.. _livetests:
|
||||
|
||||
running "live" tests with temporary accounts
|
||||
---------------------------------------------
|
||||
|
||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL``::
|
||||
|
||||
export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=1h_4w4r8h7y9nmcdsy
|
||||
|
||||
With this, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
|
||||
These accounts exists for one 1hour and then are removed completely.
|
||||
One hour is enough to invoke pytest and run all offline and online tests:
|
||||
|
||||
pytest
|
||||
|
||||
# or if you have installed pytest-xdist for parallel test execution
|
||||
pytest -n6
|
||||
|
||||
Each test run creates new accounts.
|
||||
|
||||
|
||||
.. _sourceinstall:
|
||||
|
||||
Installing bindings from source (Updated: July 2020)
|
||||
=========================================================
|
||||
|
||||
Install Rust and Cargo first.
|
||||
The easiest is probably to use `rustup <https://rustup.rs/>`_.
|
||||
|
||||
Bootstrap Rust and Cargo by using rustup::
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh
|
||||
|
||||
Then clone the deltachat-core-rust repo::
|
||||
|
||||
git clone https://github.com/deltachat/deltachat-core-rust
|
||||
cd deltachat-core-rust
|
||||
|
||||
To install the Delta Chat Python bindings make sure you have Python3 installed.
|
||||
E.g. on Debian-based systems `apt install python3 python3-pip
|
||||
python3-venv` should give you a usable python installation.
|
||||
|
||||
Ensure you are in the deltachat-core-rust/python directory, create the
|
||||
virtual environment and activate it in your shell::
|
||||
|
||||
cd python
|
||||
python3 -m venv venv # or: virtualenv venv
|
||||
source venv/bin/activate
|
||||
|
||||
You should now be able to build the python bindings using the supplied script::
|
||||
|
||||
python install_python_bindings.py
|
||||
|
||||
The core compilation and bindings building might take a while,
|
||||
depending on the speed of your machine.
|
||||
The bindings will be installed in release mode but with debug symbols.
|
||||
The release mode is currently necessary because some tests generate RSA keys
|
||||
which is prohibitively slow in non-release mode.
|
||||
|
||||
|
||||
Code examples
|
||||
=============
|
||||
|
||||
|
||||
3
python/doc/_templates/globaltoc.html
vendored
3
python/doc/_templates/globaltoc.html
vendored
@@ -9,7 +9,8 @@
|
||||
</ul>
|
||||
<b>external links:</b>
|
||||
<ul>
|
||||
<li><a href="https://github.com/deltachat/deltachat-core-rust">github repository</a></li>
|
||||
<li><a href="https://github.com/deltachat/deltachat-core">github repository</a></li>
|
||||
<!-- <li><a href="https://lists.codespeak.net/postorius/lists/muacrypt.lists.codespeak.net">Mailing list</></li> <-->
|
||||
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -32,16 +32,16 @@ class GroupTrackingPlugin:
|
||||
print("chat member: {}".format(member.addr))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
print("ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr))
|
||||
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||
for member in chat.get_contacts():
|
||||
print("chat member: {}".format(member.addr))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
def ac_member_removed(self, chat, contact, message):
|
||||
print("ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr))
|
||||
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
|
||||
@@ -69,11 +69,11 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
|
||||
lp.sec("now looking at what the bot received")
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_added {}*from*{}*
|
||||
""".format(contact3.addr, ac1.get_config("addr")))
|
||||
*ac_member_added {}*
|
||||
""".format(contact3.addr))
|
||||
|
||||
lp.sec("contact successfully added, now removing")
|
||||
ch.remove_contact(contact3)
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_removed {}*from*{}*
|
||||
""".format(contact3.addr, ac1.get_config("addr")))
|
||||
*ac_member_removed {}*
|
||||
""".format(contact3.addr))
|
||||
|
||||
@@ -246,6 +246,7 @@ class Account(object):
|
||||
addr = as_dc_charpointer(addr)
|
||||
name = as_dc_charpointer(name)
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL, contact_id
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def delete_contact(self, contact):
|
||||
@@ -606,24 +607,12 @@ class Account(object):
|
||||
self.stop_io()
|
||||
|
||||
self.log("remove dc_context references")
|
||||
|
||||
# if _dc_context is unref'ed the event thread should quickly
|
||||
# receive the termination signal. However, some python code might
|
||||
# still hold a reference and so we use a secondary signal
|
||||
# to make sure the even thread terminates if it receives any new
|
||||
# event, indepedently from waiting for the core to send NULL to
|
||||
# get_next_event().
|
||||
self._event_thread.mark_shutdown()
|
||||
# the dc_context_unref triggers get_next_event to return ffi.NULL
|
||||
# which in turns makes the event thread finish execution
|
||||
self._dc_context = None
|
||||
|
||||
self.log("wait for event thread to finish")
|
||||
try:
|
||||
self._event_thread.wait(timeout=2)
|
||||
except RuntimeError as e:
|
||||
self.log("Waiting for event thread failed: {}".format(e))
|
||||
|
||||
if self._event_thread.is_alive():
|
||||
self.log("WARN: event thread did not terminate yet, ignoring.")
|
||||
self._event_thread.wait()
|
||||
|
||||
self._shutdown_event.set()
|
||||
|
||||
|
||||
@@ -139,22 +139,6 @@ class Chat(object):
|
||||
"""
|
||||
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
|
||||
|
||||
def get_ephemeral_timer(self):
|
||||
""" get ephemeral timer.
|
||||
|
||||
:returns: ephemeral timer value in seconds
|
||||
"""
|
||||
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
|
||||
|
||||
def set_ephemeral_timer(self, timer):
|
||||
""" set ephemeral timer.
|
||||
|
||||
:param: timer value in seconds
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
|
||||
|
||||
def get_type(self):
|
||||
""" (deprecated) return type of this chat.
|
||||
|
||||
|
||||
@@ -159,13 +159,9 @@ class DirectImap:
|
||||
log("---------", imapfolder, len(messages), "messages ---------")
|
||||
# get message content without auto-marking it as seen
|
||||
# fetching 'RFC822' would mark it as seen.
|
||||
requested = [b'BODY.PEEK[]', FLAGS]
|
||||
requested = [b'BODY.PEEK[HEADER]', FLAGS]
|
||||
for uid, data in self.conn.fetch(messages, requested).items():
|
||||
body_bytes = data[b'BODY[]']
|
||||
if not body_bytes:
|
||||
log("Message", uid, "has empty body")
|
||||
continue
|
||||
|
||||
body_bytes = data[b'BODY[HEADER]']
|
||||
flags = data[FLAGS]
|
||||
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
@@ -196,7 +192,6 @@ class DirectImap:
|
||||
raise TimeoutError
|
||||
if terminate:
|
||||
self.idle_done()
|
||||
self.account.log("imap-direct: idle_check returned {!r}".format(res))
|
||||
return res
|
||||
|
||||
def idle_wait_for_seen(self):
|
||||
|
||||
@@ -86,11 +86,11 @@ class FFIEventTracker:
|
||||
if rex.match(ev.name):
|
||||
return ev
|
||||
|
||||
def get_info_contains(self, regex):
|
||||
rex = re.compile(regex)
|
||||
def get_info_matching(self, regex):
|
||||
rex = re.compile("(?:{}).*".format(regex))
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
if rex.search(ev.data2):
|
||||
if rex.match(ev.data2):
|
||||
return ev
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
@@ -139,7 +139,6 @@ class EventThread(threading.Thread):
|
||||
self.account = account
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.setDaemon(True)
|
||||
self._marked_for_shutdown = False
|
||||
self.start()
|
||||
|
||||
@contextmanager
|
||||
@@ -148,15 +147,12 @@ class EventThread(threading.Thread):
|
||||
yield
|
||||
self.account.log(message + " FINISHED")
|
||||
|
||||
def mark_shutdown(self):
|
||||
self._marked_for_shutdown = True
|
||||
|
||||
def wait(self, timeout=None):
|
||||
def wait(self):
|
||||
if self == threading.current_thread():
|
||||
# we are in the callback thread and thus cannot
|
||||
# wait for the thread-loop to finish.
|
||||
return
|
||||
self.join(timeout=timeout)
|
||||
self.join()
|
||||
|
||||
def run(self):
|
||||
""" get and run events until shutdown. """
|
||||
@@ -168,12 +164,10 @@ class EventThread(threading.Thread):
|
||||
lib.dc_get_event_emitter(self.account._dc_context),
|
||||
lib.dc_event_emitter_unref,
|
||||
)
|
||||
while not self._marked_for_shutdown:
|
||||
while 1:
|
||||
event = lib.dc_get_next_event(event_emitter)
|
||||
if event == ffi.NULL:
|
||||
break
|
||||
if self._marked_for_shutdown:
|
||||
break
|
||||
evt = lib.dc_event_get_id(event)
|
||||
data1 = lib.dc_event_get_data1_int(event)
|
||||
# the following code relates to the deltachat/_build.py's helper
|
||||
|
||||
@@ -16,7 +16,7 @@ class PerAccount:
|
||||
""" per-Account-instance hook specifications.
|
||||
|
||||
All hooks are executed in a dedicated Event thread.
|
||||
Hooks are generally not allowed to block/last long as this
|
||||
Hooks are not allowed to block/last long as this
|
||||
blocks overall event processing on the python side.
|
||||
"""
|
||||
@classmethod
|
||||
@@ -31,6 +31,10 @@ class PerAccount:
|
||||
|
||||
ffi_event has "name", "data1", "data2" values as specified
|
||||
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
|
||||
|
||||
DANGER: this hook is executed from the callback invoked by core.
|
||||
Hook implementations need to be short running and can typically
|
||||
not call back into core because this would easily cause recursion issues.
|
||||
"""
|
||||
|
||||
@account_hookspec
|
||||
@@ -51,37 +55,19 @@ class PerAccount:
|
||||
|
||||
@account_hookspec
|
||||
def ac_message_delivered(self, message):
|
||||
""" Called when an outgoing message has been delivered to SMTP.
|
||||
|
||||
:param message: Message that was just delivered.
|
||||
"""
|
||||
""" Called when an outgoing message has been delivered to SMTP. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_chat_modified(self, chat):
|
||||
""" Chat was created or modified regarding membership, avatar, title.
|
||||
|
||||
:param chat: Chat which was modified.
|
||||
"""
|
||||
""" Chat was created or modified regarding membership, avatar, title. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
""" Called for each contact added to an accepted chat.
|
||||
|
||||
:param chat: Chat where contact was added.
|
||||
:param contact: Contact that was added.
|
||||
:param actor: Who added the contact (None if it was our self-addr)
|
||||
:param message: The original system message that reports the addition.
|
||||
"""
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
""" Called for each contact added to an accepted chat. """
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
""" Called for each contact removed from a chat.
|
||||
|
||||
:param chat: Chat where contact was removed.
|
||||
:param contact: Contact that was removed.
|
||||
:param actor: Who removed the contact (None if it was our self-addr)
|
||||
:param message: The original system message that reports the removal.
|
||||
"""
|
||||
def ac_member_removed(self, chat, contact, message):
|
||||
""" Called for each contact removed from a chat. """
|
||||
|
||||
|
||||
class Global:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
""" The Message object. """
|
||||
|
||||
import os
|
||||
import re
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
@@ -155,26 +154,6 @@ class Message(object):
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timer(self):
|
||||
"""Ephemeral timer in seconds
|
||||
|
||||
:returns: timer in seconds or None if there is no timer
|
||||
"""
|
||||
timer = lib.dc_msg_get_ephemeral_timer(self._dc_msg)
|
||||
if timer:
|
||||
return timer
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timestamp(self):
|
||||
"""UTC time when the message will be deleted.
|
||||
|
||||
:returns: naive datetime.datetime() object or None if the timer is not started.
|
||||
"""
|
||||
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
def get_mime_headers(self):
|
||||
""" return mime-header object for an incoming message.
|
||||
|
||||
@@ -357,43 +336,20 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
def map_system_message(msg):
|
||||
if msg.is_system_message():
|
||||
res = parse_system_add_remove(msg.text)
|
||||
if not res:
|
||||
return
|
||||
action, affected, actor = res
|
||||
affected = msg.account.get_contact_by_addr(affected)
|
||||
if actor == "me":
|
||||
actor = None
|
||||
else:
|
||||
actor = msg.account.get_contact_by_addr(actor)
|
||||
d = dict(chat=msg.chat, contact=affected, actor=actor, message=msg)
|
||||
return "ac_member_" + res[0], d
|
||||
|
||||
|
||||
def extract_addr(text):
|
||||
m = re.match(r'.*\((.+@.+)\)', text)
|
||||
if m:
|
||||
text = m.group(1)
|
||||
text = text.rstrip(".")
|
||||
return text.strip()
|
||||
if res:
|
||||
contact = msg.account.get_contact_by_addr(res[1])
|
||||
if contact:
|
||||
d = dict(chat=msg.chat, contact=contact, message=msg)
|
||||
return "ac_member_" + res[0], d
|
||||
|
||||
|
||||
def parse_system_add_remove(text):
|
||||
""" return add/remove info from parsing the given system message text.
|
||||
|
||||
returns a (action, affected, actor) triple """
|
||||
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# Member x@y added by a@b
|
||||
# Member With space (tmp1@x.org) removed by tmp2@x.org.
|
||||
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
# Group left by some one (tmp1@x.org).
|
||||
# Group left by tmp1@x.org.
|
||||
# Member x@y removed by a@b
|
||||
text = text.lower()
|
||||
m = re.match(r'member (.+) (removed|added) by (.+)', text)
|
||||
if m:
|
||||
affected, action, actor = m.groups()
|
||||
return action, extract_addr(affected), extract_addr(actor)
|
||||
if text.startswith("group left by "):
|
||||
addr = extract_addr(text[13:])
|
||||
if addr:
|
||||
return "removed", addr, addr
|
||||
parts = text.split()
|
||||
if parts[0] == "member":
|
||||
if parts[2] in ("removed", "added"):
|
||||
return parts[2], parts[1]
|
||||
if parts[3] in ("removed", "added"):
|
||||
return parts[3], parts[2].strip("()")
|
||||
|
||||
@@ -32,10 +32,6 @@ def pytest_addoption(parser):
|
||||
"--ignored", action="store_true",
|
||||
help="Also run tests marked with the ignored marker",
|
||||
)
|
||||
parser.addoption(
|
||||
"--strict-tls", action="store_true",
|
||||
help="Never accept invalid TLS certificates for test accounts",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
@@ -156,7 +152,7 @@ class SessionLiveConfigFromURL:
|
||||
assert index == len(self.configlist), index
|
||||
res = requests.post(self.url)
|
||||
if res.status_code != 200:
|
||||
pytest.skip("creating newtmpuser failed with code {}: '{}'".format(res.status_code, res.text))
|
||||
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
self.configlist.append(config)
|
||||
@@ -235,13 +231,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
def make_account(self, path, logid, quiet=False):
|
||||
ac = Account(path, logging=self._logging)
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
ac._evtracker.set_timeout(30)
|
||||
ac.addr = ac.get_self_contact().addr
|
||||
ac.set_config("displayname", logid)
|
||||
if not quiet:
|
||||
logger = FFIEventLogger(ac)
|
||||
logger.init_time = self.init_time
|
||||
ac.add_account_plugin(logger)
|
||||
ac.add_account_plugin(FFIEventLogger(ac))
|
||||
self._accounts.append(ac)
|
||||
return ac
|
||||
|
||||
@@ -251,7 +244,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
def get_unconfigured_account(self):
|
||||
self.offline_count += 1
|
||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||
return self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(2)
|
||||
return ac
|
||||
|
||||
def _preconfigure_key(self, account, addr):
|
||||
# Only set a key if we haven't used it yet for another account.
|
||||
@@ -286,15 +282,16 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
|
||||
if pytestconfig.getoption("--strict-tls"):
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(30)
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
|
||||
@@ -335,6 +332,8 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, account.get_config("addr"))
|
||||
ac._evtracker.init_time = self.init_time
|
||||
ac._evtracker.set_timeout(30)
|
||||
ac.update_config(dict(
|
||||
addr=account.get_config("addr"),
|
||||
mail_pw=account.get_config("mail_pw"),
|
||||
@@ -350,7 +349,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
if hasattr(acc, "_configtracker"):
|
||||
acc._configtracker.wait_finish()
|
||||
del acc._configtracker
|
||||
acc.set_config("bcc_self", "0")
|
||||
if acc.is_configured() and not acc.is_started():
|
||||
acc.start_io()
|
||||
print("{}: {} account was successfully setup".format(
|
||||
|
||||
@@ -11,21 +11,8 @@ from datetime import datetime, timedelta
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msgtext,res", [
|
||||
("Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by me",
|
||||
("removed", "tmp1@x.org", "me")),
|
||||
("Group left by some one (tmp1@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Group left by tmp1@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member nothing bla bla", None),
|
||||
("Another unknown system message", None),
|
||||
("Member Me (tmp1@x.org) removed by tmp2@x.org.", ("removed", "tmp1@x.org")),
|
||||
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org")),
|
||||
])
|
||||
def test_parse_system_add_remove(msgtext, res):
|
||||
from deltachat.message import parse_system_add_remove
|
||||
@@ -129,11 +116,6 @@ class TestOfflineContact:
|
||||
assert not contact1.is_blocked()
|
||||
assert not contact1.is_verified()
|
||||
|
||||
def test_create_self_contact(self, acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
contact1 = ac1.create_contact(ac1.get_config("addr"))
|
||||
assert contact1.id == 1
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
@@ -470,12 +452,12 @@ class TestOfflineChat:
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor):
|
||||
in_list.append(("added", chat, contact, actor))
|
||||
def ac_member_added(self, chat, contact):
|
||||
in_list.append(("added", chat, contact))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor):
|
||||
in_list.append(("removed", chat, contact, actor))
|
||||
def ac_member_removed(self, chat, contact):
|
||||
in_list.append(("removed", chat, contact))
|
||||
|
||||
ac1.add_account_plugin(InPlugin())
|
||||
|
||||
@@ -504,11 +486,10 @@ class TestOfflineChat:
|
||||
|
||||
assert len(in_list) == 10
|
||||
chat_contacts = chat.get_contacts()
|
||||
for in_cmd, in_chat, in_contact, in_actor in in_list:
|
||||
for in_cmd, in_chat, in_contact in in_list:
|
||||
assert in_cmd == "added"
|
||||
assert in_chat == chat
|
||||
assert in_contact in chat_contacts
|
||||
assert in_actor is None
|
||||
chat_contacts.remove(in_contact)
|
||||
|
||||
assert chat_contacts[0].id == 1 # self contact
|
||||
@@ -536,7 +517,7 @@ def test_basic_imap_api(acfactory, tmpdir):
|
||||
|
||||
imap2 = ac2.direct_imap
|
||||
|
||||
imap2.idle_start()
|
||||
ac2.direct_imap.idle_start()
|
||||
chat12.send_text("hello")
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
@@ -742,9 +723,9 @@ class TestOnlineAccount:
|
||||
|
||||
def test_move_works_on_self_sent(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True)
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure_and_start_io()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message1")
|
||||
@@ -1561,97 +1542,6 @@ class TestOnlineAccount:
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac2.create_chat(ac4)
|
||||
|
||||
def test_immediate_autodelete(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=False, move=False, sentbox=False)
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
imap2 = ac2.direct_imap
|
||||
imap2.idle_start()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
sent_msg = chat1.send_text("hello")
|
||||
imap2.idle_check(terminate=False)
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
imap2.idle_check(terminate=True)
|
||||
ac2._evtracker.get_info_contains("close/expunge succeeded")
|
||||
|
||||
assert len(imap2.get_all_messages()) == 0
|
||||
|
||||
# Mark deleted message as seen and check that read receipt arrives
|
||||
msg.mark_seen()
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 == chat1.id
|
||||
assert ev.data2 == sent_msg.id
|
||||
|
||||
def test_ephemeral_timer(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
chat2 = ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("ac1: set ephemeral timer to 60")
|
||||
chat1.set_ephemeral_timer(60)
|
||||
|
||||
lp.sec("ac1: check that ephemeral timer is set for chat")
|
||||
assert chat1.get_ephemeral_timer() == 60
|
||||
chat1_summary = chat1.get_summary()
|
||||
assert chat1_summary["ephemeral_timer"] == {'Enabled': {'duration': 60}}
|
||||
|
||||
lp.sec("ac2: receive system message about ephemeral timer modification")
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
system_message1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert chat2.get_ephemeral_timer() == 60
|
||||
assert system_message1.is_system_message()
|
||||
|
||||
# Disabled until markers are implemented
|
||||
# assert "Ephemeral timer: 60\n" in system_message1.get_message_info()
|
||||
|
||||
lp.sec("ac2: send message to ac1")
|
||||
sent_message = chat2.send_text("message")
|
||||
assert sent_message.ephemeral_timer == 60
|
||||
assert "Ephemeral timer: 60\n" in sent_message.get_message_info()
|
||||
|
||||
# Timer is started immediately for sent messages
|
||||
assert sent_message.ephemeral_timestamp is not None
|
||||
assert "Expires: " in sent_message.get_message_info()
|
||||
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
text_message = ac1._evtracker.wait_next_incoming_message()
|
||||
assert text_message.text == "message"
|
||||
assert text_message.ephemeral_timer == 60
|
||||
assert "Ephemeral timer: 60\n" in text_message.get_message_info()
|
||||
|
||||
# Timer should not start until message is displayed
|
||||
assert text_message.ephemeral_timestamp is None
|
||||
assert "Expires: " not in text_message.get_message_info()
|
||||
text_message.mark_seen()
|
||||
text_message = ac1.get_message_by_id(text_message.id)
|
||||
assert text_message.ephemeral_timestamp is not None
|
||||
assert "Expires: " in text_message.get_message_info()
|
||||
|
||||
lp.sec("ac2: set ephemeral timer to 0")
|
||||
chat2.set_ephemeral_timer(0)
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
|
||||
lp.sec("ac1: receive system message about ephemeral timer modification")
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
system_message2 = ac1._evtracker.wait_next_incoming_message()
|
||||
assert system_message2.ephemeral_timer is None
|
||||
assert "Ephemeral timer: " not in system_message2.get_message_info()
|
||||
assert chat1.get_ephemeral_timer() == 0
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
def test_group_many_members_add_leave_remove(self, acfactory, lp):
|
||||
|
||||
@@ -7,7 +7,7 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
|
||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
passenv =
|
||||
TRAVIS
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.45.0
|
||||
1.43.1
|
||||
|
||||
@@ -5,29 +5,19 @@ import sys
|
||||
import re
|
||||
import pathlib
|
||||
import subprocess
|
||||
from argparse import ArgumentParser
|
||||
|
||||
rex = re.compile(r'version = "(\S+)"')
|
||||
|
||||
|
||||
def regex_matches(relpath, regex=rex):
|
||||
def read_toml_version(relpath):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
for line in open(str(p)):
|
||||
m = regex.match(line)
|
||||
m = rex.match(line)
|
||||
if m is not None:
|
||||
return m
|
||||
|
||||
|
||||
def read_toml_version(relpath):
|
||||
res = regex_matches(relpath, rex)
|
||||
if res is not None:
|
||||
return res.group(1)
|
||||
return m.group(1)
|
||||
raise ValueError("no version found in {}".format(relpath))
|
||||
|
||||
|
||||
def replace_toml_version_and_lto(relpath, newversion):
|
||||
lto_rex = re.compile(r'#?\s*lto =.*')
|
||||
def replace_toml_version(relpath, newversion):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
tmp_path = str(p) + "_tmp"
|
||||
@@ -35,32 +25,18 @@ def replace_toml_version_and_lto(relpath, newversion):
|
||||
for line in open(str(p)):
|
||||
m = rex.match(line)
|
||||
if m is not None:
|
||||
print("{}: set version={}".format(relpath, newversion))
|
||||
f.write('version = "{}"\n'.format(newversion))
|
||||
else:
|
||||
m = lto_rex.match(line)
|
||||
if m:
|
||||
print("{}: setting lto = true".format(relpath))
|
||||
line = "lto = true\n"
|
||||
f.write(line)
|
||||
os.rename(tmp_path, str(p))
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(prog="set_core_version")
|
||||
parser.add_argument("newversion")
|
||||
|
||||
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml"]
|
||||
try:
|
||||
opts = parser.parse_args()
|
||||
except SystemExit:
|
||||
print()
|
||||
for x in toml_list:
|
||||
if len(sys.argv) < 2:
|
||||
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
|
||||
print("{}: {}".format(x, read_toml_version(x)))
|
||||
print()
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
|
||||
newversion = opts.newversion
|
||||
newversion = sys.argv[1]
|
||||
if newversion.count(".") < 2:
|
||||
raise SystemExit("need at least two dots in version")
|
||||
|
||||
@@ -76,13 +52,10 @@ def main():
|
||||
else:
|
||||
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
|
||||
|
||||
replace_toml_version_and_lto("Cargo.toml", newversion)
|
||||
replace_toml_version_and_lto("deltachat-ffi/Cargo.toml", newversion)
|
||||
replace_toml_version("Cargo.toml", newversion)
|
||||
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
||||
|
||||
print("running cargo check")
|
||||
subprocess.call(["cargo", "check"])
|
||||
|
||||
print("adding changes to git index")
|
||||
subprocess.call(["git", "add", "-u"])
|
||||
# subprocess.call(["cargo", "update", "-p", "deltachat"])
|
||||
|
||||
@@ -90,8 +63,3 @@ def main():
|
||||
print("")
|
||||
print(" git tag {}".format(newversion))
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
64
src/blob.rs
64
src/blob.rs
@@ -168,9 +168,6 @@ impl<'a> BlobObject<'a> {
|
||||
/// subdirectory is used and [BlobObject::sanitise_name] does not
|
||||
/// modify the filename.
|
||||
///
|
||||
/// Paths into the blob directory may be either defined by an absolute path
|
||||
/// or by the relative prefix `$BLOBDIR`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This merely delegates to the [BlobObject::create_and_copy] and
|
||||
@@ -182,11 +179,6 @@ impl<'a> BlobObject<'a> {
|
||||
) -> std::result::Result<BlobObject<'_>, BlobError> {
|
||||
if src.as_ref().starts_with(context.get_blobdir()) {
|
||||
BlobObject::from_path(context, src)
|
||||
} else if src.as_ref().starts_with("$BLOBDIR/") {
|
||||
BlobObject::from_name(
|
||||
context,
|
||||
src.as_ref().to_str().unwrap_or_default().to_string(),
|
||||
)
|
||||
} else {
|
||||
BlobObject::create_and_copy(context, src).await
|
||||
}
|
||||
@@ -505,7 +497,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap();
|
||||
let fname = t.ctx.get_blobdir().join("foo");
|
||||
let data = fs::read(fname).await.unwrap();
|
||||
@@ -516,7 +508,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_lowercase_ext() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -525,7 +517,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_as_file_name() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -534,7 +526,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_as_rel_path() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -543,7 +535,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_suffix() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -554,7 +546,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_dup() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
BlobObject::create(&t.ctx, "foo.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -578,7 +570,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_double_ext_preserved() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -603,7 +595,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_long_names() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let s = "1".repeat(150);
|
||||
let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap();
|
||||
let blobname = blob.as_name().split('/').last().unwrap();
|
||||
@@ -612,7 +604,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_and_copy() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let src = t.dir.path().join("src");
|
||||
fs::write(&src, b"boo").await.unwrap();
|
||||
let blob = BlobObject::create_and_copy(&t.ctx, &src).await.unwrap();
|
||||
@@ -628,7 +620,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_from_path() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
|
||||
let src_ext = t.dir.path().join("external");
|
||||
fs::write(&src_ext, b"boo").await.unwrap();
|
||||
@@ -646,7 +638,7 @@ mod tests {
|
||||
}
|
||||
#[async_std::test]
|
||||
async fn test_create_from_name_long() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
||||
fs::write(&src_ext, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
|
||||
@@ -668,40 +660,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_sanitise_name() {
|
||||
let (stem, ext) =
|
||||
let (_, ext) =
|
||||
BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
|
||||
assert_eq!(ext, ".txt");
|
||||
assert!(!stem.is_empty());
|
||||
|
||||
// the extensions are kept together as between stem and extension a number may be added -
|
||||
// and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz`
|
||||
let (stem, ext) = BlobObject::sanitise_name("wot.tar.gz");
|
||||
assert_eq!(stem, "wot");
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name(".foo.bar");
|
||||
assert_eq!(stem, "");
|
||||
assert_eq!(ext, ".foo.bar");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
|
||||
assert!(stem.contains("foo"));
|
||||
assert!(!stem.contains("?"));
|
||||
assert_eq!(ext, ".bar");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("no-extension");
|
||||
assert_eq!(stem, "no-extension");
|
||||
assert_eq!(ext, "");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("path/ignored\\this: is* forbidden?.c");
|
||||
assert_eq!(ext, ".c");
|
||||
assert!(!stem.contains("path"));
|
||||
assert!(!stem.contains("ignored"));
|
||||
assert!(stem.contains("this"));
|
||||
assert!(stem.contains("forbidden"));
|
||||
assert!(!stem.contains("/"));
|
||||
assert!(!stem.contains("\\"));
|
||||
assert!(!stem.contains(":"));
|
||||
assert!(!stem.contains("*"));
|
||||
assert!(!stem.contains("?"));
|
||||
}
|
||||
}
|
||||
|
||||
237
src/chat.rs
237
src/chat.rs
@@ -15,7 +15,6 @@ use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::{delete_expired_messages, schedule_ephemeral_task, Timer as EphemeralTimer};
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::events::Event;
|
||||
use crate::job::{self, Action};
|
||||
@@ -25,25 +24,6 @@ use crate::param::*;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
/// An chat item, such as a message or a marker.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ChatItem {
|
||||
Message {
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// A marker without inherent meaning. It is inserted before user
|
||||
/// supplied MsgId.
|
||||
Marker1,
|
||||
|
||||
/// Day marker, separating messages that correspond to different
|
||||
/// days according to local time.
|
||||
DayMarker {
|
||||
/// Marker timestamp, for day markers
|
||||
timestamp: i64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Chat ID, including reserved IDs.
|
||||
///
|
||||
/// Some chat IDs are reserved to identify special chat types. This
|
||||
@@ -730,7 +710,6 @@ impl Chat {
|
||||
.unwrap_or_else(std::path::PathBuf::new),
|
||||
draft,
|
||||
is_muted: self.is_muted(),
|
||||
ephemeral_timer: self.id.get_ephemeral_timer(context).await?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -907,10 +886,11 @@ impl Chat {
|
||||
|
||||
// the whole list of messages referenced may be huge;
|
||||
// only use the oldest and and the parent message
|
||||
let parent_references = parent_references
|
||||
.find(' ')
|
||||
.and_then(|n| parent_references.get(..n))
|
||||
.unwrap_or(&parent_references);
|
||||
let parent_references = if let Some(n) = parent_references.find(' ') {
|
||||
&parent_references[0..n]
|
||||
} else {
|
||||
&parent_references
|
||||
};
|
||||
|
||||
if !parent_references.is_empty() && !parent_rfc724_mid.is_empty() {
|
||||
// angle brackets are added by the mimefactory later
|
||||
@@ -958,20 +938,10 @@ impl Chat {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let ephemeral_timer = if msg.param.get_cmd() == SystemMessage::EphemeralTimerChanged {
|
||||
EphemeralTimer::Disabled
|
||||
} else {
|
||||
self.id.get_ephemeral_timer(context).await?
|
||||
};
|
||||
let ephemeral_timestamp = match ephemeral_timer {
|
||||
EphemeralTimer::Disabled => 0,
|
||||
EphemeralTimer::Enabled { duration } => timestamp + i64::from(duration),
|
||||
};
|
||||
|
||||
// add message to the database
|
||||
|
||||
if context.sql.execute(
|
||||
"INSERT INTO msgs (rfc724_mid, chat_id, from_id, to_id, timestamp, type, state, txt, param, hidden, mime_in_reply_to, mime_references, location_id, ephemeral_timer, ephemeral_timestamp) VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?);",
|
||||
"INSERT INTO msgs (rfc724_mid, chat_id, from_id, to_id, timestamp, type, state, txt, param, hidden, mime_in_reply_to, mime_references, location_id) VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?);",
|
||||
paramsv![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
@@ -986,8 +956,6 @@ impl Chat {
|
||||
new_in_reply_to,
|
||||
new_references,
|
||||
location_id as i32,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp
|
||||
]
|
||||
).await.is_ok() {
|
||||
msg_id = context.sql.get_rowid(
|
||||
@@ -1006,7 +974,6 @@ impl Chat {
|
||||
} else {
|
||||
error!(context, "Cannot send message, not configured.",);
|
||||
}
|
||||
schedule_ephemeral_task(context).await;
|
||||
|
||||
Ok(MsgId::new(msg_id))
|
||||
}
|
||||
@@ -1034,13 +1001,13 @@ impl rusqlite::types::ToSql for ChatVisibility {
|
||||
|
||||
impl rusqlite::types::FromSql for ChatVisibility {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).map(|val| {
|
||||
i64::column_result(value).and_then(|val| {
|
||||
match val {
|
||||
2 => ChatVisibility::Pinned,
|
||||
1 => ChatVisibility::Archived,
|
||||
0 => ChatVisibility::Normal,
|
||||
2 => Ok(ChatVisibility::Pinned),
|
||||
1 => Ok(ChatVisibility::Archived),
|
||||
0 => Ok(ChatVisibility::Normal),
|
||||
// fallback to to Normal for unknown values, may happen eg. on imports created by a newer version.
|
||||
_ => ChatVisibility::Normal,
|
||||
_ => Ok(ChatVisibility::Normal),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1103,9 +1070,6 @@ pub struct ChatInfo {
|
||||
///
|
||||
/// The exact time its muted can be found out via the `chat.mute_duration` property
|
||||
pub is_muted: bool,
|
||||
|
||||
/// Ephemeral message timer.
|
||||
pub ephemeral_timer: EphemeralTimer,
|
||||
// ToDo:
|
||||
// - [ ] deaddrop,
|
||||
// - [ ] summary,
|
||||
@@ -1374,12 +1338,11 @@ pub(crate) fn msgtype_has_file(msgtype: Viewtype) -> bool {
|
||||
Viewtype::Voice => true,
|
||||
Viewtype::Video => true,
|
||||
Viewtype::File => true,
|
||||
Viewtype::VideochatInvitation => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> {
|
||||
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
|
||||
if msg.viewtype == Viewtype::Text {
|
||||
// the caller should check if the message text is empty
|
||||
} else if msgtype_has_file(msg.viewtype) {
|
||||
let blob = msg
|
||||
@@ -1612,59 +1575,16 @@ pub async fn send_text_msg(
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Result<MsgId, Error> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
"video chat invitation cannot be sent to special chat: {}",
|
||||
chat_id
|
||||
);
|
||||
|
||||
let instance = if let Some(instance) = context.get_config(Config::WebrtcInstance).await {
|
||||
if !instance.is_empty() {
|
||||
instance
|
||||
} else {
|
||||
bail!("webrtc_instance is empty");
|
||||
}
|
||||
} else {
|
||||
bail!("webrtc_instance not set");
|
||||
};
|
||||
|
||||
let room = dc_create_id();
|
||||
|
||||
let instance = if instance.contains("$ROOM") {
|
||||
instance.replace("$ROOM", &room)
|
||||
} else {
|
||||
format!("{}{}", instance, room)
|
||||
};
|
||||
|
||||
let mut msg = Message::new(Viewtype::VideochatInvitation);
|
||||
msg.param.set(Param::WebrtcRoom, &instance);
|
||||
msg.text = Some(
|
||||
context
|
||||
.stock_string_repl_str(
|
||||
StockMessage::VideochatInviteMsgBody,
|
||||
Message::parse_webrtc_instance(&instance).1,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
pub async fn get_chat_msgs(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
flags: u32,
|
||||
marker1before: Option<MsgId>,
|
||||
) -> Vec<ChatItem> {
|
||||
match delete_expired_messages(context).await {
|
||||
) -> Vec<MsgId> {
|
||||
match delete_device_expired_messages(context).await {
|
||||
Err(err) => warn!(context, "Failed to delete expired messages: {}", err),
|
||||
Ok(messages_deleted) => {
|
||||
if messages_deleted {
|
||||
// Trigger reload of chatlist.
|
||||
//
|
||||
// On desktop chatlist is always shown on the side,
|
||||
// and it is important to update the last message shown
|
||||
// there.
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
@@ -1683,20 +1603,18 @@ pub async fn get_chat_msgs(
|
||||
let (curr_id, ts) = row?;
|
||||
if let Some(marker_id) = marker1before {
|
||||
if curr_id == marker_id {
|
||||
ret.push(ChatItem::Marker1);
|
||||
ret.push(MsgId::new(DC_MSG_ID_MARKER1));
|
||||
}
|
||||
}
|
||||
if (flags & DC_GCM_ADDDAYMARKER) != 0 {
|
||||
let curr_local_timestamp = ts + cnv_to_local;
|
||||
let curr_day = curr_local_timestamp / 86400;
|
||||
if curr_day != last_day {
|
||||
ret.push(ChatItem::DayMarker {
|
||||
timestamp: curr_day,
|
||||
});
|
||||
ret.push(MsgId::new(DC_MSG_ID_DAYMARKER));
|
||||
last_day = curr_day;
|
||||
}
|
||||
}
|
||||
ret.push(ChatItem::Message { msg_id: curr_id });
|
||||
ret.push(curr_id);
|
||||
}
|
||||
Ok(ret)
|
||||
};
|
||||
@@ -1828,6 +1746,52 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes messages which are expired according to "delete_device_after" setting.
|
||||
///
|
||||
/// Returns true if any message is deleted, so event can be emitted. If nothing
|
||||
/// has been deleted, returns false.
|
||||
pub async fn delete_device_expired_messages(context: &Context) -> Result<bool, Error> {
|
||||
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
|
||||
let threshold_timestamp = time() - delete_device_after;
|
||||
|
||||
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
|
||||
// Delete expired messages
|
||||
//
|
||||
// Only update the rows that have to be updated, to avoid emitting
|
||||
// unnecessary "chat modified" events.
|
||||
let rows_modified = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE timestamp < ? \
|
||||
AND chat_id > ? \
|
||||
AND chat_id != ? \
|
||||
AND chat_id != ?",
|
||||
paramsv![
|
||||
DC_CHAT_ID_TRASH,
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(rows_modified > 0)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_chat_media(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
@@ -2708,12 +2672,10 @@ pub(crate) async fn get_chat_id_by_grpid(
|
||||
/// Adds a message to device chat.
|
||||
///
|
||||
/// Optional `label` can be provided to ensure that message is added only once.
|
||||
/// If `important` is true, a notification will be sent.
|
||||
pub async fn add_device_msg_with_importance(
|
||||
pub async fn add_device_msg(
|
||||
context: &Context,
|
||||
label: Option<&str>,
|
||||
msg: Option<&mut Message>,
|
||||
important: bool,
|
||||
) -> Result<MsgId, Error> {
|
||||
ensure!(
|
||||
label.is_some() || msg.is_some(),
|
||||
@@ -2773,24 +2735,12 @@ pub async fn add_device_msg_with_importance(
|
||||
}
|
||||
|
||||
if !msg_id.is_unset() {
|
||||
if important {
|
||||
context.emit_event(Event::IncomingMsg { chat_id, msg_id });
|
||||
} else {
|
||||
context.emit_event(Event::MsgsChanged { chat_id, msg_id });
|
||||
}
|
||||
context.emit_event(Event::IncomingMsg { chat_id, msg_id });
|
||||
}
|
||||
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
pub async fn add_device_msg(
|
||||
context: &Context,
|
||||
label: Option<&str>,
|
||||
msg: Option<&mut Message>,
|
||||
) -> Result<MsgId, Error> {
|
||||
add_device_msg_with_importance(context, label, msg, false).await
|
||||
}
|
||||
|
||||
pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result<bool, Error> {
|
||||
ensure!(!label.is_empty(), "empty label");
|
||||
if let Ok(()) = context
|
||||
@@ -2833,16 +2783,9 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
/// For example, it can be a message showing that a member was added to a group.
|
||||
pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
|
||||
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
|
||||
let ephemeral_timer = match chat_id.get_ephemeral_timer(context).await {
|
||||
Err(e) => {
|
||||
warn!(context, "Could not get timer for info msg: {}", e);
|
||||
return;
|
||||
}
|
||||
Ok(ephemeral_timer) => ephemeral_timer,
|
||||
};
|
||||
|
||||
if let Err(e) = context.sql.execute(
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid,ephemeral_timer) VALUES (?,?,?, ?,?,?, ?,?,?);",
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid) VALUES (?,?,?, ?,?,?, ?,?);",
|
||||
paramsv![
|
||||
chat_id,
|
||||
DC_CONTACT_ID_INFO,
|
||||
@@ -2852,7 +2795,6 @@ pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl
|
||||
MessageState::InNoticed,
|
||||
text.as_ref().to_string(),
|
||||
rfc724_mid,
|
||||
ephemeral_timer
|
||||
]
|
||||
).await {
|
||||
warn!(context, "Could not add info msg: {}", e);
|
||||
@@ -2879,7 +2821,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_chat_info() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let bob = Contact::create(&t.ctx, "bob", "bob@example.com")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2902,8 +2844,7 @@ mod tests {
|
||||
"color": 15895624,
|
||||
"profile_image": "",
|
||||
"draft": "",
|
||||
"is_muted": false,
|
||||
"ephemeral_timer": "Disabled"
|
||||
"is_muted": false
|
||||
}
|
||||
"#;
|
||||
|
||||
@@ -2914,7 +2855,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_draft_no_draft() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2924,7 +2865,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_draft_special_chat_id() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let draft = ChatId::new(DC_CHAT_ID_LAST_SPECIAL)
|
||||
.get_draft(&t.ctx)
|
||||
.await
|
||||
@@ -2936,14 +2877,14 @@ mod tests {
|
||||
async fn test_get_draft_no_chat() {
|
||||
// This is a weird case, maybe this should be an error but we
|
||||
// do not get this info from the database currently.
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let draft = ChatId::new(42).get_draft(&t.ctx).await.unwrap();
|
||||
assert!(draft.is_none());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_draft() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2959,7 +2900,7 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_add_contact_to_chat_ex_add_self() {
|
||||
// Adding self to a contact should succeed, even though it's pointless.
|
||||
let t = TestContext::new().await;
|
||||
let t = test_context().await;
|
||||
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2971,7 +2912,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_self_talk() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2992,7 +2933,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_deaddrop_chat() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let chat = Chat::load_from_db(&t.ctx, ChatId::new(DC_CHAT_ID_DEADDROP))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3007,7 +2948,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_add_device_msg_unlabelled() {
|
||||
let t = TestContext::new().await;
|
||||
let t = test_context().await;
|
||||
|
||||
// add two device-messages
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
@@ -3042,7 +2983,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_add_device_msg_labelled() {
|
||||
let t = TestContext::new().await;
|
||||
let t = test_context().await;
|
||||
|
||||
// add two device-messages with the same label (second attempt is not added)
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
@@ -3096,7 +3037,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_add_device_msg_label_only() {
|
||||
let t = TestContext::new().await;
|
||||
let t = test_context().await;
|
||||
let res = add_device_msg(&t.ctx, Some(""), None).await;
|
||||
assert!(res.is_err());
|
||||
let res = add_device_msg(&t.ctx, Some("some-label"), None).await;
|
||||
@@ -3116,7 +3057,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_was_device_msg_ever_added() {
|
||||
let t = TestContext::new().await;
|
||||
let t = test_context().await;
|
||||
add_device_msg(&t.ctx, Some("some-label"), None).await.ok();
|
||||
assert!(was_device_msg_ever_added(&t.ctx, "some-label")
|
||||
.await
|
||||
@@ -3140,7 +3081,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_delete_device_chat() {
|
||||
let t = TestContext::new().await;
|
||||
let t = test_context().await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("message text".to_string());
|
||||
@@ -3160,7 +3101,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_device_chat_cannot_sent() {
|
||||
let t = TestContext::new().await;
|
||||
let t = test_context().await;
|
||||
t.ctx.update_device_chats().await.unwrap();
|
||||
let (device_chat_id, _) =
|
||||
create_or_lookup_by_contact_id(&t.ctx, DC_CONTACT_ID_DEVICE, Blocked::Not)
|
||||
@@ -3180,7 +3121,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_delete_and_reset_all_device_msgs() {
|
||||
let t = TestContext::new().await;
|
||||
let t = test_context().await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("message text".to_string());
|
||||
let msg_id1 = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg))
|
||||
@@ -3217,7 +3158,7 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_archive() {
|
||||
// create two chats
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("foo".to_string());
|
||||
let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).await.unwrap();
|
||||
@@ -3331,7 +3272,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_pinned() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
|
||||
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
@@ -3390,7 +3331,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_set_chat_name() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3414,7 +3355,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_same_chat_twice() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3433,7 +3374,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_shall_attach_selfavatar() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3457,7 +3398,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_set_mute_duration() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3525,7 +3466,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parent_is_encrypted() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::chat::*;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::*;
|
||||
use crate::ephemeral::delete_expired_messages;
|
||||
use crate::error::{bail, ensure, Result};
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
@@ -77,7 +76,7 @@ impl Chatlist {
|
||||
/// chats
|
||||
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
/// and hides the device-chat,
|
||||
/// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
||||
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
|
||||
@@ -100,7 +99,7 @@ impl Chatlist {
|
||||
|
||||
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
|
||||
// messages get deleted to avoid reloading the same chatlist.
|
||||
if let Err(err) = delete_expired_messages(context).await {
|
||||
if let Err(err) = delete_device_expired_messages(context).await {
|
||||
warn!(context, "Failed to hide expired messages: {}", err);
|
||||
}
|
||||
|
||||
@@ -148,12 +147,11 @@ impl Chatlist {
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
AND m.timestamp=(
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
@@ -175,12 +173,11 @@ impl Chatlist {
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
AND m.timestamp=(
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
AND (hidden=0 OR state=?))
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.archived=1
|
||||
@@ -209,12 +206,11 @@ impl Chatlist {
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
AND m.timestamp=(
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked=0
|
||||
AND c.name LIKE ?3
|
||||
@@ -240,12 +236,11 @@ impl Chatlist {
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
AND m.timestamp=(
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked=0
|
||||
AND NOT c.archived=?3
|
||||
@@ -429,7 +424,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_try_load() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -477,7 +472,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_sort_self_talk_up_on_forward() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
t.ctx.update_device_chats().await.unwrap();
|
||||
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||
.await
|
||||
@@ -502,7 +497,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_search_special_chat_names() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
t.ctx.update_device_chats().await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
|
||||
@@ -535,7 +530,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_summary_unwrap() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::dc_tools::*;
|
||||
use crate::events::Event;
|
||||
use crate::message::MsgId;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::{scheduler::InterruptInfo, stock::StockMessage};
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
@@ -117,21 +117,9 @@ pub enum Config {
|
||||
|
||||
#[strum(serialize = "sys.config_keys")]
|
||||
SysConfigKeys,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
/// Whether we send a warning if the password is wrong (set to false when we send a warning
|
||||
/// because we do not want to send a second warning)
|
||||
NotifyAboutWrongPw,
|
||||
|
||||
/// address to webrtc instance to use for videochats
|
||||
WebrtcInstance,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub async fn config_exists(&self, key: Config) -> bool {
|
||||
self.sql.get_raw_config(self, 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) -> Option<String> {
|
||||
let value = match key {
|
||||
@@ -213,6 +201,22 @@ impl Context {
|
||||
None => self.sql.set_raw_config(self, key, None).await,
|
||||
}
|
||||
}
|
||||
Config::InboxWatch => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
self.interrupt_inbox(InterruptInfo::new(false, None)).await;
|
||||
ret
|
||||
}
|
||||
Config::SentboxWatch => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
self.interrupt_sentbox(InterruptInfo::new(false, None))
|
||||
.await;
|
||||
ret
|
||||
}
|
||||
Config::MvboxWatch => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
self.interrupt_mvbox(InterruptInfo::new(false, None)).await;
|
||||
ret
|
||||
}
|
||||
Config::Selfstatus => {
|
||||
let def = self.stock_str(StockMessage::StatusLine).await;
|
||||
let val = if value.is_none() || value.unwrap() == def {
|
||||
@@ -282,7 +286,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_outside_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let avatar_src = t.dir.path().join("avatar.jpg");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
File::create(&avatar_src)
|
||||
@@ -311,7 +315,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_in_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let avatar_src = t.ctx.get_blobdir().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
File::create(&avatar_src)
|
||||
@@ -337,7 +341,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_selfavatar_copy_without_recode() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||
File::create(&avatar_src)
|
||||
@@ -361,7 +365,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_media_quality_config_option() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
|
||||
assert_eq!(media_quality, 0);
|
||||
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||
|
||||
@@ -68,21 +68,16 @@ impl Context {
|
||||
async fn inner_configure(&self) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let was_configured_before = self.is_configured().await;
|
||||
let mut param = LoginParam::from_database(self, "").await;
|
||||
let success = configure(self, &mut param).await;
|
||||
self.set_config(Config::NotifyAboutWrongPw, None).await?;
|
||||
|
||||
if let Some(provider) = provider::get_provider_info(¶m.addr) {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
if !self.config_exists(def.key).await {
|
||||
if !was_configured_before {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
info!(self, "apply config_defaults {}={}", def.key, def.value);
|
||||
self.set_config(def.key, Some(def.value)).await?;
|
||||
} else {
|
||||
info!(
|
||||
self,
|
||||
"skip already set config_defaults {}={}", def.key, def.value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,12 +96,11 @@ impl Context {
|
||||
|
||||
match success {
|
||||
Ok(_) => {
|
||||
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
progress!(self, 1000);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
error!(self, "Configure Failed: {}", err);
|
||||
progress!(self, 0);
|
||||
Err(err)
|
||||
}
|
||||
@@ -459,10 +453,7 @@ async fn try_imap_connections(
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
bail!("autoconfig did not succeed");
|
||||
return Ok(()); // we directly return here if it was autoconfig or the connection succeeded
|
||||
}
|
||||
|
||||
progress!(context, 670);
|
||||
@@ -496,7 +487,7 @@ async fn try_imap_connection(
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
bail!("autoconfig did not succeed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
progress!(context, 650 + variation * 30);
|
||||
@@ -556,7 +547,7 @@ async fn try_smtp_connections(
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
bail!("No SMTP connection");
|
||||
return Ok(());
|
||||
}
|
||||
progress!(context, 850);
|
||||
|
||||
@@ -630,7 +621,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_no_panic_on_bad_credentials() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::Addr, Some("probably@unexistant.addr"))
|
||||
.await
|
||||
@@ -644,7 +635,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_offline_autoconfig() {
|
||||
let context = TestContext::new().await.ctx;
|
||||
let context = dummy_context().await.ctx;
|
||||
|
||||
let mut params = LoginParam::new();
|
||||
params.addr = "someone123@example.org".to_string();
|
||||
|
||||
@@ -84,19 +84,6 @@ impl Default for KeyGenType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(i8)]
|
||||
pub enum VideochatType {
|
||||
Unknown = 0,
|
||||
BasicWebrtc = 1,
|
||||
}
|
||||
|
||||
impl Default for VideochatType {
|
||||
fn default() -> Self {
|
||||
VideochatType::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
|
||||
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
|
||||
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
|
||||
@@ -309,9 +296,6 @@ pub enum Viewtype {
|
||||
/// The file is set via dc_msg_set_file()
|
||||
/// and retrieved via dc_msg_get_file().
|
||||
File = 60,
|
||||
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation = 70,
|
||||
}
|
||||
|
||||
impl Default for Viewtype {
|
||||
@@ -339,6 +323,54 @@ const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
|
||||
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
|
||||
const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
|
||||
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
|
||||
const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
|
||||
|
||||
// TODO: Strings need some doumentation about used placeholders.
|
||||
// These constants are used to set stock translation strings
|
||||
|
||||
const DC_STR_NOMESSAGES: usize = 1;
|
||||
const DC_STR_SELF: usize = 2;
|
||||
const DC_STR_DRAFT: usize = 3;
|
||||
const DC_STR_VOICEMESSAGE: usize = 7;
|
||||
const DC_STR_DEADDROP: usize = 8;
|
||||
const DC_STR_IMAGE: usize = 9;
|
||||
const DC_STR_VIDEO: usize = 10;
|
||||
const DC_STR_AUDIO: usize = 11;
|
||||
const DC_STR_FILE: usize = 12;
|
||||
const DC_STR_STATUSLINE: usize = 13;
|
||||
const DC_STR_NEWGROUPDRAFT: usize = 14;
|
||||
const DC_STR_MSGGRPNAME: usize = 15;
|
||||
const DC_STR_MSGGRPIMGCHANGED: usize = 16;
|
||||
const DC_STR_MSGADDMEMBER: usize = 17;
|
||||
const DC_STR_MSGDELMEMBER: usize = 18;
|
||||
const DC_STR_MSGGROUPLEFT: usize = 19;
|
||||
const DC_STR_GIF: usize = 23;
|
||||
const DC_STR_ENCRYPTEDMSG: usize = 24;
|
||||
const DC_STR_E2E_AVAILABLE: usize = 25;
|
||||
const DC_STR_ENCR_TRANSP: usize = 27;
|
||||
const DC_STR_ENCR_NONE: usize = 28;
|
||||
const DC_STR_CANTDECRYPT_MSG_BODY: usize = 29;
|
||||
const DC_STR_FINGERPRINTS: usize = 30;
|
||||
const DC_STR_READRCPT: usize = 31;
|
||||
const DC_STR_READRCPT_MAILBODY: usize = 32;
|
||||
const DC_STR_MSGGRPIMGDELETED: usize = 33;
|
||||
const DC_STR_E2E_PREFERRED: usize = 34;
|
||||
const DC_STR_CONTACT_VERIFIED: usize = 35;
|
||||
const DC_STR_CONTACT_NOT_VERIFIED: usize = 36;
|
||||
const DC_STR_CONTACT_SETUP_CHANGED: usize = 37;
|
||||
const DC_STR_ARCHIVEDCHATS: usize = 40;
|
||||
const DC_STR_STARREDMSGS: usize = 41;
|
||||
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
|
||||
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
|
||||
const DC_STR_CANNOT_LOGIN: usize = 60;
|
||||
const DC_STR_SERVER_RESPONSE: usize = 61;
|
||||
const DC_STR_MSGACTIONBYUSER: usize = 62;
|
||||
const DC_STR_MSGACTIONBYME: usize = 63;
|
||||
const DC_STR_MSGLOCATIONENABLED: usize = 64;
|
||||
const DC_STR_MSGLOCATIONDISABLED: usize = 65;
|
||||
const DC_STR_LOCATION: usize = 66;
|
||||
const DC_STR_STICKER: usize = 67;
|
||||
const DC_STR_COUNT: usize = 67;
|
||||
|
||||
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
|
||||
|
||||
|
||||
100
src/contact.rs
100
src/contact.rs
@@ -1029,10 +1029,10 @@ pub fn addr_normalize(addr: &str) -> &str {
|
||||
let norm = addr.trim();
|
||||
|
||||
if norm.starts_with("mailto:") {
|
||||
norm.get(7..).unwrap_or(norm)
|
||||
} else {
|
||||
norm
|
||||
return &norm[7..];
|
||||
}
|
||||
|
||||
norm
|
||||
}
|
||||
|
||||
fn sanitize_name_and_addr(name: impl AsRef<str>, addr: impl AsRef<str>) -> (String, String) {
|
||||
@@ -1042,15 +1042,11 @@ fn sanitize_name_and_addr(name: impl AsRef<str>, addr: impl AsRef<str>) -> (Stri
|
||||
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.as_ref().is_empty() {
|
||||
captures
|
||||
.get(1)
|
||||
.map_or("".to_string(), |m| normalize_name(m.as_str()))
|
||||
normalize_name(&captures[1])
|
||||
} else {
|
||||
name.as_ref().to_string()
|
||||
},
|
||||
captures
|
||||
.get(2)
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
captures[2].to_string(),
|
||||
)
|
||||
} else {
|
||||
(name.as_ref().to_string(), addr.as_ref().to_string())
|
||||
@@ -1089,45 +1085,21 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
||||
}
|
||||
}
|
||||
|
||||
/// Set profile image for a contact.
|
||||
///
|
||||
/// The given profile image is expected to be already in the blob directory
|
||||
/// as profile images can be set only by receiving messages, this should be always the case, however.
|
||||
///
|
||||
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar;
|
||||
/// this typically happens if we see message with our own profile image, sent from another device.
|
||||
pub(crate) async fn set_profile_image(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
profile_image: &AvatarAction,
|
||||
was_encrypted: bool,
|
||||
) -> Result<()> {
|
||||
// the given profile image is expected to be already in the blob directory
|
||||
// as profile images can be set only by receiving messages, this should be always the case, however.
|
||||
let mut contact = Contact::load_from_db(context, contact_id).await?;
|
||||
let changed = match profile_image {
|
||||
AvatarAction::Change(profile_image) => {
|
||||
if contact_id == DC_CONTACT_ID_SELF {
|
||||
if was_encrypted {
|
||||
context
|
||||
.set_config(Config::Selfavatar, Some(profile_image))
|
||||
.await?;
|
||||
} else {
|
||||
info!(context, "Do not use unencrypted selfavatar.");
|
||||
}
|
||||
} else {
|
||||
contact.param.set(Param::ProfileImage, profile_image);
|
||||
}
|
||||
contact.param.set(Param::ProfileImage, profile_image);
|
||||
true
|
||||
}
|
||||
AvatarAction::Delete => {
|
||||
if contact_id == DC_CONTACT_ID_SELF {
|
||||
if was_encrypted {
|
||||
context.set_config(Config::Selfavatar, None).await?;
|
||||
} else {
|
||||
info!(context, "Do not use unencrypted selfavatar deletion.");
|
||||
}
|
||||
} else {
|
||||
contact.param.remove(Param::ProfileImage);
|
||||
}
|
||||
contact.param.remove(Param::ProfileImage);
|
||||
true
|
||||
}
|
||||
};
|
||||
@@ -1141,21 +1113,38 @@ pub(crate) async fn set_profile_image(
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
/// - Convert names as "Petersen, Björn" to "Björn Petersen"
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: impl AsRef<str>) -> String {
|
||||
let full_name = full_name.as_ref().trim();
|
||||
let mut full_name = full_name.as_ref().trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
|
||||
match full_name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||
.get(1..full_name.len() - 1)
|
||||
.map_or("".to_string(), |s| s.trim().into()),
|
||||
_ => full_name.to_string(),
|
||||
let len = full_name.len();
|
||||
if len > 1 {
|
||||
let firstchar = full_name.as_bytes()[0];
|
||||
let lastchar = full_name.as_bytes()[len - 1];
|
||||
if firstchar == b'\'' && lastchar == b'\''
|
||||
|| firstchar == b'\"' && lastchar == b'\"'
|
||||
|| firstchar == b'<' && lastchar == b'>'
|
||||
{
|
||||
full_name = &full_name[1..len - 1];
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(p1) = full_name.find(',') {
|
||||
let (last_name, first_name) = full_name.split_at(p1);
|
||||
|
||||
let last_name = last_name.trim();
|
||||
let first_name = (&first_name[1..]).trim();
|
||||
|
||||
return format!("{} {}", first_name, last_name);
|
||||
}
|
||||
|
||||
full_name.trim().into()
|
||||
}
|
||||
|
||||
fn cat_fingerprint(
|
||||
@@ -1246,6 +1235,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_normalize_name() {
|
||||
assert_eq!(&normalize_name("Doe, John"), "John Doe");
|
||||
assert_eq!(&normalize_name(" hello world "), "hello world");
|
||||
assert_eq!(&normalize_name("<"), "<");
|
||||
assert_eq!(&normalize_name(">"), ">");
|
||||
@@ -1280,7 +1270,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_contacts() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("some2"))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1304,10 +1294,10 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_is_self_addr() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let t = test_context().await;
|
||||
assert!(t.ctx.is_self_addr("me@me.org").await.is_err());
|
||||
|
||||
let addr = t.configure_alice().await;
|
||||
let addr = configure_alice_keypair(&t.ctx).await;
|
||||
assert_eq!(t.ctx.is_self_addr("me@me.org").await?, false);
|
||||
assert_eq!(t.ctx.is_self_addr(&addr).await?, true);
|
||||
|
||||
@@ -1317,7 +1307,7 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_add_or_lookup() {
|
||||
// add some contacts, this also tests add_address_book()
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let book = concat!(
|
||||
" Name one \n one@eins.org \n",
|
||||
"Name two\ntwo@deux.net\n",
|
||||
@@ -1410,10 +1400,10 @@ mod tests {
|
||||
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
|
||||
assert_eq!(sth_modified, Modifier::None);
|
||||
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "Wonderland, Alice");
|
||||
assert_eq!(contact.get_display_name(), "Wonderland, Alice");
|
||||
assert_eq!(contact.get_name(), "Alice Wonderland");
|
||||
assert_eq!(contact.get_display_name(), "Alice Wonderland");
|
||||
assert_eq!(contact.get_addr(), "alice@w.de");
|
||||
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
|
||||
assert_eq!(contact.get_name_n_addr(), "Alice Wonderland (alice@w.de)");
|
||||
|
||||
// check SELF
|
||||
let contact = Contact::load_from_db(&t.ctx, DC_CONTACT_ID_SELF)
|
||||
@@ -1430,7 +1420,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_remote_authnames() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
|
||||
// incoming mail `From: bob1 <bob@example.org>` - this should init authname and name
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
@@ -1493,7 +1483,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_remote_authnames_create_empty() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
|
||||
// manually create "claire@example.org" without a given name
|
||||
let contact_id = Contact::create(&t.ctx, "", "claire@example.org")
|
||||
@@ -1540,7 +1530,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_remote_authnames_edit_empty() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
|
||||
// manually create "dave@example.org"
|
||||
let contact_id = Contact::create(&t.ctx, "dave1", "dave@example.org")
|
||||
@@ -1584,7 +1574,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_name_in_address() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
|
||||
let contact_id = Contact::create(&t.ctx, "", "<dave@example.org>")
|
||||
.await
|
||||
@@ -1597,7 +1587,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "Mueller, Dave");
|
||||
assert_eq!(contact.get_name(), "Dave Mueller");
|
||||
assert_eq!(contact.get_addr(), "dave@example.org");
|
||||
|
||||
let contact_id = Contact::create(&t.ctx, "name1", "name2 <dave@example.org>")
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::ops::Deref;
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
|
||||
use async_std::task;
|
||||
|
||||
use crate::chat::*;
|
||||
use crate::config::Config;
|
||||
@@ -15,10 +14,12 @@ use crate::contact::*;
|
||||
use crate::dc_tools::duration_to_str;
|
||||
use crate::error::*;
|
||||
use crate::events::{Event, EventEmitter, Events};
|
||||
use crate::job::{self, Action};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::message::{self, Message, MessengerMessage, MsgId};
|
||||
use crate::param::Params;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::sql::Sql;
|
||||
use std::time::SystemTime;
|
||||
@@ -51,13 +52,10 @@ pub struct InnerContext {
|
||||
pub(crate) generating_key_mutex: Mutex<()>,
|
||||
/// Mutex to enforce only a single running oauth2 is running.
|
||||
pub(crate) oauth2_mutex: Mutex<()>,
|
||||
/// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messeges being sent.
|
||||
pub(crate) wrong_pw_warning_mutex: Mutex<()>,
|
||||
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
|
||||
pub(crate) events: Events,
|
||||
|
||||
pub(crate) scheduler: RwLock<Scheduler>,
|
||||
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
|
||||
|
||||
creation_time: SystemTime,
|
||||
}
|
||||
@@ -120,11 +118,9 @@ impl Context {
|
||||
last_smeared_timestamp: RwLock::new(0),
|
||||
generating_key_mutex: Mutex::new(()),
|
||||
oauth2_mutex: Mutex::new(()),
|
||||
wrong_pw_warning_mutex: Mutex::new(()),
|
||||
translated_stockstrings: RwLock::new(HashMap::new()),
|
||||
events: Events::default(),
|
||||
scheduler: RwLock::new(Scheduler::Stopped),
|
||||
ephemeral_task: RwLock::new(None),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
};
|
||||
|
||||
@@ -458,6 +454,34 @@ impl Context {
|
||||
self.get_config(Config::ConfiguredMvboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
}
|
||||
|
||||
pub async fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
|
||||
if !self.get_config_bool(Config::MvboxMove).await {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_mvbox(folder).await {
|
||||
return;
|
||||
}
|
||||
if let Ok(msg) = Message::load_from_db(self, msg_id).await {
|
||||
if msg.is_setupmessage() {
|
||||
// do not move setup messages;
|
||||
// there may be a non-delta device that wants to handle it
|
||||
return;
|
||||
}
|
||||
|
||||
match msg.is_dc_message {
|
||||
MessengerMessage::No => {}
|
||||
MessengerMessage::Yes | MessengerMessage::Reply => {
|
||||
job::add(
|
||||
self,
|
||||
job::Job::new(Action::MoveMsg, msg.id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InnerContext {
|
||||
@@ -516,7 +540,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_fresh_msgs() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let fresh = t.ctx.get_fresh_msgs().await;
|
||||
assert!(fresh.is_empty())
|
||||
}
|
||||
@@ -571,13 +595,13 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn no_crashes_on_context_deref() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
std::mem::drop(t.ctx);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_info() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
|
||||
let info = t.ctx.get_info().await;
|
||||
assert!(info.get("database_dir").is_some());
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
|
||||
use crate::error::{bail, ensure, format_err, Result};
|
||||
use crate::events::Event;
|
||||
use crate::headerdef::HeaderDef;
|
||||
@@ -46,14 +45,13 @@ pub async fn dc_receive_imf(
|
||||
) -> Result<()> {
|
||||
info!(
|
||||
context,
|
||||
"Receiving message {}/{}, seen={}...",
|
||||
"Receiving message {}/{}...",
|
||||
if !server_folder.as_ref().is_empty() {
|
||||
server_folder.as_ref()
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
server_uid,
|
||||
seen
|
||||
);
|
||||
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" {
|
||||
@@ -197,14 +195,7 @@ pub async fn dc_receive_imf(
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.user_avatar {
|
||||
match contact::set_profile_image(
|
||||
&context,
|
||||
from_id,
|
||||
avatar_action,
|
||||
mime_parser.was_encrypted(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
match contact::set_profile_image(&context, from_id, avatar_action).await {
|
||||
Ok(()) => {
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
}
|
||||
@@ -231,29 +222,24 @@ pub async fn dc_receive_imf(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else if insert_msg_id
|
||||
.needs_move(context, server_folder.as_ref())
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
} else {
|
||||
// Move message if we don't delete it immediately.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MoveMsg, insert_msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
|
||||
// This is a Delta Chat MDN. Mark as read.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(
|
||||
Action::MarkseenMsgOnImap,
|
||||
insert_msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
context
|
||||
.do_heuristics_moves(server_folder.as_ref(), insert_msg_id)
|
||||
.await;
|
||||
if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
|
||||
// This is a Delta Chat MDN. Mark as read.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(
|
||||
Action::MarkseenMsgOnImap,
|
||||
insert_msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,78 +618,10 @@ async fn add_parts(
|
||||
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract ephemeral timer from the message.
|
||||
let mut ephemeral_timer = if let Some(value) = mime_parser.get(HeaderDef::EphemeralTimer) {
|
||||
match value.parse::<EphemeralTimer>() {
|
||||
Ok(timer) => timer,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"can't parse ephemeral timer \"{}\": {}", value, err
|
||||
);
|
||||
EphemeralTimer::Disabled
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EphemeralTimer::Disabled
|
||||
};
|
||||
|
||||
let location_kml_is = mime_parser.location_kml.is_some();
|
||||
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
||||
|
||||
// Apply ephemeral timer changes to the chat.
|
||||
//
|
||||
// Only non-hidden timers are applied now. Timers from hidden
|
||||
// messages such as read receipts can be useful to detect
|
||||
// ephemeral timer support, but timer changes without visible
|
||||
// received messages may be confusing to the user.
|
||||
if !*hidden
|
||||
&& !location_kml_is
|
||||
&& !is_mdn
|
||||
&& (*chat_id).get_ephemeral_timer(context).await? != ephemeral_timer
|
||||
{
|
||||
match (*chat_id)
|
||||
.inner_set_ephemeral_timer(context, ephemeral_timer)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
|
||||
set_better_msg(
|
||||
mime_parser,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
);
|
||||
|
||||
// Do not delete the system message itself.
|
||||
//
|
||||
// This prevents confusion when timer is changed
|
||||
// to 1 week, and then changed to 1 hour: after 1
|
||||
// hour, only the message about the change to 1
|
||||
// week is left.
|
||||
ephemeral_timer = EphemeralTimer::Disabled;
|
||||
} else {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
*chat_id,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"failed to modify timer for chat {}: {}", chat_id, err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// correct message_timestamp, it should not be used before,
|
||||
// however, we cannot do this earlier as we need from_id to be set
|
||||
let in_fresh = state == MessageState::InFresh;
|
||||
let rcvd_timestamp = time();
|
||||
let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, *chat_id, in_fresh).await;
|
||||
let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, *chat_id, !seen).await;
|
||||
*sent_timestamp = std::cmp::min(*sent_timestamp, rcvd_timestamp);
|
||||
|
||||
// unarchive chat
|
||||
@@ -730,6 +648,7 @@ async fn add_parts(
|
||||
|
||||
let mut parts = std::mem::replace(&mut mime_parser.parts, Vec::new());
|
||||
let server_folder = server_folder.as_ref().to_string();
|
||||
let location_kml_is = mime_parser.location_kml.is_some();
|
||||
let is_system_message = mime_parser.is_system_message;
|
||||
let mime_headers = if save_mime_headers {
|
||||
Some(String::from_utf8_lossy(imf_raw).to_string())
|
||||
@@ -739,6 +658,7 @@ async fn add_parts(
|
||||
let sent_timestamp = *sent_timestamp;
|
||||
let is_hidden = *hidden;
|
||||
let chat_id = *chat_id;
|
||||
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
||||
|
||||
// TODO: can this clone be avoided?
|
||||
let rfc724_mid = rfc724_mid.to_string();
|
||||
@@ -755,8 +675,8 @@ async fn add_parts(
|
||||
"INSERT INTO msgs \
|
||||
(rfc724_mid, server_folder, server_uid, chat_id, from_id, to_id, timestamp, \
|
||||
timestamp_sent, timestamp_rcvd, type, state, msgrmsg, txt, txt_raw, param, \
|
||||
bytes, hidden, mime_headers, mime_in_reply_to, mime_references, error, ephemeral_timer, ephemeral_timestamp) \
|
||||
VALUES (?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?, ?,?, ?,?,?);",
|
||||
bytes, hidden, mime_headers, mime_in_reply_to, mime_references, error) \
|
||||
VALUES (?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?, ?,?, ?);",
|
||||
)?;
|
||||
|
||||
let is_location_kml = location_kml_is
|
||||
@@ -778,15 +698,6 @@ async fn add_parts(
|
||||
part.param.set_int(Param::Cmd, is_system_message as i32);
|
||||
}
|
||||
|
||||
let ephemeral_timestamp = if in_fresh {
|
||||
0
|
||||
} else {
|
||||
match ephemeral_timer {
|
||||
EphemeralTimer::Disabled => 0,
|
||||
EphemeralTimer::Enabled { duration } => rcvd_timestamp + i64::from(duration)
|
||||
}
|
||||
};
|
||||
|
||||
stmt.execute(paramsv![
|
||||
rfc724_mid,
|
||||
server_folder,
|
||||
@@ -810,8 +721,6 @@ async fn add_parts(
|
||||
mime_in_reply_to,
|
||||
mime_references,
|
||||
part.error,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp
|
||||
])?;
|
||||
|
||||
drop(stmt);
|
||||
@@ -867,17 +776,15 @@ async fn add_parts(
|
||||
chat.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
if !is_mdn {
|
||||
update_last_subject(context, chat_id, mime_parser)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
context,
|
||||
"Could not update LastSubject of chat: {}",
|
||||
e.to_string()
|
||||
)
|
||||
});
|
||||
}
|
||||
update_last_subject(context, chat_id, mime_parser)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
context,
|
||||
"Could not update LastSubject of chat: {}",
|
||||
e.to_string()
|
||||
)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1199,19 +1106,23 @@ async fn create_or_lookup_group(
|
||||
|
||||
// again, check chat_id
|
||||
if chat_id.is_special() {
|
||||
if mime_parser.decrypting_failed {
|
||||
// It is possible that the message was sent to a valid,
|
||||
// yet unknown group, which was rejected because
|
||||
// Chat-Group-Name, which is in the encrypted part, was
|
||||
// not found. We can't create a properly named group in
|
||||
// this case, so assign error message to 1:1 chat with the
|
||||
// sender instead.
|
||||
return Ok((ChatId::new(0), Blocked::Not));
|
||||
return if group_explicitly_left {
|
||||
Ok((ChatId::new(DC_CHAT_ID_TRASH), chat_id_blocked))
|
||||
} else {
|
||||
// The message was decrypted successfully, but contains a late "quit" or otherwise
|
||||
// unwanted message.
|
||||
return Ok((ChatId::new(DC_CHAT_ID_TRASH), chat_id_blocked));
|
||||
}
|
||||
create_or_lookup_adhoc_group(
|
||||
context,
|
||||
mime_parser,
|
||||
allow_creation,
|
||||
create_blocked,
|
||||
from_id,
|
||||
to_ids,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!(context, "failed to create ad-hoc group: {:?}", err);
|
||||
err
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// We have a valid chat_id > DC_CHAT_ID_LAST_SPECIAL.
|
||||
@@ -1396,10 +1307,12 @@ async fn create_or_lookup_adhoc_group(
|
||||
// decrypted.
|
||||
//
|
||||
// The subject may be encrypted and contain a placeholder such
|
||||
// as "...". It can also be a COI group, with encrypted
|
||||
// Chat-Group-ID and incompatible Message-ID format.
|
||||
//
|
||||
// Instead, assign the message to 1:1 chat with the sender.
|
||||
// as "...". Besides that, it is possible that the message was
|
||||
// sent to a valid, yet unknown group, which was rejected
|
||||
// because Chat-Group-Name, which is in the encrypted part,
|
||||
// was not found. Generating a new ID in this case would
|
||||
// result in creation of a twin group with a different group
|
||||
// ID.
|
||||
warn!(
|
||||
context,
|
||||
"not creating ad-hoc group for message that cannot be decrypted"
|
||||
@@ -1531,7 +1444,6 @@ async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String {
|
||||
hex_hash(&members)
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn hex_hash(s: impl AsRef<str>) -> String {
|
||||
let bytes = s.as_ref().as_bytes();
|
||||
let result = Sha256::digest(bytes);
|
||||
@@ -1584,7 +1496,7 @@ async fn search_chat_ids_by_contact_ids(
|
||||
matches = 0;
|
||||
mismatches = 0;
|
||||
}
|
||||
if contact_ids.get(matches) == Some(&contact_id) {
|
||||
if matches < contact_ids.len() && contact_id == contact_ids[matches] {
|
||||
matches += 1;
|
||||
} else {
|
||||
mismatches += 1;
|
||||
@@ -1712,11 +1624,10 @@ async fn check_verified_properties(
|
||||
|
||||
fn set_better_msg(mime_parser: &mut MimeMessage, better_msg: impl AsRef<str>) {
|
||||
let msg = better_msg.as_ref();
|
||||
if !msg.is_empty() {
|
||||
if let Some(part) = mime_parser.parts.get_mut(0) {
|
||||
if part.typ == Viewtype::Text {
|
||||
part.msg = msg.to_string();
|
||||
}
|
||||
if !msg.is_empty() && !mime_parser.parts.is_empty() {
|
||||
let part = &mut mime_parser.parts[0];
|
||||
if part.typ == Viewtype::Text {
|
||||
part.msg = msg.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1875,7 +1786,7 @@ fn dc_create_incoming_rfc724_mid(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{ChatItem, ChatVisibility};
|
||||
use crate::chat::ChatVisibility;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::*;
|
||||
@@ -1890,7 +1801,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_grpid_simple() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = b"From: hello\n\
|
||||
Subject: outer-subject\n\
|
||||
In-Reply-To: <lqkjwelq123@123123>\n\
|
||||
@@ -1907,7 +1818,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_grpid_from_multiple() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = b"From: hello\n\
|
||||
Subject: outer-subject\n\
|
||||
In-Reply-To: <Gr.HcxyMARjyJy.9-qweqwe@asd.net>\n\
|
||||
@@ -1944,7 +1855,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_is_known_rfc724_mid() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("first message".to_string());
|
||||
let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg))
|
||||
@@ -1960,7 +1871,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_is_msgrmsg_rfc724_mid() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("first message".to_string());
|
||||
let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg))
|
||||
@@ -1974,34 +1885,34 @@ mod tests {
|
||||
assert!(!is_msgrmsg_rfc724_mid(&t.ctx, "nonexistant@message.id").await);
|
||||
}
|
||||
|
||||
static MSGRMSG: &[u8] = b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
static MSGRMSG: &[u8] = b"From: Bob <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Chat: hello\n\
|
||||
Message-ID: <Mr.1111@example.com>\n\
|
||||
Message-ID: <Mr.1111@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n";
|
||||
|
||||
static ONETOONE_NOREPLY_MAIL: &[u8] = b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
static ONETOONE_NOREPLY_MAIL: &[u8] = b"From: Bob <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Chat: hello\n\
|
||||
Message-ID: <2222@example.com>\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n";
|
||||
|
||||
static GRP_MAIL: &[u8] = b"From: bob@example.com\n\
|
||||
To: alice@example.com, claire@example.com\n\
|
||||
static GRP_MAIL: &[u8] = b"From: bob@example.org\n\
|
||||
To: alice@example.org, claire@example.org\n\
|
||||
Subject: group with Alice, Bob and Claire\n\
|
||||
Message-ID: <3333@example.com>\n\
|
||||
Message-ID: <3333@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n";
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_adhoc_group_show_chats_only() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
assert_eq!(t.ctx.get_config_int(Config::ShowEmails).await, 0);
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
@@ -2028,7 +1939,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_adhoc_group_show_accepted_contact_unknown() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::ShowEmails, Some("1"))
|
||||
.await
|
||||
@@ -2044,12 +1955,12 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_adhoc_group_show_accepted_contact_known() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::ShowEmails, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
Contact::create(&t.ctx, "Bob", "bob@example.com")
|
||||
Contact::create(&t.ctx, "Bob", "bob@example.org")
|
||||
.await
|
||||
.unwrap();
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false)
|
||||
@@ -2064,7 +1975,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_adhoc_group_show_accepted_contact_accepted() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::ShowEmails, Some("1"))
|
||||
.await
|
||||
@@ -2110,7 +2021,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_adhoc_group_show_all() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
@@ -2135,7 +2046,7 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_read_receipt_and_unarchive() {
|
||||
// create alice's account
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
|
||||
// create one-to-one with bob, archive one-to-one
|
||||
let bob_id = Contact::create(&t.ctx, "bob", "bob@exampel.org")
|
||||
@@ -2178,14 +2089,14 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
format!(
|
||||
"From: alice@example.com\n\
|
||||
To: bob@example.com\n\
|
||||
"From: alice@example.org\n\
|
||||
To: bob@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <Gr.{}.12345678901@example.com>\n\
|
||||
Message-ID: <Gr.{}.12345678901@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: {}\n\
|
||||
Chat-Group-Name: foo\n\
|
||||
Chat-Disposition-Notification-To: alice@example.com\n\
|
||||
Chat-Disposition-Notification-To: alice@example.org\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
@@ -2200,11 +2111,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, group_id, 0, None).await;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let msg_id = msgs.first().unwrap();
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2218,12 +2125,12 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
format!(
|
||||
"From: bob@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: message opened\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <Mr.12345678902@example.com>\n\
|
||||
Message-ID: <Mr.12345678902@example.org>\n\
|
||||
Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\
|
||||
\n\
|
||||
\n\
|
||||
@@ -2237,9 +2144,9 @@ mod tests {
|
||||
Content-Type: message/disposition-notification\n\
|
||||
\n\
|
||||
Reporting-UA: Delta Chat 1.28.0\n\
|
||||
Original-Recipient: rfc822;bob@example.com\n\
|
||||
Final-Recipient: rfc822;bob@example.com\n\
|
||||
Original-Message-ID: <Gr.{}.12345678901@example.com>\n\
|
||||
Original-Recipient: rfc822;bob@example.org\n\
|
||||
Final-Recipient: rfc822;bob@example.org\n\
|
||||
Original-Message-ID: <Gr.{}.12345678901@example.org>\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
\n\
|
||||
\n\
|
||||
@@ -2279,7 +2186,7 @@ mod tests {
|
||||
// are very rare, however, we have to add them to the database (they go to the
|
||||
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
let context = &t.ctx;
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
@@ -2287,9 +2194,9 @@ mod tests {
|
||||
|
||||
dc_receive_imf(
|
||||
context,
|
||||
b"To: bob@example.com\n\
|
||||
b"To: bob@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <3924@example.com>\n\
|
||||
Message-ID: <3924@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
@@ -2308,7 +2215,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_escaped_from() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
let contact_id = Contact::create(&t.ctx, "foobar", "foobar@example.com")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2318,9 +2225,9 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <asdklfjjaweofi@example.com>\n\
|
||||
Message-ID: <asdklfjjaweofi@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
@@ -2335,15 +2242,11 @@ mod tests {
|
||||
.await
|
||||
.unwrap()
|
||||
.get_authname(),
|
||||
"Имя, Фамилия",
|
||||
"Фамилия Имя", // The name was "Имя, Фамилия" and ("lastname, firstname") and should be swapped to "firstname, lastname"
|
||||
);
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let msg_id = msgs.first().unwrap();
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2354,7 +2257,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_escaped_recipients() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
Contact::create(&t.ctx, "foobar", "foobar@example.com")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2368,10 +2271,10 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
b"From: Foobar <foobar@example.com>\n\
|
||||
To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.com\n\
|
||||
To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.org\n\
|
||||
Cc: =?utf-8?q?=3Ch2=3E?= <carl@host.tld>\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <asdklfjjaweofi@example.com>\n\
|
||||
Message-ID: <asdklfjjaweofi@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: <foobar@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
@@ -2402,7 +2305,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_cc_to_contact() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
Contact::create(&t.ctx, "foobar", "foobar@example.com")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2420,10 +2323,10 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
b"From: Foobar <foobar@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Cc: Carl <carl@host.tld>\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <asdklfjjaweofi@example.com>\n\
|
||||
Message-ID: <asdklfjjaweofi@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: <foobar@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
@@ -2524,8 +2427,7 @@ mod tests {
|
||||
raw_ndn: &[u8],
|
||||
error_msg: &str,
|
||||
) {
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr(self_addr).await;
|
||||
let t = configured_offline_context_with_addr(self_addr).await;
|
||||
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
@@ -2570,19 +2472,18 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_ndn_group_msg() {
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr("alice@gmail.com").await;
|
||||
let t = configured_offline_context_with_addr("alice@gmail.com").await;
|
||||
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
b"From: alice@gmail.com\n\
|
||||
To: bob@example.com, assidhfaaspocwaeofi@gmail.com\n\
|
||||
To: bob@example.org, assidhfaaspocwaeofi@gmail.com\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <CADWx9Cs32Wa7Gy-gM0bvbq54P_FEHe7UcsAV=yW7sVVW=fiMYQ@mail.gmail.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde\n\
|
||||
Chat-Group-Name: foo\n\
|
||||
Chat-Disposition-Notification-To: alice@example.com\n\
|
||||
Chat-Disposition-Notification-To: alice@example.org\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
@@ -2606,12 +2507,9 @@ mod tests {
|
||||
assert_eq!(msg.state, MessageState::OutFailed);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, msg.chat_id, 0, None).await;
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let last_msg = Message::load_from_db(&t.ctx, *msg_id).await.unwrap();
|
||||
let last_msg = Message::load_from_db(&t.ctx, *msgs.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
last_msg.text,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
use core::cmp::{max, min};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -23,7 +22,6 @@ pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
||||
|
||||
/// Shortens a string to a specified length and adds "[...]" to the
|
||||
/// end of the shortened string.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
let ellipse = "[...]";
|
||||
|
||||
@@ -56,7 +54,6 @@ const COLORS: [u32; 16] = [
|
||||
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
|
||||
];
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
|
||||
let str_lower = s.as_ref().to_lowercase();
|
||||
let mut checksum = 0;
|
||||
@@ -201,7 +198,7 @@ fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
|
||||
pub(crate) fn dc_create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
|
||||
let hostname = from_addr
|
||||
.find('@')
|
||||
.and_then(|k| from_addr.get(k..))
|
||||
.map(|k| &from_addr[k..])
|
||||
.unwrap_or("@nohost");
|
||||
match grpid {
|
||||
Some(grpid) => format!("Gr.{}.{}{}", grpid, dc_create_id(), hostname),
|
||||
@@ -243,9 +240,9 @@ pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
|
||||
|
||||
/// Returns the `(width, height)` of the given image buffer.
|
||||
pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
|
||||
let image = image::io::Reader::new(Cursor::new(buf));
|
||||
let dimensions = image.into_dimensions()?;
|
||||
Ok(dimensions)
|
||||
let meta = image_meta::load_from_buf(buf)?;
|
||||
|
||||
Ok((meta.dimensions.width, meta.dimensions.height))
|
||||
}
|
||||
|
||||
/// Expand paths relative to $BLOBDIR into absolute paths.
|
||||
@@ -777,7 +774,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_file_handling() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let context = &t.ctx;
|
||||
macro_rules! dc_file_exist {
|
||||
($ctx:expr, $fname:expr) => {
|
||||
@@ -856,7 +853,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_smeared_timestamp() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
assert_ne!(
|
||||
dc_create_smeared_timestamp(&t.ctx).await,
|
||||
dc_create_smeared_timestamp(&t.ctx).await
|
||||
@@ -872,7 +869,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_smeared_timestamps() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
|
||||
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
|
||||
let next = dc_smeared_time(&t.ctx).await;
|
||||
|
||||
44
src/e2ee.rs
44
src/e2ee.rs
@@ -115,12 +115,6 @@ impl EncryptHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to decrypt a message, but only if it is structured as an
|
||||
/// Autocrypt message, i.e. encrypted and signed with a valid
|
||||
/// signature.
|
||||
///
|
||||
/// Returns decrypted body and a set of valid signature fingerprints
|
||||
/// if successful.
|
||||
pub async fn try_decrypt(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
@@ -193,23 +187,24 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
|
||||
"Not a multipart/encrypted message: {}",
|
||||
mail.ctype.mimetype
|
||||
);
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
ensure!(
|
||||
first_part.ctype.mimetype == "application/pgp-encrypted",
|
||||
"Invalid Autocrypt Level 1 version part: {:?}",
|
||||
first_part.ctype,
|
||||
);
|
||||
ensure!(
|
||||
mail.subparts.len() == 2,
|
||||
"Invalid Autocrypt Level 1 Mime Parts"
|
||||
);
|
||||
|
||||
ensure!(
|
||||
second_part.ctype.mimetype == "application/octet-stream",
|
||||
"Invalid Autocrypt Level 1 encrypted part: {:?}",
|
||||
second_part.ctype
|
||||
);
|
||||
ensure!(
|
||||
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
|
||||
"Invalid Autocrypt Level 1 version part: {:?}",
|
||||
mail.subparts[0].ctype,
|
||||
);
|
||||
|
||||
Ok(second_part)
|
||||
} else {
|
||||
bail!("Invalid Autocrypt Level 1 Mime Parts")
|
||||
}
|
||||
ensure!(
|
||||
mail.subparts[1].ctype.mimetype == "application/octet-stream",
|
||||
"Invalid Autocrypt Level 1 encrypted part: {:?}",
|
||||
mail.subparts[1].ctype
|
||||
);
|
||||
|
||||
Ok(&mail.subparts[1])
|
||||
}
|
||||
|
||||
async fn decrypt_if_autocrypt_message<'a>(
|
||||
@@ -272,7 +267,6 @@ async fn decrypt_part(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
|
||||
if let Some(index) = input.iter().position(|b| *b > b' ') {
|
||||
if input.len() - index > 26 {
|
||||
@@ -333,14 +327,14 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_prexisting() {
|
||||
let t = TestContext::new().await;
|
||||
let test_addr = t.configure_alice().await;
|
||||
let t = dummy_context().await;
|
||||
let test_addr = configure_alice_keypair(&t.ctx).await;
|
||||
assert_eq!(ensure_secret_key_exists(&t.ctx).await.unwrap(), test_addr);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_not_configured() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
assert!(ensure_secret_key_exists(&t.ctx).await.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
528
src/ephemeral.rs
528
src/ephemeral.rs
@@ -1,528 +0,0 @@
|
||||
//! # Ephemeral messages
|
||||
//!
|
||||
//! Ephemeral messages are messages that have an Ephemeral-Timer
|
||||
//! header attached to them, which specifies time in seconds after
|
||||
//! which the message should be deleted both from the device and from
|
||||
//! the server. The timer is started when the message is marked as
|
||||
//! seen, which usually happens when its contents is displayed on
|
||||
//! device screen.
|
||||
//!
|
||||
//! Each chat, including 1:1, group chats and "saved messages" chat,
|
||||
//! has its own ephemeral timer setting, which is applied to all
|
||||
//! messages sent to the chat. The setting is synchronized to all the
|
||||
//! devices participating in the chat by applying the timer value from
|
||||
//! all received messages, including BCC-self ones, to the chat. This
|
||||
//! way the setting is eventually synchronized among all participants.
|
||||
//!
|
||||
//! When user changes ephemeral timer setting for the chat, a system
|
||||
//! message is automatically sent to update the setting for all
|
||||
//! participants. This allows changing the setting for a chat like any
|
||||
//! group chat setting, e.g. name and avatar, without the need to
|
||||
//! write an actual message.
|
||||
//!
|
||||
//! ## Device settings
|
||||
//!
|
||||
//! In addition to per-chat ephemeral message setting, each device has
|
||||
//! two global user-configured settings that complement per-chat
|
||||
//! settings: `delete_device_after` and `delete_server_after`. These
|
||||
//! settings are not synchronized among devices and apply to all
|
||||
//! messages known to the device, including messages sent or received
|
||||
//! before configuring the setting.
|
||||
//!
|
||||
//! `delete_device_after` configures the maximum time device is
|
||||
//! storing the messages locally. `delete_server_after` configures the
|
||||
//! time after which device will delete the messages it knows about
|
||||
//! from the server.
|
||||
//!
|
||||
//! ## How messages are deleted
|
||||
//!
|
||||
//! When the message is deleted locally, its contents is removed and
|
||||
//! it is moved to the trash chat. This database entry is then used to
|
||||
//! track the Message-ID and corresponding IMAP folder and UID until
|
||||
//! the message is deleted from the server. Vice versa, when device
|
||||
//! deletes the message from the server, it removes IMAP folder and
|
||||
//! UID information, but keeps the message contents. When database
|
||||
//! entry is both moved to trash chat and does not contain UID
|
||||
//! information, it is deleted from the database, leaving no trace of
|
||||
//! the message.
|
||||
//!
|
||||
//! ## When messages are deleted
|
||||
//!
|
||||
//! Local deletion happens when the chatlist or chat is loaded. A
|
||||
//! `MsgsChanged` event is emitted when a message deletion is due, to
|
||||
//! make UI reload displayed messages and cause actual deletion.
|
||||
//!
|
||||
//! Server deletion happens by generating IMAP deletion jobs based on
|
||||
//! the database entries which are expired either according to their
|
||||
//! ephemeral message timers or global `delete_server_after` setting.
|
||||
|
||||
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
|
||||
use crate::constants::{
|
||||
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::error::{ensure, Error};
|
||||
use crate::events::Event;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
use async_std::task;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum Timer {
|
||||
Disabled,
|
||||
Enabled { duration: u32 },
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
pub fn to_u32(self) -> u32 {
|
||||
match self {
|
||||
Self::Disabled => 0,
|
||||
Self::Enabled { duration } => duration,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_u32(duration: u32) -> Self {
|
||||
if duration == 0 {
|
||||
Self::Disabled
|
||||
} else {
|
||||
Self::Enabled { duration }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Timer {
|
||||
fn default() -> Self {
|
||||
Self::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Timer {
|
||||
fn to_string(&self) -> String {
|
||||
self.to_u32().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Timer {
|
||||
type Err = ParseIntError;
|
||||
|
||||
fn from_str(input: &str) -> Result<Timer, ParseIntError> {
|
||||
input.parse::<u32>().map(Self::from_u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for Timer {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Integer(match self {
|
||||
Self::Disabled => 0,
|
||||
Self::Enabled { duration } => i64::from(*duration),
|
||||
});
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::FromSql for Timer {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|value| {
|
||||
if value == 0 {
|
||||
Ok(Self::Disabled)
|
||||
} else if let Ok(duration) = u32::try_from(value) {
|
||||
Ok(Self::Enabled { duration })
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(value))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatId {
|
||||
/// Get ephemeral message timer value in seconds.
|
||||
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
|
||||
let timer = context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timer FROM chats WHERE id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?;
|
||||
Ok(timer.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Set ephemeral timer value without sending a message.
|
||||
///
|
||||
/// Used when a message arrives indicating that someone else has
|
||||
/// changed the timer value for a chat.
|
||||
pub(crate) async fn inner_set_ephemeral_timer(
|
||||
self,
|
||||
context: &Context,
|
||||
timer: Timer,
|
||||
) -> Result<(), Error> {
|
||||
ensure!(!self.is_special(), "Invalid chat ID");
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats
|
||||
SET ephemeral_timer=?
|
||||
WHERE id=?;",
|
||||
paramsv![timer, self],
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(Event::ChatEphemeralTimerModified {
|
||||
chat_id: self,
|
||||
timer,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set ephemeral message timer value in seconds.
|
||||
///
|
||||
/// If timer value is 0, disable ephemeral message timer.
|
||||
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<(), Error> {
|
||||
if timer == self.get_ephemeral_timer(context).await? {
|
||||
return Ok(());
|
||||
}
|
||||
self.inner_set_ephemeral_timer(context, timer).await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(stock_ephemeral_timer_changed(context, timer, DC_CONTACT_ID_SELF).await);
|
||||
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
|
||||
if let Err(err) = send_msg(context, self, &mut msg).await {
|
||||
error!(
|
||||
context,
|
||||
"Failed to send a message about ephemeral message timer change: {:?}", err
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a stock message saying that ephemeral timer is changed to `timer` by `from_id`.
|
||||
pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
context: &Context,
|
||||
timer: Timer,
|
||||
from_id: u32,
|
||||
) -> String {
|
||||
let stock_message = match timer {
|
||||
Timer::Disabled => StockMessage::MsgEphemeralTimerDisabled,
|
||||
Timer::Enabled { duration } => match duration {
|
||||
60 => StockMessage::MsgEphemeralTimerMinute,
|
||||
3600 => StockMessage::MsgEphemeralTimerHour,
|
||||
86400 => StockMessage::MsgEphemeralTimerDay,
|
||||
604_800 => StockMessage::MsgEphemeralTimerWeek,
|
||||
2_419_200 => StockMessage::MsgEphemeralTimerFourWeeks,
|
||||
_ => StockMessage::MsgEphemeralTimerEnabled,
|
||||
},
|
||||
};
|
||||
|
||||
context
|
||||
.stock_system_msg(stock_message, timer.to_string(), "", from_id)
|
||||
.await
|
||||
}
|
||||
|
||||
impl MsgId {
|
||||
/// Returns ephemeral message timer value for the message.
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timer FROM msgs WHERE id=?",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?
|
||||
{
|
||||
None | Some(0) => Timer::Disabled,
|
||||
Some(duration) => Timer::Enabled { duration },
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Starts ephemeral message timer for the message if it is not started yet.
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> crate::sql::Result<()> {
|
||||
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
|
||||
let ephemeral_timestamp = time() + i64::from(duration);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? \
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
|
||||
AND id = ?",
|
||||
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
|
||||
)
|
||||
.await?;
|
||||
schedule_ephemeral_task(context).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes messages which are expired according to
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// Returns true if any message is deleted, so caller can emit
|
||||
/// MsgsChanged event. If nothing has been deleted, returns
|
||||
/// false. This function does not emit the MsgsChanged event itself,
|
||||
/// because it is also called when chatlist is reloaded, and emitting
|
||||
/// MsgsChanged there will cause infinite reload loop.
|
||||
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, Error> {
|
||||
let mut updated = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE \
|
||||
ephemeral_timestamp != 0 \
|
||||
AND ephemeral_timestamp < ? \
|
||||
AND chat_id != ?",
|
||||
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
|
||||
)
|
||||
.await?
|
||||
> 0;
|
||||
|
||||
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
|
||||
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
|
||||
let threshold_timestamp = time() - delete_device_after;
|
||||
|
||||
// Delete expired messages
|
||||
//
|
||||
// Only update the rows that have to be updated, to avoid emitting
|
||||
// unnecessary "chat modified" events.
|
||||
let rows_modified = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE timestamp < ? \
|
||||
AND chat_id > ? \
|
||||
AND chat_id != ? \
|
||||
AND chat_id != ?",
|
||||
paramsv![
|
||||
DC_CHAT_ID_TRASH,
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
updated |= rows_modified > 0;
|
||||
}
|
||||
|
||||
schedule_ephemeral_task(context).await;
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
/// Schedule a task to emit MsgsChanged event when the next local
|
||||
/// deletion happens. Existing task is cancelled to make sure at most
|
||||
/// one such task is scheduled at a time.
|
||||
///
|
||||
/// UI is expected to reload the chatlist or the chat in response to
|
||||
/// MsgsChanged event, this will trigger actual deletion.
|
||||
///
|
||||
/// This takes into account only per-chat timeouts, because global device
|
||||
/// timeouts are at least one hour long and deletion is triggered often enough
|
||||
/// by user actions.
|
||||
pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
let ephemeral_timestamp: Option<i64> = match context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timestamp \
|
||||
FROM msgs \
|
||||
WHERE ephemeral_timestamp != 0 \
|
||||
AND chat_id != ? \
|
||||
ORDER BY ephemeral_timestamp ASC \
|
||||
LIMIT 1",
|
||||
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
warn!(context, "Can't calculate next ephemeral timeout: {}", err);
|
||||
return;
|
||||
}
|
||||
Ok(ephemeral_timestamp) => ephemeral_timestamp,
|
||||
};
|
||||
|
||||
// Cancel existing task, if any
|
||||
if let Some(ephemeral_task) = context.ephemeral_task.write().await.take() {
|
||||
ephemeral_task.cancel().await;
|
||||
}
|
||||
|
||||
if let Some(ephemeral_timestamp) = ephemeral_timestamp {
|
||||
let now = SystemTime::now();
|
||||
let until = UNIX_EPOCH
|
||||
+ Duration::from_secs(ephemeral_timestamp.try_into().unwrap_or(u64::MAX))
|
||||
+ Duration::from_secs(1);
|
||||
|
||||
if let Ok(duration) = until.duration_since(now) {
|
||||
// Schedule a task, ephemeral_timestamp is in the future
|
||||
let context1 = context.clone();
|
||||
let ephemeral_task = task::spawn(async move {
|
||||
async_std::task::sleep(duration).await;
|
||||
emit_event!(
|
||||
context1,
|
||||
Event::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0)
|
||||
}
|
||||
);
|
||||
});
|
||||
*context.ephemeral_task.write().await = Some(ephemeral_task);
|
||||
} else {
|
||||
// Emit event immediately
|
||||
emit_event!(
|
||||
context,
|
||||
Event::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns ID of any expired message that should be deleted from the server.
|
||||
///
|
||||
/// It looks up the trash chat too, to find messages that are already
|
||||
/// deleted locally, but not deleted on the server.
|
||||
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
||||
let now = time();
|
||||
|
||||
let threshold_timestamp = match context.get_config_delete_server_after().await {
|
||||
None => 0,
|
||||
Some(delete_server_after) => now - delete_server_after,
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs \
|
||||
WHERE ( \
|
||||
timestamp < ? \
|
||||
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp < ?) \
|
||||
) \
|
||||
AND server_uid != 0 \
|
||||
LIMIT 1",
|
||||
paramsv![threshold_timestamp, now],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start ephemeral timers for seen messages if they are not started
|
||||
/// yet.
|
||||
///
|
||||
/// It is possible that timers are not started due to a missing or
|
||||
/// failed `MsgId.start_ephemeral_timer()` call, either in the current
|
||||
/// or previous version of Delta Chat.
|
||||
///
|
||||
/// This function is supposed to be called in the background,
|
||||
/// e.g. from housekeeping task.
|
||||
pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET ephemeral_timestamp = ? + ephemeral_timer \
|
||||
WHERE ephemeral_timer > 0 \
|
||||
AND ephemeral_timestamp = 0 \
|
||||
AND state NOT IN (?, ?, ?)",
|
||||
paramsv![
|
||||
time(),
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
MessageState::OutDraft
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_ephemeral_messages() {
|
||||
let context = TestContext::new().await.ctx;
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, DC_CONTACT_ID_SELF).await,
|
||||
"Message deletion timer is disabled by me."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, 0).await,
|
||||
"Message deletion timer is disabled."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, 0).await,
|
||||
"Message deletion timer is set to 1 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, 0).await,
|
||||
"Message deletion timer is set to 30 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, 0).await,
|
||||
"Message deletion timer is set to 1 minute."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 * 60 }, 0).await,
|
||||
"Message deletion timer is set to 1 hour."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 day."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 7 * 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 week."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 4 * 7 * 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 4 weeks."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ use async_std::sync::{channel, Receiver, Sender, TrySendError};
|
||||
use strum::EnumProperty;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::message::MsgId;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -140,6 +139,7 @@ pub enum Event {
|
||||
/// Network errors should be reported to users in a non-disturbing way,
|
||||
/// however, as network errors may come in a sequence,
|
||||
/// it is not useful to raise each an every error to the user.
|
||||
/// For this purpose, data1 is set to 1 if the error is probably worth reporting.
|
||||
///
|
||||
/// Moreover, if the UI detects that the device is offline,
|
||||
/// it is probably more useful to report this to the user
|
||||
@@ -189,19 +189,9 @@ pub enum Event {
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||
/// and dc_remove_contact_from_chat().
|
||||
///
|
||||
/// This event does not include ephemeral timer modification, which
|
||||
/// is a separate event.
|
||||
#[strum(props(id = "2020"))]
|
||||
ChatModified(ChatId),
|
||||
|
||||
/// Chat ephemeral timer changed.
|
||||
#[strum(props(id = "2021"))]
|
||||
ChatEphemeralTimerModified {
|
||||
chat_id: ChatId,
|
||||
timer: EphemeralTimer,
|
||||
},
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -35,7 +35,6 @@ pub enum HeaderDef {
|
||||
ChatContent,
|
||||
ChatDuration,
|
||||
ChatDispositionNotificationTo,
|
||||
ChatWebrtcRoom,
|
||||
Autocrypt,
|
||||
AutocryptSetupMessage,
|
||||
SecureJoin,
|
||||
@@ -43,7 +42,6 @@ pub enum HeaderDef {
|
||||
SecureJoinFingerprint,
|
||||
SecureJoinInvitenumber,
|
||||
SecureJoinAuth,
|
||||
EphemeralTimer,
|
||||
_TestHeader,
|
||||
}
|
||||
|
||||
|
||||
225
src/imap/idle.rs
225
src/imap/idle.rs
@@ -1,7 +1,6 @@
|
||||
use super::Imap;
|
||||
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use async_imap::types::UnsolicitedResponse;
|
||||
use async_std::prelude::*;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -14,19 +13,19 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("IMAP IDLE protocol failed to init/complete: {0}")]
|
||||
#[error("IMAP IDLE protocol failed to init/complete")]
|
||||
IdleProtocolFailed(#[from] async_imap::error::Error),
|
||||
|
||||
#[error("IMAP IDLE protocol timed out: {0}")]
|
||||
#[error("IMAP IDLE protocol timed out")]
|
||||
IdleTimeout(#[from] async_std::future::TimeoutError),
|
||||
|
||||
#[error("IMAP server does not have IDLE capability")]
|
||||
IdleAbilityMissing,
|
||||
|
||||
#[error("IMAP select folder error: {0}")]
|
||||
#[error("IMAP select folder error")]
|
||||
SelectFolderError(#[from] select_folder::Error),
|
||||
|
||||
#[error("Setup handle error: {0}")]
|
||||
#[error("Setup handle error")]
|
||||
SetupHandleError(#[from] super::Error),
|
||||
}
|
||||
|
||||
@@ -49,27 +48,11 @@ impl Imap {
|
||||
|
||||
self.select_folder(context, watch_folder.clone()).await?;
|
||||
|
||||
let session = self.session.take();
|
||||
let timeout = Duration::from_secs(23 * 60);
|
||||
let mut info = Default::default();
|
||||
|
||||
if let Some(session) = self.session.take() {
|
||||
// if we have unsolicited responses we directly return
|
||||
let mut unsolicited_exists = false;
|
||||
while let Ok(response) = session.unsolicited_responses.try_recv() {
|
||||
match response {
|
||||
UnsolicitedResponse::Exists(_) => {
|
||||
warn!(context, "skip idle, got unsolicited EXISTS {:?}", response);
|
||||
unsolicited_exists = true;
|
||||
}
|
||||
_ => info!(context, "ignoring unsolicited response {:?}", response),
|
||||
}
|
||||
}
|
||||
|
||||
if unsolicited_exists {
|
||||
self.session = Some(session);
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
if let Some(session) = session {
|
||||
let mut handle = session.idle();
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
@@ -82,43 +65,68 @@ impl Imap {
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
|
||||
let probe_network = self.idle_interrupt.recv().await;
|
||||
|
||||
// cancel imap idle connection properly
|
||||
if self.skip_next_idle_wait {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait = false;
|
||||
drop(idle_wait);
|
||||
drop(interrupt);
|
||||
|
||||
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
|
||||
});
|
||||
info!(context, "Idle wait was skipped");
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(
|
||||
self.idle_interrupt.recv().map(|probe_network| {
|
||||
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
|
||||
}),
|
||||
);
|
||||
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
|
||||
info!(context, "Idle has NewData {:?}", x);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Ok(Event::Interrupt(i)) => {
|
||||
info = i;
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Idle wait errored: {:?}", err);
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(_))) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Ok(Event::Interrupt(i)) => {
|
||||
info = i;
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Idle wait errored: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session = handle
|
||||
// if we can't properly terminate the idle
|
||||
// protocol let's break the connection.
|
||||
let res = handle
|
||||
.done()
|
||||
.timeout(Duration::from_secs(15))
|
||||
.await
|
||||
.map_err(Error::IdleTimeout)??;
|
||||
self.session = Some(Session { inner: session });
|
||||
} else {
|
||||
warn!(context, "Attempted to idle without a session");
|
||||
.map_err(|err| {
|
||||
self.trigger_reconnect();
|
||||
Error::IdleTimeout(err)
|
||||
})?;
|
||||
|
||||
match res {
|
||||
Ok(session) => {
|
||||
self.session = Some(Session { inner: session });
|
||||
}
|
||||
Err(err) => {
|
||||
// if we cannot terminate IDLE it probably
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(info)
|
||||
@@ -140,66 +148,73 @@ impl Imap {
|
||||
return self.idle_interrupt.recv().await.unwrap_or_default();
|
||||
}
|
||||
|
||||
// check every minute if there are new messages
|
||||
// TODO: grow sleep durations / make them more flexible
|
||||
let mut interval = async_std::stream::interval(Duration::from_secs(60));
|
||||
let mut info: InterruptInfo = Default::default();
|
||||
if self.skip_next_idle_wait {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait = false;
|
||||
info!(context, "fake-idle wait was skipped");
|
||||
} else {
|
||||
// check every minute if there are new messages
|
||||
// TODO: grow sleep durations / make them more flexible
|
||||
let mut interval = async_std::stream::interval(Duration::from_secs(60));
|
||||
|
||||
enum Event {
|
||||
Tick,
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
// loop until we are interrupted or if we fetched something
|
||||
let info = loop {
|
||||
use futures::future::FutureExt;
|
||||
match interval
|
||||
.next()
|
||||
.map(|_| Event::Tick)
|
||||
.race(
|
||||
self.idle_interrupt
|
||||
.recv()
|
||||
.map(|probe_network| Event::Interrupt(probe_network.unwrap_or_default())),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Event::Tick => {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if let Err(err) = self.connect_configured(context).await {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break InterruptInfo::new(false, None);
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
enum Event {
|
||||
Tick,
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
// loop until we are interrupted or if we fetched something
|
||||
info =
|
||||
loop {
|
||||
use futures::future::FutureExt;
|
||||
match interval
|
||||
.next()
|
||||
.map(|_| Event::Tick)
|
||||
.race(self.idle_interrupt.recv().map(|probe_network| {
|
||||
Event::Interrupt(probe_network.unwrap_or_default())
|
||||
}))
|
||||
.await
|
||||
{
|
||||
Event::Tick => {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if let Err(err) = self.connect_configured(context).await {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break InterruptInfo::new(false, None);
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
match self.fetch_new_messages(context, watch_folder).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
if res {
|
||||
break InterruptInfo::new(false, None);
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
match self.fetch_new_messages(context, watch_folder).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
if res {
|
||||
break InterruptInfo::new(false, None);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
}
|
||||
}
|
||||
Event::Interrupt(info) => {
|
||||
// Interrupt
|
||||
break info;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Interrupt(info) => {
|
||||
// Interrupt
|
||||
break info;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
|
||||
127
src/imap/mod.rs
127
src/imap/mod.rs
@@ -23,12 +23,12 @@ use crate::events::Event;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::job::{self, Action};
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::message::{self, update_server_uid, MessageState};
|
||||
use crate::message::{self, update_server_uid};
|
||||
use crate::mimeparser;
|
||||
use crate::oauth2::dc_get_oauth2_access_token;
|
||||
use crate::param::Params;
|
||||
use crate::provider::get_provider_info;
|
||||
use crate::{chat, scheduler::InterruptInfo, stock::StockMessage};
|
||||
use crate::{scheduler::InterruptInfo, stock::StockMessage};
|
||||
|
||||
mod client;
|
||||
mod idle;
|
||||
@@ -36,7 +36,6 @@ pub mod select_folder;
|
||||
mod session;
|
||||
|
||||
use client::Client;
|
||||
use message::Message;
|
||||
use session::Session;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -116,8 +115,8 @@ pub struct Imap {
|
||||
session: Option<Session>,
|
||||
connected: bool,
|
||||
interrupt: Option<stop_token::StopSource>,
|
||||
skip_next_idle_wait: bool,
|
||||
should_reconnect: bool,
|
||||
login_failed_once: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -190,8 +189,8 @@ impl Imap {
|
||||
session: Default::default(),
|
||||
connected: Default::default(),
|
||||
interrupt: Default::default(),
|
||||
skip_next_idle_wait: Default::default(),
|
||||
should_reconnect: Default::default(),
|
||||
login_failed_once: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,40 +295,18 @@ impl Imap {
|
||||
// needs to be set here to ensure it is set on reconnects.
|
||||
self.connected = true;
|
||||
self.session = Some(session);
|
||||
self.login_failed_once = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Err((err, _)) => {
|
||||
let imap_user = self.config.imap_user.to_owned();
|
||||
let message = context
|
||||
.stock_string_repl_str(StockMessage::CannotLogin, &imap_user)
|
||||
.await;
|
||||
|
||||
warn!(context, "{} ({})", message, err);
|
||||
emit_event!(context, Event::ErrorNetwork(message.clone()));
|
||||
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await
|
||||
{
|
||||
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
|
||||
warn!(context, "{}", e);
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(message);
|
||||
if let Err(e) =
|
||||
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
|
||||
.await
|
||||
{
|
||||
warn!(context, "{}", e);
|
||||
}
|
||||
} else {
|
||||
self.login_failed_once = true;
|
||||
}
|
||||
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorNetwork(format!("{} ({})", message, err))
|
||||
);
|
||||
self.trigger_reconnect();
|
||||
Err(Error::LoginFailed(format!("cannot login as {}", imap_user)))
|
||||
}
|
||||
@@ -755,17 +732,9 @@ impl Imap {
|
||||
folder: S,
|
||||
server_uids: &[u32],
|
||||
) -> (Option<u32>, usize) {
|
||||
let set = match server_uids {
|
||||
[] => return (None, 0),
|
||||
[server_uid] => server_uid.to_string(),
|
||||
[first_uid, .., last_uid] => {
|
||||
// XXX: it is assumed that UIDs are sorted and
|
||||
// contiguous. If UIDs are not contiguous, more
|
||||
// messages than needed will be downloaded.
|
||||
debug_assert!(first_uid < last_uid, "uids must be sorted");
|
||||
format!("{}:{}", first_uid, last_uid)
|
||||
}
|
||||
};
|
||||
if server_uids.is_empty() {
|
||||
return (None, 0);
|
||||
}
|
||||
|
||||
if !self.is_connected() {
|
||||
warn!(context, "Not connected");
|
||||
@@ -781,6 +750,15 @@ impl Imap {
|
||||
|
||||
let session = self.session.as_mut().unwrap();
|
||||
|
||||
let set = if server_uids.len() == 1 {
|
||||
server_uids[0].to_string()
|
||||
} else {
|
||||
let first_uid = server_uids[0];
|
||||
let last_uid = server_uids[server_uids.len() - 1];
|
||||
assert!(first_uid < last_uid, "uids must be sorted");
|
||||
format!("{}:{}", first_uid, last_uid)
|
||||
};
|
||||
|
||||
let mut msgs = match session.uid_fetch(&set, BODY_FLAGS).await {
|
||||
Ok(msgs) => msgs,
|
||||
Err(err) => {
|
||||
@@ -804,6 +782,7 @@ impl Imap {
|
||||
let mut last_uid = None;
|
||||
let mut count = 0;
|
||||
|
||||
let mut tasks = Vec::with_capacity(server_uids.len());
|
||||
while let Some(Ok(msg)) = msgs.next().await {
|
||||
let server_uid = msg.uid.unwrap_or_default();
|
||||
|
||||
@@ -823,17 +802,32 @@ impl Imap {
|
||||
let context = context.clone();
|
||||
let folder = folder.clone();
|
||||
|
||||
// safe, as we checked above that there is a body.
|
||||
let body = msg.body().unwrap();
|
||||
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
|
||||
let task = async_std::task::spawn(async move {
|
||||
// safe, as we checked above that there is a body.
|
||||
let body = msg.body().unwrap();
|
||||
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
|
||||
|
||||
match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await {
|
||||
Ok(_) => last_uid = Some(server_uid),
|
||||
Err(err) => {
|
||||
warn!(context, "dc_receive_imf error: {}", err);
|
||||
match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await {
|
||||
Ok(_) => Some(server_uid),
|
||||
Err(err) => {
|
||||
warn!(context, "dc_receive_imf error: {}", err);
|
||||
read_errors += 1;
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
for task in futures::future::join_all(tasks).await {
|
||||
match task {
|
||||
Some(uid) => {
|
||||
last_uid = Some(uid);
|
||||
}
|
||||
None => {
|
||||
read_errors += 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if count != server_uids.len() {
|
||||
@@ -1366,26 +1360,14 @@ async fn precheck_imf(
|
||||
let delete_server_after = context.get_config_delete_server_after().await;
|
||||
|
||||
if delete_server_after != Some(0) {
|
||||
if msg_id
|
||||
.needs_move(context, server_folder)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// If the bcc-self message is not moved, directly
|
||||
// add MarkSeen job, otherwise MarkSeen job is
|
||||
// added after the Move Job completed.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MoveMsg, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
context
|
||||
.do_heuristics_moves(server_folder.as_ref(), msg_id)
|
||||
.await;
|
||||
} else {
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else if old_server_folder != server_folder {
|
||||
info!(
|
||||
@@ -1420,13 +1402,6 @@ async fn precheck_imf(
|
||||
|
||||
if old_server_folder != server_folder || old_server_uid != server_uid {
|
||||
update_server_uid(context, rfc724_mid, server_folder, server_uid).await;
|
||||
if let Ok(MessageState::InSeen) = msg_id.get_state(context).await {
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
};
|
||||
context
|
||||
.interrupt_inbox(InterruptInfo::new(false, Some(msg_id)))
|
||||
.await;
|
||||
|
||||
@@ -51,14 +51,6 @@ impl Imap {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Issues a CLOSE command if selected folder needs expunge.
|
||||
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> Result<()> {
|
||||
if self.config.selected_folder_needs_expunge {
|
||||
self.close_folder(context).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// select a folder, possibly update uid_validity and, if needed,
|
||||
/// expunge the folder to remove delete-marked messages.
|
||||
pub(super) async fn select_folder<S: AsRef<str>>(
|
||||
@@ -84,7 +76,10 @@ impl Imap {
|
||||
}
|
||||
|
||||
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
|
||||
self.maybe_close_folder(context).await?;
|
||||
let needs_expunge = { self.config.selected_folder_needs_expunge };
|
||||
if needs_expunge {
|
||||
self.close_folder(context).await?;
|
||||
}
|
||||
|
||||
// select new folder
|
||||
if let Some(ref folder) = folder {
|
||||
|
||||
53
src/imex.rs
53
src/imex.rs
@@ -178,11 +178,10 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
///
|
||||
/// The `passphrase` must be at least 2 characters long.
|
||||
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
|
||||
let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
|
||||
passphrase_begin
|
||||
} else {
|
||||
bail!("Passphrase must be at least 2 chars long.");
|
||||
};
|
||||
ensure!(
|
||||
passphrase.len() >= 2,
|
||||
"Passphrase must be at least 2 chars long."
|
||||
);
|
||||
let private_key = SignedSecretKey::load_self(context).await?;
|
||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
|
||||
false => None,
|
||||
@@ -197,7 +196,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
||||
"Passphrase-Format: numeric9x4\r\n",
|
||||
"Passphrase-Begin: {}"
|
||||
),
|
||||
passphrase_begin
|
||||
&passphrase[..2]
|
||||
);
|
||||
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
|
||||
|
||||
@@ -449,33 +448,27 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
|
||||
);
|
||||
|
||||
// Load IDs only for now, without the file contents, to avoid
|
||||
// consuming too much memory.
|
||||
let file_ids = context
|
||||
let files = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM backup_blobs ORDER BY id",
|
||||
"SELECT file_name, file_content FROM backup_blobs ORDER BY id;",
|
||||
paramsv![],
|
||||
|row| row.get(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<i64>, _>>()
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let blob: Vec<u8> = row.get(1)?;
|
||||
|
||||
Ok((name, blob))
|
||||
},
|
||||
|files| {
|
||||
files
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut all_files_extracted = true;
|
||||
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
|
||||
// Load a single blob into memory
|
||||
let (file_name, file_blob) = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
|
||||
paramsv![file_id],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for (processed_files_cnt, (file_name, file_blob)) in files.into_iter().enumerate() {
|
||||
if context.shall_stop_ongoing().await {
|
||||
all_files_extracted = false;
|
||||
break;
|
||||
@@ -783,9 +776,9 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_render_setup_file() {
|
||||
let t = TestContext::new().await;
|
||||
let t = test_context().await;
|
||||
|
||||
t.configure_alice().await;
|
||||
configure_alice_keypair(&t.ctx).await;
|
||||
let msg = render_setup_file(&t.ctx, "hello").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
// Check some substrings, indicating things got substituted.
|
||||
@@ -802,12 +795,12 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_render_setup_file_newline_replace() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
t.ctx
|
||||
.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
t.configure_alice().await;
|
||||
configure_alice_keypair(&t.ctx).await;
|
||||
let msg = render_setup_file(&t.ctx, "pw").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
assert!(msg.contains("<p>hello<br>there</p>"));
|
||||
@@ -815,7 +808,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_setup_code() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let setupcode = create_setup_code(&t.ctx);
|
||||
assert_eq!(setupcode.len(), 44);
|
||||
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
|
||||
@@ -830,7 +823,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_export_key_to_asc_file() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let key = alice_keypair().public;
|
||||
let blobdir = "$BLOBDIR";
|
||||
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
|
||||
|
||||
113
src/job.rs
113
src/job.rs
@@ -21,7 +21,6 @@ use crate::constants::*;
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::load_imap_deletion_msgid;
|
||||
use crate::error::{bail, ensure, format_err, Error, Result};
|
||||
use crate::events::Event;
|
||||
use crate::imap::*;
|
||||
@@ -95,6 +94,7 @@ pub enum Action {
|
||||
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
|
||||
Housekeeping = 105, // low priority ...
|
||||
EmptyServer = 107,
|
||||
OldDeleteMsgOnImap = 110,
|
||||
MarkseenMsgOnImap = 130,
|
||||
|
||||
// Moving message is prioritized lower than deletion so we don't
|
||||
@@ -123,6 +123,7 @@ impl From<Action> for Thread {
|
||||
Unknown => Thread::Unknown,
|
||||
|
||||
Housekeeping => Thread::Imap,
|
||||
OldDeleteMsgOnImap => Thread::Imap,
|
||||
DeleteMsgOnImap => Thread::Imap,
|
||||
EmptyServer => Thread::Imap,
|
||||
MarkseenMsgOnImap => Thread::Imap,
|
||||
@@ -253,48 +254,40 @@ impl Job {
|
||||
|
||||
let res = match err {
|
||||
async_smtp::smtp::error::Error::Permanent(ref response) => {
|
||||
// Workaround for incorrectly configured servers returning permanent errors
|
||||
// instead of temporary ones.
|
||||
let maybe_transient = match response.code {
|
||||
match response.code {
|
||||
// Sometimes servers send a permanent error when actually it is a temporary error
|
||||
// For documentation see https://tools.ietf.org/html/rfc3463
|
||||
|
||||
// Code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
|
||||
Code {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
..
|
||||
} => {
|
||||
// Ignore status code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
|
||||
// Maybe incorrectly configured Postfix milter with "reject" instead of "tempfail", which returns
|
||||
// "550 5.5.0 Service unavailable" instead of "451 4.7.1 Service unavailable - try again later".
|
||||
//
|
||||
// Other enhanced status codes, such as Postfix
|
||||
// "550 5.1.1 <foobar@example.org>: Recipient address rejected: User unknown in local recipient table"
|
||||
// are not ignored.
|
||||
response.message.get(0) == Some(&"5.5.0".to_string())
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
} => Status::RetryLater,
|
||||
|
||||
if maybe_transient {
|
||||
Status::RetryLater
|
||||
} else {
|
||||
// If we do not retry, add an info message to the chat.
|
||||
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
|
||||
// should definitely go here, because user has to open the link to
|
||||
// resume message sending.
|
||||
let msg_id = MsgId::new(self.foreign_id);
|
||||
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(message) => {
|
||||
chat::add_info_msg(context, message.chat_id, err.to_string())
|
||||
_ => {
|
||||
// If we do not retry, add an info message to the chat
|
||||
// Error 5.7.1 should definitely go here: Yandex sends 5.7.1 with a link when it thinks that the email is SPAM.
|
||||
match Message::load_from_db(context, MsgId::new(self.foreign_id))
|
||||
.await
|
||||
{
|
||||
Ok(message) => {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
message.chat_id,
|
||||
err.to_string(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(e) => error!(
|
||||
context,
|
||||
"couldn't load chat_id to inform user about SMTP error: {}", e
|
||||
),
|
||||
};
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
}
|
||||
Err(e) => warn!(
|
||||
context,
|
||||
"couldn't load chat_id to inform user about SMTP error: {}",
|
||||
e
|
||||
),
|
||||
};
|
||||
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
}
|
||||
}
|
||||
}
|
||||
async_smtp::smtp::error::Error::Transient(_) => {
|
||||
@@ -631,9 +624,7 @@ impl Job {
|
||||
}
|
||||
}
|
||||
if self.foreign_id & DC_EMPTY_INBOX > 0 {
|
||||
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
|
||||
imap.empty_folder(context, &inbox_folder).await;
|
||||
}
|
||||
imap.empty_folder(context, "INBOX").await;
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
@@ -647,21 +638,7 @@ impl Job {
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
|
||||
let folder = msg.server_folder.as_ref().unwrap();
|
||||
|
||||
let result = if msg.server_uid == 0 {
|
||||
// The message is moved or deleted by us.
|
||||
//
|
||||
// Do not call set_seen with zero UID, as it will return
|
||||
// ImapActionResult::RetryLater, but we do not want to
|
||||
// retry. If the message was moved, we will create another
|
||||
// job to mark the message as seen later. If it was
|
||||
// deleted, there is nothing to do.
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
imap.set_seen(context, folder, msg.server_uid).await
|
||||
};
|
||||
|
||||
match result {
|
||||
match imap.set_seen(context, folder, msg.server_uid).await {
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
|
||||
ImapActionResult::Success | ImapActionResult::Failed => {
|
||||
@@ -851,6 +828,25 @@ pub(crate) enum Connection<'a> {
|
||||
Smtp(&'a mut Smtp),
|
||||
}
|
||||
|
||||
async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after().await {
|
||||
let threshold_timestamp = time() - delete_server_after;
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs \
|
||||
WHERE timestamp < ? \
|
||||
AND server_uid != 0",
|
||||
paramsv![threshold_timestamp],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
|
||||
let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? {
|
||||
Some(Job::new(
|
||||
@@ -974,6 +970,7 @@ async fn perform_job_action(
|
||||
location::job_maybe_send_locations_ended(context, job).await
|
||||
}
|
||||
Action::EmptyServer => job.empty_server(context, connection.inbox()).await,
|
||||
Action::OldDeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
|
||||
@@ -983,7 +980,10 @@ async fn perform_job_action(
|
||||
}
|
||||
};
|
||||
|
||||
info!(context, "Finished immediate try {} of job {}", tries, job);
|
||||
info!(
|
||||
context,
|
||||
"Inbox finished immediate try {} of job {}", tries, job
|
||||
);
|
||||
|
||||
try_res
|
||||
}
|
||||
@@ -1031,6 +1031,7 @@ pub async fn add(context: &Context, job: Job) {
|
||||
Action::Unknown => unreachable!(),
|
||||
Action::Housekeeping
|
||||
| Action::EmptyServer
|
||||
| Action::OldDeleteMsgOnImap
|
||||
| Action::DeleteMsgOnImap
|
||||
| Action::MarkseenMsgOnImap
|
||||
| Action::MoveMsg => {
|
||||
@@ -1208,7 +1209,7 @@ mod tests {
|
||||
// We want to ensure that loading jobs skips over jobs which
|
||||
// fails to load from the database instead of failing to load
|
||||
// all jobs.
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
insert_job(&t.ctx, -1).await; // This can not be loaded into Job struct.
|
||||
let jobs = load_next(
|
||||
&t.ctx,
|
||||
@@ -1230,7 +1231,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_next_job_one() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
|
||||
insert_job(&t.ctx, 1).await;
|
||||
|
||||
|
||||
16
src/key.rs
16
src/key.rs
@@ -247,7 +247,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||
}),
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let start = std::time::SystemTime::now();
|
||||
let start = std::time::Instant::now();
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
|
||||
.unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keytype);
|
||||
@@ -258,7 +258,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
info!(
|
||||
context,
|
||||
"Keypair generated in {:.3}s.",
|
||||
start.elapsed().unwrap_or_default().as_secs()
|
||||
start.elapsed().as_secs()
|
||||
);
|
||||
Ok(keypair)
|
||||
}
|
||||
@@ -550,8 +550,8 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[async_std::test]
|
||||
async fn test_load_self_existing() {
|
||||
let alice = alice_keypair();
|
||||
let t = TestContext::new().await;
|
||||
t.configure_alice().await;
|
||||
let t = dummy_context().await;
|
||||
configure_alice_keypair(&t.ctx).await;
|
||||
let pubkey = SignedPublicKey::load_self(&t.ctx).await.unwrap();
|
||||
assert_eq!(alice.public, pubkey);
|
||||
let seckey = SignedSecretKey::load_self(&t.ctx).await.unwrap();
|
||||
@@ -560,7 +560,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_public() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.await
|
||||
@@ -571,7 +571,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_secret() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.await
|
||||
@@ -584,7 +584,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
async fn test_load_self_generate_concurrent() {
|
||||
use std::thread;
|
||||
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.await
|
||||
@@ -611,7 +611,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
async fn test_save_self_key_twice() {
|
||||
// Saving the same key twice should result in only one row in
|
||||
// the keypairs table.
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let ctx = Arc::new(t.ctx);
|
||||
|
||||
let ctx1 = ctx.clone();
|
||||
|
||||
@@ -79,8 +79,8 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_keyring_load_self() {
|
||||
// new_self() implies load_self()
|
||||
let t = TestContext::new().await;
|
||||
t.configure_alice().await;
|
||||
let t = dummy_context().await;
|
||||
configure_alice_keypair(&t.ctx).await;
|
||||
let alice = alice_keypair();
|
||||
|
||||
let pub_ring: Keyring<SignedPublicKey> = Keyring::new_self(&t.ctx).await.unwrap();
|
||||
|
||||
10
src/lib.rs
10
src/lib.rs
@@ -1,11 +1,6 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(
|
||||
clippy::correctness,
|
||||
missing_debug_implementations,
|
||||
clippy::all,
|
||||
clippy::indexing_slicing
|
||||
)]
|
||||
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
|
||||
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
|
||||
#![allow(clippy::match_bool)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate num_derive;
|
||||
@@ -48,7 +43,6 @@ pub mod constants;
|
||||
pub mod contact;
|
||||
pub mod context;
|
||||
mod e2ee;
|
||||
pub mod ephemeral;
|
||||
mod imap;
|
||||
pub mod imex;
|
||||
mod scheduler;
|
||||
|
||||
@@ -530,7 +530,7 @@ pub async fn save(
|
||||
accuracy,
|
||||
..
|
||||
} = location;
|
||||
let (loc_id, ts) = context
|
||||
context
|
||||
.sql
|
||||
.with_conn(move |mut conn| {
|
||||
let mut stmt_test = conn
|
||||
@@ -569,11 +569,9 @@ pub async fn save(
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok((newest_location_id, newest_timestamp))
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
newest_timestamp = ts;
|
||||
newest_location_id = loc_id;
|
||||
}
|
||||
|
||||
Ok(newest_location_id)
|
||||
@@ -724,11 +722,11 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::dummy_context;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_kml_parse() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
|
||||
let xml =
|
||||
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";
|
||||
|
||||
11
src/log.rs
11
src/log.rs
@@ -41,17 +41,6 @@ macro_rules! error {
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error_network {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
error_network!($ctx, $msg,)
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
emit_event!($ctx, $crate::Event::ErrorNetwork(formatted));
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! emit_event {
|
||||
($ctx:expr, $event:expr) => {
|
||||
|
||||
148
src/message.rs
148
src/message.rs
@@ -6,7 +6,6 @@ use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::*;
|
||||
@@ -69,38 +68,18 @@ impl MsgId {
|
||||
self.0 == 0
|
||||
}
|
||||
|
||||
/// Returns message state.
|
||||
pub async fn get_state(self, context: &Context) -> crate::sql::Result<MessageState> {
|
||||
let result = context
|
||||
.sql
|
||||
.query_get_value_result("SELECT state FROM msgs WHERE id=?", paramsv![self])
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
Ok(result)
|
||||
/// Whether the message ID is the special marker1 marker.
|
||||
///
|
||||
/// See the docs of the `dc_get_chat_msgs` C API for details.
|
||||
pub fn is_marker1(self) -> bool {
|
||||
self.0 == DC_MSG_ID_MARKER1
|
||||
}
|
||||
|
||||
/// Returns true if the message needs to be moved from `folder`.
|
||||
pub async fn needs_move(self, context: &Context, folder: &str) -> Result<bool, Error> {
|
||||
if !context.get_config_bool(Config::MvboxMove).await {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if context.is_mvbox(folder).await {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
|
||||
if msg.is_setupmessage() {
|
||||
// do not move setup messages;
|
||||
// there may be a non-delta device that wants to handle it
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match msg.is_dc_message {
|
||||
MessengerMessage::No => Ok(false),
|
||||
MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
|
||||
}
|
||||
/// Whether the message ID is the special day marker.
|
||||
///
|
||||
/// See the docs of the `dc_get_chat_msgs` C API for details.
|
||||
pub fn is_daymarker(self) -> bool {
|
||||
self.0 == DC_MSG_ID_DAYMARKER
|
||||
}
|
||||
|
||||
/// Put message into trash chat and delete message text.
|
||||
@@ -164,7 +143,16 @@ impl MsgId {
|
||||
|
||||
impl std::fmt::Display for MsgId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Msg#{}", self.0)
|
||||
// Would be nice if we could use match here, but no computed values in ranges.
|
||||
if self.0 == DC_MSG_ID_MARKER1 {
|
||||
write!(f, "Msg#Marker1")
|
||||
} else if self.0 == DC_MSG_ID_DAYMARKER {
|
||||
write!(f, "Msg#DayMarker")
|
||||
} else if self.0 <= DC_MSG_ID_LAST_SPECIAL {
|
||||
write!(f, "Msg#UnknownSpecial")
|
||||
} else {
|
||||
write!(f, "Msg#{}", self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,8 +246,6 @@ pub struct Message {
|
||||
pub(crate) timestamp_sort: i64,
|
||||
pub(crate) timestamp_sent: i64,
|
||||
pub(crate) timestamp_rcvd: i64,
|
||||
pub(crate) ephemeral_timer: u32,
|
||||
pub(crate) ephemeral_timestamp: i64,
|
||||
pub(crate) text: Option<String>,
|
||||
pub(crate) rfc724_mid: String,
|
||||
pub(crate) in_reply_to: Option<String>,
|
||||
@@ -302,8 +288,6 @@ impl Message {
|
||||
" m.timestamp AS timestamp,",
|
||||
" m.timestamp_sent AS timestamp_sent,",
|
||||
" m.timestamp_rcvd AS timestamp_rcvd,",
|
||||
" m.ephemeral_timer AS ephemeral_timer,",
|
||||
" m.ephemeral_timestamp AS ephemeral_timestamp,",
|
||||
" m.type AS type,",
|
||||
" m.state AS state,",
|
||||
" m.error AS error,",
|
||||
@@ -332,8 +316,6 @@ impl Message {
|
||||
msg.timestamp_sort = row.get("timestamp")?;
|
||||
msg.timestamp_sent = row.get("timestamp_sent")?;
|
||||
msg.timestamp_rcvd = row.get("timestamp_rcvd")?;
|
||||
msg.ephemeral_timer = row.get("ephemeral_timer")?;
|
||||
msg.ephemeral_timestamp = row.get("ephemeral_timestamp")?;
|
||||
msg.viewtype = row.get("type")?;
|
||||
msg.state = row.get("state")?;
|
||||
msg.error = row.get("error")?;
|
||||
@@ -528,14 +510,6 @@ impl Message {
|
||||
self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
|
||||
}
|
||||
|
||||
pub fn get_ephemeral_timer(&self) -> u32 {
|
||||
self.ephemeral_timer
|
||||
}
|
||||
|
||||
pub fn get_ephemeral_timestamp(&self) -> i64 {
|
||||
self.ephemeral_timestamp
|
||||
}
|
||||
|
||||
pub async fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Lot {
|
||||
let mut ret = Lot::new();
|
||||
|
||||
@@ -638,39 +612,6 @@ impl Message {
|
||||
None
|
||||
}
|
||||
|
||||
/// split a webrtc_instance as defined by the corresponding config-value into a type and a url
|
||||
pub fn parse_webrtc_instance(instance: &str) -> (VideochatType, String) {
|
||||
let mut split = instance.splitn(2, ':');
|
||||
let type_str = split.next().unwrap_or_default().to_lowercase();
|
||||
let url = split.next();
|
||||
if type_str == "basicwebrtc" {
|
||||
(
|
||||
VideochatType::BasicWebrtc,
|
||||
url.unwrap_or_default().to_string(),
|
||||
)
|
||||
} else {
|
||||
(VideochatType::Unknown, instance.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_videochat_url(&self) -> Option<String> {
|
||||
if self.viewtype == Viewtype::VideochatInvitation {
|
||||
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
|
||||
return Some(Message::parse_webrtc_instance(instance).1);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_videochat_type(&self) -> Option<VideochatType> {
|
||||
if self.viewtype == Viewtype::VideochatInvitation {
|
||||
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
|
||||
return Some(Message::parse_webrtc_instance(instance).0);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: Option<String>) {
|
||||
self.text = text;
|
||||
}
|
||||
@@ -950,17 +891,6 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
|
||||
ret += "\n";
|
||||
}
|
||||
|
||||
if msg.ephemeral_timer != 0 {
|
||||
ret += &format!("Ephemeral timer: {}\n", msg.ephemeral_timer);
|
||||
}
|
||||
|
||||
if msg.ephemeral_timestamp != 0 {
|
||||
ret += &format!(
|
||||
"Expires: {}\n",
|
||||
dc_timestamp_to_str(msg.ephemeral_timestamp)
|
||||
);
|
||||
}
|
||||
|
||||
if msg.from_id == DC_CONTACT_ID_INFO || msg.to_id == DC_CONTACT_ID_INFO {
|
||||
// device-internal message, no further details needed
|
||||
return ret;
|
||||
@@ -1166,14 +1096,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
|
||||
let mut send_event = false;
|
||||
|
||||
for (id, curr_state, curr_blocked) in msgs.into_iter() {
|
||||
if let Err(err) = id.start_ephemeral_timer(context).await {
|
||||
error!(
|
||||
context,
|
||||
"Failed to start ephemeral timer for message {}: {}", id, err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if curr_blocked == Blocked::Not {
|
||||
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
|
||||
update_msg_state(context, id, MessageState::InSeen).await;
|
||||
@@ -1230,7 +1152,7 @@ pub async fn star_msgs(context: &Context, msg_ids: Vec<MsgId>, star: bool) -> bo
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Returns a summary text.
|
||||
/// Returns a summary test.
|
||||
pub async fn get_summarytext_by_raw(
|
||||
viewtype: Viewtype,
|
||||
text: Option<impl AsRef<str>>,
|
||||
@@ -1274,13 +1196,6 @@ pub async fn get_summarytext_by_raw(
|
||||
format!("{} – {}", label, file_name)
|
||||
}
|
||||
}
|
||||
Viewtype::VideochatInvitation => {
|
||||
append_text = false;
|
||||
context
|
||||
.stock_str(StockMessage::VideochatInvitation)
|
||||
.await
|
||||
.into_owned()
|
||||
}
|
||||
_ => {
|
||||
if param.get_cmd() != SystemMessage::LocationOnly {
|
||||
"".to_string()
|
||||
@@ -1378,7 +1293,7 @@ pub async fn handle_mdn(
|
||||
rfc724_mid: &str,
|
||||
timestamp_sent: i64,
|
||||
) -> Option<(ChatId, MsgId)> {
|
||||
if from_id <= DC_CONTACT_ID_LAST_SPECIAL || rfc724_mid.is_empty() {
|
||||
if from_id <= DC_MSG_ID_LAST_SPECIAL || rfc724_mid.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1730,7 +1645,7 @@ mod tests {
|
||||
async fn test_prepare_message_and_send() {
|
||||
use crate::config::Config;
|
||||
|
||||
let d = test::TestContext::new().await;
|
||||
let d = test::dummy_context().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
let contact = Contact::create(ctx, "", "dest@example.com")
|
||||
@@ -1754,7 +1669,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_summarytext_by_raw() {
|
||||
let d = test::TestContext::new().await;
|
||||
let d = test::dummy_context().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
let some_text = Some("bla bla".to_string());
|
||||
@@ -1839,19 +1754,4 @@ mod tests {
|
||||
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_webrtc_instance() {
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
|
||||
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
|
||||
assert_eq!(url, "https://foo/bar");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("bAsIcwEbrTc:url");
|
||||
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
|
||||
assert_eq!(url, "url");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("https://foo/bar?key=val#key=val");
|
||||
assert_eq!(webrtc_type, VideochatType::Unknown);
|
||||
assert_eq!(url, "https://foo/bar?key=val#key=val");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::contact::*;
|
||||
use crate::context::{get_version_str, Context};
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee::*;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::location;
|
||||
use crate::message::{self, Message};
|
||||
@@ -499,16 +498,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let e2ee_guaranteed = self.is_e2ee_guaranteed();
|
||||
let encrypt_helper = EncryptHelper::new(self.context).await?;
|
||||
|
||||
let subject = if subject_str
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
|
||||
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
|
||||
// but we do not want to encode all subjects just because they contain a space.
|
||||
{
|
||||
subject_str
|
||||
} else {
|
||||
encode_words(&subject_str)
|
||||
};
|
||||
let subject = encode_words(&subject_str);
|
||||
|
||||
let mut message = match self.loaded {
|
||||
Loaded::Message { .. } => {
|
||||
@@ -536,14 +526,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
Loaded::MDN { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
|
||||
};
|
||||
|
||||
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(self.context).await?;
|
||||
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
|
||||
protected_headers.push(Header::new(
|
||||
"Ephemeral-Timer".to_string(),
|
||||
duration.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// we could also store the message-id in the protected headers
|
||||
// which would probably help to survive providers like
|
||||
// Outlook.com or hotmail which mangle the Message-ID.
|
||||
@@ -794,26 +776,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
"location-streaming-enabled".into(),
|
||||
));
|
||||
}
|
||||
SystemMessage::EphemeralTimerChanged => {
|
||||
protected_headers.push(Header::new(
|
||||
"Chat-Content".to_string(),
|
||||
"ephemeral-timer-changed".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::LocationOnly => {
|
||||
// This should prevent automatic replies,
|
||||
// such as non-delivery reports.
|
||||
//
|
||||
// See https://tools.ietf.org/html/rfc3834
|
||||
//
|
||||
// Adding this header without encryption leaks some
|
||||
// information about the message contents, but it can
|
||||
// already be easily guessed from message timing and size.
|
||||
unprotected_headers.push(Header::new(
|
||||
"Auto-Submitted".to_string(),
|
||||
"auto-generated".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::AutocryptSetupMessage => {
|
||||
unprotected_headers
|
||||
.push(Header::new("Autocrypt-Setup-Message".into(), "v1".into()));
|
||||
@@ -875,19 +837,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
|
||||
if self.msg.viewtype == Viewtype::Sticker {
|
||||
protected_headers.push(Header::new("Chat-Content".into(), "sticker".into()));
|
||||
} else if self.msg.viewtype == Viewtype::VideochatInvitation {
|
||||
protected_headers.push(Header::new(
|
||||
"Chat-Content".into(),
|
||||
"videochat-invitation".into(),
|
||||
));
|
||||
protected_headers.push(Header::new(
|
||||
"Chat-Webrtc-Room".into(),
|
||||
self.msg
|
||||
.param
|
||||
.get(Param::WebrtcRoom)
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
if self.msg.viewtype == Viewtype::Voice
|
||||
@@ -1261,6 +1210,7 @@ mod tests {
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::mimeparser::*;
|
||||
use crate::test_utils::configured_offline_context;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
@@ -1350,10 +1300,10 @@ mod tests {
|
||||
// 1.: Receive a mail from an MUA or Delta Chat
|
||||
assert_eq!(
|
||||
msg_to_subject_str(
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
b"From: Bob <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Antw: Chat: hello\n\
|
||||
Message-ID: <2222@example.com>\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
@@ -1364,10 +1314,10 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
msg_to_subject_str(
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
b"From: Bob <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Infos: 42\n\
|
||||
Message-ID: <2222@example.com>\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
@@ -1379,11 +1329,11 @@ mod tests {
|
||||
// 2. Receive a message from Delta Chat when we did not send any messages before
|
||||
assert_eq!(
|
||||
msg_to_subject_str(
|
||||
b"From: Charlie <charlie@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
b"From: Charlie <charlie@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Chat: hello\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2223@example.com>\n\
|
||||
Message-ID: <2223@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
@@ -1393,11 +1343,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// 3. Send the first message to a new contact
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
assert_eq!(first_subject_str(t).await, "Message from alice@example.org");
|
||||
|
||||
assert_eq!(first_subject_str(t).await, "Message from alice@example.com");
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
t.ctx
|
||||
.set_config(Config::Displayname, Some("Alice"))
|
||||
.await
|
||||
@@ -1406,11 +1355,11 @@ mod tests {
|
||||
|
||||
// 4. Receive messages with unicode characters and make sure that we do not panic (we do not care about the result)
|
||||
msg_to_subject_str(
|
||||
"From: Charlie <charlie@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
"From: Charlie <charlie@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: äääää\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
Message-ID: <2893@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
@@ -1419,70 +1368,22 @@ mod tests {
|
||||
.await;
|
||||
|
||||
msg_to_subject_str(
|
||||
"From: Charlie <charlie@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
"From: Charlie <charlie@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: aäääää\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
Message-ID: <2893@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
.as_bytes(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// 5. Receive an mdn (read receipt) and make sure the mdn's subject is not used
|
||||
let t = TestContext::new_alice().await;
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
b"From: alice@example.com\n\
|
||||
To: Charlie <charlie@example.com>\n\
|
||||
Subject: Hello, Charlie\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let new_msg = incoming_msg_to_reply_msg(b"From: charlie@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: message opened\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <Mr.12345678902@example.com>\n\
|
||||
Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\
|
||||
\n\
|
||||
\n\
|
||||
--SNIPP\n\
|
||||
Content-Type: text/plain; charset=utf-8\n\
|
||||
\n\
|
||||
Read receipts do not guarantee sth. was read.\n\
|
||||
\n\
|
||||
\n\
|
||||
--SNIPP\n\
|
||||
Content-Type: message/disposition-notification\n\
|
||||
\n\
|
||||
Reporting-UA: Delta Chat 1.28.0\n\
|
||||
Original-Recipient: rfc822;charlie@example.com\n\
|
||||
Final-Recipient: rfc822;charlie@example.com\n\
|
||||
Original-Message-ID: <2893@example.com>\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
\n", &t.ctx).await;
|
||||
let mf = MimeFactory::from_msg(&t.ctx, &new_msg, false)
|
||||
.await
|
||||
.unwrap();
|
||||
// The subject string should not be "Re: message opened"
|
||||
assert_eq!("Re: Hello, Charlie", mf.subject_str().await);
|
||||
}
|
||||
|
||||
async fn first_subject_str(t: TestContext) -> String {
|
||||
let contact_id =
|
||||
Contact::add_or_lookup(&t.ctx, "Dave", "dave@example.com", Origin::ManuallyCreated)
|
||||
Contact::add_or_lookup(&t.ctx, "Dave", "dave@example.org", Origin::ManuallyCreated)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
@@ -1506,7 +1407,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn msg_to_subject_str(imf_raw: &[u8]) -> String {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
let new_msg = incoming_msg_to_reply_msg(imf_raw, &t.ctx).await;
|
||||
let mf = MimeFactory::from_msg(&t.ctx, &new_msg, false)
|
||||
.await
|
||||
@@ -1514,7 +1415,7 @@ mod tests {
|
||||
mf.subject_str().await
|
||||
}
|
||||
|
||||
// Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`.
|
||||
// Creates a mimefactory for a message that replies "Hi" to the incoming message in `imf_raw`.
|
||||
async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message {
|
||||
context
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
@@ -1544,15 +1445,15 @@ mod tests {
|
||||
#[async_std::test]
|
||||
// This test could still be extended
|
||||
async fn test_render_reply() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = configured_offline_context().await;
|
||||
let context = &t.ctx;
|
||||
|
||||
let msg = incoming_msg_to_reply_msg(
|
||||
b"From: Charlie <charlie@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
b"From: Charlie <charlie@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Chat: hello\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2223@example.com>\n\
|
||||
Message-ID: <2223@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
@@ -1563,7 +1464,7 @@ mod tests {
|
||||
let mimefactory = MimeFactory::from_msg(&t.ctx, &msg, false).await.unwrap();
|
||||
|
||||
let recipients = mimefactory.recipients();
|
||||
assert_eq!(recipients, vec!["charlie@example.com"]);
|
||||
assert_eq!(recipients, vec!["charlie@example.org"]);
|
||||
|
||||
let rendered_msg = mimefactory.render().await.unwrap();
|
||||
|
||||
|
||||
@@ -46,14 +46,7 @@ pub struct MimeMessage {
|
||||
pub from: Vec<SingleInfo>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub decrypting_failed: bool,
|
||||
|
||||
/// Set of valid signature fingerprints if a message is an
|
||||
/// Autocrypt encrypted and signed message.
|
||||
///
|
||||
/// If a message is not encrypted or the signature is not valid,
|
||||
/// this set is empty.
|
||||
pub signatures: HashSet<Fingerprint>,
|
||||
|
||||
pub gossipped_addr: HashSet<String>,
|
||||
pub is_forwarded: bool,
|
||||
pub is_system_message: SystemMessage,
|
||||
@@ -83,9 +76,6 @@ pub enum SystemMessage {
|
||||
SecurejoinMessage = 7,
|
||||
LocationStreamingEnabled = 8,
|
||||
LocationOnly = 9,
|
||||
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged = 10,
|
||||
}
|
||||
|
||||
impl Default for SystemMessage {
|
||||
@@ -224,8 +214,6 @@ impl MimeMessage {
|
||||
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
|
||||
if value == "location-streaming-enabled" {
|
||||
self.is_system_message = SystemMessage::LocationStreamingEnabled;
|
||||
} else if value == "ephemeral-timer-changed" {
|
||||
self.is_system_message = SystemMessage::EphemeralTimerChanged;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -242,24 +230,10 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_videochat_headers(&mut self) {
|
||||
if let Some(value) = self.get(HeaderDef::ChatContent).cloned() {
|
||||
if value == "videochat-invitation" {
|
||||
let instance = self.get(HeaderDef::ChatWebrtcRoom).cloned();
|
||||
if let Some(part) = self.parts.first_mut() {
|
||||
part.typ = Viewtype::VideochatInvitation;
|
||||
part.param
|
||||
.set(Param::WebrtcRoom, instance.unwrap_or_default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Squashes mutlipart chat messages with attachment into single-part messages.
|
||||
///
|
||||
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
|
||||
/// containing an explanation. If such a message is detected, first part can be safely dropped.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn squash_attachment_parts(&mut self) {
|
||||
if let [textpart, filepart] = &self.parts[..] {
|
||||
let need_drop = {
|
||||
@@ -283,8 +257,7 @@ impl MimeMessage {
|
||||
self.parts[0].msg = "".to_string();
|
||||
|
||||
// swap new with old
|
||||
self.parts.push(filepart); // push to the end
|
||||
let _ = self.parts.swap_remove(0); // drops first element, replacing it with the last one in O(1)
|
||||
std::mem::replace(&mut self.parts[0], filepart);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,21 +266,22 @@ impl MimeMessage {
|
||||
fn parse_attachments(&mut self) {
|
||||
// Attachment messages should be squashed into a single part
|
||||
// before calling this function.
|
||||
if self.parts.len() != 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(mut part) = self.parts.pop() {
|
||||
if part.typ == Viewtype::Audio && self.get(HeaderDef::ChatVoiceMessage).is_some() {
|
||||
part.typ = Viewtype::Voice;
|
||||
if self.parts.len() == 1 {
|
||||
if self.parts[0].typ == Viewtype::Audio
|
||||
&& self.get(HeaderDef::ChatVoiceMessage).is_some()
|
||||
{
|
||||
let part_mut = &mut self.parts[0];
|
||||
part_mut.typ = Viewtype::Voice;
|
||||
}
|
||||
if part.typ == Viewtype::Image {
|
||||
if self.parts[0].typ == Viewtype::Image {
|
||||
if let Some(value) = self.get(HeaderDef::ChatContent) {
|
||||
if value == "sticker" {
|
||||
part.typ = Viewtype::Sticker;
|
||||
let part_mut = &mut self.parts[0];
|
||||
part_mut.typ = Viewtype::Sticker;
|
||||
}
|
||||
}
|
||||
}
|
||||
let part = &self.parts[0];
|
||||
if part.typ == Viewtype::Audio
|
||||
|| part.typ == Viewtype::Voice
|
||||
|| part.typ == Viewtype::Video
|
||||
@@ -315,19 +289,17 @@ impl MimeMessage {
|
||||
if let Some(field_0) = self.get(HeaderDef::ChatDuration) {
|
||||
let duration_ms = field_0.parse().unwrap_or_default();
|
||||
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
|
||||
part.param.set_int(Param::Duration, duration_ms);
|
||||
let part_mut = &mut self.parts[0];
|
||||
part_mut.param.set_int(Param::Duration, duration_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_headers(&mut self, context: &Context) -> Result<()> {
|
||||
self.parse_system_message_headers(context)?;
|
||||
self.parse_avatar_headers();
|
||||
self.parse_videochat_headers();
|
||||
self.squash_attachment_parts();
|
||||
|
||||
if let Some(ref subject) = self.get_subject() {
|
||||
@@ -343,11 +315,12 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
if prepend_subject {
|
||||
let subj = subject
|
||||
.find('[')
|
||||
.and_then(|n| subject.get(..n))
|
||||
.unwrap_or(subject)
|
||||
.trim();
|
||||
let subj = if let Some(n) = subject.find('[') {
|
||||
&subject[0..n]
|
||||
} else {
|
||||
subject
|
||||
}
|
||||
.trim();
|
||||
|
||||
if !subj.is_empty() {
|
||||
for part in self.parts.iter_mut() {
|
||||
@@ -405,7 +378,8 @@ impl MimeMessage {
|
||||
Some(AvatarAction::Delete)
|
||||
} else {
|
||||
let mut i = 0;
|
||||
while let Some(part) = self.parts.get_mut(i) {
|
||||
while i != self.parts.len() {
|
||||
let part = &mut self.parts[i];
|
||||
if let Some(part_filename) = &part.org_filename {
|
||||
if part_filename == &header_value {
|
||||
if let Some(blob) = part.param.get(Param::File) {
|
||||
@@ -422,11 +396,6 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the message was encrypted as defined in
|
||||
/// Autocrypt standard.
|
||||
///
|
||||
/// This means the message was both encrypted and signed with a
|
||||
/// valid signature.
|
||||
pub fn was_encrypted(&self) -> bool {
|
||||
!self.signatures.is_empty()
|
||||
}
|
||||
@@ -574,7 +543,7 @@ impl MimeMessage {
|
||||
contains exactly two body parts. The first body
|
||||
part is the body part over which the digital signature was created [...]
|
||||
The second body part contains the control information necessary to
|
||||
verify the digital signature." We simply take the first body part and
|
||||
verify the digital signature." We simpliy take the first body part and
|
||||
skip the rest. (see
|
||||
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
|
||||
for background information why we use encrypted+signed) */
|
||||
@@ -795,11 +764,16 @@ impl MimeMessage {
|
||||
}
|
||||
|
||||
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
|
||||
if let Some(part) = self.parts.first_mut() {
|
||||
part.typ = Viewtype::Text;
|
||||
part.msg = format!("[{}]", error_msg.as_ref());
|
||||
self.parts.truncate(1);
|
||||
if self.parts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let part = &mut self.parts[0];
|
||||
part.typ = Viewtype::Text;
|
||||
part.msg = format!("[{}]", error_msg.as_ref());
|
||||
self.parts.truncate(1);
|
||||
|
||||
assert_eq!(self.parts.len(), 1);
|
||||
}
|
||||
|
||||
pub fn get_rfc724_mid(&self) -> Option<String> {
|
||||
@@ -850,11 +824,7 @@ impl MimeMessage {
|
||||
report: &mailparse::ParsedMail<'_>,
|
||||
) -> Result<Option<Report>> {
|
||||
// parse as mailheaders
|
||||
let report_body = if let Some(subpart) = report.subparts.get(1) {
|
||||
subpart.get_body_raw()?
|
||||
} else {
|
||||
bail!("Report does not have second MIME part");
|
||||
};
|
||||
let report_body = report.subparts[1].get_body_raw()?;
|
||||
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
||||
|
||||
// must be present
|
||||
@@ -932,15 +902,17 @@ impl MimeMessage {
|
||||
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
|
||||
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
|
||||
/// Also you should add a test in dc_receive_imf.rs (there already are lots of test_parse_ndn_* tests).
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn heuristically_parse_ndn(&mut self, context: &Context) -> Option<()> {
|
||||
let maybe_ndn = if let Some(from) = self.get(HeaderDef::From_) {
|
||||
let from = from.to_ascii_lowercase();
|
||||
from.contains("mailer-daemon") || from.contains("mail-daemon")
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if maybe_ndn && self.failure_report.is_none() {
|
||||
if self
|
||||
.get(HeaderDef::Subject)?
|
||||
.to_ascii_lowercase()
|
||||
.contains("fail")
|
||||
&& self
|
||||
.get(HeaderDef::From_)?
|
||||
.to_ascii_lowercase()
|
||||
.contains("mailer-daemon")
|
||||
&& self.failure_report.is_none()
|
||||
{
|
||||
lazy_static! {
|
||||
static ref RE: regex::Regex = regex::Regex::new(r"Message-ID:(.*)").unwrap();
|
||||
}
|
||||
@@ -991,10 +963,11 @@ impl MimeMessage {
|
||||
if let Some(failure_report) = &self.failure_report {
|
||||
let error = parts.iter().find(|p| p.typ == Viewtype::Text).map(|p| {
|
||||
let msg = &p.msg;
|
||||
msg.find("\n--- ")
|
||||
.and_then(|footer_start| msg.get(..footer_start))
|
||||
.unwrap_or(msg)
|
||||
.trim()
|
||||
match msg.find("\n--- ") {
|
||||
Some(footer_start) => &msg[..footer_start],
|
||||
None => msg,
|
||||
}
|
||||
.trim()
|
||||
});
|
||||
message::handle_ndn(context, failure_report, error).await
|
||||
}
|
||||
@@ -1060,7 +1033,6 @@ pub(crate) struct FailureReport {
|
||||
pub failed_recipient: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn parse_message_ids(ids: &str) -> Result<Vec<String>> {
|
||||
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
|
||||
let mut msgids = Vec::new();
|
||||
@@ -1267,7 +1239,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_dc_mimeparser_crash() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = include_bytes!("../test-data/message/issue_523.txt");
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
@@ -1279,7 +1251,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_rfc724_mid_exists() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = include_bytes!("../test-data/message/mail_with_message_id.txt");
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
@@ -1293,7 +1265,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_rfc724_mid_not_exists() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = include_bytes!("../test-data/message/issue_523.txt");
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
@@ -1351,7 +1323,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_first_addr() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = b"From: hello@one.org, world@two.org\n\
|
||||
Chat-Disposition-Notification-To: wrong\n\
|
||||
Content-Type: text/plain\n\
|
||||
@@ -1372,7 +1344,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_mimeparser_with_context() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = b"From: hello\n\
|
||||
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
|
||||
Subject: outer-subject\n\
|
||||
@@ -1422,7 +1394,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_mimeparser_with_avatars() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
|
||||
let raw = include_bytes!("../test-data/message/mail_attach_txt.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
|
||||
@@ -1463,31 +1435,9 @@ mod tests {
|
||||
assert!(mimeparser.group_avatar.unwrap().is_change());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_mimeparser_with_videochat() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let raw = include_bytes!("../test-data/message/videochat_invitation.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::VideochatInvitation);
|
||||
assert_eq!(
|
||||
mimeparser.parts[0]
|
||||
.param
|
||||
.get(Param::WebrtcRoom)
|
||||
.unwrap_or_default(),
|
||||
"https://example.org/p2p/?roomname=6HiduoAn4xN"
|
||||
);
|
||||
assert!(mimeparser.parts[0]
|
||||
.msg
|
||||
.contains("https://example.org/p2p/?roomname=6HiduoAn4xN"));
|
||||
assert_eq!(mimeparser.user_avatar, None);
|
||||
assert_eq!(mimeparser.group_avatar, None);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_mimeparser_message_kml() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = b"Chat-Version: 1.0\n\
|
||||
From: foo <foo@example.org>\n\
|
||||
To: bar <bar@example.org>\n\
|
||||
@@ -1532,7 +1482,7 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_mdn() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
|
||||
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -1582,7 +1532,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
/// multipart MIME messages.
|
||||
#[async_std::test]
|
||||
async fn test_parse_multiple_mdns() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
|
||||
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -1658,7 +1608,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_mdn_with_additional_message_ids() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
|
||||
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -1713,7 +1663,7 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_inline_attachment() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC)
|
||||
From: sender@example.com
|
||||
To: receiver@example.com
|
||||
@@ -1753,7 +1703,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
|
||||
|
||||
#[async_std::test]
|
||||
async fn parse_inline_image() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = br#"Message-ID: <foobar@example.org>
|
||||
From: foo <foo@example.org>
|
||||
Subject: example
|
||||
@@ -1799,7 +1749,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||
|
||||
#[async_std::test]
|
||||
async fn parse_thunderbird_html_embedded_image() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = br#"To: Alice <alice@example.org>
|
||||
From: Bob <bob@example.org>
|
||||
Subject: Test subject
|
||||
@@ -1872,7 +1822,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||
// Outlook specifies filename in the "name" attribute of Content-Type
|
||||
#[async_std::test]
|
||||
async fn parse_outlook_html_embedded_image() {
|
||||
let context = TestContext::new().await;
|
||||
let context = dummy_context().await;
|
||||
let raw = br##"From: Anonymous <anonymous@example.org>
|
||||
To: Anonymous <anonymous@example.org>
|
||||
Subject: Delta Chat is great stuff!
|
||||
|
||||
@@ -9,8 +9,6 @@ use serde::Deserialize;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::provider;
|
||||
use crate::provider::Oauth2Authorizer;
|
||||
|
||||
const OAUTH2_GMAIL: Oauth2 = Oauth2 {
|
||||
// see https://developers.google.com/identity/protocols/OAuth2InstalledApp
|
||||
@@ -278,12 +276,8 @@ impl Oauth2 {
|
||||
.find('@')
|
||||
.map(|index| addr_normalized.split_at(index + 1).1)
|
||||
{
|
||||
if let Some(provider) = provider::get_provider_info(&addr_normalized) {
|
||||
match &provider.oauth2_authorizer {
|
||||
Some(Oauth2Authorizer::Gmail) => Some(OAUTH2_GMAIL),
|
||||
Some(Oauth2Authorizer::Yandex) => Some(OAUTH2_YANDEX),
|
||||
None => None, // provider known to not support oauth2, no mx-lookup required
|
||||
}
|
||||
if let Some(provider) = Oauth2::lookup_whitelist(domain) {
|
||||
Some(provider)
|
||||
} else {
|
||||
Oauth2::lookup_mx(domain).await
|
||||
}
|
||||
@@ -292,6 +286,16 @@ impl Oauth2 {
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_whitelist(domain: impl AsRef<str>) -> Option<Self> {
|
||||
let domain = domain.as_ref();
|
||||
match domain {
|
||||
"gmail.com" | "googlemail.com" => Some(OAUTH2_GMAIL),
|
||||
"yandex.com" | "yandex.by" | "yandex.kz" | "yandex.ru" | "yandex.ua" | "ya.ru"
|
||||
| "narod.ru" => Some(OAUTH2_YANDEX),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn lookup_mx(domain: impl AsRef<str>) -> Option<Self> {
|
||||
if let Ok(resolver) = resolver(
|
||||
config::ResolverConfig::default(),
|
||||
@@ -436,7 +440,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_dc_get_oauth2_addr() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let code = "fail";
|
||||
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await;
|
||||
@@ -446,7 +450,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_dc_get_oauth2_url() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let redirect_uri = "chat.delta:/com.b44t.messenger";
|
||||
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri).await;
|
||||
@@ -456,7 +460,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_dc_get_oauth2_token() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let code = "fail";
|
||||
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await;
|
||||
|
||||
11
src/param.rs
11
src/param.rs
@@ -68,9 +68,6 @@ pub enum Param {
|
||||
/// For Messages
|
||||
AttachGroupImage = b'A',
|
||||
|
||||
/// For Messages
|
||||
WebrtcRoom = b'V',
|
||||
|
||||
/// For Messages: space-separated list of messaged IDs of forwarded copies.
|
||||
///
|
||||
/// This is used when a [crate::message::Message] is in the
|
||||
@@ -174,7 +171,7 @@ impl str::FromStr for Params {
|
||||
let key = key.unwrap_or_default().trim();
|
||||
let value = value.unwrap_or_default().trim();
|
||||
|
||||
if let Some(key) = key.as_bytes().first().and_then(|key| Param::from_u8(*key)) {
|
||||
if let Some(key) = Param::from_u8(key.as_bytes()[0]) {
|
||||
inner.insert(key, value.to_string());
|
||||
} else {
|
||||
bail!("Unknown key: {}", key);
|
||||
@@ -417,7 +414,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_params_file_fs_path() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
if let ParamsFile::FsPath(p) = ParamsFile::from_param(&t.ctx, "/foo/bar/baz").unwrap() {
|
||||
assert_eq!(p, Path::new("/foo/bar/baz"));
|
||||
} else {
|
||||
@@ -427,7 +424,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_params_file_blob() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
if let ParamsFile::Blob(b) = ParamsFile::from_param(&t.ctx, "$BLOBDIR/foo").unwrap() {
|
||||
assert_eq!(b.as_name(), "$BLOBDIR/foo");
|
||||
} else {
|
||||
@@ -438,7 +435,7 @@ mod tests {
|
||||
// Tests for Params::get_file(), Params::get_path() and Params::get_blob().
|
||||
#[async_std::test]
|
||||
async fn test_params_get_fileparam() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let fname = t.dir.path().join("foo");
|
||||
let mut p = Params::new();
|
||||
p.set(Param::File, fname.to_str().unwrap());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
@@ -323,9 +324,11 @@ impl<'a> Peerstate<'a> {
|
||||
|
||||
pub fn render_gossip_header(&self, min_verified: PeerstateVerifiedStatus) -> Option<String> {
|
||||
if let Some(key) = self.peek_key(min_verified) {
|
||||
// TODO: avoid cloning
|
||||
let public_key = SignedPublicKey::try_from(key.clone()).ok()?;
|
||||
let header = Aheader::new(
|
||||
self.addr.clone(),
|
||||
key.clone(), // TODO: avoid cloning
|
||||
public_key,
|
||||
// Autocrypt 1.1.0 specification says that
|
||||
// `prefer-encrypt` attribute SHOULD NOT be included,
|
||||
// but we include it anyway to propagate encryption
|
||||
@@ -470,7 +473,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_save_to_db() {
|
||||
let ctx = crate::test_utils::TestContext::new().await;
|
||||
let ctx = crate::test_utils::dummy_context().await;
|
||||
let addr = "hello@mail.com";
|
||||
|
||||
let pub_key = alice_keypair().public;
|
||||
@@ -513,7 +516,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_double_create() {
|
||||
let ctx = crate::test_utils::TestContext::new().await;
|
||||
let ctx = crate::test_utils::dummy_context().await;
|
||||
let addr = "hello@mail.com";
|
||||
let pub_key = alice_keypair().public;
|
||||
|
||||
@@ -546,7 +549,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_with_empty_gossip_key_save_to_db() {
|
||||
let ctx = crate::test_utils::TestContext::new().await;
|
||||
let ctx = crate::test_utils::dummy_context().await;
|
||||
let addr = "hello@mail.com";
|
||||
|
||||
let pub_key = alice_keypair().public;
|
||||
|
||||
12
src/pgp.rs
12
src/pgp.rs
@@ -365,13 +365,11 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
|
||||
let decryptor = enc_msg.decrypt_with_password(|| passphrase)?;
|
||||
|
||||
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
|
||||
if let Some(msg) = msgs.first() {
|
||||
match msg.get_content()? {
|
||||
Some(content) => Ok(content),
|
||||
None => bail!("Decrypted message is empty"),
|
||||
}
|
||||
} else {
|
||||
bail!("No valid messages found")
|
||||
ensure!(!msgs.is_empty(), "No valid messages found");
|
||||
|
||||
match msgs[0].get_content()? {
|
||||
Some(content) => Ok(content),
|
||||
None => bail!("Decrypted message is empty"),
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -20,7 +20,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// aol.md: aol.com
|
||||
@@ -33,22 +32,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// arcor.de.md: arcor.de
|
||||
static ref P_ARCOR_DE: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/arcor-de",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.arcor.de", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "mail.arcor.de", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// autistici.org.md: autistici.org
|
||||
@@ -63,7 +46,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// bluewin.ch.md: bluewin.ch
|
||||
@@ -78,7 +60,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// chello.at.md: chello.at
|
||||
@@ -93,34 +74,13 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// comcast.md: xfinity.com, comcast.net
|
||||
static ref P_COMCAST: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/comcast",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// dismail.de.md: dismail.de
|
||||
static ref P_DISMAIL_DE: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/dismail-de",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// disroot.md: disroot.org
|
||||
static ref P_DISROOT: Provider = Provider {
|
||||
@@ -132,25 +92,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// dubby.org.md: dubby.org
|
||||
static ref P_DUBBY_ORG: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/dubby-org",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: Some(vec![
|
||||
ConfigDefault { key: Config::BccSelf, value: "1" },
|
||||
ConfigDefault { key: Config::SentboxWatch, value: "0" },
|
||||
ConfigDefault { key: Config::MvboxWatch, value: "0" },
|
||||
ConfigDefault { key: Config::MvboxMove, value: "0" },
|
||||
]),
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// example.com.md: example.com, example.org
|
||||
@@ -165,7 +106,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// fastmail.md: fastmail.com
|
||||
@@ -178,7 +118,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// five.chat.md: five.chat
|
||||
@@ -189,14 +128,8 @@ lazy_static::lazy_static! {
|
||||
overview_page: "https://providers.delta.chat/five-chat",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: Some(vec![
|
||||
ConfigDefault { key: Config::BccSelf, value: "1" },
|
||||
ConfigDefault { key: Config::SentboxWatch, value: "0" },
|
||||
ConfigDefault { key: Config::MvboxWatch, value: "0" },
|
||||
ConfigDefault { key: Config::MvboxMove, value: "0" },
|
||||
]),
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// freenet.de.md: freenet.de
|
||||
@@ -211,7 +144,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// gmail.md: gmail.com, googlemail.com
|
||||
@@ -226,7 +158,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: Some(Oauth2Authorizer::Gmail),
|
||||
};
|
||||
|
||||
// gmx.net.md: gmx.net, gmx.de, gmx.at, gmx.ch, gmx.org, gmx.eu, gmx.info, gmx.biz, gmx.com
|
||||
@@ -242,34 +173,10 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// hey.com.md: hey.com
|
||||
static ref P_HEY_COM: Provider = Provider {
|
||||
status: Status::BROKEN,
|
||||
before_login_hint: "hey.com does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to hey.com.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/hey-com",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// i.ua.md: i.ua
|
||||
static ref P_I_UA: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/i-ua",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// icloud.md: icloud.com, me.com, mac.com
|
||||
static ref P_ICLOUD: Provider = Provider {
|
||||
@@ -283,47 +190,16 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// kolst.com.md: kolst.com
|
||||
static ref P_KOLST_COM: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/kolst-com",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// kontent.com.md: kontent.com
|
||||
static ref P_KONTENT_COM: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/kontent-com",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// mail.ru.md: mail.ru, inbox.ru, bk.ru, list.ru
|
||||
static ref P_MAIL_RU: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/mail-ru",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// mailbox.org.md: mailbox.org, secure.mailbox.org
|
||||
static ref P_MAILBOX_ORG: Provider = Provider {
|
||||
@@ -335,7 +211,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// nauta.cu.md: nauta.cu
|
||||
@@ -358,7 +233,6 @@ lazy_static::lazy_static! {
|
||||
ConfigDefault { key: Config::MediaQuality, value: "1" },
|
||||
]),
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
|
||||
@@ -373,7 +247,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
|
||||
@@ -388,7 +261,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// protonmail.md: protonmail.com, protonmail.ch
|
||||
@@ -401,7 +273,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// riseup.net.md: riseup.net
|
||||
@@ -414,21 +285,10 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// rogers.com.md: rogers.com
|
||||
static ref P_ROGERS_COM: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/rogers-com",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// systemli.org.md: systemli.org
|
||||
static ref P_SYSTEMLI_ORG: Provider = Provider {
|
||||
@@ -440,7 +300,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// t-online.md: t-online.de, magenta.de
|
||||
@@ -453,7 +312,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// testrun.md: testrun.org
|
||||
@@ -467,14 +325,8 @@ lazy_static::lazy_static! {
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "testrun.org", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "testrun.org", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: Some(vec![
|
||||
ConfigDefault { key: Config::BccSelf, value: "1" },
|
||||
ConfigDefault { key: Config::SentboxWatch, value: "0" },
|
||||
ConfigDefault { key: Config::MvboxWatch, value: "0" },
|
||||
ConfigDefault { key: Config::MvboxMove, value: "0" },
|
||||
]),
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// tiscali.it.md: tiscali.it
|
||||
@@ -489,34 +341,13 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// ukr.net.md: ukr.net
|
||||
static ref P_UKR_NET: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/ukr-net",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// vfemail.md: vfemail.net
|
||||
static ref P_VFEMAIL: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/vfemail",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// web.de.md: web.de, email.de, flirt.ms, hallo.ms, kuss.ms, love.ms, magic.ms, singles.ms, cool.ms, kanzler.ms, okay.ms, party.ms, pop.ms, stars.ms, techno.ms, clever.ms, deutschland.ms, genial.ms, ich.ms, online.ms, smart.ms, wichtig.ms, action.ms, fussball.ms, joker.ms, planet.ms, power.ms
|
||||
static ref P_WEB_DE: Provider = Provider {
|
||||
@@ -531,7 +362,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// yahoo.md: yahoo.com, yahoo.de, yahoo.it, yahoo.fr, yahoo.es, yahoo.se, yahoo.co.uk, yahoo.co.nz, yahoo.com.au, yahoo.com.ar, yahoo.com.br, yahoo.com.mx, ymail.com, rocketmail.com, yahoodns.net
|
||||
@@ -546,10 +376,9 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// yandex.ru.md: yandex.com, yandex.by, yandex.kz, yandex.ru, yandex.ua, ya.ru, narod.ru
|
||||
// yandex.ru.md: yandex.ru, yandex.com
|
||||
static ref P_YANDEX_RU: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "For Yandex accounts, you have to set IMAP protocol option turned on.",
|
||||
@@ -559,7 +388,6 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: Some(Oauth2Authorizer::Yandex),
|
||||
};
|
||||
|
||||
// ziggo.nl.md: ziggo.nl
|
||||
@@ -574,21 +402,15 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [
|
||||
("aktivix.org", &*P_AKTIVIX_ORG),
|
||||
("aol.com", &*P_AOL),
|
||||
("arcor.de", &*P_ARCOR_DE),
|
||||
("autistici.org", &*P_AUTISTICI_ORG),
|
||||
("bluewin.ch", &*P_BLUEWIN_CH),
|
||||
("chello.at", &*P_CHELLO_AT),
|
||||
("xfinity.com", &*P_COMCAST),
|
||||
("comcast.net", &*P_COMCAST),
|
||||
("dismail.de", &*P_DISMAIL_DE),
|
||||
("disroot.org", &*P_DISROOT),
|
||||
("dubby.org", &*P_DUBBY_ORG),
|
||||
("example.com", &*P_EXAMPLE_COM),
|
||||
("example.org", &*P_EXAMPLE_COM),
|
||||
("fastmail.com", &*P_FASTMAIL),
|
||||
@@ -605,17 +427,9 @@ lazy_static::lazy_static! {
|
||||
("gmx.info", &*P_GMX_NET),
|
||||
("gmx.biz", &*P_GMX_NET),
|
||||
("gmx.com", &*P_GMX_NET),
|
||||
("hey.com", &*P_HEY_COM),
|
||||
("i.ua", &*P_I_UA),
|
||||
("icloud.com", &*P_ICLOUD),
|
||||
("me.com", &*P_ICLOUD),
|
||||
("mac.com", &*P_ICLOUD),
|
||||
("kolst.com", &*P_KOLST_COM),
|
||||
("kontent.com", &*P_KONTENT_COM),
|
||||
("mail.ru", &*P_MAIL_RU),
|
||||
("inbox.ru", &*P_MAIL_RU),
|
||||
("bk.ru", &*P_MAIL_RU),
|
||||
("list.ru", &*P_MAIL_RU),
|
||||
("mailbox.org", &*P_MAILBOX_ORG),
|
||||
("secure.mailbox.org", &*P_MAILBOX_ORG),
|
||||
("nauta.cu", &*P_NAUTA_CU),
|
||||
@@ -676,14 +490,11 @@ lazy_static::lazy_static! {
|
||||
("protonmail.com", &*P_PROTONMAIL),
|
||||
("protonmail.ch", &*P_PROTONMAIL),
|
||||
("riseup.net", &*P_RISEUP_NET),
|
||||
("rogers.com", &*P_ROGERS_COM),
|
||||
("systemli.org", &*P_SYSTEMLI_ORG),
|
||||
("t-online.de", &*P_T_ONLINE),
|
||||
("magenta.de", &*P_T_ONLINE),
|
||||
("testrun.org", &*P_TESTRUN),
|
||||
("tiscali.it", &*P_TISCALI_IT),
|
||||
("ukr.net", &*P_UKR_NET),
|
||||
("vfemail.net", &*P_VFEMAIL),
|
||||
("web.de", &*P_WEB_DE),
|
||||
("email.de", &*P_WEB_DE),
|
||||
("flirt.ms", &*P_WEB_DE),
|
||||
@@ -726,13 +537,8 @@ lazy_static::lazy_static! {
|
||||
("ymail.com", &*P_YAHOO),
|
||||
("rocketmail.com", &*P_YAHOO),
|
||||
("yahoodns.net", &*P_YAHOO),
|
||||
("yandex.com", &*P_YANDEX_RU),
|
||||
("yandex.by", &*P_YANDEX_RU),
|
||||
("yandex.kz", &*P_YANDEX_RU),
|
||||
("yandex.ru", &*P_YANDEX_RU),
|
||||
("yandex.ua", &*P_YANDEX_RU),
|
||||
("ya.ru", &*P_YANDEX_RU),
|
||||
("narod.ru", &*P_YANDEX_RU),
|
||||
("yandex.com", &*P_YANDEX_RU),
|
||||
("ziggo.nl", &*P_ZIGGO_NL),
|
||||
].iter().copied().collect();
|
||||
}
|
||||
|
||||
@@ -35,13 +35,6 @@ pub enum UsernamePattern {
|
||||
EMAILLOCALPART = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum Oauth2Authorizer {
|
||||
Yandex = 1,
|
||||
Gmail = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Server {
|
||||
pub protocol: Protocol,
|
||||
@@ -80,7 +73,6 @@ pub struct Provider {
|
||||
pub server: Vec<Server>,
|
||||
pub config_defaults: Option<Vec<ConfigDefault>>,
|
||||
pub strict_tls: bool,
|
||||
pub oauth2_authorizer: Option<Oauth2Authorizer>,
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
|
||||
@@ -103,9 +103,6 @@ def process_data(data, file):
|
||||
strict_tls = data.get("strict_tls", False)
|
||||
strict_tls = "true" if strict_tls else "false"
|
||||
|
||||
oauth2 = data.get("oauth2", "")
|
||||
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
|
||||
|
||||
provider = ""
|
||||
before_login_hint = cleanstr(data.get("before_login_hint", ""))
|
||||
after_login_hint = cleanstr(data.get("after_login_hint", ""))
|
||||
@@ -118,7 +115,6 @@ def process_data(data, file):
|
||||
provider += " server: vec![\n" + server + " ],\n"
|
||||
provider += " config_defaults: " + config_defaults + ",\n"
|
||||
provider += " strict_tls: " + strict_tls + ",\n"
|
||||
provider += " oauth2_authorizer: " + oauth2 + ",\n"
|
||||
provider += " };\n\n"
|
||||
else:
|
||||
raise TypeError("SMTP and IMAP must be specified together or left out both")
|
||||
@@ -129,11 +125,11 @@ def process_data(data, file):
|
||||
# finally, add the provider
|
||||
global out_all, out_domains
|
||||
out_all += " // " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
|
||||
|
||||
# also add provider with no special things to do -
|
||||
# eg. _not_ supporting oauth2 is also an information and we can skip the mx-lookup in this case
|
||||
out_all += provider
|
||||
out_domains += domains
|
||||
if status == "OK" and before_login_hint == "" and after_login_hint == "" and server == "" and config_defaults == "None" and strict_tls == "false":
|
||||
out_all += " // - skipping provider with status OK and no special things to do\n\n"
|
||||
else:
|
||||
out_all += provider
|
||||
out_domains += domains
|
||||
|
||||
|
||||
def process_file(file):
|
||||
|
||||
47
src/qr.rs
47
src/qr.rs
@@ -68,7 +68,6 @@ pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
|
||||
|
||||
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
|
||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
|
||||
|
||||
@@ -188,14 +187,13 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
}
|
||||
|
||||
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn decode_account(_context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[DCACCOUNT_SCHEME.len()..];
|
||||
|
||||
let mut lot = Lot::new();
|
||||
|
||||
if let Ok(url) = url::Url::parse(payload) {
|
||||
if url.scheme() == "http" || url.scheme() == "https" {
|
||||
if url.scheme() == "https" {
|
||||
lot.state = LotState::QrAccount;
|
||||
lot.text1 = url.host_str().map(|x| x.to_string());
|
||||
} else {
|
||||
@@ -219,7 +217,6 @@ struct CreateAccountResponse {
|
||||
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
|
||||
/// download additional information from the contained url and set the parameters.
|
||||
/// on success, a configure::configure() should be able to log in to the account
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
|
||||
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
|
||||
|
||||
@@ -243,7 +240,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error
|
||||
/// Extract address for the mailto scheme.
|
||||
///
|
||||
/// Scheme: `mailto:addr...?subject=...&body=..`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_mailto(context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[MAILTO_SCHEME.len()..];
|
||||
|
||||
@@ -265,7 +261,6 @@ async fn decode_mailto(context: &Context, qr: &str) -> Lot {
|
||||
/// Extract address for the smtp scheme.
|
||||
///
|
||||
/// Scheme: `SMTP:addr...:subject...:body...`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_smtp(context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[SMTP_SCHEME.len()..];
|
||||
|
||||
@@ -288,7 +283,6 @@ async fn decode_smtp(context: &Context, qr: &str) -> Lot {
|
||||
/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
|
||||
///
|
||||
/// There may or may not be linebreaks after the fields.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_matmsg(context: &Context, qr: &str) -> Lot {
|
||||
// Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
|
||||
// we ignore this case.
|
||||
@@ -322,15 +316,14 @@ lazy_static! {
|
||||
/// Extract address for the matmsg scheme.
|
||||
///
|
||||
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_vcard(context: &Context, qr: &str) -> Lot {
|
||||
let name = VCARD_NAME_RE
|
||||
.captures(qr)
|
||||
.and_then(|caps| {
|
||||
let last_name = caps.get(1)?.as_str().trim();
|
||||
let first_name = caps.get(2)?.as_str().trim();
|
||||
.map(|caps| {
|
||||
let last_name = &caps[1];
|
||||
let first_name = &caps[2];
|
||||
|
||||
Some(format!("{} {}", first_name, last_name))
|
||||
format!("{} {}", first_name.trim(), last_name.trim())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -390,11 +383,11 @@ fn normalize_address(addr: &str) -> Result<String, Error> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::dummy_context;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_http() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(&ctx.ctx, "http://www.hello.com").await;
|
||||
|
||||
@@ -406,7 +399,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_https() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(&ctx.ctx, "https://www.hello.com").await;
|
||||
|
||||
@@ -418,7 +411,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_text() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(&ctx.ctx, "I am so cool").await;
|
||||
|
||||
@@ -430,7 +423,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_vcard() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
@@ -448,7 +441,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_matmsg() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
@@ -466,7 +459,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_mailto() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
@@ -492,7 +485,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_smtp() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await;
|
||||
|
||||
@@ -506,7 +499,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_openpgp_group() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
@@ -535,7 +528,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_openpgp_secure_join() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
@@ -563,7 +556,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_openpgp_without_addr() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
@@ -598,7 +591,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_account() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
@@ -620,10 +613,10 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_account_bad_scheme() {
|
||||
let ctx = TestContext::new().await;
|
||||
let ctx = dummy_context().await;
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"DCACCOUNT:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.get_state(), LotState::QrError);
|
||||
@@ -632,7 +625,7 @@ mod tests {
|
||||
// Test it again with lowercased "dcaccount:" uri scheme
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
"dcaccount:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.get_state(), LotState::QrError);
|
||||
|
||||
106
src/scheduler.rs
106
src/scheduler.rs
@@ -36,6 +36,14 @@ impl Context {
|
||||
self.scheduler.read().await.interrupt_inbox(info).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_sentbox(&self, info: InterruptInfo) {
|
||||
self.scheduler.read().await.interrupt_sentbox(info).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_mvbox(&self, info: InterruptInfo) {
|
||||
self.scheduler.read().await.interrupt_mvbox(info).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_smtp(&self, info: InterruptInfo) {
|
||||
self.scheduler.read().await.interrupt_smtp(info).await;
|
||||
}
|
||||
@@ -74,18 +82,7 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
}
|
||||
None => {
|
||||
jobs_loaded = 0;
|
||||
|
||||
// Expunge folder if needed, e.g. if some jobs have
|
||||
// deleted messages on the server.
|
||||
if let Err(err) = connection.maybe_close_folder(&ctx).await {
|
||||
warn!(ctx, "failed to close folder: {:?}", err);
|
||||
}
|
||||
|
||||
info = if ctx.get_config_bool(Config::InboxWatch).await {
|
||||
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
|
||||
} else {
|
||||
connection.fake_idle(&ctx, None).await
|
||||
};
|
||||
info = fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +102,7 @@ async fn fetch(ctx: &Context, connection: &mut Imap) {
|
||||
match ctx.get_config(Config::ConfiguredInboxFolder).await {
|
||||
Some(watch_folder) => {
|
||||
if let Err(err) = connection.connect_configured(&ctx).await {
|
||||
error_network!(ctx, "{}", err);
|
||||
error!(ctx, "{}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,7 +124,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
Some(watch_folder) => {
|
||||
// connect and fake idle if unable to connect
|
||||
if let Err(err) = connection.connect_configured(&ctx).await {
|
||||
warn!(ctx, "imap connection failed: {}", err);
|
||||
error!(ctx, "imap connection failed: {}", err);
|
||||
return connection.fake_idle(&ctx, None).await;
|
||||
}
|
||||
|
||||
@@ -246,21 +243,29 @@ impl Scheduler {
|
||||
let (smtp, smtp_handlers) = SmtpConnectionState::new();
|
||||
let (inbox, inbox_handlers) = ImapConnectionState::new();
|
||||
|
||||
*self = Scheduler::Running {
|
||||
inbox,
|
||||
mvbox,
|
||||
sentbox,
|
||||
smtp,
|
||||
inbox_handle: None,
|
||||
mvbox_handle: None,
|
||||
sentbox_handle: None,
|
||||
smtp_handle: None,
|
||||
};
|
||||
|
||||
let (inbox_start_send, inbox_start_recv) = channel(1);
|
||||
let (mvbox_start_send, mvbox_start_recv) = channel(1);
|
||||
let mut mvbox_handle = None;
|
||||
let (sentbox_start_send, sentbox_start_recv) = channel(1);
|
||||
let mut sentbox_handle = None;
|
||||
let (smtp_start_send, smtp_start_recv) = channel(1);
|
||||
|
||||
let ctx1 = ctx.clone();
|
||||
let inbox_handle = Some(task::spawn(async move {
|
||||
inbox_loop(ctx1, inbox_start_send, inbox_handlers).await
|
||||
}));
|
||||
|
||||
if ctx.get_config_bool(Config::MvboxWatch).await {
|
||||
if let Scheduler::Running { inbox_handle, .. } = self {
|
||||
let ctx1 = ctx.clone();
|
||||
mvbox_handle = Some(task::spawn(async move {
|
||||
*inbox_handle = Some(task::spawn(async move {
|
||||
inbox_loop(ctx1, inbox_start_send, inbox_handlers).await
|
||||
}));
|
||||
}
|
||||
|
||||
let (mvbox_start_send, mvbox_start_recv) = channel(1);
|
||||
if let Scheduler::Running { mvbox_handle, .. } = self {
|
||||
let ctx1 = ctx.clone();
|
||||
*mvbox_handle = Some(task::spawn(async move {
|
||||
simple_imap_loop(
|
||||
ctx1,
|
||||
mvbox_start_send,
|
||||
@@ -269,13 +274,12 @@ impl Scheduler {
|
||||
)
|
||||
.await
|
||||
}));
|
||||
} else {
|
||||
mvbox_start_send.send(()).await;
|
||||
}
|
||||
|
||||
if ctx.get_config_bool(Config::SentboxWatch).await {
|
||||
let (sentbox_start_send, sentbox_start_recv) = channel(1);
|
||||
if let Scheduler::Running { sentbox_handle, .. } = self {
|
||||
let ctx1 = ctx.clone();
|
||||
sentbox_handle = Some(task::spawn(async move {
|
||||
*sentbox_handle = Some(task::spawn(async move {
|
||||
simple_imap_loop(
|
||||
ctx1,
|
||||
sentbox_start_send,
|
||||
@@ -284,25 +288,15 @@ impl Scheduler {
|
||||
)
|
||||
.await
|
||||
}));
|
||||
} else {
|
||||
sentbox_start_send.send(()).await;
|
||||
}
|
||||
|
||||
let ctx1 = ctx.clone();
|
||||
let smtp_handle = Some(task::spawn(async move {
|
||||
smtp_loop(ctx1, smtp_start_send, smtp_handlers).await
|
||||
}));
|
||||
|
||||
*self = Scheduler::Running {
|
||||
inbox,
|
||||
mvbox,
|
||||
sentbox,
|
||||
smtp,
|
||||
inbox_handle,
|
||||
mvbox_handle,
|
||||
sentbox_handle,
|
||||
smtp_handle,
|
||||
};
|
||||
let (smtp_start_send, smtp_start_recv) = channel(1);
|
||||
if let Scheduler::Running { smtp_handle, .. } = self {
|
||||
let ctx1 = ctx.clone();
|
||||
*smtp_handle = Some(task::spawn(async move {
|
||||
smtp_loop(ctx1, smtp_start_send, smtp_handlers).await
|
||||
}));
|
||||
}
|
||||
|
||||
// wait for all loops to be started
|
||||
if let Err(err) = inbox_start_recv
|
||||
@@ -392,18 +386,10 @@ impl Scheduler {
|
||||
smtp_handle,
|
||||
..
|
||||
} => {
|
||||
if let Some(handle) = inbox_handle.take() {
|
||||
handle.await;
|
||||
}
|
||||
if let Some(handle) = mvbox_handle.take() {
|
||||
handle.await;
|
||||
}
|
||||
if let Some(handle) = sentbox_handle.take() {
|
||||
handle.await;
|
||||
}
|
||||
if let Some(handle) = smtp_handle.take() {
|
||||
handle.await;
|
||||
}
|
||||
inbox_handle.take().expect("inbox not started").await;
|
||||
mvbox_handle.take().expect("mvbox not started").await;
|
||||
sentbox_handle.take().expect("sentbox not started").await;
|
||||
smtp_handle.take().expect("smtp not started").await;
|
||||
|
||||
*self = Scheduler::Stopped;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> O
|
||||
|
||||
Some(format!(
|
||||
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
fingerprint,
|
||||
self_addr_urlencoded,
|
||||
&group_name_urlencoded,
|
||||
&chat.grpid,
|
||||
@@ -129,11 +129,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> O
|
||||
// parameters used: a=n=i=s=
|
||||
Some(format!(
|
||||
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
self_addr_urlencoded,
|
||||
self_name_urlencoded,
|
||||
&invitenumber,
|
||||
&auth,
|
||||
fingerprint, self_addr_urlencoded, self_name_urlencoded, &invitenumber, &auth,
|
||||
))
|
||||
};
|
||||
|
||||
@@ -352,8 +348,9 @@ async fn send_handshake_msg(
|
||||
}
|
||||
|
||||
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
|
||||
contact_id
|
||||
let contacts = chat::get_chat_contacts(context, contact_chat_id).await;
|
||||
if contacts.len() == 1 {
|
||||
contacts[0]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@@ -364,8 +361,10 @@ async fn fingerprint_equals_sender(
|
||||
fingerprint: &Fingerprint,
|
||||
contact_chat_id: ChatId,
|
||||
) -> bool {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
let contacts = chat::get_chat_contacts(context, contact_chat_id).await;
|
||||
|
||||
if contacts.len() == 1 {
|
||||
if let Ok(contact) = Contact::load_from_db(context, contacts[0]).await {
|
||||
if let Some(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
if peerstate.public_key_fingerprint.is_some()
|
||||
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
|
||||
@@ -423,7 +422,6 @@ pub(crate) enum HandshakeMessage {
|
||||
/// When handle_securejoin_handshake() is called,
|
||||
/// the message is not yet filed in the database;
|
||||
/// this is done by receive_imf() later on as needed.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) async fn handle_securejoin_handshake(
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
@@ -1008,19 +1006,24 @@ fn encrypted_and_signed(
|
||||
if !mimeparser.was_encrypted() {
|
||||
warn!(context, "Message not encrypted.",);
|
||||
false
|
||||
} else if let Some(expected_fingerprint) = expected_fingerprint {
|
||||
if !mimeparser.signatures.contains(expected_fingerprint) {
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.", expected_fingerprint,
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
} else if mimeparser.signatures.is_empty() {
|
||||
warn!(context, "Message not signed.",);
|
||||
false
|
||||
} else if expected_fingerprint.is_none() {
|
||||
warn!(context, "Fingerprint for comparison missing.");
|
||||
false
|
||||
} else if !mimeparser
|
||||
.signatures
|
||||
.contains(expected_fingerprint.unwrap())
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.",
|
||||
expected_fingerprint.unwrap(),
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,14 @@
|
||||
// but for non-delta-compatibility, that seems to be better.
|
||||
// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
|
||||
pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
if let Some(text) = text.strip_prefix("--") {
|
||||
"-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-")
|
||||
if text.starts_with("--") {
|
||||
"-\u{200B}-".to_string() + &text[2..].replace("\n--", "\n-\u{200B}-")
|
||||
} else {
|
||||
text.replace("\n--", "\n-\u{200B}-")
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove standard (RFC 3676, §4.3) footer if it is found.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
|
||||
let mut nearly_standard_footer = None;
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
@@ -42,7 +41,6 @@ fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
|
||||
|
||||
/// Remove nonstandard footer and a boolean indicating whether such
|
||||
/// footer was removed.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
if line == "--"
|
||||
@@ -109,7 +107,6 @@ fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
let mut last_quoted_line = None;
|
||||
for (l, line) in lines.iter().enumerate().rev() {
|
||||
@@ -135,7 +132,6 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
let mut last_quoted_line = None;
|
||||
let mut has_quoted_headline = false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
pub mod send;
|
||||
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use async_smtp::smtp::client::net::*;
|
||||
use async_smtp::*;
|
||||
@@ -55,7 +55,7 @@ pub(crate) struct Smtp {
|
||||
/// Timestamp of last successful send/receive network interaction
|
||||
/// (eg connect or send succeeded). On initialization and disconnect
|
||||
/// it is set to None.
|
||||
last_success: Option<SystemTime>,
|
||||
last_success: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Smtp {
|
||||
@@ -76,11 +76,7 @@ impl Smtp {
|
||||
/// have been successfully used the last 60 seconds
|
||||
pub async fn has_maybe_stale_connection(&self) -> bool {
|
||||
if let Some(last_success) = self.last_success {
|
||||
SystemTime::now()
|
||||
.duration_since(last_success)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
> 60
|
||||
Instant::now().duration_since(last_success).as_secs() > 60
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -183,7 +179,7 @@ impl Smtp {
|
||||
.stock_string_repl_str2(
|
||||
StockMessage::ServerResponse,
|
||||
format!("SMTP {}:{}", domain, port),
|
||||
err.to_string(),
|
||||
format!("{}, ({:?})", err.to_string(), err),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -192,7 +188,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
self.transport = Some(trans);
|
||||
self.last_success = Some(SystemTime::now());
|
||||
self.last_success = Some(Instant::now());
|
||||
|
||||
context.emit_event(Event::SmtpConnected(format!(
|
||||
"SMTP-LOGIN as {} ok",
|
||||
|
||||
@@ -53,7 +53,7 @@ impl Smtp {
|
||||
"Message len={} was smtp-sent to {}",
|
||||
message_len, recipients_display
|
||||
)));
|
||||
self.last_success = Some(std::time::SystemTime::now());
|
||||
self.last_success = Some(std::time::Instant::now());
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
|
||||
45
src/sql.rs
45
src/sql.rs
@@ -13,7 +13,6 @@ use crate::chat::{update_device_icon, update_saved_messages_icon};
|
||||
use crate::constants::{ShowEmails, DC_CHAT_ID_TRASH};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
|
||||
@@ -569,24 +568,16 @@ pub async fn housekeeping(context: &Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = start_ephemeral_timers(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Housekeeping: cannot start ephemeral timers: {}", err
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(err) = prune_tombstones(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Housekeeping: Cannot prune message tombstones: {}", err
|
||||
"Houskeeping: Cannot prune message tombstones: {}", err
|
||||
);
|
||||
}
|
||||
|
||||
info!(context, "Housekeeping done.",);
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, name: &str) -> bool {
|
||||
let name_to_check = if let Some(namespc) = namespc_opt {
|
||||
let name_len = name.len();
|
||||
@@ -602,9 +593,11 @@ fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, nam
|
||||
}
|
||||
|
||||
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());
|
||||
if !file.as_ref().starts_with("$BLOBDIR") {
|
||||
return;
|
||||
}
|
||||
|
||||
files_in_use.insert(file.as_ref()[9..].into());
|
||||
}
|
||||
|
||||
async fn maybe_add_from_param(
|
||||
@@ -1257,32 +1250,6 @@ async fn open(
|
||||
.await?;
|
||||
sql.set_raw_config_int(context, "dbversion", 64).await?;
|
||||
}
|
||||
if dbversion < 65 {
|
||||
info!(context, "[migration] v65");
|
||||
sql.execute(
|
||||
"ALTER TABLE chats ADD COLUMN ephemeral_timer INTEGER",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
// Timer value in seconds. For incoming messages this
|
||||
// timer starts when message is read, so we want to have
|
||||
// the value stored here until the timer starts.
|
||||
sql.execute(
|
||||
"ALTER TABLE msgs ADD COLUMN ephemeral_timer INTEGER DEFAULT 0",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
// Timestamp indicating when the message should be
|
||||
// deleted. It is convenient to store it here because UI
|
||||
// needs this value to display how much time is left until
|
||||
// the message is deleted.
|
||||
sql.execute(
|
||||
"ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
sql.set_raw_config_int(context, "dbversion", 65).await?;
|
||||
}
|
||||
|
||||
// (2) updates that require high-level objects
|
||||
// (the structure is complete now and all objects are usable)
|
||||
@@ -1345,12 +1312,10 @@ mod test {
|
||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||
maybe_add_file(&mut files, "world2.txt");
|
||||
maybe_add_file(&mut files, "$BLOBDIR");
|
||||
|
||||
assert!(files.contains("hello"));
|
||||
assert!(files.contains("world.txt"));
|
||||
assert!(!files.contains("world2.txt"));
|
||||
assert!(!files.contains("$BLOBDIR"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
65
src/stock.rs
65
src/stock.rs
@@ -130,9 +130,7 @@ pub enum StockMessage {
|
||||
))]
|
||||
AcSetupMsgBody = 43,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
|
||||
))]
|
||||
#[strum(props(fallback = "Cannot login as %1$s."))]
|
||||
CannotLogin = 60,
|
||||
|
||||
#[strum(props(fallback = "Could not connect to %1$s: %2$s"))]
|
||||
@@ -179,7 +177,7 @@ pub enum StockMessage {
|
||||
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
|
||||
WelcomeMessage = 71,
|
||||
|
||||
#[strum(props(fallback = "Unknown sender for this chat. See 'info' for more details."))]
|
||||
#[strum(props(fallback = "Unknown Sender for this chat. See 'info' for more details."))]
|
||||
UnknownSenderForChat = 72,
|
||||
|
||||
#[strum(props(fallback = "Message from %1$s"))]
|
||||
@@ -187,35 +185,6 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "Failed to send message to %1$s."))]
|
||||
FailedSendingTo = 74,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is disabled."))]
|
||||
MsgEphemeralTimerDisabled = 75,
|
||||
|
||||
// A fallback message for unknown timer values.
|
||||
// "s" stands for "second" SI unit here.
|
||||
#[strum(props(fallback = "Message deletion timer is set to %1$s s."))]
|
||||
MsgEphemeralTimerEnabled = 76,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 minute."))]
|
||||
MsgEphemeralTimerMinute = 77,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 hour."))]
|
||||
MsgEphemeralTimerHour = 78,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 day."))]
|
||||
MsgEphemeralTimerDay = 79,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 week."))]
|
||||
MsgEphemeralTimerWeek = 80,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 4 weeks."))]
|
||||
MsgEphemeralTimerFourWeeks = 81,
|
||||
|
||||
#[strum(props(fallback = "Video chat invitation"))]
|
||||
VideochatInvitation = 82,
|
||||
|
||||
#[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))]
|
||||
VideochatInviteMsgBody = 83,
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -365,10 +334,10 @@ impl Context {
|
||||
let action1 = action.trim_end_matches('.');
|
||||
match from_id {
|
||||
0 => action,
|
||||
DC_CONTACT_ID_SELF => {
|
||||
1 => {
|
||||
self.stock_string_repl_str(StockMessage::MsgActionByMe, action1)
|
||||
.await
|
||||
}
|
||||
} // DC_CONTACT_ID_SELF
|
||||
_ => {
|
||||
let displayname = Contact::get_by_id(self, from_id)
|
||||
.await
|
||||
@@ -440,7 +409,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_set_stock_translation() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
t.ctx
|
||||
.set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
|
||||
.await
|
||||
@@ -450,7 +419,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_set_stock_translation_wrong_replacements() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
assert!(t
|
||||
.ctx
|
||||
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
|
||||
@@ -465,7 +434,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_str() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
assert_eq!(
|
||||
t.ctx.stock_str(StockMessage::NoMessages).await,
|
||||
"No messages."
|
||||
@@ -474,7 +443,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_string_repl_str() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
// uses %1$s substitution
|
||||
assert_eq!(
|
||||
t.ctx
|
||||
@@ -487,7 +456,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_string_repl_int() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
assert_eq!(
|
||||
t.ctx
|
||||
.stock_string_repl_int(StockMessage::MsgAddMember, 42)
|
||||
@@ -498,7 +467,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_string_repl_str2() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
assert_eq!(
|
||||
t.ctx
|
||||
.stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar")
|
||||
@@ -509,7 +478,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_system_msg_simple() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
assert_eq!(
|
||||
t.ctx
|
||||
.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)
|
||||
@@ -520,7 +489,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_system_msg_add_member_by_me() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
assert_eq!(
|
||||
t.ctx
|
||||
.stock_system_msg(
|
||||
@@ -536,7 +505,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
Contact::create(&t.ctx, "Alice", "alice@example.com")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
@@ -555,7 +524,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let contact_id = {
|
||||
Contact::create(&t.ctx, "Alice", "alice@example.com")
|
||||
.await
|
||||
@@ -579,7 +548,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_system_msg_grp_name() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
assert_eq!(
|
||||
t.ctx
|
||||
.stock_system_msg(
|
||||
@@ -595,7 +564,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_system_msg_grp_name_other() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
let id = Contact::create(&t.ctx, "Alice", "alice@example.com")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
@@ -610,7 +579,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_update_device_chats() {
|
||||
let t = TestContext::new().await;
|
||||
let t = dummy_context().await;
|
||||
t.ctx.update_device_chats().await.ok();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 2);
|
||||
|
||||
@@ -18,59 +18,44 @@ pub(crate) struct TestContext {
|
||||
pub dir: TempDir,
|
||||
}
|
||||
|
||||
impl TestContext {
|
||||
/// Create a new [TestContext].
|
||||
///
|
||||
/// The [Context] will be created and have an SQLite database named "db.sqlite" in the
|
||||
/// [TestContext.dir] directory. This directory is cleaned up when the [TestContext] is
|
||||
/// dropped.
|
||||
///
|
||||
/// [Context]: crate::context::Context
|
||||
pub async fn new() -> Self {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let ctx = Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
|
||||
Self { ctx, dir }
|
||||
}
|
||||
/// Create a new, opened [TestContext] using given callback.
|
||||
///
|
||||
/// The [Context] will be opened with the SQLite database named
|
||||
/// "db.sqlite" in the [TestContext.dir] directory.
|
||||
///
|
||||
/// [Context]: crate::context::Context
|
||||
pub(crate) async fn test_context() -> TestContext {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let ctx = Context::new("FakeOs".into(), dbfile.into()).await.unwrap();
|
||||
TestContext { ctx, dir }
|
||||
}
|
||||
|
||||
/// Create a new configured [TestContext].
|
||||
///
|
||||
/// This is a shortcut which automatically calls [TestContext::configure_alice] after
|
||||
/// creating the context.
|
||||
pub async fn new_alice() -> Self {
|
||||
let t = Self::new().await;
|
||||
t.configure_alice().await;
|
||||
t
|
||||
}
|
||||
/// Return a dummy [TestContext].
|
||||
///
|
||||
/// The context will be opened and use the SQLite database as
|
||||
/// specified in [test_context] but there is no callback hooked up,
|
||||
/// i.e. [Context::call_cb] will always return `0`.
|
||||
pub(crate) async fn dummy_context() -> TestContext {
|
||||
test_context().await
|
||||
}
|
||||
|
||||
/// Configure with alice@example.com.
|
||||
///
|
||||
/// The context will be fake-configured as the alice user, with a pre-generated secret
|
||||
/// key. The email address of the user is returned as a string.
|
||||
pub async fn configure_alice(&self) -> String {
|
||||
let keypair = alice_keypair();
|
||||
self.configure_addr(&keypair.addr.to_string()).await;
|
||||
key::store_self_keypair(&self.ctx, &keypair, key::KeyPairUse::Default)
|
||||
.await
|
||||
.expect("Failed to save Alice's key");
|
||||
keypair.addr.to_string()
|
||||
}
|
||||
pub(crate) async fn configured_offline_context() -> TestContext {
|
||||
configured_offline_context_with_addr("alice@example.org").await
|
||||
}
|
||||
|
||||
/// Configure as a given email address.
|
||||
///
|
||||
/// The context will be configured but the key will not be pre-generated so if a key is
|
||||
/// used the fingerprint will be different every time.
|
||||
pub async fn configure_addr(&self, addr: &str) {
|
||||
self.ctx.set_config(Config::Addr, Some(addr)).await.unwrap();
|
||||
self.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some(addr))
|
||||
.await
|
||||
.unwrap();
|
||||
self.ctx
|
||||
.set_config(Config::Configured, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
pub(crate) async fn configured_offline_context_with_addr(addr: &str) -> TestContext {
|
||||
let t = dummy_context().await;
|
||||
t.ctx.set_config(Config::Addr, Some(addr)).await.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some(addr))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::Configured, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
t
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for alice@example.com from disk.
|
||||
@@ -93,6 +78,20 @@ pub(crate) fn alice_keypair() -> key::KeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates Alice with a pre-generated keypair.
|
||||
///
|
||||
/// Returns the address of the keypair created (alice@example.com).
|
||||
pub(crate) async fn configure_alice_keypair(ctx: &Context) -> String {
|
||||
let keypair = alice_keypair();
|
||||
ctx.set_config(Config::ConfiguredAddr, Some(&keypair.addr.to_string()))
|
||||
.await
|
||||
.unwrap();
|
||||
key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default)
|
||||
.await
|
||||
.expect("Failed to save Alice's key");
|
||||
keypair.addr.to_string()
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for bob@example.net from disk.
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Subject: Message from user
|
||||
Message-ID: <Mr.f1O61111evx.ikocf333353@example.org>
|
||||
Date: Mon, 20 Jul 2020 14:28:30 +0000
|
||||
X-Mailer: Delta Chat Core 1.40.0/CLI
|
||||
Chat-Version: 1.0
|
||||
Chat-Content: videochat-invitation
|
||||
Chat-Webrtc-Room: https://example.org/p2p/?roomname=6HiduoAn4xN
|
||||
To: <tunis3@example.org>
|
||||
From: "=?utf-8?q??=" <tunis4@example.org>
|
||||
|
||||
You are invited to an videochat, click https://example.org/p2p/?roomname=6HiduoAn4xN to join.
|
||||
|
||||
--
|
||||
Sent with my Delta Chat Messenger: https://delta.chat
|
||||
Reference in New Issue
Block a user