mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 05:26:42 +03:00
Compare commits
86 Commits
1.55.0
...
robust-utf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6d4ba7aca | ||
|
|
d821cdf1c8 | ||
|
|
62e9fbf68c | ||
|
|
15664be4f6 | ||
|
|
62388514dd | ||
|
|
ad7c7e90b3 | ||
|
|
b16785bb62 | ||
|
|
d12d9d94d6 | ||
|
|
991d15615e | ||
|
|
5dee1efa59 | ||
|
|
1870684c43 | ||
|
|
1803db2dfe | ||
|
|
7fee3d995c | ||
|
|
4b62500989 | ||
|
|
8f2cb1e8ab | ||
|
|
72ebd83479 | ||
|
|
2842042304 | ||
|
|
25fed9ab52 | ||
|
|
751b9add09 | ||
|
|
b727190da5 | ||
|
|
368fa9fc44 | ||
|
|
c07c5bb358 | ||
|
|
67f8fb4b66 | ||
|
|
056721b916 | ||
|
|
4fe3a80f96 | ||
|
|
6c530b4c77 | ||
|
|
c616b65ce4 | ||
|
|
50f680a00b | ||
|
|
47e0f224ca | ||
|
|
8b872b7e6f | ||
|
|
29356a6ca8 | ||
|
|
c5539de4da | ||
|
|
b017af78ce | ||
|
|
3b897eac53 | ||
|
|
d8a3014896 | ||
|
|
4209960c0f | ||
|
|
04c8622e94 | ||
|
|
002e33d28c | ||
|
|
35aeda3849 | ||
|
|
af287ee9a8 | ||
|
|
cc3e8c5117 | ||
|
|
1127521923 | ||
|
|
bf7f64d50b | ||
|
|
8380ac28c1 | ||
|
|
d8ba466c6a | ||
|
|
0c2a3c8347 | ||
|
|
e3e2adeea5 | ||
|
|
46e901be78 | ||
|
|
d899a38d17 | ||
|
|
b60994b313 | ||
|
|
8b19b6f9fe | ||
|
|
ea88a567f9 | ||
|
|
38c23104da | ||
|
|
2a83147d90 | ||
|
|
7e3bfd38f0 | ||
|
|
8b78e12b36 | ||
|
|
15660f2741 | ||
|
|
03520fbd4b | ||
|
|
b1228cbbe5 | ||
|
|
09f5b015bb | ||
|
|
67ca93b093 | ||
|
|
dabf31204c | ||
|
|
c22580e07f | ||
|
|
0028f579b6 | ||
|
|
9522240992 | ||
|
|
8ab3415c58 | ||
|
|
e858fca356 | ||
|
|
7d4affcc8d | ||
|
|
60d596bb0e | ||
|
|
c57bfde010 | ||
|
|
afd7e7eaac | ||
|
|
cfc324c95b | ||
|
|
aa5d6077a8 | ||
|
|
4a2beac6fc | ||
|
|
b5dc954408 | ||
|
|
a9f5077cf9 | ||
|
|
09280508bc | ||
|
|
4391835a8d | ||
|
|
05f9ac0583 | ||
|
|
fd9d632cd6 | ||
|
|
c9e626322b | ||
|
|
f343ec47b4 | ||
|
|
efb1534d5c | ||
|
|
6ec765cad6 | ||
|
|
372a4ee539 | ||
|
|
418b591602 |
@@ -27,7 +27,10 @@ jobs:
|
||||
- checkout
|
||||
# the following commands on success produces
|
||||
# workspace/{wheelhouse,py-docs} as artefact directories
|
||||
- run: bash scripts/remote_python_packaging.sh
|
||||
- run:
|
||||
# building aarch64 packages under qemu is very slow
|
||||
no_output_timeout: 60m
|
||||
command: bash scripts/remote_python_packaging.sh
|
||||
- persist_to_workspace:
|
||||
root: workspace
|
||||
paths:
|
||||
@@ -51,18 +54,24 @@ workflows:
|
||||
jobs:
|
||||
- remote_python_packaging:
|
||||
filters:
|
||||
tags:
|
||||
only: /.+/
|
||||
branches:
|
||||
only: master
|
||||
ignore: /.*/
|
||||
|
||||
- build_doxygen:
|
||||
filters:
|
||||
tags:
|
||||
only: /.+/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
||||
- upload_docs_wheels:
|
||||
requires:
|
||||
- remote_python_packaging
|
||||
- build_doxygen
|
||||
filters:
|
||||
tags:
|
||||
only: /.+/
|
||||
branches:
|
||||
only: master
|
||||
|
||||
- build_doxygen:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
ignore: /.*/
|
||||
|
||||
52
.github/workflows/ci.yml
vendored
52
.github/workflows/ci.yml
vendored
@@ -65,26 +65,16 @@ jobs:
|
||||
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
strategy:
|
||||
matrix:
|
||||
# macOS disabled due to random failures related to caching
|
||||
#os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
rust: [1.50.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
|
||||
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.50.0
|
||||
python: 3.6
|
||||
- os: windows-latest
|
||||
rust: 1.50.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
@@ -125,3 +115,29 @@ jobs:
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
|
||||
- name: install python
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
- name: install tox
|
||||
if: ${{ matrix.python }}
|
||||
run: pip install tox
|
||||
|
||||
- name: build C library
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: -p deltachat_ffi
|
||||
|
||||
- name: run python tests
|
||||
if: ${{ matrix.python }}
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e lint,doc,py3
|
||||
|
||||
18
.github/workflows/remote_tests.yml
vendored
18
.github/workflows/remote_tests.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Remote tests
|
||||
on: [push]
|
||||
jobs:
|
||||
remote_tests_python:
|
||||
name: Remote Python tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: mkdir -m 700 -p ~/.ssh
|
||||
- run: touch ~/.ssh/id_ed25519
|
||||
- run: chmod 600 ~/.ssh/id_ed25519
|
||||
- run: 'echo "$SSH_KEY" | base64 -d > ~/.ssh/id_ed25519'
|
||||
shell: bash
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SSH_KEY }}
|
||||
- run: scripts/remote_tests_python.sh "deltachat-core/python/${{ github.ref }}/${{ github.run_number }}"
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,5 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## 1.56.0
|
||||
|
||||
- fix downscaling images #2469
|
||||
|
||||
- fix outgoing messages popping up in selfchat #2456
|
||||
|
||||
- securejoin: display error reason if there is any #2470
|
||||
|
||||
- do not allow deleting contacts with ongoing chats #2458
|
||||
|
||||
- fix: ignore drafts folder when scanning #2454
|
||||
|
||||
- fix: scan folders also when inbox is not watched #2446
|
||||
|
||||
- more robust In-Reply-To parsing #2182
|
||||
|
||||
- update dependencies #2441 #2438 #2439 #2440 #2447 #2448 #2449 #2452 #2453 #2460 #2464 #2466
|
||||
|
||||
- update provider-database #2471
|
||||
|
||||
- refactorings #2459 #2457
|
||||
|
||||
- improve tests and ci #2445 #2450 #2451
|
||||
|
||||
|
||||
## 1.55.0
|
||||
|
||||
- fix panic when receiving some HTML messages #2434
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(deltachat)
|
||||
project(deltachat LANGUAGES C)
|
||||
|
||||
find_program(CARGO cargo)
|
||||
|
||||
|
||||
175
Cargo.lock
generated
175
Cargo.lock
generated
@@ -138,9 +138,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.40"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
|
||||
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
@@ -374,9 +374,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-std-resolver"
|
||||
version = "0.20.2"
|
||||
version = "0.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d613d619c2886fc0f4b5a777eceab405b23de82d73f0fc61ae402fdb9bc6fb2"
|
||||
checksum = "ed4e2c3da14d8ad45acb1e3191db7a918e9505b6f155b218e70a7c9a1a48c638"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -748,6 +748,17 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "4.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e4ea1881992efc993e4dc50a324cdbd03216e41bdc8385720ff47efc9bd2ca8"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
"str-buf",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
@@ -815,9 +826,12 @@ checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cd5a7748210e7ec1a9696610b1015e6e31fbf58f77a160801f124bd1c36592a"
|
||||
checksum = "dec1028182c380cc45a2e2c5ec841134f2dfd0f8f5f0a5bcd68004f81b5efdf4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpuid-bool"
|
||||
@@ -851,7 +865,7 @@ dependencies = [
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"csv",
|
||||
"itertools 0.10.0",
|
||||
"itertools 0.10.1",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
@@ -1094,7 +1108,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.55.0"
|
||||
version = "1.56.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1122,7 +1136,7 @@ dependencies = [
|
||||
"hex",
|
||||
"image",
|
||||
"indexmap",
|
||||
"itertools 0.10.0",
|
||||
"itertools 0.10.1",
|
||||
"kamadak-exif",
|
||||
"lettre_email",
|
||||
"libc",
|
||||
@@ -1173,7 +1187,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.55.0"
|
||||
version = "1.56.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
@@ -1452,6 +1466,16 @@ dependencies = [
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-code"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5115567ac25674e0043e472be13d14e537f37ea8aa4bdc4aef0c89add1db1ff"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"str-buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "escaper"
|
||||
version = "0.1.1"
|
||||
@@ -1497,6 +1521,16 @@ dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fd-lock"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0010f02effd88c702318c5dde0463206be67495d0b4d906ba7c0a8f166cc7f06"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.14"
|
||||
@@ -1552,21 +1586,11 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d5813545e459ad3ca1bff9915e9ad7f1a47dc6a91b627ce321d5863b7dd253"
|
||||
checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1579,9 +1603,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce79c6a52a299137a6013061e0cf0e688fce5d7f1bc60125f520912fdb29ec25"
|
||||
checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -1589,15 +1613,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "098cd1c6dda6ca01650f1a37a794245eb73181d0d4d4e955e2f3c37db7af1815"
|
||||
checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10f6cb7042eda00f0049b1d2080aa4b93442997ee507eb3828e8bd7577f94c9d"
|
||||
checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -1606,15 +1630,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "365a1a1fb30ea1c03a830fdb2158f5236833ac81fa0ad12fe35b29cddc35cb04"
|
||||
checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "1.11.3"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4481d0cd0de1d204a4fa55e7d45f07b1d958abcb06714b3446438e2eff695fb"
|
||||
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
@@ -1627,10 +1651,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "668c6733a182cd7deb4f1de7ba3bf2120823835b3bcfbeacf7d2c4a773c1bb8b"
|
||||
checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1",
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1639,22 +1664,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c5629433c555de3d82861a7a4e3794a4c40040390907cfbfd7143a92a426c23"
|
||||
checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba7aa51095076f3ba6d9a1f702f74bd05ec65f555d70d2033d55ba8d69f581bc"
|
||||
checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c144ad54d60f23927f0a6b6d816e4271278b64f005ad65e4e35291d2de9c025"
|
||||
checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
@@ -2002,9 +2028,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
|
||||
checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
@@ -2102,9 +2128,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.94"
|
||||
version = "0.2.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
|
||||
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -2423,9 +2449,9 @@ checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.7.2"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
|
||||
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -3154,14 +3180,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustyline"
|
||||
version = "8.0.0"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e1b597fcd1eeb1d6b25b493538e5aa19629eb08932184b85fef931ba87e893"
|
||||
checksum = "fbd4eaf7a7738f76c98e4f0395253ae853be3eb018f7b0bb57fe1b6c17e31874"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"clipboard-win",
|
||||
"dirs-next",
|
||||
"fs2",
|
||||
"fd-lock",
|
||||
"libc",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -3271,9 +3298,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.125"
|
||||
version = "1.0.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
|
||||
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -3290,9 +3317,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.125"
|
||||
version = "1.0.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
|
||||
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3336,9 +3363,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sha-1"
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b659df5fc3ce22274daac600ffb845300bd2125bcfaec047823075afdab81c00"
|
||||
checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -3355,9 +3382,9 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8f6b75b17576b792bef0db1bcc4b8b8bcdf9506744cf34b974195487af6cff2"
|
||||
checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -3542,6 +3569,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "str-buf"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
|
||||
|
||||
[[package]]
|
||||
name = "stream-cipher"
|
||||
version = "0.7.1"
|
||||
@@ -3560,15 +3593,15 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c"
|
||||
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.20.1"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149"
|
||||
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3603,9 +3636,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.72"
|
||||
version = "1.0.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
|
||||
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3658,18 +3691,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.24"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
|
||||
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.24"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
|
||||
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3771,9 +3804,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "trust-dns-proto"
|
||||
version = "0.20.2"
|
||||
version = "0.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "952a078337565ba39007de99b151770f41039253a31846f0a3d5cd5a4ac8eedf"
|
||||
checksum = "ad0d7f5db438199a6e2609debe3f69f808d074e0a2888ee0bccb45fe234d03f4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -3795,9 +3828,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "trust-dns-resolver"
|
||||
version = "0.20.2"
|
||||
version = "0.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da9c97f7d103e0f94dbe384a57908833505ae5870126492f166821b7cf685589"
|
||||
checksum = "f6ad17b608a64bd0735e67bde16b0636f8aa8591f831a25d18443ed00a699770"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"futures-util",
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.55.0"
|
||||
version = "1.56.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -15,11 +15,11 @@ lto = true
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1.0.40"
|
||||
anyhow = "1.0.41"
|
||||
async-imap = "0.5.0"
|
||||
async-native-tls = { version = "0.3.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
|
||||
async-std-resolver = "0.20.2"
|
||||
async-std-resolver = "0.20.3"
|
||||
async-std = { version = "~1.9.0", features = ["unstable"] }
|
||||
async-tar = "0.3.0"
|
||||
async-trait = "0.1.50"
|
||||
@@ -33,21 +33,21 @@ dirs = { version = "3.0.2", optional=true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
escaper = "0.1.1"
|
||||
futures = "0.3.14"
|
||||
futures = "0.3.15"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
indexmap = "1.3.0"
|
||||
itertools = "0.10.0"
|
||||
itertools = "0.10.1"
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2.51"
|
||||
libc = "0.2.97"
|
||||
log = {version = "0.4.8", optional = true }
|
||||
mailparse = "0.13.4"
|
||||
native-tls = "0.2.3"
|
||||
num_cpus = "1.13.0"
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
once_cell = "1.4.1"
|
||||
once_cell = "1.8.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.7.0", default-features = false }
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
@@ -58,18 +58,18 @@ rand = "0.7.0"
|
||||
regex = "1.4.6"
|
||||
rusqlite = "0.25"
|
||||
rust-hsluv = "0.1.4"
|
||||
rustyline = { version = "8.0.0", optional = true }
|
||||
rustyline = { version = "8.2.0", optional = true }
|
||||
sanitize-filename = "0.3.0"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.9.5"
|
||||
sha2 = "0.9.4"
|
||||
sha-1 = "0.9.6"
|
||||
sha2 = "0.9.5"
|
||||
smallvec = "1.0.0"
|
||||
stop-token = "0.2.0"
|
||||
strum = "0.20.0"
|
||||
strum_macros = "0.20.1"
|
||||
strum = "0.21.0"
|
||||
strum_macros = "0.21.1"
|
||||
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1.0.14"
|
||||
thiserror = "1.0.25"
|
||||
toml = "0.5.6"
|
||||
url = "2.2.2"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
@@ -78,7 +78,7 @@ uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
ansi_term = "0.12.0"
|
||||
async-std = { version = "1.9.0", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
futures-lite = "1.7.0"
|
||||
futures-lite = "1.12.0"
|
||||
log = "0.4.11"
|
||||
pretty_assertions = "0.7.2"
|
||||
pretty_env_logger = "0.4.0"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
> Deltachat-core written in Rust
|
||||
|
||||
[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
|
||||
[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/remote_tests.yml)
|
||||
[](https://circleci.com/gh/deltachat/deltachat-core-rust/)
|
||||
|
||||
## Installing Rust and Cargo
|
||||
|
||||
@@ -17,7 +17,7 @@ async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
Contact::add_address_book(&context, book).await.unwrap();
|
||||
Contact::add_address_book(&context, &book).await.unwrap();
|
||||
|
||||
let query: Option<&str> = None;
|
||||
for _ in 0..read_count {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.55.0"
|
||||
version = "1.56.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -21,8 +21,8 @@ human-panic = "1.0.1"
|
||||
num-traits = "0.2.6"
|
||||
serde_json = "1.0"
|
||||
async-std = "1.9.0"
|
||||
anyhow = "1.0.40"
|
||||
thiserror = "1.0.14"
|
||||
anyhow = "1.0.41"
|
||||
thiserror = "1.0.25"
|
||||
rand = "0.7.3"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1737,9 +1737,13 @@ pub unsafe extern "C" fn dc_block_contact(
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
if block == 0 {
|
||||
Contact::unblock(&ctx, contact_id).await;
|
||||
Contact::unblock(&ctx, contact_id)
|
||||
.await
|
||||
.ok_or_log_msg(&ctx, "Can't unblock contact");
|
||||
} else {
|
||||
Contact::block(&ctx, contact_id).await;
|
||||
Contact::block(&ctx, contact_id)
|
||||
.await
|
||||
.ok_or_log_msg(&ctx, "Can't block contact");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ license = "MPL-2.0"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0.72"
|
||||
syn = "1.0.73"
|
||||
quote = "1.0.2"
|
||||
|
||||
@@ -372,6 +372,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
getlocations [<contact-id>]\n\
|
||||
send <text>\n\
|
||||
sendimage <file> [<text>]\n\
|
||||
sendsticker <file> [<text>]\n\
|
||||
sendfile <file> [<text>]\n\
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
videochat\n\
|
||||
@@ -865,12 +866,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
||||
}
|
||||
"sendimage" | "sendfile" => {
|
||||
"sendimage" | "sendsticker" | "sendfile" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "No file given.");
|
||||
|
||||
let mut msg = Message::new(if arg0 == "sendimage" {
|
||||
Viewtype::Image
|
||||
} else if arg0 == "sendsticker" {
|
||||
Viewtype::Sticker
|
||||
} else {
|
||||
Viewtype::File
|
||||
});
|
||||
@@ -1134,12 +1137,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"block" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
Contact::block(&context, contact_id).await;
|
||||
Contact::block(&context, contact_id).await?;
|
||||
}
|
||||
"unblock" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
Contact::unblock(&context, contact_id).await;
|
||||
Contact::unblock(&context, contact_id).await?;
|
||||
}
|
||||
"listblocked" => {
|
||||
let contacts = Contact::get_all_blocked(&context).await?;
|
||||
|
||||
@@ -251,7 +251,16 @@ class DirectImap:
|
||||
return res
|
||||
|
||||
def append(self, folder, msg):
|
||||
"""Upload a message to *folder*.
|
||||
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
||||
"""
|
||||
if msg.startswith("\n"):
|
||||
msg = msg[1:]
|
||||
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(folder, msg)
|
||||
|
||||
def get_uid_by_message_id(self, message_id):
|
||||
msgs = self.conn.search(['HEADER', 'MESSAGE-ID', message_id])
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert len(sys.argv) == 2
|
||||
workspacedir = sys.argv[1]
|
||||
arch = platform.machine()
|
||||
for relpath in os.listdir(workspacedir):
|
||||
if relpath.startswith("deltachat"):
|
||||
p = os.path.join(workspacedir, relpath)
|
||||
subprocess.check_call(
|
||||
["auditwheel", "repair", p, "-w", workspacedir,
|
||||
"--plat", "manylinux2014_x86_64"])
|
||||
"--plat", "manylinux2014_" + arch])
|
||||
|
||||
@@ -983,7 +983,7 @@ class TestOnlineAccount:
|
||||
chat2 = msg2.chat
|
||||
assert msg2 in chat2.get_messages()
|
||||
assert chat2.is_deaddrop()
|
||||
assert chat2.count_fresh_messages() == 0
|
||||
assert chat2.count_fresh_messages() == 1
|
||||
assert msg2.time_received >= msg1.time_sent
|
||||
|
||||
lp.sec("create new chat with contact and verify it's proper")
|
||||
@@ -1033,6 +1033,33 @@ class TestOnlineAccount:
|
||||
except queue.Empty:
|
||||
pass # mark_seen_messages() has generated events before it returns
|
||||
|
||||
def test_moved_markseen(self, acfactory, lp):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, config={"inbox_watch": "0"})
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure_and_start_io([ac1, ac2])
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
ac1.direct_imap.idle_start()
|
||||
ac1.create_chat(ac2).send_text("Hello!")
|
||||
ac1.direct_imap.idle_check(terminate=True)
|
||||
ac1.stop_io()
|
||||
|
||||
# Emulate moving of the message to DeltaChat folder by Sieve rule.
|
||||
# mailcow server contains this rule by default.
|
||||
ac1.direct_imap.conn.move(["*"], "DeltaChat")
|
||||
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
ac1.direct_imap.idle_start()
|
||||
ac1.start_io()
|
||||
ac1.direct_imap.idle_wait_for_seen()
|
||||
ac1.direct_imap.idle_done()
|
||||
|
||||
fetch = list(ac1.direct_imap.conn.fetch("*", b'FLAGS').values())
|
||||
flags = fetch[-1][b'FLAGS']
|
||||
is_seen = b'\\Seen' in flags
|
||||
assert is_seen
|
||||
|
||||
def test_message_override_sender_name(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -1071,10 +1098,10 @@ class TestOnlineAccount:
|
||||
# We had so many problems with markseen, if in doubt, rather create another test, it can't harm.
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
|
||||
ac2 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
acfactory.wait_configure_and_start_io()
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
ac1.set_config("bcc_self", "0")
|
||||
|
||||
folder = "mvbox" if mvbox_move else "inbox"
|
||||
ac1.direct_imap.select_config_folder(folder)
|
||||
@@ -1082,6 +1109,9 @@ class TestOnlineAccount:
|
||||
ac1.direct_imap.idle_start()
|
||||
ac2.direct_imap.idle_start()
|
||||
|
||||
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
ac1.direct_imap.idle_wait_for_seen() # Check that the mdn is marked as seen
|
||||
@@ -1289,9 +1319,11 @@ class TestOnlineAccount:
|
||||
assert not device_chat.can_send()
|
||||
assert device_chat.get_draft() is None
|
||||
|
||||
def test_dont_show_emails_in_draft_folder(self, acfactory):
|
||||
def test_dont_show_emails_in_draft_folder(self, acfactory, lp):
|
||||
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
|
||||
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email."""
|
||||
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
|
||||
|
||||
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac1.set_config("show_emails", "2")
|
||||
ac1.create_contact("alice@example.com").create_chat()
|
||||
@@ -1312,7 +1344,7 @@ class TestOnlineAccount:
|
||||
Message-ID: <aepiors@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
message in Drafts
|
||||
message in Drafts that is moved to Sent later
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
ac1.direct_imap.append("Sent", """
|
||||
From: ac1 <{}>
|
||||
@@ -1325,6 +1357,7 @@ class TestOnlineAccount:
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
lp.sec("All prepared, now let DC find the message")
|
||||
ac1.start_io()
|
||||
|
||||
msg = ac1._evtracker.wait_next_messages_changed()
|
||||
@@ -1335,6 +1368,18 @@ class TestOnlineAccount:
|
||||
assert msg.text == "subj – message in Sent"
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
ac1.stop_io()
|
||||
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
|
||||
ac1.direct_imap.select_folder("Drafts")
|
||||
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
|
||||
ac1.direct_imap.conn.move(uid, "Sent")
|
||||
|
||||
ac1.start_io()
|
||||
msg2 = ac1._evtracker.wait_next_messages_changed()
|
||||
|
||||
assert msg2.text == "subj – message in Drafts that is moved to Sent later"
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
|
||||
def test_prefer_encrypt(self, acfactory, lp):
|
||||
"""Test quorum rule for encryption preference in 1:1 and group chat."""
|
||||
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
|
||||
@@ -2396,26 +2441,31 @@ class TestOnlineAccount:
|
||||
assert received_reply.quoted_text == "hello"
|
||||
assert received_reply.quote.id == out_msg.id
|
||||
|
||||
@pytest.mark.parametrize("folder,move,expected_destination,", [
|
||||
("xyz", False, "xyz"), # Test that emails are recognized in a random folder but not moved
|
||||
("xyz", True, "DeltaChat"), # ...emails are found in a random folder and moved to DeltaChat
|
||||
("Spam", False, "INBOX") # ...emails are moved from the spam folder to the Inbox
|
||||
@pytest.mark.parametrize("folder,move,expected_destination,inbox_watch,", [
|
||||
("xyz", False, "xyz", "1"), # Test that emails are recognized in a random folder but not moved
|
||||
("xyz", True, "DeltaChat", "1"), # ...emails are found in a random folder and moved to DeltaChat
|
||||
("Spam", False, "INBOX", "1"), # ...emails are moved from the spam folder to the Inbox
|
||||
("INBOX", False, "INBOX", "0"), # ...emails are found in the `Inbox` folder even if `inbox_watch` is "0"
|
||||
])
|
||||
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
|
||||
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
|
||||
def test_scan_folders(self, acfactory, lp, folder, move, expected_destination):
|
||||
def test_scan_folders(self, acfactory, lp, folder, move, expected_destination, inbox_watch):
|
||||
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
|
||||
variant = folder + "-" + str(move) + "-" + expected_destination
|
||||
lp.sec("Testing variant " + variant)
|
||||
ac1 = acfactory.get_online_configuring_account(move=move)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1.set_config("inbox_watch", inbox_watch)
|
||||
|
||||
acfactory.wait_configure(ac1)
|
||||
ac1.direct_imap.create_folder(folder)
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
# Wait until each folder was selected once and we are IDLEing:
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
if inbox_watch == "1":
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
else:
|
||||
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
|
||||
ac1.stop_io()
|
||||
|
||||
# Send a message to ac1 and move it to the mvbox:
|
||||
@@ -2432,7 +2482,10 @@ class TestOnlineAccount:
|
||||
assert msg.text == "hello"
|
||||
|
||||
# Wait until the message was moved (if at all) and we are IDLEing again:
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
if inbox_watch == "1":
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
else:
|
||||
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
|
||||
ac1.direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
|
||||
@@ -48,3 +48,7 @@ to avoid the relatively large download::
|
||||
cd scripts # where all CI things are
|
||||
docker build -t deltachat/coredeps docker-coredeps
|
||||
docker build -t deltachat/doxygen docker-doxygen
|
||||
|
||||
Additionally, you can install qemu and build arm64 docker image:
|
||||
apt-get install qemu binfmt-support qemu-user-static
|
||||
docker build -t deltachat/coredeps-arm64 docker-coredeps-arm64
|
||||
|
||||
21
scripts/docker-coredeps-arm64/Dockerfile
Normal file
21
scripts/docker-coredeps-arm64/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM quay.io/pypa/manylinux2014_aarch64
|
||||
|
||||
# Configure ld.so/ldconfig and pkg-config
|
||||
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
||||
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
||||
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
||||
|
||||
# Install a recent Perl, needed to install the openssl crate
|
||||
ADD deps/build_perl.sh /builder/build_perl.sh
|
||||
RUN rm /usr/bin/perl
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||
|
||||
# Install python tools (auditwheels,tox, ...)
|
||||
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
|
||||
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||
21
scripts/docker-coredeps-arm64/deps/build_openssl.sh
Executable file
21
scripts/docker-coredeps-arm64/deps/build_openssl.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -x
|
||||
|
||||
OPENSSL_VERSION=1.1.1a
|
||||
OPENSSL_SHA256=fc20130f8b7cbd2fb918b2f14e2f429e109c31ddd0fb38fc5d71d9ffed3f9f41
|
||||
|
||||
curl -O https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz
|
||||
echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar xzf openssl-${OPENSSL_VERSION}.tar.gz
|
||||
cd openssl-${OPENSSL_VERSION}
|
||||
./config shared no-ssl2 no-ssl3 -fPIC --prefix=/usr/local
|
||||
|
||||
sed -i "s/^SHLIB_MAJOR=.*/SHLIB_MAJOR=200/" Makefile && \
|
||||
sed -i "s/^SHLIB_MINOR=.*/SHLIB_MINOR=0.0/" Makefile && \
|
||||
sed -i "s/^SHLIB_VERSION_NUMBER=.*/SHLIB_VERSION_NUMBER=200.0.0/" Makefile
|
||||
|
||||
make depend
|
||||
make
|
||||
make install_sw install_ssldirs
|
||||
ldconfig -v | grep ssl
|
||||
12
scripts/docker-coredeps-arm64/deps/build_perl.sh
Executable file
12
scripts/docker-coredeps-arm64/deps/build_perl.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
PERL_VERSION=5.30.0
|
||||
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
||||
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar -xzf perl-${PERL_VERSION}.tar.gz
|
||||
cd perl-${PERL_VERSION}
|
||||
|
||||
./Configure -de
|
||||
make
|
||||
make install
|
||||
14
scripts/docker-coredeps-arm64/deps/build_python.sh
Executable file
14
scripts/docker-coredeps-arm64/deps/build_python.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
16
scripts/docker-coredeps-arm64/deps/build_rust.sh
Executable file
16
scripts/docker-coredeps-arm64/deps/build_rust.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
#
|
||||
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
curl "https://static.rust-lang.org/dist/rust-1.52.1-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-1.52.1-$(uname -m)-unknown-linux-gnu"
|
||||
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
|
||||
rustc --version
|
||||
cd ..
|
||||
rm -fr "rust-1.52.1-$(uname -m)-unknown-linux-gnu"
|
||||
@@ -5,7 +5,7 @@ RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
||||
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
||||
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
||||
|
||||
# Install a recent Perl, needed to install the openssl crate
|
||||
# Install a recent Perl, needed to install the openssl crate
|
||||
ADD deps/build_perl.sh /builder/build_perl.sh
|
||||
RUN rm /usr/bin/perl
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.5 environment as the base environment
|
||||
/opt/python/cp35-cp35m/bin/pip install tox devpi-client auditwheel
|
||||
# we use the python3.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.5.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.5.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.5.*/bin/auditwheel
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.50.0-x86_64-unknown-linux-gnu -y
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain "1.50.0-$(uname -m)-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.50.0-x86_64-unknown-linux-gnu/share
|
||||
rm -rf "/root/.rustup/toolchains/1.50.0-$(uname -m)-unknown-linux-gnu/share"
|
||||
|
||||
@@ -15,6 +15,7 @@ git ls-files >.rsynclist
|
||||
# we seem to need .git for setuptools_scm versioning
|
||||
find .git >>.rsynclist
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR-arm64"
|
||||
|
||||
set +x
|
||||
|
||||
@@ -26,24 +27,41 @@ set +x
|
||||
# everything is terminated/cleaned up and we have no orphaned
|
||||
# useless still-running docker-containers consuming resources.
|
||||
|
||||
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
|
||||
for arch in "" "-arm64"; do
|
||||
|
||||
ssh $SSHTARGET bash -c "cat >${BUILDDIR}${arch}/exec_docker_run" <<_HERE
|
||||
set +x -e
|
||||
shopt -s huponexit
|
||||
cd $BUILDDIR
|
||||
cd ${BUILDDIR}${arch}
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
set -x
|
||||
|
||||
# run everything else inside docker
|
||||
docker run -e DCC_NEW_TMP_EMAIL \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps scripts/run_all.sh
|
||||
deltachat/coredeps${arch} scripts/run_all.sh
|
||||
|
||||
_HERE
|
||||
|
||||
done
|
||||
|
||||
echo "--- Running $CIRCLE_JOB remotely"
|
||||
|
||||
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
|
||||
echo "--- Building aarch64 wheels"
|
||||
ssh -o ServerAliveInterval=30 -t $SSHTARGET bash "$BUILDDIR-arm64/exec_docker_run"
|
||||
|
||||
echo "--- Building x86_64 wheels"
|
||||
ssh -o ServerAliveInterval=30 -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
|
||||
|
||||
mkdir -p workspace
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
|
||||
|
||||
# Wheels
|
||||
for arch in "" "-arm64"; do
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR${arch}/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
|
||||
done
|
||||
|
||||
# Source packages
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR${arch}/python/.docker-tox/dist/*" workspace/wheelhouse/
|
||||
|
||||
# Documentation
|
||||
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs
|
||||
|
||||
@@ -19,14 +19,17 @@ export DCC_RS_TARGET=release
|
||||
|
||||
# Configure access to a base python and to several python interpreters
|
||||
# needed by tox below.
|
||||
export PATH=$PATH:/opt/python/cp35-cp35m/bin
|
||||
export PATH=$PATH:/opt/python/cp36-cp36m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
pushd /bin
|
||||
rm -f python3.5
|
||||
ln -s /opt/python/cp35-cp35m/bin/python3.5
|
||||
rm -f python3.6
|
||||
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||
rm -f python3.7
|
||||
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||
rm -f python3.8
|
||||
ln -s /opt/python/cp38-cp38/bin/python3.8
|
||||
rm -f python3.9
|
||||
ln -s /opt/python/cp39-cp39/bin/python3.9
|
||||
popd
|
||||
|
||||
pushd python
|
||||
@@ -40,7 +43,7 @@ mkdir -p $TOXWORKDIR
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,auditwheels
|
||||
popd
|
||||
|
||||
|
||||
|
||||
63
spec.md
63
spec.md
@@ -1,6 +1,6 @@
|
||||
# chat-mail specification
|
||||
|
||||
Version: 0.32.0
|
||||
Version: 0.33.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
|
||||
@@ -301,9 +301,9 @@ to add a `Chat-Group-Avatar` only on image changes.
|
||||
|
||||
A user MAY have a profile-image that MAY be distributed to their contacts.
|
||||
To change or set the profile-image,
|
||||
the messenger MUST attach an image file to a message
|
||||
and MUST add the header `Chat-User-Avatar`
|
||||
with the value set to the image name.
|
||||
the messenger MUST add the header `Chat-User-Avatar: base64:IMAGEDATA`.
|
||||
To bypass limits of headers, it is recommended not to use the outer header
|
||||
and to limit the size to 20k.
|
||||
|
||||
To remove the profile-image,
|
||||
the messenger MUST add the header `Chat-User-Avatar: 0`.
|
||||
@@ -320,19 +320,14 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
|
||||
From: sender@domain
|
||||
To: rcpt@domain
|
||||
Chat-Version: 1.0
|
||||
Chat-User-Avatar: photo.jpg
|
||||
Subject: Chat: Hello, ...
|
||||
Content-Type: multipart/mixed; boundary="==break=="
|
||||
|
||||
--==break==
|
||||
Content-Type: text/plain
|
||||
Chat-User-Avatar: base64:AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQY ...
|
||||
|
||||
Hello, I've changed my profile image.
|
||||
--==break==
|
||||
Content-Type: image/jpeg
|
||||
Content-Disposition: attachment; filename="photo.jpg"
|
||||
|
||||
AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQYFBA ...
|
||||
--==break==--
|
||||
|
||||
The image format SHOULD be image/jpeg or image/png.
|
||||
@@ -342,6 +337,11 @@ in the same message.
|
||||
To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
|
||||
only on image changes.
|
||||
|
||||
In older specs, the profile-image was sent as an attachment
|
||||
and `Chat-User-Avatar:` specified its name.
|
||||
However, it turned out that these attachements are kind of unuexpected to users,
|
||||
therefore the profile-image go to the header now.
|
||||
|
||||
|
||||
# Locations
|
||||
|
||||
@@ -401,9 +401,41 @@ it is fine if the location is detected on forwarding etc.
|
||||
</kml>
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
# Stickers
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
Stickers are send as normal images
|
||||
with the additional header `Chat-Content: sticker`.
|
||||
|
||||
It is discouraged to send stickers together with user generated text,
|
||||
however, stickers can be used as a reply to a message
|
||||
and also the footer should be set as usual.
|
||||
|
||||
From: alice@example.org
|
||||
To: bob@example.com
|
||||
Chat-Version: 1.0
|
||||
Chat-Content: sticker
|
||||
Message-ID: Mr.12345uvwxyZ.0005@example.org
|
||||
Subject: Message from Alice
|
||||
Content-Type: multipart/mixed; boundary="==break=="
|
||||
|
||||
--==break==
|
||||
Content-Type: text/plain
|
||||
|
||||
--
|
||||
Hi there! I am using this new messenger!
|
||||
--==break==
|
||||
Content-Type: image/png
|
||||
Content-Disposition: attachment; filename="sticker.png"
|
||||
|
||||
R0lGODlhpAGkAfe9AP+zd2eQkZhrI//z9v++PMb///+scrdDT3BtbtrZ2f/LQSsREcdIVf9 ...
|
||||
--==break==--
|
||||
|
||||
Typical sticker formats are `image/png`, `image/gif` and `image/webp`.
|
||||
Animated stickers are supported
|
||||
by just using an image format that supports animation.
|
||||
|
||||
|
||||
# Voice messages
|
||||
|
||||
Messengers SHOULD add a `Chat-Voice-message: 1` header
|
||||
if an attached audio file is a voice message.
|
||||
@@ -417,6 +449,11 @@ This allows the receiver to show the time without knowing the file format.
|
||||
Chat-Voice-Message: 1
|
||||
Chat-Duration: 10000
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
|
||||
Messengers MAY send and receive Message Disposition Notifications
|
||||
(MDNs, [RFC 8098](https://tools.ietf.org/html/rfc8098),
|
||||
[RFC 3503](https://tools.ietf.org/html/rfc3503))
|
||||
@@ -437,4 +474,4 @@ as the sending time of the message as indicated by its Date header,
|
||||
or the time of first receipt if that date is in the future or unavailable.
|
||||
|
||||
|
||||
Copyright © 2017-2020 Delta Chat contributors.
|
||||
Copyright © 2017-2021 Delta Chat contributors.
|
||||
|
||||
157
src/blob.rs
157
src/blob.rs
@@ -452,7 +452,7 @@ impl<'a> BlobObject<'a> {
|
||||
img.write_to(encoded, image::ImageFormat::Jpeg)?;
|
||||
Ok(())
|
||||
}
|
||||
fn encode_img_exceeds_bytes(
|
||||
fn encoded_img_exceeds_bytes(
|
||||
context: &Context,
|
||||
img: &DynamicImage,
|
||||
max_bytes: Option<usize>,
|
||||
@@ -477,7 +477,7 @@ impl<'a> BlobObject<'a> {
|
||||
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
|
||||
|
||||
let do_scale =
|
||||
exceeds_width || encode_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
|
||||
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
|
||||
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
|
||||
|
||||
if do_scale || do_rotate {
|
||||
@@ -500,7 +500,7 @@ impl<'a> BlobObject<'a> {
|
||||
loop {
|
||||
let new_img = img.thumbnail(img_wh, img_wh);
|
||||
|
||||
if encode_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
|
||||
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
"Failed to scale image to below {}B",
|
||||
@@ -511,6 +511,10 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
img_wh = img_wh * 2 / 3;
|
||||
} else {
|
||||
if encoded.is_empty() {
|
||||
encode_img(&new_img, &mut encoded)?;
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Final scaled-down image size: {}B ({}px)",
|
||||
@@ -617,7 +621,8 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::{message::Message, test_utils::TestContext};
|
||||
use image::Pixel;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create() {
|
||||
@@ -915,4 +920,148 @@ mod tests {
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_recode_image() {
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
|
||||
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
|
||||
.await
|
||||
.unwrap();
|
||||
send_image_check_mediaquality(
|
||||
Some("1"),
|
||||
bytes,
|
||||
1000,
|
||||
1000,
|
||||
0,
|
||||
WORSE_IMAGE_SIZE,
|
||||
WORSE_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
||||
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
bytes,
|
||||
2000,
|
||||
1800,
|
||||
270,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let mut bytes = vec![];
|
||||
img_rotated
|
||||
.write_to(&mut bytes, image::ImageFormat::Jpeg)
|
||||
.unwrap();
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
&bytes,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
0,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("1"),
|
||||
&bytes,
|
||||
BALANCED_IMAGE_SIZE * 1800 / 2000,
|
||||
BALANCED_IMAGE_SIZE,
|
||||
0,
|
||||
WORSE_IMAGE_SIZE * 1800 / 2000,
|
||||
WORSE_IMAGE_SIZE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
}
|
||||
|
||||
fn assert_correct_rotation(img: &DynamicImage) {
|
||||
// The test images are black in the bottom left corner after correctly applying
|
||||
// the EXIF orientation
|
||||
|
||||
let [luma] = img.get_pixel(10, 10).to_luma().0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img
|
||||
.get_pixel(img.width() - 10, img.height() - 10)
|
||||
.to_luma()
|
||||
.0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
|
||||
assert_eq!(luma, 0);
|
||||
}
|
||||
|
||||
async fn send_image_check_mediaquality(
|
||||
media_quality_config: Option<&str>,
|
||||
bytes: &[u8],
|
||||
original_width: u32,
|
||||
original_height: u32,
|
||||
orientation: i32,
|
||||
compressed_width: u32,
|
||||
compressed_height: u32,
|
||||
) -> anyhow::Result<DynamicImage> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(Config::MediaQuality, media_quality_config)
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file.jpg");
|
||||
|
||||
File::create(&file).await?.write_all(bytes).await?;
|
||||
let img = image::open(&file)?;
|
||||
assert_eq!(img.width(), original_width);
|
||||
assert_eq!(img.height(), original_height);
|
||||
|
||||
let blob = BlobObject::new_from_path(&alice, &file).await?;
|
||||
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
let img = image::open(alice_msg.get_file(&alice).unwrap())?;
|
||||
assert_eq!(img.width() as u32, compressed_width);
|
||||
assert_eq!(img.height() as u32, compressed_height);
|
||||
|
||||
bob.recv_msg(&sent).await;
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file = bob_msg.get_file(&bob).unwrap();
|
||||
|
||||
let blob = BlobObject::new_from_path(&bob, &file).await?;
|
||||
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
|
||||
|
||||
let img = image::open(file)?;
|
||||
assert_eq!(img.width() as u32, compressed_width);
|
||||
assert_eq!(img.height() as u32, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
}
|
||||
|
||||
119
src/chat.rs
119
src/chat.rs
@@ -574,10 +574,26 @@ impl ChatId {
|
||||
|
||||
/// Returns number of messages in a chat.
|
||||
pub async fn get_msg_cnt(self, context: &Context) -> Result<usize> {
|
||||
let count = context
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM msgs WHERE chat_id=?", paramsv![self])
|
||||
.await?;
|
||||
let count = if self.is_deaddrop() {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs
|
||||
WHERE hidden=0
|
||||
AND chat_id IN (SELECT id FROM chats WHERE blocked=?)",
|
||||
paramsv![Blocked::Deaddrop],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id=?",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?
|
||||
};
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
@@ -592,17 +608,31 @@ impl ChatId {
|
||||
// the times are average, no matter if there are fresh messages or not -
|
||||
// and have to be multiplied by the number of items shown at once on the chatlist,
|
||||
// so savings up to 2 seconds are possible on older devices - newer ones will feel "snappier" :)
|
||||
let count = context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
let count = if self.is_deaddrop() {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id IN (SELECT id FROM chats WHERE blocked=?)",
|
||||
paramsv![MessageState::InFresh, Blocked::Deaddrop],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs
|
||||
WHERE state=10
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?;
|
||||
paramsv![MessageState::InFresh, self],
|
||||
)
|
||||
.await?
|
||||
};
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
@@ -3066,6 +3096,8 @@ mod tests {
|
||||
use crate::contact::Contact;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
use async_std::fs::File;
|
||||
use async_std::prelude::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_chat_info() {
|
||||
@@ -4003,6 +4035,8 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), DC_CHAT_ID_DEADDROP);
|
||||
assert_eq!(DC_CHAT_ID_DEADDROP.get_msg_cnt(&t).await?, 1);
|
||||
assert_eq!(DC_CHAT_ID_DEADDROP.get_fresh_msg_cnt(&t).await?, 1);
|
||||
let msgs = get_chat_msgs(&t, DC_CHAT_ID_DEADDROP, 0, None).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = match msgs.first().unwrap() {
|
||||
@@ -4023,4 +4057,65 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
let file = alice.get_blobdir().join(filename);
|
||||
File::create(&file).await?.write_all(bytes).await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let mime = sent_msg.payload();
|
||||
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
|
||||
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, bob_chat.id);
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
assert_eq!(msg.get_filename(), Some(filename.to_string()));
|
||||
assert_eq!(msg.get_width(), w);
|
||||
assert_eq!(msg.get_height(), h);
|
||||
assert!(msg.get_filebytes(&bob).await > 250);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_sticker_png() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.png",
|
||||
include_bytes!("../test-data/image/avatar64x64.png"),
|
||||
64,
|
||||
64,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_sticker_jpeg() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.jpg",
|
||||
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_sticker_gif() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.gif",
|
||||
include_bytes!("../test-data/image/image100x50.gif"),
|
||||
100,
|
||||
50,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,18 @@ use crate::stock_str;
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, AsRefStr, EnumIter, EnumProperty,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Display,
|
||||
EnumString,
|
||||
AsRefStr,
|
||||
EnumIter,
|
||||
EnumProperty,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Config {
|
||||
|
||||
@@ -345,10 +345,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
progress!(ctx, 600);
|
||||
|
||||
// Configure IMAP
|
||||
let (_s, r) = async_std::channel::bounded(1);
|
||||
let mut imap = Imap::new(r);
|
||||
|
||||
let mut imap_configured = false;
|
||||
let mut imap: Option<Imap> = None;
|
||||
let imap_servers: Vec<&ServerParams> = servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::Imap)
|
||||
@@ -361,18 +359,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
param.imap.port = imap_server.port;
|
||||
param.imap.security = imap_server.socket;
|
||||
|
||||
match try_imap_one_param(
|
||||
ctx,
|
||||
¶m.imap,
|
||||
¶m.addr,
|
||||
oauth2,
|
||||
provider_strict_tls,
|
||||
&mut imap,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
imap_configured = true;
|
||||
match try_imap_one_param(ctx, ¶m.imap, ¶m.addr, oauth2, provider_strict_tls).await {
|
||||
Ok(configured_imap) => {
|
||||
imap = Some(configured_imap);
|
||||
break;
|
||||
}
|
||||
Err(e) => errors.push(e),
|
||||
@@ -382,9 +371,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
|
||||
);
|
||||
}
|
||||
if !imap_configured {
|
||||
bail!(nicer_configuration_error(ctx, errors).await);
|
||||
}
|
||||
let mut imap = match imap {
|
||||
Some(imap) => imap,
|
||||
None => bail!(nicer_configuration_error(ctx, errors).await),
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
|
||||
@@ -520,26 +510,38 @@ async fn try_imap_one_param(
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
provider_strict_tls: bool,
|
||||
imap: &mut Imap,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
) -> Result<Imap, ConfigurationError> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
|
||||
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if let Err(err) = imap
|
||||
.connect(context, param, addr, oauth2, provider_strict_tls)
|
||||
.await
|
||||
{
|
||||
info!(context, "failure: {}", err);
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
})
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
Ok(())
|
||||
let (_s, r) = async_std::channel::bounded(1);
|
||||
|
||||
let mut imap = match Imap::new(param, addr, oauth2, provider_strict_tls, r).await {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
return Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(imap) => imap,
|
||||
};
|
||||
|
||||
match imap.connect(context).await {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
return Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(()) => {
|
||||
info!(context, "success: {}", inf);
|
||||
return Ok(imap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
138
src/contact.rs
138
src/contact.rs
@@ -236,13 +236,13 @@ impl Contact {
|
||||
}
|
||||
|
||||
/// Block the given contact.
|
||||
pub async fn block(context: &Context, id: u32) {
|
||||
set_block_contact(context, id, true).await;
|
||||
pub async fn block(context: &Context, id: u32) -> Result<()> {
|
||||
set_block_contact(context, id, true).await
|
||||
}
|
||||
|
||||
/// Unblock the given contact.
|
||||
pub async fn unblock(context: &Context, id: u32) {
|
||||
set_block_contact(context, id, false).await;
|
||||
pub async fn unblock(context: &Context, id: u32) -> Result<()> {
|
||||
set_block_contact(context, id, false).await
|
||||
}
|
||||
|
||||
/// Add a single contact as a result of an _explicit_ user action.
|
||||
@@ -270,7 +270,7 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
if blocked {
|
||||
Contact::unblock(context, contact_id).await;
|
||||
Contact::unblock(context, contact_id).await?;
|
||||
}
|
||||
|
||||
Ok(contact_id)
|
||||
@@ -860,7 +860,7 @@ impl Contact {
|
||||
"Can not delete special contact"
|
||||
);
|
||||
|
||||
let count_contacts = context
|
||||
let count_chats = context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
|
||||
@@ -868,19 +868,7 @@ impl Contact {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let count_msgs = if count_contacts > 0 {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
|
||||
paramsv![contact_id as i32, contact_id as i32],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if count_msgs == 0 {
|
||||
if count_chats == 0 {
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -902,9 +890,9 @@ impl Contact {
|
||||
|
||||
info!(
|
||||
context,
|
||||
"could not delete contact {}, there are {} messages with it", contact_id, count_msgs
|
||||
"could not delete contact {}, there are {} chats with it", contact_id, count_chats
|
||||
);
|
||||
bail!("Could not delete contact with messages in it");
|
||||
bail!("Could not delete contact with ongoing chats");
|
||||
}
|
||||
|
||||
/// Get a single contact object. For a list, see eg. dc_get_contacts().
|
||||
@@ -1174,56 +1162,58 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
|
||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return;
|
||||
}
|
||||
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) -> Result<()> {
|
||||
ensure!(
|
||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
||||
"Can't block special contact {}",
|
||||
contact_id
|
||||
);
|
||||
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
if contact.blocked != new_blocking
|
||||
&& context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||
paramsv![new_blocking as i32, contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
|
||||
// non-destructive blocking->unblocking.
|
||||
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
|
||||
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
|
||||
// this would result in recreating the same group...)
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
|
||||
if contact.blocked != new_blocking {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||
paramsv![new_blocking as i32, contact_id as i32],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
|
||||
// non-destructive blocking->unblocking.
|
||||
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
|
||||
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
|
||||
// this would result in recreating the same group...)
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
UPDATE chats
|
||||
SET blocked=?
|
||||
WHERE type=? AND id IN (
|
||||
SELECT chat_id FROM chats_contacts WHERE contact_id=?
|
||||
);
|
||||
"#,
|
||||
paramsv![new_blocking, Chattype::Single, contact_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Contact::mark_noticed(context, contact_id).await;
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
}
|
||||
paramsv![new_blocking, Chattype::Single, contact_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Contact::mark_noticed(context, contact_id).await;
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
}
|
||||
|
||||
// also unblock mailinglist
|
||||
// if the contact is a mailinglist address explicitly created to allow unblocking
|
||||
if !new_blocking && contact.origin == Origin::MailinglistAddress {
|
||||
if let Ok((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await
|
||||
{
|
||||
chat_id.set_blocked(context, Blocked::Not).await;
|
||||
}
|
||||
// also unblock mailinglist
|
||||
// if the contact is a mailinglist address explicitly created to allow unblocking
|
||||
if !new_blocking && contact.origin == Origin::MailinglistAddress {
|
||||
if let Ok((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await {
|
||||
chat_id.set_blocked(context, Blocked::Not).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set profile image for a contact.
|
||||
@@ -1618,6 +1608,34 @@ mod tests {
|
||||
assert!(!contact.is_blocked());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_delete() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
assert!(Contact::delete(&alice, DC_CONTACT_ID_SELF).await.is_err());
|
||||
|
||||
// Create Bob contact
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
|
||||
// Can't delete a contact with ongoing chats.
|
||||
assert!(Contact::delete(&alice, contact_id).await.is_err());
|
||||
|
||||
// Delete chat.
|
||||
chat.get_id().delete(&alice).await?;
|
||||
|
||||
// Can delete contact now.
|
||||
Contact::delete(&alice, contact_id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_remote_authnames() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
@@ -161,7 +161,9 @@ impl Context {
|
||||
|
||||
{
|
||||
let l = &mut *self.inner.scheduler.write().await;
|
||||
l.start(self.clone()).await;
|
||||
if let Err(err) = l.start(self.clone()).await {
|
||||
error!(self, "Failed to start IO: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,19 +540,16 @@ impl Context {
|
||||
|
||||
pub async fn is_sentbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
|
||||
|
||||
Ok(sentbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
|
||||
|
||||
Ok(mvbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
pub async fn is_spam_folder(&self, folder_name: &str) -> Result<bool> {
|
||||
let spam = self.get_config(Config::ConfiguredSpamFolder).await?;
|
||||
|
||||
Ok(spam.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
|
||||
@@ -685,11 +685,26 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
if !context.is_sentbox(server_folder).await?
|
||||
// If the message is outgoing AND there is no Received header AND it's not in the sentbox,
|
||||
// then ignore the email.
|
||||
//
|
||||
// We only apply this heuristic to classical emails, as it is not reliable (some servers
|
||||
// such as systemli.org in June 2021 remove their own Received headers on incoming mails)
|
||||
// and we know Delta Chat never stores drafts on IMAP servers.
|
||||
let is_draft = !context.is_sentbox(server_folder).await?
|
||||
&& mime_parser.get(HeaderDef::Received).is_none()
|
||||
{
|
||||
&& mime_parser.get(HeaderDef::ChatVersion).is_none();
|
||||
|
||||
// Mozilla Thunderbird does not set \Draft flag on "Templates", but sets
|
||||
// X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates
|
||||
// created by Thunderbird.
|
||||
//
|
||||
// This check is not necessary now, but may become useful if the `Received:` header check
|
||||
// is removed completely later.
|
||||
let is_draft = is_draft || mime_parser.get(HeaderDef::XMozillaDraftInfo).is_some();
|
||||
|
||||
if is_draft {
|
||||
// Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
|
||||
// So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
|
||||
info!(context, "Email is probably just a draft (TRASH)");
|
||||
*chat_id = DC_CHAT_ID_TRASH;
|
||||
allow_creation = false;
|
||||
@@ -2742,11 +2757,14 @@ mod tests {
|
||||
|
||||
// Check that the ndn would be downloaded:
|
||||
let headers = mailparse::parse_mail(raw_ndn).unwrap().headers;
|
||||
assert!(
|
||||
crate::imap::prefetch_should_download(&t, &headers, ShowEmails::Off)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
assert!(crate::imap::prefetch_should_download(
|
||||
&t,
|
||||
&headers,
|
||||
std::iter::empty(),
|
||||
ShowEmails::Off
|
||||
)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
dc_receive_imf(&t, raw_ndn, "INBOX", 1, false)
|
||||
.await
|
||||
@@ -3015,7 +3033,9 @@ mod tests {
|
||||
assert_eq!(msgs.len(), 0);
|
||||
|
||||
// Unblock contact and check if the next message arrives in real chat
|
||||
Contact::unblock(&t, *blocked.first().unwrap()).await;
|
||||
Contact::unblock(&t, *blocked.first().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let blocked = Contact::get_all_blocked(&t).await.unwrap();
|
||||
assert_eq!(blocked.len(), 0);
|
||||
|
||||
@@ -3986,4 +4006,33 @@ YEAAAAAA!.
|
||||
.await,
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_dont_show_all_outgoing_msgs_in_self_chat() {
|
||||
// Regression test for https://github.com/deltachat/deltachat-android/issues/1940:
|
||||
// Some servers add a `Bcc: <Self>` header, which caused all outgoing messages to
|
||||
// be shown in the self-chat.
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"Bcc: alice@example.com
|
||||
Received: from [127.0.0.1]
|
||||
Subject: s
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <abcd@gmail.com>
|
||||
To: <me@other.maildomain.com>
|
||||
From: <alice@example.com>
|
||||
|
||||
Message content",
|
||||
"Inbox",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ pub enum HeaderDef {
|
||||
/// we need to check that header as well.
|
||||
XMicrosoftOriginalMessageId,
|
||||
|
||||
/// Thunderbird header used to store Draft information.
|
||||
///
|
||||
/// Thunderbird 78.11.0 does not set \Draft flag on messages saved as "Template", but sets this
|
||||
/// header, so it can be used to ignore such messages.
|
||||
XMozillaDraftInfo,
|
||||
|
||||
ListId,
|
||||
References,
|
||||
InReplyTo,
|
||||
|
||||
429
src/imap.rs
429
src/imap.rs
@@ -8,13 +8,12 @@ use std::{cmp, cmp::max, collections::BTreeMap};
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use async_imap::{
|
||||
error::Result as ImapResult,
|
||||
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
|
||||
types::{Fetch, Flag, Mailbox, Name, NameAttribute},
|
||||
};
|
||||
use async_std::channel::Receiver;
|
||||
use async_std::prelude::*;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::chat;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
Chattype, ShowEmails, Viewtype, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION,
|
||||
@@ -36,6 +35,7 @@ use crate::param::Params;
|
||||
use crate::provider::Socket;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::stock_str;
|
||||
use crate::{chat, constants::DC_CONTACT_ID_SELF};
|
||||
|
||||
mod client;
|
||||
mod idle;
|
||||
@@ -92,6 +92,10 @@ pub struct Imap {
|
||||
interrupt: Option<stop_token::StopSource>,
|
||||
should_reconnect: bool,
|
||||
login_failed_once: bool,
|
||||
|
||||
/// True if CAPABILITY command was run successfully once and config.can_* contain correct
|
||||
/// values.
|
||||
capabilities_determined: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -111,14 +115,27 @@ impl async_imap::Authenticator for OAuth2 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
enum FolderMeaning {
|
||||
Unknown,
|
||||
Spam,
|
||||
SentObjects,
|
||||
Sent,
|
||||
Drafts,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl FolderMeaning {
|
||||
fn to_config(self) -> Option<Config> {
|
||||
match self {
|
||||
FolderMeaning::Unknown => None,
|
||||
FolderMeaning::Spam => Some(Config::ConfiguredSpamFolder),
|
||||
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
|
||||
FolderMeaning::Drafts => None,
|
||||
FolderMeaning::Other => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ImapConfig {
|
||||
pub addr: String,
|
||||
@@ -128,6 +145,7 @@ struct ImapConfig {
|
||||
pub selected_folder: Option<String>,
|
||||
pub selected_mailbox: Option<Mailbox>,
|
||||
pub selected_folder_needs_expunge: bool,
|
||||
|
||||
pub can_idle: bool,
|
||||
|
||||
/// True if the server has MOVE capability as defined in
|
||||
@@ -135,58 +153,93 @@ struct ImapConfig {
|
||||
pub can_move: bool,
|
||||
}
|
||||
|
||||
impl Default for ImapConfig {
|
||||
fn default() -> Self {
|
||||
ImapConfig {
|
||||
addr: "".into(),
|
||||
lp: Default::default(),
|
||||
strict_tls: false,
|
||||
oauth2: false,
|
||||
impl Imap {
|
||||
/// Creates new disconnected IMAP client using the specific login parameters.
|
||||
///
|
||||
/// `addr` is used to renew token if OAuth2 authentication is used.
|
||||
pub async fn new(
|
||||
lp: &ServerLoginParam,
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
provider_strict_tls: bool,
|
||||
idle_interrupt: Receiver<InterruptInfo>,
|
||||
) -> Result<Self> {
|
||||
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
|
||||
bail!("Incomplete IMAP connection parameters");
|
||||
}
|
||||
|
||||
let strict_tls = match lp.certificate_checks {
|
||||
CertificateChecks::Automatic => provider_strict_tls,
|
||||
CertificateChecks::Strict => true,
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => false,
|
||||
};
|
||||
let config = ImapConfig {
|
||||
addr: addr.to_string(),
|
||||
lp: lp.clone(),
|
||||
strict_tls,
|
||||
oauth2,
|
||||
selected_folder: None,
|
||||
selected_mailbox: None,
|
||||
selected_folder_needs_expunge: false,
|
||||
can_idle: false,
|
||||
can_move: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
impl Imap {
|
||||
pub fn new(idle_interrupt: Receiver<InterruptInfo>) -> Self {
|
||||
Imap {
|
||||
let imap = Imap {
|
||||
idle_interrupt,
|
||||
config: Default::default(),
|
||||
session: Default::default(),
|
||||
connected: Default::default(),
|
||||
interrupt: Default::default(),
|
||||
should_reconnect: Default::default(),
|
||||
login_failed_once: Default::default(),
|
||||
config,
|
||||
session: None,
|
||||
connected: false,
|
||||
interrupt: None,
|
||||
should_reconnect: false,
|
||||
login_failed_once: false,
|
||||
capabilities_determined: false,
|
||||
};
|
||||
|
||||
Ok(imap)
|
||||
}
|
||||
|
||||
/// Creates new disconnected IMAP client using configured parameters.
|
||||
pub async fn new_configured(
|
||||
context: &Context,
|
||||
idle_interrupt: Receiver<InterruptInfo>,
|
||||
) -> Result<Self> {
|
||||
if !context.is_configured().await? {
|
||||
bail!("IMAP Connect without configured params");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.connected
|
||||
}
|
||||
let param = LoginParam::from_database(context, "configured_").await?;
|
||||
// the trailing underscore is correct
|
||||
|
||||
pub fn should_reconnect(&self) -> bool {
|
||||
self.should_reconnect
|
||||
}
|
||||
|
||||
pub fn trigger_reconnect(&mut self) {
|
||||
self.should_reconnect = true;
|
||||
let imap = Self::new(
|
||||
¶m.imap,
|
||||
¶m.addr,
|
||||
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
|
||||
param.provider.map_or(false, |provider| provider.strict_tls),
|
||||
idle_interrupt,
|
||||
)
|
||||
.await?;
|
||||
Ok(imap)
|
||||
}
|
||||
|
||||
/// Connects or reconnects if needed.
|
||||
///
|
||||
/// It is safe to call this function if already connected, actions
|
||||
/// are performed only as needed.
|
||||
async fn try_setup_handle(&mut self, context: &Context) -> Result<()> {
|
||||
/// It is safe to call this function if already connected, actions are performed only as needed.
|
||||
///
|
||||
/// Does not emit network errors, can be used to try various parameters during
|
||||
/// autoconfiguration.
|
||||
///
|
||||
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
|
||||
/// instead if you are going to actually use connection rather than trying connection
|
||||
/// parameters.
|
||||
pub async fn connect(&mut self, context: &Context) -> Result<()> {
|
||||
if self.config.lp.server.is_empty() {
|
||||
bail!("IMAP operation attempted while it is torn down");
|
||||
}
|
||||
|
||||
if self.should_reconnect() {
|
||||
self.unsetup_handle(context).await;
|
||||
self.disconnect(context).await;
|
||||
self.should_reconnect = false;
|
||||
} else if self.is_connected() {
|
||||
return Ok(());
|
||||
@@ -256,6 +309,10 @@ impl Imap {
|
||||
self.connected = true;
|
||||
self.session = Some(session);
|
||||
self.login_failed_once = false;
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ImapConnected(format!("IMAP-LOGIN as {}", self.config.lp.user))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -292,20 +349,49 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects or reconnects if not already connected.
|
||||
/// Determine server capabilities if not done yet.
|
||||
async fn determine_capabilities(&mut self) -> Result<()> {
|
||||
if self.capabilities_determined {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match &mut self.session {
|
||||
Some(ref mut session) => match session.capabilities().await {
|
||||
Ok(caps) => {
|
||||
self.config.can_idle = caps.has_str("IDLE");
|
||||
self.config.can_move = caps.has_str("MOVE");
|
||||
self.capabilities_determined = true;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("CAPABILITY command error: {}", err);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
bail!("Can't determine server capabilities because connection was not established")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare for IMAP operation.
|
||||
///
|
||||
/// This function emits network error if it fails. It should not
|
||||
/// be used during configuration to avoid showing failed attempt
|
||||
/// errors to the user.
|
||||
async fn setup_handle(&mut self, context: &Context) -> Result<()> {
|
||||
let res = self.try_setup_handle(context).await;
|
||||
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
|
||||
/// determined.
|
||||
///
|
||||
/// This function emits network error if it fails. It should not be used during configuration
|
||||
/// to avoid showing failed attempt errors to the user.
|
||||
pub async fn prepare(&mut self, context: &Context) -> Result<()> {
|
||||
let res = self.connect(context).await;
|
||||
if let Err(ref err) = res {
|
||||
emit_event!(context, EventType::ErrorNetwork(err.to_string()));
|
||||
}
|
||||
|
||||
self.ensure_configured_folders(context, true).await?;
|
||||
self.determine_capabilities().await?;
|
||||
res
|
||||
}
|
||||
|
||||
async fn unsetup_handle(&mut self, context: &Context) {
|
||||
async fn disconnect(&mut self, context: &Context) {
|
||||
// Close folder if messages should be expunged
|
||||
if let Err(err) = self.close_folder(context).await {
|
||||
warn!(context, "failed to close folder: {:?}", err);
|
||||
@@ -318,139 +404,21 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
self.connected = false;
|
||||
self.capabilities_determined = false;
|
||||
self.config.selected_folder = None;
|
||||
self.config.selected_mailbox = None;
|
||||
}
|
||||
|
||||
async fn free_connect_params(&mut self) {
|
||||
let mut cfg = &mut self.config;
|
||||
|
||||
cfg.addr = "".into();
|
||||
cfg.lp = Default::default();
|
||||
|
||||
cfg.can_idle = false;
|
||||
cfg.can_move = false;
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.connected
|
||||
}
|
||||
|
||||
/// Connects to IMAP account using already-configured parameters.
|
||||
///
|
||||
/// Emits network error if connection fails.
|
||||
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
|
||||
if self.is_connected() && !self.should_reconnect() {
|
||||
return Ok(());
|
||||
}
|
||||
if !context.is_configured().await? {
|
||||
bail!("IMAP Connect without configured params");
|
||||
}
|
||||
|
||||
let param = LoginParam::from_database(context, "configured_").await?;
|
||||
// the trailing underscore is correct
|
||||
|
||||
if let Err(err) = self
|
||||
.connect(
|
||||
context,
|
||||
¶m.imap,
|
||||
¶m.addr,
|
||||
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
|
||||
param.provider.map_or(false, |provider| provider.strict_tls),
|
||||
)
|
||||
.await
|
||||
{
|
||||
bail!("IMAP Connection Failed with params {}: {}", param, err);
|
||||
} else {
|
||||
self.ensure_configured_folders(context, true).await
|
||||
}
|
||||
pub fn should_reconnect(&self) -> bool {
|
||||
self.should_reconnect
|
||||
}
|
||||
|
||||
/// Tries connecting to imap account using the specific login parameters.
|
||||
///
|
||||
/// `addr` is used to renew token if OAuth2 authentication is used.
|
||||
///
|
||||
/// Does not emit network errors, can be used to try various
|
||||
/// parameters during autoconfiguration.
|
||||
pub async fn connect(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
lp: &ServerLoginParam,
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
provider_strict_tls: bool,
|
||||
) -> Result<()> {
|
||||
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
|
||||
bail!("Incomplete IMAP connection parameters");
|
||||
}
|
||||
|
||||
{
|
||||
let mut config = &mut self.config;
|
||||
config.addr = addr.to_string();
|
||||
config.lp = lp.clone();
|
||||
config.strict_tls = match lp.certificate_checks {
|
||||
CertificateChecks::Automatic => provider_strict_tls,
|
||||
CertificateChecks::Strict => true,
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => false,
|
||||
};
|
||||
config.oauth2 = oauth2;
|
||||
}
|
||||
|
||||
if let Err(err) = self.try_setup_handle(context).await {
|
||||
warn!(context, "try_setup_handle: {}", err);
|
||||
self.free_connect_params().await;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let teardown = match &mut self.session {
|
||||
Some(ref mut session) => match session.capabilities().await {
|
||||
Ok(caps) => {
|
||||
if !context.sql.is_open().await {
|
||||
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.user,);
|
||||
true
|
||||
} else {
|
||||
let can_idle = caps.has_str("IDLE");
|
||||
let can_move = caps.has_str("MOVE");
|
||||
let caps_list = caps.iter().fold(String::new(), |s, c| {
|
||||
if let Capability::Atom(x) = c {
|
||||
s + &format!(" {}", x)
|
||||
} else {
|
||||
s + &format!(" {:?}", c)
|
||||
}
|
||||
});
|
||||
|
||||
self.config.can_idle = can_idle;
|
||||
self.config.can_move = can_move;
|
||||
self.connected = true;
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ImapConnected(format!(
|
||||
"IMAP-LOGIN as {}, capabilities: {}",
|
||||
lp.user, caps_list,
|
||||
))
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
info!(context, "CAPABILITY command error: {}", err);
|
||||
true
|
||||
}
|
||||
},
|
||||
None => true,
|
||||
};
|
||||
|
||||
if teardown {
|
||||
self.disconnect(context).await;
|
||||
|
||||
warn!(
|
||||
context,
|
||||
"IMAP disconnected immediately after connecting due to error"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn disconnect(&mut self, context: &Context) {
|
||||
self.unsetup_handle(context).await;
|
||||
self.free_connect_params().await;
|
||||
pub fn trigger_reconnect(&mut self) {
|
||||
self.should_reconnect = true;
|
||||
}
|
||||
|
||||
pub async fn fetch(&mut self, context: &Context, watch_folder: &str) -> Result<()> {
|
||||
@@ -458,7 +426,7 @@ impl Imap {
|
||||
// probably shutdown
|
||||
bail!("IMAP operation attempted while it is torn down");
|
||||
}
|
||||
self.setup_handle(context).await?;
|
||||
self.prepare(context).await?;
|
||||
|
||||
while self
|
||||
.fetch_new_messages(context, &watch_folder, false)
|
||||
@@ -696,6 +664,7 @@ impl Imap {
|
||||
current_uid,
|
||||
&headers,
|
||||
&msg_id,
|
||||
msg.flags(),
|
||||
folder,
|
||||
show_emails,
|
||||
)
|
||||
@@ -974,10 +943,6 @@ impl Imap {
|
||||
(last_uid, read_errors)
|
||||
}
|
||||
|
||||
pub async fn can_move(&self) -> bool {
|
||||
self.config.can_move
|
||||
}
|
||||
|
||||
pub async fn mv(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -1002,7 +967,7 @@ impl Imap {
|
||||
let set = format!("{}", uid);
|
||||
let display_folder_id = format!("{}/{}", folder, uid);
|
||||
|
||||
if self.can_move().await {
|
||||
if self.config.can_move {
|
||||
if let Some(ref mut session) = &mut self.session {
|
||||
match session.uid_mv(&set, &dest_folder).await {
|
||||
Ok(_) => {
|
||||
@@ -1128,7 +1093,7 @@ impl Imap {
|
||||
// TODO: make INBOX/SENT/MVBOX perform the jobs on their
|
||||
// respective folders to avoid select_folder network traffic
|
||||
// and the involved error states
|
||||
if let Err(err) = self.connect_configured(context).await {
|
||||
if let Err(err) = self.prepare(context).await {
|
||||
warn!(context, "prepare_imap_op failed: {}", err);
|
||||
return Some(ImapActionResult::RetryLater);
|
||||
}
|
||||
@@ -1300,9 +1265,8 @@ impl Imap {
|
||||
|
||||
let mut delimiter = ".".to_string();
|
||||
let mut delimiter_is_default = true;
|
||||
let mut sentbox_folder = None;
|
||||
let mut spam_folder = None;
|
||||
let mut mvbox_folder = None;
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
let mut fallback_folder = get_fallback_folder(&delimiter);
|
||||
|
||||
while let Some(folder) = folders.next().await {
|
||||
@@ -1321,31 +1285,26 @@ impl Imap {
|
||||
let folder_meaning = get_folder_meaning(&folder);
|
||||
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
|
||||
if folder.name() == "DeltaChat" {
|
||||
// Always takes precendent
|
||||
// Always takes precedence
|
||||
mvbox_folder = Some(folder.name().to_string());
|
||||
} else if folder.name() == fallback_folder {
|
||||
// only set iff none has been already set
|
||||
// only set if none has been already set
|
||||
if mvbox_folder.is_none() {
|
||||
mvbox_folder = Some(folder.name().to_string());
|
||||
}
|
||||
} else if folder_meaning == FolderMeaning::SentObjects {
|
||||
// Always takes precedent
|
||||
sentbox_folder = Some(folder.name().to_string());
|
||||
} else if folder_meaning == FolderMeaning::Spam {
|
||||
spam_folder = Some(folder.name().to_string());
|
||||
} else if folder_name_meaning == FolderMeaning::SentObjects {
|
||||
// only set iff none has been already set
|
||||
if sentbox_folder.is_none() {
|
||||
sentbox_folder = Some(folder.name().to_string());
|
||||
}
|
||||
} else if folder_name_meaning == FolderMeaning::Spam && spam_folder.is_none() {
|
||||
spam_folder = Some(folder.name().to_string());
|
||||
} else if let Some(config) = folder_meaning.to_config() {
|
||||
// Always takes precedence
|
||||
folder_configs.insert(config, folder.name().to_string());
|
||||
} else if let Some(config) = folder_name_meaning.to_config() {
|
||||
// only set if none has been already set
|
||||
folder_configs
|
||||
.entry(config)
|
||||
.or_insert_with(|| folder.name().to_string());
|
||||
}
|
||||
}
|
||||
drop(folders);
|
||||
|
||||
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
|
||||
info!(context, "sentbox folder is {:?}", sentbox_folder);
|
||||
|
||||
if mvbox_folder.is_none() && create_mvbox {
|
||||
info!(context, "Creating MVBOX-folder \"DeltaChat\"...",);
|
||||
@@ -1393,15 +1352,8 @@ impl Imap {
|
||||
.set_config(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
|
||||
.await?;
|
||||
}
|
||||
if let Some(ref sentbox_folder) = sentbox_folder {
|
||||
context
|
||||
.set_config(Config::ConfiguredSentboxFolder, Some(sentbox_folder))
|
||||
.await?;
|
||||
}
|
||||
if let Some(ref spam_folder) = spam_folder {
|
||||
context
|
||||
.set_config(Config::ConfiguredSpamFolder, Some(spam_folder))
|
||||
.await?;
|
||||
for (config, name) in folder_configs {
|
||||
context.set_config(config, Some(&name)).await?;
|
||||
}
|
||||
context
|
||||
.sql
|
||||
@@ -1474,29 +1426,50 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
|
||||
"迷惑メール",
|
||||
"스팸",
|
||||
];
|
||||
const DRAFT_NAMES: &[&str] = &[
|
||||
"Drafts",
|
||||
"Kladder",
|
||||
"Entw?rfe",
|
||||
"Borradores",
|
||||
"Brouillons",
|
||||
"Bozze",
|
||||
"Concepten",
|
||||
"Wersje robocze",
|
||||
"Rascunhos",
|
||||
"Entwürfe",
|
||||
"Koncepty",
|
||||
"Kopie robocze",
|
||||
"Taslaklar",
|
||||
"Utkast",
|
||||
"Πρόχειρα",
|
||||
"Черновики",
|
||||
"下書き",
|
||||
"草稿",
|
||||
"임시보관함",
|
||||
];
|
||||
let lower = folder_name.to_lowercase();
|
||||
|
||||
if SENT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||
FolderMeaning::SentObjects
|
||||
FolderMeaning::Sent
|
||||
} else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||
FolderMeaning::Spam
|
||||
} else if DRAFT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||
FolderMeaning::Drafts
|
||||
} else {
|
||||
FolderMeaning::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
fn get_folder_meaning(folder_name: &Name) -> FolderMeaning {
|
||||
let special_names = vec!["\\Trash", "\\Drafts"];
|
||||
|
||||
for attr in folder_name.attributes() {
|
||||
if let NameAttribute::Custom(ref label) = attr {
|
||||
if special_names.iter().any(|s| *s == label) {
|
||||
return FolderMeaning::Other;
|
||||
} else if label == "\\Sent" {
|
||||
return FolderMeaning::SentObjects;
|
||||
} else if label == "\\Spam" || label == "\\Junk" {
|
||||
return FolderMeaning::Spam;
|
||||
}
|
||||
match label.as_ref() {
|
||||
"\\Trash" => return FolderMeaning::Other,
|
||||
"\\Sent" => return FolderMeaning::Sent,
|
||||
"\\Spam" | "\\Junk" => return FolderMeaning::Spam,
|
||||
"\\Drafts" => return FolderMeaning::Drafts,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
FolderMeaning::Unknown
|
||||
@@ -1614,6 +1587,7 @@ fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String>
|
||||
pub(crate) async fn prefetch_should_download(
|
||||
context: &Context,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
mut flags: impl Iterator<Item = Flag<'_>>,
|
||||
show_emails: ShowEmails,
|
||||
) -> Result<bool> {
|
||||
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
||||
@@ -1651,12 +1625,17 @@ pub(crate) async fn prefetch_should_download(
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||
.is_some();
|
||||
|
||||
let (_contact_id, blocked_contact, origin) =
|
||||
let (from_id, blocked_contact, origin) =
|
||||
from_field_to_contact_id(context, &mimeparser::get_from(headers), true).await?;
|
||||
// prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact.
|
||||
// (prevent_rename is the last argument of from_field_to_contact_id())
|
||||
let accepted_contact = origin.is_known();
|
||||
|
||||
if flags.any(|f| f == Flag::Draft) && from_id == DC_CONTACT_ID_SELF {
|
||||
info!(context, "Ignoring draft message");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let accepted_contact = origin.is_known();
|
||||
let show = is_autocrypt_setup_message
|
||||
|| match show_emails {
|
||||
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
|
||||
@@ -1675,6 +1654,7 @@ async fn message_needs_processing(
|
||||
current_uid: u32,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
msg_id: &str,
|
||||
flags: impl Iterator<Item = Flag<'_>>,
|
||||
folder: &str,
|
||||
show_emails: ShowEmails,
|
||||
) -> bool {
|
||||
@@ -1698,7 +1678,7 @@ async fn message_needs_processing(
|
||||
// we do not know the message-id
|
||||
// or the message-id is missing (in this case, we create one in the further process)
|
||||
// or some other error happened
|
||||
let show = match prefetch_should_download(context, headers, show_emails).await {
|
||||
let show = match prefetch_should_download(context, headers, flags, show_emails).await {
|
||||
Ok(show) => show,
|
||||
Err(err) => {
|
||||
warn!(context, "prefetch_should_download error: {}", err);
|
||||
@@ -1861,25 +1841,16 @@ mod tests {
|
||||
use crate::test_utils::TestContext;
|
||||
#[test]
|
||||
fn test_get_folder_meaning_by_name() {
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("Gesendet"),
|
||||
FolderMeaning::SentObjects
|
||||
);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("GESENDET"),
|
||||
FolderMeaning::SentObjects
|
||||
);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("gesendet"),
|
||||
FolderMeaning::SentObjects
|
||||
);
|
||||
assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent);
|
||||
assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent);
|
||||
assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("Messages envoyés"),
|
||||
FolderMeaning::SentObjects
|
||||
FolderMeaning::Sent
|
||||
);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
|
||||
FolderMeaning::SentObjects
|
||||
FolderMeaning::Sent
|
||||
);
|
||||
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
|
||||
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);
|
||||
|
||||
@@ -25,7 +25,7 @@ impl Imap {
|
||||
if !self.can_idle() {
|
||||
bail!("IMAP server does not have IDLE capability");
|
||||
}
|
||||
self.setup_handle(context).await?;
|
||||
self.prepare(context).await?;
|
||||
|
||||
self.select_folder(context, watch_folder.as_deref()).await?;
|
||||
|
||||
@@ -158,7 +158,7 @@ impl Imap {
|
||||
// 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 {
|
||||
if let Err(err) = self.prepare(context).await {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::time::Instant;
|
||||
use std::{collections::BTreeMap, time::Instant};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::{config::Config, log::LogExt};
|
||||
use crate::{context::Context, imap::FolderMeaning};
|
||||
use async_std::prelude::*;
|
||||
|
||||
use super::{get_folder_meaning, get_folder_meaning_by_name, FolderMeaning};
|
||||
use super::{get_folder_meaning, get_folder_meaning_by_name};
|
||||
|
||||
impl Imap {
|
||||
pub async fn scan_folders(&mut self, context: &Context) -> Result<()> {
|
||||
@@ -25,14 +25,13 @@ impl Imap {
|
||||
}
|
||||
info!(context, "Starting full folder scan");
|
||||
|
||||
self.setup_handle(context).await?;
|
||||
self.prepare(context).await?;
|
||||
let session = self.session.as_mut();
|
||||
let session = session.context("scan_folders(): IMAP No Connection established")?;
|
||||
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
|
||||
let watched_folders = get_watched_folders(context).await;
|
||||
|
||||
let mut sentbox_folder = None;
|
||||
let mut spam_folder = None;
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
|
||||
for folder in folders {
|
||||
let folder = match folder {
|
||||
@@ -43,38 +42,41 @@ impl Imap {
|
||||
}
|
||||
};
|
||||
|
||||
let foldername = folder.name();
|
||||
let folder_meaning = get_folder_meaning(&folder);
|
||||
let folder_name_meaning = get_folder_meaning_by_name(foldername);
|
||||
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
|
||||
|
||||
if folder_meaning == FolderMeaning::SentObjects {
|
||||
// Always takes precedent
|
||||
sentbox_folder = Some(folder.name().to_string());
|
||||
} else if folder_meaning == FolderMeaning::Spam {
|
||||
spam_folder = Some(folder.name().to_string());
|
||||
} else if folder_name_meaning == FolderMeaning::SentObjects {
|
||||
// only set iff none has been already set
|
||||
if sentbox_folder.is_none() {
|
||||
sentbox_folder = Some(folder.name().to_string());
|
||||
}
|
||||
} else if folder_name_meaning == FolderMeaning::Spam && spam_folder.is_none() {
|
||||
spam_folder = Some(folder.name().to_string());
|
||||
if let Some(config) = folder_meaning.to_config() {
|
||||
// Always takes precedence
|
||||
folder_configs.insert(config, folder.name().to_string());
|
||||
} else if let Some(config) = folder_name_meaning.to_config() {
|
||||
// only set if none has been already set
|
||||
folder_configs
|
||||
.entry(config)
|
||||
.or_insert_with(|| folder.name().to_string());
|
||||
}
|
||||
|
||||
let is_drafts = folder_meaning == FolderMeaning::Drafts
|
||||
|| (folder_meaning == FolderMeaning::Unknown
|
||||
&& folder_name_meaning == FolderMeaning::Drafts);
|
||||
|
||||
// Don't scan folders that are watched anyway
|
||||
if !watched_folders.contains(&foldername.to_string()) {
|
||||
if let Err(e) = self.fetch_new_messages(context, foldername, false).await {
|
||||
warn!(context, "Can't fetch new msgs in scanned folder: {:#}", e);
|
||||
}
|
||||
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
|
||||
self.fetch_new_messages(context, folder.name(), false)
|
||||
.await
|
||||
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
|
||||
}
|
||||
}
|
||||
|
||||
context
|
||||
.set_config(Config::ConfiguredSentboxFolder, sentbox_folder.as_deref())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::ConfiguredSpamFolder, spam_folder.as_deref())
|
||||
.await?;
|
||||
// We iterate over both folder meanings to make sure that if e.g. the "Sent" folder was deleted,
|
||||
// `ConfiguredSentboxFolder` is set to `None`:
|
||||
for config in &[
|
||||
Config::ConfiguredSentboxFolder,
|
||||
Config::ConfiguredSpamFolder,
|
||||
] {
|
||||
context
|
||||
.set_config(*config, folder_configs.get(config).map(|s| s.as_str()))
|
||||
.await?;
|
||||
}
|
||||
|
||||
last_scan.replace(Instant::now());
|
||||
Ok(())
|
||||
|
||||
28
src/imex.rs
28
src/imex.rs
@@ -612,8 +612,13 @@ async fn import_backup_old(context: &Context, backup_to_import: &Path) -> Result
|
||||
|
||||
let mut all_files_extracted = true;
|
||||
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
|
||||
if context.shall_stop_ongoing().await {
|
||||
all_files_extracted = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Load a single blob into memory
|
||||
let (file_name, file_blob) = context
|
||||
match context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
|
||||
@@ -624,12 +629,17 @@ async fn import_backup_old(context: &Context, backup_to_import: &Path) -> Result
|
||||
Ok((file_name, file_blob))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if context.shall_stop_ongoing().await {
|
||||
all_files_extracted = false;
|
||||
break;
|
||||
.await
|
||||
{
|
||||
Ok((file_name, file_blob)) => {
|
||||
let path_filename = context.get_blobdir().join(file_name);
|
||||
dc_write_file(context, &path_filename, &file_blob).await?;
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "Can't import file {}: {}", file_id, err);
|
||||
}
|
||||
}
|
||||
|
||||
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
|
||||
if permille < 10 {
|
||||
permille = 10
|
||||
@@ -638,12 +648,6 @@ async fn import_backup_old(context: &Context, backup_to_import: &Path) -> Result
|
||||
permille = 990
|
||||
}
|
||||
context.emit_event(EventType::ImexProgress(permille));
|
||||
if file_blob.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path_filename = context.get_blobdir().join(file_name);
|
||||
dc_write_file(context, &path_filename, &file_blob).await?;
|
||||
}
|
||||
|
||||
if all_files_extracted {
|
||||
|
||||
10
src/job.rs
10
src/job.rs
@@ -532,7 +532,7 @@ impl Job {
|
||||
}
|
||||
|
||||
async fn move_msg(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
@@ -594,7 +594,7 @@ impl Job {
|
||||
/// records pointing to the same message on the server, the job
|
||||
/// also removes the message on the server.
|
||||
async fn delete_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
@@ -682,7 +682,7 @@ impl Job {
|
||||
if job_try!(context.get_config_bool(Config::Bot).await) {
|
||||
return Status::Finished(Ok(())); // Bots don't want those messages
|
||||
}
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
@@ -755,7 +755,7 @@ impl Job {
|
||||
/// Chat in contrast to the Sent folder, which is normally managed
|
||||
/// by the user via webmail or another email client.
|
||||
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
@@ -779,7 +779,7 @@ impl Job {
|
||||
}
|
||||
|
||||
async fn markseen_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::events::EventType;
|
||||
use crate::job::{self, Action};
|
||||
use crate::log::LogExt;
|
||||
use crate::lot::{Lot, LotState, Meaning};
|
||||
use crate::mimeparser::{FailureReport, SystemMessage};
|
||||
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
use crate::stock_str;
|
||||
@@ -400,7 +400,9 @@ impl Message {
|
||||
let msg = Message {
|
||||
id: row.get("id")?,
|
||||
rfc724_mid: row.get::<_, String>("rfc724mid")?,
|
||||
in_reply_to: row.get::<_, Option<String>>("mime_in_reply_to")?,
|
||||
in_reply_to: row
|
||||
.get::<_, Option<String>>("mime_in_reply_to")?
|
||||
.and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
|
||||
server_folder: row.get::<_, Option<String>>("server_folder")?,
|
||||
server_uid: row.get("server_uid")?,
|
||||
chat_id: row.get("chat_id")?,
|
||||
@@ -1194,7 +1196,11 @@ pub async fn decide_on_contact_request(
|
||||
Err(e) => warn!(context, "decide_on_contact_request error: {}", e),
|
||||
},
|
||||
|
||||
(Block, false) => Contact::block(context, msg.from_id).await,
|
||||
(Block, false) => {
|
||||
if let Err(e) = Contact::block(context, msg.from_id).await {
|
||||
warn!(context, "Can't block contact: {}", e);
|
||||
}
|
||||
}
|
||||
(Block, true) => {
|
||||
if !msg.chat_id.set_blocked(context, Blocked::Manually).await {
|
||||
warn!(context, "Block mailing list failed.")
|
||||
|
||||
@@ -398,7 +398,7 @@ impl MimeMessage {
|
||||
if part.typ == Viewtype::Audio && self.get(HeaderDef::ChatVoiceMessage).is_some() {
|
||||
part.typ = Viewtype::Voice;
|
||||
}
|
||||
if part.typ == Viewtype::Image {
|
||||
if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
|
||||
if let Some(value) = self.get(HeaderDef::ChatContent) {
|
||||
if value == "sticker" {
|
||||
part.typ = Viewtype::Sticker;
|
||||
@@ -1585,7 +1585,7 @@ fn get_attachment_filename(
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||
get_all_addresses_from_header(headers, |header_key| {
|
||||
header_key == "to" || header_key == "cc" || header_key == "bcc"
|
||||
header_key == "to" || header_key == "cc"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2955,4 +2955,46 @@ On 2020-10-25, Bob wrote:
|
||||
Some("Mr.6Dx7ITn4w38.n9j7epIcuQI@outlook.com".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_long_in_reply_to() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// A message with a long Message-ID.
|
||||
// Long message-IDs are generated by Mailjet.
|
||||
let raw = br###"Date: Thu, 28 Jan 2021 00:26:57 +0000
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <ABCDEFGH.1234567_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@mailjet.com>
|
||||
To: Bob <bob@example.org>
|
||||
From: Alice <alice@example.org>
|
||||
Subject: ...
|
||||
|
||||
Some quote.
|
||||
"###;
|
||||
dc_receive_imf(&t, raw, "INBOX", 1, false).await?;
|
||||
|
||||
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
|
||||
let raw = br###"In-Reply-To:
|
||||
<ABCDEFGH.1234567_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@mailjet.com>
|
||||
Date: Thu, 28 Jan 2021 00:26:57 +0000
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <foobar@example.org>
|
||||
To: Alice <alice@example.org>
|
||||
From: Bob <bob@example.org>
|
||||
Subject: ...
|
||||
|
||||
> Some quote.
|
||||
|
||||
Some reply
|
||||
"###;
|
||||
|
||||
dc_receive_imf(&t, raw, "INBOX", 2, false).await?;
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.get_text().unwrap(), "Some reply");
|
||||
let quoted_message = msg.quoted_message(&t).await?.unwrap();
|
||||
assert_eq!(quoted_message.get_text().unwrap(), "Some quote.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,6 +876,35 @@ static P_ROGERS_COM: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// systemausfall.org.md: systemausfall.org, solidaris.me
|
||||
static P_SYSTEMAUSFALL_ORG: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "systemausfall.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/systemausfall-org",
|
||||
server: vec![
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "mail.systemausfall.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "mail.systemausfall.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// systemli.org.md: systemli.org
|
||||
static P_SYSTEMLI_ORG: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "systemli.org",
|
||||
@@ -991,6 +1020,23 @@ static P_TISCALI_IT: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// tutanota.md: tutanota.com, tutanota.de, tutamail.com, tuta.io, keemail.me
|
||||
static P_TUTANOTA: Lazy<Provider> = Lazy::new(|| {
|
||||
Provider {
|
||||
id: "tutanota",
|
||||
status: Status::Broken,
|
||||
before_login_hint: "Tutanota does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Tutanota.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/tutanota",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
oauth2_authorizer: None,
|
||||
}
|
||||
});
|
||||
|
||||
// ukr.net.md: ukr.net
|
||||
static P_UKR_NET: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "ukr.net",
|
||||
@@ -1285,11 +1331,18 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
|
||||
("protonmail.ch", &*P_PROTONMAIL),
|
||||
("riseup.net", &*P_RISEUP_NET),
|
||||
("rogers.com", &*P_ROGERS_COM),
|
||||
("systemausfall.org", &*P_SYSTEMAUSFALL_ORG),
|
||||
("solidaris.me", &*P_SYSTEMAUSFALL_ORG),
|
||||
("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),
|
||||
("tutanota.com", &*P_TUTANOTA),
|
||||
("tutanota.de", &*P_TUTANOTA),
|
||||
("tutamail.com", &*P_TUTANOTA),
|
||||
("tuta.io", &*P_TUTANOTA),
|
||||
("keemail.me", &*P_TUTANOTA),
|
||||
("ukr.net", &*P_UKR_NET),
|
||||
("undernet.uy", &*P_UNDERNET_UY),
|
||||
("vfemail.net", &*P_VFEMAIL),
|
||||
@@ -1389,10 +1442,12 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
("protonmail", &*P_PROTONMAIL),
|
||||
("riseup.net", &*P_RISEUP_NET),
|
||||
("rogers.com", &*P_ROGERS_COM),
|
||||
("systemausfall.org", &*P_SYSTEMAUSFALL_ORG),
|
||||
("systemli.org", &*P_SYSTEMLI_ORG),
|
||||
("t-online", &*P_T_ONLINE),
|
||||
("testrun", &*P_TESTRUN),
|
||||
("tiscali.it", &*P_TISCALI_IT),
|
||||
("tutanota", &*P_TUTANOTA),
|
||||
("ukr.net", &*P_UKR_NET),
|
||||
("undernet.uy", &*P_UNDERNET_UY),
|
||||
("vfemail", &*P_VFEMAIL),
|
||||
@@ -1408,4 +1463,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
});
|
||||
|
||||
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd(2021, 4, 10));
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd(2021, 6, 7));
|
||||
|
||||
@@ -113,7 +113,6 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
None
|
||||
};
|
||||
|
||||
// what is up with that param name?
|
||||
let name = if let Some(encoded_name) = param.get("n") {
|
||||
let encoded_name = encoded_name.replace("+", "%20"); // sometimes spaces are encoded as `+`
|
||||
match percent_decode_str(&encoded_name).decode_utf8() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::{bail, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{
|
||||
channel::{self, Receiver, Sender},
|
||||
@@ -130,7 +131,7 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
async fn fetch(ctx: &Context, connection: &mut Imap) {
|
||||
match ctx.get_config(Config::ConfiguredInboxFolder).await {
|
||||
Ok(Some(watch_folder)) => {
|
||||
if let Err(err) = connection.connect_configured(ctx).await {
|
||||
if let Err(err) = connection.prepare(ctx).await {
|
||||
error_network!(ctx, "{}", err);
|
||||
return;
|
||||
}
|
||||
@@ -159,7 +160,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
match ctx.get_config(folder).await {
|
||||
Ok(Some(watch_folder)) => {
|
||||
// connect and fake idle if unable to connect
|
||||
if let Err(err) = connection.connect_configured(ctx).await {
|
||||
if let Err(err) = connection.prepare(ctx).await {
|
||||
warn!(ctx, "imap connection failed: {}", err);
|
||||
return connection.fake_idle(ctx, Some(watch_folder)).await;
|
||||
}
|
||||
@@ -301,11 +302,11 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
|
||||
|
||||
impl Scheduler {
|
||||
/// Start the scheduler, panics if it is already running.
|
||||
pub async fn start(&mut self, ctx: Context) {
|
||||
let (mvbox, mvbox_handlers) = ImapConnectionState::new();
|
||||
let (sentbox, sentbox_handlers) = ImapConnectionState::new();
|
||||
pub async fn start(&mut self, ctx: Context) -> Result<()> {
|
||||
let (mvbox, mvbox_handlers) = ImapConnectionState::new(&ctx).await?;
|
||||
let (sentbox, sentbox_handlers) = ImapConnectionState::new(&ctx).await?;
|
||||
let (smtp, smtp_handlers) = SmtpConnectionState::new();
|
||||
let (inbox, inbox_handlers) = ImapConnectionState::new();
|
||||
let (inbox, inbox_handlers) = ImapConnectionState::new(&ctx).await?;
|
||||
|
||||
let (inbox_start_send, inbox_start_recv) = channel::bounded(1);
|
||||
let (mvbox_start_send, mvbox_start_recv) = channel::bounded(1);
|
||||
@@ -321,11 +322,7 @@ impl Scheduler {
|
||||
}))
|
||||
};
|
||||
|
||||
if ctx
|
||||
.get_config_bool(Config::MvboxWatch)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if ctx.get_config_bool(Config::MvboxWatch).await? {
|
||||
let ctx = ctx.clone();
|
||||
mvbox_handle = Some(task::spawn(async move {
|
||||
simple_imap_loop(
|
||||
@@ -343,11 +340,7 @@ impl Scheduler {
|
||||
.expect("mvbox start send, missing receiver");
|
||||
}
|
||||
|
||||
if ctx
|
||||
.get_config_bool(Config::SentboxWatch)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if ctx.get_config_bool(Config::SentboxWatch).await? {
|
||||
let ctx = ctx.clone();
|
||||
sentbox_handle = Some(task::spawn(async move {
|
||||
simple_imap_loop(
|
||||
@@ -391,10 +384,11 @@ impl Scheduler {
|
||||
.try_join(smtp_start_recv.recv())
|
||||
.await
|
||||
{
|
||||
error!(ctx, "failed to start scheduler: {}", err);
|
||||
bail!("failed to start scheduler: {}", err);
|
||||
}
|
||||
|
||||
info!(ctx, "scheduler is running");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn maybe_network(&self) {
|
||||
@@ -588,13 +582,13 @@ pub(crate) struct ImapConnectionState {
|
||||
|
||||
impl ImapConnectionState {
|
||||
/// Construct a new connection.
|
||||
fn new() -> (Self, ImapConnectionHandlers) {
|
||||
async fn new(context: &Context) -> Result<(Self, ImapConnectionHandlers)> {
|
||||
let (stop_sender, stop_receiver) = channel::bounded(1);
|
||||
let (shutdown_sender, shutdown_receiver) = channel::bounded(1);
|
||||
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
|
||||
|
||||
let handlers = ImapConnectionHandlers {
|
||||
connection: Imap::new(idle_interrupt_receiver),
|
||||
connection: Imap::new_configured(context, idle_interrupt_receiver).await?,
|
||||
stop_receiver,
|
||||
shutdown_sender,
|
||||
};
|
||||
@@ -607,7 +601,7 @@ impl ImapConnectionState {
|
||||
|
||||
let conn = ImapConnectionState { state };
|
||||
|
||||
(conn, handlers)
|
||||
Ok((conn, handlers))
|
||||
}
|
||||
|
||||
/// Interrupt any form of idle.
|
||||
|
||||
@@ -253,17 +253,17 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum JoinError {
|
||||
#[error("Unknown QR-code")]
|
||||
#[error("Unknown QR-code: {0}")]
|
||||
QrCode(#[from] QrError),
|
||||
#[error("A setup-contact/secure-join protocol is already running")]
|
||||
AlreadyRunning,
|
||||
#[error("An \"ongoing\" process is already running")]
|
||||
OngoingRunning,
|
||||
#[error("Failed to send handshake message")]
|
||||
#[error("Failed to send handshake message: {0}")]
|
||||
SendMessage(#[from] SendMsgError),
|
||||
// Note that this can currently only occur if there is a bug in the QR/Lot code as this
|
||||
// is supposed to create a contact for us.
|
||||
#[error("Unknown contact (this is a bug)")]
|
||||
#[error("Unknown contact (this is a bug): {0}")]
|
||||
UnknownContact(#[source] anyhow::Error),
|
||||
// Note that this can only occur if we failed to create the chat correctly.
|
||||
#[error("Ongoing sender dropped (this is a bug)")]
|
||||
|
||||
42
src/sql.rs
42
src/sql.rs
@@ -198,7 +198,7 @@ impl Sql {
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Opened {:?}.", dbfile);
|
||||
info!(context, "Opened database {:?}.", dbfile);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -641,8 +641,12 @@ async fn maybe_add_from_param(
|
||||
paramsv![],
|
||||
|row| row.get::<_, String>(0),
|
||||
|rows| {
|
||||
for row in rows {
|
||||
let param: Params = row?.parse().unwrap_or_default();
|
||||
// Rows that can't be parsed, for example if they are
|
||||
// not UTF-8 strings, are ignored. It is possible
|
||||
// when upgrading from C core to Rust core, which
|
||||
// guarantees UTF-8 strings everywhere.
|
||||
for row in rows.filter_map(|row| row.ok()) {
|
||||
let param: Params = row.parse().unwrap_or_default();
|
||||
if let Some(file) = param.get(param_id) {
|
||||
maybe_add_file(files_in_use, file);
|
||||
}
|
||||
@@ -802,4 +806,36 @@ mod test {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_migration_flags() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
t.evtracker.get_info_contains("Opened database").await;
|
||||
|
||||
// as migrations::run() was already executed on context creation,
|
||||
// another call should not result in any action needed.
|
||||
// this test catches some bugs where dbversion was forgotten to be persisted.
|
||||
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
|
||||
migrations::run(&t, &t.sql).await?;
|
||||
assert!(!recalc_fingerprints);
|
||||
assert!(!update_icons);
|
||||
assert!(!disable_server_delete);
|
||||
assert!(!recode_avatar);
|
||||
|
||||
info!(&t, "test_migration_flags: XXX");
|
||||
|
||||
loop {
|
||||
if let EventType::Info(info) = t.evtracker.recv().await.unwrap() {
|
||||
assert!(
|
||||
!info.contains("[migration]"),
|
||||
"Migrations were run twice, you probably forgot to update the db version"
|
||||
);
|
||||
if info.contains("test_migration_flags: XXX") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ CREATE TABLE tokens (
|
||||
ALTER TABLE acpeerstates ADD COLUMN verified_key;
|
||||
ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';
|
||||
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);"#,
|
||||
38,
|
||||
39,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -454,7 +454,7 @@ paramsv![]
|
||||
info!(context, "[migration] v75");
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';",
|
||||
74,
|
||||
75,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -466,6 +466,7 @@ paramsv![]
|
||||
if dbversion < 77 {
|
||||
info!(context, "[migration] v77");
|
||||
recode_avatar = true;
|
||||
sql.set_db_version(77).await?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::{collections::BTreeMap, panic};
|
||||
use std::{fmt, thread};
|
||||
|
||||
use ansi_term::Color;
|
||||
use async_std::channel::Receiver;
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::sync::{Arc, RwLock};
|
||||
use async_std::{channel, pin::Pin};
|
||||
@@ -47,6 +48,7 @@ static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
|
||||
pub(crate) struct TestContext {
|
||||
pub ctx: Context,
|
||||
pub dir: TempDir,
|
||||
pub evtracker: EvTracker,
|
||||
/// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only.
|
||||
recv_idx: RwLock<u32>,
|
||||
/// Functions to call for events received.
|
||||
@@ -103,6 +105,8 @@ impl TestContext {
|
||||
let event_sinks: Arc<RwLock<Vec<Box<EventSink>>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
let sinks = Arc::clone(&event_sinks);
|
||||
let (poison_sender, poison_receiver) = channel::bounded(1);
|
||||
let (evtracker_sender, evtracker_receiver) = channel::unbounded();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
// Make sure that the test fails if there is a panic on this thread here:
|
||||
let current_id = task::current().id();
|
||||
@@ -122,13 +126,15 @@ impl TestContext {
|
||||
sink(event.clone()).await;
|
||||
}
|
||||
}
|
||||
receive_event(event);
|
||||
receive_event(&event);
|
||||
evtracker_sender.send(event.typ).await.ok();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
ctx,
|
||||
dir,
|
||||
evtracker: EvTracker(evtracker_receiver),
|
||||
recv_idx: RwLock::new(0),
|
||||
event_sinks,
|
||||
poison_receiver,
|
||||
@@ -568,6 +574,28 @@ pub fn bob_keypair() -> key::KeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EvTracker(Receiver<EventType>);
|
||||
|
||||
impl EvTracker {
|
||||
pub async fn get_info_contains(&self, s: &str) -> EventType {
|
||||
loop {
|
||||
let event = self.0.recv().await.unwrap();
|
||||
if let EventType::Info(i) = &event {
|
||||
if i.contains(s) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EvTracker {
|
||||
type Target = Receiver<EventType>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a specific message from a chat and asserts that the chat has a specific length.
|
||||
///
|
||||
/// Panics if the length of the chat is not `asserted_msgs_count` or if the chat item at `index` is not a Message.
|
||||
@@ -591,12 +619,12 @@ pub(crate) async fn get_chat_msg(
|
||||
/// Pretty-print an event to stdout
|
||||
///
|
||||
/// Done during tests this is captured by `cargo test` and associated with the test itself.
|
||||
fn receive_event(event: Event) {
|
||||
fn receive_event(event: &Event) {
|
||||
let green = Color::Green.normal();
|
||||
let yellow = Color::Yellow.normal();
|
||||
let red = Color::Red.normal();
|
||||
|
||||
let msg = match event.typ {
|
||||
let msg = match &event.typ {
|
||||
EventType::Info(msg) => format!("INFO: {}", msg),
|
||||
EventType::SmtpConnected(msg) => format!("[SMTP_CONNECTED] {}", msg),
|
||||
EventType::ImapConnected(msg) => format!("[IMAP_CONNECTED] {}", msg),
|
||||
|
||||
BIN
test-data/image/rectangle2000x1800-rotated.jpg
Normal file
BIN
test-data/image/rectangle2000x1800-rotated.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
test-data/image/rectangle200x180-rotated.jpg
Normal file
BIN
test-data/image/rectangle200x180-rotated.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Reference in New Issue
Block a user