Compare commits

..

6 Commits

Author SHA1 Message Date
holger krekel
0dc87e9a0f remove one attr from mimeparser 2019-12-07 22:41:43 +01:00
holger krekel
c011a8cfef run lint without installing the package 2019-12-07 21:44:34 +01:00
holger krekel
2d7d79fc44 allow to use a different buildhost :) 2019-12-07 21:44:34 +01:00
holger krekel
119d4fa689 - test and fix receiving text/html attachment in multipart/mixed situations
They are now preserved as attachment, instead of diving into parsing-html
  and simplifying.

- adapt mime-debugging
2019-12-07 21:44:34 +01:00
holger krekel
b7a9f73329 passes test but needs cleanup 2019-12-07 21:44:34 +01:00
holger krekel
a52a3e0d24 try fix incoming text/html in multipart/mixed 2019-12-07 21:44:34 +01:00
64 changed files with 1706 additions and 2928 deletions

View File

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

View File

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

View File

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

853
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.0.0-beta.19"
version = "1.0.0-beta.12"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"

View File

@@ -3645,15 +3645,11 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Check if a contact was verified. E.g. by a secure-join QR code scan
* and if the key has not changed since this verification.
* Same as dc_contact_is_verified() but allows speeding up things
* by adding the peerstate belonging to the contact.
* If you do not have the peerstate available, it is loaded automatically.
*
* The UI may draw a checkbox or something like that beside verified contacts.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0: contact is not verified.
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
* @private @memberof dc_context_t
*/
int dc_contact_is_verified (dc_contact_t* contact);
@@ -4054,6 +4050,11 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_CERTCK_STRICT 1
/**
* Accept invalid hostnames, but not invalid certificates.
*/
#define DC_CERTCK_ACCEPT_INVALID_HOSTNAMES 2
/**
* Accept invalid certificates, including self-signed ones
* or having incorrect hostname.

View File

@@ -29,8 +29,6 @@ use deltachat::message::MsgId;
use deltachat::stock::StockMessage;
use deltachat::*;
mod dc_array;
mod string;
use self::string::*;
@@ -509,7 +507,7 @@ pub unsafe extern "C" fn dc_interrupt_imap_idle(context: *mut dc_context_t) {
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_inbox_idle(ctx))
.with_inner(|ctx| job::interrupt_inbox_idle(ctx, true))
.unwrap_or(())
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -174,7 +174,7 @@ class Message(object):
@property
def _msgstate(self):
if self.id == 0:
dc_msg = self._dc_msg
dc_msg = self.message._dc_msg
else:
# load message from db to get a fresh/current state
dc_msg = ffi.gc(

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

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

View File

@@ -65,8 +65,8 @@ commands =
[pytest]
addopts = -v -ra
python_files = tests/test_*.py
addopts = -v -rs
python_files = tests/test_*.py
norecursedirs = .tox
xfail_strict=true
timeout = 60

View File

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

26
spec.md
View File

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

View File

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

View File

@@ -35,6 +35,7 @@ pub struct Chat {
pub grpid: String,
blocked: Blocked,
pub param: Params,
pub gossiped_timestamp: i64,
is_sending_locations: bool,
}
@@ -43,7 +44,7 @@ impl Chat {
pub fn load_from_db(context: &Context, chat_id: u32) -> Result<Self, Error> {
let res = context.sql.query_row(
"SELECT c.id,c.type,c.name, c.grpid,c.param,c.archived, \
c.blocked, c.locations_send_until \
c.blocked, c.gossiped_timestamp, c.locations_send_until \
FROM chats c WHERE c.id=?;",
params![chat_id as i32],
|row| {
@@ -55,7 +56,8 @@ impl Chat {
param: row.get::<_, String>(4)?.parse().unwrap_or_default(),
archived: row.get(5)?,
blocked: row.get::<_, Option<_>>(6)?.unwrap_or_default(),
is_sending_locations: row.get(7)?,
gossiped_timestamp: row.get(7)?,
is_sending_locations: row.get(8)?,
};
Ok(c)
@@ -116,12 +118,10 @@ impl Chat {
self.param.exists(Param::Selftalk)
}
/// Returns true if chat is a device chat.
pub fn is_device_talk(&self) -> bool {
self.param.exists(Param::Devicetalk)
}
/// Returns true if user can send messages to this chat.
pub fn can_send(&self) -> bool {
self.id > DC_CHAT_ID_LAST_SPECIAL && !self.is_device_talk()
}
@@ -136,17 +136,14 @@ impl Chat {
Ok(())
}
/// Returns chat ID.
pub fn get_id(&self) -> u32 {
self.id
}
/// Returns chat type.
pub fn get_type(&self) -> Chattype {
self.typ
}
/// Returns chat name.
pub fn get_name(&self) -> &str {
&self.name
}
@@ -172,7 +169,7 @@ impl Chat {
}
if self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup {
if self.id == DC_CHAT_ID_DEADDROP {
if self.id == 1 {
return context.stock_str(StockMessage::DeadDrop).into();
}
let cnt = get_chat_contact_cnt(context, self.id);
@@ -219,10 +216,6 @@ impl Chat {
None
}
pub fn get_gossiped_timestamp(&self, context: &Context) -> i64 {
get_gossiped_timestamp(context, self.id)
}
pub fn get_color(&self, context: &Context) -> u32 {
let mut color = 0;
@@ -253,12 +246,10 @@ impl Chat {
!self.is_unpromoted()
}
/// Returns true if chat is a verified group chat.
pub fn is_verified(&self) -> bool {
self.typ == Chattype::VerifiedGroup
(self.typ == Chattype::VerifiedGroup)
}
/// Returns true if location streaming is enabled in the chat.
pub fn is_sending_locations(&self) -> bool {
self.is_sending_locations
}
@@ -319,10 +310,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)?;
}
@@ -602,6 +593,12 @@ pub fn set_blocking(context: &Context, chat_id: u32, new_blocking: Blocked) -> b
.is_ok()
}
fn copy_device_icon_to_blobs(context: &Context) -> Result<String, Error> {
let icon = include_bytes!("../assets/icon-device.png");
let blob = BlobObject::create(context, "icon-device.png".to_string(), icon)?;
Ok(blob.as_name().to_string())
}
pub fn update_saved_messages_icon(context: &Context) -> Result<(), Error> {
// if there is no saved-messages chat, there is nothing to update. this is no error.
if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_SELF) {
@@ -616,24 +613,6 @@ pub fn update_saved_messages_icon(context: &Context) -> Result<(), Error> {
Ok(())
}
pub fn update_device_icon(context: &Context) -> Result<(), Error> {
// if there is no device-chat, there is nothing to update. this is no error.
if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE) {
let icon = include_bytes!("../assets/icon-device.png");
let blob = BlobObject::create(context, "icon-device.png".to_string(), icon)?;
let icon = blob.as_name().to_string();
let mut chat = Chat::load_from_db(context, chat_id)?;
chat.param.set(Param::ProfileImage, &icon);
chat.update_param(context)?;
let mut contact = Contact::load_from_db(context, DC_CONTACT_ID_DEVICE)?;
contact.param.set(Param::ProfileImage, icon);
contact.update_param(context)?;
}
Ok(())
}
pub fn create_or_lookup_by_contact_id(
context: &Context,
contact_id: u32,
@@ -653,19 +632,23 @@ pub fn create_or_lookup_by_contact_id(
sql::execute(
context,
&context.sql,
"INSERT INTO chats (type, name, param, blocked, grpid, created_timestamp) VALUES(?, ?, ?, ?, ?, ?)",
params![
format!(
"INSERT INTO chats (type, name, param, blocked, grpid, created_timestamp) VALUES({}, '{}', '{}', {}, '{}', {})",
100,
chat_name,
match contact_id {
DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk
DC_CONTACT_ID_DEVICE => "D=1".to_string(), // D = Param::Devicetalk
DC_CONTACT_ID_DEVICE => {
let icon = copy_device_icon_to_blobs(context)?;
format!("D=1\ni={}", icon) // D = Param::Devicetalk, i = Param::ProfileImage
},
_ => "".to_string()
},
create_blocked as u8,
contact.get_addr(),
time(),
]
),
params![],
)?;
let chat_id = sql::get_rowid(context, &context.sql, "chats", "grpid", contact.get_addr());
@@ -673,14 +656,15 @@ pub fn create_or_lookup_by_contact_id(
sql::execute(
context,
&context.sql,
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
params![chat_id, contact_id],
format!(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES({}, {})",
chat_id, contact_id
),
params![],
)?;
if contact_id == DC_CONTACT_ID_SELF {
update_saved_messages_icon(context)?;
} else if contact_id == DC_CONTACT_ID_DEVICE {
update_device_icon(context)?;
}
Ok((chat_id, create_blocked))
@@ -741,11 +725,20 @@ fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> {
if msg.type_0 == Viewtype::Text {
// the caller should check if the message text is empty
} else if msgtype_has_file(msg.type_0) {
let blob = msg
.param
.get_blob(Param::File, context, !msg.is_increation())?
.ok_or_else(|| format_err!("Attachment missing for message of type #{}", msg.type_0))?;
msg.param.set(Param::File, blob.as_name());
let blob = if let Some(f) = msg.param.get_file(Param::File, context)? {
match f {
ParamsFile::Blob(blob) => blob,
ParamsFile::FsPath(path) => {
// path is outside the blobdir, let's copy
let blob = BlobObject::create_and_copy(context, path)?;
msg.param.set(Param::File, blob.as_name());
blob
}
}
} else {
bail!("Attachment missing for message of type #{}", msg.type_0);
};
if msg.type_0 == Viewtype::File || msg.type_0 == Viewtype::Image {
// Correct the type, take care not to correct already very special
@@ -821,7 +814,6 @@ fn last_msg_in_chat_encrypted(context: &Context, sql: &Sql, chat_id: u32) -> boo
}
}
/// Returns whether a contact is in a chat or not.
pub fn is_contact_in_chat(context: &Context, chat_id: u32, contact_id: u32) -> bool {
/* this function works for group and for normal chats, however, it is more useful for group chats.
DC_CONTACT_ID_SELF may be used to check, if the user itself is in a group chat (DC_CONTACT_ID_SELF is not added to normal chats) */
@@ -1112,7 +1104,6 @@ pub fn get_chat_msgs(
}
}
/// Returns number of messages in a chat.
pub fn get_msg_cnt(context: &Context, chat_id: u32) -> usize {
context
.sql
@@ -1283,7 +1274,6 @@ pub fn get_next_media(
ret
}
/// Archives or unarchives a chat.
pub fn archive(context: &Context, chat_id: u32, archive: bool) -> Result<(), Error> {
ensure!(
chat_id > DC_CHAT_ID_LAST_SPECIAL,
@@ -1319,7 +1309,6 @@ pub fn archive(context: &Context, chat_id: u32, archive: bool) -> Result<(), Err
Ok(())
}
/// Deletes a chat.
pub fn delete(context: &Context, chat_id: u32) -> Result<(), Error> {
ensure!(
chat_id > DC_CHAT_ID_LAST_SPECIAL,
@@ -1450,7 +1439,6 @@ pub fn add_to_chat_contacts_table(context: &Context, chat_id: u32, contact_id: u
.is_ok()
}
/// Adds a contact to the chat.
pub fn add_contact_to_chat(context: &Context, chat_id: u32, contact_id: u32) -> bool {
match add_contact_to_chat_ex(context, chat_id, contact_id, false) {
Ok(res) => res,
@@ -1475,7 +1463,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)?;
@@ -1573,28 +1561,12 @@ fn real_group_exists(context: &Context, chat_id: u32) -> bool {
.unwrap_or_default()
}
pub fn reset_gossiped_timestamp(context: &Context, chat_id: u32) -> crate::sql::Result<()> {
set_gossiped_timestamp(context, chat_id, 0)
pub fn reset_gossiped_timestamp(context: &Context, chat_id: u32) {
set_gossiped_timestamp(context, chat_id, 0);
}
/// Get timestamp of the last gossip sent in the chat.
/// Zero return value means that gossip was never sent.
pub fn get_gossiped_timestamp(context: &Context, chat_id: u32) -> i64 {
context
.sql
.query_get_value::<_, i64>(
context,
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
params![chat_id as i32],
)
.unwrap_or_default()
}
pub fn set_gossiped_timestamp(
context: &Context,
chat_id: u32,
timestamp: i64,
) -> crate::sql::Result<()> {
// Should return Result
pub fn set_gossiped_timestamp(context: &Context, chat_id: u32, timestamp: i64) {
if 0 != chat_id {
info!(
context,
@@ -1607,6 +1579,7 @@ pub fn set_gossiped_timestamp(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
params![timestamp, chat_id as i32],
)
.ok();
} else {
info!(
context,
@@ -1618,58 +1591,10 @@ pub fn set_gossiped_timestamp(
"UPDATE chats SET gossiped_timestamp=?;",
params![timestamp],
)
.ok();
}
}
pub fn shall_attach_selfavatar(context: &Context, chat_id: u32) -> Result<bool, Error> {
// versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others.
// to avoid sending out previously set selfavatars unexpectedly we added this additional check.
// it can be removed after some time.
if !context
.sql
.get_raw_config_bool(context, "attach_selfavatar")
{
return Ok(false);
}
let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60;
let needs_attach = context.sql.query_map(
"SELECT c.selfavatar_sent
FROM chats_contacts cc
LEFT JOIN contacts c ON c.id=cc.contact_id
WHERE cc.chat_id=? AND cc.contact_id!=?;",
params![chat_id, DC_CONTACT_ID_SELF],
|row| Ok(row.get::<_, i64>(0)),
|rows| {
let mut needs_attach = false;
for row in rows {
if let Ok(selfavatar_sent) = row {
let selfavatar_sent = selfavatar_sent?;
if selfavatar_sent < timestamp_some_days_ago {
needs_attach = true;
}
}
}
Ok(needs_attach)
},
)?;
Ok(needs_attach)
}
pub fn set_selfavatar_timestamp(
context: &Context,
chat_id: u32,
timestamp: i64,
) -> Result<(), Error> {
context.sql.execute(
"UPDATE contacts
SET selfavatar_sent=?
WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);",
params![timestamp, chat_id],
)?;
Ok(())
}
pub fn remove_contact_from_chat(
context: &Context,
chat_id: u32,
@@ -1883,7 +1808,6 @@ pub fn set_chat_profile_image(
_ => Err(err),
},
)?;
image_blob.recode_to_avatar_size(context)?;
chat.param.set(Param::ProfileImage, image_blob.as_name());
msg.param.set(Param::Arg, image_blob.as_name());
msg.text = Some(context.stock_system_msg(
@@ -2027,7 +1951,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.get_gossiped_timestamp(context),
"gossiped_timestamp": chat.gossiped_timestamp,
"is_sending_locations": chat.is_sending_locations,
"color": chat.get_color(context),
"profile_image": profile_image,
@@ -2082,9 +2006,6 @@ pub fn get_chat_id_by_grpid(context: &Context, grpid: impl AsRef<str>) -> (u32,
.unwrap_or((0, false, Blocked::Not))
}
/// Adds a message to device chat.
///
/// Optional `label` can be provided to ensure that message is added only once.
pub fn add_device_msg(
context: &Context,
label: Option<&str>,
@@ -2159,9 +2080,6 @@ pub fn was_device_msg_ever_added(context: &Context, label: &str) -> Result<bool,
Ok(false)
}
/// Adds an informational message to chat.
///
/// For example, it can be a message showing that a member was added to a group.
pub fn add_info_msg(context: &Context, chat_id: u32, text: impl AsRef<str>) {
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
@@ -2494,42 +2412,4 @@ mod tests {
"bar"
);
}
#[test]
fn test_create_same_chat_twice() {
let context = dummy_context();
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap();
assert_ne!(contact1, 0);
let chat_id = create_by_contact_id(&context.ctx, contact1).unwrap();
assert!(
chat_id > DC_CHAT_ID_LAST_SPECIAL,
"chat_id too small {}",
chat_id
);
let chat = Chat::load_from_db(&context.ctx, chat_id).unwrap();
let chat2_id = create_by_contact_id(&context.ctx, contact1).unwrap();
assert_eq!(chat2_id, chat_id);
let chat2 = Chat::load_from_db(&context.ctx, chat2_id).unwrap();
assert_eq!(chat2.name, chat.name);
}
#[test]
fn test_shall_attach_selfavatar() {
let t = dummy_context();
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap();
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
let (contact_id, _) =
Contact::add_or_lookup(&t.ctx, "", "foo@bar.org", Origin::IncomingUnknownTo).unwrap();
add_contact_to_chat(&t.ctx, chat_id, contact_id);
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
t.ctx.set_config(Config::Selfavatar, None).unwrap(); // setting to None also forces re-sending
assert!(shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
assert!(set_selfavatar_timestamp(&t.ctx, chat_id, time()).is_ok());
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,9 +18,6 @@ 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,
@@ -94,17 +91,7 @@ impl Imap {
}
}
}
// 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 {
match handle.done().await {
Ok(session) => {
*self.session.lock().await = Some(Session::Secure(session));
}
@@ -148,17 +135,7 @@ impl Imap {
}
}
}
// 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 {
match handle.done().await {
Ok(session) => {
*self.session.lock().await = Some(Session::Insecure(session));
}
@@ -241,7 +218,7 @@ impl Imap {
"IMAP-fake-IDLE done after {:.4}s",
SystemTime::now()
.duration_since(fake_idle_start_time)
.unwrap_or_default()
.unwrap()
.as_millis() as f64
/ 1000.,
);

View File

@@ -7,7 +7,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use async_imap::{
error::Result as ImapResult,
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
types::{Fetch, Flag, Mailbox, Name, NameAttribute},
};
use async_std::sync::{Mutex, RwLock};
use async_std::task;
@@ -23,6 +23,7 @@ use crate::message::{self, update_server_uid};
use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::stock::StockMessage;
use crate::wrapmime;
mod idle;
pub mod select_folder;
@@ -63,9 +64,6 @@ pub enum Error {
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[fail(display = "No mailbox selected, folder: {:?}", _0)]
NoMailbox(String),
#[fail(display = "IMAP other error: {:?}", _0)]
Other(String),
}
@@ -331,7 +329,9 @@ impl Imap {
/// Connects to imap account using already-configured parameters.
pub fn connect_configured(&self, context: &Context) -> Result<()> {
if async_std::task::block_on(self.is_connected()) && !self.should_reconnect() {
if async_std::task::block_on(async move {
self.is_connected().await && !self.should_reconnect()
}) {
return Ok(());
}
if !context.sql.get_raw_config_bool(context, "configured") {
@@ -341,7 +341,7 @@ impl Imap {
let param = LoginParam::from_database(context, "configured_");
// the trailing underscore is correct
if task::block_on(self.connect(context, &param)) {
if self.connect(context, &param) {
self.ensure_configured_folders(context, true)
} else {
Err(Error::ConnectionFailed(format!("{}", param)))
@@ -350,80 +350,77 @@ impl Imap {
/// tries connecting to imap account using the specific login
/// parameters
pub async fn connect(&self, context: &Context, lp: &LoginParam) -> bool {
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
return false;
}
pub fn connect(&self, context: &Context, lp: &LoginParam) -> bool {
task::block_on(async move {
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
return false;
}
{
let addr = &lp.addr;
let imap_server = &lp.mail_server;
let imap_port = lp.mail_port as u16;
let imap_user = &lp.mail_user;
let imap_pw = &lp.mail_pw;
let server_flags = lp.server_flags as usize;
{
let addr = &lp.addr;
let imap_server = &lp.mail_server;
let imap_port = lp.mail_port as u16;
let imap_user = &lp.mail_user;
let imap_pw = &lp.mail_pw;
let server_flags = lp.server_flags as usize;
let mut config = self.config.write().await;
config.addr = addr.to_string();
config.imap_server = imap_server.to_string();
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
config.certificate_checks = lp.imap_certificate_checks;
config.server_flags = server_flags;
}
let mut config = self.config.write().await;
config.addr = addr.to_string();
config.imap_server = imap_server.to_string();
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
config.certificate_checks = lp.imap_certificate_checks;
config.server_flags = server_flags;
}
if let Err(err) = self.setup_handle_if_needed(context).await {
warn!(context, "failed to setup imap handle: {}", err);
self.free_connect_params().await;
return false;
}
if let Err(err) = self.setup_handle_if_needed(context).await {
warn!(context, "failed to setup imap handle: {}", err);
self.free_connect_params().await;
return false;
}
let teardown = match &mut *self.session.lock().await {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open() {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,);
true
} else {
let can_idle = caps.has_str("IDLE");
let has_xlist = caps.has_str("XLIST");
let caps_list = caps.iter().fold(String::new(), |s, c| {
if let Capability::Atom(x) = c {
s + &format!(" {}", x)
} else {
s + &format!(" {:?}", c)
}
});
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
Event::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.mail_user, caps_list,
))
);
false
let teardown = match &mut *self.session.lock().await {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open() {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,);
true
} else {
let can_idle = caps.has_str("IDLE");
let has_xlist = caps.has_str("XLIST");
let caps_list = caps
.iter()
.fold(String::new(), |s, c| s + &format!(" {:?}", c));
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
Event::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.mail_user, caps_list,
))
);
false
}
}
}
Err(err) => {
info!(context, "CAPABILITY command error: {}", err);
true
}
},
None => true,
};
Err(err) => {
info!(context, "CAPABILITY command error: {}", err);
true
}
},
None => true,
};
if teardown {
self.disconnect(context);
if teardown {
self.disconnect(context);
false
} else {
true
}
false
} else {
true
}
})
}
pub fn disconnect(&self, context: &Context) {
@@ -481,10 +478,7 @@ impl Imap {
let (uid_validity, last_seen_uid) = self.get_config_last_seen_uid(context, &folder);
let config = self.config.read().await;
let mailbox = config
.selected_mailbox
.as_ref()
.ok_or_else(|| Error::NoMailbox(folder.to_string()))?;
let mailbox = config.selected_mailbox.as_ref().expect("just selected");
let new_uid_validity = match mailbox.uid_validity {
Some(v) => v,
@@ -719,17 +713,7 @@ impl Imap {
if !is_deleted && msg.body().is_some() {
let body = msg.body().unwrap_or_default();
if let Err(err) =
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32)
{
warn!(
context,
"dc_receive_imf failed for imap-message {}/{}: {:?}",
folder.as_ref(),
server_uid,
err
);
}
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32);
}
}
@@ -1228,18 +1212,6 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
}
}
fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> {
let value = std::str::from_utf8(message_id)?;
let addrs = mailparse::addrparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
if let Some(info) = addrs.extract_single_info() {
return Ok(info.addr);
}
bail!("could not parse message_id: {}", value);
}
fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
if prefetch_msg.envelope().is_none() {
return Err(Error::Other(
@@ -1252,22 +1224,5 @@ fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
return Err(Error::Other("prefetch: No message ID found".to_string()));
}
parse_message_id(&message_id.unwrap()).map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_message_id() {
assert_eq!(
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
assert_eq!(
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
}
wrapmime::parse_message_id(&message_id.unwrap()).map_err(Into::into)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

59
src/wrapmime.rs Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

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

View File

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

View File

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

View File

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