Compare commits
47 Commits
fix_grpid_
...
fix_html_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dc87e9a0f | ||
|
|
c011a8cfef | ||
|
|
2d7d79fc44 | ||
|
|
119d4fa689 | ||
|
|
b7a9f73329 | ||
|
|
a52a3e0d24 | ||
|
|
f32876708a | ||
|
|
84e0e7e020 | ||
|
|
38cddff6e9 | ||
|
|
f1aba8115b | ||
|
|
0db5ff55fd | ||
|
|
7421b0de6c | ||
|
|
6c275c30a7 | ||
|
|
91f1d793e9 | ||
|
|
cddfd04a7f | ||
|
|
7fa94c33bf | ||
|
|
30dd20dc7b | ||
|
|
339c0d3dc7 | ||
|
|
2304d63bb3 | ||
|
|
36014f9fe5 | ||
|
|
351383dfa4 | ||
|
|
defad94f5a | ||
|
|
a0517478e3 | ||
|
|
7f4e68f21c | ||
|
|
c0a425c26d | ||
|
|
e756859b16 | ||
|
|
174bc017ad | ||
|
|
4a9fb0212f | ||
|
|
212848409f | ||
|
|
e45ee0eb81 | ||
|
|
3b8e37de58 | ||
|
|
c2e8cc9bd6 | ||
|
|
ec81d29580 | ||
|
|
c4de0f3b17 | ||
|
|
965d41990e | ||
|
|
d96dba336b | ||
|
|
a7e1b4653e | ||
|
|
e38b42bc21 | ||
|
|
36bd502292 | ||
|
|
594bf3dfc8 | ||
|
|
6d30ccfc63 | ||
|
|
1b79f513a3 | ||
|
|
74825a0f57 | ||
|
|
4a23d12df2 | ||
|
|
7f117574ab | ||
|
|
f91474c2f8 | ||
|
|
2a081aac2b |
33
CHANGELOG.md
@@ -1,5 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## next (pending)
|
||||
|
||||
- restructured (but not change) imap idle handling into own file. cc @link2xt
|
||||
|
||||
## 1.0.0-beta.12
|
||||
|
||||
- fix python bindings to use core for copying attachments to blobdir
|
||||
and fix core to actually do it. @hpk42
|
||||
|
||||
## 1.0.0-beta.11
|
||||
|
||||
- trigger reconnect more often on imap error states. Should fix an
|
||||
issue observed when trying to empty a folder. @hpk42
|
||||
|
||||
- un-split qr tests: we fixed qr-securejoin protocol flakyness
|
||||
last weeks. @hpk42
|
||||
|
||||
## 1.0.0-beta.10
|
||||
|
||||
- fix grpid-determination from in-reply-to and references headers. @hpk42
|
||||
|
||||
- only send Autocrypt-gossip headers on encrypted messages. @dignifiedquire
|
||||
|
||||
- fix reply-to-encrypted message to also be encrypted. @hpk42
|
||||
|
||||
- remove last unsafe code from dc_receive_imf :) @hpk42
|
||||
|
||||
- add experimental new dc_chat_get_info_json FFI/API so that desktop devs
|
||||
can play with using it. @jikstra
|
||||
|
||||
- fix encoding of subjects and attachment-filenames @hpk42
|
||||
@dignifiedquire .
|
||||
|
||||
## 1.0.0-beta.9
|
||||
|
||||
- historic: we now use the mailparse crate and lettre-email to generate mime
|
||||
|
||||
42
Cargo.lock
generated
@@ -607,7 +607,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.0.0-beta.9"
|
||||
version = "1.0.0-beta.12"
|
||||
dependencies = [
|
||||
"async-imap 0.1.1 (git+https://github.com/async-email/async-imap)",
|
||||
"async-std 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -620,6 +620,7 @@ dependencies = [
|
||||
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"debug_stub_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"deltachat_derive 0.1.0",
|
||||
"encoded-words 0.1.0 (git+https://github.com/async-email/encoded-words)",
|
||||
"escaper 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -682,9 +683,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.0.0-beta.9"
|
||||
version = "1.0.0-beta.12"
|
||||
dependencies = [
|
||||
"deltachat 1.0.0-beta.9",
|
||||
"deltachat 1.0.0-beta.12",
|
||||
"deltachat-provider-database 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"human-panic 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -796,6 +797,20 @@ dependencies = [
|
||||
"version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoded-words"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/async-email/encoded-words#2631c258183620f6d976abffabbfc2dcc697d793"
|
||||
dependencies = [
|
||||
"base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"encoding_rs 0.8.20 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thiserror 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding"
|
||||
version = "0.2.33"
|
||||
@@ -2760,6 +2775,24 @@ dependencies = [
|
||||
"wincolor 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread-local-object"
|
||||
version = "0.1.0"
|
||||
@@ -3428,6 +3461,7 @@ dependencies = [
|
||||
"checksum ed25519-dalek 1.0.0-pre.2 (registry+https://github.com/rust-lang/crates.io-index)" = "845aaacc16f01178f33349e7c992ecd0cee095aa5e577f0f4dee35971bd36455"
|
||||
"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
|
||||
"checksum email 0.0.21 (git+https://github.com/deltachat/rust-email)" = "<none>"
|
||||
"checksum encoded-words 0.1.0 (git+https://github.com/async-email/encoded-words)" = "<none>"
|
||||
"checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
|
||||
"checksum encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
|
||||
"checksum encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
|
||||
@@ -3644,6 +3678,8 @@ dependencies = [
|
||||
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
|
||||
"checksum termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "adc4587ead41bf016f11af03e55a624c06568b5a19db4e90fde573d805074f83"
|
||||
"checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e"
|
||||
"checksum thiserror 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6f357d1814b33bc2dc221243f8424104bfe72dbe911d5b71b3816a2dff1c977e"
|
||||
"checksum thiserror-impl 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "eb2e25d25307eb8436894f727aba8f65d07adf02e5b35a13cebed48bd282bfef"
|
||||
"checksum thread-local-object 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7da3caa820d0308c84c8654f6cafd81cc3195d45433311cbe22fcf44fc8be071"
|
||||
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
|
||||
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.0.0-beta.9"
|
||||
version = "1.0.0-beta.12"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL"
|
||||
@@ -54,6 +54,7 @@ rustls = "0.16.0"
|
||||
webpki-roots = "0.18.0"
|
||||
webpki = "0.21.0"
|
||||
mailparse = "0.10.1"
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
|
||||
@@ -87,6 +87,15 @@ $ cargo test --all
|
||||
$ cargo build -p deltachat_ffi --release
|
||||
```
|
||||
|
||||
## Debugging environment variables
|
||||
|
||||
- `DCC_IMAP_DEBUG`: if set IMAP protocol commands and responses will be
|
||||
printed
|
||||
|
||||
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
||||
|
||||
|
||||
|
||||
### Expensive tests
|
||||
|
||||
Some tests are expensive and marked with `#[ignore]`, to run these
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -7,42 +7,42 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||
sodipodi:docname="icon-device.svg"
|
||||
id="svg4344"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 45 45"
|
||||
height="60"
|
||||
width="60"
|
||||
version="1.0"
|
||||
inkscape:export-filename="/home/kerle/Workspace/deltachat-core-rust/assets/icon-device.png"
|
||||
inkscape:export-ydpi="409.60001"
|
||||
inkscape:export-xdpi="409.60001"
|
||||
inkscape:export-ydpi="409.60001">
|
||||
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-device.png"
|
||||
version="1.0"
|
||||
width="60"
|
||||
height="60"
|
||||
viewBox="0 0 45 45"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
id="svg4344"
|
||||
sodipodi:docname="icon-device.svg"
|
||||
inkscape:version="1.0beta1 (32d4812, 2019-09-19)">
|
||||
<defs
|
||||
id="defs4348" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="svg4344"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-x="0"
|
||||
inkscape:cy="28.471578"
|
||||
inkscape:cx="28.03926"
|
||||
inkscape:zoom="5.6"
|
||||
units="px"
|
||||
showgrid="false"
|
||||
id="namedview4346"
|
||||
inkscape:window-height="1019"
|
||||
inkscape:window-width="1248"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:document-rotation="0"
|
||||
bordercolor="#666666"
|
||||
inkscape:snap-global="false"
|
||||
pagecolor="#ffffff"
|
||||
inkscape:snap-global="false" />
|
||||
bordercolor="#666666"
|
||||
inkscape:document-rotation="0"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1600"
|
||||
inkscape:window-height="1035"
|
||||
id="namedview4346"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:zoom="3.959798"
|
||||
inkscape:cx="28.322498"
|
||||
inkscape:cy="24.898474"
|
||||
inkscape:window-x="45"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4344" />
|
||||
<metadata
|
||||
id="metadata4336">
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
@@ -56,35 +56,28 @@ Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<circle
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke-width:0.731071"
|
||||
id="path859"
|
||||
cx="22.5"
|
||||
cy="22.5"
|
||||
r="22.5" />
|
||||
<circle
|
||||
r="22.5"
|
||||
cy="22.5"
|
||||
cx="22.5"
|
||||
id="path4917"
|
||||
style="fill:#364e54;fill-opacity:0.67681497;stroke-width:0.58855402"
|
||||
inkscape:export-xdpi="409"
|
||||
inkscape:export-ydpi="409" />
|
||||
<rect
|
||||
y="-4.4408921e-16"
|
||||
x="0"
|
||||
height="45"
|
||||
width="45"
|
||||
id="rect860"
|
||||
style="opacity:1;fill:#76868b;fill-opacity:1;stroke-width:0.819271" />
|
||||
<g
|
||||
id="g4342"
|
||||
transform="matrix(0.00255113,0,0,-0.00255113,5.586152,38.200477)"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
fill="#000000">
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
transform="matrix(0.00255113,0,0,-0.00255113,5.586152,38.200477)"
|
||||
id="g4342">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4338"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
d="m 8175,12765 c -703,-114 -1248,-608 -1387,-1258 -17,-82 -21,-136 -22,-277 0,-202 15,-307 70,-470 149,-446 499,-733 1009,-828 142,-26 465,-23 619,6 691,131 1201,609 1328,1244 31,158 31,417 0,565 -114,533 -482,889 -1038,1004 -133,27 -448,35 -579,14 z"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
id="path4338"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4340"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
d="m 7070,9203 c -212,-20 -275,-27 -397,-48 -691,-117 -1400,-444 -2038,-940 -182,-142 -328,-270 -585,-517 -595,-571 -911,-974 -927,-1181 -6,-76 11,-120 69,-184 75,-80 159,-108 245,-79 109,37 263,181 632,595 539,606 774,826 1035,969 135,75 231,105 341,106 82,1 94,-2 138,-27 116,-68 161,-209 122,-376 -9,-36 -349,-868 -757,-1850 -407,-982 -785,-1892 -838,-2021 -287,-694 -513,-1389 -615,-1889 -70,-342 -90,-683 -52,-874 88,-440 381,-703 882,-792 124,-23 401,-30 562,-16 783,69 1674,461 2561,1125 796,596 1492,1354 1607,1751 43,146 -33,308 -168,360 -61,23 -100,15 -173,-36 -105,-74 -202,-170 -539,-529 -515,-551 -762,-783 -982,-927 -251,-164 -437,-186 -543,-65 -56,64 -74,131 -67,247 13,179 91,434 249,815 135,324 1588,4102 1646,4280 106,325 151,561 159,826 9,281 -22,463 -112,652 -58,122 -114,199 -211,292 -245,233 -582,343 -1044,338 -91,-1 -181,-3 -200,-5 z"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
id="path4340"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -7,42 +7,42 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||
sodipodi:docname="icon-saved-messages.svg"
|
||||
id="svg4344"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 45 45"
|
||||
height="60"
|
||||
width="60"
|
||||
version="1.0"
|
||||
inkscape:export-filename="/home/kerle/test-icon.png"
|
||||
inkscape:export-ydpi="409.60001"
|
||||
inkscape:export-xdpi="409.60001"
|
||||
inkscape:export-ydpi="409.60001">
|
||||
inkscape:export-filename="/home/kerle/test-icon.png"
|
||||
version="1.0"
|
||||
width="60"
|
||||
height="60"
|
||||
viewBox="0 0 45 45"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
id="svg4344"
|
||||
sodipodi:docname="icon-saved-messages.svg"
|
||||
inkscape:version="1.0beta1 (32d4812, 2019-09-19)">
|
||||
<defs
|
||||
id="defs4348" />
|
||||
<sodipodi:namedview
|
||||
inkscape:lockguides="false"
|
||||
inkscape:current-layer="svg4344"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="130"
|
||||
inkscape:window-x="89"
|
||||
inkscape:cy="43.28624"
|
||||
inkscape:cx="-21.066324"
|
||||
inkscape:zoom="4"
|
||||
units="px"
|
||||
showgrid="false"
|
||||
id="namedview4346"
|
||||
inkscape:window-height="855"
|
||||
inkscape:window-width="1395"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:document-rotation="0"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
inkscape:document-rotation="0"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1395"
|
||||
inkscape:window-height="855"
|
||||
id="namedview4346"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:zoom="4"
|
||||
inkscape:cx="29.308676"
|
||||
inkscape:cy="49.03624"
|
||||
inkscape:window-x="89"
|
||||
inkscape:window-y="108"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4344"
|
||||
inkscape:lockguides="false" />
|
||||
<metadata
|
||||
id="metadata4336">
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
@@ -56,21 +56,16 @@ Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<circle
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke-width:0.731071"
|
||||
id="path859"
|
||||
cx="22.5"
|
||||
cy="22.5"
|
||||
r="22.5" />
|
||||
<circle
|
||||
r="22.5"
|
||||
cy="22.50293"
|
||||
cx="22.5"
|
||||
id="path4917"
|
||||
style="fill:#87aade;fill-opacity:1;stroke-width:0.58855402" />
|
||||
<rect
|
||||
y="0"
|
||||
x="0"
|
||||
height="45"
|
||||
width="45"
|
||||
id="rect1420"
|
||||
style="fill:#87aade;fill-opacity:1;stroke:none;stroke-width:0.968078" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="M 13.5,7.5 V 39 h 0.08654 L 22.533801,29.370239 31.482419,39 h 0.01758 V 7.5 Z m 9.004056,4.108698 1.879508,4.876388 5.039514,0.359779 -3.879358,3.363728 1.227764,5.095749 -4.276893,-2.796643 -4.280949,2.788618 1.237229,-5.093073 -3.873949,-3.371754 5.040866,-0.350417 z"
|
||||
id="rect846"
|
||||
style="fill:#ffffff;stroke-width:0.58409804"
|
||||
id="rect846" />
|
||||
d="M 13.5,7.5 V 39 h 0.08654 L 22.533801,29.370239 31.482419,39 h 0.01758 V 7.5 Z m 9.004056,4.108698 1.879508,4.876388 5.039514,0.359779 -3.879358,3.363728 1.227764,5.095749 -4.276893,-2.796643 -4.280949,2.788618 1.237229,-5.093073 -3.873949,-3.371754 5.040866,-0.350417 z"
|
||||
inkscape:connector-curvature="0" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
||||
export SSHTARGET=ci@b1.delta.chat
|
||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
|
||||
# we construct the BUILDDIR such that we can easily share the
|
||||
# CARGO_TARGET_DIR between runs ("..")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
||||
export SSHTARGET=ci@b1.delta.chat
|
||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||
|
||||
# we construct the BUILDDIR such that we can easily share the
|
||||
# CARGO_TARGET_DIR between runs ("..")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.0.0-beta.9"
|
||||
version = "1.0.0-beta.12"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -994,11 +994,20 @@ uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t co
|
||||
*
|
||||
* Example:
|
||||
* ~~~
|
||||
* char* blobdir = dc_get_blobdir(context);
|
||||
* char* file_to_send = mprintf("%s/%s", blobdir, "send.mp4")
|
||||
*
|
||||
* dc_msg_t* msg = dc_msg_new(context, DC_MSG_VIDEO);
|
||||
* dc_msg_set_file(msg, "/file/to/send.mp4", NULL);
|
||||
* dc_msg_set_file(msg, file_to_send, NULL);
|
||||
* dc_prepare_msg(context, chat_id, msg);
|
||||
* // ... after /file/to/send.mp4 is ready:
|
||||
*
|
||||
* // ... create the file ...
|
||||
*
|
||||
* dc_send_msg(context, chat_id, msg);
|
||||
*
|
||||
* dc_msg_unref(msg);
|
||||
* free(file_to_send);
|
||||
* dc_str_unref(file_to_send);
|
||||
* ~~~
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -1024,8 +1033,11 @@ uint32_t dc_prepare_msg (dc_context_t* context, uint32_t ch
|
||||
* Example:
|
||||
* ~~~
|
||||
* dc_msg_t* msg = dc_msg_new(context, DC_MSG_IMAGE);
|
||||
*
|
||||
* dc_msg_set_file(msg, "/file/to/send.jpg", NULL);
|
||||
* dc_send_msg(context, chat_id, msg);
|
||||
*
|
||||
* dc_msg_unref(msg);
|
||||
* ~~~
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -1134,6 +1146,9 @@ void dc_set_draft (dc_context_t* context, uint32_t ch
|
||||
* // add a changelog
|
||||
* dc_add_device_msg(context, "update-123", changelog_msg);
|
||||
* }
|
||||
*
|
||||
* dc_msg_unref(changelog_msg);
|
||||
* dc_msg_unref(welome_msg);
|
||||
* ~~~
|
||||
*/
|
||||
uint32_t dc_add_device_msg (dc_context_t* context, const char* label, dc_msg_t* msg);
|
||||
@@ -1947,7 +1962,7 @@ void dc_imex (dc_context_t* context, int what, c
|
||||
* }
|
||||
* while (!configure_succeeded())
|
||||
* }
|
||||
* free(file);
|
||||
* dc_str_unref(file);
|
||||
* }
|
||||
* ~~~
|
||||
*
|
||||
@@ -2602,6 +2617,25 @@ dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, siz
|
||||
dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
|
||||
|
||||
|
||||
/**
|
||||
* Get info summary for a chat, in json format.
|
||||
*
|
||||
* The returned json string has the following key/values:
|
||||
*
|
||||
* id: chat id
|
||||
* name: chat/group name
|
||||
* color: color of this chat
|
||||
* last-message-from: who sent the last message
|
||||
* last-message-text: message (truncated)
|
||||
* last-message-state: DC_STATE* constant
|
||||
* last-message-date:
|
||||
* avatar-path: path-to-blobfile
|
||||
* is_verified: yes/no
|
||||
|
||||
* @return a utf8-encoded json string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
|
||||
*/
|
||||
char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id);
|
||||
|
||||
/**
|
||||
* @class dc_chat_t
|
||||
*
|
||||
|
||||
@@ -2377,6 +2377,27 @@ pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> l
|
||||
ffi_chat.chat.is_sending_locations() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_get_info_json(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_get_info_json()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| match chat::get_info_json(ctx, chat_id) {
|
||||
Ok(s) => s.strdup(),
|
||||
Err(err) => {
|
||||
error!(ctx, "get_info_json({}) returned: {}", chat_id, err);
|
||||
return "".strdup();
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| "".strdup())
|
||||
}
|
||||
|
||||
// dc_msg_t
|
||||
|
||||
/// FFI struct for [dc_msg_t]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import mimetypes
|
||||
import calendar
|
||||
import json
|
||||
from datetime import datetime
|
||||
import os
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||
@@ -242,6 +243,12 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_marknoticed_chat(self._dc_context, self.id)
|
||||
|
||||
def get_summary(self):
|
||||
""" return dictionary with summary information. """
|
||||
dc_res = lib.dc_chat_get_info_json(self._dc_context, self.id)
|
||||
s = from_dc_charpointer(dc_res)
|
||||
return json.loads(s)
|
||||
|
||||
# ------ group management API ------------------------------
|
||||
|
||||
def add_contact(self, contact):
|
||||
@@ -324,6 +331,18 @@ class Chat(object):
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
|
||||
def get_color(self):
|
||||
"""return the color of the chat.
|
||||
:returns: color as 0x00rrggbb
|
||||
"""
|
||||
return lib.dc_chat_get_color(self._dc_chat)
|
||||
|
||||
def get_subtitle(self):
|
||||
"""return the subtitle of the chat
|
||||
:returns: the subtitle
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_chat_get_subtitle(self._dc_chat))
|
||||
|
||||
# ------ location streaming API ------------------------------
|
||||
|
||||
def is_sending_locations(self):
|
||||
@@ -332,6 +351,12 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id)
|
||||
|
||||
def is_archived(self):
|
||||
"""return True if this chat is archived.
|
||||
:returns: True if archived.
|
||||
"""
|
||||
return lib.dc_chat_get_archived(self._dc_chat)
|
||||
|
||||
def enable_sending_locations(self, seconds):
|
||||
"""enable sending locations for this chat.
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
""" The Message object. """
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
@@ -58,8 +57,6 @@ class Message(object):
|
||||
|
||||
def set_text(self, text):
|
||||
"""set text of this message. """
|
||||
assert self.id > 0, "message not prepared"
|
||||
assert self.is_out_preparing()
|
||||
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
|
||||
|
||||
@props.with_doc
|
||||
@@ -72,19 +69,6 @@ class Message(object):
|
||||
mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type)
|
||||
if not os.path.exists(path):
|
||||
raise ValueError("path does not exist: {!r}".format(path))
|
||||
blobdir = self.account.get_blobdir()
|
||||
if not path.startswith(blobdir):
|
||||
for i in range(50):
|
||||
ext = "" if i == 0 else "-" + str(i)
|
||||
dest = os.path.join(blobdir, os.path.basename(path) + ext)
|
||||
if os.path.exists(dest):
|
||||
continue
|
||||
shutil.copyfile(path, dest)
|
||||
break
|
||||
else:
|
||||
raise ValueError("could not create blobdir-path for {}".format(path))
|
||||
path = dest
|
||||
assert path.startswith(blobdir), path
|
||||
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
||||
|
||||
@props.with_doc
|
||||
|
||||
@@ -155,6 +155,18 @@ class TestOfflineChat:
|
||||
chat.set_name("title2")
|
||||
assert chat.get_name() == "title2"
|
||||
|
||||
d = chat.get_summary()
|
||||
print(d)
|
||||
assert d["id"] == chat.id
|
||||
assert d["type"] == chat.get_type()
|
||||
assert d["name"] == chat.get_name()
|
||||
assert d["archived"] == chat.is_archived()
|
||||
# assert d["param"] == chat.param
|
||||
assert d["color"] == chat.get_color()
|
||||
assert d["profile_image"] == "" if chat.get_profile_image() is None else chat.get_profile_image()
|
||||
assert d["subtitle"] == chat.get_subtitle()
|
||||
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._evlogger.consume_events()
|
||||
@@ -430,6 +442,65 @@ class TestOnlineAccount:
|
||||
assert self_addr not in ev[2]
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
|
||||
|
||||
def test_send_file_twice_unicode_filename_mangling(self, tmpdir, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = self.get_chat(ac1, ac2)
|
||||
|
||||
basename = "somedäüta.html.zip"
|
||||
p = os.path.join(tmpdir.strpath, basename)
|
||||
with open(p, "w") as f:
|
||||
f.write("some data")
|
||||
|
||||
def send_and_receive_message():
|
||||
lp.sec("ac1: prepare and send attachment + text to ac2")
|
||||
msg1 = Message.new_empty(ac1, "file")
|
||||
msg1.set_text("withfile")
|
||||
msg1.set_file(p)
|
||||
message = chat.prepare_message(msg1)
|
||||
assert message.is_out_preparing()
|
||||
assert message.text == "withfile"
|
||||
chat.send_prepared(message)
|
||||
|
||||
lp.sec("ac2: receive message")
|
||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
assert ev[2] > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
return ac2.get_message_by_id(ev[2])
|
||||
|
||||
msg = send_and_receive_message()
|
||||
assert msg.text == "withfile"
|
||||
assert open(msg.filename).read() == "some data"
|
||||
assert msg.filename.endswith(basename)
|
||||
|
||||
msg2 = send_and_receive_message()
|
||||
assert msg2.text == "withfile"
|
||||
assert open(msg2.filename).read() == "some data"
|
||||
assert msg2.filename.endswith("html.zip")
|
||||
assert msg.filename != msg2.filename
|
||||
|
||||
def test_send_file_html_attachment(self, tmpdir, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = self.get_chat(ac1, ac2)
|
||||
|
||||
basename = "test.html"
|
||||
content = "<html><body>text</body>data"
|
||||
|
||||
p = os.path.join(tmpdir.strpath, basename)
|
||||
with open(p, "w") as f:
|
||||
# write wrong html to see if core tries to parse it
|
||||
# (it shouldn't as it's a file attachment)
|
||||
f.write(content)
|
||||
|
||||
lp.sec("ac1: prepare and send attachment + text to ac2")
|
||||
chat.send_file(p, mime_type="text/html")
|
||||
|
||||
lp.sec("ac2: receive message")
|
||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
assert ev[2] > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
msg = ac2.get_message_by_id(ev[2])
|
||||
|
||||
assert open(msg.filename).read() == content
|
||||
assert msg.filename.endswith(basename)
|
||||
|
||||
def test_mvbox_sentbox_threads(self, acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, sentbox=True)
|
||||
@@ -634,6 +705,32 @@ class TestOnlineAccount:
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
assert not msg.is_encrypted()
|
||||
|
||||
def test_send_first_message_as_long_unicode_with_cr(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac2.set_config("save_mime_headers", "1")
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat = self.get_chat(ac1, ac2, both_created=True)
|
||||
|
||||
lp.sec("sending multi-line non-unicode message from ac1 to ac2")
|
||||
text1 = "hello\nworld"
|
||||
msg_out = chat.send_text(text1)
|
||||
assert not msg_out.is_encrypted()
|
||||
|
||||
lp.sec("sending multi-line unicode text message from ac1 to ac2")
|
||||
text2 = "äalis\nthis is ßßÄ"
|
||||
msg_out = chat.send_text(text2)
|
||||
assert not msg_out.is_encrypted()
|
||||
|
||||
lp.sec("wait for ac2 to receive multi-line non-unicode message")
|
||||
msg_in = ac2.wait_next_incoming_message()
|
||||
assert msg_in.text == text1
|
||||
|
||||
lp.sec("wait for ac2 to receive multi-line unicode message")
|
||||
msg_in = ac2.wait_next_incoming_message()
|
||||
assert msg_in.text == text2
|
||||
assert ac1.get_config("addr") in msg_in.chat.get_name()
|
||||
|
||||
def test_reply_encrypted(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
|
||||
@@ -7,11 +7,7 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
# (some qr tests are pretty heavy in terms of send/received
|
||||
# messages and async-imap's likely has concurrency problems,
|
||||
# eg https://github.com/async-email/async-imap/issues/4 )
|
||||
pytest -n6 --reruns 3 --reruns-delay 5 -v -rsXx -k "not qr" {posargs:tests}
|
||||
pytest -n6 --reruns 5 --reruns-delay 5 -v -rsXx -k "qr" {posargs:tests}
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
|
||||
# python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
passenv =
|
||||
TRAVIS
|
||||
@@ -36,7 +32,7 @@ commands =
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
usedevelop = True
|
||||
skip_install = True
|
||||
deps =
|
||||
flake8
|
||||
# pygments required by rst-lint
|
||||
|
||||
115
src/aheader.rs
@@ -95,16 +95,25 @@ impl Aheader {
|
||||
|
||||
impl fmt::Display for Aheader {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
// TODO replace 78 with enum /rtn
|
||||
// adds a whitespace every 78 characters, this allows libEtPan to
|
||||
// wrap the lines according to RFC 5322
|
||||
write!(fmt, "addr={};", self.addr)?;
|
||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
write!(fmt, " prefer-encrypt=mutual;")?;
|
||||
}
|
||||
|
||||
// adds a whitespace every 78 characters, this allows
|
||||
// email crate to wrap the lines according to RFC 5322
|
||||
// (which may insert a linebreak before every whitespace)
|
||||
let keydata = self.public_key.to_base64(78);
|
||||
write!(
|
||||
fmt,
|
||||
"addr={}; prefer-encrypt={}; keydata={}",
|
||||
self.addr, self.prefer_encrypt, keydata
|
||||
)
|
||||
let keydata = self.public_key.to_base64().chars().enumerate().fold(
|
||||
String::new(),
|
||||
|mut res, (i, c)| {
|
||||
if i % 78 == 78 - "keydata=".len() {
|
||||
res.push(' ')
|
||||
}
|
||||
res.push(c);
|
||||
res
|
||||
},
|
||||
);
|
||||
write!(fmt, " keydata={}", keydata)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,13 +159,10 @@ impl str::FromStr for Aheader {
|
||||
}
|
||||
};
|
||||
|
||||
let prefer_encrypt = match attributes
|
||||
let prefer_encrypt = attributes
|
||||
.remove("prefer-encrypt")
|
||||
.and_then(|raw| raw.parse().ok())
|
||||
{
|
||||
Some(pref) => pref,
|
||||
None => EncryptPreference::NoPreference,
|
||||
};
|
||||
.unwrap_or_default();
|
||||
|
||||
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
||||
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
||||
@@ -176,15 +182,13 @@ impl str::FromStr for Aheader {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn rawkey() -> String {
|
||||
"xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=".into()
|
||||
}
|
||||
const RAWKEY: &str = "xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=";
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
let h: Aheader = format!(
|
||||
"addr=me@mail.com; prefer-encrypt=mutual; keydata={}",
|
||||
rawkey()
|
||||
RAWKEY
|
||||
)
|
||||
.parse()
|
||||
.expect("failed to parse");
|
||||
@@ -193,9 +197,22 @@ mod tests {
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||
}
|
||||
|
||||
// EncryptPreference::Reset is an internal value, parser should never return it
|
||||
#[test]
|
||||
fn test_from_str_reset() {
|
||||
let raw = format!(
|
||||
"addr=reset@example.com; prefer-encrypt=reset; keydata={}",
|
||||
RAWKEY
|
||||
);
|
||||
let h: Aheader = raw.parse().expect("failed to parse");
|
||||
|
||||
assert_eq!(h.addr, "reset@example.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_non_critical() {
|
||||
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={}", rawkey());
|
||||
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={}", RAWKEY);
|
||||
let h: Aheader = raw.parse().expect("failed to parse");
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
@@ -206,33 +223,57 @@ mod tests {
|
||||
fn test_from_str_superflous_critical() {
|
||||
let raw = format!(
|
||||
"addr=me@mail.com; _foo=one; _bar=two; other=me; keydata={}",
|
||||
rawkey()
|
||||
RAWKEY
|
||||
);
|
||||
assert!(raw.parse::<Aheader>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_headers() {
|
||||
let fixed_header = "addr=a@b.example.org; prefer-encrypt=mutual; keydata=xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g 4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8 ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu88 80iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTll HOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+ws CJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiML AAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiF Nyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAg dLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72 rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v 81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgC u3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOt kb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKC LhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4s WVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWv BuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiML AAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGr wdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktE k6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0 j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7x egRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=";
|
||||
let fixed_header = concat!(
|
||||
"addr=a@b.example.org; prefer-encrypt=mutual; ",
|
||||
"keydata=xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJg",
|
||||
" WL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6",
|
||||
" CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKK",
|
||||
" bhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1Kv",
|
||||
" VL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbG",
|
||||
" UuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaK",
|
||||
" rc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087",
|
||||
" LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VN",
|
||||
" HtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Ddd",
|
||||
" fxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCv",
|
||||
" SJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vau",
|
||||
" f1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+",
|
||||
" G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjm",
|
||||
" kRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09",
|
||||
" /JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHR",
|
||||
" TR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaK",
|
||||
" rc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivX",
|
||||
" urm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9Mtrm",
|
||||
" ZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb",
|
||||
" +F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNg",
|
||||
" wm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc="
|
||||
);
|
||||
|
||||
let ah = Aheader::from_str(fixed_header).expect("failed to parse");
|
||||
assert_eq!(ah.addr, "a@b.example.org");
|
||||
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
||||
assert_eq!(format!("{}", ah), fixed_header);
|
||||
|
||||
let rendered = ah.to_string();
|
||||
assert_eq!(rendered, fixed_header);
|
||||
|
||||
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", rawkey())).expect("failed to parse");
|
||||
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", RAWKEY)).expect("failed to parse");
|
||||
assert_eq!(ah.addr, "a@b.example.org");
|
||||
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
Aheader::from_str(&format!(
|
||||
"addr=a@b.example.org; prefer-encrypt=ignoreUnknownValues; keydata={}",
|
||||
rawkey()
|
||||
RAWKEY
|
||||
))
|
||||
.expect("failed to parse");
|
||||
|
||||
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", rawkey()))
|
||||
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", RAWKEY))
|
||||
.expect("failed to parse");
|
||||
}
|
||||
|
||||
@@ -244,4 +285,30 @@ mod tests {
|
||||
assert!(Aheader::from_str(" ;;").is_err());
|
||||
assert!(Aheader::from_str("addr=a@t.de; unknwon=1; keydata=jau").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_aheader() {
|
||||
assert!(format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
)
|
||||
.contains("prefer-encrypt=mutual;"));
|
||||
|
||||
// According to Autocrypt Level 1 specification,
|
||||
// only "prefer-encrypt=mutual;" can be used.
|
||||
// If the setting is nopreference, the whole attribute is omitted.
|
||||
assert!(!format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
|
||||
EncryptPreference::NoPreference
|
||||
)
|
||||
)
|
||||
.contains("prefer-encrypt"));
|
||||
}
|
||||
}
|
||||
|
||||
69
src/chat.rs
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use itertools::Itertools;
|
||||
use num_traits::FromPrimitive;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::blob::{BlobError, BlobObject};
|
||||
use crate::chatlist::*;
|
||||
@@ -724,11 +725,21 @@ fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> {
|
||||
if msg.type_0 == Viewtype::Text {
|
||||
// the caller should check if the message text is empty
|
||||
} else if msgtype_has_file(msg.type_0) {
|
||||
let blob = msg
|
||||
.param
|
||||
.get_blob(Param::File, context, !msg.is_increation())?
|
||||
.ok_or_else(|| format_err!("Attachment missing for message of type #{}", msg.type_0))?;
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
let blob = if let Some(f) = msg.param.get_file(Param::File, context)? {
|
||||
match f {
|
||||
ParamsFile::Blob(blob) => blob,
|
||||
ParamsFile::FsPath(path) => {
|
||||
// path is outside the blobdir, let's copy
|
||||
let blob = BlobObject::create_and_copy(context, path)?;
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
|
||||
blob
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("Attachment missing for message of type #{}", msg.type_0);
|
||||
};
|
||||
|
||||
if msg.type_0 == Viewtype::File || msg.type_0 == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
// formats as GIF or VOICE.
|
||||
@@ -1903,6 +1914,54 @@ pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: u32) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_info_json(context: &Context, chat_id: u32) -> Result<String, Error> {
|
||||
let chat = Chat::load_from_db(context, chat_id).unwrap();
|
||||
|
||||
// ToDo:
|
||||
// - [x] id
|
||||
// - [x] type
|
||||
// - [x] name
|
||||
// - [x] archived
|
||||
// - [x] color
|
||||
// - [x] profileImage
|
||||
// - [x] subtitle
|
||||
// - [x] draft,
|
||||
// - [ ] deaddrop,
|
||||
// - [ ] summary,
|
||||
// - [ ] lastUpdated,
|
||||
// - [ ] freshMessageCounter,
|
||||
// - [ ] email
|
||||
|
||||
let profile_image = match chat.get_profile_image(context) {
|
||||
Some(path) => path.into_os_string().into_string().unwrap(),
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
let draft = match get_draft(context, chat_id) {
|
||||
Ok(message) => match message {
|
||||
Some(m) => m.text.unwrap_or_else(|| "".to_string()),
|
||||
None => "".to_string(),
|
||||
},
|
||||
Err(_) => "".to_string(),
|
||||
};
|
||||
|
||||
let s = json!({
|
||||
"id": chat.id,
|
||||
"type": chat.typ as u32,
|
||||
"name": chat.name,
|
||||
"archived": chat.archived,
|
||||
"param": chat.param.to_string(),
|
||||
"gossiped_timestamp": chat.gossiped_timestamp,
|
||||
"is_sending_locations": chat.is_sending_locations,
|
||||
"color": chat.get_color(context),
|
||||
"profile_image": profile_image,
|
||||
"subtitle": chat.get_subtitle(context),
|
||||
"draft": draft
|
||||
});
|
||||
|
||||
Ok(s.to_string())
|
||||
}
|
||||
|
||||
pub fn get_chat_contact_cnt(context: &Context, chat_id: u32) -> usize {
|
||||
context
|
||||
.sql
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! # Thunderbird's Autoconfiguration implementation
|
||||
//!
|
||||
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
|
||||
use failure::Fail;
|
||||
|
||||
use quick_xml;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Outlook's Autodiscover
|
||||
|
||||
use failure::Fail;
|
||||
|
||||
use quick_xml;
|
||||
use quick_xml::events::BytesEnd;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::context::Context;
|
||||
use failure::Fail;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
|
||||
@@ -47,6 +47,11 @@ pub fn dc_receive_imf(
|
||||
server_uid,
|
||||
);
|
||||
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" {
|
||||
info!(context, "dc_receive_imf: incoming message mime-body:");
|
||||
println!("{}", String::from_utf8_lossy(imf_raw));
|
||||
}
|
||||
|
||||
let mime_parser = MimeParser::from_bytes(context, imf_raw);
|
||||
let mut mime_parser = if let Err(err) = mime_parser {
|
||||
warn!(context, "dc_receive_imf parse error: {}", err);
|
||||
@@ -332,7 +337,7 @@ fn add_parts(
|
||||
}
|
||||
|
||||
// 1 or 0 for yes/no
|
||||
msgrmsg = mime_parser.is_send_by_messenger as _;
|
||||
msgrmsg = mime_parser.has_chat_version() as _;
|
||||
if msgrmsg == 0 && 0 != dc_is_reply_to_messenger_message(context, mime_parser) {
|
||||
// 2=no, but is reply to messenger message
|
||||
msgrmsg = 2;
|
||||
@@ -1107,7 +1112,7 @@ fn create_or_lookup_group(
|
||||
// the only critical situation is if the user hits "Reply" instead
|
||||
// of "Reply all" in a non-messenger-client */
|
||||
if to_ids_cnt == 1
|
||||
&& !mime_parser.is_send_by_messenger
|
||||
&& !mime_parser.has_chat_version()
|
||||
&& chat::get_chat_contact_cnt(context, chat_id) > 3
|
||||
{
|
||||
// to_ids_cnt==1 may be "From: A, To: B, SELF" as SELF is not counted in to_ids_cnt.
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
use itertools::Itertools;
|
||||
|
||||
/// Encode non-ascii-strings as `=?UTF-8?Q?Bj=c3=b6rn_Petersen?=`.
|
||||
/// Belongs to RFC 2047: https://tools.ietf.org/html/rfc2047
|
||||
///
|
||||
/// We do not fold at position 72; this would result in empty words as `=?utf-8?Q??=` which are correct,
|
||||
/// but cannot be displayed by some mail programs (eg. Android Stock Mail).
|
||||
/// however, this is not needed, as long as _one_ word is not longer than 72 characters.
|
||||
/// _if_ it is, the display may get weird. This affects the subject only.
|
||||
/// the best solution wor all this would be if libetpan encodes the line as only libetpan knowns when a header line is full.
|
||||
///
|
||||
/// @param to_encode Null-terminated UTF-8-string to encode.
|
||||
/// @return Returns the encoded string which must be free()'d when no longed needed.
|
||||
/// On errors, NULL is returned.
|
||||
pub fn dc_encode_header_words(input: impl AsRef<str>) -> String {
|
||||
let mut result = String::default();
|
||||
for (_, group) in &input.as_ref().chars().group_by(|c| c.is_whitespace()) {
|
||||
let word: String = group.collect();
|
||||
result.push_str("e_word(&word.as_bytes()));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn must_encode(byte: u8) -> bool {
|
||||
static SPECIALS: &[u8] = b",:!\"#$@[\\]^`{|}~=?_";
|
||||
|
||||
SPECIALS.iter().any(|b| *b == byte)
|
||||
}
|
||||
|
||||
fn quote_word(word: &[u8]) -> String {
|
||||
let mut result = String::default();
|
||||
let mut encoded = false;
|
||||
|
||||
for byte in word {
|
||||
let byte = *byte;
|
||||
if byte >= 128 || must_encode(byte) {
|
||||
result.push_str(&format!("={:2X}", byte));
|
||||
encoded = true;
|
||||
} else if byte == b' ' {
|
||||
result.push('_');
|
||||
encoded = true;
|
||||
} else {
|
||||
result.push(byte as _);
|
||||
}
|
||||
}
|
||||
|
||||
if encoded {
|
||||
result = format!("=?utf-8?Q?{}?=", &result);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/* ******************************************************************************
|
||||
* Encode/decode header words, RFC 2047
|
||||
******************************************************************************/
|
||||
|
||||
pub fn dc_needs_ext_header(to_check: impl AsRef<str>) -> bool {
|
||||
let to_check = to_check.as_ref();
|
||||
|
||||
if to_check.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
to_check.chars().any(|c| {
|
||||
!c.is_ascii_alphanumeric() && c != '-' && c != '_' && c != '.' && c != '~' && c != '%'
|
||||
})
|
||||
}
|
||||
@@ -278,11 +278,9 @@ pub fn dc_derive_safe_stem_ext(filename: &str) -> (String, String) {
|
||||
// the returned suffix is lower-case
|
||||
#[allow(non_snake_case)]
|
||||
pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
|
||||
if let Some(p) = Path::new(path_filename.as_ref()).extension() {
|
||||
Some(p.to_string_lossy().to_lowercase())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Path::new(path_filename.as_ref())
|
||||
.extension()
|
||||
.map(|p| p.to_string_lossy().to_lowercase())
|
||||
}
|
||||
|
||||
/// Returns the `(width, height)` of the given image buffer.
|
||||
|
||||
@@ -247,13 +247,13 @@ fn decrypt_if_autocrypt_message<'a>(
|
||||
// Errors are returned for failures related to decryption of AC-messages.
|
||||
|
||||
let encrypted_data_part = match wrapmime::get_autocrypt_mime(mail) {
|
||||
Err(err) => {
|
||||
// not a proper autocrypt message, abort and ignore
|
||||
info!(context, "Not an autocrypt message: {:?}", err);
|
||||
Err(_) => {
|
||||
// not an autocrypt mime message, abort and ignore
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(res) => res,
|
||||
};
|
||||
info!(context, "Detected Autocrypt-mime message");
|
||||
|
||||
decrypt_part(
|
||||
context,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! # Error handling
|
||||
|
||||
use failure::Fail;
|
||||
use lettre_email::mime;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
|
||||
250
src/imap/idle.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use super::Imap;
|
||||
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::imap_client::*;
|
||||
|
||||
use super::select_folder;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
|
||||
IdleProtocolFailed(#[cause] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "IMAP server does not have IDLE capability")]
|
||||
IdleAbilityMissing,
|
||||
|
||||
#[fail(display = "IMAP select folder error")]
|
||||
SelectFolderError(#[cause] select_folder::Error),
|
||||
|
||||
#[fail(display = "IMAP error")]
|
||||
ImapError(#[cause] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "Setup handle error")]
|
||||
SetupHandleError(#[cause] super::Error),
|
||||
}
|
||||
|
||||
impl From<select_folder::Error> for Error {
|
||||
fn from(err: select_folder::Error) -> Error {
|
||||
Error::SelectFolderError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl Imap {
|
||||
pub fn can_idle(&self) -> bool {
|
||||
task::block_on(async move { self.config.read().await.can_idle })
|
||||
}
|
||||
|
||||
pub fn idle(&self, context: &Context, watch_folder: Option<String>) -> Result<()> {
|
||||
task::block_on(async move {
|
||||
if !self.can_idle() {
|
||||
return Err(Error::IdleAbilityMissing);
|
||||
}
|
||||
|
||||
self.setup_handle_if_needed(context)
|
||||
.await
|
||||
.map_err(Error::SetupHandleError)?;
|
||||
|
||||
self.select_folder(context, watch_folder.clone()).await?;
|
||||
|
||||
let session = self.session.lock().await.take();
|
||||
let timeout = Duration::from_secs(23 * 60);
|
||||
if let Some(session) = session {
|
||||
match session.idle() {
|
||||
// BEWARE: If you change the Secure branch you
|
||||
// typically also need to change the Insecure branch.
|
||||
IdleHandle::Secure(mut handle) => {
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
|
||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
||||
std::mem::drop(idle_wait);
|
||||
info!(context, "Idle wait was skipped");
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
match idle_wait.await {
|
||||
IdleResponse::NewData(_) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
IdleResponse::Timeout => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
IdleResponse::ManualInterrupt => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
}
|
||||
}
|
||||
match handle.done().await {
|
||||
Ok(session) => {
|
||||
*self.session.lock().await = Some(Session::Secure(session));
|
||||
}
|
||||
Err(err) => {
|
||||
// if we cannot terminate IDLE it probably
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
IdleHandle::Insecure(mut handle) => {
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
|
||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
||||
std::mem::drop(idle_wait);
|
||||
info!(context, "Idle wait was skipped");
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
match idle_wait.await {
|
||||
IdleResponse::NewData(_) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
IdleResponse::Timeout => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
IdleResponse::ManualInterrupt => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
}
|
||||
}
|
||||
match handle.done().await {
|
||||
Ok(session) => {
|
||||
*self.session.lock().await = Some(Session::Insecure(session));
|
||||
}
|
||||
Err(err) => {
|
||||
// if we cannot terminate IDLE it probably
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fake_idle(&self, context: &Context, watch_folder: Option<String>) {
|
||||
// Idle using polling. This is also needed if we're not yet configured -
|
||||
// in this case, we're waiting for a configure job (and an interrupt).
|
||||
task::block_on(async move {
|
||||
let fake_idle_start_time = SystemTime::now();
|
||||
|
||||
info!(context, "IMAP-fake-IDLEing...");
|
||||
|
||||
let interrupt = stop_token::StopSource::new();
|
||||
|
||||
// check every minute if there are new messages
|
||||
// TODO: grow sleep durations / make them more flexible
|
||||
let interval = async_std::stream::interval(Duration::from_secs(60));
|
||||
let mut interrupt_interval = interrupt.stop_token().stop_stream(interval);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
||||
info!(context, "fake-idle wait was skipped");
|
||||
} else {
|
||||
// loop until we are interrupted or if we fetched something
|
||||
while let Some(_) = interrupt_interval.next().await {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if let Err(err) = self.connect_configured(context) {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.read().await.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break;
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
match self.fetch_new_messages(context, watch_folder).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
if res {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.interrupt.lock().await.take();
|
||||
|
||||
info!(
|
||||
context,
|
||||
"IMAP-fake-IDLE done after {:.4}s",
|
||||
SystemTime::now()
|
||||
.duration_since(fake_idle_start_time)
|
||||
.unwrap()
|
||||
.as_millis() as f64
|
||||
/ 1000.,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn interrupt_idle(&self, context: &Context) {
|
||||
task::block_on(async move {
|
||||
let mut interrupt: Option<stop_token::StopSource> = self.interrupt.lock().await.take();
|
||||
if interrupt.is_none() {
|
||||
// idle wait is not running, signal it needs to skip
|
||||
self.skip_next_idle_wait.store(true, Ordering::SeqCst);
|
||||
|
||||
// meanwhile idle-wait may have produced the StopSource
|
||||
interrupt = self.interrupt.lock().await.take();
|
||||
}
|
||||
// let's manually drop the StopSource
|
||||
if interrupt.is_some() {
|
||||
// the imap thread provided us a stop token but might
|
||||
// not have entered idle_wait yet, give it some time
|
||||
// for that to happen. XXX handle this without extra wait
|
||||
// https://github.com/deltachat/deltachat-core-rust/issues/925
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
info!(context, "low-level: dropping stop-source to interrupt idle");
|
||||
std::mem::drop(interrupt)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
440
src/imap/mod.rs
@@ -4,14 +4,11 @@
|
||||
//! to implement connect, fetch, delete functionality with standard IMAP servers.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use async_imap::{
|
||||
error::Result as ImapResult,
|
||||
extensions::idle::IdleResponse,
|
||||
types::{Fetch, Flag, Mailbox, Name, NameAttribute},
|
||||
};
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use async_std::task;
|
||||
|
||||
@@ -28,6 +25,7 @@ use crate::param::Params;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::wrapmime;
|
||||
|
||||
mod idle;
|
||||
pub mod select_folder;
|
||||
|
||||
const DC_IMAP_SEEN: usize = 0x0001;
|
||||
@@ -51,22 +49,16 @@ pub enum Error {
|
||||
#[fail(display = "IMAP Could not login as {}", _0)]
|
||||
LoginFailed(String),
|
||||
|
||||
#[fail(display = "IMAP Could not fetch {}", _0)]
|
||||
#[fail(display = "IMAP Could not fetch")]
|
||||
FetchFailed(#[cause] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
|
||||
IdleProtocolFailed(#[cause] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "IMAP server does not have IDLE capability")]
|
||||
IdleAbilityMissing,
|
||||
|
||||
#[fail(display = "IMAP operation attempted while it is torn down")]
|
||||
InTeardown,
|
||||
|
||||
#[fail(display = "IMAP operation attempted while it is torn down")]
|
||||
SqlError(#[cause] crate::sql::Error),
|
||||
|
||||
#[fail(display = "IMAP got error from elsewhere: {:?}", _0)]
|
||||
#[fail(display = "IMAP got error from elsewhere")]
|
||||
WrappedError(#[cause] crate::error::Error),
|
||||
|
||||
#[fail(display = "IMAP select folder error")]
|
||||
@@ -201,112 +193,107 @@ impl Imap {
|
||||
self.should_reconnect.store(true, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn setup_handle_if_needed(&self, context: &Context) -> Result<()> {
|
||||
task::block_on(async move {
|
||||
if self.config.read().await.imap_server.is_empty() {
|
||||
return Err(Error::InTeardown);
|
||||
}
|
||||
async fn setup_handle_if_needed(&self, context: &Context) -> Result<()> {
|
||||
if self.config.read().await.imap_server.is_empty() {
|
||||
return Err(Error::InTeardown);
|
||||
}
|
||||
|
||||
if self.should_reconnect() {
|
||||
self.unsetup_handle(context).await;
|
||||
self.should_reconnect.store(false, Ordering::Relaxed);
|
||||
} else if self.is_connected().await {
|
||||
return Ok(());
|
||||
}
|
||||
if self.should_reconnect() {
|
||||
self.unsetup_handle(context).await;
|
||||
self.should_reconnect.store(false, Ordering::Relaxed);
|
||||
} else if self.is_connected().await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let server_flags = self.config.read().await.server_flags as i32;
|
||||
let server_flags = self.config.read().await.server_flags as i32;
|
||||
|
||||
let connection_res: ImapResult<Client> =
|
||||
if (server_flags & (DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_PLAIN)) != 0 {
|
||||
let config = self.config.read().await;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
let connection_res: ImapResult<Client> =
|
||||
if (server_flags & (DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_PLAIN)) != 0 {
|
||||
let config = self.config.read().await;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
|
||||
match Client::connect_insecure((imap_server, imap_port)).await {
|
||||
Ok(client) => {
|
||||
if (server_flags & DC_LP_IMAP_SOCKET_STARTTLS) != 0 {
|
||||
client.secure(imap_server, config.certificate_checks).await
|
||||
} else {
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
} else {
|
||||
let config = self.config.read().await;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
|
||||
Client::connect_secure(
|
||||
(imap_server, imap_port),
|
||||
imap_server,
|
||||
config.certificate_checks,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
let login_res = match connection_res {
|
||||
Ok(client) => {
|
||||
let config = self.config.read().await;
|
||||
let imap_user: &str = config.imap_user.as_ref();
|
||||
let imap_pw: &str = config.imap_pw.as_ref();
|
||||
|
||||
if (server_flags & DC_LP_AUTH_OAUTH2) != 0 {
|
||||
let addr: &str = config.addr.as_ref();
|
||||
|
||||
if let Some(token) =
|
||||
dc_get_oauth2_access_token(context, addr, imap_pw, true)
|
||||
{
|
||||
let auth = OAuth2 {
|
||||
user: imap_user.into(),
|
||||
access_token: token,
|
||||
};
|
||||
client.authenticate("XOAUTH2", &auth).await
|
||||
match Client::connect_insecure((imap_server, imap_port)).await {
|
||||
Ok(client) => {
|
||||
if (server_flags & DC_LP_IMAP_SOCKET_STARTTLS) != 0 {
|
||||
client.secure(imap_server, config.certificate_checks).await
|
||||
} else {
|
||||
return Err(Error::OauthError);
|
||||
Ok(client)
|
||||
}
|
||||
} else {
|
||||
client.login(imap_user, imap_pw).await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
Err(err) => {
|
||||
let message = {
|
||||
let config = self.config.read().await;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
context.stock_string_repl_str2(
|
||||
StockMessage::ServerResponse,
|
||||
format!("{}:{}", imap_server, imap_port),
|
||||
err.to_string(),
|
||||
)
|
||||
};
|
||||
// IMAP connection failures are reported to users
|
||||
emit_event!(context, Event::ErrorNetwork(message));
|
||||
return Err(Error::ConnectionFailed(err.to_string()));
|
||||
}
|
||||
} else {
|
||||
let config = self.config.read().await;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
|
||||
Client::connect_secure(
|
||||
(imap_server, imap_port),
|
||||
imap_server,
|
||||
config.certificate_checks,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
self.should_reconnect.store(false, Ordering::Relaxed);
|
||||
let login_res = match connection_res {
|
||||
Ok(client) => {
|
||||
let config = self.config.read().await;
|
||||
let imap_user: &str = config.imap_user.as_ref();
|
||||
let imap_pw: &str = config.imap_pw.as_ref();
|
||||
|
||||
match login_res {
|
||||
Ok(session) => {
|
||||
*self.session.lock().await = Some(session);
|
||||
Ok(())
|
||||
}
|
||||
Err((err, _)) => {
|
||||
let imap_user = self.config.read().await.imap_user.to_owned();
|
||||
let message =
|
||||
context.stock_string_repl_str(StockMessage::CannotLogin, &imap_user);
|
||||
if (server_flags & DC_LP_AUTH_OAUTH2) != 0 {
|
||||
let addr: &str = config.addr.as_ref();
|
||||
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorNetwork(format!("{} ({})", message, err))
|
||||
);
|
||||
self.trigger_reconnect();
|
||||
Err(Error::LoginFailed(format!("cannot login as {}", imap_user)))
|
||||
if let Some(token) = dc_get_oauth2_access_token(context, addr, imap_pw, true) {
|
||||
let auth = OAuth2 {
|
||||
user: imap_user.into(),
|
||||
access_token: token,
|
||||
};
|
||||
client.authenticate("XOAUTH2", &auth).await
|
||||
} else {
|
||||
return Err(Error::OauthError);
|
||||
}
|
||||
} else {
|
||||
client.login(imap_user, imap_pw).await
|
||||
}
|
||||
}
|
||||
})
|
||||
Err(err) => {
|
||||
let message = {
|
||||
let config = self.config.read().await;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
context.stock_string_repl_str2(
|
||||
StockMessage::ServerResponse,
|
||||
format!("{}:{}", imap_server, imap_port),
|
||||
err.to_string(),
|
||||
)
|
||||
};
|
||||
// IMAP connection failures are reported to users
|
||||
emit_event!(context, Event::ErrorNetwork(message));
|
||||
return Err(Error::ConnectionFailed(err.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
self.should_reconnect.store(false, Ordering::Relaxed);
|
||||
|
||||
match login_res {
|
||||
Ok(session) => {
|
||||
*self.session.lock().await = Some(session);
|
||||
Ok(())
|
||||
}
|
||||
Err((err, _)) => {
|
||||
let imap_user = self.config.read().await.imap_user.to_owned();
|
||||
let message = context.stock_string_repl_str(StockMessage::CannotLogin, &imap_user);
|
||||
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorNetwork(format!("{} ({})", message, err))
|
||||
);
|
||||
self.trigger_reconnect();
|
||||
Err(Error::LoginFailed(format!("cannot login as {}", imap_user)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn unsetup_handle(&self, context: &Context) {
|
||||
@@ -387,7 +374,7 @@ impl Imap {
|
||||
config.server_flags = server_flags;
|
||||
}
|
||||
|
||||
if let Err(err) = self.setup_handle_if_needed(context) {
|
||||
if let Err(err) = self.setup_handle_if_needed(context).await {
|
||||
warn!(context, "failed to setup imap handle: {}", err);
|
||||
self.free_connect_params().await;
|
||||
return false;
|
||||
@@ -443,20 +430,17 @@ impl Imap {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn fetch(&self, context: &Context, watch_folder: &str) -> Result<()> {
|
||||
task::block_on(async move {
|
||||
if !context.sql.is_open() {
|
||||
// probably shutdown
|
||||
return Err(Error::InTeardown);
|
||||
}
|
||||
while self
|
||||
.fetch_from_single_folder(context, &watch_folder)
|
||||
.await?
|
||||
{
|
||||
// We fetch until no more new messages are there.
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
pub async fn fetch(&self, context: &Context, watch_folder: &str) -> Result<()> {
|
||||
if !context.sql.is_open() {
|
||||
// probably shutdown
|
||||
return Err(Error::InTeardown);
|
||||
}
|
||||
self.setup_handle_if_needed(context).await?;
|
||||
|
||||
while self.fetch_new_messages(context, &watch_folder).await? {
|
||||
// We fetch until no more new messages are there.
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_config_last_seen_uid<S: AsRef<str>>(&self, context: &Context, folder: S) -> (u32, u32) {
|
||||
@@ -560,7 +544,7 @@ impl Imap {
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_from_single_folder<S: AsRef<str>>(
|
||||
async fn fetch_new_messages<S: AsRef<str>>(
|
||||
&self,
|
||||
context: &Context,
|
||||
folder: S,
|
||||
@@ -593,9 +577,11 @@ impl Imap {
|
||||
for msg in &list {
|
||||
let cur_uid = msg.uid.unwrap_or_default();
|
||||
if cur_uid <= last_seen_uid {
|
||||
warn!(
|
||||
// seems that at least dovecot sends the last available UID
|
||||
// even if we asked for higher UID+N:*
|
||||
info!(
|
||||
context,
|
||||
"unexpected uid {}, last seen was {}", cur_uid, last_seen_uid
|
||||
"fetch_new_messages: ignoring uid {}, last seen was {}", cur_uid, last_seen_uid
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -734,210 +720,6 @@ impl Imap {
|
||||
1
|
||||
}
|
||||
|
||||
pub fn idle(&self, context: &Context, watch_folder: Option<String>) -> Result<()> {
|
||||
task::block_on(async move {
|
||||
if !self.config.read().await.can_idle {
|
||||
return Err(Error::IdleAbilityMissing);
|
||||
}
|
||||
|
||||
self.setup_handle_if_needed(context)?;
|
||||
|
||||
self.select_folder(context, watch_folder.clone()).await?;
|
||||
|
||||
let session = self.session.lock().await.take();
|
||||
let timeout = Duration::from_secs(23 * 60);
|
||||
if let Some(session) = session {
|
||||
match session.idle() {
|
||||
// BEWARE: If you change the Secure branch you
|
||||
// typically also need to change the Insecure branch.
|
||||
IdleHandle::Secure(mut handle) => {
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
|
||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
||||
std::mem::drop(idle_wait);
|
||||
info!(context, "Idle wait was skipped");
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
match idle_wait.await {
|
||||
IdleResponse::NewData(_) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
IdleResponse::Timeout => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
IdleResponse::ManualInterrupt => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
}
|
||||
}
|
||||
match handle.done().await {
|
||||
Ok(session) => {
|
||||
*self.session.lock().await = Some(Session::Secure(session));
|
||||
}
|
||||
Err(err) => {
|
||||
// if we cannot terminate IDLE it probably
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
IdleHandle::Insecure(mut handle) => {
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
|
||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
||||
std::mem::drop(idle_wait);
|
||||
info!(context, "Idle wait was skipped");
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
match idle_wait.await {
|
||||
IdleResponse::NewData(_) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
IdleResponse::Timeout => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
IdleResponse::ManualInterrupt => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
}
|
||||
}
|
||||
match handle.done().await {
|
||||
Ok(session) => {
|
||||
*self.session.lock().await = Some(Session::Insecure(session));
|
||||
}
|
||||
Err(err) => {
|
||||
// if we cannot terminate IDLE it probably
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fake_idle(&self, context: &Context, watch_folder: Option<String>) {
|
||||
// Idle using polling. This is also needed if we're not yet configured -
|
||||
// in this case, we're waiting for a configure job (and an interrupt).
|
||||
task::block_on(async move {
|
||||
let fake_idle_start_time = SystemTime::now();
|
||||
|
||||
info!(context, "IMAP-fake-IDLEing...");
|
||||
|
||||
let interrupt = stop_token::StopSource::new();
|
||||
|
||||
// check every minute if there are new messages
|
||||
// TODO: grow sleep durations / make them more flexible
|
||||
let interval = async_std::stream::interval(Duration::from_secs(60));
|
||||
let mut interrupt_interval = interrupt.stop_token().stop_stream(interval);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
||||
info!(context, "fake-idle wait was skipped");
|
||||
} else {
|
||||
// loop until we are interrupted or if we fetched something
|
||||
while let Some(_) = interrupt_interval.next().await {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if let Err(err) = self.connect_configured(context) {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.read().await.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break;
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
match self.fetch_from_single_folder(context, watch_folder).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_from_single_folder returned {:?}", res);
|
||||
if res {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.interrupt.lock().await.take();
|
||||
|
||||
info!(
|
||||
context,
|
||||
"IMAP-fake-IDLE done after {:.4}s",
|
||||
SystemTime::now()
|
||||
.duration_since(fake_idle_start_time)
|
||||
.unwrap()
|
||||
.as_millis() as f64
|
||||
/ 1000.,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn interrupt_idle(&self, context: &Context) {
|
||||
task::block_on(async move {
|
||||
let mut interrupt: Option<stop_token::StopSource> = self.interrupt.lock().await.take();
|
||||
if interrupt.is_none() {
|
||||
// idle wait is not running, signal it needs to skip
|
||||
self.skip_next_idle_wait.store(true, Ordering::SeqCst);
|
||||
|
||||
// meanwhile idle-wait may have produced the StopSource
|
||||
interrupt = self.interrupt.lock().await.take();
|
||||
}
|
||||
// let's manually drop the StopSource
|
||||
if interrupt.is_some() {
|
||||
// the imap thread provided us a stop token but might
|
||||
// not have entered idle_wait yet, give it some time
|
||||
// for that to happen. XXX handle this without extra wait
|
||||
// https://github.com/deltachat/deltachat-core-rust/issues/925
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
info!(context, "low-level: dropping stop-source to interrupt idle");
|
||||
std::mem::drop(interrupt)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn mv(
|
||||
&self,
|
||||
context: &Context,
|
||||
@@ -1322,13 +1104,17 @@ impl Imap {
|
||||
task::block_on(async move {
|
||||
info!(context, "emptying folder {}", folder);
|
||||
|
||||
// we want to report all error to the user
|
||||
// (no retry should be attempted)
|
||||
if folder.is_empty() {
|
||||
error!(context, "cannot perform empty, folder not set");
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.setup_handle_if_needed(context).await {
|
||||
error!(context, "could not setup imap connection: {:?}", err);
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.select_folder(context, Some(&folder)).await {
|
||||
// we want to report all error to the user
|
||||
// (no retry should be attempted)
|
||||
error!(
|
||||
context,
|
||||
"Could not select {} for expunging: {:?}", folder, err
|
||||
|
||||
@@ -34,6 +34,7 @@ impl Imap {
|
||||
let mut cfg = self.config.write().await;
|
||||
cfg.selected_folder = None;
|
||||
cfg.selected_folder_needs_expunge = false;
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
|
||||
@@ -61,6 +62,7 @@ impl Imap {
|
||||
info!(context, "close/expunge succeeded");
|
||||
}
|
||||
Err(err) => {
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::CloseExpungeFailed(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ use async_tls::client::TlsStream;
|
||||
|
||||
use crate::login_param::{dc_build_tls_config, CertificateChecks};
|
||||
|
||||
const DCC_IMAP_DEBUG: &str = "DCC_IMAP_DEBUG";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Client {
|
||||
Secure(ImapClient<TlsStream<TcpStream>>),
|
||||
@@ -42,7 +40,7 @@ impl Client {
|
||||
let tls_connector: async_tls::TlsConnector = Arc::new(tls_config).into();
|
||||
let tls_stream = tls_connector.connect(domain.as_ref(), stream)?.await?;
|
||||
let mut client = ImapClient::new(tls_stream);
|
||||
if std::env::var(DCC_IMAP_DEBUG).is_ok() {
|
||||
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
|
||||
client.debug = true;
|
||||
}
|
||||
|
||||
@@ -58,7 +56,7 @@ impl Client {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
|
||||
let mut client = ImapClient::new(stream);
|
||||
if std::env::var(DCC_IMAP_DEBUG).is_ok() {
|
||||
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
|
||||
client.debug = true;
|
||||
}
|
||||
let _greeting = client
|
||||
|
||||
42
src/job.rs
@@ -8,6 +8,8 @@ use std::time::Duration;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use async_std::task;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat;
|
||||
use crate::config::Config;
|
||||
@@ -192,6 +194,10 @@ impl Job {
|
||||
// was sent we need to mark it in the database ASAP as we
|
||||
// otherwise might send it twice.
|
||||
let mut smtp = context.smtp.lock().unwrap();
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(context, "smtp-sending out mime message:");
|
||||
println!("{}", String::from_utf8_lossy(&body));
|
||||
}
|
||||
match smtp.send(context, recipients_list, body, self.job_id) {
|
||||
Err(crate::smtp::send::Error::SendError(err)) => {
|
||||
// Remote error, retry later.
|
||||
@@ -394,31 +400,37 @@ pub fn job_kill_action(context: &Context, action: Action) -> bool {
|
||||
pub fn perform_inbox_fetch(context: &Context) {
|
||||
let use_network = context.get_config_bool(Config::InboxWatch);
|
||||
|
||||
context
|
||||
.inbox_thread
|
||||
.write()
|
||||
.unwrap()
|
||||
.fetch(context, use_network);
|
||||
task::block_on(
|
||||
context
|
||||
.inbox_thread
|
||||
.write()
|
||||
.unwrap()
|
||||
.fetch(context, use_network),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn perform_mvbox_fetch(context: &Context) {
|
||||
let use_network = context.get_config_bool(Config::MvboxWatch);
|
||||
|
||||
context
|
||||
.mvbox_thread
|
||||
.write()
|
||||
.unwrap()
|
||||
.fetch(context, use_network);
|
||||
task::block_on(
|
||||
context
|
||||
.mvbox_thread
|
||||
.write()
|
||||
.unwrap()
|
||||
.fetch(context, use_network),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn perform_sentbox_fetch(context: &Context) {
|
||||
let use_network = context.get_config_bool(Config::SentboxWatch);
|
||||
|
||||
context
|
||||
.sentbox_thread
|
||||
.write()
|
||||
.unwrap()
|
||||
.fetch(context, use_network);
|
||||
task::block_on(
|
||||
context
|
||||
.sentbox_thread
|
||||
.write()
|
||||
.unwrap()
|
||||
.fetch(context, use_network),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn perform_inbox_idle(context: &Context) {
|
||||
|
||||
@@ -73,7 +73,7 @@ impl JobThread {
|
||||
info!(context, "Interrupting {}-IDLE... finished", self.name);
|
||||
}
|
||||
|
||||
pub fn fetch(&mut self, context: &Context, use_network: bool) {
|
||||
pub async fn fetch(&mut self, context: &Context, use_network: bool) {
|
||||
{
|
||||
let &(ref lock, _) = &*self.state.clone();
|
||||
let mut state = lock.lock().unwrap();
|
||||
@@ -86,10 +86,10 @@ impl JobThread {
|
||||
}
|
||||
|
||||
if use_network {
|
||||
if let Err(err) = self.connect_and_fetch(context) {
|
||||
if let Err(err) = self.connect_and_fetch(context).await {
|
||||
warn!(context, "connect+fetch failed: {}, reconnect & retry", err);
|
||||
self.imap.trigger_reconnect();
|
||||
if let Err(err) = self.connect_and_fetch(context) {
|
||||
if let Err(err) = self.connect_and_fetch(context).await {
|
||||
warn!(context, "connect+fetch failed: {}", err);
|
||||
}
|
||||
}
|
||||
@@ -97,14 +97,18 @@ impl JobThread {
|
||||
self.state.0.lock().unwrap().using_handle = false;
|
||||
}
|
||||
|
||||
fn connect_and_fetch(&mut self, context: &Context) -> Result<()> {
|
||||
async fn connect_and_fetch(&mut self, context: &Context) -> Result<()> {
|
||||
let prefix = format!("{}-fetch", self.name);
|
||||
match self.imap.connect_configured(context) {
|
||||
Ok(()) => {
|
||||
if let Some(watch_folder) = self.get_watch_folder(context) {
|
||||
let start = std::time::Instant::now();
|
||||
info!(context, "{} started...", prefix);
|
||||
let res = self.imap.fetch(context, &watch_folder).map_err(Into::into);
|
||||
let res = self
|
||||
.imap
|
||||
.fetch(context, &watch_folder)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
let elapsed = start.elapsed().as_millis();
|
||||
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
|
||||
|
||||
@@ -170,20 +174,19 @@ impl JobThread {
|
||||
let prefix = format!("{}-IDLE", self.name);
|
||||
let do_fake_idle = match self.imap.connect_configured(context) {
|
||||
Ok(()) => {
|
||||
info!(context, "{} started...", prefix);
|
||||
let watch_folder = self.get_watch_folder(context);
|
||||
let res = self.imap.idle(context, watch_folder);
|
||||
info!(context, "{} ended...", prefix);
|
||||
match res {
|
||||
Ok(()) => false,
|
||||
Err(crate::imap::Error::IdleAbilityMissing) => true, // we have to do fake_idle
|
||||
Err(err) => {
|
||||
if !self.imap.can_idle() {
|
||||
true // we have to do fake_idle
|
||||
} else {
|
||||
let watch_folder = self.get_watch_folder(context);
|
||||
info!(context, "{} started...", prefix);
|
||||
let res = self.imap.idle(context, watch_folder);
|
||||
info!(context, "{} ended...", prefix);
|
||||
if let Err(err) = res {
|
||||
warn!(context, "{} failed: {} -> reconnecting", prefix, err);
|
||||
// something is borked, let's start afresh on the next occassion
|
||||
self.imap.disconnect(context);
|
||||
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
15
src/key.rs
@@ -178,20 +178,9 @@ impl Key {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_base64(&self, break_every: usize) -> String {
|
||||
pub fn to_base64(&self) -> String {
|
||||
let buf = self.to_bytes();
|
||||
|
||||
let encoded = base64::encode(&buf);
|
||||
encoded
|
||||
.chars()
|
||||
.enumerate()
|
||||
.fold(String::new(), |mut res, (i, c)| {
|
||||
if i > 0 && i % break_every == 0 {
|
||||
res.push(' ')
|
||||
}
|
||||
res.push(c);
|
||||
res
|
||||
})
|
||||
base64::encode(&buf)
|
||||
}
|
||||
|
||||
pub fn to_armored_string(
|
||||
|
||||
10
src/lib.rs
@@ -1,5 +1,6 @@
|
||||
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
|
||||
#![warn(
|
||||
// for now we hide warnings to not clutter/hide errors during "cargo clippy"
|
||||
#![allow(
|
||||
clippy::type_complexity,
|
||||
clippy::cognitive_complexity,
|
||||
clippy::too_many_arguments,
|
||||
@@ -75,8 +76,13 @@ mod dehtml;
|
||||
pub mod dc_array;
|
||||
pub mod dc_receive_imf;
|
||||
mod dc_simplify;
|
||||
mod dc_strencode;
|
||||
pub mod dc_tools;
|
||||
|
||||
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
|
||||
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
|
||||
|
||||
/// if set IMAP protocol commands and responses will be printed
|
||||
pub const DCC_IMAP_DEBUG: &str = "DCC_IMAP_DEBUG";
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::{get_version_str, Context};
|
||||
use crate::dc_strencode::*;
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee::*;
|
||||
use crate::error::Error;
|
||||
@@ -333,13 +332,19 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
Loaded::Message => {
|
||||
match self.chat {
|
||||
Some(ref chat) => {
|
||||
let raw_subject = message::get_summarytext_by_raw(
|
||||
let raw = message::get_summarytext_by_raw(
|
||||
self.msg.type_0,
|
||||
self.msg.text.as_ref(),
|
||||
&self.msg.param,
|
||||
32,
|
||||
self.context,
|
||||
);
|
||||
let mut lines = raw.lines();
|
||||
let raw_subject = if let Some(line) = lines.next() {
|
||||
line
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let afwd_email = self.msg.param.exists(Param::Forwarded);
|
||||
let fwd = if afwd_email { "Fwd: " } else { "" };
|
||||
@@ -377,7 +382,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let mut unprotected_headers: Vec<Header> = Vec::new();
|
||||
|
||||
let from = Address::new_mailbox_with_name(
|
||||
dc_encode_header_words(&self.from_displayname),
|
||||
encode_words(&self.from_displayname),
|
||||
self.from_addr.clone(),
|
||||
);
|
||||
|
||||
@@ -389,7 +394,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
} else {
|
||||
to.push(Address::new_mailbox_with_name(
|
||||
dc_encode_header_words(name),
|
||||
encode_words(name),
|
||||
addr.clone(),
|
||||
));
|
||||
}
|
||||
@@ -445,7 +450,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let e2ee_guranteed = self.is_e2ee_guranteed();
|
||||
let mut encrypt_helper = EncryptHelper::new(self.context)?;
|
||||
|
||||
let subject = dc_encode_header_words(subject_str);
|
||||
let subject = encode_words(&subject_str);
|
||||
|
||||
let mut message = match self.loaded {
|
||||
Loaded::Message => {
|
||||
@@ -520,6 +525,12 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
outer_message = outer_message.header(header);
|
||||
}
|
||||
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(self.context, "mimefactory: outgoing message mime:");
|
||||
let raw_message = message.clone().build().as_string();
|
||||
println!("{}", raw_message);
|
||||
}
|
||||
|
||||
let encrypted =
|
||||
encrypt_helper.encrypt(self.context, min_verified, message, &peerstates)?;
|
||||
|
||||
@@ -605,7 +616,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
|
||||
protected_headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
|
||||
|
||||
let encoded = dc_encode_header_words(&chat.name);
|
||||
let encoded = encode_words(&chat.name);
|
||||
protected_headers.push(Header::new("Chat-Group-Name".into(), encoded));
|
||||
|
||||
match command {
|
||||
@@ -975,20 +986,18 @@ fn build_body_file(
|
||||
}
|
||||
};
|
||||
|
||||
let needs_ext = dc_needs_ext_header(&filename_to_send);
|
||||
|
||||
// create mime part, for Content-Disposition, see RFC 2183.
|
||||
// `Content-Disposition: attachment` seems not to make a difference to `Content-Disposition: inline`
|
||||
// at least on tested Thunderbird and Gma'l in 2017.
|
||||
// But I've heard about problems with inline and outl'k, so we just use the attachment-type until we
|
||||
// run into other problems ...
|
||||
let cd_value = if needs_ext {
|
||||
format!("attachment; filename=\"{}\"", &filename_to_send)
|
||||
} else {
|
||||
let cd_value = if needs_encoding(&filename_to_send) {
|
||||
format!(
|
||||
"attachment; filename*=\"{}\"",
|
||||
dc_encode_header_words(&filename_to_send)
|
||||
encode_words(&filename_to_send)
|
||||
)
|
||||
} else {
|
||||
format!("attachment; filename=\"{}\"", &filename_to_send)
|
||||
};
|
||||
|
||||
let body = std::fs::read(blob.to_abs_path())?;
|
||||
@@ -1022,3 +1031,23 @@ fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/* ******************************************************************************
|
||||
* Encode/decode header words, RFC 2047
|
||||
******************************************************************************/
|
||||
|
||||
fn encode_words(word: &str) -> String {
|
||||
encoded_words::encode(word, None, encoded_words::EncodingFlag::Shortest, None)
|
||||
}
|
||||
|
||||
pub fn needs_encoding(to_check: impl AsRef<str>) -> bool {
|
||||
let to_check = to_check.as_ref();
|
||||
|
||||
if to_check.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
to_check.chars().any(|c| {
|
||||
!c.is_ascii_alphanumeric() && c != '-' && c != '_' && c != '.' && c != '~' && c != '%'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use lettre_email::mime::{self, Mime};
|
||||
use mailparse::MailHeaderMap;
|
||||
use mailparse::{DispositionType, MailHeaderMap};
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::blob::BlobObject;
|
||||
@@ -22,6 +22,7 @@ use crate::param::*;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::securejoin::handle_degrade_event;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::{bail, ensure};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MimeParser<'a> {
|
||||
@@ -29,7 +30,6 @@ pub struct MimeParser<'a> {
|
||||
pub parts: Vec<Part>,
|
||||
pub header: HashMap<String, String>,
|
||||
pub subject: Option<String>,
|
||||
pub is_send_by_messenger: bool,
|
||||
pub decrypting_failed: bool,
|
||||
pub encrypted: bool,
|
||||
pub signatures: HashSet<String>,
|
||||
@@ -74,7 +74,6 @@ impl<'a> MimeParser<'a> {
|
||||
parts: Vec::new(),
|
||||
header: Default::default(),
|
||||
subject: None,
|
||||
is_send_by_messenger: false,
|
||||
decrypting_failed: false,
|
||||
encrypted: false,
|
||||
signatures: Default::default(),
|
||||
@@ -109,6 +108,10 @@ impl<'a> MimeParser<'a> {
|
||||
if let Some(raw) = raw {
|
||||
mail_raw = raw;
|
||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(context, "decrypted message mime-body:");
|
||||
println!("{}", String::from_utf8_lossy(&mail_raw));
|
||||
}
|
||||
|
||||
// Handle any gossip headers if the mail was encrypted. See section
|
||||
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
|
||||
@@ -148,10 +151,6 @@ impl<'a> MimeParser<'a> {
|
||||
self.subject = Some(field.clone());
|
||||
}
|
||||
|
||||
if self.lookup_field("Chat-Version").is_some() {
|
||||
self.is_send_by_messenger = true
|
||||
}
|
||||
|
||||
if self.lookup_field("Autocrypt-Setup-Message").is_some() {
|
||||
let has_setup_file = self.parts.iter().any(|p| {
|
||||
p.mimetype.is_some() && p.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
|
||||
@@ -198,7 +197,7 @@ impl<'a> MimeParser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_send_by_messenger && self.parts.len() == 2 {
|
||||
if self.has_chat_version() && self.parts.len() == 2 {
|
||||
let need_drop = {
|
||||
let textpart = &self.parts[0];
|
||||
let filepart = &self.parts[1];
|
||||
@@ -232,7 +231,7 @@ impl<'a> MimeParser<'a> {
|
||||
let colon = subject.find(':');
|
||||
if colon == Some(2)
|
||||
|| colon == Some(3)
|
||||
|| self.is_send_by_messenger
|
||||
|| self.has_chat_version()
|
||||
|| subject.contains("Chat:")
|
||||
{
|
||||
prepend_subject = 0i32
|
||||
@@ -318,7 +317,7 @@ impl<'a> MimeParser<'a> {
|
||||
part.typ = Viewtype::Text;
|
||||
|
||||
if let Some(ref subject) = self.subject {
|
||||
if !self.is_send_by_messenger {
|
||||
if !self.has_chat_version() {
|
||||
part.msg = subject.to_string();
|
||||
}
|
||||
}
|
||||
@@ -337,6 +336,10 @@ impl<'a> MimeParser<'a> {
|
||||
self.parts.iter_mut().rev().find(|part| !part.is_meta)
|
||||
}
|
||||
|
||||
pub(crate) fn has_chat_version(&self) -> bool {
|
||||
self.header.contains_key("chat-version")
|
||||
}
|
||||
|
||||
pub fn lookup_field(&self, field_name: &str) -> Option<&String> {
|
||||
self.header.get(&field_name.to_lowercase())
|
||||
}
|
||||
@@ -487,41 +490,10 @@ impl<'a> MimeParser<'a> {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Add all parts (in fact,
|
||||
// AddSinglePartIfKnown() later check if the parts are really supported)
|
||||
// HACK: the following lines are a hack for clients who use
|
||||
// multipart/mixed instead of multipart/alternative for
|
||||
// combined text/html messages (eg. Stock Android "Mail" does so).
|
||||
// So, if we detect such a message below, we skip the Html
|
||||
// part. However, not sure, if there are useful situations to use
|
||||
// plain+html in multipart/mixed - if so, we should disable the hack.
|
||||
let mut skip_part = -1;
|
||||
let mut html_part = -1;
|
||||
let mut plain_cnt = 0;
|
||||
let mut html_cnt = 0;
|
||||
|
||||
for (i, cur_data) in mail.subparts.iter().enumerate() {
|
||||
match get_mime_type(cur_data)?.0.type_() {
|
||||
mime::TEXT => {
|
||||
plain_cnt += 1;
|
||||
}
|
||||
mime::HTML => {
|
||||
html_part = i as isize;
|
||||
html_cnt += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if plain_cnt == 1 && html_cnt == 1 {
|
||||
warn!(
|
||||
self.context,
|
||||
"HACK: multipart/mixed message found with Plain and HTML, we\'ll skip the HTML part as this seems to be unwanted."
|
||||
);
|
||||
skip_part = html_part;
|
||||
}
|
||||
|
||||
for (i, cur_data) in mail.subparts.iter().enumerate() {
|
||||
if i as isize != skip_part && self.parse_mime_recursive(cur_data)? {
|
||||
// Add all parts (in fact, AddSinglePartIfKnown() later check if
|
||||
// the parts are really supported)
|
||||
for cur_data in mail.subparts.iter() {
|
||||
if self.parse_mime_recursive(cur_data)? {
|
||||
any_part_added = true;
|
||||
}
|
||||
}
|
||||
@@ -536,84 +508,60 @@ impl<'a> MimeParser<'a> {
|
||||
let (mime_type, msg_type) = get_mime_type(mail)?;
|
||||
let raw_mime = mail.ctype.mimetype.to_lowercase();
|
||||
|
||||
let filename = get_attachment_filename(mail);
|
||||
info!(
|
||||
self.context,
|
||||
"add_single_part_if_known {:?} {:?} {:?}", mime_type, msg_type, filename
|
||||
);
|
||||
|
||||
let old_part_count = self.parts.len();
|
||||
|
||||
// regard `Content-Transfer-Encoding:`
|
||||
match mime_type.type_() {
|
||||
mime::TEXT | mime::HTML => {
|
||||
let decoded_data = match mail.get_body() {
|
||||
Ok(decoded_data) => decoded_data,
|
||||
Err(err) => {
|
||||
warn!(self.context, "Invalid body parsed {:?}", err);
|
||||
// Note that it's not always an error - might be no data
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
// check header directly as is_send_by_messenger is not yet set up
|
||||
let is_msgrmsg = self.lookup_field("Chat-Version").is_some();
|
||||
|
||||
let mut simplifier = Simplify::new();
|
||||
let simplified_txt = if decoded_data.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
let is_html = mime_type == mime::TEXT_HTML;
|
||||
simplifier.simplify(&decoded_data, is_html, is_msgrmsg)
|
||||
};
|
||||
|
||||
if !simplified_txt.is_empty() {
|
||||
let mut part = Part::default();
|
||||
part.typ = Viewtype::Text;
|
||||
part.mimetype = Some(mime_type);
|
||||
part.msg = simplified_txt;
|
||||
part.msg_raw = Some(decoded_data);
|
||||
self.do_add_single_part(part);
|
||||
if let Ok(filename) = filename {
|
||||
self.do_add_single_file_part(
|
||||
msg_type,
|
||||
mime_type,
|
||||
&raw_mime,
|
||||
&mail.get_body_raw()?,
|
||||
&filename,
|
||||
);
|
||||
} else {
|
||||
match mime_type.type_() {
|
||||
mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
|
||||
bail!("missing attachment");
|
||||
}
|
||||
mime::TEXT | mime::HTML => {
|
||||
let decoded_data = match mail.get_body() {
|
||||
Ok(decoded_data) => decoded_data,
|
||||
Err(err) => {
|
||||
warn!(self.context, "Invalid body parsed {:?}", err);
|
||||
// Note that it's not always an error - might be no data
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
if simplifier.is_forwarded {
|
||||
self.is_forwarded = true;
|
||||
}
|
||||
}
|
||||
mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
|
||||
// try to get file name from
|
||||
// `Content-Disposition: ... filename*=...`
|
||||
// or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...`
|
||||
// or `Content-Disposition: ... filename=...`
|
||||
|
||||
let ct = mail.get_content_disposition()?;
|
||||
let mut desired_filename = ct
|
||||
.params
|
||||
.iter()
|
||||
.filter(|(key, _value)| key.starts_with("filename"))
|
||||
.fold(String::new(), |mut acc, (_key, value)| {
|
||||
acc += value;
|
||||
acc
|
||||
});
|
||||
|
||||
if desired_filename.is_empty() {
|
||||
if let Some(param) = ct.params.get("name") {
|
||||
// might be a wrongly encoded filename
|
||||
desired_filename = param.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// if there is still no filename, guess one
|
||||
if desired_filename.is_empty() {
|
||||
if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
|
||||
desired_filename = format!("file.{}", subtype,);
|
||||
let mut simplifier = Simplify::new();
|
||||
let simplified_txt = if decoded_data.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
return Ok(false);
|
||||
let is_html = mime_type == mime::TEXT_HTML;
|
||||
simplifier.simplify(&decoded_data, is_html, self.has_chat_version())
|
||||
};
|
||||
|
||||
if !simplified_txt.is_empty() {
|
||||
let mut part = Part::default();
|
||||
part.typ = Viewtype::Text;
|
||||
part.mimetype = Some(mime_type);
|
||||
part.msg = simplified_txt;
|
||||
part.msg_raw = Some(decoded_data);
|
||||
self.do_add_single_part(part);
|
||||
}
|
||||
|
||||
if simplifier.is_forwarded {
|
||||
self.is_forwarded = true;
|
||||
}
|
||||
}
|
||||
self.do_add_single_file_part(
|
||||
msg_type,
|
||||
mime_type,
|
||||
&raw_mime,
|
||||
&mail.get_body_raw()?,
|
||||
&desired_filename,
|
||||
);
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// add object? (we do not add all objects, eg. signatures etc. are ignored)
|
||||
@@ -662,6 +610,7 @@ impl<'a> MimeParser<'a> {
|
||||
return;
|
||||
}
|
||||
};
|
||||
info!(self.context, "added blobfile: {:?}", blob.as_name());
|
||||
|
||||
/* create and register Mime part referencing the new Blob object */
|
||||
let mut part = Part::default();
|
||||
@@ -812,11 +761,11 @@ impl<'a> MimeParser<'a> {
|
||||
mdn_consumed = true;
|
||||
}
|
||||
|
||||
if self.is_send_by_messenger || mdn_consumed {
|
||||
if self.has_chat_version() || mdn_consumed {
|
||||
let mut param = Params::new();
|
||||
param.set(Param::ServerFolder, server_folder.as_ref());
|
||||
param.set_int(Param::ServerUid, server_uid as i32);
|
||||
if self.is_send_by_messenger && self.context.get_config_bool(Config::MvboxMove) {
|
||||
if self.has_chat_version() && self.context.get_config_bool(Config::MvboxMove) {
|
||||
param.set_int(Param::AlsoMove, 1);
|
||||
}
|
||||
job_add(self.context, Action::MarkseenMdnOnImap, 0, param, 0);
|
||||
@@ -913,12 +862,13 @@ pub struct Part {
|
||||
pub param: Params,
|
||||
}
|
||||
|
||||
/// return mimetype and viewtype for a parsed mail
|
||||
fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
|
||||
let viewtype = match mimetype.type_() {
|
||||
mime::TEXT => {
|
||||
if !mailmime_is_attachment_disposition(mail) {
|
||||
if !is_attachment_disposition(mail) {
|
||||
match mimetype.subtype() {
|
||||
mime::PLAIN | mime::HTML => Viewtype::Text,
|
||||
_ => Viewtype::File,
|
||||
@@ -952,14 +902,62 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
||||
Ok((mimetype, viewtype))
|
||||
}
|
||||
|
||||
fn mailmime_is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
|
||||
if let Some(ct) = mail.ctype.params.get("Content-Disposition") {
|
||||
return ct.to_lowercase().starts_with("attachment");
|
||||
fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
|
||||
if let Ok(ct) = mail.get_content_disposition() {
|
||||
return ct.disposition == DispositionType::Attachment
|
||||
&& ct
|
||||
.params
|
||||
.iter()
|
||||
.any(|(key, _value)| key.starts_with("filename"));
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<String> {
|
||||
// try to get file name from
|
||||
// `Content-Disposition: ... filename*=...`
|
||||
// or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...`
|
||||
// or `Content-Disposition: ... filename=...`
|
||||
|
||||
let ct = mail.get_content_disposition()?;
|
||||
ensure!(
|
||||
ct.disposition == DispositionType::Attachment,
|
||||
"disposition not an attachment: {:?}",
|
||||
ct.disposition
|
||||
);
|
||||
|
||||
let mut desired_filename = ct
|
||||
.params
|
||||
.iter()
|
||||
.filter(|(key, _value)| key.starts_with("filename"))
|
||||
.fold(String::new(), |mut acc, (_key, value)| {
|
||||
acc += value;
|
||||
acc
|
||||
});
|
||||
println!("get_attachment_filename1: {:?}", desired_filename);
|
||||
|
||||
if desired_filename.is_empty() {
|
||||
if let Some(param) = ct.params.get("name") {
|
||||
// might be a wrongly encoded filename
|
||||
desired_filename = param.to_string();
|
||||
}
|
||||
}
|
||||
println!("get_attachment_filename2: {:?}", desired_filename);
|
||||
|
||||
// if there is still no filename, guess one
|
||||
if desired_filename.is_empty() {
|
||||
if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
|
||||
desired_filename = format!("file.{}", subtype,);
|
||||
} else {
|
||||
bail!("could not determine filename: {:?}", ct.disposition);
|
||||
}
|
||||
}
|
||||
println!("get_attachment_filename3: {:?}", desired_filename);
|
||||
|
||||
Ok(desired_filename)
|
||||
}
|
||||
|
||||
// returned addresses are normalized.
|
||||
fn get_recipients<S: AsRef<str>, T: Iterator<Item = (S, S)>>(headers: T) -> HashSet<String> {
|
||||
let mut recipients: HashSet<String> = Default::default();
|
||||
@@ -1064,6 +1062,29 @@ mod tests {
|
||||
assert_eq!(recipients.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_attachment() {
|
||||
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
|
||||
let mail = mailparse::parse_mail(raw).unwrap();
|
||||
assert!(!is_attachment_disposition(&mail));
|
||||
|
||||
let raw = include_bytes!("../test-data/message/mail_attach_txt.eml");
|
||||
let mail = mailparse::parse_mail(raw).unwrap();
|
||||
assert!(!is_attachment_disposition(&mail));
|
||||
assert!(!is_attachment_disposition(&mail.subparts[0]));
|
||||
assert!(is_attachment_disposition(&mail.subparts[1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_attachment_filename() {
|
||||
let raw = include_bytes!("../test-data/message/html_attach.eml");
|
||||
let mail = mailparse::parse_mail(raw).unwrap();
|
||||
assert!(get_attachment_filename(&mail).is_err());
|
||||
assert!(get_attachment_filename(&mail.subparts[0]).is_err());
|
||||
let filename = get_attachment_filename(&mail.subparts[1]).unwrap();
|
||||
assert_eq!(filename, "test.html")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mailparse_content_type() {
|
||||
let ctype =
|
||||
|
||||
@@ -5,8 +5,6 @@ pub mod send;
|
||||
use lettre::smtp::client::net::*;
|
||||
use lettre::*;
|
||||
|
||||
use failure::Fail;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! # SQLite wrapper
|
||||
|
||||
use failure::Fail;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
25
test-data/message/html_attach.eml
Normal file
@@ -0,0 +1,25 @@
|
||||
Chat-Disposition-Notification-To: tmp_6272287793210918@testrun.org
|
||||
Subject: =?utf-8?q?Chat=3A_File_=E2=80=93_test=2Ehtml?=
|
||||
Message-ID: Mr.XA6y3og8-az.WGbH9_dNcQx@testrun.org
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||
X-Mailer: Delta Chat Core 1.0.0-beta.12/DcFFI
|
||||
Chat-Version: 1.0
|
||||
To: <tmp_5890965001269692@testrun.org>
|
||||
From: "=?utf-8?q??=" <tmp_6272287793210918@testrun.org>
|
||||
Content-Type: multipart/mixed; boundary="mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z"
|
||||
|
||||
|
||||
--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
--
|
||||
Sent with my Delta Chat Messenger: https://delta.chat
|
||||
|
||||
--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z
|
||||
Content-Type: text/html
|
||||
Content-Disposition: attachment; filename="test.html"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
PGh0bWw+PGJvZHk+dGV4dDwvYm9keT5kYXRh
|
||||
|
||||
--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z--
|
||||
40
test-data/message/mail_attach_txt.eml
Normal file
@@ -0,0 +1,40 @@
|
||||
From holger@merlinux.eu Sat Dec 7 11:53:58 2019
|
||||
Return-Path: <holger@merlinux.eu>
|
||||
X-Original-To: holger+test@merlinux.eu
|
||||
Delivered-To: holger+test@merlinux.eu
|
||||
Received: from [127.0.0.1] (localhost [127.0.0.1])
|
||||
(using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mail.merlinux.eu (Postfix) with ESMTPSA id 61825100531;
|
||||
Sat, 7 Dec 2019 10:53:58 +0000 (UTC)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=merlinux.eu;
|
||||
s=default; t=1575716038;
|
||||
bh=xhaPssQzVOHRafcciTQqDnZ1Zi4GMwsmg9pHLH3i8P8=;
|
||||
h=Date:From:To:Subject;
|
||||
b=U4HxGDZ8RwLwRPFtIvRsb+x5BiyICnbbY2ZOGlZdLt12MuDTfiYi/phHiQUC402EY
|
||||
GXb8dYgYr5+0PDiPBa7dyt2VQLC/h9QRfOA82tb1vpJYC+KksSAH0nYQqJvs7XrqCN
|
||||
i95/jwZnsWrV7w72+xsrO5qPujIE68TmM5I9Cyec=
|
||||
Received: by beto.merlinux.eu (Postfix, from userid 1000)
|
||||
id 229D3820070; Sat, 7 Dec 2019 11:53:58 +0100 (CET)
|
||||
Date: Sat, 7 Dec 2019 11:53:57 +0100
|
||||
From: holger krekel <holger@merlinux.eu>
|
||||
To: holger+test@merlinux.eu
|
||||
Subject: hello
|
||||
Message-ID: <20191207105357.GA6266@beto>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary="YiEDa0DAkWCtVeE4"
|
||||
Content-Disposition: inline
|
||||
|
||||
--YiEDa0DAkWCtVeE4
|
||||
Content-Type: text/plain; charset=us-ascii
|
||||
Content-Disposition: inline
|
||||
|
||||
siehe anhang
|
||||
|
||||
--YiEDa0DAkWCtVeE4
|
||||
Content-Type: text/plain; charset=us-ascii
|
||||
Content-Disposition: attachment; filename="x.txt"
|
||||
|
||||
hello
|
||||
|
||||
--YiEDa0DAkWCtVeE4--
|
||||