Compare commits

...
Author SHA1 Message Date
jikstra 0b03331f0d Fix spawning task and introduce ProviderInfo event 2022-03-04 12:52:43 -05:00
jikstra 420a9fbd9f Start working on fixing blocking provider info 2022-03-04 00:11:35 -05:00
Hocuri 7c4a6ddcdf Add AcManager (#3073)
* Add AcManager

See https://github.com/deltachat/deltachat-core-rust/pull/2901#issuecomment-998285039

This reduces boilerplate code again therefore, improving the
signal-noise-ratio and reducing the mental barrier to start
writing a unit test.

Slightly off-topic:

I didn't add any advanced functions like `manager.get("alice");` because
they're not needed yet; however, once we have the AcManager we can
think about fancy things like:

```rust
acm.send_text(&alice, "Hi Bob, this is Alice!", &bob);
```
which automatically lets bob receive the message.

However, this may be less useful than it seems at first, since most of
the tests I looked at wouldn't benefit from it, so at least I won't do
it until I have a test that would benefit from it.

* Remove unnecessary RefCell

* Rename AcManager to TestContextManager

* Don't store TestContext's in a vec for now as we don't need this; we can re-add it later

* Rename acm -> tcm
2022-02-23 19:34:47 +01:00
link2xt 7ab6d95b6c mimefactory: place common IMF headers at the top of the message
This moves most common headers like From, To, Subject etc. defined in
the Internet Message Format standard at the top of the message
in the same order as used in RFC 5322.
2022-02-23 17:51:15 +00:00
link2xt 6c32b89906 smtp: add more logging 2022-02-23 17:04:30 +00:00
bjoern 7c5eb0ae37 prepare 1.76 (#3082)
* update changelog for 1.76.0

* bump version to 1.76.0
2022-02-20 22:58:46 -05:00
link2xt 36bce6c468 Remove unused async-std feature 2022-02-19 11:30:48 +00:00
Hocuri 19a32cdfd3 Let MS Exchange MDNs mark the In-Reply-To message as read (#3075)
Fix https://github.com/deltachat/deltachat-core-rust/issues/2891
2022-02-17 09:56:00 +01:00
link2xt d708f386a1 smtp: set message state to failed when retry limit is exceeded 2022-02-13 08:59:52 +00:00
link2xt 0f837f4bed Fix a comment typo 2022-02-12 16:29:42 +00:00
link2xt 242e8e2bb3 smtp: remove the message in case of permanent failure
When `smtp_send` returns `Status::Finished`,
the message should be removed from the queue even in case of
failure, such as a permanent error.

In addition to this bugfix, move the retry count increase to
the beginning of `send_msg_to_smtp` to ensure no message is
retried infinitely even in case of similar bugs.
2022-02-12 16:20:13 +00:00
link2xt 1d56b24b67 cargo update 2022-02-12 16:19:44 +00:00
Hocuri bb9138708a Fix disappearing drafts (#3067) 2022-02-10 10:05:30 +01:00
Hocuri 34f5510f1f Don't directly download messages from the Spam folder (#3015)
fix #3007

My approach is:

We don't download any messages from the spam folder anymore, and only download them if they were moved out. This means that is-it-spam logic only resides in spam_target_folder(). This has some implications, see the comments.
2022-02-10 09:06:22 +01:00
link2xt 6c6d47c89c Fix CI
timeout_func_only makes pytest-rerunfailures work with pytest-timeout,
but it only works with default timeout_method.

See pytest-rerunfailures issue for details:
https://github.com/pytest-dev/pytest-rerunfailures/issues/99
2022-02-08 20:50:11 +00:00
link2xt 196075c031 imap: batch message deletion 2022-02-06 11:42:30 +00:00
18 changed files with 572 additions and 502 deletions
+15 -1
View File
@@ -3,12 +3,26 @@
## Unreleased
### Changes
- refactorings #3026
- add more SMTP logging #3093
- place common headers like `From:` before the large `Autocrypt:` header #3079
## 1.76.0
### Changes
- move messages in batches #3058
- delete messages in batches #3060
- python: remove arbitrary timeouts from tests #3059
- refactorings #3026
### Fixes
- avoid archived, fresh chats #3053
- treat "NO" IMAP response to MOVE and COPY commands as an error #3058
- Fix a bug where messages in the Spam folder created contact requests #3015
- Fix a bug where drafts disappeared after some days #3067
- Parse MS Exchange read receipts and mark the original message as read #3075
- do not retry message sending infinitely in case of permanent SMTP failure #3070
- set message state to failed when retry limit is exceeded #3072
## 1.75.0
Generated
+86 -85
View File
@@ -242,7 +242,7 @@ dependencies = [
"parking",
"polling",
"slab",
"socket2 0.4.3",
"socket2 0.4.4",
"waker-fn",
"winapi",
]
@@ -374,9 +374,9 @@ dependencies = [
[[package]]
name = "async-task"
version = "4.0.3"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0"
checksum = "677d306121baf53310a3fd342d88dc0824f6bbeace68347593658525565abee8"
[[package]]
name = "async-trait"
@@ -408,15 +408,18 @@ dependencies = [
[[package]]
name = "autocfg"
version = "0.1.7"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"
dependencies = [
"autocfg 1.1.0",
]
[[package]]
name = "autocfg"
version = "1.0.1"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
@@ -493,9 +496,9 @@ dependencies = [
[[package]]
name = "block-buffer"
version = "0.10.0"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
@@ -719,9 +722,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
version = "4.4.0"
version = "4.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4d3d118de1bf9678546f65e12f749b46abb5a56129d435af21fc7e42768f974"
checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db"
dependencies = [
"error-code",
"str-buf",
@@ -779,9 +782,9 @@ dependencies = [
[[package]]
name = "core-foundation"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
@@ -816,9 +819,9 @@ checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0"
[[package]]
name = "crc32fast"
version = "1.3.1"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2209c310e29876f7f0b2721e7e26b84aff178aa3da5d091f9bfbf47669e60e3"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if 1.0.0",
]
@@ -882,9 +885,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.6"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762"
checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils",
@@ -895,9 +898,9 @@ dependencies = [
[[package]]
name = "crossbeam-queue"
version = "0.3.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b979d76c9fcb84dffc80a73f7290da0f83e4c95773494674cb44b76d13a7a110"
checksum = "4dd435b205a4842da59efd07628f921c096bc1cc0a156835b4fa0bcb9a19bcce"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils",
@@ -905,9 +908,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.6"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120"
checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6"
dependencies = [
"cfg-if 1.0.0",
"lazy_static",
@@ -915,9 +918,9 @@ dependencies = [
[[package]]
name = "crypto-common"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0"
checksum = "a4600d695eb3f6ce1cd44e6e291adceb2cc3ab12f20a33777ecd0bf6eba34e06"
dependencies = [
"generic-array",
]
@@ -1063,7 +1066,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.75.0"
version = "1.76.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1143,7 +1146,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.75.0"
version = "1.76.0"
dependencies = [
"anyhow",
"async-std",
@@ -1203,13 +1206,12 @@ dependencies = [
[[package]]
name = "digest"
version = "0.10.1"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b"
checksum = "8cb780dce4f9a8f5c087362b3a4595936b2019e7c8b30f2c3e9a7e94e6ae9837"
dependencies = [
"block-buffer 0.10.0",
"block-buffer 0.10.2",
"crypto-common",
"generic-array",
]
[[package]]
@@ -1590,9 +1592,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "futures"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4"
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
dependencies = [
"futures-channel",
"futures-core",
@@ -1605,9 +1607,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b"
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
dependencies = [
"futures-core",
"futures-sink",
@@ -1615,15 +1617,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-executor"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
dependencies = [
"futures-core",
"futures-task",
@@ -1632,9 +1634,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
[[package]]
name = "futures-lite"
@@ -1653,9 +1655,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [
"proc-macro2",
"quote",
@@ -1664,21 +1666,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508"
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
[[package]]
name = "futures-task"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
[[package]]
name = "futures-util"
version = "0.3.19"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [
"futures-channel",
"futures-core",
@@ -1752,15 +1754,14 @@ checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
[[package]]
name = "gloo-timers"
version = "0.2.2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f16c88aa13d2656ef20d1c042086b8767bbe2bdb62526894275a1b062161b2e"
checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
@@ -1894,9 +1895,9 @@ dependencies = [
[[package]]
name = "httparse"
version = "1.5.1"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4"
[[package]]
name = "human-panic"
@@ -2121,15 +2122,15 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.114"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50"
checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c"
[[package]]
name = "libm"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db"
[[package]]
name = "libsqlite3-sys"
@@ -2156,9 +2157,9 @@ checksum = "95f5690fef754d905294c56f7ac815836f2513af966aa47f2e07ac79be07827f"
[[package]]
name = "lock_api"
version = "0.4.5"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
dependencies = [
"scopeguard",
]
@@ -2228,7 +2229,7 @@ version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg 1.0.1",
"autocfg 1.1.0",
]
[[package]]
@@ -2263,7 +2264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
dependencies = [
"adler",
"autocfg 1.0.1",
"autocfg 1.1.0",
]
[[package]]
@@ -2352,7 +2353,7 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
dependencies = [
"autocfg 1.0.1",
"autocfg 1.1.0",
"num-integer",
"num-traits",
]
@@ -2363,7 +2364,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d51546d704f52ef14b3c962b5776e53d5b862e5790e40a350d366c209bd7f7a"
dependencies = [
"autocfg 0.1.7",
"autocfg 0.1.8",
"byteorder",
"lazy_static",
"libm",
@@ -2393,7 +2394,7 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg 1.0.1",
"autocfg 1.1.0",
"num-traits",
]
@@ -2403,7 +2404,7 @@ version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
dependencies = [
"autocfg 1.0.1",
"autocfg 1.1.0",
"num-integer",
"num-traits",
]
@@ -2414,7 +2415,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg 1.0.1",
"autocfg 1.1.0",
"num-integer",
"num-traits",
]
@@ -2425,7 +2426,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg 1.0.1",
"autocfg 1.1.0",
]
[[package]]
@@ -2500,7 +2501,7 @@ version = "0.9.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
dependencies = [
"autocfg 1.0.1",
"autocfg 1.1.0",
"cc",
"libc",
"openssl-src",
@@ -2997,7 +2998,7 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
dependencies = [
"autocfg 1.0.1",
"autocfg 1.1.0",
"crossbeam-deque",
"either",
"rayon-core",
@@ -3151,7 +3152,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.4",
"semver 1.0.5",
]
[[package]]
@@ -3256,9 +3257,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "security-framework"
version = "2.5.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce"
checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
dependencies = [
"bitflags",
"core-foundation",
@@ -3269,9 +3270,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.5.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7"
checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
dependencies = [
"core-foundation-sys",
"libc",
@@ -3288,9 +3289,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7"
[[package]]
name = "semver-parser"
@@ -3330,9 +3331,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
dependencies = [
"itoa 1.0.1",
"ryu",
@@ -3383,7 +3384,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
"digest 0.10.1",
"digest 0.10.2",
]
[[package]]
@@ -3422,7 +3423,7 @@ checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
"digest 0.10.1",
"digest 0.10.2",
]
[[package]]
@@ -3513,9 +3514,9 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b"
checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
dependencies = [
"libc",
"winapi",
@@ -3852,9 +3853,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.15.0"
version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
dependencies = [
"pin-project-lite",
]
@@ -3972,9 +3973,9 @@ dependencies = [
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-width"
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.75.0"
version = "1.76.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -24,7 +24,7 @@ async-imap = { git = "https://github.com/async-email/async-imap" }
async-native-tls = { version = "0.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
async-std-resolver = "0.20"
async-std = { version = "1", features = ["unstable"] }
async-std = { version = "1" }
async-tar = { version = "0.4", default-features=false }
async-trait = "0.1"
backtrace = "0.3"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.75.0"
version = "1.76.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
+31 -22
View File
@@ -3928,35 +3928,44 @@ pub type dc_provider_t = provider::Provider;
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email(
context: *const dc_context_t,
addr: *const libc::c_char,
) -> *const dc_provider_t {
context: *mut dc_context_t,
addr: *mut libc::c_char,
) {
if context.is_null() || addr.is_null() {
eprintln!("ignoring careless call to dc_provider_new_from_email()");
return ptr::null();
return;
}
let addr = to_string_lossy(addr);
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(
ctx,
addr.as_str(),
socks5_enabled,
)) {
Some(provider) => provider,
None => ptr::null_mut(),
let addr = to_string_lossy(addr);
spawn(async move {
let socks5_enabled = ctx.get_config_bool(config::Config::Socks5Enabled)
.await
.log_err(ctx, "Can't get config");
match socks5_enabled {
Ok(socks5_enabled) => {
match provider::get_provider_info(
ctx,
addr.as_str(),
socks5_enabled,
).await {
Some(provider) => {
// send event with provider info
},
None => {
// send event with provider info null
},
};
}
Err(err) => {
ctx.emit_event(EventType::Error(format!(
"Failed to get provider info: {:#}",
err
)));
}
}
Err(_) => ptr::null_mut(),
}
});
}
#[no_mangle]
+19 -4
View File
@@ -1398,11 +1398,13 @@ class TestOnlineAccount:
assert not device_chat.can_send()
assert device_chat.get_draft() is None
def test_dont_show_emails_in_draft_folder(self, acfactory, lp):
def test_dont_show_emails(self, acfactory, lp):
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown.
Also, test that unknown emails in the Spam folder are not shown."""
ac1 = acfactory.get_online_configuring_account()
ac1.set_config("show_emails", "2")
ac1.create_contact("alice@example.org").create_chat()
@@ -1410,6 +1412,7 @@ class TestOnlineAccount:
acfactory.wait_configure(ac1)
ac1.direct_imap.create_folder("Drafts")
ac1.direct_imap.create_folder("Sent")
ac1.direct_imap.create_folder("Spam")
acfactory.wait_configure_and_start_io()
# Wait until each folder was selected once and we are IDLEing again:
@@ -1434,6 +1437,15 @@ class TestOnlineAccount:
message in Sent
""".format(ac1.get_config("configured_addr")))
ac1.direct_imap.append("Spam", """
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Spam
""".format(ac1.get_config("configured_addr")))
ac1.set_config("scan_all_folders_debounce_secs", "0")
lp.sec("All prepared, now let DC find the message")
@@ -1447,6 +1459,10 @@ class TestOnlineAccount:
assert msg.text == "subj – message in Sent"
assert len(msg.chat.get_messages()) == 1
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
ac1.direct_imap.select_folder("Spam")
assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org")
ac1.stop_io()
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
ac1.direct_imap.select_folder("Drafts")
@@ -2500,8 +2516,7 @@ class TestOnlineAccount:
lp.sec("ac2: deleting all messages except third")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
for msg in to_delete:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
+1 -1
View File
@@ -79,7 +79,7 @@ addopts = -v -ra --strict-markers
norecursedirs = .tox
xfail_strict=true
timeout = 90
timeout_method = thread
timeout_func_only = True
markers =
ignored: ignore this test in default test runs, use --ignored to run.
+2 -90
View File
@@ -193,7 +193,6 @@ pub(crate) async fn dc_receive_imf_inner(
&mut mime_parser,
imf_raw,
incoming,
incoming_origin,
server_folder,
&to_ids,
&rfc724_mid,
@@ -415,7 +414,6 @@ async fn add_parts(
mime_parser: &mut MimeMessage,
imf_raw: &[u8],
incoming: bool,
incoming_origin: Origin,
server_folder: &str,
to_ids: &[u32],
rfc724_mid: &str,
@@ -432,7 +430,6 @@ async fn add_parts(
) -> Result<ReceivedMsg> {
let mut chat_id = None;
let mut chat_id_blocked = Blocked::Not;
let mut incoming_origin = incoming_origin;
let parent = get_parent_message(context, mime_parser).await?;
@@ -676,7 +673,6 @@ async fn add_parts(
if chat_id_blocked != Blocked::Not {
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
if create_blocked == Blocked::Request && parent.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
@@ -687,9 +683,6 @@ async fn add_parts(
context,
"Message is a reply to a known message, mark sender as known.",
);
if !incoming_origin.is_known() {
incoming_origin = Origin::IncomingReplyTo;
}
}
}
}
@@ -701,15 +694,6 @@ async fn add_parts(
} else {
MessageState::InFresh
};
let is_spam = (chat_id_blocked == Blocked::Request)
&& !incoming_origin.is_known()
&& (is_dc_message == MessengerMessage::No)
&& context.is_spam_folder(server_folder).await?;
if is_spam {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is probably spam (TRASH)");
}
} else {
// Outgoing
@@ -2363,12 +2347,10 @@ fn dc_create_incoming_rfc724_mid(mime: &MimeMessage) -> String {
#[cfg(test)]
mod tests {
use chat::get_chat_contacts;
use mailparse::MailHeaderMap;
use super::*;
use crate::chat::get_chat_contacts;
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
use crate::chatlist::Chatlist;
use crate::constants::{DC_CONTACT_ID_INFO, DC_GCL_NO_SPECIALS};
@@ -2977,7 +2959,7 @@ mod tests {
&headers,
"some-other-message-id",
std::iter::empty(),
ShowEmails::Off
ShowEmails::Off,
)
.await
.unwrap());
@@ -4298,76 +4280,6 @@ YEAAAAAA!.
assert_eq!(msg.text.unwrap(), "Reply");
}
#[async_std::test]
async fn test_dont_show_spam() {
async fn is_shown(t: &TestContext, raw: &[u8], server_folder: &str) -> bool {
let mail = mailparse::parse_mail(raw).unwrap();
dc_receive_imf(t, raw, server_folder, false).await.unwrap();
t.get_last_msg().await.rfc724_mid
== mail.get_headers().get_first_value("Message-Id").unwrap()
}
let t = TestContext::new_alice().await;
t.set_config(Config::ConfiguredSpamFolder, Some("Spam"))
.await
.unwrap();
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
assert!(
is_shown(
&t,
b"Message-Id: abcd1@exmaple.com\n\
From: bob@example.org\n\
Chat-Version: 1.0\n",
"Inbox",
)
.await,
);
assert!(
is_shown(
&t,
b"Message-Id: abcd2@exmaple.com\n\
From: bob@example.org\n",
"Inbox",
)
.await,
);
assert!(
is_shown(
&t,
b"Message-Id: abcd3@exmaple.com\n\
From: bob@example.org\n\
Chat-Version: 1.0\n",
"Spam",
)
.await,
);
assert!(
// Note the `!`:
!is_shown(
&t,
b"Message-Id: abcd4@exmaple.com\n\
From: bob@example.org\n",
"Spam",
)
.await,
);
Contact::create(&t, "", "bob@example.org").await.unwrap();
assert!(
is_shown(
&t,
b"Message-Id: abcd5@exmaple.com\n\
From: bob@example.org\n",
"Spam",
)
.await,
);
}
#[async_std::test]
async fn test_dont_show_all_outgoing_msgs_in_self_chat() {
// Regression test for <https://github.com/deltachat/deltachat-android/issues/1940>:
+3
View File
@@ -333,4 +333,7 @@ pub enum EventType {
msg_id: MsgId,
status_update_id: StatusUpdateId,
},
#[strum(props(id = "2201"))]
ProviderInfo(String),
}
+98 -108
View File
@@ -458,12 +458,9 @@ impl Imap {
.await
.context("fetch_new_messages")?;
self.move_messages(context, watch_folder)
self.move_delete_messages(context, watch_folder)
.await
.context("move_messages")?;
self.delete_messages(context, watch_folder)
.await
.context("delete_messages")?;
.context("move_delete_messages")?;
Ok(())
}
@@ -751,6 +748,11 @@ impl Imap {
// message, move it to the movebox and then download the second message before
// downloading the first one, if downloading from inbox before moving is allowed.
if folder == target
// Never download messages directly from the spam folder.
// If the sender is known, the message will be moved to the Inbox or Mvbox
// and then we download the message from there.
// Also see `spam_target_folder()`.
&& !context.is_spam_folder(folder).await?
&& prefetch_should_download(
context,
&headers,
@@ -827,6 +829,37 @@ impl Imap {
Ok(read_cnt > 0)
}
/// Deletes batch of messages identified by their UID from the currently
/// selected folder.
async fn delete_message_batch(
&mut self,
context: &Context,
uid_set: &str,
row_ids: Vec<i64>,
) -> Result<()> {
// mark the message for deletion
self.add_flag_finalized_with_set(uid_set, "\\Deleted")
.await?;
context
.sql
.execute(
format!(
"DELETE FROM imap WHERE id IN ({})",
row_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("cannot remove deleted messages from imap table")?;
context.emit_event(EventType::ImapMessageDeleted(format!(
"IMAP messages {} marked as deleted",
uid_set
)));
self.config.selected_folder_needs_expunge = true;
Ok(())
}
/// Moves batch of messages identified by their UID from the currently
/// selected folder to the target folder.
async fn move_message_batch(
@@ -907,17 +940,16 @@ impl Imap {
}
}
/// Moves messages.
/// Moves and deletes messages as planned in the `imap` table.
///
/// This is the only place where messages are moved on the IMAP server.
async fn move_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
/// This is the only place where messages are moved or deleted on the IMAP server.
async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
let mut rows = context
.sql
.query_map(
"SELECT id, uid, target FROM imap
WHERE folder = ?
AND target != folder
AND target != '' -- Not planned for deletion.
ORDER BY target, uid",
paramsv![folder],
|row| {
@@ -970,57 +1002,20 @@ impl Imap {
}
}
// Execute request.
self.move_message_batch(context, &uid_set, rowid_set, &target)
.await
.with_context(|| {
format!(
"cannot move batch of messages {:?} to folder {:?}",
&uid_set, target
)
})?;
}
Ok(())
}
/// Deletes messages that are marked as planned for deletion in `imap` table.
///
/// This is the only place where messages are deleted from the IMAP server.
async fn delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
let rows = context
.sql
.query_map(
"SELECT id, uid FROM imap
WHERE folder=? AND target=''
ORDER BY uid ASC
LIMIT 50", // Do not try to delete too many messages at once.
paramsv![folder],
|row| {
let rowid: i64 = row.get(0)?;
let uid: u32 = row.get(1)?;
Ok((rowid, uid))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
if rows.is_empty() {
return Ok(());
}
for (rowid, uid) in rows {
match self.delete_msg(context, folder, uid).await {
ImapActionResult::Failed | ImapActionResult::RetryLater => {
warn!(context, "Deletion of message {}/{} failed", folder, uid);
break;
}
ImapActionResult::Success => {
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
}
// Empty target folder name means messages should be deleted.
if target.is_empty() {
self.delete_message_batch(context, &uid_set, rowid_set)
.await
.with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
} else {
self.move_message_batch(context, &uid_set, rowid_set, &target)
.await
.with_context(|| {
format!(
"cannot move batch of messages {:?} to folder {:?}",
&uid_set, target
)
})?;
}
}
@@ -1449,39 +1444,6 @@ impl Imap {
}
}
pub async fn delete_msg(
&mut self,
context: &Context,
folder: &str,
uid: u32,
) -> ImapActionResult {
if let Some(imapresult) = self
.prepare_imap_operation_on_msg(context, folder, uid)
.await
{
return imapresult;
}
// we are connected, and the folder is selected
let display_imap_id = format!("{}/{}", folder, uid);
// mark the message for deletion
if let Err(err) = self.add_flag_finalized(uid, "\\Deleted").await {
warn!(
context,
"Cannot mark message {} as \"Deleted\": {}.", display_imap_id, err
);
ImapActionResult::RetryLater
} else {
context.emit_event(EventType::ImapMessageDeleted(format!(
"IMAP Message {} marked as deleted",
display_imap_id
)));
self.config.selected_folder_needs_expunge = true;
ImapActionResult::Success
}
}
pub async fn ensure_configured_folders(
&mut self,
context: &Context,
@@ -1641,16 +1603,28 @@ impl Imap {
}
}
/// Returns target folder for a message found in the Spam folder.
async fn spam_target_folder(
async fn should_move_out_of_spam(
context: &Context,
folder: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Config>> {
if let Some(chat) = prefetch_get_chat(context, headers).await? {
if chat.blocked != Blocked::Not {
) -> Result<bool> {
if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
// If this is a chat message (i.e. has a ChatVersion header), then this might be
// a securejoin message. We can't find out at this point as we didn't prefetch
// the SecureJoin header. So, we always move chat messages out of Spam.
// Two possibilities to change this would be:
// 1. Remove the `&& !context.is_spam_folder(folder).await?` check from
// `fetch_new_messages()`, and then let `dc_receive_imf()` check
// if it's a spam message and should be hidden.
// 2. Or add a flag to the ChatVersion header that this is a securejoin
// request, and return `true` here only if the message has this flag.
// `dc_receive_imf()` can then check if the securejoin request is valid.
return Ok(true);
}
if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
if msg.chat_blocked != Blocked::Not {
// Blocked or contact request message in the spam folder, leave it there.
return Ok(None);
return Ok(false);
}
} else {
// No chat found.
@@ -1658,22 +1632,38 @@ async fn spam_target_folder(
from_field_to_contact_id(context, &mimeparser::get_from(headers), true).await?;
if blocked_contact {
// Contact is blocked, leave the message in spam.
return Ok(None);
return Ok(false);
}
if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
if chat_id_blocked.blocked != Blocked::Not {
return Ok(None);
return Ok(false);
}
} else if from_id != DC_CONTACT_ID_SELF {
// No chat with this contact found.
return Ok(None);
return Ok(false);
}
}
Ok(true)
}
/// Returns target folder for a message found in the Spam folder.
/// If this returns None, the message will not be moved out of the
/// Spam folder, and as `fetch_new_messages()` doesn't download
/// messages from the Spam folder, the message will be ignored.
async fn spam_target_folder(
context: &Context,
folder: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Config>> {
if !should_move_out_of_spam(context, headers).await? {
return Ok(None);
}
if needs_move_to_mvbox(context, headers).await?
// We don't want to move the message to the inbox or sentbox where we wouldn't
// fetch it again:
// If OnlyFetchMvbox is set, we don't want to move the message to
// the inbox or sentbox where we wouldn't fetch it again:
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
{
Ok(Some(Config::ConfiguredMvboxFolder))
@@ -2426,7 +2416,7 @@ mod tests {
("Spam", true, true, "DeltaChat"),
];
// These are the same as above, but all messages in Spam stay in Spam
// These are the same as above, but non-chat messages in Spam stay in Spam
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
("INBOX", false, false, "INBOX"),
("INBOX", false, true, "INBOX"),
@@ -2437,9 +2427,9 @@ mod tests {
("Sent", true, false, "Sent"),
("Sent", true, true, "DeltaChat"),
("Spam", false, false, "Spam"),
("Spam", false, true, "Spam"),
("Spam", false, true, "INBOX"),
("Spam", true, false, "Spam"),
("Spam", true, true, "Spam"),
("Spam", true, true, "DeltaChat"),
];
#[async_std::test]
+81 -56
View File
@@ -503,33 +503,65 @@ impl<'a> MimeFactory<'a> {
}
}
// Start with Internet Message Format headers in the order of the standard example
// <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.1.1>.
headers
.unprotected
.push(Header::new("MIME-Version".into(), "1.0".into()));
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
if let Some(sender_displayname) = &self.sender_displayname {
let sender =
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
headers
.unprotected
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
}
headers
.unprotected
.push(Header::new_with_value("To".into(), to).unwrap());
let subject_str = self.subject_str(context).await?;
let encoded_subject = if subject_str
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
// but we do not want to encode all subjects just because they contain a space.
{
subject_str.clone()
} else {
encode_words(&subject_str)
};
headers
.protected
.push(Header::new("Subject".into(), encoded_subject));
let date = chrono::Utc
.from_local_datetime(&chrono::NaiveDateTime::from_timestamp(self.timestamp, 0))
.unwrap()
.to_rfc2822();
headers.unprotected.push(Header::new("Date".into(), date));
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
Loaded::Mdn { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
};
headers.unprotected.push(Header::new(
"Message-ID".into(),
render_rfc724_mid(&rfc724_mid),
));
// Reply headers as in <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.2>.
if !self.in_reply_to.is_empty() {
headers
.unprotected
.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
}
if !self.references.is_empty() {
headers
.unprotected
.push(Header::new("References".into(), self.references.clone()));
}
if !self.in_reply_to.is_empty() {
headers
.unprotected
.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
}
let date = chrono::Utc
.from_local_datetime(&chrono::NaiveDateTime::from_timestamp(self.timestamp, 0))
.unwrap()
.to_rfc2822();
headers.unprotected.push(Header::new("Date".into(), date));
headers
.unprotected
.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
if let Loaded::Mdn { .. } = self.loaded {
headers.unprotected.push(Header::new(
"Auto-Submitted".to_string(),
@@ -542,6 +574,11 @@ impl<'a> MimeFactory<'a> {
));
}
// Non-standard headers.
headers
.unprotected
.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
if self.req_mdn {
// we use "Chat-Disposition-Notification-To"
// because replies to "Disposition-Notification-To" are weird in many cases
@@ -556,21 +593,9 @@ impl<'a> MimeFactory<'a> {
let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext();
let skip_autocrypt = self.should_skip_autocrypt();
let subject_str = self.subject_str(context).await?;
let e2ee_guaranteed = self.is_e2ee_guaranteed();
let encrypt_helper = EncryptHelper::new(context).await?;
let encoded_subject = if subject_str
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
// but we do not want to encode all subjects just because they contain a space.
{
subject_str.clone()
} else {
encode_words(&subject_str)
};
if !skip_autocrypt {
// unless determined otherwise we add the Autocrypt header
let aheader = encrypt_helper.get_aheader().to_string();
@@ -579,15 +604,6 @@ impl<'a> MimeFactory<'a> {
.push(Header::new("Autocrypt".into(), aheader));
}
headers
.protected
.push(Header::new("Subject".into(), encoded_subject));
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
Loaded::Mdn { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
};
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?;
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
headers.protected.push(Header::new(
@@ -596,25 +612,11 @@ impl<'a> MimeFactory<'a> {
));
}
headers.unprotected.push(Header::new(
"Message-ID".into(),
render_rfc724_mid(&rfc724_mid),
));
// MIME header <https://datatracker.ietf.org/doc/html/rfc2045>.
// Content-Type
headers
.unprotected
.push(Header::new_with_value("To".into(), to).unwrap());
headers
.unprotected
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
if let Some(sender_displayname) = &self.sender_displayname {
let sender =
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
headers
.unprotected
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
}
.push(Header::new("MIME-Version".into(), "1.0".into()));
let mut is_gossiped = false;
@@ -2084,4 +2086,27 @@ mod tests {
Ok(())
}
/// Tests that standard IMF header "From:" comes before non-standard "Autocrypt:" header.
#[async_std::test]
async fn test_from_before_autocrypt() -> Result<()> {
// create chat with bob
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("this is the text!".to_string()));
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let payload = sent_msg.payload();
assert_eq!(payload.match_indices("Autocrypt:").count(), 1);
assert_eq!(payload.match_indices("From:").count(), 1);
assert!(payload.match_indices("From:").next() < payload.match_indices("Autocrypt:").next());
Ok(())
}
}
+30 -5
View File
@@ -1203,6 +1203,9 @@ impl MimeMessage {
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
let original_message_id = report_fields
.get_header_value(HeaderDef::OriginalMessageId)
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
// the original message id into the In-Reply-To header:
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
.and_then(|v| parse_message_id(&v).ok());
let additional_message_ids = report_fields
.get_header_value(HeaderDef::AdditionalMessageIds)
@@ -1480,8 +1483,8 @@ async fn update_gossip_peerstates(
pub(crate) struct Report {
/// Original-Message-ID header
///
/// It MUST be present if the original message has a Message-ID according to RFC 8098, but MS
/// Exchange does not add it nevertheless, in which case it is `None`.
/// It MUST be present if the original message has a Message-ID according to RFC 8098.
/// In case we can't find it (shouldn't happen), this is None.
original_message_id: Option<String>,
/// Additional-Message-IDs
additional_message_ids: Vec<String>,
@@ -3183,10 +3186,32 @@ Message.
#[async_std::test]
async fn test_ms_exchange_mdn() -> Result<()> {
let t = TestContext::new_alice().await;
let raw =
t.set_config(Config::ShowEmails, Some("2")).await?;
let original =
include_bytes!("../test-data/message/ms_exchange_report_original_message.eml");
dc_receive_imf(&t, original, "INBOX", false).await?;
let original_msg_id = t.get_last_msg().await.id;
// 1. Test mimeparser directly
let mdn =
include_bytes!("../test-data/message/ms_exchange_report_disposition_notification.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await?;
assert!(!mimeparser.mdn_reports.is_empty());
let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn).await?;
assert_eq!(mimeparser.mdn_reports.len(), 1);
assert_eq!(
mimeparser.mdn_reports[0].original_message_id.as_deref(),
Some("d5904dc344eeb5deaf9bb44603f0c716@posteo.de")
);
assert!(mimeparser.mdn_reports[0].additional_message_ids.is_empty());
// 2. Test that marking the original msg as read works
dc_receive_imf(&t, mdn, "INBOX", false).await?;
assert_eq!(
original_msg_id.get_state(&t).await?,
MessageState::OutMdnRcvd
);
Ok(())
}
}
+14 -45
View File
@@ -942,21 +942,13 @@ mod tests {
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::peerstate::Peerstate;
use crate::test_utils::{LogSink, TestContext};
use crate::test_utils::{TestContext, TestContextManager};
#[async_std::test]
async fn test_setup_contact() -> Result<()> {
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
@@ -1143,17 +1135,9 @@ mod tests {
#[async_std::test]
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Ensure Bob knows Alice_FP
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await?;
@@ -1276,17 +1260,9 @@ mod tests {
#[async_std::test]
async fn test_setup_contact_concurrent_calls() -> Result<()> {
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// do a scan that is not working as claire is never responding
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
@@ -1315,17 +1291,10 @@ mod tests {
#[async_std::test]
async fn test_secure_join() -> Result<()> {
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
+60 -25
View File
@@ -225,7 +225,7 @@ pub(crate) async fn smtp_send(
let status = match send_result {
Err(crate::smtp::send::Error::SmtpSend(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {:?}", &err);
info!(context, "SMTP failed to send: {:?}", &err);
let res = match err {
async_smtp::smtp::error::Error::Permanent(ref response) => {
@@ -252,8 +252,10 @@ pub(crate) async fn smtp_send(
};
if maybe_transient {
info!(context, "Permanent error that is likely to actually be transient, postponing retry for later");
Status::RetryLater
} else {
info!(context, "Permanent error, message sending failed");
// If we do not retry, add an info message to the chat.
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
// should definitely go here, because user has to open the link to
@@ -277,23 +279,38 @@ pub(crate) async fn smtp_send(
info!(context, "Received extended status code {} for a transient error. This looks like a misconfigured smtp server, let's fail immediatly", first_word);
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
} else {
info!(
context,
"Transient error with status code {}, postponing retry for later",
first_word
);
Status::RetryLater
}
} else {
info!(
context,
"Transient error without status code, postponing retry for later"
);
Status::RetryLater
}
}
_ => {
info!(
context,
"Message sending failed without error returned by the server"
);
if smtp.has_maybe_stale_connection().await {
info!(context, "stale connection? immediately reconnecting");
info!(context, "Connection is probably stale, retry immediately");
Status::RetryNow
} else {
info!(context, "Connection is not stale, retry later");
Status::RetryLater
}
}
};
// this clears last_success info
info!(context, "Failed to send message over SMTP, disconnecting");
smtp.disconnect().await;
res
@@ -343,19 +360,52 @@ pub(crate) async fn send_msg_to_smtp(
return Err(err);
}
let (body, recipients, msg_id) = context
// Increase retry count as soon as we have an SMTP connection. This ensures that the message is
// eventually removed from the queue by exceeding retry limit even in case of an error that
// keeps happening early in the message sending code, e.g. failure to read the message from the
// database.
context
.sql
.execute(
"UPDATE smtp SET retries=retries+1 WHERE id=?",
paramsv![rowid],
)
.await
.context("failed to update retries count")?;
let (body, recipients, msg_id, retries) = context
.sql
.query_row(
"SELECT mime, recipients, msg_id FROM smtp WHERE id=?",
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
paramsv![rowid],
|row| {
let mime: String = row.get(0)?;
let recipients: String = row.get(1)?;
let msg_id: MsgId = row.get(2)?;
Ok((mime, recipients, msg_id))
let retries: i64 = row.get(3)?;
Ok((mime, recipients, msg_id, retries))
},
)
.await?;
if retries > 6 {
message::set_msg_failed(
context,
msg_id,
Some("Number of retries exceeded the limit."),
)
.await;
context
.sql
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
.await
.context("failed to remove message with exceeded retry limit from smtp table")?;
bail!("Number of retries exceeded the limit");
}
info!(
context,
"Retry number {} to send message {} over SMTP", retries, msg_id
);
let recipients_list = recipients
.split(' ')
.filter_map(
@@ -421,27 +471,16 @@ pub(crate) async fn send_msg_to_smtp(
};
match status {
Status::Finished(res) => {
context
.sql
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
.await?;
if res.is_ok() {
msg_id.set_delivered(context).await?;
context
.sql
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
.await?;
}
res
}
Status::RetryNow | Status::RetryLater => {
context
.sql
.execute(
"UPDATE smtp SET retries=retries+1 WHERE id=?",
paramsv![rowid],
)
.await
.context("failed to update retries count")?;
Err(format_err!("Retry"))
}
Status::RetryNow | Status::RetryLater => Err(format_err!("Retry")),
}
}
@@ -454,10 +493,6 @@ pub(crate) async fn send_smtp_messages(
connection: &mut Smtp,
) -> anyhow::Result<()> {
context.send_sync_msg().await?; // Add sync message to the end of the queue if needed.
context
.sql
.execute("DELETE FROM smtp WHERE retries > 5", paramsv![])
.await?;
let rowids = context
.sql
.query_map(
+18 -1
View File
@@ -787,7 +787,7 @@ async fn maybe_add_from_param(
async fn prune_tombstones(sql: &Sql) -> Result<()> {
sql.execute(
"DELETE FROM msgs
WHERE (chat_id=? OR hidden)
WHERE chat_id=?
AND NOT EXISTS (
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
)",
@@ -906,6 +906,23 @@ mod tests {
}
}
/// Regression test for a bug where housekeeping deleted drafts since their
/// `hidden` flag is set.
#[async_std::test]
async fn test_housekeeping_dont_delete_drafts() {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
let mut new_draft = Message::new(Viewtype::Text);
new_draft.set_text(Some("This is my draft".to_string()));
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
housekeeping(&t).await.unwrap();
let loaded_draft = chat.id.get_draft(&t).await.unwrap();
assert_eq!(loaded_draft.unwrap().text.unwrap(), "This is my draft");
}
/// Regression test.
///
/// Previously the code checking for existence of `config` table
+32 -11
View File
@@ -39,6 +39,34 @@ pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avata
static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
Lazy::new(|| std::sync::RwLock::new(BTreeMap::new()));
pub struct TestContextManager {
log_tx: Sender<Event>,
_log_sink: LogSink,
}
impl TestContextManager {
pub async fn new() -> Self {
let (log_tx, _log_sink) = LogSink::create();
Self { log_tx, _log_sink }
}
pub async fn alice(&mut self) -> TestContext {
TestContext::builder()
.configure_alice()
.with_log_sink(self.log_tx.clone())
.build()
.await
}
pub async fn bob(&mut self) -> TestContext {
TestContext::builder()
.configure_bob()
.with_log_sink(self.log_tx.clone())
.build()
.await
}
}
#[derive(Debug, Clone, Default)]
pub struct TestContextBuilder {
key_pair: Option<KeyPair>,
@@ -849,17 +877,10 @@ mod tests {
#[async_std::test]
async fn test_with_both() {
let (log_sender, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_sender.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_sender)
.build()
.await;
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
alice.ctx.emit_event(EventType::Info("hello".into()));
bob.ctx.emit_event(EventType::Info("there".into()));
// panic!("Both fail");
@@ -1,43 +1,43 @@
Return-Path: <anonymous@example.org>
Return-Path: <bob@example.net>
Delivered-To: anonymous@posteo.de
Received: from proxy02.posteo.name ([127.0.0.1])
by dovecot16.posteo.name (Dovecot) with LMTP id Cp2uFxP1sWHbCQEAchYRkQ
for <anonymous@posteo.de>; Thu, 09 Dec 2021 13:25:38 +0100
by dovecot16.posteo.name (Dovecot) with LMTP id GaxcARout2HxiwMAchYRkQ
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100
Received: from proxy02.posteo.de ([127.0.0.1])
by proxy02.posteo.name (Dovecot) with LMTP id MWsaCwrvsWG0wgEAGFAyLg
; Thu, 09 Dec 2021 13:25:38 +0100
Received: from mailin06.posteo.de (unknown [10.0.1.6])
by proxy02.posteo.de (Postfix) with ESMTPS id 4J8tXy0KkMz120l
for <anonymous@posteo.de>; Thu, 9 Dec 2021 13:25:38 +0100 (CET)
Received: from mx04.posteo.de (mailin06.posteo.de [127.0.0.1])
by mailin06.posteo.de (Postfix) with ESMTPS id F24DE215B8
for <anonymous@posteo.de>; Thu, 9 Dec 2021 13:25:37 +0100 (CET)
by proxy02.posteo.name (Dovecot) with LMTP id q/LiCqwqt2FMTQEAGFAyLg
; Mon, 13 Dec 2021 12:35:32 +0100
Received: from mailin05.posteo.de (unknown [10.0.1.5])
by proxy02.posteo.de (Postfix) with ESMTPS id 4JCKFJ1LCLz1214
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100 (CET)
Received: from mx03.posteo.de (mailin05.posteo.de [127.0.0.1])
by mailin05.posteo.de (Postfix) with ESMTPS id 1B26420012
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100 (CET)
X-Virus-Scanned: amavisd-new at posteo.de
X-Spam-Flag: NO
X-Spam-Score: 0.011
X-Spam-Level:
X-Spam-Status: No, score=0.011 tagged_above=-1000 required=7
tests=[HTML_MESSAGE=0.001, T_POSTEO_TLSINY=0.01] autolearn=disabled
X-Posteo-Antispam-Signature: v=1; e=base64; a=aes-256-gcm; d=27yedFdXeAzOobR4x685XJ/5e6WQmX8PP5pSnOlGU2a9Ismhk38wb5AS44xh1yeL5PUxla78UEsHwGkPR0IyPRlHWaLMFLd5CJZN3GzFfrj/2CuB+cd1hOLpp9hRmCebc3rchuDr
X-Posteo-Antispam-Signature: v=1; e=base64; a=aes-256-gcm; d=RzB41PpqvrD+cuxf3UAqQLiXQL4MHazHZcKeOYJw75deIl7zxtrLXqfAZCeq2IPKt/njRRONUbfuvNdvLxg4mBJ0Rnb53wFOKOqtEpTzxYoQff3yqBpGSohr0DBG26PyBHi7ba/7
Authentication-Results: posteo.de; dmarc=none (p=none dis=none) header.from=example.org
X-Posteo-TLS-Received-Status: TLSv1.2
Received: from mail.example.org (mail.example.org [0.0.0.0])
by mx04.posteo.de (Postfix) with ESMTPS id 4J8tXx38vRz10yw
for <anonymous@posteo.at>; Thu, 9 Dec 2021 13:25:37 +0100 (CET)
Received: from [192.168.1.11] (port=22105 helo=mail.example.org)
by mx03.posteo.de (Postfix) with ESMTPS id 4JCKFH2ZM7zyx0
for <alice@example.org>; Mon, 13 Dec 2021 12:35:31 +0100 (CET)
Received: from [192.168.1.11] (port=27040 helo=mail.example.org)
by mail.example.org with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
(Exim 4.94.2)
(envelope-from <anonymous@example.org>)
id 1mvIUG-0007VC-2U
for anonymous@posteo.at; Thu, 09 Dec 2021 13:25:24 +0100
From: Anonymous <anonymous@example.org>
To: Anonymous <anonymous@posteo.at>
(envelope-from <bob@example.net>)
id 1mwjc6-0003hM-2K
for alice@example.org; Mon, 13 Dec 2021 12:35:26 +0100
From: Anonymous_2 <bob@example.net>
To: Anonymous_1 <alice@example.org>
Subject: Gelesen: Test message
Thread-Topic: Test message
Thread-Index: AQHX7Dt/+5f88Aokk0KrqG0hbF8dN6wqFvxh
Date: Thu, 9 Dec 2021 12:25:24 +0000
Message-ID: <1711fc3548cd4b2699ccd4fffac17713@anonymous>
In-Reply-To: <75dd051097b02468183707ad0dd62ebd@posteo.de>
Thread-Index: AQHX8BVZ9B3+kB6CNUCtJ9eQIONSNawwSqpi
Date: Mon, 13 Dec 2021 11:35:26 +0000
Message-ID: <59b1d0c94a8d4834b7ab779a76647d44@mail.example.org>
In-Reply-To: <d5904dc344eeb5deaf9bb44603f0c716@posteo.de>
Accept-Language: de-AT, de-DE, en-US
Content-Language: de-DE
X-MS-Has-Attach:
@@ -45,29 +45,29 @@ X-MS-TNEF-Correlator:
x-ms-exchange-transport-fromentityheader: Hosted
x-originating-ip: [192.168.120.215]
Content-Type: multipart/report;
boundary="_000_1711fc3548cd4b2699ccd4fffac17713anonymous_";
boundary="_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_";
report-type=disposition-notification
MIME-Version: 1.0
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
Content-Type: multipart/alternative;
boundary="_002_1711fc3548cd4b2699ccd4fffac17713anonymous_"
boundary="_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_"
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
Content-Type: text/plain; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
Ihre Nachricht
An: Anonymous
An: Anonymous_2
Betreff: Test message
Gesendet: Mittwoch, 8. Dezember 2021 14:57:05 (UTC+01:00) Amsterdam, Ber=
lin, Bern, Rom, Stockholm, Wien
Gesendet: Montag, 13. Dezember 2021 12:33:58 (UTC+01:00) Amsterdam, Berl=
in, Bern, Rom, Stockholm, Wien
wurde am Donnerstag, 9. Dezember 2021 13:24:34 (UTC+01:00) Amsterdam, Berl=
in, Bern, Rom, Stockholm, Wien gelesen.
wurde am Montag, 13. Dezember 2021 12:34:40 (UTC+01:00) Amsterdam, Berlin,=
Bern, Rom, Stockholm, Wien gelesen.
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
Content-Type: text/html; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
@@ -83,26 +83,26 @@ ding-left: 4pt; border-left: #800000 2px solid; } --></style>
<font size=3D"2"><span style=3D"font-size:10pt;">
<div class=3D"PlainText">Ihre Nachricht <br>
<br>
&nbsp;&nbsp; An: Anonymous<br>
&nbsp;&nbsp; An: Anonymous_2<br>
&nbsp;&nbsp; Betreff: Test message<br>
&nbsp;&nbsp; Gesendet: Mittwoch, 8. Dezember 2021 14:57:05 (UTC&#43;01:00) =
Amsterdam, Berlin, Bern, Rom, Stockholm, Wien<br>
&nbsp;&nbsp; Gesendet: Montag, 13. Dezember 2021 12:33:58 (UTC&#43;01:00) A=
msterdam, Berlin, Bern, Rom, Stockholm, Wien<br>
<br>
&nbsp;wurde am Donnerstag, 9. Dezember 2021 13:24:34 (UTC&#43;01:00) Amster=
dam, Berlin, Bern, Rom, Stockholm, Wien gelesen.</div>
&nbsp;wurde am Montag, 13. Dezember 2021 12:34:40 (UTC&#43;01:00) Amsterdam=
, Berlin, Bern, Rom, Stockholm, Wien gelesen.</div>
</span></font>
</body>
</html>
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_--
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_--
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
Content-Type: message/disposition-notification
Final-recipient: RFC822; anonymous@example.org
Final-recipient: RFC822; bob@example.net
Disposition: automatic-action/MDN-sent-automatically; displayed
X-MSExch-Correlation-Key: coNC5vaCQkiAOjek1v1Uew==
X-Display-Name: Anonymous
X-MSExch-Correlation-Key: nf7/jgN6Qk+WzsrkY5s9WA==
X-Display-Name: Anonymous_2
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_--
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_--
@@ -0,0 +1,34 @@
Received: We have to put a Received header here. Otherwise, the message would be ignored
because DC thinks it's a draft, and the test fails.
Alternatively, we could configure the Sentobox folder in the test.
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="=_3293d145f71bf71c4bb97415536759d6"
Date: Mon, 13 Dec 2021 12:33:58 +0100
From: Anonymous_1 <alice@example.org>
To: Anonymous_2 <bob@example.net>
Subject: Test message
Return-Receipt-To: Anonymous_1 <alice@example.org>
Disposition-Notification-To: Anonymous_1 <alice@example.org>
Message-ID: <d5904dc344eeb5deaf9bb44603f0c716@posteo.de>
X-Sender: alice@example.org
User-Agent: Posteo Webmail
--=_3293d145f71bf71c4bb97415536759d6
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset=US-ASCII
This is a test!
Best regards
--=_3293d145f71bf71c4bb97415536759d6
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">
<html><body style=3D'font-size: 10pt; font-family: Verdana,Geneva,sans-seri=
f'>
This is a test!<br /><br />Best regards
</body></html>
--=_3293d145f71bf71c4bb97415536759d6--