Compare commits

..

55 Commits

Author SHA1 Message Date
Alexander Krotov
b0a1f7b421 Update mailparse to parse names with comma 2020-02-12 23:08:45 +03:00
holger krekel
5c17ec5f01 use new URL format and service provided by mailadm 2020-02-11 22:48:24 +01:00
B. Petersen
8b4edc46a7 implement dc_set_config_from_qr() 2020-02-11 21:04:18 +01:00
B. Petersen
2b7a0a4585 prototype dc_set_config_from_qr() 2020-02-11 21:04:18 +01:00
B. Petersen
1882176489 let dc_check_qr() accept DCACCOUNT-schemes 2020-02-11 21:04:18 +01:00
holger krekel
875e89e71a better event information for moved messages 2020-02-11 20:38:14 +01:00
Alexander Krotov
52520635ea Fix a typo ("requests") 2020-02-11 22:32:35 +03:00
Friedel Ziegelmayer
aa50a9ba83 Merge pull request #1258 from deltachat/cargo-check-ci
GitHub actions: check all packages, examples, tests and features
2020-02-11 11:50:26 +01:00
Alexander Krotov
489e5111ac GitHub actions: check all packages, examples, tests and features 2020-02-11 02:09:47 +03:00
Alexander Krotov
2d4c20af35 Revert "Make dc_receive_imf non-public"
This reverts commit cabb58a9aa.

dc_receive_imf() is used in the "repl" example.
This was not caught by CI because it does not build examples.
2020-02-11 02:03:36 +03:00
Alexander Krotov
66fdb447f7 Rename get_headerdef into get_header_value 2020-02-11 01:59:41 +03:00
Alexander Krotov
c801775a39 Implement get_headerdef method for MailHeader slices 2020-02-11 01:59:41 +03:00
Alexander Krotov
f5bb57d6a6 Fix a typo ("Messag") 2020-02-11 00:07:37 +03:00
Alexander Krotov
1071ab05db Move created_db_entries into cleanup closure instead of borrowing 2020-02-10 23:31:46 +03:00
Simon Laux
8461cf6443 Merge pull request #1143 from deltachat/feat_mute_chat
feat: mute chat
2020-02-10 01:39:50 +01:00
Alexander Krotov
bb10501f56 Improve test_one_account_send_bcc_setting
Clone the first account and check that second device actually sees the
message copied via BCC-to-self.
2020-02-10 01:17:28 +01:00
Alexander Krotov
c4d5f657da python tests: remove peek_online_config()
Using peek_online_config() results in a message being sent to some
other account, used by previous test. If tests are run in parallel,
for example with pytest-xdist, we could be sending a message to a still
online DC client.
2020-02-10 01:17:28 +01:00
Alexander Krotov
cabb58a9aa Make dc_receive_imf non-public 2020-02-09 23:30:06 +00:00
Alexander Krotov
e64ce5bb4f Reduce is_msgrmsg_rfc724_mid_in_list scope to pub(crate) 2020-02-09 23:28:29 +00:00
Alexander Krotov
bc750d61d2 Remove outdated comment 2020-02-09 23:28:29 +00:00
Alexander Krotov
3150901b6e Improve logs for prefetch checks 2020-02-09 23:28:29 +00:00
Alexander Krotov
4f6745b742 Check if contact is blocked or accepted before downloading it 2020-02-09 23:28:29 +00:00
Alexander Krotov
bcdc323a97 Factor from_field_to_contact_id out of dc_receive_imf 2020-02-09 23:28:29 +00:00
Alexander Krotov
4fd4cc709d Prefetch FROM field 2020-02-09 23:28:29 +00:00
Alexander Krotov
0fac463144 Prefetch Message-ID header instead of envelope
Envelope has the same Message-ID, but using other fields from the
envelope, such as From field, is error-prone. They may contain values
different from the message body. As we don't parse the envelope later on,
it is better not to fetch it during prefetch too.
2020-02-09 23:28:29 +00:00
Alexander Krotov
d809dfac65 Do not download messages that are not displayed 2020-02-09 23:28:29 +00:00
Alexander Krotov
983fd70260 Update imap-proto to 0.10.2
It adds support for parsing of BODY[HEADER.FIELDS (...)] fetches.
2020-02-09 23:28:29 +00:00
Alexander Krotov
ea11a5274e Remove unwrap() in prefetch_get_message_id 2020-02-09 23:28:29 +00:00
Alexander Krotov
42356c2a67 Fix a typo in prefetch_get_message_id 2020-02-09 23:28:29 +00:00
Alexander Krotov
5a84ab2011 Fix a typo (s/ideling/idling/) 2020-02-09 23:28:29 +00:00
Alexander Krotov
b4573e341f Return &'static str from HeaderDef.get_headername() 2020-02-09 19:17:27 +01:00
Alexander Krotov
df252c4704 Do not check for XLIST capability
This capability is not used by Delta Chat. Moreover, XLIST is deprecated
in favor of https://tools.ietf.org/html/rfc6154, which should be
supported instead. Delta Chat already looks for \Sent attribute in
get_folder_meaning().
2020-02-09 19:15:20 +01:00
Alexander Krotov
d1912f873b Resultify fetch_single_msg 2020-02-09 19:14:36 +01:00
Floris Bruynooghe
e525c42c7d Unmute should also raise exceptions
And tests should not care about return values.
2020-02-09 19:12:21 +01:00
Floris Bruynooghe
0242322d24 Tweak error halding a little
- Python should raise exceptions on error.  Not return False.
- Negative durations are nonense.
- ChatID validity is already checked by the Rust API, let's not duplicate.
2020-02-09 18:01:42 +01:00
Simon Laux
ded6fafc8a Merge pull request #1250 from deltachat/flub-feat-mute-chat
Use SystemTime instead of i64
2020-02-09 15:46:19 +01:00
Floris Bruynooghe
4aebd678c3 Use SystemTime instead of i64
This clarifies some behaviour with negative times and propagates
errors a bit better around places.
2020-02-09 13:35:37 +01:00
Simon Laux
afc9ed2274 fix python formatting 2020-02-09 12:36:37 +01:00
Simon Laux
d73d021e3c cargo fmt 2020-02-09 12:25:47 +01:00
bjoern
c8fe81e21d Merge pull request #1243 from Ampli-fier/master
Minor changes in comments
2020-02-09 12:01:31 +01:00
Simon Laux
621f1df913 rename MutedUntilTimestamp to Until 2020-02-09 12:01:09 +01:00
Simon Laux
5f4274b449 fix naming 2020-02-09 10:42:32 +01:00
Simon Laux
4acb37156f fix building of ffi 2020-02-09 10:39:21 +01:00
Simon Laux
1fb75c1af3 rename dc_chat_get_mute_duration
to dc_chat_get_remaining_mute_duration
2020-02-08 13:29:24 +01:00
Simon Laux
e04d28c885 use from- and to-sql traits 2020-02-08 13:29:24 +01:00
Simon Laux
6d80b3675a dtransform mute chat to use relative durations
instead of absolute timestamps
2020-02-08 13:26:48 +01:00
Simon Laux
07d698f8dc add python tests 2020-02-08 13:26:21 +01:00
Simon Laux
ef158504e7 remove success variable
(replace with else statement)
2020-02-08 13:26:21 +01:00
Simon Laux
63be1ae5a9 change muted forever to -1 2020-02-08 13:25:22 +01:00
Simon Laux
b9ba1a4f69 implement ffi part 2020-02-08 13:25:22 +01:00
Simon Laux
e006d9b033 rustfmt 2020-02-08 13:25:22 +01:00
Simon Laux
1538684c6c simplify test code 2020-02-08 13:23:55 +01:00
Simon Laux
b37e83caab add possibility to mute chat in core 2020-02-08 13:23:55 +01:00
Simon Laux
d11d3ab08b fix typo 2020-02-08 13:11:22 +01:00
Ampli-fier
b876e49393 Minor changes in comments 2020-02-07 20:01:17 +01:00
21 changed files with 607 additions and 124 deletions

