Compare commits

..

5 Commits

Author SHA1 Message Date
holger krekel
944a397ea8 use encoded-words crate, which friedel ported from python 2019-12-05 18:56:34 +01:00
holger krekel
225fdf560a try fix filename encoding bug -- fails in one test 2019-12-05 14:52:16 +01:00
dignifiedquire
454aa90ed6 switch to quoted-printable, which is already used by mailparse 2019-12-05 13:23:31 +01:00
holger krekel
9b2c04ad34 use rfc2047 crate from @valodim and remove dc_strencode.rs completely 2019-12-05 13:11:51 +01:00
holger krekel
7e907f3f54 fix multi-line subject encoding and introduce MIME debugging env var 2019-12-05 11:06:42 +01:00
80 changed files with 2354 additions and 4067 deletions

View File

@@ -36,6 +36,12 @@ jobs:
executor: default
steps:
- checkout
- run:
name: Update submodules
command: git submodule update --init --recursive
- run:
name: Calculate dependencies
command: cargo generate-lockfile
- restore_cache:
keys:
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
@@ -43,6 +49,7 @@ jobs:
- run: rustup default $(cat rust-toolchain)
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
- run: rustup component add --toolchain $(cat rust-toolchain) clippy-preview
- run: cargo update
- run: cargo fetch
- run: rustc +stable --version
- run: rustc +$(cat rust-toolchain) --version
@@ -84,6 +91,7 @@ jobs:
curl https://sh.rustup.rs -sSf | sh -s -- -y
- run: rustup install $(cat rust-toolchain)
- run: rustup default $(cat rust-toolchain)
- run: cargo update
- run: cargo fetch
- run:
name: Test
@@ -179,7 +187,7 @@ jobs:
- *restore-cache
- run:
name: Run cargo clippy
command: cargo clippy
command: cargo clippy --all
workflows:
@@ -187,7 +195,7 @@ workflows:
test:
jobs:
# - cargo_fetch
- cargo_fetch
- remote_tests_rust
@@ -197,12 +205,12 @@ workflows:
# requires:
# - build_test_docs_wheel
# - build_doxygen
# - rustfmt:
# requires:
# - cargo_fetch
# - clippy:
# requires:
# - cargo_fetch
- rustfmt:
requires:
- cargo_fetch
- clippy:
requires:
- cargo_fetch
- build_doxygen

View File

@@ -1,47 +0,0 @@
on: push
name: Code Quality
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2019-11-06
override: true
- uses: actions-rs/cargo@v1
with:
command: check
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2019-11-06
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2019-11-06
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features

View File

@@ -1,131 +1,6 @@
# Changelog
## 1.0.0-beta.20
- #1074 fix OAUTH2/gmail
- #1072 fix group members not appearing in contact list
- #1071 never block interrupt_idle (thus hopefully also not on maybe_network())
- #1069 reduce smtp-timeout to 30 seconds
- #1066 #1065 avoid unwrap in dehtml, make literals more readable
## 1.0.0-beta.19
- #1058 timeout smtp-send if it doesn't complete in 15 minutes
- #1059 trim down logging
## 1.0.0-beta.18
- #1056 avoid panicking when we couldn't read imap-server's greeting
message
- #1055 avoid panicking when we don't have a selected folder
- #1052 #1049 #1051 improve logging to add thread-id/name and
file/lineno to each info/warn message.
- #1050 allow python bindings to initialize Account with "os_name".
## 1.0.0-beta.17
- #1044 implement avatar recoding to 192x192 in core to keep file sizes small.
- #1024 fix #1021 SQL/injection malformed Chat-Group-Name breakage
- #1036 fix smtp crash by pulling in a fixed async-smtp
- #1039 fix read-receipts appearing as normal messages when you change
MDN settings
- #1040 do not panic on SystemTimeDifference
- #1043 avoid potential crashes in malformed From/Chat-Disposition... headers
- #1045 #1041 #1038 #1035 #1034 #1029 #1025 various cleanups and doc
improvments
## 1.0.0-beta.16
- alleviate login problems with providers which only
support RSA1024 keys by switching back from Rustls
to native-tls, by using the new async-email/async-native-tls
crate from @dignifiedquire. thanks @link2xt.
- introduce per-contact profile images to send out
own profile image heuristically, and fix sending
out of profile images in "in-prepare" groups.
this also extends the Chat-spec that is maintained
in core to specify Chat-Group-Image and Chat-Group-Avatar
headers. thanks @r10s and @hpk42.
- fix merging of protected headers from the encrypted
to the unencrypted parts, now not happening recursively
anymore. thanks @hpk and @r10s
- fix/optimize autocrypt gossip headers to only get
sent when there are more than 2 people in a chat.
thanks @link2xt
- fix displayname to use the authenticated name
when available (displayname as coming from contacts
themselves). thanks @simon-laux
- introduce preliminary support for offline autoconfig
for nauta provider. thanks @hpk42 @r10s
## 1.0.0-beta.15
- fix #994 attachment appeared doubled in chats (and where actually
downloaded after smtp-send). @hpk42
## 1.0.0-beta.14
- fix packaging issue with our rust-email fork, now we are tracking
master again there. hpk42
## 1.0.0-beta.13
- fix #976 -- unicode-issues in display-name of email addresses. @hpk42
- fix #985 group add/remove member bugs resulting in broken groups. @hpk42
- fix hanging IMAP connections -- we now detect with a 15second timeout
if we cannot terminate the IDLE IMAP protocol. @hpk42 @link2xt
- fix incoming multipart/mixed containing html, to show up as
attachments again. Fixes usage for simplebot which sends html
files for users to interact with the bot. @adbenitez @hpk42
- refinements to internal autocrypt-handling code, do not send
prefer-encrypt=nopreference as it is the default if no attribute
is present. @linkxt
- simplify, modularize and rustify several parts
of dc-core (general WIP). @link2xt @flub @hpk42 @r10s
- use async-email/async-smtp to handle SMTP connections, might
fix connection/reconnection issues. @link2xt
- more tests and refinements for dealing with blobstorage @flub @hpk42
- use a dedicated build-server for CI testing of core PRs
## 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
## 1.0.0-beta.10 (pending)
- fix grpid-determination from in-reply-to and references headers. @hpk42
@@ -135,11 +10,6 @@
- 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

857
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.0.0-beta.20"
version = "1.0.0-beta.9"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL"
@@ -9,22 +9,18 @@ license = "MPL"
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.4.0", default-features = false }
pgp = { git = "https://github.com/rpgp/rpgp", branch = "master", default-features = false }
hex = "0.4.0"
sha2 = "0.8.0"
rand = "0.7.0"
smallvec = "1.0.0"
reqwest = { version = "0.9.15" }
num-derive = "0.3.0"
rand = "0.6.5"
smallvec = "0.6.9"
reqwest = { version = "0.9.15", default-features = false, features = ["rustls-tls"] }
num-derive = "0.2.5"
num-traits = "0.2.6"
async-smtp = { git = "https://github.com/async-email/async-smtp" }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "native_tls" }
# XXX newer commits of async-imap lead to import-export tests hanging
async-imap = { git = "https://github.com/async-email/async-imap", branch = "dcc-stable" }
async-native-tls = "0.1.1"
lettre = { git = "https://github.com/deltachat/lettre", branch = "feat/mail" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "feat/mail" }
async-imap = { git = "https://github.com/async-email/async-imap", branch="master" }
async-tls = "0.6"
async-std = { version = "1.0", features = ["unstable"] }
base64 = "0.11"
charset = "0.1"
@@ -34,7 +30,6 @@ serde_json = "1.0"
chrono = "0.4.6"
failure = "0.1.5"
failure_derive = "0.1.5"
indexmap = "1.3.0"
# TODO: make optional
rustyline = "4.1.0"
lazy_static = "1.4.0"
@@ -49,16 +44,17 @@ backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.8.0"
image-meta = "0.1.0"
quick-xml = "0.17.1"
quick-xml = "0.15.0"
escaper = "0.1.0"
bitflags = "1.1.0"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
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" }
native-tls = "0.2.3"
image = "0.22.3"
[dev-dependencies]
tempfile = "3.0"
@@ -83,6 +79,6 @@ path = "examples/repl/main.rs"
[features]
default = ["nightly", "ringbuf"]
vendored = ["native-tls/vendored", "reqwest/default-tls-vendored"]
vendored = []
nightly = ["pgp/nightly"]
ringbuf = ["pgp/ringbuf"]

View File

@@ -8,6 +8,7 @@ install:
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -vV
- cargo -vV
- cargo update
build: false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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:export-ydpi="409.60001"
inkscape:export-xdpi="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"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="icon-device.svg"
inkscape:version="1.0beta1 (32d4812, 2019-09-19)">
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-xdpi="409.60001"
inkscape:export-ydpi="409.60001">
<defs
id="defs4348" />
<sodipodi:namedview
inkscape:snap-global="false"
pagecolor="#ffffff"
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:current-layer="svg4344"
inkscape:window-maximized="0"
inkscape:current-layer="svg4344" />
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"
pagecolor="#ffffff"
inkscape:snap-global="false" />
<metadata
id="metadata4336">
Created by potrace 1.15, written by Peter Selinger 2001-2017
@@ -56,28 +56,35 @@ Created by potrace 1.15, written by Peter Selinger 2001-2017
</cc:Work>
</rdf:RDF>
</metadata>
<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" />
<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" />
<g
fill="#000000"
stroke="none"
style="fill:#ffffff;fill-opacity:1"
id="g4342"
transform="matrix(0.00255113,0,0,-0.00255113,5.586152,38.200477)"
id="g4342">
style="fill:#ffffff;fill-opacity:1"
stroke="none"
fill="#000000">
<path
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"
inkscape:connector-curvature="0"
id="path4338"
inkscape:connector-curvature="0" />
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" />
<path
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"
inkscape:connector-curvature="0"
id="path4340"
inkscape:connector-curvature="0" />
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" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -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:export-ydpi="409.60001"
inkscape:export-xdpi="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"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="icon-saved-messages.svg"
inkscape:version="1.0beta1 (32d4812, 2019-09-19)">
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-xdpi="409.60001"
inkscape:export-ydpi="409.60001">
<defs
id="defs4348" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
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:lockguides="false"
inkscape:current-layer="svg4344"
inkscape:lockguides="false" />
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"
bordercolor="#666666"
pagecolor="#ffffff" />
<metadata
id="metadata4336">
Created by potrace 1.15, written by Peter Selinger 2001-2017
@@ -56,16 +56,21 @@ Created by potrace 1.15, written by Peter Selinger 2001-2017
</cc:Work>
</rdf:RDF>
</metadata>
<rect
y="0"
x="0"
height="45"
width="45"
id="rect1420"
style="fill:#87aade;fill-opacity:1;stroke:none;stroke-width:0.968078" />
<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" />
<path
id="rect846"
style="fill:#ffffff;stroke-width:0.58409804"
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"
inkscape:connector-curvature="0" />
style="fill:#ffffff;stroke-width:0.58409804"
id="rect846" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -2,7 +2,7 @@
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
export SSHTARGET=ci@b1.delta.chat
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")

View File

@@ -2,7 +2,7 @@
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
export SSHTARGET=ci@b1.delta.chat
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.0.0-beta.20"
version = "1.0.0-beta.9"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -21,7 +21,6 @@ libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"
failure = "0.1.6"
serde_json = "1.0"
[features]
default = ["vendored", "nightly", "ringbuf"]

View File

