mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
37 Commits
1.65.0
...
e2ee-force
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd81ecdb5d | ||
|
|
7f97768c56 | ||
|
|
ecd548a7aa | ||
|
|
62efb0795b | ||
|
|
53f042ee08 | ||
|
|
6ce97bd0cd | ||
|
|
c29149e74c | ||
|
|
487f7593ce | ||
|
|
6b3b33d2a0 | ||
|
|
2d70ccc2bf | ||
|
|
e90fc9504a | ||
|
|
5108314c03 | ||
|
|
f2b86a1c0f | ||
|
|
9583d41446 | ||
|
|
e594064f93 | ||
|
|
7c52fd95ec | ||
|
|
416bf3a829 | ||
|
|
cc78347293 | ||
|
|
27e4ae992a | ||
|
|
a1767dc153 | ||
|
|
eb610c27bf | ||
|
|
016fb2ceb2 | ||
|
|
5c571520a0 | ||
|
|
ddefd2cf09 | ||
|
|
30a3eeece8 | ||
|
|
5919388588 | ||
|
|
2cc738f481 | ||
|
|
babd405928 | ||
|
|
e885857875 | ||
|
|
a1d57a2645 | ||
|
|
a28aecd4d1 | ||
|
|
a3f1ff2827 | ||
|
|
c4d1a639b0 | ||
|
|
8732b7a55c | ||
|
|
1b9148f28e | ||
|
|
e0129c3b43 | ||
|
|
60d41022ea |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## 1.68.0
|
||||
|
||||
### Fixes
|
||||
- fix chat assignment when forwarding #2843
|
||||
- fix layout issues with the generated QR code svg #2842
|
||||
|
||||
|
||||
## 1.67.0
|
||||
|
||||
### API changes
|
||||
- `dc_get_securejoin_qr_svg(chat_id)` added #2815
|
||||
- added stock-strings `DC_STR_SETUP_CONTACT_QR_DESC` and `DC_STR_SECURE_JOIN_GROUP_QR_DESC`
|
||||
|
||||
|
||||
## 1.66.0
|
||||
|
||||
### API changes
|
||||
- `dc_contact_get_last_seen()` added #2823
|
||||
- python: `Contact.last_seen` added #2823
|
||||
- removed `DC_STR_NEWGROUPDRAFT`, we don't set draft after creating group anymore #2805
|
||||
|
||||
### Changes
|
||||
- python: add cutil.from_optional_dc_charpointer() #2824
|
||||
- refactorings #2807 #2822 #2825
|
||||
|
||||
|
||||
## 1.65.0
|
||||
|
||||
### Changes
|
||||
|
||||
216
Cargo.lock
generated
216
Cargo.lock
generated
@@ -114,9 +114,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.45"
|
||||
version = "1.0.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
|
||||
checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
@@ -372,7 +372,6 @@ dependencies = [
|
||||
"libc",
|
||||
"pin-project",
|
||||
"redox_syscall",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -469,21 +468,6 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bitfield"
|
||||
version = "0.13.2"
|
||||
@@ -722,7 +706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"textwrap",
|
||||
"textwrap 0.11.0",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
@@ -1072,7 +1056,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.65.0"
|
||||
version = "1.68.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1100,8 +1084,6 @@ dependencies = [
|
||||
"hex",
|
||||
"humansize",
|
||||
"image",
|
||||
"indexmap",
|
||||
"itertools",
|
||||
"kamadak-exif",
|
||||
"lettre_email",
|
||||
"libc",
|
||||
@@ -1114,9 +1096,9 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pgp",
|
||||
"pretty_assertions",
|
||||
"pretty_env_logger",
|
||||
"proptest",
|
||||
"qrcodegen",
|
||||
"quick-xml",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
@@ -1135,7 +1117,9 @@ dependencies = [
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"surf",
|
||||
"tagger",
|
||||
"tempfile",
|
||||
"textwrap 0.14.2",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"url",
|
||||
@@ -1152,7 +1136,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.65.0"
|
||||
version = "1.68.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
@@ -1201,12 +1185,6 @@ dependencies = [
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
@@ -1573,9 +1551,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
|
||||
checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1588,9 +1566,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
|
||||
checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -1598,15 +1576,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
|
||||
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
|
||||
checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -1615,9 +1593,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
|
||||
checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -1636,12 +1614,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
|
||||
checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1",
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -1649,23 +1625,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
|
||||
checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
|
||||
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
|
||||
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
@@ -1675,8 +1650,6 @@ dependencies = [
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"proc-macro-hack",
|
||||
"proc-macro-nested",
|
||||
"slab",
|
||||
]
|
||||
|
||||
@@ -1959,16 +1932,6 @@ dependencies = [
|
||||
"nom 6.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "infer"
|
||||
version = "0.2.3"
|
||||
@@ -2104,9 +2067,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.106"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673"
|
||||
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -2116,9 +2079,9 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.22.2"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d"
|
||||
checksum = "abd5850c449b40bacb498b2bbdfaff648b1b055630073ba8db499caf2d0ea9f2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
@@ -2517,15 +2480,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "output_vt100"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packed_simd"
|
||||
version = "0.3.3"
|
||||
@@ -2741,18 +2695,6 @@ version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0cfe1b2403f172ba0f234e500906ee0a3e493fb81092dac23ebefe129301cc"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"ctor",
|
||||
"diff",
|
||||
"output_vt100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_env_logger"
|
||||
version = "0.4.0"
|
||||
@@ -2793,12 +2735,6 @@ version = "0.5.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-nested"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.32"
|
||||
@@ -2814,7 +2750,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
@@ -2824,10 +2759,14 @@ dependencies = [
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
"rusty-fork",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcodegen"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135e6754eed8ca897dd70584d895e72e36860b3e163b6bcedce48571cbaef343"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
@@ -2877,9 +2816,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "r2d2_sqlite"
|
||||
version = "0.18.0"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d24607049214c5e42d3df53ac1d8a23c34cc6a5eefe3122acb2c72174719959"
|
||||
checksum = "54ca3c9468a76fc2ad724c486a59682fc362efeac7b18d1c012958bc19f34800"
|
||||
dependencies = [
|
||||
"r2d2",
|
||||
"rusqlite",
|
||||
@@ -3113,9 +3052,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.25.3"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57adcf67c8faaf96f3248c2a7b419a0dbc52ebe36ba83dd57fe83827c1ea4eb3"
|
||||
checksum = "8a82b0b91fad72160c56bf8da7a549b25d7c31109f52cc1437eac4c0ad2550a7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
@@ -3157,16 +3096,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusty-fork"
|
||||
version = "0.3.0"
|
||||
name = "rustversion"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"quick-error 1.2.3",
|
||||
"tempfile",
|
||||
"wait-timeout",
|
||||
]
|
||||
checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
|
||||
|
||||
[[package]]
|
||||
name = "rustyline"
|
||||
@@ -3324,9 +3257,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.69"
|
||||
version = "1.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8"
|
||||
checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -3457,6 +3390,12 @@ version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.3.19"
|
||||
@@ -3590,19 +3529,20 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.22.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e"
|
||||
checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.22.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb"
|
||||
checksum = "00ad150e9d51e33e8142984f577662c1324d49f3be45ed37bac8645fdcbe0fe5"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -3657,6 +3597,12 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagger"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c933e626196d509b053f49573e35ad237c0ff6a36c23c1aa61ffeab49251d24f"
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
@@ -3695,6 +3641,17 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.30"
|
||||
@@ -3892,6 +3849,15 @@ version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.19"
|
||||
@@ -3986,15 +3952,6 @@ version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.1.0"
|
||||
@@ -4187,15 +4144,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.3.0"
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.65.0"
|
||||
version = "1.68.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -22,7 +22,7 @@ async-native-tls = { version = "0.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
|
||||
async-std-resolver = "0.20"
|
||||
async-std = { version = "1", features = ["unstable"] }
|
||||
async-tar = "0.4"
|
||||
async-tar = { version = "0.4", default-features=false }
|
||||
async-trait = "0.1"
|
||||
backtrace = "0.3"
|
||||
base64 = "0.13"
|
||||
@@ -36,8 +36,6 @@ escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
indexmap = "1.7"
|
||||
itertools = "0.10"
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
@@ -53,10 +51,10 @@ pgp = { version = "0.7", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.22"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.18"
|
||||
r2d2_sqlite = "0.19"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
rusqlite = "0.25"
|
||||
rusqlite = "0.26"
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "9.0", optional = true }
|
||||
sanitize-filename = "0.3"
|
||||
@@ -66,8 +64,8 @@ sha-1 = "0.9"
|
||||
sha2 = "0.9"
|
||||
smallvec = "1"
|
||||
stop-token = "0.6"
|
||||
strum = "0.22"
|
||||
strum_macros = "0.22"
|
||||
strum = "0.23"
|
||||
strum_macros = "0.23"
|
||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
@@ -75,6 +73,9 @@ url = "2"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
fast-socks5 = "0.4"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "3.2.1"
|
||||
textwrap = "0.14.2"
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
@@ -82,9 +83,8 @@ async-std = { version = "1", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_assertions = "1.0"
|
||||
pretty_env_logger = "0.4"
|
||||
proptest = "1"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
|
||||
[workspace]
|
||||
|
||||
10
assets/qrcode_logo_footer.svg
Normal file
10
assets/qrcode_logo_footer.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:bold;font-size:24.4118px;line-height:1.25;font-family:sans-serif;fill:#aaaaaa;fill-opacity:1;stroke:none;stroke-width:0.915439"
|
||||
x="42.325161"
|
||||
y="23.32255"
|
||||
id="text72398">get.delta.chat</text>
|
||||
<path
|
||||
id="path84310"
|
||||
style="opacity:0.25;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.915439"
|
||||
d="M 17.13769,0.00129321 C 7.6753075,0.11650893 0,7.8915283 0,17.362467 c 0,9.47094 7.6753075,17.059745 17.13769,16.944599 8.99669,-0.03598 6.880074,-5.025654 16.824785,-0.405885 -5.447648,-8.510047 0.184241,-9.642482 0.311117,-16.955289 0,-9.4709395 -7.673512,-17.0597453 -17.135895,-16.94459879 z M 17.0769,4.9986797 c 1.84214,0 3.447355,0.253959 4.815003,0.7616693 1.381603,0.5076411 2.072253,1.207862 2.072253,2.0990711 0,0.4286855 -0.167495,0.7836052 -0.50242,1.0656242 -0.334921,0.2819844 -0.724544,0.4237724 -1.171121,0.4237724 -0.641952,0 -1.396532,-0.3909376 -2.261778,-1.169353 C 19.14963,7.3898036 18.402555,6.83791 17.788507,6.5220182 17.188416,6.1950547 16.484552,6.0321266 15.675129,6.0321266 c -1.032717,0 -1.883352,0.1854523 -2.553215,0.5578447 -0.655913,0.372254 -0.98517,0.8460916 -0.98517,1.4214436 0,0.5414792 0.272815,1.0495355 0.817093,1.5233385 0.544275,0.4738026 1.946291,1.3367446 4.207097,2.5889976 2.414319,1.342419 4.117377,2.390985 5.108232,3.146807 1.004795,0.755857 1.821505,1.675853 2.449514,2.758846 0.628002,1.082993 0.942253,2.227607 0.942253,3.434674 0,2.120834 -0.929555,3.993314 -2.785656,5.617786 -1.84214,1.613228 -3.99694,2.41915 -6.467082,2.41915 -2.246845,0 -4.145607,-0.647976 -5.694677,-1.945312 -1.5490699,-1.297336 -2.3225722,-3.028063 -2.3225722,-5.194049 0,-2.087031 0.8506345,-3.83094 2.5532182,-5.229825 1.716541,-1.398884 3.824203,-2.245599 6.322256,-2.538897 -0.697774,-0.631749 -1.668763,-1.387225 -2.910816,-2.267155 -1.367648,-0.970199 -2.287914,-1.73045 -2.762402,-2.283243 -0.474491,-0.5640381 -0.711618,-1.1795944 -0.711618,-1.8451814 0,-0.9927581 0.572093,-1.7710351 1.716451,-2.3351077 1.144362,-0.5753173 2.636724,-0.8635642 4.478865,-0.8635642 z m 1.110327,10.3738083 c -4.005262,0.5302 -6.007576,2.75279 -6.007576,6.667322 0,2.01932 0.49495,3.587291 1.485805,4.704157 1.004806,1.116832 2.169696,1.675299 3.495479,1.675299 1.381602,0 2.520072,-0.535632 3.413229,-1.60738 0.893168,-1.082959 1.339187,-2.545264 1.339187,-4.384079 0,-2.662348 -1.242022,-5.013441 -3.726124,-7.055319 z" />
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.65.0"
|
||||
version = "1.68.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -287,6 +287,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* To save traffic, however, the avatar is attached only as needed
|
||||
* and also recoded to a reasonable size.
|
||||
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
|
||||
* - `e2ee_force` = 1=ignore encryption preferences of others,
|
||||
* 0=use majority vote when deciding whether to encrypt (default).
|
||||
* - `mdns_enabled` = 0=do not send or request read receipts,
|
||||
* 1=send and request read receipts (default)
|
||||
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
|
||||
@@ -2243,6 +2245,20 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
char* dc_get_securejoin_qr (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Get QR code image from the QR code text generated by dc_get_securejoin_qr().
|
||||
* See dc_get_securejoin_qr() for details about the contained QR code.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id group-chat-id for secure-join or 0 for setup-contact,
|
||||
* see dc_get_securejoin_qr() for details.
|
||||
* @return SVG-Image with the QR code.
|
||||
* On errors, an empty string is returned.
|
||||
* The returned string must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
/**
|
||||
* Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
* started on another device with dc_get_securejoin_qr().
|
||||
@@ -4382,6 +4398,16 @@ uint32_t dc_contact_get_color (const dc_contact_t* contact);
|
||||
*/
|
||||
char* dc_contact_get_status (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Get the contact's last seen timestamp.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return Last seen timestamp.
|
||||
* 0 on error or if the contact was never seen.
|
||||
*/
|
||||
int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Check if a contact is blocked.
|
||||
*
|
||||
@@ -5613,12 +5639,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// if nothing else is set by the dc_set_config()-option `selfstatus`.
|
||||
#define DC_STR_STATUSLINE 13
|
||||
|
||||
/// "Hi, i've created the group %1$s for us."
|
||||
///
|
||||
/// Used as a draft text after group creation.
|
||||
/// - %1$s will be replaced by the group name
|
||||
#define DC_STR_NEWGROUPDRAFT 14
|
||||
|
||||
/// "Group name changed from %1$s to %2$s."
|
||||
///
|
||||
/// Used in status messages for group name changes.
|
||||
@@ -6073,6 +6093,20 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by the name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_REPLIES 118
|
||||
|
||||
/// "Scan to chat with %1$s"
|
||||
///
|
||||
/// Subtitle for verification qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the inviter.
|
||||
#define DC_STR_SETUP_CONTACT_QR_DESC 119
|
||||
|
||||
/// "Scan to join %1$s"
|
||||
///
|
||||
/// Subtitle for group join qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced with the group name.
|
||||
#define DC_STR_SECURE_JOIN_GROUP_QR_DESC 120
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,7 @@ use std::time::{Duration, SystemTime};
|
||||
use anyhow::Context as _;
|
||||
use async_std::sync::RwLock;
|
||||
use async_std::task::{block_on, spawn};
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
|
||||
@@ -2075,6 +2076,27 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_securejoin_qr_svg(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to generate_verification_qr()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
let chat_id = if chat_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(get_securejoin_qr_svg(ctx, chat_id))
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_join_securejoin(
|
||||
context: *mut dc_context_t,
|
||||
@@ -3544,6 +3566,16 @@ pub unsafe extern "C" fn dc_contact_get_status(contact: *mut dc_contact_t) -> *m
|
||||
ffi_contact.contact.get_status().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) -> i64 {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_last_seen()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.last_seen()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
|
||||
33
draft/aeap-mvp.rst
Normal file
33
draft/aeap-mvp.rst
Normal file
@@ -0,0 +1,33 @@
|
||||
AEAP MVP
|
||||
========
|
||||
|
||||
Changes to the UIs
|
||||
------------------
|
||||
|
||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
||||
|
||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
||||
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- No changes for 1:1 chats, there simply is a new one
|
||||
|
||||
- When we send a message to a group, and the primary address is not a member of a group, but a secondary address is:
|
||||
|
||||
Add Chat-Group-Member-Removed=<old address> and Chat-Group-Member-Added=<new address> headers to this message
|
||||
|
||||
- On the receiving side, make sure that we accept this (even in verified groups) if the message is signed and the key stayed the same
|
||||
|
||||
Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
@@ -423,6 +423,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
listblocked\n\
|
||||
======================================Misc.==\n\
|
||||
getqr [<chat-id>]\n\
|
||||
getqrsvg [<chat-id>]\n\
|
||||
getbadqr\n\
|
||||
checkqr <qr-content>\n\
|
||||
joinqr <qr-content>\n\
|
||||
@@ -754,7 +755,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"groupname" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
chat::set_chat_name(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
|
||||
chat::set_chat_name(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
&format!("{} {}", arg1, arg2).trim(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Chat name set");
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ use rustyline::{
|
||||
|
||||
mod cmdline;
|
||||
use self::cmdline::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use std::fs;
|
||||
|
||||
/// Event Handler
|
||||
fn receive_event(event: EventType) {
|
||||
@@ -224,8 +226,9 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 11] = [
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
@@ -427,6 +430,20 @@ async fn handle_cmd(
|
||||
io::stderr().write_all(&output.stderr).unwrap();
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&ctx, group).await {
|
||||
Ok(svg) => {
|
||||
fs::write(&file, svg)?;
|
||||
println!("QR code svg written to: {:#?}", file);
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
"joinqr" => {
|
||||
ctx.start_io().await;
|
||||
if !arg0.is_empty() {
|
||||
|
||||
@@ -8,7 +8,7 @@ import os
|
||||
from array import array
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array, DCLot
|
||||
from .chat import Chat
|
||||
from .message import Message
|
||||
from .contact import Contact
|
||||
@@ -79,7 +79,7 @@ class Account(object):
|
||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||
name, self._configkeys))
|
||||
|
||||
def get_info(self):
|
||||
def get_info(self) -> Dict[str, str]:
|
||||
""" return dictionary of built config parameters. """
|
||||
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||
d = {}
|
||||
@@ -135,7 +135,7 @@ class Account(object):
|
||||
valuebytes = ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
|
||||
|
||||
def get_config(self, name: str):
|
||||
def get_config(self, name: str) -> str:
|
||||
""" return unicode string value.
|
||||
|
||||
:param name: configuration key to lookup (eg "addr" or "mail_pw")
|
||||
@@ -200,11 +200,9 @@ class Account(object):
|
||||
""" return the latest backup file in a given directory.
|
||||
"""
|
||||
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
|
||||
if res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(res)
|
||||
return from_optional_dc_charpointer(res)
|
||||
|
||||
def get_blobdir(self) -> Optional[str]:
|
||||
def get_blobdir(self) -> str:
|
||||
""" return the directory for files.
|
||||
|
||||
All sent files are copied to this directory if necessary.
|
||||
@@ -477,7 +475,7 @@ class Account(object):
|
||||
def imex(self, path, imex_cmd):
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
|
||||
def initiate_key_transfer(self):
|
||||
def initiate_key_transfer(self) -> str:
|
||||
"""return setup code after a Autocrypt setup message
|
||||
has been successfully sent to our own e-mail address ("self-sent message").
|
||||
If sending out was unsuccessful, a RuntimeError is raised.
|
||||
@@ -488,7 +486,7 @@ class Account(object):
|
||||
raise RuntimeError("could not send out autocrypt setup message")
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_setup_contact_qr(self) -> Optional[str]:
|
||||
def get_setup_contact_qr(self) -> str:
|
||||
""" get/create Setup-Contact QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
@@ -584,7 +582,7 @@ class Account(object):
|
||||
def get_connectivity(self):
|
||||
return lib.dc_get_connectivity(self._dc_context)
|
||||
|
||||
def get_connectivity_html(self):
|
||||
def get_connectivity_html(self) -> str:
|
||||
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
|
||||
|
||||
def all_work_done(self):
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
""" Contact object. """
|
||||
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from . import const, props
|
||||
from .capi import ffi, lib
|
||||
from .chat import Chat
|
||||
from . import const
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer
|
||||
|
||||
|
||||
class Contact(object):
|
||||
@@ -35,18 +37,25 @@ class Contact(object):
|
||||
)
|
||||
|
||||
@props.with_doc
|
||||
def addr(self):
|
||||
def addr(self) -> str:
|
||||
""" normalized e-mail address for this account. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||
|
||||
@props.with_doc
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
""" display name for this contact. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||
|
||||
# deprecated alias
|
||||
display_name = name
|
||||
|
||||
@props.with_doc
|
||||
def last_seen(self) -> date:
|
||||
"""Last seen timestamp."""
|
||||
return datetime.fromtimestamp(
|
||||
lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc
|
||||
)
|
||||
|
||||
def is_blocked(self):
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
@@ -67,15 +76,13 @@ class Contact(object):
|
||||
""" Return True if the contact is verified. """
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
|
||||
def get_profile_image(self):
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
:returns: path to profile image, None if no profile image exists.
|
||||
"""
|
||||
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
|
||||
if dc_res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
return from_optional_dc_charpointer(dc_res)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
|
||||
@@ -19,7 +19,13 @@ def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None
|
||||
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
||||
|
||||
|
||||
def from_dc_charpointer(obj) -> Optional[str]:
|
||||
def from_dc_charpointer(obj) -> str:
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
raise ValueError
|
||||
|
||||
|
||||
def from_optional_dc_charpointer(obj) -> Optional[str]:
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
return None
|
||||
|
||||
@@ -9,7 +9,7 @@ from .hookspec import account_hookimpl
|
||||
from contextlib import contextmanager
|
||||
from .capi import ffi, lib
|
||||
from .message import map_system_message
|
||||
from .cutil import from_dc_charpointer
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
|
||||
|
||||
class FFIEvent:
|
||||
@@ -235,7 +235,7 @@ class EventThread(threading.Thread):
|
||||
# function which provides us signature info of an event call
|
||||
evt_name = deltachat.get_dc_event_name(evt)
|
||||
if lib.dc_event_has_string_data(evt):
|
||||
data2 = from_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
else:
|
||||
data2 = lib.dc_event_get_data2_int(event)
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import os
|
||||
import re
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Message(object):
|
||||
@@ -75,7 +76,7 @@ class Message(object):
|
||||
return lib.dc_msg_get_id(self._dc_msg)
|
||||
|
||||
@props.with_doc
|
||||
def text(self):
|
||||
def text(self) -> str:
|
||||
"""unicode text of this messages (might be empty if not a text message). """
|
||||
return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg))
|
||||
|
||||
@@ -84,9 +85,9 @@ class Message(object):
|
||||
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
|
||||
|
||||
@props.with_doc
|
||||
def html(self):
|
||||
def html(self) -> str:
|
||||
"""html text of this messages (might be empty if not an html message). """
|
||||
return from_dc_charpointer(
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
|
||||
|
||||
def has_html(self):
|
||||
@@ -113,12 +114,13 @@ class Message(object):
|
||||
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
||||
|
||||
@props.with_doc
|
||||
def basename(self):
|
||||
def basename(self) -> str:
|
||||
"""basename of the attachment if it exists, otherwise empty string. """
|
||||
# FIXME, it does not return basename
|
||||
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
||||
|
||||
@props.with_doc
|
||||
def filemime(self):
|
||||
def filemime(self) -> str:
|
||||
"""mime type of the file (if it exists)"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
@@ -130,7 +132,7 @@ class Message(object):
|
||||
""" return True if this message is a setup message. """
|
||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||
|
||||
def get_setupcodebegin(self):
|
||||
def get_setupcodebegin(self) -> str:
|
||||
""" return the first characters of a setup code in a setup message. """
|
||||
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
|
||||
|
||||
@@ -146,7 +148,7 @@ class Message(object):
|
||||
""" return True if this message was forwarded. """
|
||||
return bool(lib.dc_msg_is_forwarded(self._dc_msg))
|
||||
|
||||
def get_message_info(self):
|
||||
def get_message_info(self) -> str:
|
||||
""" Return informational text for a single message.
|
||||
|
||||
The text is multiline and may contain eg. the raw text of the message.
|
||||
@@ -203,11 +205,11 @@ class Message(object):
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@property
|
||||
def quoted_text(self):
|
||||
def quoted_text(self) -> Optional[str]:
|
||||
"""Text inside the quote
|
||||
|
||||
:returns: Quoted text"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
|
||||
@property
|
||||
def quote(self):
|
||||
@@ -240,9 +242,9 @@ class Message(object):
|
||||
return email.message_from_string(s)
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
|
||||
@property
|
||||
def chat(self):
|
||||
@@ -255,12 +257,12 @@ class Message(object):
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
@props.with_doc
|
||||
def override_sender_name(self):
|
||||
def override_sender_name(self) -> Optional[str]:
|
||||
"""the name that should be shown over the message instead of the contact display name.
|
||||
|
||||
Usually used to impersonate someone else.
|
||||
"""
|
||||
return from_dc_charpointer(
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_msg_get_override_sender_name(self._dc_msg))
|
||||
|
||||
def set_override_sender_name(self, name):
|
||||
|
||||
@@ -24,19 +24,19 @@ class Provider(object):
|
||||
self._provider = provider
|
||||
|
||||
@property
|
||||
def overview_page(self):
|
||||
def overview_page(self) -> str:
|
||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_overview_page(self._provider))
|
||||
|
||||
@property
|
||||
def get_before_login_hints(self):
|
||||
def get_before_login_hints(self) -> str:
|
||||
"""Should be shown to the user on login."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_before_login_hints(self._provider))
|
||||
lib.dc_provider_get_before_login_hint(self._provider))
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
def status(self) -> int:
|
||||
"""The status of the provider information.
|
||||
|
||||
This is one of the
|
||||
|
||||
@@ -265,23 +265,23 @@ 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.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
|
||||
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
|
||||
ac1._evtracker.consume_events()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %2$s")
|
||||
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
|
||||
assert chat.get_name() == "title1"
|
||||
assert contact1 in chat.get_contacts()
|
||||
assert contact2 in chat.get_contacts()
|
||||
assert not chat.is_promoted()
|
||||
msg = chat.get_draft()
|
||||
assert msg.text == "xyz title1"
|
||||
chat = ac1.create_group_chat(name="homework", contacts=[])
|
||||
assert chat.get_name() == "homework"
|
||||
chat.send_text("Now we have a group for homework")
|
||||
assert chat.is_promoted()
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
|
||||
@@ -19,11 +19,9 @@ export DCC_RS_TARGET=release
|
||||
|
||||
# Configure access to a base python and to several python interpreters
|
||||
# needed by tox below.
|
||||
export PATH=$PATH:/opt/python/cp36-cp36m/bin
|
||||
export PATH=$PATH:/opt/python/cp37-cp37m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
pushd /bin
|
||||
rm -f python3.6
|
||||
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||
rm -f python3.7
|
||||
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||
rm -f python3.8
|
||||
|
||||
100
src/chat.rs
100
src/chat.rs
@@ -7,7 +7,6 @@ use std::time::{Duration, SystemTime};
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
@@ -2267,7 +2266,6 @@ pub async fn create_group_chat(
|
||||
let chat_name = improve_single_line_input(chat_name);
|
||||
ensure!(!chat_name.is_empty(), "Invalid chat name");
|
||||
|
||||
let draft_txt = stock_str::new_group_draft(context, &chat_name).await;
|
||||
let grpid = dc_create_id();
|
||||
|
||||
let row_id = context
|
||||
@@ -2288,9 +2286,6 @@ pub async fn create_group_chat(
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await? {
|
||||
add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await?;
|
||||
let mut draft_msg = Message::new(Viewtype::Text);
|
||||
draft_msg.set_text(Some(draft_txt));
|
||||
chat_id.set_draft_raw(context, &mut draft_msg).await?;
|
||||
}
|
||||
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
@@ -2821,7 +2816,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.query_map(
|
||||
format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
msg_ids.iter().map(|_| "?").join(",")
|
||||
msg_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
|
||||
),
|
||||
rusqlite::params_from_iter(msg_ids),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
@@ -2851,6 +2846,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||
msg.in_reply_to = None;
|
||||
|
||||
// do not leak data as group names; a default subject is generated by mimfactory
|
||||
msg.subject = "".to_string();
|
||||
@@ -4296,6 +4292,98 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_forward_quote() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
// Alice sends a message to Bob.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
|
||||
// Bob quotes received message and sends a reply to Alice.
|
||||
let mut reply = Message::new(Viewtype::Text);
|
||||
reply.set_text(Some("Reply".to_owned()));
|
||||
reply.set_quote(&bob, &received_msg).await?;
|
||||
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
|
||||
alice.recv_msg(&sent_reply).await;
|
||||
let received_reply = alice.get_last_msg().await;
|
||||
|
||||
// Alice forwards a reply.
|
||||
forward_msgs(&alice, &[received_reply.id], alice_chat.get_id()).await?;
|
||||
let forwarded_msg = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&forwarded_msg).await;
|
||||
|
||||
let alice_forwarded_msg = alice.get_last_msg().await;
|
||||
assert!(alice_forwarded_msg.quoted_message(&alice).await?.is_none());
|
||||
assert_eq!(
|
||||
alice_forwarded_msg.quoted_text(),
|
||||
Some("Hi Bob".to_string())
|
||||
);
|
||||
|
||||
let bob_forwarded_msg = bob.get_last_msg().await;
|
||||
assert!(bob_forwarded_msg.quoted_message(&bob).await?.is_none());
|
||||
assert_eq!(bob_forwarded_msg.quoted_text(), Some("Hi Bob".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_forward_group() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
// Alice creates a group with Bob.
|
||||
let alice_group_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "Claire", "claire@example.net").await?;
|
||||
add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(&alice, alice_group_chat_id, claire_id).await?;
|
||||
let sent_group_msg = alice
|
||||
.send_text(alice_group_chat_id, "Hi Bob and Claire")
|
||||
.await;
|
||||
bob.recv_msg(&sent_group_msg).await;
|
||||
let bob_group_chat_id = bob.get_last_msg().await.chat_id;
|
||||
|
||||
// Alice deletes a message on her device.
|
||||
// This is needed to make assignment of further messages received in this group
|
||||
// based on `References:` header harder.
|
||||
// Previously this exposed a bug, so this is a regression test.
|
||||
message::delete_msgs(&alice, &[sent_group_msg.sender_msg_id]).await?;
|
||||
|
||||
// Alice sends a message to Bob.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
assert_eq!(received_msg.get_text(), Some("Hi Bob".to_string()));
|
||||
assert_eq!(received_msg.chat_id, bob_chat.id);
|
||||
|
||||
// Alice sends another message to Bob, this has first message as a parent.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hello Bob").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
assert_eq!(received_msg.get_text(), Some("Hello Bob".to_string()));
|
||||
assert_eq!(received_msg.chat_id, bob_chat.id);
|
||||
|
||||
// Bob forwards message to a group chat with Alice.
|
||||
forward_msgs(&bob, &[received_msg.id], bob_group_chat_id).await?;
|
||||
let forwarded_msg = bob.pop_sent_msg().await;
|
||||
alice.recv_msg(&forwarded_msg).await;
|
||||
|
||||
let received_forwarded_msg = alice.get_last_msg_in(alice_group_chat_id).await;
|
||||
assert!(received_forwarded_msg.is_forwarded());
|
||||
assert_eq!(received_forwarded_msg.chat_id, alice_group_chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_only_minimal_data_are_forwarded() -> Result<()> {
|
||||
// send a message from Alice to a group with Bob
|
||||
|
||||
@@ -399,10 +399,20 @@ mod tests {
|
||||
assert_eq!(chats.get_chat_id(1), chat_id2);
|
||||
assert_eq!(chats.get_chat_id(2), chat_id1);
|
||||
|
||||
// drafts are sorted to the top
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
// New drafts are sorted to the top
|
||||
// We have to set a draft on the other two messages, too, as
|
||||
// chat timestamps are only exact to the second and sorting by timestamp
|
||||
// would not work.
|
||||
// Message timestamps are "smeared" and unique, so we don't have this problem
|
||||
// if we have any message (can be a draft) in all chats.
|
||||
// Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
|
||||
// 2s here.
|
||||
for chat_id in &[chat_id1, chat_id3, chat_id2] {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
}
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ pub(crate) fn str_to_color(s: &str) -> u32 {
|
||||
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
|
||||
}
|
||||
|
||||
pub fn color_int_to_hex_string(color: u32) -> String {
|
||||
format!("{:#08x}", color).replace("0x", "#")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -64,6 +64,13 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
E2eeEnabled,
|
||||
|
||||
/// Ignore Autocrypt recommendation for message encryption if possible.
|
||||
///
|
||||
/// The only expection is when recommendation is "disable", i.e. encryption is not possible
|
||||
/// because some recipient has no OpenPGP key.
|
||||
#[strum(props(default = "0"))]
|
||||
E2eeForce,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
MdnsEnabled,
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ mod server_params;
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use itertools::Itertools;
|
||||
use job::Action;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
@@ -682,7 +681,11 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
|
||||
return first_err.msg.to_string();
|
||||
}
|
||||
|
||||
errors.iter().map(|e| e.to_string()).join("\n\n")
|
||||
errors
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@@ -5,7 +5,6 @@ use std::convert::{TryFrom, TryInto};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::path::PathBuf;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
@@ -67,6 +66,9 @@ pub struct Contact {
|
||||
/// Blocked state. Use dc_contact_is_blocked to access this field.
|
||||
pub blocked: bool,
|
||||
|
||||
/// Time when the contact was seen last time, Unix time in seconds.
|
||||
last_seen: i64,
|
||||
|
||||
/// The origin/source of the contact.
|
||||
pub origin: Origin,
|
||||
|
||||
@@ -185,7 +187,8 @@ impl Contact {
|
||||
let mut contact = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
|
||||
c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
@@ -194,15 +197,17 @@ impl Contact {
|
||||
let addr: String = row.get(1)?;
|
||||
let origin: Origin = row.get(2)?;
|
||||
let blocked: Option<bool> = row.get(3)?;
|
||||
let authname: String = row.get(4)?;
|
||||
let param: String = row.get(5)?;
|
||||
let status: Option<String> = row.get(6)?;
|
||||
let last_seen: i64 = row.get(4)?;
|
||||
let authname: String = row.get(5)?;
|
||||
let param: String = row.get(6)?;
|
||||
let status: Option<String> = row.get(7)?;
|
||||
let contact = Self {
|
||||
id: contact_id,
|
||||
name,
|
||||
authname,
|
||||
addr,
|
||||
blocked: blocked.unwrap_or_default(),
|
||||
last_seen,
|
||||
origin,
|
||||
param: param.parse().unwrap_or_default(),
|
||||
status: status.unwrap_or_default(),
|
||||
@@ -234,6 +239,11 @@ impl Contact {
|
||||
self.blocked
|
||||
}
|
||||
|
||||
/// Returns last seen timestamp.
|
||||
pub fn last_seen(&self) -> i64 {
|
||||
self.last_seen
|
||||
}
|
||||
|
||||
/// Check if a contact is blocked.
|
||||
pub async fn is_blocked_load(context: &Context, id: u32) -> Result<bool> {
|
||||
let blocked = Self::load_from_db(context, id).await?.blocked;
|
||||
@@ -1289,6 +1299,27 @@ pub(crate) async fn set_status(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates last seen timestamp of the contact if it is earlier than the given `timestamp`.
|
||||
pub(crate) async fn update_last_seen(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
||||
"Can not update special contact last seen timestamp"
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
|
||||
paramsv![timestamp, contact_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
@@ -1356,15 +1387,13 @@ pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
|
||||
|
||||
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
book.lines()
|
||||
.collect::<Vec<&str>>()
|
||||
.chunks(2)
|
||||
.into_iter()
|
||||
.filter_map(|mut chunk| {
|
||||
let name = chunk.next().unwrap();
|
||||
let addr = match chunk.next() {
|
||||
Some(a) => a,
|
||||
None => return None,
|
||||
};
|
||||
Some((name, addr))
|
||||
.filter_map(|chunk| {
|
||||
let name = chunk.get(0)?;
|
||||
let addr = chunk.get(1)?;
|
||||
Some((*name, *addr))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1377,6 +1406,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
@@ -2034,4 +2064,34 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_last_seen() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
|
||||
.await?;
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), 0);
|
||||
|
||||
let mime = br#"Subject: Hello
|
||||
Message-ID: message@example.net
|
||||
To: Alice <alice@example.com>
|
||||
From: Bob <bob@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
Chat-Version: 1.0
|
||||
Date: Sun, 22 Mar 2020 22:37:55 +0000
|
||||
|
||||
Hi."#;
|
||||
dc_receive_imf(&alice, mime, "Inbox", 1, false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
let timestamp = msg.get_timestamp();
|
||||
assert!(timestamp > 0);
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), timestamp);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +307,7 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||
let e2ee_force = self.get_config_int(Config::E2eeForce).await?;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
|
||||
@@ -394,6 +395,7 @@ impl Context {
|
||||
res.insert("configured_mvbox_folder", configured_mvbox_folder);
|
||||
res.insert("mdns_enabled", mdns_enabled.to_string());
|
||||
res.insert("e2ee_enabled", e2ee_enabled.to_string());
|
||||
res.insert("e2ee_force", e2ee_force.to_string());
|
||||
res.insert(
|
||||
"key_gen_type",
|
||||
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Internet Message Format reception pipeline.
|
||||
|
||||
use std::cmp::min;
|
||||
use std::collections::BTreeSet;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use itertools::join;
|
||||
use mailparse::SingleInfo;
|
||||
use num_traits::FromPrimitive;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -36,9 +36,6 @@ use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on
|
||||
use crate::stock_str;
|
||||
use crate::{contact, location};
|
||||
|
||||
// IndexSet is like HashSet but maintains order of insertion.
|
||||
type ContactIds = indexmap::IndexSet<u32>;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum CreateEvent {
|
||||
MsgsChanged,
|
||||
@@ -163,23 +160,19 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
|
||||
let incoming = from_id != DC_CONTACT_ID_SELF;
|
||||
|
||||
let mut to_ids = ContactIds::new();
|
||||
|
||||
to_ids.extend(
|
||||
&dc_add_or_lookup_contacts_by_address_list(
|
||||
context,
|
||||
&mime_parser.recipients,
|
||||
if !incoming {
|
||||
Origin::OutgoingTo
|
||||
} else if incoming_origin.is_known() {
|
||||
Origin::IncomingTo
|
||||
} else {
|
||||
Origin::IncomingUnknownTo
|
||||
},
|
||||
prevent_rename,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
let to_ids = dc_add_or_lookup_contacts_by_address_list(
|
||||
context,
|
||||
&mime_parser.recipients,
|
||||
if !incoming {
|
||||
Origin::OutgoingTo
|
||||
} else if incoming_origin.is_known() {
|
||||
Origin::IncomingTo
|
||||
} else {
|
||||
Origin::IncomingUnknownTo
|
||||
},
|
||||
prevent_rename,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let rcvd_timestamp = dc_smeared_time(context).await;
|
||||
let sent_timestamp = mime_parser
|
||||
@@ -217,6 +210,10 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
.await
|
||||
.map_err(|err| err.context("add_parts error"))?;
|
||||
|
||||
if from_id > DC_CONTACT_ID_LAST_SPECIAL {
|
||||
contact::update_last_seen(context, from_id, sent_timestamp).await?;
|
||||
}
|
||||
|
||||
// Update gossiped timestamp for the chat if someone else or our other device sent
|
||||
// Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves
|
||||
// and waste traffic.
|
||||
@@ -399,7 +396,7 @@ pub async fn from_field_to_contact_id(
|
||||
"mail has more than one From address, only using first: {:?}", from_address_list
|
||||
);
|
||||
}
|
||||
let from_id = from_ids.get_index(0).cloned().unwrap_or_default();
|
||||
let from_id = from_ids.get(0).cloned().unwrap_or_default();
|
||||
|
||||
let mut from_id_blocked = false;
|
||||
let mut incoming_origin = Origin::Unknown;
|
||||
@@ -427,7 +424,7 @@ async fn add_parts(
|
||||
incoming_origin: Origin,
|
||||
server_folder: &str,
|
||||
server_uid: u32,
|
||||
to_ids: &ContactIds,
|
||||
to_ids: &[u32],
|
||||
rfc724_mid: String,
|
||||
sent_timestamp: i64,
|
||||
rcvd_timestamp: i64,
|
||||
@@ -721,7 +718,7 @@ async fn add_parts(
|
||||
// the mail is on the IMAP server, probably it is also delivered.
|
||||
// We cannot recreate other states (read, error).
|
||||
state = MessageState::OutDelivered;
|
||||
to_id = to_ids.get_index(0).cloned().unwrap_or_default();
|
||||
to_id = to_ids.get(0).cloned().unwrap_or_default();
|
||||
|
||||
let self_sent = from_id == DC_CONTACT_ID_SELF
|
||||
&& to_ids.len() == 1
|
||||
@@ -1313,7 +1310,7 @@ async fn lookup_chat_by_reply(
|
||||
mime_parser: &mut MimeMessage,
|
||||
parent: &Option<Message>,
|
||||
from_id: u32,
|
||||
to_ids: &ContactIds,
|
||||
to_ids: &[u32],
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
// Try to assign message to the same chat as the parent message.
|
||||
|
||||
@@ -1353,7 +1350,7 @@ async fn lookup_chat_by_reply(
|
||||
/// If it returns false, it shall be assigned to the parent chat.
|
||||
async fn is_probably_private_reply(
|
||||
context: &Context,
|
||||
to_ids: &indexmap::IndexSet<u32>,
|
||||
to_ids: &[u32],
|
||||
mime_parser: &MimeMessage,
|
||||
parent_chat_id: ChatId,
|
||||
from_id: u32,
|
||||
@@ -1365,7 +1362,7 @@ async fn is_probably_private_reply(
|
||||
// should be assigned to the group chat. We restrict this exception to classical emails, as chat-group-messages
|
||||
// contain a Chat-Group-Id header and can be sorted into the correct chat this way.
|
||||
|
||||
let private_message = to_ids == &[DC_CONTACT_ID_SELF].iter().copied().collect::<ContactIds>();
|
||||
let private_message = to_ids == [DC_CONTACT_ID_SELF].iter().copied().collect::<Vec<u32>>();
|
||||
if !private_message {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -1394,7 +1391,7 @@ async fn create_or_lookup_group(
|
||||
allow_creation: bool,
|
||||
create_blocked: Blocked,
|
||||
from_id: u32,
|
||||
to_ids: &ContactIds,
|
||||
to_ids: &[u32],
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
let grpid = if let Some(grpid) = try_getting_grpid(mime_parser) {
|
||||
grpid
|
||||
@@ -1538,7 +1535,7 @@ async fn apply_group_changes(
|
||||
sent_timestamp: i64,
|
||||
chat_id: ChatId,
|
||||
from_id: u32,
|
||||
to_ids: &ContactIds,
|
||||
to_ids: &[u32],
|
||||
) -> Result<()> {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Group {
|
||||
@@ -1931,7 +1928,11 @@ async fn create_adhoc_group(
|
||||
/// are hidden in BCC. This group ID is sent by DC in the messages sent to this chat,
|
||||
/// so having the same ID prevents group split.
|
||||
async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> Result<String> {
|
||||
let member_ids_str = join(member_ids.iter().map(|x| x.to_string()), ",");
|
||||
let member_ids_str = member_ids
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
let member_cs = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
@@ -1974,7 +1975,7 @@ async fn check_verified_properties(
|
||||
context: &Context,
|
||||
mimeparser: &MimeMessage,
|
||||
from_id: u32,
|
||||
to_ids: &ContactIds,
|
||||
to_ids: &[u32],
|
||||
) -> Result<()> {
|
||||
let contact = Contact::load_from_db(context, from_id).await?;
|
||||
|
||||
@@ -2017,13 +2018,20 @@ async fn check_verified_properties(
|
||||
}
|
||||
|
||||
// we do not need to check if we are verified with ourself
|
||||
let mut to_ids = to_ids.clone();
|
||||
to_ids.remove(&DC_CONTACT_ID_SELF);
|
||||
let to_ids = to_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| *id != DC_CONTACT_ID_SELF)
|
||||
.collect::<Vec<u32>>();
|
||||
|
||||
if to_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let to_ids_str = join(to_ids.iter().map(|x| x.to_string()), ",");
|
||||
let to_ids_str = to_ids
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
|
||||
let rows = context
|
||||
.sql
|
||||
@@ -2105,6 +2113,8 @@ fn set_better_msg(mime_parser: &mut MimeMessage, better_msg: impl AsRef<str>) {
|
||||
/// Returns the last message referenced from `References` header if it is in the database.
|
||||
///
|
||||
/// For Delta Chat messages it is the last message in the chat of the sender.
|
||||
///
|
||||
/// Note that the returned message may be trashed.
|
||||
async fn get_previous_message(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
@@ -2120,6 +2130,8 @@ async fn get_previous_message(
|
||||
}
|
||||
|
||||
/// Given a list of Message-IDs, returns the latest message found in the database.
|
||||
///
|
||||
/// Only messages that are not in the trash chat are considered.
|
||||
async fn get_rfc724_mid_in_list(context: &Context, mid_list: &str) -> Result<Option<Message>> {
|
||||
if mid_list.is_empty() {
|
||||
return Ok(None);
|
||||
@@ -2127,7 +2139,10 @@ async fn get_rfc724_mid_in_list(context: &Context, mid_list: &str) -> Result<Opt
|
||||
|
||||
for id in parse_message_ids(mid_list).iter().rev() {
|
||||
if let Some((_, _, msg_id)) = rfc724_mid_exists(context, id).await? {
|
||||
return Ok(Some(Message::load_from_db(context, msg_id).await?));
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
if msg.chat_id != DC_CHAT_ID_TRASH {
|
||||
return Ok(Some(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2177,6 +2192,10 @@ pub(crate) async fn get_prefetch_parent_message(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Looks up contact IDs from the database given the list of recipients.
|
||||
///
|
||||
/// Returns vector of IDs guaranteed to be unique.
|
||||
///
|
||||
/// * param `prevent_rename`: if true, the display_name of this contact will not be changed. Useful for
|
||||
/// mailing lists: In some mailing lists, many users write from the same address but with different
|
||||
/// display names. We don't want the display name to change everytime the user gets a new email from
|
||||
@@ -2186,8 +2205,8 @@ async fn dc_add_or_lookup_contacts_by_address_list(
|
||||
address_list: &[SingleInfo],
|
||||
origin: Origin,
|
||||
prevent_rename: bool,
|
||||
) -> Result<ContactIds> {
|
||||
let mut contact_ids = ContactIds::new();
|
||||
) -> Result<Vec<u32>> {
|
||||
let mut contact_ids = BTreeSet::new();
|
||||
for info in address_list.iter() {
|
||||
let display_name = if prevent_rename {
|
||||
Some("")
|
||||
@@ -2199,7 +2218,7 @@ async fn dc_add_or_lookup_contacts_by_address_list(
|
||||
);
|
||||
}
|
||||
|
||||
Ok(contact_ids)
|
||||
Ok(contact_ids.into_iter().collect::<Vec<u32>>())
|
||||
}
|
||||
|
||||
/// Add contacts to database on receiving messages.
|
||||
@@ -4729,4 +4748,66 @@ Second thread."#;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_parent_message() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
|
||||
let mime = br#"Subject: First
|
||||
Message-ID: first@example.net
|
||||
To: Alice <alice@example.com>
|
||||
From: Bob <bob@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
First."#;
|
||||
dc_receive_imf(&t, mime, "INBOX", 1, false).await?;
|
||||
let first = t.get_last_msg().await;
|
||||
let mime = br#"Subject: Second
|
||||
Message-ID: second@example.net
|
||||
To: Alice <alice@example.com>
|
||||
From: Bob <bob@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
First."#;
|
||||
dc_receive_imf(&t, mime, "INBOX", 2, false).await?;
|
||||
let second = t.get_last_msg().await;
|
||||
let mime = br#"Subject: Third
|
||||
Message-ID: third@example.net
|
||||
To: Alice <alice@example.com>
|
||||
From: Bob <bob@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
First."#;
|
||||
dc_receive_imf(&t, mime, "INBOX", 3, false).await?;
|
||||
let third = t.get_last_msg().await;
|
||||
|
||||
let mime = br#"Subject: Message with references.
|
||||
Message-ID: second@example.net
|
||||
To: Alice <alice@example.com>
|
||||
From: Bob <bob@example.net>
|
||||
In-Reply-To: <third@example.net>
|
||||
References: <second@example.net> <nonexistent@example.net> <first@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
Message with references."#;
|
||||
let mime_parser = MimeMessage::from_bytes(&t, &mime[..]).await?;
|
||||
|
||||
let parent = get_parent_message(&t, &mime_parser).await?.unwrap();
|
||||
assert_eq!(parent.id, first.id);
|
||||
|
||||
message::delete_msgs(&t, &[first.id]).await?;
|
||||
let parent = get_parent_message(&t, &mime_parser).await?.unwrap();
|
||||
assert_eq!(parent.id, second.id);
|
||||
|
||||
message::delete_msgs(&t, &[second.id]).await?;
|
||||
let parent = get_parent_message(&t, &mime_parser).await?.unwrap();
|
||||
assert_eq!(parent.id, third.id);
|
||||
|
||||
message::delete_msgs(&t, &[third.id]).await?;
|
||||
let parent = get_parent_message(&t, &mime_parser).await?;
|
||||
assert!(parent.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
82
src/e2ee.rs
82
src/e2ee.rs
@@ -19,6 +19,7 @@ use crate::pgp;
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptHelper {
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
force_preference: bool,
|
||||
pub addr: String,
|
||||
pub public_key: SignedPublicKey,
|
||||
}
|
||||
@@ -28,6 +29,7 @@ impl EncryptHelper {
|
||||
let prefer_encrypt =
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
|
||||
.unwrap_or_default();
|
||||
let force_preference = context.get_config_bool(Config::E2eeForce).await?;
|
||||
let addr = match context.get_config(Config::ConfiguredAddr).await? {
|
||||
None => {
|
||||
bail!("addr not configured!");
|
||||
@@ -39,6 +41,7 @@ impl EncryptHelper {
|
||||
|
||||
Ok(EncryptHelper {
|
||||
prefer_encrypt,
|
||||
force_preference,
|
||||
addr,
|
||||
public_key,
|
||||
})
|
||||
@@ -100,11 +103,17 @@ impl EncryptHelper {
|
||||
}
|
||||
}
|
||||
|
||||
// Count number of recipients, including self.
|
||||
// This does not depend on whether we send a copy to self or not.
|
||||
let recipients_count = peerstates.len() + 1;
|
||||
let want_encrypt = if self.force_preference {
|
||||
// Ignore preferences of others.
|
||||
self.prefer_encrypt == EncryptPreference::Mutual
|
||||
} else {
|
||||
// Count number of recipients, including self.
|
||||
// This does not depend on whether we send a copy to self or not.
|
||||
let recipients_count = peerstates.len() + 1;
|
||||
2 * prefer_encrypt_count > recipients_count
|
||||
};
|
||||
|
||||
Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
|
||||
Ok(e2ee_guaranteed || want_encrypt)
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
@@ -381,6 +390,7 @@ mod tests {
|
||||
|
||||
use crate::chat;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::Message;
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::ToSave;
|
||||
@@ -602,4 +612,68 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_e2ee_force() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
alice.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
bob.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
|
||||
// Alice does not prefer encryption.
|
||||
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
bob.set_config(Config::E2eeEnabled, Some("1")).await?;
|
||||
|
||||
// Alice sends her key to Bob.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
assert!(!received_msg.get_showpadlock());
|
||||
|
||||
// Bob should not encrypt, because Alice does not prefer encryption.
|
||||
let sent_msg = bob
|
||||
.send_text(bob_chat.id, "This should not be encrypted")
|
||||
.await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
let received_msg = alice.get_last_msg().await;
|
||||
assert!(!received_msg.get_showpadlock());
|
||||
|
||||
// Bob ignores Alice's preference for no encryption.
|
||||
bob.set_config(Config::E2eeForce, Some("1")).await?;
|
||||
let sent_msg = bob.send_text(bob_chat.id, "This should be encrypted").await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
let received_msg = alice.get_last_msg().await;
|
||||
assert!(received_msg.get_showpadlock());
|
||||
|
||||
// Alice switches to MUA without Autocrypt support.
|
||||
dc_receive_imf(
|
||||
&bob,
|
||||
br#"Subject: Hello from MUA
|
||||
Message-ID: foobar@example.com
|
||||
To: Bob <bob@example.net>
|
||||
From: Alice <alice@example.com>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Date: Sun, 14 Mar 2500 00:00:00 +0000
|
||||
|
||||
Hello from MUA."#,
|
||||
"INBOX",
|
||||
100,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Bob can't encrypt now because Alice has no key.
|
||||
let sent_msg = bob
|
||||
.send_text(bob_chat.id, "This should not be encrypted again")
|
||||
.await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
let received_msg = alice.get_last_msg().await;
|
||||
assert!(!received_msg.get_showpadlock());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::future::Future;
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
|
||||
use async_smtp::smtp::response::{Category, Code, Detail};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
@@ -840,7 +839,7 @@ pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
|
||||
async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
|
||||
let q = format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
job_ids.iter().map(|_| "?").join(",")
|
||||
job_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
|
||||
);
|
||||
context
|
||||
.sql
|
||||
|
||||
@@ -77,6 +77,7 @@ pub mod peerstate;
|
||||
pub mod pgp;
|
||||
pub mod provider;
|
||||
pub mod qr;
|
||||
pub mod qr_code_generator;
|
||||
pub mod quota;
|
||||
pub mod securejoin;
|
||||
mod simplify;
|
||||
|
||||
@@ -877,7 +877,7 @@ impl Message {
|
||||
}
|
||||
|
||||
pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
|
||||
if self.param.get(Param::Quote).is_some() {
|
||||
if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
|
||||
if let Some(in_reply_to) = &self.in_reply_to {
|
||||
if let Some((_, _, msg_id)) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
@@ -1412,7 +1412,6 @@ mod tests {
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
|
||||
use async_std::fs::File;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_render_email_address() {
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::str;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use async_std::path::PathBuf;
|
||||
use itertools::Itertools;
|
||||
use num_traits::FromPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -182,7 +181,7 @@ impl fmt::Display for Params {
|
||||
f,
|
||||
"{}={}",
|
||||
*key as u8 as char,
|
||||
value.split('\n').join("\n\n")
|
||||
value.split('\n').collect::<Vec<&str>>().join("\n\n")
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -524,7 +524,6 @@ pub(crate) async fn deduplicate_peerstates(sql: &Sql) -> Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::alice_keypair;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_save_to_db() {
|
||||
|
||||
269
src/qr_code_generator.rs
Normal file
269
src/qr_code_generator.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
use anyhow::Result;
|
||||
use qrcodegen::{QrCode, QrCodeEcc};
|
||||
|
||||
use crate::{
|
||||
blob::BlobObject,
|
||||
chat::{Chat, ChatId},
|
||||
color::color_int_to_hex_string,
|
||||
config::Config,
|
||||
constants::DC_CONTACT_ID_SELF,
|
||||
contact::Contact,
|
||||
context::Context,
|
||||
securejoin, stock_str,
|
||||
};
|
||||
|
||||
pub async fn get_securejoin_qr_svg(context: &Context, chat_id: Option<ChatId>) -> Result<String> {
|
||||
if let Some(chat_id) = chat_id {
|
||||
generate_join_group_qr_code(context, chat_id).await
|
||||
} else {
|
||||
generate_verification_qr(context).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Result<String> {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
let avatar = match chat.get_profile_image(context).await? {
|
||||
Some(path) => {
|
||||
let avatar_blob = BlobObject::from_path(context, &path)?;
|
||||
Some(std::fs::read(avatar_blob.to_abs_path())?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
inner_generate_secure_join_qr_code(
|
||||
&stock_str::secure_join_group_qr_description(context, &chat).await,
|
||||
&securejoin::dc_get_securejoin_qr(context, Some(chat_id)).await?,
|
||||
&color_int_to_hex_string(chat.get_color(context).await?),
|
||||
avatar,
|
||||
chat.get_name().chars().next().unwrap_or('#'),
|
||||
)
|
||||
}
|
||||
|
||||
async fn generate_verification_qr(context: &Context) -> Result<String> {
|
||||
let contact = Contact::get_by_id(context, DC_CONTACT_ID_SELF).await?;
|
||||
|
||||
let avatar = match contact.get_profile_image(context).await? {
|
||||
Some(path) => {
|
||||
let avatar_blob = BlobObject::from_path(context, &path)?;
|
||||
Some(std::fs::read(avatar_blob.to_abs_path())?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let displayname = match context.get_config(Config::Displayname).await? {
|
||||
Some(name) => name,
|
||||
None => contact.get_addr().to_owned(),
|
||||
};
|
||||
|
||||
inner_generate_secure_join_qr_code(
|
||||
&stock_str::setup_contact_qr_description(context, &displayname, contact.get_addr()).await,
|
||||
&securejoin::dc_get_securejoin_qr(context, None).await?,
|
||||
&color_int_to_hex_string(contact.get_color()),
|
||||
avatar,
|
||||
displayname.chars().next().unwrap_or('#'),
|
||||
)
|
||||
}
|
||||
|
||||
fn inner_generate_secure_join_qr_code(
|
||||
raw_qrcode_description: &str,
|
||||
qrcode_content: &str,
|
||||
color: &str,
|
||||
avatar: Option<Vec<u8>>,
|
||||
avatar_letter: char,
|
||||
) -> Result<String> {
|
||||
let qrcode_description = &escaper::encode_minimal(raw_qrcode_description);
|
||||
// config
|
||||
let width = 515.0;
|
||||
let height = 630.0;
|
||||
let logo_offset = 28.0;
|
||||
let qr_code_size = 400.0;
|
||||
let qr_translate_up = 40.0;
|
||||
let text_y_pos = ((height - qr_code_size) / 2.0) + qr_code_size;
|
||||
let avatar_border_size = 9.0;
|
||||
let card_border_size = 2.0;
|
||||
let card_roundness = 40.0;
|
||||
|
||||
let qr = QrCode::encode_text(qrcode_content, QrCodeEcc::Medium)?;
|
||||
let mut svg = String::with_capacity(28000);
|
||||
let mut w = tagger::new(&mut svg);
|
||||
|
||||
w.elem("svg", |d| {
|
||||
d.attr("xmlns", "http://www.w3.org/2000/svg")
|
||||
.attr("viewBox", format_args!("0 0 {} {}", width, height));
|
||||
})
|
||||
.build(|w| {
|
||||
// White Background apears like a card
|
||||
w.single("rect", |d| {
|
||||
d.attr("x", card_border_size)
|
||||
.attr("y", card_border_size)
|
||||
.attr("rx", card_roundness)
|
||||
.attr("stroke", "#c6c6c6")
|
||||
.attr("stroke-width", card_border_size)
|
||||
.attr("width", width - (card_border_size * 2.0))
|
||||
.attr("height", height - (card_border_size * 2.0))
|
||||
.attr("style", "fill:#f2f2f2");
|
||||
});
|
||||
// Qrcode
|
||||
w.elem("g", |d| {
|
||||
d.attr(
|
||||
"transform",
|
||||
format!(
|
||||
"translate({},{})",
|
||||
(width - qr_code_size) / 2.0,
|
||||
((height - qr_code_size) / 2.0) - qr_translate_up
|
||||
),
|
||||
);
|
||||
// If the qr code should be in the wrong place,
|
||||
// we could also translate and scale the points in the path already,
|
||||
// but that would make the resulting svg way bigger in size and might bring up rounding issues,
|
||||
// so better avoid doing it manually if possible
|
||||
})
|
||||
.build(|w| {
|
||||
w.single("path", |d| {
|
||||
let mut path_data = String::with_capacity(0);
|
||||
let scale = qr_code_size / qr.size() as f32;
|
||||
|
||||
for y in 0..qr.size() {
|
||||
for x in 0..qr.size() {
|
||||
if qr.get_module(x, y) {
|
||||
path_data += &format!("M{},{}h1v1h-1z", x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.attr("style", "fill:#000000")
|
||||
.attr("d", path_data)
|
||||
.attr("transform", format!("scale({})", scale));
|
||||
});
|
||||
});
|
||||
|
||||
// Text
|
||||
const BIG_TEXT_CHARS_PER_LINE: usize = 32;
|
||||
const SMALL_TEXT_CHARS_PER_LINE: usize = 38;
|
||||
let chars_per_line = if qrcode_description.len() > SMALL_TEXT_CHARS_PER_LINE*2 {
|
||||
SMALL_TEXT_CHARS_PER_LINE
|
||||
} else {
|
||||
BIG_TEXT_CHARS_PER_LINE
|
||||
};
|
||||
let lines = textwrap::fill(qrcode_description, chars_per_line);
|
||||
let (text_font_size, text_y_shift) = if lines.split('\n').count() <= 2 {
|
||||
(27.0, 0.0)
|
||||
} else {
|
||||
(19.0, -10.0)
|
||||
};
|
||||
for (count, line) in lines.split('\n').enumerate()
|
||||
{
|
||||
w.elem("text", |d| {
|
||||
d.attr("y", (count as f32 * (text_font_size * 1.2)) + text_y_pos + text_y_shift)
|
||||
.attr("x", width / 2.0)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr(
|
||||
"style",
|
||||
format!(
|
||||
"font-family:sans-serif;\
|
||||
font-weight:bold;\
|
||||
font-size:{}px;\
|
||||
fill:#000000;\
|
||||
stroke:none",
|
||||
text_font_size
|
||||
),
|
||||
);
|
||||
})
|
||||
.build(|w| {
|
||||
w.put_raw(line);
|
||||
});
|
||||
}
|
||||
// contact avatar in middle of qrcode
|
||||
const LOGO_SIZE: f32 = 94.4;
|
||||
const HALF_LOGO_SIZE: f32 = LOGO_SIZE / 2.0;
|
||||
let logo_position_in_qr = (qr_code_size / 2.0) - HALF_LOGO_SIZE;
|
||||
let logo_position_x = ((width - qr_code_size) / 2.0) + logo_position_in_qr;
|
||||
let logo_position_y =
|
||||
((height - qr_code_size) / 2.0) - qr_translate_up + logo_position_in_qr;
|
||||
|
||||
w.single("circle", |d| {
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)
|
||||
.attr("cy", logo_position_y + HALF_LOGO_SIZE)
|
||||
.attr("r", HALF_LOGO_SIZE + avatar_border_size)
|
||||
.attr("style", "fill:#f2f2f2");
|
||||
});
|
||||
|
||||
if let Some(img) = avatar {
|
||||
w.elem("defs", |_| {}).build(|w| {
|
||||
w.elem("clipPath", |d| {
|
||||
d.attr("id", "avatar-cut");
|
||||
})
|
||||
.build(|w| {
|
||||
w.single("circle", |d| {
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)
|
||||
.attr("cy", logo_position_y + HALF_LOGO_SIZE)
|
||||
.attr("r", HALF_LOGO_SIZE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
w.single("image", |d| {
|
||||
d.attr("x", logo_position_x)
|
||||
.attr("y", logo_position_y)
|
||||
.attr("width", HALF_LOGO_SIZE * 2.0)
|
||||
.attr("height", HALF_LOGO_SIZE * 2.0)
|
||||
.attr("preserveAspectRatio", "none")
|
||||
.attr("clip-path", "url(#avatar-cut)")
|
||||
.attr(
|
||||
"href" /*might need xlink:href instead if it doesn't work on older devices?*/,
|
||||
format!("data:image/jpeg;base64,{}", base64::encode(img)),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
w.single("circle", |d| {
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)
|
||||
.attr("cy", logo_position_y + HALF_LOGO_SIZE)
|
||||
.attr("r", HALF_LOGO_SIZE)
|
||||
.attr("style", format!("fill:{}", &color));
|
||||
});
|
||||
|
||||
let avatar_font_size = LOGO_SIZE * 0.65;
|
||||
let font_offset = avatar_font_size * 0.1;
|
||||
w.elem("text", |d| {
|
||||
d.attr("y", logo_position_y + HALF_LOGO_SIZE + font_offset)
|
||||
.attr("x", logo_position_x + HALF_LOGO_SIZE)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dominant-baseline", "central")
|
||||
.attr("alignment-baseline", "middle")
|
||||
.attr(
|
||||
"style",
|
||||
format!(
|
||||
"font-family:sans-serif;\
|
||||
font-weight:400;\
|
||||
font-size:{}px;\
|
||||
fill:#ffffff;",
|
||||
avatar_font_size
|
||||
),
|
||||
);
|
||||
})
|
||||
.build(|w| {
|
||||
w.put_raw(avatar_letter.to_uppercase());
|
||||
});
|
||||
}
|
||||
|
||||
// Footer logo
|
||||
const FOOTER_HEIGHT: f32 = 35.0;
|
||||
const FOOTER_WIDTH: f32 = 198.0;
|
||||
w.elem("g", |d| {
|
||||
d.attr(
|
||||
"transform",
|
||||
format!(
|
||||
"translate({},{})",
|
||||
(width - FOOTER_WIDTH) / 2.0,
|
||||
height - logo_offset - FOOTER_HEIGHT - text_y_shift
|
||||
),
|
||||
);
|
||||
})
|
||||
.build(|w| {
|
||||
w.put_raw(include_str!("../assets/qrcode_logo_footer.svg"));
|
||||
});
|
||||
});
|
||||
|
||||
Ok(svg)
|
||||
}
|
||||
12
src/quota.rs
12
src/quota.rs
@@ -2,7 +2,7 @@
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_imap::types::{Quota, QuotaResource};
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
@@ -43,7 +43,7 @@ pub struct QuotaInfo {
|
||||
/// set to `Err()` if the provider does not support quota or on other errors,
|
||||
/// set to `Ok()` for valid quota information.
|
||||
/// Updated by `Action::UpdateRecentQuota`
|
||||
pub(crate) recent: Result<IndexMap<String, Vec<QuotaResource>>>,
|
||||
pub(crate) recent: Result<BTreeMap<String, Vec<QuotaResource>>>,
|
||||
|
||||
/// Timestamp when structure was modified.
|
||||
pub(crate) modified: i64,
|
||||
@@ -52,8 +52,8 @@ pub struct QuotaInfo {
|
||||
async fn get_unique_quota_roots_and_usage(
|
||||
folders: Vec<String>,
|
||||
imap: &mut Imap,
|
||||
) -> Result<IndexMap<String, Vec<QuotaResource>>> {
|
||||
let mut unique_quota_roots: IndexMap<String, Vec<QuotaResource>> = IndexMap::new();
|
||||
) -> Result<BTreeMap<String, Vec<QuotaResource>>> {
|
||||
let mut unique_quota_roots: BTreeMap<String, Vec<QuotaResource>> = BTreeMap::new();
|
||||
for folder in folders {
|
||||
let (quota_roots, quotas) = &imap.get_quota_roots(&folder).await?;
|
||||
// if there are new quota roots found in this imap folder, add them to the list
|
||||
@@ -69,7 +69,7 @@ async fn get_unique_quota_roots_and_usage(
|
||||
// messages could be recieved and so the usage could have been changed
|
||||
*unique_quota_roots
|
||||
.entry(quota_root_name.clone())
|
||||
.or_insert(vec![]) = quota.resources;
|
||||
.or_insert_with(Vec::new) = quota.resources;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ async fn get_unique_quota_roots_and_usage(
|
||||
}
|
||||
|
||||
fn get_highest_usage<'t>(
|
||||
unique_quota_roots: &'t IndexMap<String, Vec<QuotaResource>>,
|
||||
unique_quota_roots: &'t BTreeMap<String, Vec<QuotaResource>>,
|
||||
) -> Result<(u64, &'t String, &QuotaResource)> {
|
||||
let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
|
||||
for (name, resources) in unique_quota_roots {
|
||||
|
||||
@@ -346,6 +346,10 @@ impl Context {
|
||||
<body>"#
|
||||
.to_string();
|
||||
|
||||
// =============================================================================================
|
||||
// Get the states from the RwLock
|
||||
// =============================================================================================
|
||||
|
||||
let lock = self.scheduler.read().await;
|
||||
let (folders_states, smtp) = match &*lock {
|
||||
Scheduler::Running {
|
||||
@@ -380,6 +384,13 @@ impl Context {
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Incoming messages
|
||||
// - "Inbox": Connected
|
||||
// - "Sent": Connected
|
||||
// =============================================================================================
|
||||
|
||||
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
|
||||
for (folder, watch, state) in &folders_states {
|
||||
let w = self.get_config(*watch).await.ok_or_log(self);
|
||||
@@ -417,6 +428,12 @@ impl Context {
|
||||
}
|
||||
ret += "</ul>";
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Outgoing messages
|
||||
// Your last message was sent successfully
|
||||
// =============================================================================================
|
||||
|
||||
ret += &format!(
|
||||
"<h3>{}</h3><ul><li>",
|
||||
stock_str::outgoing_messages(self).await
|
||||
@@ -427,6 +444,13 @@ impl Context {
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
|
||||
ret += "</li></ul>";
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Storage on testrun.org
|
||||
// 1.34 GiB of 2 GiB used
|
||||
// [======67%===== ]
|
||||
// =============================================================================================
|
||||
|
||||
let domain = dc_tools::EmailAddress::new(
|
||||
&self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
@@ -527,6 +551,8 @@ impl Context {
|
||||
}
|
||||
ret += "</ul>";
|
||||
|
||||
// =============================================================================================
|
||||
|
||||
ret += "</body></html>\n";
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! # Simplify incoming plaintext.
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
// protect lines starting with `--` against being treated as a footer.
|
||||
// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
|
||||
// this should be invisible on most systems and there is no need to unescape it again
|
||||
@@ -158,6 +156,7 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
|
||||
s.strip_prefix('>')
|
||||
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
|
||||
l_last -= 1
|
||||
@@ -204,6 +203,7 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
|
||||
s.strip_prefix('>')
|
||||
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ use async_smtp::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
use crate::constants::DEFAULT_MAX_SMTP_RCPT_TO;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use itertools::Itertools;
|
||||
use std::time::Duration;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -43,10 +42,14 @@ impl Smtp {
|
||||
}
|
||||
|
||||
for recipients_chunk in recipients.chunks(chunk_size).into_iter() {
|
||||
let recipients = recipients_chunk.to_vec();
|
||||
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
|
||||
let recipients_display = recipients_chunk
|
||||
.iter()
|
||||
.map(|x| x.as_ref())
|
||||
.collect::<Vec<&str>>()
|
||||
.join(",");
|
||||
|
||||
let envelope = Envelope::new(self.from.clone(), recipients).map_err(Error::Envelope)?;
|
||||
let envelope = Envelope::new(self.from.clone(), recipients_chunk.to_vec())
|
||||
.map_err(Error::Envelope)?;
|
||||
let mail = SendableEmail::new(
|
||||
envelope,
|
||||
format!("{}", job_id), // only used for internal logging
|
||||
|
||||
@@ -8,7 +8,7 @@ use strum::EnumProperty;
|
||||
use strum_macros::EnumProperty;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, ChatId, ProtectionStatus};
|
||||
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::contact::{Contact, Origin};
|
||||
@@ -56,9 +56,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Sent with my Delta Chat Messenger: https://delta.chat"))]
|
||||
StatusLine = 13,
|
||||
|
||||
#[strum(props(fallback = "Hello, I\'ve just created the group \"%1$s\" for us."))]
|
||||
NewGroupDraft = 14,
|
||||
|
||||
#[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\"."))]
|
||||
MsgGrpName = 15,
|
||||
|
||||
@@ -333,6 +330,12 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
|
||||
SecureJoinReplies = 118,
|
||||
|
||||
#[strum(props(fallback = "Scan to chat with %1$s"))]
|
||||
SetupContactQRDescription = 119,
|
||||
|
||||
#[strum(props(fallback = "Scan to join group %1$s"))]
|
||||
SecureJoinGroupQRDescription = 120,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -457,13 +460,6 @@ pub(crate) async fn status_line(context: &Context) -> String {
|
||||
translated(context, StockMessage::StatusLine).await
|
||||
}
|
||||
|
||||
/// Stock string: `Hello, I've just created the group "%1$s" for us.`.
|
||||
pub(crate) async fn new_group_draft(context: &Context, group_name: impl AsRef<str>) -> String {
|
||||
translated(context, StockMessage::NewGroupDraft)
|
||||
.await
|
||||
.replace1(group_name)
|
||||
}
|
||||
|
||||
/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
|
||||
pub(crate) async fn msg_grp_name(
|
||||
context: &Context,
|
||||
@@ -624,6 +620,29 @@ pub(crate) async fn secure_join_replies(context: &Context, contact_id: u32) -> S
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to chat with %1$s`.
|
||||
pub(crate) async fn setup_contact_qr_description(
|
||||
context: &Context,
|
||||
display_name: &str,
|
||||
addr: &str,
|
||||
) -> String {
|
||||
let name = if display_name == addr {
|
||||
addr.to_owned()
|
||||
} else {
|
||||
format!("{} ({})", display_name, addr)
|
||||
};
|
||||
translated(context, StockMessage::SetupContactQRDescription)
|
||||
.await
|
||||
.replace1(name)
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to join %1$s`.
|
||||
pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
|
||||
translated(context, StockMessage::SecureJoinGroupQRDescription)
|
||||
.await
|
||||
.replace1(chat.get_name())
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s verified.`.
|
||||
pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef<str>) -> String {
|
||||
translated(context, StockMessage::ContactVerified)
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::message::{Message, MessageState};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::stock_str;
|
||||
use itertools::Itertools;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
@@ -174,7 +173,7 @@ impl Message {
|
||||
summary_content
|
||||
};
|
||||
|
||||
summary.split_whitespace().join(" ")
|
||||
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
|
||||
use crate::token::Namespace;
|
||||
use crate::{chat, stock_str, token};
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use lettre_email::mime::{self};
|
||||
use lettre_email::PartBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -177,7 +176,10 @@ impl Context {
|
||||
} else {
|
||||
Ok(Some((
|
||||
format!("{{\"items\":[\n{}\n]}}", serialized),
|
||||
ids.iter().map(|x| x.to_string()).join(","),
|
||||
ids.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(","),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user