View File

@@ -15,6 +15,7 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --examples --tests --all-features
fmt:
name: Rustfmt

14
Cargo.lock generated
View File

@@ -656,7 +656,7 @@ dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lettre_email 0.9.2 (git+https://github.com/deltachat/lettre)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"mailparse 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
"mailparse 0.10.4 (git+https://github.com/link2xt/mailparse?branch=address-comma)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1515,12 +1515,12 @@ dependencies = [
[[package]]
name = "mailparse"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
version = "0.10.4"
source = "git+https://github.com/link2xt/mailparse?branch=address-comma#f5e7a2175966f85f5c079a32c5ab171fc6e9d08e"
dependencies = [
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
"charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"quoted_printable 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -2073,7 +2073,7 @@ dependencies = [
[[package]]
name = "quoted_printable"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -3429,7 +3429,7 @@ dependencies = [
"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
"checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
"checksum mach 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1"
"checksum mailparse 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "04c64e94d3a5d02abe0cf459507689d70fb3b9b1d5fea5773a9f5273117959fb"
"checksum mailparse 0.10.4 (git+https://github.com/link2xt/mailparse?branch=address-comma)" = "<none>"
"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
"checksum md-5 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a18af3dcaf2b0219366cdb4e2af65a6101457b415c3d1a5c71dd9c2b7c77b9c8"
@@ -3487,7 +3487,7 @@ dependencies = [
"checksum quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
"checksum quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "86cedf331228892e747bb85beb130b6bb23fc628c40dde9ea01eb6becea3c798"
"checksum quoted_printable 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2"
"checksum r2d2 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)" = "22b5c5fc5fba064373f03887337b412e0e1562d63023393db77251146cb75553"
"checksum r2d2_sqlite 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b061f5b16692bbe81eeb260f92e6fc7d13aea455c4cbe67f5c4aa20aa92d1d9e"
"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"

View File

@@ -53,7 +53,7 @@ bitflags = "1.1.0"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.10.2"
mailparse = { git = "https://github.com/link2xt/mailparse", branch="address-comma" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }

View File

@@ -403,6 +403,7 @@ int dc_set_config (dc_context_t* context, const char*
*/
char* dc_get_config (dc_context_t* context, const char* key);
/**
* Set stock string translation.
*
@@ -417,6 +418,22 @@ char* dc_get_config (dc_context_t* context, const char*
int dc_set_stock_translation(dc_context_t* context, uint32_t stock_id, const char* stock_msg);
/**
* Set configuration values from a QR code containing an account.
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT.
*
* Internally, the function will call dc_set_config()
* at least with the keys `addr` and `mail_pw`.
*
* @memberof dc_context_t
* @param context The context object
* @param qr scanned QR code
* @return int (==0 on error, 1 on success)
*/
int dc_set_config_from_qr (dc_context_t* context, const char* qr);
/**
* Get information about the context.
*
@@ -1577,6 +1594,22 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
int dc_set_chat_profile_image (dc_context_t* context, uint32_t chat_id, const char* image);
/**
* Set mute duration of a chat.
*
* This value can be checked by the ui upon receiving a new message to decide whether it should trigger an notification.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*
* @memberof dc_context_t
* @param chat_id The chat ID to set the mute duration.
* @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
* @param context The context as created by dc_context_new().
* @return 1=success, 0=error
*/
int dc_set_chat_mute_duration (dc_context_t* context, uint32_t chat_id, int64_t duration);
// handle messages
/**
@@ -2095,6 +2128,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_OK 210 // id=contact
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
@@ -2112,12 +2146,12 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
* - DC_QR_ACCOUNT allows creation of an account, dc_lot_t::text1=domain
* - DC_QR_ADDR with dc_lot_t::id=Contact ID
* - DC_QR_TEXT with dc_lot_t::text1=Text
* - DC_QR_URL with dc_lot_t::text1=URL
* - DC_QR_ERROR with dc_lot_t::text1=Error string
*
*
* @memberof dc_context_t
* @param context The context object.
* @param qr The text of the scanned QR code.
@@ -2919,6 +2953,26 @@ int dc_chat_is_verified (const dc_chat_t* chat);
int dc_chat_is_sending_locations (const dc_chat_t* chat);
/**
* Check whether the chat is currently muted
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=muted, 0=not muted
*/
int dc_chat_is_muted (const dc_chat_t* chat);
/**
* Get the exact state of the mute of a chat
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 0=not muted, -1=forever muted, (x>0)=remaining seconds until the mute is lifted
*/
int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
/**
* @class dc_msg_t
*

View File

@@ -20,11 +20,13 @@ use std::fmt::Write;
use std::ptr;
use std::str::FromStr;
use std::sync::RwLock;
use std::time::{Duration, SystemTime};
use libc::uintptr_t;
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::chat::ChatId;
use deltachat::chat::MuteDuration;
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::Contact;
use deltachat::context::Context;
@@ -421,6 +423,28 @@ pub unsafe extern "C" fn dc_set_stock_translation(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_config_from_qr(
context: *mut dc_context_t,
qr: *mut libc::c_char,
) -> libc::c_int {
if context.is_null() || qr.is_null() {
eprintln!("ignoring careless call to dc_set_config_from_qr");
return 0;
}
let qr = to_string_lossy(qr);
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| match qr::set_config_from_qr(ctx, &qr) {
Ok(()) => 1,
Err(err) => {
error!(ctx, "Failed to create account from QR code: {}", err);
0
}
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
@@ -1407,6 +1431,37 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_mute_duration(
context: *mut dc_context_t,
chat_id: u32,
duration: i64,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_mute_duration()");
return 0;
}
let ffi_context = &*context;
let muteDuration = match duration {
0 => MuteDuration::NotMuted,
-1 => MuteDuration::Forever,
n if n > 0 => MuteDuration::Until(SystemTime::now() + Duration::from_secs(duration as u64)),
_ => {
ffi_context.warning(
"dc_chat_set_mute_duration(): Can not use negative duration other than -1",
);
return 0;
}
};
ffi_context
.with_inner(|ctx| {
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set mute duration")
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_info(
context: *mut dc_context_t,
@@ -2481,6 +2536,37 @@ pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> l
ffi_chat.chat.is_sending_locations() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_muted(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_muted()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_muted() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_remaining_mute_duration(chat: *mut dc_chat_t) -> i64 {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_remaining_mute_duration()");
return 0;
}
let ffi_chat = &*chat;
if !ffi_chat.chat.is_muted() {
return 0;
}
// If the chat was muted to before the epoch, it is not muted.
match ffi_chat.chat.mute_duration {
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
MuteDuration::Until(when) => when
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_info_json(
context: *mut dc_context_t,

View File

@@ -967,6 +967,10 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
res.get_text2()
);
}
"setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
set_config_from_qr(context, arg1);
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {

View File

@@ -52,20 +52,21 @@ Python packages before running the tests::
pytest -v tests
running "live" tests (experimental)
-----------------------------------
running "live" tests with temporary accounts
---------------------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_PY_LIVECONFIG`` to:
``DCC_NEW_TMP_EMAIL`` to:
- a particular https-url that you can ask for from the delta
chat devs.
chat devs. This is implemented on the server side via
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
- or the path of a file that contains two lines, each describing
via "addr=... mail_pw=..." a test account login that will
be used for the live tests.
With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
e-mail accounts and run through all functional "liveconfig" tests.
@@ -129,7 +130,7 @@ organization::
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e DCC_PY_LIVECONFIG \
$ docker run -e DCC_NEW_TMP_EMAIL \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh

View File

@@ -58,6 +58,13 @@ class Chat(object):
"""
return self.id == const.DC_CHAT_ID_DEADDROP
def is_muted(self):
""" return true if this chat is muted.
:returns: True if chat is muted, False otherwise.
"""
return lib.dc_chat_is_muted(self._dc_chat)
def is_promoted(self):
""" return True if this chat is promoted, i.e.
the member contacts are aware of their membership,
@@ -84,12 +91,43 @@ class Chat(object):
def set_name(self, name):
""" set name of this chat.
:param: name as a unicode string.
:param name: as a unicode string.
:returns: None
"""
name = as_dc_charpointer(name)
return lib.dc_set_chat_name(self._dc_context, self.id, name)
def mute(self, duration=None):
""" mutes the chat
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
:returns: None
"""
if duration is None:
mute_duration = -1
else:
mute_duration = duration
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration)
if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self):
""" unmutes the chat
:returns: None
"""
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0)
if not bool(ret):
raise ValueError("Failed to unmute chat")
def get_mute_duration(self):
""" Returns the number of seconds until the mute of this chat is lifted.
:param duration:
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
"""
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
def get_type(self):
""" return type of this chat.

View File

@@ -28,7 +28,7 @@ def pytest_configure(config):
)
cfg = config.getoption('--liveconfig')
if not cfg:
cfg = os.getenv('DCC_PY_LIVECONFIG')
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
if cfg:
config.option.liveconfig = cfg
@@ -97,17 +97,16 @@ class SessionLiveConfigFromFile:
class SessionLiveConfigFromURL:
def __init__(self, url, create_token):
def __init__(self, url):
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)})
res = requests.post(self.url)
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
@@ -124,8 +123,7 @@ def session_liveconfig(request):
liveconfig_opt = request.config.option.liveconfig
if liveconfig_opt:
if liveconfig_opt.startswith("http"):
url, create_token = liveconfig_opt.split("#", 1)
return SessionLiveConfigFromURL(url, create_token)
return SessionLiveConfigFromURL(liveconfig_opt)
else:
return SessionLiveConfigFromFile(liveconfig_opt)
@@ -194,14 +192,9 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
lib.dc_set_config(ac._dc_context, b"configured", b"1")
return ac
def peek_online_config(self):
if not session_liveconfig:
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
return session_liveconfig.get(self.live_count)
def get_online_config(self, pre_generated_key=True):
if not session_liveconfig:
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
configdict = session_liveconfig.get(self.live_count)
self.live_count += 1
if "e2ee_enabled" not in configdict:

View File

@@ -7,7 +7,6 @@ from deltachat import const, Account
from deltachat.message import Message
from datetime import datetime, timedelta
from conftest import (wait_configuration_progress,
wait_successful_IMAP_SMTP_connection,
wait_securejoin_inviter_progress)
@@ -220,6 +219,18 @@ class TestOfflineChat:
chat.remove_profile_image()
assert chat.get_profile_image() is None
def test_mute(self, ac1):
chat = ac1.create_group_chat(name="title1")
assert not chat.is_muted()
chat.mute()
assert chat.is_muted()
chat.unmute()
assert not chat.is_muted()
chat.mute(50)
assert chat.is_muted()
with pytest.raises(ValueError):
chat.mute(-51)
def test_delete_and_send_fails(self, ac1, chat1):
chat1.delete()
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
@@ -449,37 +460,53 @@ class TestOnlineAccount:
def test_one_account_send_bcc_setting(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2_config = acfactory.peek_online_config()
c2 = ac1.create_contact(email=ac2_config["addr"])
chat = ac1.create_chat_by_contact(c2)
assert chat.id > const.DC_CHAT_ID_LAST_SPECIAL
wait_successful_IMAP_SMTP_connection(ac1)
ac2 = acfactory.get_online_configuring_account()
# Clone the first account: we will test if sent messages
# are copied to it via BCC.
ac1_clone = acfactory.clone_online_account(ac1)
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
wait_configuration_progress(ac1_clone, 1000)
chat = self.get_chat(ac1, ac2)
self_addr = ac1.get_config("addr")
other_addr = ac2.get_config("addr")
lp.sec("send out message without bcc to ourselves")
ac1.set_config("bcc_self", "0")
msg_out = chat.send_text("message1")
assert not msg_out.is_forwarded()
# wait for send out (no BCC)
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "0"
# make sure we are not sending message to ourselves
assert self_addr not in ev[2]
assert other_addr in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
lp.sec("ac1: setting bcc_self=1")
ac1.set_config("bcc_self", "1")
lp.sec("send out message with bcc to ourselves")
msg_out = chat.send_text("message2")
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev[2] == msg_out.id
# wait for send out (BCC)
assert ac1.get_config("bcc_self") == "1"
self_addr = ac1.get_config("addr")
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "1"
# now make sure we are sending message to ourselves too
assert self_addr in ev[2]
assert other_addr in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
ac1._evlogger.consume_events()
lp.sec("send out message without bcc")
ac1.set_config("bcc_self", "0")
msg_out = chat.send_text("message3")
assert not msg_out.is_forwarded()
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev[2] == msg_out.id
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert self_addr not in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
# Second client receives only second message, but not the first
ev = ac1_clone._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ac1_clone.get_message_by_id(ev[2]).text == msg_out.text
def test_send_file_twice_unicode_filename_mangling(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -676,7 +703,7 @@ class TestOnlineAccount:
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")
lp.sec("check the message arrived in contact-requests/deaddrop")
chat2 = msg_in.chat
assert msg_in in chat2.get_messages()
assert chat2.is_deaddrop()

View File

@@ -8,6 +8,7 @@ use std::{fmt, str};
use crate::contact::*;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, SignedPublicKey};
/// Possible values for encryption preference
@@ -74,9 +75,7 @@ impl Aheader {
wanted_from: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Option<Self> {
use mailparse::MailHeaderMap;
if let Ok(Some(value)) = headers.get_first_value("Autocrypt") {
if let Ok(Some(value)) = headers.get_header_value(HeaderDef::Autocrypt) {
match Self::from_str(&value) {
Ok(header) => {
if addr_cmp(&header.addr, wanted_from) {

View File

@@ -1,6 +1,8 @@
//! # Chat module
use std::convert::TryFrom;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use itertools::Itertools;
use num_traits::FromPrimitive;
@@ -422,6 +424,7 @@ pub struct Chat {
blocked: Blocked,
pub param: Params,
is_sending_locations: bool,
pub mute_duration: MuteDuration,
}
impl Chat {
@@ -429,7 +432,7 @@ impl Chat {
pub fn load_from_db(context: &Context, chat_id: ChatId) -> Result<Self, Error> {
let res = context.sql.query_row(
"SELECT c.type, c.name, c.grpid, c.param, c.archived,
c.blocked, c.locations_send_until
c.blocked, c.locations_send_until, c.muted_until
FROM chats c
WHERE c.id=?;",
params![chat_id],
@@ -443,6 +446,7 @@ impl Chat {
archived: row.get(4)?,
blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
is_sending_locations: row.get(6)?,
mute_duration: row.get(7)?,
};
Ok(c)
},
@@ -658,6 +662,7 @@ impl Chat {
profile_image: self.get_profile_image(context).unwrap_or_else(PathBuf::new),
subtitle: self.get_subtitle(context),
draft,
is_muted: self.is_muted(),
})
}
@@ -684,6 +689,14 @@ impl Chat {
self.is_sending_locations
}
pub fn is_muted(&self) -> bool {
match self.mute_duration {
MuteDuration::NotMuted => false,
MuteDuration::Forever => true,
MuteDuration::Until(when) => when > SystemTime::now(),
}
}
fn prepare_msg_raw(
&mut self,
context: &Context,
@@ -968,6 +981,11 @@ pub struct ChatInfo {
/// which contain non-text parts. Perhaps it should be a
/// simple `has_draft` bool instead.
pub draft: String,
/// Whether the chat is muted
///
/// The exact time its muted can be found out via the `chat.mute_duration` property
pub is_muted: bool,
// ToDo:
// - [ ] deaddrop,
// - [ ] summary,
@@ -1901,6 +1919,66 @@ pub fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<boo
Ok(needs_attach)
}
#[derive(Debug, Clone, PartialEq)]
pub enum MuteDuration {
NotMuted,
Forever,
Until(SystemTime),
}
impl rusqlite::types::ToSql for MuteDuration {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let duration: i64 = match &self {
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
MuteDuration::Until(when) => {
let duration = when
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
i64::try_from(duration.as_secs())
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?
}
};
let val = rusqlite::types::Value::Integer(duration);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
impl rusqlite::types::FromSql for MuteDuration {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
// Negative values other than -1 should not be in the
// database. If found they'll be NotMuted.
match i64::column_result(value)? {
0 => Ok(MuteDuration::NotMuted),
-1 => Ok(MuteDuration::Forever),
n if n > 0 => match SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(n as u64)) {
Some(t) => Ok(MuteDuration::Until(t)),
None => Err(rusqlite::types::FromSqlError::OutOfRange(n)),
},
_ => Ok(MuteDuration::NotMuted),
}
}
}
pub fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<(), Error> {
ensure!(!chat_id.is_special(), "Invalid chat ID");
if real_group_exists(context, chat_id)
&& sql::execute(
context,
&context.sql,
"UPDATE chats SET muted_until=? WHERE id=?;",
params![duration, chat_id],
)
.is_ok()
{
context.call_cb(Event::ChatModified(chat_id));
} else {
bail!("Failed to set name");
}
Ok(())
}
pub fn remove_contact_from_chat(
context: &Context,
chat_id: ChatId,
@@ -2398,7 +2476,7 @@ mod tests {
let chat = Chat::load_from_db(&t.ctx, chat_id).unwrap();
let info = chat.get_info(&t.ctx).unwrap();
// Ensure we can serialise this.
// Ensure we can serialize this.
println!("{}", serde_json::to_string_pretty(&info).unwrap());
let expected = r#"
@@ -2413,11 +2491,12 @@ mod tests {
"color": 15895624,
"profile_image": "",
"subtitle": "bob@example.com",
"draft": ""
"draft": "",
"is_muted": false
}
"#;
// Ensure we can deserialise this.
// Ensure we can deserialize this.
let loaded: ChatInfo = serde_json::from_str(expected).unwrap();
assert_eq!(info, loaded);
}
@@ -2777,4 +2856,49 @@ mod tests {
assert!(chat_id.set_selfavatar_timestamp(&t.ctx, time()).is_ok());
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
}
#[test]
fn test_set_mute_duration() {
let t = dummy_context();
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap();
// Initial
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
// Forever
set_muted(&t.ctx, chat_id, MuteDuration::Forever).unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
true
);
// unMute
set_muted(&t.ctx, chat_id, MuteDuration::NotMuted).unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
// Timed in the future
set_muted(
&t.ctx,
chat_id,
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
)
.unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
true
);
// Time in the past
set_muted(
&t.ctx,
chat_id,
MuteDuration::Until(SystemTime::now() - Duration::from_secs(3600)),
)
.unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
}
}

View File

@@ -74,18 +74,12 @@ pub fn dc_receive_imf(
// helper method to handle early exit and memory cleanup
let cleanup = |context: &Context,
create_event_to_send: &Option<CreateEvent>,
created_db_entries: &Vec<(ChatId, MsgId)>| {
created_db_entries: Vec<(ChatId, MsgId)>| {
if let Some(create_event_to_send) = create_event_to_send {
for (chat_id, msg_id) in created_db_entries {
let event = match create_event_to_send {
CreateEvent::MsgsChanged => Event::MsgsChanged {
msg_id: *msg_id,
chat_id: *chat_id,
},
CreateEvent::IncomingMsg => Event::IncomingMsg {
msg_id: *msg_id,
chat_id: *chat_id,
},
CreateEvent::MsgsChanged => Event::MsgsChanged { msg_id, chat_id },
CreateEvent::IncomingMsg => Event::IncomingMsg { msg_id, chat_id },
};
context.call_cb(event);
}
@@ -165,7 +159,7 @@ pub fn dc_receive_imf(
&mut created_db_entries,
&mut create_event_to_send,
) {
cleanup(context, &create_event_to_send, &created_db_entries);
cleanup(context, &create_event_to_send, created_db_entries);
bail!("add_parts error: {:?}", err);
}
} else {
@@ -216,7 +210,7 @@ pub fn dc_receive_imf(
"received message {} has Message-Id: {}", server_uid, rfc724_mid
);
cleanup(context, &create_event_to_send, &created_db_entries);
cleanup(context, &create_event_to_send, created_db_entries);
mime_parser.handle_reports(context, from_id, sent_timestamp, &server_folder, server_uid);
@@ -524,7 +518,7 @@ fn add_parts(
if chat_id.is_unset() && self_sent {
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
// maybe an Autocrypt Setup Messag
// maybe an Autocrypt Setup Message
let (id, bl) =
chat::create_or_lookup_by_contact_id(context, DC_CONTACT_ID_SELF, Blocked::Not)
.unwrap_or_default();
@@ -1525,7 +1519,7 @@ fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -
false
}
pub fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
if let Ok(ids) = mailparse::addrparse(mid_list) {
for id in ids.iter() {
if is_msgrmsg_rfc724_mid(context, id) {

View File

@@ -3,7 +3,7 @@
use std::collections::HashSet;
use std::convert::TryFrom;
use mailparse::{MailHeaderMap, ParsedMail};
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
use crate::aheader::*;
@@ -11,6 +11,7 @@ use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::EmailAddress;
use crate::error::*;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{self, Key, KeyPairUse, SignedPublicKey};
use crate::keyring::*;
use crate::peerstate::*;
@@ -124,7 +125,7 @@ pub fn try_decrypt(
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
let from = mail
.headers
.get_first_value("From")?
.get_header_value(HeaderDef::From_)?
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)

View File

@@ -1,4 +1,7 @@
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames)]
use crate::strum::AsStaticRef;
use mailparse::{MailHeader, MailHeaderMap, MailParseError};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[strum(serialize_all = "kebab_case")]
#[allow(dead_code)]
pub enum HeaderDef {
@@ -32,6 +35,7 @@ pub enum HeaderDef {
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
SecureJoinGroup,
@@ -43,8 +47,18 @@ pub enum HeaderDef {
impl HeaderDef {
/// Returns the corresponding Event id.
pub fn get_headername(&self) -> String {
self.to_string()
pub fn get_headername(&self) -> &'static str {
self.as_static()
}
}
pub trait HeaderDefMap {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError>;
}
impl HeaderDefMap for [MailHeader<'_>] {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError> {
self.get_first_value(headerdef.get_headername())
}
}
@@ -55,8 +69,29 @@ mod tests {
#[test]
/// Test that kebab_case serialization works as expected
fn kebab_test() {
assert_eq!(HeaderDef::From_.to_string(), "from");
assert_eq!(HeaderDef::From_.get_headername(), "from");
assert_eq!(HeaderDef::_TestHeader.to_string(), "test-header");
assert_eq!(HeaderDef::_TestHeader.get_headername(), "test-header");
}
#[test]
/// Test that headers are parsed case-insensitively
fn test_get_header_value_case() {
let (headers, _) =
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
assert_eq!(
headers
.get_header_value(HeaderDef::AutocryptSetupMessage)
.unwrap(),
Some("v99".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::From_).unwrap(),
Some("Bob".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::Autocrypt).unwrap(),
None
);
}
}

View File

@@ -14,8 +14,6 @@ use async_imap::{
use async_std::sync::{Mutex, RwLock};
use async_std::task;
use mailparse::MailHeaderMap;
use crate::config::*;
use crate::constants::*;
use crate::context::Context;
@@ -23,7 +21,7 @@ use crate::dc_receive_imf::{
dc_receive_imf, from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list,
};
use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap_client::*;
use crate::job::{job_add, Action};
use crate::login_param::{CertificateChecks, LoginParam};
@@ -184,7 +182,6 @@ struct ImapConfig {
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
pub can_idle: bool,
pub has_xlist: bool,
pub imap_delimiter: char,
}
@@ -202,7 +199,6 @@ impl Default for ImapConfig {
selected_mailbox: None,
selected_folder_needs_expunge: false,
can_idle: false,
has_xlist: false,
imap_delimiter: '.',
}
}
@@ -356,7 +352,6 @@ impl Imap {
cfg.imap_port = 0;
cfg.can_idle = false;
cfg.has_xlist = false;
}
/// Connects to imap account using already-configured parameters.
@@ -417,7 +412,6 @@ impl Imap {
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)
@@ -427,7 +421,6 @@ impl Imap {
});
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
@@ -658,12 +651,13 @@ impl Imap {
);
} else {
// check passed, go fetch the rest
if self.fetch_single_msg(context, &folder, cur_uid).await == 0 {
if let Err(err) = self.fetch_single_msg(context, &folder, cur_uid).await {
info!(
context,
"Read error for message {} from \"{}\", trying over later.",
"Read error for message {} from \"{}\", trying over later: {}.",
message_id,
folder.as_ref()
folder.as_ref(),
err
);
read_errors += 1;
}
@@ -711,17 +705,19 @@ impl Imap {
context.sql.set_raw_config(context, &key, Some(&val)).ok();
}
/// Fetches a single message by server UID.
///
/// If it succeeds, the message should be treated as received even
/// if no database entries are created. If the function returns an
/// error, the caller should try again later.
async fn fetch_single_msg<S: AsRef<str>>(
&self,
context: &Context,
folder: S,
server_uid: u32,
) -> usize {
// the function returns:
// 0 the caller should try over again later
// or 1 if the messages should be treated as received, the caller should not try to read the message again (even if no database entries are returned)
) -> Result<()> {
if !self.is_connected().await {
return 0;
return Err(Error::Other("Not connected".to_string()));
}
let set = format!("{}", server_uid);
@@ -740,13 +736,13 @@ impl Imap {
folder.as_ref(),
err
);
return 0;
return Err(Error::FetchFailed(err));
}
}
} else {
// we could not get a valid imap session, this should be retried
self.trigger_reconnect();
return 0;
return Err(Error::Other("Could not get IMAP session".to_string()));
};
if msgs.is_empty() {
@@ -787,7 +783,7 @@ impl Imap {
}
}
1
Ok(())
}
pub fn mv(
@@ -851,9 +847,23 @@ impl Imap {
Ok(_) => {
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete FAILED)",
display_folder_id, dest_folder
))
);
ImapActionResult::Failed
} else {
self.config.write().await.selected_folder_needs_expunge = true;
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete successfull)",
display_folder_id, dest_folder
))
);
ImapActionResult::Success
}
}
@@ -1157,7 +1167,6 @@ impl Imap {
}
async fn list_folders(&self, session: &mut Session, context: &Context) -> Option<Vec<Name>> {
// TODO: use xlist when available
match session.list(Some(""), Some("*")).await {
Ok(list) => {
if list.is_empty() {
@@ -1307,31 +1316,24 @@ fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>>
}
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
if let Some(message_id) = headers.get_first_value(&HeaderDef::MessageId.get_headername())? {
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId)? {
Ok(parse_message_id(&message_id)?)
} else {
Err(Error::Other("prefetch: No message ID found".to_string()))
}
}
/// Checks if fetch result contains a header
fn prefetch_has_header(headers: &[mailparse::MailHeader], headerdef: HeaderDef) -> Result<bool> {
Ok(headers
.get_first_value(&headerdef.get_headername())?
.is_some())
}
fn prefetch_is_reply_to_chat_message(
context: &Context,
headers: &[mailparse::MailHeader],
) -> Result<bool> {
if let Some(value) = headers.get_first_value(&HeaderDef::InReplyTo.get_headername())? {
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo)? {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
}
}
if let Some(value) = headers.get_first_value(&HeaderDef::References.get_headername())? {
if let Some(value) = headers.get_header_value(HeaderDef::References)? {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
}
@@ -1345,15 +1347,16 @@ fn prefetch_should_download(
headers: &[mailparse::MailHeader],
show_emails: ShowEmails,
) -> Result<bool> {
let is_chat_message = prefetch_has_header(&headers, HeaderDef::ChatVersion)?;
let is_autocrypt_setup_message =
prefetch_has_header(&headers, HeaderDef::AutocryptSetupMessage)?;
// Autocrypt Setup Message should be shown even if it is from non-chat client.
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion)?.is_some();
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers)?;
// Autocrypt Setup Message should be shown even if it is from non-chat client.
let is_autocrypt_setup_message = headers
.get_header_value(HeaderDef::AutocryptSetupMessage)?
.is_some();
let from_field = headers
.get_first_value(&HeaderDef::From_.get_headername())?
.get_header_value(HeaderDef::From_)?
.unwrap_or_default();
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(context, &from_field)?;

View File

@@ -369,7 +369,7 @@ impl Job {
}
self.smtp_send(context, recipients, body, self.job_id, || {
// Remove additional SendMdn jobs we have aggretated into this one.
// Remove additional SendMdn jobs we have aggregated into this one.
job_kill_ids(context, &additional_job_ids)?;
Ok(())
})
@@ -900,8 +900,8 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
// some configuration jobs are "exclusive":
// - they are always executed in the imap-thread and the smtp-thread is suspended during execution
// - they may change the database handle change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not save in the database for later execution
// - they may change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not saved in the database for later execution
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
job_kill_action(context, job.action);
context
@@ -1162,7 +1162,7 @@ pub fn interrupt_smtp_idle(context: &Context) {
///
/// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load
/// IMAP jobs. The `probe_network` parameter decides how to query
/// jobs, this is tricky and probably wrong currently. Look at the
/// jobs, this is tricky and probably wrong currently. Look at the
/// SQL queries for details.
fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Option<Job> {
let query = if !probe_network {

View File

@@ -86,6 +86,9 @@ pub enum LotState {
/// test1=formatted fingerprint
QrFprWithoutAddr = 230,
/// text1=domain
QrAccount = 250,
/// id=contact
QrAddr = 320,

View File

@@ -15,7 +15,7 @@ use crate::dehtml::dehtml;
use crate::e2ee;
use crate::error::Result;
use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{job_add, Action};
use crate::location;
use crate::message;
@@ -96,7 +96,7 @@ impl MimeMessage {
let message_time = mail
.headers
.get_first_value("Date")?
.get_header_value(HeaderDef::Date)?
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
@@ -402,7 +402,7 @@ impl MimeMessage {
}
pub fn get(&self, headerdef: HeaderDef) -> Option<&String> {
self.header.get(&headerdef.get_headername())
self.header.get(headerdef.get_headername())
}
fn parse_first_addr(&self, context: &Context, headerdef: HeaderDef) -> Option<MailAddr> {
@@ -781,16 +781,19 @@ impl MimeMessage {
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_header_value(HeaderDef::Disposition)
.ok()
.flatten()
{
if let Some(original_message_id) = report_fields
.get_first_value(&HeaderDef::OriginalMessageId.get_headername())
.get_header_value(HeaderDef::OriginalMessageId)
.ok()
.flatten()
.and_then(|v| parse_message_id(&v))
{
let additional_message_ids = report_fields
.get_first_value(&HeaderDef::AdditionalMessageIds.get_headername())
.get_header_value(HeaderDef::AdditionalMessageIds)
.ok()
.flatten()
.map_or_else(Vec::new, |v| {
@@ -806,7 +809,7 @@ impl MimeMessage {
warn!(
context,
"ignoring unknown disposition-notification, Message-Id: {:?}",
report_fields.get_first_value("Message-ID").ok()
report_fields.get_header_value(HeaderDef::MessageId).ok()
);
Ok(None)
@@ -1486,4 +1489,16 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
&["foo@example.com", "foo@example.net"]
);
}
#[test]
fn mailparse_test() {
let body = b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>";
let mail = mailparse::parse_mail(body).unwrap();
let from = mail.headers[0].get_value().unwrap();
assert_eq!(&from, "Имя, Фамилия <foobar@example.com>");
let parsed = mailparse::addrparse(&from).unwrap();
assert_eq!(parsed.len(), 1);
}
}

View File

@@ -4,6 +4,7 @@ use lazy_static::lazy_static;
use percent_encoding::percent_decode_str;
use crate::chat;
use crate::config::*;
use crate::constants::Blocked;
use crate::contact::*;
use crate::context::Context;
@@ -13,8 +14,11 @@ use crate::key::dc_normalize_fingerprint;
use crate::lot::{Lot, LotState};
use crate::param::*;
use crate::peerstate::*;
use reqwest::Url;
use serde::Deserialize;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
const MAILTO_SCHEME: &str = "mailto:";
const MATMSG_SCHEME: &str = "MATMSG:";
const VCARD_SCHEME: &str = "BEGIN:VCARD";
@@ -43,6 +47,8 @@ pub fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
if qr.starts_with(OPENPGP4FPR_SCHEME) {
decode_openpgp(context, qr)
} else if qr.starts_with(DCACCOUNT_SCHEME) {
decode_account(context, qr)
} else if qr.starts_with(MAILTO_SCHEME) {
decode_mailto(context, qr)
} else if qr.starts_with(SMTP_SCHEME) {
@@ -179,6 +185,73 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
lot
}
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
fn decode_account(_context: &Context, qr: &str) -> Lot {
let payload = &qr[DCACCOUNT_SCHEME.len()..];
let mut lot = Lot::new();
if let Ok(url) = Url::parse(payload) {
if url.scheme() == "https" {
lot.state = LotState::QrAccount;
lot.text1 = url.host_str().map(|x| x.to_string());
} else {
lot.state = LotState::QrError;
lot.text1 = Some(format!("Bad scheme for account url: {}", payload));
}
} else {
lot.state = LotState::QrError;
lot.text1 = Some(format!("Invalid account url: {}", payload));
}
lot
}
#[derive(Debug, Deserialize)]
struct CreateAccountResponse {
email: String,
password: String,
}
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
/// download additional information from the contained url and set the parameters.
/// on success, a configure::configure() should be able to log in to the account
pub fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
let response = reqwest::blocking::Client::new().post(url_str).send();
if response.is_err() {
return Err(format_err!(
"Cannot create account, request to {} failed",
url_str
));
}
let response = response.unwrap();
if !response.status().is_success() {
return Err(format_err!(
"Request to {} unsuccessful: {:?}",
url_str,
response
));
}
let parsed: reqwest::Result<CreateAccountResponse> = response.json();
if parsed.is_err() {
return Err(format_err!(
"Failed to parse JSON response from {}: error: {:?}",
url_str,
parsed
));
}
println!("response: {:?}", &parsed);
let parsed = parsed.unwrap();
context.set_config(Config::Addr, Some(&parsed.email))?;
context.set_config(Config::MailPw, Some(&parsed.password))?;
Ok(())
}
/// Extract address for the mailto scheme.
///
/// Scheme: `mailto:addr...?subject=...&body=..`
@@ -493,4 +566,28 @@ mod tests {
assert_eq!(res.get_state(), LotState::QrError);
assert_eq!(res.get_id(), 0);
}
#[test]
fn test_decode_account() {
let ctx = dummy_context();
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
);
assert_eq!(res.get_state(), LotState::QrAccount);
assert_eq!(res.get_text1().unwrap(), "example.org");
}
#[test]
fn test_decode_account_bad_scheme() {
let ctx = dummy_context();
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
);
assert_eq!(res.get_state(), LotState::QrError);
assert!(res.get_text1().is_some());
}
}

View File

@@ -885,6 +885,14 @@ fn open(
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 61)?;
}
if dbversion < 62 {
info!(context, "[migration] v62");
sql.execute(
"ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
NO_PARAMS,
)?;
sql.set_raw_config_int(context, "dbversion", 62)?;
}
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)