Compare commits

...
Author SHA1 Message Date
link2xt 57d22f8406 feat: dc_accounts_set_push_device_token and dc_get_push_state APIs 2024-03-04 19:53:34 +00:00
link2xt 5fe6d0b106 build: increase MSRV to 1.74.0
This is what updated dependencies require.

Also update Rust used to build manylinux wheels
from 1.72.0 to 1.76.0.
2024-03-04 18:54:23 +00:00
link2xt d40673af1d chore: cargo update 2024-03-04 18:30:20 +00:00
link2xt a6d3ff6cf7 chore(deps): update mio to fix RUSTSEC-2024-0019 2024-03-04 18:26:18 +00:00
link2xt 15f3af418a chore(deps): update buffer-redux to remove unmaintained safemem 2024-03-04 18:25:37 +00:00
holger krekel 7cc2a09627 test: fix pytest compat (#5317)
seems pytest_report_header has changed with pytest incompatible but we
don't need it anyway so we can just leave it out.
2024-03-04 15:58:57 +00:00
link2xt c96139b579 build: do not vendor OpenSSL when cross-compiling (#5316)
Compilation of vendored OpenSSL inside Nix is broken since
<https://github.com/alexcrichton/openssl-src-rs/pull/229> due to build
script changes.

There is anyway no need to compile vendored OpenSSL as nixpkgs already
contains OpenSSL package.

This fixes `nix build .#deltachat-rpc-server-x86_64-linux` and similar
commands which are used during releases.
2024-03-04 15:57:17 +00:00
Hocuri c52998f2ff feat(Self-Reporting): Report number of protected/encrypted/unencrypted chats (#5292) 2024-03-04 11:03:37 +01:00
dependabot[bot] 12587684ca Merge pull request #5305 from deltachat/dependabot/cargo/tempfile-3.10.1 2024-03-03 20:44:06 +00:00
dependabot[bot] 288eccf722 chore(cargo): bump image from 0.24.8 to 0.24.9
Bumps [image](https://github.com/image-rs/image) from 0.24.8 to 0.24.9.
- [Changelog](https://github.com/image-rs/image/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.24.8...v0.24.9)

---
updated-dependencies:
- dependency-name: image
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-03 14:46:53 +00:00
dependabot[bot] 0c1d578207 chore(cargo): bump tempfile from 3.10.0 to 3.10.1
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.10.0 to 3.10.1.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.10.0...v3.10.1)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-03 08:05:01 +00:00
dependabot[bot] 99ee769580 chore(cargo): bump textwrap from 0.16.0 to 0.16.1
Bumps [textwrap](https://github.com/mgeisler/textwrap) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/mgeisler/textwrap/releases)
- [Changelog](https://github.com/mgeisler/textwrap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mgeisler/textwrap/compare/0.16.0...0.16.1)

---
updated-dependencies:
- dependency-name: textwrap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-03 08:01:51 +00:00
dependabot[bot] 345759d653 chore(cargo): bump syn from 2.0.48 to 2.0.52
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.48 to 2.0.52.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.48...2.0.52)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-03 04:15:51 +00:00
link2xt db0143f01a build: remove deprecated unmaintained field from deny.toml 2024-03-03 03:49:27 +00:00
link2xt 4da0c19766 test: fixup tests/test_3_offline.py::TestOfflineAccountBasic::test_wrong_db 2024-03-03 03:28:47 +00:00
link2xt 08247a5d37 refactor: build contexts using ContextBuilder 2024-03-02 17:19:50 +00:00
link2xt ceadd8928e api: add ContextBuilder.build() to build Context without opening 2024-03-02 17:19:50 +00:00
dependabot[bot] c3d96814ca chore(cargo): bump walkdir from 2.4.0 to 2.5.0
Bumps [walkdir](https://github.com/BurntSushi/walkdir) from 2.4.0 to 2.5.0.
- [Commits](https://github.com/BurntSushi/walkdir/compare/2.4.0...2.5.0)

---
updated-dependencies:
- dependency-name: walkdir
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 17:08:47 +00:00
dependabot[bot] c2953623b9 chore(cargo): bump serde from 1.0.196 to 1.0.197
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.196 to 1.0.197.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.196...v1.0.197)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 17:07:36 +00:00
dependabot[bot] 1907d1859e chore(cargo): bump anyhow from 1.0.79 to 1.0.80
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.79 to 1.0.80.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.79...1.0.80)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 16:00:57 +00:00
dependabot[bot] a1970e998f chore(cargo): bump log from 0.4.20 to 0.4.21
Bumps [log](https://github.com/rust-lang/log) from 0.4.20 to 0.4.21.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.20...0.4.21)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 16:00:33 +00:00
dependabot[bot] 1e9baefca0 chore(cargo): bump serde_json from 1.0.113 to 1.0.114
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.113 to 1.0.114.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.113...v1.0.114)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 15:59:55 +00:00
iequidoo e16322d99d test: get_protected_chat(): Use FFIEventTracker instead of dc_wait_next_msgs() (#5207)
The way it was implemented it threw out all remaining messages after finding the next incoming
message. Better use FFIEventTracker functions, they are used in all the tests anyway.
2024-03-02 12:12:26 -03:00
dependabot[bot] ecfe3898c6 Merge pull request #5311 from deltachat/dependabot/cargo/rusqlite-0.31.0 2024-03-02 03:12:56 +00:00
link2xt 5499ca52bf refactor: get rid of ImapActionResult 2024-03-02 01:31:29 +00:00
link2xt 4e8979f7c8 refactor: merge ImapConfig into Imap 2024-03-01 21:12:21 +00:00
dependabot[bot] 417db31098 chore(cargo): bump rusqlite from 0.30.0 to 0.31.0
Bumps [rusqlite](https://github.com/rusqlite/rusqlite) from 0.30.0 to 0.31.0.
- [Release notes](https://github.com/rusqlite/rusqlite/releases)
- [Changelog](https://github.com/rusqlite/rusqlite/blob/master/Changelog.md)
- [Commits](https://github.com/rusqlite/rusqlite/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: rusqlite
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 21:11:09 +00:00
link2xt cd9f6c3d5b ci: build c.delta.chat docs with nix 2024-03-01 20:28:20 +00:00
link2xt 07870a6d69 refactor(imap): remove Session from Imap structure
Connection establishment now happens only in one place in each IMAP loop.
Now all connection establishment happens in one place
and is limited by the ratelimit.

Backoff was removed from fake_idle
as it does not establish connections anymore.
If connection fails, fake_idle will return an error.
We then drop the connection and get back to the beginning of IMAP
loop.

Backoff may be still nice to have to delay retries
in case of constant connection failures
so we don't immediately hit ratelimit if the network is unusable
and returns immediate error on each connection attempt
(e.g. ICMP network unreachable error),
but adding backoff for connection failures is out of scope for this change.
2024-03-01 18:36:03 +00:00
link2xt b08a4d6fcf ci: upload cffi docs without GH actions 2024-03-01 02:30:55 +00:00
link2xt b3a82b416f ci: upload python docs without GH actions 2024-03-01 02:21:35 +00:00
link2xt 4e5d7fb821 ci: build Python docs with Nix 2024-03-01 02:00:53 +00:00
link2xt 1d73f97ef3 nix: add deltachat-time to sources 2024-03-01 01:58:11 +00:00
36 changed files with 1063 additions and 923 deletions
+2 -2
View File
@@ -89,9 +89,9 @@ jobs:
- os: macos-latest
rust: 1.76.0
# Minimum Supported Rust Version = 1.70.0
# Minimum Supported Rust Version = 1.74.0
- os: ubuntu-latest
rust: 1.70.0
rust: 1.74.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
+40 -2
View File
@@ -1,4 +1,4 @@
name: Build & Deploy Documentation on rs.delta.chat
name: Build & Deploy Documentation on rs.delta.chat, c.delta.chat, py.delta.chat
on:
push:
@@ -6,7 +6,7 @@ on:
- main
jobs:
build:
build-rs:
runs-on: ubuntu-latest
steps:
@@ -24,3 +24,41 @@ jobs:
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/rs/"
build-python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
build-c:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build C documentation
run: nix build .#docs
- name: Upload to py.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
+5 -7
View File
@@ -21,10 +21,8 @@ jobs:
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/cffi/"
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"
-26
View File
@@ -1,26 +0,0 @@
name: Build & Deploy Documentation on py.delta.chat
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Build Python documentation
run: scripts/build-python-docs.sh
- name: Upload to py.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: delta
KEY: ${{ secrets.CODESPEAK_KEY }}
HOST: "lists.codespeak.net"
SOURCE: "dist/html/"
TARGET: "/home/delta/build/master"
Generated
+205 -198
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -3,7 +3,7 @@ name = "deltachat"
version = "1.135.1"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
rust-version = "1.74"
[profile.dev]
debug = 0
@@ -56,7 +56,7 @@ futures-lite = "2.2.0"
hex = "0.4.0"
hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.24.8", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.9", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.2", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
@@ -78,7 +78,7 @@ quoted_printable = "0.5"
rand = "0.8"
regex = "1.10"
reqwest = { version = "0.11.24", features = ["json"] }
rusqlite = { version = "0.30", features = ["sqlcipher"] }
rusqlite = { version = "0.31", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.5"
serde_json = "1"
@@ -89,7 +89,7 @@ smallvec = "1"
strum = "0.26"
strum_macros = "0.26"
tagger = "4.3.4"
textwrap = "0.16.0"
textwrap = "0.16.1"
thiserror = "1"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
+28
View File
@@ -686,6 +686,24 @@ int dc_get_connectivity (dc_context_t* context);
char* dc_get_connectivity_html (dc_context_t* context);
#define DC_PUSH_NOT_CONNECTED 0
#define DC_PUSH_HEARTBEAT 1
#define DC_PUSH_CONNECTED 2
/**
* Get the current push notification state.
* One of:
* - DC_PUSH_NOT_CONNECTED
* - DC_PUSH_HEARTBEAT
* - DC_PUSH_CONNECTED
*
* @memberof dc_context_t
* @param context The context object.
* @return Push notification state.
*/
int dc_get_push_state (dc_context_t* context);
/**
* Standalone version of dc_accounts_all_work_done().
* Only used by the python tests.
@@ -3165,6 +3183,16 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
*/
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
/**
* Sets device token for Apple Push Notification service.
* Returns immediately.
*
* @memberof dc_accounts_t
* @param token Hexadecimal device token
*/
void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const char *token);
/**
* Create the event emitter that is used to receive events.
*
+45 -15
View File
@@ -26,7 +26,7 @@ use anyhow::Context as _;
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::context::{Context, ContextBuilder};
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::preconfigure_keypair;
@@ -34,7 +34,6 @@ use deltachat::message::MsgId;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
@@ -104,12 +103,11 @@ pub unsafe extern "C" fn dc_context_new(
let ctx = if blobdir.is_null() || *blobdir == 0 {
// generate random ID as this functionality is not yet available on the C-api.
let id = rand::thread_rng().gen();
block_on(Context::new(
as_path(dbfile),
id,
Events::new(),
StockStrings::new(),
))
block_on(
ContextBuilder::new(as_path(dbfile).to_path_buf())
.with_id(id)
.open(),
)
} else {
eprintln!("blobdir can not be defined explicitly anymore");
return ptr::null_mut();
@@ -133,12 +131,11 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
}
let id = rand::thread_rng().gen();
match block_on(Context::new_closed(
as_path(dbfile),
id,
Events::new(),
StockStrings::new(),
)) {
match block_on(
ContextBuilder::new(as_path(dbfile).to_path_buf())
.with_id(id)
.build(),
) {
Ok(context) => Box::into_raw(Box::new(context)),
Err(err) => {
eprintln!("failed to create context: {err:#}");
@@ -387,7 +384,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
return 0;
}
let ctx = &*context;
block_on(async move { ctx.get_connectivity().await as u32 as libc::c_int })
block_on(ctx.get_connectivity()) as u32 as libc::c_int
}
#[no_mangle]
@@ -410,6 +407,16 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_push_state(context: *const dc_context_t) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_push_state()");
return 0;
}
let ctx = &*context;
block_on(ctx.push_state()) as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
@@ -4922,6 +4929,29 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
1
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
accounts: *mut dc_accounts_t,
token: *const libc::c_char,
) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_set_push_device_token()");
return;
}
let accounts = &*accounts;
let token = to_string_lossy(token);
block_on(async move {
let mut accounts = accounts.write().await;
if let Err(err) = accounts.set_push_device_token(&token).await {
accounts.emit_event(EventType::Error(format!(
"Failed to set notify token: {err:#}."
)));
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
accounts: *mut dc_accounts_t,
+2 -2
View File
@@ -17,7 +17,7 @@ deltachat = { path = ".." }
num-traits = "0.2"
schemars = "0.8.13"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.9.0"
tempfile = "3.10.1"
log = "0.4"
async-channel = { version = "2.0.0" }
futures = { version = "0.3.30" }
@@ -26,7 +26,7 @@ yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { version = "1.33.0" }
sanitize-filename = "0.5"
walkdir = "2.3.3"
walkdir = "2.5.0"
base64 = "0.21"
# optional dependencies
+2 -2
View File
@@ -9,9 +9,9 @@ ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.20"
log = "0.4.21"
pretty_env_logger = "0.5"
rusqlite = "0.30"
rusqlite = "0.31"
rustyline = "13"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
+5 -4
View File
@@ -9,7 +9,6 @@ extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use ansi_term::Color;
@@ -20,8 +19,7 @@ use deltachat::context::*;
use deltachat::oauth2::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use deltachat::EventType;
use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
@@ -312,7 +310,10 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
let context = ContextBuilder::new(args[1].clone().into())
.with_id(1)
.open()
.await?;
let events = context.get_event_emitter();
tokio::task::spawn(async move {
+7 -1
View File
@@ -1,5 +1,4 @@
[advisories]
unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
@@ -10,6 +9,12 @@ ignore = [
# There is no fix at the time of writing this (2023-11-28).
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
# Unmaintained ansi_term
"RUSTSEC-2021-0139",
# Unmaintained encoding
"RUSTSEC-2021-0153",
]
[bans]
@@ -69,6 +74,7 @@ skip = [
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
{ name = "winnow", version = "0.5.40" },
]
+6 -3
View File
@@ -165,9 +165,6 @@
version = manifest.version;
strictDeps = true;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
@@ -180,6 +177,11 @@
CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
OPENSSL_LIB_DIR = "${pkgsCross.pkgsStatic.openssl.out}/lib";
OPENSSL_INCLUDE_DIR = "${pkgsCross.pkgsStatic.openssl.dev}/include";
OPENSSL_STATIC = "1";
OPENSSL_NO_VENDOR = "1";
};
mk-aarch64-RustPackage = mkCrossRustPackage "aarch64-unknown-linux-musl" "aarch64-unknown-linux-musl";
@@ -250,6 +252,7 @@
./deltachat-ratelimit
./deltachat-repl
./deltachat-rpc-client
./deltachat-time
./deltachat-rpc-server
./format-flowed
./release-date.in
-16
View File
@@ -375,22 +375,6 @@ class Account:
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
def _wait_next_message_ids(self) -> List[int]:
"""Return IDs of all next messages from all chats."""
dc_array = ffi.gc(lib.dc_wait_next_msgs(self._dc_context), lib.dc_array_unref)
return [lib.dc_array_get_id(dc_array, i) for i in range(lib.dc_array_get_cnt(dc_array))]
def wait_next_incoming_message(self) -> Message:
"""Waits until the next incoming message
with ID higher than given is received and returns it."""
while True:
message_ids = self._wait_next_message_ids()
for msg_id in message_ids:
message = Message.from_db(self, msg_id)
if message and not message.is_from_self() and not message.is_from_device():
self.set_config("last_msg_id", str(msg_id))
return message
def create_chat(self, obj) -> Chat:
"""Create a 1:1 chat with Account, Contact or e-mail address."""
return self.create_contact(obj).create_chat()
+6
View File
@@ -182,6 +182,12 @@ class FFIEventTracker:
print(f"** SECUREJOINT-INVITER PROGRESS {target}", self.account)
break
def wait_securejoin_joiner_progress(self, target):
while True:
event = self.get_matching("DC_EVENT_SECUREJOIN_JOINER_PROGRESS")
if event.data2 >= target:
break
def wait_idle_inbox_ready(self):
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
so that new messages are not mistaken for old ones:
+7 -15
View File
@@ -10,7 +10,6 @@ import time
import weakref
import random
from queue import Queue
from threading import Event
from typing import Callable, Dict, List, Optional, Set
import pytest
@@ -116,7 +115,7 @@ def pytest_configure(config):
deltachat.register_global_plugin(la)
def pytest_report_header(config, startdir):
def pytest_report_header(config):
info = get_core_info()
summary = [
"Deltachat core={} sqlite={} journal_mode={}".format(
@@ -592,23 +591,16 @@ class ACFactory:
return ac1.create_chat(ac2)
def get_protected_chat(self, ac1: Account, ac2: Account):
class SetupPlugin:
def __init__(self) -> None:
self.member_added = Event()
@account_hookimpl
def ac_member_added(self, chat: deltachat.Chat, contact, actor, message):
self.member_added.set()
setupplugin = SetupPlugin()
ac1.add_account_plugin(setupplugin)
chat = ac1.create_group_chat("Protected Group", verified=True)
qr = chat.get_join_qr()
ac2.qr_join_chat(qr)
setupplugin.member_added.wait()
msg = ac2.wait_next_incoming_message()
ac2._evtracker.wait_securejoin_joiner_progress(1000)
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg is not None
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
msg = ac2.wait_next_incoming_message()
msg = ac2._evtracker.wait_next_incoming_message()
assert msg is not None
assert "Member Me " in msg.text and " added by " in msg.text
return chat
+4 -4
View File
@@ -44,21 +44,21 @@ def test_configure_generate_key(acfactory, lp):
lp.sec("ac1: send unencrypted message to ac2")
chat.send_text("message1")
lp.sec("ac2: waiting for message from ac1")
msg_in = ac2.wait_next_incoming_message()
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message1"
assert not msg_in.is_encrypted()
lp.sec("ac2: send encrypted message to ac1")
msg_in.chat.send_text("message2")
lp.sec("ac1: waiting for message from ac2")
msg2_in = ac1.wait_next_incoming_message()
msg2_in = ac1._evtracker.wait_next_incoming_message()
assert msg2_in.text == "message2"
assert msg2_in.is_encrypted()
lp.sec("ac1: send encrypted message to ac2")
msg2_in.chat.send_text("message3")
lp.sec("ac2: waiting for message from ac1")
msg3_in = ac2.wait_next_incoming_message()
msg3_in = ac2._evtracker.wait_next_incoming_message()
assert msg3_in.text == "message3"
assert msg3_in.is_encrypted()
@@ -520,7 +520,7 @@ def test_forward_encrypted_to_unencrypted(acfactory, lp):
lp.sec("ac1: send encrypted message to ac2")
txt = "This should be encrypted"
chat.send_text(txt)
msg = ac2.wait_next_incoming_message()
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == txt
assert msg.is_encrypted()
+2 -2
View File
@@ -52,8 +52,8 @@ class TestOfflineAccountBasic:
def test_wrong_db(self, tmp_path):
p = tmp_path / "hello.db"
p.write_text("123")
account = Account(str(p))
assert not account.is_open()
with pytest.raises(ValueError):
_account = Account(str(p))
def test_os_name(self, tmp_path):
p = tmp_path / "hello.db"
-2
View File
@@ -37,8 +37,6 @@ and an own build machine.
- `android-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Android NDK.
- `build-python-docs.sh` builds Python documentation into `dist/html/`.
## Triggering runs on the build machine locally (fast!)
There is experimental support for triggering a remote Python or Rust test run
-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export DCC_RS_TARGET=debug
export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi --features jsonrpc
python3 -m venv venv
venv/bin/pip install ./python
venv/bin/pip install ./deltachat-rpc-client
venv/bin/pip install sphinx breathe sphinx_rtd_theme
venv/bin/sphinx-build -b html -a python/doc/ dist/html
-49
View File
@@ -15,55 +15,6 @@ resources:
tag_filter: "v*"
jobs:
- name: doxygen
plan:
- get: deltachat-core-rust
trigger: true
# Build Doxygen documentation
- task: build-doxygen
config:
inputs:
- name: deltachat-core-rust
outputs:
- name: c-docs
image_resource:
source:
repository: alpine
type: registry-image
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache doxygen git
cd deltachat-core-rust
scripts/run-doxygen.sh
cd ..
cp -av deltachat-core-rust/deltachat-ffi/html deltachat-core-rust/deltachat-ffi/xml c-docs/
- task: upload-c-docs
config:
inputs:
- name: c-docs
image_resource:
type: registry-image
source:
repository: alpine
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache rsync openssh-client
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete c-docs/html/ delta@c.delta.chat:build-c/master
- name: python-x86_64
plan:
- get: deltachat-core-rust
+1 -1
View File
@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.72.0
RUST_VERSION=1.76.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
+46 -29
View File
@@ -17,8 +17,9 @@ use tokio::sync::oneshot;
#[cfg(not(target_os = "ios"))]
use tokio::time::{sleep, Duration};
use crate::context::Context;
use crate::context::{Context, ContextBuilder};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::push::PushSubscriber;
use crate::stock_str::StockStrings;
/// Account manager, that can handle multiple accounts in a single place.
@@ -37,6 +38,9 @@ pub struct Accounts {
/// This way changing a translation for one context automatically
/// changes it for all other contexts.
pub(crate) stockstrings: StockStrings,
/// Push notification subscriber shared between accounts.
push_subscriber: PushSubscriber,
}
impl Accounts {
@@ -73,8 +77,9 @@ impl Accounts {
.context("failed to load accounts config")?;
let events = Events::new();
let stockstrings = StockStrings::new();
let push_subscriber = PushSubscriber::new();
let accounts = config
.load_accounts(&events, &stockstrings, &dir)
.load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
.await
.context("failed to load accounts")?;
@@ -84,6 +89,7 @@ impl Accounts {
accounts,
events,
stockstrings,
push_subscriber,
})
}
@@ -120,13 +126,17 @@ impl Accounts {
let account_config = self.config.new_account().await?;
let dbfile = account_config.dbfile(&self.dir);
let ctx = Context::new(
&dbfile,
account_config.id,
self.events.clone(),
self.stockstrings.clone(),
)
.await?;
let ctx = ContextBuilder::new(dbfile)
.with_id(account_config.id)
.with_events(self.events.clone())
.with_stock_strings(self.stockstrings.clone())
.with_push_subscriber(self.push_subscriber.clone())
.build()
.await?;
// Try to open without a passphrase,
// but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -135,14 +145,15 @@ impl Accounts {
/// Adds a new closed account.
pub async fn add_closed_account(&mut self) -> Result<u32> {
let account_config = self.config.new_account().await?;
let dbfile = account_config.dbfile(&self.dir);
let ctx = Context::new_closed(
&account_config.dbfile(&self.dir),
account_config.id,
self.events.clone(),
self.stockstrings.clone(),
)
.await?;
let ctx = ContextBuilder::new(dbfile)
.with_id(account_config.id)
.with_events(self.events.clone())
.with_stock_strings(self.stockstrings.clone())
.with_push_subscriber(self.push_subscriber.clone())
.build()
.await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -337,6 +348,12 @@ impl Accounts {
pub fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
}
/// Sets notification token for Apple Push Notification service.
pub async fn set_push_device_token(&mut self, token: &str) -> Result<()> {
self.push_subscriber.set_device_token(token).await;
Ok(())
}
}
/// Configuration file name.
@@ -522,24 +539,24 @@ impl Config {
&self,
events: &Events,
stockstrings: &StockStrings,
push_subscriber: PushSubscriber,
dir: &Path,
) -> Result<BTreeMap<u32, Context>> {
let mut accounts = BTreeMap::new();
for account_config in &self.inner.accounts {
let ctx = Context::new(
&account_config.dbfile(dir),
account_config.id,
events.clone(),
stockstrings.clone(),
)
.await
.with_context(|| {
format!(
"failed to create context from file {:?}",
account_config.dbfile(dir)
)
})?;
let dbfile = account_config.dbfile(dir);
let ctx = ContextBuilder::new(dbfile.clone())
.with_id(account_config.id)
.with_events(events.clone())
.with_stock_strings(stockstrings.clone())
.with_push_subscriber(push_subscriber.clone())
.build()
.await
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
// Try to open without a passphrase,
// but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?;
accounts.insert(account_config.id, ctx);
}
+9 -10
View File
@@ -25,7 +25,7 @@ use tokio::task;
use crate::config::{self, Config};
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::imap::Imap;
use crate::imap::{session::Session as ImapSession, Imap};
use crate::log::LogExt;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{Message, Viewtype};
@@ -395,7 +395,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// Configure IMAP
let mut imap: Option<Imap> = None;
let mut imap: Option<(Imap, ImapSession)> = None;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Imap)
@@ -433,7 +433,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
let mut imap = match imap {
let (mut imap, mut imap_session) = match imap {
Some(imap) => imap,
None => bail!(nicer_configuration_error(ctx, errors).await),
};
@@ -454,11 +454,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let create_mvbox = ctx.should_watch_mvbox().await?;
imap.configure_folders(ctx, create_mvbox).await?;
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
imap.session
.as_mut()
.context("no IMAP connection established")?
imap_session
.select_with_uidvalidity(ctx, "INBOX")
.await
.context("could not read INBOX status")?;
@@ -579,7 +578,7 @@ async fn try_imap_one_param(
socks5_config: &Option<Socks5Config>,
addr: &str,
provider_strict_tls: bool,
) -> Result<Imap, ConfigurationError> {
) -> Result<(Imap, ImapSession), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
param.user,
@@ -617,9 +616,9 @@ async fn try_imap_one_param(
msg: format!("{err:#}"),
})
}
Ok(()) => {
Ok(session) => {
info!(context, "success: {}", inf);
Ok(imap)
Ok((imap, session))
}
}
}
+162 -21
View File
@@ -20,14 +20,17 @@ use crate::config::Config;
use crate::constants::{
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
};
use crate::contact::Contact;
use crate::contact::{Contact, ContactId};
use crate::debug_logging::DebugLogging;
use crate::download::DownloadState;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
use crate::login_param::LoginParam;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::push::PushSubscriber;
use crate::quota::QuotaInfo;
use crate::scheduler::{convert_folder_meaning, SchedulerState};
use crate::sql::Sql;
@@ -84,6 +87,8 @@ pub struct ContextBuilder {
events: Events,
stock_strings: StockStrings,
password: Option<String>,
push_subscriber: Option<PushSubscriber>,
}
impl ContextBuilder {
@@ -99,6 +104,7 @@ impl ContextBuilder {
events: Events::new(),
stock_strings: StockStrings::new(),
password: None,
push_subscriber: None,
}
}
@@ -153,11 +159,32 @@ impl ContextBuilder {
self
}
/// Opens the [`Context`].
/// Sets push subscriber.
pub(crate) fn with_push_subscriber(mut self, push_subscriber: PushSubscriber) -> Self {
self.push_subscriber = Some(push_subscriber);
self
}
/// Builds the [`Context`] without opening it.
pub async fn build(self) -> Result<Context> {
let push_subscriber = self.push_subscriber.unwrap_or_default();
let context = Context::new_closed(
&self.dbfile,
self.id,
self.events,
self.stock_strings,
push_subscriber,
)
.await?;
Ok(context)
}
/// Builds the [`Context`] and opens it.
///
/// Returns error if context cannot be opened with the given passphrase.
pub async fn open(self) -> Result<Context> {
let context =
Context::new_closed(&self.dbfile, self.id, self.events, self.stock_strings).await?;
let password = self.password.unwrap_or_default();
let password = self.password.clone().unwrap_or_default();
let context = self.build().await?;
match context.open(password).await? {
true => Ok(context),
false => bail!("database could not be decrypted, incorrect or missing password"),
@@ -253,6 +280,13 @@ pub struct InnerContext {
/// Standard RwLock instead of [`tokio::sync::RwLock`] is used
/// because the lock is used from synchronous [`Context::emit_event`].
pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,
/// Push subscriber to store device token
/// and register for heartbeat notifications.
pub(crate) push_subscriber: PushSubscriber,
/// True if account has subscribed to push notifications via IMAP.
pub(crate) push_subscribed: AtomicBool,
}
/// The state of ongoing process.
@@ -298,7 +332,8 @@ impl Context {
events: Events,
stock_strings: StockStrings,
) -> Result<Context> {
let context = Self::new_closed(dbfile, id, events, stock_strings).await?;
let context =
Self::new_closed(dbfile, id, events, stock_strings, Default::default()).await?;
// Open the database if is not encrypted.
if context.check_passphrase("".to_string()).await? {
@@ -313,6 +348,7 @@ impl Context {
id: u32,
events: Events,
stockstrings: StockStrings,
push_subscriber: PushSubscriber,
) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
@@ -321,7 +357,14 @@ impl Context {
if !blobdir.exists() {
tokio::fs::create_dir_all(&blobdir).await?;
}
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events, stockstrings)?;
let context = Context::with_blobdir(
dbfile.into(),
blobdir,
id,
events,
stockstrings,
push_subscriber,
)?;
Ok(context)
}
@@ -364,6 +407,7 @@ impl Context {
id: u32,
events: Events,
stockstrings: StockStrings,
push_subscriber: PushSubscriber,
) -> Result<Context> {
ensure!(
blobdir.is_dir(),
@@ -398,6 +442,8 @@ impl Context {
last_full_folder_scan: Mutex::new(None),
last_error: std::sync::RwLock::new("".to_string()),
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
};
let ctx = Context {
@@ -461,13 +507,13 @@ impl Context {
// connection
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
connection.prepare(self).await?;
let mut session = connection.prepare(self).await?;
// fetch imap folders
for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] {
let (_, watch_folder) = convert_folder_meaning(self, folder_meaning).await?;
connection
.fetch_move_delete(self, &watch_folder, folder_meaning)
.fetch_move_delete(self, &mut session, &watch_folder, folder_meaning)
.await?;
}
@@ -484,10 +530,8 @@ impl Context {
};
if quota_needs_update {
if let Some(session) = connection.session.as_mut() {
if let Err(err) = self.update_recent_quota(session).await {
warn!(self, "Failed to update quota: {err:#}.");
}
if let Err(err) = self.update_recent_quota(&mut session).await {
warn!(self, "Failed to update quota: {err:#}.");
}
}
@@ -879,6 +923,16 @@ impl Context {
}
async fn get_self_report(&self) -> Result<String> {
#[derive(Default)]
struct ChatNumbers {
protected: u32,
protection_broken: u32,
opportunistic_dc: u32,
opportunistic_mua: u32,
unencrypted_dc: u32,
unencrypted_mua: u32,
}
let mut res = String::new();
res += &format!("core_version {}\n", get_version_str());
@@ -906,6 +960,76 @@ impl Context {
let key_created = secret_key.created_at().timestamp();
res += &format!("key_created {}\n", key_created);
// how many of the chats active in the last months are:
// - protected
// - protection-broken
// - opportunistic-encrypted and the contact uses Delta Chat
// - opportunistic-encrypted and the contact uses a classical MUA
// - unencrypted and the contact uses Delta Chat
// - unencrypted and the contact uses a classical MUA
let three_months_ago = time().saturating_sub(3600 * 24 * 30 * 3);
let chats = self
.sql
.query_map(
"SELECT c.protected, m.param, m.msgrmsg
FROM chats c
JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND hidden=0
AND download_state=?
AND to_id!=?
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND (c.blocked=0 OR c.blocked=2)
AND IFNULL(m.timestamp,c.created_timestamp) > ?
GROUP BY c.id",
(DownloadState::Done, ContactId::INFO, three_months_ago),
|row| {
let protected: ProtectionStatus = row.get(0)?;
let message_param: Params =
row.get::<_, String>(1)?.parse().unwrap_or_default();
let is_dc_message: bool = row.get(2)?;
Ok((protected, message_param, is_dc_message))
},
|rows| {
let mut chats = ChatNumbers::default();
for row in rows {
let (protected, message_param, is_dc_message) = row?;
let encrypted = message_param
.get_bool(Param::GuaranteeE2ee)
.unwrap_or(false);
if protected == ProtectionStatus::Protected {
chats.protected += 1;
} else if protected == ProtectionStatus::ProtectionBroken {
chats.protection_broken += 1;
} else if encrypted {
if is_dc_message {
chats.opportunistic_dc += 1;
} else {
chats.opportunistic_mua += 1;
}
} else if is_dc_message {
chats.unencrypted_dc += 1;
} else {
chats.unencrypted_mua += 1;
}
}
Ok(chats)
},
)
.await?;
res += &format!("chats_protected {}\n", chats.protected);
res += &format!("chats_protection_broken {}\n", chats.protection_broken);
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
res += &format!("chats_unencrypted_mua {}\n", chats.unencrypted_mua);
let self_reporting_id = match self.get_config(Config::SelfReportingId).await? {
Some(id) => id,
None => {
@@ -1421,7 +1545,14 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
let res = Context::with_blobdir(
dbfile,
blobdir,
1,
Events::new(),
StockStrings::new(),
Default::default(),
);
assert!(res.is_err());
}
@@ -1430,7 +1561,14 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
let res = Context::with_blobdir(
dbfile,
blobdir,
1,
Events::new(),
StockStrings::new(),
Default::default(),
);
assert!(res.is_err());
}
@@ -1653,16 +1791,18 @@ mod tests {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
let context = ContextBuilder::new(dbfile.clone())
.with_id(1)
.build()
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
assert_eq!(context.is_open().await, true);
drop(context);
let id = 2;
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
let context = ContextBuilder::new(dbfile)
.with_id(2)
.build()
.await
.context("failed to create context")?;
assert_eq!(context.is_open().await, false);
@@ -1678,8 +1818,9 @@ mod tests {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
let context = ContextBuilder::new(dbfile)
.with_id(1)
.build()
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
+20 -32
View File
@@ -3,13 +3,13 @@
use std::cmp::max;
use std::collections::BTreeMap;
use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::context::Context;
use crate::imap::{Imap, ImapActionResult};
use crate::imap::session::Session;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::tools::time;
@@ -129,9 +129,11 @@ impl Message {
/// Actually download a message partially downloaded before.
///
/// Most messages are downloaded automatically on fetch instead.
pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Imap) -> Result<()> {
imap.prepare(context).await?;
pub(crate) async fn download_msg(
context: &Context,
msg_id: MsgId,
session: &mut Session,
) -> Result<()> {
let msg = Message::load_from_db(context, msg_id).await?;
let row = context
.sql
@@ -152,7 +154,7 @@ pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Im
return Err(anyhow!("Call download_full() again to try over."));
};
match imap
session
.fetch_single_msg(
context,
&server_folder,
@@ -160,16 +162,11 @@ pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Im
server_uid,
msg.rfc724_mid.clone(),
)
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
Err(anyhow!("Call download_full() again to try over."))
}
ImapActionResult::Success => Ok(()),
}
.await?;
Ok(())
}
impl Imap {
impl Session {
/// Download a single message and pipe it to receive_imf().
///
/// receive_imf() is not directly aware that this is a result of a call to download_msg(),
@@ -181,23 +178,19 @@ impl Imap {
uidvalidity: u32,
uid: u32,
rfc724_mid: String,
) -> ImapActionResult {
if let Some(imapresult) = self
.prepare_imap_operation_on_msg(context, folder, uid)
.await
{
return imapresult;
) -> Result<()> {
if uid == 0 {
bail!("Attempt to fetch UID 0");
}
self.select_folder(context, Some(folder)).await?;
// we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid);
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid);
let Some(session) = self.session.as_mut() else {
return ImapActionResult::Failed;
};
let (last_uid, _received) = match session
let (last_uid, _received) = self
.fetch_many_msgs(
context,
folder,
@@ -207,16 +200,11 @@ impl Imap {
false,
false,
)
.await
{
Ok(res) => res,
Err(_) => return ImapActionResult::Failed,
};
.await?;
if last_uid.is_none() {
ImapActionResult::Failed
} else {
ImapActionResult::Success
bail!("Failed to fetch UID {uid}");
}
Ok(())
}
}
+136 -197
View File
@@ -8,6 +8,7 @@ use std::{
collections::{BTreeMap, BTreeSet, HashMap},
iter::Peekable,
mem::take,
sync::atomic::Ordering,
time::Duration,
};
@@ -54,13 +55,6 @@ use session::Session;
pub(crate) const GENERATED_PREFIX: &str = "GEN_";
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
pub enum ImapActionResult {
Failed,
RetryLater,
Success,
}
const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
@@ -69,10 +63,19 @@ const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
#[derive(Debug)]
pub struct Imap {
pub(crate) struct Imap {
pub(crate) idle_interrupt_receiver: Receiver<()>,
config: ImapConfig,
pub(crate) session: Option<Session>,
/// Email address.
addr: String,
/// Login parameters.
lp: ServerLoginParam,
/// SOCKS 5 configuration.
socks5_config: Option<Socks5Config>,
strict_tls: bool,
login_failed_once: bool,
pub(crate) connectivity: ConnectivityStore,
@@ -152,17 +155,6 @@ impl FolderMeaning {
}
}
#[derive(Debug)]
struct ImapConfig {
/// Email address.
pub addr: String,
pub lp: ServerLoginParam,
/// SOCKS 5 configuration.
pub socks5_config: Option<Socks5Config>,
pub strict_tls: bool,
}
struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
inner: Peekable<T>,
}
@@ -245,17 +237,13 @@ impl Imap {
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
let config = ImapConfig {
let imap = Imap {
idle_interrupt_receiver,
addr: addr.to_string(),
lp: lp.clone(),
socks5_config,
strict_tls,
};
let imap = Imap {
idle_interrupt_receiver,
config,
session: None,
login_failed_once: false,
connectivity: Default::default(),
// 1 connection per minute + a burst of 2.
@@ -298,15 +286,11 @@ impl Imap {
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
/// instead if you are going to actually use connection rather than trying connection
/// parameters.
pub async fn connect(&mut self, context: &Context) -> Result<()> {
if self.config.lp.server.is_empty() {
pub(crate) async fn connect(&mut self, context: &Context) -> Result<Session> {
if self.lp.server.is_empty() {
bail!("IMAP operation attempted while it is torn down");
}
if self.session.is_some() {
return Ok(());
}
let ratelimit_duration = self.ratelimit.read().await.until_can_send();
if !ratelimit_duration.is_zero() {
warn!(
@@ -331,65 +315,61 @@ impl Imap {
info!(context, "Connecting to IMAP server");
self.connectivity.set_connecting(context).await;
self.ratelimit.write().await.send();
let connection_res: Result<Client> = if self.config.lp.security == Socket::Starttls
|| self.config.lp.security == Socket::Plain
{
let config = &mut self.config;
let imap_server: &str = config.lp.server.as_ref();
let imap_port = config.lp.port;
let connection_res: Result<Client> =
if self.lp.security == Socket::Starttls || self.lp.security == Socket::Plain {
let imap_server: &str = self.lp.server.as_ref();
let imap_port = self.lp.port;
if let Some(socks5_config) = &config.socks5_config {
if config.lp.security == Socket::Starttls {
Client::connect_starttls_socks5(
if let Some(socks5_config) = &self.socks5_config {
if self.lp.security == Socket::Starttls {
Client::connect_starttls_socks5(
context,
imap_server,
imap_port,
socks5_config.clone(),
self.strict_tls,
)
.await
} else {
Client::connect_insecure_socks5(
context,
imap_server,
imap_port,
socks5_config.clone(),
)
.await
}
} else if self.lp.security == Socket::Starttls {
Client::connect_starttls(context, imap_server, imap_port, self.strict_tls).await
} else {
Client::connect_insecure(context, imap_server, imap_port).await
}
} else {
let imap_server: &str = self.lp.server.as_ref();
let imap_port = self.lp.port;
if let Some(socks5_config) = &self.socks5_config {
Client::connect_secure_socks5(
context,
imap_server,
imap_port,
self.strict_tls,
socks5_config.clone(),
config.strict_tls,
)
.await
} else {
Client::connect_insecure_socks5(
context,
imap_server,
imap_port,
socks5_config.clone(),
)
.await
Client::connect_secure(context, imap_server, imap_port, self.strict_tls).await
}
} else if config.lp.security == Socket::Starttls {
Client::connect_starttls(context, imap_server, imap_port, config.strict_tls).await
} else {
Client::connect_insecure(context, imap_server, imap_port).await
}
} else {
let config = &self.config;
let imap_server: &str = config.lp.server.as_ref();
let imap_port = config.lp.port;
if let Some(socks5_config) = &config.socks5_config {
Client::connect_secure_socks5(
context,
imap_server,
imap_port,
config.strict_tls,
socks5_config.clone(),
)
.await
} else {
Client::connect_secure(context, imap_server, imap_port, config.strict_tls).await
}
};
};
let client = connection_res?;
let config = &self.config;
let imap_user: &str = config.lp.user.as_ref();
let imap_pw: &str = config.lp.password.as_ref();
let oauth2 = self.config.lp.oauth2;
let imap_user: &str = self.lp.user.as_ref();
let imap_pw: &str = self.lp.password.as_ref();
let oauth2 = self.lp.oauth2;
let login_res = if oauth2 {
info!(context, "Logging into IMAP server with OAuth 2");
let addr: &str = config.addr.as_ref();
let addr: &str = self.addr.as_ref();
let token = get_oauth2_access_token(context, addr, imap_pw, true)
.await?
@@ -410,19 +390,18 @@ impl Imap {
let mut lock = context.server_id.write().await;
*lock = session.capabilities.server_id.clone();
self.session = Some(session);
self.login_failed_once = false;
context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}",
self.config.lp.user
self.lp.user
)));
self.connectivity.set_connected(context).await;
info!(context, "Successfully logged into IMAP server");
Ok(())
Ok(session)
}
Err(err) => {
let imap_user = self.config.lp.user.to_owned();
let imap_user = self.lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user).await;
warn!(context, "{} ({:#})", message, err);
@@ -461,22 +440,26 @@ impl Imap {
///
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
/// determined.
pub async fn prepare(&mut self, context: &Context) -> Result<()> {
if let Err(err) = self.connect(context).await {
self.connectivity.set_err(context, &err).await;
return Err(err);
pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
let mut session = match self.connect(context).await {
Ok(session) => session,
Err(err) => {
self.connectivity.set_err(context, &err).await;
return Err(err);
}
};
let folders_configured = context
.sql
.get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
.await?;
if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
let create_mvbox = true;
self.configure_folders(context, &mut session, create_mvbox)
.await?;
}
self.ensure_configured_folders(context, true).await?;
Ok(())
}
/// Drops the session without disconnecting properly.
/// Useful in case of an IMAP error, when it's unclear if it's in a correct state and it's
/// easier to setup a new connection.
pub fn trigger_reconnect(&mut self, context: &Context) {
info!(context, "Dropping an IMAP connection.");
self.session = None;
Ok(session)
}
/// FETCH-MOVE-DELETE iteration.
@@ -486,6 +469,7 @@ impl Imap {
pub async fn fetch_move_delete(
&mut self,
context: &Context,
session: &mut Session,
watch_folder: &str,
folder_meaning: FolderMeaning,
) -> Result<()> {
@@ -493,10 +477,9 @@ impl Imap {
// probably shutdown
bail!("IMAP operation attempted while it is torn down");
}
self.prepare(context).await?;
let msgs_fetched = self
.fetch_new_messages(context, watch_folder, folder_meaning, false)
.fetch_new_messages(context, session, watch_folder, folder_meaning, false)
.await
.context("fetch_new_messages")?;
if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
@@ -507,10 +490,6 @@ impl Imap {
context.scheduler.interrupt_ephemeral_task().await;
}
let session = self
.session
.as_mut()
.context("no IMAP connection established")?;
session
.move_delete_messages(context, watch_folder)
.await
@@ -525,6 +504,7 @@ impl Imap {
pub(crate) async fn fetch_new_messages(
&mut self,
context: &Context,
session: &mut Session,
folder: &str,
folder_meaning: FolderMeaning,
fetch_existing_msgs: bool,
@@ -534,7 +514,6 @@ impl Imap {
return Ok(false);
}
let session = self.session.as_mut().context("No IMAP session")?;
let new_emails = session
.select_with_uidvalidity(context, folder)
.await
@@ -694,10 +673,7 @@ impl Imap {
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
for (uid, fp) in uids_fetch {
if fp != fetch_partially {
let (largest_uid_fetched_in_batch, received_msgs_in_batch) = self
.session
.as_mut()
.context("No IMAP session")?
let (largest_uid_fetched_in_batch, received_msgs_in_batch) = session
.fetch_many_msgs(
context,
folder,
@@ -724,10 +700,7 @@ impl Imap {
// Largest known UID is normally less than UIDNEXT,
// but a message may have arrived between determining UIDNEXT
// and executing the FETCH command.
let mailbox_uid_next = self
.session
.as_ref()
.context("No IMAP session")?
let mailbox_uid_next = session
.selected_mailbox
.as_ref()
.with_context(|| format!("Expected {folder:?} to be selected"))?
@@ -762,13 +735,15 @@ impl Imap {
///
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
/// and show them in the chat list.
pub(crate) async fn fetch_existing_msgs(&mut self, context: &Context) -> Result<()> {
pub(crate) async fn fetch_existing_msgs(
&mut self,
context: &Context,
session: &mut Session,
) -> Result<()> {
if context.get_config_bool(Config::Bot).await? {
return Ok(()); // Bots don't want those messages
}
self.prepare(context).await.context("could not connect")?;
let session = self.session.as_mut().context("No IMAP session")?;
add_all_recipients_as_contacts(context, session, Config::ConfiguredSentboxFolder)
.await
.context("failed to get recipients from the sentbox")?;
@@ -794,7 +769,7 @@ impl Imap {
context,
"Fetching existing messages from folder {folder:?}."
);
self.fetch_new_messages(context, &folder, meaning, true)
self.fetch_new_messages(context, session, &folder, meaning, true)
.await
.context("could not fetch existing messages")?;
}
@@ -1471,6 +1446,37 @@ impl Session {
*lock = Some(ServerMetadata { comment, admin });
Ok(())
}
/// Stores device token into /private/devicetoken IMAP METADATA of the Inbox.
pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
if context.push_subscribed.load(Ordering::Relaxed) {
return Ok(());
}
let Some(device_token) = context.push_subscriber.device_token().await else {
return Ok(());
};
if self.can_metadata() && self.can_push() {
let folder = context
.get_config(Config::ConfiguredInboxFolder)
.await?
.context("INBOX is not configured")?;
self.run_command_and_check_ok(format!(
"SETMETADATA \"{folder}\" (/private/devicetoken \"{device_token}\")"
))
.await
.context("SETMETADATA command failed")?;
context.push_subscribed.store(true, Ordering::Relaxed);
} else if !context.push_subscriber.heartbeat_subscribed().await {
let context = context.clone();
// Subscribe for heartbeat notifications.
tokio::spawn(async move { context.push_subscriber.subscribe().await });
}
Ok(())
}
}
impl Session {
@@ -1493,70 +1499,6 @@ impl Session {
}
Ok(())
}
}
impl Imap {
pub(crate) async fn prepare_imap_operation_on_msg(
&mut self,
context: &Context,
folder: &str,
uid: u32,
) -> Option<ImapActionResult> {
if uid == 0 {
return Some(ImapActionResult::RetryLater);
}
if let Err(err) = self.prepare(context).await {
warn!(context, "prepare_imap_op failed: {}", err);
return Some(ImapActionResult::RetryLater);
}
let session = match self
.session
.as_mut()
.context("no IMAP connection established")
{
Err(err) => {
error!(context, "Failed to prepare IMAP operation: {:#}", err);
return Some(ImapActionResult::Failed);
}
Ok(session) => session,
};
match session.select_folder(context, Some(folder)).await {
Ok(_) => None,
Err(select_folder::Error::ConnectionLost) => {
warn!(context, "Lost imap connection");
Some(ImapActionResult::RetryLater)
}
Err(select_folder::Error::BadFolderName(folder_name)) => {
warn!(context, "invalid folder name: {:?}", folder_name);
Some(ImapActionResult::Failed)
}
Err(err) => {
warn!(context, "failed to select folder {:?}: {:#}", folder, err);
Some(ImapActionResult::RetryLater)
}
}
}
pub async fn ensure_configured_folders(
&mut self,
context: &Context,
create_mvbox: bool,
) -> Result<()> {
let folders_configured = context
.sql
.get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
.await?;
if folders_configured.unwrap_or_default() >= constants::DC_FOLDERS_CONFIGURED_VERSION {
return Ok(());
}
if let Err(err) = self.connect(context).await {
self.connectivity.set_err(context, &err).await;
return Err(err);
}
self.configure_folders(context, create_mvbox).await
}
/// Attempts to configure mvbox.
///
@@ -1572,27 +1514,22 @@ impl Imap {
folders: &[&'a str],
create_mvbox: bool,
) -> Result<Option<&'a str>> {
let session = self
.session
.as_mut()
.context("no IMAP connection established")?;
// Close currently selected folder if needed.
// We are going to select folders using low-level EXAMINE operations below.
session.select_folder(context, None).await?;
self.select_folder(context, None).await?;
for folder in folders {
info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
let res = session.examine(&folder).await;
let res = self.examine(&folder).await;
if res.is_ok() {
info!(
context,
"MVBOX-folder {:?} successfully selected, using it.", &folder
);
session.close().await?;
self.close().await?;
// Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise
// emails moved before that wouldn't be fetched but considered "old" instead.
session.select_with_uidvalidity(context, folder).await?;
self.select_with_uidvalidity(context, folder).await?;
return Ok(Some(folder));
}
}
@@ -1603,7 +1540,7 @@ impl Imap {
let Some(folder) = folders.first() else {
return Ok(None);
};
match session.select_with_uidvalidity(context, folder).await {
match self.select_with_uidvalidity(context, folder).await {
Ok(_) => {
info!(context, "MVBOX-folder {} created.", folder);
return Ok(Some(folder));
@@ -1614,13 +1551,15 @@ impl Imap {
}
Ok(None)
}
}
pub async fn configure_folders(&mut self, context: &Context, create_mvbox: bool) -> Result<()> {
let session = self
.session
.as_mut()
.context("no IMAP connection established")?;
impl Imap {
pub(crate) async fn configure_folders(
&mut self,
context: &Context,
session: &mut Session,
create_mvbox: bool,
) -> Result<()> {
let mut folders = session
.list(Some(""), Some("*"))
.await
@@ -1657,7 +1596,7 @@ impl Imap {
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
let fallback_folder = format!("INBOX{delimiter}DeltaChat");
let mvbox_folder = self
let mvbox_folder = session
.configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
.await
.context("failed to configure mvbox")?;
+7
View File
@@ -25,6 +25,13 @@ pub(crate) struct Capabilities {
/// <https://tools.ietf.org/html/rfc5464>
pub can_metadata: bool,
/// True if the server supports XDELTAPUSH capability.
/// This capability means setting /private/devicetoken IMAP METADATA
/// on the INBOX results in new mail notifications
/// via notifications.delta.chat service.
/// This is supported by <https://github.com/deltachat/chatmail>
pub can_push: bool,
/// Server ID if the server supports ID capability.
pub server_id: Option<HashMap<String, String>>,
}
+1
View File
@@ -60,6 +60,7 @@ async fn determine_capabilities(
can_check_quota: caps.has_str("QUOTA"),
can_condstore: caps.has_str("CONDSTORE"),
can_metadata: caps.has_str("METADATA"),
can_push: caps.has_str("XDELTAPUSH"),
server_id,
};
Ok(capabilities)
+19 -71
View File
@@ -4,13 +4,12 @@ use anyhow::{bail, Context as _, Result};
use async_channel::Receiver;
use async_imap::extensions::idle::IdleResponse;
use futures_lite::FutureExt;
use tokio::time::timeout;
use super::session::Session;
use super::Imap;
use crate::config::Config;
use crate::context::Context;
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
use crate::log::LogExt;
use crate::tools::{self, time_elapsed};
/// Timeout after which IDLE is finished
@@ -98,90 +97,38 @@ impl Session {
}
impl Imap {
/// Idle using polling.
pub(crate) async fn fake_idle(
&mut self,
context: &Context,
session: &mut Session,
watch_folder: String,
folder_meaning: FolderMeaning,
) {
// Idle using polling. This is also needed if we're not yet configured -
// in this case, we're waiting for a configure job (and an interrupt).
) -> Result<()> {
let fake_idle_start_time = tools::Time::now();
// Do not poll, just wait for an interrupt when no folder is passed in.
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
const TIMEOUT_INIT_MS: u64 = 60_000;
let mut timeout_ms: u64 = TIMEOUT_INIT_MS;
enum Event {
Tick,
Interrupt,
}
// loop until we are interrupted or if we fetched something
// Loop until we are interrupted or until we fetch something.
loop {
use futures::future::FutureExt;
use rand::Rng;
let mut interval = tokio::time::interval(Duration::from_millis(timeout_ms));
timeout_ms = timeout_ms
.saturating_add(rand::thread_rng().gen_range((timeout_ms / 2)..=timeout_ms));
interval.tick().await; // The first tick completes immediately.
match interval
.tick()
.map(|_| Event::Tick)
.race(
self.idle_interrupt_receiver
.recv()
.map(|_| Event::Interrupt),
)
.await
{
Event::Tick => {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.prepare(context).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if let Some(session) = &self.session {
if session.can_idle()
&& !context
.get_config_bool(Config::DisableIdle)
.await
.context("Failed to get disable_idle config")
.log_err(context)
.unwrap_or_default()
{
// we only fake-idled because network was gone during IDLE, probably
break;
}
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
Err(_) => {
// Let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
match self
.fetch_new_messages(context, &watch_folder, folder_meaning, false)
.await
{
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
timeout_ms = TIMEOUT_INIT_MS;
if res {
break;
}
}
Err(err) => {
error!(context, "could not fetch from folder: {:#}", err);
self.trigger_reconnect(context);
}
let res = self
.fetch_new_messages(context, session, &watch_folder, folder_meaning, false)
.await?;
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break;
}
}
Event::Interrupt => {
info!(context, "Fake IDLE interrupted");
Ok(_) => {
info!(context, "Fake IDLE interrupted.");
break;
}
}
@@ -192,5 +139,6 @@ impl Imap {
"IMAP-fake-IDLE done after {:.4}s",
time_elapsed(&fake_idle_start_time).as_millis() as f64 / 1000.,
);
Ok(())
}
}
+7 -7
View File
@@ -4,14 +4,18 @@ use anyhow::{Context as _, Result};
use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name};
use crate::config::Config;
use crate::imap::Imap;
use crate::imap::{session::Session, Imap};
use crate::log::LogExt;
use crate::tools::{self, time_elapsed};
use crate::{context::Context, imap::FolderMeaning};
impl Imap {
/// Returns true if folders were scanned, false if scanning was postponed.
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<bool> {
pub(crate) async fn scan_folders(
&mut self,
context: &Context,
session: &mut Session,
) -> Result<bool> {
// First of all, debounce to once per minute:
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
@@ -26,8 +30,6 @@ impl Imap {
}
info!(context, "Starting full folder scan");
self.prepare(context).await?;
let session = self.session.as_mut().context("No IMAP session")?;
let folders = session.list_folders().await?;
let watched_folders = get_watched_folders(context).await?;
@@ -64,18 +66,16 @@ impl Imap {
&& folder_meaning != FolderMeaning::Drafts
&& folder_meaning != FolderMeaning::Trash
{
let session = self.session.as_mut().context("no session")?;
// Drain leftover unsolicited EXISTS messages
session.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, folder.name(), folder_meaning)
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
.await
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
let session = self.session.as_mut().context("no session")?;
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !session.server_sent_unsolicited_exists(context)? {
break;
+2 -2
View File
@@ -52,7 +52,7 @@ impl ImapSession {
/// Selects a folder, possibly updating uid_validity and, if needed,
/// expunging the folder to remove delete-marked messages.
/// Returns whether a new folder was selected.
pub(super) async fn select_folder(
pub(crate) async fn select_folder(
&mut self,
context: &Context,
folder: Option<&str>,
@@ -256,7 +256,7 @@ impl ImapSession {
}
#[derive(PartialEq, Debug, Copy, Clone, Eq)]
pub(super) enum NewlySelected {
pub(crate) enum NewlySelected {
/// The folder was newly selected during this call to select_folder().
Yes,
/// No SELECT command was run because the folder already was selected
+4
View File
@@ -90,6 +90,10 @@ impl Session {
self.capabilities.can_metadata
}
pub fn can_push(&self) -> bool {
self.capabilities.can_push
}
/// Returns the names of all folders on the IMAP server.
pub async fn list_folders(&mut self) -> Result<Vec<async_imap::types::Name>> {
let list = self.list(Some(""), Some("*")).await?.try_collect().await?;
+1
View File
@@ -98,6 +98,7 @@ mod color;
pub mod html;
pub mod net;
pub mod plaintext;
mod push;
pub mod summary;
mod debug_logging;
+115
View File
@@ -0,0 +1,115 @@
use std::sync::atomic::Ordering;
use std::sync::Arc;
use anyhow::Result;
use tokio::sync::RwLock;
use crate::context::Context;
use crate::net::http;
/// Manages subscription to Apple Push Notification services.
///
/// This structure is created by account manager and is shared between accounts.
/// To enable notifications, application should request the device token as described in
/// <https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns>
/// and give it to the account manager, which will forward the token in this structure.
///
/// Each account (context) can then retrieve device token
/// from this structure and give it to the email server.
/// If email server does not support push notifications,
/// account can call `subscribe` method
/// to register device token with the heartbeat
/// notification provider server as a fallback.
#[derive(Debug, Clone, Default)]
pub struct PushSubscriber {
inner: Arc<RwLock<PushSubscriberState>>,
}
impl PushSubscriber {
/// Creates new push notification subscriber.
pub(crate) fn new() -> Self {
Default::default()
}
/// Sets device token for Apple Push Notification service.
pub(crate) async fn set_device_token(&mut self, token: &str) {
self.inner.write().await.device_token = Some(token.to_string());
}
/// Retrieves device token.
///
/// Token may be not available if application is not running on Apple platform,
/// failed to register for remote notifications or is in the process of registering.
///
/// IMAP loop should periodically check if device token is available
/// and send the token to the email server if it supports push notifications.
pub(crate) async fn device_token(&self) -> Option<String> {
self.inner.read().await.device_token.clone()
}
/// Subscribes for heartbeat notifications with previously set device token.
pub(crate) async fn subscribe(&self) -> Result<()> {
let mut state = self.inner.write().await;
if state.heartbeat_subscribed {
return Ok(());
}
let Some(ref token) = state.device_token else {
return Ok(());
};
let socks5_config = None;
let response = http::get_client(socks5_config)?
.post("https://notifications.delta.chat/register")
.body(format!("{{\"token\":\"{token}\"}}"))
.send()
.await?;
let response_status = response.status();
if response_status.is_success() {
state.heartbeat_subscribed = true;
}
Ok(())
}
pub(crate) async fn heartbeat_subscribed(&self) -> bool {
self.inner.read().await.heartbeat_subscribed
}
}
#[derive(Debug, Default)]
pub(crate) struct PushSubscriberState {
/// Device token.
device_token: Option<String>,
/// If subscribed to heartbeat push notifications.
heartbeat_subscribed: bool,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i8)]
pub enum NotifyState {
/// Not subscribed to push notifications.
#[default]
NotConnected = 0,
/// Subscribed to heartbeat push notifications.
Heartbeat = 1,
/// Subscribed to push notifications for new messages.
Connected = 2,
}
impl Context {
/// Returns push notification subscriber state.
pub async fn push_state(&self) -> NotifyState {
if self.push_subscribed.load(Ordering::Relaxed) {
NotifyState::Connected
} else if self.push_subscriber.heartbeat_subscribed().await {
NotifyState::Heartbeat
} else {
NotifyState::NotConnected
}
}
}
+163 -187
View File
@@ -19,7 +19,7 @@ use crate::context::Context;
use crate::download::{download_msg, DownloadState};
use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::events::EventType;
use crate::imap::{FolderMeaning, Imap};
use crate::imap::{session::Session, FolderMeaning, Imap};
use crate::location;
use crate::log::LogExt;
use crate::message::MsgId;
@@ -330,7 +330,7 @@ pub(crate) struct Scheduler {
recently_seen_loop: RecentlySeenLoop,
}
async fn download_msgs(context: &Context, imap: &mut Imap) -> Result<()> {
async fn download_msgs(context: &Context, session: &mut Session) -> Result<()> {
let msg_ids = context
.sql
.query_map(
@@ -349,7 +349,7 @@ async fn download_msgs(context: &Context, imap: &mut Imap) -> Result<()> {
.await?;
for msg_id in msg_ids {
if let Err(err) = download_msg(context, msg_id, imap).await {
if let Err(err) = download_msg(context, msg_id, session).await {
warn!(context, "Failed to download message {msg_id}: {:#}.", err);
// Update download state to failure
@@ -392,94 +392,26 @@ async fn inbox_loop(
return;
};
let mut old_session: Option<Session> = None;
loop {
if let Err(err) = connection.prepare(&ctx).await {
warn!(ctx, "Failed to prepare connection: {:#}.", err);
}
{
// Update quota no more than once a minute.
let quota_needs_update = {
let quota = ctx.quota.read().await;
quota
.as_ref()
.filter(|quota| time_elapsed(&quota.modified) > Duration::from_secs(60))
.is_none()
};
if quota_needs_update {
if let Some(session) = connection.session.as_mut() {
if let Err(err) = ctx.update_recent_quota(session).await {
warn!(ctx, "Failed to update quota: {:#}.", err);
}
let session = if let Some(session) = old_session.take() {
session
} else {
match connection.prepare(&ctx).await {
Err(err) => {
warn!(ctx, "Failed to prepare connection: {:#}.", err);
continue;
}
}
}
let resync_requested = ctx.resync_request.swap(false, Ordering::Relaxed);
if resync_requested {
if let Some(session) = connection.session.as_mut() {
if let Err(err) = session.resync_folders(&ctx).await {
warn!(ctx, "Failed to resync folders: {:#}.", err);
ctx.resync_request.store(true, Ordering::Relaxed);
}
}
}
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.log_err(&ctx).ok();
}
}
Err(err) => {
warn!(ctx, "Failed to get last housekeeping time: {}", err);
Ok(session) => session,
}
};
match ctx.get_config_bool(Config::FetchedExistingMsgs).await {
Ok(fetched_existing_msgs) => {
if !fetched_existing_msgs {
// Consider it done even if we fail.
//
// This operation is not critical enough to retry,
// especially if the error is persistent.
if let Err(err) = ctx
.set_config_internal(
Config::FetchedExistingMsgs,
config::from_bool(true),
)
.await
{
warn!(ctx, "Can't set Config::FetchedExistingMsgs: {:#}", err);
}
if let Err(err) = connection.fetch_existing_msgs(&ctx).await {
warn!(ctx, "Failed to fetch existing messages: {:#}", err);
connection.trigger_reconnect(&ctx);
}
}
}
Err(err) => {
warn!(ctx, "Can't get Config::FetchedExistingMsgs: {:#}", err);
match inbox_fetch_idle(&ctx, &mut connection, session).await {
Err(err) => warn!(ctx, "Failed fetch_idle: {err:#}"),
Ok(session) => {
old_session = Some(session);
}
}
if let Err(err) = download_msgs(&ctx, &mut connection).await {
warn!(ctx, "Failed to download messages: {:#}", err);
}
if let Some(session) = connection.session.as_mut() {
if let Err(err) = session.fetch_metadata(&ctx).await {
warn!(ctx, "Failed to fetch metadata: {err:#}.");
}
}
fetch_idle(&ctx, &mut connection, FolderMeaning::Inbox).await;
}
};
@@ -525,82 +457,127 @@ pub async fn convert_folder_meaning(
Ok((folder_config, watch_folder))
}
/// Implement a single iteration of IMAP loop.
///
/// This function performs all IMAP operations on a single folder, selecting it if necessary and
/// handling all the errors. In case of an error, it is logged, but not propagated upwards. If
/// critical operation fails such as fetching new messages fails, connection is reset via
/// `trigger_reconnect`, so a fresh one can be opened.
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: FolderMeaning) {
let create_mvbox = true;
if let Err(err) = connection
.ensure_configured_folders(ctx, create_mvbox)
.await
{
warn!(
ctx,
"Cannot watch {folder_meaning}, ensure_configured_folders() failed: {:#}", err,
);
connection.idle_interrupt_receiver.recv().await.ok();
return;
async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> Result<Session> {
// Update quota no more than once a minute.
let quota_needs_update = {
let quota = ctx.quota.read().await;
quota
.as_ref()
.filter(|quota| time_elapsed(&quota.modified) > Duration::from_secs(60))
.is_none()
};
if quota_needs_update {
if let Err(err) = ctx.update_recent_quota(&mut session).await {
warn!(ctx, "Failed to update quota: {:#}.", err);
}
}
let (folder_config, watch_folder) = match convert_folder_meaning(ctx, folder_meaning).await {
Ok(meaning) => meaning,
Err(error) => {
// Warning instead of error because the folder may not be configured.
// For example, this happens if the server does not have Sent folder
// but watching Sent folder is enabled.
warn!(ctx, "Error converting IMAP Folder name: {:?}", error);
connection.connectivity.set_not_configured(ctx).await;
connection.idle_interrupt_receiver.recv().await.ok();
return;
let resync_requested = ctx.resync_request.swap(false, Ordering::Relaxed);
if resync_requested {
if let Err(err) = session.resync_folders(ctx).await {
warn!(ctx, "Failed to resync folders: {:#}.", err);
ctx.resync_request.store(true, Ordering::Relaxed);
}
}
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.log_err(ctx).ok();
}
}
Err(err) => {
warn!(ctx, "Failed to get last housekeeping time: {}", err);
}
};
// connect and fake idle if unable to connect
if let Err(err) = connection
.prepare(ctx)
.await
.context("prepare IMAP connection")
{
warn!(ctx, "{:#}", err);
connection.trigger_reconnect(ctx);
return;
}
match ctx.get_config_bool(Config::FetchedExistingMsgs).await {
Ok(fetched_existing_msgs) => {
if !fetched_existing_msgs {
// Consider it done even if we fail.
//
// This operation is not critical enough to retry,
// especially if the error is persistent.
if let Err(err) = ctx
.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(true))
.await
{
warn!(ctx, "Can't set Config::FetchedExistingMsgs: {:#}", err);
}
if folder_config == Config::ConfiguredInboxFolder {
if let Some(session) = connection.session.as_mut() {
session
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap")
.log_err(ctx)
.ok();
} else {
warn!(ctx, "No session even though we just prepared it");
if let Err(err) = imap.fetch_existing_msgs(ctx, &mut session).await {
warn!(ctx, "Failed to fetch existing messages: {:#}", err);
}
}
}
Err(err) => {
warn!(ctx, "Can't get Config::FetchedExistingMsgs: {:#}", err);
}
}
// Fetch the watched folder.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, folder_meaning)
download_msgs(ctx, &mut session)
.await
.context("fetch_move_delete")
{
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
return;
.context("Failed to download messages")?;
session
.fetch_metadata(ctx)
.await
.context("Failed to fetch metadata")?;
session
.register_token(ctx)
.await
.context("Failed to register push token")?;
let session = fetch_idle(ctx, imap, session, FolderMeaning::Inbox).await?;
Ok(session)
}
/// Implement a single iteration of IMAP loop.
///
/// This function performs all IMAP operations on a single folder, selecting it if necessary and
/// handling all the errors. In case of an error, an error is returned and connection is dropped,
/// otherwise connection is returned.
async fn fetch_idle(
ctx: &Context,
connection: &mut Imap,
mut session: Session,
folder_meaning: FolderMeaning,
) -> Result<Session> {
let (folder_config, watch_folder) = match convert_folder_meaning(ctx, folder_meaning).await {
Ok(meaning) => meaning,
Err(err) => {
// Warning instead of error because the folder may not be configured.
// For example, this happens if the server does not have Sent folder
// but watching Sent folder is enabled.
warn!(ctx, "Error converting IMAP Folder name: {err:#}.");
connection.connectivity.set_not_configured(ctx).await;
connection.idle_interrupt_receiver.recv().await.ok();
return Err(err);
}
};
if folder_config == Config::ConfiguredInboxFolder {
session
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap")?;
}
// Fetch the watched folder.
connection
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
.await
.context("fetch_move_delete")?;
// 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.
delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages")
.log_err(ctx)
.ok();
.context("delete_expired_imap_messages")?;
// Scan additional folders only after finishing fetching the watched folder.
//
@@ -608,7 +585,11 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
// be able to scan all folders before time is up if there are many of them.
if folder_config == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
match connection.scan_folders(ctx).await.context("scan_folders") {
match connection
.scan_folders(ctx, &mut session)
.await
.context("scan_folders")
{
Err(err) => {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
@@ -621,42 +602,26 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
// 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, folder_meaning)
connection
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
.await
.context("fetch_move_delete after scan_folders")
{
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
return;
}
.context("fetch_move_delete after scan_folders")?;
}
Ok(false) => {}
}
}
// Synchronize Seen flags.
if let Some(session) = connection.session.as_mut() {
session
.sync_seen_flags(ctx, &watch_folder)
.await
.context("sync_seen_flags")
.log_err(ctx)
.ok();
} else {
warn!(ctx, "No IMAP session, skipping flag synchronization.");
}
session
.sync_seen_flags(ctx, &watch_folder)
.await
.context("sync_seen_flags")
.log_err(ctx)
.ok();
connection.connectivity.set_idle(ctx).await;
ctx.emit_event(EventType::ImapInboxIdle);
let Some(session) = connection.session.take() else {
warn!(ctx, "No IMAP session, going to fake idle.");
connection
.fake_idle(ctx, watch_folder, folder_meaning)
.await;
return;
};
if !session.can_idle() {
info!(
@@ -664,9 +629,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
"IMAP session does not support IDLE, going to fake idle."
);
connection
.fake_idle(ctx, watch_folder, folder_meaning)
.await;
return;
.fake_idle(ctx, &mut session, watch_folder, folder_meaning)
.await?;
return Ok(session);
}
if ctx
@@ -678,29 +643,22 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
{
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
connection
.fake_idle(ctx, watch_folder, folder_meaning)
.await;
return;
.fake_idle(ctx, &mut session, watch_folder, folder_meaning)
.await?;
return Ok(session);
}
info!(ctx, "IMAP session supports IDLE, using it.");
match session
let session = session
.idle(
ctx,
connection.idle_interrupt_receiver.clone(),
&watch_folder,
)
.await
.context("idle")
{
Ok(session) => {
connection.session = Some(session);
}
Err(err) => {
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
}
}
.context("idle")?;
Ok(session)
}
async fn simple_imap_loop(
@@ -726,8 +684,26 @@ async fn simple_imap_loop(
return;
}
let mut old_session: Option<Session> = None;
loop {
fetch_idle(&ctx, &mut connection, folder_meaning).await;
let session = if let Some(session) = old_session.take() {
session
} else {
match connection.prepare(&ctx).await {
Err(err) => {
warn!(ctx, "Failed to prepare connection: {:#}.", err);
continue;
}
Ok(session) => session,
}
};
match fetch_idle(&ctx, &mut connection, session, folder_meaning).await {
Err(err) => warn!(ctx, "Failed fetch_idle: {err:#}"),
Ok(session) => {
old_session = Some(session);
}
}
}
};