Compare commits

...

42 Commits

Author SHA1 Message Date
B. Petersen
fd57253cf3 use DC_DOWNLOAD_NO_URL 2021-08-25 13:45:06 +02:00
B. Petersen
25ee17cc6a draft, 2nd round 2021-08-25 13:45:04 +02:00
B. Petersen
1df131c6ae wording 2021-08-25 13:43:46 +02:00
B. Petersen
aa942b0c2a draft a possible download-api 2021-08-25 13:43:05 +02:00
link2xt
3aa2b57ac1 Never ignore SQL errors when reading SOCKS5 settings
Otherwise we may accidentally connect directly due to temporary error.
2021-08-22 23:30:34 +03:00
link2xt
ab1de69fbc mimeparser: rename MimeMessage.get() into MimeMessage.get_header() 2021-08-22 23:21:22 +03:00
Jikstra
90703b0dd2 Implement socks5 support
This adds following settings:

- Socks5Enabled
- Socks5Host
- Socks5Port
- Socks5User
- Socks5Password

Currently http requests and dns requests are not getting executed as they currently can't get tunneled through socks5 proxy. Therefore gmail with oauth2 wont work through tor.
2021-08-22 19:55:38 +02:00
link2xt
a163be9248 Log dc_get_chatlist() errors
Previously errors such as empty query ("missing query") silently
returned NULL.
2021-08-22 16:43:30 +03:00
link2xt
2b7bf11b05 Rust documentation improvements
Document all public modules and some methods.