@@ -994,20 +994,11 @@ 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, NULL);
* dc_msg_set_file(msg, "/file/to/send.mp4", NULL);
* dc_prepare_msg(context, chat_id, msg);
*
* // ... create the file ...
*
* // ... after /file/to/send.mp4 is ready:
* 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
@@ -1033,11 +1024,8 @@ 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
@@ -1146,9 +1134,6 @@ 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);
@@ -1962,7 +1947,7 @@ void dc_imex (dc_context_t* context, int what, c
* }
* while (!configure_succeeded())
* }
* dc_str_unref(file);
* free(file);
* }
* ~~~
*
@@ -2617,25 +2602,6 @@ 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
*
@@ -3645,15 +3611,11 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Check if a contact was verified. E.g. by a secure-join QR code scan
* and if the key has not changed since this verification.
* Same as dc_contact_is_verified() but allows speeding up things
* by adding the peerstate belonging to the contact.
* If you do not have the peerstate available, it is loaded automatically.
*
* The UI may draw a checkbox or something like that beside verified contacts.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0: contact is not verified.
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
* @private @memberof dc_context_t
*/
int dc_contact_is_verified (dc_contact_t* contact);
@@ -4054,6 +4016,11 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_CERTCK_STRICT 1
/**
* Accept invalid hostnames, but not invalid certificates.
*/
#define DC_CERTCK_ACCEPT_INVALID_HOSTNAMES 2
/**
* Accept invalid certificates, including self-signed ones
* or having incorrect hostname.

View File

@@ -10,7 +10,6 @@
#[macro_use]
extern crate human_panic;
extern crate num_traits;
extern crate serde_json;
use std::collections::HashMap;
use std::convert::TryInto;
@@ -30,8 +29,6 @@ use deltachat::message::MsgId;
use deltachat::stock::StockMessage;
use deltachat::*;
mod dc_array;
mod string;
use self::string::*;
@@ -510,7 +507,7 @@ pub unsafe extern "C" fn dc_interrupt_imap_idle(context: *mut dc_context_t) {
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_inbox_idle(ctx))
.with_inner(|ctx| job::interrupt_inbox_idle(ctx, true))
.unwrap_or(())
}
@@ -2380,42 +2377,6 @@ pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> l
ffi_chat.chat.is_sending_locations() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_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| {
let chat = match chat::Chat::load_from_db(ctx, chat_id) {
Ok(chat) => chat,
Err(err) => {
error!(ctx, "dc_get_chat_info_json() failed to load chat: {}", err);
return "".strdup();
}
};
let info = match chat.get_info(ctx) {
Ok(info) => info,
Err(err) => {
error!(
ctx,
"dc_get_chat_info_json() failed to get chat info: {}", err
);
return "".strdup();
}
};
serde_json::to_string(&info)
.unwrap_or_log_default(ctx, "dc_get_chat_info_json() failed to serialise to json")
.strdup()
})
.unwrap_or_else(|_| "".strdup())
}
// dc_msg_t
/// FFI struct for [dc_msg_t]

View File

@@ -94,9 +94,7 @@ pub fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> {
let data = dc_read_file(context, filename)?;
if let Err(err) = dc_receive_imf(context, &data, "import", 0, 0) {
println!("dc_receive_imf errored: {:?}", err);
}
dc_receive_imf(context, &data, "import", 0, 0);
Ok(())
}
@@ -491,7 +489,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
println!("{:#?}", context.get_info());
}
"interrupt" => {
interrupt_inbox_idle(context);
interrupt_inbox_idle(context, true);
}
"maybenetwork" => {
maybe_network(context);
@@ -938,14 +936,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let contact = Contact::get_by_id(context, contact_id)?;
let name_n_addr = contact.get_name_n_addr();
let mut res = format!(
"Contact info for: {}:\nIcon: {}\n",
name_n_addr,
match contact.get_profile_image(context) {
Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(),
}
);
let mut res = format!("Contact info for: {}:\n\n", name_n_addr);
res += &Contact::get_encrinfo(context, contact_id)?;

View File

@@ -202,7 +202,7 @@ fn stop_threads(context: &Context) {
println!("Stopping threads");
IS_RUNNING.store(false, Ordering::Relaxed);
interrupt_inbox_idle(context);
interrupt_inbox_idle(context, true);
interrupt_mvbox_idle(context);
interrupt_sentbox_idle(context);
interrupt_smtp_idle(context);

View File

@@ -104,7 +104,7 @@ fn main() {
println!("stopping threads");
*running.write().unwrap() = false;
deltachat::job::interrupt_inbox_idle(&ctx);
deltachat::job::interrupt_inbox_idle(&ctx, true);
deltachat::job::interrupt_smtp_idle(&ctx);
println!("joining");

View File

@@ -3,7 +3,6 @@
from __future__ import print_function
import atexit
import threading
import os
import re
import time
from array import array
@@ -26,7 +25,7 @@ class Account(object):
by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects.
"""
def __init__(self, db_path, logid=None, eventlogging=True, os_name=None, debug=True):
def __init__(self, db_path, logid=None, eventlogging=True, debug=True):
""" initialize account object.
:param db_path: a path to the account database. The database
@@ -34,11 +33,10 @@ class Account(object):
:param logid: an optional logging prefix that should be used with
the default internal logging.
:param eventlogging: if False no eventlogging and no context callback will be configured
:param os_name: this will be put to the X-Mailer header in outgoing messages
:param debug: turn on debug logging for events.
"""
self._dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
_destroy_dc_context,
)
if eventlogging:
@@ -96,12 +94,9 @@ class Account(object):
"""
self._check_config_key(name)
name = name.encode("utf8")
value = value.encode("utf8")
if name == b"addr" and self.is_configured():
raise ValueError("can not change 'addr' after account is configured.")
if value is not None:
value = value.encode("utf8")
else:
value = ffi.NULL
lib.dc_set_config(self._dc_context, name, value)
def get_config(self, name):
@@ -137,18 +132,6 @@ class Account(object):
"""
return lib.dc_is_configured(self._dc_context)
def set_avatar(self, img_path):
"""Set self avatar.
:raises ValueError: if profile image could not be set
:returns: None
"""
if img_path is None:
self.set_config("selfavatar", None)
else:
assert os.path.exists(img_path), img_path
self.set_config("selfavatar", img_path)
def check_is_configured(self):
""" Raise ValueError if this account is not configured. """
if not self.is_configured():

View File

@@ -2,7 +2,6 @@
import mimetypes
import calendar
import json
from datetime import datetime
import os
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
@@ -109,30 +108,6 @@ class Chat(object):
# ------ chat messaging API ------------------------------
def send_msg(self, msg):
"""send a message by using a ready Message object.
:param msg: a :class:`deltachat.message.Message` instance
previously returned by
e.g. :meth:`deltachat.message.Message.new_empty` or
:meth:`prepare_file`.
:raises ValueError: if message can not be sent.
:returns: a :class:`deltachat.message.Message` instance as
sent out. This is the same object as was passed in, which
has been modified with the new state of the core.
"""
if msg.is_out_preparing():
assert msg.id != 0
# get a fresh copy of dc_msg, the core needs it
msg = Message.from_db(self.account, msg.id)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
# modify message in place to avoid bad state for the caller
msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg
return msg
def send_text(self, text):
""" send a text message and return the resulting Message instance.
@@ -154,12 +129,9 @@ class Chat(object):
:raises ValueError: if message can not be send/chat does not exist.
:returns: the resulting :class:`deltachat.message.Message` instance
"""
msg = Message.new_empty(self.account, view_type="file")
msg.set_file(path, mime_type)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id)
msg = self.prepare_message_file(path=path, mime_type=mime_type)
self.send_prepared(msg)
return msg
def send_image(self, path):
""" send an image message and return the resulting Message instance.
@@ -169,12 +141,9 @@ class Chat(object):
:returns: the resulting :class:`deltachat.message.Message` instance
"""
mime_type = mimetypes.guess_type(path)[0]
msg = Message.new_empty(self.account, view_type="image")
msg.set_file(path, mime_type)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id)
msg = self.prepare_message_file(path=path, mime_type=mime_type, view_type="image")
self.send_prepared(msg)
return msg
def prepare_message(self, msg):
""" create a new prepared message.
@@ -273,12 +242,6 @@ 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):
@@ -361,18 +324,6 @@ 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):
@@ -381,12 +332,6 @@ 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.

View File

@@ -68,6 +68,7 @@ DC_LP_SMTP_SOCKET_SSL = 0x20000
DC_LP_SMTP_SOCKET_PLAIN = 0x40000
DC_CERTCK_AUTO = 0
DC_CERTCK_STRICT = 1
DC_CERTCK_ACCEPT_INVALID_HOSTNAMES = 2
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3
DC_EMPTY_MVBOX = 0x01
DC_EMPTY_INBOX = 0x02

View File

@@ -47,13 +47,3 @@ class Contact(object):
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)
def get_profile_image(self):
"""Get contact profile image.
:returns: path to profile image, None if no profile image exists.
"""
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)

View File

@@ -1,6 +1,7 @@
""" The Message object. """
import os
import shutil
from . import props
from .cutil import from_dc_charpointer, as_dc_charpointer
from .capi import lib, ffi
@@ -69,6 +70,19 @@ 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
@@ -174,7 +188,7 @@ class Message(object):
@property
def _msgstate(self):
if self.id == 0:
dc_msg = self._dc_msg
dc_msg = self.message._dc_msg
else:
# load message from db to get a fresh/current state
dc_msg = ffi.gc(

View File

@@ -85,21 +85,16 @@ class SessionLiveConfigFromFile:
class SessionLiveConfigFromURL:
def __init__(self, url, create_token):
self.configlist = []
self.url = url
self.create_token = create_token
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url, json={"token_create_user": int(self.create_token)})
for i in range(2):
res = requests.post(url, json={"token_create_user": int(create_token)})
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
return config
def get(self, index):
return self.configlist[index]
def exists(self):
return bool(self.configlist)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -16,14 +16,6 @@ class TestOfflineAccountBasic:
with pytest.raises(ValueError):
Account(p.strpath)
def test_os_name(self, tmpdir):
p = tmpdir.join("hello.db")
# we can't easily test if os_name is used in X-Mailer
# outgoing messages without a full Online test
# but we at least check Account accepts the arg
ac1 = Account(p.strpath, os_name="solarpunk")
ac1.get_info()
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
d = ac1.get_info()
@@ -163,18 +155,6 @@ 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()
@@ -390,23 +370,6 @@ class TestOfflineChat:
assert not res.is_ask_verifygroup()
assert res.contact_id == 10
def test_group_chat_many_members_add_remove(self, ac1, lp):
lp.sec("ac1: creating group chat with 10 other members")
chat = ac1.create_group_chat(name="title1")
contacts = []
for i in range(10):
contact = ac1.create_contact("some{}@example.org".format(i))
contacts.append(contact)
chat.add_contact(contact)
num_contacts = len(chat.get_contacts())
assert num_contacts == 11
lp.sec("ac1: removing two contacts and checking things are right")
chat.remove_contact(contacts[9])
chat.remove_contact(contacts[3])
assert len(chat.get_contacts()) == 9
class TestOnlineAccount:
def get_chat(self, ac1, ac2, both_created=False):
@@ -467,60 +430,30 @@ 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):
def test_prepare_file_with_unicode(self, 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)
chat.send_msg(msg1)
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")
blobdir = ac1.get_blobdir()
basename = "somedäüta.txt"
p = os.path.join(blobdir, basename)
with open(p, "w") as f:
f.write("some data")
msg = Message.new_empty(ac1, "file")
msg.set_text("hello ä world")
msg.set_file(p)
message = chat.prepare_message(msg)
assert message.is_out_preparing()
assert message.text == "hello ä world"
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
msg = ac2.get_message_by_id(ev[2])
assert open(msg.filename).read() == content
msg = ac2.get_message_by_id(ev[1])
assert msg.text == "hello ä world"
assert open(msg.filename).read() == "some data"
assert msg.filename.endswith(basename)
def test_mvbox_sentbox_threads(self, acfactory, lp):
@@ -640,9 +573,6 @@ class TestOnlineAccount:
def test_send_and_receive_message_markseen(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
# make DC's life harder wrt to encodings
ac1.set_config("displayname", "ä name")
lp.sec("ac1: create chat with ac2")
chat = self.get_chat(ac1, ac2)
@@ -660,7 +590,6 @@ class TestOnlineAccount:
msg_in = ac2.get_message_by_id(msg_out.id)
assert msg_in.text == "message1"
assert not msg_in.is_forwarded()
assert msg_in.get_sender_contact().display_name == ac1.get_config("displayname")
lp.sec("check the message arrived in contact-requets/deaddrop")
chat2 = msg_in.chat
@@ -695,39 +624,6 @@ class TestOnlineAccount:
except queue.Empty:
pass # mark_seen_messages() has generated events before it returns
def test_mdn_asymetric(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")
chat = self.get_chat(ac1, ac2, both_created=True)
# make sure mdns are enabled (usually enabled by default already)
ac1.set_config("mdns_enabled", "1")
ac2.set_config("mdns_enabled", "1")
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
assert len(chat.get_messages()) == 1
lp.sec("disable ac1 MDNs")
ac1.set_config("mdns_enabled", "0")
lp.sec("wait for ac2 to receive message")
msg = ac2.wait_next_incoming_message()
assert len(msg.chat.get_messages()) == 1
lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg])
lp.sec("ac1: waiting for incoming activity")
# wait for MOVED event because even ignored read-receipts should be moved
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
assert len(chat.get_messages()) == 1
assert not msg_out.is_out_mdn_received()
def test_send_and_receive_will_encrypt_decrypt(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -756,12 +652,6 @@ class TestOnlineAccount:
assert msg_back.text == "message-back"
assert msg_back.is_encrypted()
# Test that we do not gossip peer keys in 1-to-1 chat,
# as it makes no sense to gossip to peers their own keys.
# Gossip is only sent in encrypted messages,
# and we sent encrypted msg_back right above.
assert chat2b.get_summary()["gossiped_timestamp"] == 0
lp.sec("create group chat with two members, one of which has no encrypt state")
chat = ac1.create_group_chat("encryption test")
chat.add_contact(ac1.create_contact(ac2.get_config("addr")))
@@ -1023,54 +913,7 @@ class TestOnlineAccount:
assert msg.text == "world"
assert msg.is_encrypted()
def test_set_get_contact_avatar(self, acfactory, data, lp):
lp.sec("configuring ac1 and ac2")
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: set own profile image")
p = data.get_path("d.png")
ac1.set_avatar(p)
lp.sec("ac1: create 1:1 chat with ac2")
chat = self.get_chat(ac1, ac2, both_created=True)
msg = chat.send_text("hi -- do you see my brand new avatar?")
assert not msg.is_encrypted()
lp.sec("ac2: wait for receiving message and avatar from ac1")
msg1 = ac2.wait_next_incoming_message()
assert not msg1.chat.is_deaddrop()
received_path = msg1.get_sender_contact().get_profile_image()
assert open(received_path, "rb").read() == open(p, "rb").read()
lp.sec("ac2: set own profile image")
p = data.get_path("d.png")
ac2.set_avatar(p)
lp.sec("ac2: send back message")
m = msg1.chat.send_text("yes, i received your avatar -- how do you like mine?")
assert m.is_encrypted()
lp.sec("ac1: wait for receiving message and avatar from ac2")
msg2 = ac1.wait_next_incoming_message()
received_path = msg2.get_sender_contact().get_profile_image()
assert received_path is not None, "did not get avatar through encrypted message"
assert open(received_path, "rb").read() == open(p, "rb").read()
ac2._evlogger.consume_events()
ac1._evlogger.consume_events()
# XXX not sure if the following is correct / possible. you may remove it
lp.sec("ac1: delete profile image from chat, and send message to ac2")
ac1.set_avatar(None)
m = msg2.chat.send_text("i don't like my avatar anymore and removed it")
assert m.is_encrypted()
lp.sec("ac2: wait for message along with avatar deletion of ac1")
msg3 = ac2.wait_next_incoming_message()
assert msg3.get_sender_contact().get_profile_image() is None
def test_set_get_group_image(self, acfactory, data, lp):
def test_set_get_profile_image(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("create unpromoted group chat")
@@ -1172,61 +1015,6 @@ class TestOnlineAccount:
assert not locations3
class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):
lp.sec("creating and configuring five accounts")
ac1 = acfactory.get_online_configuring_account()
accounts = [acfactory.get_online_configuring_account() for i in range(3)]
wait_configuration_progress(ac1, 1000)
for acc in accounts:
wait_configuration_progress(acc, 1000)
lp.sec("ac1: creating group chat with 3 other members")
chat = ac1.create_group_chat("title1")
contacts = []
chars = list("äöüsr")
for acc in accounts:
contact = ac1.create_contact(acc.get_config("addr"), name=chars.pop())
contacts.append(contact)
chat.add_contact(contact)
# make sure the other side accepts our messages
c1 = acc.create_contact(ac1.get_config("addr"), "ä member")
acc.create_chat_by_contact(c1)
assert not chat.is_promoted()
lp.sec("ac1: send mesage to new group chat")
chat.send_text("hello")
assert chat.is_promoted()
num_contacts = len(chat.get_contacts())
assert num_contacts == 3 + 1
lp.sec("ac2: checking that the chat arrived correctly")
ac2 = accounts[0]
msg = ac2.wait_next_incoming_message()
assert msg.text == "hello"
print("chat is", msg.chat)
assert len(msg.chat.get_contacts()) == 4
lp.sec("ac3: checking that 'ac4' is a known contact")
ac3 = accounts[1]
msg3 = ac3.wait_next_incoming_message()
assert msg3.text == "hello"
contacts = ac3.get_contacts()
assert len(contacts) == 3
ac4_contacts = ac3.get_contacts(query=accounts[2].get_config("addr"))
assert len(ac4_contacts) == 1
lp.sec("ac1: removing one contacts and checking things are right")
to_remove = msg.chat.get_contacts()[-1]
msg.chat.remove_contact(to_remove)
sysmsg = ac1.wait_next_incoming_message()
assert to_remove.addr in sysmsg.text
assert len(sysmsg.chat.get_contacts()) == 3
class TestOnlineConfigureFails:
def test_invalid_password(self, acfactory):
ac1, configdict = acfactory.get_online_config()

View File

@@ -1,49 +1,10 @@
from __future__ import print_function
import os.path
import shutil
import pytest
from filecmp import cmp
from conftest import wait_configuration_progress, wait_msgs_changed
from deltachat import const
from conftest import wait_configuration_progress, wait_msgs_changed
class TestOnlineInCreation:
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
lp.sec("Creating in-creation file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join('file.txt').ensure(file=1)
with pytest.raises(Exception):
chat.prepare_message_file(src.strpath)
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
lp.sec("Creating file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join('file.txt')
src.write("hello there\n")
chat.send_file(src.strpath)
blob_src = os.path.join(ac1.get_blobdir(), 'file.txt')
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
@@ -56,10 +17,7 @@ class TestOnlineInCreation:
wait_msgs_changed(ac1, 0, 0) # why no chat id?
lp.sec("create a message with a file in creation")
orig = data.get_path("d.png")
path = os.path.join(ac1.get_blobdir(), 'd.png')
with open(path, "x") as fp:
fp.write("preparing")
path = data.get_path("d.png")
prepared_original = chat.prepare_message_file(path)
assert prepared_original.is_out_preparing()
wait_msgs_changed(ac1, chat.id, prepared_original.id)
@@ -80,7 +38,6 @@ class TestOnlineInCreation:
lp.sec("finish creating the file and send it")
assert prepared_original.is_out_preparing()
shutil.copyfile(orig, path)
chat.send_prepared(prepared_original)
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
wait_msgs_changed(ac1, chat.id, prepared_original.id)
@@ -102,11 +59,11 @@ class TestOnlineInCreation:
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
received_original = ac2.get_message_by_id(ev1[2])
assert cmp(received_original.filename, orig, shallow=False)
assert cmp(received_original.filename, path, False)
lp.sec("wait2 for original or forwarded messages to arrive")
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev2[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev2[1] != ev1[1]
received_copy = ac2.get_message_by_id(ev2[2])
assert cmp(received_copy.filename, orig, shallow=False)
assert cmp(received_copy.filename, path, False)

View File

@@ -7,7 +7,11 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
# (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}
# python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
@@ -32,7 +36,7 @@ commands =
[testenv:lint]
skipsdist = True
skip_install = True
usedevelop = True
deps =
flake8
# pygments required by rst-lint
@@ -65,8 +69,8 @@ commands =
[pytest]
addopts = -v -ra
python_files = tests/test_*.py
addopts = -v -rs
python_files = tests/test_*.py
norecursedirs = .tox
xfail_strict=true
timeout = 60

View File

@@ -55,9 +55,7 @@ if __name__ == "__main__":
replace_toml_version("Cargo.toml", newversion)
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
subprocess.call(["cargo", "check"])
subprocess.call(["git", "add", "-u"])
# subprocess.call(["cargo", "update", "-p", "deltachat"])
subprocess.call(["cargo", "update", "-p", "deltachat"])
print("after commit make sure to: ")
print("")

26
spec.md
View File

@@ -1,6 +1,6 @@
# Chat-over-Email specification
Version 0.20.0
Version 0.19.0
This document describes how emails can be used
to implement typical messenger functions
@@ -248,11 +248,11 @@ and the message SHOULD appear as a message or action from the sender.
A group MAY have a group-image.
To change or set the group-image,
the messenger MUST attach an image file to a message
and MUST add the header `Chat-Group-Avatar`
and MUST add the header `Chat-Group-Image`
with the value set to the image name.
To remove the group-image,
the messenger MUST add the header `Chat-Group-Avatar: 0`.
the messenger MUST add the header `Chat-Group-Image: 0`.
The messenger SHOULD send an explicit mail for each group image change.
The body of the message SHOULD contain
@@ -265,7 +265,7 @@ and the message SHOULD appear as a message or action from the sender.
Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ
Chat-Group-Name: Our Group
Chat-Group-Avatar: image.jpg
Chat-Group-Image: image.jpg
Message-ID: Gr.12345uvwxyZ.0005@domain
Subject: Chat: Our Group: Hello, ...
Content-Type: multipart/mixed; boundary="==break=="
@@ -283,25 +283,25 @@ and the message SHOULD appear as a message or action from the sender.
The image format SHOULD be image/jpeg or image/png.
To save data, it is RECOMMENDED
to add a `Chat-Group-Avatar` only on image changes.
to add a `Chat-Group-Image` only on image changes.
# Set profile image
A user MAY have a profile-image that MAY be spread to their contacts.
A user MAY have a profile-image that MAY be spread to his contacts.
To change or set the profile-image,
the messenger MUST attach an image file to a message
and MUST add the header `Chat-User-Avatar`
and MUST add the header `Chat-Profile-Image`
with the value set to the image name.
To remove the profile-image,
the messenger MUST add the header `Chat-User-Avatar: 0`.
the messenger MUST add the header `Chat-Profile-Image: 0`.
To spread the image,
the messenger MAY send the profile image
together with the next mail to a given contact
(to do this only once,
the messenger has to keep a `user_avatar_update_state` somewhere).
the messenger has to keep a `profile_image_update_state` somewhere).
Alternatively, the messenger MAY send an explicit mail
for each profile-image change to all contacts using a compatible messenger.
The messenger SHOULD NOT send an explicit mail to normal MUAs.
@@ -309,7 +309,7 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Chat-User-Avatar: photo.jpg
Chat-Profile-Image: photo.jpg
Subject: Chat: Hello, ...
Content-Type: multipart/mixed; boundary="==break=="
@@ -325,10 +325,10 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
--==break==--
The image format SHOULD be image/jpeg or image/png.
Note that `Chat-User-Avatar` may appear together with all other headers,
eg. there may be a `Chat-User-Avatar` and a `Chat-Group-Avatar` header
Note that `Chat-Profile-Image` may appear together with all other headers,
eg. there may be a `Chat-Profile-Image` and a `Chat-Group-Image` header
in the same message.
To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
To save data, it is RECOMMENDED to add a `Chat-Profile-Image` header
only on image changes.

View File

@@ -95,25 +95,16 @@ impl Aheader {
impl fmt::Display for Aheader {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
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
// TODO replace 78 with enum /rtn
// adds a whitespace every 78 characters, this allows libEtPan to
// wrap the lines according to RFC 5322
// (which may insert a linebreak before every whitespace)
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)
let keydata = self.public_key.to_base64(78);
write!(
fmt,
"addr={}; prefer-encrypt={}; keydata={}",
self.addr, self.prefer_encrypt, keydata
)
}
}
@@ -159,10 +150,13 @@ impl str::FromStr for Aheader {
}
};
let prefer_encrypt = attributes
let prefer_encrypt = match attributes
.remove("prefer-encrypt")
.and_then(|raw| raw.parse().ok())
.unwrap_or_default();
{
Some(pref) => pref,
None => EncryptPreference::NoPreference,
};
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
// Autocrypt-Level0: unknown attribute, treat the header as invalid
@@ -182,13 +176,15 @@ impl str::FromStr for Aheader {
mod tests {
use super::*;
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=";
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()
}
#[test]
fn test_from_str() {
let h: Aheader = format!(
"addr=me@mail.com; prefer-encrypt=mutual; keydata={}",
RAWKEY
rawkey()
)
.parse()
.expect("failed to parse");
@@ -197,22 +193,9 @@ 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");
@@ -223,57 +206,33 @@ 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 = 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 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 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");
}
@@ -285,30 +244,4 @@ 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"));
}
}

View File

@@ -6,13 +6,9 @@ use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use self::image::GenericImageView;
use crate::constants::AVATAR_SIZE;
use crate::context::Context;
use crate::events::Event;
extern crate image;
/// Represents a file in the blob directory.
///
/// The object has a name, which will always be valid UTF-8. Having a
@@ -163,7 +159,7 @@ impl<'a> BlobObject<'a> {
/// This merely delegates to the [BlobObject::create_and_copy] and
/// the [BlobObject::from_path] methods. See those for possible
/// errors.
pub fn new_from_path(
pub fn create_from_path(
context: &Context,
src: impl AsRef<Path>,
) -> std::result::Result<BlobObject, BlobError> {
@@ -353,31 +349,6 @@ impl<'a> BlobObject<'a> {
}
true
}
pub fn recode_to_avatar_size(&self, context: &Context) -> Result<(), BlobError> {
let blob_abs = self.to_abs_path();
let img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
return Ok(());
}
let img = img.thumbnail(AVATAR_SIZE, AVATAR_SIZE);
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
Ok(())
}
}
impl<'a> fmt::Display for BlobObject<'a> {
@@ -411,13 +382,6 @@ pub enum BlobError {
cause: std::io::Error,
backtrace: failure::Backtrace,
},
RecodeFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
cause: image::ImageError,
backtrace: failure::Backtrace,
},
WrongBlobdir {
blobdir: PathBuf,
src: PathBuf,
@@ -465,9 +429,6 @@ impl fmt::Display for BlobError {
blobname,
blobdir.display(),
),
BlobError::RecodeFailure {
blobdir, blobname, ..
} => write!(f, "Failed to recode {} in {}", blobname, blobdir.display(),),
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
f,
"File path {} is not in blobdir {}",
@@ -598,14 +559,14 @@ mod tests {
let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
let blob = BlobObject::create_from_path(&t.ctx, &src_ext).unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/external");
let data = fs::read(blob.to_abs_path()).unwrap();
assert_eq!(data, b"boo");
let src_int = t.ctx.get_blobdir().join("internal");
fs::write(&src_int, b"boo").unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_int).unwrap();
let blob = BlobObject::create_from_path(&t.ctx, &src_int).unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
let data = fs::read(blob.to_abs_path()).unwrap();
assert_eq!(data, b"boo");
@@ -615,7 +576,7 @@ mod tests {
let t = dummy_context();
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
let blob = BlobObject::create_from_path(&t.ctx, &src_ext).unwrap();
assert_eq!(
blob.as_name(),
"$BLOBDIR/autocrypt-setup-message-4137848473.html"

View File

@@ -4,7 +4,6 @@ use std::path::{Path, PathBuf};
use itertools::Itertools;
use num_traits::FromPrimitive;
use serde::{Deserialize, Serialize};
use crate::blob::{BlobError, BlobObject};
use crate::chatlist::*;
@@ -35,6 +34,7 @@ pub struct Chat {
pub grpid: String,
blocked: Blocked,
pub param: Params,
pub gossiped_timestamp: i64,
is_sending_locations: bool,
}
@@ -43,7 +43,7 @@ impl Chat {
pub fn load_from_db(context: &Context, chat_id: u32) -> Result<Self, Error> {
let res = context.sql.query_row(
"SELECT c.id,c.type,c.name, c.grpid,c.param,c.archived, \
c.blocked, c.locations_send_until \
c.blocked, c.gossiped_timestamp, c.locations_send_until \
FROM chats c WHERE c.id=?;",
params![chat_id as i32],
|row| {
@@ -55,7 +55,8 @@ impl Chat {
param: row.get::<_, String>(4)?.parse().unwrap_or_default(),
archived: row.get(5)?,
blocked: row.get::<_, Option<_>>(6)?.unwrap_or_default(),
is_sending_locations: row.get(7)?,
gossiped_timestamp: row.get(7)?,
is_sending_locations: row.get(8)?,
};
Ok(c)
@@ -116,12 +117,10 @@ impl Chat {
self.param.exists(Param::Selftalk)
}
/// Returns true if chat is a device chat.
pub fn is_device_talk(&self) -> bool {
self.param.exists(Param::Devicetalk)
}
/// Returns true if user can send messages to this chat.
pub fn can_send(&self) -> bool {
self.id > DC_CHAT_ID_LAST_SPECIAL && !self.is_device_talk()
}
@@ -136,17 +135,14 @@ impl Chat {
Ok(())
}
/// Returns chat ID.
pub fn get_id(&self) -> u32 {
self.id
}
/// Returns chat type.
pub fn get_type(&self) -> Chattype {
self.typ
}
/// Returns chat name.
pub fn get_name(&self) -> &str {
&self.name
}
@@ -172,7 +168,7 @@ impl Chat {
}
if self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup {
if self.id == DC_CHAT_ID_DEADDROP {
if self.id == 1 {
return context.stock_str(StockMessage::DeadDrop).into();
}
let cnt = get_chat_contact_cnt(context, self.id);
@@ -219,10 +215,6 @@ impl Chat {
None
}
pub fn get_gossiped_timestamp(&self, context: &Context) -> i64 {
get_gossiped_timestamp(context, self.id)
}
pub fn get_color(&self, context: &Context) -> u32 {
let mut color = 0;
@@ -240,30 +232,6 @@ impl Chat {
color
}
/// Returns a struct describing the current state of the chat.
///
/// This is somewhat experimental, even more so than the rest of
/// deltachat, and the data returned is still subject to change.
pub fn get_info(&self, context: &Context) -> Result<ChatInfo, Error> {
let draft = match get_draft(context, self.id)? {
Some(message) => message.text.unwrap_or_else(String::new),
_ => String::new(),
};
Ok(ChatInfo {
id: self.id,
type_: self.typ as u32,
name: self.name.clone(),
archived: self.archived,
param: self.param.to_string(),
gossiped_timestamp: self.get_gossiped_timestamp(context),
is_sending_locations: self.is_sending_locations,
color: self.get_color(context),
profile_image: self.get_profile_image(context).unwrap_or_else(PathBuf::new),
subtitle: self.get_subtitle(context),
draft,
})
}
/// Returns true if the chat is archived.
pub fn is_archived(&self) -> bool {
self.archived
@@ -277,12 +245,10 @@ impl Chat {
!self.is_unpromoted()
}
/// Returns true if chat is a verified group chat.
pub fn is_verified(&self) -> bool {
self.typ == Chattype::VerifiedGroup
(self.typ == Chattype::VerifiedGroup)
}
/// Returns true if location streaming is enabled in the chat.
pub fn is_sending_locations(&self) -> bool {
self.is_sending_locations
}
@@ -343,10 +309,10 @@ impl Chat {
self.id
);
}
} else if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup)
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
} else if self.typ == Chattype::Group
|| self.typ == Chattype::VerifiedGroup
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
{
msg.param.set_int(Param::AttachGroupImage, 1);
self.param.remove(Param::Unpromoted);
self.update_param(context)?;
}
@@ -521,68 +487,6 @@ impl Chat {
}
}
/// The current state of a chat.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatInfo {
/// The chat ID.
pub id: u32,
/// The type of chat as a `u32` representation of [Chattype].
///
/// On the C API this number is one of the
/// `DC_CHAT_TYPE_UNDEFINED`, `DC_CHAT_TYPE_SINGLE`,
/// `DC_CHAT_TYPE_GROUP` or `DC_CHAT_TYPE_VERIFIED_GROUP`
/// constants.
#[serde(rename = "type")]
pub type_: u32,
/// The name of the chat.
pub name: String,
/// Whether the chat is archived.
pub archived: bool,
/// The "params" of the chat.
///
/// This is the string-serialised version of [Params] currently.
pub param: String,
/// Something to do with gossiping and timestamps?
pub gossiped_timestamp: i64,
/// Whether this chat is currently sending location-stream messages.
pub is_sending_locations: bool,
/// Colour this chat should be represented in by the UI.
///
/// Yes, spelling colour is hard.
pub color: u32,
/// The path to the profile image.
///
/// If there is no profile image set this will be an empty string
/// currently.
pub profile_image: PathBuf,
/// Subtitle for the chat.
pub subtitle: String,
/// The draft message text.
///
/// If the chat has not draft this is an empty string.
///
/// TODO: This doesn't seem rich enough, it can not handle drafts
/// which contain non-text parts. Perhaps it should be a
/// simple `has_draft` bool instead.
pub draft: String,
// ToDo:
// - [ ] deaddrop,
// - [ ] summary,
// - [ ] lastUpdated,
// - [ ] freshMessageCounter,
// - [ ] email
}
/// Create a normal chat or a group chat by a messages ID that comes typically
/// from the deaddrop, DC_CHAT_ID_DEADDROP (1).
///
@@ -688,6 +592,12 @@ pub fn set_blocking(context: &Context, chat_id: u32, new_blocking: Blocked) -> b
.is_ok()
}
fn copy_device_icon_to_blobs(context: &Context) -> Result<String, Error> {
let icon = include_bytes!("../assets/icon-device.png");
let blob = BlobObject::create(context, "icon-device.png".to_string(), icon)?;
Ok(blob.as_name().to_string())
}
pub fn update_saved_messages_icon(context: &Context) -> Result<(), Error> {
// if there is no saved-messages chat, there is nothing to update. this is no error.
if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_SELF) {
@@ -702,24 +612,6 @@ pub fn update_saved_messages_icon(context: &Context) -> Result<(), Error> {
Ok(())
}
pub fn update_device_icon(context: &Context) -> Result<(), Error> {
// if there is no device-chat, there is nothing to update. this is no error.
if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE) {
let icon = include_bytes!("../assets/icon-device.png");
let blob = BlobObject::create(context, "icon-device.png".to_string(), icon)?;
let icon = blob.as_name().to_string();
let mut chat = Chat::load_from_db(context, chat_id)?;
chat.param.set(Param::ProfileImage, &icon);
chat.update_param(context)?;
let mut contact = Contact::load_from_db(context, DC_CONTACT_ID_DEVICE)?;
contact.param.set(Param::ProfileImage, icon);
contact.update_param(context)?;
}
Ok(())
}
pub fn create_or_lookup_by_contact_id(
context: &Context,
contact_id: u32,
@@ -739,19 +631,23 @@ pub fn create_or_lookup_by_contact_id(
sql::execute(
context,
&context.sql,
"INSERT INTO chats (type, name, param, blocked, grpid, created_timestamp) VALUES(?, ?, ?, ?, ?, ?)",
params![
format!(
"INSERT INTO chats (type, name, param, blocked, grpid, created_timestamp) VALUES({}, '{}', '{}', {}, '{}', {})",
100,
chat_name,
match contact_id {
DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk
DC_CONTACT_ID_DEVICE => "D=1".to_string(), // D = Param::Devicetalk
DC_CONTACT_ID_DEVICE => {
let icon = copy_device_icon_to_blobs(context)?;
format!("D=1\ni={}", icon) // D = Param::Devicetalk, i = Param::ProfileImage
},
_ => "".to_string()
},
create_blocked as u8,
contact.get_addr(),
time(),
]
),
params![],
)?;
let chat_id = sql::get_rowid(context, &context.sql, "chats", "grpid", contact.get_addr());
@@ -759,14 +655,15 @@ pub fn create_or_lookup_by_contact_id(
sql::execute(
context,
&context.sql,
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
params![chat_id, contact_id],
format!(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES({}, {})",
chat_id, contact_id
),
params![],
)?;
if contact_id == DC_CONTACT_ID_SELF {
update_saved_messages_icon(context)?;
} else if contact_id == DC_CONTACT_ID_DEVICE {
update_device_icon(context)?;
}
Ok((chat_id, create_blocked))
@@ -832,7 +729,6 @@ fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> {
.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());
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.
@@ -907,7 +803,6 @@ fn last_msg_in_chat_encrypted(context: &Context, sql: &Sql, chat_id: u32) -> boo
}
}
/// Returns whether a contact is in a chat or not.
pub fn is_contact_in_chat(context: &Context, chat_id: u32, contact_id: u32) -> bool {
/* this function works for group and for normal chats, however, it is more useful for group chats.
DC_CONTACT_ID_SELF may be used to check, if the user itself is in a group chat (DC_CONTACT_ID_SELF is not added to normal chats) */
@@ -1198,7 +1093,6 @@ pub fn get_chat_msgs(
}
}
/// Returns number of messages in a chat.
pub fn get_msg_cnt(context: &Context, chat_id: u32) -> usize {
context
.sql
@@ -1369,7 +1263,6 @@ pub fn get_next_media(
ret
}
/// Archives or unarchives a chat.
pub fn archive(context: &Context, chat_id: u32, archive: bool) -> Result<(), Error> {
ensure!(
chat_id > DC_CHAT_ID_LAST_SPECIAL,
@@ -1405,7 +1298,6 @@ pub fn archive(context: &Context, chat_id: u32, archive: bool) -> Result<(), Err
Ok(())
}
/// Deletes a chat.
pub fn delete(context: &Context, chat_id: u32) -> Result<(), Error> {
ensure!(
chat_id > DC_CHAT_ID_LAST_SPECIAL,
@@ -1536,7 +1428,6 @@ pub fn add_to_chat_contacts_table(context: &Context, chat_id: u32, contact_id: u
.is_ok()
}
/// Adds a contact to the chat.
pub fn add_contact_to_chat(context: &Context, chat_id: u32, contact_id: u32) -> bool {
match add_contact_to_chat_ex(context, chat_id, contact_id, false) {
Ok(res) => res,
@@ -1561,7 +1452,7 @@ pub(crate) fn add_contact_to_chat_ex(
let contact = Contact::get_by_id(context, contact_id)?;
let mut msg = Message::default();
reset_gossiped_timestamp(context, chat_id)?;
reset_gossiped_timestamp(context, chat_id);
/*this also makes sure, not contacts are added to special or normal chats*/
let mut chat = Chat::load_from_db(context, chat_id)?;
@@ -1659,28 +1550,12 @@ fn real_group_exists(context: &Context, chat_id: u32) -> bool {
.unwrap_or_default()
}
pub fn reset_gossiped_timestamp(context: &Context, chat_id: u32) -> crate::sql::Result<()> {
set_gossiped_timestamp(context, chat_id, 0)
pub fn reset_gossiped_timestamp(context: &Context, chat_id: u32) {
set_gossiped_timestamp(context, chat_id, 0);
}
/// Get timestamp of the last gossip sent in the chat.
/// Zero return value means that gossip was never sent.
pub fn get_gossiped_timestamp(context: &Context, chat_id: u32) -> i64 {
context
.sql
.query_get_value::<_, i64>(
context,
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
params![chat_id as i32],
)
.unwrap_or_default()
}
pub fn set_gossiped_timestamp(
context: &Context,
chat_id: u32,
timestamp: i64,
) -> crate::sql::Result<()> {
// Should return Result
pub fn set_gossiped_timestamp(context: &Context, chat_id: u32, timestamp: i64) {
if 0 != chat_id {
info!(
context,
@@ -1693,6 +1568,7 @@ pub fn set_gossiped_timestamp(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
params![timestamp, chat_id as i32],
)
.ok();
} else {
info!(
context,
@@ -1704,58 +1580,10 @@ pub fn set_gossiped_timestamp(
"UPDATE chats SET gossiped_timestamp=?;",
params![timestamp],
)
.ok();
}
}
pub fn shall_attach_selfavatar(context: &Context, chat_id: u32) -> Result<bool, Error> {
// versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others.
// to avoid sending out previously set selfavatars unexpectedly we added this additional check.
// it can be removed after some time.
if !context
.sql
.get_raw_config_bool(context, "attach_selfavatar")
{
return Ok(false);
}
let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60;
let needs_attach = context.sql.query_map(
"SELECT c.selfavatar_sent
FROM chats_contacts cc
LEFT JOIN contacts c ON c.id=cc.contact_id
WHERE cc.chat_id=? AND cc.contact_id!=?;",
params![chat_id, DC_CONTACT_ID_SELF],
|row| Ok(row.get::<_, i64>(0)),
|rows| {
let mut needs_attach = false;
for row in rows {
if let Ok(selfavatar_sent) = row {
let selfavatar_sent = selfavatar_sent?;
if selfavatar_sent < timestamp_some_days_ago {
needs_attach = true;
}
}
}
Ok(needs_attach)
},
)?;
Ok(needs_attach)
}
pub fn set_selfavatar_timestamp(
context: &Context,
chat_id: u32,
timestamp: i64,
) -> Result<(), Error> {
context.sql.execute(
"UPDATE contacts
SET selfavatar_sent=?
WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);",
params![timestamp, chat_id],
)?;
Ok(())
}
pub fn remove_contact_from_chat(
context: &Context,
chat_id: u32,
@@ -1969,7 +1797,6 @@ pub fn set_chat_profile_image(
_ => Err(err),
},
)?;
image_blob.recode_to_avatar_size(context)?;
chat.param.set(Param::ProfileImage, image_blob.as_name());
msg.param.set(Param::Arg, image_blob.as_name());
msg.text = Some(context.stock_system_msg(
@@ -2120,9 +1947,6 @@ pub fn get_chat_id_by_grpid(context: &Context, grpid: impl AsRef<str>) -> (u32,
.unwrap_or((0, false, Blocked::Not))
}
/// Adds a message to device chat.
///
/// Optional `label` can be provided to ensure that message is added only once.
pub fn add_device_msg(
context: &Context,
label: Option<&str>,
@@ -2197,9 +2021,6 @@ pub fn was_device_msg_ever_added(context: &Context, label: &str) -> Result<bool,
Ok(false)
}
/// Adds an informational message to chat.
///
/// For example, it can be a message showing that a member was added to a group.
pub fn add_info_msg(context: &Context, chat_id: u32, text: impl AsRef<str>) {
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
@@ -2230,41 +2051,8 @@ pub fn add_info_msg(context: &Context, chat_id: u32, text: impl AsRef<str>) {
mod tests {
use super::*;
use crate::contact::Contact;
use crate::test_utils::*;
#[test]
fn test_chat_info() {
let t = dummy_context();
let bob = Contact::create(&t.ctx, "bob", "bob@example.com").unwrap();
let chat_id = create_by_contact_id(&t.ctx, bob).unwrap();
let chat = Chat::load_from_db(&t.ctx, chat_id).unwrap();
let info = chat.get_info(&t.ctx).unwrap();
// Ensure we can serialise this.
println!("{}", serde_json::to_string_pretty(&info).unwrap());
let expected = r#"
{
"id": 10,
"type": 100,
"name": "bob",
"archived": false,
"param": "",
"gossiped_timestamp": 0,
"is_sending_locations": false,
"color": 15895624,
"profile_image": "",
"subtitle": "bob@example.com",
"draft": ""
}
"#;
// Ensure we can deserialise this.
let loaded: ChatInfo = serde_json::from_str(expected).unwrap();
assert_eq!(info, loaded);
}
#[test]
fn test_get_draft_no_draft() {
let t = dummy_context();
@@ -2565,42 +2353,4 @@ mod tests {
"bar"
);
}
#[test]
fn test_create_same_chat_twice() {
let context = dummy_context();
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap();
assert_ne!(contact1, 0);
let chat_id = create_by_contact_id(&context.ctx, contact1).unwrap();
assert!(
chat_id > DC_CHAT_ID_LAST_SPECIAL,
"chat_id too small {}",
chat_id
);
let chat = Chat::load_from_db(&context.ctx, chat_id).unwrap();
let chat2_id = create_by_contact_id(&context.ctx, contact1).unwrap();
assert_eq!(chat2_id, chat_id);
let chat2 = Chat::load_from_db(&context.ctx, chat2_id).unwrap();
assert_eq!(chat2.name, chat.name);
}
#[test]
fn test_shall_attach_selfavatar() {
let t = dummy_context();
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap();
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
let (contact_id, _) =
Contact::add_or_lookup(&t.ctx, "", "foo@bar.org", Origin::IncomingUnknownTo).unwrap();
add_contact_to_chat(&t.ctx, chat_id, contact_id);
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
t.ctx.set_config(Config::Selfavatar, None).unwrap(); // setting to None also forces re-sending
assert!(shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
assert!(set_selfavatar_timestamp(&t.ctx, chat_id, time()).is_ok());
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
}
}

View File

@@ -314,7 +314,7 @@ impl Chatlist {
}
}
/// Returns the number of archived chats
/// Get the number of archived chats
pub fn dc_get_archived_cnt(context: &Context) -> u32 {
context
.sql

View File

@@ -9,7 +9,6 @@ use crate::context::Context;
use crate::dc_tools::*;
use crate::job::*;
use crate::stock::StockMessage;
use rusqlite::NO_PARAMS;
/// The available configuration keys.
#[derive(
@@ -128,23 +127,13 @@ impl Context {
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
pub fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
match key {
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", NO_PARAMS)?;
self.sql
.set_raw_config_bool(self, "attach_selfavatar", true)?;
match value {
Some(value) => {
let blob = BlobObject::new_from_path(&self, value)?;
blob.recode_to_avatar_size(self)?;
self.sql.set_raw_config(self, key, Some(blob.as_name()))
}
None => self.sql.set_raw_config(self, key, None),
}
Config::Selfavatar if value.is_some() => {
let blob = BlobObject::create_from_path(&self, value.unwrap())?;
self.sql.set_raw_config(self, key, Some(blob.as_name()))
}
Config::InboxWatch => {
let ret = self.sql.set_raw_config(self, key, value);
interrupt_inbox_idle(self);
interrupt_inbox_idle(self, true);
ret
}
Config::SentboxWatch => {
@@ -191,8 +180,6 @@ mod tests {
use std::string::ToString;
use crate::test_utils::*;
use std::fs::File;
use std::io::Write;
#[test]
fn test_to_string() {
@@ -212,17 +199,16 @@ mod tests {
}
#[test]
fn test_selfavatar_outside_blobdir() -> failure::Fallible<()> {
fn test_selfavatar() -> failure::Fallible<()> {
let t = dummy_context();
let avatar_src = t.dir.path().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src)?.write_all(avatar_bytes)?;
std::fs::write(&avatar_src, b"avatar")?;
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists());
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))?;
assert!(avatar_blob.exists());
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
assert_eq!(std::fs::read(&avatar_blob)?, b"avatar");
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
Ok(())
@@ -232,8 +218,7 @@ mod tests {
fn test_selfavatar_in_blobdir() -> failure::Fallible<()> {
let t = dummy_context();
let avatar_src = t.ctx.get_blobdir().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src)?.write_all(avatar_bytes)?;
std::fs::write(&avatar_src, b"avatar")?;
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))?;
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);

View File

@@ -1,6 +1,8 @@
//! # 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};

View File

@@ -1,5 +1,7 @@
//! Outlook's Autodiscover
use failure::Fail;
use quick_xml;
use quick_xml::events::BytesEnd;

View File

@@ -6,15 +6,13 @@ mod read_url;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use async_std::task;
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::job::*;
use crate::login_param::{CertificateChecks, LoginParam};
use crate::login_param::LoginParam;
use crate::oauth2::*;
use crate::param::Params;
@@ -94,11 +92,9 @@ pub fn JobConfigureImap(context: &Context) {
let mut param_domain = "undefined.undefined".to_owned();
let mut param_addr_urlencoded: String =
"Internal Error: this value should never be used".to_owned();
let mut keep_flags = 0;
const STEP_12_USE_AUTOCONFIG: u8 = 12;
const STEP_13_AFTER_AUTOCONFIG: u8 = 13;
let mut keep_flags = std::i32::MAX;
const STEP_3_INDEX: u8 = 13;
let mut step_counter: u8 = 0;
while !context.shall_stop_ongoing() {
step_counter += 1;
@@ -114,7 +110,7 @@ pub fn JobConfigureImap(context: &Context) {
}
// Step 1: Load the parameters and check email-address and password
2 => {
if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 {
if 0 != param.server_flags & 0x2 {
// the used oauth2 addr may differ, check this.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation,
// just use the given one.
@@ -149,7 +145,6 @@ pub fn JobConfigureImap(context: &Context) {
// Step 2: Autoconfig
4 => {
progress!(context, 200);
if param.mail_server.is_empty()
&& param.mail_port == 0
/*&&param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then */
@@ -157,18 +152,12 @@ pub fn JobConfigureImap(context: &Context) {
&& param.send_port == 0
&& param.send_user.is_empty()
/*&&param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for autoconfig or not */
&& (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0
&& param.server_flags & !0x2 == 0
{
// no advanced parameters entered by the user: query provider-database or do Autoconfig
keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2;
if let Some(new_param) = get_offline_autoconfig(context, &param) {
// got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting
param_autoconfig = Some(new_param);
step_counter = STEP_12_USE_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop
}
keep_flags = param.server_flags & 0x2;
} else {
// advanced parameters entered by the user: skip Autoconfig
step_counter = STEP_13_AFTER_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop
// Autoconfig is not needed so skip it.
step_counter = STEP_3_INDEX - 1;
}
true
}
@@ -253,10 +242,8 @@ pub fn JobConfigureImap(context: &Context) {
}
true
}
/* C. Do we have any autoconfig result?
If you change the match-number here, also update STEP_12_COPY_AUTOCONFIG above
*/
STEP_12_USE_AUTOCONFIG => {
/* C. Do we have any result? */
12 => {
progress!(context, 500);
if let Some(ref cfg) = param_autoconfig {
info!(context, "Got autoconfig: {}", &cfg);
@@ -269,15 +256,15 @@ pub fn JobConfigureImap(context: &Context) {
param.send_port = cfg.send_port;
param.send_user = cfg.send_user.clone();
param.server_flags = cfg.server_flags;
/* although param_autoconfig's data are no longer needed from,
it is used to later to prevent trying variations of port/server/logins */
/* although param_autoconfig's data are no longer needed from, it is important to keep the object as
we may enter "deep guessing" if we could not read a configuration */
}
param.server_flags |= keep_flags;
true
}
// Step 3: Fill missing fields with defaults
// If you change the match-number here, also update STEP_13_AFTER_AUTOCONFIG above
STEP_13_AFTER_AUTOCONFIG => {
13 => {
// if you move this, don't forget to update STEP_3_INDEX, too
if param.mail_server.is_empty() {
param.mail_server = format!("imap.{}", param_domain,)
}
@@ -443,42 +430,6 @@ pub fn JobConfigureImap(context: &Context) {
progress!(context, if success { 1000 } else { 0 });
}
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
// XXX we don't have https://github.com/deltachat/provider-db APIs
// integrated yet but we'll already add nauta as a first use case, also
// showing what we need from provider-db in the future.
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if param.addr.ends_with("@nauta.cu") {
let mut p = LoginParam::new();
p.addr = param.addr.clone();
p.mail_server = "imap.nauta.cu".to_string();
p.mail_user = param.addr.clone();
p.mail_pw = param.mail_pw.clone();
p.mail_port = 143;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.send_server = "smtp.nauta.cu".to_string();
p.send_user = param.addr.clone();
p.send_pw = param.mail_pw.clone();
p.send_port = 25;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags = DC_LP_AUTH_NORMAL as i32
| DC_LP_IMAP_SOCKET_STARTTLS as i32
| DC_LP_SMTP_SOCKET_STARTTLS as i32;
info!(context, "found offline autoconfig: {}", p);
Some(p)
} else {
info!(context, "no offline autoconfig found");
None
}
}
fn try_imap_connections(
context: &Context,
mut param: &mut LoginParam,
@@ -534,22 +485,17 @@ fn try_imap_connection(
fn try_imap_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
let inf = format!(
"imap: {}@{}:{} flags=0x{:x} certificate_checks={}",
param.mail_user,
param.mail_server,
param.mail_port,
param.server_flags,
param.imap_certificate_checks
"imap: {}@{}:{} flags=0x{:x}",
param.mail_user, param.mail_server, param.mail_port, param.server_flags
);
info!(context, "Trying: {}", inf);
if task::block_on(
context
.inbox_thread
.read()
.unwrap()
.imap
.connect(context, &param),
) {
if context
.inbox_thread
.read()
.unwrap()
.imap
.connect(context, &param)
{
info!(context, "success: {}", inf);
return Some(true);
}
@@ -621,7 +567,6 @@ fn try_smtp_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::*;
use crate::configure::JobConfigureImap;
use crate::test_utils::*;
@@ -635,19 +580,4 @@ mod tests {
t.ctx.set_config(Config::MailPw, Some("123456")).unwrap();
JobConfigureImap(&t.ctx);
}
#[test]
fn test_get_offline_autoconfig() {
let context = dummy_context().ctx;
let mut params = LoginParam::new();
params.addr = "someone123@example.org".to_string();
assert!(get_offline_autoconfig(&context, &params).is_none());
let mut params = LoginParam::new();
params.addr = "someone123@nauta.cu".to_string();
let found_params = get_offline_autoconfig(&context, &params).unwrap();
assert_eq!(found_params.mail_server, "imap.nauta.cu".to_string());
assert_eq!(found_params.send_server, "smtp.nauta.cu".to_string());
}
}

View File

@@ -1,4 +1,5 @@
use crate::context::Context;
use failure::Fail;
#[derive(Debug, Fail)]
pub enum Error {

View File

@@ -58,9 +58,6 @@ pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
pub const DC_GCL_VERIFIED_ONLY: usize = 0x01;
pub const DC_GCL_ADD_SELF: usize = 0x02;
// unchanged user avatars are resent to the recipients every some days
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
// values for DC_PARAM_FORCE_PLAINTEXT
pub(crate) const DC_FP_NO_AUTOCRYPT_HEADER: i32 = 2;
pub(crate) const DC_FP_ADD_AUTOCRYPT_HEADER: i32 = 1;
@@ -114,7 +111,7 @@ pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// approx. max. length returned by dc_msg_get_text()
const DC_MAX_GET_TEXT_LEN: usize = 30000;
/// approx. max. length returned by dc_get_msg_info()
const DC_MAX_GET_INFO_LEN: usize = 100_000;
const DC_MAX_GET_INFO_LEN: usize = 100000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
@@ -122,10 +119,6 @@ pub const DC_CONTACT_ID_INFO: u32 = 2;
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
// decorative address that is used for DC_CONTACT_ID_DEVICE
// when an api that returns an email is called.
pub const DC_CONTACT_ID_DEVICE_ADDR: &str = "device@localhost";
// Flags for empty server job
pub const DC_EMPTY_MVBOX: u32 = 0x01;
@@ -184,9 +177,6 @@ pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
pub const DC_BOB_ERROR: i32 = 0;
pub const DC_BOB_SUCCESS: i32 = 1;
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(i32)]
pub enum Viewtype {

View File

@@ -1,5 +1,3 @@
//! Contacts module
use std::path::PathBuf;
use deltachat_derive::*;
@@ -12,13 +10,11 @@ use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::error::{Error, Result};
use crate::error::Result;
use crate::events::Event;
use crate::key::*;
use crate::login_param::LoginParam;
use crate::message::{MessageState, MsgId};
use crate::mimeparser::AvatarAction;
use crate::param::*;
use crate::peerstate::*;
use crate::sql;
use crate::stock::StockMessage;
@@ -62,8 +58,6 @@ pub struct Contact {
blocked: bool,
/// The origin/source of the contact.
pub origin: Origin,
/// Parameters as Param::ProfileImage
pub param: Params,
}
/// Possible origins of a contact.
@@ -100,11 +94,11 @@ pub enum Origin {
/// address is in our address book
AdressBook = 0x80000,
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
SecurejoinInvited = 0x0100_0000,
SecurejoinInvited = 0x1000000,
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
SecurejoinJoined = 0x0200_0000,
SecurejoinJoined = 0x2000000,
/// contact added mannually by dc_create_contact(), this should be the largets origin as otherwise the user cannot modify the names
ManuallyCreated = 0x0400_0000,
ManuallyCreated = 0x4000000,
}
impl Default for Origin {
@@ -114,11 +108,14 @@ impl Default for Origin {
}
impl Origin {
/// Contacts that are known, i. e. they came in via accepted contacts or
/// themselves an accepted contact. Known contacts are shown in the
/// contact list when one creates a chat and wants to add members etc.
pub fn is_known(self) -> bool {
self >= Origin::IncomingReplyTo
/// Contacts that are verified and known not to be spam.
pub fn is_verified(self) -> bool {
self as i32 >= 0x100
}
/// Contacts that are shown in the contact list.
pub fn include_in_contactlist(self) -> bool {
self as i32 >= DC_ORIGIN_MIN_CONTACT_LIST
}
}
@@ -142,10 +139,32 @@ pub enum VerifiedStatus {
impl Contact {
pub fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
let mut res = context.sql.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param
FROM contacts c
WHERE c.id=?;",
if contact_id == DC_CONTACT_ID_SELF {
let contact = Contact {
id: contact_id,
name: context.stock_str(StockMessage::SelfMsg).into(),
authname: "".into(),
addr: context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default(),
blocked: false,
origin: Origin::Unknown,
};
return Ok(contact);
} else if contact_id == DC_CONTACT_ID_DEVICE {
let contact = Contact {
id: contact_id,
name: context.stock_str(StockMessage::DeviceMessages).into(),
authname: "".into(),
addr: "device@localhost".into(),
blocked: false,
origin: Origin::Unknown,
};
return Ok(contact);
}
context.sql.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname FROM contacts c WHERE c.id=?;",
params![contact_id as i32],
|row| {
let contact = Self {
@@ -155,21 +174,10 @@ impl Contact {
addr: row.get::<_, String>(1)?,
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
origin: row.get(2)?,
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
};
Ok(contact)
},
)?;
if contact_id == DC_CONTACT_ID_SELF {
res.name = context.stock_str(StockMessage::SelfMsg).to_string();
res.addr = context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
} else if contact_id == DC_CONTACT_ID_DEVICE {
res.name = context.stock_str(StockMessage::DeviceMessages).to_string();
res.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
}
Ok(res)
}
)
}
/// Returns `true` if this contact is blocked.
@@ -398,7 +406,6 @@ impl Contact {
{
row_id = sql::get_rowid(context, &context.sql, "contacts", "addr", addr);
sth_modified = Modifier::Created;
info!(context, "added contact id={} addr={}", row_id, addr);
} else {
error!(context, "Cannot add contact.");
}
@@ -484,7 +491,7 @@ impl Contact {
params![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
Origin::IncomingReplyTo,
0x100,
&s3str_like_cmd,
&s3str_like_cmd,
if flag_verified_only { 0 } else { 1 },
@@ -702,16 +709,6 @@ impl Contact {
Ok(Contact::load_from_db(context, contact_id)?)
}
pub fn update_param(&mut self, context: &Context) -> Result<()> {
sql::execute(
context,
&context.sql,
"UPDATE contacts SET param=? WHERE id=?",
params![self.param.to_string(), self.id as i32],
)?;
Ok(())
}
/// Get the ID of the contact.
pub fn get_id(&self) -> u32 {
self.id
@@ -745,9 +742,6 @@ impl Contact {
if !self.name.is_empty() {
return &self.name;
}
if !self.authname.is_empty() {
return &self.authname;
}
&self.addr
}
@@ -783,11 +777,8 @@ impl Contact {
if let Some(p) = context.get_config(Config::Selfavatar) {
return Some(PathBuf::from(p));
}
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Some(dc_get_abs_path(context, image_rel));
}
}
// TODO: else get image_abs from contact param
None
}
@@ -870,14 +861,14 @@ impl Contact {
.unwrap_or_default() as usize
}
pub fn get_origin_by_id(context: &Context, contact_id: u32, ret_blocked: &mut bool) -> Origin {
pub fn get_origin_by_id(context: &Context, contact_id: u32, ret_blocked: &mut i32) -> Origin {
let mut ret = Origin::Unknown;
*ret_blocked = false;
*ret_blocked = 0;
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
/* we could optimize this by loading only the needed fields */
if contact.blocked {
*ret_blocked = true;
*ret_blocked = 1;
} else {
ret = contact.origin;
}
@@ -966,32 +957,6 @@ fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
}
}
pub fn set_profile_image(
context: &Context,
contact_id: u32,
profile_image: &AvatarAction,
) -> Result<()> {
// the given profile image is expected to be already in the blob directory
// as profile images can be set only by receiving messages, this should be always the case, however.
let mut contact = Contact::load_from_db(context, contact_id)?;
let changed = match profile_image {
AvatarAction::Change(profile_image) => {
contact.param.set(Param::ProfileImage, profile_image);
true
}
AvatarAction::Delete => {
contact.param.remove(Param::ProfileImage);
true
}
AvatarAction::None => false,
};
if changed {
contact.update_param(context)?;
context.call_cb(Event::ContactsChanged(Some(contact_id)));
}
Ok(())
}
/// Normalize a name.
///
/// - Remove quotes (come from some bad MUA implementations)
@@ -1056,18 +1021,6 @@ fn cat_fingerprint(
}
}
impl Context {
/// determine whether the specified addr maps to the/a self addr
pub fn is_self_addr(&self, addr: &str) -> Result<bool> {
let self_addr = match self.get_config(Config::ConfiguredAddr) {
Some(s) => s,
None => return Err(Error::NotConfigured),
};
Ok(addr_cmp(self_addr, addr))
}
}
pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
let norm1 = addr_normalize(addr1.as_ref()).to_lowercase();
let norm2 = addr_normalize(addr2.as_ref()).to_lowercase();
@@ -1075,6 +1028,15 @@ pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
norm1 == norm2
}
pub fn addr_equals_self(context: &Context, addr: impl AsRef<str>) -> bool {
if !addr.as_ref().is_empty() {
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr) {
return addr_cmp(addr, self_addr);
}
}
false
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
book.lines()
.chunks(2)
@@ -1158,18 +1120,6 @@ mod tests {
assert_eq!(contacts.len(), 0);
}
#[test]
fn test_is_self_addr() -> Result<()> {
let t = test_context(None);
assert!(t.ctx.is_self_addr("me@me.org").is_err());
let addr = configure_alice_keypair(&t.ctx);
assert_eq!(t.ctx.is_self_addr("me@me.org")?, false);
assert_eq!(t.ctx.is_self_addr(&addr)?, true);
Ok(())
}
#[test]
fn test_add_or_lookup() {
// add some contacts, this also tests add_address_book()

View File

@@ -1,4 +1,4 @@
//! Context module
//! Contacts module
use std::collections::HashMap;
use std::ffi::OsString;
@@ -298,11 +298,6 @@ impl Context {
res.insert("database_version", dbversion.to_string());
res.insert("blobdir", self.get_blobdir().display().to_string());
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
res.insert(
"selfavatar",
self.get_config(Config::Selfavatar)
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());

File diff suppressed because it is too large Load Diff

View File

@@ -1,169 +1,175 @@
use crate::dehtml::*;
/// Remove standard (RFC 3676, §4.3) footer if it is found.
fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
#[derive(Copy, Clone)]
pub struct Simplify {
pub is_forwarded: bool,
}
/// Return index of footer line in vector of message lines, or vector length if
/// no footer is found.
///
/// Also return whether not-standard (rfc3676, §4.3) footer is found.
fn find_message_footer(lines: &[&str]) -> (usize, bool) {
for (ix, &line) in lines.iter().enumerate() {
// quoted-printable may encode `-- ` to `-- =20` which is converted
// back to `-- `
match line {
"-- " | "-- " => return &lines[..ix],
"-- " | "-- " => return (ix, false),
"--" | "---" | "----" => return (ix, true),
_ => (),
}
}
lines
(lines.len(), false)
}
/// Remove nonstandard footer and a boolean indicating whether such
/// footer was removed.
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
for (ix, &line) in lines.iter().enumerate() {
if line == "--"
|| line == "---"
|| line == "----"
|| line.starts_with("-----")
|| line.starts_with("_____")
|| line.starts_with("=====")
|| line.starts_with("*****")
|| line.starts_with("~~~~~")
{
return (&lines[..ix], true);
impl Simplify {
pub fn new() -> Self {
Simplify {
is_forwarded: false,
}
}
(lines, false)
}
fn split_lines(buf: &str) -> Vec<&str> {
buf.split('\n').collect()
}
/// Simplify and normalise text: Remove quotes, signatures, unnecessary
/// lineends etc.
/// The data returned from simplify() must be free()'d when no longer used.
pub fn simplify(&mut self, input: &str, is_html: bool, is_msgrmsg: bool) -> String {
let mut out = if is_html {
dehtml(input)
} else {
input.to_string()
};
/// Simplify message text for chat display.
/// Remove quotes, signatures, trailing empty lines etc.
pub fn simplify(input: &str, is_html: bool, is_chat_message: bool) -> (String, bool) {
let mut out = if is_html {
dehtml(input)
} else {
input.to_string()
};
out.retain(|c| c != '\r');
out = self.simplify_plain_text(&out, is_msgrmsg);
out.retain(|c| c != '\r');
out.retain(|c| c != '\r');
let lines = split_lines(&out);
let (lines, is_forwarded) = skip_forward_header(&lines);
let lines = remove_message_footer(lines);
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
let (lines, has_bottom_quote) = if !is_chat_message {
remove_bottom_quote(lines)
} else {
(lines, false)
};
let (lines, has_top_quote) = if !is_chat_message {
remove_top_quote(lines)
} else {
(lines, false)
};
// re-create buffer from the remaining lines
let text = render_message(
lines,
has_top_quote,
has_nonstandard_footer || has_bottom_quote,
);
(text, is_forwarded)
}
/// Skips "forwarded message" header.
/// Returns message body lines and a boolean indicating whether
/// a message is forwarded or not.
fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
if lines.len() >= 3
&& lines[0] == "---------- Forwarded message ----------"
&& lines[1].starts_with("From: ")
&& lines[2].is_empty()
{
(&lines[3..], true)
} else {
(lines, false)
out
}
}
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
let mut last_quoted_line = None;
for (l, line) in lines.iter().enumerate().rev() {
if is_plain_quote(line) {
last_quoted_line = Some(l)
} else if !is_empty_line(line) {
break;
}
}
if let Some(mut l_last) = last_quoted_line {
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
l_last -= 1
}
if l_last > 1 {
let line = lines[l_last - 1];
if is_quoted_headline(line) {
l_last -= 1
/**
* Simplify Plain Text
*/
#[allow(non_snake_case, clippy::mut_range_bound)]
fn simplify_plain_text(&mut self, buf_terminated: &str, is_msgrmsg: bool) -> String {
/* This function ...
... removes all text after the line `-- ` (footer mark)
... removes full quotes at the beginning and at the end of the text -
these are all lines starting with the character `>`
... remove a non-empty line before the removed quote (contains sth. like "On 2.9.2016, Bjoern wrote:" in different formats and lanugages) */
/* split the given buffer into lines */
let lines: Vec<_> = buf_terminated.split('\n').collect();
let mut l_first: usize = 0;
let mut is_cut_at_begin = false;
let (mut l_last, mut is_cut_at_end) = find_message_footer(&lines);
if l_last > l_first + 2 {
let line0 = lines[l_first];
let line1 = lines[l_first + 1];
let line2 = lines[l_first + 2];
if line0 == "---------- Forwarded message ----------"
&& line1.starts_with("From: ")
&& line2.is_empty()
{
self.is_forwarded = true;
l_first += 3
}
}
(&lines[..l_last], true)
} else {
(lines, false)
}
}
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
let mut last_quoted_line = None;
let mut has_quoted_headline = false;
for (l, line) in lines.iter().enumerate() {
if is_plain_quote(line) {
last_quoted_line = Some(l)
} else if !is_empty_line(line) {
if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() {
has_quoted_headline = true
} else {
/* non-quoting line found */
for l in l_first..l_last {
let line = lines[l];
if line == "-----"
|| line == "_____"
|| line == "====="
|| line == "*****"
|| line == "~~~~~"
{
l_last = l;
is_cut_at_end = true;
/* done */
break;
}
}
}
if let Some(last_quoted_line) = last_quoted_line {
(&lines[last_quoted_line + 1..], true)
} else {
(lines, false)
}
}
fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) -> String {
let mut ret = String::new();
if is_cut_at_begin {
ret += "[...]";
}
/* we write empty lines only in case and non-empty line follows */
let mut pending_linebreaks = 0;
let mut empty_body = true;
for line in lines {
if is_empty_line(line) {
pending_linebreaks += 1
} else {
if !empty_body {
if pending_linebreaks > 2 {
pending_linebreaks = 2
}
while 0 != pending_linebreaks {
ret += "\n";
pending_linebreaks -= 1
if !is_msgrmsg {
let mut l_lastQuotedLine = None;
for l in (l_first..l_last).rev() {
let line = lines[l];
if is_plain_quote(line) {
l_lastQuotedLine = Some(l)
} else if !is_empty_line(line) {
break;
}
}
if let Some(last_quoted_line) = l_lastQuotedLine {
l_last = last_quoted_line;
is_cut_at_end = true;
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
l_last -= 1
}
if l_last > 1 {
let line = lines[l_last - 1];
if is_quoted_headline(line) {
l_last -= 1
}
}
}
// the incoming message might contain invalid UTF8
ret += line;
empty_body = false;
pending_linebreaks = 1
}
if !is_msgrmsg {
let mut l_lastQuotedLine_0 = None;
let mut hasQuotedHeadline = 0;
for l in l_first..l_last {
let line = lines[l];
if is_plain_quote(line) {
l_lastQuotedLine_0 = Some(l)
} else if !is_empty_line(line) {
if is_quoted_headline(line)
&& 0 == hasQuotedHeadline
&& l_lastQuotedLine_0.is_none()
{
hasQuotedHeadline = 1i32
} else {
/* non-quoting line found */
break;
}
}
}
if let Some(last_quoted_line) = l_lastQuotedLine_0 {
l_first = last_quoted_line + 1;
is_cut_at_begin = true
}
}
/* re-create buffer from the remaining lines */
let mut ret = String::new();
if is_cut_at_begin {
ret += "[...]";
}
/* we write empty lines only in case and non-empty line follows */
let mut pending_linebreaks = 0;
let mut content_lines_added = 0;
for l in l_first..l_last {
let line = lines[l];
if is_empty_line(line) {
pending_linebreaks += 1
} else {
if 0 != content_lines_added {
if pending_linebreaks > 2i32 {
pending_linebreaks = 2i32
}
while 0 != pending_linebreaks {
ret += "\n";
pending_linebreaks -= 1
}
}
// the incoming message might contain invalid UTF8
ret += line;
content_lines_added += 1;
pending_linebreaks = 1i32
}
}
if is_cut_at_end && (!is_cut_at_begin || 0 != content_lines_added) {
ret += " [...]";
}
ret
}
if is_cut_at_end && (!is_cut_at_begin || !empty_body) {
ret += " [...]";
}
ret
}
/**
@@ -207,59 +213,50 @@ mod tests {
#[test]
// proptest does not support [[:graphical:][:space:]] regex.
fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") {
let (output, _is_forwarded) = simplify(&input, false, true);
let output = Simplify::new().simplify_plain_text(&input, true);
assert!(output.split('\n').all(|s| s != "-- "));
}
}
#[test]
fn test_simplify_trim() {
let mut simplify = Simplify::new();
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
let (plain, is_forwarded) = simplify(html, true, false);
let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "line1\nline2");
assert!(!is_forwarded);
}
#[test]
fn test_simplify_parse_href() {
let mut simplify = Simplify::new();
let html = "<a href=url>text</a";
let (plain, is_forwarded) = simplify(html, true, false);
let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "[text](url)");
assert!(!is_forwarded);
}
#[test]
fn test_simplify_bold_text() {
let mut simplify = Simplify::new();
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let (plain, is_forwarded) = simplify(html, true, false);
let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "text *bold*<>");
assert!(!is_forwarded);
}
#[test]
fn test_simplify_forwarded_message() {
let text = "---------- Forwarded message ----------\r\nFrom: test@example.com\r\n\r\nForwarded message\r\n-- \r\nSignature goes here";
let (plain, is_forwarded) = simplify(text, false, false);
assert_eq!(plain, "Forwarded message");
assert!(is_forwarded);
}
#[test]
fn test_simplify_html_encoded() {
let mut simplify = Simplify::new();
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let (plain, is_forwarded) = simplify(html, true, false);
let plain = simplify.simplify(html, true, false);
assert_eq!(
plain,
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
);
assert!(!is_forwarded);
}
#[test]
@@ -273,19 +270,4 @@ mod tests {
assert!(!is_plain_quote("Life is pain"));
assert!(!is_plain_quote(""));
}
#[test]
fn test_remove_top_quote() {
let (lines, has_top_quote) = remove_top_quote(&["> first", "> second"]);
assert!(lines.is_empty());
assert!(has_top_quote);
let (lines, has_top_quote) = remove_top_quote(&["> first", "> second", "not a quote"]);
assert_eq!(lines, &["not a quote"]);
assert!(has_top_quote);
let (lines, has_top_quote) = remove_top_quote(&["not a quote", "> first", "> second"]);
assert_eq!(lines, &["not a quote", "> first", "> second"]);
assert!(!has_top_quote);
}
}

View File

@@ -49,8 +49,8 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Co
/// - harmonize together while being different enough
/// (therefore, we cannot just use random rgb colors :)
const COLORS: [u32; 16] = [
0xe5_65_55, 0xf2_8c_48, 0x8e_85_ee, 0x76_c8_4d, 0x5b_b6_cc, 0x54_9c_dd, 0xd2_5c_99, 0xb3_78_00,
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
0xe56555, 0xf28c48, 0x8e85ee, 0x76c84d, 0x5bb6cc, 0x549cdd, 0xd25c99, 0xb37800, 0xf23030,
0x39b249, 0xbb243b, 0x964078, 0x66874f, 0x308ab9, 0x127ed0, 0xbe450c,
];
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
@@ -59,7 +59,7 @@ pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
let bytes = str_lower.as_bytes();
for (i, byte) in bytes.iter().enumerate() {
checksum += (i + 1) * *byte as usize;
checksum %= 0x00ff_ffff;
checksum %= 0xffffff;
}
let color_index = checksum % COLORS.len();
@@ -182,6 +182,22 @@ fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
String::from_utf8(wrapped_writer).unwrap()
}
pub(crate) fn dc_create_incoming_rfc724_mid(
message_timestamp: i64,
contact_id_from: u32,
contact_ids_to: &[u32],
) -> Option<String> {
/* create a deterministic rfc724_mid from input such that
repeatedly calling it with the same input results in the same Message-id */
let largest_id_to = contact_ids_to.iter().max().copied().unwrap_or_default();
let result = format!(
"{}-{}-{}@stub",
message_timestamp, contact_id_from, largest_id_to
);
Some(result)
}
/// Function generates a Message-ID that can be used for a new outgoing message.
/// - this function is called for all outgoing messages.
/// - the message ID should be globally unique
@@ -262,9 +278,11 @@ 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> {
Path::new(path_filename.as_ref())
.extension()
.map(|p| p.to_string_lossy().to_lowercase())
if let Some(p) = Path::new(path_filename.as_ref()).extension() {
Some(p.to_string_lossy().to_lowercase())
} else {
None
}
}
/// Returns the `(width, height)` of the given image buffer.
@@ -381,14 +399,11 @@ pub(crate) fn dc_copy_file(
}
}
pub(crate) fn dc_create_folder(
context: &Context,
path: impl AsRef<std::path::Path>,
) -> Result<(), std::io::Error> {
pub(crate) fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() {
match fs::create_dir_all(path_abs) {
Ok(_) => Ok(()),
Ok(_) => true,
Err(err) => {
warn!(
context,
@@ -396,11 +411,11 @@ pub(crate) fn dc_create_folder(
path.as_ref().display(),
err
);
Err(err)
false
}
}
} else {
Ok(())
true
}
}
@@ -767,6 +782,14 @@ mod tests {
}
}
#[test]
fn test_dc_create_incoming_rfc724_mid() {
let res = dc_create_incoming_rfc724_mid(123, 45, &[6, 7]);
assert_eq!(res, Some("123-45-7@stub".into()));
let res = dc_create_incoming_rfc724_mid(123, 45, &[]);
assert_eq!(res, Some("123-45-0@stub".into()));
}
#[test]
fn test_file_get_safe_basename() {
assert_eq!(get_safe_basename("12312/hello"), "hello");
@@ -828,7 +851,7 @@ mod tests {
assert!(dc_delete_file(context, "$BLOBDIR/foobar"));
assert!(dc_delete_file(context, "$BLOBDIR/dada"));
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder").is_ok());
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder"));
assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",));
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder"));

View File

@@ -139,12 +139,13 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
dehtml.add_text = AddText::YesPreserveLineEnds;
}
"a" => {
if let Some(href) = event
.html_attributes()
.filter_map(|attr| attr.ok())
.find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "href")
{
if let Some(href) = event.html_attributes().find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "href")
.unwrap_or_default()
}) {
let href = href
.unwrap()
.unescape_and_decode_value(reader)
.unwrap_or_default()
.to_lowercase();

View File

@@ -2,7 +2,7 @@
use std::collections::HashSet;
use mailparse::{MailHeaderMap, ParsedMail};
use mailparse::MailHeaderMap;
use num_traits::FromPrimitive;
use crate::aheader::*;
@@ -14,6 +14,7 @@ use crate::keyring::*;
use crate::peerstate::*;
use crate::pgp;
use crate::securejoin::handle_degrade_event;
use crate::wrapmime;
#[derive(Debug)]
pub struct EncryptHelper {
@@ -117,7 +118,7 @@ impl EncryptHelper {
pub fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
mail: &mailparse::ParsedMail<'_>,
message_time: i64,
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
let from = mail
@@ -231,36 +232,9 @@ fn load_or_generate_self_public_key(context: &Context, self_addr: impl AsRef<str
}
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
ensure!(
mail.ctype.mimetype == "multipart/encrypted",
"Not a multipart/encrypted message: {}",
mail.ctype.mimetype
);
ensure!(
mail.subparts.len() == 2,
"Invalid Autocrypt Level 1 Mime Parts"
);
ensure!(
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
mail.subparts[0].ctype,
);
ensure!(
mail.subparts[1].ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
mail.subparts[1].ctype
);
Ok(&mail.subparts[1])
}
fn decrypt_if_autocrypt_message<'a>(
context: &Context,
mail: &ParsedMail<'a>,
mail: &mailparse::ParsedMail<'a>,
private_keyring: &Keyring,
public_keyring_for_validate: &Keyring,
ret_valid_signatures: &mut HashSet<String>,
@@ -272,14 +246,14 @@ fn decrypt_if_autocrypt_message<'a>(
//
// Errors are returned for failures related to decryption of AC-messages.
let encrypted_data_part = match get_autocrypt_mime(mail) {
Err(_) => {
// not an autocrypt mime message, abort and ignore
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);
return Ok(None);
}
Ok(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
decrypt_part(
context,
@@ -293,7 +267,7 @@ fn decrypt_if_autocrypt_message<'a>(
/// Returns Ok(None) if nothing encrypted was found.
fn decrypt_part(
_context: &Context,
mail: &ParsedMail<'_>,
mail: &mailparse::ParsedMail<'_>,
private_keyring: &Keyring,
public_keyring_for_validate: &Keyring,
ret_valid_signatures: &mut HashSet<String>,
@@ -339,7 +313,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
/// However, Delta Chat itself has no problem with encrypted multipart/report
/// parts and MUAs should be encouraged to encrpyt multipart/reports as well so
/// that we could use the normal Autocrypt processing.
fn contains_report(mail: &ParsedMail<'_>) -> bool {
fn contains_report(mail: &mailparse::ParsedMail<'_>) -> bool {
mail.ctype.mimetype == "multipart/report"
}

View File

@@ -1,5 +1,6 @@
//! # Error handling
use failure::Fail;
use lettre_email::mime;
#[derive(Debug, Fail)]
@@ -28,14 +29,12 @@ pub enum Error {
InvalidMsgId,
#[fail(display = "Watch folder not found {:?}", _0)]
WatchFolderNotFound(String),
#[fail(display = "Invalid Email: {:?}", _0)]
#[fail(display = "Inalid Email: {:?}", _0)]
MailParseError(#[cause] mailparse::MailParseError),
#[fail(display = "Building invalid Email: {:?}", _0)]
LettreError(#[cause] lettre_email::error::Error),
#[fail(display = "FromStr error: {:?}", _0)]
FromStr(#[cause] mime::FromStrError),
#[fail(display = "Not Configured")]
NotConfigured,
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -1,58 +0,0 @@
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames)]
#[strum(serialize_all = "kebab_case")]
#[allow(dead_code)]
pub enum HeaderDef {
MessageId,
Subject,
Date,
From_,
To,
Cc,
Disposition,
OriginalMessageId,
ListId,
References,
InReplyTo,
Precedence,
ChatVersion,
ChatGroupId,
ChatGroupName,
ChatGroupNameChanged,
ChatVerified,
ChatGroupImage, // deprecated
ChatGroupAvatar,
ChatUserAvatar,
ChatVoiceMessage,
ChatGroupMemberRemoved,
ChatGroupMemberAdded,
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
AutocryptSetupMessage,
SecureJoin,
SecureJoinGroup,
SecureJoinFingerprint,
SecureJoinInvitenumber,
SecureJoinAuth,
_TestHeader,
}
impl HeaderDef {
/// Returns the corresponding Event id.
pub fn get_headername(&self) -> String {
self.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
/// Test that kebab_case serialization works as expected
fn kebab_test() {
assert_eq!(HeaderDef::From_.to_string(), "from");
assert_eq!(HeaderDef::_TestHeader.to_string(), "test-header");
}
}

View File

@@ -1,273 +0,0 @@
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 IDLE protocol timed out")]
IdleTimeout(#[cause] async_std::future::TimeoutError),
#[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");
}
}
}
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
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");
}
}
}
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
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_or_default()
.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)
}
});
}
}

View File

@@ -4,11 +4,14 @@
//! 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,
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
extensions::idle::IdleResponse,
types::{Fetch, Flag, Mailbox, Name, NameAttribute},
};
use async_std::prelude::*;
use async_std::sync::{Mutex, RwLock};
use async_std::task;
@@ -23,8 +26,8 @@ use crate::message::{self, update_server_uid};
use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::stock::StockMessage;
use crate::wrapmime;
mod idle;
pub mod select_folder;
const DC_IMAP_SEEN: usize = 0x0001;
@@ -48,24 +51,27 @@ pub enum Error {
#[fail(display = "IMAP Could not login as {}", _0)]
LoginFailed(String),
#[fail(display = "IMAP Could not fetch")]
#[fail(display = "IMAP Could not fetch {}", _0)]
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")]
#[fail(display = "IMAP got error from elsewhere: {:?}", _0)]
WrappedError(#[cause] crate::error::Error),
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[fail(display = "No mailbox selected, folder: {:?}", _0)]
NoMailbox(String),
#[fail(display = "IMAP other error: {:?}", _0)]
Other(String),
}
@@ -195,107 +201,112 @@ impl Imap {
self.should_reconnect.store(true, Ordering::Relaxed)
}
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(());
}
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;
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
} else {
return Err(Error::OauthError);
}
} else {
client.login(imap_user, imap_pw).await
}
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);
}
Err(err) => {
let message = {
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 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;
context.stock_string_repl_str2(
StockMessage::ServerResponse,
format!("{}:{}", imap_server, imap_port),
err.to_string(),
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
};
// 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);
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) {
@@ -331,7 +342,9 @@ impl Imap {
/// Connects to imap account using already-configured parameters.
pub fn connect_configured(&self, context: &Context) -> Result<()> {
if async_std::task::block_on(self.is_connected()) && !self.should_reconnect() {
if async_std::task::block_on(async move {
self.is_connected().await && !self.should_reconnect()
}) {
return Ok(());
}
if !context.sql.get_raw_config_bool(context, "configured") {
@@ -341,7 +354,7 @@ impl Imap {
let param = LoginParam::from_database(context, "configured_");
// the trailing underscore is correct
if task::block_on(self.connect(context, &param)) {
if self.connect(context, &param) {
self.ensure_configured_folders(context, true)
} else {
Err(Error::ConnectionFailed(format!("{}", param)))
@@ -350,80 +363,77 @@ impl Imap {
/// tries connecting to imap account using the specific login
/// parameters
pub async fn connect(&self, context: &Context, lp: &LoginParam) -> bool {
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
return false;
}
pub fn connect(&self, context: &Context, lp: &LoginParam) -> bool {
task::block_on(async move {
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
return false;
}
{
let addr = &lp.addr;
let imap_server = &lp.mail_server;
let imap_port = lp.mail_port as u16;
let imap_user = &lp.mail_user;
let imap_pw = &lp.mail_pw;
let server_flags = lp.server_flags as usize;
{
let addr = &lp.addr;
let imap_server = &lp.mail_server;
let imap_port = lp.mail_port as u16;
let imap_user = &lp.mail_user;
let imap_pw = &lp.mail_pw;
let server_flags = lp.server_flags as usize;
let mut config = self.config.write().await;
config.addr = addr.to_string();
config.imap_server = imap_server.to_string();
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
config.certificate_checks = lp.imap_certificate_checks;
config.server_flags = server_flags;
}
let mut config = self.config.write().await;
config.addr = addr.to_string();
config.imap_server = imap_server.to_string();
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
config.certificate_checks = lp.imap_certificate_checks;
config.server_flags = server_flags;
}
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;
}
if let Err(err) = self.setup_handle_if_needed(context) {
warn!(context, "failed to setup imap handle: {}", err);
self.free_connect_params().await;
return false;
}
let teardown = match &mut *self.session.lock().await {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open() {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,);
true
} else {
let can_idle = caps.has_str("IDLE");
let has_xlist = caps.has_str("XLIST");
let caps_list = caps.iter().fold(String::new(), |s, c| {
if let Capability::Atom(x) = c {
s + &format!(" {}", x)
} else {
s + &format!(" {:?}", c)
}
});
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
Event::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.mail_user, caps_list,
))
);
false
let teardown = match &mut *self.session.lock().await {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open() {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,);
true
} else {
let can_idle = caps.has_str("IDLE");
let has_xlist = caps.has_str("XLIST");
let caps_list = caps
.iter()
.fold(String::new(), |s, c| s + &format!(" {:?}", c));
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
Event::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.mail_user, caps_list,
))
);
false
}
}
}
Err(err) => {
info!(context, "CAPABILITY command error: {}", err);
true
}
},
None => true,
};
Err(err) => {
info!(context, "CAPABILITY command error: {}", err);
true
}
},
None => true,
};
if teardown {
self.disconnect(context);
if teardown {
self.disconnect(context);
false
} else {
true
}
false
} else {
true
}
})
}
pub fn disconnect(&self, context: &Context) {
@@ -433,17 +443,20 @@ impl Imap {
});
}
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(())
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(())
})
}
fn get_config_last_seen_uid<S: AsRef<str>>(&self, context: &Context, folder: S) -> (u32, u32) {
@@ -481,10 +494,7 @@ impl Imap {
let (uid_validity, last_seen_uid) = self.get_config_last_seen_uid(context, &folder);
let config = self.config.read().await;
let mailbox = config
.selected_mailbox
.as_ref()
.ok_or_else(|| Error::NoMailbox(folder.to_string()))?;
let mailbox = config.selected_mailbox.as_ref().expect("just selected");
let new_uid_validity = match mailbox.uid_validity {
Some(v) => v,
@@ -550,7 +560,7 @@ impl Imap {
})
}
async fn fetch_new_messages<S: AsRef<str>>(
async fn fetch_from_single_folder<S: AsRef<str>>(
&self,
context: &Context,
folder: S,
@@ -583,11 +593,9 @@ impl Imap {
for msg in &list {
let cur_uid = msg.uid.unwrap_or_default();
if cur_uid <= last_seen_uid {
// seems that at least dovecot sends the last available UID
// even if we asked for higher UID+N:*
info!(
warn!(
context,
"fetch_new_messages: ignoring uid {}, last seen was {}", cur_uid, last_seen_uid
"unexpected uid {}, last seen was {}", cur_uid, last_seen_uid
);
continue;
}
@@ -719,23 +727,217 @@ impl Imap {
if !is_deleted && msg.body().is_some() {
let body = msg.body().unwrap_or_default();
if let Err(err) =
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32)
{
warn!(
context,
"dc_receive_imf failed for imap-message {}/{}: {:?}",
folder.as_ref(),
server_uid,
err
);
}
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32);
}
}
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,
@@ -1120,17 +1322,13 @@ 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
@@ -1228,18 +1426,6 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
}
}
fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> {
let value = std::str::from_utf8(message_id)?;
let addrs = mailparse::addrparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
if let Some(info) = addrs.extract_single_info() {
return Ok(info.addr);
}
bail!("could not parse message_id: {}", value);
}
fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
if prefetch_msg.envelope().is_none() {
return Err(Error::Other(
@@ -1252,22 +1438,5 @@ fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
return Err(Error::Other("prefetch: No message ID found".to_string()));
}
parse_message_id(&message_id.unwrap()).map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_message_id() {
assert_eq!(
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
assert_eq!(
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
}
wrapmime::parse_message_id(&message_id.unwrap()).map_err(Into::into)
}

View File

@@ -34,7 +34,6 @@ 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);
}
@@ -62,7 +61,6 @@ impl Imap {
info!(context, "close/expunge succeeded");
}
Err(err) => {
self.trigger_reconnect();
return Err(Error::CloseExpungeFailed(err));
}
}

View File

@@ -4,11 +4,12 @@ use async_imap::{
types::{Capabilities, Fetch, Mailbox, Name},
Client as ImapClient, Session as ImapSession,
};
use async_native_tls::TlsStream;
use async_std::net::{self, TcpStream};
use async_std::prelude::*;
use async_std::sync::Arc;
use async_tls::client::TlsStream;
use crate::login_param::{dc_build_tls, CertificateChecks};
use crate::login_param::{dc_build_tls_config, CertificateChecks};
#[derive(Debug)]
pub(crate) enum Client {
@@ -35,9 +36,9 @@ impl Client {
certificate_checks: CertificateChecks,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks)?;
let tls_connector: async_native_tls::TlsConnector = tls.into();
let tls_stream = tls_connector.connect(domain.as_ref(), stream).await?;
let tls_config = dc_build_tls_config(certificate_checks);
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(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
@@ -46,7 +47,7 @@ impl Client {
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
.expect("failed to read greeting");
Ok(Client::Secure(client))
}
@@ -61,7 +62,7 @@ impl Client {
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
.expect("failed to read greeting");
Ok(Client::Insecure(client))
}
@@ -73,10 +74,10 @@ impl Client {
) -> ImapResult<Client> {
match self {
Client::Insecure(client) => {
let tls = dc_build_tls(certificate_checks)?;
let tls_stream = tls.into();
let tls_config = dc_build_tls_config(certificate_checks);
let tls: async_tls::TlsConnector = Arc::new(tls_config).into();
let client_sec = client.secure(domain, &tls_stream).await?;
let client_sec = client.secure(domain, &tls).await?;
Ok(Client::Secure(client_sec))
}

View File

@@ -380,7 +380,7 @@ pub fn JobImexImap(context: &Context, job: &Job) -> Result<()> {
context.free_ongoing();
bail!("Cannot create private key or private key not available.");
} else {
dc_create_folder(context, &param)?;
dc_create_folder(context, &param);
}
}
let path = Path::new(param);

View File

@@ -8,8 +8,6 @@ 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;
@@ -167,15 +165,13 @@ impl Job {
if let Some(recipients) = self.param.get(Param::Recipients) {
let recipients_list = recipients
.split('\x1e')
.filter_map(
|addr| match async_smtp::EmailAddress::new(addr.to_string()) {
Ok(addr) => Some(addr),
Err(err) => {
warn!(context, "invalid recipient: {} {:?}", addr, err);
None
}
},
)
.filter_map(|addr| match lettre::EmailAddress::new(addr.to_string()) {
Ok(addr) => Some(addr),
Err(err) => {
warn!(context, "invalid recipient: {} {:?}", addr, err);
None
}
})
.collect::<Vec<_>>();
/* if there is a msg-id and it does not exist in the db, cancel sending.
@@ -200,19 +196,11 @@ impl Job {
info!(context, "smtp-sending out mime message:");
println!("{}", String::from_utf8_lossy(&body));
}
match task::block_on(smtp.send(context, recipients_list, body, self.job_id)) {
Err(crate::smtp::send::Error::SendTimeout(err)) => {
warn!(context, "SMTP send timed out {:?}", err);
smtp.disconnect();
self.try_again_later(
TryAgain::AtOnce,
Some("send-timeout".to_string()),
);
}
match smtp.send(context, recipients_list, body, self.job_id) {
Err(crate::smtp::send::Error::SendError(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {}", err);
smtp.disconnect();
info!(context, "SMTP failed to send: {}", err);
self.try_again_later(TryAgain::AtOnce, Some(err.to_string()));
}
Err(crate::smtp::send::Error::EnvelopeError(err)) => {
@@ -410,37 +398,31 @@ 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);
task::block_on(
context
.inbox_thread
.write()
.unwrap()
.fetch(context, use_network),
);
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);
task::block_on(
context
.mvbox_thread
.write()
.unwrap()
.fetch(context, use_network),
);
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);
task::block_on(
context
.sentbox_thread
.write()
.unwrap()
.fetch(context, use_network),
);
context
.sentbox_thread
.write()
.unwrap()
.fetch(context, use_network);
}
pub fn perform_inbox_idle(context: &Context) {
@@ -480,19 +462,19 @@ pub fn perform_sentbox_idle(context: &Context) {
.idle(context, use_network);
}
pub fn interrupt_inbox_idle(context: &Context) {
info!(context, "interrupt_inbox_idle called");
// we do not block on trying to obtain the thread lock
// because we don't know in which state the thread is.
// If it's currently fetching then we can not get the lock
// but we flag it for checking jobs so that idle will be skipped.
match context.inbox_thread.try_read() {
Ok(inbox_thread) => {
inbox_thread.interrupt_idle(context);
}
Err(err) => {
*context.perform_inbox_jobs_needed.write().unwrap() = true;
warn!(context, "could not interrupt idle: {}", err);
pub fn interrupt_inbox_idle(context: &Context, block: bool) {
info!(context, "interrupt_inbox_idle called blocking={}", block);
if block {
context.inbox_thread.read().unwrap().interrupt_idle(context);
} else {
match context.inbox_thread.try_read() {
Ok(inbox_thread) => {
inbox_thread.interrupt_idle(context);
}
Err(err) => {
*context.perform_inbox_jobs_needed.write().unwrap() = true;
warn!(context, "could not interrupt idle: {}", err);
}
}
}
}
@@ -604,7 +586,7 @@ pub fn maybe_network(context: &Context) {
}
interrupt_smtp_idle(context);
interrupt_inbox_idle(context);
interrupt_inbox_idle(context, true);
interrupt_mvbox_idle(context);
interrupt_sentbox_idle(context);
}
@@ -639,23 +621,15 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> {
msg.try_calc_and_set_dimensions(context).ok();
/* create message */
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
let needs_encryption = msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default();
let attach_selfavatar = match chat::shall_attach_selfavatar(context, msg.chat_id) {
Ok(attach_selfavatar) => attach_selfavatar,
Err(err) => {
warn!(context, "job: cannot get selfavatar-state: {}", err);
false
}
};
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar)?;
let mimefactory = MimeFactory::from_msg(context, &msg)?;
let mut rendered_msg = mimefactory.render().map_err(|err| {
message::set_msg_failed(context, msg_id, Some(err.to_string()));
err
})?;
if needs_encryption && !rendered_msg.is_encrypted {
if 0 != needs_encryption && !rendered_msg.is_encrypted {
/* unrecoverable */
message::set_msg_failed(
context,
@@ -686,9 +660,8 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> {
}
if rendered_msg.is_gossiped {
chat::set_gossiped_timestamp(context, msg.chat_id, time())?;
chat::set_gossiped_timestamp(context, msg.chat_id, time());
}
if 0 != rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()) {
error!(context, "Failed to set kml sent_timestamp: {:?}", err);
@@ -701,14 +674,7 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> {
}
}
}
if attach_selfavatar {
if let Err(err) = chat::set_selfavatar_timestamp(context, msg.chat_id, time()) {
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
}
}
if rendered_msg.is_encrypted && !needs_encryption {
if rendered_msg.is_encrypted && needs_encryption == 0 {
msg.param.set_int(Param::GuaranteeE2ee, 1);
msg.save_param_to_disk(context);
}
@@ -972,7 +938,7 @@ pub fn job_add(
).ok();
match thread {
Thread::Imap => interrupt_inbox_idle(context),
Thread::Imap => interrupt_inbox_idle(context, false),
Thread::Smtp => interrupt_smtp_idle(context),
Thread::Unknown => {}
}

View File

@@ -73,7 +73,7 @@ impl JobThread {
info!(context, "Interrupting {}-IDLE... finished", self.name);
}
pub async fn fetch(&mut self, context: &Context, use_network: bool) {
pub 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).await {
if let Err(err) = self.connect_and_fetch(context) {
warn!(context, "connect+fetch failed: {}, reconnect & retry", err);
self.imap.trigger_reconnect();
if let Err(err) = self.connect_and_fetch(context).await {
if let Err(err) = self.connect_and_fetch(context) {
warn!(context, "connect+fetch failed: {}", err);
}
}
@@ -97,18 +97,14 @@ impl JobThread {
self.state.0.lock().unwrap().using_handle = false;
}
async fn connect_and_fetch(&mut self, context: &Context) -> Result<()> {
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)
.await
.map_err(Into::into);
let res = self.imap.fetch(context, &watch_folder).map_err(Into::into);
let elapsed = start.elapsed().as_millis();
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
@@ -174,19 +170,20 @@ impl JobThread {
let prefix = format!("{}-IDLE", self.name);
let do_fake_idle = match self.imap.connect_configured(context) {
Ok(()) => {
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 {
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) => {
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) => {

View File

@@ -178,9 +178,20 @@ impl Key {
}
}
pub fn to_base64(&self) -> String {
pub fn to_base64(&self, break_every: usize) -> String {
let buf = self.to_bytes();
base64::encode(&buf)
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
})
}
pub fn to_armored_string(

View File

@@ -1,9 +1,18 @@
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
// for now we hide warnings to not clutter/hide errors during "cargo clippy"
#![allow(clippy::cognitive_complexity, clippy::too_many_arguments)]
#![allow(clippy::match_bool)]
#![allow(
clippy::type_complexity,
clippy::cognitive_complexity,
clippy::too_many_arguments,
clippy::block_in_if_condition_stmt,
clippy::large_enum_variant
)]
#![allow(
clippy::unreadable_literal,
clippy::needless_range_loop,
clippy::match_bool
)]
#![feature(ptr_wrapping_offset_from)]
#![feature(drain_filter)]
#[macro_use]
extern crate failure_derive;
@@ -20,12 +29,10 @@ extern crate strum_macros;
extern crate debug_stub_derive;
#[macro_use]
pub mod log;
mod log;
#[macro_use]
pub mod error;
pub mod headerdef;
pub(crate) mod events;
pub use events::*;
@@ -63,8 +70,10 @@ pub mod sql;
pub mod stock;
mod token;
#[macro_use]
mod wrapmime;
mod dehtml;
pub mod dc_array;
pub mod dc_receive_imf;
mod dc_simplify;
pub mod dc_tools;

View File

@@ -7,13 +7,7 @@ macro_rules! info {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
let thread = ::std::thread::current();
let full = format!("{thid:?} {file}:{line}: {msg}",
thid = thread.id(),
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::Event::Info(full));
emit_event!($ctx, $crate::Event::Info(formatted));
}};
}
@@ -24,13 +18,7 @@ macro_rules! warn {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
let thread = ::std::thread::current();
let full = format!("{thid:?} {file}:{line}: {msg}",
thid = thread.id(),
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::Event::Warning(full));
emit_event!($ctx, $crate::Event::Warning(formatted));
}};
}

View File

@@ -4,6 +4,11 @@ use std::borrow::Cow;
use std::fmt;
use crate::context::Context;
use crate::error::Error;
use async_std::sync::Arc;
use rustls;
use webpki;
use webpki_roots;
#[derive(Copy, Clone, Debug, Display, FromPrimitive)]
#[repr(i32)]
@@ -11,11 +16,7 @@ use crate::context::Context;
pub enum CertificateChecks {
Automatic = 0,
Strict = 1,
/// Same as AcceptInvalidCertificates
/// Previously known as AcceptInvalidHostnames, now deprecated.
AcceptInvalidCertificates2 = 2,
AcceptInvalidHostnames = 2,
AcceptInvalidCertificates = 3,
}
@@ -129,7 +130,7 @@ impl LoginParam {
&self,
context: &Context,
prefix: impl AsRef<str>,
) -> crate::sql::Result<()> {
) -> Result<(), Error> {
let prefix = prefix.as_ref();
let sql = &context.sql;
@@ -258,25 +259,49 @@ fn get_readable_flags(flags: i32) -> String {
res
}
pub fn dc_build_tls(
certificate_checks: CertificateChecks,
) -> Result<native_tls::TlsConnector, native_tls::Error> {
let mut tls_builder = native_tls::TlsConnector::builder();
pub struct NoCertificateVerification {}
impl rustls::ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
&self,
_roots: &rustls::RootCertStore,
_presented_certs: &[rustls::Certificate],
_dns_name: webpki::DNSNameRef<'_>,
_ocsp: &[u8],
) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
Ok(rustls::ServerCertVerified::assertion())
}
}
pub fn dc_build_tls_config(certificate_checks: CertificateChecks) -> rustls::ClientConfig {
let mut config = rustls::ClientConfig::new();
config
.root_store
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
match certificate_checks {
CertificateChecks::Strict => {}
CertificateChecks::Automatic => {
// Same as AcceptInvalidCertificates for now.
// TODO: use provider database when it becomes available
tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
}
CertificateChecks::AcceptInvalidCertificates => {
// TODO: only accept invalid certs
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
}
CertificateChecks::AcceptInvalidHostnames => {
// TODO: only accept invalid hostnames
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
}
CertificateChecks::Strict => &mut tls_builder,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true),
}
.build()
config
}
#[cfg(test)]
@@ -288,8 +313,8 @@ mod tests {
use std::string::ToString;
assert_eq!(
"accept_invalid_certificates".to_string(),
CertificateChecks::AcceptInvalidCertificates.to_string()
"accept_invalid_hostnames".to_string(),
CertificateChecks::AcceptInvalidHostnames.to_string()
);
}
}

View File

@@ -720,7 +720,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
return ret;
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = dc_truncate(rawtxt.trim(), 100_000, false);
let rawtxt = dc_truncate(rawtxt.trim(), 100000, false);
let fts = dc_timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {}", fts);

View File

@@ -1,7 +1,6 @@
use chrono::TimeZone;
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
use crate::blob::BlobObject;
use crate::chat::{self, Chat};
use crate::config::Config;
use crate::constants::*;
@@ -42,7 +41,6 @@ pub struct MimeFactory<'a, 'b> {
pub req_mdn: bool,
pub context: &'a Context,
last_added_location_id: u32,
attach_selfavatar: bool,
}
/// Result of rendering a message, ready to be submitted to a send job.
@@ -64,11 +62,7 @@ pub struct RenderedEmail {
}
impl<'a, 'b> MimeFactory<'a, 'b> {
pub fn from_msg(
context: &'a Context,
msg: &'b Message,
add_selfavatar: bool,
) -> Result<MimeFactory<'a, 'b>, Error> {
pub fn from_msg(context: &'a Context, msg: &'b Message) -> Result<MimeFactory<'a, 'b>, Error> {
let chat = Chat::load_from_db(context, msg.chat_id)?;
let mut factory = MimeFactory {
@@ -90,7 +84,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
references: String::default(),
req_mdn: false,
last_added_location_id: 0,
attach_selfavatar: add_selfavatar,
context,
};
@@ -159,10 +152,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let in_reply_to: String = row.get(0)?;
let references: String = row.get(1)?;
Ok((
render_rfc724_mid_list(&in_reply_to),
render_rfc724_mid_list(&references),
))
Ok((in_reply_to, references))
},
);
@@ -217,7 +207,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
references: String::default(),
req_mdn: false,
last_added_location_id: 0,
attach_selfavatar: false,
})
}
@@ -304,8 +293,9 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Loaded::Message => {
let chat = self.chat.as_ref().unwrap();
// beside key- and member-changes, force re-gossip every 48 hours
let gossiped_timestamp = chat.get_gossiped_timestamp(self.context);
if gossiped_timestamp == 0 || (gossiped_timestamp + (2 * 24 * 60 * 60)) > time() {
if chat.gossiped_timestamp == 0
|| (chat.gossiped_timestamp + (2 * 24 * 60 * 60)) > time()
{
return true;
}
@@ -331,15 +321,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
_ => {}
}
if self
.msg
.param
.get_bool(Param::AttachGroupImage)
.unwrap_or_default()
{
return chat.param.get(Param::ProfileImage).map(Into::into);
}
None
}
Loaded::MDN => None,
@@ -401,7 +382,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let mut unprotected_headers: Vec<Header> = Vec::new();
let from = Address::new_mailbox_with_name(
self.from_displayname.to_string(),
encode_words(&self.from_displayname),
self.from_addr.clone(),
);
@@ -413,7 +394,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
to.push(Address::new_mailbox(addr.clone()));
} else {
to.push(Address::new_mailbox_with_name(
name.to_string(),
encode_words(name),
addr.clone(),
));
}
@@ -462,6 +443,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
let min_verified = self.min_verified();
let do_gossip = self.should_do_gossip();
let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext();
let subject_str = self.subject_str();
@@ -495,31 +477,19 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Loaded::MDN => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
};
// we could also store the message-id in the protected headers
// which would probably help to survive providers like
// Outlook.com or hotmail which mangle the Message-ID.
// but they also strip the Autocrypt header so we probably
// never get a chance to tunnel our protected headers in a
// cryptographic payload.
unprotected_headers.push(Header::new(
"Message-ID".into(),
render_rfc724_mid(&rfc724_mid),
));
protected_headers.push(Header::new("Message-ID".into(), rfc724_mid.clone()));
unprotected_headers.push(Header::new_with_value("To".into(), to).unwrap());
unprotected_headers.push(Header::new_with_value("From".into(), vec![from]).unwrap());
let mut is_gossiped = false;
let outer_message = if is_encrypted {
// Add gossip headers in chats with multiple recipients
if peerstates.len() > 1 && self.should_do_gossip() {
// Add gossip headers
if do_gossip {
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if peerstate.peek_key(min_verified).is_some() {
if let Some(header) = peerstate.render_gossip_header(min_verified) {
message =
message.header(Header::new("Autocrypt-Gossip".into(), header));
is_gossiped = true;
}
}
}
@@ -555,12 +525,6 @@ 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)?;
@@ -600,6 +564,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
message
};
let is_gossiped = is_encrypted && do_gossip && !peerstates.is_empty();
let MimeFactory {
recipients_addr,
from_addr,
@@ -636,7 +602,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let command = self.msg.param.get_cmd();
let mut placeholdertext = None;
let mut meta_part = None;
let mut add_compatibility_header = false;
if chat.typ == Chattype::VerifiedGroup {
protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
@@ -677,7 +642,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
"vg-member-added".to_string(),
));
}
add_compatibility_header = true;
}
SystemMessage::GroupNameChanged => {
let value_to_add = self.msg.param.get(Param::Arg).unwrap_or_default();
@@ -688,17 +652,10 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
));
}
SystemMessage::GroupImageChanged => {
protected_headers.push(Header::new(
"Chat-Content".to_string(),
"group-avatar-changed".to_string(),
));
if grpimage.is_none() {
protected_headers.push(Header::new(
"Chat-Group-Avatar".to_string(),
"0".to_string(),
));
protected_headers
.push(Header::new("Chat-Group-Image".to_string(), "0".to_string()));
}
add_compatibility_header = true;
}
_ => {}
}
@@ -766,18 +723,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image")?;
meta_part = Some(mail);
protected_headers.push(Header::new(
"Chat-Group-Avatar".into(),
filename_as_sent.clone(),
));
// add the old group-image headers for versions <=0.973 resp. <=beta.15 (december 2019)
// image deletion is not supported in the compatibility layer.
// this can be removed some time after releasing 1.0,
// grep for #DeprecatedAvatar to get the place where compatibility parsing takes place.
if add_compatibility_header {
protected_headers.push(Header::new("Chat-Group-Image".into(), filename_as_sent));
}
protected_headers.push(Header::new("Chat-Group-Image".into(), filename_as_sent));
}
if self.msg.type_0 == Viewtype::Sticker {
@@ -909,19 +855,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
if self.attach_selfavatar {
match context.get_config(Config::Selfavatar) {
Some(path) => match build_selfavatar_file(context, path) {
Ok((part, filename)) => {
parts.push(part);
protected_headers.push(Header::new("Chat-User-Avatar".into(), filename))
}
Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err),
},
None => protected_headers.push(Header::new("Chat-User-Avatar".into(), "0".into())),
}
}
// Single part, render as regular message.
if parts.len() == 1 {
return Ok(parts.pop().unwrap());
@@ -1002,21 +935,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
/// Returns base64-encoded buffer `buf` split into 78-bytes long
/// chunks separated by CRLF.
///
/// This line length limit is an
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
fn wrapped_base64_encode(buf: &[u8]) -> String {
let base64 = base64::encode(&buf);
let mut chars = base64.chars();
(0..)
.map(|_| chars.by_ref().take(78).collect::<String>())
.take_while(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("\r\n")
}
fn build_body_file(
context: &Context,
msg: &Message,
@@ -1077,7 +995,7 @@ fn build_body_file(
};
let body = std::fs::read(blob.to_abs_path())?;
let encoded_body = wrapped_base64_encode(&body);
let encoded_body = base64::encode(&body);
let mail = PartBuilder::new()
.content_type(&mimetype)
@@ -1088,31 +1006,6 @@ fn build_body_file(
Ok((mail, filename_to_send))
}
fn build_selfavatar_file(context: &Context, path: String) -> Result<(PartBuilder, String), Error> {
let blob = BlobObject::from_path(context, path)?;
let filename_to_send = match blob.suffix() {
Some(suffix) => format!("avatar.{}", suffix),
None => "avatar".to_string(),
};
let mimetype = match message::guess_msgtype_from_suffix(blob.as_rel_path()) {
Some(res) => res.1.parse()?,
None => mime::APPLICATION_OCTET_STREAM,
};
let body = std::fs::read(blob.to_abs_path())?;
let encoded_body = wrapped_base64_encode(&body);
let part = PartBuilder::new()
.content_type(&mimetype)
.header((
"Content-Disposition",
format!("attachment; filename=\"{}\"", &filename_to_send),
))
.header(("Content-Transfer-Encoding", "base64"))
.body(encoded_body);
Ok((part, filename_to_send))
}
pub(crate) fn vec_contains_lowercase(vec: &[String], part: &str) -> bool {
let partlc = part.to_lowercase();
for cur in vec.iter() {
@@ -1133,25 +1026,6 @@ fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
}
}
fn render_rfc724_mid(rfc724_mid: &str) -> String {
let rfc724_mid = rfc724_mid.trim().to_string();
if rfc724_mid.chars().nth(0).unwrap_or_default() == '<' {
rfc724_mid
} else {
format!("<{}>", rfc724_mid)
}
}
fn render_rfc724_mid_list(mid_list: &str) -> String {
mid_list
.trim()
.split_ascii_whitespace()
.map(render_rfc724_mid)
.collect::<Vec<String>>()
.join(" ")
}
/* ******************************************************************************
* Encode/decode header words, RFC 2047
******************************************************************************/
@@ -1171,60 +1045,3 @@ pub fn needs_encoding(to_check: impl AsRef<str>) -> bool {
!c.is_ascii_alphanumeric() && c != '-' && c != '_' && c != '.' && c != '~' && c != '%'
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_email_address() {
let display_name = "ä space";
let addr = "x@y.org";
assert!(!display_name.is_ascii());
let s = format!(
"{}",
Address::new_mailbox_with_name(display_name.to_string(), addr.to_string())
);
println!("{}", s);
assert_eq!(s, "=?utf-8?q?=C3=A4_space?= <x@y.org>");
}
#[test]
fn test_render_rfc724_mid() {
assert_eq!(
render_rfc724_mid("kqjwle123@qlwe"),
"<kqjwle123@qlwe>".to_string()
);
assert_eq!(
render_rfc724_mid(" kqjwle123@qlwe "),
"<kqjwle123@qlwe>".to_string()
);
assert_eq!(
render_rfc724_mid("<kqjwle123@qlwe>"),
"<kqjwle123@qlwe>".to_string()
);
}
#[test]
fn test_render_rc724_mid_list() {
assert_eq!(render_rfc724_mid_list("123@q "), "<123@q>".to_string());
assert_eq!(render_rfc724_mid_list(" 123@q "), "<123@q>".to_string());
assert_eq!(
render_rfc724_mid_list("123@q 456@d "),
"<123@q> <456@d>".to_string()
);
}
#[test]
fn test_wrapped_base64_encode() {
let input = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
let output =
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU\r\n\
FBQUFBQUFBQQ==";
assert_eq!(wrapped_base64_encode(input), output);
}
}

View File

@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
use deltachat_derive::{FromSql, ToSql};
use lettre_email::mime::{self, Mime};
use mailparse::{DispositionType, MailAddr, MailHeaderMap};
use mailparse::MailHeaderMap;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
@@ -14,52 +14,35 @@ use crate::dc_simplify::*;
use crate::dc_tools::*;
use crate::e2ee;
use crate::error::Result;
use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::job::{job_add, Action};
use crate::location;
use crate::message;
use crate::message::MsgId;
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> {
pub context: &'a Context,
pub parts: Vec<Part>,
header: HashMap<String, String>,
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>,
pub gossipped_addr: HashSet<String>,
pub is_forwarded: bool,
pub is_system_message: SystemMessage,
pub location_kml: Option<location::Kml>,
pub message_kml: Option<location::Kml>,
pub user_avatar: AvatarAction,
pub group_avatar: AvatarAction,
reports: Vec<Report>,
mdns_enabled: bool,
parsed_protected_headers: bool,
}
#[derive(Debug, PartialEq)]
pub enum AvatarAction {
None,
Delete,
Change(String),
}
impl AvatarAction {
pub fn is_change(&self) -> bool {
match self {
AvatarAction::None => false,
AvatarAction::Delete => false,
AvatarAction::Change(_) => true,
}
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[repr(i32)]
pub enum SystemMessage {
@@ -85,13 +68,15 @@ const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
impl<'a> MimeParser<'a> {
pub fn from_bytes(context: &'a Context, body: &[u8]) -> Result<Self> {
let mail = mailparse::parse_mail(body)?;
let mdns_enabled = context.get_config_bool(Config::MdnsEnabled);
let mut parser = MimeParser {
parts: Vec::new(),
header: Default::default(),
subject: None,
is_send_by_messenger: false,
decrypting_failed: false,
// only non-empty if it was a valid autocrypt message
encrypted: false,
signatures: Default::default(),
gossipped_addr: Default::default(),
is_forwarded: false,
@@ -100,8 +85,7 @@ impl<'a> MimeParser<'a> {
is_system_message: SystemMessage::Unknown,
location_kml: None,
message_kml: None,
user_avatar: AvatarAction::None,
group_avatar: AvatarAction::None,
mdns_enabled,
parsed_protected_headers: false,
};
@@ -111,8 +95,7 @@ impl<'a> MimeParser<'a> {
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
// init known headers with what mailparse provided us
parser.merge_headers(&mail.headers);
parser.hash_header(&mail.headers);
// Memory location for a possible decrypted message.
let mail_raw;
@@ -120,15 +103,12 @@ impl<'a> MimeParser<'a> {
let mail = match e2ee::try_decrypt(parser.context, &mail, message_time) {
Ok((raw, signatures)) => {
// Valid autocrypt message, encrypted
parser.encrypted = raw.is_some();
parser.signatures = signatures;
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
@@ -137,10 +117,6 @@ impl<'a> MimeParser<'a> {
parser.gossipped_addr =
update_gossip_peerstates(context, message_time, &mail, gossip_headers)?;
// let known protected headers from the decrypted
// part override the unencrypted top-level
parser.merge_headers(&decrypted_mail.headers);
decrypted_mail
} else {
// Message was not encrypted
@@ -168,35 +144,61 @@ impl<'a> MimeParser<'a> {
}
fn parse_headers(&mut self) -> Result<()> {
if self.get(HeaderDef::AutocryptSetupMessage).is_some() {
self.parts.drain_filter(|part| {
part.mimetype.is_some()
&& part.mimetype.as_ref().unwrap().as_ref() != MIME_AC_SETUP_FILE
if let Some(field) = self.lookup_field("Subject") {
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
});
if self.parts.len() == 1 {
if has_setup_file {
self.is_system_message = SystemMessage::AutocryptSetupMessage;
} else {
warn!(self.context, "could not determine ASM mime-part");
// TODO: replace the following code with this
// once drain_filter stabilizes.
//
// See https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain_filter
// and https://github.com/rust-lang/rust/issues/43244
//
// mimeparser
// .parts
// .drain_filter(|part| part.int_mimetype != 111)
// .for_each(|part| dc_mimepart_unref(part));
let mut i = 0;
while i != self.parts.len() {
let mimetype = &self.parts[i].mimetype;
if mimetype.is_none()
|| mimetype.as_ref().unwrap().as_ref() != MIME_AC_SETUP_FILE
{
self.parts.remove(i);
} else {
i += 1;
}
}
}
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
} else if let Some(value) = self.lookup_field("Chat-Content") {
if value == "location-streaming-enabled" {
self.is_system_message = SystemMessage::LocationStreamingEnabled;
}
}
if let Some(header_value) = self.get(HeaderDef::ChatGroupAvatar).cloned() {
self.group_avatar = self.avatar_action_from_header(header_value);
} else if let Some(header_value) = self.get(HeaderDef::ChatGroupImage).cloned() {
// parse the old group-image headers for versions <=0.973 resp. <=beta.15 (december 2019)
// grep for #DeprecatedAvatar to get the place where a compatibility header is generated.
self.group_avatar = self.avatar_action_from_header(header_value);
if self.lookup_field("Chat-Group-Image").is_some() && !self.parts.is_empty() {
let textpart = &self.parts[0];
if textpart.typ == Viewtype::Text && self.parts.len() >= 2 {
let imgpart = &mut self.parts[1];
if imgpart.typ == Viewtype::Image {
imgpart.is_meta = true;
}
}
}
if let Some(header_value) = self.get(HeaderDef::ChatUserAvatar).cloned() {
self.user_avatar = self.avatar_action_from_header(header_value);
}
if self.has_chat_version() && self.parts.len() == 2 {
if self.is_send_by_messenger && self.parts.len() == 2 {
let need_drop = {
let textpart = &self.parts[0];
let filepart = &self.parts[1];
@@ -208,6 +210,7 @@ impl<'a> MimeParser<'a> {
|| filepart.typ == Viewtype::Voice
|| filepart.typ == Viewtype::Video
|| filepart.typ == Viewtype::File)
&& !filepart.is_meta
};
if need_drop {
@@ -223,19 +226,19 @@ impl<'a> MimeParser<'a> {
std::mem::replace(&mut self.parts[0], filepart);
}
}
if let Some(ref subject) = self.get_subject() {
let mut prepend_subject = true;
if let Some(ref subject) = self.subject {
let mut prepend_subject = 1i32;
if !self.decrypting_failed {
let colon = subject.find(':');
if colon == Some(2)
|| colon == Some(3)
|| self.has_chat_version()
|| self.is_send_by_messenger
|| subject.contains("Chat:")
{
prepend_subject = false
prepend_subject = 0i32
}
}
if prepend_subject {
if 0 != prepend_subject {
let subj = if let Some(n) = subject.find('[') {
&subject[0..n]
} else {
@@ -260,13 +263,13 @@ impl<'a> MimeParser<'a> {
}
if self.parts.len() == 1 {
if self.parts[0].typ == Viewtype::Audio
&& self.get(HeaderDef::ChatVoiceMessage).is_some()
&& self.lookup_field("Chat-Voice-Message").is_some()
{
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Voice;
}
if self.parts[0].typ == Viewtype::Image {
if let Some(value) = self.get(HeaderDef::ChatContent) {
if let Some(value) = self.lookup_field("Chat-Content") {
if value == "sticker" {
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Sticker;
@@ -278,7 +281,7 @@ impl<'a> MimeParser<'a> {
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video
{
if let Some(field_0) = self.get(HeaderDef::ChatDuration) {
if let Some(field_0) = self.lookup_field("Chat-Duration") {
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
let part_mut = &mut self.parts[0];
@@ -287,32 +290,35 @@ impl<'a> MimeParser<'a> {
}
}
}
if !self.decrypting_failed {
if let Some(dn_field) = self.lookup_field("Chat-Disposition-Notification-To") {
if self.get_last_nonmeta().is_some() {
let addrs = mailparse::addrparse(&dn_field).unwrap();
// See if an MDN is requested from the other side
if !self.decrypting_failed && !self.parts.is_empty() {
if let Some(ref dn_to_addr) =
self.parse_first_addr(HeaderDef::ChatDispositionNotificationTo)
{
if let Some(ref from_addr) = self.parse_first_addr(HeaderDef::From_) {
if compare_addrs(from_addr, dn_to_addr) {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
if let Some(dn_to_addr) = addrs.first() {
if let Some(from_field) = self.lookup_field("From") {
let from_addrs = mailparse::addrparse(&from_field).unwrap();
if let Some(from_addr) = from_addrs.first() {
if compare_addrs(from_addr, dn_to_addr) {
if let Some(part_4) = self.get_last_nonmeta_mut() {
part_4.param.set_int(Param::WantsMdn, 1);
}
}
}
}
}
}
}
}
// If there were no parts, especially a non-DC mail user may
// just have send a message in the subject with an empty body.
// Besides, we want to show something in case our incoming-processing
// failed to properly handle an incoming message.
if self.parts.is_empty() && self.reports.is_empty() {
// Cleanup - and try to create at least an empty part if there are no parts yet
if self.get_last_nonmeta().is_none() && self.reports.is_empty() {
let mut part = Part::default();
part.typ = Viewtype::Text;
if let Some(ref subject) = self.get_subject() {
if !self.has_chat_version() {
if let Some(ref subject) = self.subject {
if !self.is_send_by_messenger {
part.msg = subject.to_string();
}
}
@@ -323,68 +329,16 @@ impl<'a> MimeParser<'a> {
Ok(())
}
fn avatar_action_from_header(&mut self, header_value: String) -> AvatarAction {
if header_value == "0" {
return AvatarAction::Delete;
} else {
let mut i = 0;
while i != self.parts.len() {
let part = &mut self.parts[i];
if let Some(part_filename) = &part.org_filename {
if part_filename == &header_value {
if let Some(blob) = part.param.get(Param::File) {
let res = AvatarAction::Change(blob.to_string());
self.parts.remove(i);
return res;
}
break;
}
}
i += 1;
}
}
AvatarAction::None
pub fn get_last_nonmeta(&self) -> Option<&Part> {
self.parts.iter().rev().find(|part| !part.is_meta)
}
pub fn was_encrypted(&self) -> bool {
!self.signatures.is_empty()
pub fn get_last_nonmeta_mut(&mut self) -> Option<&mut Part> {
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(crate) fn has_headers(&self) -> bool {
!self.header.is_empty()
}
pub(crate) fn get_subject(&self) -> Option<String> {
if let Some(s) = self.get(HeaderDef::Subject) {
if s.is_empty() {
return None;
}
Some(s.to_string())
} else {
None
}
}
pub fn get(&self, headerdef: HeaderDef) -> Option<&String> {
self.header.get(&headerdef.get_headername())
}
fn parse_first_addr(&self, headerdef: HeaderDef) -> Option<MailAddr> {
if let Some(value) = self.get(headerdef.clone()) {
match mailparse::addrparse(&value) {
Ok(ref addrs) => {
return addrs.first().cloned();
}
Err(err) => {
warn!(self.context, "header {} parse error: {:?}", headerdef, err);
}
}
}
None
pub fn lookup_field(&self, field_name: &str) -> Option<&String> {
self.header.get(&field_name.to_lowercase())
}
fn parse_mime_recursive(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
@@ -397,7 +351,12 @@ impl<'a> MimeParser<'a> {
return Ok(false);
}
warn!(self.context, "Ignoring nested protected headers");
if self.parsed_protected_headers {
warn!(self.context, "Ignoring nested protected headers");
} else {
self.hash_header(&mail.headers);
self.parsed_protected_headers = true;
}
}
enum MimeS {
@@ -483,9 +442,6 @@ impl<'a> MimeParser<'a> {
}
}
(mime::MULTIPART, "encrypted") => {
// we currently do not try to decrypt non-autocrypt messages
// at all. If we see an encrypted part, we set
// decrypting_failed.
let msg_body = self.context.stock_str(StockMessage::CantDecryptMsgBody);
let txt = format!("[{}]", msg_body);
@@ -531,10 +487,41 @@ impl<'a> MimeParser<'a> {
}
}
_ => {
// 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)? {
// 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)? {
any_part_added = true;
}
}
@@ -549,55 +536,84 @@ 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);
let old_part_count = self.parts.len();
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);
}
};
let (simplified_txt, is_forwarded) = if decoded_data.is_empty() {
("".into(), false)
} else {
let is_html = mime_type == mime::TEXT_HTML;
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);
// 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);
}
};
if is_forwarded {
self.is_forwarded = true;
}
// 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 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,);
} else {
return Ok(false);
}
}
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)
@@ -646,7 +662,6 @@ 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();
@@ -658,7 +673,6 @@ impl<'a> MimeParser<'a> {
}
part.typ = msg_type;
part.org_filename = Some(filename.to_string());
part.mimetype = Some(mime_type);
part.bytes = decoded_data.len();
part.param.set(Param::File, blob.as_name());
@@ -668,24 +682,50 @@ impl<'a> MimeParser<'a> {
}
fn do_add_single_part(&mut self, mut part: Part) {
if self.was_encrypted() {
part.param.set_int(Param::GuaranteeE2ee, 1);
if self.encrypted {
if !self.signatures.is_empty() {
part.param.set_int(Param::GuaranteeE2ee, 1);
} else {
// XXX if the message was encrypted but not signed
// it's not neccessarily an error we need to signal.
// we could just treat it as if it was not encrypted.
part.param.set_int(Param::ErroneousE2ee, 0x2);
}
}
self.parts.push(part);
}
pub fn is_mailinglist_message(&self) -> bool {
if self.get(HeaderDef::ListId).is_some() {
if self.lookup_field("List-Id").is_some() {
return true;
}
if let Some(precedence) = self.get(HeaderDef::Precedence) {
if let Some(precedence) = self.lookup_field("Precedence") {
precedence == "list" || precedence == "bulk"
} else {
false
}
}
pub fn sender_equals_recipient(&self) -> bool {
/* get From: and check there is exactly one sender */
if let Some(field) = self.lookup_field("From") {
if let Ok(addrs) = mailparse::addrparse(field) {
if addrs.len() != 1 {
return false;
}
if let mailparse::MailAddr::Single(ref info) = addrs[0] {
let from_addr_norm = addr_normalize(&info.addr);
let recipients = get_recipients(self.header.iter());
if recipients.len() == 1 && recipients.contains(from_addr_norm) {
return true;
}
}
}
}
false
}
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
if self.parts.is_empty() {
return;
@@ -700,14 +740,14 @@ impl<'a> MimeParser<'a> {
}
pub fn get_rfc724_mid(&self) -> Option<String> {
if let Some(msgid) = self.get(HeaderDef::MessageId) {
parse_message_id(msgid)
} else {
None
// get Message-ID from header
if let Some(field) = self.lookup_field("Message-ID") {
return parse_message_id(field);
}
None
}
fn merge_headers(&mut self, fields: &[mailparse::MailHeader<'_>]) {
fn hash_header(&mut self, fields: &[mailparse::MailHeader<'_>]) {
for field in fields {
if let Ok(key) = field.get_key() {
// lowercasing all headers is technically not correct, but makes things work better
@@ -724,15 +764,19 @@ impl<'a> MimeParser<'a> {
}
fn process_report(&self, report: &mailparse::ParsedMail<'_>) -> Result<Option<Report>> {
// to get a clear functionality, do not show incoming MDNs if the options is disabled
if !self.mdns_enabled {
return Ok(None);
}
// parse as mailheaders
let report_body = report.subparts[1].get_body_raw()?;
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// must be present
let disp = HeaderDef::Disposition.get_headername();
if let Some(_disposition) = report_fields.get_first_value(&disp).ok().flatten() {
if let Some(_disposition) = report_fields.get_first_value("Disposition").ok().flatten() {
if let Some(original_message_id) = report_fields
.get_first_value(&HeaderDef::OriginalMessageId.get_headername())
.get_first_value("Original-Message-ID")
.ok()
.flatten()
.and_then(|v| parse_message_id(&v))
@@ -742,50 +786,37 @@ impl<'a> MimeParser<'a> {
}));
}
}
warn!(
self.context,
"ignoring unknown disposition-notification, Message-Id: {:?}",
report_fields.get_first_value("Message-ID").ok()
);
Ok(None)
}
// Handle reports (only MDNs for now)
// Handle reports (mainly MDNs)
pub fn handle_reports(
&self,
from_id: u32,
sent_timestamp: i64,
rr_event_to_send: &mut Vec<(u32, MsgId)>,
server_folder: impl AsRef<str>,
server_uid: u32,
) {
if self.reports.is_empty() {
return;
}
// If a user disabled MDNs we do not show pending incoming ones anymore
// but we do want them to potentially get moved from the INBOX still.
let mdns_enabled = self.context.get_config_bool(Config::MdnsEnabled);
for report in &self.reports {
let mut mdn_recognized = false;
let mut mdn_consumed = false;
if mdns_enabled {
if let Some((chat_id, msg_id)) = message::mdn_from_ext(
self.context,
from_id,
&report.original_message_id,
sent_timestamp,
) {
self.context.call_cb(Event::MsgRead { chat_id, msg_id });
mdn_recognized = true;
}
if let Some((chat_id, msg_id)) = message::mdn_from_ext(
self.context,
from_id,
&report.original_message_id,
sent_timestamp,
) {
rr_event_to_send.push((chat_id, msg_id));
mdn_consumed = true;
}
if self.has_chat_version() || mdn_recognized || !mdns_enabled {
if self.is_send_by_messenger || 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.has_chat_version() && self.context.get_config_bool(Config::MvboxMove) {
if self.is_send_by_messenger && self.context.get_config_bool(Config::MvboxMove) {
param.set_int(Param::AlsoMove, 1);
}
job_add(self.context, Action::MarkseenMdnOnImap, 0, param, 0);
@@ -874,21 +905,20 @@ fn is_known(key: &str) -> bool {
#[derive(Debug, Default, Clone)]
pub struct Part {
pub typ: Viewtype,
pub is_meta: bool,
pub mimetype: Option<Mime>,
pub msg: String,
pub msg_raw: Option<String>,
pub bytes: usize,
pub param: Params,
org_filename: Option<String>,
}
/// 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 !is_attachment_disposition(mail) {
if !mailmime_is_attachment_disposition(mail) {
match mimetype.subtype() {
mime::PLAIN | mime::HTML => Viewtype::Text,
_ => Viewtype::File,
@@ -922,59 +952,14 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
Ok((mimetype, viewtype))
}
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"));
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");
}
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
});
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,);
} else {
bail!("could not determine filename: {:?}", ct.disposition);
}
}
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();
@@ -1033,7 +1018,7 @@ mod tests {
let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeParser::from_bytes(&context.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.get_subject(), None);
assert_eq!(mimeparser.subject, None);
assert_eq!(mimeparser.parts.len(), 1);
}
@@ -1079,29 +1064,6 @@ 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 =
@@ -1115,40 +1077,20 @@ mod tests {
);
}
#[test]
fn test_parse_first_addr() {
let context = dummy_context();
let raw = b"From: hello@one.org, world@two.org\n\
Chat-Disposition-Notification-To: wrong
Content-Type: text/plain;
Chat-Version: 1.0\n\
\n\
test1\n\
\x00";
let mimeparser = MimeParser::from_bytes(&context.ctx, &raw[..]).unwrap();
let of = mimeparser.parse_first_addr(HeaderDef::From_).unwrap();
assert_eq!(of, mailparse::addrparse("hello@one.org").unwrap()[0]);
let of = mimeparser.parse_first_addr(HeaderDef::ChatDispositionNotificationTo);
assert!(of.is_none());
}
#[test]
fn test_mimeparser_with_context() {
let context = dummy_context();
let raw = b"From: hello\n\
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
Subject: outer-subject\n\
Secure-Join-Group: no\n\
Test-Header: Bar\nChat-Version: 0.0\n\
X-Special-A: special-a\n\
Foo: Bar\nChat-Version: 0.0\n\
\n\
--==break==\n\
Content-Type: text/plain; protected-headers=\"v1\";\n\
Subject: inner-subject\n\
SecureBar-Join-Group: yes\n\
Test-Header: Xy\n\
X-Special-B: special-b\n\
Foo: Xy\n\
Chat-Version: 1.0\n\
\n\
test1\n\
@@ -1156,65 +1098,18 @@ mod tests {
--==break==--\n\
\n\
\x00";
let mimeparser = MimeParser::from_bytes(&context.ctx, &raw[..]).unwrap();
// non-overwritten headers do not bubble up
let of = mimeparser.get(HeaderDef::SecureJoinGroup).unwrap();
assert_eq!(of, "no");
assert_eq!(mimeparser.subject, Some("inner-subject".into()));
// unknown headers do not bubble upwards
let of = mimeparser.get(HeaderDef::_TestHeader).unwrap();
let of = mimeparser.lookup_field("X-Special-A").unwrap();
assert_eq!(of, "special-a");
let of = mimeparser.lookup_field("Foo").unwrap();
assert_eq!(of, "Bar");
// the following fields would bubble up
// if the test would really use encryption for the protected part
// however, as this is not the case, the outer things stay valid
assert_eq!(mimeparser.get_subject(), Some("outer-subject".into()));
let of = mimeparser.get(HeaderDef::ChatVersion).unwrap();
assert_eq!(of, "0.0");
let of = mimeparser.lookup_field("Chat-Version").unwrap();
assert_eq!(of, "1.0");
assert_eq!(mimeparser.parts.len(), 1);
}
#[test]
fn test_mimeparser_with_avatars() {
let t = dummy_context();
let raw = include_bytes!("../test-data/message/mail_attach_txt.eml");
let mimeparser = MimeParser::from_bytes(&t.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.user_avatar, AvatarAction::None);
assert_eq!(mimeparser.group_avatar, AvatarAction::None);
let raw = include_bytes!("../test-data/message/mail_with_user_avatar.eml");
let mimeparser = MimeParser::from_bytes(&t.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert!(mimeparser.user_avatar.is_change());
assert_eq!(mimeparser.group_avatar, AvatarAction::None);
let raw = include_bytes!("../test-data/message/mail_with_user_avatar_deleted.eml");
let mimeparser = MimeParser::from_bytes(&t.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert_eq!(mimeparser.user_avatar, AvatarAction::Delete);
assert_eq!(mimeparser.group_avatar, AvatarAction::None);
let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml");
let mimeparser = MimeParser::from_bytes(&t.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert!(mimeparser.user_avatar.is_change());
assert!(mimeparser.group_avatar.is_change());
// if the Chat-User-Avatar header is missing, the avatar become a normal attachment
let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml");
let raw = String::from_utf8_lossy(raw).to_string();
let raw = raw.replace("Chat-User-Avatar:", "Xhat-Xser-Xvatar:");
let mimeparser = MimeParser::from_bytes(&t.ctx, raw.as_bytes()).unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Image);
assert_eq!(mimeparser.user_avatar, AvatarAction::None);
assert!(mimeparser.group_avatar.is_change());
}
}

View File

@@ -48,10 +48,6 @@ pub enum Param {
Arg4 = b'H',
/// For Messages
Error = b'L',
/// For Messages
AttachGroupImage = b'A',
/// For Messages: space-separated list of messaged IDs of forwarded copies.
///
/// This is used when a [crate::message::Message] is in the
@@ -196,11 +192,6 @@ impl Params {
self.get(key).and_then(|s| s.parse().ok())
}
/// Get the given parameter and parse as `bool`.
pub fn get_bool(&self, key: Param) -> Option<bool> {
self.get_int(key).map(|v| v != 0)
}
/// Get the parameter behind `Param::Cmd` interpreted as `SystemMessage`.
pub fn get_cmd(&self) -> SystemMessage {
self.get_int(Param::Cmd)
@@ -259,7 +250,7 @@ impl Params {
let file = ParamsFile::from_param(context, val)?;
let blob = match file {
ParamsFile::FsPath(path) => match create {
true => BlobObject::new_from_path(context, path)?,
true => BlobObject::create_from_path(context, path)?,
false => BlobObject::from_path(context, path)?,
},
ParamsFile::Blob(blob) => blob,

View File

@@ -417,7 +417,7 @@ impl<'a> Peerstate<'a> {
&self.addr,
],
)?;
reset_gossiped_timestamp(self.context, 0)?;
reset_gossiped_timestamp(self.context, 0);
} else if self.to_save == Some(ToSave::Timestamps) {
sql::execute(
self.context,

View File

@@ -16,7 +16,7 @@ use pgp::types::{
};
use rand::{thread_rng, CryptoRng, Rng};
use crate::error::Result;
use crate::error::Error;
use crate::key::*;
use crate::keyring::*;
@@ -88,7 +88,9 @@ impl<'a> PublicKeyTrait for SignedPublicKeyOrSubkey<'a> {
/// Split data from PGP Armored Data as defined in https://tools.ietf.org/html/rfc4880#section-6.2.
///
/// Returns (type, headers, base64 encoded body).
pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>)> {
pub fn split_armored_data(
buf: &[u8],
) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>), Error> {
use std::io::Read;
let cursor = Cursor::new(buf);
@@ -192,7 +194,7 @@ pub fn pk_encrypt(
plain: &[u8],
public_keys_for_encryption: &Keyring,
private_key_for_signing: Option<&Key>,
) -> Result<String> {
) -> Result<String, Error> {
let lit_msg = Message::new_literal_bytes("", plain);
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
.keys()
@@ -234,7 +236,7 @@ pub fn pk_decrypt(
private_keys_for_decryption: &Keyring,
public_keys_for_validation: &Keyring,
ret_signature_fingerprints: Option<&mut HashSet<String>>,
) -> Result<Vec<u8>> {
) -> Result<Vec<u8>, Error> {
let (msg, _) = Message::from_armor_single(Cursor::new(ctext))?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption
.keys()
@@ -246,7 +248,7 @@ pub fn pk_decrypt(
.collect();
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
let msgs = decryptor.collect::<Result<Vec<_>, _>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
let dec_msg = &msgs[0];
@@ -278,7 +280,7 @@ pub fn pk_decrypt(
}
/// Symmetric encryption.
pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String, Error> {
let mut rng = thread_rng();
let lit_msg = Message::new_literal_bytes("", plain);
@@ -295,11 +297,11 @@ pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
pub fn symm_decrypt<T: std::io::Read + std::io::Seek>(
passphrase: &str,
ctext: T,
) -> Result<Vec<u8>> {
) -> Result<Vec<u8>, Error> {
let (enc_msg, _) = Message::from_armor_single(ctext)?;
let decryptor = enc_msg.decrypt_with_password(|| passphrase.into())?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
let msgs = decryptor.collect::<Result<Vec<_>, _>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
match msgs[0].get_content()? {

View File

@@ -11,7 +11,6 @@ use crate::context::Context;
use crate::e2ee::*;
use crate::error::Error;
use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::key::*;
use crate::lot::LotState;
use crate::message::Message;
@@ -351,7 +350,7 @@ pub(crate) fn handle_securejoin_handshake(
"handle_securejoin_handshake(): called with special contact id"
);
let step = mimeparser
.get(HeaderDef::SecureJoin)
.lookup_field("Secure-Join")
.ok_or_else(|| format_err!("This message is not a Secure-Join message"))?;
info!(
@@ -379,7 +378,7 @@ pub(crate) fn handle_securejoin_handshake(
// it just ensures, we have Bobs key now. If we do _not_ have the key because eg. MitM has removed it,
// send_message() will fail with the error "End-to-end-encryption unavailable unexpectedly.", so, there is no additional check needed here.
// verify that the `Secure-Join-Invitenumber:`-header matches invitenumber written to the QR code
let invitenumber = match mimeparser.get(HeaderDef::SecureJoinInvitenumber) {
let invitenumber = match mimeparser.lookup_field("Secure-Join-Invitenumber") {
Some(n) => n,
None => {
warn!(context, "Secure-join denied (invitenumber missing).",);
@@ -423,7 +422,7 @@ pub(crate) fn handle_securejoin_handshake(
could_not_establish_secure_connection(
context,
contact_chat_id,
if mimeparser.was_encrypted() {
if mimeparser.encrypted {
"No valid signature."
} else {
"Not encrypted."
@@ -468,7 +467,7 @@ pub(crate) fn handle_securejoin_handshake(
==== Step 6 in "Out-of-band verified groups" protocol ====
============================================================ */
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
let fingerprint = match mimeparser.get(HeaderDef::SecureJoinFingerprint) {
let fingerprint = match mimeparser.lookup_field("Secure-Join-Fingerprint") {
Some(fp) => fp,
None => {
could_not_establish_secure_connection(
@@ -497,7 +496,7 @@ pub(crate) fn handle_securejoin_handshake(
}
info!(context, "Fingerprint verified.",);
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
let auth_0 = match mimeparser.get(HeaderDef::SecureJoinAuth) {
let auth_0 = match mimeparser.lookup_field("Secure-Join-Auth") {
Some(auth) => auth,
None => {
could_not_establish_secure_connection(
@@ -527,7 +526,7 @@ pub(crate) fn handle_securejoin_handshake(
inviter_progress!(context, contact_id, 600);
if join_vg {
let field_grpid = mimeparser
.get(HeaderDef::SecureJoinGroup)
.lookup_field("Secure-Join-Group")
.map(|s| s.as_str())
.unwrap_or_else(|| "");
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, field_grpid);
@@ -601,10 +600,10 @@ pub(crate) fn handle_securejoin_handshake(
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined);
emit_event!(context, Event::ContactsChanged(None));
let cg_member_added = mimeparser
.get(HeaderDef::ChatGroupMemberAdded)
.lookup_field("Chat-Group-Member-Added")
.map(|s| s.as_str())
.unwrap_or_else(|| "");
if join_vg && !context.is_self_addr(cg_member_added)? {
if join_vg && !addr_equals_self(context, cg_member_added) {
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
return Ok(ret);
}
@@ -636,7 +635,7 @@ pub(crate) fn handle_securejoin_handshake(
inviter_progress!(context, contact_id, 800);
inviter_progress!(context, contact_id, 1000);
let field_grpid = mimeparser
.get(HeaderDef::SecureJoinGroup)
.lookup_field("Secure-Join-Group")
.map(|s| s.as_str())
.unwrap_or_else(|| "");
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid);
@@ -718,7 +717,7 @@ fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Res
******************************************************************************/
fn encrypted_and_signed(mimeparser: &MimeParser, expected_fingerprint: impl AsRef<str>) -> bool {
if !mimeparser.was_encrypted() {
if !mimeparser.encrypted {
warn!(mimeparser.context, "Message not encrypted.",);
false
} else if mimeparser.signatures.is_empty() {

View File

@@ -2,20 +2,17 @@
pub mod send;
use std::time::Duration;
use lettre::smtp::client::net::*;
use lettre::*;
use async_smtp::smtp::client::net::*;
use async_smtp::*;
use failure::Fail;
use crate::constants::*;
use crate::context::Context;
use crate::events::Event;
use crate::login_param::{dc_build_tls, LoginParam};
use crate::login_param::{dc_build_tls_config, LoginParam};
use crate::oauth2::*;
/// SMTP write and read timeout in seconds.
const SMTP_TIMEOUT: u64 = 30;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Bad parameters")]
@@ -24,22 +21,14 @@ pub enum Error {
InvalidLoginAddress {
address: String,
#[cause]
error: error::Error,
error: lettre::error::Error,
},
#[fail(display = "SMTP failed to connect: {:?}", _0)]
ConnectionFailure(#[cause] smtp::error::Error),
ConnectionFailure(#[cause] lettre::smtp::error::Error),
#[fail(display = "SMTP: failed to setup connection {:?}", _0)]
ConnectionSetupFailure(#[cause] smtp::error::Error),
ConnectionSetupFailure(#[cause] lettre::smtp::error::Error),
#[fail(display = "SMTP: oauth2 error {:?}", _0)]
Oauth2Error { address: String },
#[fail(display = "TLS error")]
Tls(#[cause] native_tls::Error),
}
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Error::Tls(err)
}
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -47,7 +36,8 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Default, DebugStub)]
pub struct Smtp {
#[debug_stub(some = "SmtpTransport")]
transport: Option<smtp::SmtpTransport>,
transport: Option<lettre::smtp::SmtpTransport>,
transport_connected: bool,
/// Email address we are sending from.
from: Option<EmailAddress>,
}
@@ -60,25 +50,22 @@ impl Smtp {
/// Disconnect the SMTP transport and drop it entirely.
pub fn disconnect(&mut self) {
if let Some(mut transport) = self.transport.take() {
async_std::task::block_on(transport.close()).ok();
if self.transport.is_none() || !self.transport_connected {
return;
}
let mut transport = self.transport.take().unwrap();
transport.close();
self.transport_connected = false;
}
/// Check whether we are connected.
/// Check if a connection already exists.
pub fn is_connected(&self) -> bool {
self.transport
.as_ref()
.map(|t| t.is_connected())
.unwrap_or_default()
self.transport.is_some()
}
/// Connect using the provided login params.
/// Connect using the provided login params
pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> {
async_std::task::block_on(self.inner_connect(context, lp))
}
async fn inner_connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> {
if self.is_connected() {
warn!(context, "SMTP already connected.");
return Ok(());
@@ -99,7 +86,7 @@ impl Smtp {
let domain = &lp.send_server;
let port = lp.send_port as u16;
let tls_config = dc_build_tls(lp.smtp_certificate_checks)?.into();
let tls_config = dc_build_tls_config(lp.smtp_certificate_checks);
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls_config);
let (creds, mechanism) = if 0 != lp.server_flags & (DC_LP_AUTH_OAUTH2 as i32) {
@@ -114,21 +101,21 @@ impl Smtp {
}
let user = &lp.send_user;
(
smtp::authentication::Credentials::new(
lettre::smtp::authentication::Credentials::new(
user.to_string(),
access_token.unwrap_or_default(),
),
vec![smtp::authentication::Mechanism::Xoauth2],
vec![lettre::smtp::authentication::Mechanism::Xoauth2],
)
} else {
// plain
let user = lp.send_user.clone();
let pw = lp.send_pw.clone();
(
smtp::authentication::Credentials::new(user, pw),
lettre::smtp::authentication::Credentials::new(user, pw),
vec![
smtp::authentication::Mechanism::Plain,
smtp::authentication::Mechanism::Login,
lettre::smtp::authentication::Mechanism::Plain,
lettre::smtp::authentication::Mechanism::Login,
],
)
};
@@ -136,31 +123,28 @@ impl Smtp {
let security = if 0
!= lp.server_flags & (DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_PLAIN) as i32
{
smtp::ClientSecurity::Opportunistic(tls_parameters)
lettre::smtp::ClientSecurity::Opportunistic(tls_parameters)
} else {
smtp::ClientSecurity::Wrapper(tls_parameters)
lettre::smtp::ClientSecurity::Wrapper(tls_parameters)
};
let client = smtp::SmtpClient::with_security((domain.as_str(), port), security)
.await
let client = lettre::smtp::SmtpClient::new((domain.as_str(), port), security)
.map_err(Error::ConnectionSetupFailure)?;
let client = client
.smtp_utf8(true)
.credentials(creds)
.authentication_mechanism(mechanism)
.connection_reuse(smtp::ConnectionReuseParameters::ReuseUnlimited)
.timeout(Some(Duration::from_secs(SMTP_TIMEOUT)));
let mut trans = client.into_transport();
trans.connect().await.map_err(Error::ConnectionFailure)?;
.connection_reuse(lettre::smtp::ConnectionReuseParameters::ReuseUnlimited);
let mut trans = client.transport();
trans.connect().map_err(Error::ConnectionFailure)?;
self.transport = Some(trans);
self.transport_connected = true;
context.call_cb(Event::SmtpConnected(format!(
"SMTP-LOGIN as {} ok",
lp.send_user,
)));
Ok(())
}
}

View File

@@ -1,7 +1,7 @@
//! # SMTP message sending
use super::Smtp;
use async_smtp::*;
use lettre::*;
use crate::context::Context;
use crate::events::Event;
@@ -11,25 +11,17 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Envelope error: {}", _0)]
EnvelopeError(#[cause] async_smtp::error::Error),
EnvelopeError(#[cause] lettre::error::Error),
#[fail(display = "Send error: {}", _0)]
SendError(#[cause] async_smtp::smtp::error::Error),
SendError(#[cause] lettre::smtp::error::Error),
#[fail(display = "SMTP has no transport")]
NoTransport,
#[fail(display = "SMTP send timed out")]
SendTimeout(#[cause] async_std::future::TimeoutError),
}
impl From<async_std::future::TimeoutError> for Error {
fn from(err: async_std::future::TimeoutError) -> Error {
Error::SendTimeout(err)
}
}
impl Smtp {
/// Send a prepared mail to recipients.
/// On successful send out Ok() is returned.
pub async fn send(
pub fn send(
&mut self,
context: &Context,
recipients: Vec<EmailAddress>,
@@ -44,21 +36,22 @@ impl Smtp {
.collect::<Vec<String>>()
.join(",");
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
message,
);
if let Some(ref mut transport) = self.transport {
transport.send(mail).await.map_err(Error::SendError)?;
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
message,
);
transport.send(mail).map_err(Error::SendError)?;
context.call_cb(Event::SmtpMessageSent(format!(
"Message len={} was smtp-sent to {}",
message_len, recipients_display
)));
self.transport_connected = true;
Ok(())
} else {
warn!(

View File

@@ -1,5 +1,7 @@
//! # SQLite wrapper
use failure::Fail;
use std::collections::HashSet;
use std::sync::{Arc, RwLock};
use std::time::Duration;
@@ -7,7 +9,7 @@ use std::time::Duration;
use rusqlite::{Connection, OpenFlags, Statement, NO_PARAMS};
use thread_local_object::ThreadLocal;
use crate::chat::{update_device_icon, update_saved_messages_icon};
use crate::chat::update_saved_messages_icon;
use crate::constants::ShowEmails;
use crate::context::Context;
use crate::dc_tools::*;
@@ -544,7 +546,7 @@ fn open(
// --------------------------------------------------------------------
let mut dbversion = dbversion_before_update;
let mut recalc_fingerprints = false;
let mut recalc_fingerprints = 0;
let mut update_icons = false;
if dbversion < 1 {
@@ -687,7 +689,7 @@ fn open(
"CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);",
params![],
)?;
recalc_fingerprints = true;
recalc_fingerprints = 1;
dbversion = 34;
sql.set_raw_config_int(context, "dbversion", 34)?;
}
@@ -848,6 +850,7 @@ fn open(
if exists_before_update && sql.get_raw_config_int(context, "bcc_self").is_none() {
sql.set_raw_config_int(context, "bcc_self", 1)?;
}
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 59)?;
}
if dbversion < 60 {
@@ -858,21 +861,12 @@ fn open(
)?;
sql.set_raw_config_int(context, "dbversion", 60)?;
}
if dbversion < 61 {
info!(context, "[migration] v61");
sql.execute(
"ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;",
NO_PARAMS,
)?;
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 61)?;
}
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)
// --------------------------------------------------------------------
if recalc_fingerprints {
if 0 != recalc_fingerprints {
info!(context, "[migration] recalc fingerprints");
sql.query_map(
"SELECT addr FROM acpeerstates;",
@@ -892,7 +886,6 @@ fn open(
}
if update_icons {
update_saved_messages_icon(context)?;
update_device_icon(context)?;
}
}

View File

@@ -185,9 +185,6 @@ pub enum StockMessage {
Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
WelcomeMessage = 71,
#[strum(props(fallback = "Unknown Sender for this chat. See 'info' for more details."))]
UnknownSenderForChat = 72,
}
/*

59
src/wrapmime.rs Normal file
View File

@@ -0,0 +1,59 @@
use mailparse::ParsedMail;
use crate::error::Error;
pub fn parse_message_id(message_id: &[u8]) -> Result<String, Error> {
let value = std::str::from_utf8(message_id)?;
let addrs = mailparse::addrparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
if let Some(info) = addrs.extract_single_info() {
return Ok(info.addr);
}
bail!("could not parse message_id: {}", value);
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
pub fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>, Error> {
ensure!(
mail.ctype.mimetype == "multipart/encrypted",
"Not a multipart/encrypted message: {}",
mail.ctype.mimetype
);
ensure!(
mail.subparts.len() == 2,
"Invalid Autocrypt Level 1 Mime Parts"
);
ensure!(
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
mail.subparts[0].ctype,
);
ensure!(
mail.subparts[1].ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
mail.subparts[1].ctype
);
Ok(&mail.subparts[1])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_message_id() {
assert_eq!(
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
assert_eq!(
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,25 +0,0 @@
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--

View File

@@ -1,40 +0,0 @@
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--

View File

@@ -1,55 +0,0 @@
Chat-Group-ID: WVnDtF5azch
Chat-Group-Name: =?utf-8?q?testgr1?=
Chat-Group-Avatar: group-image.png
Chat-User-Avatar: avatar.png
Subject: =?utf-8?q?Chat=3A_testgr1=3A_hi!_?=
Date: Thu, 12 Dec 2019 17:24:03 +0000
X-Mailer: Delta Chat Core 1.0.0-beta.15/CLI
Chat-Version: 1.0
Message-ID: <Gr.WVnDtF5azch.c6vUZfnnXYx@testrun.org>
To: <bpetersen@b44t.com>
From: =?utf-8?q??= <tunis4@testrun.org>
Content-Type: multipart/mixed; boundary="LV8nfXkpyyn39fsVyoB1b29PKDMeb5"
--LV8nfXkpyyn39fsVyoB1b29PKDMeb5
Content-Type: text/plain; charset=utf-8
hi!
--
Sent with my Delta Chat Messenger: https://delta.chat
--LV8nfXkpyyn39fsVyoB1b29PKDMeb5
Content-Type: image/png
Content-Disposition: attachment; filename="group-image.png"
Content-Transfer-Encoding: base64
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT
1Iw0AYht+mSqVUHCwi4pChOlkQFXHUKhShQqgVWnUwufQPmjQkKS6OgmvBwZ/FqoOLs64OroIg+APi
4uqk6CIlfpcUWsR4x3EP733vy913gNCoMM3qGgc03TbTyYSYza2KoVdEEMYATUFmljEnSSn4jq97BP
h+F+dZ/nV/jl41bzEgIBLPMsO0iTeIpzdtg/M+cZSVZJX4nHjMpAsSP3Jd8fiNc9FlgWdGzUx6njhK
LBY7WOlgVjI14inimKrplC9kPVY5b3HWKjXWuid/YSSvryxzndYwkljEEiSIUFBDGRXYiNOuk2IhTe
cJH/+Q65fIpZCrDEaOBVShQXb94H/wu7dWYXLCS4okgO4Xx/kYAUK7QLPuON/HjtM8AYLPwJXe9lcb
wMwn6fW2FjsC+raBi+u2puwBlzvA4JMhm7IrBWkJhQLwfkbflAP6b4Hwmte31jlOH4AM9Sp1AxwcAq
NFyl73eXdPZ9/+rWn17wcR7HKATfSiTAAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+MMChYX
Fh+1IOwAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAGElEQVQoz2P858hAEm
BiYBjVMKphuGoAAAO8AV+n297RAAAAAElFTkSuQmCC
--LV8nfXkpyyn39fsVyoB1b29PKDMeb5
Content-Type: image/png
Content-Disposition: attachment; filename="avatar.png"
Content-Transfer-Encoding: base64
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT
1Iw0AcxV9TpUUqIhYRcchQnSyIFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi
4uqk6CIl/q8ptIjx4Lgf7+497t4BQr3MNKtrAtB020wl4mImuyoGXhFCEIOIoV9mljEnSUl4jq97+P
h6F+VZ3uf+HL1qzmKATySeZYZpE28QT2/aBud94jAryirxOfG4SRckfuS64vIb50KTBZ4ZNtOpeeIw
sVjoYKWDWdHUiKeII6qmU76QcVnlvMVZK1dZ6578haGcvrLMdZojSGARS5AgQkEVJZRhI0qrToqFFO
3HPfzDTb9ELoVcJTByLKACDXLTD/4Hv7u18rFJNykUB7pfHOdjFAjsAo2a43wfO07jBPA/A1d621+p
AzOfpNfaWuQI6NsGLq7bmrIHXO4AQ0+GbMpNyU9TyOeB9zP6piwwcAv0rLm9tfZx+gCkqavkDXBwCI
wVKHvd493Bzt7+PdPq7wd6nHKqMKZUTAAAAANQTFRF/sYAhYATyAAAAAlwSFlzAAAuIwAALiMBeKU/
dgAAAAd0SU1FB+MMCBY0D29+N8YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAA
AADElEQVQI12NgIA0AAAAwAAHHqoWOAAAAAElFTkSuQmCC
--LV8nfXkpyyn39fsVyoB1b29PKDMeb5--

View File

@@ -1,37 +0,0 @@
Chat-User-Avatar: avatar.png
Subject: =?utf-8?q?Chat=3A_this_is_a_message_with_a_=2E=2E=2E?=
Message-ID: Mr.wOBwZNbBTVt.NZpmQDwWoNk@example.org
In-Reply-To: Mr.ETXqza5-WpB.zDEYOLECxAw@example.org
Date: Sun, 08 Dec 2019 23:12:55 +0000
X-Mailer: Delta Chat Core 1.0.0-beta.12/CLI
Chat-Version: 1.0
To: <tunis3@example.org>
From: "=?utf-8?q??=" <tunis4@example.org>
Content-Type: multipart/mixed; boundary="luTiGu6GBoVLCvTkzVtmZmwsmhkNMw"
--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw
Content-Type: text/plain; charset=utf-8
this is a message with a profile-image attached
--
Sent with my Delta Chat Messenger: https://delta.chat
--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw
Content-Type: image/png
Content-Disposition: attachment; filename="avatar.png"
Content-Transfer-Encoding: base64
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT
1Iw0AcxV9TpUUqIhYRcchQnSyIFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi
4uqk6CIl/q8ptIjx4Lgf7+497t4BQr3MNKtrAtB020wl4mImuyoGXhFCEIOIoV9mljEnSUl4jq97+P
h6F+VZ3uf+HL1qzmKATySeZYZpE28QT2/aBud94jAryirxOfG4SRckfuS64vIb50KTBZ4ZNtOpeeIw
sVjoYKWDWdHUiKeII6qmU76QcVnlvMVZK1dZ6578haGcvrLMdZojSGARS5AgQkEVJZRhI0qrToqFFO
3HPfzDTb9ELoVcJTByLKACDXLTD/4Hv7u18rFJNykUB7pfHOdjFAjsAo2a43wfO07jBPA/A1d621+p
AzOfpNfaWuQI6NsGLq7bmrIHXO4AQ0+GbMpNyU9TyOeB9zP6piwwcAv0rLm9tfZx+gCkqavkDXBwCI
wVKHvd493Bzt7+PdPq7wd6nHKqMKZUTAAAAANQTFRF/sYAhYATyAAAAAlwSFlzAAAuIwAALiMBeKU/
dgAAAAd0SU1FB+MMCBY0D29+N8YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAA
AADElEQVQI12NgIA0AAAAwAAHHqoWOAAAAAElFTkSuQmCC
--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw--

View File

@@ -1,14 +0,0 @@
Content-Type: text/plain; charset=utf-8
Chat-User-Avatar: 0
Subject: =?utf-8?q?Chat=3A_profile_image_deleted?=
Message-ID: Mr.tsgoJgn-cBf.0TkFWKJzeSp@example.org
Date: Sun, 08 Dec 2019 23:28:30 +0000
X-Mailer: Delta Chat Core 1.0.0-beta.12/CLI
Chat-Version: 1.0
To: <tunis3@example>
From: "=?utf-8?q??=" <tunis4@example.org>
profile image deleted
--
Sent with my Delta Chat Messenger: https://delta.chat

View File

@@ -2,7 +2,9 @@
use std::collections::HashSet;
use deltachat::chat::{self, Chat};
use deltachat::config;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::keyring::*;
use deltachat::pgp;
@@ -225,3 +227,20 @@ fn test_stress_tests() {
let context = create_test_context();
stress_functions(&context.ctx);
}
#[test]
fn test_chat() {
let context = create_test_context();
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap();
assert_ne!(contact1, 0);
let chat_id = chat::create_by_contact_id(&context.ctx, contact1).unwrap();
assert!(chat_id > 9, "chat_id too small {}", chat_id);
let chat = Chat::load_from_db(&context.ctx, chat_id).unwrap();
let chat2_id = chat::create_by_contact_id(&context.ctx, contact1).unwrap();
assert_eq!(chat2_id, chat_id);
let chat2 = Chat::load_from_db(&context.ctx, chat2_id).unwrap();
assert_eq!(chat2.name, chat.name);
}