mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
107 Commits
fix_html_p
...
no_format_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed790879cd | ||
|
|
558466d506 | ||
|
|
b424ef9ebb | ||
|
|
69f5fd86a4 | ||
|
|
f9ba9ae912 | ||
|
|
b33fec6236 | ||
|
|
70e12485aa | ||
|
|
b2b7674b59 | ||
|
|
8fa175f36d | ||
|
|
95fbc904d1 | ||
|
|
ce37a8dda2 | ||
|
|
e0499c9552 | ||
|
|
4825f2510f | ||
|
|
7a12134795 | ||
|
|
66adfa074b | ||
|
|
9d201eb9c6 | ||
|
|
789fc0a7e0 | ||
|
|
9e309132f8 | ||
|
|
88923173c2 | ||
|
|
7b55863418 | ||
|
|
38d5fad4e5 | ||
|
|
4d1357568b | ||
|
|
ccc190f991 | ||
|
|
0a9f880fb4 | ||
|
|
b8faf54f0b | ||
|
|
888507f7ba | ||
|
|
29866092de | ||
|
|
36fc2c83f2 | ||
|
|
5e777b3c51 | ||
|
|
5690c48863 | ||
|
|
8ab3363097 | ||
|
|
f6861ca5f5 | ||
|
|
3bb58be2b5 | ||
|
|
409c96e571 | ||
|
|
d681fa6cba | ||
|
|
7c3d8356c4 | ||
|
|
a8842da50a | ||
|
|
ff29b84146 | ||
|
|
ab12a4eb39 | ||
|
|
c3fd0889e2 | ||
|
|
c62532a665 | ||
|
|
ca63d6ba1c | ||
|
|
a1f496b019 | ||
|
|
da421438cd | ||
|
|
7f723ef2bf | ||
|
|
3cf39dace0 | ||
|
|
021ad4f12c | ||
|
|
36edf447e7 | ||
|
|
ea2273aef4 | ||
|
|
251aa22c4c | ||
|
|
541710147a | ||
|
|
a197ca4cf7 | ||
|
|
775c36bb65 | ||
|
|
1953b95c57 | ||
|
|
542a33952f | ||
|
|
1819712667 | ||
|
|
69a596fdff | ||
|
|
4f88b93495 | ||
|
|
a388052e2a | ||
|
|
8b81ea3b6f | ||
|
|
b913de5928 | ||
|
|
4e551cde66 | ||
|
|
b681cbd47f | ||
|
|
551253b4e0 | ||
|
|
4ad9166b5a | ||
|
|
8487255c33 | ||
|
|
2792d4ea1e | ||
|
|
95180a850f | ||
|
|
56ee7a0abd | ||
|
|
d0a04be825 | ||
|
|
2cbf287998 | ||
|
|
054cf98754 | ||
|
|
a95fbfe271 | ||
|
|
17ce02a87c | ||
|
|
4dc5e0378f | ||
|
|
c33797ff84 | ||
|
|
f242b40d0a | ||
|
|
5f916f5a9c | ||
|
|
6edb525540 | ||
|
|
0c04d5b2ab | ||
|
|
301852fd87 | ||
|
|
0e8df7d633 | ||
|
|
e4155e0e16 | ||
|
|
2c4dbe6e68 | ||
|
|
a99b96e36e | ||
|
|
0889467c7b | ||
|
|
d141e228de | ||
|
|
9b15c42801 | ||
|
|
a781b631e1 | ||
|
|
08af5c8e09 | ||
|
|
93e8cca02f | ||
|
|
a8e9a1fbe5 | ||
|
|
54eb30f3db | ||
|
|
c08a1adc9b | ||
|
|
cd951ad396 | ||
|
|
33793d878b | ||
|
|
357955015d | ||
|
|
d6d94adab0 | ||
|
|
099cc9f727 | ||
|
|
61f8d6f171 | ||
|
|
a7af4685f1 | ||
|
|
2cebed4f77 | ||
|
|
3f2a371599 | ||
|
|
a3ca3d9179 | ||
|
|
86ace1a4af | ||
|
|
1abdf62045 | ||
|
|
9e2a96675d |
@@ -36,12 +36,6 @@ 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 }}
|
||||
@@ -49,7 +43,6 @@ 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
|
||||
@@ -91,7 +84,6 @@ 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
|
||||
@@ -187,7 +179,7 @@ jobs:
|
||||
- *restore-cache
|
||||
- run:
|
||||
name: Run cargo clippy
|
||||
command: cargo clippy --all
|
||||
command: cargo clippy
|
||||
|
||||
|
||||
workflows:
|
||||
@@ -195,7 +187,7 @@ workflows:
|
||||
|
||||
test:
|
||||
jobs:
|
||||
- cargo_fetch
|
||||
# - cargo_fetch
|
||||
|
||||
- remote_tests_rust
|
||||
|
||||
@@ -205,12 +197,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
|
||||
|
||||
|
||||
47
.github/workflows/code-quality.yml
vendored
Normal file
47
.github/workflows/code-quality.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
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
|
||||
67
CHANGELOG.md
67
CHANGELOG.md
@@ -1,8 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## next (pending)
|
||||
## 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
|
||||
|
||||
- restructured (but not change) imap idle handling into own file. cc @link2xt
|
||||
|
||||
## 1.0.0-beta.12
|
||||
|
||||
|
||||
730
Cargo.lock
generated
730
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
30
Cargo.toml
30
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.0.0-beta.12"
|
||||
version = "1.0.0-beta.16"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL"
|
||||
@@ -9,18 +9,19 @@ license = "MPL"
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
libc = "0.2.51"
|
||||
pgp = { git = "https://github.com/rpgp/rpgp", branch = "master", default-features = false }
|
||||
pgp = { version = "0.4.0", default-features = false }
|
||||
hex = "0.4.0"
|
||||
sha2 = "0.8.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"
|
||||
rand = "0.7.0"
|
||||
smallvec = "1.0.0"
|
||||
reqwest = { version = "0.9.15" }
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
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-smtp = { git = "https://github.com/async-email/async-smtp", branch = "master" }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "native_tls" }
|
||||
async-imap = { git = "https://github.com/async-email/async-imap", branch="native_tls", default-features = false, features = ["tls_native"] }
|
||||
async-native-tls = "0.1.1"
|
||||
async-std = { version = "1.0", features = ["unstable"] }
|
||||
base64 = "0.11"
|
||||
charset = "0.1"
|
||||
@@ -30,6 +31,7 @@ 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"
|
||||
@@ -44,17 +46,15 @@ backtrace = "0.3.33"
|
||||
byteorder = "1.3.1"
|
||||
itertools = "0.8.0"
|
||||
image-meta = "0.1.0"
|
||||
quick-xml = "0.15.0"
|
||||
quick-xml = "0.17.1"
|
||||
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"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
@@ -79,6 +79,6 @@ path = "examples/repl/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["nightly", "ringbuf"]
|
||||
vendored = []
|
||||
vendored = ["native-tls/vendored", "reqwest/default-tls-vendored"]
|
||||
nightly = ["pgp/nightly"]
|
||||
ringbuf = ["pgp/ringbuf"]
|
||||
|
||||
@@ -8,7 +8,6 @@ install:
|
||||
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
|
||||
- rustc -vV
|
||||
- cargo -vV
|
||||
- cargo update
|
||||
|
||||
build: false
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.0.0-beta.12"
|
||||
version = "1.0.0-beta.16"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -3645,11 +3645,15 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* @private @memberof dc_context_t
|
||||
* 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.
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
@@ -4050,11 +4054,6 @@ 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.
|
||||
|
||||
@@ -29,6 +29,8 @@ use deltachat::message::MsgId;
|
||||
use deltachat::stock::StockMessage;
|
||||
use deltachat::*;
|
||||
|
||||
mod dc_array;
|
||||
|
||||
mod string;
|
||||
use self::string::*;
|
||||
|
||||
|
||||
@@ -94,7 +94,9 @@ 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)?;
|
||||
|
||||
dc_receive_imf(context, &data, "import", 0, 0);
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", 0, 0) {
|
||||
println!("dc_receive_imf errored: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -936,7 +938,14 @@ 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: {}:\n\n", 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(),
|
||||
}
|
||||
);
|
||||
|
||||
res += &Contact::get_encrinfo(context, contact_id)?;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import print_function
|
||||
import atexit
|
||||
import threading
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from array import array
|
||||
@@ -94,9 +95,12 @@ 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):
|
||||
@@ -132,6 +136,18 @@ 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():
|
||||
|
||||
@@ -109,6 +109,30 @@ 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.
|
||||
|
||||
@@ -130,9 +154,12 @@ class Chat(object):
|
||||
:raises ValueError: if message can not be send/chat does not exist.
|
||||
:returns: the resulting :class:`deltachat.message.Message` instance
|
||||
"""
|
||||
msg = self.prepare_message_file(path=path, mime_type=mime_type)
|
||||
self.send_prepared(msg)
|
||||
return msg
|
||||
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)
|
||||
|
||||
def send_image(self, path):
|
||||
""" send an image message and return the resulting Message instance.
|
||||
@@ -142,9 +169,12 @@ class Chat(object):
|
||||
:returns: the resulting :class:`deltachat.message.Message` instance
|
||||
"""
|
||||
mime_type = mimetypes.guess_type(path)[0]
|
||||
msg = self.prepare_message_file(path=path, mime_type=mime_type, view_type="image")
|
||||
self.send_prepared(msg)
|
||||
return msg
|
||||
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)
|
||||
|
||||
def prepare_message(self, msg):
|
||||
""" create a new prepared message.
|
||||
|
||||
@@ -68,7 +68,6 @@ 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
|
||||
|
||||
@@ -47,3 +47,13 @@ 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)
|
||||
|
||||
@@ -174,7 +174,7 @@ class Message(object):
|
||||
@property
|
||||
def _msgstate(self):
|
||||
if self.id == 0:
|
||||
dc_msg = self.message._dc_msg
|
||||
dc_msg = self._dc_msg
|
||||
else:
|
||||
# load message from db to get a fresh/current state
|
||||
dc_msg = ffi.gc(
|
||||
|
||||
@@ -85,16 +85,21 @@ class SessionLiveConfigFromFile:
|
||||
class SessionLiveConfigFromURL:
|
||||
def __init__(self, url, create_token):
|
||||
self.configlist = []
|
||||
for i in range(2):
|
||||
res = requests.post(url, json={"token_create_user": int(create_token)})
|
||||
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)})
|
||||
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)
|
||||
|
||||
def get(self, index):
|
||||
return self.configlist[index]
|
||||
return config
|
||||
|
||||
def exists(self):
|
||||
return bool(self.configlist)
|
||||
|
||||
@@ -382,6 +382,23 @@ 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):
|
||||
@@ -456,10 +473,7 @@ class TestOnlineAccount:
|
||||
msg1 = Message.new_empty(ac1, "file")
|
||||
msg1.set_text("withfile")
|
||||
msg1.set_file(p)
|
||||
message = chat.prepare_message(msg1)
|
||||
assert message.is_out_preparing()
|
||||
assert message.text == "withfile"
|
||||
chat.send_prepared(message)
|
||||
chat.send_msg(msg1)
|
||||
|
||||
lp.sec("ac2: receive message")
|
||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
@@ -618,6 +632,9 @@ 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)
|
||||
|
||||
@@ -635,6 +652,7 @@ 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
|
||||
@@ -697,6 +715,12 @@ 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")))
|
||||
@@ -958,7 +982,54 @@ class TestOnlineAccount:
|
||||
assert msg.text == "world"
|
||||
assert msg.is_encrypted()
|
||||
|
||||
def test_set_get_profile_image(self, acfactory, data, lp):
|
||||
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):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
lp.sec("create unpromoted group chat")
|
||||
@@ -1060,6 +1131,52 @@ 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("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()
|
||||
|
||||
@@ -1,10 +1,49 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import os.path
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
from filecmp import cmp
|
||||
from deltachat import const
|
||||
|
||||
from conftest import wait_configuration_progress, wait_msgs_changed
|
||||
from deltachat import const
|
||||
|
||||
|
||||
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()
|
||||
@@ -17,7 +56,10 @@ class TestOnlineInCreation:
|
||||
wait_msgs_changed(ac1, 0, 0) # why no chat id?
|
||||
|
||||
lp.sec("create a message with a file in creation")
|
||||
path = data.get_path("d.png")
|
||||
orig = data.get_path("d.png")
|
||||
path = os.path.join(ac1.get_blobdir(), 'd.png')
|
||||
with open(path, "x") as fp:
|
||||
fp.write("preparing")
|
||||
prepared_original = chat.prepare_message_file(path)
|
||||
assert prepared_original.is_out_preparing()
|
||||
wait_msgs_changed(ac1, chat.id, prepared_original.id)
|
||||
@@ -38,6 +80,7 @@ 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)
|
||||
@@ -59,11 +102,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, path, False)
|
||||
assert cmp(received_original.filename, orig, shallow=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, path, False)
|
||||
assert cmp(received_copy.filename, orig, shallow=False)
|
||||
|
||||
@@ -65,8 +65,8 @@ commands =
|
||||
|
||||
|
||||
[pytest]
|
||||
addopts = -v -rs
|
||||
python_files = tests/test_*.py
|
||||
addopts = -v -ra
|
||||
python_files = tests/test_*.py
|
||||
norecursedirs = .tox
|
||||
xfail_strict=true
|
||||
timeout = 60
|
||||
|
||||
@@ -55,7 +55,8 @@ if __name__ == "__main__":
|
||||
replace_toml_version("Cargo.toml", newversion)
|
||||
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
||||
|
||||
subprocess.call(["cargo", "update", "-p", "deltachat"])
|
||||
subprocess.call(["git", "add", "-u"])
|
||||
# subprocess.call(["cargo", "update", "-p", "deltachat"])
|
||||
|
||||
print("after commit make sure to: ")
|
||||
print("")
|
||||
|
||||
26
spec.md
26
spec.md
@@ -1,6 +1,6 @@
|
||||
# Chat-over-Email specification
|
||||
|
||||
Version 0.19.0
|
||||
Version 0.20.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-Image`
|
||||
and MUST add the header `Chat-Group-Avatar`
|
||||
with the value set to the image name.
|
||||
|
||||
To remove the group-image,
|
||||
the messenger MUST add the header `Chat-Group-Image: 0`.
|
||||
the messenger MUST add the header `Chat-Group-Avatar: 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-Image: image.jpg
|
||||
Chat-Group-Avatar: 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-Image` only on image changes.
|
||||
to add a `Chat-Group-Avatar` only on image changes.
|
||||
|
||||
|
||||
# Set profile image
|
||||
|
||||
A user MAY have a profile-image that MAY be spread to his contacts.
|
||||
A user MAY have a profile-image that MAY be spread to their contacts.
|
||||
To change or set the profile-image,
|
||||
the messenger MUST attach an image file to a message
|
||||
and MUST add the header `Chat-Profile-Image`
|
||||
and MUST add the header `Chat-User-Avatar`
|
||||
with the value set to the image name.
|
||||
|
||||
To remove the profile-image,
|
||||
the messenger MUST add the header `Chat-Profile-Image: 0`.
|
||||
the messenger MUST add the header `Chat-User-Avatar: 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 `profile_image_update_state` somewhere).
|
||||
the messenger has to keep a `user_avatar_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-Profile-Image: photo.jpg
|
||||
Chat-User-Avatar: 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-Profile-Image` may appear together with all other headers,
|
||||
eg. there may be a `Chat-Profile-Image` and a `Chat-Group-Image` header
|
||||
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
|
||||
in the same message.
|
||||
To save data, it is RECOMMENDED to add a `Chat-Profile-Image` header
|
||||
To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
|
||||
only on image changes.
|
||||
|
||||
|
||||
|
||||
@@ -159,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 create_from_path(
|
||||
pub fn new_from_path(
|
||||
context: &Context,
|
||||
src: impl AsRef<Path>,
|
||||
) -> std::result::Result<BlobObject, BlobError> {
|
||||
@@ -559,14 +559,14 @@ mod tests {
|
||||
|
||||
let src_ext = t.dir.path().join("external");
|
||||
fs::write(&src_ext, b"boo").unwrap();
|
||||
let blob = BlobObject::create_from_path(&t.ctx, &src_ext).unwrap();
|
||||
let blob = BlobObject::new_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::create_from_path(&t.ctx, &src_int).unwrap();
|
||||
let blob = BlobObject::new_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");
|
||||
@@ -576,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::create_from_path(&t.ctx, &src_ext).unwrap();
|
||||
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
|
||||
assert_eq!(
|
||||
blob.as_name(),
|
||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
||||
|
||||
190
src/chat.rs
190
src/chat.rs
@@ -35,7 +35,6 @@ pub struct Chat {
|
||||
pub grpid: String,
|
||||
blocked: Blocked,
|
||||
pub param: Params,
|
||||
pub gossiped_timestamp: i64,
|
||||
is_sending_locations: bool,
|
||||
}
|
||||
|
||||
@@ -44,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.gossiped_timestamp, c.locations_send_until \
|
||||
c.blocked, c.locations_send_until \
|
||||
FROM chats c WHERE c.id=?;",
|
||||
params![chat_id as i32],
|
||||
|row| {
|
||||
@@ -56,8 +55,7 @@ impl Chat {
|
||||
param: row.get::<_, String>(4)?.parse().unwrap_or_default(),
|
||||
archived: row.get(5)?,
|
||||
blocked: row.get::<_, Option<_>>(6)?.unwrap_or_default(),
|
||||
gossiped_timestamp: row.get(7)?,
|
||||
is_sending_locations: row.get(8)?,
|
||||
is_sending_locations: row.get(7)?,
|
||||
};
|
||||
|
||||
Ok(c)
|
||||
@@ -216,6 +214,10 @@ 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;
|
||||
|
||||
@@ -310,10 +312,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)?;
|
||||
}
|
||||
@@ -593,12 +595,6 @@ 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) {
|
||||
@@ -613,6 +609,24 @@ 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,
|
||||
@@ -638,10 +652,7 @@ pub fn create_or_lookup_by_contact_id(
|
||||
chat_name,
|
||||
match contact_id {
|
||||
DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk
|
||||
DC_CONTACT_ID_DEVICE => {
|
||||
let icon = copy_device_icon_to_blobs(context)?;
|
||||
format!("D=1\ni={}", icon) // D = Param::Devicetalk, i = Param::ProfileImage
|
||||
},
|
||||
DC_CONTACT_ID_DEVICE => "D=1".to_string(), // D = Param::Devicetalk
|
||||
_ => "".to_string()
|
||||
},
|
||||
create_blocked as u8,
|
||||
@@ -665,6 +676,8 @@ pub fn create_or_lookup_by_contact_id(
|
||||
|
||||
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))
|
||||
@@ -725,20 +738,11 @@ fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> {
|
||||
if msg.type_0 == Viewtype::Text {
|
||||
// the caller should check if the message text is empty
|
||||
} else if msgtype_has_file(msg.type_0) {
|
||||
let blob = if let Some(f) = msg.param.get_file(Param::File, context)? {
|
||||
match f {
|
||||
ParamsFile::Blob(blob) => blob,
|
||||
ParamsFile::FsPath(path) => {
|
||||
// path is outside the blobdir, let's copy
|
||||
let blob = BlobObject::create_and_copy(context, path)?;
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
|
||||
blob
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("Attachment missing for message of type #{}", msg.type_0);
|
||||
};
|
||||
let blob = msg
|
||||
.param
|
||||
.get_blob(Param::File, context, !msg.is_increation())?
|
||||
.ok_or_else(|| format_err!("Attachment missing for message of type #{}", msg.type_0))?;
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
|
||||
if msg.type_0 == Viewtype::File || msg.type_0 == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
@@ -1463,7 +1467,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)?;
|
||||
@@ -1561,12 +1565,28 @@ fn real_group_exists(context: &Context, chat_id: u32) -> bool {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn reset_gossiped_timestamp(context: &Context, chat_id: u32) {
|
||||
set_gossiped_timestamp(context, chat_id, 0);
|
||||
pub fn reset_gossiped_timestamp(context: &Context, chat_id: u32) -> crate::sql::Result<()> {
|
||||
set_gossiped_timestamp(context, chat_id, 0)
|
||||
}
|
||||
|
||||
// Should return Result
|
||||
pub fn set_gossiped_timestamp(context: &Context, chat_id: u32, timestamp: i64) {
|
||||
/// 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<()> {
|
||||
if 0 != chat_id {
|
||||
info!(
|
||||
context,
|
||||
@@ -1579,7 +1599,6 @@ pub fn set_gossiped_timestamp(context: &Context, chat_id: u32, timestamp: i64) {
|
||||
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
|
||||
params![timestamp, chat_id as i32],
|
||||
)
|
||||
.ok();
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
@@ -1591,10 +1610,58 @@ pub fn set_gossiped_timestamp(context: &Context, chat_id: u32, timestamp: i64) {
|
||||
"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,
|
||||
@@ -1848,11 +1915,8 @@ pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: u32) -> Resul
|
||||
ensure!(chat.can_send(), "cannot send to chat #{}", chat_id);
|
||||
curr_timestamp = dc_create_smeared_timestamps(context, msg_ids.len());
|
||||
let ids = context.sql.query_map(
|
||||
format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
msg_ids.iter().map(|_| "?").join(",")
|
||||
),
|
||||
msg_ids,
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
params![msg_ids.iter().map(|_| "?").join(",")],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)?;
|
||||
@@ -1951,7 +2015,7 @@ pub fn get_info_json(context: &Context, chat_id: u32) -> Result<String, Error> {
|
||||
"name": chat.name,
|
||||
"archived": chat.archived,
|
||||
"param": chat.param.to_string(),
|
||||
"gossiped_timestamp": chat.gossiped_timestamp,
|
||||
"gossiped_timestamp": chat.get_gossiped_timestamp(context),
|
||||
"is_sending_locations": chat.is_sending_locations,
|
||||
"color": chat.get_color(context),
|
||||
"profile_image": profile_image,
|
||||
@@ -2412,4 +2476,42 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::job::*;
|
||||
use crate::stock::StockMessage;
|
||||
use rusqlite::NO_PARAMS;
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
@@ -127,9 +128,18 @@ 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 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::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)?;
|
||||
self.sql.set_raw_config(self, key, Some(blob.as_name()))
|
||||
}
|
||||
None => self.sql.set_raw_config(self, key, None),
|
||||
}
|
||||
}
|
||||
Config::InboxWatch => {
|
||||
let ret = self.sql.set_raw_config(self, key, value);
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee;
|
||||
use crate::job::*;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::oauth2::*;
|
||||
use crate::param::Params;
|
||||
|
||||
@@ -92,9 +92,11 @@ 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 = std::i32::MAX;
|
||||
let mut keep_flags = 0;
|
||||
|
||||
const STEP_12_USE_AUTOCONFIG: u8 = 12;
|
||||
const STEP_13_AFTER_AUTOCONFIG: u8 = 13;
|
||||
|
||||
const STEP_3_INDEX: u8 = 13;
|
||||
let mut step_counter: u8 = 0;
|
||||
while !context.shall_stop_ongoing() {
|
||||
step_counter += 1;
|
||||
@@ -110,7 +112,7 @@ pub fn JobConfigureImap(context: &Context) {
|
||||
}
|
||||
// Step 1: Load the parameters and check email-address and password
|
||||
2 => {
|
||||
if 0 != param.server_flags & 0x2 {
|
||||
if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 {
|
||||
// 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.
|
||||
@@ -145,6 +147,7 @@ pub fn JobConfigureImap(context: &Context) {
|
||||
// Step 2: Autoconfig
|
||||
4 => {
|
||||
progress!(context, 200);
|
||||
|
||||
if param.mail_server.is_empty()
|
||||
&& param.mail_port == 0
|
||||
/*&¶m.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then */
|
||||
@@ -152,12 +155,18 @@ pub fn JobConfigureImap(context: &Context) {
|
||||
&& param.send_port == 0
|
||||
&& param.send_user.is_empty()
|
||||
/*&¶m.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for autoconfig or not */
|
||||
&& param.server_flags & !0x2 == 0
|
||||
&& (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0
|
||||
{
|
||||
keep_flags = param.server_flags & 0x2;
|
||||
// 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, ¶m) {
|
||||
// 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
|
||||
}
|
||||
} else {
|
||||
// Autoconfig is not needed so skip it.
|
||||
step_counter = STEP_3_INDEX - 1;
|
||||
// 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
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -242,8 +251,10 @@ pub fn JobConfigureImap(context: &Context) {
|
||||
}
|
||||
true
|
||||
}
|
||||
/* C. Do we have any result? */
|
||||
12 => {
|
||||
/* 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 => {
|
||||
progress!(context, 500);
|
||||
if let Some(ref cfg) = param_autoconfig {
|
||||
info!(context, "Got autoconfig: {}", &cfg);
|
||||
@@ -256,15 +267,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 important to keep the object as
|
||||
we may enter "deep guessing" if we could not read a configuration */
|
||||
/* although param_autoconfig's data are no longer needed from,
|
||||
it is used to later to prevent trying variations of port/server/logins */
|
||||
}
|
||||
param.server_flags |= keep_flags;
|
||||
true
|
||||
}
|
||||
// Step 3: Fill missing fields with defaults
|
||||
13 => {
|
||||
// if you move this, don't forget to update STEP_3_INDEX, too
|
||||
// If you change the match-number here, also update STEP_13_AFTER_AUTOCONFIG above
|
||||
STEP_13_AFTER_AUTOCONFIG => {
|
||||
if param.mail_server.is_empty() {
|
||||
param.mail_server = format!("imap.{}", param_domain,)
|
||||
}
|
||||
@@ -430,6 +441,42 @@ 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,
|
||||
@@ -485,8 +532,12 @@ fn try_imap_connection(
|
||||
|
||||
fn try_imap_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} flags=0x{:x}",
|
||||
param.mail_user, param.mail_server, param.mail_port, param.server_flags
|
||||
"imap: {}@{}:{} flags=0x{:x} certificate_checks={}",
|
||||
param.mail_user,
|
||||
param.mail_server,
|
||||
param.mail_port,
|
||||
param.server_flags,
|
||||
param.imap_certificate_checks
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
if context
|
||||
@@ -567,6 +618,7 @@ 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::*;
|
||||
@@ -580,4 +632,19 @@ 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, ¶ms).is_none());
|
||||
|
||||
let mut params = LoginParam::new();
|
||||
params.addr = "someone123@nauta.cu".to_string();
|
||||
let found_params = get_offline_autoconfig(&context, ¶ms).unwrap();
|
||||
assert_eq!(found_params.mail_server, "imap.nauta.cu".to_string());
|
||||
assert_eq!(found_params.send_server, "smtp.nauta.cu".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,9 @@ 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;
|
||||
@@ -119,6 +122,10 @@ 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;
|
||||
|
||||
134
src/contact.rs
134
src/contact.rs
@@ -10,11 +10,13 @@ use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee;
|
||||
use crate::error::Result;
|
||||
use crate::error::{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;
|
||||
@@ -58,6 +60,8 @@ 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.
|
||||
@@ -139,32 +143,10 @@ pub enum VerifiedStatus {
|
||||
|
||||
impl Contact {
|
||||
pub fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
|
||||
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=?;",
|
||||
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=?;",
|
||||
params![contact_id as i32],
|
||||
|row| {
|
||||
let contact = Self {
|
||||
@@ -174,10 +156,21 @@ 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.
|
||||
@@ -709,6 +702,16 @@ 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
|
||||
@@ -742,6 +745,9 @@ impl Contact {
|
||||
if !self.name.is_empty() {
|
||||
return &self.name;
|
||||
}
|
||||
if !self.authname.is_empty() {
|
||||
return &self.authname;
|
||||
}
|
||||
&self.addr
|
||||
}
|
||||
|
||||
@@ -777,8 +783,11 @@ 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
|
||||
}
|
||||
|
||||
@@ -861,14 +870,14 @@ impl Contact {
|
||||
.unwrap_or_default() as usize
|
||||
}
|
||||
|
||||
pub fn get_origin_by_id(context: &Context, contact_id: u32, ret_blocked: &mut i32) -> Origin {
|
||||
pub fn get_origin_by_id(context: &Context, contact_id: u32, ret_blocked: &mut bool) -> Origin {
|
||||
let mut ret = Origin::Unknown;
|
||||
*ret_blocked = 0;
|
||||
*ret_blocked = false;
|
||||
|
||||
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 = 1;
|
||||
*ret_blocked = true;
|
||||
} else {
|
||||
ret = contact.origin;
|
||||
}
|
||||
@@ -957,6 +966,32 @@ 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)
|
||||
@@ -1021,6 +1056,18 @@ 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();
|
||||
@@ -1028,15 +1075,6 @@ 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)
|
||||
@@ -1120,6 +1158,18 @@ 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()
|
||||
|
||||
@@ -298,6 +298,11 @@ 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
@@ -49,7 +49,7 @@ impl Simplify {
|
||||
/**
|
||||
* Simplify Plain Text
|
||||
*/
|
||||
#[allow(non_snake_case, clippy::mut_range_bound)]
|
||||
#[allow(non_snake_case, clippy::mut_range_bound, clippy::needless_range_loop)]
|
||||
fn simplify_plain_text(&mut self, buf_terminated: &str, is_msgrmsg: bool) -> String {
|
||||
/* This function ...
|
||||
... removes all text after the line `-- ` (footer mark)
|
||||
|
||||
@@ -182,22 +182,6 @@ 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
|
||||
@@ -780,14 +764,6 @@ 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");
|
||||
|
||||
@@ -28,12 +28,14 @@ pub enum Error {
|
||||
InvalidMsgId,
|
||||
#[fail(display = "Watch folder not found {:?}", _0)]
|
||||
WatchFolderNotFound(String),
|
||||
#[fail(display = "Inalid Email: {:?}", _0)]
|
||||
#[fail(display = "Invalid 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>;
|
||||
|
||||
58
src/headerdef.rs
Normal file
58
src/headerdef.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@ 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,
|
||||
|
||||
@@ -91,7 +94,17 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
}
|
||||
match handle.done().await {
|
||||
// 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));
|
||||
}
|
||||
@@ -135,7 +148,17 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
}
|
||||
match handle.done().await {
|
||||
// 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));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use async_imap::{
|
||||
error::Result as ImapResult,
|
||||
types::{Fetch, Flag, Mailbox, Name, NameAttribute},
|
||||
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
|
||||
};
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use async_std::task;
|
||||
@@ -329,9 +329,7 @@ impl Imap {
|
||||
|
||||
/// Connects to imap account using already-configured parameters.
|
||||
pub fn connect_configured(&self, context: &Context) -> Result<()> {
|
||||
if async_std::task::block_on(async move {
|
||||
self.is_connected().await && !self.should_reconnect()
|
||||
}) {
|
||||
if async_std::task::block_on(self.is_connected()) && !self.should_reconnect() {
|
||||
return Ok(());
|
||||
}
|
||||
if !context.sql.get_raw_config_bool(context, "configured") {
|
||||
@@ -389,9 +387,14 @@ impl Imap {
|
||||
} 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));
|
||||
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;
|
||||
@@ -713,7 +716,17 @@ impl Imap {
|
||||
|
||||
if !is_deleted && msg.body().is_some() {
|
||||
let body = msg.body().unwrap_or_default();
|
||||
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32);
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@ 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_config, CertificateChecks};
|
||||
use crate::login_param::{dc_build_tls, CertificateChecks};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Client {
|
||||
@@ -36,9 +35,9 @@ impl Client {
|
||||
certificate_checks: CertificateChecks,
|
||||
) -> ImapResult<Self> {
|
||||
let stream = TcpStream::connect(addr).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 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 mut client = ImapClient::new(tls_stream);
|
||||
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
|
||||
client.debug = true;
|
||||
@@ -74,10 +73,10 @@ impl Client {
|
||||
) -> ImapResult<Client> {
|
||||
match self {
|
||||
Client::Insecure(client) => {
|
||||
let tls_config = dc_build_tls_config(certificate_checks);
|
||||
let tls: async_tls::TlsConnector = Arc::new(tls_config).into();
|
||||
let tls = dc_build_tls(certificate_checks)?;
|
||||
let tls_stream = tls.into();
|
||||
|
||||
let client_sec = client.secure(domain, &tls).await?;
|
||||
let client_sec = client.secure(domain, &tls_stream).await?;
|
||||
|
||||
Ok(Client::Secure(client_sec))
|
||||
}
|
||||
|
||||
40
src/job.rs
40
src/job.rs
@@ -167,13 +167,15 @@ impl Job {
|
||||
if let Some(recipients) = self.param.get(Param::Recipients) {
|
||||
let recipients_list = recipients
|
||||
.split('\x1e')
|
||||
.filter_map(|addr| match lettre::EmailAddress::new(addr.to_string()) {
|
||||
Ok(addr) => Some(addr),
|
||||
Err(err) => {
|
||||
warn!(context, "invalid recipient: {} {:?}", addr, err);
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(
|
||||
|addr| match async_smtp::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.
|
||||
@@ -198,11 +200,11 @@ impl Job {
|
||||
info!(context, "smtp-sending out mime message:");
|
||||
println!("{}", String::from_utf8_lossy(&body));
|
||||
}
|
||||
match smtp.send(context, recipients_list, body, self.job_id) {
|
||||
match task::block_on(smtp.send(context, recipients_list, body, self.job_id)) {
|
||||
Err(crate::smtp::send::Error::SendError(err)) => {
|
||||
// Remote error, retry later.
|
||||
smtp.disconnect();
|
||||
info!(context, "SMTP failed to send: {}", err);
|
||||
smtp.disconnect();
|
||||
self.try_again_later(TryAgain::AtOnce, Some(err.to_string()));
|
||||
}
|
||||
Err(crate::smtp::send::Error::EnvelopeError(err)) => {
|
||||
@@ -631,7 +633,15 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> {
|
||||
/* create message */
|
||||
let needs_encryption = msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, &msg)?;
|
||||
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 mut rendered_msg = mimefactory.render().map_err(|err| {
|
||||
message::set_msg_failed(context, msg_id, Some(err.to_string()));
|
||||
err
|
||||
@@ -668,8 +678,9 @@ 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);
|
||||
@@ -682,6 +693,13 @@ 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 == 0 {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
msg.save_param_to_disk(context);
|
||||
|
||||
14
src/lib.rs
14
src/lib.rs
@@ -3,16 +3,11 @@
|
||||
#![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
|
||||
clippy::too_many_arguments
|
||||
)]
|
||||
#![allow(clippy::unreadable_literal, clippy::match_bool)]
|
||||
#![feature(ptr_wrapping_offset_from)]
|
||||
#![feature(drain_filter)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate failure_derive;
|
||||
@@ -33,6 +28,8 @@ mod log;
|
||||
#[macro_use]
|
||||
pub mod error;
|
||||
|
||||
pub mod headerdef;
|
||||
|
||||
pub(crate) mod events;
|
||||
pub use events::*;
|
||||
|
||||
@@ -73,7 +70,6 @@ mod token;
|
||||
mod wrapmime;
|
||||
mod dehtml;
|
||||
|
||||
pub mod dc_array;
|
||||
pub mod dc_receive_imf;
|
||||
mod dc_simplify;
|
||||
pub mod dc_tools;
|
||||
|
||||
@@ -4,11 +4,6 @@ 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)]
|
||||
@@ -16,7 +11,11 @@ use webpki_roots;
|
||||
pub enum CertificateChecks {
|
||||
Automatic = 0,
|
||||
Strict = 1,
|
||||
AcceptInvalidHostnames = 2,
|
||||
|
||||
/// Same as AcceptInvalidCertificates
|
||||
/// Previously known as AcceptInvalidHostnames, now deprecated.
|
||||
AcceptInvalidCertificates2 = 2,
|
||||
|
||||
AcceptInvalidCertificates = 3,
|
||||
}
|
||||
|
||||
@@ -130,7 +129,7 @@ impl LoginParam {
|
||||
&self,
|
||||
context: &Context,
|
||||
prefix: impl AsRef<str>,
|
||||
) -> Result<(), Error> {
|
||||
) -> crate::sql::Result<()> {
|
||||
let prefix = prefix.as_ref();
|
||||
let sql = &context.sql;
|
||||
|
||||
@@ -259,49 +258,25 @@ fn get_readable_flags(flags: i32) -> String {
|
||||
res
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
pub fn dc_build_tls(
|
||||
certificate_checks: CertificateChecks,
|
||||
) -> Result<native_tls::TlsConnector, native_tls::Error> {
|
||||
let mut tls_builder = native_tls::TlsConnector::builder();
|
||||
match certificate_checks {
|
||||
CertificateChecks::Strict => {}
|
||||
CertificateChecks::Automatic => {
|
||||
// Same as AcceptInvalidCertificates for now.
|
||||
// TODO: use provider database when it becomes available
|
||||
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 {}));
|
||||
tls_builder
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.danger_accept_invalid_certs(true)
|
||||
}
|
||||
CertificateChecks::Strict => &mut tls_builder,
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => tls_builder
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.danger_accept_invalid_certs(true),
|
||||
}
|
||||
config
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -313,8 +288,8 @@ mod tests {
|
||||
use std::string::ToString;
|
||||
|
||||
assert_eq!(
|
||||
"accept_invalid_hostnames".to_string(),
|
||||
CertificateChecks::AcceptInvalidHostnames.to_string()
|
||||
"accept_invalid_certificates".to_string(),
|
||||
CertificateChecks::AcceptInvalidCertificates.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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::*;
|
||||
@@ -41,6 +42,7 @@ 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.
|
||||
@@ -62,7 +64,11 @@ pub struct RenderedEmail {
|
||||
}
|
||||
|
||||
impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
pub fn from_msg(context: &'a Context, msg: &'b Message) -> Result<MimeFactory<'a, 'b>, Error> {
|
||||
pub fn from_msg(
|
||||
context: &'a Context,
|
||||
msg: &'b Message,
|
||||
add_selfavatar: bool,
|
||||
) -> Result<MimeFactory<'a, 'b>, Error> {
|
||||
let chat = Chat::load_from_db(context, msg.chat_id)?;
|
||||
|
||||
let mut factory = MimeFactory {
|
||||
@@ -84,6 +90,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
references: String::default(),
|
||||
req_mdn: false,
|
||||
last_added_location_id: 0,
|
||||
attach_selfavatar: add_selfavatar,
|
||||
context,
|
||||
};
|
||||
|
||||
@@ -152,7 +159,10 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let in_reply_to: String = row.get(0)?;
|
||||
let references: String = row.get(1)?;
|
||||
|
||||
Ok((in_reply_to, references))
|
||||
Ok((
|
||||
render_rfc724_mid_list(&in_reply_to),
|
||||
render_rfc724_mid_list(&references),
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
@@ -207,6 +217,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
references: String::default(),
|
||||
req_mdn: false,
|
||||
last_added_location_id: 0,
|
||||
attach_selfavatar: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -293,9 +304,8 @@ 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
|
||||
if chat.gossiped_timestamp == 0
|
||||
|| (chat.gossiped_timestamp + (2 * 24 * 60 * 60)) > time()
|
||||
{
|
||||
let gossiped_timestamp = chat.get_gossiped_timestamp(self.context);
|
||||
if gossiped_timestamp == 0 || (gossiped_timestamp + (2 * 24 * 60 * 60)) > time() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -321,6 +331,15 @@ 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,
|
||||
@@ -382,7 +401,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let mut unprotected_headers: Vec<Header> = Vec::new();
|
||||
|
||||
let from = Address::new_mailbox_with_name(
|
||||
encode_words(&self.from_displayname),
|
||||
self.from_displayname.to_string(),
|
||||
self.from_addr.clone(),
|
||||
);
|
||||
|
||||
@@ -394,7 +413,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
} else {
|
||||
to.push(Address::new_mailbox_with_name(
|
||||
encode_words(name),
|
||||
name.to_string(),
|
||||
addr.clone(),
|
||||
));
|
||||
}
|
||||
@@ -443,7 +462,6 @@ 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();
|
||||
@@ -477,19 +495,31 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
Loaded::MDN => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
|
||||
};
|
||||
|
||||
protected_headers.push(Header::new("Message-ID".into(), rfc724_mid.clone()));
|
||||
// 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),
|
||||
));
|
||||
|
||||
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
|
||||
if do_gossip {
|
||||
// Add gossip headers in chats with multiple recipients
|
||||
if peerstates.len() > 1 && self.should_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -570,8 +600,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
message
|
||||
};
|
||||
|
||||
let is_gossiped = is_encrypted && do_gossip && !peerstates.is_empty();
|
||||
|
||||
let MimeFactory {
|
||||
recipients_addr,
|
||||
from_addr,
|
||||
@@ -608,6 +636,7 @@ 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()));
|
||||
@@ -648,6 +677,7 @@ 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();
|
||||
@@ -658,10 +688,17 @@ 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-Image".to_string(), "0".to_string()));
|
||||
protected_headers.push(Header::new(
|
||||
"Chat-Group-Avatar".to_string(),
|
||||
"0".to_string(),
|
||||
));
|
||||
}
|
||||
add_compatibility_header = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -729,7 +766,18 @@ 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-Image".into(), filename_as_sent));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
if self.msg.type_0 == Viewtype::Sticker {
|
||||
@@ -861,6 +909,19 @@ 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());
|
||||
@@ -1012,6 +1073,31 @@ 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 = 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() {
|
||||
@@ -1032,6 +1118,25 @@ 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
|
||||
******************************************************************************/
|
||||
@@ -1051,3 +1156,51 @@ 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::dc_simplify::*;
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee;
|
||||
use crate::error::Result;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::job::{job_add, Action};
|
||||
use crate::location;
|
||||
use crate::message;
|
||||
@@ -28,21 +29,38 @@ use crate::{bail, ensure};
|
||||
pub struct MimeParser<'a> {
|
||||
pub context: &'a Context,
|
||||
pub parts: Vec<Part>,
|
||||
pub header: HashMap<String, String>,
|
||||
pub subject: Option<String>,
|
||||
header: HashMap<String, String>,
|
||||
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 {
|
||||
@@ -73,9 +91,9 @@ impl<'a> MimeParser<'a> {
|
||||
let mut parser = MimeParser {
|
||||
parts: Vec::new(),
|
||||
header: Default::default(),
|
||||
subject: None,
|
||||
decrypting_failed: false,
|
||||
encrypted: false,
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signatures: Default::default(),
|
||||
gossipped_addr: Default::default(),
|
||||
is_forwarded: false,
|
||||
@@ -84,6 +102,8 @@ 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,
|
||||
};
|
||||
@@ -94,7 +114,8 @@ impl<'a> MimeParser<'a> {
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
parser.hash_header(&mail.headers);
|
||||
// init known headers with what mailparse provided us
|
||||
parser.merge_headers(&mail.headers);
|
||||
|
||||
// Memory location for a possible decrypted message.
|
||||
let mail_raw;
|
||||
@@ -102,7 +123,6 @@ 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 {
|
||||
@@ -120,6 +140,10 @@ 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
|
||||
@@ -147,54 +171,32 @@ impl<'a> MimeParser<'a> {
|
||||
}
|
||||
|
||||
fn parse_headers(&mut self) -> Result<()> {
|
||||
if let Some(field) = self.lookup_field("Subject") {
|
||||
self.subject = Some(field.clone());
|
||||
}
|
||||
|
||||
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.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 has_setup_file {
|
||||
if self.parts.len() == 1 {
|
||||
self.is_system_message = SystemMessage::AutocryptSetupMessage;
|
||||
|
||||
// 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 {
|
||||
warn!(self.context, "could not determine ASM mime-part");
|
||||
}
|
||||
} else if let Some(value) = self.lookup_field("Chat-Content") {
|
||||
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
|
||||
if value == "location-streaming-enabled" {
|
||||
self.is_system_message = SystemMessage::LocationStreamingEnabled;
|
||||
}
|
||||
}
|
||||
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::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 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 {
|
||||
@@ -225,7 +227,7 @@ impl<'a> MimeParser<'a> {
|
||||
std::mem::replace(&mut self.parts[0], filepart);
|
||||
}
|
||||
}
|
||||
if let Some(ref subject) = self.subject {
|
||||
if let Some(ref subject) = self.get_subject() {
|
||||
let mut prepend_subject = 1i32;
|
||||
if !self.decrypting_failed {
|
||||
let colon = subject.find(':');
|
||||
@@ -262,13 +264,13 @@ impl<'a> MimeParser<'a> {
|
||||
}
|
||||
if self.parts.len() == 1 {
|
||||
if self.parts[0].typ == Viewtype::Audio
|
||||
&& self.lookup_field("Chat-Voice-Message").is_some()
|
||||
&& self.get(HeaderDef::ChatVoiceMessage).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.lookup_field("Chat-Content") {
|
||||
if let Some(value) = self.get(HeaderDef::ChatContent) {
|
||||
if value == "sticker" {
|
||||
let part_mut = &mut self.parts[0];
|
||||
part_mut.typ = Viewtype::Sticker;
|
||||
@@ -280,7 +282,7 @@ impl<'a> MimeParser<'a> {
|
||||
|| part.typ == Viewtype::Voice
|
||||
|| part.typ == Viewtype::Video
|
||||
{
|
||||
if let Some(field_0) = self.lookup_field("Chat-Duration") {
|
||||
if let Some(field_0) = self.get(HeaderDef::ChatDuration) {
|
||||
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];
|
||||
@@ -290,12 +292,12 @@ impl<'a> MimeParser<'a> {
|
||||
}
|
||||
}
|
||||
if !self.decrypting_failed {
|
||||
if let Some(dn_field) = self.lookup_field("Chat-Disposition-Notification-To") {
|
||||
if let Some(dn_field) = self.get(HeaderDef::ChatDispositionNotificationTo) {
|
||||
if self.get_last_nonmeta().is_some() {
|
||||
let addrs = mailparse::addrparse(&dn_field).unwrap();
|
||||
|
||||
if let Some(dn_to_addr) = addrs.first() {
|
||||
if let Some(from_field) = self.lookup_field("From") {
|
||||
if let Some(from_field) = self.get(HeaderDef::From_) {
|
||||
let from_addrs = mailparse::addrparse(&from_field).unwrap();
|
||||
|
||||
if let Some(from_addr) = from_addrs.first() {
|
||||
@@ -316,7 +318,7 @@ impl<'a> MimeParser<'a> {
|
||||
let mut part = Part::default();
|
||||
part.typ = Viewtype::Text;
|
||||
|
||||
if let Some(ref subject) = self.subject {
|
||||
if let Some(ref subject) = self.get_subject() {
|
||||
if !self.has_chat_version() {
|
||||
part.msg = subject.to_string();
|
||||
}
|
||||
@@ -328,6 +330,29 @@ 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)
|
||||
}
|
||||
@@ -336,12 +361,31 @@ impl<'a> MimeParser<'a> {
|
||||
self.parts.iter_mut().rev().find(|part| !part.is_meta)
|
||||
}
|
||||
|
||||
pub fn was_encrypted(&self) -> bool {
|
||||
!self.signatures.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn has_chat_version(&self) -> bool {
|
||||
self.header.contains_key("chat-version")
|
||||
}
|
||||
|
||||
pub fn lookup_field(&self, field_name: &str) -> Option<&String> {
|
||||
self.header.get(&field_name.to_lowercase())
|
||||
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_mime_recursive(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
|
||||
@@ -354,12 +398,7 @@ impl<'a> MimeParser<'a> {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if self.parsed_protected_headers {
|
||||
warn!(self.context, "Ignoring nested protected headers");
|
||||
} else {
|
||||
self.hash_header(&mail.headers);
|
||||
self.parsed_protected_headers = true;
|
||||
}
|
||||
warn!(self.context, "Ignoring nested protected headers");
|
||||
}
|
||||
|
||||
enum MimeS {
|
||||
@@ -445,6 +484,9 @@ 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);
|
||||
|
||||
@@ -509,10 +551,6 @@ impl<'a> MimeParser<'a> {
|
||||
let raw_mime = mail.ctype.mimetype.to_lowercase();
|
||||
|
||||
let filename = get_attachment_filename(mail);
|
||||
info!(
|
||||
self.context,
|
||||
"add_single_part_if_known {:?} {:?} {:?}", mime_type, msg_type, filename
|
||||
);
|
||||
|
||||
let old_part_count = self.parts.len();
|
||||
|
||||
@@ -622,6 +660,7 @@ 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());
|
||||
@@ -631,50 +670,24 @@ impl<'a> MimeParser<'a> {
|
||||
}
|
||||
|
||||
fn do_add_single_part(&mut self, mut part: Part) {
|
||||
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);
|
||||
}
|
||||
if self.was_encrypted() {
|
||||
part.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
}
|
||||
self.parts.push(part);
|
||||
}
|
||||
|
||||
pub fn is_mailinglist_message(&self) -> bool {
|
||||
if self.lookup_field("List-Id").is_some() {
|
||||
if self.get(HeaderDef::ListId).is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(precedence) = self.lookup_field("Precedence") {
|
||||
if let Some(precedence) = self.get(HeaderDef::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;
|
||||
@@ -689,14 +702,14 @@ impl<'a> MimeParser<'a> {
|
||||
}
|
||||
|
||||
pub fn get_rfc724_mid(&self) -> Option<String> {
|
||||
// get Message-ID from header
|
||||
if let Some(field) = self.lookup_field("Message-ID") {
|
||||
return parse_message_id(field);
|
||||
if let Some(msgid) = self.get(HeaderDef::MessageId) {
|
||||
parse_message_id(msgid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn hash_header(&mut self, fields: &[mailparse::MailHeader<'_>]) {
|
||||
fn merge_headers(&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
|
||||
@@ -723,9 +736,10 @@ impl<'a> MimeParser<'a> {
|
||||
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
||||
|
||||
// must be present
|
||||
if let Some(_disposition) = report_fields.get_first_value("Disposition").ok().flatten() {
|
||||
let disp = HeaderDef::Disposition.get_headername();
|
||||
if let Some(_disposition) = report_fields.get_first_value(&disp).ok().flatten() {
|
||||
if let Some(original_message_id) = report_fields
|
||||
.get_first_value("Original-Message-ID")
|
||||
.get_first_value(&HeaderDef::OriginalMessageId.get_headername())
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| parse_message_id(&v))
|
||||
@@ -860,6 +874,7 @@ pub struct Part {
|
||||
pub msg_raw: Option<String>,
|
||||
pub bytes: usize,
|
||||
pub param: Params,
|
||||
org_filename: Option<String>,
|
||||
}
|
||||
|
||||
/// return mimetype and viewtype for a parsed mail
|
||||
@@ -935,7 +950,6 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<String> {
|
||||
acc += value;
|
||||
acc
|
||||
});
|
||||
println!("get_attachment_filename1: {:?}", desired_filename);
|
||||
|
||||
if desired_filename.is_empty() {
|
||||
if let Some(param) = ct.params.get("name") {
|
||||
@@ -943,7 +957,6 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<String> {
|
||||
desired_filename = param.to_string();
|
||||
}
|
||||
}
|
||||
println!("get_attachment_filename2: {:?}", desired_filename);
|
||||
|
||||
// if there is still no filename, guess one
|
||||
if desired_filename.is_empty() {
|
||||
@@ -953,7 +966,6 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<String> {
|
||||
bail!("could not determine filename: {:?}", ct.disposition);
|
||||
}
|
||||
}
|
||||
println!("get_attachment_filename3: {:?}", desired_filename);
|
||||
|
||||
Ok(desired_filename)
|
||||
}
|
||||
@@ -1016,7 +1028,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.subject, None);
|
||||
assert_eq!(mimeparser.get_subject(), None);
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
}
|
||||
|
||||
@@ -1104,14 +1116,14 @@ mod tests {
|
||||
let raw = b"From: hello\n\
|
||||
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
|
||||
Subject: outer-subject\n\
|
||||
X-Special-A: special-a\n\
|
||||
Foo: Bar\nChat-Version: 0.0\n\
|
||||
Secure-Join-Group: no\n\
|
||||
Test-Header: Bar\nChat-Version: 0.0\n\
|
||||
\n\
|
||||
--==break==\n\
|
||||
Content-Type: text/plain; protected-headers=\"v1\";\n\
|
||||
Subject: inner-subject\n\
|
||||
X-Special-B: special-b\n\
|
||||
Foo: Xy\n\
|
||||
SecureBar-Join-Group: yes\n\
|
||||
Test-Header: Xy\n\
|
||||
Chat-Version: 1.0\n\
|
||||
\n\
|
||||
test1\n\
|
||||
@@ -1119,18 +1131,65 @@ mod tests {
|
||||
--==break==--\n\
|
||||
\n\
|
||||
\x00";
|
||||
|
||||
let mimeparser = MimeParser::from_bytes(&context.ctx, &raw[..]).unwrap();
|
||||
|
||||
assert_eq!(mimeparser.subject, Some("inner-subject".into()));
|
||||
// non-overwritten headers do not bubble up
|
||||
let of = mimeparser.get(HeaderDef::SecureJoinGroup).unwrap();
|
||||
assert_eq!(of, "no");
|
||||
|
||||
let of = mimeparser.lookup_field("X-Special-A").unwrap();
|
||||
assert_eq!(of, "special-a");
|
||||
|
||||
let of = mimeparser.lookup_field("Foo").unwrap();
|
||||
// unknown headers do not bubble upwards
|
||||
let of = mimeparser.get(HeaderDef::_TestHeader).unwrap();
|
||||
assert_eq!(of, "Bar");
|
||||
|
||||
let of = mimeparser.lookup_field("Chat-Version").unwrap();
|
||||
assert_eq!(of, "1.0");
|
||||
// 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");
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
11
src/param.rs
11
src/param.rs
@@ -48,6 +48,10 @@ 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
|
||||
@@ -192,6 +196,11 @@ 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)
|
||||
@@ -250,7 +259,7 @@ impl Params {
|
||||
let file = ParamsFile::from_param(context, val)?;
|
||||
let blob = match file {
|
||||
ParamsFile::FsPath(path) => match create {
|
||||
true => BlobObject::create_from_path(context, path)?,
|
||||
true => BlobObject::new_from_path(context, path)?,
|
||||
false => BlobObject::from_path(context, path)?,
|
||||
},
|
||||
ParamsFile::Blob(blob) => blob,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,6 +11,7 @@ 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;
|
||||
@@ -350,7 +351,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
"handle_securejoin_handshake(): called with special contact id"
|
||||
);
|
||||
let step = mimeparser
|
||||
.lookup_field("Secure-Join")
|
||||
.get(HeaderDef::SecureJoin)
|
||||
.ok_or_else(|| format_err!("This message is not a Secure-Join message"))?;
|
||||
|
||||
info!(
|
||||
@@ -378,7 +379,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.lookup_field("Secure-Join-Invitenumber") {
|
||||
let invitenumber = match mimeparser.get(HeaderDef::SecureJoinInvitenumber) {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
warn!(context, "Secure-join denied (invitenumber missing).",);
|
||||
@@ -422,7 +423,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
if mimeparser.encrypted {
|
||||
if mimeparser.was_encrypted() {
|
||||
"No valid signature."
|
||||
} else {
|
||||
"Not encrypted."
|
||||
@@ -467,7 +468,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.lookup_field("Secure-Join-Fingerprint") {
|
||||
let fingerprint = match mimeparser.get(HeaderDef::SecureJoinFingerprint) {
|
||||
Some(fp) => fp,
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
@@ -496,7 +497,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.lookup_field("Secure-Join-Auth") {
|
||||
let auth_0 = match mimeparser.get(HeaderDef::SecureJoinAuth) {
|
||||
Some(auth) => auth,
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
@@ -526,7 +527,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
inviter_progress!(context, contact_id, 600);
|
||||
if join_vg {
|
||||
let field_grpid = mimeparser
|
||||
.lookup_field("Secure-Join-Group")
|
||||
.get(HeaderDef::SecureJoinGroup)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| "");
|
||||
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, field_grpid);
|
||||
@@ -600,10 +601,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
|
||||
.lookup_field("Chat-Group-Member-Added")
|
||||
.get(HeaderDef::ChatGroupMemberAdded)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| "");
|
||||
if join_vg && !addr_equals_self(context, cg_member_added) {
|
||||
if join_vg && !context.is_self_addr(cg_member_added)? {
|
||||
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
|
||||
return Ok(ret);
|
||||
}
|
||||
@@ -635,7 +636,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
inviter_progress!(context, contact_id, 800);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
let field_grpid = mimeparser
|
||||
.lookup_field("Secure-Join-Group")
|
||||
.get(HeaderDef::SecureJoinGroup)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| "");
|
||||
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid);
|
||||
@@ -717,7 +718,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.encrypted {
|
||||
if !mimeparser.was_encrypted() {
|
||||
warn!(mimeparser.context, "Message not encrypted.",);
|
||||
false
|
||||
} else if mimeparser.signatures.is_empty() {
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
pub mod send;
|
||||
|
||||
use lettre::smtp::client::net::*;
|
||||
use lettre::*;
|
||||
use async_smtp::smtp::client::net::*;
|
||||
use async_smtp::*;
|
||||
|
||||
use async_std::task;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
use crate::login_param::{dc_build_tls_config, LoginParam};
|
||||
use crate::login_param::{dc_build_tls, LoginParam};
|
||||
use crate::oauth2::*;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
@@ -19,14 +21,22 @@ pub enum Error {
|
||||
InvalidLoginAddress {
|
||||
address: String,
|
||||
#[cause]
|
||||
error: lettre::error::Error,
|
||||
error: async_smtp::error::Error,
|
||||
},
|
||||
#[fail(display = "SMTP failed to connect: {:?}", _0)]
|
||||
ConnectionFailure(#[cause] lettre::smtp::error::Error),
|
||||
ConnectionFailure(#[cause] async_smtp::smtp::error::Error),
|
||||
#[fail(display = "SMTP: failed to setup connection {:?}", _0)]
|
||||
ConnectionSetupFailure(#[cause] lettre::smtp::error::Error),
|
||||
ConnectionSetupFailure(#[cause] async_smtp::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>;
|
||||
@@ -34,8 +44,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
#[derive(Default, DebugStub)]
|
||||
pub struct Smtp {
|
||||
#[debug_stub(some = "SmtpTransport")]
|
||||
transport: Option<lettre::smtp::SmtpTransport>,
|
||||
transport_connected: bool,
|
||||
transport: Option<async_smtp::smtp::SmtpTransport>,
|
||||
/// Email address we are sending from.
|
||||
from: Option<EmailAddress>,
|
||||
}
|
||||
@@ -48,16 +57,12 @@ impl Smtp {
|
||||
|
||||
/// Disconnect the SMTP transport and drop it entirely.
|
||||
pub fn disconnect(&mut self) {
|
||||
if self.transport.is_none() || !self.transport_connected {
|
||||
return;
|
||||
if let Some(ref mut transport) = self.transport.take() {
|
||||
transport.close();
|
||||
}
|
||||
|
||||
let mut transport = self.transport.take().unwrap();
|
||||
transport.close();
|
||||
self.transport_connected = false;
|
||||
}
|
||||
|
||||
/// Check if a connection already exists.
|
||||
/// check whether we are connected
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.transport.is_some()
|
||||
}
|
||||
@@ -84,7 +89,7 @@ impl Smtp {
|
||||
let domain = &lp.send_server;
|
||||
let port = lp.send_port as u16;
|
||||
|
||||
let tls_config = dc_build_tls_config(lp.smtp_certificate_checks);
|
||||
let tls_config = dc_build_tls(lp.smtp_certificate_checks)?.into();
|
||||
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls_config);
|
||||
|
||||
let (creds, mechanism) = if 0 != lp.server_flags & (DC_LP_AUTH_OAUTH2 as i32) {
|
||||
@@ -99,21 +104,21 @@ impl Smtp {
|
||||
}
|
||||
let user = &lp.send_user;
|
||||
(
|
||||
lettre::smtp::authentication::Credentials::new(
|
||||
async_smtp::smtp::authentication::Credentials::new(
|
||||
user.to_string(),
|
||||
access_token.unwrap_or_default(),
|
||||
),
|
||||
vec![lettre::smtp::authentication::Mechanism::Xoauth2],
|
||||
vec![async_smtp::smtp::authentication::Mechanism::Xoauth2],
|
||||
)
|
||||
} else {
|
||||
// plain
|
||||
let user = lp.send_user.clone();
|
||||
let pw = lp.send_pw.clone();
|
||||
(
|
||||
lettre::smtp::authentication::Credentials::new(user, pw),
|
||||
async_smtp::smtp::authentication::Credentials::new(user, pw),
|
||||
vec![
|
||||
lettre::smtp::authentication::Mechanism::Plain,
|
||||
lettre::smtp::authentication::Mechanism::Login,
|
||||
async_smtp::smtp::authentication::Mechanism::Plain,
|
||||
async_smtp::smtp::authentication::Mechanism::Login,
|
||||
],
|
||||
)
|
||||
};
|
||||
@@ -121,24 +126,26 @@ impl Smtp {
|
||||
let security = if 0
|
||||
!= lp.server_flags & (DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_PLAIN) as i32
|
||||
{
|
||||
lettre::smtp::ClientSecurity::Opportunistic(tls_parameters)
|
||||
async_smtp::smtp::ClientSecurity::Opportunistic(tls_parameters)
|
||||
} else {
|
||||
lettre::smtp::ClientSecurity::Wrapper(tls_parameters)
|
||||
async_smtp::smtp::ClientSecurity::Wrapper(tls_parameters)
|
||||
};
|
||||
|
||||
let client = lettre::smtp::SmtpClient::new((domain.as_str(), port), security)
|
||||
.map_err(Error::ConnectionSetupFailure)?;
|
||||
let client = task::block_on(async_smtp::smtp::SmtpClient::with_security(
|
||||
(domain.as_str(), port),
|
||||
security,
|
||||
))
|
||||
.map_err(Error::ConnectionSetupFailure)?;
|
||||
|
||||
let client = client
|
||||
.smtp_utf8(true)
|
||||
.credentials(creds)
|
||||
.authentication_mechanism(mechanism)
|
||||
.connection_reuse(lettre::smtp::ConnectionReuseParameters::ReuseUnlimited);
|
||||
let mut trans = client.transport();
|
||||
trans.connect().map_err(Error::ConnectionFailure)?;
|
||||
.connection_reuse(async_smtp::smtp::ConnectionReuseParameters::ReuseUnlimited);
|
||||
let mut trans = client.into_transport();
|
||||
task::block_on(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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! # SMTP message sending
|
||||
|
||||
use super::Smtp;
|
||||
use lettre::*;
|
||||
use async_smtp::*;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
@@ -11,9 +11,9 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "Envelope error: {}", _0)]
|
||||
EnvelopeError(#[cause] lettre::error::Error),
|
||||
EnvelopeError(#[cause] async_smtp::error::Error),
|
||||
#[fail(display = "Send error: {}", _0)]
|
||||
SendError(#[cause] lettre::smtp::error::Error),
|
||||
SendError(#[cause] async_smtp::smtp::error::Error),
|
||||
#[fail(display = "SMTP has no transport")]
|
||||
NoTransport,
|
||||
}
|
||||
@@ -21,7 +21,7 @@ pub enum Error {
|
||||
impl Smtp {
|
||||
/// Send a prepared mail to recipients.
|
||||
/// On successful send out Ok() is returned.
|
||||
pub fn send(
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
recipients: Vec<EmailAddress>,
|
||||
@@ -36,22 +36,21 @@ 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 {
|
||||
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)?;
|
||||
transport.send(mail).await.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!(
|
||||
|
||||
13
src/sql.rs
13
src/sql.rs
@@ -7,7 +7,7 @@ use std::time::Duration;
|
||||
use rusqlite::{Connection, OpenFlags, Statement, NO_PARAMS};
|
||||
use thread_local_object::ThreadLocal;
|
||||
|
||||
use crate::chat::update_saved_messages_icon;
|
||||
use crate::chat::{update_device_icon, update_saved_messages_icon};
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
@@ -848,7 +848,6 @@ 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 {
|
||||
@@ -859,6 +858,15 @@ 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)
|
||||
@@ -884,6 +892,7 @@ fn open(
|
||||
}
|
||||
if update_icons {
|
||||
update_saved_messages_icon(context)?;
|
||||
update_device_icon(context)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
55
test-data/message/mail_with_user_and_group_avatars.eml
Normal file
55
test-data/message/mail_with_user_and_group_avatars.eml
Normal file
@@ -0,0 +1,55 @@
|
||||
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--
|
||||
37
test-data/message/mail_with_user_avatar.eml
Normal file
37
test-data/message/mail_with_user_avatar.eml
Normal file
@@ -0,0 +1,37 @@
|
||||
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--
|
||||
14
test-data/message/mail_with_user_avatar_deleted.eml
Normal file
14
test-data/message/mail_with_user_avatar_deleted.eml
Normal file
@@ -0,0 +1,14 @@
|
||||
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
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use deltachat::chat::{self, Chat};
|
||||
use deltachat::config;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::keyring::*;
|
||||
use deltachat::pgp;
|
||||
@@ -227,20 +225,3 @@ 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user