Compare commits

...

43 Commits

Author SHA1 Message Date
link2xt
d7db65afd8 Remove direct dependency on futures-lite 2022-04-26 18:33:36 +02:00
holger krekel
3ffc985968 try fix a sometimes failing test: don't test python's imap idle as it's not needed. also add some more logging. 2022-04-26 15:28:24 +02:00
holger krekel
369609b26c streamline emitting MsgsChanged and IncomingMsg event to go through particular functions. 2022-04-26 10:09:21 +02:00
link2xt
d033dcf395 Run python tests in a single thread
Work around mailcow rate limits
2022-04-25 15:43:47 +02:00
link2xt
5ef2a85c10 python: do not crash in get_locations() when location has no marker 2022-04-25 15:05:34 +02:00
link2xt
8c0bc9080c Create "Junk" folder in test_dont_show_emails()
This folder may not exist on the test server.
2022-04-24 00:20:41 +00:00
link2xt
6bf2c5415f Don't count IMAP jobs in scheduler
The limit on the number of jobs executed in a row was introduced to
prevent large queues of small jobs like MarkseenMsgOnImap, MoveMsg and
DeleteMsgOnImap from delaying message fetching. Since all these jobs
are now removed and IMAP operations they did are now batched, it is
impossible to have 20 or more queued IMAP jobs.
2022-04-23 19:53:10 +00:00
link2xt
2f31033a88 Ignore messages from all spam folders if there are many
For example, if there is both a Spam and Junk folder,
both of them should be ignored, even though only one
of them can be a ConfiguredSpamFolder.
2022-04-23 19:23:31 +00:00
Hocuri
ceaed0f552 Speedup rust tests (#3242)
Speeds up running the rust tests by about a factor of 2.
2022-04-23 13:50:58 +02:00
Hocuri
e9963ecc0d Also run clippy for benchmarks in CI (#3241)
…and fix two lints
2022-04-22 15:06:34 +02:00
link2xt
9ef0b43c36 Remove job::{Action,Thread}::Unknown variants 2022-04-17 00:00:00 +00:00
holger krekel
801d636eb5 enhance waiting for direct imap idle events to prevent randomness 2022-04-20 23:32:45 +02:00
link2xt
dfbfd4fe74 Remove housekeeping job
Run housekeeping directly from the inbox loop
instead of creating inbox loop job.
2022-04-17 00:00:00 +00:00
bjoern
7455989729 webxdc documentation: make function declaration correct JavaScript (#3237)
* webxdc docs: make function declaration correct js

* add 'let'
2022-04-20 12:16:40 +02:00
Sebastian Klähn
8b2b9e1093 update dev-reference to reflect new promise feature for setup (#3220)
* update dev-reference

* Update draft/webxdc-dev-reference.md

Co-authored-by: holger krekel  <holger@merlinux.eu>

* Update draft/webxdc-dev-reference.md

Co-authored-by: Simon Laux <Simon-Laux@users.noreply.github.com>
Co-authored-by: holger krekel  <holger@merlinux.eu>
2022-04-19 15:43:05 +02:00
link2xt
a8cf05ea5d accounts: retry remove_account multiple times on failure
When removing an account, try 60 times with 1 second sleep in between
in case removal of database files fails. This happens on Windows
platform sometimes due to a known bug in r2d2 which may result in 30
seconds delay until all connections are closed [1].

[1] https://github.com/sfackler/r2d2/issues/99
2022-04-19 14:14:38 +02:00
Robert Schütz
969508ae36 dynamic libraries use dylib extension on Darwin 2022-04-19 08:56:29 +02:00
bjoern
f581ecc805 url-safe id generation (#3231)
* test dc_create_id() for invalid characters

* create url-save ids (as documented)
2022-04-18 17:21:33 +02:00
link2xt
a63464765c dc_receive_imf: remove Received: based draft detection heuristic
Proper draft detection was implemented in
bf7f64d50b

Removing this heuristic also removes the need to pass IMAP folder name
around.
2022-04-17 00:00:00 +00:00
link2xt
e9fe8ce118 Merge branch 'markseen-imap-loop' 2022-04-17 17:55:25 +03:00
link2xt
1fa892c239 Rename store_seen_flags into store_seen_flags_on_imap 2022-04-17 14:55:10 +00:00
link2xt
1afbbbc737 Rename markseen_on_imap to markseen_on_imap_table and document it 2022-04-17 11:56:48 +00:00
link2xt
66c4de2607 Mark messages as seen in IMAP loop
MarkseenMsgOnImap job, that was responsible for marking messages as
seen on IMAP and sending MDNs, has been removed.

Messages waiting to be marked as seen are now stored in a
single-column imap_markseen table consisting of foreign keys pointing
to corresponding imap table records.

Messages are marked as seen in batches in the inbox loop. UIDs are
grouped by folders to reduce the number of requests, including folder
selection requests. UID grouping logic has been factored out of
move_delete_messages into UidGrouper iterator to avoid code duplication.

Messages are marked as seen right before fetching from the inbox
folder. This ensures that even if new messages arrive into inbox while
the connection has another folder selected to mark messages there, all
messages are fetched before going IDLE. Ideally marking messages as
seen should be done after fetching and moving, as it is a low-priority
task, but this requires skipping IDLE if UIDNEXT has advanced since
previous time inbox has been selected. This is outside of the scope of
this change.

MDNs are now queued independently of marking the messages as seen.
SendMdn job is created directly rather than after marking the message
as seen on IMAP. Previously sending MDNs was done in MarkseenMsgOnImap
avoid duplicate MDN sending by setting $MDNSent flag together with
\Seen flag and skipping MDN sending if the flag is already set. This
is not the case anymore as $MDNSent flag support has been removed in
9c077c98cd and duplicate MDN sending in
multi-device case is avoided by synchronizing Seen status since
833e5f46cc as long as the server
supports CONDSTORE extension.
2022-04-17 09:50:04 +00:00
link2xt
213e67dea2 Merge branch 'faster_fetch' 2022-04-17 09:47:42 +00:00
link2xt
86c884fe1e Update the comment before delete_expired_imap_messages 2022-04-17 09:47:17 +00:00
bjoern
a4d5c8cd2f show 'Not connected' if storage information are not yet available (#3222)
* show 'Not connected' if storage information are not yet available

'One moment' is a bit misleading in case the device is offline
as it will take more than a moment until that will be updated :)

* update CHANGELOG
2022-04-16 18:35:25 +02:00
Simon Laux
330665afbe don't start io on unconfigured context 2022-04-16 17:48:16 +02:00
holger krekel
be1d87c3c3 do imap expiration/deletion after fetching messages, to avoid unneccessary delays. 2022-04-16 17:19:31 +02:00
link2xt
14ab3c8651 Remove unused stop-token dependency
stop-token 0.2.0 is required by async-imap. This Cargo.toml entry
additionally pulled in unused stop-token 0.7.0.
2022-04-16 11:24:23 +00:00
holger krekel
9c04ed483e Streamline access/working with configured params and configured addr (#3219) 2022-04-16 09:50:26 +02:00
Hocuri
a8a5e184ab Don't unnecessarily interrupt ephemeral loop (#3221)
Follow-up for #3211
2022-04-15 21:30:01 +02:00
Sebastian Klähn
c571595980 Fix #3186 (#3218)
* fix

* code-review fixes

* check if chat is restored correctly

* add changelog-entry

* Update src/dc_receive_imf.rs

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>

* Update CHANGELOG.md

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>
2022-04-14 19:12:31 +02:00
Sebastian Klähn
80f2ccc9ed improve error message (#3217)
use actual path instead of placeholder
2022-04-14 12:32:44 +02:00
bjoern
b2e9e57859 add status_update_serial to webxdc_status_update event (#3215)
this bypasses the replication safety introduced by letting the caller track the last serial,
however, in case of bots that do not track much state and do not playback updates anyway,
it is still useful.
2022-04-14 11:12:17 +02:00
Hocuri
3c75b36148 clippy 2022-04-12 22:26:44 +00:00
Hocuri
bcd8e330cb Add test for ephemeral deletion 2022-04-12 22:26:44 +00:00
link2xt
f75f8ad76d Interrupt ephemeral loop when new messages are fetched 2022-04-12 22:26:44 +00:00
link2xt
574b78cf31 Interrupt ephemeral loop when delete_device_after is set 2022-04-12 22:26:44 +00:00
link2xt
92f0e8472b Take delete_device_after into account when calculating ephemeral loop timeout 2022-04-12 22:26:44 +00:00
bjoern
69bd5d2ab0 Webxdc scaling/viewport is implementation specific (#3214)
at least on iOS we would need to inject a script to force a behavior,
that is hard to merge with settings set by the Webxdc.

therefore, the easiest approach seems to be to leave that completely up to the Webxdc -
and, in practise, many Webxdc already set viewports, including scaling.
2022-04-12 14:54:05 +02:00
Floris Bruynooghe
6eaf04107d Auto-approve dependabot PRs
There is very little to be gained by having to approve those PRs, and
it is a lot of UI interaction to get them approved.
They still will need to be merged manually regardless, so they might
as well be approved by a bot.
2022-04-12 12:44:04 +02:00
bjoern
d80fdb20c7 make Connectivity-View-HTML not scalable (#3213)
* make Connectivity-View-HTML not scalable

in practise, Android and Desktop already disallow scaling
by some other methods,
however, for iOS this is needed as we otherwise
have to do far more complicated things as drafted at
https://github.com/deltachat/deltachat-ios/pull/1531/files

* update CHANGELOG
2022-04-12 12:26:40 +02:00
Hocuri
f618c87ee5 Follow http redirects for autoconfigure (#3208) 2022-04-10 19:27:05 +02:00
57 changed files with 980 additions and 892 deletions

View File

@@ -45,7 +45,7 @@ jobs:
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --examples
args: --workspace --tests --examples --benches
docs:
name: Rust doc comments

21
.github/workflows/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Dependabot auto-approve
on: pull_request
permissions:
pull-requests: write
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.1.1
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve a PR
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

4
.gitignore vendored
View File

@@ -29,3 +29,7 @@ deltachat-ffi/xml
coverage/
.DS_Store
.vscode/launch.json
python/accounts.txt
python/all-testaccounts.txt
tmp/

View File

@@ -1,5 +1,29 @@
# Changelog
## Unreleased
### API-Changes
- replaced stock string `DC_STR_ONE_MOMENT` by `DC_STR_NOT_CONNECTED` #3222
### Fixes
- Take `delete_device_after` into account when calculating ephemeral loop timeout #3211
- Fix a bug where a blocked contact could send a contact request #3218
- Make sure, videochat-room-names are always URL-safe #3231
- Try removing account folder multiple times in case of failure #3229
- Ignore messages from all spam folders if there are many #3246
### Changes
- Further improve finding the correct server after logging in #3208
- `get_connectivity_html()` returns HTML as non-scalable #3213
- add update-serial to `DC_EVENT_WEBXDC_STATUS_UPDATE` #3215
- Speed up message receiving via IMAP a bit #3225
- mark messages as seen on IMAP in batches #3223
- remove Received: based draft detection heuristic #3230
## 1.77.0
### API changes

View File

@@ -4,10 +4,18 @@ include(GNUInstallDirs)
find_program(CARGO cargo)
if(APPLE)
set(DYNAMIC_EXT "dylib")
elseif(UNIX)
set(DYNAMIC_EXT "so")
else()
set(DYNAMIC_EXT "dll")
endif()
add_custom_command(
OUTPUT
"target/release/libdeltachat.a"
"target/release/libdeltachat.so"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/pkgconfig/deltachat.pc"
COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX}
@@ -32,11 +40,11 @@ add_custom_target(
ALL
DEPENDS
"target/release/libdeltachat.a"
"target/release/libdeltachat.so"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/pkgconfig/deltachat.pc"
)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.so" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

16
Cargo.lock generated
View File

@@ -224,7 +224,7 @@ dependencies = [
"once_cell",
"ouroboros",
"pin-utils",
"stop-token 0.2.0",
"stop-token",
"thiserror",
]
@@ -1090,7 +1090,6 @@ dependencies = [
"escaper",
"fast-socks5",
"futures",
"futures-lite",
"hex",
"humansize",
"image",
@@ -1123,7 +1122,6 @@ dependencies = [
"sha-1 0.10.0",
"sha2 0.10.2",
"smallvec",
"stop-token 0.7.0",
"strum",
"strum_macros",
"surf",
@@ -3609,18 +3607,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "stop-token"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b"
dependencies = [
"async-channel",
"cfg-if 1.0.0",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "str-buf"
version = "1.0.5"

View File

@@ -62,7 +62,6 @@ serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1"
stop-token = "0.7"
strum = "0.24"
strum_macros = "0.24"
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
@@ -81,7 +80,6 @@ zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
ansi_term = "0.12.0"
async-std = { version = "1", features = ["unstable", "attributes"] }
criterion = { version = "0.3.4", features = ["async_std"] }
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"
proptest = { version = "1", default-features = false, features = ["std"] }

View File

@@ -29,7 +29,7 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("chat::get_chat_msgs (load messages from 10 chats)", |b| {
b.to_async(AsyncStdExecutor)
.iter(|| get_chat_msgs_benchmark(black_box(&path.as_ref()), black_box(&chats)))
.iter(|| get_chat_msgs_benchmark(black_box(path.as_ref()), black_box(&chats)))
});
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");

View File

@@ -5,7 +5,7 @@ use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
async fn get_chat_list_benchmark(context: &Context) {
Chatlist::try_load(&context, 0, None, None).await.unwrap();
Chatlist::try_load(context, 0, None, None).await.unwrap();
}
fn criterion_benchmark(c: &mut Criterion) {

View File

@@ -31,7 +31,7 @@ Hello {i}",
i = i,
i_dec = i - 1,
);
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), "INBOX", false)
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
.unwrap();
}

View File

@@ -5606,11 +5606,16 @@ void dc_event_unref(dc_event_t* event);
/**
* webxdc status update received.
* To get the received status update, use dc_get_webxdc_status_updates() with
* `serial` set to the last known update.
* `serial` set to the last known update
* (in case of special bots, `status_update_serial` from `data2`
* may help to calculate the last known update for dc_get_webxdc_status_updates();
* UIs must not peek at this parameter to avoid races in the status replication
* eg. when events arrive while initial updates are played back).
*
* To send status updates, use dc_send_webxdc_status_update().
*
* @param data1 (int) msg_id
* @param data2 (int) 0
* @param data2 (int) status_update_serial - must not be used by UI implementations.
*/
#define DC_EVENT_WEBXDC_STATUS_UPDATE 2120
@@ -6218,9 +6223,7 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by the domain of the configured e-mail address.
#define DC_STR_STORAGE_ON_DOMAIN 105
/// "One moment…"
///
/// Used in the connectivity view when some information are not yet there.
/// @deprecated Deprecated 2022-04-16, this string is no longer needed.
#define DC_STR_ONE_MOMENT 106
/// "Connected"
@@ -6309,6 +6312,11 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced with the group name.
#define DC_STR_SECURE_JOIN_GROUP_QR_DESC 120
/// "Not connected"
///
/// Used as status in the connectivity view.
#define DC_STR_NOT_CONNECTED 121
/**
* @}
*/

View File

@@ -503,7 +503,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
contact_id.to_u32() as libc::c_int
}
EventType::WebxdcStatusUpdate(msg_id) => msg_id.to_u32() as libc::c_int,
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
}
}
@@ -535,9 +535,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ImexFileWritten(_)
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::WebxdcStatusUpdate(_)
| EventType::ChatModified(_) => 0,
| EventType::SelfavatarChanged => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
@@ -546,6 +545,10 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
EventType::SecurejoinInviterProgress { progress, .. }
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
EventType::WebxdcStatusUpdate {
status_update_serial,
..
} => status_update_serial.to_u32() as libc::c_int,
}
}
@@ -587,7 +590,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::SecurejoinJoinerProgress { .. }
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::WebxdcStatusUpdate(_)
| EventType::WebxdcStatusUpdate { .. }
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {

View File

@@ -54,12 +54,13 @@ and you won't get the update by `setUpdateListener()`.
### setUpdateListener()
```js
window.webxdc.setUpdateListener((update) => {}, serial);
let promise = window.webxdc.setUpdateListener((update) => {}, serial);
```
With `setUpdateListener()` you define a callback that receives the updates
sent by `sendUpdate()`. The callback is called for updates sent by you or other peers.
The `serial` specifies the last serial that you know about (defaults to 0).
The returned promise resolves when the listener has processed all the update messages known at the time when `setUpdateListener` was called.
Each `update` which is passed to the callback comes with the following properties:
@@ -176,6 +177,9 @@ just clone and start adapting things to your need.
- older devices might not have the newest js features in their webview,
you may want to transpile your code down to an older js version eg. with https://babeljs.io
- viewport and scaling features are implementation specific,
if you want to have an explicit behavior, you can add eg.
`<meta name="viewport" content="initial-scale=1; user-scalable=no">` to your Webxdc
- there are tons of ideas for enhancements of the API and the file format,
eg. in the future, we will may define icon- and manifest-files,
allow to aggregate the state or add metadata.

View File

@@ -21,7 +21,6 @@ use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::EventType;
use deltachat::{config, provider};
use std::fs;
use std::time::{Duration, SystemTime};
@@ -93,16 +92,13 @@ async fn reset_tables(context: &Context, bits: i32) {
println!("(8) Rest but server config reset.");
}
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
context.emit_msgs_changed_without_ids();
}
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
let data = dc_read_file(context, filename).await?;
if let Err(err) = dc_receive_imf(context, &data, "import", false).await {
if let Err(err) = dc_receive_imf(context, &data, false).await {
println!("dc_receive_imf errored: {:?}", err);
}
Ok(())
@@ -164,10 +160,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
if read_cnt > 0 {
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
context.emit_msgs_changed_without_ids();
}
true
}

View File

@@ -24,7 +24,7 @@ if __name__ == "__main__":
print("running:", " ".join(cmd))
subprocess.check_call(cmd)
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
subprocess.check_call("rm -rf build/ src/deltachat/*.so src/deltachat/*.dylib src/deltachat/*.dll" , shell=True)
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
subprocess.check_call([

View File

@@ -5,7 +5,7 @@ import calendar
import json
from datetime import datetime, timezone
import os
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array
from .capi import lib, ffi
from . import const
from .message import Message
@@ -517,7 +517,7 @@ class Chat(object):
lib.dc_array_get_timestamp(dc_array, i),
timezone.utc
),
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
marker=from_optional_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
)
for i in range(lib.dc_array_get_cnt(dc_array))
]

View File

@@ -40,8 +40,6 @@ def dc_account_extra_configure(account):
# We just deleted the folder, so we have to make DC forget about it, too
if account.get_config("configured_sentbox_folder") == folder:
account.set_config("configured_sentbox_folder", None)
if account.get_config("configured_spam_folder") == folder:
account.set_config("configured_spam_folder", None)
setattr(account, "direct_imap", imap)
@@ -230,7 +228,15 @@ class DirectImap:
self.account.log("imap-direct: idle_check returned {!r}".format(res))
return res
def idle_wait_for_seen(self):
def idle_wait_for_new_message(self, terminate=False):
while 1:
for item in self.idle_check():
if item[1] in (b'EXISTS', b'RECENT'):
if terminate:
self.idle_done()
return item
def idle_wait_for_seen(self, terminate=False):
""" Return first message with SEEN flag
from a running idle-stream REtiurn.
"""
@@ -239,6 +245,8 @@ class DirectImap:
if item[1] == FETCH:
if item[2][0] == FLAGS:
if SEEN in item[2][1]:
if terminate:
self.idle_done()
return item[0]
def idle_done(self):

View File

@@ -1013,7 +1013,7 @@ class TestOnlineAccount:
assert ev.data1 == msg2.chat.id
assert ev.data2 == 0
ac2.direct_imap.idle_check(terminate=True)
ac2.direct_imap.idle_wait_for_new_message(terminate=True)
lp.step("1")
for i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
@@ -1044,8 +1044,7 @@ class TestOnlineAccount:
ac1.create_chat(ac2).send_text("Hello!")
# Wait for the message to arrive.
ac2.direct_imap.idle_check(terminate=True)
ac2.direct_imap.idle_wait_for_new_message(terminate=True)
# Emulate moving of the message to DeltaChat folder by Sieve rule.
# mailcow server contains this rule by default.
@@ -1059,8 +1058,7 @@ class TestOnlineAccount:
# Accept the contact request.
msg.chat.accept()
ac2.mark_seen_messages([msg])
ac2.direct_imap.idle_wait_for_seen()
ac2.direct_imap.idle_done()
ac2.direct_imap.idle_wait_for_seen(terminate=True)
fetch = list(ac2.direct_imap.conn.fetch("*", b'FLAGS').values())
flags = fetch[-1][b'FLAGS']
@@ -1437,6 +1435,7 @@ class TestOnlineAccount:
ac1.direct_imap.create_folder("Drafts")
ac1.direct_imap.create_folder("Sent")
ac1.direct_imap.create_folder("Spam")
ac1.direct_imap.create_folder("Junk")
acfactory.wait_configure_and_start_io()
# Wait until each folder was selected once and we are IDLEing again:
@@ -1470,6 +1469,15 @@ class TestOnlineAccount:
Unknown message in Spam
""".format(ac1.get_config("configured_addr")))
ac1.direct_imap.append("Junk", """
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Junk
""".format(ac1.get_config("configured_addr")))
ac1.set_config("scan_all_folders_debounce_secs", "0")
lp.sec("All prepared, now let DC find the message")
@@ -2211,7 +2219,7 @@ class TestOnlineAccount:
ac1.direct_imap.idle_start()
ac2.create_chat(ac1).send_text("Hi")
ac1.direct_imap.idle_check(terminate=False)
ac1.direct_imap.idle_wait_for_new_message(terminate=False)
ac1.maybe_network()
ac1._evtracker.wait_for_all_work_done()
@@ -2223,7 +2231,7 @@ class TestOnlineAccount:
ac2.create_chat(ac1).send_text("Hi 2")
ac1.direct_imap.idle_check(terminate=True)
ac1.direct_imap.idle_wait_for_new_message(terminate=True)
ac1.maybe_network()
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
@@ -2248,7 +2256,7 @@ class TestOnlineAccount:
ac1.direct_imap.idle_start()
ac2.create_chat(ac1).send_text("Hi")
ac1.direct_imap.idle_check(terminate=True)
ac1.direct_imap.idle_wait_for_new_message(terminate=True)
ac1.maybe_network()
while 1:
@@ -2438,25 +2446,23 @@ class TestOnlineAccount:
acfactory.wait_configure_and_start_io()
imap2 = ac2.direct_imap
imap2.idle_start()
lp.sec("ac1: create chat with ac2")
chat1 = ac1.create_chat(ac2)
ac2.create_chat(ac1)
lp.sec("ac1: send message to ac2")
sent_msg = chat1.send_text("hello")
imap2.idle_check(terminate=False)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
imap2.idle_check(terminate=True)
lp.sec("ac2: wait for close/expunge on autodelete")
ac2._evtracker.get_info_contains("close/expunge succeeded")
assert len(imap2.get_all_messages()) == 0
lp.sec("ac2: check that message was autodeleted on server")
assert len(ac2.direct_imap.get_all_messages()) == 0
# Mark deleted message as seen and check that read receipt arrives
lp.sec("ac2: Mark deleted message as seen and check that read receipt arrives")
msg.mark_seen()
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 == chat1.id
@@ -2717,13 +2723,7 @@ class TestOnlineAccount:
ac1.direct_imap.select_config_folder("inbox")
ac1.direct_imap.idle_start()
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
while True:
if len(ac1.direct_imap.idle_check(terminate=True)) > 1:
# If length is 1, it's [(b'OK', b'Still here')]
# Could happen on very slow network.
#
# More is usually [(1, b'EXISTS'), (1, b'RECENT')]
break
ac1.direct_imap.idle_wait_for_new_message(terminate=True)
ac1.direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
lp.sec("Everything prepared, now see if DeltaChat finds the message (" + variant + ")")

View File

@@ -8,7 +8,7 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pytest -n1 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS

View File

@@ -54,7 +54,11 @@ impl Accounts {
ensure!(dir.exists().await, "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists().await, "accounts.toml does not exist");
ensure!(
config_file.exists().await,
"{:?} does not exist",
config_file
);
let config = Config::from_file(config_file)
.await
@@ -144,9 +148,27 @@ impl Accounts {
drop(ctx);
if let Some(cfg) = self.config.get_account(id).await {
fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
.await
.context("failed to remove account data")?;
// Spend up to 1 minute trying to remove the files.
// Files may remain locked up to 30 seconds due to r2d2 bug:
// https://github.com/sfackler/r2d2/issues/99
let mut counter = 0;
loop {
counter += 1;
if let Err(err) = fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
.await
.context("failed to remove account data")
{
if counter > 60 {
return Err(err);
}
// Wait 1 second and try again.
async_std::task::sleep(std::time::Duration::from_millis(1000)).await;
} else {
break;
}
}
}
self.config.remove_account(id).await?;

View File

@@ -940,7 +940,7 @@ mod tests {
}
#[async_std::test]
async fn test_recode_image() {
async fn test_recode_image_1() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
@@ -957,7 +957,10 @@ mod tests {
)
.await
.unwrap();
}
#[async_std::test]
async fn test_recode_image_2() {
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
let img_rotated = send_image_check_mediaquality(
@@ -978,18 +981,24 @@ mod tests {
.write_to(&mut buf, image::ImageFormat::Jpeg)
.unwrap();
let bytes = buf.into_inner();
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
// Do this in parallel to speed up the test a bit
// (it still takes very long though)
let bytes2 = bytes.clone();
let join_handle = async_std::task::spawn(async move {
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes2,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
});
let img_rotated = send_image_check_mediaquality(
Some("1"),
@@ -1004,6 +1013,11 @@ mod tests {
.unwrap();
assert_correct_rotation(&img_rotated);
join_handle.await;
}
#[async_std::test]
async fn test_recode_image_3() {
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
.await

View File

@@ -29,7 +29,6 @@ use crate::dc_tools::{
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::html::new_html_mimepart;
use crate::job::{self, Action};
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -215,10 +214,7 @@ impl ChatId {
}
}
};
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
context.emit_msgs_changed_without_ids();
Ok(chat_id)
}
@@ -493,10 +489,7 @@ impl ChatId {
})
.await?;
context.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
context.emit_msgs_changed_without_ids();
Ok(())
}
@@ -551,14 +544,10 @@ impl ChatId {
.execute("DELETE FROM chats WHERE id=?;", paramsv![self])
.await?;
context.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
context.emit_msgs_changed_without_ids();
job::kill_action(context, Action::Housekeeping).await?;
let j = job::Job::new(Action::Housekeeping, 0, Params::new(), 10);
job::add(context, j).await?;
context.set_config(Config::LastHousekeeping, None).await?;
context.interrupt_inbox(InterruptInfo::new(false)).await;
if chat.is_self_talk() {
let mut msg = Message::new(Viewtype::Text);
@@ -583,9 +572,9 @@ impl ChatId {
};
if changed {
context.emit_event(EventType::MsgsChanged {
chat_id: self,
msg_id: if msg.is_some() {
context.emit_msgs_changed(
self,
if msg.is_some() {
match self.get_draft_msg_id(context).await? {
Some(msg_id) => msg_id,
None => MsgId::new(0),
@@ -593,7 +582,7 @@ impl ChatId {
} else {
MsgId::new(0)
},
});
);
}
Ok(())
@@ -1254,11 +1243,7 @@ impl Chat {
}
}
let from = context
.get_config(Config::ConfiguredAddr)
.await?
.context("Cannot prepare message for sending, address is not configured.")?;
let from = context.get_configured_addr().await?;
let new_rfc724_mid = {
let grpid = match self.typ {
Chattype::Group => Some(self.grpid.as_str()),
@@ -1799,10 +1784,7 @@ pub async fn prepare_msg(context: &Context, chat_id: ChatId, msg: &mut Message)
);
let msg_id = prepare_msg_common(context, chat_id, msg, MessageState::OutPreparing).await?;
context.emit_event(EventType::MsgsChanged {
chat_id: msg.chat_id,
msg_id: msg.id,
});
context.emit_msgs_changed(msg.chat_id, msg.id);
Ok(msg_id)
}
@@ -1970,20 +1952,14 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
.await
.context("failed to send message, queued for later sending")?;
context.emit_event(EventType::MsgsChanged {
chat_id: msg.chat_id,
msg_id: msg.id,
});
context.emit_msgs_changed(msg.chat_id, msg.id);
}
Ok(msg.id)
}
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
context.emit_event(EventType::MsgsChanged {
chat_id: msg.chat_id,
msg_id: msg.id,
});
context.emit_msgs_changed(msg.chat_id, msg.id);
if msg.param.exists(Param::SetLatitude) {
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
@@ -2047,10 +2023,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
let mut recipients = mimefactory.recipients();
let from = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let from = context.get_configured_addr().await?;
let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled and we are not going to
@@ -2574,10 +2547,7 @@ pub async fn create_group_chat(
add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?;
}
context.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
context.emit_msgs_changed_without_ids();
if protect == ProtectionStatus::Protected {
// this part is to stay compatible to verified groups,
@@ -2631,10 +2601,7 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
.await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
context.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
context.emit_msgs_changed_without_ids();
Ok(chat_id)
}
@@ -2723,10 +2690,7 @@ pub(crate) async fn add_contact_to_chat_ex(
context.sync_qr_code_tokens(Some(chat_id)).await?;
context.send_sync_msg().await?;
}
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let self_addr = context.get_configured_addr().await.unwrap_or_default();
if addr_cmp(contact.get_addr(), &self_addr) {
// ourself is added using ContactId::SELF, do not add this address explicitly.
// if SELF is not in the group, members cannot be added at all.
@@ -3000,10 +2964,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
msg.param.set(Param::Arg, &chat.name);
}
msg.id = send_msg(context, chat_id, &mut msg).await?;
context.emit_event(EventType::MsgsChanged {
chat_id,
msg_id: msg.id,
});
context.emit_msgs_changed(chat_id, msg.id);
}
context.emit_event(EventType::ChatModified(chat_id));
success = true;
@@ -3065,10 +3026,7 @@ pub async fn set_chat_profile_image(
chat.update_param(context).await?;
if chat.is_promoted() && !chat.is_mailing_list() {
msg.id = send_msg(context, chat_id, &mut msg).await?;
context.emit_event(EventType::MsgsChanged {
chat_id,
msg_id: msg.id,
});
context.emit_msgs_changed(chat_id, msg.id);
}
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
@@ -3163,10 +3121,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
}
}
for (chat_id, msg_id) in created_chats.iter().zip(created_msgs.iter()) {
context.emit_event(EventType::MsgsChanged {
chat_id: *chat_id,
msg_id: *msg_id,
});
context.emit_msgs_changed(*chat_id, *msg_id);
}
Ok(())
}
@@ -3306,9 +3261,9 @@ pub async fn add_device_msg_with_importance(
if !msg_id.is_unset() {
if important {
context.emit_event(EventType::IncomingMsg { chat_id, msg_id });
context.emit_incoming_msg(chat_id, msg_id);
} else {
context.emit_event(EventType::MsgsChanged { chat_id, msg_id });
context.emit_msgs_changed(chat_id, msg_id);
}
}
@@ -3403,7 +3358,8 @@ pub(crate) async fn add_info_msg_with_cmd(
).await?;
let msg_id = MsgId::new(row_id.try_into()?);
context.emit_event(EventType::MsgsChanged { chat_id, msg_id });
context.emit_msgs_changed(chat_id, msg_id);
Ok(msg_id)
}
@@ -4236,7 +4192,6 @@ mod tests {
num
)
.as_bytes(),
"INBOX",
false,
)
.await?;
@@ -4686,9 +4641,7 @@ mod tests {
assert_eq!(msg.match_indices("Gr.").count(), 1);
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
dc_receive_imf(&bob, msg.as_bytes(), "INBOX", false)
.await
.unwrap();
dc_receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
let msg = bob.get_last_msg().await;
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
@@ -4707,9 +4660,7 @@ mod tests {
assert_eq!(msg.match_indices("Chat-").count(), 0);
// Alice receives this message - she can still detect the group by the `References:`-header
dc_receive_imf(&alice, msg.as_bytes(), "INBOX", false)
.await
.unwrap();
dc_receive_imf(&alice, msg.as_bytes(), false).await.unwrap();
let msg = alice.get_last_msg().await;
assert_eq!(msg.chat_id, alice_chat_id);
assert_eq!(msg.text, Some("ho!".to_string()));
@@ -4734,7 +4685,6 @@ mod tests {
Date: Fri, 23 Apr 2021 10:00:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await?;
@@ -4782,7 +4732,6 @@ mod tests {
Date: Sun, 22 Mar 2021 19:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await?;
@@ -4830,7 +4779,6 @@ mod tests {
Date: Sun, 22 Mar 2021 19:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await?;
@@ -4877,7 +4825,6 @@ mod tests {
Date: Sun, 22 Mar 2021 19:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await?;

View File

@@ -503,7 +503,6 @@ mod tests {
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
\n\
hello foo\n",
"INBOX",
false,
)
.await?;
@@ -564,7 +563,6 @@ mod tests {
Date: Sun, 22 Mar 2021 22:38:57 +0000\n\
\n\
hello foo\n",
"INBOX",
false,
)
.await?;

View File

@@ -1,16 +1,14 @@
//! # Key-value configuration management.
use anyhow::{ensure, Result};
use anyhow::{ensure, Context as _, Result};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input};
use crate::events::EventType;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
@@ -131,7 +129,6 @@ pub enum Config {
ConfiguredInboxFolder,
ConfiguredMvboxFolder,
ConfiguredSentboxFolder,
ConfiguredSpamFolder,
ConfiguredTimestamp,
ConfiguredProvider,
Configured,
@@ -229,6 +226,12 @@ impl Context {
Ok(self.get_config_int(key).await? != 0)
}
pub(crate) async fn get_configured_addr(&self) -> Result<String> {
self.get_config(Config::ConfiguredAddr)
.await?
.context("no address configured")
}
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
@@ -293,11 +296,8 @@ impl Context {
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(key, value).await;
// Force chatlist reload to delete old messages immediately.
self.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
// Interrupt ephemeral loop to delete old messages immediately.
self.interrupt_ephemeral_task().await;
ret?
}
Config::Displayname => {

View File

@@ -85,7 +85,7 @@ impl Context {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let mut param = LoginParam::from_database(self, "").await?;
let mut param = LoginParam::load_candidate_params(self).await?;
let success = configure(self, &mut param).await;
self.set_config(Config::NotifyAboutWrongPw, None).await?;
@@ -454,7 +454,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 910);
// the trailing underscore is correct
param.save_to_database(ctx, "configured_").await?;
param.save_as_configured_params(ctx).await?;
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
@@ -699,11 +699,11 @@ pub enum Error {
error: quick_xml::Error,
},
#[error("Failed to get URL: {0}")]
ReadUrl(#[from] self::read_url::Error),
#[error("Number of redirection is exceeded")]
Redirection,
#[error("{0:#}")]
Other(#[from] anyhow::Error),
}
#[cfg(test)]

View File

@@ -1,20 +1,40 @@
use crate::context::Context;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("URL request error")]
GetError(surf::Error),
}
use anyhow::format_err;
use anyhow::Context as _;
pub async fn read_url(context: &Context, url: &str) -> Result<String, Error> {
info!(context, "Requesting URL {}", url);
match surf::get(url).recv_string().await {
Ok(res) => Ok(res),
Err(err) => {
info!(context, "Can\'t read URL {}: {}", url, err);
Err(Error::GetError(err))
pub async fn read_url(context: &Context, url: &str) -> anyhow::Result<String> {
match read_url_inner(context, url).await {
Ok(s) => {
info!(context, "Successfully read url {}", url);
Ok(s)
}
Err(e) => {
info!(context, "Can't read URL {}: {:#}", url, e);
Err(format_err!("Can't read URL {}: {:#}", url, e))
}
}
}
pub async fn read_url_inner(context: &Context, mut url: &str) -> anyhow::Result<String> {
let mut _temp; // For the borrow checker
// Follow up to 10 http-redirects
for _i in 0..10 {
let mut response = surf::get(url).send().await.map_err(|e| e.into_inner())?;
if response.status().is_redirection() {
_temp = response
.header("location")
.context("Redirection doesn't have a target location")?
.last()
.to_string();
info!(context, "Following redirect to {}", _temp);
url = &_temp;
continue;
}
return response.body_string().await.map_err(|e| e.into_inner());
}
Err(format_err!("Followed 10 redirections"))
}

View File

@@ -296,10 +296,7 @@ impl Contact {
.await?;
if contact_id == ContactId::SELF {
contact.name = stock_str::self_msg(context).await;
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
contact.addr = context.get_configured_addr().await.unwrap_or_default();
contact.status = context
.get_config(Config::Selfstatus)
.await?
@@ -398,10 +395,8 @@ impl Contact {
let addr_normalized = addr_normalize(addr);
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await? {
if addr_cmp(addr_normalized, &addr_self) {
return Ok(Some(ContactId::SELF));
}
if context.is_self_addr(addr_normalized).await? {
return Ok(Some(ContactId::SELF));
}
let id = context
.sql
@@ -452,11 +447,10 @@ impl Contact {
ensure!(origin != Origin::Unknown, "Missing valid origin");
let addr = addr_normalize(addr).to_string();
let addr_self = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
// during configuration process some chats are added and addr
// might not be configured yet
let addr_self = context.get_configured_addr().await.unwrap_or_default();
if addr_cmp(&addr, &addr_self) {
return Ok((ContactId::SELF, sth_modified));
}
@@ -692,11 +686,7 @@ impl Contact {
listflags: u32,
query: Option<impl AsRef<str>>,
) -> Result<Vec<ContactId>> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let self_addr = context.get_configured_addr().await.unwrap_or_default();
let mut add_self = false;
let mut ret = Vec::new();
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
@@ -870,7 +860,7 @@ impl Contact {
let mut ret = String::new();
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let loginparam = LoginParam::from_database(context, "configured_").await?;
let loginparam = LoginParam::load_configured_params(context).await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
if let Some(peerstate) = peerstate.filter(|peerstate| {
@@ -2163,7 +2153,7 @@ Chat-Version: 1.0
Date: Sun, 22 Mar 2020 22:37:55 +0000
Hi."#;
dc_receive_imf(&alice, mime, "Inbox", false).await?;
dc_receive_imf(&alice, mime, false).await?;
let msg = alice.get_last_msg().await;
let timestamp = msg.get_timestamp();

View File

@@ -195,6 +195,11 @@ impl Context {
return;
}
if let Ok(false) = self.is_configured().await {
warn!(self, "can not start io on a context that is not configured");
return;
}
{
let l = &mut *self.inner.scheduler.write().await;
if let Err(err) = l.start(self.clone()).await {
@@ -236,6 +241,24 @@ impl Context {
});
}
/// Emits a generic MsgsChanged event (without chat or message id)
pub fn emit_msgs_changed_without_ids(&self) {
self.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
/// Emits a MsgsChanged event with specified chat and message ids
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
}
/// Emits an IncomingMsg event with specified chat and message ids
pub fn emit_incoming_msg(&self, chat_id: ChatId, msg_id: MsgId) {
self.emit_event(EventType::IncomingMsg { chat_id, msg_id });
}
/// Returns a receiver for emitted events.
///
/// Multiple emitters can be created, but note that in this case each emitted event will
@@ -311,8 +334,8 @@ impl Context {
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let unset = "0";
let l = LoginParam::from_database(self, "").await?;
let l2 = LoginParam::from_database(self, "configured_").await?;
let l = LoginParam::load_candidate_params(self).await?;
let l2 = LoginParam::load_configured_params(self).await?;
let displayname = self.get_config(Config::Displayname).await?;
let chats = get_chat_cnt(self).await? as usize;
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await as usize;
@@ -603,11 +626,6 @@ impl Context {
Ok(mvbox.as_deref() == Some(folder_name))
}
pub async fn is_spam_folder(&self, folder_name: &str) -> Result<bool> {
let spam = self.get_config(Config::ConfiguredSpamFolder).await?;
Ok(spam.as_deref() == Some(folder_name))
}
pub(crate) fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
@@ -709,9 +727,7 @@ mod tests {
dc_create_outgoing_rfc724_mid(None, contact.get_addr())
);
println!("{}", msg);
dc_receive_imf(t, msg.as_bytes(), "INBOX", false)
.await
.unwrap();
dc_receive_imf(t, msg.as_bytes(), false).await.unwrap();
}
#[async_std::test]

View File

@@ -24,7 +24,7 @@ use crate::download::DownloadState;
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{self, Action};
use crate::imap::markseen_on_imap_table;
use crate::location;
use crate::log::LogExt;
use crate::message::{
@@ -70,7 +70,6 @@ struct AddedParts {
pub async fn dc_receive_imf(
context: &Context,
imf_raw: &[u8],
server_folder: &str,
seen: bool,
) -> Result<Option<ReceivedMsg>> {
let mail = parse_mail(imf_raw).context("can't parse mail")?;
@@ -79,16 +78,7 @@ pub async fn dc_receive_imf(
.get_header_value(HeaderDef::MessageId)
.and_then(|msgid| parse_message_id(&msgid).ok())
.unwrap_or_else(dc_create_id);
dc_receive_imf_inner(
context,
&rfc724_mid,
imf_raw,
server_folder,
seen,
None,
false,
)
.await
dc_receive_imf_inner(context, &rfc724_mid, imf_raw, seen, None, false).await
}
/// Receive a message and add it to the database.
@@ -109,15 +99,11 @@ pub(crate) async fn dc_receive_imf_inner(
context: &Context,
rfc724_mid: &str,
imf_raw: &[u8],
server_folder: &str,
seen: bool,
is_partial_download: Option<u32>,
fetching_existing_messages: bool,
) -> Result<Option<ReceivedMsg>> {
info!(
context,
"Receiving message, folder={}, seen={}...", server_folder, seen
);
info!(context, "Receiving message, seen={}...", seen);
if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" {
info!(context, "dc_receive_imf: incoming message mime-body:");
@@ -206,7 +192,6 @@ pub(crate) async fn dc_receive_imf_inner(
&mut mime_parser,
imf_raw,
incoming,
server_folder,
&to_ids,
rfc724_mid,
sent_timestamp,
@@ -339,32 +324,20 @@ pub(crate) async fn dc_receive_imf_inner(
.await?;
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
// This is a Delta Chat MDN. Mark as read.
job::add(
context,
job::Job::new(
Action::MarkseenMsgOnImap,
insert_msg_id.to_u32(),
Params::new(),
0,
),
)
.await?;
markseen_on_imap_table(context, rfc724_mid).await?;
}
}
if replace_partial_download {
context.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id,
});
context.emit_msgs_changed(chat_id, MsgId::new(0));
} else if !chat_id.is_trash() {
let fresh = added_parts.received_msg.state == MessageState::InFresh;
for msg_id in added_parts.created_db_entries {
let event = if incoming && added_parts.received_msg.state == MessageState::InFresh {
EventType::IncomingMsg { msg_id, chat_id }
if incoming && fresh {
context.emit_incoming_msg(chat_id, msg_id);
} else {
EventType::MsgsChanged { msg_id, chat_id }
context.emit_msgs_changed(chat_id, msg_id);
};
context.emit_event(event);
}
}
@@ -427,7 +400,6 @@ async fn add_parts(
mime_parser: &mut MimeMessage,
imf_raw: &[u8],
incoming: bool,
server_folder: &str,
to_ids: &[ContactId],
rfc724_mid: &str,
sent_timestamp: i64,
@@ -669,7 +641,12 @@ async fn add_parts(
let create_blocked = if from_id == ContactId::SELF {
Blocked::Not
} else {
Blocked::Request
let contact = Contact::load_from_db(context, from_id).await?;
if contact.is_blocked() {
Blocked::Yes
} else {
Blocked::Request
}
};
if let Some(chat) = test_normal_chat {
@@ -741,25 +718,12 @@ async fn add_parts(
chat_id = Some(DC_CHAT_ID_TRASH);
}
// If the message is outgoing AND there is no Received header AND it's not in the sentbox,
// then ignore the email.
//
// We only apply this heuristic to classical emails, as it is not reliable (some servers
// such as systemli.org in June 2021 remove their own Received headers on incoming mails)
// and we know Delta Chat never stores drafts on IMAP servers.
let is_draft = !context.is_sentbox(server_folder).await?
&& mime_parser.get_header(HeaderDef::Received).is_none()
&& mime_parser.get_header(HeaderDef::ChatVersion).is_none();
// Mozilla Thunderbird does not set \Draft flag on "Templates", but sets
// X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates
// created by Thunderbird.
//
// This check is not necessary now, but may become useful if the `Received:` header check
// is removed completely later.
let is_draft = is_draft
|| mime_parser
.get_header(HeaderDef::XMozillaDraftInfo)
.is_some();
let is_draft = mime_parser
.get_header(HeaderDef::XMozillaDraftInfo)
.is_some();
if is_draft {
// Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
@@ -1387,7 +1351,7 @@ async fn is_probably_private_reply(
}
/// This function tries to extract the group-id from the message and returns the corresponding
/// chat_id. If the chat does not exist, it is created. If there is no group-id and there are more
/// chat_id. If the chat does not exist, it is created. If there is no group-id and there are more
/// than two members, a new ad hoc group is created.
///
/// On success the function returns the found/created (chat_id, chat_blocked) tuple.
@@ -1451,10 +1415,7 @@ async fn create_or_lookup_group(
ProtectionStatus::Unprotected
};
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context("no address configured")?;
let self_addr = context.get_configured_addr().await?;
if chat_id.is_none()
&& !mime_parser.is_mailinglist_message()
&& !grpid.is_empty()
@@ -1550,11 +1511,7 @@ async fn apply_group_changes(
return Ok(None);
}
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context("no address configured")?;
let self_addr = context.get_configured_addr().await?;
let mut recreate_member_list = false;
let mut send_event_chat_modified = false;
@@ -1998,12 +1955,7 @@ async fn create_adhoc_group(
/// are hidden in BCC. This group ID is sent by DC in the messages sent to this chat,
/// so having the same ID prevents group split.
async fn create_adhoc_grp_id(context: &Context, member_ids: &[ContactId]) -> Result<String> {
let member_cs = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_else(|| "no-self".to_string())
.to_lowercase();
let member_cs = context.get_configured_addr().await?.to_lowercase();
let query = format!(
"SELECT addr FROM contacts WHERE id IN({}) AND id!=?",
sql::repeat_vars(member_ids.len())?
@@ -2307,6 +2259,7 @@ mod tests {
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
use crate::chatlist::Chatlist;
use crate::constants::DC_GCL_NO_SPECIALS;
use crate::imap::prefetch_should_download;
use crate::message::Message;
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
@@ -2393,17 +2346,17 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
dc_receive_imf(&t, MSGRMSG, "INBOX", false).await.unwrap();
dc_receive_imf(&t, MSGRMSG, false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, "INBOX", false)
dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, false)
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
}
@@ -2412,7 +2365,7 @@ mod tests {
async fn test_adhoc_group_show_accepted_contact_unknown() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
// adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
@@ -2424,7 +2377,7 @@ mod tests {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
Contact::create(&t, "Bob", "bob@example.com").await.unwrap();
dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
// adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts
// (and existent chat is required)
@@ -2438,7 +2391,7 @@ mod tests {
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
// accept Bob by accepting a delta-message from Bob
dc_receive_imf(&t, MSGRMSG, "INBOX", false).await.unwrap();
dc_receive_imf(&t, MSGRMSG, false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0).unwrap();
@@ -2459,7 +2412,7 @@ mod tests {
);
// receive a non-delta-message from Bob, shows up because of the show_emails setting
dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, "INBOX", false)
dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, false)
.await
.unwrap();
@@ -2472,7 +2425,7 @@ mod tests {
);
// let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting
dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2);
let chat_id = chats.get_chat_id(0).unwrap();
@@ -2486,7 +2439,7 @@ mod tests {
async fn test_adhoc_group_show_all() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
// adhoc-group with unknown contacts with show_emails=all will show up in a single chat
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
@@ -2558,7 +2511,6 @@ mod tests {
group.grpid, group.grpid
)
.as_bytes(),
"INBOX",
false,
)
.await?;
@@ -2603,7 +2555,6 @@ mod tests {
group.grpid
)
.as_bytes(),
"INBOX",
false,
)
.await?;
@@ -2645,7 +2596,6 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -2674,7 +2624,6 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
).await.unwrap();
assert_eq!(
@@ -2715,7 +2664,6 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -2759,7 +2707,6 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -2879,7 +2826,6 @@ mod tests {
self_addr, foreign_addr, rfc724_mid_outgoing
)
.as_bytes(),
"INBOX",
false,
)
.await
@@ -2890,7 +2836,7 @@ mod tests {
// Check that the ndn would be downloaded:
let headers = mailparse::parse_mail(raw_ndn).unwrap().headers;
assert!(crate::imap::prefetch_should_download(
assert!(prefetch_should_download(
&t,
&headers,
"some-other-message-id",
@@ -2900,7 +2846,7 @@ mod tests {
.await
.unwrap());
dc_receive_imf(&t, raw_ndn, "INBOX", false).await.unwrap();
dc_receive_imf(&t, raw_ndn, false).await.unwrap();
let msg = Message::load_from_db(&t, msg_id).await.unwrap();
assert_eq!(msg.state, MessageState::OutFailed);
@@ -2927,7 +2873,6 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await?;
@@ -2936,7 +2881,7 @@ mod tests {
let msg_id = chats.get_msg_id(0)?.unwrap();
let raw = include_bytes!("../test-data/message/gmail_ndn_group.eml");
dc_receive_imf(&t, raw, "INBOX", false).await?;
dc_receive_imf(&t, raw, false).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
@@ -2963,9 +2908,7 @@ mod tests {
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
dc_receive_imf(context, imf_raw, "INBOX", false)
.await
.unwrap();
dc_receive_imf(context, imf_raw, false).await.unwrap();
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
let msg_id = chats.get_msg_id(0).unwrap().unwrap();
Message::load_from_db(context, msg_id).await.unwrap()
@@ -3009,7 +2952,7 @@ mod tests {
let t = TestContext::new_alice().await;
t.ctx.set_config(Config::ShowEmails, Some("2")).await?;
dc_receive_imf(&t.ctx, GH_MAILINGLIST, "INBOX", false).await?;
dc_receive_imf(&t.ctx, GH_MAILINGLIST, false).await?;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?;
assert_eq!(chats.len(), 1);
@@ -3023,7 +2966,7 @@ mod tests {
assert_eq!(chat.name, "deltachat/deltachat-core-rust");
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1);
dc_receive_imf(&t.ctx, GH_MAILINGLIST2.as_bytes(), "INBOX", false).await?;
dc_receive_imf(&t.ctx, GH_MAILINGLIST2.as_bytes(), false).await?;
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?;
assert!(!chat.can_send(&t.ctx).await?);
@@ -3078,9 +3021,7 @@ mod tests {
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", false)
.await
.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let chat_id = chats.get_chat_id(0).unwrap();
chat_id.accept(&t).await.unwrap();
@@ -3111,7 +3052,7 @@ mod tests {
Hello mailinglist!\r\n"
));
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", false).await?;
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false).await?;
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?;
assert!(chat.can_send(&t.ctx).await?);
@@ -3123,9 +3064,7 @@ Hello mailinglist!\r\n"
async fn test_other_device_writes_to_mailinglist() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
dc_receive_imf(&t, DC_MAILINGLIST, "INBOX", false)
.await
.unwrap();
dc_receive_imf(&t, DC_MAILINGLIST, false).await.unwrap();
let first_msg = t.get_last_msg().await;
let first_chat = Chat::load_from_db(&t, first_msg.chat_id).await?;
assert_eq!(
@@ -3159,7 +3098,6 @@ Hello mailinglist!\r\n"
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
body 4\n",
"INBOX",
false,
)
.await
@@ -3180,9 +3118,7 @@ Hello mailinglist!\r\n"
.await
.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", false)
.await
.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0).unwrap();
@@ -3195,7 +3131,7 @@ Hello mailinglist!\r\n"
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); // Test that the message disappeared
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", false)
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false)
.await
.unwrap();
@@ -3213,9 +3149,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
dc_receive_imf(&t, DC_MAILINGLIST, "INBOX", false)
.await
.unwrap();
dc_receive_imf(&t, DC_MAILINGLIST, false).await.unwrap();
let blocked = Contact::get_all_blocked(&t).await.unwrap();
assert_eq!(blocked.len(), 0);
@@ -3235,7 +3169,7 @@ Hello mailinglist!\r\n"
let blocked = Contact::get_all_blocked(&t).await.unwrap();
assert_eq!(blocked.len(), 0);
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", false)
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false)
.await
.unwrap();
let msg = t.get_last_msg().await;
@@ -3251,9 +3185,7 @@ Hello mailinglist!\r\n"
.await
.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", false)
.await
.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
let msg = t.get_last_msg().await;
let chat_id = msg.get_chat_id();
@@ -3266,7 +3198,7 @@ Hello mailinglist!\r\n"
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
assert_eq!(msgs.len(), 1); // ...and contains 1 message
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", false)
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false)
.await
.unwrap();
@@ -3286,9 +3218,7 @@ Hello mailinglist!\r\n"
.await
.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", false)
.await
.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
let msg = t.get_last_msg().await;
let chat_id = msg.get_chat_id();
@@ -3298,7 +3228,7 @@ Hello mailinglist!\r\n"
assert_eq!(chats.len(), 1); // Test that the message is shown
assert!(!chat_id.is_special());
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", false)
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false)
.await
.unwrap();
@@ -3325,7 +3255,6 @@ Hello mailinglist!\r\n"
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -3357,7 +3286,6 @@ Hello mailinglist!\r\n"
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -3386,7 +3314,6 @@ Hello mailinglist!\r\n"
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -3410,7 +3337,6 @@ Hello mailinglist!\r\n"
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_dhl.eml"),
"INBOX",
false,
)
.await
@@ -3436,7 +3362,6 @@ Hello mailinglist!\r\n"
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_dpd.eml"),
"INBOX",
false,
)
.await
@@ -3462,7 +3387,6 @@ Hello mailinglist!\r\n"
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_xt_local_microsoft.eml"),
"INBOX",
false,
)
.await?;
@@ -3474,7 +3398,6 @@ Hello mailinglist!\r\n"
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_xt_local_spiegel.eml"),
"INBOX",
false,
)
.await?;
@@ -3494,7 +3417,6 @@ Hello mailinglist!\r\n"
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_xing.eml"),
"INBOX",
false,
)
.await?;
@@ -3516,7 +3438,6 @@ Hello mailinglist!\r\n"
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_ttline.eml"),
"INBOX",
false,
)
.await?;
@@ -3543,7 +3464,6 @@ Hello mailinglist!\r\n"
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_with_mimepart_footer.eml"),
"INBOX",
false,
)
.await
@@ -3573,7 +3493,6 @@ Hello mailinglist!\r\n"
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_with_mimepart_footer_signed.eml"),
"INBOX",
false,
)
.await
@@ -3599,9 +3518,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
dc_receive_imf(&t, GH_MAILINGLIST, "INBOX", false)
.await
.unwrap();
dc_receive_imf(&t, GH_MAILINGLIST, false).await.unwrap();
let chat_id = t.get_last_msg().await.chat_id;
chat_id.accept(&t).await.unwrap();
@@ -3609,9 +3526,7 @@ Hello mailinglist!\r\n"
assert!(chat.can_send(&t).await.unwrap());
let imf_raw = format!("In-Reply-To: 3333@example.org\n{}", GH_MAILINGLIST2);
dc_receive_imf(&t, imf_raw.as_bytes(), "INBOX", false)
.await
.unwrap();
dc_receive_imf(&t, imf_raw.as_bytes(), false).await.unwrap();
assert_eq!(
t.get_last_msg().await.in_reply_to.unwrap(),
@@ -3678,7 +3593,6 @@ YEAAAAAA!.
addr
)
.as_bytes(),
"Sent",
false,
)
.await
@@ -3735,7 +3649,6 @@ YEAAAAAA!.
dc_receive_imf(
&t,
include_bytes!("../test-data/message/many_images_amazon_via_apple_mail.eml"),
"INBOX",
false,
)
.await
@@ -3768,7 +3681,6 @@ YEAAAAAA!.
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello foo\n",
"INBOX",
false,
)
.await
@@ -3786,7 +3698,6 @@ YEAAAAAA!.
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
reply foo\n",
"INBOX",
false,
)
.await
@@ -3835,7 +3746,6 @@ YEAAAAAA!.
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello foo\n",
"INBOX",
false,
)
.await
@@ -3854,7 +3764,6 @@ YEAAAAAA!.
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
classic reply\n",
"INBOX",
false,
)
.await
@@ -3881,7 +3790,6 @@ YEAAAAAA!.
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
chat reply\n",
"INBOX",
false,
)
.await
@@ -3909,7 +3817,6 @@ YEAAAAAA!.
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
private reply\n",
"INBOX",
false,
)
.await
@@ -4024,7 +3931,7 @@ YEAAAAAA!.
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
dc_receive_imf(&alice, claire_request.as_bytes(), "INBOX", false)
dc_receive_imf(&alice, claire_request.as_bytes(), false)
.await
.unwrap();
@@ -4050,7 +3957,7 @@ YEAAAAAA!.
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
dc_receive_imf(&claire, claire_request.as_bytes(), "INBOX", false)
dc_receive_imf(&claire, claire_request.as_bytes(), false)
.await
.unwrap();
@@ -4086,7 +3993,7 @@ YEAAAAAA!.
// Check that Alice gets the message in the same chat.
let request = alice.get_last_msg().await;
dc_receive_imf(&alice, reply, "INBOX", false).await.unwrap();
dc_receive_imf(&alice, reply, false).await.unwrap();
let answer = alice.get_last_msg().await;
assert_eq!(answer.get_subject(), "Re: i have a question");
assert!(answer.get_text().unwrap().contains("the version is 1.0"));
@@ -4109,9 +4016,7 @@ YEAAAAAA!.
// Check that Claire also gets the message in the same chat.
let request = claire.get_last_msg().await;
dc_receive_imf(&claire, reply, "INBOX", false)
.await
.unwrap();
dc_receive_imf(&claire, reply, false).await.unwrap();
let answer = claire.get_last_msg().await;
assert_eq!(answer.get_subject(), "Re: i have a question");
assert!(answer.get_text().unwrap().contains("the version is 1.0"));
@@ -4179,7 +4084,6 @@ YEAAAAAA!.
Message-ID: <4444@example.org>\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -4206,7 +4110,6 @@ YEAAAAAA!.
In-Reply-To: <4444@example.org\n\
\n\
Reply\n",
"INBOX",
false,
)
.await
@@ -4234,7 +4137,6 @@ To: <me@other.maildomain.com>
From: <alice@example.org>
Message content",
"Inbox",
false,
)
.await
@@ -4264,7 +4166,6 @@ To: <bob@example.org>
From: <alice@example.org>
Message content",
"Sent",
false,
)
.await
@@ -4313,18 +4214,18 @@ Message content
--
Second signature";
dc_receive_imf(&alice, first_message, "Inbox", false).await?;
dc_receive_imf(&alice, first_message, false).await?;
let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
assert_eq!(contact.get_status(), "First signature");
assert_eq!(contact.get_display_name(), "Bob1");
dc_receive_imf(&alice, second_message, "Inbox", false).await?;
dc_receive_imf(&alice, second_message, false).await?;
let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
assert_eq!(contact.get_status(), "Second signature");
assert_eq!(contact.get_display_name(), "Bob2");
// Duplicate message, should be ignored
dc_receive_imf(&alice, first_message, "Inbox", false).await?;
dc_receive_imf(&alice, first_message, false).await?;
// No change because last message is duplicate of the first.
let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
@@ -4364,7 +4265,6 @@ Message-ID: <Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de>"
}
)
.as_bytes(),
"Inbox",
false,
)
.await
@@ -4406,7 +4306,6 @@ Private reply"#,
}
)
.as_bytes(),
"Inbox",
false,
)
.await
@@ -4457,7 +4356,6 @@ Message-ID: <Gr.iy1KCE2y65_.mH2TM52miv9@testrun.org>"
}
)
.as_bytes(),
"Inbox",
false,
)
.await
@@ -4504,7 +4402,6 @@ Sent with my Delta Chat Messenger: https://delta.chat
}
)
.as_bytes(),
"Inbox",
false,
)
.await
@@ -4547,7 +4444,6 @@ Message-ID: <Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de>"
}
)
.as_bytes(),
"Inbox",
false,
)
.await
@@ -4586,7 +4482,6 @@ Outgoing reply to all"#,
}
)
.as_bytes(),
"Inbox",
false,
)
.await
@@ -4610,7 +4505,6 @@ MIME-Version: 1.0
In-Reply-To: <outgoing@testrun.org>
Reply to all"#,
"Inbox",
false,
)
.await
@@ -4653,15 +4547,15 @@ Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Second thread."#;
// Alice receives two classic emails from Claire.
dc_receive_imf(&alice, first_thread_mime, "Inbox", false).await?;
dc_receive_imf(&alice, first_thread_mime, false).await?;
let alice_first_msg = alice.get_last_msg().await;
dc_receive_imf(&alice, second_thread_mime, "Inbox", false).await?;
dc_receive_imf(&alice, second_thread_mime, false).await?;
let alice_second_msg = alice.get_last_msg().await;
// Bob receives the same two emails.
dc_receive_imf(&bob, first_thread_mime, "Inbox", false).await?;
dc_receive_imf(&bob, first_thread_mime, false).await?;
let bob_first_msg = bob.get_last_msg().await;
dc_receive_imf(&bob, second_thread_mime, "Inbox", false).await?;
dc_receive_imf(&bob, second_thread_mime, false).await?;
let bob_second_msg = bob.get_last_msg().await;
// Messages go to separate chats both for Alice and Bob.
@@ -4714,7 +4608,7 @@ Second thread."#;
let mdn_body = rendered_mdn.message;
// Alice receives the read receipt.
dc_receive_imf(&alice, mdn_body.as_bytes(), "INBOX", false).await?;
dc_receive_imf(&alice, mdn_body.as_bytes(), false).await?;
// Chat should not pop up in the chatlist.
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
@@ -4731,7 +4625,6 @@ Second thread."#;
dc_receive_imf(
&t,
include_bytes!("../test-data/message/gmx-forward.eml"),
"INBOX",
false,
)
.await?;
@@ -4748,7 +4641,7 @@ Second thread."#;
async fn test_incoming_contact_request() -> Result<()> {
let t = TestContext::new_alice().await;
dc_receive_imf(&t, MSGRMSG, "INBOX", false).await?;
dc_receive_imf(&t, MSGRMSG, false).await?;
let msg = t.get_last_msg().await;
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
assert!(chat.is_contact_request());
@@ -4781,7 +4674,7 @@ From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#;
dc_receive_imf(&t, mime, "INBOX", false).await?;
dc_receive_imf(&t, mime, false).await?;
let first = t.get_last_msg().await;
let mime = br#"Subject: Second
Message-ID: second@example.net
@@ -4790,7 +4683,7 @@ From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#;
dc_receive_imf(&t, mime, "INBOX", false).await?;
dc_receive_imf(&t, mime, false).await?;
let second = t.get_last_msg().await;
let mime = br#"Subject: Third
Message-ID: third@example.net
@@ -4799,7 +4692,7 @@ From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#;
dc_receive_imf(&t, mime, "INBOX", false).await?;
dc_receive_imf(&t, mime, false).await?;
let third = t.get_last_msg().await;
let mime = br#"Subject: Message with references.
@@ -4850,7 +4743,7 @@ Message with references."#;
// Alice sends a message to Bob using Thunderbird.
let raw = include_bytes!("../test-data/message/rfc1847_encapsulation.eml");
dc_receive_imf(&bob, raw, "INBOX", false).await?;
dc_receive_imf(&bob, raw, false).await?;
let msg = bob.get_last_msg().await;
assert!(msg.get_showpadlock());
@@ -4865,7 +4758,7 @@ Message with references."#;
let mime = include_bytes!("../test-data/message/invalid_email_to.eml");
// dc_receive_imf should not fail on this mail with invalid To: field
dc_receive_imf(&alice, mime, "Inbox", false).await?;
dc_receive_imf(&alice, mime, false).await?;
Ok(())
}
@@ -4891,7 +4784,6 @@ From: <alice@example.org>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hi, I created a group"#,
"INBOX",
false,
)
.await?;
@@ -4916,7 +4808,6 @@ To: holger <alice@example.org>
Reply from different address
",
"INBOX",
false,
)
.await?;
@@ -5116,4 +5007,70 @@ Reply from different address
Ok(())
}
#[async_std::test]
async fn test_no_private_reply_to_blocked_account() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// =============== Bob creates a group ===============
let group_id =
chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
chat::add_to_chat_contacts_table(
&bob,
group_id,
bob.add_or_lookup_contact(&alice).await.id,
)
.await?;
// =============== Bob sends the first message to the group ===============
let sent = bob.send_text(group_id, "Hello all!").await;
alice.recv_msg(&sent).await;
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
assert_eq!(chats.len(), 1);
// =============== Bob blocks Alice ================
Contact::block(&bob, bob.add_or_lookup_contact(&alice).await.id).await?;
// =============== Alice replies private to Bob ==============
let received = alice.get_last_msg().await;
assert_eq!(received.text, Some("Hello all!".to_string()));
let received_group = Chat::load_from_db(&alice, received.chat_id).await?;
assert_eq!(received_group.typ, Chattype::Group);
let mut msg_out = Message::new(Viewtype::Text);
msg_out.set_text(Some("Private reply".to_string()));
msg_out.set_quote(&alice, Some(&received)).await?;
let alice_bob_chat = alice.create_chat(&bob).await;
let sent2 = alice.send_msg(alice_bob_chat.id, &mut msg_out).await;
bob.recv_msg(&sent2).await;
// ========= check that no contact request was created ============
let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0).unwrap();
let chat = Chat::load_from_db(&bob, chat_id).await.unwrap();
// since only chat is a group, no new open chat has been created
assert_eq!(chat.typ, Chattype::Group);
let received = bob.get_last_msg().await;
assert_eq!(received.text, Some("Hello all!".to_string()));
// =============== Bob unblocks Alice ================
// test if the blocked chat is restored correctly
Contact::unblock(&bob, bob.add_or_lookup_contact(&alice).await.id).await?;
let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2);
let chat_id = chats.get_chat_id(0).unwrap();
let chat = Chat::load_from_db(&bob, chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Single);
let received = bob.get_last_msg().await;
assert_eq!(received.text, Some("Private reply".to_string()));
Ok(())
}
}

View File

@@ -86,7 +86,7 @@ pub(crate) fn dc_gm2local_offset() -> i64 {
// `last_smeared_timestamp` is again in sync with the normal time.
// - however, we do not do all this for the far future,
// but at max `MAX_SECONDS_TO_LEND_FROM_FUTURE`
const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
pub(crate) const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
/// Returns the current smeared timestamp,
///
@@ -214,7 +214,10 @@ pub(crate) fn dc_create_id() -> String {
rng.fill(&mut arr[..]);
// Take 11 base64 characters containing 66 random bits.
base64::encode(&arr).chars().take(11).collect()
base64::encode_config(&arr, base64::URL_SAFE)
.chars()
.take(11)
.collect()
}
/// Function generates a Message-ID that can be used for a new outgoing message.
@@ -700,7 +703,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
dc_receive_imf(&t, raw, "INBOX", false).await.unwrap();
dc_receive_imf(&t, raw, false).await.unwrap();
let msg = t.get_last_msg().await;
let msg_info = get_msg_info(&t, msg.id).await.unwrap();
@@ -762,6 +765,15 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
assert_eq!(buf.len(), 11);
}
#[test]
fn test_dc_create_id_invalid_chars() {
for _ in 1..1000 {
let buf = dc_create_id();
assert!(!buf.contains('/')); // `/` must not be used to be URL-safe
assert!(!buf.contains('.')); // `.` is used as a delimiter when extracting grpid from Message-ID
}
}
#[test]
fn test_dc_extract_grpid_from_rfc724_mid() {
// Should return None if we pass invalid mid

View File

@@ -346,7 +346,6 @@ mod tests {
&t,
"Mr.12345678901@example.com",
header.as_bytes(),
"INBOX",
false,
Some(100000),
false,
@@ -364,7 +363,6 @@ mod tests {
&t,
"Mr.12345678901@example.com",
format!("{}\n\n100k text...", header).as_bytes(),
"INBOX",
false,
None,
false,
@@ -400,7 +398,6 @@ mod tests {
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain",
"INBOX",
false,
Some(100000),
false,

View File

@@ -2,7 +2,7 @@
use std::collections::HashSet;
use anyhow::{bail, format_err, Context as _, Result};
use anyhow::{format_err, Context as _, Result};
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
@@ -28,13 +28,7 @@ impl EncryptHelper {
let prefer_encrypt =
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
.unwrap_or_default();
let addr = match context.get_config(Config::ConfiguredAddr).await? {
None => {
bail!("addr not configured!");
}
Some(addr) => addr,
};
let addr = context.get_configured_addr().await?;
let public_key = SignedPublicKey::load_self(context).await?;
Ok(EncryptHelper {
@@ -387,13 +381,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
/// [Config::ConfiguredAddr] is configured, this address is returned.
// TODO, remove this once deltachat::key::Key no longer exists.
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context(concat!(
"Failed to get self address, ",
"cannot ensure secret key if not configured."
))?;
let self_addr = context.get_configured_addr().await?;
SignedPublicKey::load_self(context).await?;
Ok(self_addr)
}

View File

@@ -399,37 +399,90 @@ WHERE
}
if updated {
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
context.emit_msgs_changed_without_ids();
}
Ok(())
}
pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiver<()>) {
loop {
let ephemeral_timestamp: Option<i64> = match context
/// Calculates the next timestamp when a message will be deleted due to
/// `delete_device_after` setting being set.
async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<i64>> {
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
let self_chat_id = ChatId::lookup_by_contact(context, ContactId::SELF)
.await?
.unwrap_or_default();
let device_chat_id = ChatId::lookup_by_contact(context, ContactId::DEVICE)
.await?
.unwrap_or_default();
let oldest_message_timestamp: Option<i64> = context
.sql
.query_get_value(
r#"
SELECT min(ephemeral_timestamp)
SELECT min(timestamp)
FROM msgs
WHERE ephemeral_timestamp != 0
WHERE chat_id > ?
AND chat_id != ?
AND chat_id != ?;
"#,
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
paramsv![DC_CHAT_ID_TRASH, self_chat_id, device_chat_id],
)
.await
{
.await?;
Ok(oldest_message_timestamp.map(|x| x.saturating_add(delete_device_after)))
} else {
Ok(None)
}
}
/// Calculates next timestamp when expiration of some message will happen.
///
/// Expiration can happen either because user has set `delete_device_after` setting or because the
/// message itself has an ephemeral timer.
async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
let ephemeral_timestamp: Option<i64> = match context
.sql
.query_get_value(
r#"
SELECT min(ephemeral_timestamp)
FROM msgs
WHERE ephemeral_timestamp != 0
AND chat_id != ?;
"#,
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
)
.await
{
Err(err) => {
warn!(context, "Can't calculate next ephemeral timeout: {}", err);
None
}
Ok(ephemeral_timestamp) => ephemeral_timestamp,
};
let delete_device_after_timestamp: Option<i64> =
match next_delete_device_after_timestamp(context).await {
Err(err) => {
warn!(context, "Can't calculate next ephemeral timeout: {}", err);
warn!(
context,
"Can't calculate timestamp of the next message expiration: {}", err
);
None
}
Ok(ephemeral_timestamp) => ephemeral_timestamp,
Ok(timestamp) => timestamp,
};
ephemeral_timestamp
.into_iter()
.chain(delete_device_after_timestamp.into_iter())
.min()
}
pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiver<()>) {
loop {
let ephemeral_timestamp = next_expiration_timestamp(context).await;
let now = SystemTime::now();
let until = if let Some(ephemeral_timestamp) = ephemeral_timestamp {
UNIX_EPOCH
@@ -524,6 +577,7 @@ mod tests {
use super::*;
use crate::config::Config;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::MAX_SECONDS_TO_LEND_FROM_FUTURE;
use crate::download::DownloadState;
use crate::test_utils::TestContext;
use crate::{
@@ -812,44 +866,106 @@ mod tests {
#[async_std::test]
async fn test_ephemeral_delete_msgs() -> Result<()> {
let t = TestContext::new_alice().await;
let chat = t.get_self_chat().await;
let self_chat = t.get_self_chat().await;
t.send_text(chat.id, "Saved message, which we delete manually")
assert_eq!(next_expiration_timestamp(&t).await, None);
t.send_text(self_chat.id, "Saved message, which we delete manually")
.await;
let msg = t.get_last_msg_in(chat.id).await;
let msg = t.get_last_msg_in(self_chat.id).await;
msg.id.delete_from_db(&t).await?;
check_msg_was_deleted(&t, &chat, msg.id).await;
check_msg_is_deleted(&t, &self_chat, msg.id).await;
chat.id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
self_chat
.id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 })
.await
.unwrap();
// Send a saved message which will be deleted after 3600s
let now = time();
let msg = t
.send_text(chat.id, "Saved message, disappearing after 60s")
.await;
let msg = t.send_text(self_chat.id, "Message text").await;
delete_expired_messages(&t, now + 59).await?;
let loaded = Message::load_from_db(&t, msg.sender_msg_id).await?;
assert_eq!(
loaded.text.unwrap(),
"Saved message, disappearing after 60s"
);
assert_eq!(loaded.chat_id, chat.id);
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
.await
.unwrap();
delete_expired_messages(&t, time() + 61).await?;
let loaded = Message::load_from_db(&t, msg.sender_msg_id).await?;
assert_eq!(loaded.text.unwrap(), "");
assert_eq!(loaded.chat_id, DC_CHAT_ID_TRASH);
// Set DeleteDeviceAfter to 1800s. Thend send a saved message which will
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
.await?;
// Check that the msg was deleted locally.
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
let now = time();
let msg = t.send_text(self_chat.id, "Message text").await;
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
.await
.unwrap();
// Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter.
let bob_chat = t.create_chat_with_contact("", "bob@example.net").await;
let now = time();
let msg = t.send_text(bob_chat.id, "Message text").await;
check_msg_will_be_deleted(
&t,
msg.sender_msg_id,
&bob_chat,
now + 1799,
// The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and
// therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later.
time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE,
)
.await
.unwrap();
// Enable ephemeral messages with Bob -> message will be deleted after 60s.
// This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time).
bob_chat
.id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
.await?;
let now = time();
let msg = t.send_text(bob_chat.id, "Message text").await;
check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
.await
.unwrap();
Ok(())
}
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
async fn check_msg_will_be_deleted(
t: &TestContext,
msg_id: MsgId,
chat: &Chat,
not_deleted_at: i64,
deleted_at: i64,
) -> Result<()> {
let next_expiration = next_expiration_timestamp(t).await.unwrap();
assert!(next_expiration > not_deleted_at);
delete_expired_messages(t, not_deleted_at).await?;
let loaded = Message::load_from_db(t, msg_id).await?;
assert_eq!(loaded.text.unwrap(), "Message text");
assert_eq!(loaded.chat_id, chat.id);
assert!(next_expiration < deleted_at);
delete_expired_messages(t, deleted_at).await?;
let loaded = Message::load_from_db(t, msg_id).await?;
assert_eq!(loaded.text.unwrap(), "");
assert_eq!(loaded.chat_id, DC_CHAT_ID_TRASH);
// Check that the msg was deleted locally.
check_msg_is_deleted(t, chat, msg_id).await;
Ok(())
}
async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await.unwrap();
// Check that the chat is empty except for possibly info messages:
for item in &chat_items {
@@ -999,7 +1115,6 @@ mod tests {
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await?;
@@ -1020,7 +1135,6 @@ mod tests {
Ephemeral-Timer: 60\n\
\n\
second message\n",
"INBOX",
false,
)
.await?;
@@ -1057,7 +1171,6 @@ mod tests {
In-Reply-To: <first@example.com>\n\
\n\
> hello\n",
"INBOX",
false,
)
.await?;

View File

@@ -10,6 +10,7 @@ use crate::chat::ChatId;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::webxdc::StatusUpdateSerial;
#[derive(Debug)]
pub struct Events {
@@ -335,5 +336,8 @@ pub enum EventType {
SelfavatarChanged,
#[strum(props(id = "2120"))]
WebxdcStatusUpdate(MsgId),
WebxdcStatusUpdate {
msg_id: MsgId,
status_update_serial: StatusUpdateSerial,
},
}

View File

@@ -440,7 +440,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
.create_chat_with_contact("", "sender@testrun.org")
.await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
dc_receive_imf(&alice, raw, "INBOX", false).await.unwrap();
dc_receive_imf(&alice, raw, false).await.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::No);
@@ -489,7 +489,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
.create_chat_with_contact("", "sender@testrun.org")
.await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
dc_receive_imf(&alice, raw, "INBOX", false).await.unwrap();
dc_receive_imf(&alice, raw, false).await.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
// forward the message to saved-messages,
@@ -555,7 +555,6 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
dc_receive_imf(
&t,
include_bytes!("../test-data/message/cp1252-html.eml"),
"INBOX",
false,
)
.await?;

View File

@@ -7,6 +7,7 @@ use std::{
cmp,
cmp::max,
collections::{BTreeMap, BTreeSet},
iter::Peekable,
};
use anyhow::{bail, format_err, Context as _, Result};
@@ -31,14 +32,13 @@ use crate::dc_receive_imf::{
use crate::dc_tools::dc_create_id;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{self, Action};
use crate::job;
use crate::login_param::{
CertificateChecks, LoginParam, ServerAddress, ServerLoginParam, Socks5Config,
};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::mimeparser;
use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::provider::Socket;
use crate::scheduler::connectivity::ConnectivityStore;
use crate::scheduler::InterruptInfo;
@@ -131,7 +131,7 @@ impl FolderMeaning {
fn to_config(self) -> Option<Config> {
match self {
FolderMeaning::Unknown => None,
FolderMeaning::Spam => Some(Config::ConfiguredSpamFolder),
FolderMeaning::Spam => None,
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
FolderMeaning::Drafts => None,
FolderMeaning::Other => None,
@@ -165,6 +165,67 @@ struct ImapConfig {
pub can_condstore: bool,
}
struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
inner: Peekable<T>,
}
impl<T, I> From<I> for UidGrouper<T>
where
T: Iterator<Item = (i64, u32, String)>,
I: IntoIterator<IntoIter = T>,
{
fn from(inner: I) -> Self {
Self {
inner: inner.into_iter().peekable(),
}
}
}
impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
// Tuple of folder, row IDs, and UID range as a string.
type Item = (String, Vec<i64>, String);
fn next(&mut self) -> Option<Self::Item> {
let (_, _, folder) = self.inner.peek().cloned()?;
let mut uid_set = String::new();
let mut rowid_set = Vec::new();
while uid_set.len() < 1000 {
// Construct a new range.
if let Some((start_rowid, start_uid, _)) = self
.inner
.next_if(|(_, _, start_folder)| start_folder == &folder)
{
rowid_set.push(start_rowid);
let mut end_uid = start_uid;
while let Some((next_rowid, next_uid, _)) =
self.inner.next_if(|(_, next_uid, next_folder)| {
next_folder == &folder && *next_uid == end_uid + 1
})
{
end_uid = next_uid;
rowid_set.push(next_rowid);
}
let uid_range = UidRange {
start: start_uid,
end: end_uid,
};
if !uid_set.is_empty() {
uid_set.push(',');
}
uid_set.push_str(&uid_range.to_string());
} else {
break;
}
}
Some((folder, rowid_set, uid_set))
}
}
impl Imap {
/// Creates new disconnected IMAP client using the specific login parameters.
///
@@ -224,7 +285,7 @@ impl Imap {
bail!("IMAP Connect without configured params");
}
let param = LoginParam::from_database(context, "configured_").await?;
let param = LoginParam::load_configured_params(context).await?;
// the trailing underscore is correct
let imap = Self::new(
@@ -451,16 +512,29 @@ impl Imap {
///
/// Prefetches headers and downloads new message from the folder, moves messages away from the
/// folder and deletes messages in the folder.
pub async fn fetch_move_delete(&mut self, context: &Context, watch_folder: &str) -> Result<()> {
pub async fn fetch_move_delete(
&mut self,
context: &Context,
watch_folder: &str,
is_spam_folder: bool,
) -> Result<()> {
if !context.sql.is_open().await {
// probably shutdown
bail!("IMAP operation attempted while it is torn down");
}
self.prepare(context).await?;
self.fetch_new_messages(context, watch_folder, false)
let msgs_fetched = self
.fetch_new_messages(context, watch_folder, is_spam_folder, false)
.await
.context("fetch_new_messages")?;
if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
// New messages were fetched and shall be deleted later, restart ephemeral loop.
// Note that the `Config::DeleteDeviceAfter` timer starts as soon as the messages are
// fetched while the per-chat ephemeral timers start as soon as the messages are marked
// as noticed.
context.interrupt_ephemeral_task().await;
}
self.move_delete_messages(context, watch_folder)
.await
@@ -658,13 +732,17 @@ impl Imap {
Ok(false)
}
/// Fetches new messages.
///
/// Returns true if at least one message was fetched.
pub(crate) async fn fetch_new_messages(
&mut self,
context: &Context,
folder: &str,
is_spam_folder: bool,
fetch_existing_msgs: bool,
) -> Result<bool> {
if should_ignore_folder(context, folder).await? {
if should_ignore_folder(context, folder, is_spam_folder).await? {
info!(context, "Not fetching from {}", folder);
return Ok(false);
}
@@ -707,7 +785,7 @@ impl Imap {
// Get the Message-ID or generate a fake one to identify the message in the database.
let message_id = prefetch_get_message_id(&headers).unwrap_or_else(dc_create_id);
let target = match target_folder(context, folder, &headers).await? {
let target = match target_folder(context, folder, is_spam_folder, &headers).await? {
Some(config) => match context.get_config(config).await? {
Some(target) => target,
None => folder.to_string(),
@@ -738,7 +816,7 @@ impl Imap {
// If the sender is known, the message will be moved to the Inbox or Mvbox
// and then we download the message from there.
// Also see `spam_target_folder()`.
&& !context.is_spam_folder(folder).await?
&& !is_spam_folder
&& prefetch_should_download(
context,
&headers,
@@ -933,7 +1011,7 @@ impl Imap {
///
/// This is the only place where messages are moved or deleted on the IMAP server.
async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
let mut rows = context
let rows = context
.sql
.query_map(
"SELECT id, uid, target FROM imap
@@ -949,48 +1027,12 @@ impl Imap {
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?
.into_iter()
.peekable();
.await?;
self.prepare(context).await?;
self.select_folder(context, Some(folder)).await?;
while let Some((_, _, target)) = rows.peek().cloned() {
// Construct next request for the target folder.
let mut uid_set = String::new();
let mut rowid_set = Vec::new();
while uid_set.len() < 1000 {
// Construct a new range.
if let Some((start_rowid, start_uid, _)) =
rows.next_if(|(_, _, start_target)| start_target == &target)
{
rowid_set.push(start_rowid);
let mut end_uid = start_uid;
while let Some((next_rowid, next_uid, _)) =
rows.next_if(|(_, next_uid, next_target)| {
next_target == &target && *next_uid == end_uid + 1
})
{
end_uid = next_uid;
rowid_set.push(next_rowid);
}
let uid_range = UidRange {
start: start_uid,
end: end_uid,
};
if !uid_set.is_empty() {
uid_set.push(',');
}
uid_set.push_str(&uid_range.to_string());
} else {
break;
}
}
for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
// Empty target folder name means messages should be deleted.
if target.is_empty() {
self.delete_message_batch(context, &uid_set, rowid_set)
@@ -1017,6 +1059,62 @@ impl Imap {
Ok(())
}
/// Stores pending `\Seen` flags for messages in `imap_markseen` table.
pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
self.prepare(context).await?;
let rows = context
.sql
.query_map(
"SELECT imap.id, uid, folder FROM imap, imap_markseen
WHERE imap.id = imap_markseen.id AND target = folder
ORDER BY folder, uid",
[],
|row| {
let rowid: i64 = row.get(0)?;
let uid: u32 = row.get(1)?;
let folder: String = row.get(2)?;
Ok((rowid, uid, folder))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
self.select_folder(context, Some(&folder))
.await
.context("failed to select folder")?;
if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
warn!(
context,
"Cannot mark messages {} in folder {} as seen, will retry later: {}.",
uid_set,
folder,
err
);
} else {
info!(
context,
"Marked messages {} in folder {} as seen.", uid_set, folder
);
context
.sql
.execute(
format!(
"DELETE FROM imap_markseen WHERE id IN ({})",
sql::repeat_vars(rowid_set.len())?
),
rusqlite::params_from_iter(rowid_set),
)
.await
.context("cannot remove messages marked as seen from imap_markseen table")?;
}
}
Ok(())
}
/// Synchronizes `\Seen` flags using `CONDSTORE` extension.
pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
if !self.config.can_condstore {
@@ -1108,11 +1206,7 @@ impl Imap {
.session
.as_mut()
.context("IMAP No Connection established")?;
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context("not configured")?;
let self_addr = context.get_configured_addr().await?;
let search_command = format!("FROM \"{}\"", self_addr);
let uids = session
.uid_search(search_command)
@@ -1262,8 +1356,6 @@ impl Imap {
}
};
let folder = folder.to_string();
while let Some(Ok(msg)) = msgs.next().await {
let server_uid = msg.uid.unwrap_or_default();
@@ -1297,7 +1389,6 @@ impl Imap {
// XXX put flags into a set and pass them to dc_receive_imf
let context = context.clone();
let folder = folder.clone();
// safe, as we checked above that there is a body.
let body = body
@@ -1318,7 +1409,6 @@ impl Imap {
&context,
rfc724_mid,
body,
&folder,
is_seen,
partial,
fetching_existing_messages,
@@ -1357,11 +1447,6 @@ impl Imap {
/// the flag, or other imap-errors, returns true as well.
///
/// Returning error means that the operation can be retried.
async fn add_flag_finalized(&mut self, server_uid: u32, flag: &str) -> Result<()> {
let s = server_uid.to_string();
self.add_flag_finalized_with_set(&s, flag).await
}
async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
if self.should_reconnect() {
bail!("Can't set flag, should reconnect");
@@ -1379,7 +1464,7 @@ impl Imap {
Ok(())
}
pub async fn prepare_imap_operation_on_msg(
pub(crate) async fn prepare_imap_operation_on_msg(
&mut self,
context: &Context,
folder: &str,
@@ -1419,32 +1504,6 @@ impl Imap {
}
}
pub(crate) async fn set_seen(
&mut self,
context: &Context,
folder: &str,
uid: u32,
) -> ImapActionResult {
if let Some(imapresult) = self
.prepare_imap_operation_on_msg(context, folder, uid)
.await
{
return imapresult;
}
// we are connected, and the folder is selected
info!(context, "Marking message {}/{} as seen...", folder, uid,);
if let Err(err) = self.add_flag_finalized(uid, "\\Seen").await {
warn!(
context,
"Cannot mark message {} in folder {} as seen, ignoring: {}.", uid, folder, err
);
ImapActionResult::Failed
} else {
ImapActionResult::Success
}
}
pub async fn ensure_configured_folders(
&mut self,
context: &Context,
@@ -1677,13 +1736,14 @@ async fn spam_target_folder(
pub async fn target_folder(
context: &Context,
folder: &str,
is_spam_folder: bool,
headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Config>> {
if context.is_mvbox(folder).await? {
return Ok(None);
}
if context.is_spam_folder(folder).await? {
if is_spam_folder {
spam_target_folder(context, headers).await
} else if needs_move_to_mvbox(context, headers).await? {
Ok(Some(Config::ConfiguredMvboxFolder))
@@ -1875,13 +1935,11 @@ pub(crate) async fn prefetch_should_download(
mut flags: impl Iterator<Item = Flag<'_>>,
show_emails: ShowEmails,
) -> Result<bool> {
if let Some(msg_id) = message::rfc724_mid_exists(context, message_id).await? {
// We know the Message-ID already, it must be a Bcc: to self.
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
)
.await?;
if message::rfc724_mid_exists(context, message_id)
.await?
.is_some()
{
markseen_on_imap_table(context, message_id).await?;
return Ok(false);
}
@@ -2015,6 +2073,22 @@ async fn mark_seen_by_uid(
}
}
/// Schedule marking the message as Seen on IMAP by adding all known IMAP messages corresponding to
/// the given Message-ID to `imap_markseen` table.
pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
context
.sql
.execute(
"INSERT OR IGNORE INTO imap_markseen (id)
SELECT id FROM imap WHERE rfc724_mid=?",
paramsv![message_id],
)
.await?;
context.interrupt_inbox(InterruptInfo::new(false)).await;
Ok(())
}
/// uid_next is the next unique identifier value from the last time we fetched a folder
/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
/// This function is used to update our uid_next after fetching messages.
@@ -2115,7 +2189,11 @@ pub async fn get_config_last_seen_uid(context: &Context, folder: &str) -> Result
///
/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
/// not explicitly watched should not be fetched.
async fn should_ignore_folder(context: &Context, folder: &str) -> Result<bool> {
async fn should_ignore_folder(
context: &Context,
folder: &str,
is_spam_folder: bool,
) -> Result<bool> {
if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
return Ok(false);
}
@@ -2123,7 +2201,7 @@ async fn should_ignore_folder(context: &Context, folder: &str) -> Result<bool> {
// Still respect the SentboxWatch setting.
return Ok(!context.get_config_bool(Config::SentboxWatch).await?);
}
Ok(!(context.is_mvbox(folder).await? || context.is_spam_folder(folder).await?))
Ok(!(context.is_mvbox(folder).await? || is_spam_folder))
}
/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
@@ -2299,9 +2377,6 @@ mod tests {
folder, mvbox_move, chat_msg, accepted_chat, outgoing, setupmessage);
let t = TestContext::new_alice().await;
t.ctx
.set_config(Config::ConfiguredSpamFolder, Some("Spam"))
.await?;
t.ctx
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
.await?;
@@ -2343,11 +2418,13 @@ mod tests {
let (headers, _) = mailparse::parse_headers(bytes)?;
let actual = if let Some(config) = target_folder(&t, folder, &headers).await? {
t.get_config(config).await?
} else {
None
};
let is_spam_folder = folder == "Spam";
let actual =
if let Some(config) = target_folder(&t, folder, is_spam_folder, &headers).await? {
t.get_config(config).await?
} else {
None
};
let expected = if expected_destination == folder {
None

View File

@@ -157,7 +157,10 @@ impl Imap {
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
match self.fetch_new_messages(context, &watch_folder, false).await {
match self
.fetch_new_messages(context, &watch_folder, false, false)
.await
{
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {

View File

@@ -60,6 +60,9 @@ impl Imap {
let is_drafts = folder_meaning == FolderMeaning::Drafts
|| (folder_meaning == FolderMeaning::Unknown
&& folder_name_meaning == FolderMeaning::Drafts);
let is_spam_folder = folder_meaning == FolderMeaning::Spam
|| (folder_meaning == FolderMeaning::Unknown
&& folder_name_meaning == FolderMeaning::Spam);
// Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
@@ -67,7 +70,7 @@ impl Imap {
self.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, folder.name())
self.fetch_move_delete(context, folder.name(), is_spam_folder)
.await
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
@@ -79,16 +82,15 @@ impl Imap {
}
}
// We iterate over both folder meanings to make sure that if e.g. the "Sent" folder was deleted,
// `ConfiguredSentboxFolder` is set to `None`:
for config in &[
Config::ConfiguredSentboxFolder,
Config::ConfiguredSpamFolder,
] {
context
.set_config(*config, folder_configs.get(config).map(|s| s.as_str()))
.await?;
}
// Set the `ConfiguredSentboxFolder` or set it to `None` if the folder was deleted.
context
.set_config(
Config::ConfiguredSentboxFolder,
folder_configs
.get(&Config::ConfiguredSentboxFolder)
.map(|s| s.as_str()),
)
.await?;
last_scan.replace(Instant::now());
Ok(true)

View File

@@ -351,9 +351,8 @@ async fn set_self_key(
}
};
let self_addr = context.get_config(Config::ConfiguredAddr).await?;
ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
let self_addr = context.get_configured_addr().await?;
let addr = EmailAddress::new(&self_addr)?;
let keypair = pgp::KeyPair {
addr,
public: public_key,

View File

@@ -4,7 +4,7 @@
//! and job types.
use std::fmt;
use anyhow::{bail, format_err, Context as _, Result};
use anyhow::{format_err, Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use rand::{thread_rng, Rng};
@@ -13,9 +13,8 @@ use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::dc_tools::time;
use crate::events::EventType;
use crate::imap::{Imap, ImapActionResult};
use crate::imap::Imap;
use crate::location;
use crate::log::LogExt;
use crate::message::{Message, MsgId};
use crate::mimefactory::MimeFactory;
use crate::param::{Param, Params};
@@ -32,7 +31,6 @@ const JOB_RETRIES: u32 = 17;
)]
#[repr(u32)]
pub(crate) enum Thread {
Unknown = 0,
Imap = 100,
Smtp = 5000,
}
@@ -60,12 +58,6 @@ macro_rules! job_try {
};
}
impl Default for Thread {
fn default() -> Self {
Thread::Unknown
}
}
#[derive(
Debug,
Display,
@@ -81,12 +73,8 @@ impl Default for Thread {
)]
#[repr(u32)]
pub enum Action {
Unknown = 0,
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
Housekeeping = 105, // low priority ...
FetchExistingMsgs = 110,
MarkseenMsgOnImap = 130,
// this is user initiated so it should have a fairly high priority
UpdateRecentQuota = 140,
@@ -107,23 +95,13 @@ pub enum Action {
SendMdn = 5010,
}
impl Default for Action {
fn default() -> Self {
Action::Unknown
}
}
impl From<Action> for Thread {
fn from(action: Action) -> Thread {
use Action::*;
match action {
Unknown => Thread::Unknown,
Housekeeping => Thread::Imap,
FetchExistingMsgs => Thread::Imap,
ResyncFolders => Thread::Imap,
MarkseenMsgOnImap => Thread::Imap,
UpdateRecentQuota => Thread::Imap,
DownloadMsg => Thread::Imap,
@@ -357,7 +335,7 @@ impl Job {
Config::ConfiguredSentboxFolder,
] {
if let Some(folder) = job_try!(context.get_config(*config).await) {
if let Err(e) = imap.fetch_new_messages(context, &folder, true).await {
if let Err(e) = imap.fetch_new_messages(context, &folder, false, true).await {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(context, "Could not fetch messages, retrying: {:#}", e);
return Status::RetryLater;
@@ -403,67 +381,6 @@ impl Job {
Status::Finished(Ok(()))
}
}
async fn markseen_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let row = job_try!(
context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap
WHERE rfc724_mid=? AND folder=target
ORDER BY uid ASC
LIMIT 1",
paramsv![msg.rfc724_mid],
|row| {
let uid: u32 = row.get(0)?;
let folder: String = row.get(1)?;
Ok((uid, folder))
}
)
.await
);
if let Some((server_uid, server_folder)) = row {
let result = imap.set_seen(context, &server_folder, server_uid).await;
match result {
ImapActionResult::RetryLater => return Status::RetryLater,
ImapActionResult::Success | ImapActionResult::Failed => {}
}
} else {
info!(
context,
"Can't mark the message {} as seen on IMAP because there is no known UID",
msg.rfc724_mid
);
}
// XXX we send MDN even in case of failure to mark the messages as seen, e.g. if it was
// already deleted on the server by another device. The job will not be retried so locally
// there is no risk of double-sending MDNs.
//
// Read receipts for system messages are never sent. These messages have no place to
// display received read receipt anyway. And since their text is locally generated,
// quoting them is dangerous as it may contain contact names. E.g., for original message
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can
// be a display name stored in address book rather than the name sent in the From field by
// the user.
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default() && !msg.is_system_message() {
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
if mdns_enabled {
if let Err(err) = send_mdn(context, &msg).await {
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
return Status::Finished(Err(err));
}
}
}
Status::Finished(Ok(()))
}
}
/// Delete all pending jobs with the given action.
@@ -653,19 +570,13 @@ async fn perform_job_action(
);
let try_res = match job.action {
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
Action::SendMdn => job.send_mdn(context, connection.smtp()).await,
Action::MaybeSendLocations => location::job_maybe_send_locations(context, job).await,
Action::MaybeSendLocationsEnded => {
location::job_maybe_send_locations_ended(context, job).await
}
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
Action::Housekeeping => {
sql::housekeeping(context).await.ok_or_log(context);
Status::Finished(Ok(()))
}
Action::UpdateRecentQuota => match context.update_recent_quota(connection.inbox()).await {
Ok(status) => status,
Err(err) => Status::Finished(Err(err)),
@@ -698,13 +609,13 @@ fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
}
}
async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
pub(crate) async fn send_mdn(context: &Context, msg_id: MsgId, from_id: ContactId) -> Result<()> {
let mut param = Params::new();
param.set(Param::MsgId, msg.id.to_u32().to_string());
param.set(Param::MsgId, msg_id.to_u32().to_string());
add(
context,
Job::new(Action::SendMdn, msg.from_id.to_u32(), param, 0),
Job::new(Action::SendMdn, from_id.to_u32(), param, 0),
)
.await?;
@@ -729,10 +640,7 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
if delay_seconds == 0 {
match action {
Action::Unknown => unreachable!(),
Action::Housekeeping
| Action::ResyncFolders
| Action::MarkseenMsgOnImap
Action::ResyncFolders
| Action::FetchExistingMsgs
| Action::UpdateRecentQuota
| Action::DownloadMsg => {
@@ -748,18 +656,6 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
Ok(())
}
async fn load_housekeeping_job(context: &Context) -> Result<Option<Job>> {
let last_time = context.get_config_i64(Config::LastHousekeeping).await?;
let next_time = last_time + (60 * 60 * 24);
if next_time <= time() {
kill_action(context, Action::Housekeeping).await?;
Ok(Some(Job::new(Action::Housekeeping, 0, Params::new(), 0)))
} else {
Ok(None)
}
}
/// Load jobs from the database.
///
/// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load
@@ -803,7 +699,7 @@ LIMIT 1;
params = paramsv![thread_i];
};
let job = loop {
loop {
let job_res = context
.sql
.query_row_optional(query, params.clone(), |row| {
@@ -822,7 +718,7 @@ LIMIT 1;
.await;
match job_res {
Ok(job) => break job,
Ok(job) => return Ok(job),
Err(err) => {
// Remove invalid job from the DB
info!(context, "cleaning up job, because of {}", err);
@@ -840,20 +736,6 @@ LIMIT 1;
.with_context(|| format!("Failed to delete invalid job {}", id))?;
}
}
};
match thread {
Thread::Unknown => {
bail!("unknown thread for job")
}
Thread::Imap => {
if let Some(job) = job {
Ok(Some(job))
} else {
Ok(load_housekeeping_job(context).await?)
}
}
Thread::Smtp => Ok(job),
}
}
@@ -901,8 +783,7 @@ mod tests {
&InterruptInfo::new(false),
)
.await?;
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
assert_eq!(jobs.unwrap().action, Action::Housekeeping);
assert!(jobs.is_none());
insert_job(&t, 1, true).await;
let jobs = load_next(

View File

@@ -198,10 +198,7 @@ impl DcSecretKey for SignedSecretKey {
}
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context("no address configured")?;
let addr = context.get_configured_addr().await?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().await;

View File

@@ -6,7 +6,6 @@ use bitflags::bitflags;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::chat::{self, ChatId};
use crate::config::Config;
use crate::contact::ContactId;
use crate::context::Context;
use crate::dc_tools::time;
@@ -424,10 +423,7 @@ pub async fn delete_all(context: &Context) -> Result<()> {
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
let mut last_added_location_id = 0;
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let self_addr = context.get_configured_addr().await?;
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",

View File

@@ -139,8 +139,18 @@ pub struct LoginParam {
}
impl LoginParam {
/// Load entered (candidate) account settings
pub async fn load_candidate_params(context: &Context) -> Result<Self> {
LoginParam::from_database(context, "").await
}
/// Load configured (working) account settings
pub async fn load_configured_params(context: &Context) -> Result<Self> {
LoginParam::from_database(context, "configured_").await
}
/// Read the login parameters from the database.
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Result<Self> {
async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Result<Self> {
let prefix = prefix.as_ref();
let sql = &context.sql;
@@ -242,8 +252,8 @@ impl LoginParam {
}
/// Save this loginparam to the database.
pub async fn save_to_database(&self, context: &Context, prefix: impl AsRef<str>) -> Result<()> {
let prefix = prefix.as_ref();
pub async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
let prefix = "configured_";
let sql = &context.sql;
let key = format!("{}addr", prefix);
@@ -438,8 +448,8 @@ mod tests {
socks5_config: None,
};
param.save_to_database(&t, "foobar_").await?;
let loaded = LoginParam::from_database(&t, "foobar_").await?;
param.save_as_configured_params(&t).await?;
let loaded = LoginParam::load_configured_params(&t).await?;
assert_eq!(param, loaded);
Ok(())

View File

@@ -9,6 +9,7 @@ use rusqlite::types::ValueRef;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
};
@@ -21,7 +22,8 @@ use crate::dc_tools::{
use crate::download::DownloadState;
use crate::ephemeral::{start_ephemeral_timers_msgids, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::job::{self, Action};
use crate::imap::markseen_on_imap_table;
use crate::job;
use crate::log::LogExt;
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
use crate::param::{Param, Params};
@@ -1252,19 +1254,13 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
}
if !msg_ids.is_empty() {
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
job::kill_action(context, Action::Housekeeping).await?;
job::add(
context,
job::Job::new(Action::Housekeeping, 0, Params::new(), 10),
)
.await?;
context.emit_msgs_changed_without_ids();
// Run housekeeping to delete unused blobs.
context.set_config(Config::LastHousekeeping, None).await?;
}
// Interrupt Inbox loop to start message deletion.
// Interrupt Inbox loop to start message deletion and run housekeeping.
context.interrupt_inbox(InterruptInfo::new(false)).await;
Ok(())
}
@@ -1294,6 +1290,9 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
m.chat_id AS chat_id,
m.state AS state,
m.ephemeral_timer AS ephemeral_timer,
m.param AS param,
m.from_id AS from_id,
m.rfc724_mid AS rfc724_mid,
c.blocked AS blocked
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
WHERE m.id IN ({}) AND m.chat_id>9",
@@ -1304,12 +1303,18 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let state: MessageState = row.get("state")?;
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
let from_id: ContactId = row.get("from_id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
let blocked: Option<Blocked> = row.get("blocked")?;
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
Ok((
id,
chat_id,
state,
param,
from_id,
rfc724_mid,
blocked.unwrap_or_default(),
ephemeral_timer,
))
@@ -1318,30 +1323,52 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
)
.await?;
if msgs
.iter()
.any(|(_id, _chat_id, _state, _blocked, ephemeral_timer)| {
if msgs.iter().any(
|(_id, _chat_id, _state, _param, _from_id, _rfc724_mid, _blocked, ephemeral_timer)| {
*ephemeral_timer != EphemeralTimer::Disabled
})
{
},
) {
start_ephemeral_timers_msgids(context, &msg_ids)
.await
.context("failed to start ephemeral timers")?;
}
let mut updated_chat_ids = BTreeSet::new();
for (id, curr_chat_id, curr_state, curr_blocked, _curr_ephemeral_timer) in msgs.into_iter() {
for (
id,
curr_chat_id,
curr_state,
curr_param,
curr_from_id,
curr_rfc724_mid,
curr_blocked,
_curr_ephemeral_timer,
) in msgs.into_iter()
{
if curr_blocked == Blocked::Not
&& (curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed)
{
update_msg_state(context, id, MessageState::InSeen).await?;
info!(context, "Seen message {}.", id);
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0),
)
.await?;
markseen_on_imap_table(context, &curr_rfc724_mid).await?;
// Read receipts for system messages are never sent. These messages have no place to
// display received read receipt anyway. And since their text is locally generated,
// quoting them is dangerous as it may contain contact names. E.g., for original message
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can
// be a display name stored in address book rather than the name sent in the From field by
// the user.
if curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
&& curr_param.get_cmd() == SystemMessage::Unknown
{
let mdns_enabled = context.get_config_bool(Config::MdnsEnabled).await?;
if mdns_enabled {
if let Err(err) = job::send_mdn(context, id, curr_from_id).await {
warn!(context, "could not send out mdn for {}: {}", id, err);
}
}
}
updated_chat_ids.insert(curr_chat_id);
}
}
@@ -2017,7 +2044,6 @@ mod tests {
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -2226,7 +2252,6 @@ mod tests {
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await?;
@@ -2244,7 +2269,6 @@ mod tests {
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
\n\
hello again\n",
"INBOX",
false,
)
.await?;

View File

@@ -133,11 +133,7 @@ impl<'a> MimeFactory<'a> {
) -> Result<MimeFactory<'a>> {
let chat = Chat::load_from_db(context, msg.chat_id).await?;
let from_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let from_addr = context.get_configured_addr().await?;
let config_displayname = context
.get_config(Config::Displayname)
.await?
@@ -237,10 +233,7 @@ impl<'a> MimeFactory<'a> {
ensure!(!msg.chat_id.is_special(), "Invalid chat id");
let contact = Contact::load_from_db(context, msg.from_id).await?;
let from_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let from_addr = context.get_configured_addr().await?;
let from_displayname = context
.get_config(Config::Displayname)
.await?
@@ -278,11 +271,7 @@ impl<'a> MimeFactory<'a> {
&self,
context: &Context,
) -> Result<Vec<(Option<Peerstate>, &str)>> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context("not configured")?;
let self_addr = context.get_configured_addr().await?;
let mut res = Vec::new();
for (_, addr) in self
.recipients
@@ -1672,7 +1661,6 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -1765,7 +1753,6 @@ mod tests {
t.get_last_msg().await.rfc724_mid
)
.as_bytes(),
"INBOX",
false,
)
.await?;
@@ -1876,7 +1863,6 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
Some other, completely unrelated content\n",
"INBOX",
false,
)
.await
@@ -1901,9 +1887,7 @@ mod tests {
.await
.unwrap();
dc_receive_imf(context, imf_raw, "INBOX", false)
.await
.unwrap();
dc_receive_imf(context, imf_raw, false).await.unwrap();
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();

View File

@@ -2915,7 +2915,6 @@ On 2020-10-25, Bob wrote:
dc_receive_imf(
&t.ctx,
include_bytes!("../test-data/message/subj_with_multimedia_msg.eml"),
"INBOX",
false,
)
.await
@@ -3064,7 +3063,7 @@ Subject: ...
Some quote.
"###;
dc_receive_imf(&t, raw, "INBOX", false).await?;
dc_receive_imf(&t, raw, false).await?;
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
let raw = br###"In-Reply-To:
@@ -3081,7 +3080,7 @@ Subject: ...
Some reply
"###;
dc_receive_imf(&t, raw, "INBOX", false).await?;
dc_receive_imf(&t, raw, false).await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "Some reply");
@@ -3109,13 +3108,13 @@ Message.
"###;
// Bob receives message.
dc_receive_imf(&bob, raw, "INBOX", false).await?;
dc_receive_imf(&bob, raw, false).await?;
let msg = bob.get_last_msg().await;
// Message is incoming.
assert!(msg.param.get_bool(Param::WantsMdn).unwrap());
// Alice receives copy-to-self.
dc_receive_imf(&alice, raw, "INBOX", false).await?;
dc_receive_imf(&alice, raw, false).await?;
let msg = alice.get_last_msg().await;
// Message is outgoing, don't send read receipt to self.
assert!(msg.param.get_bool(Param::WantsMdn).is_none());
@@ -3141,7 +3140,6 @@ Message.
\n\
hello\n"
.as_bytes(),
"INBOX",
false,
)
.await?;
@@ -3179,7 +3177,6 @@ Message.
\n\
--SNIPP--"
.as_bytes(),
"INBOX",
false,
)
.await?;
@@ -3202,7 +3199,7 @@ Message.
let original =
include_bytes!("../test-data/message/ms_exchange_report_original_message.eml");
dc_receive_imf(&t, original, "INBOX", false).await?;
dc_receive_imf(&t, original, false).await?;
let original_msg_id = t.get_last_msg().await.id;
// 1. Test mimeparser directly
@@ -3217,7 +3214,7 @@ Message.
assert!(mimeparser.mdn_reports[0].additional_message_ids.is_empty());
// 2. Test that marking the original msg as read works
dc_receive_imf(&t, mdn, "INBOX", false).await?;
dc_receive_imf(&t, mdn, false).await?;
assert_eq!(
original_msg_id.get_state(&t).await?,

View File

@@ -479,7 +479,7 @@ mod tests {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(
async_std::task::block_on(pk_encrypt(
CLEARTEXT,
keyring,
Some(KEYS.alice_secret.clone()),
@@ -492,7 +492,7 @@ mod tests {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
async_std::task::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
});
#[test]

View File

@@ -8,11 +8,13 @@ use async_std::{
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::maybe_add_time_based_warnings;
use crate::dc_tools::time;
use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::imap::Imap;
use crate::job::{self, Thread};
use crate::log::LogExt;
use crate::smtp::{send_smtp_messages, Smtp};
use crate::sql;
use self::connectivity::ConnectivityStore;
@@ -85,8 +87,6 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
.expect("inbox loop, missing started receiver");
let ctx = ctx1;
// track number of continously executed jobs
let mut jobs_loaded = 0;
let mut info = InterruptInfo::default();
loop {
let job = match job::load_next(&ctx, Thread::Imap, &info).await {
@@ -98,22 +98,26 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
};
match job {
Some(job) if jobs_loaded <= 20 => {
jobs_loaded += 1;
Some(job) => {
job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await;
info = Default::default();
}
Some(job) => {
// Let the fetch run, but return back to the job afterwards.
jobs_loaded = 0;
info!(ctx, "postponing imap-job {} to run fetch...", job);
fetch(&ctx, &mut connection).await;
}
None => {
jobs_loaded = 0;
maybe_add_time_based_warnings(&ctx).await;
match ctx.get_config_i64(Config::LastHousekeeping).await {
Ok(last_housekeeping_time) => {
let next_housekeeping_time =
last_housekeeping_time.saturating_add(60 * 60 * 24);
if next_housekeeping_time <= time() {
sql::housekeeping(&ctx).await.ok_or_log(&ctx);
}
}
Err(err) => {
warn!(ctx, "Failed to get last housekeeping time: {}", err);
}
};
info = fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await;
}
}
@@ -133,32 +137,6 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
.expect("inbox loop, missing shutdown receiver");
}
async fn fetch(ctx: &Context, connection: &mut Imap) {
match ctx.get_config(Config::ConfiguredInboxFolder).await {
Ok(Some(watch_folder)) => {
if let Err(err) = connection.prepare(ctx).await {
warn!(ctx, "Could not connect: {}", err);
return;
}
// fetch
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
}
}
Ok(None) => {
info!(ctx, "Can not fetch inbox folder, not set");
}
Err(err) => {
warn!(
ctx,
"Can not fetch inbox folder, failed to get config: {:?}", err
);
}
}
}
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo {
match ctx.get_config(folder).await {
Ok(Some(watch_folder)) => {
@@ -168,7 +146,30 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
// Mark expired messages for deletion.
if folder == Config::ConfiguredInboxFolder {
if let Err(err) = connection
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap failed")
{
warn!(ctx, "{:#}", err);
}
}
// Fetch the watched folder.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
// Mark expired messages for deletion. Marked messages will be deleted from the server
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
// called right before `fetch_move_delete` because it is not well optimized and would
// otherwise slow down message fetching.
if let Err(err) = delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages failed")
@@ -176,13 +177,6 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
warn!(ctx, "{:#}", err);
}
// Fetch the watched folder.
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
// Scan additional folders only after finishing fetching the watched folder.
//
// On iOS the application has strictly limited time to work in background, so we may not
@@ -202,7 +196,10 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
// In most cases this will select the watched folder and return because there are
// no new messages. We want to select the watched folder anyway before going IDLE
// there, so this does not take additional protocol round-trip.
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);

View File

@@ -304,7 +304,7 @@ impl Context {
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1.0" />
<meta name="viewport" content="initial-scale=1.0; user-scalable=no" />
<style>
ul {
list-style-type: none;
@@ -449,13 +449,7 @@ impl Context {
// [======67%===== ]
// =============================================================================================
let domain = dc_tools::EmailAddress::new(
&self
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default(),
)?
.domain;
let domain = dc_tools::EmailAddress::new(&self.get_configured_addr().await?)?.domain;
ret += &format!(
"<h3>{}</h3><ul>",
stock_str::storage_on_domain(self, domain).await
@@ -544,7 +538,7 @@ impl Context {
self.schedule_quota_update().await?;
}
} else {
ret += &format!("<li>{}</li>", stock_str::one_moment(self).await);
ret += &format!("<li>{}</li>", stock_str::not_connected(self).await);
self.schedule_quota_update().await?;
}
ret += "</ul>";

View File

@@ -66,19 +66,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
.is_none();
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, group).await;
let auth = token::lookup_or_new(context, Namespace::Auth, group).await;
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
Ok(Some(addr)) => addr,
Ok(None) => {
bail!("Not configured, cannot generate QR code.");
}
Err(err) => {
bail!(
"Unable to retrieve configuration, cannot generate QR code: {:?}",
err
);
}
};
let self_addr = context.get_configured_addr().await?;
let self_name = context
.get_config(Config::Displayname)
.await?

View File

@@ -87,7 +87,7 @@ impl Smtp {
}
self.connectivity.set_connecting(context).await;
let lp = LoginParam::from_database(context, "configured_").await?;
let lp = LoginParam::load_configured_params(context).await?;
self.connect(
context,
&lp.smtp,

View File

@@ -181,6 +181,7 @@ impl Sql {
PRAGMA secure_delete=on;
PRAGMA busy_timeout = {};
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
PRAGMA foreign_keys=on;
",
Duration::from_secs(10).as_millis()
))?;

View File

@@ -395,7 +395,7 @@ UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
if dbversion < 71 {
info!(context, "[migration] v71");
if let Some(addr) = context.get_config(Config::ConfiguredAddr).await? {
if let Ok(addr) = context.get_configured_addr().await {
if let Ok(domain) = addr.parse::<EmailAddress>().map(|email| email.domain) {
context
.set_config(
@@ -613,6 +613,17 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
sql.execute_migration("DROP TABLE IF EXISTS backup_blobs;", 88)
.await?;
}
if dbversion < 89 {
info!(context, "[migration] v89");
sql.execute_migration(
r#"CREATE TABLE imap_markseen (
id INTEGER,
FOREIGN KEY(id) REFERENCES imap(id) ON DELETE CASCADE
);"#,
89,
)
.await?;
}
Ok((
recalc_fingerprints,

View File

@@ -287,9 +287,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Storage on %1$s"))]
StorageOnDomain = 105,
#[strum(props(fallback = "One moment…"))]
OneMoment = 106,
#[strum(props(fallback = "Connected"))]
Connected = 107,
@@ -332,6 +329,9 @@ pub enum StockMessage {
#[strum(props(fallback = "Scan to join group %1$s"))]
SecureJoinGroupQRDescription = 120,
#[strum(props(fallback = "Not connected"))]
NotConnected = 121,
}
impl StockMessage {
@@ -1009,9 +1009,9 @@ pub(crate) async fn storage_on_domain(context: &Context, domain: impl AsRef<str>
.replace1(domain)
}
/// Stock string: `One moment…`.
pub(crate) async fn one_moment(context: &Context) -> String {
translated(context, StockMessage::OneMoment).await
/// Stock string: `Not connected`.
pub(crate) async fn not_connected(context: &Context) -> String {
translated(context, StockMessage::NotConnected).await
}
/// Stock string: `Connected`.

View File

@@ -364,7 +364,7 @@ impl TestContext {
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n"
.to_owned()
+ msg.payload();
dc_receive_imf(&self.ctx, received_msg.as_bytes(), "INBOX", false)
dc_receive_imf(&self.ctx, received_msg.as_bytes(), false)
.await
.unwrap();
}
@@ -407,12 +407,7 @@ impl TestContext {
.await
.unwrap_or_default()
.unwrap_or_default();
let addr = other
.ctx
.get_config(Config::ConfiguredAddr)
.await
.unwrap()
.unwrap();
let addr = other.ctx.get_configured_addr().await.unwrap();
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
// origin when creating this contact.
let (contact_id, modified) =

View File

@@ -99,7 +99,6 @@ mod tests {
Date: Sun, 22 Mar 2021 23:37:57 +0000\n\
\n\
second message\n",
"INBOX",
false,
)
.await?;
@@ -113,7 +112,6 @@ mod tests {
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
\n\
first message\n",
"INBOX",
false,
)
.await?;
@@ -143,7 +141,6 @@ mod tests {
Date: Sun, 22 Mar 2021 01:00:00 +0000\n\
\n\
first message\n",
"INBOX",
false,
)
.await?;
@@ -163,7 +160,6 @@ mod tests {
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
\n\
third message\n",
"INBOX",
false,
)
.await?;
@@ -179,7 +175,6 @@ mod tests {
Date: Sun, 22 Mar 2021 02:00:00 +0000\n\
\n\
second message\n",
"INBOX",
false,
)
.await?;

View File

@@ -239,10 +239,7 @@ impl Context {
{
instance.param.set(Param::WebxdcSummary, summary);
instance.update_param(self).await;
self.emit_event(EventType::MsgsChanged {
chat_id: instance.chat_id,
msg_id: instance.id,
});
self.emit_msgs_changed(instance.chat_id, instance.id);
}
}
@@ -254,9 +251,14 @@ impl Context {
)
.await?;
self.emit_event(EventType::WebxdcStatusUpdate(instance.id));
let status_update_serial = StatusUpdateSerial(u32::try_from(rowid)?);
Ok(StatusUpdateSerial(u32::try_from(rowid)?))
self.emit_event(EventType::WebxdcStatusUpdate {
msg_id: instance.id,
status_update_serial,
});
Ok(status_update_serial)
}
/// Sends a status update for an webxdc instance.
@@ -769,7 +771,6 @@ mod tests {
dc_receive_imf(
&t,
include_bytes!("../test-data/message/webxdc_good_extension.eml"),
"INBOX",
false,
)
.await?;
@@ -780,7 +781,6 @@ mod tests {
dc_receive_imf(
&t,
include_bytes!("../test-data/message/webxdc_bad_extension.eml"),
"INBOX",
false,
)
.await?;
@@ -1016,7 +1016,10 @@ mod tests {
.get_matching(|evt| matches!(evt, EventType::WebxdcStatusUpdate { .. }))
.await;
match event {
EventType::WebxdcStatusUpdate(msg_id) => {
EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial: _,
} => {
assert_eq!(msg_id, instance_id);
}
_ => unreachable!(),