Compare commits

..
Author SHA1 Message Date
dignifiedquire 97afe582e1 fix: dont use self.error when it is not available 2020-01-29 18:54:43 +01:00
= 4498954918 simplify failure 2020-01-29 17:31:20 +00:00
= 61ff2c4bee add a failing test taht doesn't use pytest anymore 2020-01-29 17:14:17 +00:00
= 14902ecf15 another iteration 2020-01-29 17:01:47 +00:00
= 289d8fdabf simplify thread handling 2020-01-29 16:48:09 +00:00
= 26c39f876b correctly use dc_context_new 2020-01-29 16:43:01 +00:00
= 1f53d489cb snapshot to try to get test_lowlevel.py to pass 2020-01-29 16:36:00 +00:00
holger krekel 35d055ea62 fix lint 2020-01-29 16:25:39 +01:00
dignifiedquire a2c94fc715 add threads start & stop 2020-01-29 16:05:39 +01:00
dignifiedquire e93911effb update ffi 2020-01-29 15:44:20 +01:00
dignifiedquire 41a7e06332 refactor: make run take the callback and avoid boxing 2020-01-29 14:19:36 +01:00
dignifiedquire 29bb2ec58b some fixes 2020-01-29 14:09:21 +01:00
dignifiedquire efcad4126d cargo fmt 2020-01-29 14:09:21 +01:00
dignifiedquire b4a5767347 start integration work 2020-01-29 14:09:21 +01:00
dignifiedquire 4dbcab9e6d feat: implement minimal rust threads 2020-01-29 14:08:35 +01:00
67 changed files with 1319 additions and 3468 deletions
-1
View File
@@ -15,7 +15,6 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --examples --tests --all-features
fmt:
name: Rustfmt
Generated
+53 -10
View File
@@ -99,7 +99,7 @@ dependencies = [
"byte-pool 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"imap-proto 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
"imap-proto 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -498,6 +498,19 @@ dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crossbeam"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crossbeam-channel"
version = "0.4.0"
@@ -641,6 +654,7 @@ dependencies = [
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"debug_stub_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"deltachat_derive 2.0.0",
"email 0.0.21 (git+https://github.com/deltachat/rust-email)",
@@ -656,7 +670,7 @@ dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lettre_email 0.9.2 (git+https://github.com/deltachat/lettre)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"mailparse 0.10.4 (git+https://github.com/link2xt/mailparse?branch=address-comma)",
"mailparse 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -685,6 +699,17 @@ dependencies = [
"thread-local-object 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "deltachat-provider-database"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "deltachat_derive"
version = "2.0.0"
@@ -698,6 +723,7 @@ name = "deltachat_ffi"
version = "1.0.0-beta.24"
dependencies = [
"deltachat 1.0.0-beta.24",
"deltachat-provider-database 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"human-panic 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1155,6 +1181,11 @@ name = "glob"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "h2"
version = "0.2.1"
@@ -1326,7 +1357,7 @@ dependencies = [
[[package]]
name = "imap-proto"
version = "0.10.2"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1515,12 +1546,12 @@ dependencies = [
[[package]]
name = "mailparse"
version = "0.10.4"
source = "git+https://github.com/link2xt/mailparse?branch=address-comma#f5e7a2175966f85f5c079a32c5ab171fc6e9d08e"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
"charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"quoted_printable 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -2073,7 +2104,7 @@ dependencies = [
[[package]]
name = "quoted_printable"
version = "0.4.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -3241,6 +3272,14 @@ dependencies = [
"zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "yaml-rust"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zeroize"
version = "1.1.0"
@@ -3321,6 +3360,7 @@ dependencies = [
"checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
"checksum crc24 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0"
"checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
"checksum crossbeam 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e"
"checksum crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "acec9a3b0b3559f15aee4f90746c4e5e293b701c0f7d3925d24e01645267b68c"
"checksum crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3aa945d63861bfe624b55d153a39684da1e8c0bc8fba932f7ee3a3c16cea3ca"
"checksum crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5064ebdbf05ce3cb95e45c8b086f72263f4166b29b97f6baff7ef7fe047b55ac"
@@ -3334,6 +3374,7 @@ dependencies = [
"checksum darling_macro 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
"checksum debug_stub_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "496b7f8a2f853313c3ca370641d7ff3e42c32974fdccda8f0684599ed0a3ff6b"
"checksum deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
"checksum deltachat-provider-database 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "814dba060d9fdc7a989fccdc4810ada9d1c7a1f09131c78e42412bc6c634b93b"
"checksum derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
"checksum derive_builder_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
"checksum des 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "74ba5f1b5aee9772379c2670ba81306e65a93c0ee3caade7a1d22b188d88a3af"
@@ -3389,6 +3430,7 @@ dependencies = [
"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407"
"checksum gif 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af"
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
"checksum h2 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9433d71e471c1736fd5a61b671fc0b148d7a2992f666c958d03cd8feb3b88d1"
"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
"checksum hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f629dc602392d3ec14bfc8a09b5e644d7ffd725102b48b81e59f90f2633621d7"
@@ -3405,7 +3447,7 @@ dependencies = [
"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
"checksum image 0.22.4 (registry+https://github.com/rust-lang/crates.io-index)" = "53cb19c4e35102e5c6fb9ade5e0e236c5588424dc171a849af3141bf0b47768a"
"checksum image-meta 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b00861cbbb254a627d8acc0cec786b484297d896ab8f20fdc8e28536a3e918ef"
"checksum imap-proto 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f"
"checksum imap-proto 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "11ba57a7d18ba7bca5a50d4723c16a3e532d2b3014d6fd3123910c266214d4f2"
"checksum indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712d7b3ea5827fcb9d4fda14bf4da5f136f0db2ae9c8f4bd4e2d1c6fde4e6db2"
"checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff"
"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
@@ -3429,7 +3471,7 @@ dependencies = [
"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
"checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
"checksum mach 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1"
"checksum mailparse 0.10.4 (git+https://github.com/link2xt/mailparse?branch=address-comma)" = "<none>"
"checksum mailparse 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "04c64e94d3a5d02abe0cf459507689d70fb3b9b1d5fea5773a9f5273117959fb"
"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
"checksum md-5 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a18af3dcaf2b0219366cdb4e2af65a6101457b415c3d1a5c71dd9c2b7c77b9c8"
@@ -3487,7 +3529,7 @@ dependencies = [
"checksum quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
"checksum quoted_printable 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2"
"checksum quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "86cedf331228892e747bb85beb130b6bb23fc628c40dde9ea01eb6becea3c798"
"checksum r2d2 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)" = "22b5c5fc5fba064373f03887337b412e0e1562d63023393db77251146cb75553"
"checksum r2d2_sqlite 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b061f5b16692bbe81eeb260f92e6fc7d13aea455c4cbe67f5c4aa20aa92d1d9e"
"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
@@ -3621,5 +3663,6 @@ dependencies = [
"checksum winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
"checksum x25519-dalek 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217"
"checksum yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d"
"checksum zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8"
"checksum zeroize_derive 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2"
+3 -3
View File
@@ -6,7 +6,7 @@ edition = "2018"
license = "MPL-2.0"
[profile.release]
lto = true
# lto = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
@@ -53,13 +53,13 @@ bitflags = "1.1.0"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = { git = "https://github.com/link2xt/mailparse", branch="address-comma" }
mailparse = "0.10.2"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
pretty_env_logger = "0.3.1"
rustyline = { version = "4.1.0", optional = true }
crossbeam = "0.7.3"
[dev-dependencies]
tempfile = "3.0"
+1 -1
View File
@@ -6,7 +6,7 @@
set -e -x
# for core-building and python install step
export DCC_RS_TARGET=debug
export DCC_RS_TARGET=release
export DCC_RS_DEV=`pwd`
cd python
+1
View File
@@ -16,6 +16,7 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-provider-database = "0.2.1"
libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"
+58 -417
View File
@@ -243,7 +243,7 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
*/
dc_context_t* dc_context_new (dc_callback_t cb, void* userdata, const char* os_name);
dc_context_t* dc_context_new (void* userdata, const char* os_name);
/**
@@ -403,7 +403,6 @@ int dc_set_config (dc_context_t* context, const char*
*/
char* dc_get_config (dc_context_t* context, const char* key);
/**
* Set stock string translation.
*
@@ -418,22 +417,6 @@ char* dc_get_config (dc_context_t* context, const char*
int dc_set_stock_translation(dc_context_t* context, uint32_t stock_id, const char* stock_msg);
/**
* Set configuration values from a QR code containing an account.
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT.
*
* Internally, the function will call dc_set_config()
* at least with the keys `addr` and `mail_pw`.
*
* @memberof dc_context_t
* @param context The context object
* @param qr scanned QR code
* @return int (==0 on error, 1 on success)
*/
int dc_set_config_from_qr (dc_context_t* context, const char* qr);
/**
* Get information about the context.
*
@@ -555,307 +538,14 @@ int dc_is_configured (const dc_context_t* context);
/**
* Execute pending imap-jobs.
* This function and dc_perform_imap_fetch() and dc_perform_imap_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* imap_thread_func(void* context)
* {
* while (true) {
* dc_perform_imap_jobs(context);
* dc_perform_imap_fetch(context);
* dc_perform_imap_idle(context);
* }
* }
*
* // start imap-thread that runs forever
* pthread_t imap_thread;
* pthread_create(&imap_thread, NULL, imap_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_imap_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
* TODO: Document
*/
void dc_perform_imap_jobs (dc_context_t* context);
void dc_context_run (dc_context_t* context, dc_callback_t cb);
/**
* Fetch new messages, if any.
* This function and dc_perform_imap_jobs() and dc_perform_imap_idle() must be called from the same thread,
* typically in a loop.
*
* See dc_perform_imap_jobs() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
* TODO: Document
*/
void dc_perform_imap_fetch (dc_context_t* context);
/**
* Wait for messages or jobs.
* This function and dc_perform_imap_jobs() and dc_perform_imap_fetch() must be called from the same thread,
* typically in a loop.
*
* You should call this function directly after calling dc_perform_imap_fetch().
*
* See dc_perform_imap_jobs() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_imap_idle (dc_context_t* context);
/**
* Interrupt waiting for imap-jobs.
* If dc_perform_imap_jobs(), dc_perform_imap_fetch() and dc_perform_imap_idle() are called in a loop,
* calling this function causes imap-jobs to be executed and messages to be fetched.
*
* dc_interrupt_imap_idle() does _not_ interrupt dc_perform_imap_jobs() or dc_perform_imap_fetch().
* If the imap-thread is inside one of these functions when dc_interrupt_imap_idle() is called, however,
* the next call of the imap-thread to dc_perform_imap_idle() is interrupted immediately.
*
* Internally, this function is called whenever a imap-jobs should be processed
* (delete message, markseen etc.).
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_imap_idle (dc_context_t* context);
/**
* Execute pending mvbox-jobs.
* This function and dc_perform_mvbox_fetch() and dc_perform_mvbox_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* mvbox_thread_func(void* context)
* {
* while (true) {
* dc_perform_mvbox_jobs(context);
* dc_perform_mvbox_fetch(context);
* dc_perform_mvbox_idle(context);
* }
* }
*
* // start mvbox-thread that runs forever
* pthread_t mvbox_thread;
* pthread_create(&mvbox_thread, NULL, mvbox_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_mvbox_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_jobs (dc_context_t* context);
/**
* Fetch new messages from the MVBOX, if any.
* The MVBOX is a folder on the account where chat messages are moved to.
* The moving is done to not disturb shared accounts that are used by both,
* Delta Chat and a classical MUA.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_fetch (dc_context_t* context);
/**
* Wait for messages or jobs in the MVBOX-thread.
* This function and dc_perform_mvbox_fetch().
* must be called from the same thread, typically in a loop.
*
* You should call this function directly after calling dc_perform_mvbox_fetch().
*
* See dc_perform_mvbox_fetch() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_idle (dc_context_t* context);
/**
* Interrupt waiting for MVBOX-fetch.
* dc_interrupt_mvbox_idle() does _not_ interrupt dc_perform_mvbox_fetch().
* If the MVBOX-thread is inside this function when dc_interrupt_mvbox_idle() is called, however,
* the next call of the MVBOX-thread to dc_perform_mvbox_idle() is interrupted immediately.
*
* Internally, this function is called whenever a imap-jobs should be processed.
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_mvbox_idle (dc_context_t* context);
/**
* Execute pending sentbox-jobs.
* This function and dc_perform_sentbox_fetch() and dc_perform_sentbox_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* sentbox_thread_func(void* context)
* {
* while (true) {
* dc_perform_sentbox_jobs(context);
* dc_perform_sentbox_fetch(context);
* dc_perform_sentbox_idle(context);
* }
* }
*
* // start sentbox-thread that runs forever
* pthread_t sentbox_thread;
* pthread_create(&sentbox_thread, NULL, sentbox_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_sentbox_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_jobs (dc_context_t* context);
/**
* Fetch new messages from the Sent folder, if any.
* This function and dc_perform_sentbox_idle()
* must be called from the same thread, typically in a loop.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_fetch (dc_context_t* context);
/**
* Wait for messages or jobs in the SENTBOX-thread.
* This function and dc_perform_sentbox_fetch()
* must be called from the same thread, typically in a loop.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_idle (dc_context_t* context);
/**
* Interrupt waiting for messages or jobs in the SENTBOX-thread.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_sentbox_idle (dc_context_t* context);
/**
* Execute pending smtp-jobs.
* This function and dc_perform_smtp_idle() must be called from the same thread,
* typically in a loop.
*
* Example:
*
* void* smtp_thread_func(void* context)
* {
* while (true) {
* dc_perform_smtp_jobs(context);
* dc_perform_smtp_idle(context);
* }
* }
*
* // start smtp-thread that runs forever
* pthread_t smtp_thread;
* pthread_create(&smtp_thread, NULL, smtp_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_smtp_idle() in the thread above
* // to return so that jobs are executed
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_smtp_jobs (dc_context_t* context);
/**
* Wait for smtp-jobs.
* This function and dc_perform_smtp_jobs() must be called from the same thread,
* typically in a loop.
*
* See dc_interrupt_smtp_idle() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_smtp_idle (dc_context_t* context);
/**
* Interrupt waiting for smtp-jobs.
* If dc_perform_smtp_jobs() and dc_perform_smtp_idle() are called in a loop,
* calling this function causes jobs to be executed.
*
* dc_interrupt_smtp_idle() does _not_ interrupt dc_perform_smtp_jobs().
* If the smtp-thread is inside this function when dc_interrupt_smtp_idle() is called, however,
* the next call of the smtp-thread to dc_perform_smtp_idle() is interrupted immediately.
*
* Internally, this function is called whenever a message is to be sent.
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_smtp_idle (dc_context_t* context);
void dc_context_shutdown (dc_context_t* context);
/**
* This function can be called whenever there is a hint
@@ -869,27 +559,6 @@ void dc_interrupt_smtp_idle (dc_context_t* context);
void dc_maybe_network (dc_context_t* context);
/**
* Save a keypair as the default keys for the user.
*
* This API is only for testing purposes and should not be used as part of a
* normal application, use the import-export APIs instead.
*
* This saves a public/private keypair as the default keypair in the context.
* It allows avoiding having to generate a secret key for unittests which need
* one.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param addr The email address of the user. This must match the
* configured_addr setting of the context as well as the UID of the key.
* @param public_data The public key as base64.
* @param secret_data The secret key as base64.
* @return 1 on success, 0 on failure.
*/
int dc_preconfigure_keypair (dc_context_t* context, const char *addr, const char *public_data, const char *secret_data);
// handle chatlists
#define DC_GCL_ARCHIVED_ONLY 0x01
@@ -1594,22 +1263,6 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
int dc_set_chat_profile_image (dc_context_t* context, uint32_t chat_id, const char* image);
/**
* Set mute duration of a chat.
*
* This value can be checked by the ui upon receiving a new message to decide whether it should trigger an notification.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*
* @memberof dc_context_t
* @param chat_id The chat ID to set the mute duration.
* @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
* @param context The context as created by dc_context_new().
* @return 1=success, 0=error
*/
int dc_set_chat_mute_duration (dc_context_t* context, uint32_t chat_id, int64_t duration);
// handle messages
/**
@@ -2128,7 +1781,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_OK 210 // id=contact
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
@@ -2146,12 +1798,12 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
* - DC_QR_ACCOUNT allows creation of an account, dc_lot_t::text1=domain
* - DC_QR_ADDR with dc_lot_t::id=Contact ID
* - DC_QR_TEXT with dc_lot_t::text1=Text
* - DC_QR_URL with dc_lot_t::text1=URL
* - DC_QR_ERROR with dc_lot_t::text1=Error string
*
*
* @memberof dc_context_t
* @param context The context object.
* @param qr The text of the scanned QR code.
@@ -2953,26 +2605,6 @@ int dc_chat_is_verified (const dc_chat_t* chat);
int dc_chat_is_sending_locations (const dc_chat_t* chat);
/**
* Check whether the chat is currently muted
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=muted, 0=not muted
*/
int dc_chat_is_muted (const dc_chat_t* chat);
/**
* Get the exact state of the mute of a chat
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 0=not muted, -1=forever muted, (x>0)=remaining seconds until the mute is lifted
*/
int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
/**
* @class dc_msg_t
*
@@ -3761,19 +3393,30 @@ int dc_contact_is_verified (dc_contact_t* contact);
*/
/**
* Create a provider struct for the given domain.
*
* @memberof dc_provider_t
* @param domain The domain to get provider info for.
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_domain (const char* domain);
/**
* Create a provider struct for the given email address.
*
* The provider is extracted from the email address and it's information is returned.
*
* @memberof dc_provider_t
* @param context The context object as created by dc_context_new().
* @param email The user's email address to extract the provider info form.
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_email (const dc_context_t*, const char* email);
dc_provider_t* dc_provider_new_from_email (const char* email);
/**
@@ -3783,35 +3426,53 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t*, cons
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return String with a fully-qualified URL,
* if there is no such URL, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_overview_page (const dc_provider_t* provider);
/**
* Get hints to be shown to the user on the login screen.
* Depending on the @ref DC_PROVIDER_STATUS returned by dc_provider_get_status(),
* the ui may want to highlight the hint.
* The provider's name.
*
* Moreover, the ui should display a "More information" link
* that forwards to the url returned by dc_provider_get_overview_page().
* The name of the provider, e.g. "POSTEO".
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string with the hint to show to the user, may contain multiple lines,
* if there is no such hint, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_before_login_hint (const dc_provider_t* provider);
char* dc_provider_get_name (const dc_provider_t* provider);
/**
* The markdown content of the providers page.
*
* This contains the preparation steps or additional information if the status
* is @ref DC_PROVIDER_STATUS_BROKEN.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_markdown (const dc_provider_t* provider);
/**
* Date of when the state was last checked/updated.
*
* This is returned as a string.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_status_date (const dc_provider_t* provider);
/**
* Whether DC works with this provider.
*
* Can be one of #DC_PROVIDER_STATUS_OK,
* #DC_PROVIDER_STATUS_PREPARATION or #DC_PROVIDER_STATUS_BROKEN.
* Can be one of @ref DC_PROVIDER_STATUS_OK, @ref
* DC_PROVIDER_STATUS_PREPARATION and @ref DC_PROVIDER_STATUS_BROKEN.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
@@ -3826,7 +3487,7 @@ int dc_provider_get_status (const dc_provider_t* prov
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
*/
void dc_provider_unref (dc_provider_t* provider);
void dc_provider_unref (const dc_provider_t* provider);
/**
@@ -4552,43 +4213,23 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
*/
/**
* Prover works out-of-the-box.
* This provider status is returned for provider where the login
* works by just entering the name or the email-address.
* Provider status returned by dc_provider_get_status().
*
* - There is no need for the user to do any special things
* (enable IMAP or so) in the provider's webinterface or at other places.
* - There is no need for the user to enter advanced settings;
* server, port etc. are known by the core.
*
* The status is returned by dc_provider_get_status().
* Works right out of the box without any preperation steps needed
*/
#define DC_PROVIDER_STATUS_OK 1
/**
* Provider works, but there are preparations needed.
* Provider status returned by dc_provider_get_status().
*
* - The user has to do some special things as "Enable IMAP in the Webinterface",
* what exactly, is described in the string returnd by dc_provider_get_before_login_hints()
* and, typically more detailed, in the page linked by dc_provider_get_overview_page().
* - There is no need for the user to enter advanced settings;
* server, port etc. should be known by the core.
*
* The status is returned by dc_provider_get_status().
* Works, but preparation steps are needed
*/
#define DC_PROVIDER_STATUS_PREPARATION 2
/**
* Provider is not working.
* This provider status is returned for providers
* that are known to not work with Delta Chat.
* The ui should block logging in with this provider.
* Provider status returned by dc_provider_get_status().
*
* More information about that is typically provided
* in the string returned by dc_provider_get_before_login_hints()
* and in the page linked by dc_provider_get_overview_page().
*
* The status is returned by dc_provider_get_status().
* Doesn't work (too unstable to use falls also in this category)
*/
#define DC_PROVIDER_STATUS_BROKEN 3
+112 -490
View File
@@ -20,17 +20,14 @@ use std::fmt::Write;
use std::ptr;
use std::str::FromStr;
use std::sync::RwLock;
use std::time::{Duration, SystemTime};
use libc::uintptr_t;
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::chat::ChatId;
use deltachat::chat::MuteDuration;
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::stock::StockMessage;
use deltachat::*;
@@ -60,7 +57,6 @@ use self::string::*;
/// and protected by an [RwLock]. Other than that it needs to store
/// the data which is passed into [dc_context_new].
pub struct ContextWrapper {
cb: Option<dc_callback_t>,
userdata: *mut libc::c_void,
os_name: String,
inner: RwLock<Option<context::Context>>,
@@ -94,15 +90,10 @@ impl ContextWrapper {
///
/// This function makes it easy to log an error.
unsafe fn error(&self, msg: &str) {
self.translate_cb(Event::Error(msg.to_string()));
}
/// Log a warning on the FFI context.
///
/// Like [error] but logs as a warning which only goes to the
/// logfile rather than being shown directly to the user.
unsafe fn warning(&self, msg: &str) {
self.translate_cb(Event::Warning(msg.to_string()));
self.with_inner(|ctx| {
ctx.call_cb(Event::Error(msg.to_string()));
})
.unwrap();
}
/// Unlock the context and execute a closure with it.
@@ -120,117 +111,27 @@ impl ContextWrapper {
/// the appropriate return value for an error return since this
/// differs for various functions on the FFI API: sometimes 0,
/// NULL, an empty string etc.
///
/// Prefer to use [ContextWrapper::try_inner], we might want to
/// remove this function at some point to reduce the cognitive
/// overload of having two functions which are too similar.
unsafe fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
where
F: FnOnce(&Context) -> T,
{
self.try_inner(|ctx| Ok(ctxfn(ctx))).map_err(|err| {
self.warning(&err.to_string());
})
}
/// Unlock the context and execute a closure with it.
///
/// This is like [ContextWrapper::with_inner] but uses
/// [failure::Error] as error type. This allows you to write a
/// closure which could produce many errors, use the `?` operator
/// to return them and handle them all as the return of this call.
fn try_inner<T, F>(&self, ctxfn: F) -> Result<T, failure::Error>
where
F: FnOnce(&Context) -> Result<T, failure::Error>,
{
let guard = self.inner.read().unwrap();
match guard.as_ref() {
Some(ref ctx) => ctxfn(ctx),
None => Err(failure::err_msg("context not open")),
Some(ref ctx) => Ok(ctxfn(ctx)),
None => {
eprintln!("context not open");
Err(())
}
}
}
/// Translates the callback from the rust style to the C-style version.
unsafe fn translate_cb(&self, event: Event) {
if let Some(ffi_cb) = self.cb {
let event_id = event.as_id();
match event {
Event::Info(msg)
| Event::SmtpConnected(msg)
| Event::ImapConnected(msg)
| Event::SmtpMessageSent(msg)
| Event::ImapMessageDeleted(msg)
| Event::ImapMessageMoved(msg)
| Event::ImapFolderEmptied(msg)
| Event::NewBlobFile(msg)
| Event::DeletedBlobFile(msg)
| Event::Warning(msg)
| Event::Error(msg)
| Event::ErrorNetwork(msg)
| Event::ErrorSelfNotInGroup(msg) => {
let data2 = CString::new(msg).unwrap_or_default();
ffi_cb(self, event_id, 0, data2.as_ptr() as uintptr_t);
}
Event::MsgsChanged { chat_id, msg_id }
| Event::IncomingMsg { chat_id, msg_id }
| Event::MsgDelivered { chat_id, msg_id }
| Event::MsgFailed { chat_id, msg_id }
| Event::MsgRead { chat_id, msg_id } => {
ffi_cb(
self,
event_id,
chat_id.to_u32() as uintptr_t,
msg_id.to_u32() as uintptr_t,
);
}
Event::ChatModified(chat_id) => {
ffi_cb(self, event_id, chat_id.to_u32() as uintptr_t, 0);
}
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
let id = id.unwrap_or_default();
ffi_cb(self, event_id, id as uintptr_t, 0);
}
Event::ConfigureProgress(progress) | Event::ImexProgress(progress) => {
ffi_cb(self, event_id, progress as uintptr_t, 0);
}
Event::ImexFileWritten(file) => {
let data1 = file.to_c_string().unwrap_or_default();
ffi_cb(self, event_id, data1.as_ptr() as uintptr_t, 0);
}
Event::SecurejoinInviterProgress {
contact_id,
progress,
}
| Event::SecurejoinJoinerProgress {
contact_id,
progress,
} => {
ffi_cb(
self,
event_id,
contact_id as uintptr_t,
progress as uintptr_t,
);
}
Event::SecurejoinMemberAdded {
chat_id,
contact_id,
} => {
ffi_cb(
self,
event_id,
chat_id.to_u32() as uintptr_t,
contact_id as uintptr_t,
);
}
}
}
fn is_open(&self) -> bool {
self.inner.read().unwrap().is_some()
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_new(
cb: Option<dc_callback_t>,
userdata: *mut libc::c_void,
os_name: *const libc::c_char,
) -> *mut dc_context_t {
@@ -242,11 +143,11 @@ pub unsafe extern "C" fn dc_context_new(
to_string_lossy(os_name)
};
let ffi_ctx = ContextWrapper {
cb,
userdata,
os_name,
inner: RwLock::new(None),
};
Box::into_raw(Box::new(ffi_ctx))
}
@@ -284,17 +185,11 @@ pub unsafe extern "C" fn dc_open(
return 0;
}
let ffi_context = &*context;
let rust_cb = move |_ctx: &Context, evt: Event| ffi_context.translate_cb(evt);
let ctx = if blobdir.is_null() || *blobdir == 0 {
Context::new(
Box::new(rust_cb),
ffi_context.os_name.clone(),
as_path(dbfile).to_path_buf(),
)
Context::new(ffi_context.os_name.clone(), as_path(dbfile).to_path_buf())
} else {
Context::with_blobdir(
Box::new(rust_cb),
ffi_context.os_name.clone(),
as_path(dbfile).to_path_buf(),
as_path(blobdir).to_path_buf(),
@@ -327,11 +222,7 @@ pub unsafe extern "C" fn dc_is_open(context: *mut dc_context_t) -> libc::c_int {
return 0;
}
let ffi_context = &*context;
let inner_guard = ffi_context.inner.read().unwrap();
match *inner_guard {
Some(_) => 1,
None => 0,
}
ffi_context.is_open() as libc::c_int
}
#[no_mangle]
@@ -423,28 +314,6 @@ pub unsafe extern "C" fn dc_set_stock_translation(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_config_from_qr(
context: *mut dc_context_t,
qr: *mut libc::c_char,
) -> libc::c_int {
if context.is_null() || qr.is_null() {
eprintln!("ignoring careless call to dc_set_config_from_qr");
return 0;
}
let qr = to_string_lossy(qr);
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| match qr::set_config_from_qr(ctx, &qr) {
Ok(()) => 1,
Err(err) => {
error!(ctx, "Failed to create account from QR code: {}", err);
0
}
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
@@ -504,9 +373,7 @@ pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| configure::configure(ctx))
.unwrap_or(())
ffi_context.with_inner(|ctx| ctx.configure()).unwrap_or(())
}
#[no_mangle]
@@ -517,188 +384,120 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| configure::dc_is_configured(ctx) as libc::c_int)
.with_inner(|ctx| ctx.is_configured() as libc::c_int)
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_imap_jobs(context: *mut dc_context_t) {
pub unsafe extern "C" fn dc_context_run(context: *mut dc_context_t, cb: Option<dc_callback_t>) {
eprintln!("dc_context_run");
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_imap_jobs()");
eprintln!("ignoring careless call to dc_run()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_inbox_jobs(ctx))
.with_inner(|ctx| {
eprintln!("RUN");
ctx.run(|_ctx, event| {
translate_cb(ffi_context, cb, event);
});
eprintln!("RUN DONE");
})
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_imap_fetch(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_imap_fetch()");
return;
/// Translates the callback from the rust style to the C-style version.
unsafe fn translate_cb(ctx: &ContextWrapper, ffi_cb: Option<dc_callback_t>, event: Event) {
if let Some(ffi_cb) = ffi_cb {
let event_id = event.as_id();
match event {
Event::Info(msg)
| Event::SmtpConnected(msg)
| Event::ImapConnected(msg)
| Event::SmtpMessageSent(msg)
| Event::ImapMessageDeleted(msg)
| Event::ImapMessageMoved(msg)
| Event::ImapFolderEmptied(msg)
| Event::NewBlobFile(msg)
| Event::DeletedBlobFile(msg)
| Event::Warning(msg)
| Event::Error(msg)
| Event::ErrorNetwork(msg)
| Event::ErrorSelfNotInGroup(msg) => {
let data2 = CString::new(msg).unwrap_or_default();
ffi_cb(ctx, event_id, 0, data2.as_ptr() as uintptr_t);
}
Event::MsgsChanged { chat_id, msg_id }
| Event::IncomingMsg { chat_id, msg_id }
| Event::MsgDelivered { chat_id, msg_id }
| Event::MsgFailed { chat_id, msg_id }
| Event::MsgRead { chat_id, msg_id } => {
ffi_cb(
ctx,
event_id,
chat_id.to_u32() as uintptr_t,
msg_id.to_u32() as uintptr_t,
);
}
Event::ChatModified(chat_id) => {
ffi_cb(ctx, event_id, chat_id.to_u32() as uintptr_t, 0);
}
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
let id = id.unwrap_or_default();
ffi_cb(ctx, event_id, id as uintptr_t, 0);
}
Event::ConfigureProgress(progress) | Event::ImexProgress(progress) => {
ffi_cb(ctx, event_id, progress as uintptr_t, 0);
}
Event::ImexFileWritten(file) => {
let data1 = file.to_c_string().unwrap_or_default();
ffi_cb(ctx, event_id, data1.as_ptr() as uintptr_t, 0);
}
Event::SecurejoinInviterProgress {
contact_id,
progress,
}
| Event::SecurejoinJoinerProgress {
contact_id,
progress,
} => {
ffi_cb(
ctx,
event_id,
contact_id as uintptr_t,
progress as uintptr_t,
);
}
Event::SecurejoinMemberAdded {
chat_id,
contact_id,
} => {
ffi_cb(
ctx,
event_id,
chat_id.to_u32() as uintptr_t,
contact_id as uintptr_t,
);
}
}
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_inbox_fetch(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_imap_idle(context: *mut dc_context_t) {
// TODO rename function in co-ordination with UIs
pub unsafe extern "C" fn dc_context_shutdown(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_imap_idle()");
eprintln!("ignoring careless call to dc_shutdown()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_inbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_imap_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_imap_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_inbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_mvbox_fetch(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_mvbox_fetch()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_mvbox_fetch(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_mvbox_jobs(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_mvbox_jobs()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_mvbox_jobs(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_mvbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_mvbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_mvbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_mvbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_mvbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_mvbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_sentbox_fetch(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_sentbox_fetch()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_sentbox_fetch(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_sentbox_jobs(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_sentbox_jobs()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_sentbox_jobs(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_sentbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_sentbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_sentbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_sentbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_sentbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_sentbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_smtp_jobs(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_smtp_jobs()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_smtp_jobs(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_smtp_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_smtp_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_smtp_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_smtp_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_smtp_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_smtp_idle(ctx))
.with_inner(|ctx| {
eprintln!("SHUTDOWN");
ctx.shutdown();
eprintln!("SHUTDOWN:DONE")
})
.unwrap_or(())
}
@@ -714,35 +513,6 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_preconfigure_keypair(
context: *mut dc_context_t,
addr: *const libc::c_char,
public_data: *const libc::c_char,
secret_data: *const libc::c_char,
) -> i32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_preconfigure_keypair()");
return 0;
}
let ffi_context = &*context;
ffi_context
.try_inner(|ctx| {
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
let public = key::SignedPublicKey::from_base64(&to_string_lossy(public_data))?;
let secret = key::SignedSecretKey::from_base64(&to_string_lossy(secret_data))?;
let keypair = key::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default)?;
Ok(1)
})
.log_warn(ffi_context, "Failed to save keypair")
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_chatlist(
context: *mut dc_context_t,
@@ -1431,37 +1201,6 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_mute_duration(
context: *mut dc_context_t,
chat_id: u32,
duration: i64,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_mute_duration()");
return 0;
}
let ffi_context = &*context;
let muteDuration = match duration {
0 => MuteDuration::NotMuted,
-1 => MuteDuration::Forever,
n if n > 0 => MuteDuration::Until(SystemTime::now() + Duration::from_secs(duration as u64)),
_ => {
ffi_context.warning(
"dc_chat_set_mute_duration(): Can not use negative duration other than -1",
);
return 0;
}
};
ffi_context
.with_inner(|ctx| {
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set mute duration")
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_info(
context: *mut dc_context_t,
@@ -1864,9 +1603,7 @@ pub unsafe extern "C" fn dc_imex_has_backup(
.with_inner(|ctx| match imex::has_backup(ctx, to_string_lossy(dir)) {
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);
error!(ctx, "dc_imex_has_backup: {}", err);
ptr::null_mut()
}
})
@@ -2536,37 +2273,6 @@ pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> l
ffi_chat.chat.is_sending_locations() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_muted(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_muted()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_muted() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_remaining_mute_duration(chat: *mut dc_chat_t) -> i64 {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_remaining_mute_duration()");
return 0;
}
let ffi_chat = &*chat;
if !ffi_chat.chat.is_muted() {
return 0;
}
// If the chat was muted to before the epoch, it is not muted.
match ffi_chat.chat.mute_duration {
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
MuteDuration::Until(when) => when
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_info_json(
context: *mut dc_context_t,
@@ -3298,21 +3004,11 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
}
pub mod providers;
pub trait ResultExt<T, E> {
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
fn log_err(self, context: &context::Context, message: &str) -> Result<T, E>;
/// Log a warning to a [ContextWrapper] for an [Err] result.
///
/// Does nothing for an [Ok]. This is usually preferable over
/// [ResultExt::log_err] because warnings go to the logfile and
/// errors are displayed directly to the user. Usually problems
/// on the FFI layer are coding errors and not errors which need
/// to be displayed to the user.
///
/// You can do this as soon as the wrapper exists, it does not
/// have to be open (which is required for the `warn!()` macro).
fn log_warn(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E>;
}
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
@@ -3332,15 +3028,6 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
err
})
}
fn log_warn(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E> {
self.map_err(|err| {
unsafe {
wrapper.warning(&format!("{}: {}", message, err));
}
err
})
}
}
unsafe fn strdup_opt(s: Option<impl AsRef<str>>) -> *mut libc::c_char {
@@ -3373,68 +3060,3 @@ fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> V
msg_ids
}
// dc_provider_t
#[no_mangle]
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 {
if context.is_null() || addr.is_null() {
eprintln!("ignoring careless call to dc_provider_new_from_email()");
return ptr::null();
}
let addr = to_string_lossy(addr);
match provider::get_provider_info(addr.as_str()) {
Some(provider) => provider,
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_overview_page(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_overview_page()");
return "".strdup();
}
let provider = &*provider;
provider.overview_page.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_before_login_hint(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_before_login_hint()");
return "".strdup();
}
let provider = &*provider;
provider.before_login_hint.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> libc::c_int {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_status()");
return 0;
}
let provider = &*provider;
provider.status as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_unref()");
return;
}
// currently, there is nothing to free, the provider info is a static object.
// this may change once we start localizing string.
}
+91
View File
@@ -0,0 +1,91 @@
extern crate deltachat_provider_database;
use std::ptr;
use crate::string::{to_string_lossy, StrExt};
use deltachat_provider_database::StatusState;
#[no_mangle]
pub type dc_provider_t = deltachat_provider_database::Provider;
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_domain(
domain: *const libc::c_char,
) -> *const dc_provider_t {
match deltachat_provider_database::get_provider_info(&to_string_lossy(domain)) {
Some(provider) => provider,
None => ptr::null(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email(
email: *const libc::c_char,
) -> *const dc_provider_t {
let email = to_string_lossy(email);
let domain = deltachat_provider_database::get_domain_from_email(&email);
match deltachat_provider_database::get_provider_info(domain) {
Some(provider) => provider,
None => ptr::null(),
}
}
macro_rules! null_guard {
($context:tt) => {
if $context.is_null() {
return ptr::null_mut() as *mut libc::c_char;
}
};
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_overview_page(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
format!(
"{}/{}",
deltachat_provider_database::PROVIDER_OVERVIEW_URL,
(*provider).overview_page
)
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_name(provider: *const dc_provider_t) -> *mut libc::c_char {
null_guard!(provider);
(*provider).name.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_markdown(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
(*provider).markdown.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status_date(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
(*provider).status.date.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> u32 {
if provider.is_null() {
return 0;
}
match (*provider).status.state {
StatusState::OK => 1,
StatusState::PREPARATION => 2,
StatusState::BROKEN => 3,
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {}
// TODO expose general provider overview url?
+1 -24
View File
@@ -3,6 +3,7 @@ use std::str::FromStr;
use deltachat::chat::{self, Chat, ChatId};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
@@ -18,7 +19,6 @@ use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::Event;
use deltachat::{config, provider};
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
@@ -392,7 +392,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
getqr [<chat-id>]\n\
getbadqr\n\
checkqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
@@ -967,28 +966,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
res.get_text2()
);
}
"setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
set_config_from_qr(context, arg1);
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);
println!("before_login_hint: {}", info.before_login_hint);
println!("after_login_hint: {}", info.after_login_hint);
println!("overview_page: {}", info.overview_page);
for server in info.server.iter() {
println!("server: {}:{}", server.hostname, server.port,);
}
}
None => {
println!("No information for provider belonging to {} found.", arg1);
}
}
}
// TODO: implement this again, unclear how to match this through though, without writing a parser.
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
+22 -130
View File
@@ -9,22 +9,17 @@ extern crate deltachat;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate rusqlite;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::sync::{Arc, RwLock};
use deltachat::chat::ChatId;
use deltachat::config;
use deltachat::configure::*;
use deltachat::context::*;
use deltachat::job::*;
use deltachat::oauth2::*;
use deltachat::securejoin::*;
use deltachat::Event;
@@ -114,107 +109,6 @@ fn receive_event(_context: &Context, event: Event) {
}
}
// Threads for waiting for messages and for jobs
lazy_static! {
static ref HANDLE: Arc<Mutex<Option<Handle>>> = Arc::new(Mutex::new(None));
static ref IS_RUNNING: AtomicBool = AtomicBool::new(true);
}
struct Handle {
handle_imap: Option<std::thread::JoinHandle<()>>,
handle_mvbox: Option<std::thread::JoinHandle<()>>,
handle_sentbox: Option<std::thread::JoinHandle<()>>,
handle_smtp: Option<std::thread::JoinHandle<()>>,
}
macro_rules! while_running {
($code:block) => {
if IS_RUNNING.load(Ordering::Relaxed) {
$code
} else {
break;
}
};
}
fn start_threads(c: Arc<RwLock<Context>>) {
if HANDLE.clone().lock().unwrap().is_some() {
return;
}
println!("Starting threads");
IS_RUNNING.store(true, Ordering::Relaxed);
let ctx = c.clone();
let handle_imap = std::thread::spawn(move || loop {
while_running!({
perform_inbox_jobs(&ctx.read().unwrap());
perform_inbox_fetch(&ctx.read().unwrap());
while_running!({
let context = ctx.read().unwrap();
perform_inbox_idle(&context);
});
});
});
let ctx = c.clone();
let handle_mvbox = std::thread::spawn(move || loop {
while_running!({
perform_mvbox_fetch(&ctx.read().unwrap());
while_running!({
perform_mvbox_idle(&ctx.read().unwrap());
});
});
});
let ctx = c.clone();
let handle_sentbox = std::thread::spawn(move || loop {
while_running!({
perform_sentbox_fetch(&ctx.read().unwrap());
while_running!({
perform_sentbox_idle(&ctx.read().unwrap());
});
});
});
let ctx = c;
let handle_smtp = std::thread::spawn(move || loop {
while_running!({
perform_smtp_jobs(&ctx.read().unwrap());
while_running!({
perform_smtp_idle(&ctx.read().unwrap());
});
});
});
*HANDLE.clone().lock().unwrap() = Some(Handle {
handle_imap: Some(handle_imap),
handle_mvbox: Some(handle_mvbox),
handle_sentbox: Some(handle_sentbox),
handle_smtp: Some(handle_smtp),
});
}
fn stop_threads(context: &Context) {
if let Some(ref mut handle) = *HANDLE.clone().lock().unwrap() {
println!("Stopping threads");
IS_RUNNING.store(false, Ordering::Relaxed);
interrupt_inbox_idle(context);
interrupt_mvbox_idle(context);
interrupt_sentbox_idle(context);
interrupt_smtp_idle(context);
handle.handle_imap.take().unwrap().join().unwrap();
handle.handle_mvbox.take().unwrap().join().unwrap();
handle.handle_sentbox.take().unwrap().join().unwrap();
handle.handle_smtp.take().unwrap().join().unwrap();
}
}
// === The main loop
struct DcHelper {
completer: FilenameCompleter,
highlighter: MatchingBracketHighlighter,
@@ -420,9 +314,7 @@ fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
}
rl.save_history(".dc-history.txt")?;
println!("history saved");
{
stop_threads(&ctx.read().unwrap());
}
ctx.read().unwrap().shutdown();
Ok(())
}
@@ -440,28 +332,20 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
match arg0 {
"connect" => {
start_threads(ctx);
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
}
"disconnect" => {
stop_threads(&ctx.read().unwrap());
}
"smtp-jobs" => {
if HANDLE.clone().lock().unwrap().is_some() {
println!("smtp-jobs are already running in a thread.",);
} else {
perform_smtp_jobs(&ctx.read().unwrap());
}
}
"imap-jobs" => {
if HANDLE.clone().lock().unwrap().is_some() {
println!("inbox-jobs are already running in a thread.");
} else {
perform_inbox_jobs(&ctx.read().unwrap());
}
ctx.read().unwrap().shutdown();
}
"configure" => {
start_threads(ctx.clone());
configure(&ctx.read().unwrap());
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
ctx.read().unwrap().configure();
}
"oauth2" => {
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
@@ -484,7 +368,11 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
print!("\x1b[1;1H\x1b[2J");
}
"getqr" | "getbadqr" => {
start_threads(ctx.clone());
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
if let Some(mut qr) = dc_get_securejoin_qr(
&ctx.read().unwrap(),
ChatId::new(arg1.parse().unwrap_or_default()),
@@ -504,8 +392,12 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
}
}
"joinqr" => {
start_threads(ctx.clone());
if !arg0.is_empty() {
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
dc_join_securejoin(&ctx.read().unwrap(), arg1);
}
}
+42 -73
View File
@@ -1,19 +1,13 @@
extern crate deltachat;
use std::sync::{Arc, RwLock};
use std::{thread, time};
use tempfile::tempdir;
use deltachat::chat;
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::configure::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::job::{
perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle,
perform_smtp_jobs,
};
use deltachat::Event;
fn cb(_ctx: &Context, event: Event) {
@@ -21,13 +15,13 @@ fn cb(_ctx: &Context, event: Event) {
match event {
Event::ConfigureProgress(progress) => {
println!(" progress: {}", progress);
print!(" progress: {}\n", progress);
}
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
println!(" {}", msg);
print!(" {}\n", msg);
}
_ => {
println!();
print!("\n");
}
}
}
@@ -36,77 +30,52 @@ fn main() {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
println!("creating database {:?}", dbfile);
let ctx =
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context");
let running = Arc::new(RwLock::new(true));
let ctx = Context::new("FakeOs".into(), dbfile).expect("Failed to create context");
let info = ctx.get_info();
let duration = time::Duration::from_millis(4000);
let duration = time::Duration::from_millis(8000);
println!("info: {:#?}", info);
let ctx = Arc::new(ctx);
let ctx1 = ctx.clone();
let r1 = running.clone();
let t1 = thread::spawn(move || {
while *r1.read().unwrap() {
perform_inbox_jobs(&ctx1);
if *r1.read().unwrap() {
perform_inbox_fetch(&ctx1);
crossbeam::scope(|s| {
let t1 = s.spawn(|_| {
ctx.run(cb);
});
if *r1.read().unwrap() {
perform_inbox_idle(&ctx1);
}
}
println!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 2, "missing password");
let pw = args[1].clone();
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
ctx.configure();
thread::sleep(duration);
println!("sending a message");
let contact_id =
Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
println!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
for i in 0..chats.len() {
let summary = chats.get_summary(&ctx, 0, None);
let text1 = summary.get_text1();
let text2 = summary.get_text2();
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
}
});
let ctx1 = ctx.clone();
let r1 = running.clone();
let t2 = thread::spawn(move || {
while *r1.read().unwrap() {
perform_smtp_jobs(&ctx1);
if *r1.read().unwrap() {
perform_smtp_idle(&ctx1);
}
}
});
thread::sleep(duration);
println!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 2, "missing password");
let pw = args[1].clone();
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
configure(&ctx);
println!("stopping threads");
ctx.shutdown();
thread::sleep(duration);
println!("joining");
t1.join().unwrap();
println!("sending a message");
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
println!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
for i in 0..chats.len() {
let summary = chats.get_summary(&ctx, 0, None);
let text1 = summary.get_text1();
let text2 = summary.get_text2();
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
}
thread::sleep(duration);
println!("stopping threads");
*running.write().unwrap() = false;
deltachat::job::interrupt_inbox_idle(&ctx);
deltachat::job::interrupt_smtp_idle(&ctx);
println!("joining");
t1.join().unwrap();
t2.join().unwrap();
println!("closing");
println!("closing");
})
.unwrap();
}
+6 -7
View File
@@ -52,21 +52,20 @@ Python packages before running the tests::
pytest -v tests
running "live" tests with temporary accounts
---------------------------------------------
running "live" tests (experimental)
-----------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_NEW_TMP_EMAIL`` to:
``DCC_PY_LIVECONFIG`` to:
- a particular https-url that you can ask for from the delta
chat devs. This is implemented on the server side via
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
chat devs.
- or the path of a file that contains two lines, each describing
via "addr=... mail_pw=..." a test account login that will
be used for the live tests.
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real
e-mail accounts and run through all functional "liveconfig" tests.
@@ -130,7 +129,7 @@ organization::
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e DCC_NEW_TMP_EMAIL \
$ docker run -e DCC_PY_LIVECONFIG \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
+7
View File
@@ -0,0 +1,7 @@
from __future__ import print_function
from deltachat import capi
from deltachat.capi import ffi, lib
if __name__ == "__main__":
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
lib.dc_context_shutdown(ctx)
+8 -5
View File
@@ -11,15 +11,18 @@ import sys
if __name__ == "__main__":
target = os.environ.get("DCC_RS_TARGET")
if target is None:
os.environ["DCC_RS_TARGET"] = target = "debug"
os.environ["DCC_RS_TARGET"] = target = "release"
if "DCC_RS_DEV" not in os.environ:
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.environ["DCC_RS_DEV"] = dn
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
if target == 'release':
cmd.append("--release")
subprocess.check_call(cmd)
# build the core library in release + debug mode because
# as of Nov 2019 rPGP generates RSA keys which take
# prohibitively long for non-release installs
os.environ["RUSTFLAGS"] = "-g"
subprocess.check_call([
"cargo", "build", "-p", "deltachat_ffi", "--" + target
])
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
+11 -74
View File
@@ -38,7 +38,7 @@ class Account(object):
:param debug: turn on debug logging for events.
"""
self._dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
lib.dc_context_new(ffi.NULL, as_dc_charpointer(os_name)),
_destroy_dc_context,
)
if eventlogging:
@@ -118,18 +118,6 @@ class Account(object):
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr, public, secret):
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
"""
res = lib.dc_preconfigure_keypair(self._dc_context,
as_dc_charpointer(addr),
as_dc_charpointer(public),
as_dc_charpointer(secret))
if res == 0:
raise Exception("Failed to set key")
def configure(self, **kwargs):
""" set config values and configure this account.
@@ -392,8 +380,6 @@ class Account(object):
def _export(self, path, imex_cmd):
self._imex_events_clear()
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
files_written = []
while True:
ev = self._imex_events.get()
@@ -422,8 +408,6 @@ class Account(object):
def _import(self, path, imex_cmd):
self._imex_events_clear()
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
if not self._imex_events.get():
raise ValueError("import from path '{}' failed".format(path))
@@ -502,7 +486,7 @@ class Account(object):
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
return self.get_message_by_id(ev[2])
def start_threads(self, mvbox=False, sentbox=False):
def start_threads(self):
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
@@ -510,7 +494,7 @@ class Account(object):
"""
if not self.is_configured():
self.configure()
self._threads.start(mvbox=mvbox, sentbox=sentbox)
self._threads.start()
def stop_threads(self, wait=True):
""" stop IMAP/SMTP threads. """
@@ -572,16 +556,9 @@ class IOThreads:
def is_started(self):
return len(self._name2thread) > 0
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
def start(self):
assert not self.is_started()
if imap:
self._start_one_thread("inbox", self.imap_thread_run)
if mvbox:
self._start_one_thread("mvbox", self.mvbox_thread_run)
if sentbox:
self._start_one_thread("sentbox", self.sentbox_thread_run)
if smtp:
self._start_one_thread("smtp", self.smtp_thread_run)
self._start_one_thread("deltachat", self.dc_thread_run)
def _start_one_thread(self, name, func):
self._name2thread[name] = t = threading.Thread(target=func, name=name)
@@ -589,58 +566,18 @@ class IOThreads:
t.start()
def stop(self, wait=False):
self._thread_quitflag = True
# Workaround for a race condition. Make sure that thread is
# not in between checking for quitflag and entering idle.
time.sleep(0.5)
lib.dc_interrupt_imap_idle(self._dc_context)
lib.dc_interrupt_smtp_idle(self._dc_context)
lib.dc_interrupt_mvbox_idle(self._dc_context)
lib.dc_interrupt_sentbox_idle(self._dc_context)
lib.dc_context_shutdown(self._dc_context)
if wait:
for name, thread in self._name2thread.items():
thread.join()
def imap_thread_run(self):
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_imap_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_idle(self._dc_context)
def dc_thread_run(self):
self._log_event("py-bindings-info", 0, "DC THREAD START")
lib.dc_context_run(self._dc_context, lib.py_dc_callback)
self._log_event("py-bindings-info", 0, "INBOX THREAD FINISHED")
def mvbox_thread_run(self):
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_mvbox_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
def sentbox_thread_run(self):
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_sentbox_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
def smtp_thread_run(self):
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
while not self._thread_quitflag:
lib.dc_perform_smtp_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_smtp_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
class EventLogger:
_loglock = threading.RLock()
+1 -39
View File
@@ -58,13 +58,6 @@ class Chat(object):
"""
return self.id == const.DC_CHAT_ID_DEADDROP
def is_muted(self):
""" return true if this chat is muted.
:returns: True if chat is muted, False otherwise.
"""
return lib.dc_chat_is_muted(self._dc_chat)
def is_promoted(self):
""" return True if this chat is promoted, i.e.
the member contacts are aware of their membership,
@@ -91,43 +84,12 @@ class Chat(object):
def set_name(self, name):
""" set name of this chat.
:param name: as a unicode string.
:param: name as a unicode string.
:returns: None
"""
name = as_dc_charpointer(name)
return lib.dc_set_chat_name(self._dc_context, self.id, name)
def mute(self, duration=None):
""" mutes the chat
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
:returns: None
"""
if duration is None:
mute_duration = -1
else:
mute_duration = duration
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration)
if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self):
""" unmutes the chat
:returns: None
"""
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0)
if not bool(ret):
raise ValueError("Failed to unmute chat")
def get_mute_duration(self):
""" Returns the number of seconds until the mute of this chat is lifted.
:param duration:
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
"""
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
def get_type(self):
""" return type of this chat.
+26 -6
View File
@@ -11,18 +11,27 @@ class ProviderNotFoundError(Exception):
class Provider(object):
"""Provider information.
:param domain: The email to get the provider info for.
:param domain: The domain to get the provider info for, this is
normally the part following the `@` of the domain.
"""
def __init__(self, account, addr):
def __init__(self, domain):
provider = ffi.gc(
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
lib.dc_provider_new_from_domain(as_dc_charpointer(domain)),
lib.dc_provider_unref,
)
if provider == ffi.NULL:
raise ProviderNotFoundError("Provider not found")
self._provider = provider
@classmethod
def from_email(cls, email):
"""Create provider info from an email address.
:param email: Email address to get provider info for.
"""
return cls(email.split('@')[-1])
@property
def overview_page(self):
"""URL to the overview page of the provider on providers.delta.chat."""
@@ -30,10 +39,21 @@ class Provider(object):
lib.dc_provider_get_overview_page(self._provider))
@property
def get_before_login_hints(self):
"""Should be shown to the user on login."""
def name(self):
"""The name of the provider."""
return from_dc_charpointer(lib.dc_provider_get_name(self._provider))
@property
def markdown(self):
"""Content of the information page, formatted as markdown."""
return from_dc_charpointer(
lib.dc_provider_get_before_login_hints(self._provider))
lib.dc_provider_get_markdown(self._provider))
@property
def status_date(self):
"""The date the provider info was last updated, as a string."""
return from_dc_charpointer(
lib.dc_provider_get_status_date(self._provider))
@property
def status(self):
+20 -58
View File
@@ -1,6 +1,5 @@
from __future__ import print_function
import os
import py
import pytest
import requests
import time
@@ -16,29 +15,16 @@ def pytest_addoption(parser):
help="a file with >=2 lines where each line "
"contains NAME=VALUE config settings for one account"
)
parser.addoption(
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
def pytest_configure(config):
config.addinivalue_line(
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
)
cfg = config.getoption('--liveconfig')
if not cfg:
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
cfg = os.getenv('DCC_PY_LIVECONFIG')
if cfg:
config.option.liveconfig = cfg
def pytest_runtest_setup(item):
if (list(item.iter_markers(name="ignored"))
and not item.config.getoption("ignored")):
pytest.skip("Ignored tests not requested, use --ignored")
def pytest_report_header(config, startdir):
summary = []
@@ -97,16 +83,17 @@ class SessionLiveConfigFromFile:
class SessionLiveConfigFromURL:
def __init__(self, url):
def __init__(self, url, create_token):
self.configlist = []
self.url = url
self.create_token = create_token
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url)
res = requests.post(self.url, json={"token_create_user": int(self.create_token)})
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
@@ -123,24 +110,14 @@ def session_liveconfig(request):
liveconfig_opt = request.config.option.liveconfig
if liveconfig_opt:
if liveconfig_opt.startswith("http"):
return SessionLiveConfigFromURL(liveconfig_opt)
url, create_token = liveconfig_opt.split("#", 1)
return SessionLiveConfigFromURL(url, create_token)
else:
return SessionLiveConfigFromFile(liveconfig_opt)
@pytest.fixture(scope='session')
def datadir():
"""The py.path.local object of the test-data/ directory."""
for path in reversed(py.path.local(__file__).parts()):
datadir = path.join('test-data')
if datadir.isdir():
return datadir
else:
pytest.skip('test-data directory not found')
@pytest.fixture
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
class AccountMaker:
def __init__(self):
@@ -148,8 +125,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
self.offline_count = 0
self._finalizers = []
self.init_time = time.time()
self._generated_keys = ["alice", "bob", "charlie",
"dom", "elena", "fiona"]
def finalize(self):
while self._finalizers:
@@ -169,32 +144,26 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
ac._evlogger.set_timeout(2)
return ac
def _preconfigure_key(self, account, addr):
# Only set a key if we haven't used it yet for another account.
if self._generated_keys:
keyname = self._generated_keys.pop(0)
fname_pub = "key/{name}-public.asc".format(name=keyname)
fname_sec = "key/{name}-secret.asc".format(name=keyname)
account._preconfigure_keypair(addr,
datadir.join(fname_pub).read(),
datadir.join(fname_sec).read())
def get_configured_offline_account(self):
ac = self.get_unconfigured_account()
# do a pseudo-configured account
addr = "addr{}@offline.org".format(self.offline_count)
ac.set_config("addr", addr)
self._preconfigure_key(ac, addr)
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
ac.set_config("mail_pw", "123")
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
lib.dc_set_config(ac._dc_context, b"configured", b"1")
return ac
def get_online_config(self, pre_generated_key=True):
def peek_online_config(self):
if not session_liveconfig:
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
return session_liveconfig.get(self.live_count)
def get_online_config(self):
if not session_liveconfig:
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
configdict = session_liveconfig.get(self.live_count)
self.live_count += 1
if "e2ee_enabled" not in configdict:
@@ -206,23 +175,18 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False,
pre_generated_key=True):
ac, configdict = self.get_online_config(
pre_generated_key=pre_generated_key)
def get_online_configuring_account(self, mvbox=False, sentbox=False):
ac, configdict = self.get_online_config()
ac.configure(**configdict)
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
ac.start_threads()
return ac
def get_one_online_account(self, pre_generated_key=True):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key)
def get_one_online_account(self):
ac1 = self.get_online_configuring_account()
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
return ac1
@@ -236,12 +200,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
wait_configuration_progress(ac2, 1000)
return ac1, ac2
def clone_online_account(self, account, pre_generated_key=True):
def clone_online_account(self, account):
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, account.get_config("addr"))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
+23 -62
View File
@@ -6,8 +6,7 @@ import time
from deltachat import const, Account
from deltachat.message import Message
from datetime import datetime, timedelta
from conftest import (wait_configuration_progress,
wait_securejoin_inviter_progress)
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection, wait_securejoin_inviter_progress
class TestOfflineAccountBasic:
@@ -25,12 +24,6 @@ class TestOfflineAccountBasic:
ac1 = Account(p.strpath, os_name="solarpunk")
ac1.get_info()
def test_preconfigure_keypair(self, acfactory, datadir):
ac = acfactory.get_unconfigured_account()
ac._preconfigure_keypair("alice@example.com",
datadir.join("key/alice-public.asc").read(),
datadir.join("key/alice-secret.asc").read())
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
d = ac1.get_info()
@@ -183,6 +176,7 @@ class TestOfflineChat:
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
ac1.start_threads()
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
ac1._evlogger.consume_events()
with pytest.raises(ValueError):
@@ -202,6 +196,7 @@ class TestOfflineChat:
assert not chat.is_promoted()
msg = chat.get_draft()
assert msg.text == "xyz title1"
ac1.stop_threads()
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
@@ -219,18 +214,6 @@ class TestOfflineChat:
chat.remove_profile_image()
assert chat.get_profile_image() is None
def test_mute(self, ac1):
chat = ac1.create_group_chat(name="title1")
assert not chat.is_muted()
chat.mute()
assert chat.is_muted()
chat.unmute()
assert not chat.is_muted()
chat.mute(50)
assert chat.is_muted()
with pytest.raises(ValueError):
chat.mute(-51)
def test_delete_and_send_fails(self, ac1, chat1):
chat1.delete()
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
@@ -428,12 +411,6 @@ class TestOfflineChat:
class TestOnlineAccount:
@pytest.mark.ignored
def test_configure_generate_key(self, acfactory):
# A slow test which will generate a new key.
ac = acfactory.get_one_online_account(pre_generated_key=False)
ac.check_is_configured()
def get_chat(self, ac1, ac2, both_created=False):
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
@@ -460,53 +437,37 @@ class TestOnlineAccount:
def test_one_account_send_bcc_setting(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
# Clone the first account: we will test if sent messages
# are copied to it via BCC.
ac1_clone = acfactory.clone_online_account(ac1)
ac2_config = acfactory.peek_online_config()
c2 = ac1.create_contact(email=ac2_config["addr"])
chat = ac1.create_chat_by_contact(c2)
assert chat.id > const.DC_CHAT_ID_LAST_SPECIAL
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
wait_configuration_progress(ac1_clone, 1000)
chat = self.get_chat(ac1, ac2)
self_addr = ac1.get_config("addr")
other_addr = ac2.get_config("addr")
lp.sec("send out message without bcc to ourselves")
ac1.set_config("bcc_self", "0")
msg_out = chat.send_text("message1")
assert not msg_out.is_forwarded()
# wait for send out (no BCC)
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "0"
# make sure we are not sending message to ourselves
assert self_addr not in ev[2]
assert other_addr in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
lp.sec("ac1: setting bcc_self=1")
ac1.set_config("bcc_self", "1")
lp.sec("send out message with bcc to ourselves")
msg_out = chat.send_text("message2")
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev[2] == msg_out.id
# wait for send out (BCC)
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "1"
# now make sure we are sending message to ourselves too
self_addr = ac1.get_config("addr")
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert self_addr in ev[2]
assert other_addr in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
# Second client receives only second message, but not the first
ev = ac1_clone._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ac1_clone.get_message_by_id(ev[2]).text == msg_out.text
ac1._evlogger.consume_events()
lp.sec("send out message without bcc")
ac1.set_config("bcc_self", "0")
msg_out = chat.send_text("message3")
assert not msg_out.is_forwarded()
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev[2] == msg_out.id
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert self_addr not in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
def test_send_file_twice_unicode_filename_mangling(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -703,7 +664,7 @@ class TestOnlineAccount:
assert not msg_in.is_forwarded()
assert msg_in.get_sender_contact().display_name == ac1.get_config("displayname")
lp.sec("check the message arrived in contact-requests/deaddrop")
lp.sec("check the message arrived in contact-requets/deaddrop")
chat2 = msg_in.chat
assert msg_in in chat2.get_messages()
assert chat2.is_deaddrop()
+48 -14
View File
@@ -1,25 +1,48 @@
from __future__ import print_function
import threading
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
from deltachat.capi import ffi
from deltachat.capi import lib
from deltachat.account import EventLogger
class EventThread(threading.Thread):
def __init__(self, dc_context):
self.dc_context = dc_context
super(EventThread, self).__init__()
self.setDaemon(1)
def run(self):
lib.dc_context_run(self.dc_context, lib.py_dc_callback)
def stop(self):
lib.dc_context_shutdown(self.dc_context)
def test_empty_context():
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL)
capi.lib.dc_close(ctx)
def test_callback_None2int():
ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL)
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
set_context_callback(ctx, lambda *args: None)
capi.lib.dc_close(ctx)
clear_context_callback(ctx)
def test_start_stop_event_thread_basic():
print("1")
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
print("2")
ev_thread = EventThread(ctx)
print("3 -- starting event thread")
ev_thread.start()
print("4 -- stopping event thread")
ev_thread.stop()
def test_dc_close_events(tmpdir):
ctx = ffi.gc(
capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL),
capi.lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
evlog = EventLogger(ctx)
@@ -28,6 +51,9 @@ def test_dc_close_events(tmpdir):
ctx,
lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)
)
ev_thread = EventThread(ctx)
ev_thread.start()
p = tmpdir.join("hello.db")
lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL)
capi.lib.dc_close(ctx)
@@ -46,11 +72,12 @@ def test_dc_close_events(tmpdir):
find("disconnecting mvbox-thread")
find("disconnecting SMTP")
find("Database closed")
ev_thread.stop()
def test_wrong_db(tmpdir):
dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
p = tmpdir.join("hello.db")
@@ -62,7 +89,7 @@ def test_wrong_db(tmpdir):
def test_empty_blobdir(tmpdir):
# Apparently some client code expects this to be the same as passing NULL.
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("hello.db")
@@ -102,17 +129,24 @@ def test_get_special_message_id_returns_empty_message(acfactory):
assert msg.id == 0
def test_provider_info():
provider = lib.dc_provider_new_from_email(cutil.as_dc_charpointer("ex@example.com"))
assert cutil.from_dc_charpointer(
lib.dc_provider_get_overview_page(provider)
) == "https://providers.delta.chat/example.com"
assert cutil.from_dc_charpointer(lib.dc_provider_get_name(provider)) == "Example"
assert cutil.from_dc_charpointer(lib.dc_provider_get_markdown(provider)) == "\n..."
assert cutil.from_dc_charpointer(lib.dc_provider_get_status_date(provider)) == "2018-09"
assert lib.dc_provider_get_status(provider) == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
assert lib.dc_provider_new_from_email(cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_closed():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
@@ -122,7 +156,7 @@ def test_get_info_closed():
def test_get_info_open(tmpdir):
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("test.db")
@@ -134,7 +168,7 @@ def test_get_info_open(tmpdir):
def test_is_open_closed():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_is_open(ctx) == 0
@@ -142,7 +176,7 @@ def test_is_open_closed():
def test_is_open_actually_open(tmpdir):
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("test.db")
+27
View File
@@ -0,0 +1,27 @@
import pytest
from deltachat import const
from deltachat import provider
def test_provider_info_from_email():
example = provider.Provider.from_email("email@example.com")
assert example.overview_page == "https://providers.delta.chat/example.com"
assert example.name == "Example"
assert example.markdown == "\n..."
assert example.status_date == "2018-09"
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_from_domain():
example = provider.Provider("example.com")
assert example.overview_page == "https://providers.delta.chat/example.com"
assert example.name == "Example"
assert example.markdown == "\n..."
assert example.status_date == "2018-09"
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
with pytest.raises(provider.ProviderNotFoundError):
provider.Provider.from_email("email@unexistent.no")
+2 -2
View File
@@ -7,7 +7,7 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs:tests}
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
@@ -65,7 +65,7 @@ commands =
[pytest]
addopts = -v -ra --strict-markers
addopts = -v -ra
python_files = tests/test_*.py
norecursedirs = .tox
xfail_strict=true
+1 -1
View File
@@ -1 +1 @@
nightly-2019-11-06
nightly-2020-01-26
+24 -15
View File
@@ -6,10 +6,10 @@ use std::collections::BTreeMap;
use std::str::FromStr;
use std::{fmt, str};
use crate::constants::*;
use crate::contact::*;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, SignedPublicKey};
use crate::key::*;
/// Possible values for encryption preference
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
@@ -52,17 +52,13 @@ impl str::FromStr for EncryptPreference {
#[derive(Debug)]
pub struct Aheader {
pub addr: String,
pub public_key: SignedPublicKey,
pub public_key: Key,
pub prefer_encrypt: EncryptPreference,
}
impl Aheader {
/// Creates new autocrypt header
pub fn new(
addr: String,
public_key: SignedPublicKey,
prefer_encrypt: EncryptPreference,
) -> Self {
pub fn new(addr: String, public_key: Key, prefer_encrypt: EncryptPreference) -> Self {
Aheader {
addr,
public_key,
@@ -75,7 +71,9 @@ impl Aheader {
wanted_from: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Option<Self> {
if let Ok(Some(value)) = headers.get_header_value(HeaderDef::Autocrypt) {
use mailparse::MailHeaderMap;
if let Ok(Some(value)) = headers.get_first_value("Autocrypt") {
match Self::from_str(&value) {
Ok(header) => {
if addr_cmp(&header.addr, wanted_from) {
@@ -144,11 +142,22 @@ impl str::FromStr for Aheader {
return Err(());
}
};
let public_key: SignedPublicKey = attributes
let public_key = match attributes
.remove("keydata")
.ok_or(())
.and_then(|raw| SignedPublicKey::from_base64(&raw).or(Err(())))
.and_then(|key| key.verify().and(Ok(key)).or(Err(())))?;
.and_then(|raw| Key::from_base64(&raw, KeyType::Public))
{
Some(key) => {
if key.verify() {
key
} else {
return Err(());
}
}
None => {
return Err(());
}
};
let prefer_encrypt = attributes
.remove("prefer-encrypt")
@@ -283,7 +292,7 @@ mod tests {
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
EncryptPreference::Mutual
)
)
@@ -296,7 +305,7 @@ mod tests {
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
EncryptPreference::NoPreference
)
)
+16 -136
View File
@@ -1,8 +1,6 @@
//! # Chat module
use std::convert::TryFrom;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use itertools::Itertools;
use num_traits::FromPrimitive;
@@ -47,7 +45,7 @@ impl ChatId {
/// An unset ChatId
///
/// Like [ChatId::is_error], from which it is indistinguishable, this is
/// Like [is_error], from which it is indistinguishable, this is
/// transitional and should not be used in new code.
pub fn is_unset(self) -> bool {
self.0 == 0
@@ -424,7 +422,6 @@ pub struct Chat {
blocked: Blocked,
pub param: Params,
is_sending_locations: bool,
pub mute_duration: MuteDuration,
}
impl Chat {
@@ -432,7 +429,7 @@ impl Chat {
pub fn load_from_db(context: &Context, chat_id: ChatId) -> Result<Self, Error> {
let res = context.sql.query_row(
"SELECT c.type, c.name, c.grpid, c.param, c.archived,
c.blocked, c.locations_send_until, c.muted_until
c.blocked, c.locations_send_until
FROM chats c
WHERE c.id=?;",
params![chat_id],
@@ -446,7 +443,6 @@ impl Chat {
archived: row.get(4)?,
blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
is_sending_locations: row.get(6)?,
mute_duration: row.get(7)?,
};
Ok(c)
},
@@ -587,19 +583,23 @@ impl Chat {
sql.query_row(&query, params, collect).ok()
}
fn parent_is_encrypted(&self, context: &Context) -> Result<bool, Error> {
fn parent_is_encrypted(&self, context: &Context) -> bool {
let sql = &context.sql;
let params = params![self.id];
let query = Self::parent_query("param");
let packed: Option<String> = sql.query_get_value_result(&query, params)?;
let packed: Option<String> = sql.query_get_value(context, &query, params);
if let Some(ref packed) = packed {
let param = packed.parse::<Params>()?;
Ok(param.exists(Param::GuaranteeE2ee))
match packed.parse::<Params>() {
Ok(param) => param.exists(Param::GuaranteeE2ee),
Err(err) => {
error!(context, "invalid params stored: '{}', {:?}", packed, err);
false
}
}
} else {
// No messages
Ok(false)
false
}
}
@@ -662,7 +662,6 @@ impl Chat {
profile_image: self.get_profile_image(context).unwrap_or_else(PathBuf::new),
subtitle: self.get_subtitle(context),
draft,
is_muted: self.is_muted(),
})
}
@@ -689,14 +688,6 @@ impl Chat {
self.is_sending_locations
}
pub fn is_muted(&self) -> bool {
match self.mute_duration {
MuteDuration::NotMuted => false,
MuteDuration::Forever => true,
MuteDuration::Until(when) => when > SystemTime::now(),
}
}
fn prepare_msg_raw(
&mut self,
context: &Context,
@@ -810,7 +801,7 @@ impl Chat {
}
}
if can_encrypt && (all_mutual || self.parent_is_encrypted(context)?) {
if can_encrypt && (all_mutual || self.parent_is_encrypted(context)) {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
}
@@ -981,11 +972,6 @@ pub struct ChatInfo {
/// which contain non-text parts. Perhaps it should be a
/// simple `has_draft` bool instead.
pub draft: String,
/// Whether the chat is muted
///
/// The exact time its muted can be found out via the `chat.mute_duration` property
pub is_muted: bool,
// ToDo:
// - [ ] deaddrop,
// - [ ] summary,
@@ -1919,66 +1905,6 @@ pub fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<boo
Ok(needs_attach)
}
#[derive(Debug, Clone, PartialEq)]
pub enum MuteDuration {
NotMuted,
Forever,
Until(SystemTime),
}
impl rusqlite::types::ToSql for MuteDuration {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let duration: i64 = match &self {
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
MuteDuration::Until(when) => {
let duration = when
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
i64::try_from(duration.as_secs())
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?
}
};
let val = rusqlite::types::Value::Integer(duration);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
impl rusqlite::types::FromSql for MuteDuration {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
// Negative values other than -1 should not be in the
// database. If found they'll be NotMuted.
match i64::column_result(value)? {
0 => Ok(MuteDuration::NotMuted),
-1 => Ok(MuteDuration::Forever),
n if n > 0 => match SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(n as u64)) {
Some(t) => Ok(MuteDuration::Until(t)),
None => Err(rusqlite::types::FromSqlError::OutOfRange(n)),
},
_ => Ok(MuteDuration::NotMuted),
}
}
}
pub fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<(), Error> {
ensure!(!chat_id.is_special(), "Invalid chat ID");
if real_group_exists(context, chat_id)
&& sql::execute(
context,
&context.sql,
"UPDATE chats SET muted_until=? WHERE id=?;",
params![duration, chat_id],
)
.is_ok()
{
context.call_cb(Event::ChatModified(chat_id));
} else {
bail!("Failed to set name");
}
Ok(())
}
pub fn remove_contact_from_chat(
context: &Context,
chat_id: ChatId,
@@ -2476,7 +2402,7 @@ mod tests {
let chat = Chat::load_from_db(&t.ctx, chat_id).unwrap();
let info = chat.get_info(&t.ctx).unwrap();
// Ensure we can serialize this.
// Ensure we can serialise this.
println!("{}", serde_json::to_string_pretty(&info).unwrap());
let expected = r#"
@@ -2491,12 +2417,11 @@ mod tests {
"color": 15895624,
"profile_image": "",
"subtitle": "bob@example.com",
"draft": "",
"is_muted": false
"draft": ""
}
"#;
// Ensure we can deserialize this.
// Ensure we can deserialise this.
let loaded: ChatInfo = serde_json::from_str(expected).unwrap();
assert_eq!(info, loaded);
}
@@ -2856,49 +2781,4 @@ mod tests {
assert!(chat_id.set_selfavatar_timestamp(&t.ctx, time()).is_ok());
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
}
#[test]
fn test_set_mute_duration() {
let t = dummy_context();
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap();
// Initial
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
// Forever
set_muted(&t.ctx, chat_id, MuteDuration::Forever).unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
true
);
// unMute
set_muted(&t.ctx, chat_id, MuteDuration::NotMuted).unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
// Timed in the future
set_muted(
&t.ctx,
chat_id,
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
)
.unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
true
);
// Time in the past
set_muted(
&t.ctx,
chat_id,
MuteDuration::Until(SystemTime::now() - Duration::from_secs(3600)),
)
.unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
}
}
+26 -75
View File
@@ -12,13 +12,11 @@ use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::job::{self, job_add, job_kill_action};
use crate::e2ee;
use crate::job;
use crate::login_param::{CertificateChecks, LoginParam};
use crate::oauth2::*;
use crate::param::Params;
use crate::{chat, e2ee, provider};
use crate::message::Message;
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
@@ -32,21 +30,6 @@ macro_rules! progress {
};
}
// connect
pub fn configure(context: &Context) {
if context.has_ongoing() {
warn!(context, "There is already another ongoing process running.",);
return;
}
job_kill_action(context, job::Action::ConfigureImap);
job_add(context, job::Action::ConfigureImap, 0, Params::new(), 0);
}
/// Check if the context is already configured.
pub fn dc_is_configured(context: &Context) -> bool {
context.sql.get_raw_config_bool(context, "configured")
}
/*******************************************************************************
* Configure JOB
******************************************************************************/
@@ -437,77 +420,45 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
LoginParam::from_database(context, "configured_raw_").save_to_database(context, "");
}
if let Some(provider) = provider::get_provider_info(&param.addr) {
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(provider.after_login_hint.to_string());
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg)).is_err() {
warn!(context, "cannot add after_login_hint as core-provider-info");
}
}
}
context.free_ongoing();
progress!(context, if success { 1000 } else { 0 });
job::Status::Finished(Ok(()))
}
#[allow(clippy::unnecessary_unwrap)]
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
// XXX we don't have https://github.com/deltachat/provider-db APIs
// integrated yet but we'll already add nauta as a first use case, also
// showing what we need from provider-db in the future.
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&param.addr) {
match provider.status {
provider::Status::OK | provider::Status::PREPARATION => {
let imap = provider.get_imap_server();
let smtp = provider.get_smtp_server();
// clippy complains about these is_some()/unwrap() settings,
// however, rewriting the code to "if let" would make things less obvious,
// esp. if we allow more combinations of servers (pop, jmap).
// therefore, #[allow(clippy::unnecessary_unwrap)] is added above.
if imap.is_some() && smtp.is_some() {
let imap = imap.unwrap();
let smtp = smtp.unwrap();
if param.addr.ends_with("@nauta.cu") {
let mut p = LoginParam::new();
let mut p = LoginParam::new();
p.addr = param.addr.clone();
p.addr = param.addr.clone();
p.mail_server = "imap.nauta.cu".to_string();
p.mail_user = param.addr.clone();
p.mail_pw = param.mail_pw.clone();
p.mail_port = 143;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.mail_server = imap.hostname.to_string();
p.mail_user = imap.apply_username_pattern(param.addr.clone());
p.mail_port = imap.port as i32;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match imap.socket {
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
};
p.send_server = "smtp.nauta.cu".to_string();
p.send_user = param.addr.clone();
p.send_pw = param.mail_pw.clone();
p.send_port = 25;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags = DC_LP_AUTH_NORMAL as i32
| DC_LP_IMAP_SOCKET_STARTTLS as i32
| DC_LP_SMTP_SOCKET_STARTTLS as i32;
p.send_server = smtp.hostname.to_string();
p.send_user = smtp.apply_username_pattern(param.addr.clone());
p.send_port = smtp.port as i32;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match smtp.socket {
provider::Socket::STARTTLS => DC_LP_SMTP_SOCKET_STARTTLS as i32,
provider::Socket::SSL => DC_LP_SMTP_SOCKET_SSL as i32,
};
info!(context, "offline autoconfig found: {}", p);
return Some(p);
} else {
info!(context, "offline autoconfig found, but no servers defined");
return None;
}
}
provider::Status::BROKEN => {
info!(context, "offline autoconfig found, provider is broken");
return None;
}
}
info!(context, "found offline autoconfig: {}", p);
Some(p)
} else {
info!(context, "no offline autoconfig found");
None
}
info!(context, "no offline autoconfig found");
None
}
fn try_imap_connections(
+139 -26
View File
@@ -3,7 +3,13 @@
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Condvar, Mutex, RwLock,
};
use crossbeam::channel::select;
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
use crate::chat::*;
use crate::config::Config;
@@ -12,9 +18,9 @@ use crate::contact::*;
use crate::error::*;
use crate::events::Event;
use crate::imap::*;
use crate::job::*;
use crate::job;
use crate::job_thread::JobThread;
use crate::key::Key;
use crate::key::*;
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId};
@@ -32,7 +38,7 @@ use crate::sql::Sql;
/// * `data2` - Depends on the event parameter, see [Event].
pub type ContextCallback = dyn Fn(&Context, Event) -> () + Send + Sync;
#[derive(DebugStub)]
#[derive(Debug)]
pub struct Context {
/// Database file path
dbfile: PathBuf,
@@ -47,8 +53,6 @@ pub struct Context {
pub smtp: Arc<Mutex<Smtp>>,
pub smtp_state: Arc<(Mutex<SmtpState>, Condvar)>,
pub oauth2_critical: Arc<Mutex<()>>,
#[debug_stub = "Callback"]
cb: Box<ContextCallback>,
pub os_name: Option<String>,
pub cmdline_sel_chat_id: Arc<RwLock<ChatId>>,
pub bob: Arc<RwLock<BobStatus>>,
@@ -57,6 +61,13 @@ pub struct Context {
/// Mutex to avoid generating the key for the user more than once.
pub generating_key_mutex: Mutex<()>,
pub translated_stockstrings: RwLock<HashMap<usize, String>>,
event_sender: Sender<Event>,
event_receiver: Receiver<Event>,
shutdown_sender: Sender<()>,
shutdown_receiver: Receiver<()>,
is_running: AtomicBool,
}
#[derive(Debug, PartialEq, Eq)]
@@ -80,11 +91,19 @@ pub fn get_info() -> HashMap<&'static str, String> {
res
}
macro_rules! while_running {
($self:expr, $code:block) => {
if $self.is_running.load(Ordering::Relaxed) {
$code
} else {
break;
}
};
}
impl Context {
/// Creates new context.
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
pretty_env_logger::try_init_timed().ok();
pub fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -92,24 +111,22 @@ impl Context {
if !blobdir.exists() {
std::fs::create_dir_all(&blobdir)?;
}
Context::with_blobdir(cb, os_name, dbfile, blobdir)
Context::with_blobdir(os_name, dbfile, blobdir)
}
pub fn with_blobdir(
cb: Box<ContextCallback>,
os_name: String,
dbfile: PathBuf,
blobdir: PathBuf,
) -> Result<Context> {
pub fn with_blobdir(os_name: String, dbfile: PathBuf, blobdir: PathBuf) -> Result<Context> {
ensure!(
blobdir.is_dir(),
"Blobdir does not exist: {}",
blobdir.display()
);
let (event_sender, event_receiver) = unbounded();
let (shutdown_sender, shutdown_receiver) = bounded(0);
let ctx = Context {
blobdir,
dbfile,
cb,
os_name: Some(os_name),
running_state: Arc::new(RwLock::new(Default::default())),
sql: Sql::new(),
@@ -138,6 +155,11 @@ impl Context {
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
event_sender,
event_receiver,
shutdown_sender,
shutdown_receiver,
is_running: Default::default(),
};
ensure!(
@@ -159,7 +181,98 @@ impl Context {
}
pub fn call_cb(&self, event: Event) {
(*self.cb)(self, event);
self.event_sender.send(event).unwrap();
}
/// Start the run loop.
pub fn run<F>(&self, cb: F)
where
F: Fn(&Context, Event) -> () + Send + Sync,
{
if self.is_running.swap(true, Ordering::Relaxed) {
panic!("Run can only be called once");
}
crossbeam::scope(|s| {
let imap_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_inbox_jobs(self);
while_running!(self, {
job::perform_inbox_fetch(self);
while_running!(self, { job::perform_inbox_idle(self) });
});
});
});
let mvbox_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_mvbox_fetch(self);
while_running!(self, {
job::perform_mvbox_idle(self);
});
});
});
let sentbox_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_sentbox_fetch(self);
while_running!(self, {
job::perform_sentbox_idle(self);
});
});
});
let smtp_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_smtp_jobs(self);
while_running!(self, {
job::perform_smtp_idle(self);
});
});
});
loop {
select! {
recv(self.event_receiver) -> event => {
// This gurantees that the callback is always called from the thread
// that called `run`.
cb(self, event.unwrap())
},
recv(self.shutdown_receiver) -> _ => break,
}
}
imap_handle.join().unwrap();
mvbox_handle.join().unwrap();
sentbox_handle.join().unwrap();
smtp_handle.join().unwrap();
})
.unwrap()
}
/// Stop the run loop. Blocks until all threads have shutdown.
pub fn shutdown(&self) {
if !self.is_running.swap(false, Ordering::Relaxed) {
// already shutdown
return;
}
self.shutdown_sender.send(()).unwrap();
job::interrupt_inbox_idle(self);
job::interrupt_mvbox_idle(self);
job::interrupt_sentbox_idle(self);
job::interrupt_smtp_idle(self);
}
pub fn configure(&self) {
if self.has_ongoing() {
warn!(self, "There is already another ongoing process running.");
return;
}
job::job_kill_action(self, job::Action::ConfigureImap);
job::job_add(self, job::Action::ConfigureImap, 0, Params::new(), 0);
}
/// Check if the context is already configured.
pub fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured")
}
/*******************************************************************************
@@ -441,9 +554,9 @@ impl Context {
match msg.is_dc_message {
MessengerMessage::No => {}
MessengerMessage::Yes | MessengerMessage::Reply => {
job_add(
job::job_add(
self,
Action::MoveMsg,
job::Action::MoveMsg,
msg.id.to_u32() as i32,
Params::new(),
0,
@@ -521,7 +634,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new(Box::new(|_, _| ()), "FakeOs".into(), dbfile);
let res = Context::new("FakeOs".into(), dbfile);
assert!(res.is_err());
}
@@ -536,7 +649,7 @@ mod tests {
fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
Context::new("FakeOS".into(), dbfile).unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
@@ -547,7 +660,7 @@ mod tests {
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile);
let res = Context::new("FakeOS".into(), dbfile);
assert!(res.is_err());
}
@@ -557,7 +670,7 @@ mod tests {
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
Context::new("FakeOS".into(), dbfile).unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
@@ -567,7 +680,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir("FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}
@@ -576,7 +689,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir("FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}
+56 -66
View File
@@ -74,12 +74,18 @@ pub fn dc_receive_imf(
// helper method to handle early exit and memory cleanup
let cleanup = |context: &Context,
create_event_to_send: &Option<CreateEvent>,
created_db_entries: Vec<(ChatId, MsgId)>| {
created_db_entries: &Vec<(ChatId, MsgId)>| {
if let Some(create_event_to_send) = create_event_to_send {
for (chat_id, msg_id) in created_db_entries {
let event = match create_event_to_send {
CreateEvent::MsgsChanged => Event::MsgsChanged { msg_id, chat_id },
CreateEvent::IncomingMsg => Event::IncomingMsg { msg_id, chat_id },
CreateEvent::MsgsChanged => Event::MsgsChanged {
msg_id: *msg_id,
chat_id: *chat_id,
},
CreateEvent::IncomingMsg => Event::IncomingMsg {
msg_id: *msg_id,
chat_id: *chat_id,
},
};
context.call_cb(event);
}
@@ -94,15 +100,41 @@ pub fn dc_receive_imf(
// 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)
// or if From: is equal to SELF (in this case, it is any outgoing messages,
// we do not check Return-Path any more as this is unreliable, see
// https://github.com/deltachat/deltachat-core/issues/150)
let (from_id, from_id_blocked, incoming_origin) =
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
from_field_to_contact_id(context, field_from)?
// we do not check Return-Path any more as this is unreliable, see issue #150)
let mut from_id = 0;
let mut from_id_blocked = false;
let mut incoming = true;
let mut incoming_origin = Origin::Unknown;
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
let from_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&field_from,
Origin::IncomingUnknownFrom,
)?;
if from_ids.contains(&DC_CONTACT_ID_SELF) {
incoming = false;
from_id = DC_CONTACT_ID_SELF;
incoming_origin = Origin::OutgoingBcc;
} else if !from_ids.is_empty() {
if from_ids.len() > 1 {
warn!(
context,
"mail has more than one From address, only using first: {:?}", field_from
);
}
from_id = from_ids.get_index(0).cloned().unwrap_or_default();
if let Ok(contact) = Contact::load_from_db(context, from_id) {
incoming_origin = contact.origin;
from_id_blocked = contact.blocked;
}
} else {
(0, false, Origin::Unknown)
};
let incoming = from_id != DC_CONTACT_ID_SELF;
warn!(context, "mail has an empty From header: {:?}", field_from);
// if there is no from given, from_id stays 0 which is just fine. These messages
// are very rare, however, we have to add them to the database (they go to the
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
}
}
let mut to_ids = ContactIds::new();
for header_def in &[HeaderDef::To, HeaderDef::Cc] {
@@ -159,7 +191,7 @@ pub fn dc_receive_imf(
&mut created_db_entries,
&mut create_event_to_send,
) {
cleanup(context, &create_event_to_send, created_db_entries);
cleanup(context, &create_event_to_send, &created_db_entries);
bail!("add_parts error: {:?}", err);
}
} else {
@@ -210,54 +242,13 @@ pub fn dc_receive_imf(
"received message {} has Message-Id: {}", server_uid, rfc724_mid
);
cleanup(context, &create_event_to_send, created_db_entries);
cleanup(context, &create_event_to_send, &created_db_entries);
mime_parser.handle_reports(context, from_id, sent_timestamp, &server_folder, server_uid);
mime_parser.handle_reports(from_id, sent_timestamp, &server_folder, server_uid);
Ok(())
}
/// Converts "From" field to contact id.
///
/// Also returns whether it is blocked or not and its origin.
pub fn from_field_to_contact_id(
context: &Context,
field_from: &str,
) -> Result<(u32, bool, Origin)> {
let from_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&field_from,
Origin::IncomingUnknownFrom,
)?;
if from_ids.contains(&DC_CONTACT_ID_SELF) {
Ok((DC_CONTACT_ID_SELF, false, Origin::OutgoingBcc))
} else if !from_ids.is_empty() {
if from_ids.len() > 1 {
warn!(
context,
"mail has more than one From address, only using first: {:?}", field_from
);
}
let from_id = from_ids.get_index(0).cloned().unwrap_or_default();
let mut from_id_blocked = false;
let mut incoming_origin = Origin::Unknown;
if let Ok(contact) = Contact::load_from_db(context, from_id) {
from_id_blocked = contact.blocked;
incoming_origin = contact.origin;
}
Ok((from_id, from_id_blocked, incoming_origin))
} else {
warn!(context, "mail has an empty From header: {:?}", field_from);
// if there is no from given, from_id stays 0 which is just fine. These messages
// are very rare, however, we have to add them to the database (they go to the
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
Ok((0, false, Origin::Unknown))
}
}
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
fn add_parts(
context: &Context,
@@ -308,7 +299,8 @@ fn add_parts(
} else {
MessengerMessage::No
};
// incoming non-chat messages may be discarded
// incoming non-chat messages may be discarded;
// maybe this can be optimized later, by checking the state before the message body is downloaded
let mut allow_creation = true;
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
@@ -316,13 +308,11 @@ fn add_parts(
&& msgrmsg == MessengerMessage::No
{
// this message is a classic email not a chat-message nor a reply to one
match show_emails {
ShowEmails::Off => {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
allow_creation = false;
}
ShowEmails::AcceptedContacts => allow_creation = false,
ShowEmails::All => {}
if show_emails == ShowEmails::Off {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
allow_creation = false
} else if show_emails == ShowEmails::AcceptedContacts {
allow_creation = false
}
}
@@ -518,7 +508,7 @@ fn add_parts(
if chat_id.is_unset() && self_sent {
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
// maybe an Autocrypt Setup Message
// maybe an Autocrypt Setup Messag
let (id, bl) =
chat::create_or_lookup_by_contact_id(context, DC_CONTACT_ID_SELF, Blocked::Not)
.unwrap_or_default();
@@ -1064,7 +1054,7 @@ fn create_or_lookup_group(
}
/// try extract a grpid from a message-id list header value
fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str> {
fn extract_grpid<'a>(mime_parser: &'a MimeMessage, headerdef: HeaderDef) -> Option<&'a str> {
let header = mime_parser.get(headerdef)?;
let parts = header
.split(',')
@@ -1519,7 +1509,7 @@ fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -
false
}
pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
if let Ok(ids) = mailparse::addrparse(mid_list) {
for id in ids.iter() {
if is_msgrmsg_rfc724_mid(context, id) {
+2 -8
View File
@@ -460,7 +460,7 @@ pub(crate) fn time() -> i64 {
///
/// ```
/// use deltachat::dc_tools::EmailAddress;
/// let email = match EmailAddress::new("someone@example.com") {
/// let email = match "someone@example.com".parse::<EmailAddress>() {
/// Ok(addr) => addr,
/// Err(e) => panic!("Error parsing address, error was {}", e),
/// };
@@ -468,18 +468,12 @@ pub(crate) fn time() -> i64 {
/// assert_eq!(&email.domain, "example.com");
/// assert_eq!(email.to_string(), "someone@example.com");
/// ```
#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(Debug, PartialEq, Clone)]
pub struct EmailAddress {
pub local: String,
pub domain: String,
}
impl EmailAddress {
pub fn new(input: &str) -> Result<Self, Error> {
input.parse::<EmailAddress>()
}
}
impl fmt::Display for EmailAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}@{}", self.local, self.domain)
+31 -25
View File
@@ -1,18 +1,15 @@
//! End-to-end encryption support.
use std::collections::HashSet;
use std::convert::TryFrom;
use mailparse::ParsedMail;
use mailparse::{MailHeaderMap, ParsedMail};
use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::EmailAddress;
use crate::error::*;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{self, Key, KeyPairUse, SignedPublicKey};
use crate::key::*;
use crate::keyring::*;
use crate::peerstate::*;
use crate::pgp;
@@ -22,7 +19,7 @@ use crate::securejoin::handle_degrade_event;
pub struct EncryptHelper {
pub prefer_encrypt: EncryptPreference,
pub addr: String,
pub public_key: SignedPublicKey,
pub public_key: Key,
}
impl EncryptHelper {
@@ -105,8 +102,8 @@ impl EncryptHelper {
})?;
keyring.add_ref(key);
}
let public_key = Key::from(self.public_key.clone());
keyring.add_ref(&public_key);
keyring.add_ref(&self.public_key);
let sign_key = Key::from_self_private(context, self.addr.clone(), &context.sql)
.ok_or_else(|| format_err!("missing own private key"))?;
@@ -125,7 +122,7 @@ pub fn try_decrypt(
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
let from = mail
.headers
.get_header_value(HeaderDef::From_)?
.get_first_value("From")?
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
@@ -194,20 +191,15 @@ pub fn try_decrypt(
/// storing a new one when one doesn't exist yet. Care is taken to
/// only generate one key per context even when multiple threads call
/// this function concurrently.
fn load_or_generate_self_public_key(
context: &Context,
self_addr: impl AsRef<str>,
) -> Result<SignedPublicKey> {
fn load_or_generate_self_public_key(context: &Context, self_addr: impl AsRef<str>) -> Result<Key> {
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
return SignedPublicKey::try_from(key)
.map_err(|_| Error::Message("Not a public key".into()));
return Ok(key);
}
let _guard = context.generating_key_mutex.lock().unwrap();
// Check again in case the key was generated while we were waiting for the lock.
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
return SignedPublicKey::try_from(key)
.map_err(|_| Error::Message("Not a public key".into()));
return Ok(key);
}
let start = std::time::Instant::now();
@@ -215,14 +207,28 @@ fn load_or_generate_self_public_key(
context,
"Generating keypair with {} bits, e={} ...", 2048, 65537,
);
let keypair = pgp::create_keypair(EmailAddress::new(self_addr.as_ref())?)?;
key::store_self_keypair(context, &keypair, KeyPairUse::Default)?;
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
);
Ok(keypair.public)
match pgp::create_keypair(&self_addr) {
Some((public_key, private_key)) => {
if dc_key_save_self_keypair(
context,
&public_key,
&private_key,
&self_addr,
true,
&context.sql,
) {
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
);
Ok(public_key)
} else {
Err(format_err!("Failed to save keypair"))
}
}
None => Err(format_err!("Failed to generate keypair")),
}
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
-15
View File
@@ -16,9 +16,6 @@ pub enum Error {
#[fail(display = "{:?}", _0)]
Message(String),
#[fail(display = "{:?}", _0)]
MessageWithCause(String, #[cause] failure::Error, failure::Backtrace),
#[fail(display = "{:?}", _0)]
Image(image_meta::ImageError),
@@ -121,18 +118,6 @@ impl From<crate::message::InvalidMsgId> for Error {
}
}
impl From<crate::key::SaveKeyError> for Error {
fn from(err: crate::key::SaveKeyError) -> Error {
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
}
}
impl From<crate::pgp::PgpKeygenError> for Error {
fn from(err: crate::pgp::PgpKeygenError) -> Error {
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)
+5 -40
View File
@@ -1,7 +1,4 @@
use crate::strum::AsStaticRef;
use mailparse::{MailHeader, MailHeaderMap, MailParseError};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames)]
#[strum(serialize_all = "kebab_case")]
#[allow(dead_code)]
pub enum HeaderDef {
@@ -35,7 +32,6 @@ pub enum HeaderDef {
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
SecureJoinGroup,
@@ -47,18 +43,8 @@ pub enum HeaderDef {
impl HeaderDef {
/// Returns the corresponding Event id.
pub fn get_headername(&self) -> &'static str {
self.as_static()
}
}
pub trait HeaderDefMap {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError>;
}
impl HeaderDefMap for [MailHeader<'_>] {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError> {
self.get_first_value(headerdef.get_headername())
pub fn get_headername(&self) -> String {
self.to_string()
}
}
@@ -69,29 +55,8 @@ mod tests {
#[test]
/// Test that kebab_case serialization works as expected
fn kebab_test() {
assert_eq!(HeaderDef::From_.get_headername(), "from");
assert_eq!(HeaderDef::From_.to_string(), "from");
assert_eq!(HeaderDef::_TestHeader.get_headername(), "test-header");
}
#[test]
/// Test that headers are parsed case-insensitively
fn test_get_header_value_case() {
let (headers, _) =
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
assert_eq!(
headers
.get_header_value(HeaderDef::AutocryptSetupMessage)
.unwrap(),
Some("v99".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::From_).unwrap(),
Some("Bob".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::Autocrypt).unwrap(),
None
);
assert_eq!(HeaderDef::_TestHeader.to_string(), "test-header");
}
}
+52 -171
View File
@@ -5,8 +5,6 @@
use std::sync::atomic::{AtomicBool, Ordering};
use num_traits::FromPrimitive;
use async_imap::{
error::Result as ImapResult,
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
@@ -14,14 +12,10 @@ use async_imap::{
use async_std::sync::{Mutex, RwLock};
use async_std::task;
use crate::config::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_receive_imf::{
dc_receive_imf, from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list,
};
use crate::dc_receive_imf::dc_receive_imf;
use crate::events::Event;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap_client::*;
use crate::job::{job_add, Action};
use crate::login_param::{CertificateChecks, LoginParam};
@@ -69,9 +63,6 @@ pub enum Error {
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[fail(display = "Mail parse error")]
MailParseError(#[cause] mailparse::MailParseError),
#[fail(display = "No mailbox selected, folder: {:?}", _0)]
NoMailbox(String),
@@ -103,12 +94,6 @@ impl From<select_folder::Error> for Error {
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
pub enum ImapActionResult {
Failed,
@@ -117,20 +102,7 @@ pub enum ImapActionResult {
Success,
}
/// Prefetch:
/// - Message-ID to check if we already have the message.
/// - In-Reply-To and References to check if message is a reply to chat message.
/// - Chat-Version to check if a message is a chat message
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
/// not necessarily sent by Delta Chat.
const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
FROM \
IN-REPLY-TO REFERENCES \
CHAT-VERSION \
AUTOCRYPT-SETUP-MESSAGE\
)])";
const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
const PREFETCH_FLAGS: &str = "(UID ENVELOPE)";
const JUST_UID: &str = "(UID)";
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
const SELECT_ALL: &str = "1:*";
@@ -182,6 +154,7 @@ struct ImapConfig {
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
pub can_idle: bool,
pub has_xlist: bool,
pub imap_delimiter: char,
}
@@ -199,6 +172,7 @@ impl Default for ImapConfig {
selected_mailbox: None,
selected_folder_needs_expunge: false,
can_idle: false,
has_xlist: false,
imap_delimiter: '.',
}
}
@@ -352,6 +326,7 @@ impl Imap {
cfg.imap_port = 0;
cfg.can_idle = false;
cfg.has_xlist = false;
}
/// Connects to imap account using already-configured parameters.
@@ -412,6 +387,7 @@ impl Imap {
true
} else {
let can_idle = caps.has_str("IDLE");
let has_xlist = caps.has_str("XLIST");
let caps_list = caps.iter().fold(String::new(), |s, c| {
if let Capability::Atom(x) = c {
s + &format!(" {}", x)
@@ -421,6 +397,7 @@ impl Imap {
});
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
@@ -578,9 +555,6 @@ impl Imap {
context: &Context,
folder: S,
) -> Result<bool> {
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
let (uid_validity, last_seen_uid) =
self.select_with_uidvalidity(context, folder.as_ref())?;
@@ -606,16 +580,11 @@ impl Imap {
list.sort_unstable_by_key(|msg| msg.uid.unwrap_or_default());
for fetch in &list {
let cur_uid = fetch.uid.unwrap_or_default();
for msg in &list {
let cur_uid = msg.uid.unwrap_or_default();
if cur_uid <= last_seen_uid {
// If the mailbox is not empty, results always include
// at least one UID, even if last_seen_uid+1 is past
// the last UID in the mailbox. It happens because
// uid+1:* is interpreted the same way as *:uid+1.
// See https://tools.ietf.org/html/rfc3501#page-61 for
// standard reference. Therefore, sometimes we receive
// already seen messages and have to filter them out.
// seems that at least dovecot sends the last available UID
// even if we asked for higher UID+N:*
info!(
context,
"fetch_new_messages: ignoring uid {}, last seen was {}", cur_uid, last_seen_uid
@@ -624,9 +593,20 @@ impl Imap {
}
read_cnt += 1;
let headers = get_fetch_headers(fetch)?;
let message_id = prefetch_get_message_id(&headers).unwrap_or_default();
if precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
let message_id = prefetch_get_message_id(msg).unwrap_or_default();
if !precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
// check passed, go fetch the rest
if self.fetch_single_msg(context, &folder, cur_uid).await == 0 {
info!(
context,
"Read error for message {} from \"{}\", trying over later.",
message_id,
folder.as_ref()
);
read_errors += 1;
}
} else {
// we know the message-id already or don't want the message otherwise.
info!(
context,
@@ -634,34 +614,6 @@ impl Imap {
message_id,
folder.as_ref(),
);
} else {
let show = prefetch_should_download(context, &headers, show_emails)
.map_err(|err| {
warn!(context, "prefetch_should_download error: {}", err);
err
})
.unwrap_or(true);
if !show {
info!(
context,
"Ignoring new message {} from \"{}\".",
message_id,
folder.as_ref(),
);
} else {
// check passed, go fetch the rest
if let Err(err) = self.fetch_single_msg(context, &folder, cur_uid).await {
info!(
context,
"Read error for message {} from \"{}\", trying over later: {}.",
message_id,
folder.as_ref(),
err
);
read_errors += 1;
}
}
}
if read_errors == 0 {
new_last_seen_uid = cur_uid;
@@ -705,19 +657,17 @@ impl Imap {
context.sql.set_raw_config(context, &key, Some(&val)).ok();
}
/// Fetches a single message by server UID.
///
/// If it succeeds, the message should be treated as received even
/// if no database entries are created. If the function returns an
/// error, the caller should try again later.
async fn fetch_single_msg<S: AsRef<str>>(
&self,
context: &Context,
folder: S,
server_uid: u32,
) -> Result<()> {
) -> usize {
// the function returns:
// 0 the caller should try over again later
// or 1 if the messages should be treated as received, the caller should not try to read the message again (even if no database entries are returned)
if !self.is_connected().await {
return Err(Error::Other("Not connected".to_string()));
return 0;
}
let set = format!("{}", server_uid);
@@ -736,13 +686,13 @@ impl Imap {
folder.as_ref(),
err
);
return Err(Error::FetchFailed(err));
return 0;
}
}
} else {
// we could not get a valid imap session, this should be retried
self.trigger_reconnect();
return Err(Error::Other("Could not get IMAP session".to_string()));
return 0;
};
if msgs.is_empty() {
@@ -783,7 +733,7 @@ impl Imap {
}
}
Ok(())
1
}
pub fn mv(
@@ -847,23 +797,9 @@ impl Imap {
Ok(_) => {
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete FAILED)",
display_folder_id, dest_folder
))
);
ImapActionResult::Failed
} else {
self.config.write().await.selected_folder_needs_expunge = true;
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete successfull)",
display_folder_id, dest_folder
))
);
ImapActionResult::Success
}
}
@@ -999,11 +935,9 @@ impl Imap {
// double-check that we are deleting the correct message-id
// this comes at the expense of another imap query
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_fetch(set, DELETE_CHECK_FLAGS).await {
match session.uid_fetch(set, PREFETCH_FLAGS).await {
Ok(msgs) => {
let fetch = if let Some(fetch) = msgs.first() {
fetch
} else {
if msgs.is_empty() {
warn!(
context,
"Cannot delete on IMAP, {}: imap entry gone '{}'",
@@ -1011,11 +945,9 @@ impl Imap {
message_id,
);
return ImapActionResult::Failed;
};
let remote_message_id = get_fetch_headers(fetch)
.and_then(|headers| prefetch_get_message_id(&headers))
.unwrap_or_default();
}
let remote_message_id =
prefetch_get_message_id(msgs.first().unwrap()).unwrap_or_default();
if remote_message_id != message_id {
warn!(
@@ -1167,6 +1099,7 @@ impl Imap {
}
async fn list_folders(&self, session: &mut Session, context: &Context) -> Option<Vec<Name>> {
// TODO: use xlist when available
match session.list(Some(""), Some("*")).await {
Ok(list) => {
if list.is_empty() {
@@ -1295,7 +1228,8 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
}
}
fn parse_message_id(value: &str) -> crate::error::Result<String> {
fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> {
let value = std::str::from_utf8(message_id)?;
let addrs = mailparse::addrparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
@@ -1306,72 +1240,19 @@ fn parse_message_id(value: &str) -> crate::error::Result<String> {
bail!("could not parse message_id: {}", value);
}
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
let header_bytes = match prefetch_msg.header() {
Some(header_bytes) => header_bytes,
None => return Ok(Vec::new()),
};
let (headers, _) = mailparse::parse_headers(header_bytes)?;
Ok(headers)
}
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId)? {
Ok(parse_message_id(&message_id)?)
} else {
Err(Error::Other("prefetch: No message ID found".to_string()))
}
}
fn prefetch_is_reply_to_chat_message(
context: &Context,
headers: &[mailparse::MailHeader],
) -> Result<bool> {
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo)? {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
}
fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
if prefetch_msg.envelope().is_none() {
return Err(Error::Other(
"prefectch: message has no envelope".to_string(),
));
}
if let Some(value) = headers.get_header_value(HeaderDef::References)? {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
}
let message_id = prefetch_msg.envelope().unwrap().message_id;
if message_id.is_none() {
return Err(Error::Other("prefetch: No message ID found".to_string()));
}
Ok(false)
}
fn prefetch_should_download(
context: &Context,
headers: &[mailparse::MailHeader],
show_emails: ShowEmails,
) -> Result<bool> {
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion)?.is_some();
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers)?;
// Autocrypt Setup Message should be shown even if it is from non-chat client.
let is_autocrypt_setup_message = headers
.get_header_value(HeaderDef::AutocryptSetupMessage)?
.is_some();
let from_field = headers
.get_header_value(HeaderDef::From_)?
.unwrap_or_default();
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(context, &from_field)?;
let accepted_contact = origin.is_known();
let show = is_autocrypt_setup_message
|| match show_emails {
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
ShowEmails::AcceptedContacts => {
is_chat_message || is_reply_to_chat_message || accepted_contact
}
ShowEmails::All => true,
};
let show = show && !blocked_contact;
Ok(show)
parse_message_id(&message_id.unwrap()).map_err(Into::into)
}
#[cfg(test)]
@@ -1381,11 +1262,11 @@ mod tests {
#[test]
fn test_parse_message_id() {
assert_eq!(
parse_message_id("Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
assert_eq!(
parse_message_id("<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
}
+31 -21
View File
@@ -10,7 +10,6 @@ use crate::blob::BlobObject;
use crate::chat;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::config::Config;
use crate::configure::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
@@ -18,7 +17,7 @@ use crate::e2ee;
use crate::error::*;
use crate::events::Event;
use crate::job::*;
use crate::key::{self, Key};
use crate::key::*;
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
@@ -289,6 +288,7 @@ fn set_self_key(
.and_then(|(k, h)| k.split_key().map(|pub_key| (k, pub_key, h)));
ensure!(keys.is_some(), "Not a valid private key");
let (private_key, public_key, header) = keys.unwrap();
let preferencrypt = header.get("Autocrypt-Prefer-Encrypt");
match preferencrypt.map(|s| s.as_str()) {
@@ -313,26 +313,34 @@ fn set_self_key(
let self_addr = context.get_config(Config::ConfiguredAddr);
ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
let (public, secret) = match (public_key, private_key) {
(Key::Public(p), Key::Secret(s)) => (p, s),
_ => bail!("wrong keys unpacked"),
};
let keypair = pgp::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(
// XXX maybe better make dc_key_save_self_keypair delete things
sql::execute(
context,
&keypair,
if set_default {
key::KeyPairUse::Default
} else {
key::KeyPairUse::ReadOnly
},
&context.sql,
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key.to_bytes(), private_key.to_bytes()],
)?;
if set_default {
sql::execute(
context,
&context.sql,
"UPDATE keypairs SET is_default=0;",
params![],
)?;
}
if !dc_key_save_self_keypair(
context,
&public_key,
&private_key,
self_addr.unwrap_or_default(),
set_default,
&context.sql,
) {
bail!("Cannot save keypair, internal key-state possibly corrupted now!");
}
Ok(())
}
@@ -414,7 +422,7 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
);
ensure!(
!dc_is_configured(context),
!context.is_configured(),
"Cannot import backups to accounts in use."
);
context.sql.close(&context);
@@ -737,6 +745,7 @@ fn export_key_to_asc_file(
#[cfg(test)]
mod tests {
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::test_utils::*;
use ::pgp::armor::BlockType;
@@ -791,7 +800,8 @@ mod tests {
#[test]
fn test_export_key_to_asc_file() {
let context = dummy_context();
let key = Key::from(alice_keypair().public);
let base64 = include_str!("../test-data/key/public.asc");
let key = Key::from_base64(base64, KeyType::Public).unwrap();
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
+44 -61
View File
@@ -369,7 +369,7 @@ impl Job {
}
self.smtp_send(context, recipients, body, self.job_id, || {
// Remove additional SendMdn jobs we have aggregated into this one.
// Remove additional SendMdn jobs we have aggretated into this one.
job_kill_ids(context, &additional_job_ids)?;
Ok(())
})
@@ -553,7 +553,7 @@ pub fn job_kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
)
}
pub fn perform_inbox_fetch(context: &Context) {
pub(crate) fn perform_inbox_fetch(context: &Context) {
let use_network = context.get_config_bool(Config::InboxWatch);
task::block_on(
@@ -565,7 +565,7 @@ pub fn perform_inbox_fetch(context: &Context) {
);
}
pub fn perform_mvbox_fetch(context: &Context) {
pub(crate) fn perform_mvbox_fetch(context: &Context) {
let use_network = context.get_config_bool(Config::MvboxWatch);
task::block_on(
@@ -577,7 +577,7 @@ pub fn perform_mvbox_fetch(context: &Context) {
);
}
pub fn perform_sentbox_fetch(context: &Context) {
pub(crate) fn perform_sentbox_fetch(context: &Context) {
let use_network = context.get_config_bool(Config::SentboxWatch);
task::block_on(
@@ -589,7 +589,7 @@ pub fn perform_sentbox_fetch(context: &Context) {
);
}
pub fn perform_inbox_idle(context: &Context) {
pub(crate) fn perform_inbox_idle(context: &Context) {
if *context.perform_inbox_jobs_needed.clone().read().unwrap() {
info!(
context,
@@ -606,7 +606,7 @@ pub fn perform_inbox_idle(context: &Context) {
.idle(context, use_network);
}
pub fn perform_mvbox_idle(context: &Context) {
pub(crate) fn perform_mvbox_idle(context: &Context) {
let use_network = context.get_config_bool(Config::MvboxWatch);
context
@@ -616,7 +616,7 @@ pub fn perform_mvbox_idle(context: &Context) {
.idle(context, use_network);
}
pub fn perform_sentbox_idle(context: &Context) {
pub(crate) fn perform_sentbox_idle(context: &Context) {
let use_network = context.get_config_bool(Config::SentboxWatch);
context
@@ -626,7 +626,7 @@ pub fn perform_sentbox_idle(context: &Context) {
.idle(context, use_network);
}
pub fn interrupt_inbox_idle(context: &Context) {
pub(crate) fn interrupt_inbox_idle(context: &Context) {
info!(context, "interrupt_inbox_idle called");
// we do not block on trying to obtain the thread lock
// because we don't know in which state the thread is.
@@ -643,11 +643,11 @@ pub fn interrupt_inbox_idle(context: &Context) {
}
}
pub fn interrupt_mvbox_idle(context: &Context) {
pub(crate) fn interrupt_mvbox_idle(context: &Context) {
context.mvbox_thread.read().unwrap().interrupt_idle(context);
}
pub fn interrupt_sentbox_idle(context: &Context) {
pub(crate) fn interrupt_sentbox_idle(context: &Context) {
context
.sentbox_thread
.read()
@@ -655,7 +655,7 @@ pub fn interrupt_sentbox_idle(context: &Context) {
.interrupt_idle(context);
}
pub fn perform_smtp_jobs(context: &Context) {
pub(crate) fn perform_smtp_jobs(context: &Context) {
let probe_smtp_network = {
let &(ref lock, _) = &*context.smtp_state.clone();
let mut state = lock.lock().unwrap();
@@ -684,7 +684,7 @@ pub fn perform_smtp_jobs(context: &Context) {
}
}
pub fn perform_smtp_idle(context: &Context) {
pub(crate) fn perform_smtp_idle(context: &Context) {
info!(context, "SMTP-idle started...",);
{
let &(ref lock, ref cvar) = &*context.smtp_state.clone();
@@ -792,32 +792,7 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
};
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar)?;
let mut recipients = mimefactory.recipients();
let from = context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
let lowercase_from = from.to_lowercase();
if context.get_config_bool(Config::BccSelf)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
recipients.push(from);
}
if recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id);
return Ok(());
}
let rendered_msg = mimefactory.render().map_err(|err| {
let mut rendered_msg = mimefactory.render().map_err(|err| {
message::set_msg_failed(context, msg_id, Some(err.to_string()));
err
})?;
@@ -836,6 +811,26 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
);
}
let lowercase_from = rendered_msg.from.to_lowercase();
if context.get_config_bool(Config::BccSelf)
&& !rendered_msg
.recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
rendered_msg.recipients.push(rendered_msg.from.clone());
}
if rendered_msg.recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id);
return Ok(());
}
if rendered_msg.is_gossiped {
chat::set_gossiped_timestamp(context, msg.chat_id, time())?;
}
@@ -864,18 +859,12 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
msg.save_param_to_disk(context);
}
add_smtp_job(
context,
Action::SendMsgToSmtp,
msg.id,
recipients,
&rendered_msg,
)?;
add_smtp_job(context, Action::SendMsgToSmtp, msg.id, &rendered_msg)?;
Ok(())
}
pub fn perform_inbox_jobs(context: &Context) {
pub(crate) fn perform_inbox_jobs(context: &Context) {
info!(context, "dc_perform_inbox_jobs starting.",);
let probe_imap_network = *context.probe_imap_network.clone().read().unwrap();
@@ -886,22 +875,14 @@ pub fn perform_inbox_jobs(context: &Context) {
info!(context, "dc_perform_inbox_jobs ended.",);
}
pub fn perform_mvbox_jobs(context: &Context) {
info!(context, "dc_perform_mbox_jobs EMPTY (for now).",);
}
pub fn perform_sentbox_jobs(context: &Context) {
info!(context, "dc_perform_sentbox_jobs EMPTY (for now).",);
}
fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
while let Some(mut job) = load_next_job(context, thread, probe_network) {
info!(context, "{}-job {} started...", thread, job);
// some configuration jobs are "exclusive":
// - they are always executed in the imap-thread and the smtp-thread is suspended during execution
// - they may change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not saved in the database for later execution
// - they may change the database handle change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not save in the database for later execution
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
job_kill_action(context, job.action);
context
@@ -1091,15 +1072,17 @@ fn add_smtp_job(
context: &Context,
action: Action,
msg_id: MsgId,
recipients: Vec<String>,
rendered_msg: &RenderedEmail,
) -> Result<()> {
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
ensure!(
!rendered_msg.recipients.is_empty(),
"no recipients for smtp job set"
);
let mut param = Params::new();
let bytes = &rendered_msg.message;
let blob = BlobObject::create(context, &rendered_msg.rfc724_mid, bytes)?;
let recipients = recipients.join("\x1e");
let recipients = rendered_msg.recipients.join("\x1e");
param.set(Param::File, blob.as_name());
param.set(Param::Recipients, &recipients);
@@ -1146,7 +1129,7 @@ pub fn job_add(
}
}
pub fn interrupt_smtp_idle(context: &Context) {
pub(crate) fn interrupt_smtp_idle(context: &Context) {
info!(context, "Interrupting SMTP-idle...",);
let &(ref lock, ref cvar) = &*context.smtp_state.clone();
@@ -1162,7 +1145,7 @@ pub fn interrupt_smtp_idle(context: &Context) {
///
/// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load
/// IMAP jobs. The `probe_network` parameter decides how to query
/// jobs, this is tricky and probably wrong currently. Look at the
/// jobs, this is tricky and probably wrong currently. Look at the
/// SQL queries for details.
fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Option<Job> {
let query = if !probe_network {
+1 -1
View File
@@ -143,7 +143,7 @@ impl JobThread {
if state.jobs_needed {
info!(
context,
"{}-IDLE will not be started as it was interrupted while not idling.",
"{}-IDLE will not be started as it was interrupted while not ideling.",
self.name,
);
state.jobs_needed = false;
+34 -227
View File
@@ -4,84 +4,14 @@ use std::collections::BTreeMap;
use std::io::Cursor;
use std::path::Path;
use pgp::composed::Deserializable;
use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::sql::Sql;
// Re-export key types
pub use crate::pgp::KeyPair;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
/// Error type for deltachat key handling.
#[derive(Fail, Debug)]
pub enum Error {
#[fail(display = "Could not decode base64")]
Base64Decode(#[cause] base64::DecodeError, failure::Backtrace),
#[fail(display = "rPGP error: {}", _0)]
PgpError(#[cause] pgp::errors::Error, failure::Backtrace),
}
impl From<base64::DecodeError> for Error {
fn from(err: base64::DecodeError) -> Error {
Error::Base64Decode(err, failure::Backtrace::new())
}
}
impl From<pgp::errors::Error> for Error {
fn from(err: pgp::errors::Error) -> Error {
Error::PgpError(err, failure::Backtrace::new())
}
}
pub type Result<T> = std::result::Result<T, Error>;
/// Convenience trait for working with keys.
///
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub trait DcKey: Serialize + Deserializable {
type KeyType: Serialize + Deserializable;
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self::KeyType> {
Ok(<Self::KeyType as Deserializable>::from_bytes(Cursor::new(
bytes,
))?)
}
/// Create a key from a base64 string.
fn from_base64(data: &str) -> Result<Self::KeyType> {
// strip newlines and other whitespace
let cleaned: String = data.trim().split_whitespace().collect();
let bytes = base64::decode(cleaned.as_bytes())?;
Self::from_slice(&bytes)
}
/// Serialise the key to a base64 string.
fn to_base64(&self) -> String {
// Not using Serialize::to_bytes() to make clear *why* it is
// safe to ignore this error.
// Because we write to a Vec<u8> the io::Write impls never
// fail and we can hide this error.
let mut buf = Vec::new();
self.to_writer(&mut buf).unwrap();
base64::encode(&buf)
}
}
impl DcKey for SignedPublicKey {
type KeyType = SignedPublicKey;
}
impl DcKey for SignedSecretKey {
type KeyType = SignedSecretKey;
}
use crate::sql::{self, Sql};
/// Cryptographic key
#[derive(Debug, PartialEq, Eq, Clone)]
@@ -105,7 +35,7 @@ impl From<SignedSecretKey> for Key {
impl std::convert::TryFrom<Key> for SignedSecretKey {
type Error = ();
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
fn try_from(value: Key) -> Result<Self, Self::Error> {
match value {
Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key),
@@ -116,7 +46,7 @@ impl std::convert::TryFrom<Key> for SignedSecretKey {
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
type Error = ();
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
fn try_from(value: &'a Key) -> Result<Self, Self::Error> {
match value {
Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key),
@@ -127,7 +57,7 @@ impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
impl std::convert::TryFrom<Key> for SignedPublicKey {
type Error = ();
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
fn try_from(value: Key) -> Result<Self, Self::Error> {
match value {
Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()),
@@ -138,7 +68,7 @@ impl std::convert::TryFrom<Key> for SignedPublicKey {
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedPublicKey {
type Error = ();
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
fn try_from(value: &'a Key) -> Result<Self, Self::Error> {
match value {
Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()),
@@ -162,7 +92,7 @@ impl Key {
if bytes.is_empty() {
return None;
}
let res: std::result::Result<Key, _> = match key_type {
let res: Result<Key, _> = match key_type {
KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes)).map(Into::into),
KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes)).map(Into::into),
};
@@ -181,7 +111,7 @@ impl Key {
key_type: KeyType,
) -> Option<(Self, BTreeMap<String, String>)> {
let bytes = data.as_bytes();
let res: std::result::Result<(Key, _), _> = match key_type {
let res: Result<(Key, _), _> = match key_type {
KeyType::Public => SignedPublicKey::from_armor_single(Cursor::new(bytes))
.map(|(k, h)| (Into::into(k), h)),
KeyType::Private => SignedSecretKey::from_armor_single(Cursor::new(bytes))
@@ -197,6 +127,15 @@ impl Key {
}
}
pub fn from_base64(encoded_data: &str, key_type: KeyType) -> Option<Self> {
// strip newlines and other whitespace
let cleaned: String = encoded_data.trim().split_whitespace().collect();
let bytes = cleaned.as_bytes();
base64::decode(bytes)
.ok()
.and_then(|decoded| Self::from_slice(&decoded, key_type))
}
pub fn from_self_public(
context: &Context,
self_addr: impl AsRef<str>,
@@ -303,97 +242,20 @@ impl Key {
}
}
/// Use of a [KeyPair] for encryption or decryption.
///
/// This is used by [store_self_keypair] to know what kind of key is
/// being saved.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum KeyPairUse {
/// The default key used to encrypt new messages.
Default,
/// Only used to decrypt existing message.
ReadOnly,
}
/// Error saving a keypair to the database.
#[derive(Fail, Debug)]
#[fail(display = "SaveKeyError: {}", message)]
pub struct SaveKeyError {
message: String,
#[cause]
cause: failure::Error,
backtrace: failure::Backtrace,
}
impl SaveKeyError {
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
Self {
message: message.into(),
cause: cause.into(),
backtrace: failure::Backtrace::new(),
}
}
}
/// Store the keypair as an owned keypair for addr in the database.
///
/// This will save the keypair as keys for the given address. The
/// "self" here refers to the fact that this DC instance owns the
/// keypair. Usually `addr` will be [Config::ConfiguredAddr].
///
/// If either the public or private keys are already present in the
/// database, this entry will be removed first regardless of the
/// address associated with it. Practically this means saving the
/// same key again overwrites it.
///
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
pub fn store_self_keypair(
pub fn dc_key_save_self_keypair(
context: &Context,
keypair: &KeyPair,
default: KeyPairUse,
) -> std::result::Result<(), SaveKeyError> {
// Everything should really be one transaction, more refactoring
// is needed for that.
let public_key = keypair
.public
.to_bytes()
.map_err(|err| SaveKeyError::new("failed to serialise public key", err))?;
let secret_key = keypair
.secret
.to_bytes()
.map_err(|err| SaveKeyError::new("failed to serialise secret key", err))?;
context
.sql
.execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key, secret_key],
)
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", params![])
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
let is_default = match default {
KeyPairUse::Default => true,
KeyPairUse::ReadOnly => false,
};
context
.sql
.execute(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
VALUES (?,?,?,?,?);",
params![
keypair.addr.to_string(),
is_default as i32,
public_key,
secret_key,
time()
],
)
.map(|_| ())
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))
public_key: &Key,
private_key: &Key,
addr: impl AsRef<str>,
is_default: bool,
sql: &Sql,
) -> bool {
sql::execute(
context,
sql,
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);",
params![addr.as_ref(), is_default as i32, public_key.to_bytes(), private_key.to_bytes(), time()],
).is_ok()
}
/// Make a fingerprint human-readable, in hex format.
@@ -425,14 +287,6 @@ pub fn dc_normalize_fingerprint(fp: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use std::convert::TryFrom;
use lazy_static::lazy_static;
lazy_static! {
static ref KEYPAIR: KeyPair = alice_keypair();
}
#[test]
fn test_normalize_fingerprint() {
@@ -519,9 +373,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[test]
#[ignore] // is too expensive
fn test_from_slice_roundtrip() {
let public_key = Key::from(KEYPAIR.public.clone());
let private_key = Key::from(KEYPAIR.secret.clone());
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap();
let binary = public_key.to_bytes();
let public_key2 = Key::from_slice(&binary, KeyType::Public).expect("invalid public key");
@@ -554,9 +408,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[test]
#[ignore] // is too expensive
fn test_ascii_roundtrip() {
let public_key = Key::from(KEYPAIR.public.clone());
let private_key = Key::from(KEYPAIR.secret.clone());
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap();
let s = public_key.to_armored_string(None).unwrap();
let (public_key2, _) =
@@ -569,51 +423,4 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
Key::from_armored_string(&s, KeyType::Private).expect("invalid private key");
assert_eq!(private_key, private_key2);
}
#[test]
fn test_split_key() {
let private_key = Key::from(KEYPAIR.secret.clone());
let public_wrapped = private_key.split_key().unwrap();
let public = SignedPublicKey::try_from(public_wrapped).unwrap();
assert_eq!(public.primary_key, KEYPAIR.public.primary_key);
}
#[test]
fn test_save_self_key_twice() {
// Saving the same key twice should result in only one row in
// the keypairs table.
let t = dummy_context();
let nrows = || {
t.ctx
.sql
.query_get_value::<_, u32>(&t.ctx, "SELECT COUNT(*) FROM keypairs;", params![])
.unwrap()
};
assert_eq!(nrows(), 0);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
assert_eq!(nrows(), 1);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
assert_eq!(nrows(), 1);
}
// Convenient way to create a new key if you need one, run with
// `cargo test key::tests::gen_key`.
// #[test]
// fn gen_key() {
// let name = "fiona";
// let keypair = crate::pgp::create_keypair(
// EmailAddress::new(&format!("{}@example.net", name)).unwrap(),
// )
// .unwrap();
// std::fs::write(
// format!("test-data/key/{}-public.asc", name),
// keypair.public.to_base64(),
// )
// .unwrap();
// std::fs::write(
// format!("test-data/key/{}-secret.asc", name),
// keypair.secret.to_base64(),
// )
// .unwrap();
// }
}
+2 -2
View File
@@ -1,8 +1,8 @@
use std::borrow::Cow;
use crate::constants::KeyType;
use crate::constants::*;
use crate::context::Context;
use crate::key::Key;
use crate::key::*;
use crate::sql::Sql;
#[derive(Default, Clone, Debug)]
-2
View File
@@ -1,4 +1,3 @@
#![forbid(unsafe_code)]
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
#![allow(clippy::match_bool)]
@@ -54,7 +53,6 @@ pub mod oauth2;
mod param;
pub mod peerstate;
pub mod pgp;
pub mod provider;
pub mod qr;
pub mod securejoin;
mod simplify;
-3
View File
@@ -86,9 +86,6 @@ pub enum LotState {
/// test1=formatted fingerprint
QrFprWithoutAddr = 230,
/// text1=domain
QrAccount = 250,
/// id=contact
QrAddr = 320,
+2 -1
View File
@@ -203,7 +203,8 @@ impl Message {
pub fn load_from_db(context: &Context, id: MsgId) -> Result<Message, Error> {
ensure!(
!id.is_special(),
"Can not load special message IDs from DB."
"Can not load special message IDs from DB. ({:?})",
id
);
context
.sql
+8 -8
View File
@@ -60,6 +60,9 @@ pub struct RenderedEmail {
pub is_gossiped: bool,
pub last_added_location_id: u32,
pub from: String,
pub recipients: Vec<String>,
/// Message ID (Message in the sense of Email)
pub rfc724_mid: String,
}
@@ -346,13 +349,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
pub fn recipients(&self) -> Vec<String> {
self.recipients
.iter()
.map(|(_, addr)| addr.clone())
.collect()
}
pub fn render(mut self) -> Result<RenderedEmail, Error> {
// Headers that are encrypted
// - Chat-*, except Chat-Version
@@ -569,6 +565,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
};
let MimeFactory {
recipients,
from_addr,
last_added_location_id,
..
} = self;
@@ -579,6 +577,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
is_encrypted,
is_gossiped,
last_added_location_id,
recipients: recipients.into_iter().map(|(_, addr)| addr).collect(),
from: from_addr,
rfc724_mid,
})
}
@@ -1120,7 +1120,7 @@ fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
fn render_rfc724_mid(rfc724_mid: &str) -> String {
let rfc724_mid = rfc724_mid.trim().to_string();
if rfc724_mid.chars().nth(0).unwrap_or_default() == '<' {
if rfc724_mid.chars().next().unwrap_or_default() == '<' {
rfc724_mid
} else {
format!("<{}>", rfc724_mid)
+59 -91
View File
@@ -15,7 +15,7 @@ use crate::dehtml::dehtml;
use crate::e2ee;
use crate::error::Result;
use crate::events::Event;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::headerdef::HeaderDef;
use crate::job::{job_add, Action};
use crate::location;
use crate::message;
@@ -36,7 +36,8 @@ use crate::{bail, ensure};
/// It is created by parsing the raw data of an actual MIME message
/// using the [MimeMessage::from_bytes] constructor.
#[derive(Debug)]
pub struct MimeMessage {
pub struct MimeMessage<'a> {
pub context: &'a Context,
pub parts: Vec<Part>,
header: HashMap<String, String>,
pub decrypting_failed: bool,
@@ -90,13 +91,13 @@ impl Default for SystemMessage {
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
impl MimeMessage {
pub fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
impl<'a> MimeMessage<'a> {
pub fn from_bytes(context: &'a Context, body: &[u8]) -> Result<Self> {
let mail = mailparse::parse_mail(body)?;
let message_time = mail
.headers
.get_header_value(HeaderDef::Date)?
.get_first_value("Date")?
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
@@ -159,6 +160,7 @@ impl MimeMessage {
signatures,
gossipped_addr,
is_forwarded: false,
context,
reports: Vec::new(),
is_system_message: SystemMessage::Unknown,
location_kml: None,
@@ -166,14 +168,14 @@ impl MimeMessage {
user_avatar: AvatarAction::None,
group_avatar: AvatarAction::None,
};
parser.parse_mime_recursive(context, &mail)?;
parser.parse_headers(context)?;
parser.parse_mime_recursive(&mail)?;
parser.parse_headers()?;
Ok(parser)
}
/// Parses system messages.
fn parse_system_message_headers(&mut self, context: &Context) -> Result<()> {
fn parse_system_message_headers(&mut self) -> Result<()> {
if self.get(HeaderDef::AutocryptSetupMessage).is_some() {
self.parts = self
.parts
@@ -188,7 +190,7 @@ impl MimeMessage {
if self.parts.len() == 1 {
self.is_system_message = SystemMessage::AutocryptSetupMessage;
} else {
warn!(context, "could not determine ASM mime-part");
warn!(self.context, "could not determine ASM mime-part");
}
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "location-streaming-enabled" {
@@ -282,8 +284,8 @@ impl MimeMessage {
}
}
fn parse_headers(&mut self, context: &Context) -> Result<()> {
self.parse_system_message_headers(context)?;
fn parse_headers(&mut self) -> Result<()> {
self.parse_system_message_headers()?;
self.parse_avatar_headers();
self.squash_attachment_parts();
@@ -328,9 +330,9 @@ impl MimeMessage {
// See if an MDN is requested from the other side
if !self.decrypting_failed && !self.parts.is_empty() {
if let Some(ref dn_to_addr) =
self.parse_first_addr(context, HeaderDef::ChatDispositionNotificationTo)
self.parse_first_addr(HeaderDef::ChatDispositionNotificationTo)
{
if let Some(ref from_addr) = self.parse_first_addr(context, HeaderDef::From_) {
if let Some(ref from_addr) = self.parse_first_addr(HeaderDef::From_) {
if compare_addrs(from_addr, dn_to_addr) {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
@@ -402,38 +404,34 @@ impl MimeMessage {
}
pub fn get(&self, headerdef: HeaderDef) -> Option<&String> {
self.header.get(headerdef.get_headername())
self.header.get(&headerdef.get_headername())
}
fn parse_first_addr(&self, context: &Context, headerdef: HeaderDef) -> Option<MailAddr> {
fn parse_first_addr(&self, headerdef: HeaderDef) -> Option<MailAddr> {
if let Some(value) = self.get(headerdef.clone()) {
match mailparse::addrparse(&value) {
Ok(ref addrs) => {
return addrs.first().cloned();
}
Err(err) => {
warn!(context, "header {} parse error: {:?}", headerdef, err);
warn!(self.context, "header {} parse error: {:?}", headerdef, err);
}
}
}
None
}
fn parse_mime_recursive(
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
) -> Result<bool> {
fn parse_mime_recursive(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
if mail.ctype.params.get("protected-headers").is_some() {
if mail.ctype.mimetype == "text/rfc822-headers" {
warn!(
context,
self.context,
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
);
return Ok(false);
}
warn!(context, "Ignoring nested protected headers");
warn!(self.context, "Ignoring nested protected headers");
}
enum MimeS {
@@ -461,7 +459,7 @@ impl MimeMessage {
};
match m {
MimeS::Multiple => self.handle_multiple(context, mail),
MimeS::Multiple => self.handle_multiple(mail),
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
@@ -469,17 +467,13 @@ impl MimeMessage {
}
let mail = mailparse::parse_mail(&raw).unwrap();
self.parse_mime_recursive(context, &mail)
self.parse_mime_recursive(&mail)
}
MimeS::Single => self.add_single_part_if_known(context, mail),
MimeS::Single => self.add_single_part_if_known(mail),
}
}
fn handle_multiple(
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
) -> Result<bool> {
fn handle_multiple(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
let mut any_part_added = false;
let mimetype = get_mime_type(mail)?.0;
match (mimetype.type_(), mimetype.subtype().as_str()) {
@@ -490,7 +484,7 @@ impl MimeMessage {
(mime::MULTIPART, "alternative") => {
for cur_data in &mail.subparts {
if get_mime_type(cur_data)?.0 == "multipart/mixed" {
any_part_added = self.parse_mime_recursive(context, cur_data)?;
any_part_added = self.parse_mime_recursive(cur_data)?;
break;
}
}
@@ -498,7 +492,7 @@ impl MimeMessage {
/* search for text/plain and add this */
for cur_data in &mail.subparts {
if get_mime_type(cur_data)?.0.type_() == mime::TEXT {
any_part_added = self.parse_mime_recursive(context, cur_data)?;
any_part_added = self.parse_mime_recursive(cur_data)?;
break;
}
}
@@ -506,7 +500,7 @@ impl MimeMessage {
if !any_part_added {
/* `text/plain` not found - use the first part */
for cur_part in &mail.subparts {
if self.parse_mime_recursive(context, cur_part)? {
if self.parse_mime_recursive(cur_part)? {
any_part_added = true;
break;
}
@@ -519,14 +513,14 @@ impl MimeMessage {
being the first one, which may not be always true ...
however, most times it seems okay. */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first)?;
any_part_added = self.parse_mime_recursive(first)?;
}
}
(mime::MULTIPART, "encrypted") => {
// we currently do not try to decrypt non-autocrypt messages
// at all. If we see an encrypted part, we set
// decrypting_failed.
let msg_body = context.stock_str(StockMessage::CantDecryptMsgBody);
let msg_body = self.context.stock_str(StockMessage::CantDecryptMsgBody);
let txt = format!("[{}]", msg_body);
let mut part = Part::default();
@@ -549,7 +543,7 @@ impl MimeMessage {
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
for background information why we use encrypted+signed) */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first)?;
any_part_added = self.parse_mime_recursive(first)?;
}
}
(mime::MULTIPART, "report") => {
@@ -557,14 +551,14 @@ impl MimeMessage {
if mail.subparts.len() >= 2 {
if let Some(report_type) = mail.ctype.params.get("report-type") {
if report_type == "disposition-notification" {
if let Some(report) = self.process_report(context, mail)? {
if let Some(report) = self.process_report(mail)? {
self.reports.push(report);
}
} else {
/* eg. `report-type=delivery-status`;
maybe we should show them as a little error icon */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first)?;
any_part_added = self.parse_mime_recursive(first)?;
}
}
}
@@ -574,7 +568,7 @@ impl MimeMessage {
// Add all parts (in fact, AddSinglePartIfKnown() later check if
// the parts are really supported)
for cur_data in mail.subparts.iter() {
if self.parse_mime_recursive(context, cur_data)? {
if self.parse_mime_recursive(cur_data)? {
any_part_added = true;
}
}
@@ -584,11 +578,7 @@ impl MimeMessage {
Ok(any_part_added)
}
fn add_single_part_if_known(
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
) -> Result<bool> {
fn add_single_part_if_known(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
// return true if a part was added
let (mime_type, msg_type) = get_mime_type(mail)?;
let raw_mime = mail.ctype.mimetype.to_lowercase();
@@ -599,7 +589,6 @@ impl MimeMessage {
if let Ok(filename) = filename {
self.do_add_single_file_part(
context,
msg_type,
mime_type,
&raw_mime,
@@ -615,7 +604,7 @@ impl MimeMessage {
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
Err(err) => {
warn!(context, "Invalid body parsed {:?}", err);
warn!(self.context, "Invalid body parsed {:?}", err);
// Note that it's not always an error - might be no data
return Ok(false);
}
@@ -656,7 +645,6 @@ impl MimeMessage {
fn do_add_single_file_part(
&mut self,
context: &Context,
msg_type: Viewtype,
mime_type: Mime,
raw_mime: &str,
@@ -671,9 +659,9 @@ impl MimeMessage {
// XXX what if somebody sends eg an "location-highlights.kml"
// attachment unrelated to location streaming?
if filename.starts_with("location") || filename.starts_with("message") {
let parsed = location::Kml::parse(context, decoded_data)
let parsed = location::Kml::parse(self.context, decoded_data)
.map_err(|err| {
warn!(context, "failed to parse kml part: {}", err);
warn!(self.context, "failed to parse kml part: {}", err);
})
.ok();
if filename.starts_with("location") {
@@ -687,17 +675,17 @@ impl MimeMessage {
/* we have a regular file attachment,
write decoded data to new blob object */
let blob = match BlobObject::create(context, filename, decoded_data) {
let blob = match BlobObject::create(self.context, filename, decoded_data) {
Ok(blob) => blob,
Err(err) => {
error!(
context,
self.context,
"Could not add blob for mime part {}, error {}", filename, err
);
return;
}
};
info!(context, "added blobfile: {:?}", blob.as_name());
info!(self.context, "added blobfile: {:?}", blob.as_name());
/* create and register Mime part referencing the new Blob object */
let mut part = Part::default();
@@ -771,29 +759,22 @@ impl MimeMessage {
}
}
fn process_report(
&self,
context: &Context,
report: &mailparse::ParsedMail<'_>,
) -> Result<Option<Report>> {
fn process_report(&self, report: &mailparse::ParsedMail<'_>) -> Result<Option<Report>> {
// parse as mailheaders
let report_body = report.subparts[1].get_body_raw()?;
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// must be present
if let Some(_disposition) = report_fields
.get_header_value(HeaderDef::Disposition)
.ok()
.flatten()
{
let disp = HeaderDef::Disposition.get_headername();
if let Some(_disposition) = report_fields.get_first_value(&disp).ok().flatten() {
if let Some(original_message_id) = report_fields
.get_header_value(HeaderDef::OriginalMessageId)
.get_first_value(&HeaderDef::OriginalMessageId.get_headername())
.ok()
.flatten()
.and_then(|v| parse_message_id(&v))
{
let additional_message_ids = report_fields
.get_header_value(HeaderDef::AdditionalMessageIds)
.get_first_value(&HeaderDef::AdditionalMessageIds.get_headername())
.ok()
.flatten()
.map_or_else(Vec::new, |v| {
@@ -807,9 +788,9 @@ impl MimeMessage {
}
}
warn!(
context,
self.context,
"ignoring unknown disposition-notification, Message-Id: {:?}",
report_fields.get_header_value(HeaderDef::MessageId).ok()
report_fields.get_first_value("Message-ID").ok()
);
Ok(None)
@@ -818,7 +799,6 @@ impl MimeMessage {
/// Handle reports (only MDNs for now)
pub fn handle_reports(
&self,
context: &Context,
from_id: u32,
sent_timestamp: i64,
server_folder: impl AsRef<str>,
@@ -833,10 +813,13 @@ impl MimeMessage {
for original_message_id in
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
{
if let Some((chat_id, msg_id)) =
message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp)
{
context.call_cb(Event::MsgRead { chat_id, msg_id });
if let Some((chat_id, msg_id)) = message::mdn_from_ext(
self.context,
from_id,
original_message_id,
sent_timestamp,
) {
self.context.call_cb(Event::MsgRead { chat_id, msg_id });
mdn_recognized = true;
}
}
@@ -846,10 +829,10 @@ impl MimeMessage {
let mut param = Params::new();
param.set(Param::ServerFolder, server_folder.as_ref());
param.set_int(Param::ServerUid, server_uid as i32);
if self.has_chat_version() && context.get_config_bool(Config::MvboxMove) {
if self.has_chat_version() && self.context.get_config_bool(Config::MvboxMove) {
param.set_int(Param::AlsoMove, 1);
}
job_add(context, Action::MarkseenMdnOnImap, 0, param, 0);
job_add(self.context, Action::MarkseenMdnOnImap, 0, param, 0);
}
}
}
@@ -1183,13 +1166,10 @@ mod tests {
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
let of = mimeparser
.parse_first_addr(&context.ctx, HeaderDef::From_)
.unwrap();
let of = mimeparser.parse_first_addr(HeaderDef::From_).unwrap();
assert_eq!(of, mailparse::addrparse("hello@one.org").unwrap()[0]);
let of =
mimeparser.parse_first_addr(&context.ctx, HeaderDef::ChatDispositionNotificationTo);
let of = mimeparser.parse_first_addr(HeaderDef::ChatDispositionNotificationTo);
assert!(of.is_none());
}
@@ -1489,16 +1469,4 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
&["foo@example.com", "foo@example.net"]
);
}
#[test]
fn mailparse_test() {
let body = b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>";
let mail = mailparse::parse_mail(body).unwrap();
let from = mail.headers[0].get_value().unwrap();
assert_eq!(&from, "Имя, Фамилия <foobar@example.com>");
let parsed = mailparse::addrparse(&from).unwrap();
assert_eq!(parsed.len(), 1);
}
}
+36 -21
View File
@@ -1,6 +1,5 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
use std::collections::HashSet;
use std::convert::TryFrom;
use std::fmt;
use num_traits::FromPrimitive;
@@ -8,7 +7,7 @@ use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::constants::*;
use crate::context::Context;
use crate::key::{Key, SignedPublicKey};
use crate::key::*;
use crate::sql::{self, Sql};
#[derive(Debug)]
@@ -127,7 +126,7 @@ impl<'a> Peerstate<'a> {
res.last_seen_autocrypt = message_time;
res.to_save = Some(ToSave::All);
res.prefer_encrypt = header.prefer_encrypt;
res.public_key = Some(Key::from(header.public_key.clone()));
res.public_key = Some(header.public_key.clone());
res.recalc_fingerprint();
res
@@ -138,7 +137,7 @@ impl<'a> Peerstate<'a> {
res.gossip_timestamp = message_time;
res.to_save = Some(ToSave::All);
res.gossip_key = Some(Key::from(gossip_header.public_key.clone()));
res.gossip_key = Some(gossip_header.public_key.clone());
res.recalc_fingerprint();
res
@@ -294,8 +293,8 @@ impl<'a> Peerstate<'a> {
self.to_save = Some(ToSave::All)
}
if self.public_key.as_ref() != Some(&Key::from(header.public_key.clone())) {
self.public_key = Some(Key::from(header.public_key.clone()));
if self.public_key.as_ref() != Some(&header.public_key) {
self.public_key = Some(header.public_key.clone());
self.recalc_fingerprint();
self.to_save = Some(ToSave::All);
}
@@ -310,9 +309,8 @@ impl<'a> Peerstate<'a> {
if message_time > self.gossip_timestamp {
self.gossip_timestamp = message_time;
self.to_save = Some(ToSave::Timestamps);
let hdr_key = Key::from(gossip_header.public_key.clone());
if self.gossip_key.as_ref() != Some(&hdr_key) {
self.gossip_key = Some(hdr_key);
if self.gossip_key.as_ref() != Some(&gossip_header.public_key) {
self.gossip_key = Some(gossip_header.public_key.clone());
self.recalc_fingerprint();
self.to_save = Some(ToSave::All)
}
@@ -322,10 +320,9 @@ impl<'a> Peerstate<'a> {
pub fn render_gossip_header(&self, min_verified: PeerstateVerifiedStatus) -> Option<String> {
if let Some(key) = self.peek_key(min_verified) {
// TODO: avoid cloning
let public_key = SignedPublicKey::try_from(key.clone()).ok()?;
let header = Aheader::new(
self.addr.clone(),
public_key,
key.clone(),
EncryptPreference::NoPreference,
);
Some(header.to_string())
@@ -335,13 +332,18 @@ impl<'a> Peerstate<'a> {
}
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Key> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(),
PeerstateVerifiedStatus::Unverified => self
.public_key
.as_ref()
.or_else(|| self.gossip_key.as_ref()),
if self.public_key.is_none() && self.gossip_key.is_none() && self.verified_key.is_none() {
return None;
}
if min_verified != PeerstateVerifiedStatus::Unverified {
return self.verified_key.as_ref();
}
if self.public_key.is_some() {
return self.public_key.as_ref();
}
self.gossip_key.as_ref()
}
pub fn set_verified(
@@ -448,8 +450,8 @@ impl<'a> Peerstate<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
@@ -457,7 +459,11 @@ mod tests {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let mut peerstate = Peerstate {
context: &ctx.ctx,
@@ -497,7 +503,12 @@ mod tests {
fn test_peerstate_double_create() {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let peerstate = Peerstate {
context: &ctx.ctx,
@@ -531,7 +542,11 @@ mod tests {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let mut peerstate = Peerstate {
context: &ctx.ctx,
+10 -228
View File
@@ -16,7 +16,6 @@ use pgp::types::{
};
use rand::{thread_rng, CryptoRng, Rng};
use crate::dc_tools::EmailAddress;
use crate::error::Result;
use crate::key::*;
use crate::keyring::*;
@@ -112,43 +111,10 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
Ok((typ, headers, bytes))
}
/// Error with generating a PGP keypair.
///
/// Most of these are likely coding errors rather than user errors
/// since all variability is hardcoded.
#[derive(Fail, Debug)]
#[fail(display = "PgpKeygenError: {}", message)]
pub(crate) struct PgpKeygenError {
message: String,
#[cause]
cause: failure::Error,
backtrace: failure::Backtrace,
}
impl PgpKeygenError {
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
Self {
message: message.into(),
cause: cause.into(),
backtrace: failure::Backtrace::new(),
}
}
}
/// A PGP keypair.
///
/// This has it's own struct to be able to keep the public and secret
/// keys together as they are one unit.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct KeyPair {
pub addr: EmailAddress,
pub public: SignedPublicKey,
pub secret: SignedSecretKey,
}
/// Create a new key pair.
pub(crate) fn create_keypair(addr: EmailAddress) -> std::result::Result<KeyPair, PgpKeygenError> {
let user_id = format!("<{}>", addr);
pub fn create_keypair(addr: impl AsRef<str>) -> Option<(Key, Key)> {
let user_id = format!("<{}>", addr.as_ref());
let key_params = SecretKeyParamsBuilder::default()
.key_type(PgpKeyType::Rsa(2048))
.can_create_certificates(true)
@@ -180,29 +146,20 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> std::result::Result<KeyPair,
.unwrap(),
)
.build()
.map_err(|err| PgpKeygenError::new("invalid key params", failure::err_msg(err)))?;
let key = key_params
.generate()
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
.expect("invalid key params");
let key = key_params.generate().expect("invalid params");
let private_key = key.sign(|| "".into()).expect("failed to sign secret key");
let public_key = private_key.public_key();
let public_key = public_key
.sign(&private_key, || "".into())
.map_err(|err| PgpKeygenError::new("failed to sign public key", err))?;
.expect("failed to sign public key");
private_key
.verify()
.map_err(|err| PgpKeygenError::new("invalid private key generated", err))?;
public_key
.verify()
.map_err(|err| PgpKeygenError::new("invalid public key generated", err))?;
private_key.verify().expect("invalid private key generated");
public_key.verify().expect("invalid public key generated");
Ok(KeyPair {
addr,
public: public_key,
secret: private_key,
})
Some((Key::Public(public_key), Key::Secret(private_key)))
}
/// Select public key or subkey to use for encryption.
@@ -354,8 +311,6 @@ pub fn symm_decrypt<T: std::io::Read + std::io::Seek>(
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use lazy_static::lazy_static;
#[test]
fn test_split_armored_data_1() {
@@ -383,177 +338,4 @@ mod tests {
assert!(!base64.is_empty());
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
}
#[test]
#[ignore] // is too expensive
fn test_create_keypair() {
let keypair0 = create_keypair(EmailAddress::new("foo@bar.de").unwrap()).unwrap();
let keypair1 = create_keypair(EmailAddress::new("two@zwo.de").unwrap()).unwrap();
assert_ne!(keypair0.public, keypair1.public);
}
/// [Key] objects to use in tests.
struct TestKeys {
alice_secret: Key,
alice_public: Key,
bob_secret: Key,
bob_public: Key,
}
impl TestKeys {
fn new() -> TestKeys {
let alice = alice_keypair();
let bob = bob_keypair();
TestKeys {
alice_secret: Key::from(alice.secret.clone()),
alice_public: Key::from(alice.public.clone()),
bob_secret: Key::from(bob.secret.clone()),
bob_public: Key::from(bob.public.clone()),
}
}
}
/// The original text of [CTEXT_SIGNED]
static CLEARTEXT: &[u8] = b"This is a test";
lazy_static! {
/// Initialised [TestKeys] for tests.
static ref KEYS: TestKeys = TestKeys::new();
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
static ref CTEXT_SIGNED: String = {
let mut keyring = Keyring::default();
keyring.add_owned(KEYS.alice_public.clone());
keyring.add_ref(&KEYS.bob_public);
pk_encrypt(CLEARTEXT, &keyring, Some(&KEYS.alice_secret)).unwrap()
};
/// A cyphertext encrypted to Alice & Bob, not signed.
static ref CTEXT_UNSIGNED: String = {
let mut keyring = Keyring::default();
keyring.add_owned(KEYS.alice_public.clone());
keyring.add_ref(&KEYS.bob_public);
pk_encrypt(CLEARTEXT, &keyring, None).unwrap()
};
}
#[test]
fn test_encrypt_signed() {
assert!(!CTEXT_SIGNED.is_empty());
assert!(CTEXT_SIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
}
#[test]
fn test_encrypt_unsigned() {
assert!(!CTEXT_UNSIGNED.is_empty());
assert!(CTEXT_UNSIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
}
#[test]
fn test_decrypt_singed() {
// Check decrypting as Alice
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.alice_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.map_err(|err| println!("{:?}", err))
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.map_err(|err| println!("{:?}", err))
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
}
#[test]
fn test_decrypt_no_sig_check() {
let mut keyring = Keyring::default();
keyring.add_ref(&KEYS.alice_secret);
let empty_keyring = Keyring::default();
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&keyring,
&empty_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[test]
fn test_decrypt_signed_no_key() {
// The validation does not have the public key of the signer.
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.bob_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[test]
fn test_decrypt_unsigned() {
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let sig_check_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.alice_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_UNSIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[test]
fn test_decrypt_signed_no_sigret() {
// Check decrypting signed cyphertext without providing the HashSet for signatures.
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public);
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
None,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
}
}
-308
View File
@@ -1,308 +0,0 @@
// file generated by src/provider/update.py
use crate::provider::Protocol::*;
use crate::provider::Socket::*;
use crate::provider::UsernamePattern::*;
use crate::provider::*;
use std::collections::HashMap;
lazy_static::lazy_static! {
// aktivix.org.md: aktivix.org
static ref P_AKTIVIX_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/aktivix-org",
server: vec![
Server { protocol: IMAP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 25, username_pattern: EMAIL },
],
};
// autistici.org.md: autistici.org
static ref P_AUTISTICI_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/autistici-org",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "mail.autistici.org", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.autistici.org", port: 465, username_pattern: EMAIL },
],
};
// bluewin.ch.md: bluewin.ch
static ref P_BLUEWIN_CH: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/bluewin-ch",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imaps.bluewin.ch", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtpauths.bluewin.ch", port: 465, username_pattern: EMAIL },
],
};
// comcast.md: xfinity.com, comcast.net
// - skipping provider with status OK and no special things to do
// dismail.de.md: dismail.de
// - skipping provider with status OK and no special things to do
// disroot.md: disroot.org
// - skipping provider with status OK and no special things to do
// example.com.md: example.com, example.org
static ref P_EXAMPLE_COM: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "Hush this provider doesn't exist!",
after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider for Delta Chat, take a look at providers.delta.chat!",
overview_page: "https://providers.delta.chat/example-com",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.example.com", port: 1337, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.example.com", port: 1337, username_pattern: EMAIL },
],
};
// freenet.de.md: freenet.de
static ref P_FREENET_DE: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/freenet-de",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "mx.freenet.de", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "mx.freenet.de", port: 587, username_pattern: EMAIL },
],
};
// gmail.md: gmail.com, googlemail.com
static ref P_GMAIL: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "For Gmail accounts, you need to create an app-password if you have \"2-Step Verification\" enabled. If this setting is not available, you need to enable \"less secure apps\".",
after_login_hint: "",
overview_page: "https://providers.delta.chat/gmail",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.gmail.com", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.gmail.com", port: 465, username_pattern: EMAIL },
],
};
// gmx.net.md: gmx.net, gmx.de, gmx.at, gmx.ch, gmx.org, gmx.eu, gmx.info, gmx.biz, gmx.com
static ref P_GMX_NET: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "You must allow IMAP access to your account before you can login.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/gmx-net",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.gmx.net", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "mail.gmx.net", port: 465, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "mail.gmx.net", port: 587, username_pattern: EMAIL },
],
};
// i.ua.md: i.ua
// - skipping provider with status OK and no special things to do
// icloud.md: icloud.com, me.com, mac.com
static ref P_ICLOUD: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "You must create an app-specific password for Delta Chat before you can login.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/icloud",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.mail.me.com", port: 993, username_pattern: EMAILLOCALPART },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.mail.me.com", port: 587, username_pattern: EMAIL },
],
};
// kolst.com.md: kolst.com
// - skipping provider with status OK and no special things to do
// kontent.com.md: kontent.com
// - skipping provider with status OK and no special things to do
// mail.ru.md: mail.ru, inbox.ru, bk.ru, list.ru
// - skipping provider with status OK and no special things to do
// mailbox.org.md: mailbox.org, secure.mailbox.org
// - skipping provider with status OK and no special things to do
// nauta.cu.md: nauta.cu
static ref P_NAUTA_CU: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "Atención - con nauta.cu, puede enviar mensajes sólo a un máximo de 20 personas a la vez. En grupos más grandes, no puede enviar mensajes o abandonar el grupo.",
overview_page: "https://providers.delta.chat/nauta-cu",
server: vec![
Server { protocol: IMAP, socket: STARTTLS, hostname: "imap.nauta.cu", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.nauta.cu", port: 25, username_pattern: EMAIL },
],
};
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
static ref P_OUTLOOK_COM: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "Outlook.com email addresses will not work as expected as these servers remove some important transport information. Hopefully sooner or later there will be a fix, for now we suggest to use another email address.",
after_login_hint: "Outlook.com email addresses will not work as expected as these servers remove some important transport information. Unencrypted 1-on-1 chats kind of work, but groups and encryption don't. Hopefully sooner or later there will be a fix, for now we suggest to use another email address.",
overview_page: "https://providers.delta.chat/outlook-com",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap-mail.outlook.com", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp-mail.outlook.com", port: 587, username_pattern: EMAIL },
],
};
// posteo.md: posteo.de
static ref P_POSTEO: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/posteo",
server: vec![
Server { protocol: IMAP, socket: STARTTLS, hostname: "posteo.de", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "posteo.de", port: 587, username_pattern: EMAIL },
],
};
// riseup.net.md: riseup.net
// - skipping provider with status OK and no special things to do
// rogers.com.md: rogers.com
// - skipping provider with status OK and no special things to do
// tiscali.it.md: tiscali.it
static ref P_TISCALI_IT: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/tiscali-it",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.tiscali.it", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.tiscali.it", port: 465, username_pattern: EMAIL },
],
};
// ukr.net.md: ukr.net
// - skipping provider with status OK and no special things to do
// vfemail.md: vfemail.net
// - skipping provider with status OK and no special things to do
// web.de.md: web.de, email.de, flirt.ms, hallo.ms, kuss.ms, love.ms, magic.ms, singles.ms, cool.ms, kanzler.ms, okay.ms, party.ms, pop.ms, stars.ms, techno.ms, clever.ms, deutschland.ms, genial.ms, ich.ms, online.ms, smart.ms, wichtig.ms, action.ms, fussball.ms, joker.ms, planet.ms, power.ms
static ref P_WEB_DE: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "You must allow IMAP access to your account before you can login.",
after_login_hint: "Note: if you have your web.de spam settings too strict, you won't receive contact requests from new people. If you want to receive contact requests, you should disable the \"3-Wege-Spamschutz\" in the web.de settings. Read how: https://hilfe.web.de/email/spam-und-viren/spamschutz-einstellungen.html",
overview_page: "https://providers.delta.chat/web-de",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.web.de", port: 993, username_pattern: EMAILLOCALPART },
Server { protocol: IMAP, socket: STARTTLS, hostname: "imap.web.de", port: 143, username_pattern: EMAILLOCALPART },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.web.de", port: 587, username_pattern: EMAILLOCALPART },
],
};
// yahoo.md: yahoo.com, yahoo.de, yahoo.it, yahoo.fr, yahoo.es, yahoo.se, yahoo.co.uk, yahoo.co.nz, yahoo.com.au, yahoo.com.ar, yahoo.com.br, yahoo.com.mx, ymail.com, rocketmail.com, yahoodns.net
static ref P_YAHOO: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "To use Delta Chat with your Yahoo email address you have to allow \"less secure apps\" in the Yahoo webinterface.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/yahoo",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.mail.yahoo.com", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.mail.yahoo.com", port: 465, username_pattern: EMAIL },
],
};
// yandex.ru.md: yandex.ru, yandex.com
// - skipping provider with status OK and no special things to do
// ziggo.nl.md: ziggo.nl
static ref P_ZIGGO_NL: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/ziggo-nl",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.ziggo.nl", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.ziggo.nl", port: 587, username_pattern: EMAIL },
],
};
// zoho.com.md: zoho.com
// - skipping provider with status OK and no special things to do
pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [
("aktivix.org", &*P_AKTIVIX_ORG),
("autistici.org", &*P_AUTISTICI_ORG),
("bluewin.ch", &*P_BLUEWIN_CH),
("example.com", &*P_EXAMPLE_COM),
("example.org", &*P_EXAMPLE_COM),
("freenet.de", &*P_FREENET_DE),
("gmail.com", &*P_GMAIL),
("googlemail.com", &*P_GMAIL),
("gmx.net", &*P_GMX_NET),
("gmx.de", &*P_GMX_NET),
("gmx.at", &*P_GMX_NET),
("gmx.ch", &*P_GMX_NET),
("gmx.org", &*P_GMX_NET),
("gmx.eu", &*P_GMX_NET),
("gmx.info", &*P_GMX_NET),
("gmx.biz", &*P_GMX_NET),
("gmx.com", &*P_GMX_NET),
("icloud.com", &*P_ICLOUD),
("me.com", &*P_ICLOUD),
("mac.com", &*P_ICLOUD),
("nauta.cu", &*P_NAUTA_CU),
("hotmail.com", &*P_OUTLOOK_COM),
("outlook.com", &*P_OUTLOOK_COM),
("office365.com", &*P_OUTLOOK_COM),
("outlook.com.tr", &*P_OUTLOOK_COM),
("live.com", &*P_OUTLOOK_COM),
("posteo.de", &*P_POSTEO),
("tiscali.it", &*P_TISCALI_IT),
("web.de", &*P_WEB_DE),
("email.de", &*P_WEB_DE),
("flirt.ms", &*P_WEB_DE),
("hallo.ms", &*P_WEB_DE),
("kuss.ms", &*P_WEB_DE),
("love.ms", &*P_WEB_DE),
("magic.ms", &*P_WEB_DE),
("singles.ms", &*P_WEB_DE),
("cool.ms", &*P_WEB_DE),
("kanzler.ms", &*P_WEB_DE),
("okay.ms", &*P_WEB_DE),
("party.ms", &*P_WEB_DE),
("pop.ms", &*P_WEB_DE),
("stars.ms", &*P_WEB_DE),
("techno.ms", &*P_WEB_DE),
("clever.ms", &*P_WEB_DE),
("deutschland.ms", &*P_WEB_DE),
("genial.ms", &*P_WEB_DE),
("ich.ms", &*P_WEB_DE),
("online.ms", &*P_WEB_DE),
("smart.ms", &*P_WEB_DE),
("wichtig.ms", &*P_WEB_DE),
("action.ms", &*P_WEB_DE),
("fussball.ms", &*P_WEB_DE),
("joker.ms", &*P_WEB_DE),
("planet.ms", &*P_WEB_DE),
("power.ms", &*P_WEB_DE),
("yahoo.com", &*P_YAHOO),
("yahoo.de", &*P_YAHOO),
("yahoo.it", &*P_YAHOO),
("yahoo.fr", &*P_YAHOO),
("yahoo.es", &*P_YAHOO),
("yahoo.se", &*P_YAHOO),
("yahoo.co.uk", &*P_YAHOO),
("yahoo.co.nz", &*P_YAHOO),
("yahoo.com.au", &*P_YAHOO),
("yahoo.com.ar", &*P_YAHOO),
("yahoo.com.br", &*P_YAHOO),
("yahoo.com.mx", &*P_YAHOO),
("ymail.com", &*P_YAHOO),
("rocketmail.com", &*P_YAHOO),
("yahoodns.net", &*P_YAHOO),
("ziggo.nl", &*P_ZIGGO_NL),
].iter().copied().collect();
}
-144
View File
@@ -1,144 +0,0 @@
mod data;
use crate::dc_tools::EmailAddress;
use crate::provider::data::PROVIDER_DATA;
#[derive(Debug, Copy, Clone, PartialEq, ToPrimitive)]
#[repr(u8)]
pub enum Status {
OK = 1,
PREPARATION = 2,
BROKEN = 3,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Protocol {
SMTP = 1,
IMAP = 2,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Socket {
STARTTLS = 1,
SSL = 2,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum UsernamePattern {
EMAIL = 1,
EMAILLOCALPART = 2,
}
#[derive(Debug)]
pub struct Server {
pub protocol: Protocol,
pub socket: Socket,
pub hostname: &'static str,
pub port: u16,
pub username_pattern: UsernamePattern,
}
impl Server {
pub fn apply_username_pattern(&self, addr: String) -> String {
match self.username_pattern {
UsernamePattern::EMAIL => addr,
UsernamePattern::EMAILLOCALPART => {
if let Some(at) = addr.find('@') {
return addr.split_at(at).0.to_string();
}
addr
}
}
}
}
#[derive(Debug)]
pub struct Provider {
pub status: Status,
pub before_login_hint: &'static str,
pub after_login_hint: &'static str,
pub overview_page: &'static str,
pub server: Vec<Server>,
}
impl Provider {
pub fn get_server(&self, protocol: Protocol) -> Option<&Server> {
for record in self.server.iter() {
if record.protocol == protocol {
return Some(record);
}
}
None
}
pub fn get_imap_server(&self) -> Option<&Server> {
self.get_server(Protocol::IMAP)
}
pub fn get_smtp_server(&self) -> Option<&Server> {
self.get_server(Protocol::SMTP)
}
}
pub fn get_provider_info(addr: &str) -> Option<&Provider> {
let domain = match addr.parse::<EmailAddress>() {
Ok(addr) => addr.domain,
Err(_err) => return None,
}
.to_lowercase();
if let Some(provider) = PROVIDER_DATA.get(domain.as_str()) {
return Some(*provider);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_provider_info_unexistant() {
let provider = get_provider_info("user@unexistant.org");
assert!(provider.is_none());
}
#[test]
fn test_get_provider_info_mixed_case() {
let provider = get_provider_info("uSer@nAUta.Cu").unwrap();
assert!(provider.status == Status::OK);
}
#[test]
fn test_get_provider_info() {
let provider = get_provider_info("nauta.cu"); // this is no email address
assert!(provider.is_none());
let provider = get_provider_info("user@nauta.cu").unwrap();
assert!(provider.status == Status::OK);
let server = provider.get_imap_server().unwrap();
assert_eq!(server.protocol, Protocol::IMAP);
assert_eq!(server.socket, Socket::STARTTLS);
assert_eq!(server.hostname, "imap.nauta.cu");
assert_eq!(server.port, 143);
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
let server = provider.get_smtp_server().unwrap();
assert_eq!(server.protocol, Protocol::SMTP);
assert_eq!(server.socket, Socket::STARTTLS);
assert_eq!(server.hostname, "smtp.nauta.cu");
assert_eq!(server.port, 25);
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
let provider = get_provider_info("user@gmail.com").unwrap();
assert!(provider.status == Status::PREPARATION);
assert!(!provider.before_login_hint.is_empty());
assert!(!provider.overview_page.is_empty());
let provider = get_provider_info("user@googlemail.com").unwrap();
assert!(provider.status == Status::PREPARATION);
}
}
-148
View File
@@ -1,148 +0,0 @@
#!/usr/bin/env python3
# if the yaml import fails, run "pip install pyyaml"
import sys
import os
import yaml
out_all = ""
out_domains = ""
domains_dict = {}
def cleanstr(s):
s = s.strip()
s = s.replace("\n", " ")
s = s.replace("\\", "\\\\")
s = s.replace("\"", "\\\"")
return s
def file2varname(f):
f = f[f.rindex("/")+1:].replace(".md", "")
f = f.replace(".", "_")
f = f.replace("-", "_")
return "P_" + f.upper()
def file2url(f):
f = f[f.rindex("/")+1:].replace(".md", "")
f = f.replace(".", "-")
return "https://providers.delta.chat/" + f
def process_data(data, file):
status = data.get("status", "")
if status != "OK" and status != "PREPARATION" and status != "BROKEN":
raise TypeError("bad status")
comment = ""
domains = ""
if not "domains" in data:
raise TypeError("no domains found")
for domain in data["domains"]:
domain = cleanstr(domain)
if domain == "" or domain.count(".") < 1 or domain.lower() != domain:
raise TypeError("bad domain: " + domain)
global domains_dict
if domains_dict.get(domain, False):
raise TypeError("domain used twice: " + domain)
domains_dict[domain] = True
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
comment += domain + ", "
server = ""
has_imap = False
has_smtp = False
if "server" in data:
for s in data["server"]:
hostname = cleanstr(s.get("hostname", ""))
port = int(s.get("port", ""))
if hostname == "" or hostname.count(".") < 1 or port <= 0:
raise TypeError("bad hostname or port")
protocol = s.get("type", "").upper()
if protocol == "IMAP":
has_imap = True
elif protocol == "SMTP":
has_smtp = True
else:
raise TypeError("bad protocol")
socket = s.get("socket", "").upper()
if socket != "STARTTLS" and socket != "SSL":
raise TypeError("bad socket")
username_pattern = s.get("username_pattern", "EMAIL").upper()
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
raise TypeError("bad username pattern")
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
provider = ""
before_login_hint = cleanstr(data.get("before_login_hint", ""))
after_login_hint = cleanstr(data.get("after_login_hint", ""))
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
provider += " static ref " + file2varname(file) + ": Provider = Provider {\n"
provider += " status: Status::" + status + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " };\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")
if status != "OK" and before_login_hint == "":
raise TypeError("status PREPARATION or BROKEN requires before_login_hint: " + file)
# finally, add the provider
global out_all, out_domains
out_all += " // " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
if status == "OK" and before_login_hint == "" and after_login_hint == "" and server == "":
out_all += " // - skipping provider with status OK and no special things to do\n\n"
else:
out_all += provider
out_domains += domains
def process_file(file):
print("processing file: " + file, file=sys.stderr)
with open(file) as f:
# load_all() loads "---"-separated yamls -
# by coincidence, this is also the frontmatter separator :)
data = next(yaml.load_all(f, Loader=yaml.SafeLoader))
process_data(data, file)
def process_dir(dir):
print("processing directory: " + dir, file=sys.stderr)
files = [f for f in os.listdir(dir) if f.endswith(".md")]
files.sort()
for f in files:
process_file(os.path.join(dir, f))
if __name__ == "__main__":
if len(sys.argv) < 2:
raise SystemExit("usage: update.py DIR_WITH_MD_FILES > data.rs")
out_all = ("// file generated by src/provider/update.py\n\n"
"use crate::provider::Protocol::*;\n"
"use crate::provider::Socket::*;\n"
"use crate::provider::UsernamePattern::*;\n"
"use crate::provider::*;\n"
"use std::collections::HashMap;\n\n"
"lazy_static::lazy_static! {\n\n")
process_dir(sys.argv[1])
out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n"
out_all += out_domains;
out_all += " ].iter().copied().collect();\n}"
print(out_all)
+1 -98
View File
@@ -4,21 +4,17 @@ use lazy_static::lazy_static;
use percent_encoding::percent_decode_str;
use crate::chat;
use crate::config::*;
use crate::constants::Blocked;
use crate::contact::*;
use crate::context::Context;
use crate::error::Error;
use crate::key::dc_format_fingerprint;
use crate::key::dc_normalize_fingerprint;
use crate::key::*;
use crate::lot::{Lot, LotState};
use crate::param::*;
use crate::peerstate::*;
use reqwest::Url;
use serde::Deserialize;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
const MAILTO_SCHEME: &str = "mailto:";
const MATMSG_SCHEME: &str = "MATMSG:";
const VCARD_SCHEME: &str = "BEGIN:VCARD";
@@ -47,8 +43,6 @@ pub fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
if qr.starts_with(OPENPGP4FPR_SCHEME) {
decode_openpgp(context, qr)
} else if qr.starts_with(DCACCOUNT_SCHEME) {
decode_account(context, qr)
} else if qr.starts_with(MAILTO_SCHEME) {
decode_mailto(context, qr)
} else if qr.starts_with(SMTP_SCHEME) {
@@ -185,73 +179,6 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
lot
}
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
fn decode_account(_context: &Context, qr: &str) -> Lot {
let payload = &qr[DCACCOUNT_SCHEME.len()..];
let mut lot = Lot::new();
if let Ok(url) = Url::parse(payload) {
if url.scheme() == "https" {
lot.state = LotState::QrAccount;
lot.text1 = url.host_str().map(|x| x.to_string());
} else {
lot.state = LotState::QrError;
lot.text1 = Some(format!("Bad scheme for account url: {}", payload));
}
} else {
lot.state = LotState::QrError;
lot.text1 = Some(format!("Invalid account url: {}", payload));
}
lot
}
#[derive(Debug, Deserialize)]
struct CreateAccountResponse {
email: String,
password: String,
}
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
/// download additional information from the contained url and set the parameters.
/// on success, a configure::configure() should be able to log in to the account
pub fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
let response = reqwest::blocking::Client::new().post(url_str).send();
if response.is_err() {
return Err(format_err!(
"Cannot create account, request to {} failed",
url_str
));
}
let response = response.unwrap();
if !response.status().is_success() {
return Err(format_err!(
"Request to {} unsuccessful: {:?}",
url_str,
response
));
}
let parsed: reqwest::Result<CreateAccountResponse> = response.json();
if parsed.is_err() {
return Err(format_err!(
"Failed to parse JSON response from {}: error: {:?}",
url_str,
parsed
));
}
println!("response: {:?}", &parsed);
let parsed = parsed.unwrap();
context.set_config(Config::Addr, Some(&parsed.email))?;
context.set_config(Config::MailPw, Some(&parsed.password))?;
Ok(())
}
/// Extract address for the mailto scheme.
///
/// Scheme: `mailto:addr...?subject=...&body=..`
@@ -566,28 +493,4 @@ mod tests {
assert_eq!(res.get_state(), LotState::QrError);
assert_eq!(res.get_id(), 0);
}
#[test]
fn test_decode_account() {
let ctx = dummy_context();
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
);
assert_eq!(res.get_state(), LotState::QrAccount);
assert_eq!(res.get_text1().unwrap(), "example.org");
}
#[test]
fn test_decode_account_bad_scheme() {
let ctx = dummy_context();
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
);
assert_eq!(res.get_state(), LotState::QrError);
assert!(res.get_text1().is_some());
}
}
+9 -13
View File
@@ -12,7 +12,7 @@ use crate::e2ee::*;
use crate::error::Error;
use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::key::{dc_normalize_fingerprint, Key};
use crate::key::*;
use crate::lot::LotState;
use crate::message::Message;
use crate::mimeparser::*;
@@ -485,7 +485,7 @@ pub(crate) fn handle_securejoin_handshake(
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
let auth = get_qr_attr!(context, auth).to_string();
if !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice) {
if !encrypted_and_signed(mime_message, &scanned_fingerprint_of_alice) {
could_not_establish_secure_connection(
context,
contact_chat_id,
@@ -548,7 +548,7 @@ pub(crate) fn handle_securejoin_handshake(
return Ok(HandshakeMessage::Ignore);
}
};
if !encrypted_and_signed(context, mime_message, &fingerprint) {
if !encrypted_and_signed(mime_message, &fingerprint) {
could_not_establish_secure_connection(
context,
contact_chat_id,
@@ -675,7 +675,7 @@ pub(crate) fn handle_securejoin_handshake(
true
};
if vg_expect_encrypted
&& !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice)
&& !encrypted_and_signed(mime_message, &scanned_fingerprint_of_alice)
{
could_not_establish_secure_connection(
context,
@@ -830,26 +830,22 @@ fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Res
* Tools: Misc.
******************************************************************************/
fn encrypted_and_signed(
context: &Context,
mimeparser: &MimeMessage,
expected_fingerprint: impl AsRef<str>,
) -> bool {
fn encrypted_and_signed(mimeparser: &MimeMessage, expected_fingerprint: impl AsRef<str>) -> bool {
if !mimeparser.was_encrypted() {
warn!(context, "Message not encrypted.",);
warn!(mimeparser.context, "Message not encrypted.",);
false
} else if mimeparser.signatures.is_empty() {
warn!(context, "Message not signed.",);
warn!(mimeparser.context, "Message not signed.",);
false
} else if expected_fingerprint.as_ref().is_empty() {
warn!(context, "Fingerprint for comparison missing.",);
warn!(mimeparser.context, "Fingerprint for comparison missing.",);
false
} else if !mimeparser
.signatures
.contains(expected_fingerprint.as_ref())
{
warn!(
context,
mimeparser.context,
"Message does not match expected fingerprint {}.",
expected_fingerprint.as_ref(),
);
+8 -33
View File
@@ -217,37 +217,20 @@ impl Sql {
.unwrap_or_default()
}
/// Executes a query which is expected to return one row and one
/// column. If the query does not return a value or returns SQL
/// `NULL`, returns `Ok(None)`.
pub fn query_get_value_result<P, T>(&self, query: &str, params: P) -> Result<Option<T>>
where
P: IntoIterator,
P::Item: rusqlite::ToSql,
T: rusqlite::types::FromSql,
{
match self.query_row(query, params, |row| row.get::<_, T>(0)) {
Ok(res) => Ok(Some(res)),
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
_,
_,
rusqlite::types::Type::Null,
))) => Ok(None),
Err(err) => Err(err),
}
}
/// Not resultified version of `query_get_value_result`. Returns
/// `None` on error.
pub fn query_get_value<P, T>(&self, context: &Context, query: &str, params: P) -> Option<T>
where
P: IntoIterator,
P::Item: rusqlite::ToSql,
T: rusqlite::types::FromSql,
{
match self.query_get_value_result(query, params) {
Ok(res) => res,
match self.query_row(query, params, |row| row.get::<_, T>(0)) {
Ok(res) => Some(res),
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => None,
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
_,
_,
rusqlite::types::Type::Null,
))) => None,
Err(err) => {
error!(context, "sql: Failed query_row: {}", err);
None
@@ -885,14 +868,6 @@ fn open(
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 61)?;
}
if dbversion < 62 {
info!(context, "[migration] v62");
sql.execute(
"ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
NO_PARAMS,
)?;
sql.set_raw_config_int(context, "dbversion", 62)?;
}
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)
+35 -50
View File
@@ -2,20 +2,22 @@
//!
//! This module is only compiled for test runs.
use std::sync::Arc;
use tempfile::{tempdir, TempDir};
use crate::config::Config;
use crate::constants::KeyType;
use crate::context::{Context, ContextCallback};
use crate::dc_tools::EmailAddress;
use crate::events::Event;
use crate::key::{self, DcKey};
use crate::key;
/// A Context and temporary directory.
///
/// The temporary directory can be used to store the SQLite database,
/// see e.g. [test_context] which does this.
pub(crate) struct TestContext {
pub ctx: Context,
pub struct TestContext {
pub ctx: Arc<Context>,
pub dir: TempDir,
}
@@ -25,14 +27,20 @@ pub(crate) struct TestContext {
/// "db.sqlite" in the [TestContext.dir] directory.
///
/// [Context]: crate::context::Context
pub(crate) fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
pub fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let cb: Box<ContextCallback> = match callback {
Some(cb) => cb,
None => Box::new(|_, _| ()),
};
let ctx = Context::new(cb, "FakeOs".into(), dbfile).unwrap();
let ctx = Arc::new(Context::new("FakeOs".into(), dbfile).unwrap());
let ctx_1 = ctx.clone();
std::thread::spawn(move || {
ctx_1.run(|context, event| (*cb)(context, event));
});
TestContext { ctx, dir }
}
@@ -41,11 +49,11 @@ pub(crate) fn test_context(callback: Option<Box<ContextCallback>>) -> TestContex
/// The context will be opened and use the SQLite database as
/// specified in [test_context] but there is no callback hooked up,
/// i.e. [Context::call_cb] will always return `0`.
pub(crate) fn dummy_context() -> TestContext {
pub fn dummy_context() -> TestContext {
test_context(None)
}
pub(crate) fn logging_cb(_ctx: &Context, evt: Event) {
pub fn logging_cb(_ctx: &Context, evt: Event) {
match evt {
Event::Info(msg) => println!("I: {}", msg),
Event::Warning(msg) => println!("W: {}", msg),
@@ -54,50 +62,27 @@ pub(crate) fn logging_cb(_ctx: &Context, evt: Event) {
}
}
/// Load a pre-generated keypair for alice@example.com from disk.
///
/// This saves CPU cycles by avoiding having to generate a key.
///
/// The keypair was created using the crate::key::tests::gen_key test.
pub(crate) fn alice_keypair() -> key::KeyPair {
let addr = EmailAddress::new("alice@example.com").unwrap();
let public =
key::SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
.unwrap();
let secret =
key::SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
.unwrap();
key::KeyPair {
addr,
public,
secret,
}
}
/// Creates Alice with a pre-generated keypair.
///
/// Returns the address of the keypair created (alice@example.com).
pub(crate) fn configure_alice_keypair(ctx: &Context) -> String {
let keypair = alice_keypair();
ctx.set_config(Config::ConfiguredAddr, Some(&keypair.addr.to_string()))
.unwrap();
key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default)
.expect("Failed to save Alice's key");
keypair.addr.to_string()
}
/// Returns the address of the keypair created (alice@example.org).
pub fn configure_alice_keypair(ctx: &Context) -> String {
let addr = String::from("alice@example.org");
ctx.set_config(Config::ConfiguredAddr, Some(&addr)).unwrap();
/// Load a pre-generated keypair for bob@example.net from disk.
///
/// Like [alice_keypair] but a different key and identity.
pub(crate) fn bob_keypair() -> key::KeyPair {
let addr = EmailAddress::new("bob@example.net").unwrap();
// The keypair was created using:
// let (public, private) = crate::pgp::dc_pgp_create_keypair("alice@example.com")
// .unwrap();
// println!("{}", public.to_base64(64));
// println!("{}", private.to_base64(64));
let public =
key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap();
let secret =
key::SignedSecretKey::from_base64(include_str!("../test-data/key/bob-secret.asc")).unwrap();
key::KeyPair {
addr,
public,
secret,
}
key::Key::from_base64(include_str!("../test-data/key/public.asc"), KeyType::Public)
.unwrap();
let private = key::Key::from_base64(
include_str!("../test-data/key/private.asc"),
KeyType::Private,
)
.unwrap();
let saved = key::dc_key_save_self_keypair(&ctx, &public, &private, &addr, true, &ctx.sql);
assert_eq!(saved, true, "Failed to save Alice's key");
addr
}
-1
View File
@@ -1 +0,0 @@
xsBNBF425W8BCADLIbltPzG1vk/V2ov2+eBeJJJnRu1kJHdo6e3oNB+HTIxVde5+7Uq8tTEDZB1O7m9NBUFrXr7UYQsA/86G2jmsyWKTzIu1O/t5kdcNDqsNcTVZAhBu2ixYsYVc3ws6kJONjpXLtD2u3P7vEXU3INiOb2JrBQDT8/ubEm1xas/UirYnP5DMaH068IHRdVEYs9ULFaD5scw1m/94buXYZ1CRt/2hT8iRrtBi6ki8kArnhsZC2Xr0+jRQNMUnG5k7Bwi6saCqVmd7IlqSM6MbfYank30Gi/UyDmyIrOk7daTg6WIqgiVOTHav65EK/aUvvjlr+awM+C+u35rQytzyTitZABEBAAHNEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl425ZQCGwMECwkIBwYVCAkKCwIDFgIBFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HiZQf+PLDxzWchkHAdQFbxxtoXj66aiknofjlRWHDWvUG4nULZ15tjDjnv3z22Meldr8kSV4r1+ejhLFHou9gTzAYk7eAxiybDd8AJOdK+ZgK/Nn7xjdO+HTZLhNdi+R7EektDyf8WDNktEaS8pZc74VKu4984ESi4PoqVxqGHRiSisH4cw4b2pQYxp32BkIdil7sWnqRUEoCpMoKdw2h0N7/lm+rS7/JR9cdjXaVzy1dYTqAVsTL1FTGy4osOKGOyQbkP+Cm6uNq7kC/Bt+fefsb+c2JycmI1uwdvnG7PoFslKv3lRnfkNSmrcIYlJHUl5z0yAgliophr5fqMfzQpO4zMc87ATQReNuVvAQgAuNjE1i+g4v25UNDPIMgXODU4WztE30074gQs5sZa0DQnDUMsdWc2g1o060YZDojMYJQAtBjlW1Dz8FEE7WsLNohGtRyUWmIgNxE5CpodjpwIZ0MdO4Aji0YM+g+WsOSS8kiHMs+dMFfQJuNKjujGFaMIciSaMMrUmPtzkQ/o8NEJs2Aftw90fpVR+M7Mue3++rcEX09ntbgqkgm8SV6OIrOY2kfILudtybocgYkCTeNVqz5VFXuxrnT4ceyFQ64JkwsZxb+X/pCm4V5Q2TbKRwtdonU8HfAz0nAd5tsNeGmf/dPLOKBCxlNEme399YmzWrT+kJBp7CIH5jlWQKyuLwARAQABwsB2BBgBCAAgBQJeNuWUAhsMFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HrEgf/Xu8eRPPdskwtyd98y64teidBpkHuIjuZKJpNyy2HhdGXQwYbNIzwINg0EJ+u2nkreNF/h2Lu+/saqI8Dai02dpYXjvxJIlCgP2os7sNhVaZSaS4XmmJjkHCfZuIKblZypKDJVc5AceZxrtvUbgG+94+H3zeRWVAA30S5ep6YPvxigvhmQah/sdzY7708/jd9uXcCbkP47PBaXCpuPiYLb3t7z8mOteJb7LOZUmSI1efiLDLTGj7ofkdDfA7E6/nF/1+nq+UIDWqljwiUzeNIJsFlZRa/9/uDEjcQbaDe9/knBs7k9pEDZX5u8SSwSED75L+OvRpFWenp4SSKvd2BUw==
-1
View File
@@ -1 +0,0 @@
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
-1
View File
@@ -1 +0,0 @@
xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA5//PjAzbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOeJw9kohATSqUtsRO0pFJeDvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeTxc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1JzdKTcDWryrSkvmgFdUqJ7pJDk1HFTt+x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBame1BPsE1PA7VzeTSJR2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJeMMduAhsDBAsJCAcGFQgJCgsCAxYCARYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSH9oIIALbpmicuVghM3CloiCgJhPEFLFMaQZRDV/KCVVtBcHAhw6d42q8T50mhs+W3Va5E37DN+wcenj8CgeGPQY3kPp2cnZruYtLhLkZ1+VEay5BQFUMb8kY21XrNTQQET8vc0L8cCLQ7RCgm1tGiFVp1nqbjmGUdoru90ksoufWfoqVPjNrW+9eHFvY/Z7PqchCdMnbKOJiwwv4E3NDTySZ1UVZnDztGy95Aa8OZ3cntvbq4JVi7S+N38rRPPPzpZKx+M4DUGfDAoaq7O/Xemyk1sP6C/NgQvS8rri54PgkMgKSS4TyyEzdM+fzeNYFPXFGTbgj4p0pSueQV7/JUfYHRfe3OwE0EXjDHVwEIAKIHgS2yI2niSCN1tqcbLvkhLrEJCVcpGxmA7asl1flwWYrGOBhNJE2sCuZqkofqw6qrgsQ4GFgUU5xmcBCqIZ49jRu+aY38lT4WDFHSbe/mGtaIhb2ZYK6zo9W7Y3r6ud8hbUKJTDfl9qEvJpX/Y0syMjwng8SZNTdYMWgAE4NwcgMgdU3dMA3RT6ePJ4vKs38hmXmInLyZce+GJzmo2tpZyP8viPS7JpqojoCPB3G5h9aHeakp1Y4XKQaExANeWCyBJEhNwtNEOVEpQ0txFYPyDrtxV5y5e79IUP418r/PHsnH6UnxXGzB6LfVbSeEyDyKEl+w0PrlNklySomTZFUAEQEAAcLAdgQYAQgAIAUCXjDHbgIbDBYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSHVCIIAIH694HkQLXRAJlXmi8K/xMVP96ywJovL/B5l4S/vk/iR4P1lYsF55A3Z2PK/iFtwAgVsppcBIPBlqSI0GPDMvEIxj7UFOQfQzVpDes29wG8grHJEJqI/4TlRjOacxTGaJ5fIMsLXJD6nLBuoN5Z6zm3LjqIyOx4ZGrwradPO95OMGT2Xll3YNzUqSWe33RJLqNQ5ea9I7+qvKnW5Z9Yt5nQwOo8yD+f5fql8904B3eAyLqxgkdLmngAWmYhc7KOaKdAsx7TXBAKsoeHk51OPk59u7EbX35HWD6snl/phJdUYDXiddyYN/n2ZY9g80ycle2JfgpfrQGlh7oJqgCjZuk=
-1
View File
@@ -1 +0,0 @@
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
-1
View File
@@ -1 +0,0 @@
xsBNBF48nc4BCADSWr9fE2K1FcE7eSW/Z0MOuzdozKmQJsrmkb7Abssd1yARZuf3+YYh5WqeKJrSTUFD+mJNUhqtodBqBxFH+JzITMG5qGcAdBwXaeJYFMquezLAuVZsZ2b+Njk9Fw3XF4cUZB58ItO/ViFgDi4r5lqsdiiMvGrLmQD2m2BOB8U52ibFhnSVvvYi6rlsZ1HfqB+efD8InPKfiMKDqu909fgVchGJ7OwtTKanaF3v+rzQvXsnVal1yfc0YsmOM0ekWDhIR5EoSg8pJlBHVc/yBrxQWq9h2e2PUntLK29/qp/k/xsQHN3BAj/kuQQPzMARcUUvxo9Aq8n4CMzoFmrK2G+bABEBAAHNFTxjaGFybGllQGV4YW1wbGUubmV0PsLAiQQQAQgAMwIZAQUCXjyd5AIbAwQLCQgHBhUICQoLAgMWAgEWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV7/tMCACSeaPalMt/EwsCGxnWNNEWPX7fMSiAZx1bbPYULIroEBVgmObOkDqoB6y9tWQB/HUq44ZIYdhdw7wlzZp9d81dtxGA+QcRkTy8fr/P2KmQhwjV9m22xBCGABPSpFG+/iONEJADEA8Oe3SogI/iPksepLs9Gg9Ix9Qb1ZMt6+GErE7u0aAamW03NQW7SlgLruAKhjKLjP1wxryK4h0Mdac6CuGZQH/G3yZX3OQPkE2BGjEefhzj0yEKOGhYM716uswqecjB8HoYh5RaDcE+Shcze/OhSQesqj0RxoCpeN3/6QGcC6DT70Qrqeri0RtA495SS3YTqb2p9U2WISOEJIdXzsBNBF48nc4BCADKwwUPt5jp4yIdVdVdLyXGuJS/pC6t5Q64QlcEzHH03eVJHYH42shyWPiTE7HU3giqLhnPXjYN8/piONBQ7dQqTkYJtrx0aUBAoQ9p0p6eMGoY4pMxrclwxnvGE+2YplFNzXkcqxkSPy4n5kOPeq55tVHkWG+gTblZdlT9sKM8Ksr8gF728eaWgt+TQCnhuwa9h9BVzLAZquaATm7PvPKamMd7jIVoISXrZKuPBVrSDbx+DElg0HKj8sOxh3lNz2rmRTGWDbURPhqvVyQ2tXryEVfMZRauR9B9YCYEeedRwDrOSME2LaoU483w0j2Vnb4GkOLpg8Wrw+fnIcM32GXxABEBAAHCwHYEGAEIACAFAl48neQCGwwWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV78HXCACyxjrXpr7GKqQOMJGvNKytuVf7gHUbwLq7oAXoM6PixPkfZIArH/EHnt0GaBWq2r08REj2IRQ8t/8zfkWa9L2RxITez3dRkOUjf49i+J9g3oyleJDZVrAhoU2Uzwb+40tTGaErGpD6m3GqIe5wE7gXFBAf0IwQmIjic64ULCE5j2qaWMxwfvKIDuSD/bN+mSqlQbmekeX6bud4rMoGIUnkQthNAQwBQHOjkPZvdjXiDGFxmpDlcJv5e9LYv8kb141JmYIon9iIGWF3L52+SbvYXFAoPi4qovsdgUXezTRoaiR8Mgft4KUZZIeXUhC7PRuut8PsbN2P0WCh1CjIJph9
-1
View File
@@ -1 +0,0 @@
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
-1
View File
@@ -1 +0,0 @@
xsBNBF48no8BCADwN4PxXewE4iP3HHn/FE5r0x8x8byo9mJIIEOrjL1k0JWIcz3KvC5evfZ3M/ZK2QPBLhFYOck0+gCAR/eH1zgDZeYXUB1CWUoCKlZ9jH1UbfzE34ZxxiwCZy2ZdMnKxDF/ezyXsVvoEhGQE1+8A9Xy/ZXlplUbyYVacgdK9fQlh50d8FOU+7eplUts/SzXx52T83tB98oS9QVgJ4qIC+fu0xMgqdH7e0ithSc/owfYkKjH+isbD4n9U8KSqb4zzf2Y2gz7h6jdM+PPuWqTSARbH83j7qG8q4IjC7cO3CgSryk3f/MLkquKD8z9ybvrYKXSF04RMEuhgBxXnE0vA3ZpABEBAAHNETxkb21AZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJePJ6rAhsDBAsJCAcGFQgJCgsCAxYCARYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaoXGQIAM6KRSMBLKOb0IlXqSCy1Ve9MF02fDNmGFw7xC5v2gKpdDZZJctRLxH3ZFgQmpcSIZ4P/A7XcYnlIxxGxwzyhMmas9eJ1w3sPbvX5vJ4ZYGzooZSFUQXgO6+bm7qQlf7gFcsuTkF5n6cx9tDPcyxy01b7eaexTXoWpYDX110TFbpklprub2QpI2w4gy5y1d/pfOmJYGHgMEALgAEKQCWjQ/jUS6pOVEH+pT1ddlv9vq37CmTp00bKbj+Z59mqeUBjE5+RmNWaLwkdZSR1o+LKNw/Gl07gzayRjJyh0Txj4LgHtVSKvGj6GexhddEgqp8qwMMfNbbqWATh9zHNJ+gjSzOwE0EXjyejwEIAKbISj1O986suBXKzHPlEzqetkGEWNV4OHHfhphIxsoW/f3IOdqv0uqwz9AhzXE6YbsFzrc6ZwP+pYTH51T1ugtG0LBMFikh4tyyYjJV0v9gUq2JqixxHfJ/XfVryKO1OWqs2GtBxaQP5FVf+vQqSPpAz4B/6hqwUvx2XdI6bbGn1fQlTQyfGR6Uclm5kIaY8/VBzuVySaAycfOdBhjoyYhl2eMxdxz4NCb6pwNkl6KbjSfGHVpfR6fbuTihIbZFL1qAfYi6BnvrB9HgnfYItIA9VV2SlLIRfBgxgG9vxKmD83j9r5Dn/e1KC5PoMndMZ5I410H06kjR6dLJbTuy7+MAEQEAAcLAdgQYAQgAIAUCXjyeqwIbDBYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaon5UIAMJLU7EKjCsJ4ldIYYRYE7wzVO2DpiFY5yEVu/Jq2O2C+go7b2oBgUYYB3ExwMUwPnU8H6j+jbzukxOltKeRZ1QU1d9iwzYVHsP3kw02DceoyMaab0j1DLHPSVPSmsP5/U0eFK++vFvsY8KuwRUPLK+m7+qFET2gidyRSJGqyiJlEz+wyR3b44Ff1C3RDdRx8EzPVYx+gXRGLIBqsBNuSLhimiwAekMgnpAdsQu8WTCPQSO2Yrz6I6uFVNAr8FnnFoeclwLHbQ++aWhBTuhUp9oJ/388ySkEuMAZrd4YU/RwthM4zsW/VAf936FBkvi8FBJQ0Vdps1xksQeXK7hQrVw=
-1
View File
@@ -1 +0,0 @@
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
-1
View File
@@ -1 +0,0 @@
xsBNBF48nxEBCADRa0HRyoj7KdbchkVycHq5jYxYN4NJsRf1bojhuifuLYlJQu7I7Rp862sbvSPN9tM/3dQF6fTyUllFkhoS50fUaf/bJWwi5XWVsLa1+DdOW4As2zkJslu6Hp/hUFM2FXthRYIwg3c2jparkNtsLyQvtDsG62Acg7dr+NwZ5mxepyJ5WkXciOPLp9egcrTVoBbcnn4gzj2Wx6MkUByY+bENtUrqsZerWOt+F75DIWkHwzo5VwfpvH6RrIJabu8KLMLoUTZoovQcDKfk0yv8bdBLEQf84SLY+0BCZ6aZHrqnwEmDBeFGfTcIMFsBxUZfvsMYlAiG6khbZzhQxJ44+YaxABEBAAHNEzxlbGVuYUBleGFtcGxlLm5ldD7CwIkEEAEIADMCGQEFAl48ny0CGwMECwkIBwYVCAkKCwIDFgIBFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noIpsAgAiP+E48xEhCvEHVIMiQigQvC3kucg2TRk0yrp0ydDeS19l8jXkN8lA9byXVq5VDg8Bs4tN9WR/Gy8x9Xmd6/VyFiquBqGTObO/1u1kegHR0+sp5FxxffZhGoCRBIW4iXww1BzcGUd+TeRlInl5adGpA6vQAGifLsKQQU9/I0bSRBAMGbo0PKZ+EllTLqDeo4D4ELNO6CKyh65FExUIZTtbWZTyLn95ekypUIhgEAKgEs/qE74nJBqcrzkXn3xmDJetYxw2xQzwS+rIwR12ONub01nm/74Z30zbMViwQF4yTV7VB9uklY6WayCBpT3BkFQiYRPt7lnipBI2PplYXvYIs7ATQRePJ8RAQgAwvvNWB4eABzpUylyhI7q8WNK8GHCGLfqprSLFiAv14psluRW9MezLEFM1N8Az5yqzs3hsuEMiIBPiLrkW8ZkCledlYENorM/6G5+xK9TI4iWnP1LP+qESGGNSF7pXciZE7/XrE/CPL06nXuJRd1qOHvIUnaCQRiqLFPkUG3KhtC+/Ayvz6l2RvSqoTTayxYckgigkEneS/RXMSYnG/A5RJB0fOACp39HzHN/XU7JFLz8WlXgLfWJi7v9uVft9QBCtVseWF+ElKJ5NkVNRrV/SQwFYB91Y+LHqKz6CTQq2Di0vGHjY9ecai16D/CGUqkJg9t5jnLxZvQeR70o13JqlQARAQABwsB2BBgBCAAgBQJePJ8uAhsMFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noLj2gf9EuLnhKUv0rWW2HShJlKF5oqKmx6tSoNMOd5hbJzL1Z9d7ZjFQJS25jAzl9V6858heAm3HYTlJfg2RkzHte12k0hr9Svbpadf/OYB2UVsAwGRFGbdq/3H/ZaP5EbCdv8Egcx6pjdgvXb5n6gaMj7LJG4/YvokGLOx3VgNNRdqB0gtKWwN0tzdM/6YfDJTEVDbMUR3aFqYLTSB8KCjNtWcnWO2a17P1Qf6fDhcxpyELkLb1T4sPD4Tz9bFPTJzL87uS4+Ba+Xq2YE7aWa6e+C2HMgH6OghDLqoGpeNy01N3XSikyoFNdEmPYY/RdmuJNSuuyptQHgzh1vIOV7AOoQ+Rw==
-1
View File
@@ -1 +0,0 @@
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
-1
View File
@@ -1 +0,0 @@
xsBNBF48n2MBCAC741/YKzU76pWNijBCYC+hGSKZvBdanm4eJzi/eKPevUypdW2GJzFFJxWjx2wIcV8cLwQvY2Z+ieJaPPwGbUr0nH2S9lmghkCCKjGxWcmrx3sQr7x9431KYOM6+/SAZNjHYjWwcwy2QhE6J7qfC74LjTF4pv+ZRgSZDC+O70NpTWdNdwi/0Kn9gdj5diSwJmnBxQBGp/tnZuu3XGZrgKXlGlPspRMF3Ug2WmvyBhxF8KKoosiaXlumc5gFFuFRnoAVfp/6yehO44l9bYz0zwYWahxmLhShQVhfcH3YBg0l7hMkfC2Fp8fDF7Hr1PUOJcBY50VDN0zlV9Bi1iiPofAtABEBAAHNEzxmaW9uYUBleGFtcGxlLm5ldD7CwIkEEAEIADMCGQEFAl48n3wCGwMECwkIBwYVCAkKCwIDFgIBFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZUGWQgApCPQYmfa76xcscYBWg3DvMAm0Exk/LvbN74MIqADHIgaNFUHoThTPDVAPeh5ogra/kg4QaLctivC81S2VOPIc/4LGEFJekvythXUgSLNjhlR63Va5ObMV4UegUH9c0O8MGndkQFxiwy98D1bzNyl3mCVOUECVZiJJbxUZwBKj4iowa7kgX/FrdjZIJrz50NMExcDuxvc+MGyDr4YQHrZTbCjcrJkxufklwsWme0qneuPTxwW2+TnDjHkDaYDdwEnoKRkwlXEy3Lu9ZhswlaWTgdtClpf2geb7tCAPQ0cd2V5m6NnKMzXrnFuJXB6Xkvp32Tyuq4ebILsbCfpLV/HzM7ATQRePJ9jAQgAzQGckmcFjCViSkwGLROhi2Gvb85ABXbkKPMg1x27LraP9YWNJoq2GOy2vv8r8m2q+FpCR25a0NdWlbyiQpPuEWh0udJnNUm+6j5j6PSAmlRRDMhDH4QwzJC+B+lM6NxvSRhxBBtwFAvMy0qeNhv1UCBaeQUHoFCODqJRYOywj7ZGXdQyttIAlC5PtVw1cV+J8/TGXNrE9DNgFGAm1BPgw/lE1OjVbF8l6NbDdi9YJoOm9mpffUPlUZMZh3+a5+J6F3KjYwNyi4wi3Y3Pt4avXEo+ib15XNhJcwMslc49La2T8PMnpf3nLClxynJjYDiMuzujcbBWbfVF1383P0KikQARAQABwsB2BBgBCAAgBQJePJ98AhsMFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZX1eAf9GnrA9nCds3OmtCUpRmVEDar29DfS79B4vBfoYXYb5kRIOxJV6yjfGYRh2IJ0CTfNYkp4AuRC/jEHPXlVUD92Vcb0wSfwO32mMw75FRzIS7/IcwWAauWOtpao3J/tsxHWkSxfh5Zo6vzODQY4k8eHnitxpXT0xSIjnYmVRMuqfb3NELk3PkF52+Fte4zKfBCP80HqO3BdVCBlQNpZiOMW+yFO/8VqLmB9442nGGhuSfCeBystI3Er0SYSDqNbg5uetBaP1MOomIZuuxhv3T5jD9DeEDHGOqDjPZ8dMdQYCnYmiVaMb8ECsKG5eMvS8Imss1ho2iz4sjYdVzODl8T0zQ==
-1
View File
@@ -1 +0,0 @@
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
+1
View File
@@ -0,0 +1 @@
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
+1
View File
@@ -0,0 +1 @@
xsBNBF086ewBCACmJKuoxIO6T87yi4Q3MyNpMch3Y8KrtHDQyUszU36eqM3Pmd1lFrbcCd8ZWo2pq6OJSwsM/jjRGn1zo2YOaQeJRRrC+KrKGqSUtRSYQBPrPjE2YjSXAMbu8jBI6VVUhHeghriBkK79PY9O/oRhIJC0p14IJe6CQ6OI2fTmTUHF9i/nJ3G4Wb3/K1bU3yVfyPZjTZQPYPvvh03vxHULKurtYkX5DTEMSWsF4qzLMps+l87VuLV9iQnbN7YMRLHHx2KkX5Ivi7JCefhCa54M0K3bDCCxuVWAM5wjQwNZjzR+WL+dYchwoFvuF8NrlzjM9dSv+2rn+J7f99ijSXarzPC7ABEBAAHNEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl086fgCGwMECwkIBwYVCAkKCwIDFgIBFiEE+iaix4d0doj87Q0ek6DcNkbrcegACgkQk6DcNkbrcei3ogf/cruUmQ+th52TFHTHdkw9OHUl3MrXtZ7QmHyOAFvbXE/6n5Eeh+eZoF8MWWV72m14Wbs+vTcNQkFVTdOPptkKA8e4cJqwDOHsyAnvQXZ7WNje9+BMzcoipIUawHP4ORFaPDsKLZQ0b4wBbKn8ziea6zjGF0/qljTdoxTtsYpv5wXYuhwbYklrLOqgSa5M7LXUe7E3g9mbg+9iX1GuB8m6GkquJN814Y+xny4xhZzGOfue6SeP12jJMNSjSP7416dRq7794VGnkkW9V/7oFEUKu5wO9FFbgDySOSlEjByGejSGuBmho0iJSjcPjZ7EY/j3M3orq4dpza5C82OeSvxDFc7ATQRdPOnsAQgA5oLxXRLnyugzOmNCy7dxV3JrDZscA6JNlJlDWIShT0YSs+zG9JzDeQql+sYXgUSxOoIayItuXtnFn7tstwGoOnYvadm/e5/7V5fKAQRtCtdN51av62n18Venlm0yNKpROPcZ6M/sc4m6uU6YRZ/a1idal8VGY0wLKlghjIBuIiBoVQ/RnoW+/fhmwIg08dQ5m8hQe3GEOZEeLrTWL/9awPlXK7Y+DmJOoR4qbHWEcRfbzS6q4zW8vk2ztB8ngwbnqYy8zrN1DCICN1gYdlU++uVw6Bb1XfY8Cdldh1VLKpF35mAmjxLZfVDcoObFH3Cv2GB7BEYxv86KC2Y6T74Q/wARAQABwsB2BBgBCAAgBQJdPOn4AhsMFiEE+iaix4d0doj87Q0ek6DcNkbrcegACgkQk6DcNkbrcegLxwf/dXshJnoWqEnRsf6rVb9/Mc66ti+NVQLfNd275dybh/QIJdK3NmSxdnTPIEJRVspJywJoupwXFNrnHG2Ff6QPvHqI+/oNu986r9d7Z1oQibbLHKt8t6kpOfg/xGxotagJuiCQvR9mMjD1DqsdB37SjDxGupZOOJSXWi6KX60IE+uM+QOBfeOZziQwuFmA5wV6RDXIeYJfqrcbeXeR1d0nfNpPHQR1gBiqmxNb6KBbdXD2+EXW60axC7D2b1APmzMlammDliPwsK9U1rc9nuquEBvGDOJf4K+Dzn+mDCqRpP6uAuQ7RKHyim4uyN0wwKObzPqgJCGwjTglkixw+aSTXw==
+122 -2
View File
@@ -1,7 +1,12 @@
//! Stress some functions for testing; if used as a lib, this file is obsolete.
use std::collections::HashSet;
use std::sync::Arc;
use deltachat::config;
use deltachat::context::*;
use deltachat::keyring::*;
use deltachat::pgp;
use deltachat::Event;
use tempfile::{tempdir, TempDir};
@@ -90,18 +95,133 @@ fn stress_functions(context: &Context) {
// free(qr.cast());
}
#[test]
#[ignore] // is too expensive
fn test_encryption_decryption() {
let (public_key, private_key) = pgp::create_keypair("foo@bar.de").unwrap();
private_key.split_key().unwrap();
let (public_key2, private_key2) = pgp::create_keypair("two@zwo.de").unwrap();
assert_ne!(public_key, public_key2);
let original_text = b"This is a test";
let mut keyring = Keyring::default();
keyring.add_owned(public_key.clone());
keyring.add_ref(&public_key2);
let ctext_signed = pgp::pk_encrypt(original_text, &keyring, Some(&private_key)).unwrap();
assert!(!ctext_signed.is_empty());
assert!(ctext_signed.starts_with("-----BEGIN PGP MESSAGE-----"));
let ctext_unsigned = pgp::pk_encrypt(original_text, &keyring, None).unwrap();
assert!(!ctext_unsigned.is_empty());
assert!(ctext_unsigned.starts_with("-----BEGIN PGP MESSAGE-----"));
let mut keyring = Keyring::default();
keyring.add_owned(private_key);
let mut public_keyring = Keyring::default();
public_keyring.add_ref(&public_key);
let mut public_keyring2 = Keyring::default();
public_keyring2.add_owned(public_key2);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text,);
assert_eq!(valid_signatures.len(), 1);
valid_signatures.clear();
let empty_keyring = Keyring::default();
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&empty_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 0);
valid_signatures.clear();
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring2,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 0);
valid_signatures.clear();
public_keyring2.add_ref(&public_key);
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring2,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 1);
valid_signatures.clear();
let plain = pgp::pk_decrypt(
ctext_unsigned.as_bytes(),
&keyring,
&public_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
valid_signatures.clear();
let mut keyring = Keyring::default();
keyring.add_ref(&private_key2);
let mut public_keyring = Keyring::default();
public_keyring.add_ref(&public_key);
let plain = pgp::pk_decrypt(ctext_signed.as_bytes(), &keyring, &public_keyring, None).unwrap();
assert_eq!(plain, original_text);
}
fn cb(_context: &Context, _event: Event) {}
#[allow(dead_code)]
struct TestContext {
ctx: Context,
ctx: Arc<Context>,
dir: TempDir,
}
fn create_test_context() -> TestContext {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let ctx = Context::new(Box::new(cb), "FakeOs".into(), dbfile).unwrap();
let ctx = Arc::new(Context::new("FakeOs".into(), dbfile).unwrap());
let ctx_1 = ctx.clone();
std::thread::spawn(move || {
ctx_1.run(cb);
});
TestContext { ctx, dir }
}