Make some internal public symbols private.
2021-08-22 15:34:14 +02:00
dependabot[bot]
f95e1db8e2 Merge pull request #2574 from deltachat/dependabot/cargo/serde_json-1.0.66 2021-08-22 11:30:51 +00:00
bjoern
d0c97bce4c tweak quota display (#2616)
* resultify get_connectivity_html()

* tweak quota titles, avoid confusing 'quota' wording

* show bars for quota usage

* skip secondady 'Storage' title, see comment for reasoning
2021-08-22 12:46:46 +02:00
link2xt
3440daca1a Reduce message length limit to 5000 chars (#2615)
- Use the same limit for info: full text can be read in HTML anyway.
- Remove DC_MAX_GET_{TEXT,INFO}_LEN constants from deltachat.h
- Fix a typo: s/DC_ELLIPSE/DC_ELLIPSIS/
- Do not truncate the text when loading from the database.
- Update the documentation: limit is in Rust chars, not bytes
2021-08-21 21:02:14 +02:00
bjoern
d0bfb555dd prepare 1.59 (#2614)
* update changelog for 1.59.0

* bump version to 1.59.0
2021-08-20 18:20:06 +02:00
bjoern
6ffaa38b37 add 'device chat about' to now existing status (#2613)
the 'device chat about' was shown as a single message
as at that time, there was just not 'status'.

meanwhile, we have the status option,
and it feels much more natural to get the information there,
esp. as the subtitle on all UIs already read
'Messages in this chat are generated locally' -
and a tap on that will show the hint, without scrolling or so.

this also teaches the user where to find such information -
and the "welcome" chat is less spammy and really starts with the text
"Welcome to Delta Chat!"
2021-08-20 12:30:55 +02:00
bjoern
339d46ecf0 update provider database, add yggmail and mail2tor (#2608)
* allow dotless domains and hostnames

* update provider database

ran ./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
to pull in recent changes from https://github.com/deltachat/provider-db
2021-08-20 10:48:27 +02:00
bjoern
5399c9151d Add Quota to Connectivity View (#2612)
* add imap::get_quota_roots()

* schedule quote-checking job on getting connectivity-html

* get quota and debug print it

* basic quota output

* update quota at most once per minute, emit event on changes

* use more meaningful names

* add some comments, move update_recent_quota() to quota.rs

* show root name only if there are several roots

* make clippy happy, some refactorings

* allow only one update-quota job per time

* add now supported QUOTA to standards.md
2021-08-20 10:40:24 +02:00
bjoern
53cd633e8d add migrated accounts to events emitter (#2607)
successor of #2559
closes #2606
2021-08-16 22:10:34 +02:00
link2xt
ade39fe026 fix: do not set WantsMdn param for outgoing messages
This bug sometimes results in sending read receipts to self in
multi-device setups.

It happens consistently in a setup where the first device is
configured to move messages to DeltaChat folder and the second device
is not. When both devices receive BCC-self message simultaneously, the
first device moves the message to DeltaChat folder, while the second
device tries to mark the message as seen in the Inbox. Regardless of
whether the second device marks the message as seen successfully or
fails because the message is already moved by the first device,
`Job.markseen_msg_on_imap()` sends the read receipt to the From
address.
2021-08-15 20:39:28 +03:00
B. Petersen
b8dad1dbaf add support for socket PLAIN coming from provider-db 2021-08-15 20:28:09 +03:00
dependabot[bot]
72d503fa32 Merge pull request #2602 from deltachat/dependabot/cargo/bitflags-1.3.1 2021-08-14 16:07:47 +00:00
Hocuri
223aeb7b1a Fix: Make emails forwarded by GMX readable (#2600)
Recognizing these emails as forwarded would probably be too complicated and require too much special-casing, but now the user can access the email text via "Show full message".

fix #2599

Co-authored-by: B. Petersen <r10s@b44t.com>
2021-08-14 18:05:17 +02:00
dependabot[bot]
b315c6f6d5 cargo: bump bitflags from 1.2.1 to 1.3.1
Bumps [bitflags](https://github.com/bitflags/bitflags) from 1.2.1 to 1.3.1.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/1.2.1...1.3.1)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-12 21:16:44 +00:00
Hocuri
481276cf46 In dc_maybe_network_lost() directly set the connectivity "Not connected" (#2551)
* In dc_maybe_network_lost() directly set the connectivity "Not connected"

r10s reported that without doing this, the connectivity would stay at
"Connected" for 16 more seconds after network is gone and
dc_maybe_network_lost() was called.

* Set the state for all connections
2021-08-09 17:25:34 +02:00
Hocuri
faab61b0d4 Connectivity view: Only set smtp to "connected" if the last message was actually sent (#2541)
fix #2539

It's always a bit ambiguous which function should do set_err or set last_send_error, I used this rule here:

If some code returns Status::RetryLater, then it sets last_send_error, because Status::RetryLater means that the user won't see the error directly on the message (if we returned Status::Failed(Err(_)), then the message would be shown as failed to the user) Also, smtp_send always sets last_send_error because, well, sending just failed or succeeded.

Also, remove unused field pending_error.
2021-08-09 12:31:33 +02:00
link2xt
20bf41b4e6 Set timestamps for system messages
Previously system messages were always added to the end of the chat,
even if the message triggering them was sent earlier.  This is
especially important for messages about disappearing timer reset
triggered by classic email messages, as they should be placed right
after the message resetting the timer.
2021-08-08 23:03:38 +03:00
link2xt
5a5b80c960 Resultify get_chat_id_by_grpid and create_or_lookup_mailinglist
Use `Option` instead of `Error` to indicate that no chat ID is found.
2021-08-08 16:26:02 +03:00
Simon Laux
ac245a6cb2 accounts: add get_selected_account_id function 2021-08-07 22:39:48 +03:00
dependabot[bot]
126beb62f3 Merge pull request #2529 from deltachat/dependabot/cargo/libc-0.2.98 2021-08-07 18:10:04 +00:00
dependabot[bot]
1f642046bc cargo: bump serde_json from 1.0.64 to 1.0.66
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.64 to 1.0.66.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.64...v1.0.66)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-07 15:20:51 +00:00
dependabot[bot]
d79e4a6571 Merge pull request #2527 from deltachat/dependabot/cargo/thiserror-1.0.26 2021-08-07 15:19:23 +00:00
dependabot[bot]
cadc0b2c00 Merge pull request #2585 from deltachat/dependabot/cargo/serde-1.0.127 2021-08-07 15:04:32 +00:00
dependabot[bot]
87071e6d4b cargo: bump thiserror from 1.0.25 to 1.0.26
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.25 to 1.0.26.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.25...1.0.26)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-07 14:49:28 +00:00
dependabot[bot]
057b004553 cargo: bump serde from 1.0.126 to 1.0.127
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.126 to 1.0.127.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.126...v1.0.127)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-07 14:45:51 +00:00
link2xt
4071fe53a0 Fix clippy warnings in deltachat-ffi 2021-08-07 12:03:25 +00:00
link2xt
c3062976c0 Allow clippy::bool_assert_comparison
assert_eq!(var, false) is easier to read than assert!(!var).
2021-08-07 11:48:51 +00:00
link2xt
85efc0ea26 Fix clippy warnings 2021-08-07 11:47:50 +00:00
link2xt
4ef80aaea5 ci: update Rust and Python versions used for testing
- Use latest stable for rustfmt and clippy.
- Update Rust in rust-toolchain and coredeps Docker container to 1.54.0
  and test against it.
- Test against 1.48.0 to ensure libdeltachat can be compiled with Debian
  bullseye "rustc" and "cargo".
2021-08-07 14:22:54 +03:00
link2xt
0f86800f5d ci: build python wheels automatically for py-* tags 2021-08-07 14:22:54 +03:00
link2xt
9c2035538c dc_receive_imf: use None instead of ChatId::new(0) 2021-08-07 00:37:31 +03:00
link2xt
0276938975 ci: update perl
Old miniperl segfaults on the latest manylinux image
2021-08-07 00:00:00 +03:00
Michael Mc Donnell
ee44a162b6 Skip Gmail labels
Gmail labels are not folders and should be skipped. For example, emails
appear in the inbox and under "All Mail" as soon as it is received. The
code used to wrongly conclude that the email had already been moved and
left it in the inbox.
2021-08-04 01:11:35 +03:00
dependabot[bot]
49fc72fa42 cargo: bump libc from 0.2.97 to 0.2.98
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.97 to 0.2.98.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.97...0.2.98)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-23 23:15:57 +00:00
74 changed files with 1789 additions and 725 deletions

View File

@@ -18,7 +18,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.50.0
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
@@ -32,7 +32,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.50.0
toolchain: stable
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
@@ -68,12 +68,23 @@ jobs:
strategy:
matrix:
include:
# Currently used Rust version, same as in `rust-toolchain` file.
- os: ubuntu-latest
rust: 1.50.0
python: 3.6
rust: 1.54.0
python: 3.9
- os: windows-latest
rust: 1.50.0
rust: 1.54.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.48.0
# This is the Debian "bullseye" release version of Rust.
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.48.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@master

View File

@@ -1,5 +1,25 @@
# Changelog
## 1.59.0
### Added
- add quota information to `dc_get_connectivity_html()`
### Changes
- refactorings #2592 #2570 #2581
- add 'device chat about' to now existing status #2613
- update provider database #2608
### Fixes
- provider database supports socket=PLAIN and dotless domains now #2604 #2608
- add migrated accounts to events emitter #2607
- fix forwarding quote-only mails #2600
- do not set WantsMdn param for outgoing messages #2603
- set timestamps for system messages #2593
- do not treat gmail labels as folders #2587
- avoid timing problems in `dc_maybe_network_lost()` #2551
- only set smtp to "connected" if the last message was actually sent #2541
## 1.58.0

118
Cargo.lock generated
View File

@@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.15.1"
@@ -236,8 +238,7 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb2df4b37a99456360a9ab475b723e3a499d51e060ab1bdd8d7565d23dcb74b"
source = "git+https://github.com/async-email/async-imap#4ce7da455618c387b87b2905a80935107bc69afc"
dependencies = [
"async-native-tls",
"async-std",
@@ -248,7 +249,7 @@ dependencies = [
"imap-proto",
"lazy_static",
"log",
"nom 5.1.2",
"nom 6.2.1",
"pin-utils",
"rental",
"stop-token",
@@ -324,13 +325,14 @@ dependencies = [
[[package]]
name = "async-smtp"
version = "0.4.0"
source = "git+https://github.com/async-email/async-smtp?rev=c8800625f7cf29f437143ac7e720ac2730a0962f#c8800625f7cf29f437143ac7e720ac2730a0962f"
source = "git+https://github.com/async-email/async-smtp?branch=master#2c21f5fb643e9a24c1097f13db4dfcd7818ada06"
dependencies = [
"async-native-tls",
"async-std",
"async-trait",
"base64 0.13.0",
"bufstream",
"fast-socks5",
"hostname 0.1.5",
"log",
"nom 5.1.2",
@@ -516,9 +518,21 @@ checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719"
[[package]]
name = "bitflags"
version = "1.2.1"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
checksum = "2da1976d75adbe5fbc88130ecd119529cf1cc6a93ae1546d8696ee66f0d21af1"
[[package]]
name = "bitvec"
version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
@@ -1107,7 +1121,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.58.0"
version = "1.59.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1129,9 +1143,11 @@ dependencies = [
"email",
"encoded-words",
"escaper",
"fast-socks5",
"futures",
"futures-lite",
"hex",
"humansize",
"image",
"indexmap",
"itertools 0.10.1",
@@ -1185,7 +1201,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.58.0"
version = "1.59.0"
dependencies = [
"anyhow",
"async-std",
@@ -1501,6 +1517,19 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fast-socks5"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c1955b65d95243f547eb1d1ee6e5ce75ecf6daaeb72b08cd6c66e549d6d88e1"
dependencies = [
"anyhow",
"async-std",
"futures",
"log",
"thiserror",
]
[[package]]
name = "fast_chemail"
version = "0.9.6"
@@ -1584,6 +1613,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "futures"
version = "0.3.16"
@@ -1914,6 +1949,12 @@ dependencies = [
"uuid",
]
[[package]]
name = "humansize"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
[[package]]
name = "humantime"
version = "1.3.0"
@@ -1959,11 +2000,11 @@ dependencies = [
[[package]]
name = "imap-proto"
version = "0.11.0"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3091b99ee5b80f9b010eb6f962af9495ad06561bf662126b077e8ca30e463182"
checksum = "3ad9b46a79efb6078e578ae04e51463d7c3e8767864687f7e63095b3cbefafbb"
dependencies = [
"nom 5.1.2",
"nom 6.2.1",
]
[[package]]
@@ -2120,9 +2161,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.97"
version = "0.2.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
[[package]]
name = "libm"
@@ -2341,6 +2382,19 @@ dependencies = [
"version_check 0.9.3",
]
[[package]]
name = "nom"
version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6"
dependencies = [
"bitvec",
"funty",
"lexical-core",
"memchr",
"version_check 0.9.3",
]
[[package]]
name = "num-bigint"
version = "0.2.6"
@@ -2872,6 +2926,12 @@ dependencies = [
"rusqlite",
]
[[package]]
name = "radium"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]]
name = "radix_trie"
version = "0.2.1"
@@ -3290,9 +3350,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.126"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
dependencies = [
"serde_derive",
]
@@ -3309,9 +3369,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.126"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
dependencies = [
"proc-macro2",
"quote",
@@ -3320,9 +3380,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.64"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
dependencies = [
"itoa",
"ryu",
@@ -3649,6 +3709,12 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.2.0"
@@ -3683,18 +3749,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.25"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6"
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.25"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d"
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
dependencies = [
"proc-macro2",
"quote",
@@ -4164,6 +4230,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]]
name = "x25519-dalek"
version = "1.1.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.58.0"
version = "1.59.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -16,16 +16,16 @@ deltachat_derive = { path = "./deltachat_derive" }
ansi_term = { version = "0.12.1", optional = true }
anyhow = "1.0.42"
async-imap = "0.5.0"
async-imap = { git = "https://github.com/async-email/async-imap" }
async-native-tls = { version = "0.3.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="c8800625f7cf29f437143ac7e720ac2730a0962f" }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
async-std-resolver = "0.20.3"
async-std = { version = "~1.9.0", features = ["unstable"] }
async-tar = "0.3.0"
async-trait = "0.1.50"
backtrace = "0.3.59"
base64 = "0.13"
bitflags = "1.1.0"
bitflags = "1.3.1"
byteorder = "1.3.1"
chrono = "0.4.6"
dirs = { version = "3.0.2", optional=true }
@@ -39,7 +39,7 @@ indexmap = "1.7.0"
itertools = "0.10.1"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2.97"
libc = "0.2.98"
log = {version = "0.4.8", optional = true }
mailparse = "0.13.5"
native-tls = "0.2.3"
@@ -68,10 +68,12 @@ stop-token = "0.2.0"
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.25"
thiserror = "1.0.26"
toml = "0.5.6"
url = "2.2.2"
uuid = { version = "0.8", features = ["serde", "v4"] }
fast-socks5 = "0.4.2"
humansize = "1.1.1"
[dev-dependencies]
ansi_term = "0.12.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.58.0"
version = "1.59.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -22,7 +22,7 @@ num-traits = "0.2.6"
serde_json = "1.0"
async-std = "1.9.0"
anyhow = "1.0.42"
thiserror = "1.0.25"
thiserror = "1.0.26"
rand = "0.7.3"
[features]

View File

@@ -30,6 +30,6 @@ fn main() {
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
.unwrap()
.write_all(&pkg_config.as_bytes())
.write_all(pkg_config.as_bytes())
.unwrap();
}

View File

@@ -271,6 +271,11 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `socks5_enabled` = SOCKS5 enabled
* - `socks5_host` = SOCKS5 proxy server host
* - `socks5_port` = SOCKS5 proxy server port
* - `socks5_user` = SOCKS5 proxy username
* - `socks5_password` = SOCKS5 proxy password
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
@@ -3220,10 +3225,6 @@ int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
#define DC_STATE_OUT_MDN_RCVD 28
#define DC_MAX_GET_TEXT_LEN 30000 // approx. max. length returned by dc_msg_get_text()
#define DC_MAX_GET_INFO_LEN 100000 // approx. max. length returned by dc_get_msg_info()
/**
* Create new message object. Message objects are needed e.g. for sending messages using
* dc_send_msg(). Moreover, they are returned e.g. from dc_get_msg(),
@@ -3902,6 +3903,54 @@ int dc_msg_get_videochat_type (const dc_msg_t* msg);
int dc_msg_has_html (dc_msg_t* msg);
/**
* Check if the message is completely downloaded
* or if some further action is needed.
*
* The function returns one of:
* - @ref DC_DOWNLOAD_NO_URL - The message does not need any further download action
* and should be rendered as usual.
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
* Tn addition to the usual message rendering,
* the UI shall show a download button that starts dc_schedule_download()
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_schedule_download() and is still in progress.
* On progress changes and if the download fails or succeeds,
* the event @ref DC_EVENT_DOWNLOAD_PROGRESS will be emitted.
* - @ref DC_DOWNLOAD_DONE - Download finished successfully
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_schedule_download() again.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return One of the @ref DC_DOWNLOAD values
*/
int dc_msg_download_status(const dc_msg_t* msg);
/**
* Advices the core to start downloading a message.
* This function is typically called when the user hits the "Download" button
* that is shown by the UI in case dc_msg_download_status()
* returns @ref DC_DOWNLOAD_AVAILABLE or @ref DC_DOWNLOAD_FAILURE.
*
* The UI may want to show a file selector and let the user chose a download location.
* The file name in the file selector may be prefilled using dc_msg_get_filename().
*
* During the download, the progress, errors and success
* are reported using @ref DC_EVENT_DOWNLOAD_PROGRESS.
*
* Once the @ref DC_EVENT_DOWNLOAD_PROGRESS reports success,
* The file can be accessed as usual using dc_msg_get_file().
*
* @memberof dc_context_t
* @param context The context object.
* @param path Path to the destination file.
* You can specify NULL here to download
* to a reasonable file name in the internal blob-directory.
* @param msg_id Message-ID to download the content for.
*/
void dc_schedule_download(dc_context_t* context, int msg_id, const char* path);
/**
* Set the text of a message object.
* This does not alter any information in the database; this may be done by dc_send_msg() later.
@@ -5152,6 +5201,16 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_CONNECTIVITY_CHANGED 2100
/**
* Inform about the progress of a download started by dc_schedule_download().
*
* @param data1 (int) Message-ID the progress is reported for.
* @param data2 (int) 0=error, 1-999=progress in permille, 1000=success and done
*/
#define DC_EVENT_DOWNLOAD_PROGRESS 2120
/**
* @}
*/
@@ -5282,6 +5341,31 @@ void dc_event_unref(dc_event_t* event);
/**
* @defgroup DC_DOWNLOAD DC_DOWNLOAD
*
* These constants describe the download state of a message.
* The download state can be retrieved using dc_msg_download_status()
* and usually changes after calling dc_schedule_download().
*
* @addtogroup DC_DOWNLOAD
* @{
*/
#define DC_DOWNLOAD_NO_URL 10 ///< Download not needed, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_AVAILABLE 20 ///< Download available, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_IN_PROGRESS 30 ///< Download in progress, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_DONE 40 ///< Download done, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_FAILURE 50 ///< Download failed, see dc_msg_download_status() for details.
/**
* @}
*/
/*
* TODO: Strings need some doumentation about used placeholders.
*
* @defgroup DC_STR DC_STR
*
* These constants are used to define strings using dc_set_stock_translation().

View File

@@ -217,7 +217,7 @@ pub unsafe extern "C" fn dc_set_config_from_qr(
let ctx = &*context;
block_on(async move {
match qr::set_config_from_qr(&ctx, &qr).await {
match qr::set_config_from_qr(ctx, &qr).await {
Ok(()) => 1,
Err(err) => {
error!(ctx, "Failed to create account from QR code: {}", err);
@@ -275,7 +275,15 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
return "".strdup();
}
let ctx = &*context;
block_on(async move { ctx.get_connectivity_html().await.strdup() })
block_on(async move {
match ctx.get_connectivity_html().await {
Ok(html) => html.strdup(),
Err(err) => {
error!(ctx, "Failed to get connectivity html: {}", err);
"".strdup()
}
}
})
}
#[no_mangle]
@@ -303,9 +311,12 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
let redirect = to_string_lossy(redirect);
block_on(async move {
match oauth2::dc_get_oauth2_url(&ctx, &addr, &redirect).await {
Some(res) => res.strdup(),
None => ptr::null_mut(),
match oauth2::dc_get_oauth2_url(ctx, &addr, &redirect)
.await
.log_err(ctx, "dc_get_oauth2_url failed")
{
Ok(Some(res)) => res.strdup(),
Ok(None) | Err(_) => ptr::null_mut(),
}
})
}
@@ -608,7 +619,7 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
public,
secret,
};
key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default).await?;
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
Ok::<_, anyhow::Error>(1)
})
.log_err(ctx, "Failed to save keypair")
@@ -632,7 +643,10 @@ pub unsafe extern "C" fn dc_get_chatlist(
let qi = if query_id == 0 { None } else { Some(query_id) };
block_on(async move {
match chatlist::Chatlist::try_load(&ctx, flags as usize, qs.as_deref(), qi).await {
match chatlist::Chatlist::try_load(ctx, flags as usize, qs.as_deref(), qi)
.await
.log_err(ctx, "Failed to get chatlist")
{
Ok(list) => {
let ffi_list = ChatlistWrapper { context, list };
Box::into_raw(Box::new(ffi_list))
@@ -654,7 +668,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
let ctx = &*context;
block_on(async move {
ChatId::create_for_contact(&ctx, contact_id)
ChatId::create_for_contact(ctx, contact_id)
.await
.log_err(ctx, "Failed to create chat from contact_id")
.map(|id| id.to_u32())
@@ -674,7 +688,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
let ctx = &*context;
block_on(async move {
ChatId::lookup_by_contact(&ctx, contact_id)
ChatId::lookup_by_contact(ctx, contact_id)
.await
.log_err(ctx, "Failed to get chat for contact_id")
.unwrap_or_default() // unwraps the Result
@@ -697,9 +711,9 @@ pub unsafe extern "C" fn dc_prepare_msg(
let ffi_msg: &mut MessageWrapper = &mut *msg;
block_on(async move {
chat::prepare_msg(&ctx, ChatId::new(chat_id), &mut ffi_msg.message)
chat::prepare_msg(ctx, ChatId::new(chat_id), &mut ffi_msg.message)
.await
.unwrap_or_log_default(&ctx, "Failed to prepare message")
.unwrap_or_log_default(ctx, "Failed to prepare message")
})
.to_u32()
}
@@ -718,9 +732,9 @@ pub unsafe extern "C" fn dc_send_msg(
let ffi_msg = &mut *msg;
block_on(async move {
chat::send_msg(&ctx, ChatId::new(chat_id), &mut ffi_msg.message)
chat::send_msg(ctx, ChatId::new(chat_id), &mut ffi_msg.message)
.await
.unwrap_or_log_default(&ctx, "Failed to send message")
.unwrap_or_log_default(ctx, "Failed to send message")
})
.to_u32()
}
@@ -739,9 +753,9 @@ pub unsafe extern "C" fn dc_send_msg_sync(
let ffi_msg = &mut *msg;
block_on(async move {
chat::send_msg_sync(&ctx, ChatId::new(chat_id), &mut ffi_msg.message)
chat::send_msg_sync(ctx, ChatId::new(chat_id), &mut ffi_msg.message)
.await
.unwrap_or_log_default(&ctx, "Failed to send message")
.unwrap_or_log_default(ctx, "Failed to send message")
})
.to_u32()
}
@@ -760,10 +774,10 @@ pub unsafe extern "C" fn dc_send_text_msg(
let text_to_send = to_string_lossy(text_to_send);
block_on(async move {
chat::send_text_msg(&ctx, ChatId::new(chat_id), text_to_send)
chat::send_text_msg(ctx, ChatId::new(chat_id), text_to_send)
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(&ctx, "Failed to send text message")
.unwrap_or_log_default(ctx, "Failed to send text message")
})
}
@@ -779,10 +793,10 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
let ctx = &*context;
block_on(async move {
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
chat::send_videochat_invitation(ctx, ChatId::new(chat_id))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(&ctx, "Failed to send video chat invitation")
.unwrap_or_log_default(ctx, "Failed to send video chat invitation")
})
}
@@ -806,7 +820,7 @@ pub unsafe extern "C" fn dc_set_draft(
block_on(async move {
ChatId::new(chat_id)
.set_draft(&ctx, msg)
.set_draft(ctx, msg)
.await
.unwrap_or_log_default(ctx, "failed to set draft");
});
@@ -831,9 +845,9 @@ pub unsafe extern "C" fn dc_add_device_msg(
};
block_on(async move {
chat::add_device_msg(&ctx, to_opt_string_lossy(label).as_deref(), msg)
chat::add_device_msg(ctx, to_opt_string_lossy(label).as_deref(), msg)
.await
.unwrap_or_log_default(&ctx, "Failed to add device message")
.unwrap_or_log_default(ctx, "Failed to add device message")
})
.to_u32()
}
@@ -850,7 +864,7 @@ pub unsafe extern "C" fn dc_was_device_msg_ever_added(
let ctx = &mut *context;
block_on(async move {
chat::was_device_msg_ever_added(&ctx, &to_string_lossy(label))
chat::was_device_msg_ever_added(ctx, &to_string_lossy(label))
.await
.unwrap_or(false) as libc::c_int
})
@@ -865,7 +879,7 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
let ctx = &*context;
block_on(async move {
match ChatId::new(chat_id).get_draft(&ctx).await {
match ChatId::new(chat_id).get_draft(ctx).await {
Ok(Some(draft)) => {
let ffi_msg = MessageWrapper {
context,
@@ -902,7 +916,7 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
block_on(async move {
Box::into_raw(Box::new(
chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag)
chat::get_chat_msgs(ctx, ChatId::new(chat_id), flags, marker_flag)
.await
.unwrap_or_log_default(ctx, "failed to get chat msgs")
.into(),
@@ -920,7 +934,7 @@ pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32
block_on(async move {
ChatId::new(chat_id)
.get_msg_cnt(&ctx)
.get_msg_cnt(ctx)
.await
.unwrap_or_log_default(ctx, "failed to get msg count") as libc::c_int
})
@@ -939,7 +953,7 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
block_on(async move {
ChatId::new(chat_id)
.get_fresh_msg_cnt(&ctx)
.get_fresh_msg_cnt(ctx)
.await
.unwrap_or_log_default(ctx, "failed to get fresh msg cnt") as libc::c_int
})
@@ -996,7 +1010,7 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
let ctx = &*context;
block_on(async move {
chat::marknoticed_chat(&ctx, ChatId::new(chat_id))
chat::marknoticed_chat(ctx, ChatId::new(chat_id))
.await
.log_err(ctx, "Failed marknoticed chat")
.unwrap_or(())
@@ -1033,7 +1047,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
block_on(async move {
Box::into_raw(Box::new(
chat::get_chat_media(
&ctx,
ctx,
ChatId::new(chat_id),
msg_type,
or_msg_type2,
@@ -1074,7 +1088,7 @@ pub unsafe extern "C" fn dc_get_next_media(
block_on(async move {
chat::get_next_media(
&ctx,
ctx,
MsgId::new(msg_id),
direction,
msg_type,
@@ -1106,7 +1120,7 @@ pub unsafe extern "C" fn dc_set_chat_protection(
};
block_on(async move {
match ChatId::new(chat_id).set_protection(&ctx, protect).await {
match ChatId::new(chat_id).set_protection(ctx, protect).await {
Ok(()) => 1,
Err(_) => 0,
}
@@ -1139,7 +1153,7 @@ pub unsafe extern "C" fn dc_set_chat_visibility(
block_on(async move {
ChatId::new(chat_id)
.set_visibility(&ctx, visibility)
.set_visibility(ctx, visibility)
.await
.log_err(ctx, "Failed setting chat visibility")
.unwrap_or(())
@@ -1156,7 +1170,7 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
block_on(async move {
ChatId::new(chat_id)
.delete(&ctx)
.delete(ctx)
.await
.ok_or_log_msg(ctx, "Failed chat delete");
})
@@ -1172,7 +1186,7 @@ pub unsafe extern "C" fn dc_block_chat(context: *mut dc_context_t, chat_id: u32)
block_on(async move {
ChatId::new(chat_id)
.block(&ctx)
.block(ctx)
.await
.ok_or_log_msg(ctx, "Failed chat block");
})
@@ -1188,7 +1202,7 @@ pub unsafe extern "C" fn dc_accept_chat(context: *mut dc_context_t, chat_id: u32
block_on(async move {
ChatId::new(chat_id)
.accept(&ctx)
.accept(ctx)
.await
.ok_or_log_msg(ctx, "Failed chat accept");
})
@@ -1207,7 +1221,7 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
block_on(async move {
let arr = dc_array_t::from(
chat::get_chat_contacts(&ctx, ChatId::new(chat_id))
chat::get_chat_contacts(ctx, ChatId::new(chat_id))
.await
.unwrap_or_log_default(ctx, "Failed get_chat_contacts"),
);
@@ -1254,7 +1268,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
let ctx = &*context;
block_on(async move {
match chat::Chat::load_from_db(&ctx, ChatId::new(chat_id)).await {
match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await {
Ok(chat) => {
let ffi_chat = ChatWrapper { context, chat };
Box::into_raw(Box::new(ffi_chat))
@@ -1283,7 +1297,7 @@ pub unsafe extern "C" fn dc_create_group_chat(
};
block_on(async move {
chat::create_group_chat(&ctx, protect, &to_string_lossy(name))
chat::create_group_chat(ctx, protect, &to_string_lossy(name))
.await
.log_err(ctx, "Failed to create group chat")
.map(|id| id.to_u32())
@@ -1303,7 +1317,7 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
}
let ctx = &*context;
block_on(async move { chat::is_contact_in_chat(&ctx, ChatId::new(chat_id), contact_id).await })
block_on(async move { chat::is_contact_in_chat(ctx, ChatId::new(chat_id), contact_id).await })
.into()
}
@@ -1320,7 +1334,7 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
let ctx = &*context;
block_on(async move {
chat::add_contact_to_chat(&ctx, ChatId::new(chat_id), contact_id).await as libc::c_int
chat::add_contact_to_chat(ctx, ChatId::new(chat_id), contact_id).await as libc::c_int
})
}
@@ -1337,10 +1351,10 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
let ctx = &*context;
block_on(async move {
chat::remove_contact_from_chat(&ctx, ChatId::new(chat_id), contact_id)
chat::remove_contact_from_chat(ctx, ChatId::new(chat_id), contact_id)
.await
.map(|_| 1)
.unwrap_or_log_default(&ctx, "Failed to remove contact")
.unwrap_or_log_default(ctx, "Failed to remove contact")
})
}
@@ -1358,10 +1372,10 @@ pub unsafe extern "C" fn dc_set_chat_name(
let ctx = &*context;
block_on(async move {
chat::set_chat_name(&ctx, ChatId::new(chat_id), &to_string_lossy(name))
chat::set_chat_name(ctx, ChatId::new(chat_id), &to_string_lossy(name))
.await
.map(|_| 1)
.unwrap_or_log_default(&ctx, "Failed to set chat name")
.unwrap_or_log_default(ctx, "Failed to set chat name")
})
}
@@ -1378,10 +1392,10 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
let ctx = &*context;
block_on(async move {
chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), to_string_lossy(image))
chat::set_chat_profile_image(ctx, ChatId::new(chat_id), to_string_lossy(image))
.await
.map(|_| 1)
.unwrap_or_log_default(&ctx, "Failed to set profile image")
.unwrap_or_log_default(ctx, "Failed to set profile image")
})
}
@@ -1412,10 +1426,10 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
};
block_on(async move {
chat::set_muted(&ctx, ChatId::new(chat_id), muteDuration)
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
.await
.map(|_| 1)
.unwrap_or_log_default(&ctx, "Failed to set mute duration")
.unwrap_or_log_default(ctx, "Failed to set mute duration")
})
}
@@ -1432,11 +1446,11 @@ pub unsafe extern "C" fn dc_get_chat_encrinfo(
block_on(async move {
ChatId::new(chat_id)
.get_encryption_info(&ctx)
.get_encryption_info(ctx)
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
error!(&ctx, "{}", e);
error!(ctx, "{}", e);
ptr::null_mut()
})
})
@@ -1497,7 +1511,7 @@ pub unsafe extern "C" fn dc_get_msg_info(
let ctx = &*context;
block_on(async move {
message::get_msg_info(&ctx, MsgId::new(msg_id))
message::get_msg_info(ctx, MsgId::new(msg_id))
.await
.unwrap_or_log_default(ctx, "failed to get msg id")
.strdup()
@@ -1515,7 +1529,7 @@ pub unsafe extern "C" fn dc_get_msg_html(
}
let ctx = &*context;
block_on(MsgId::new(msg_id).get_html(&ctx))
block_on(MsgId::new(msg_id).get_html(ctx))
.unwrap_or_log_default(ctx, "Failed get_msg_html")
.strdup()
}
@@ -1532,7 +1546,7 @@ pub unsafe extern "C" fn dc_get_mime_headers(
let ctx = &*context;
block_on(async move {
let mime = message::get_mime_headers(&ctx, MsgId::new(msg_id))
let mime = message::get_mime_headers(ctx, MsgId::new(msg_id))
.await
.unwrap_or_log_default(ctx, "failed to get mime headers");
if mime.is_empty() {
@@ -1555,7 +1569,7 @@ pub unsafe extern "C" fn dc_delete_msgs(
let ctx = &*context;
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
block_on(message::delete_msgs(&ctx, &msg_ids))
block_on(message::delete_msgs(ctx, &msg_ids))
}
#[no_mangle]
@@ -1577,9 +1591,9 @@ pub unsafe extern "C" fn dc_forward_msgs(
let ctx = &*context;
block_on(async move {
chat::forward_msgs(&ctx, &msg_ids[..], ChatId::new(chat_id))
chat::forward_msgs(ctx, &msg_ids[..], ChatId::new(chat_id))
.await
.unwrap_or_log_default(&ctx, "Failed to forward message")
.unwrap_or_log_default(ctx, "Failed to forward message")
})
}
@@ -1596,7 +1610,7 @@ pub unsafe extern "C" fn dc_markseen_msgs(
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
let ctx = &*context;
block_on(message::markseen_msgs(&ctx, msg_ids))
block_on(message::markseen_msgs(ctx, msg_ids))
.log_err(ctx, "failed dc_markseen_msgs() call")
.ok();
}
@@ -1610,19 +1624,19 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
let ctx = &*context;
block_on(async move {
let message = match message::Message::load_from_db(&ctx, MsgId::new(msg_id)).await {
let message = match message::Message::load_from_db(ctx, MsgId::new(msg_id)).await {
Ok(msg) => msg,
Err(e) => {
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
// C-core API returns empty messages, do the same
warn!(
&ctx,
ctx,
"dc_get_msg called with special msg_id={}, returning empty msg", msg_id
);
message::Message::default()
} else {
error!(
&ctx,
ctx,
"dc_get_msg could not retrieve msg_id {}: {}", msg_id, e
);
return ptr::null_mut();
@@ -1656,7 +1670,7 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
let ctx = &*context;
block_on(async move {
Contact::lookup_id_by_addr(&ctx, to_string_lossy(addr), Origin::IncomingReplyTo)
Contact::lookup_id_by_addr(ctx, to_string_lossy(addr), Origin::IncomingReplyTo)
.await
.unwrap_or_log_default(ctx, "failed to lookup id")
.unwrap_or(0)
@@ -1677,7 +1691,7 @@ pub unsafe extern "C" fn dc_create_contact(
let name = to_string_lossy(name);
block_on(async move {
Contact::create(&ctx, &name, &to_string_lossy(addr))
Contact::create(ctx, &name, &to_string_lossy(addr))
.await
.unwrap_or(0)
})
@@ -1695,7 +1709,7 @@ pub unsafe extern "C" fn dc_add_address_book(
let ctx = &*context;
block_on(async move {
match Contact::add_address_book(&ctx, &to_string_lossy(addr_book)).await {
match Contact::add_address_book(ctx, &to_string_lossy(addr_book)).await {
Ok(cnt) => cnt as libc::c_int,
Err(_) => 0,
}
@@ -1716,7 +1730,7 @@ pub unsafe extern "C" fn dc_get_contacts(
let query = to_opt_string_lossy(query);
block_on(async move {
match Contact::get_all(&ctx, flags, query).await {
match Contact::get_all(ctx, flags, query).await {
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(contacts))),
Err(_) => ptr::null_mut(),
}
@@ -1732,7 +1746,7 @@ pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc:
let ctx = &*context;
block_on(async move {
Contact::get_all_blocked(&ctx)
Contact::get_all_blocked(ctx)
.await
.unwrap_or_log_default(ctx, "failed to get blocked count")
.len() as libc::c_int
@@ -1751,9 +1765,9 @@ pub unsafe extern "C" fn dc_get_blocked_contacts(
block_on(async move {
Box::into_raw(Box::new(dc_array_t::from(
Contact::get_all_blocked(&ctx)
Contact::get_all_blocked(ctx)
.await
.log_err(&ctx, "Can't get blocked contacts")
.log_err(ctx, "Can't get blocked contacts")
.unwrap_or_default(),
)))
})
@@ -1772,13 +1786,13 @@ pub unsafe extern "C" fn dc_block_contact(
let ctx = &*context;
block_on(async move {
if block == 0 {
Contact::unblock(&ctx, contact_id)
Contact::unblock(ctx, contact_id)
.await
.ok_or_log_msg(&ctx, "Can't unblock contact");
.ok_or_log_msg(ctx, "Can't unblock contact");
} else {
Contact::block(&ctx, contact_id)
Contact::block(ctx, contact_id)
.await
.ok_or_log_msg(&ctx, "Can't block contact");
.ok_or_log_msg(ctx, "Can't block contact");
}
});
}
@@ -1795,11 +1809,11 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
let ctx = &*context;
block_on(async move {
Contact::get_encrinfo(&ctx, contact_id)
Contact::get_encrinfo(ctx, contact_id)
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
error!(&ctx, "{}", e);
error!(ctx, "{}", e);
ptr::null_mut()
})
})
@@ -1817,7 +1831,7 @@ pub unsafe extern "C" fn dc_delete_contact(
let ctx = &*context;
block_on(async move {
match Contact::delete(&ctx, contact_id).await {
match Contact::delete(ctx, contact_id).await {
Ok(_) => 1,
Err(_) => 0,
}
@@ -1836,7 +1850,7 @@ pub unsafe extern "C" fn dc_get_contact(
let ctx = &*context;
block_on(async move {
Contact::get_by_id(&ctx, contact_id)
Contact::get_by_id(ctx, contact_id)
.await
.map(|contact| Box::into_raw(Box::new(ContactWrapper { context, contact })))
.unwrap_or_else(|_| ptr::null_mut())
@@ -1866,7 +1880,7 @@ pub unsafe extern "C" fn dc_imex(
if let Some(param1) = to_opt_string_lossy(param1) {
spawn(async move {
imex::imex(&ctx, what, param1.as_ref())
imex::imex(ctx, what, param1.as_ref())
.await
.log_err(ctx, "IMEX failed")
});
@@ -1887,12 +1901,12 @@ pub unsafe extern "C" fn dc_imex_has_backup(
let ctx = &*context;
block_on(async move {
match imex::has_backup(&ctx, to_string_lossy(dir).as_ref()).await {
match imex::has_backup(ctx, to_string_lossy(dir).as_ref()).await {
Ok(res) => res.strdup(),
Err(err) => {
// do not bubble up error to the user,
// the ui will expect that the file does not exist or cannot be accessed
warn!(&ctx, "dc_imex_has_backup: {}", err);
warn!(ctx, "dc_imex_has_backup: {}", err);
ptr::null_mut()
}
}
@@ -1908,10 +1922,10 @@ pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) ->
let ctx = &*context;
block_on(async move {
match imex::initiate_key_transfer(&ctx).await {
match imex::initiate_key_transfer(ctx).await {
Ok(res) => res.strdup(),
Err(err) => {
error!(&ctx, "dc_initiate_key_transfer(): {}", err);
error!(ctx, "dc_initiate_key_transfer(): {}", err);
ptr::null_mut()
}
}
@@ -1934,12 +1948,12 @@ pub unsafe extern "C" fn dc_continue_key_transfer(
let ctx = &*context;
block_on(async move {
match imex::continue_key_transfer(&ctx, MsgId::new(msg_id), &to_string_lossy(setup_code))
match imex::continue_key_transfer(ctx, MsgId::new(msg_id), &to_string_lossy(setup_code))
.await
{
Ok(()) => 1,
Err(err) => {
warn!(&ctx, "dc_continue_key_transfer: {}", err);
warn!(ctx, "dc_continue_key_transfer: {}", err);
0
}
}
@@ -1968,7 +1982,7 @@ pub unsafe extern "C" fn dc_check_qr(
let ctx = &*context;
block_on(async move {
let lot = qr::check_qr(&ctx, &to_string_lossy(qr)).await;
let lot = qr::check_qr(ctx, &to_string_lossy(qr)).await;
Box::into_raw(Box::new(lot))
})
}
@@ -1990,7 +2004,7 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
};
block_on(async move {
securejoin::dc_get_securejoin_qr(&ctx, chat_id)
securejoin::dc_get_securejoin_qr(ctx, chat_id)
.await
.unwrap_or_else(|| "".to_string())
.strdup()
@@ -2009,7 +2023,7 @@ pub unsafe extern "C" fn dc_join_securejoin(
let ctx = &*context;
block_on(async move {
securejoin::dc_join_securejoin(&ctx, &to_string_lossy(qr))
securejoin::dc_join_securejoin(ctx, &to_string_lossy(qr))
.await
.map(|chatid| chatid.to_u32())
.log_err(ctx, "failed dc_join_securejoin() call")
@@ -2030,7 +2044,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
let ctx = &*context;
block_on(location::send_locations_to_chat(
&ctx,
ctx,
ChatId::new(chat_id),
seconds as i64,
));
@@ -2052,7 +2066,7 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
Some(ChatId::new(chat_id))
};
block_on(location::is_sending_locations_to_chat(&ctx, chat_id)) as libc::c_int
block_on(location::is_sending_locations_to_chat(ctx, chat_id)) as libc::c_int
}
#[no_mangle]
@@ -2068,7 +2082,7 @@ pub unsafe extern "C" fn dc_set_location(
}
let ctx = &*context;
block_on(location::set(&ctx, latitude, longitude, accuracy)) as _
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
}
#[no_mangle]
@@ -2097,7 +2111,7 @@ pub unsafe extern "C" fn dc_get_locations(
block_on(async move {
let res = location::get_range(
&ctx,
ctx,
chat_id,
contact_id,
timestamp_begin as i64,
@@ -2118,7 +2132,7 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
let ctx = &*context;
block_on(async move {
location::delete_all(&ctx)
location::delete_all(ctx)
.await
.log_err(ctx, "Failed to delete locations")
.ok()
@@ -2385,7 +2399,7 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
block_on(async move {
let lot = ffi_list
.list
.get_summary(&ctx, index as usize, maybe_chat)
.get_summary(ctx, index as usize, maybe_chat)
.await
.log_err(ctx, "get_summary failed")
.unwrap_or_default();
@@ -2410,7 +2424,7 @@ pub unsafe extern "C" fn dc_chatlist_get_summary2(
Some(MsgId::new(msg_id))
};
block_on(async move {
let lot = Chatlist::get_summary2(&ctx, ChatId::new(chat_id), msg_id, None)
let lot = Chatlist::get_summary2(ctx, ChatId::new(chat_id), msg_id, None)
.await
.log_err(ctx, "get_summary2 failed")
.unwrap_or_default();
@@ -2496,7 +2510,7 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
let ctx = &*ffi_chat.context;
block_on(async move {
match ffi_chat.chat.get_profile_image(&ctx).await {
match ffi_chat.chat.get_profile_image(ctx).await {
Ok(Some(p)) => p.to_string_lossy().strdup(),
Ok(None) => ptr::null_mut(),
Err(err) => {
@@ -2516,7 +2530,7 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
let ffi_chat = &*chat;
let ctx = &*ffi_chat.context;
block_on(ffi_chat.chat.get_color(&ctx)).unwrap_or_log_default(ctx, "Failed get_color")
block_on(ffi_chat.chat.get_color(ctx)).unwrap_or_log_default(ctx, "Failed get_color")
}
#[no_mangle]
@@ -2647,25 +2661,25 @@ pub unsafe extern "C" fn dc_chat_get_info_json(
let ctx = &*context;
block_on(async move {
let chat = match chat::Chat::load_from_db(&ctx, ChatId::new(chat_id)).await {
let chat = match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await {
Ok(chat) => chat,
Err(err) => {
error!(&ctx, "dc_get_chat_info_json() failed to load chat: {}", err);
error!(ctx, "dc_get_chat_info_json() failed to load chat: {}", err);
return "".strdup();
}
};
let info = match chat.get_info(&ctx).await {
let info = match chat.get_info(ctx).await {
Ok(info) => info,
Err(err) => {
error!(
&ctx,
ctx,
"dc_get_chat_info_json() failed to get chat info: {}", err
);
return "".strdup();
}
};
serde_json::to_string(&info)
.unwrap_or_log_default(&ctx, "dc_get_chat_info_json() failed to serialise to json")
.unwrap_or_log_default(ctx, "dc_get_chat_info_json() failed to serialise to json")
.strdup()
})
}
@@ -2866,7 +2880,7 @@ pub unsafe extern "C" fn dc_msg_get_filebytes(msg: *mut dc_msg_t) -> u64 {
let ffi_msg = &*msg;
let ctx = &*ffi_msg.context;
block_on(ffi_msg.message.get_filebytes(&ctx))
block_on(ffi_msg.message.get_filebytes(ctx))
}
#[no_mangle]
@@ -2958,7 +2972,7 @@ pub unsafe extern "C" fn dc_msg_get_summary(
let ctx = &*ffi_msg.context;
block_on(async move {
let lot = ffi_msg.message.get_summary(&ctx, maybe_chat).await;
let lot = ffi_msg.message.get_summary(ctx, maybe_chat).await;
Box::into_raw(Box::new(lot))
})
}
@@ -2978,7 +2992,7 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
block_on({
ffi_msg
.message
.get_summarytext(&ctx, approx_characters.try_into().unwrap_or_default())
.get_summarytext(ctx, approx_characters.try_into().unwrap_or_default())
})
.strdup()
}
@@ -3118,7 +3132,7 @@ pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut l
let ffi_msg = &*msg;
let ctx = &*ffi_msg.context;
block_on(ffi_msg.message.get_setupcodebegin(&ctx))
block_on(ffi_msg.message.get_setupcodebegin(ctx))
.unwrap_or_default()
.strdup()
}
@@ -3230,7 +3244,7 @@ pub unsafe extern "C" fn dc_msg_latefiling_mediasize(
block_on({
ffi_msg
.message
.latefiling_mediasize(&ctx, width, height, duration)
.latefiling_mediasize(ctx, width, height, duration)
});
}
@@ -3410,7 +3424,7 @@ pub unsafe extern "C" fn dc_contact_get_profile_image(
block_on(async move {
ffi_contact
.contact
.get_profile_image(&ctx)
.get_profile_image(ctx)
.await
.unwrap_or_log_default(ctx, "failed to get profile image")
.map(|p| p.to_string_lossy().strdup())
@@ -3457,7 +3471,7 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
let ffi_contact = &*contact;
let ctx = &*ffi_contact.context;
block_on(async move { ffi_contact.contact.is_verified(&ctx).await as libc::c_int })
block_on(async move { ffi_contact.contact.is_verified(ctx).await as libc::c_int })
}
// dc_lot_t
@@ -3603,9 +3617,22 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
return ptr::null();
}
let addr = to_string_lossy(addr);
match block_on(provider::get_provider_info(addr.as_str())) {
Some(provider) => provider,
None => ptr::null_mut(),
let ctx = &*context;
let socks5_enabled = block_on(async move {
ctx.get_config_bool(config::Config::Socks5Enabled)
.await
.log_err(ctx, "Can't get config")
});
match socks5_enabled {
Ok(socks5_enabled) => {
match block_on(provider::get_provider_info(addr.as_str(), socks5_enabled)) {
Some(provider) => provider,
None => ptr::null_mut(),
}
}
Err(_) => ptr::null_mut(),
}
}

View File

@@ -17,15 +17,12 @@ use std::ptr;
/// }
/// ```
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
let ret: *mut libc::c_char;
if !s.is_null() {
ret = libc::strdup(s);
assert!(!ret.is_null());
let ret: *mut libc::c_char = if !s.is_null() {
libc::strdup(s)
} else {
ret = libc::calloc(1, 1) as *mut libc::c_char;
assert!(!ret.is_null());
}
libc::calloc(1, 1) as *mut libc::c_char
};
assert!(!ret.is_null());
ret
}

View File

@@ -514,9 +514,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let file = dirs::home_dir()
.unwrap_or_default()
.join("connectivity.html");
let html = context.get_connectivity_html().await;
fs::write(&file, html)?;
println!("Report written to: {:#?}", file);
match context.get_connectivity_html().await {
Ok(html) => {
fs::write(&file, html)?;
println!("Report written to: {:#?}", file);
}
Err(err) => {
bail!("Failed to get connectivity html: {}", err);
}
}
}
"maybenetwork" => {
context.maybe_network().await;
@@ -1162,7 +1168,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1).await {
let socks5_enabled = context
.get_config_bool(config::Config::Socks5Enabled)
.await?;
match provider::get_provider_info(arg1, socks5_enabled).await {
Some(info) => {
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);

View File

@@ -392,7 +392,7 @@ async fn handle_cmd(
"oauth2" => {
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
let oauth2_url =
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
if oauth2_url.is_none() {
println!("OAuth2 not available for {}.", &addr);
} else {

View File

@@ -1 +1 @@
1.50.0
1.54.0

View File

@@ -6,6 +6,14 @@ resources:
branch: master
uri: https://github.com/deltachat/deltachat-core-rust.git
- name: deltachat-core-rust-release
type: git
icon: github
source:
branch: master
uri: https://github.com/deltachat/deltachat-core-rust.git
tag_filter: "py-*"
jobs:
- name: doxygen
plan:
@@ -58,12 +66,15 @@ jobs:
- name: python-x86_64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
@@ -86,7 +97,7 @@ jobs:
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-docs
@@ -154,12 +165,15 @@ jobs:
- name: python-aarch64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
@@ -182,7 +196,7 @@ jobs:
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-wheels

View File

@@ -1,7 +1,7 @@
#!/bin/bash
PERL_VERSION=5.30.0
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
PERL_VERSION=5.34.0
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
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

View File

@@ -8,9 +8,11 @@ set -e -x
#
# 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"
RUST_VERSION=1.54.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
./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"
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"

View File

@@ -1,7 +1,7 @@
#!/bin/bash
PERL_VERSION=5.30.0
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
PERL_VERSION=5.34.0
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
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

View File

@@ -3,9 +3,16 @@
set -e -x
# Install Rust
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
#
# 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.
RUST_VERSION=1.54.0
# remove some 300-400 MB that we don't need for automated builds
rm -rf "/root/.rustup/toolchains/1.50.0-$(uname -m)-unknown-linux-gnu/share"
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
rustc --version
cd ..
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"

View File

@@ -1,3 +1,5 @@
//! # Account manager module.
use std::collections::BTreeMap;
use async_std::channel::{Receiver, Sender};
@@ -78,6 +80,14 @@ impl Accounts {
self.accounts.read().await.get(&id).cloned()
}
/// Returns the currently selected account's id or None if no account is selected.
pub async fn get_selected_account_id(&self) -> Option<u32> {
match self.config.get_selected_account().await {
0 => None,
id => Some(id),
}
}
/// Select the given account.
pub async fn select_account(&self, id: u32) -> Result<()> {
self.config.select_account(id).await?;
@@ -171,6 +181,7 @@ impl Accounts {
account_config.id,
)
.await?;
self.emitter.add_account(&ctx).await?;
self.accounts.write().await.insert(account_config.id, ctx);
Ok(account_config.id)
}
@@ -242,12 +253,13 @@ impl Accounts {
}
}
/// Unified event emitter.
/// Returns unified event emitter.
pub async fn get_event_emitter(&self) -> EventEmitter {
self.emitter.clone()
}
}
/// Unified event emitter for multiple accounts.
#[derive(Debug, Clone)]
pub struct EventEmitter {
/// Aggregate stream of events from all accounts.
@@ -315,6 +327,7 @@ impl async_std::stream::Stream for EventEmitter {
pub const CONFIG_NAME: &str = "accounts.toml";
pub const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
#[derive(Debug, Clone)]
pub struct Config {
file: PathBuf,
@@ -389,7 +402,7 @@ impl Config {
}
/// Create a new account in the given root directory.
pub async fn new_account(&self, dir: &PathBuf) -> Result<AccountConfig> {
async fn new_account(&self, dir: &PathBuf) -> Result<AccountConfig> {
let id = {
let inner = &mut self.inner.write().await;
let id = inner.next_id;
@@ -429,7 +442,7 @@ impl Config {
self.sync().await
}
pub async fn get_account(&self, id: u32) -> Option<AccountConfig> {
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
self.inner
.read()
.await
@@ -460,8 +473,9 @@ impl Config {
}
}
/// Configuration of a single account.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct AccountConfig {
struct AccountConfig {
/// Unique id.
pub id: u32,
/// Root directory for all data for this account.

View File

@@ -1,4 +1,4 @@
//! # Autocrypt header module
//! # Autocrypt header module.
//!
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).

View File

@@ -1,4 +1,4 @@
//! # Blob directory management
//! # Blob directory management.
use core::cmp::max;
use std::ffi::OsStr;

View File

@@ -1,4 +1,4 @@
//! # Chat module
//! # Chat module.
use std::convert::{TryFrom, TryInto};
use std::str::FromStr;
@@ -381,7 +381,14 @@ impl ChatId {
msg.param.set_cmd(cmd);
send_msg(context, self, &mut msg).await?;
} else {
add_info_msg_with_cmd(context, self, msg_text, cmd).await?;
add_info_msg_with_cmd(
context,
self,
msg_text,
cmd,
dc_create_smeared_timestamp(context).await,
)
.await?;
}
Ok(())
@@ -968,6 +975,7 @@ impl Chat {
&self.name
}
/// Returns profile image path for the chat.
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
@@ -1982,6 +1990,7 @@ pub(crate) async fn marknoticed_chat_if_older_than(
Ok(())
}
/// Marks all messages in the chat as noticed.
pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
// "WHERE" below uses the index `(state, hidden, chat_id)`, see get_fresh_msg_cnt() for reasoning
// the additional SELECT statement may speed up things as no write-blocking is needed.
@@ -2102,6 +2111,7 @@ pub async fn get_next_media(
Ok(ret)
}
/// Returns a vector of contact IDs for given chat ID.
pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<u32>> {
// Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a
// groupchat but the chats stays visible, moreover, this makes displaying lists easier)
@@ -2124,6 +2134,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
Ok(list)
}
/// Creates a group chat with a given `name`.
pub async fn create_group_chat(
context: &Context,
protect: ProtectionStatus,
@@ -2171,7 +2182,7 @@ pub async fn create_group_chat(
Ok(chat_id)
}
/// add a contact to the chats_contact table
/// Adds a contact to the `chats_contacts` table.
pub(crate) async fn add_to_chat_contacts_table(
context: &Context,
chat_id: ChatId,
@@ -2559,6 +2570,7 @@ pub(crate) async fn is_group_explicitly_left(
Ok(exists)
}
/// Sets group or mailing list chat name.
pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -> Result<()> {
let new_name = improve_single_line_input(new_name);
/* the function only sets the names of group chats; normal chats get their names from the contacts */
@@ -2808,10 +2820,10 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> Result<usize> {
pub(crate) async fn get_chat_id_by_grpid(
context: &Context,
grpid: impl AsRef<str>,
) -> Result<(ChatId, bool, Blocked)> {
) -> Result<Option<(ChatId, bool, Blocked)>> {
context
.sql
.query_row(
.query_row_optional(
"SELECT id, blocked, protected FROM chats WHERE grpid=?;",
paramsv![grpid.as_ref()],
|row| {
@@ -2932,6 +2944,7 @@ pub async fn add_device_msg_with_importance(
Ok(msg_id)
}
/// Adds a message to device chat.
pub async fn add_device_msg(
context: &Context,
label: Option<&str>,
@@ -2981,6 +2994,7 @@ pub(crate) async fn add_info_msg_with_cmd(
chat_id: ChatId,
text: impl AsRef<str>,
cmd: SystemMessage,
timestamp: i64,
) -> Result<MsgId> {
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
@@ -2997,7 +3011,7 @@ pub(crate) async fn add_info_msg_with_cmd(
chat_id,
DC_CONTACT_ID_INFO,
DC_CONTACT_ID_INFO,
dc_create_smeared_timestamp(context).await,
timestamp,
Viewtype::Text,
MessageState::InNoticed,
text.as_ref().to_string(),
@@ -3012,8 +3026,16 @@ pub(crate) async fn add_info_msg_with_cmd(
Ok(msg_id)
}
pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
if let Err(e) = add_info_msg_with_cmd(context, chat_id, text, SystemMessage::Unknown).await {
/// Adds info message with a given text and `timestamp` to the chat.
pub(crate) async fn add_info_msg(
context: &Context,
chat_id: ChatId,
text: impl AsRef<str>,
timestamp: i64,
) {
if let Err(e) =
add_info_msg_with_cmd(context, chat_id, text, SystemMessage::Unknown, timestamp).await
{
warn!(context, "Could not add info msg: {}", e);
}
}
@@ -3637,7 +3659,7 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
add_info_msg(&t, chat_id, "foo info").await;
add_info_msg(&t, chat_id, "foo info", 200000).await;
let msg = t.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_chat_id(), chat_id);
@@ -3658,6 +3680,7 @@ mod tests {
chat_id,
"foo bar info",
SystemMessage::EphemeralTimerChanged,
10000,
)
.await
.unwrap();

View File

@@ -1,4 +1,4 @@
//! # Chat list module
//! # Chat list module.
use anyhow::{bail, ensure, Result};

View File

@@ -1,4 +1,4 @@
//! Implementation of Consistent Color Generation
//! Implementation of Consistent Color Generation.
//!
//! Consistent Color Generation is defined in XEP-0392.
//!

View File

@@ -1,4 +1,4 @@
//! # Key-value configuration management
//! # Key-value configuration management.
use anyhow::Result;
use strum::{EnumProperty, IntoEnumIterator};
@@ -48,6 +48,12 @@ pub enum Config {
SmtpCertificateChecks,
ServerFlags,
Socks5Enabled,
Socks5Host,
Socks5Port,
Socks5User,
Socks5Password,
Displayname,
Selfstatus,
Selfavatar,

View File

@@ -1,4 +1,4 @@
//! Email accounts autoconfiguration process module
//! Email accounts autoconfiguration process module.
mod auto_mozilla;
mod auto_outlook;
@@ -14,6 +14,7 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::dc_tools::EmailAddress;
use crate::imap::Imap;
use crate::login_param::Socks5Config;
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::message::Message;
use crate::oauth2::dc_get_oauth2_addr;
@@ -170,12 +171,17 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
DC_LP_AUTH_NORMAL as i32
};
let socks5_config = param.socks5_config.clone();
let socks5_enabled = socks5_config.is_some();
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
// Step 1: Load the parameters and check email-address and password
if oauth2 {
// Do oauth2 only if socks5 is disabled. As soon as we have a http library that can do
// socks5 requests, this can work with socks5 too
if oauth2 && !socks5_enabled {
// the used oauth2 addr may differ, check this.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
@@ -217,7 +223,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&param_domain).await {
if let Some(provider) = provider::get_provider_info(&param_domain, socks5_enabled).await {
param.provider = Some(provider);
match provider.status {
provider::Status::Ok | provider::Status::Preparation => {
@@ -256,9 +262,16 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
}
} else {
// Try receiving autoconfig
info!(ctx, "no offline autoconfig found");
param_autoconfig =
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await;
param_autoconfig = if socks5_enabled {
// Currently we can't do http requests through socks5, to not leak
// the ip, just don't do online autoconfig
info!(ctx, "socks5 enabled, skipping autoconfig");
None
} else {
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await
}
}
} else {
param_autoconfig = None;
@@ -320,6 +333,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
match try_smtp_one_param(
&context_smtp,
&smtp_param,
&socks5_config,
&smtp_addr,
oauth2,
provider_strict_tls,
@@ -359,7 +373,16 @@ 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, &param.imap, &param.addr, oauth2, provider_strict_tls).await {
match try_imap_one_param(
ctx,
&param.imap,
&param.socks5_config,
&param.addr,
oauth2,
provider_strict_tls,
)
.await
{
Ok(configured_imap) => {
imap = Some(configured_imap);
break;
@@ -507,6 +530,7 @@ async fn get_autoconfig(
async fn try_imap_one_param(
context: &Context,
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
@@ -519,7 +543,16 @@ async fn try_imap_one_param(
let (_s, r) = async_std::channel::bounded(1);
let mut imap = match Imap::new(param, addr, oauth2, provider_strict_tls, r).await {
let mut imap = match Imap::new(
param,
socks5_config.clone(),
addr,
oauth2,
provider_strict_tls,
r,
)
.await
{
Err(err) => {
info!(context, "failure: {}", err);
return Err(ConfigurationError {
@@ -548,19 +581,37 @@ async fn try_imap_one_param(
async fn try_smtp_one_param(
context: &Context,
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
smtp: &mut Smtp,
) -> Result<(), ConfigurationError> {
let inf = format!(
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
param.user,
param.server,
param.port,
param.security,
param.certificate_checks,
oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
} else {
"None".to_string()
}
);
info!(context, "Trying: {}", inf);
if let Err(err) = smtp
.connect(context, param, addr, oauth2, provider_strict_tls)
.connect(
context,
param,
socks5_config,
addr,
oauth2,
provider_strict_tls,
)
.await
{
info!(context, "failure: {}", err);

View File

@@ -1,4 +1,4 @@
//! # Constants
//! # Constants.
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@@ -165,36 +165,18 @@ pub const DC_MSG_ID_MARKER1: u32 = 1;
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// string that indicates sth. is left out or truncated
pub const DC_ELLIPSE: &str = "[...]";
/// String that indicates that something is left out or truncated.
pub const DC_ELLIPSIS: &str = "[...]";
/// to keep bubbles and chat flow usable,
/// and to avoid problems with controls using very long texts,
/// we limit the text length to DC_DESIRED_TEXT_LEN.
/// if the text is longer, the full text can be retrieved using has_html()/get_html().
/// Message length limit.
///
/// we are using a bit less than DC_MAX_GET_TEXT_LEN to avoid cutting twice
/// (a bit less as truncation may not be exact and ellipses may be added).
/// To keep bubbles and chat flow usable and to avoid problems with controls using very long texts,
/// we limit the text length to `DC_DESIRED_TEXT_LEN`. If the text is longer, the full text can be
/// retrieved using has_html()/get_html().
///
/// note, that DC_DESIRED_TEXT_LEN and DC_MAX_GET_TEXT_LEN
/// define max. number of bytes, _not_ unicode graphemes.
/// in general, that seems to be okay for such an upper limit,
/// esp. as calculating the number of graphemes is not simple
/// (one graphemes may be a sequence of code points which is a sequence of bytes).
/// also even if we have the exact number of graphemes,
/// that would not always help on getting an idea about the screen space used
/// (to keep bubbles and chat flow usable).
///
/// therefore, the number of bytes is only a very rough estimation,
/// however, the ~30K seems to work okayish for a while,
/// if it turns out, it is too few for some alphabet, we can still increase.
pub const DC_DESIRED_TEXT_LEN: usize = 29_000;
/// approx. max. length (number of bytes) returned by dc_msg_get_text()
pub const DC_MAX_GET_TEXT_LEN: usize = 30_000;
/// approx. max. length returned by dc_get_msg_info()
pub const DC_MAX_GET_INFO_LEN: usize = 100_000;
/// Note that for simplicity maximum length is defined as the number of Unicode Scalar Values (Rust
/// `char`s), not Unicode Grapheme Clusters.
pub const DC_DESIRED_TEXT_LEN: usize = 5000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;

View File

@@ -218,6 +218,7 @@ impl Contact {
} else if contact_id == DC_CONTACT_ID_DEVICE {
contact.name = stock_str::device_messages(context).await;
contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
contact.status = stock_str::device_messages_hint(context).await;
}
Ok(contact)
}
@@ -1202,7 +1203,8 @@ WHERE type=? AND id IN (
// 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 {
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await?
{
chat_id.unblock(context).await?;
}
}

View File

@@ -1,4 +1,4 @@
//! Context module
//! Context module.
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
@@ -22,6 +22,7 @@ use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
@@ -62,6 +63,10 @@ pub struct InnerContext {
pub(crate) scheduler: RwLock<Scheduler>,
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
/// Recently loaded quota information, if any.
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock<Option<QuotaInfo>>,
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
/// ID for this `Context` in the current process.
@@ -139,6 +144,7 @@ impl Context {
events: Events::default(),
scheduler: RwLock::new(Scheduler::Stopped),
ephemeral_task: RwLock::new(None),
quota: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
};
@@ -283,6 +289,7 @@ impl Context {
let request_msgs = message::get_request_msg_cnt(self).await as usize;
let contacts = Contact::get_real_cnt(self).await? as usize;
let is_configured = self.get_config_int(Config::Configured).await?;
let socks5_enabled = self.get_config_int(Config::Socks5Enabled).await?;
let dbversion = self
.sql
.get_raw_config_int("dbversion")
@@ -351,6 +358,7 @@ impl Context {
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
res.insert("socks5_enabled", socks5_enabled.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());
res.insert(
@@ -872,6 +880,10 @@ mod tests {
"send_security",
"server_flags",
"smtp_certificate_checks",
"socks5_host",
"socks5_port",
"socks5_user",
"socks5_password",
];
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();

View File

@@ -1,3 +1,5 @@
//! Internet Message Format reception pipeline.
use std::convert::TryFrom;
use anyhow::{bail, ensure, format_err, Result};
@@ -91,7 +93,7 @@ pub(crate) async fn dc_receive_imf_inner(
}
let mut sent_timestamp = if let Some(value) = mime_parser
.get(HeaderDef::Date)
.get_header(HeaderDef::Date)
.and_then(|value| mailparse::dateparse(value).ok())
{
value
@@ -132,7 +134,7 @@ pub(crate) async fn dc_receive_imf_inner(
let mut create_event_to_send = Some(CreateEvent::MsgsChanged);
let prevent_rename =
mime_parser.is_mailinglist_message() || mime_parser.get(HeaderDef::Sender).is_some();
mime_parser.is_mailinglist_message() || mime_parser.get_header(HeaderDef::Sender).is_some();
// get From: (it can be an address list!) and check if it is known (for known From:'s we add
// the other To:/Cc: in the 3rd pass)
@@ -371,7 +373,7 @@ async fn add_parts(
prevent_rename: bool,
) -> Result<ChatId> {
let mut state: MessageState;
let mut chat_id = ChatId::new(0);
let mut chat_id = None;
let mut chat_id_blocked = Blocked::Not;
let mut incoming_origin = incoming_origin;
@@ -400,7 +402,7 @@ async fn add_parts(
match show_emails {
ShowEmails::Off => {
info!(context, "Classical email not shown (TRASH)");
chat_id = DC_CHAT_ID_TRASH;
chat_id = Some(DC_CHAT_ID_TRASH);
allow_creation = false;
}
ShowEmails::AcceptedContacts => allow_creation = false,
@@ -423,9 +425,9 @@ async fn add_parts(
to_id = DC_CONTACT_ID_SELF;
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get(HeaderDef::SecureJoin).is_some() {
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
is_dc_message = MessengerMessage::Yes; // avoid discarding by show_emails setting
chat_id = ChatId::new(0);
chat_id = None;
allow_creation = true;
match handle_securejoin_handshake(context, mime_parser, from_id).await {
Ok(securejoin::HandshakeMessage::Done) => {
@@ -441,9 +443,8 @@ async fn add_parts(
// process messages as "member added" normally
}
Err(err) => {
*hidden = true;
warn!(context, "Error in Secure-Join message handling: {}", err);
return Ok(chat_id);
return Ok(DC_CHAT_ID_TRASH);
}
}
}
@@ -452,21 +453,23 @@ async fn add_parts(
.await
.unwrap_or_default();
if chat_id.is_unset() && mime_parser.failure_report.is_some() {
chat_id = DC_CHAT_ID_TRASH;
if chat_id.is_none() && mime_parser.failure_report.is_some() {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message belongs to an NDN (TRASH)",);
}
if chat_id.is_unset() {
if chat_id.is_none() {
// try to assign to a chat based on In-Reply-To/References:
let (new_chat_id, new_chat_id_blocked) =
lookup_chat_by_reply(context, &mime_parser, &parent, from_id, to_ids).await?;
chat_id = new_chat_id;
chat_id_blocked = new_chat_id_blocked;
if let Some((new_chat_id, new_chat_id_blocked)) =
lookup_chat_by_reply(context, &mime_parser, &parent, from_id, to_ids).await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
if chat_id.is_unset() {
if chat_id.is_none() {
// try to create a group
let create_blocked = match test_normal_chat {
@@ -477,7 +480,7 @@ async fn add_parts(
_ => Blocked::Request,
};
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_group(
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
context,
&mut mime_parser,
if test_normal_chat.is_none() {
@@ -489,63 +492,69 @@ async fn add_parts(
from_id,
to_ids,
)
.await?;
chat_id = new_chat_id;
chat_id_blocked = new_chat_id_blocked;
if !chat_id.is_unset()
&& chat_id_blocked != Blocked::Not
&& create_blocked == Blocked::Not
.await?
{
new_chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
if chat_id_blocked != Blocked::Not && create_blocked == Blocked::Not {
new_chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
}
}
}
// In lookup_chat_by_reply() and create_or_lookup_group(), it can happen that the message is put into a chat
// but the From-address is not a member of this chat.
if !chat_id.is_unset() && !chat::is_contact_in_chat(context, chat_id, from_id as u32).await
{
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.is_protected() {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.repl_msg_by_error(s);
} else if let Some(from) = mime_parser.from.first() {
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
// to the sender's name, indicating to the user that he/she is not part of the group.
let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
for part in mime_parser.parts.iter_mut() {
part.param.set(Param::OverrideSenderDisplayname, name);
if let Some(chat_id) = chat_id {
if !chat::is_contact_in_chat(context, chat_id, from_id as u32).await {
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.is_protected() {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.repl_msg_by_error(s);
} else if let Some(from) = mime_parser.from.first() {
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
// to the sender's name, indicating to the user that he/she is not part of the group.
let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
for part in mime_parser.parts.iter_mut() {
part.param.set(Param::OverrideSenderDisplayname, name);
}
}
}
}
if chat_id.is_unset() {
if chat_id.is_none() {
// check if the message belongs to a mailing list
match mime_parser.get_mailinglist_type() {
MailinglistType::ListIdBased => {
if let Some(list_id) = mime_parser.get(HeaderDef::ListId) {
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_mailinglist(
context,
allow_creation,
list_id,
mime_parser,
)
.await;
chat_id = new_chat_id;
chat_id_blocked = new_chat_id_blocked;
if let Some(list_id) = mime_parser.get_header(HeaderDef::ListId) {
if let Some((new_chat_id, new_chat_id_blocked)) =
create_or_lookup_mailinglist(
context,
allow_creation,
list_id,
mime_parser,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
}
MailinglistType::SenderBased => {
if let Some(sender) = mime_parser.get(HeaderDef::Sender) {
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_mailinglist(
context,
allow_creation,
sender,
mime_parser,
)
.await;
chat_id = new_chat_id;
chat_id_blocked = new_chat_id_blocked;
if let Some(sender) = mime_parser.get_header(HeaderDef::Sender) {
if let Some((new_chat_id, new_chat_id_blocked)) =
create_or_lookup_mailinglist(
context,
allow_creation,
sender,
mime_parser,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
}
MailinglistType::None => {}
@@ -564,7 +573,7 @@ async fn add_parts(
}
}
if chat_id.is_unset() {
if chat_id.is_none() {
// try to create a normal chat
let create_blocked = if from_id == to_id {
Blocked::Not
@@ -573,40 +582,39 @@ async fn add_parts(
};
if let Some(chat) = test_normal_chat {
chat_id = chat.id;
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
} else if allow_creation {
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
.await
.log_err(context, "Failed to get (new) chat for contact")
{
chat_id = chat.id;
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
}
if !chat_id.is_unset() && Blocked::Not != chat_id_blocked {
if Blocked::Not == create_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
} else if parent.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
// the contact requests will pop up and this should be just fine.
Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo).await;
info!(
context,
"Message is a reply to a known message, mark sender as known.",
);
if !incoming_origin.is_known() {
incoming_origin = Origin::IncomingReplyTo;
if let Some(chat_id) = chat_id {
if Blocked::Not != chat_id_blocked {
if Blocked::Not == create_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
} else if parent.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
// the contact requests will pop up and this should be just fine.
Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo)
.await;
info!(
context,
"Message is a reply to a known message, mark sender as known.",
);
if !incoming_origin.is_known() {
incoming_origin = Origin::IncomingReplyTo;
}
}
}
}
}
if chat_id.is_unset() {
// maybe from_id is null or sth. else is suspicious, move message to trash
chat_id = DC_CHAT_ID_TRASH;
info!(context, "No chat id for incoming msg (TRASH)")
}
// if the chat_id is blocked,
// for unknown senders and non-delta-messages set the state to NOTICED
@@ -629,7 +637,7 @@ async fn add_parts(
&& (is_dc_message == MessengerMessage::No)
&& context.is_spam_folder(server_folder).await?;
if is_spam {
chat_id = DC_CHAT_ID_TRASH;
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is probably spam (TRASH)");
}
} else {
@@ -641,9 +649,9 @@ async fn add_parts(
to_id = to_ids.get_index(0).cloned().unwrap_or_default();
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get(HeaderDef::SecureJoin).is_some() {
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
is_dc_message = MessengerMessage::Yes; // avoid discarding by show_emails setting
chat_id = ChatId::new(0);
chat_id = None;
allow_creation = true;
match observe_securejoin_on_other_device(context, mime_parser, to_id).await {
Ok(securejoin::HandshakeMessage::Done)
@@ -654,9 +662,8 @@ async fn add_parts(
// process messages as "member added" normally
}
Err(err) => {
*hidden = true;
warn!(context, "Error in Secure-Join watching: {}", err);
return Ok(chat_id);
return Ok(DC_CHAT_ID_TRASH);
}
}
}
@@ -668,8 +675,8 @@ async fn add_parts(
// 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();
&& mime_parser.get_header(HeaderDef::Received).is_none()
&& mime_parser.get_header(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
@@ -677,27 +684,32 @@ async fn add_parts(
//
// 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();
let is_draft = is_draft
|| mime_parser
.get_header(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
info!(context, "Email is probably just a draft (TRASH)");
chat_id = DC_CHAT_ID_TRASH;
chat_id = Some(DC_CHAT_ID_TRASH);
allow_creation = false;
}
if chat_id.is_unset() {
if chat_id.is_none() {
// try to assign to a chat based on In-Reply-To/References:
let (new_chat_id, new_chat_id_blocked) =
lookup_chat_by_reply(context, &mime_parser, &parent, from_id, to_ids).await?;
chat_id = new_chat_id;
chat_id_blocked = new_chat_id_blocked;
if let Some((new_chat_id, new_chat_id_blocked)) =
lookup_chat_by_reply(context, &mime_parser, &parent, from_id, to_ids).await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
if !to_ids.is_empty() {
if chat_id.is_unset() {
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_group(
if chat_id.is_none() {
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
context,
&mut mime_parser,
allow_creation,
@@ -705,16 +717,18 @@ async fn add_parts(
from_id,
to_ids,
)
.await?;
chat_id = new_chat_id;
chat_id_blocked = new_chat_id_blocked;
// automatically unblock chat when the user sends a message
if !chat_id.is_unset() && chat_id_blocked != Blocked::Not {
new_chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
// automatically unblock chat when the user sends a message
if chat_id_blocked != Blocked::Not {
new_chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
}
}
}
if chat_id.is_unset() && allow_creation {
if chat_id.is_none() && allow_creation {
let create_blocked = if !Contact::is_blocked_load(context, to_id).await {
Blocked::Not
} else {
@@ -723,16 +737,15 @@ async fn add_parts(
if let Ok(chat) =
ChatIdBlocked::get_for_contact(context, to_id, create_blocked).await
{
chat_id = chat.id;
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
if !chat_id.is_unset()
&& Blocked::Not != chat_id_blocked
&& Blocked::Not == create_blocked
{
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
if let Some(chat_id) = chat_id {
if Blocked::Not != chat_id_blocked && Blocked::Not == create_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
}
}
}
}
@@ -740,7 +753,7 @@ async fn add_parts(
&& to_ids.len() == 1
&& to_ids.contains(&DC_CONTACT_ID_SELF);
if chat_id.is_unset() && self_sent {
if chat_id.is_none() && self_sent {
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
// maybe an Autocrypt Setup Message
if let Ok(chat) =
@@ -748,29 +761,33 @@ async fn add_parts(
.await
.log_err(context, "Failed to get (new) chat for contact")
{
chat_id = chat.id;
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
if !chat_id.is_unset() && Blocked::Not != chat_id_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
if let Some(chat_id) = chat_id {
if Blocked::Not != chat_id_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
}
}
}
if chat_id.is_unset() {
chat_id = DC_CHAT_ID_TRASH;
info!(context, "No chat id for outgoing message (TRASH)")
}
}
if fetching_existing_messages && mime_parser.decrypting_failed {
chat_id = DC_CHAT_ID_TRASH;
chat_id = Some(DC_CHAT_ID_TRASH);
// We are only gathering old messages on first start. We do not want to add loads of non-decryptable messages to the chats.
info!(context, "Existing non-decipherable message. (TRASH)");
}
let chat_id = chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH)");
DC_CHAT_ID_TRASH
});
// Extract ephemeral timer from the message.
let mut ephemeral_timer = if let Some(value) = mime_parser.get(HeaderDef::EphemeralTimer) {
let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer)
{
match value.parse::<EphemeralTimer>() {
Ok(timer) => timer,
Err(err) => {
@@ -787,6 +804,12 @@ async fn add_parts(
let location_kml_is = mime_parser.location_kml.is_some();
// correct message_timestamp, it should not be used before,
// however, we cannot do this earlier as we need from_id to be set
let in_fresh = state == MessageState::InFresh;
let rcvd_timestamp = time();
let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, chat_id, in_fresh).await?;
// Apply ephemeral timer changes to the chat.
//
// Only non-hidden timers are applied now. Timers from hidden
@@ -814,6 +837,7 @@ async fn add_parts(
context,
chat_id,
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
sort_timestamp,
)
.await;
}
@@ -857,6 +881,7 @@ async fn add_parts(
context,
chat_id,
format!("Cannot set protection: {}", e),
sort_timestamp,
)
.await;
return Ok(chat_id); // do not return an error as this would result in retrying the message
@@ -872,12 +897,6 @@ async fn add_parts(
}
}
// correct message_timestamp, it should not be used before,
// however, we cannot do this earlier as we need from_id to be set
let in_fresh = state == MessageState::InFresh;
let rcvd_timestamp = time();
let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, chat_id, in_fresh).await?;
// Ensure replies to messages are sorted after the parent message.
//
// This is useful in a case where sender clocks are not
@@ -898,11 +917,11 @@ async fn add_parts(
let save_mime_headers = context.get_config_bool(Config::SaveMimeHeaders).await?;
let mime_in_reply_to = mime_parser
.get(HeaderDef::InReplyTo)
.get_header(HeaderDef::InReplyTo)
.cloned()
.unwrap_or_default();
let mime_references = mime_parser
.get(HeaderDef::References)
.get_header(HeaderDef::References)
.cloned()
.unwrap_or_default();
@@ -936,7 +955,6 @@ async fn add_parts(
let sent_timestamp = *sent_timestamp;
let is_hidden = *hidden;
let chat_id = chat_id;
let mut is_hidden = is_hidden;
let mut ids = Vec::with_capacity(parts.len());
@@ -979,7 +997,8 @@ INSERT INTO msgs
}
}
let mime_modified = save_mime_modified && !part.msg.is_empty();
let part_is_empty = part.msg.is_empty() && part.param.get(Param::Quote).is_none();
let mime_modified = save_mime_modified && !part_is_empty;
if mime_modified {
// Avoid setting mime_modified for more than one part.
save_mime_modified = false;
@@ -1201,7 +1220,7 @@ async fn lookup_chat_by_reply(
parent: &Option<Message>,
from_id: u32,
to_ids: &ContactIds,
) -> Result<(ChatId, Blocked)> {
) -> Result<Option<(ChatId, Blocked)>> {
// Try to assign message to the same chat as the parent message.
// If this was a private message just to self, it was probably a private reply.
@@ -1215,7 +1234,7 @@ async fn lookup_chat_by_reply(
// This message will be assigned to this chat, anyway.
// But if we assigned it now, create_or_lookup_group() will not be called
// and group commands will not be executed.
return Ok((ChatId::new(0), Blocked::Not));
return Ok(None);
}
}
@@ -1224,25 +1243,25 @@ async fn lookup_chat_by_reply(
// (undecipherable group msgs often get assigned to the 1:1 chat with the sender).
// We don't have any way of finding out whether a msg is undecipherable, so we check for
// error.is_some() instead.
return Ok((ChatId::new(0), Blocked::Not));
return Ok(None);
}
if parent_chat.id == DC_CHAT_ID_TRASH {
return Ok((ChatId::new(0), Blocked::Not));
return Ok(None);
}
if is_probably_private_reply(context, to_ids, mime_parser, parent_chat.id, from_id).await? {
return Ok((ChatId::new(0), Blocked::Not));
return Ok(None);
}
info!(
context,
"Assigning message to {} as it's a reply to {}", parent_chat.id, parent.rfc724_mid
);
return Ok((parent_chat.id, parent_chat.blocked));
return Ok(Some((parent_chat.id, parent_chat.blocked)));
}
Ok((ChatId::new(0), Blocked::Not))
Ok(None)
}
/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
@@ -1299,7 +1318,7 @@ async fn create_or_lookup_group(
create_blocked: Blocked,
from_id: u32,
to_ids: &ContactIds,
) -> Result<(ChatId, Blocked)> {
) -> Result<Option<(ChatId, Blocked)>> {
let mut chat_id_blocked = Blocked::Not;
let mut recreate_member_list = false;
let mut send_EVENT_CHAT_MODIFIED = false;
@@ -1324,16 +1343,12 @@ async fn create_or_lookup_group(
}
if !allow_creation {
info!(context, "creating ad-hoc group prevented from caller");
return Ok((ChatId::new(0), Blocked::Not));
return Ok(None);
}
return create_adhoc_group(context, mime_parser, create_blocked, &member_ids)
.await
.map(|chat_id| {
chat_id
.map(|chat_id| (chat_id, create_blocked))
.unwrap_or((ChatId::new(0), Blocked::Not))
})
.map(|chat_id| chat_id.map(|chat_id| (chat_id, create_blocked)))
.map_err(|err| {
info!(context, "could not create adhoc-group: {:?}", err);
err
@@ -1341,25 +1356,30 @@ async fn create_or_lookup_group(
};
// check, if we have a chat with this group ID
let (mut chat_id, _, _blocked) = chat::get_chat_id_by_grpid(context, &grpid)
.await
.unwrap_or((ChatId::new(0), false, Blocked::Not));
let mut chat_id = chat::get_chat_id_by_grpid(context, &grpid)
.await?
.map(|(chat_id, _protected, _blocked)| chat_id);
// For chat messages, we don't have to guess (is_*probably*_private_reply()) but we know for sure that
// they belong to the group because of the Chat-Group-Id or Message-Id header
if !mime_parser.has_chat_version()
&& is_probably_private_reply(context, to_ids, mime_parser, chat_id, from_id).await?
{
return Ok((ChatId::new(0), Blocked::Not));
if let Some(chat_id) = chat_id {
if !mime_parser.has_chat_version()
&& is_probably_private_reply(context, to_ids, mime_parser, chat_id, from_id).await?
{
return Ok(None);
}
}
// now we have a grpid that is non-empty
// but we might not know about this group
let grpname = mime_parser.get(HeaderDef::ChatGroupName).cloned();
let grpname = mime_parser.get_header(HeaderDef::ChatGroupName).cloned();
let mut removed_id = None;
if let Some(removed_addr) = mime_parser.get(HeaderDef::ChatGroupMemberRemoved).cloned() {
if let Some(removed_addr) = mime_parser
.get_header(HeaderDef::ChatGroupMemberRemoved)
.cloned()
{
removed_id = Contact::lookup_id_by_addr(context, &removed_addr, Origin::Unknown).await?;
match removed_id {
Some(contact_id) => {
@@ -1373,12 +1393,14 @@ async fn create_or_lookup_group(
None => warn!(context, "removed {:?} has no contact_id", removed_addr),
}
} else {
let field = mime_parser.get(HeaderDef::ChatGroupMemberAdded).cloned();
let field = mime_parser
.get_header(HeaderDef::ChatGroupMemberAdded)
.cloned();
if let Some(added_member) = field {
mime_parser.is_system_message = SystemMessage::MemberAddedToGroup;
better_msg = stock_str::msg_add_member(context, &added_member, from_id).await;
X_MrAddToGrp = Some(added_member);
} else if let Some(old_name) = mime_parser.get(HeaderDef::ChatGroupNameChanged) {
} else if let Some(old_name) = mime_parser.get_header(HeaderDef::ChatGroupNameChanged) {
X_MrGrpNameChanged = true;
better_msg = stock_str::msg_grp_name(
context,
@@ -1392,7 +1414,7 @@ async fn create_or_lookup_group(
)
.await;
mime_parser.is_system_message = SystemMessage::GroupNameChanged;
} else if let Some(value) = mime_parser.get(HeaderDef::ChatContent) {
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
@@ -1421,7 +1443,7 @@ async fn create_or_lookup_group(
.await?
.unwrap_or_default();
if chat_id.is_unset()
if chat_id.is_none()
&& !mime_parser.is_mailinglist_message()
&& !grpid.is_empty()
&& grpname.is_some()
@@ -1432,7 +1454,7 @@ async fn create_or_lookup_group(
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
{
// group does not exist but should be created
let create_protected = if mime_parser.get(HeaderDef::ChatVerified).is_some() {
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await
{
warn!(context, "verification problem: {}", err);
@@ -1446,7 +1468,7 @@ async fn create_or_lookup_group(
if !allow_creation {
info!(context, "creating group forbidden by caller");
return Ok((ChatId::new(0), Blocked::Not));
return Ok(None);
}
chat_id = create_multiuser_record(
@@ -1458,6 +1480,7 @@ async fn create_or_lookup_group(
create_protected,
)
.await
.map(Some)
.unwrap_or_else(|err| {
warn!(
context,
@@ -1467,7 +1490,7 @@ async fn create_or_lookup_group(
err,
);
ChatId::new(0)
None
});
chat_id_blocked = create_blocked;
@@ -1486,22 +1509,22 @@ async fn create_or_lookup_group(
}
// again, check chat_id
if chat_id.is_special() {
if mime_parser.decrypting_failed {
// It is possible that the message was sent to a valid,
// yet unknown group, which was rejected because
// Chat-Group-Name, which is in the encrypted part, was
// not found. We can't create a properly named group in
// this case, so assign error message to 1:1 chat with the
// sender instead.
return Ok((ChatId::new(0), Blocked::Not));
} else {
// The message was decrypted successfully, but contains a late "quit" or otherwise
// unwanted message.
info!(context, "message belongs to unwanted group (TRASH)");
return Ok((DC_CHAT_ID_TRASH, chat_id_blocked));
}
}
let chat_id = if let Some(chat_id) = chat_id {
chat_id
} else if mime_parser.decrypting_failed {
// It is possible that the message was sent to a valid,
// yet unknown group, which was rejected because
// Chat-Group-Name, which is in the encrypted part, was
// not found. We can't create a properly named group in
// this case, so assign error message to 1:1 chat with the
// sender instead.
return Ok(None);
} else {
// The message was decrypted successfully, but contains a late "quit" or otherwise
// unwanted message.
info!(context, "message belongs to unwanted group (TRASH)");
return Ok(Some((DC_CHAT_ID_TRASH, chat_id_blocked)));
};
// We have a valid chat_id > DC_CHAT_ID_LAST_SPECIAL.
@@ -1585,7 +1608,7 @@ async fn create_or_lookup_group(
if send_EVENT_CHAT_MODIFIED {
context.emit_event(EventType::ChatModified(chat_id));
}
Ok((chat_id, chat_id_blocked))
Ok(Some((chat_id, chat_id_blocked)))
}
/// Create or lookup a mailing list chat.
@@ -1603,7 +1626,7 @@ async fn create_or_lookup_mailinglist(
allow_creation: bool,
list_id_header: &str,
mime_parser: &MimeMessage,
) -> (ChatId, Blocked) {
) -> Result<Option<(ChatId, Blocked)>> {
static LIST_ID: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
let (mut name, listid) = match LIST_ID.captures(list_id_header) {
Some(cap) => (cap[1].trim().to_string(), cap[2].trim().to_string()),
@@ -1617,8 +1640,8 @@ async fn create_or_lookup_mailinglist(
),
};
if let Ok((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await {
return (chat_id, blocked);
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
return Ok(Some((chat_id, blocked)));
}
// for mailchimp lists, the name in `ListId` is just a long number.
@@ -1665,7 +1688,7 @@ async fn create_or_lookup_mailinglist(
if allow_creation {
// list does not exist but should be created
match create_multiuser_record(
let chat_id = create_multiuser_record(
context,
Chattype::Mailinglist,
&listid,
@@ -1674,30 +1697,23 @@ async fn create_or_lookup_mailinglist(
ProtectionStatus::Unprotected,
)
.await
{
Ok(chat_id) => {
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await;
(chat_id, Blocked::Request)
}
Err(e) => {
warn!(
context,
"Failed to create mailinglist '{}' for grpid={}: {}",
&name,
&listid,
e.to_string()
);
(ChatId::new(0), Blocked::Request)
}
}
.map_err(|err| {
err.context(format!(
"Failed to create mailinglist '{}' for grpid={}",
&name, &listid
))
})?;
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await;
Ok(Some((chat_id, Blocked::Request)))
} else {
info!(context, "creating list forbidden by caller");
(ChatId::new(0), Blocked::Not)
Ok(None)
}
}
fn try_getting_grpid(mime_parser: &MimeMessage) -> Option<String> {
if let Some(optional_field) = mime_parser.get(HeaderDef::ChatGroupId) {
if let Some(optional_field) = mime_parser.get_header(HeaderDef::ChatGroupId) {
return Some(optional_field.clone());
}
@@ -1719,7 +1735,7 @@ fn try_getting_grpid(mime_parser: &MimeMessage) -> Option<String> {
/// try extract a grpid from a message-id list header value
fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str> {
let header = mime_parser.get(headerdef)?;
let header = mime_parser.get_header(headerdef)?;
let parts = header
.split(',')
.map(str::trim)
@@ -1884,7 +1900,7 @@ async fn check_verified_properties(
ensure!(mimeparser.was_encrypted(), "This message is not encrypted.");
if mimeparser.get(HeaderDef::ChatVerified).is_none() {
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
// we do not fail here currently, this would exclude (a) non-deltas
// and (b) deltas with different protection views across multiple devices.
// for group creation or protection enabled/disabled, however, Chat-Verified is respected.
@@ -2030,13 +2046,13 @@ async fn get_parent_message(
context: &Context,
mime_parser: &MimeMessage,
) -> Result<Option<Message>> {
if let Some(field) = mime_parser.get(HeaderDef::References) {
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
if let Some(msg) = get_rfc724_mid_in_list(context, field).await? {
return Ok(Some(msg));
}
}
if let Some(field) = mime_parser.get(HeaderDef::InReplyTo) {
if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
if let Some(msg) = get_rfc724_mid_in_list(context, field).await? {
return Ok(Some(msg));
}
@@ -2117,13 +2133,13 @@ fn dc_create_incoming_rfc724_mid(mime: &MimeMessage) -> String {
"{}@stub",
hex_hash(&format!(
"{}-{}-{}",
mime.get(HeaderDef::Date)
mime.get_header(HeaderDef::Date)
.map(|s| s.to_string())
.unwrap_or_default(),
mime.get(HeaderDef::From_)
mime.get_header(HeaderDef::From_)
.map(|s| s.to_string())
.unwrap_or_default(),
mime.get(HeaderDef::To)
mime.get_header(HeaderDef::To)
.map(|s| s.to_string())
.unwrap_or_default()
))
@@ -4441,4 +4457,25 @@ Reply to all"#,
Ok(())
}
#[async_std::test]
async fn test_gmx_forwarded_msg() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
dc_receive_imf(
&t,
include_bytes!("../test-data/message/gmx-forward.eml"),
"INBOX",
1,
false,
)
.await?;
let msg = t.get_last_msg().await;
assert!(msg.has_html());
assert_eq!(msg.id.get_html(&t).await?.unwrap().replace("\r\n", "\n"), "<html><head></head><body><div style=\"font-family: Verdana;font-size: 12.0px;\"><div>&nbsp;</div>\n\n<div>&nbsp;\n<div>&nbsp;\n<div data-darkreader-inline-border-left=\"\" name=\"quote\" style=\"margin: 10px 5px 5px 10px; padding: 10px 0px 10px 10px; border-left: 2px solid rgb(195, 217, 229); overflow-wrap: break-word; --darkreader-inline-border-left:#274759;\">\n<div style=\"margin:0 0 10px 0;\"><b>Gesendet:</b>&nbsp;Donnerstag, 12. August 2021 um 15:52 Uhr<br/>\n<b>Von:</b>&nbsp;&quot;Claire&quot; &lt;claire@example.org&gt;<br/>\n<b>An:</b>&nbsp;alice@example.com<br/>\n<b>Betreff:</b>&nbsp;subject</div>\n\n<div name=\"quoted-content\">bodytext</div>\n</div>\n</div>\n</div></div></body></html>\n\n");
Ok(())
}
}

View File

@@ -17,7 +17,7 @@ use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng};
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{Viewtype, DC_ELLIPSE, DC_OUTDATED_WARNING_DAYS};
use crate::constants::{Viewtype, DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
use crate::context::Context;
use crate::events::EventType;
use crate::message::Message;
@@ -29,7 +29,7 @@ use crate::stock_str;
#[allow(clippy::indexing_slicing)]
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let count = buf.chars().count();
if approx_chars > 0 && count > approx_chars + DC_ELLIPSE.len() {
if count > approx_chars + DC_ELLIPSIS.len() {
let end_pos = buf
.char_indices()
.nth(approx_chars)
@@ -37,9 +37,9 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
.unwrap_or_default();
if let Some(index) = buf[..end_pos].rfind(|c| c == ' ' || c == '\n') {
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSE))
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSIS))
} else {
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSE))
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSIS))
}
} else {
Cow::Borrowed(buf)
@@ -711,10 +711,7 @@ mod tests {
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
);
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0), "[...]");
// 9 characters, so no truncation
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);

View File

@@ -1,4 +1,4 @@
//! De-HTML
//! De-HTML.
//!
//! A module to remove HTML tags from the email text

View File

@@ -178,7 +178,9 @@ pub async fn try_decrypt(
let mut signatures = HashSet::default();
if let Some(ref mut peerstate) = peerstate {
peerstate.handle_fingerprint_change(context).await?;
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
@@ -333,7 +335,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
false
}
/// Check if a MIME structure contains a multipart/report part.
/// Checks if a MIME structure contains a multipart/report part.
///
/// As reports are often unencrypted, we do not reset the Autocrypt header in
/// this case.

View File

@@ -1,4 +1,4 @@
//! # Ephemeral messages
//! # Ephemeral messages.
//!
//! Ephemeral messages are messages that have an Ephemeral-Timer
//! header attached to them, which specifies time in seconds after

View File

@@ -1,4 +1,4 @@
//! # Events specification
//! # Events specification.
use std::ops::Deref;

View File

@@ -1,13 +1,13 @@
///! # format=flowed support
///!
///! Format=flowed is defined in
///! [RFC 3676](https://tools.ietf.org/html/rfc3676).
///!
///! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
///! during formatting, i.e., DelSp parameter introduced in RFC 3676
///! is assumed to be set to "no".
///!
///! For received messages, DelSp parameter is honoured.
//! # format=flowed support.
//!
//! Format=flowed is defined in
//! [RFC 3676](https://tools.ietf.org/html/rfc3676).
//!
//! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
//! during formatting, i.e., DelSp parameter introduced in RFC 3676
//! is assumed to be set to "no".
//!
//! For received messages, DelSp parameter is honoured.
/// Wraps line to 72 characters using format=flowed soft breaks.
///

View File

@@ -1,3 +1,5 @@
//! # List of email headers.
use crate::strum::AsStaticRef;
use mailparse::{MailHeader, MailHeaderMap};

View File

@@ -1,11 +1,12 @@
///! # Get message as HTML.
///!
///! Use `Message.has_html()` to check if the UI shall render a
///! corresponding button and `MsgId.get_html()` to get the full message.
///!
///! Even when the original mime-message is not HTML,
///! `MsgId.get_html()` will return HTML -
///! this allows nice quoting, handling linebreaks properly etc.
//! # Get message as HTML.
//!
//! Use `Message.has_html()` to check if the UI shall render a
//! corresponding button and `MsgId.get_html()` to get the full message.
//!
//! Even when the original mime-message is not HTML,
//! `MsgId.get_html()` will return HTML -
//! this allows nice quoting, handling linebreaks properly etc.
use futures::future::FutureExt;
use std::future::Future;
use std::pin::Pin;

View File

@@ -1,14 +1,14 @@
//! # Imap handling module
//! # IMAP handling module.
//!
//! uses [async-email/async-imap](https://github.com/async-email/async-imap)
//! to implement connect, fetch, delete functionality with standard IMAP servers.
use std::{cmp, cmp::max, collections::BTreeMap};
use anyhow::{bail, format_err, Context as _, Result};
use anyhow::{anyhow, bail, format_err, Context as _, Result};
use async_imap::{
error::Result as ImapResult,
types::{Fetch, Flag, Mailbox, Name, NameAttribute, UnsolicitedResponse},
types::{Fetch, Flag, Mailbox, Name, NameAttribute, Quota, QuotaRoot, UnsolicitedResponse},
};
use async_std::channel::Receiver;
use async_std::prelude::*;
@@ -27,6 +27,7 @@ use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{self, Action};
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::login_param::{ServerAddress, Socks5Config};
use crate::message::{self, update_server_uid, MessageState};
use crate::mimeparser;
use crate::oauth2::dc_get_oauth2_access_token;
@@ -142,6 +143,7 @@ impl FolderMeaning {
struct ImapConfig {
pub addr: String,
pub lp: ServerLoginParam,
pub socks5_config: Option<Socks5Config>,
pub strict_tls: bool,
pub oauth2: bool,
pub selected_folder: Option<String>,
@@ -153,6 +155,10 @@ struct ImapConfig {
/// True if the server has MOVE capability as defined in
/// <https://tools.ietf.org/html/rfc6851>
pub can_move: bool,
/// True if the server has QUOTA capability as defined in
/// <https://tools.ietf.org/html/rfc2087>
pub can_check_quota: bool,
}
impl Imap {
@@ -161,6 +167,7 @@ impl Imap {
/// `addr` is used to renew token if OAuth2 authentication is used.
pub async fn new(
lp: &ServerLoginParam,
socks5_config: Option<Socks5Config>,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
@@ -179,6 +186,7 @@ impl Imap {
let config = ImapConfig {
addr: addr.to_string(),
lp: lp.clone(),
socks5_config,
strict_tls,
oauth2,
selected_folder: None,
@@ -186,6 +194,7 @@ impl Imap {
selected_folder_needs_expunge: false,
can_idle: false,
can_move: false,
can_check_quota: false,
};
let imap = Imap {
@@ -217,6 +226,7 @@ impl Imap {
let imap = Self::new(
&param.imap,
param.socks5_config.clone(),
&param.addr,
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
param.provider.map_or(false, |provider| provider.strict_tls),
@@ -256,7 +266,20 @@ impl Imap {
let imap_server: &str = config.lp.server.as_ref();
let imap_port = config.lp.port;
match Client::connect_insecure((imap_server, imap_port)).await {
let connection = if let Some(socks5_config) = &config.socks5_config {
Client::connect_insecure_socks5(
&ServerAddress {
host: imap_server.to_string(),
port: imap_port,
},
socks5_config.clone(),
)
.await
} else {
Client::connect_insecure((imap_server, imap_port)).await
};
match connection {
Ok(client) => {
if config.lp.security == Socket::Starttls {
client.secure(imap_server, config.strict_tls).await
@@ -271,7 +294,20 @@ impl Imap {
let imap_server: &str = config.lp.server.as_ref();
let imap_port = config.lp.port;
Client::connect_secure((imap_server, imap_port), imap_server, config.strict_tls).await
if let Some(socks5_config) = &config.socks5_config {
Client::connect_secure_socks5(
&ServerAddress {
host: imap_server.to_string(),
port: imap_port,
},
config.strict_tls,
socks5_config.clone(),
)
.await
} else {
Client::connect_secure((imap_server, imap_port), imap_server, config.strict_tls)
.await
}
};
let login_res = match connection_res {
@@ -362,6 +398,7 @@ impl Imap {
Ok(caps) => {
self.config.can_idle = caps.has_str("IDLE");
self.config.can_move = caps.has_str("MOVE");
self.config.can_check_quota = caps.has_str("QUOTA");
self.capabilities_determined = true;
Ok(())
}
@@ -1392,6 +1429,22 @@ impl Imap {
}
unsolicited_exists
}
pub fn can_check_quota(&self) -> bool {
self.config.can_check_quota
}
pub async fn get_quota_roots(
&mut self,
mailbox_name: &str,
) -> Result<(Vec<QuotaRoot>, Vec<Quota>)> {
if let Some(session) = self.session.as_mut() {
let quota_roots = session.get_quota_root(mailbox_name).await?;
Ok(quota_roots)
} else {
Err(anyhow!("Not connected to IMAP, no session"))
}
}
}
/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
@@ -1636,7 +1689,7 @@ pub(crate) async fn prefetch_should_download(
// deleted from the database or has not arrived yet.
if let Some(rfc724_mid) = headers.get_header_value(HeaderDef::MessageId) {
if let Some(group_id) = dc_extract_grpid_from_rfc724_mid(&rfc724_mid) {
if let Ok((_chat_id, _, _)) = get_chat_id_by_grpid(context, group_id).await {
if get_chat_id_by_grpid(context, group_id).await?.is_some() {
return Ok(true);
}
}

View File

@@ -1,16 +1,24 @@
use std::ops::{Deref, DerefMut};
use std::{
ops::{Deref, DerefMut},
time::Duration,
};
use async_imap::{
error::{Error as ImapError, Result as ImapResult},
Client as ImapClient,
};
use async_smtp::ServerAddress;
use async_std::net::{self, TcpStream};
use super::session::Session;
use crate::login_param::dc_build_tls;
use crate::login_param::{dc_build_tls, Socks5Config};
use super::session::SessionStream;
/// IMAP write and read timeout in seconds.
const IMAP_TIMEOUT: u64 = 30;
#[derive(Debug)]
pub(crate) struct Client {
is_secure: bool,
@@ -111,6 +119,63 @@ impl Client {
})
}
pub async fn connect_secure_socks5(
target_addr: &ServerAddress,
strict_tls: bool,
socks5_config: Socks5Config,
) -> ImapResult<Self> {
let socks5_stream: Box<dyn SessionStream> = Box::new(
match socks5_config
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
.await
{
Ok(s) => s,
Err(e) => return ImapResult::Err(async_imap::error::Error::Bad(e.to_string())),
},
);
let tls = dc_build_tls(strict_tls);
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(target_addr.host.clone(), socks5_stream).await?);
let mut client = ImapClient::new(tls_stream);
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client {
is_secure: true,
inner: client,
})
}
pub async fn connect_insecure_socks5(
target_addr: &ServerAddress,
socks5_config: Socks5Config,
) -> ImapResult<Self> {
let socks5_stream: Box<dyn SessionStream> = Box::new(
match socks5_config
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
.await
{
Ok(s) => s,
Err(e) => return ImapResult::Err(async_imap::error::Error::Bad(e.to_string())),
},
);
let mut client = ImapClient::new(socks5_stream);
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client {
is_secure: false,
inner: client,
})
}
pub async fn secure(self, domain: &str, strict_tls: bool) -> ImapResult<Client> {
if self.is_secure {
Ok(self)

View File

@@ -42,6 +42,15 @@ impl Imap {
}
};
// Gmail labels are not folders and should be skipped. For example,
// emails appear in the inbox and under "All Mail" as soon as it is
// received. The code used to wrongly conclude that the email had
// already been moved and left it in the inbox.
let folder_name = folder.name();
if folder_name.starts_with("[Gmail]") {
continue;
}
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
@@ -93,7 +102,7 @@ impl Imap {
}
}
async fn get_watched_folders(context: &Context) -> Vec<String> {
pub(crate) async fn get_watched_folders(context: &Context) -> Vec<String> {
let mut res = Vec::new();
let folder_watched_configured = &[
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),

View File

@@ -3,6 +3,7 @@ use std::ops::{Deref, DerefMut};
use async_imap::Session as ImapSession;
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use fast_socks5::client::Socks5Stream;
#[derive(Debug)]
pub(crate) struct Session {
@@ -17,6 +18,7 @@ pub(crate) trait SessionStream:
impl SessionStream for TlsStream<Box<dyn SessionStream>> {}
impl SessionStream for TlsStream<TcpStream> {}
impl SessionStream for TcpStream {}
impl SessionStream for Socks5Stream<TcpStream> {}
impl Deref for Session {
type Target = ImapSession<Box<dyn SessionStream>>;

View File

@@ -1,4 +1,4 @@
//! # Import/export module
//! # Import/export module.
use std::any::Any;
use std::ffi::OsStr;
@@ -66,15 +66,15 @@ pub enum ImexMode {
/// Import/export things.
///
/// What to do is defined by the *what* parameter.
/// What to do is defined by the `what` parameter.
///
/// During execution of the job,
/// some events are sent out:
///
/// - A number of #DC_EVENT_IMEX_PROGRESS events are sent and may be used to create
/// - A number of `DC_EVENT_IMEX_PROGRESS` events are sent and may be used to create
/// a progress bar or stuff like that. Moreover, you'll be informed when the imex-job is done.
///
/// - For each file written on export, the function sends #DC_EVENT_IMEX_FILE_WRITTEN
/// - For each file written on export, the function sends `DC_EVENT_IMEX_FILE_WRITTEN`
///
/// Only one import-/export-progress can run at the same time.
/// To cancel an import-/export-progress, drop the future returned by this function.
@@ -204,6 +204,7 @@ pub async fn has_backup_old(context: &Context, dir_name: &Path) -> Result<String
}
}
/// Initiates key transfer via Autocrypt Setup Message.
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
use futures::future::FutureExt;
@@ -435,7 +436,7 @@ async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
Ok(plain_text)
}
pub fn normalize_setup_code(s: &str) -> String {
fn normalize_setup_code(s: &str) -> String {
let mut out = String::new();
for c in s.chars() {
if ('0'..='9').contains(&c) {

View File

@@ -1,4 +1,4 @@
//! # Job module
//! # Job module.
//!
//! This module implements a job queue maintained in the SQLite database
//! and job types.
@@ -95,6 +95,9 @@ pub enum Action {
FetchExistingMsgs = 110,
MarkseenMsgOnImap = 130,
// this is user initiated so it should have a fairly high priority
UpdateRecentQuota = 140,
// Moving message is prioritized lower than deletion so we don't
// bother moving message if it is already scheduled for deletion.
MoveMsg = 200,
@@ -130,6 +133,7 @@ impl From<Action> for Thread {
ResyncFolders => Thread::Imap,
MarkseenMsgOnImap => Thread::Imap,
MoveMsg => Thread::Imap,
UpdateRecentQuota => Thread::Imap,
MaybeSendLocations => Thread::Smtp,
MaybeSendLocationsEnded => Thread::Smtp,
@@ -148,7 +152,6 @@ pub struct Job {
pub added_timestamp: i64,
pub tries: u32,
pub param: Params,
pub pending_error: Option<String>,
}
impl fmt::Display for Job {
@@ -169,7 +172,6 @@ impl Job {
added_timestamp: timestamp,
tries: 0,
param,
pending_error: None,
}
}
@@ -251,12 +253,13 @@ impl Job {
smtp.connectivity.set_working(context).await;
let status = match smtp.send(context, recipients, message, job_id).await {
let send_result = smtp.send(context, recipients, message, job_id).await;
smtp.last_send_error = send_result.as_ref().err().map(|e| e.to_string());
let status = match send_result {
Err(crate::smtp::send::Error::SmtpSend(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {:?}", &err);
smtp.connectivity.set_err(context, &err).await;
self.pending_error = Some(err.to_string());
let res = match err {
async_smtp::smtp::error::Error::Permanent(ref response) => {
@@ -365,6 +368,7 @@ impl Job {
// SMTP server, if not yet done
if let Err(err) = smtp.connect_configured(context).await {
warn!(context, "SMTP connection failure: {:?}", err);
smtp.last_send_error = Some(format!("SMTP connection failure: {:#}", err));
return Status::RetryLater;
}
@@ -407,6 +411,8 @@ impl Job {
}
Err(err) => {
warn!(context, "failed to check message existence: {:?}", err);
smtp.last_send_error =
Some(format!("failed to check message existence: {:#}", err));
return Status::RetryLater;
}
}
@@ -521,6 +527,7 @@ impl Job {
// connect to SMTP server, if not yet done
if let Err(err) = smtp.connect_configured(context).await {
warn!(context, "SMTP connection failure: {:?}", err);
smtp.last_send_error = Some(err.to_string());
return Status::RetryLater;
}
@@ -1145,6 +1152,7 @@ async fn perform_job_action(
sql::housekeeping(context).await.ok_or_log(context);
Status::Finished(Ok(()))
}
Action::UpdateRecentQuota => context.update_recent_quota(connection.inbox()).await,
};
info!(context, "Finished immediate try {} of job {}", tries, job);
@@ -1207,7 +1215,8 @@ pub async fn add(context: &Context, job: Job) {
| Action::ResyncFolders
| Action::MarkseenMsgOnImap
| Action::FetchExistingMsgs
| Action::MoveMsg => {
| Action::MoveMsg
| Action::UpdateRecentQuota => {
info!(context, "interrupt: imap");
context
.interrupt_inbox(InterruptInfo::new(false, None))
@@ -1321,7 +1330,6 @@ LIMIT 1;
added_timestamp: row.get("added_timestamp")?,
tries: row.get("tries")?,
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
pending_error: None,
};
Ok(job)

View File

@@ -1,4 +1,4 @@
//! Cryptographic key module
//! Cryptographic key module.
use std::collections::BTreeMap;
use std::fmt;

View File

@@ -1,3 +1,5 @@
//! # Delta Chat Core Library.
#![forbid(unsafe_code)]
#![deny(
clippy::correctness,
@@ -7,7 +9,11 @@
clippy::wildcard_imports,
clippy::needless_borrow
)]
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
#![allow(
clippy::match_bool,
clippy::eval_order_dependence,
clippy::bool_assert_comparison
)]
#[macro_use]
extern crate num_derive;
@@ -55,7 +61,7 @@ mod imap;
pub mod imex;
mod scheduler;
#[macro_use]
pub mod job;
mod job;
mod format_flowed;
pub mod key;
mod keyring;
@@ -71,6 +77,7 @@ pub mod peerstate;
pub mod pgp;
pub mod provider;
pub mod qr;
pub mod quota;
pub mod securejoin;
mod simplify;
mod smtp;

View File

@@ -1,4 +1,4 @@
//! Location handling
//! Location handling.
use std::convert::TryFrom;
use anyhow::{ensure, Error};
@@ -190,7 +190,7 @@ impl Kml {
}
}
// location streaming
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) {
let now = time();
if !(seconds < 0 || chat_id.is_special()) {
@@ -221,7 +221,7 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
.unwrap_or_default();
} else if 0 == seconds && is_sending_locations_before {
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, stock_str).await;
chat::add_info_msg(context, chat_id, stock_str, now).await;
}
context.emit_event(EventType::ChatModified(chat_id));
if 0 != seconds {
@@ -716,7 +716,8 @@ pub(crate) async fn job_maybe_send_locations_ended(
.await
);
if !(send_begin != 0 && time() <= send_until) {
let now = time();
if !(send_begin != 0 && now <= send_until) {
// still streaming -
// may happen as several calls to dc_send_locations_to_chat()
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
@@ -735,7 +736,7 @@ pub(crate) async fn job_maybe_send_locations_ended(
);
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, stock_str).await;
chat::add_info_msg(context, chat_id, stock_str, now).await;
context.emit_event(EventType::ChatModified(chat_id));
}
}

View File

@@ -1,4 +1,5 @@
//! # Logging
//! # Logging.
use crate::context::Context;
#[macro_export]

View File

@@ -1,12 +1,19 @@
//! # Login parameters
//! # Login parameters.
use std::borrow::Cow;
use std::fmt;
use std::time::Duration;
use crate::provider::{get_provider_by_id, Provider};
use crate::{context::Context, provider::Socket};
use anyhow::Result;
use async_std::io;
use async_std::net::TcpStream;
pub use async_smtp::ServerAddress;
use fast_socks5::client::Socks5Stream;
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
@@ -44,6 +51,81 @@ pub struct ServerLoginParam {
pub certificate_checks: CertificateChecks,
}
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Socks5Config {
pub host: String,
pub port: u16,
pub user_password: Option<(String, String)>,
}
impl Socks5Config {
/// Reads SOCKS5 configuration from the database.
pub async fn from_database(context: &Context) -> Result<Option<Self>> {
let sql = &context.sql;
let enabled = sql.get_raw_config_bool("socks5_enabled").await?;
if enabled {
let host = sql.get_raw_config("socks5_host").await?.unwrap_or_default();
let port: u16 = sql
.get_raw_config_int("socks5_port")
.await?
.unwrap_or_default() as u16;
let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default();
let password = sql
.get_raw_config("socks5_password")
.await?
.unwrap_or_default();
let socks5_config = Self {
host,
port,
user_password: if !user.is_empty() {
Some((user, password))
} else {
None
},
};
Ok(Some(socks5_config))
} else {
Ok(None)
}
}
pub async fn connect(
&self,
target_addr: &ServerAddress,
timeout: Option<Duration>,
) -> io::Result<Socks5Stream<TcpStream>> {
self.to_async_smtp_socks5_config()
.connect(target_addr, timeout)
.await
}
pub fn to_async_smtp_socks5_config(&self) -> async_smtp::smtp::Socks5Config {
async_smtp::smtp::Socks5Config {
host: self.host.clone(),
port: self.port,
user_password: self.user_password.clone(),
}
}
}
impl fmt::Display for Socks5Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"host:{},port:{},user_password:{}",
self.host,
self.port,
if let Some(user_password) = self.user_password.clone() {
format!("user: {}, password: ***", user_password.0)
} else {
"user: None".to_string()
}
)
}
}
#[derive(Default, Debug, Clone, PartialEq)]
pub struct LoginParam {
pub addr: String,
@@ -51,6 +133,7 @@ pub struct LoginParam {
pub smtp: ServerLoginParam,
pub server_flags: i32,
pub provider: Option<&'static Provider>,
pub socks5_config: Option<Socks5Config>,
}
impl LoginParam {
@@ -130,6 +213,8 @@ impl LoginParam {
.await?
.and_then(|provider_id| get_provider_by_id(&provider_id));
let socks5_config = Socks5Config::from_database(context).await?;
Ok(LoginParam {
addr,
imap: ServerLoginParam {
@@ -150,6 +235,7 @@ impl LoginParam {
},
provider,
server_flags,
socks5_config,
})
}
@@ -334,6 +420,8 @@ mod tests {
},
server_flags: 0,
provider: get_provider_by_id("example.com"),
// socks5_config is not saved by `save_to_database`, using default value
socks5_config: None,
};
param.save_to_database(&t, "foobar_").await?;

View File

@@ -1,3 +1,5 @@
//! # Legacy generic return values for C API.
use deltachat_derive::{FromSql, ToSql};
use crate::key::Fingerprint;

View File

@@ -1,4 +1,4 @@
//! # Messages and their identifiers
//! # Messages and their identifiers.
use std::collections::BTreeMap;
use std::convert::TryInto;
@@ -14,13 +14,13 @@ use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, VideochatType, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_INFO,
DC_CONTACT_ID_SELF, DC_MAX_GET_INFO_LEN, DC_MAX_GET_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
DC_CONTACT_ID_SELF, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::dc_tools::{
dc_get_filebytes, dc_get_filemeta, dc_gm2local_offset, dc_read_file, dc_timestamp_to_str,
dc_truncate, time,
dc_create_smeared_timestamp, dc_get_filebytes, dc_get_filemeta, dc_gm2local_offset,
dc_read_file, dc_timestamp_to_str, dc_truncate, time,
};
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
@@ -541,9 +541,7 @@ impl Message {
}
pub fn get_text(&self) -> Option<String> {
self.text
.as_ref()
.map(|text| dc_truncate(text, DC_MAX_GET_TEXT_LEN).to_string())
self.text.as_ref().map(|s| s.to_string())
}
pub fn get_subject(&self) -> &str {
@@ -1142,7 +1140,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
return Ok(ret);
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = dc_truncate(rawtxt.trim(), DC_MAX_GET_INFO_LEN);
let rawtxt = dc_truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN);
let fts = dc_timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {}", fts);
@@ -1805,7 +1803,13 @@ async fn ndn_maybe_add_info_msg(
// Tell the user which of the recipients failed if we know that (because in
// a group, this might otherwise be unclear)
let text = stock_str::failed_sending_to(context, contact.get_display_name()).await;
chat::add_info_msg(context, chat_id, text).await;
chat::add_info_msg(
context,
chat_id,
text,
dc_create_smeared_timestamp(context).await,
)
.await;
context.emit_event(EventType::ChatModified(chat_id));
}
}

View File

@@ -1,3 +1,5 @@
//! # MIME message production.
use std::convert::TryInto;
use anyhow::{bail, ensure, format_err, Result};

View File

@@ -1,3 +1,5 @@
//! # MIME message parsing module.
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
@@ -10,7 +12,7 @@ use once_cell::sync::Lazy;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSE};
use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
use crate::contact::addr_normalize;
use crate::context::Context;
use crate::dc_tools::{dc_get_filemeta, dc_truncate};
@@ -276,7 +278,7 @@ impl MimeMessage {
parser.maybe_remove_bad_parts();
parser.maybe_remove_inline_mailinglist_footer();
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context).await;
parser.parse_headers(context).await?;
if warn_empty_signature && parser.signatures.is_empty() {
for part in parser.parts.iter_mut() {
@@ -293,7 +295,7 @@ impl MimeMessage {
/// Parses system messages.
fn parse_system_message_headers(&mut self, context: &Context) {
if self.get(HeaderDef::AutocryptSetupMessage).is_some() {
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() {
self.parts = self
.parts
.iter()
@@ -309,7 +311,7 @@ impl MimeMessage {
} else {
warn!(context, "could not determine ASM mime-part");
}
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
} else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
if value == "location-streaming-enabled" {
self.is_system_message = SystemMessage::LocationStreamingEnabled;
} else if value == "ephemeral-timer-changed" {
@@ -324,19 +326,19 @@ impl MimeMessage {
/// Parses avatar action headers.
async fn parse_avatar_headers(&mut self, context: &Context) {
if let Some(header_value) = self.get(HeaderDef::ChatGroupAvatar).cloned() {
if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar).cloned() {
self.group_avatar = self.avatar_action_from_header(context, header_value).await;
}
if let Some(header_value) = self.get(HeaderDef::ChatUserAvatar).cloned() {
if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar).cloned() {
self.user_avatar = self.avatar_action_from_header(context, header_value).await;
}
}
fn parse_videochat_headers(&mut self) {
if let Some(value) = self.get(HeaderDef::ChatContent).cloned() {
if let Some(value) = self.get_header(HeaderDef::ChatContent).cloned() {
if value == "videochat-invitation" {
let instance = self.get(HeaderDef::ChatWebrtcRoom).cloned();
let instance = self.get_header(HeaderDef::ChatWebrtcRoom).cloned();
if let Some(part) = self.parts.first_mut() {
part.typ = Viewtype::VideochatInvitation;
part.param
@@ -393,11 +395,12 @@ impl MimeMessage {
}
if let Some(mut part) = self.parts.pop() {
if part.typ == Viewtype::Audio && self.get(HeaderDef::ChatVoiceMessage).is_some() {
if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
{
part.typ = Viewtype::Voice;
}
if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
if let Some(value) = self.get(HeaderDef::ChatContent) {
if let Some(value) = self.get_header(HeaderDef::ChatContent) {
if value == "sticker" {
part.typ = Viewtype::Sticker;
}
@@ -407,7 +410,7 @@ impl MimeMessage {
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video
{
if let Some(field_0) = self.get(HeaderDef::ChatDuration) {
if let Some(field_0) = self.get_header(HeaderDef::ChatDuration) {
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
part.param.set_int(Param::Duration, duration_ms);
@@ -419,7 +422,7 @@ impl MimeMessage {
}
}
async fn parse_headers(&mut self, context: &Context) {
async fn parse_headers(&mut self, context: &Context) -> Result<()> {
self.parse_system_message_headers(context);
self.parse_avatar_headers(context).await;
self.parse_videochat_headers();
@@ -464,15 +467,20 @@ impl MimeMessage {
if !self.decrypting_failed && !self.parts.is_empty() {
if let Some(ref dn_to) = self.chat_disposition_notification_to {
if let Some(from) = self.from.get(0) {
if from.addr.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
// Check that the message is not outgoing.
if !context.is_self_addr(&from.addr).await? {
if from.addr.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring",
from.addr,
dn_to.addr
);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring", from.addr, dn_to.addr
);
}
}
}
@@ -502,6 +510,8 @@ impl MimeMessage {
part.param.set(Param::Bot, "1");
}
}
Ok(())
}
async fn avatar_action_from_header(
@@ -582,12 +592,12 @@ impl MimeMessage {
}
pub(crate) fn get_subject(&self) -> Option<String> {
self.get(HeaderDef::Subject)
self.get_header(HeaderDef::Subject)
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
}
pub fn get(&self, headerdef: HeaderDef) -> Option<&String> {
pub fn get_header(&self, headerdef: HeaderDef) -> Option<&String> {
self.header.get(headerdef.get_headername())
}
@@ -896,13 +906,14 @@ impl MimeMessage {
(simplified_txt, top_quote)
};
let simplified_txt =
if simplified_txt.len() > DC_DESIRED_TEXT_LEN + DC_ELLIPSE.len() {
self.is_mime_modified = true;
dc_truncate(&*simplified_txt, DC_DESIRED_TEXT_LEN).to_string()
} else {
simplified_txt
};
let simplified_txt = if simplified_txt.chars().count()
> DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len()
{
self.is_mime_modified = true;
dc_truncate(&*simplified_txt, DC_DESIRED_TEXT_LEN).to_string()
} else {
simplified_txt
};
if !simplified_txt.is_empty() || simplified_quote.is_some() {
let mut part = Part {
@@ -1009,12 +1020,12 @@ impl MimeMessage {
}
pub(crate) fn get_mailinglist_type(&self) -> MailinglistType {
if self.get(HeaderDef::ListId).is_some() {
if self.get_header(HeaderDef::ListId).is_some() {
return MailinglistType::ListIdBased;
} else if self.get(HeaderDef::Sender).is_some() {
} else if self.get_header(HeaderDef::Sender).is_some() {
// the `Sender:`-header alone is no indicator for mailing list
// as also used for bot-impersonation via `set_override_sender_name()`
if let Some(precedence) = self.get(HeaderDef::Precedence) {
if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
if precedence == "list" || precedence == "bulk" {
return MailinglistType::SenderBased;
}
@@ -1040,8 +1051,8 @@ impl MimeMessage {
}
pub fn get_rfc724_mid(&self) -> Option<String> {
self.get(HeaderDef::XMicrosoftOriginalMessageId)
.or_else(|| self.get(HeaderDef::MessageId))
self.get_header(HeaderDef::XMicrosoftOriginalMessageId)
.or_else(|| self.get_header(HeaderDef::MessageId))
.and_then(|msgid| parse_message_id(msgid).ok())
}
@@ -1228,7 +1239,7 @@ impl MimeMessage {
/// Also you should add a test in dc_receive_imf.rs (there already are lots of test_parse_ndn_* tests).
#[allow(clippy::indexing_slicing)]
async fn heuristically_parse_ndn(&mut self, context: &Context) {
let maybe_ndn = if let Some(from) = self.get(HeaderDef::From_) {
let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
from.contains("mailer-daemon") || from.contains("mail-daemon")
} else {
@@ -1303,7 +1314,7 @@ impl MimeMessage {
/// database, returns None.
pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
let parent_timestamp = if let Some(field) = self
.get(HeaderDef::InReplyTo)
.get_header(HeaderDef::InReplyTo)
.and_then(|msgid| parse_message_id(msgid).ok())
{
context
@@ -1347,7 +1358,9 @@ async fn update_gossip_peerstates(
peerstate = Some(p);
}
if let Some(peerstate) = peerstate {
peerstate.handle_fingerprint_change(context).await?;
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
}
gossipped_addr.insert(header.addr.clone());
@@ -1595,7 +1608,6 @@ mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::constants::DC_MAX_GET_TEXT_LEN;
use crate::{
chatlist::Chatlist,
config::Config,
@@ -1971,11 +1983,11 @@ mod tests {
.unwrap();
// non-overwritten headers do not bubble up
let of = mimeparser.get(HeaderDef::SecureJoinGroup).unwrap();
let of = mimeparser.get_header(HeaderDef::SecureJoinGroup).unwrap();
assert_eq!(of, "no");
// unknown headers do not bubble upwards
let of = mimeparser.get(HeaderDef::_TestHeader).unwrap();
let of = mimeparser.get_header(HeaderDef::_TestHeader).unwrap();
assert_eq!(of, "Bar");
// the following fields would bubble up
@@ -1984,13 +1996,15 @@ mod tests {
// for Chat-Version, also the case-insensivity is tested.
assert_eq!(mimeparser.get_subject(), Some("outer-subject".into()));
let of = mimeparser.get(HeaderDef::ChatVersion).unwrap();
let of = mimeparser.get_header(HeaderDef::ChatVersion).unwrap();
assert_eq!(of, "0.0");
assert_eq!(mimeparser.parts.len(), 1);
// make sure, headers that are only allowed in the encrypted part
// cannot be set from the outer part
assert!(mimeparser.get(HeaderDef::SecureJoinFingerprint).is_none());
assert!(mimeparser
.get_header(HeaderDef::SecureJoinFingerprint)
.is_none());
}
#[async_std::test]
@@ -2871,21 +2885,20 @@ On 2020-10-25, Bob wrote:
let t = TestContext::new().await;
static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n";
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_MAX_GET_TEXT_LEN
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_DESIRED_TEXT_LEN
let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT));
assert!(DC_DESIRED_TEXT_LEN + DC_ELLIPSE.len() < DC_MAX_GET_TEXT_LEN);
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref())
.await
.unwrap();
assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT);
assert!(long_txt.len() > DC_MAX_GET_TEXT_LEN);
assert!(long_txt.len() > DC_DESIRED_TEXT_LEN);
assert!(mimemsg.is_mime_modified);
assert!(
mimemsg.parts[0].msg.matches("just repeated").count()
< DC_MAX_GET_TEXT_LEN / REPEAT_TXT.len()
<= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
);
assert!(mimemsg.parts[0].msg.len() <= DC_MAX_GET_TEXT_LEN);
assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
}
#[async_std::test]
@@ -2952,4 +2965,36 @@ Some reply
Ok(())
}
// Test that WantsMdn parameter is not set on outgoing messages.
#[async_std::test]
async fn test_outgoing_wants_mdn() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let raw = br###"Date: Thu, 28 Jan 2021 00:26:57 +0000
Chat-Version: 1.0\n\
Message-ID: <foobarbaz@example.org>
To: Bob <bob@example.org>
From: Alice <alice@example.com>
Subject: subject
Chat-Disposition-Notification-To: alice@example.com
Message.
"###;
// Bob receives message.
dc_receive_imf(&bob, raw, "INBOX", 1, false).await?;
let msg = bob.get_last_msg().await;
// Message is incoming.
assert!(msg.param.get_bool(Param::WantsMdn).unwrap());
// Alice receives copy-to-self.
dc_receive_imf(&alice, raw, "INBOX", 1, false).await?;
let msg = alice.get_last_msg().await;
// Message is outgoing, don't send read receipt to self.
assert!(msg.param.get_bool(Param::WantsMdn).is_none());
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
//! OAuth 2 module
//! OAuth 2 module.
use std::collections::HashMap;
@@ -6,6 +6,7 @@ use anyhow::Result;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::time;
use crate::provider;
@@ -55,22 +56,19 @@ pub async fn dc_get_oauth2_url(
context: &Context,
addr: &str,
redirect_uri: &str,
) -> Option<String> {
if let Some(oauth2) = Oauth2::from_address(addr).await {
if context
) -> Result<Option<String>> {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
if let Some(oauth2) = Oauth2::from_address(addr, socks5_enabled).await {
context
.sql
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri))
.await
.is_err()
{
return None;
}
.await?;
let oauth2_url = replace_in_uri(oauth2.get_code, "$CLIENT_ID", oauth2.client_id);
let oauth2_url = replace_in_uri(&oauth2_url, "$REDIRECT_URI", redirect_uri);
Some(oauth2_url)
Ok(Some(oauth2_url))
} else {
None
Ok(None)
}
}
@@ -80,7 +78,8 @@ pub async fn dc_get_oauth2_access_token(
code: &str,
regenerate: bool,
) -> Result<Option<String>> {
if let Some(oauth2) = Oauth2::from_address(addr).await {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
if let Some(oauth2) = Oauth2::from_address(addr, socks5_enabled).await {
let lock = context.oauth2_mutex.lock().await;
// read generated token
@@ -225,7 +224,8 @@ pub async fn dc_get_oauth2_addr(
addr: &str,
code: &str,
) -> Result<Option<String>> {
let oauth2 = match Oauth2::from_address(addr).await {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
let oauth2 = match Oauth2::from_address(addr, socks5_enabled).await {
Some(o) => o,
None => return Ok(None),
};
@@ -253,13 +253,13 @@ pub async fn dc_get_oauth2_addr(
}
impl Oauth2 {
async fn from_address(addr: &str) -> Option<Self> {
async fn from_address(addr: &str, skip_mx: bool) -> Option<Self> {
let addr_normalized = normalize_addr(addr);
if let Some(domain) = addr_normalized
.find('@')
.map(|index| addr_normalized.split_at(index + 1).1)
{
if let Some(oauth2_authorizer) = provider::get_provider_info(domain)
if let Some(oauth2_authorizer) = provider::get_provider_info(domain, skip_mx)
.await
.and_then(|provider| provider.oauth2_authorizer.as_ref())
{
@@ -357,29 +357,29 @@ mod tests {
#[async_std::test]
async fn test_oauth_from_address() {
assert_eq!(
Oauth2::from_address("hello@gmail.com").await,
Oauth2::from_address("hello@gmail.com", false).await,
Some(OAUTH2_GMAIL)
);
assert_eq!(
Oauth2::from_address("hello@googlemail.com").await,
Oauth2::from_address("hello@googlemail.com", false).await,
Some(OAUTH2_GMAIL)
);
assert_eq!(
Oauth2::from_address("hello@yandex.com").await,
Oauth2::from_address("hello@yandex.com", false).await,
Some(OAUTH2_YANDEX)
);
assert_eq!(
Oauth2::from_address("hello@yandex.ru").await,
Oauth2::from_address("hello@yandex.ru", false).await,
Some(OAUTH2_YANDEX)
);
assert_eq!(Oauth2::from_address("hello@web.de").await, None);
assert_eq!(Oauth2::from_address("hello@web.de", false).await, None);
}
#[async_std::test]
async fn test_oauth_from_mx() {
assert_eq!(
Oauth2::from_address("hello@google.com").await,
Oauth2::from_address("hello@google.com", false).await,
Some(OAUTH2_GMAIL)
);
}
@@ -399,7 +399,9 @@ mod tests {
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let redirect_uri = "chat.delta:/com.b44t.messenger";
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri).await;
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri)
.await
.unwrap();
assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878%2D4mvtgf6feshskf7695nfln6002mom908%2Eapps%2Egoogleusercontent%2Ecom&redirect_uri=chat%2Edelta%3A%2Fcom%2Eb44t%2Emessenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into()));
}

View File

@@ -1,4 +1,4 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module.
use std::collections::HashSet;
use std::fmt;
@@ -260,7 +260,11 @@ impl Peerstate {
}
/// Adds a warning to the chat corresponding to peerstate if fingerprint has changed.
pub(crate) async fn handle_fingerprint_change(&self, context: &Context) -> Result<()> {
pub(crate) async fn handle_fingerprint_change(
&self,
context: &Context,
timestamp: i64,
) -> Result<()> {
if self.fingerprint_changed {
if let Some(contact_id) = context
.sql
@@ -273,7 +277,7 @@ impl Peerstate {
let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await;
chat::add_info_msg(context, chat_id, msg).await;
chat::add_info_msg(context, chat_id, msg, timestamp).await;
emit_event!(context, EventType::ChatModified(chat_id));
} else {
bail!("contact with peerstate.addr {:?} not found", &self.addr);

View File

@@ -1,4 +1,4 @@
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp)
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
use std::collections::{BTreeMap, HashSet};
use std::io;

View File

@@ -1,4 +1,5 @@
///! Handle plain text together with some attributes.
//! Handle plain text together with some attributes.
use crate::simplify::split_lines;
use once_cell::sync::Lazy;

View File

@@ -1,4 +1,4 @@
//! [Provider database](https://providers.delta.chat/) module
//! [Provider database](https://providers.delta.chat/) module.
mod data;
@@ -89,15 +89,17 @@ pub struct Provider {
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
pub async fn get_provider_info(domain: &str) -> Option<&'static Provider> {
pub async fn get_provider_info(domain: &str, skip_mx: bool) -> Option<&'static Provider> {
let domain = domain.rsplitn(2, '@').next()?;
if let Some(provider) = get_provider_by_domain(domain) {
return Some(provider);
}
if let Some(provider) = get_provider_by_mx(domain).await {
return Some(provider);
if !skip_mx {
if let Some(provider) = get_provider_by_mx(domain).await {
return Some(provider);
}
}
None
@@ -221,11 +223,17 @@ mod tests {
#[async_std::test]
async fn test_get_provider_info() {
assert!(get_provider_info("").await.is_none());
assert!(get_provider_info("google.com").await.unwrap().id == "gmail");
assert!(get_provider_info("", false).await.is_none());
assert!(get_provider_info("google.com", false).await.unwrap().id == "gmail");
// get_provider_info() accepts email addresses for backwards compatibility
assert!(get_provider_info("example@google.com").await.unwrap().id == "gmail");
assert!(
get_provider_info("example@google.com", false)
.await
.unwrap()
.id
== "gmail"
);
}
#[test]

View File

@@ -585,8 +585,8 @@ static P_HEY_COM: Lazy<Provider> = Lazy::new(|| {
// i.ua.md: i.ua
static P_I_UA: Lazy<Provider> = Lazy::new(|| Provider {
id: "i.ua",
status: Status::Ok,
before_login_hint: "",
status: Status::Broken,
before_login_hint: "Протокол IMAP не предоставляется и не планируется.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/i-ua",
server: vec![],
@@ -686,6 +686,35 @@ static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| {
}
});
// mail2tor.md: mail2tor.com
static P_MAIL2TOR: Lazy<Provider> = Lazy::new(|| Provider {
id: "mail2tor",
status: Status::Preparation,
before_login_hint: "Tor is needed to connect to the email servers.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail2tor",
server: vec![
Server {
protocol: Imap,
socket: Plain,
hostname: "g77kjrad6bafzzyldqvffq6kxlsgphcygptxhnn4xlnktfgaqshilmyd.onion",
port: 143,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Plain,
hostname: "xc7tgk2c5onxni2wsy76jslfsitxjbbptejnqhw6gy2ft7khpevhc7ad.onion",
port: 25,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
// mailbox.org.md: mailbox.org, secure.mailbox.org
static P_MAILBOX_ORG: Lazy<Provider> = Lazy::new(|| Provider {
id: "mailbox.org",
@@ -1286,6 +1315,25 @@ static P_YANDEX_RU: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: Some(Oauth2Authorizer::Yandex),
});
// yggmail.md: yggmail
static P_YGGMAIL: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "yggmail",
status: Status::Preparation,
before_login_hint: "An Yggmail companion app needs to be installed on your device to access the Yggmail network.",
after_login_hint: "Make sure, the Yggmail companion app runs whenever you want to use this account. Note, that you usually cannot write from @yggmail addresses to normal e-mail-addresses (as @gmx.net). However, you can create another account in the normal e-mail-network for this purpose.",
overview_page: "https://providers.delta.chat/yggmail",
server: vec![
Server { protocol: Imap, socket: Plain, hostname: "localhost", port: 1143, username_pattern: Email },
Server { protocol: Smtp, socket: Plain, hostname: "localhost", port: 1025, username_pattern: Email },
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
// ziggo.nl.md: ziggo.nl
static P_ZIGGO_NL: Lazy<Provider> = Lazy::new(|| Provider {
id: "ziggo.nl",
@@ -1395,6 +1443,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("internet.ru", &*P_MAIL_RU),
("bk.ru", &*P_MAIL_RU),
("list.ru", &*P_MAIL_RU),
("mail2tor.com", &*P_MAIL2TOR),
("mailbox.org", &*P_MAILBOX_ORG),
("secure.mailbox.org", &*P_MAILBOX_ORG),
("mailo.com", &*P_MAILO_COM),
@@ -1528,6 +1577,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("yandex.ua", &*P_YANDEX_RU),
("ya.ru", &*P_YANDEX_RU),
("narod.ru", &*P_YANDEX_RU),
("yggmail", &*P_YGGMAIL),
("ziggo.nl", &*P_ZIGGO_NL),
("zohomail.eu", &*P_ZOHO),
("zoho.com", &*P_ZOHO),
@@ -1568,6 +1618,7 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("kolst.com", &*P_KOLST_COM),
("kontent.com", &*P_KONTENT_COM),
("mail.ru", &*P_MAIL_RU),
("mail2tor", &*P_MAIL2TOR),
("mailbox.org", &*P_MAILBOX_ORG),
("mailo.com", &*P_MAILO_COM),
("nauta.cu", &*P_NAUTA_CU),
@@ -1592,6 +1643,7 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("web.de", &*P_WEB_DE),
("yahoo", &*P_YAHOO),
("yandex.ru", &*P_YANDEX_RU),
("yggmail", &*P_YGGMAIL),
("ziggo.nl", &*P_ZIGGO_NL),
("zoho", &*P_ZOHO),
]
@@ -1601,4 +1653,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, 7, 28));
Lazy::new(|| chrono::NaiveDate::from_ymd(2021, 8, 17));

View File

@@ -63,7 +63,7 @@ def process_data(data, file):
raise TypeError("no domains found")
for domain in data["domains"]:
domain = cleanstr(domain)
if domain == "" or domain.count(".") < 1 or domain.lower() != domain:
if domain == "" or domain.lower() != domain:
raise TypeError("bad domain: " + domain)
global domains_set
@@ -84,7 +84,7 @@ def process_data(data, file):
for s in data["server"]:
hostname = cleanstr(s.get("hostname", ""))
port = int(s.get("port", ""))
if hostname == "" or hostname.count(".") < 1 or port <= 0:
if hostname == "" or hostname.lower() != hostname or port <= 0:
raise TypeError("bad hostname or port")
protocol = s.get("type", "").upper()
@@ -96,7 +96,7 @@ def process_data(data, file):
raise TypeError("bad protocol")
socket = s.get("socket", "").upper()
if socket != "STARTTLS" and socket != "SSL":
if socket != "STARTTLS" and socket != "SSL" and socket != "PLAIN":
raise TypeError("bad socket")
username_pattern = s.get("username_pattern", "EMAIL").upper()

View File

@@ -1,4 +1,4 @@
//! # QR code module
//! # QR code module.
use anyhow::{bail, ensure, format_err, Error};
use once_cell::sync::Lazy;
@@ -11,6 +11,7 @@ use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, Origin};
use crate::context::Context;
use crate::dc_tools::time;
use crate::key::Fingerprint;
use crate::log::LogExt;
use crate::lot::{Lot, LotState};
@@ -160,7 +161,13 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
.await
.log_err(context, "Failed to create (new) chat for contact")
{
chat::add_info_msg(context, chat.id, format!("{} verified.", peerstate.addr)).await;
chat::add_info_msg(
context,
chat.id,
format!("{} verified.", peerstate.addr),
time(),
)
.await;
}
} else if let Some(addr) = addr {
lot.state = LotState::QrFprMismatch;
@@ -324,11 +331,9 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error
let chat_id = if lot.state == LotState::QrReviveVerifyContact {
None
} else {
Some(
get_chat_id_by_grpid(context, &lot.text2.unwrap_or_default())
.await?
.0,
)
get_chat_id_by_grpid(context, &lot.text2.unwrap_or_default())
.await?
.map(|(chat_id, _protected, _blocked)| chat_id)
};
token::save(
context,

102
src/quota.rs Normal file
View File

@@ -0,0 +1,102 @@
//! # Support for IMAP QUOTA extension.
use anyhow::{anyhow, Result};
use async_imap::types::{Quota, QuotaResource};
use indexmap::IndexMap;
use crate::context::Context;
use crate::dc_tools::time;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::Imap;
use crate::job::{Action, Status};
use crate::param::Params;
use crate::{job, EventType};
/// warn about a nearly full mailbox after this usage percentage is reached.
/// quota icon is "yellow".
pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80;
// warning is already issued at QUOTA_WARN_THRESHOLD_PERCENTAGE,
// this threshold only makes the quota icon "red".
pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 99;
// if recent quota is older,
// it is re-fetched on dc_get_connectivity_html()
pub const QUOTA_MAX_AGE_SECONDS: i64 = 60;
#[derive(Debug)]
pub struct QuotaInfo {
/// Recently loaded quota information.
/// set to `Err()` if the provider does not support quota or on other errors,
/// set to `Ok()` for valid quota information.
/// Updated by `Action::UpdateRecentQuota`
pub(crate) recent: Result<IndexMap<String, Vec<QuotaResource>>>,
/// Timestamp when structure was modified.
pub(crate) modified: i64,
}
async fn get_unique_quota_roots_and_usage(
folders: Vec<String>,
imap: &mut Imap,
) -> Result<IndexMap<String, Vec<QuotaResource>>> {
let mut unique_quota_roots: IndexMap<String, Vec<QuotaResource>> = IndexMap::new();
for folder in folders {
let (quota_roots, quotas) = &imap.get_quota_roots(&folder).await?;
// if there are new quota roots found in this imap folder, add them to the list
for qr_entries in quota_roots {
for quota_root_name in &qr_entries.quota_root_names {
// the quota for that quota root
let quota: Quota = quotas
.iter()
.find(|q| &q.root_name == quota_root_name)
.cloned()
.ok_or_else(|| anyhow!("quota_root should have a quota"))?;
// replace old quotas, because between fetching quotaroots for folders,
// messages could be recieved and so the usage could have been changed
*unique_quota_roots
.entry(quota_root_name.clone())
.or_insert(vec![]) = quota.resources;
}
}
}
Ok(unique_quota_roots)
}
impl Context {
// Adds a job to update `quota.recent`
pub(crate) async fn schedule_quota_update(&self) {
job::kill_action(self, Action::UpdateRecentQuota).await;
job::add(
self,
job::Job::new(Action::UpdateRecentQuota, 0, Params::new(), 0),
)
.await;
}
/// Updates `quota.recent`, sets `quota.modified` to the current time
/// and emits an event to let the UIs update connectivity view.
///
/// Called in response to `Action::UpdateRecentQuota`.
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(self).await {
warn!(self, "could not connect: {:?}", err);
return Status::RetryNow;
}
let quota = if imap.can_check_quota() {
let folders = get_watched_folders(self).await;
get_unique_quota_roots_and_usage(folders, imap).await
} else {
Err(anyhow!("Quota not supported by your provider."))
};
*self.quota.write().await = Some(QuotaInfo {
recent: quota,
modified: time(),
});
self.emit_event(EventType::ConnectivityChanged);
Status::Finished(Ok(()))
}
}

View File

@@ -48,7 +48,7 @@ impl Context {
pub async fn maybe_network_lost(&self) {
let lock = self.scheduler.read().await;
lock.maybe_network_lost().await;
connectivity::idle_interrupted(lock).await;
connectivity::maybe_network_lost(self, lock).await;
}
pub(crate) async fn interrupt_inbox(&self, info: InterruptInfo) {
@@ -298,7 +298,11 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
None => {
// Fake Idle
info!(ctx, "smtp fake idle - started");
connection.connectivity.set_connected(&ctx).await;
match &connection.last_send_error {
None => connection.connectivity.set_connected(&ctx).await,
Some(err) => connection.connectivity.set_err(&ctx, err).await,
}
interrupt_info = idle_interrupt_receiver.recv().await.unwrap_or_default();
info!(ctx, "smtp fake idle - interrupted")
}

View File

@@ -3,9 +3,15 @@ use std::{ops::Deref, sync::Arc};
use async_std::sync::{Mutex, RwLockReadGuard};
use crate::dc_tools::time;
use crate::events::EventType;
use crate::{config::Config, scheduler::Scheduler};
use crate::quota::{
QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_MAX_AGE_SECONDS, QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
use crate::{config::Config, dc_tools, scheduler::Scheduler};
use crate::{context::Context, log::LogExt};
use anyhow::{anyhow, Result};
use humansize::{file_size_opts, FileSize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
pub enum Connectivity {
@@ -20,7 +26,7 @@ pub enum Connectivity {
// the top) take priority. This means that e.g. if any folder has an error - usually
// because there is no internet connection - the connectivity for the whole
// account will be `Notconnected`.
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty)]
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
enum DetailedConnectivity {
Error(String),
Uninitialized,
@@ -193,6 +199,43 @@ pub(crate) async fn idle_interrupted(scheduler: RwLockReadGuard<'_, Scheduler>)
// of what we do here.
}
/// Set the connectivity to "Not connected" after a call to dc_maybe_network_lost().
/// If we did not do this, the connectivity would stay "Connected" for quite a long time
/// after `maybe_network_lost()` was called.
pub(crate) async fn maybe_network_lost(
context: &Context,
scheduler: RwLockReadGuard<'_, Scheduler>,
) {
let stores = match &*scheduler {
Scheduler::Running {
inbox,
mvbox,
sentbox,
..
} => [
inbox.state.connectivity.clone(),
mvbox.state.connectivity.clone(),
sentbox.state.connectivity.clone(),
],
Scheduler::Stopped => return,
};
drop(scheduler);
for store in &stores {
let mut connectivity_lock = store.0.lock().await;
if !matches!(
*connectivity_lock,
DetailedConnectivity::Uninitialized
| DetailedConnectivity::Error(_)
| DetailedConnectivity::NotConfigured,
) {
*connectivity_lock = DetailedConnectivity::Error("Connection lost".to_string());
}
drop(connectivity_lock);
}
context.emit_event(EventType::ConnectivityChanged);
}
impl fmt::Debug for ConnectivityStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(guard) = self.0.try_lock() {
@@ -255,7 +298,7 @@ impl Context {
///
/// This comes as an HTML from the core so that we can easily improve it
/// and the improvement instantly reaches all UIs.
pub async fn get_connectivity_html(&self) -> String {
pub async fn get_connectivity_html(&self) -> Result<String> {
let mut ret = r#"<!DOCTYPE html>
<html>
<head>
@@ -266,13 +309,29 @@ impl Context {
list-style-type: none;
padding-left: 1em;
}
.dot {
height: 0.9em; width: 0.9em;
border: 1px solid #888;
border-radius: 50%;
display: inline-block;
position: relative; left: -0.1em; top: 0.1em;
}
.bar {
width: 90%;
border: 1px solid #888;
border-radius: .5em;
margin-top: .2em;
margin-bottom: 1em;
position: relative; left: -0.2em;
}
.progress {
min-width:1.8em;
height: 1em;
border-radius: .45em;
color: white;
text-align: center;
padding-bottom: 2px;
}
.red {
background-color: #f33b2d;
}
@@ -316,8 +375,7 @@ impl Context {
smtp.state.connectivity.clone(),
),
Scheduler::Stopped => {
ret += "Not started</body></html>\n";
return ret;
return Err(anyhow!("Not started"));
}
};
drop(lock);
@@ -366,8 +424,96 @@ impl Context {
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self));
ret += "</li></ul>";
let domain = dc_tools::EmailAddress::new(
&self
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default(),
)?
.domain;
ret += &format!("<h3>Storage on {}</h3><ul>", domain);
let quota = self.quota.read().await;
if let Some(quota) = &*quota {
match &quota.recent {
Ok(quota) => {
let roots_cnt = quota.len();
for (root_name, resources) in quota {
use async_imap::types::QuotaResourceName::*;
for resource in resources {
ret += "<li>";
// root name is empty eg. for gmail and redundant eg. for riseup.
// therefore, use it only if there are really several roots.
if roots_cnt > 1 && !root_name.is_empty() {
ret +=
&format!("<b>{}:</b> ", &*escaper::encode_minimal(root_name));
} else {
info!(self, "connectivity: root name hidden: \"{}\"", root_name);
}
ret += &match &resource.name {
Atom(resource_name) => {
format!(
"<b>{}:</b> {} of {} used",
&*escaper::encode_minimal(resource_name),
resource.usage.to_string(),
resource.limit.to_string(),
)
}
Message => {
format!(
"<b>Messages:</b> {} of {} used",
resource.usage.to_string(),
resource.limit.to_string(),
)
}
Storage => {
// do not use a special title needed for "Storage":
// - it is usually shown directly under the "Storage" headline
// - by the units "1 MB of 10 MB used" there is some difference to eg. "Messages: 1 of 10 used"
// - the string is not longer than the other strings that way (minus title, plus units) -
// additional linebreaks on small displays are unlikely therefore
// - most times, this is the only item anyway
let usage = (resource.usage * 1024)
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
let limit = (resource.limit * 1024)
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
format!("{} of {} used", usage, limit)
}
};
let percent = resource.get_usage_percentage();
let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE {
"red"
} else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
"yellow"
} else {
"green"
};
ret += &format!("<div class=\"bar\"><div class=\"progress {}\" style=\"width: {}%\">{}%</div></div>", color, percent, percent);
ret += "</li>";
}
}
}
Err(e) => {
ret += format!("<li>{}</li>", e).as_str();
}
}
if quota.modified + QUOTA_MAX_AGE_SECONDS < time() {
self.schedule_quota_update().await;
}
} else {
ret += "<li>One moment...</li>";
self.schedule_quota_update().await;
}
ret += "</ul>";
ret += "</body></html>\n";
ret
Ok(ret)
}
pub async fn all_work_done(&self) -> bool {

View File

@@ -1,9 +1,9 @@
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol)
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol).
use std::convert::TryFrom;
use std::time::{Duration, Instant};
use anyhow::{bail, Context as _, Error, Result};
use anyhow::{anyhow, bail, Context as _, Error, Result};
use async_std::channel::Receiver;
use async_std::sync::Mutex;
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
@@ -14,6 +14,7 @@ use crate::config::Config;
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
use crate::contact::{Contact, Origin, VerifiedStatus};
use crate::context::Context;
use crate::dc_tools::time;
use crate::e2ee::ensure_secret_key_exists;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
@@ -327,14 +328,14 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
let start = Instant::now();
let chatid = loop {
{
match chat::get_chat_id_by_grpid(context, &group_id).await {
Ok((chatid, _is_protected, _blocked)) => break chatid,
Err(err) => {
match chat::get_chat_id_by_grpid(context, &group_id).await? {
Some((chatid, _is_protected, _blocked)) => break chatid,
None => {
if start.elapsed() > Duration::from_secs(7) {
context.free_ongoing().await;
return Err(err
.context("Ongoing sender dropped (this is a bug)")
.into());
return Err(JoinError::Other(anyhow!(
"Ongoing sender dropped (this is a bug)"
)));
}
}
}
@@ -483,7 +484,7 @@ pub(crate) async fn handle_securejoin_handshake(
return Err(Error::msg("Can not be called with special contact ID"));
}
let step = mime_message
.get(HeaderDef::SecureJoin)
.get_header(HeaderDef::SecureJoin)
.context("Not a Secure-Join message")?;
info!(
@@ -519,7 +520,7 @@ pub(crate) async fn handle_securejoin_handshake(
// it just ensures, we have Bobs key now. If we do _not_ have the key because eg. MitM has removed it,
// send_message() will fail with the error "End-to-end-encryption unavailable unexpectedly.", so, there is no additional check needed here.
// verify that the `Secure-Join-Invitenumber:`-header matches invitenumber written to the QR code
let invitenumber = match mime_message.get(HeaderDef::SecureJoinInvitenumber) {
let invitenumber = match mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
Some(n) => n,
None => {
warn!(context, "Secure-join denied (invitenumber missing)");
@@ -575,19 +576,19 @@ pub(crate) async fn handle_securejoin_handshake(
==========================================================*/
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint)
{
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint not provided.",
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
let fingerprint: Fingerprint =
match mime_message.get_header(HeaderDef::SecureJoinFingerprint) {
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint not provided.",
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
could_not_establish_secure_connection(
context,
@@ -608,7 +609,7 @@ pub(crate) async fn handle_securejoin_handshake(
}
info!(context, "Fingerprint verified.",);
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
let auth_0 = match mime_message.get(HeaderDef::SecureJoinAuth) {
let auth_0 = match mime_message.get_header(HeaderDef::SecureJoinAuth) {
Some(auth) => auth,
None => {
could_not_establish_secure_connection(
@@ -643,15 +644,15 @@ pub(crate) async fn handle_securejoin_handshake(
// the vg-member-added message is special:
// this is a normal Chat-Group-Member-Added message
// with an additional Secure-Join header
let field_grpid = match mime_message.get(HeaderDef::SecureJoinGroup) {
let field_grpid = match mime_message.get_header(HeaderDef::SecureJoinGroup) {
Some(s) => s.as_str(),
None => {
warn!(context, "Missing Secure-Join-Group header");
return Ok(HandshakeMessage::Ignore);
}
};
match chat::get_chat_id_by_grpid(context, field_grpid).await {
Ok((group_chat_id, _, _)) => {
match chat::get_chat_id_by_grpid(context, field_grpid).await? {
Some((group_chat_id, _, _)) => {
if let Err(err) =
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
.await
@@ -659,12 +660,7 @@ pub(crate) async fn handle_securejoin_handshake(
error!(context, "failed to add contact: {}", err);
}
}
Err(err) => {
error!(context, "Chat {} not found: {}", &field_grpid, err);
return Err(
err.context(format!("Chat for group {} not found", &field_grpid))
);
}
None => bail!("Chat {} not found", &field_grpid),
}
} else {
// Alice -> Bob
@@ -733,7 +729,7 @@ pub(crate) async fn handle_securejoin_handshake(
inviter_progress!(context, contact_id, 800);
inviter_progress!(context, contact_id, 1000);
let field_grpid = mime_message
.get(HeaderDef::SecureJoinGroup)
.get_header(HeaderDef::SecureJoinGroup)
.map(|s| s.as_str())
.unwrap_or_else(|| "");
if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid).await {
@@ -782,7 +778,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Err(Error::msg("Can not be called with special contact ID"));
}
let step = mime_message
.get(HeaderDef::SecureJoin)
.get_header(HeaderDef::SecureJoin)
.context("Not a Secure-Join message")?;
info!(context, "observing secure-join message \'{}\'", step);
@@ -819,19 +815,19 @@ pub(crate) async fn observe_securejoin_on_other_device(
.await?;
return Ok(HandshakeMessage::Ignore);
}
let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint)
{
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
let fingerprint: Fingerprint =
match mime_message.get_header(HeaderDef::SecureJoinFingerprint) {
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint not provided, please update Delta Chat on all your devices.",
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
return Ok(HandshakeMessage::Ignore);
}
};
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
could_not_establish_secure_connection(
context,
@@ -864,7 +860,7 @@ async fn secure_connection_established(
"?"
};
let msg = stock_str::contact_verified(context, addr).await;
chat::add_info_msg(context, contact_chat_id, msg).await;
chat::add_info_msg(context, contact_chat_id, msg, time()).await;
emit_event!(context, EventType::ChatModified(contact_chat_id));
info!(context, "StockMessage::ContactVerified posted to 1:1 chat");
@@ -888,7 +884,7 @@ async fn could_not_establish_secure_connection(
)
.await;
chat::add_info_msg(context, contact_chat_id, &msg).await;
chat::add_info_msg(context, contact_chat_id, &msg, time()).await;
error!(
context,
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
@@ -989,8 +985,8 @@ mod tests {
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-request");
assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
// Step 3: Alice receives vc-request, sends vc-auth-required
alice.recv_msg(&sent).await;
@@ -998,7 +994,10 @@ mod tests {
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-auth-required");
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-auth-required"
);
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
bob.recv_msg(&sent).await;
@@ -1033,16 +1032,16 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-request-with-auth"
);
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
@@ -1091,7 +1090,7 @@ mod tests {
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm"
);
@@ -1140,7 +1139,7 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
}
@@ -1225,16 +1224,16 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-request-with-auth"
);
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
@@ -1266,7 +1265,7 @@ mod tests {
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm"
);
@@ -1295,7 +1294,7 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
}
@@ -1338,8 +1337,8 @@ mod tests {
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-request");
assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
// Step 3: Alice receives vg-request, sends vg-auth-required
alice.recv_msg(&sent).await;
@@ -1347,7 +1346,10 @@ mod tests {
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-auth-required");
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-auth-required"
);
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
bob.recv_msg(&sent).await;
@@ -1382,16 +1384,16 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-request-with-auth"
);
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
@@ -1419,7 +1421,10 @@ mod tests {
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-member-added");
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-member-added"
);
// Bob should not yet have Alice verified
let contact_alice_id =
@@ -1446,7 +1451,7 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-member-added-received"
);

View File

@@ -12,7 +12,7 @@ use anyhow::{Error, Result};
use async_std::sync::MutexGuard;
use crate::chat::{self, ChatId};
use crate::constants::{Blocked, Viewtype};
use crate::constants::Viewtype;
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::events::EventType;
@@ -236,7 +236,7 @@ impl BobState {
context: &Context,
mime_message: &MimeMessage,
) -> Result<Option<BobHandshakeStage>> {
let step = match mime_message.get(HeaderDef::SecureJoin) {
let step = match mime_message.get_header(HeaderDef::SecureJoin) {
Some(step) => step,
None => {
warn!(
@@ -336,9 +336,10 @@ impl BobState {
// the very handshake message we're handling now. But
// only after we have returned. It does not impact
// the security invariants of secure-join however.
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, grpid)
.await
.unwrap_or((ChatId::new(0), false, Blocked::Not));
let is_verified_group = chat::get_chat_id_by_grpid(context, grpid)
.await?
.map_or(false, |(_chat_id, is_protected, _blocked)| is_protected);
// when joining a non-verified group
// the vg-member-added message may be unencrypted
// when not all group members have keys or prefer encryption.
@@ -361,7 +362,7 @@ impl BobState {
if let QrInvite::Group { .. } = self.invite {
let member_added = mime_message
.get(HeaderDef::ChatGroupMemberAdded)
.get_header(HeaderDef::ChatGroupMemberAdded)
.map(|s| s.as_str())
.ok_or_else(|| Error::msg("Missing Chat-Group-Member-Added header"))?;
if !context.is_self_addr(member_added).await? {

View File

@@ -1,3 +1,5 @@
//! # Simplify incoming plaintext.
use itertools::Itertools;
// protect lines starting with `--` against being treated as a footer.
@@ -241,9 +243,7 @@ fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
ret.replace("\u{200B}", "")
}
/**
* Tools
*/
/// Returns true if the line contains only whitespace.
fn is_empty_line(buf: &str) -> bool {
buf.chars().all(char::is_whitespace)
// for some time, this checked for `char <= ' '`,

View File

@@ -1,15 +1,17 @@
//! # SMTP transport module
//! # SMTP transport module.
pub mod send;
use std::time::{Duration, SystemTime};
use async_smtp::smtp::client::net::ClientTlsParameters;
use async_smtp::{error, smtp, EmailAddress};
use async_smtp::{error, smtp, EmailAddress, ServerAddress};
use crate::constants::DC_LP_AUTH_OAUTH2;
use crate::events::EventType;
use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam};
use crate::login_param::{
dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam, Socks5Config,
};
use crate::oauth2::dc_get_oauth2_access_token;
use crate::provider::Socket;
use crate::{context::Context, scheduler::connectivity::ConnectivityStore};
@@ -29,8 +31,6 @@ pub enum Error {
},
#[error("SMTP failed to connect: {0}")]
ConnectionFailure(#[source] smtp::error::Error),
#[error("SMTP failed to setup connection: {0}")]
ConnectionSetupFailure(#[source] smtp::error::Error),
#[error("SMTP oauth2 error {address}")]
Oauth2 { address: String },
#[error("TLS error {0}")]
@@ -54,6 +54,9 @@ pub(crate) struct Smtp {
last_success: Option<SystemTime>,
pub(crate) connectivity: ConnectivityStore,
/// If sending the last message failed, contains the error message.
pub(crate) last_send_error: Option<String>,
}
impl Smtp {
@@ -100,20 +103,15 @@ impl Smtp {
self.connectivity.set_connecting(context).await;
let lp = LoginParam::from_database(context, "configured_").await?;
let res = self
.connect(
context,
&lp.smtp,
&lp.addr,
lp.server_flags & DC_LP_AUTH_OAUTH2 != 0,
lp.provider.map_or(false, |provider| provider.strict_tls),
)
.await;
if let Err(err) = &res {
self.connectivity.set_err(context, err).await;
}
res
self.connect(
context,
&lp.smtp,
&lp.socks5_config,
&lp.addr,
lp.server_flags & DC_LP_AUTH_OAUTH2 != 0,
lp.provider.map_or(false, |provider| provider.strict_tls),
)
.await
}
/// Connect using the provided login params.
@@ -121,6 +119,7 @@ impl Smtp {
&mut self,
context: &Context,
lp: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
@@ -190,17 +189,20 @@ impl Smtp {
_ => smtp::ClientSecurity::Wrapper(tls_parameters),
};
let client = smtp::SmtpClient::with_security((domain.as_str(), port), security)
.await
.map_err(Error::ConnectionSetupFailure)?;
let client =
smtp::SmtpClient::with_security(ServerAddress::new(domain.to_string(), port), security);
let client = client
let mut client = client
.smtp_utf8(true)
.credentials(creds)
.authentication_mechanism(mechanism)
.connection_reuse(smtp::ConnectionReuseParameters::ReuseUnlimited)
.timeout(Some(Duration::from_secs(SMTP_TIMEOUT)));
if let Some(socks5_config) = socks5_config {
client = client.use_socks5(socks5_config.to_async_smtp_socks5_config());
}
let mut trans = client.into_transport();
if let Err(err) = trans.connect().await {
return Err(Error::ConnectionFailure(err));

View File

@@ -1,4 +1,4 @@
//! # SQLite wrapper
//! # SQLite wrapper.
use async_std::path::Path;
use async_std::sync::RwLock;

View File

@@ -1,3 +1,5 @@
//! Migrations module.
use anyhow::Result;
use crate::config::Config;

View File

@@ -1,4 +1,4 @@
//! Module to work with translatable stock strings
//! Module to work with translatable stock strings.
use std::future::Future;
use std::pin::Pin;
@@ -897,10 +897,6 @@ impl Context {
// add welcome-messages. by the label, this is done only once,
// if the user has deleted the message or the chat, it is not added again.
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(device_messages_hint(self).await);
chat::add_device_msg(self, Some("core-about-device-chat"), Some(&mut msg)).await?;
let image = include_bytes!("../assets/welcome-image.jpg");
let blob = BlobObject::create(self, "welcome-image.jpg", image).await?;
let mut msg = Message::new(Viewtype::Image);

View File

@@ -413,7 +413,7 @@ impl TestContext {
// This code is mainly the same as `log_msglist` in `cmdline.rs`, so one day, we could
// merge them to a public function in the `deltachat` crate.
#[allow(dead_code)]
#[allow(clippy::clippy::indexing_slicing)]
#[allow(clippy::indexing_slicing)]
pub async fn print_chat(&self, chat_id: ChatId) {
let msglist = chat::get_chat_msgs(self, chat_id, 0x1, None).await.unwrap();
let msglist: Vec<MsgId> = msglist

View File

@@ -1,4 +1,4 @@
//! # Token module
//! # Token module.
//!
//! Functions to read/write token from/to the database. A token is any string associated with a key.
//!

View File

@@ -5,11 +5,13 @@ Some of the standards Delta Chat is based on:
Tasks | Standards
---------------------------------|---------------------------------------------
Transport | IMAP v4 ([RFC 3501](https://tools.ietf.org/html/rfc3501)), SMTP ([RFC 5321](https://tools.ietf.org/html/rfc5321)) and Internet Message Format (IMF, [RFC 5322](https://tools.ietf.org/html/rfc5322))
Proxy | SOCKS5 ([RFC 1928](https://tools.ietf.org/html/rfc1928))
Embedded media | MIME Document Series ([RFC 2045](https://tools.ietf.org/html/rfc2045), [RFC 2046](https://tools.ietf.org/html/rfc2046)), Content-Disposition Header ([RFC 2183](https://tools.ietf.org/html/rfc2183)), Multipart/Related ([RFC 2387](https://tools.ietf.org/html/rfc2387))
Text and Quote encoding | Fixed, Flowed ([RFC 3676](https://tools.ietf.org/html/rfc3676))
Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf.org/html/rfc2047)), Encoded Word Extensions ([RFC 2231](https://tools.ietf.org/html/rfc2231))
Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154))
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))
Quota | IMAP QUOTA extension ([RFC 2087](https://tools.ietf.org/html/rfc2087))
Authorization | OAuth2 ([RFC 6749](https://tools.ietf.org/html/rfc6749))
End-to-end encryption | [Autocrypt Level 1](https://autocrypt.org/level1.html), OpenPGP ([RFC 4880](https://tools.ietf.org/html/rfc4880)), Security Multiparts for MIME ([RFC 1847](https://tools.ietf.org/html/rfc1847)) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
Configuration assistance | [Autoconfigure](https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover](https://technet.microsoft.com/library/bb124251(v=exchg.150).aspx)

View File

@@ -0,0 +1,70 @@
Return-Path: <alice@example.com>
Delivered-To: bob@example.org
Received: from hq5.merlinux.eu
by hq5.merlinux.eu with LMTP
id GJ4eNagpFWF5UwAAPzvFDg
(envelope-from <alice@example.com>)
for <bob@example.org>; Thu, 12 Aug 2021 16:01:12 +0200
Received: from mout.gmx.net (mout.gmx.net [212.227.17.22])
by hq5.merlinux.eu (Postfix) with ESMTPS id 3033227A0003
for <bob@example.org>; Thu, 12 Aug 2021 16:01:12 +0200 (CEST)
Authentication-Results: hq5.merlinux.eu;
dkim=pass (1024-bit key; secure) header.d=gmx.net header.i=@gmx.net header.b="I/oyQzjt";
dkim-atps=neutral
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net;
s=badeba3b8450; t=1628776871;
bh=yVNQf4XVEEjSt/PzPPM4F9JXYSv2/ynVmb/E4dc6Qpk=;
h=X-UI-Sender-Class:From:To:Subject:Date;
b=I/oyQzjtFVDJiKkKV2/9DimrUXwhNtrHc5sgFkO7HNz6sheW8t0+8WpL76AfLuUU2
KZ/bCPyX3oItKl+31HZMoekrRnDyHiahsF1h3VrSzDXo3K0sk6nmZBjIQLuksGFW5i
/+5TkQ+p79YB/HioYm08pewz08caHfCt3EqcuJik=
X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c
Received: from [193.96.224.73] ([193.96.224.73]) by web-mail.gmx.net
(3c-app-gmx-bap57.server.lan [172.19.172.127]) (via HTTP); Thu, 12 Aug 2021
16:01:11 +0200
MIME-Version: 1.0
Message-ID: <trinity-18545f24-4f02-4dc8-9f80-8d2646646d03-1628776871644@3c-app-gmx-bap57>
From: Alice <alice@example.com>
To: bob@example.org
Subject: Fw: subject
Content-Type: text/html; charset=UTF-8
Date: Thu, 12 Aug 2021 16:01:11 +0200
Importance: normal
Sensitivity: Normal
X-UI-Message-Type: mail
X-Priority: 3
X-Provags-ID: V03:K1:vvJyGpka3W40JnBiz1FlxtNcdZw52KVodkX04BbaREoVau28F88gbimeb2Tm1t58pzQDe
HPoPiTTgz3Roj8GM/iIs4FqZxzcPiekR59a/GwFr16mPZQj+1cq6QOk144bXysBz3PHroQrc7Ctx
MtVLAY9w5+Lpuql24x9IqjA0eN2ytYrYYgX60d2FgU8CN/azK0bdEcsdyfwnAcf0bW9UY4ghE8Gt
hRe8z4WV6qEEzlhU+cI1uAixvNdQ6MFoi1oT7LdvfbUdcm1CBytWbbieGF1LjMa5Y+D4MZ3zUiiY
Ys=
X-Spam-Flag: NO
X-UI-Out-Filterresults: notjunk:1;V03:K0:pksZU4GoRZI=:jPKwLt7m9sSdgel28Ha/o7
UdLaJvQkSOD2tUGBq7n9rGeKT3opdBO5SWDRhn/qWLn+muPPYIjwmyE0XiGIjgTxLDJbY/LHL
bNWfcZ+geulQn9vH9muMcAW7ThwACRj3CCtWpc4y5ffTbo8VEinde4C4XFuhSUUdqyzu0GxYc
FklFTMlpL9ELxn5Mo3MaOnzznwrchd/2ogGzFz9wOtYUot+llyK+VLaylMeSSTIWbSLHwmA7l
MwsujGm4OvqP4VXSpVY2MecAGGwEvPsMQ/hfMgDsxRRm3sFsVZf6KFKcngZte0Nq6LZO9QU1x
tAmMgjZYPfOE+YSFiuKJ8E3YlsMk58HYTw/ON+m5T+lXSJeWVLA7sOgk9NKBGi2VzvrRz3YSg
MysXD8/h8PU7Rj7a2pttFyGxuN397xP3u1A+15LH5M2+AhUy4quzmxC0Ozb2chPdMJHgTO99e
5tmLkyYeeREmSB89pFzyOHGghENBflocaDiCidgWm6pd1lfMMjMQ8bA3S/QpE8e913WGCWhVQ
uecX4FBK1VEl8WkE/0GQhY8+2mzBE0+Jo1LCKJtAo9h8bG2fNJkujOpKKvUoududAYuajaHuq
rVl6G/xOP8JB3FDDNhZQptleN3KU5qPqNYz0qYibUCJNadS6XlwrfkZReJOk3yHnbIUvB9IG1
WGu+K/8WtQaYtzmNtZLD3c7YzQZT4v5xzxQ3TtROawkGNGk4gYJTnAd1ZWOkBHEjcSLsYFVYg
nhkLeamJ3KnnkBMJromM0tc0PmSdb/hqD/8hkrWQFvK/nmdNm9+z8UCmCTSDV98UodcwpAkJB
D+/kEFR3Y5K904h2dhgmSbnqZAVEziDNT2TylwBnxrpvIKX5Xw=
<html><head></head><body><div style="font-family: Verdana;font-size: 12.0px;"><div>&nbsp;</div>
<div>&nbsp;
<div>&nbsp;
<div data-darkreader-inline-border-left="" name="quote" style="margin: 10px 5px 5px 10px; padding: 10px 0px 10px 10px; border-left: 2px solid rgb(195, 217, 229); overflow-wrap: break-word; --darkreader-inline-border-left:#274759;">
<div style="margin:0 0 10px 0;"><b>Gesendet:</b>&nbsp;Donnerstag, 12. August 2021 um 15:52 Uhr<br/>
<b>Von:</b>&nbsp;&quot;Claire&quot; &lt;claire@example.org&gt;<br/>
<b>An:</b>&nbsp;alice@example.com<br/>
<b>Betreff:</b>&nbsp;subject</div>
<div name="quoted-content">bodytext</div>
</div>
</div>
</div></div></body></html>