mirror of
https://github.com/chatmail/core.git
synced 2026-04-03 14:02:10 +03:00
Compare commits
1 Commits
blob-api
...
e2ee-force
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd81ecdb5d |
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -8,9 +8,6 @@ on:
|
||||
- staging
|
||||
- trying
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
|
||||
jobs:
|
||||
|
||||
fmt:
|
||||
@@ -24,8 +21,6 @@ jobs:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
@@ -40,8 +35,6 @@ jobs:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -101,14 +94,31 @@ jobs:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: target
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: check
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: check
|
||||
args: --all --bins --examples --tests --features repl --benches
|
||||
args: --all --bins --examples --tests --features repl
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,26 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### API changes
|
||||
|
||||
- Removed `mvbox_watch` option. #2906
|
||||
It is automatically enabled whenever `mvbox_move` is enabled.
|
||||
|
||||
## 1.70.0
|
||||
|
||||
### Fixes
|
||||
- fix: do not abort Param parsing on unknown keys #2856
|
||||
- fix: execute `Chat-Group-Member-Removed:` even when arriving disordered #2857
|
||||
|
||||
|
||||
## 1.69.0
|
||||
|
||||
### Fixes
|
||||
- fix group-related system messages in multi-device setups #2848
|
||||
- fix "Google Workspace" (former "G Suite") issues related to bad resolvers #2852
|
||||
|
||||
|
||||
## 1.68.0
|
||||
|
||||
### Fixes
|
||||
|
||||
442
Cargo.lock
generated
442
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.70.0"
|
||||
version = "1.68.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -45,7 +45,7 @@ native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.9.0"
|
||||
once_cell = "1.8.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.7", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
@@ -54,14 +54,14 @@ r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.19"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
rusqlite = { version = "0.26", features = ["sqlcipher"] }
|
||||
rusqlite = "0.26"
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "9", optional = true }
|
||||
rustyline = { version = "9.0", optional = true }
|
||||
sanitize-filename = "0.3"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
sha-1 = "0.9"
|
||||
sha2 = "0.9"
|
||||
smallvec = "1"
|
||||
stop-token = "0.6"
|
||||
strum = "0.23"
|
||||
@@ -74,7 +74,7 @@ uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
fast-socks5 = "0.4"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "3.3.0"
|
||||
tagger = "3.2.1"
|
||||
textwrap = "0.14.2"
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -120,5 +120,5 @@ harness = false
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled-sqlcipher-vendored-openssl"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled"]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
@@ -8,7 +8,9 @@ async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
let context = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let book = (0..n)
|
||||
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
|
||||
|
||||
@@ -8,7 +8,7 @@ async fn create_accounts(n: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 2..n {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
|
||||
@@ -6,7 +6,9 @@ use std::path::Path;
|
||||
async fn search_benchmark(path: impl AsRef<Path>) {
|
||||
let dbfile = path.as_ref();
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
let context = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for _ in 0..10u32 {
|
||||
context.search_msgs(None, "hello").await.unwrap();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.70.0"
|
||||
version = "1.68.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -14,7 +14,6 @@ extern "C" {
|
||||
typedef struct _dc_context dc_context_t;
|
||||
typedef struct _dc_accounts dc_accounts_t;
|
||||
typedef struct _dc_array dc_array_t;
|
||||
typedef struct _dc_blob dc_blob_t;
|
||||
typedef struct _dc_chatlist dc_chatlist_t;
|
||||
typedef struct _dc_chat dc_chat_t;
|
||||
typedef struct _dc_msg dc_msg_t;
|
||||
@@ -288,20 +287,26 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* To save traffic, however, the avatar is attached only as needed
|
||||
* and also recoded to a reasonable size.
|
||||
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
|
||||
* - `e2ee_force` = 1=ignore encryption preferences of others,
|
||||
* 0=use majority vote when deciding whether to encrypt (default).
|
||||
* - `mdns_enabled` = 0=do not send or request read receipts,
|
||||
* 1=send and request read receipts (default)
|
||||
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
|
||||
* 1=send a copy of outgoing messages to self.
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `inbox_watch` = 1=watch `INBOX`-folder for changes (default),
|
||||
* 0=do not watch the `INBOX`-folder,
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `sentbox_watch`= 1=watch `Sent`-folder for changes (default),
|
||||
* 0=do not watch the `Sent`-folder,
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `mvbox_move` = 1=detect chat messages,
|
||||
* move them to the `DeltaChat` folder,
|
||||
* and watch the `DeltaChat` folder for updates (default),
|
||||
* 0=do not move chat-messages
|
||||
* - `mvbox_watch` = 1=watch `DeltaChat`-folder for changes (default),
|
||||
* 0=do not watch the `DeltaChat`-folder,
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `mvbox_move` = 1=heuristically detect chat-messages
|
||||
* and move them to the `DeltaChat`-folder,
|
||||
* 0=do not move chat-messages
|
||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||
* show direct replies to chats only (default),
|
||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||
@@ -680,8 +685,8 @@ void dc_maybe_network (dc_context_t* context);
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param addr The email address of the user. This must match the
|
||||
* configured_addr setting of the context as well as the UID of the key.
|
||||
* @param public_data ASCII armored public key.
|
||||
* @param secret_data ASCII armored secret key.
|
||||
* @param public_data The public key as base64.
|
||||
* @param secret_data The secret key as base64.
|
||||
* @return 1 on success, 0 on failure.
|
||||
*/
|
||||
int dc_preconfigure_keypair (dc_context_t* context, const char *addr, const char *public_data, const char *secret_data);
|
||||
@@ -963,6 +968,8 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
* @param msg The message to save as a draft.
|
||||
* Existing draft will be overwritten.
|
||||
* NULL deletes the existing draft, if any, without sending it.
|
||||
* Currently, also non-text-messages
|
||||
* will delete the existing drafts.
|
||||
*/
|
||||
void dc_set_draft (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
|
||||
|
||||
@@ -1378,6 +1385,7 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
|
||||
* Create a new group chat.
|
||||
*
|
||||
* After creation,
|
||||
* the draft of the chat is set to a default text,
|
||||
* the group has one member with the ID DC_CONTACT_ID_SELF
|
||||
* and is in _unpromoted_ state.
|
||||
* This means, you can add or remove members, change the name,
|
||||
@@ -2872,45 +2880,6 @@ int dc_array_is_independent (const dc_array_t* array, size_t in
|
||||
int dc_array_search_id (const dc_array_t* array, uint32_t needle, size_t* ret_index);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_blob_t
|
||||
*
|
||||
* An object representing a file blob.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Free a blob object.
|
||||
*
|
||||
* @memberof dc_blob_t
|
||||
* @param blob The blob object.
|
||||
*/
|
||||
void dc_blob_unref (dc_blob_t* blob);
|
||||
|
||||
/**
|
||||
* Returns blob size in bytes.
|
||||
*
|
||||
* @memberof dc_blob_t
|
||||
* @param blob The blob object.
|
||||
* @return Returns blob size. Never fails.
|
||||
*/
|
||||
|
||||
size_t dc_blob_bytes (dc_blob_t* blob);
|
||||
|
||||
/**
|
||||
* Reads data from a blob.
|
||||
* Reads exactly the number of bytes requested and
|
||||
* returns an error if it's not possible.
|
||||
*
|
||||
* @memberof dc_blob_t
|
||||
* @param blob The blob object.
|
||||
* @param count Number of bytes to read from the blob.
|
||||
* @param offset Offset of the first byte to read inside the blob.
|
||||
* @return 1 on success, 0 on error.
|
||||
*/
|
||||
int dc_blob_read (dc_blob_t* blob, void* buf, size_t count, off_t offset);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_chatlist_t
|
||||
*
|
||||
@@ -3611,16 +3580,6 @@ char* dc_msg_get_filemime (const dc_msg_t* msg);
|
||||
uint64_t dc_msg_get_filebytes (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get the file blob.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return File blob, NULL if not applicable or on errors.
|
||||
*/
|
||||
dc_blob_t *dc_msg_get_fileblob (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get width of image or video. The width is returned in pixels.
|
||||
* If the width is unknown or if the associated file is no image or video file,
|
||||
@@ -4260,15 +4219,6 @@ char* dc_msg_get_quoted_text (const dc_msg_t* msg);
|
||||
*/
|
||||
dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* Force the message to be sent in plain text.
|
||||
*
|
||||
* This API is for bots, there is no need to expose it in the UI.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
*/
|
||||
void dc_msg_force_plaintext (dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* @class dc_contact_t
|
||||
|
||||
@@ -63,7 +63,7 @@ pub type dc_context_t = Context;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_new(
|
||||
_os_name: *const libc::c_char,
|
||||
os_name: *const libc::c_char,
|
||||
dbfile: *const libc::c_char,
|
||||
blobdir: *const libc::c_char,
|
||||
) -> *mut dc_context_t {
|
||||
@@ -74,11 +74,21 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let os_name = if os_name.is_null() {
|
||||
String::from("DcFFI")
|
||||
} else {
|
||||
to_string_lossy(os_name)
|
||||
};
|
||||
|
||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||
use rand::Rng;
|
||||
// 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).to_path_buf().into(), id))
|
||||
block_on(Context::new(
|
||||
os_name,
|
||||
as_path(dbfile).to_path_buf().into(),
|
||||
id,
|
||||
))
|
||||
} else {
|
||||
eprintln!("blobdir can not be defined explicitly anymore");
|
||||
return ptr::null_mut();
|
||||
@@ -183,7 +193,7 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
.unwrap_or_default()
|
||||
.strdup(),
|
||||
Err(_) => {
|
||||
warn!(ctx, "dc_get_config(): invalid key '{}'", &key);
|
||||
warn!(ctx, "dc_get_config(): invalid key");
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
@@ -632,8 +642,8 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
|
||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
||||
let public = key::SignedPublicKey::from_base64(&to_string_lossy(public_data))?;
|
||||
let secret = key::SignedSecretKey::from_base64(&to_string_lossy(secret_data))?;
|
||||
let keypair = key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
@@ -1727,7 +1737,7 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
Contact::lookup_id_by_addr(ctx, &to_string_lossy(addr), Origin::IncomingReplyTo)
|
||||
Contact::lookup_id_by_addr(ctx, to_string_lossy(addr), Origin::IncomingReplyTo)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to lookup id")
|
||||
.unwrap_or(0)
|
||||
@@ -3425,16 +3435,6 @@ pub unsafe extern "C" fn dc_msg_get_quoted_msg(msg: *const dc_msg_t) -> *mut dc_
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_force_plaintext()");
|
||||
return;
|
||||
}
|
||||
let ffi_msg = &mut *msg;
|
||||
ffi_msg.message.force_plaintext();
|
||||
}
|
||||
|
||||
// dc_contact_t
|
||||
|
||||
/// FFI struct for [dc_contact_t]
|
||||
@@ -3753,11 +3753,7 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
|
||||
match socks5_enabled {
|
||||
Ok(socks5_enabled) => {
|
||||
match block_on(provider::get_provider_info(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
socks5_enabled,
|
||||
)) {
|
||||
match block_on(provider::get_provider_info(addr.as_str(), socks5_enabled)) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -3839,7 +3835,7 @@ pub type dc_accounts_t = AccountsWrapper;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_new(
|
||||
_os_name: *const libc::c_char,
|
||||
os_name: *const libc::c_char,
|
||||
dbfile: *const libc::c_char,
|
||||
) -> *mut dc_accounts_t {
|
||||
setup_panic!();
|
||||
@@ -3849,7 +3845,13 @@ pub unsafe extern "C" fn dc_accounts_new(
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let accs = block_on(Accounts::new(as_path(dbfile).to_path_buf().into()));
|
||||
let os_name = if os_name.is_null() {
|
||||
String::from("DcFFI")
|
||||
} else {
|
||||
to_string_lossy(os_name)
|
||||
};
|
||||
|
||||
let accs = block_on(Accounts::new(os_name, as_path(dbfile).to_path_buf().into()));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
||||
|
||||
@@ -101,7 +101,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
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, "import", 0, false).await {
|
||||
println!("dc_receive_imf errored: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
@@ -1185,7 +1185,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let socks5_enabled = context
|
||||
.get_config_bool(config::Config::Socks5Enabled)
|
||||
.await?;
|
||||
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
|
||||
match provider::get_provider_info(arg1, socks5_enabled).await {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {}:", arg1);
|
||||
println!("status: {}", info.status as u32);
|
||||
|
||||
@@ -297,7 +297,7 @@ 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]).to_path_buf(), 0).await?;
|
||||
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf(), 0).await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
async_std::task::spawn(async move {
|
||||
|
||||
@@ -36,7 +36,7 @@ async fn main() {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
log::info!("creating database {:?}", dbfile);
|
||||
let ctx = Context::new(dbfile.into(), 0)
|
||||
let ctx = Context::new("FakeOs".into(), dbfile.into(), 0)
|
||||
.await
|
||||
.expect("Failed to create context");
|
||||
let info = ctx.get_info().await;
|
||||
|
||||
@@ -75,6 +75,7 @@ def run_cmdline(argv=None, account_plugins=None):
|
||||
ac.set_config("addr", args.email)
|
||||
ac.set_config("mail_pw", args.password)
|
||||
ac.set_config("mvbox_move", "0")
|
||||
ac.set_config("mvbox_watch", "0")
|
||||
ac.set_config("sentbox_watch", "0")
|
||||
ac.set_config("bot", "1")
|
||||
configtracker = ac.configure()
|
||||
|
||||
@@ -403,10 +403,7 @@ class Account(object):
|
||||
"""
|
||||
arr = array("i")
|
||||
for msg in messages:
|
||||
if isinstance(msg, Message):
|
||||
arr.append(msg.id)
|
||||
else:
|
||||
arr.append(msg)
|
||||
arr.append(getattr(msg, "id", msg))
|
||||
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
|
||||
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class FFIEventLogger:
|
||||
|
||||
@account_hookimpl
|
||||
def ac_log_line(self, message):
|
||||
t = threading.current_thread()
|
||||
t = threading.currentThread()
|
||||
tname = getattr(t, "name", t)
|
||||
if tname == "MainThread":
|
||||
tname = "MAIN"
|
||||
@@ -193,7 +193,7 @@ class EventThread(threading.Thread):
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.daemon = True
|
||||
self.setDaemon(True)
|
||||
self._marked_for_shutdown = False
|
||||
self.start()
|
||||
|
||||
|
||||
@@ -225,10 +225,6 @@ class Message(object):
|
||||
"""Quote setter"""
|
||||
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
|
||||
|
||||
def force_plaintext(self) -> None:
|
||||
"""Force the message to be sent in plain text."""
|
||||
lib.dc_msg_force_plaintext(self._dc_msg)
|
||||
|
||||
def get_mime_headers(self):
|
||||
""" return mime-header object for an incoming message.
|
||||
|
||||
|
||||
@@ -303,20 +303,21 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, sentbox=False, move=False,
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
|
||||
pre_generated_key=True, quiet=False, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key, quiet=quiet)
|
||||
configdict.update(config)
|
||||
configdict["mvbox_watch"] = str(int(mvbox))
|
||||
configdict["mvbox_move"] = str(int(move))
|
||||
configdict["sentbox_watch"] = str(int(sentbox))
|
||||
ac.update_config(configdict)
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self, pre_generated_key=True, move=False):
|
||||
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key, move=move)
|
||||
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
|
||||
self.wait_configure_and_start_io([ac1])
|
||||
return ac1
|
||||
|
||||
@@ -335,7 +336,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
return accounts
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
""" Clones addr, mail_pw, mvbox_move, sentbox_watch and the
|
||||
""" Clones addr, mail_pw, mvbox_watch, mvbox_move, sentbox_watch and the
|
||||
direct_imap object of an online account. This simulates the user setting
|
||||
up a new device without importing a backup.
|
||||
|
||||
@@ -350,6 +351,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
ac.update_config(dict(
|
||||
addr=account.get_config("addr"),
|
||||
mail_pw=account.get_config("mail_pw"),
|
||||
mvbox_watch=account.get_config("mvbox_watch"),
|
||||
mvbox_move=account.get_config("mvbox_move"),
|
||||
sentbox_watch=account.get_config("sentbox_watch"),
|
||||
))
|
||||
@@ -465,7 +467,7 @@ class BotProcess:
|
||||
# the (unicode) lines available for readers through a queue.
|
||||
self.stdout_queue = queue.Queue()
|
||||
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
|
||||
t.daemon = True
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
def _run_stdout_thread(self) -> None:
|
||||
|
||||
@@ -57,7 +57,7 @@ class TestOfflineAccountBasic:
|
||||
alice_public = data.read_path("key/alice-public.asc")
|
||||
alice_secret = data.read_path("key/alice-secret.asc")
|
||||
assert alice_public and alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
|
||||
ac._preconfigure_keypair("alice@example.com", alice_public, alice_secret)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -860,7 +860,7 @@ class TestOnlineAccount:
|
||||
|
||||
def test_mvbox_sentbox_threads(self, acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.get_online_configuring_account(move=True, sentbox=True)
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True, sentbox=True)
|
||||
|
||||
lp.sec("ac2: start without mvbox/sentbox threads")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
@@ -874,20 +874,16 @@ class TestOnlineAccount:
|
||||
|
||||
def test_move_works(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(move=True)
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=True, move=True)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message1")
|
||||
|
||||
# Message is moved to the movebox
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
# Message is downloaded
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
def test_move_works_on_self_sent(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account(move=True)
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure_and_start_io()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
@@ -957,7 +953,7 @@ class TestOnlineAccount:
|
||||
assert msg_in.is_forwarded()
|
||||
|
||||
def test_send_self_message(self, acfactory, lp):
|
||||
ac1 = acfactory.get_one_online_account(move=True)
|
||||
ac1 = acfactory.get_one_online_account(mvbox=True, move=True)
|
||||
lp.sec("ac1: create self chat")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
chat.send_text("hello")
|
||||
@@ -987,8 +983,7 @@ class TestOnlineAccount:
|
||||
assert msg2 in chat2.get_messages()
|
||||
assert chat2.is_contact_request()
|
||||
assert chat2.count_fresh_messages() == 1
|
||||
# Like it or not, this assert is flaky
|
||||
# assert msg2.time_received >= msg1.time_sent
|
||||
assert msg2.time_received >= msg1.time_sent
|
||||
|
||||
lp.sec("create new chat with contact and verify it's proper")
|
||||
chat2b = msg2.create_chat()
|
||||
@@ -1039,34 +1034,27 @@ class TestOnlineAccount:
|
||||
|
||||
def test_moved_markseen(self, acfactory, lp):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(move=True)
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, config={"inbox_watch": "0"})
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure_and_start_io([ac1, ac2])
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
ac2.stop_io()
|
||||
ac2.direct_imap.idle_start()
|
||||
|
||||
ac1.direct_imap.idle_start()
|
||||
ac1.create_chat(ac2).send_text("Hello!")
|
||||
|
||||
# Wait for the message to arrive.
|
||||
ac2.direct_imap.idle_check(terminate=True)
|
||||
ac1.direct_imap.idle_check(terminate=True)
|
||||
ac1.stop_io()
|
||||
|
||||
# Emulate moving of the message to DeltaChat folder by Sieve rule.
|
||||
# mailcow server contains this rule by default.
|
||||
ac2.direct_imap.conn.move(["*"], "DeltaChat")
|
||||
ac1.direct_imap.conn.move(["*"], "DeltaChat")
|
||||
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
ac2.direct_imap.idle_start()
|
||||
ac2.start_io()
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
ac1.direct_imap.idle_start()
|
||||
ac1.start_io()
|
||||
ac1.direct_imap.idle_wait_for_seen()
|
||||
ac1.direct_imap.idle_done()
|
||||
|
||||
# Accept the contact request.
|
||||
msg.chat.accept()
|
||||
ac2.mark_seen_messages([msg])
|
||||
ac2.direct_imap.idle_wait_for_seen()
|
||||
ac2.direct_imap.idle_done()
|
||||
|
||||
fetch = list(ac2.direct_imap.conn.fetch("*", b'FLAGS').values())
|
||||
fetch = list(ac1.direct_imap.conn.fetch("*", b'FLAGS').values())
|
||||
flags = fetch[-1][b'FLAGS']
|
||||
is_seen = b'\\Seen' in flags
|
||||
assert is_seen
|
||||
@@ -1107,8 +1095,8 @@ class TestOnlineAccount:
|
||||
def test_markseen_message_and_mdn(self, acfactory, mvbox_move):
|
||||
# Please only change this test if you are very sure that it will still catch the issues it catches now.
|
||||
# We had so many problems with markseen, if in doubt, rather create another test, it can't harm.
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
ac2 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
|
||||
ac2 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
@@ -1154,7 +1142,7 @@ class TestOnlineAccount:
|
||||
assert not msg_reply1.chat.is_group()
|
||||
assert msg_reply1.chat.id == private_chat1.id
|
||||
|
||||
def test_mdn_asymmetric(self, acfactory, lp):
|
||||
def test_mdn_asymetric(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts(move=True)
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
@@ -1178,9 +1166,6 @@ class TestOnlineAccount:
|
||||
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
ac1.direct_imap.select_config_folder("mvbox")
|
||||
ac1.direct_imap.idle_start()
|
||||
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
@@ -1190,9 +1175,6 @@ class TestOnlineAccount:
|
||||
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
assert ac1.direct_imap.idle_wait_for_seen()
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
assert msg_out.is_out_mdn_received()
|
||||
|
||||
@@ -1377,7 +1359,7 @@ class TestOnlineAccount:
|
||||
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac1.set_config("show_emails", "2")
|
||||
ac1.create_contact("alice@example.org").create_chat()
|
||||
ac1.create_contact("alice@example.com").create_chat()
|
||||
|
||||
acfactory.wait_configure(ac1)
|
||||
ac1.direct_imap.create_folder("Drafts")
|
||||
@@ -1391,7 +1373,7 @@ class TestOnlineAccount:
|
||||
ac1.direct_imap.append("Drafts", """
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.org
|
||||
To: alice@example.com
|
||||
Message-ID: <aepiors@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
@@ -1400,7 +1382,7 @@ class TestOnlineAccount:
|
||||
ac1.direct_imap.append("Sent", """
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.org
|
||||
To: alice@example.com
|
||||
Message-ID: <hsabaeni@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
@@ -1431,35 +1413,6 @@ class TestOnlineAccount:
|
||||
assert msg2.text == "subj – message in Drafts that is moved to Sent later"
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
|
||||
def test_no_old_msg_is_fresh(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1_clone = acfactory.clone_online_account(ac1)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
ac1.set_config("e2ee_enabled", "0")
|
||||
ac1_clone.set_config("e2ee_enabled", "0")
|
||||
ac2.set_config("e2ee_enabled", "0")
|
||||
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1.create_chat(ac2)
|
||||
ac1_clone.create_chat(ac2)
|
||||
|
||||
lp.sec("Send a first message from ac2 to ac1 and check that it's 'fresh'")
|
||||
first_msg_id = ac2.create_chat(ac1).send_text("Hi")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
lp.sec("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
ac1_clone.create_chat(ac2).send_text("Hi back")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
|
||||
assert ev.data1 == first_msg_id.id
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 0
|
||||
assert len(list(ac1.get_fresh_messages())) == 0
|
||||
|
||||
def test_prefer_encrypt(self, acfactory, lp):
|
||||
"""Test quorum rule for encryption preference in 1:1 and group chat."""
|
||||
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
|
||||
@@ -2124,8 +2077,10 @@ class TestOnlineAccount:
|
||||
assert msg_back.chat == chat
|
||||
assert chat.get_profile_image() is None
|
||||
|
||||
def test_connectivity(self, acfactory, lp):
|
||||
@pytest.mark.parametrize("inbox_watch", ["0", "1"])
|
||||
def test_connectivity(self, acfactory, lp, inbox_watch):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1.set_config("inbox_watch", inbox_watch)
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
|
||||
@@ -2365,7 +2320,7 @@ class TestOnlineAccount:
|
||||
|
||||
def test_immediate_autodelete(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(move=False, sentbox=False)
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=False, move=False, sentbox=False)
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
@@ -2626,26 +2581,31 @@ class TestOnlineAccount:
|
||||
assert received_reply.quoted_text == "hello"
|
||||
assert received_reply.quote.id == out_msg.id
|
||||
|
||||
@pytest.mark.parametrize("folder,move,expected_destination,", [
|
||||
("xyz", False, "xyz"), # Test that emails are recognized in a random folder but not moved
|
||||
("xyz", True, "DeltaChat"), # ...emails are found in a random folder and moved to DeltaChat
|
||||
("Spam", False, "INBOX"), # ...emails are moved from the spam folder to the Inbox
|
||||
@pytest.mark.parametrize("folder,move,expected_destination,inbox_watch,", [
|
||||
("xyz", False, "xyz", "1"), # Test that emails are recognized in a random folder but not moved
|
||||
("xyz", True, "DeltaChat", "1"), # ...emails are found in a random folder and moved to DeltaChat
|
||||
("Spam", False, "INBOX", "1"), # ...emails are moved from the spam folder to the Inbox
|
||||
("INBOX", False, "INBOX", "0"), # ...emails are found in the `Inbox` folder even if `inbox_watch` is "0"
|
||||
])
|
||||
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
|
||||
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
|
||||
def test_scan_folders(self, acfactory, lp, folder, move, expected_destination):
|
||||
def test_scan_folders(self, acfactory, lp, folder, move, expected_destination, inbox_watch):
|
||||
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
|
||||
variant = folder + "-" + str(move) + "-" + expected_destination
|
||||
lp.sec("Testing variant " + variant)
|
||||
ac1 = acfactory.get_online_configuring_account(move=move)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1.set_config("inbox_watch", inbox_watch)
|
||||
|
||||
acfactory.wait_configure(ac1)
|
||||
ac1.direct_imap.create_folder(folder)
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
# Wait until each folder was selected once and we are IDLEing:
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
if inbox_watch == "1":
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
else:
|
||||
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
|
||||
ac1.stop_io()
|
||||
|
||||
# Send a message to ac1 and move it to the mvbox:
|
||||
@@ -2661,7 +2621,11 @@ class TestOnlineAccount:
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
# The message has been downloaded, which means it has reached its destination.
|
||||
# Wait until the message was moved (if at all) and we are IDLEing again:
|
||||
if inbox_watch == "1":
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
else:
|
||||
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
|
||||
ac1.direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
@@ -2684,7 +2648,7 @@ class TestOnlineAccount:
|
||||
if mvbox_move:
|
||||
assert ac.get_config("configured_mvbox_folder")
|
||||
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=mvbox_move, move=mvbox_move)
|
||||
ac1.set_config("sentbox_move", "1")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
|
||||
@@ -2784,7 +2748,7 @@ class TestOnlineAccount:
|
||||
|
||||
def test_delete_deltachat_folder(self, acfactory):
|
||||
"""Test that DeltaChat folder is recreated if user deletes it manually."""
|
||||
ac1 = acfactory.get_online_configuring_account(move=True)
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure(ac1)
|
||||
|
||||
@@ -2912,8 +2876,7 @@ class TestGroupStressTests:
|
||||
lp.sec("ac2: check that ac3 is removed")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert chat.num_contacts() == 2
|
||||
assert msg.chat.num_contacts() == 2
|
||||
assert msg.chat.num_contacts() == chat.num_contacts()
|
||||
acfactory.dump_imap_summary(sys.stdout)
|
||||
|
||||
|
||||
|
||||
@@ -29,21 +29,21 @@ pub struct Accounts {
|
||||
|
||||
impl Accounts {
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
pub async fn new(dir: PathBuf) -> Result<Self> {
|
||||
pub async fn new(os_name: String, dir: PathBuf) -> Result<Self> {
|
||||
if !dir.exists().await {
|
||||
Accounts::create(&dir).await?;
|
||||
Accounts::create(os_name, &dir).await?;
|
||||
}
|
||||
|
||||
Accounts::open(dir).await
|
||||
}
|
||||
|
||||
/// Creates a new default structure.
|
||||
pub async fn create(dir: &PathBuf) -> Result<()> {
|
||||
pub async fn create(os_name: String, dir: &PathBuf) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
|
||||
Config::new(dir).await?;
|
||||
Config::new(os_name.clone(), dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -106,9 +106,10 @@ impl Accounts {
|
||||
|
||||
/// Add a new account.
|
||||
pub async fn add_account(&mut self) -> Result<u32> {
|
||||
let os_name = self.config.os_name().await;
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
let ctx = Context::new(account_config.dbfile().into(), account_config.id).await?;
|
||||
let ctx = Context::new(os_name, account_config.dbfile().into(), account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
@@ -182,7 +183,13 @@ impl Accounts {
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
let ctx = Context::with_blobdir(new_dbfile, new_blobdir, account_config.id).await?;
|
||||
let ctx = Context::with_blobdir(
|
||||
self.config.os_name().await,
|
||||
new_dbfile,
|
||||
new_blobdir,
|
||||
account_config.id,
|
||||
)
|
||||
.await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
Ok(account_config.id)
|
||||
@@ -342,6 +349,7 @@ pub struct Config {
|
||||
/// This is serialized into TOML.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct InnerConfig {
|
||||
pub os_name: String,
|
||||
/// The currently selected account.
|
||||
pub selected_account: u32,
|
||||
pub next_id: u32,
|
||||
@@ -349,8 +357,9 @@ struct InnerConfig {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn new(dir: &PathBuf) -> Result<Self> {
|
||||
pub async fn new(os_name: String, dir: &PathBuf) -> Result<Self> {
|
||||
let inner = InnerConfig {
|
||||
os_name,
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
@@ -365,6 +374,10 @@ impl Config {
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub async fn os_name(&self) -> String {
|
||||
self.inner.os_name.clone()
|
||||
}
|
||||
|
||||
/// Sync the inmemory representation to disk.
|
||||
async fn sync(&self) -> Result<()> {
|
||||
fs::write(&self.file, toml::to_string_pretty(&self.inner)?)
|
||||
@@ -383,7 +396,12 @@ impl Config {
|
||||
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
|
||||
let mut accounts = BTreeMap::new();
|
||||
for account_config in &self.inner.accounts {
|
||||
let ctx = Context::new(account_config.dbfile().into(), account_config.id).await?;
|
||||
let ctx = Context::new(
|
||||
self.inner.os_name.clone(),
|
||||
account_config.dbfile().into(),
|
||||
account_config.id,
|
||||
)
|
||||
.await?;
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
|
||||
@@ -480,7 +498,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts1").into();
|
||||
|
||||
let mut accounts1 = Accounts::new(p.clone()).await.unwrap();
|
||||
let mut accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
accounts1.add_account().await.unwrap();
|
||||
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
@@ -498,7 +516,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
@@ -525,7 +543,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
@@ -546,12 +564,14 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let extern_dbfile: PathBuf = dir.path().join("other").into();
|
||||
let ctx = Context::new(extern_dbfile.clone(), 0).await.unwrap();
|
||||
let ctx = Context::new("my_os".into(), extern_dbfile.clone(), 0)
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -581,7 +601,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 1..10 {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
@@ -601,7 +621,7 @@ mod tests {
|
||||
let dummy_accounts = 10;
|
||||
|
||||
let (id0, id1, id2) = {
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 1);
|
||||
@@ -636,7 +656,7 @@ mod tests {
|
||||
assert!(id2 > id1 + dummy_accounts);
|
||||
|
||||
let (id0_reopened, id1_reopened, id2_reopened) = {
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
@@ -681,7 +701,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
|
||||
// Make sure there are no accounts.
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
|
||||
37
src/blob.rs
37
src/blob.rs
@@ -621,13 +621,10 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::{
|
||||
chat,
|
||||
message::Message,
|
||||
test_utils::{self, TestContext},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use image::Pixel;
|
||||
|
||||
#[async_std::test]
|
||||
@@ -1069,38 +1066,4 @@ mod tests {
|
||||
assert_eq!(img.height() as u32, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_increation_in_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let file = t.get_blobdir().join("anyfile.dat");
|
||||
File::create(&file).await?.write_all("bla".as_ref()).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let prepared_id = chat::prepare_msg(&t, chat_id, &mut msg).await?;
|
||||
assert_eq!(prepared_id, msg.id);
|
||||
assert!(msg.is_increation());
|
||||
|
||||
let msg = Message::load_from_db(&t, prepared_id).await?;
|
||||
assert!(msg.is_increation());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_increation_not_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
assert_ne!(t.get_blobdir().to_str(), t.dir.path().to_str());
|
||||
|
||||
let file = t.dir.path().join("anyfile.dat");
|
||||
File::create(&file).await?.write_all("bla".as_ref()).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
assert!(chat::prepare_msg(&t, chat_id, &mut msg).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
643
src/chat.rs
643
src/chat.rs
@@ -1,6 +1,5 @@
|
||||
//! # Chat module.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
@@ -22,7 +21,6 @@ use crate::constants::{
|
||||
};
|
||||
use crate::contact::{addr_cmp, Contact, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::dc_receive_imf::ReceivedMsg;
|
||||
use crate::dc_tools::{
|
||||
dc_create_id, dc_create_outgoing_rfc724_mid, dc_create_smeared_timestamp,
|
||||
dc_create_smeared_timestamps, dc_get_abs_path, dc_gm2local_offset, improve_single_line_input,
|
||||
@@ -221,30 +219,31 @@ impl ChatId {
|
||||
pub(crate) async fn create_multiuser_record(
|
||||
context: &Context,
|
||||
chattype: Chattype,
|
||||
grpid: &str,
|
||||
grpname: &str,
|
||||
grpid: impl AsRef<str>,
|
||||
grpname: impl AsRef<str>,
|
||||
create_blocked: Blocked,
|
||||
create_protected: ProtectionStatus,
|
||||
param: Option<String>,
|
||||
) -> Result<Self> {
|
||||
let row_id =
|
||||
context.sql.insert(
|
||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
|
||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
chattype,
|
||||
grpname,
|
||||
grpid,
|
||||
grpname.as_ref(),
|
||||
grpid.as_ref(),
|
||||
create_blocked,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
create_protected,
|
||||
param.unwrap_or_default(),
|
||||
],
|
||||
).await?;
|
||||
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
info!(
|
||||
context,
|
||||
"Created group/mailinglist '{}' grpid={} as {}", grpname, grpid, chat_id
|
||||
"Created group/mailinglist '{}' grpid={} as {}",
|
||||
grpname.as_ref(),
|
||||
grpid.as_ref(),
|
||||
chat_id
|
||||
);
|
||||
|
||||
Ok(chat_id)
|
||||
@@ -435,7 +434,7 @@ impl ChatId {
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&msg_text,
|
||||
msg_text,
|
||||
cmd,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
)
|
||||
@@ -571,7 +570,7 @@ impl ChatId {
|
||||
|
||||
let changed = match &mut msg {
|
||||
None => self.maybe_delete_draft(context).await?,
|
||||
Some(msg) => self.do_set_draft(context, msg).await?,
|
||||
Some(msg) => self.set_draft_raw(context, msg).await?,
|
||||
};
|
||||
|
||||
if changed {
|
||||
@@ -591,6 +590,15 @@ impl ChatId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Similar to as dc_set_draft() but does not emit an event
|
||||
async fn set_draft_raw(self, context: &Context, msg: &mut Message) -> Result<bool> {
|
||||
let deleted = self.maybe_delete_draft(context).await?;
|
||||
let set = self.do_set_draft(context, msg).await.is_ok();
|
||||
|
||||
// Can't inline. Both functions above must be called, no shortcut!
|
||||
Ok(deleted || set)
|
||||
}
|
||||
|
||||
async fn get_draft_msg_id(self, context: &Context) -> Result<Option<MsgId>> {
|
||||
let msg_id: Option<MsgId> = context
|
||||
.sql
|
||||
@@ -626,8 +634,9 @@ impl ChatId {
|
||||
}
|
||||
|
||||
/// Set provided message as draft message for specified chat.
|
||||
/// Returns true if the draft was added or updated in place.
|
||||
async fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<bool> {
|
||||
///
|
||||
/// Return true on success, false on database error.
|
||||
async fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<()> {
|
||||
match msg.viewtype {
|
||||
Viewtype::Unknown => bail!("Can not set draft of unknown type."),
|
||||
Viewtype::Text => {
|
||||
@@ -650,44 +659,9 @@ impl ChatId {
|
||||
bail!("Can't set a draft: Can't send");
|
||||
}
|
||||
|
||||
// set back draft information to allow identifying the draft later on -
|
||||
// no matter if message object is reused or reloaded from db
|
||||
msg.state = MessageState::OutDraft;
|
||||
msg.chat_id = self;
|
||||
|
||||
// if possible, replace existing draft and keep id
|
||||
if !msg.id.is_special() {
|
||||
if let Some(old_draft) = self.get_draft(context).await? {
|
||||
if old_draft.id == msg.id
|
||||
&& old_draft.chat_id == self
|
||||
&& old_draft.state == MessageState::OutDraft
|
||||
{
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET timestamp=?,type=?,txt=?, param=?,mime_in_reply_to=?
|
||||
WHERE id=?;",
|
||||
paramsv![
|
||||
time(),
|
||||
msg.viewtype,
|
||||
msg.text.as_deref().unwrap_or(""),
|
||||
msg.param.to_string(),
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
msg.id
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insert new draft
|
||||
self.maybe_delete_draft(context).await?;
|
||||
let row_id = context
|
||||
context
|
||||
.sql
|
||||
.insert(
|
||||
.execute(
|
||||
"INSERT INTO msgs (
|
||||
chat_id,
|
||||
from_id,
|
||||
@@ -712,8 +686,7 @@ impl ChatId {
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
msg.id = MsgId::new(row_id.try_into()?);
|
||||
Ok(true)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns number of messages in a chat.
|
||||
@@ -1060,12 +1033,11 @@ impl Chat {
|
||||
|
||||
/// Returns true if user can send messages to this chat.
|
||||
pub async fn can_send(&self, context: &Context) -> Result<bool> {
|
||||
let cannot_send = self.id.is_special()
|
||||
|| self.is_device_talk()
|
||||
|| self.is_contact_request()
|
||||
|| (self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty())
|
||||
|| !self.is_self_in_chat(context).await?;
|
||||
Ok(!cannot_send)
|
||||
Ok(!self.id.is_special()
|
||||
&& !self.is_device_talk()
|
||||
&& !self.is_mailing_list()
|
||||
&& !self.is_contact_request()
|
||||
&& self.is_self_in_chat(context).await?)
|
||||
}
|
||||
|
||||
/// Checks if the user is part of a chat
|
||||
@@ -1210,16 +1182,10 @@ impl Chat {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds missing values to the msg object,
|
||||
/// writes the record to the database and returns its msg_id.
|
||||
///
|
||||
/// If `update_msg_id` is set, that record is reused;
|
||||
/// if `update_msg_id` is None, a new record is created.
|
||||
async fn prepare_msg_raw(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
update_msg_id: Option<MsgId>,
|
||||
timestamp: i64,
|
||||
) -> Result<MsgId> {
|
||||
let mut new_references = "".into();
|
||||
@@ -1381,46 +1347,10 @@ impl Chat {
|
||||
|
||||
// add message to the database
|
||||
|
||||
if let Some(update_msg_id) = update_msg_id {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET rfc724_mid=?, chat_id=?, from_id=?, to_id=?, timestamp=?, type=?,
|
||||
state=?, txt=?, subject=?, param=?,
|
||||
hidden=?, mime_in_reply_to=?, mime_references=?, mime_modified=?,
|
||||
mime_headers=?, location_id=?, ephemeral_timer=?, ephemeral_timestamp=?
|
||||
WHERE id=?;",
|
||||
paramsv![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
DC_CONTACT_ID_SELF,
|
||||
to_id as i32,
|
||||
timestamp,
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
new_references,
|
||||
new_mime_headers.is_some(),
|
||||
new_mime_headers.unwrap_or_default(),
|
||||
location_id as i32,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp,
|
||||
update_msg_id
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
schedule_ephemeral_task(context).await;
|
||||
msg.id = update_msg_id;
|
||||
} else {
|
||||
let raw_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO msgs (
|
||||
let msg_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO msgs (
|
||||
rfc724_mid,
|
||||
chat_id,
|
||||
from_id,
|
||||
@@ -1440,32 +1370,31 @@ impl Chat {
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
DC_CONTACT_ID_SELF,
|
||||
to_id as i32,
|
||||
timestamp,
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
new_references,
|
||||
new_mime_headers.is_some(),
|
||||
new_mime_headers.unwrap_or_default(),
|
||||
location_id as i32,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
msg.id = MsgId::new(u32::try_from(raw_id)?);
|
||||
}
|
||||
paramsv![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
DC_CONTACT_ID_SELF,
|
||||
to_id as i32,
|
||||
timestamp,
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
new_references,
|
||||
new_mime_headers.is_some(),
|
||||
new_mime_headers.unwrap_or_default(),
|
||||
location_id as i32,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
schedule_ephemeral_task(context).await;
|
||||
Ok(msg.id)
|
||||
|
||||
Ok(MsgId::new(u32::try_from(msg_id)?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1772,7 +1701,8 @@ pub async fn prepare_msg(context: &Context, chat_id: ChatId, msg: &mut Message)
|
||||
"Cannot prepare message for special chat"
|
||||
);
|
||||
|
||||
let msg_id = prepare_msg_common(context, chat_id, msg, MessageState::OutPreparing).await?;
|
||||
msg.state = MessageState::OutPreparing;
|
||||
let msg_id = prepare_msg_common(context, chat_id, msg).await?;
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
@@ -1851,35 +1781,24 @@ async fn prepare_msg_common(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
msg: &mut Message,
|
||||
change_state_to: MessageState,
|
||||
) -> Result<MsgId> {
|
||||
msg.id = MsgId::new_unset();
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
chat_id.unarchive(context).await?;
|
||||
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(chat.can_send(context).await?, "cannot send to {}", chat_id);
|
||||
|
||||
// check current MessageState for drafts (to keep msg_id) ...
|
||||
let update_msg_id = if msg.state == MessageState::OutDraft {
|
||||
msg.hidden = false;
|
||||
if !msg.id.is_special() && msg.chat_id == chat_id {
|
||||
Some(msg.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// The OutPreparing state is set by dc_prepare_msg() before it
|
||||
// calls this function and the message is left in the OutPreparing
|
||||
// state. Otherwise we got called by send_msg() and we change the
|
||||
// state to OutPending.
|
||||
if msg.state != MessageState::OutPreparing {
|
||||
msg.state = MessageState::OutPending;
|
||||
}
|
||||
|
||||
// ... then change the MessageState in the message object
|
||||
msg.state = change_state_to;
|
||||
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
chat_id.unarchive(context).await?;
|
||||
msg.id = chat
|
||||
.prepare_msg_raw(
|
||||
context,
|
||||
msg,
|
||||
update_msg_id,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
)
|
||||
.prepare_msg_raw(context, msg, dc_create_smeared_timestamp(context).await)
|
||||
.await?;
|
||||
msg.chat_id = chat_id;
|
||||
|
||||
@@ -1997,7 +1916,7 @@ async fn prepare_send_msg(
|
||||
// the state to OutPending.
|
||||
if msg.state != MessageState::OutPreparing {
|
||||
// automatically prepare normal messages
|
||||
prepare_msg_common(context, chat_id, msg, MessageState::OutPending).await?;
|
||||
prepare_msg_common(context, chat_id, msg).await?;
|
||||
} else {
|
||||
// update message state of separately prepared messages
|
||||
ensure!(
|
||||
@@ -2226,75 +2145,6 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Marks messages preceding outgoing messages as noticed.
|
||||
///
|
||||
/// In a chat, if there is an outgoing message, it can be assumed that all previous
|
||||
/// messages were noticed. So, this function takes a Vec of messages that were
|
||||
/// just received, and for all the outgoing messages, it marks all
|
||||
/// previous messages as noticed.
|
||||
pub(crate) async fn mark_old_messages_as_noticed(
|
||||
context: &Context,
|
||||
mut msgs: Vec<ReceivedMsg>,
|
||||
) -> Result<()> {
|
||||
msgs.retain(|m| m.state.is_outgoing());
|
||||
if msgs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut msgs_by_chat: HashMap<ChatId, ReceivedMsg> = HashMap::new();
|
||||
for msg in msgs {
|
||||
let chat_id = msg.chat_id;
|
||||
if let Some(existing_msg) = msgs_by_chat.get(&chat_id) {
|
||||
if msg.sort_timestamp > existing_msg.sort_timestamp {
|
||||
msgs_by_chat.insert(chat_id, msg);
|
||||
}
|
||||
} else {
|
||||
msgs_by_chat.insert(chat_id, msg);
|
||||
}
|
||||
}
|
||||
|
||||
let changed_chats = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut changed_chats = Vec::new();
|
||||
for (_, msg) in msgs_by_chat {
|
||||
let changed_rows = transaction.execute(
|
||||
"UPDATE msgs
|
||||
SET state=?
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?
|
||||
AND timestamp<=?;",
|
||||
paramsv![
|
||||
MessageState::InNoticed,
|
||||
MessageState::InFresh,
|
||||
msg.chat_id,
|
||||
msg.sort_timestamp
|
||||
],
|
||||
)?;
|
||||
if changed_rows > 0 {
|
||||
changed_chats.push(msg.chat_id);
|
||||
}
|
||||
}
|
||||
Ok(changed_chats)
|
||||
})
|
||||
.await?;
|
||||
|
||||
if !changed_chats.is_empty() {
|
||||
info!(
|
||||
context,
|
||||
"Marking chats as noticed because there are newer outgoing messages: {:?}",
|
||||
changed_chats
|
||||
);
|
||||
}
|
||||
|
||||
for c in changed_chats {
|
||||
context.emit_event(EventType::MsgsNoticed(c));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_chat_media(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
@@ -2815,12 +2665,15 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn is_group_explicitly_left(context: &Context, grpid: &str) -> Result<bool> {
|
||||
pub(crate) async fn is_group_explicitly_left(
|
||||
context: &Context,
|
||||
grpid: impl AsRef<str>,
|
||||
) -> Result<bool> {
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM leftgrps WHERE grpid=?;",
|
||||
paramsv![grpid],
|
||||
paramsv![grpid.as_ref()],
|
||||
)
|
||||
.await?;
|
||||
Ok(exists)
|
||||
@@ -2973,11 +2826,11 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
|
||||
for id in ids {
|
||||
let src_msg_id: MsgId = id;
|
||||
let mut msg = Message::load_from_db(context, src_msg_id).await?;
|
||||
if msg.state == MessageState::OutDraft {
|
||||
bail!("cannot forward drafts.");
|
||||
let msg = Message::load_from_db(context, src_msg_id).await;
|
||||
if msg.is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut msg = msg.unwrap();
|
||||
let original_param = msg.param.clone();
|
||||
|
||||
// we tested a sort of broadcast
|
||||
@@ -3000,10 +2853,9 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
|
||||
let new_msg_id: MsgId;
|
||||
if msg.state == MessageState::OutPreparing {
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
let fresh9 = curr_timestamp;
|
||||
curr_timestamp += 1;
|
||||
new_msg_id = chat.prepare_msg_raw(context, &mut msg, fresh9).await?;
|
||||
let save_param = msg.param.clone();
|
||||
msg.param = original_param;
|
||||
msg.id = src_msg_id;
|
||||
@@ -3020,10 +2872,9 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.param = save_param;
|
||||
} else {
|
||||
msg.state = MessageState::OutPending;
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
let fresh10 = curr_timestamp;
|
||||
curr_timestamp += 1;
|
||||
new_msg_id = chat.prepare_msg_raw(context, &mut msg, fresh10).await?;
|
||||
if let Some(send_job) = job::send_msg_job(context, new_msg_id).await? {
|
||||
job::add(context, send_job).await?;
|
||||
}
|
||||
@@ -3060,13 +2911,13 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> Result<usize> {
|
||||
/// Returns a tuple of `(chatid, is_protected, blocked)`.
|
||||
pub(crate) async fn get_chat_id_by_grpid(
|
||||
context: &Context,
|
||||
grpid: &str,
|
||||
grpid: impl AsRef<str>,
|
||||
) -> Result<Option<(ChatId, bool, Blocked)>> {
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id, blocked, protected FROM chats WHERE grpid=?;",
|
||||
paramsv![grpid],
|
||||
paramsv![grpid.as_ref()],
|
||||
|row| {
|
||||
let chat_id = row.get::<_, ChatId>(0)?;
|
||||
|
||||
@@ -3236,7 +3087,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
pub(crate) async fn add_info_msg_with_cmd(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
text: &str,
|
||||
text: impl AsRef<str>,
|
||||
cmd: SystemMessage,
|
||||
timestamp: i64,
|
||||
) -> Result<MsgId> {
|
||||
@@ -3258,7 +3109,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
timestamp,
|
||||
Viewtype::Text,
|
||||
MessageState::InNoticed,
|
||||
text,
|
||||
text.as_ref().to_string(),
|
||||
rfc724_mid,
|
||||
ephemeral_timer,
|
||||
param.to_string(),
|
||||
@@ -3274,7 +3125,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
pub(crate) async fn add_info_msg(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
text: &str,
|
||||
text: impl AsRef<str>,
|
||||
timestamp: i64,
|
||||
) -> Result<MsgId> {
|
||||
add_info_msg_with_cmd(context, chat_id, text, SystemMessage::Unknown, timestamp).await
|
||||
@@ -3361,86 +3212,6 @@ mod tests {
|
||||
assert_eq!(msg_text, draft_text);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_delete_draft() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hi!".to_string()));
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
assert!(chat_id.get_draft(&t).await?.is_some());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("another".to_string()));
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
assert!(chat_id.get_draft(&t).await?.is_some());
|
||||
|
||||
chat_id.set_draft(&t, None).await?;
|
||||
assert!(chat_id.get_draft(&t).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_forwarding_draft_failing() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = &t.get_self_chat().await.id;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id);
|
||||
|
||||
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
assert!(forward_msgs(&t, &[msg.id], chat_id2).await.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_draft_stable_ids() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = &t.get_self_chat().await.id;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
assert_eq!(msg.id, MsgId::new_unset());
|
||||
assert!(chat_id.get_draft_msg_id(&t).await?.is_none());
|
||||
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
let id_after_1st_set = msg.id;
|
||||
assert_ne!(id_after_1st_set, MsgId::new_unset());
|
||||
assert_eq!(
|
||||
id_after_1st_set,
|
||||
chat_id.get_draft_msg_id(&t).await?.unwrap()
|
||||
);
|
||||
assert_eq!(id_after_1st_set, chat_id.get_draft(&t).await?.unwrap().id);
|
||||
|
||||
msg.set_text(Some("hello2".to_string()));
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
let id_after_2nd_set = msg.id;
|
||||
|
||||
assert_eq!(id_after_2nd_set, id_after_1st_set);
|
||||
assert_eq!(
|
||||
id_after_2nd_set,
|
||||
chat_id.get_draft_msg_id(&t).await?.unwrap()
|
||||
);
|
||||
let test = chat_id.get_draft(&t).await?.unwrap();
|
||||
assert_eq!(id_after_2nd_set, test.id);
|
||||
assert_eq!(id_after_2nd_set, msg.id);
|
||||
assert_eq!(test.text, Some("hello2".to_string()));
|
||||
assert_eq!(test.state, MessageState::OutDraft);
|
||||
|
||||
let id_after_prepare = prepare_msg(&t, *chat_id, &mut msg).await?;
|
||||
assert_eq!(id_after_prepare, id_after_1st_set);
|
||||
let test = Message::load_from_db(&t, id_after_prepare).await?;
|
||||
assert_eq!(test.state, MessageState::OutPreparing);
|
||||
assert!(!test.hidden); // sent draft must no longer be hidden
|
||||
|
||||
let id_after_send = send_msg(&t, *chat_id, &mut msg).await?;
|
||||
assert_eq!(id_after_send, id_after_1st_set);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_add_contact_to_chat_ex_add_self() {
|
||||
// Adding self to a contact should succeed, even though it's pointless.
|
||||
@@ -3454,220 +3225,6 @@ mod tests {
|
||||
assert_eq!(added, false);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_modify_chat_multi_device() -> Result<()> {
|
||||
let a1 = TestContext::new_alice().await;
|
||||
let a2 = TestContext::new_alice().await;
|
||||
a1.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
// create group and sync it to the second device
|
||||
let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?;
|
||||
send_text_msg(&a1, a1_chat_id, "ho!".to_string()).await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?;
|
||||
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
let a2_chat_id = a2_msg.chat_id;
|
||||
let a2_chat = Chat::load_from_db(&a2, a2_chat_id).await?;
|
||||
|
||||
assert!(!a1_msg.is_system_message());
|
||||
assert!(!a2_msg.is_system_message());
|
||||
assert_eq!(a1_chat.grpid, a2_chat.grpid);
|
||||
assert_eq!(a1_chat.name, "foo");
|
||||
assert_eq!(a2_chat.name, "foo");
|
||||
assert_eq!(a1_chat.get_profile_image(&a1).await?, None);
|
||||
assert_eq!(a2_chat.get_profile_image(&a2).await?, None);
|
||||
assert_eq!(get_chat_contacts(&a1, a1_chat_id).await?.len(), 1);
|
||||
assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1);
|
||||
|
||||
// add a member to the group
|
||||
let bob = Contact::create(&a1, "", "bob@example.org").await?;
|
||||
add_contact_to_chat(&a1, a1_chat_id, bob).await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
|
||||
assert!(a1_msg.is_system_message());
|
||||
assert!(a2_msg.is_system_message());
|
||||
assert_eq!(a1_msg.get_info_type(), SystemMessage::MemberAddedToGroup);
|
||||
assert_eq!(a2_msg.get_info_type(), SystemMessage::MemberAddedToGroup);
|
||||
assert_eq!(get_chat_contacts(&a1, a1_chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 2);
|
||||
|
||||
// rename the group
|
||||
set_chat_name(&a1, a1_chat_id, "bar").await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
|
||||
assert!(a1_msg.is_system_message());
|
||||
assert!(a2_msg.is_system_message());
|
||||
assert_eq!(a1_msg.get_info_type(), SystemMessage::GroupNameChanged);
|
||||
assert_eq!(a2_msg.get_info_type(), SystemMessage::GroupNameChanged);
|
||||
assert_eq!(Chat::load_from_db(&a1, a1_chat_id).await?.name, "bar");
|
||||
assert_eq!(Chat::load_from_db(&a2, a2_chat_id).await?.name, "bar");
|
||||
|
||||
// remove member from group
|
||||
remove_contact_from_chat(&a1, a1_chat_id, bob).await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_msg = a2.get_last_msg().await;
|
||||
|
||||
assert!(a1_msg.is_system_message());
|
||||
assert!(a2_msg.is_system_message());
|
||||
assert_eq!(
|
||||
a1_msg.get_info_type(),
|
||||
SystemMessage::MemberRemovedFromGroup
|
||||
);
|
||||
assert_eq!(
|
||||
a2_msg.get_info_type(),
|
||||
SystemMessage::MemberRemovedFromGroup
|
||||
);
|
||||
assert_eq!(get_chat_contacts(&a1, a1_chat_id).await?.len(), 1);
|
||||
assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_modify_chat_disordered() -> Result<()> {
|
||||
// Alice creates a group with Bob, Claire and Daisy and then removes Claire and Daisy
|
||||
// (sleep() is needed as otherwise smeared time from Alice looks to Bob like messages from the future which are all set to "now" then)
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "", "claire@foo.de").await?;
|
||||
let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?;
|
||||
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
let add1 = alice.pop_sent_msg().await;
|
||||
|
||||
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
let add2 = alice.pop_sent_msg().await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?;
|
||||
let add3 = alice.pop_sent_msg().await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
let remove1 = alice.pop_sent_msg().await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
|
||||
let remove2 = alice.pop_sent_msg().await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
|
||||
// Bob receives the add and deletion messages out of order
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&add1).await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
bob.recv_msg(&add3).await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
bob.recv_msg(&add2).await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 4);
|
||||
|
||||
bob.recv_msg(&remove2).await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
bob.recv_msg(&remove1).await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that group updates are robust to lost messages and eventual out of order arrival.
|
||||
#[async_std::test]
|
||||
async fn test_modify_chat_lost() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "", "claire@foo.de").await?;
|
||||
let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?;
|
||||
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?;
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let add = alice.pop_sent_msg().await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
let remove1 = alice.pop_sent_msg().await;
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
|
||||
let remove2 = alice.pop_sent_msg().await;
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
bob.recv_msg(&add).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 4);
|
||||
|
||||
// First removal message is lost.
|
||||
// Nevertheless, two members are removed.
|
||||
bob.recv_msg(&remove2).await;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
|
||||
// Eventually, first removal message arrives.
|
||||
// This has no effect.
|
||||
bob.recv_msg(&remove1).await;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_leave_group() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Create group chat with Bob.
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let bob_contact = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?;
|
||||
|
||||
// Alice sends first message to group.
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
|
||||
// Bob leaves the group.
|
||||
let bob_msg = bob.get_last_msg().await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, DC_CONTACT_ID_SELF).await?;
|
||||
|
||||
let leave_msg = bob.pop_sent_msg().await;
|
||||
alice.recv_msg(&leave_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_add_remove_contact_for_single() {
|
||||
let ctx = TestContext::new_alice().await;
|
||||
@@ -4396,7 +3953,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)
|
||||
dc_receive_imf(&bob, msg.as_bytes(), "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = bob.get_last_msg().await;
|
||||
@@ -4416,7 +3973,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)
|
||||
dc_receive_imf(&alice, msg.as_bytes(), "INBOX", 2, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = alice.get_last_msg().await;
|
||||
@@ -4437,13 +3994,14 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Fri, 23 Apr 2021 10:00:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -4485,13 +4043,14 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2021 19:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -4533,13 +4092,14 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <2@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2021 19:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -4581,12 +4141,13 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <1@example.org>\n\
|
||||
Date: Sun, 22 Mar 2021 19:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -499,7 +499,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -507,6 +507,7 @@ mod tests {
|
||||
\n\
|
||||
hello foo\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -560,7 +561,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg5678@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -568,6 +569,7 @@ mod tests {
|
||||
\n\
|
||||
hello foo\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -64,12 +64,25 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
E2eeEnabled,
|
||||
|
||||
/// Ignore Autocrypt recommendation for message encryption if possible.
|
||||
///
|
||||
/// The only expection is when recommendation is "disable", i.e. encryption is not possible
|
||||
/// because some recipient has no OpenPGP key.
|
||||
#[strum(props(default = "0"))]
|
||||
E2eeForce,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
MdnsEnabled,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
InboxWatch,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
SentboxWatch,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
MvboxWatch,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
MvboxMove,
|
||||
|
||||
|
||||
@@ -221,9 +221,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
if let Some(provider) =
|
||||
provider::get_provider_info(ctx, ¶m_domain, socks5_enabled).await
|
||||
{
|
||||
if let Some(provider) = provider::get_provider_info(¶m_domain, socks5_enabled).await {
|
||||
param.provider = Some(provider);
|
||||
match provider.status {
|
||||
provider::Status::Ok | provider::Status::Preparation => {
|
||||
@@ -443,7 +441,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxMove).await?;
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await?
|
||||
|| ctx.get_config_bool(Config::MvboxMove).await?;
|
||||
|
||||
imap.configure_folders(ctx, create_mvbox).await?;
|
||||
|
||||
|
||||
@@ -311,17 +311,17 @@ impl Contact {
|
||||
/// use `dc_may_be_valid_addr()`.
|
||||
pub async fn lookup_id_by_addr(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
addr: impl AsRef<str>,
|
||||
min_origin: Origin,
|
||||
) -> Result<Option<u32>> {
|
||||
if addr.is_empty() {
|
||||
if addr.as_ref().is_empty() {
|
||||
bail!("lookup_id_by_addr: empty address");
|
||||
}
|
||||
|
||||
let addr_normalized = addr_normalize(addr);
|
||||
let addr_normalized = addr_normalize(addr.as_ref());
|
||||
|
||||
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await? {
|
||||
if addr_cmp(addr_normalized, &addr_self) {
|
||||
if addr_cmp(addr_normalized, addr_self) {
|
||||
return Ok(Some(DC_CONTACT_ID_SELF));
|
||||
}
|
||||
}
|
||||
@@ -383,7 +383,7 @@ impl Contact {
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
if addr_cmp(&addr, &addr_self) {
|
||||
if addr_cmp(&addr, addr_self) {
|
||||
return Ok((DC_CONTACT_ID_SELF, sth_modified));
|
||||
}
|
||||
|
||||
@@ -582,7 +582,7 @@ impl Contact {
|
||||
|
||||
for (name, addr) in split_address_book(addr_book).into_iter() {
|
||||
let (name, addr) = sanitize_name_and_addr(name, addr);
|
||||
let name = normalize_name(&name);
|
||||
let name = normalize_name(name);
|
||||
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
@@ -1210,8 +1210,7 @@ WHERE type=? AND id IN (
|
||||
// also unblock mailinglist
|
||||
// if the contact is a mailinglist address explicitly created to allow unblocking
|
||||
if !new_blocking && contact.origin == Origin::MailinglistAddress {
|
||||
if let Some((chat_id, _, _)) =
|
||||
chat::get_chat_id_by_grpid(context, &contact.addr).await?
|
||||
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await?
|
||||
{
|
||||
chat_id.unblock(context).await?;
|
||||
}
|
||||
@@ -1327,8 +1326,8 @@ pub(crate) async fn update_last_seen(
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: &str) -> String {
|
||||
let full_name = full_name.trim();
|
||||
pub fn normalize_name(full_name: impl AsRef<str>) -> String {
|
||||
let full_name = full_name.as_ref().trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
@@ -1372,16 +1371,16 @@ impl Context {
|
||||
/// determine whether the specified addr maps to the/a self addr
|
||||
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
if let Some(self_addr) = self.get_config(Config::ConfiguredAddr).await? {
|
||||
Ok(addr_cmp(&self_addr, addr))
|
||||
Ok(addr_cmp(self_addr, addr))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1).to_lowercase();
|
||||
let norm2 = addr_normalize(addr2).to_lowercase();
|
||||
pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
|
||||
let norm1 = addr_normalize(addr1.as_ref()).to_lowercase();
|
||||
let norm2 = addr_normalize(addr2.as_ref()).to_lowercase();
|
||||
|
||||
norm1 == norm2
|
||||
}
|
||||
@@ -1526,9 +1525,9 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(t.is_self_addr("me@me.org").await?, false);
|
||||
|
||||
t.configure_addr("you@you.net").await;
|
||||
let addr = t.configure_alice().await;
|
||||
assert_eq!(t.is_self_addr("me@me.org").await?, false);
|
||||
assert_eq!(t.is_self_addr("you@you.net").await?, true);
|
||||
assert_eq!(t.is_self_addr(&addr).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1891,7 +1890,7 @@ mod tests {
|
||||
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let id = Contact::lookup_id_by_addr(&alice.ctx, "alice@example.org", Origin::Unknown)
|
||||
let id = Contact::lookup_id_by_addr(&alice.ctx, "alice@example.com", Origin::Unknown)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(id, Some(DC_CONTACT_ID_SELF));
|
||||
@@ -1916,7 +1915,7 @@ mod tests {
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_alice = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.create_chat_with_contact("Alice", "alice@example.com")
|
||||
.await;
|
||||
send_text_msg(&bob, chat_alice.id, "Hello".to_string()).await?;
|
||||
let msg = bob.pop_sent_msg().await;
|
||||
@@ -1928,7 +1927,7 @@ mod tests {
|
||||
"End-to-end encryption preferred.
|
||||
Fingerprints:
|
||||
|
||||
alice@example.org:
|
||||
alice@example.com:
|
||||
2E6F A2CB 23B5 32D7 2863
|
||||
4B58 64B0 8F61 A9ED 9443
|
||||
|
||||
@@ -1979,7 +1978,7 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
|
||||
// Bob replies.
|
||||
let chat = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.create_chat_with_contact("Alice", "alice@example.com")
|
||||
.await;
|
||||
|
||||
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
|
||||
@@ -2030,12 +2029,12 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
|
||||
alice1
|
||||
.evtracker
|
||||
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
|
||||
.get_matching(|e| e == EventType::SelfavatarChanged)
|
||||
.await;
|
||||
|
||||
// Bob sends a message so that Alice can encrypt to him.
|
||||
let chat = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.create_chat_with_contact("Alice", "alice@example.com")
|
||||
.await;
|
||||
|
||||
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
|
||||
@@ -2060,7 +2059,7 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
assert!(alice2.get_config(Config::Selfavatar).await?.is_some());
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
|
||||
.get_matching(|e| e == EventType::SelfavatarChanged)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
@@ -2078,14 +2077,14 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
|
||||
let mime = br#"Subject: Hello
|
||||
Message-ID: message@example.net
|
||||
To: Alice <alice@example.org>
|
||||
To: Alice <alice@example.com>
|
||||
From: Bob <bob@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
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, "Inbox", 1, false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
let timestamp = msg.get_timestamp();
|
||||
|
||||
@@ -47,6 +47,7 @@ pub struct InnerContext {
|
||||
/// Blob directory path
|
||||
pub(crate) blobdir: PathBuf,
|
||||
pub(crate) sql: Sql,
|
||||
pub(crate) os_name: Option<String>,
|
||||
pub(crate) bob: Bob,
|
||||
pub(crate) last_smeared_timestamp: RwLock<i64>,
|
||||
pub(crate) running_state: RwLock<RunningState>,
|
||||
@@ -107,7 +108,7 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
|
||||
impl Context {
|
||||
/// Creates new context.
|
||||
pub async fn new(dbfile: PathBuf, id: u32) -> Result<Context> {
|
||||
pub async fn new(os_name: String, dbfile: PathBuf, id: u32) -> Result<Context> {
|
||||
// pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let mut blob_fname = OsString::new();
|
||||
@@ -117,10 +118,11 @@ impl Context {
|
||||
if !blobdir.exists().await {
|
||||
async_std::fs::create_dir_all(&blobdir).await?;
|
||||
}
|
||||
Context::with_blobdir(dbfile, blobdir, id).await
|
||||
Context::with_blobdir(os_name, dbfile, blobdir, id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn with_blobdir(
|
||||
os_name: String,
|
||||
dbfile: PathBuf,
|
||||
blobdir: PathBuf,
|
||||
id: u32,
|
||||
@@ -135,6 +137,7 @@ impl Context {
|
||||
id,
|
||||
blobdir,
|
||||
dbfile,
|
||||
os_name: Some(os_name),
|
||||
running_state: RwLock::new(Default::default()),
|
||||
sql: Sql::new(),
|
||||
bob: Default::default(),
|
||||
@@ -304,6 +307,7 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||
let e2ee_force = self.get_config_int(Config::E2eeForce).await?;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
|
||||
@@ -322,7 +326,9 @@ impl Context {
|
||||
Err(err) => format!("<key failure: {}>", err),
|
||||
};
|
||||
|
||||
let inbox_watch = self.get_config_int(Config::InboxWatch).await?;
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
|
||||
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await?;
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
|
||||
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
|
||||
let folders_configured = self
|
||||
@@ -379,7 +385,9 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert("inbox_watch", inbox_watch.to_string());
|
||||
res.insert("sentbox_watch", sentbox_watch.to_string());
|
||||
res.insert("mvbox_watch", mvbox_watch.to_string());
|
||||
res.insert("mvbox_move", mvbox_move.to_string());
|
||||
res.insert("sentbox_move", sentbox_move.to_string());
|
||||
res.insert("folders_configured", folders_configured.to_string());
|
||||
@@ -387,6 +395,7 @@ impl Context {
|
||||
res.insert("configured_mvbox_folder", configured_mvbox_folder);
|
||||
res.insert("mdns_enabled", mdns_enabled.to_string());
|
||||
res.insert("e2ee_enabled", e2ee_enabled.to_string());
|
||||
res.insert("e2ee_force", e2ee_force.to_string());
|
||||
res.insert(
|
||||
"key_gen_type",
|
||||
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
||||
@@ -646,7 +655,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
std::fs::write(&dbfile, b"123").unwrap();
|
||||
let res = Context::new(dbfile.into(), 1).await;
|
||||
let res = Context::new("FakeOs".into(), dbfile.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -664,7 +673,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let msg = format!(
|
||||
"From: {}\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <{}>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
@@ -674,7 +683,7 @@ mod tests {
|
||||
dc_create_outgoing_rfc724_mid(None, contact.get_addr())
|
||||
);
|
||||
println!("{}", msg);
|
||||
dc_receive_imf(t, msg.as_bytes(), "INBOX", false)
|
||||
dc_receive_imf(t, msg.as_bytes(), "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -797,7 +806,9 @@ mod tests {
|
||||
async fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
Context::new(dbfile.into(), 1).await.unwrap();
|
||||
Context::new("FakeOS".into(), dbfile.into(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
@@ -808,7 +819,7 @@ mod tests {
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
std::fs::write(&blobdir, b"123").unwrap();
|
||||
let res = Context::new(dbfile.into(), 1).await;
|
||||
let res = Context::new("FakeOS".into(), dbfile.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -818,7 +829,9 @@ mod tests {
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new(dbfile.into(), 1).await.unwrap();
|
||||
Context::new("FakeOS".into(), dbfile.into(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
@@ -828,7 +841,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir(dbfile.into(), blobdir, 1).await;
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir, 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -837,7 +850,7 @@ 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.into(), blobdir.into(), 1).await;
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
137
src/dc_tools.rs
137
src/dc_tools.rs
@@ -5,7 +5,6 @@ use core::cmp::{max, min};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::str::from_utf8;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -15,9 +14,6 @@ use async_std::{fs, io};
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use chrono::{Local, TimeZone};
|
||||
use mailparse::dateparse;
|
||||
use mailparse::headers::Headers;
|
||||
use mailparse::MailHeaderMap;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::chat::{add_device_msg, add_device_msg_with_importance};
|
||||
@@ -674,144 +670,13 @@ pub fn remove_subject_prefix(last_subject: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// Types and methods to create hop-info for message-info
|
||||
|
||||
fn extract_address_from_receive_header<'a>(header: &'a str, start: &str) -> Option<&'a str> {
|
||||
let header_len = header.len();
|
||||
header.find(start).and_then(|mut begin| {
|
||||
begin += start.len();
|
||||
let end = header
|
||||
.get(begin..)?
|
||||
.find(|c: char| c.is_whitespace())
|
||||
.unwrap_or(header_len);
|
||||
header.get(begin..begin + end)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn parse_receive_header(header: &str) -> String {
|
||||
let header = header.replace(&['\r', '\n'][..], "");
|
||||
let mut hop_info = String::from("Hop: ");
|
||||
|
||||
if let Some(from) = extract_address_from_receive_header(&header, "from ") {
|
||||
hop_info += &format!("From: {}; ", from.trim());
|
||||
}
|
||||
|
||||
if let Some(by) = extract_address_from_receive_header(&header, "by ") {
|
||||
hop_info += &format!("By: {}; ", by.trim());
|
||||
}
|
||||
|
||||
if let Ok(date) = dateparse(&header) {
|
||||
// In tests, use the UTC timezone so that the test is reproducible
|
||||
#[cfg(test)]
|
||||
let date_obj = chrono::Utc.timestamp(date, 0);
|
||||
#[cfg(not(test))]
|
||||
let date_obj = Local.timestamp(date, 0);
|
||||
|
||||
hop_info += &format!("Date: {}", date_obj.to_rfc2822());
|
||||
};
|
||||
|
||||
hop_info
|
||||
}
|
||||
|
||||
/// parses "receive"-headers
|
||||
pub(crate) fn parse_receive_headers(headers: &Headers) -> String {
|
||||
headers
|
||||
.get_all_headers("Received")
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|header_map_item| from_utf8(header_map_item.get_value_raw()).ok())
|
||||
.map(parse_receive_header)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::{
|
||||
config::Config, dc_receive_imf::dc_receive_imf, message::get_msg_info,
|
||||
test_utils::TestContext,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_receive_headers() {
|
||||
// Test `parse_receive_headers()` with some more-or-less random emails from the test-data
|
||||
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
|
||||
let expected =
|
||||
"Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000\n\
|
||||
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
|
||||
check_parse_receive_headers(raw, expected);
|
||||
|
||||
let raw = include_bytes!("../test-data/message/wrong-html.eml");
|
||||
let expected =
|
||||
"Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 06 Aug 2020 16:40:31 +0000\n\
|
||||
Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 06 Aug 2020 16:40:32 +0000";
|
||||
check_parse_receive_headers(raw, expected);
|
||||
|
||||
let raw = include_bytes!("../test-data/message/posteo_ndn.eml");
|
||||
let expected =
|
||||
"Hop: By: mout01.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\
|
||||
Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\
|
||||
Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
|
||||
Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
|
||||
Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
|
||||
Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 09 Jun 2020 18:44:24 +0000";
|
||||
check_parse_receive_headers(raw, expected);
|
||||
}
|
||||
|
||||
fn check_parse_receive_headers(raw: &[u8], expected: &str) {
|
||||
let mail = mailparse::parse_mail(raw).unwrap();
|
||||
let hop_info = parse_receive_headers(&mail.get_headers());
|
||||
assert_eq!(hop_info, expected)
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_receive_headers_integration() {
|
||||
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
|
||||
let expected = r"State: Fresh
|
||||
|
||||
hi
|
||||
|
||||
Message-ID: 2dfdbde7@example.org
|
||||
|
||||
Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000
|
||||
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
|
||||
check_parse_receive_headers_integration(raw, expected).await;
|
||||
|
||||
let raw = include_bytes!("../test-data/message/encrypted_with_received_headers.eml");
|
||||
let expected = "State: Fresh, Encrypted
|
||||
|
||||
Re: Message from alice@example.org
|
||||
|
||||
hi back\r\n\
|
||||
\r\n\
|
||||
-- \r\n\
|
||||
Sent with my Delta Chat Messenger: https://delta.chat
|
||||
|
||||
Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net
|
||||
|
||||
Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000
|
||||
Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
|
||||
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000";
|
||||
check_parse_receive_headers_integration(raw, expected).await;
|
||||
}
|
||||
|
||||
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();
|
||||
let msg = t.get_last_msg().await;
|
||||
let msg_info = get_msg_info(&t, msg.id).await.unwrap();
|
||||
|
||||
// Ignore the first rows of the msg_info because they contain a
|
||||
// received time that depends on the test time which makes it impossible to
|
||||
// compare with a static string
|
||||
let capped_result = &msg_info[msg_info.find("State").unwrap()..];
|
||||
assert_eq!(expected, capped_result);
|
||||
}
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_rust_ftoa() {
|
||||
|
||||
@@ -36,9 +36,9 @@ impl Dehtml {
|
||||
""
|
||||
}
|
||||
}
|
||||
fn append_prefix(&self, line_end: &str) -> String {
|
||||
fn append_prefix(&self, line_end: impl AsRef<str>) -> String {
|
||||
// line_end is e.g. "\n\n". We add "> " if necessary.
|
||||
line_end.to_string() + self.line_prefix()
|
||||
line_end.as_ref().to_owned() + self.line_prefix()
|
||||
}
|
||||
fn get_add_text(&self) -> AddText {
|
||||
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
|
||||
|
||||
@@ -129,48 +129,24 @@ impl Job {
|
||||
}
|
||||
|
||||
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 target!=''",
|
||||
paramsv![msg.rfc724_mid],
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
Ok((server_uid, server_folder))
|
||||
}
|
||||
)
|
||||
.await
|
||||
);
|
||||
|
||||
if let Some((server_uid, server_folder)) = row {
|
||||
match imap
|
||||
.fetch_single_msg(context, &server_folder, server_uid)
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
ImapActionResult::Success => {
|
||||
// update_download_state() not needed as receive_imf() already
|
||||
// set the state and emitted the event.
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
let server_folder = msg.server_folder.unwrap_or_default();
|
||||
match imap
|
||||
.fetch_single_msg(context, &server_folder, msg.server_uid)
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
ImapActionResult::Success | ImapActionResult::AlreadyDone => {
|
||||
// update_download_state() not needed as receive_imf() already
|
||||
// set the state and emitted the event.
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
} else {
|
||||
// No IMAP record found, we don't know the UID and folder.
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,14 +172,14 @@ impl Imap {
|
||||
// we are connected, and the folder is selected
|
||||
info!(context, "Downloading message {}/{} fully...", folder, uid);
|
||||
|
||||
let (last_uid, _received) = self
|
||||
let (_, error_cnt) = self
|
||||
.fetch_many_msgs(context, folder, vec![uid], false, false)
|
||||
.await;
|
||||
if last_uid.is_none() {
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
ImapActionResult::Success
|
||||
if error_cnt > 0 {
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
|
||||
ImapActionResult::Success
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +309,16 @@ mod tests {
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\
|
||||
Content-Type: text/plain";
|
||||
|
||||
dc_receive_imf_inner(&t, header.as_bytes(), "INBOX", false, Some(100000), false).await?;
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
header.as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
@@ -346,6 +331,7 @@ mod tests {
|
||||
&t,
|
||||
format!("{}\n\n100k text...", header).as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
@@ -374,13 +360,14 @@ mod tests {
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
b"From: Bob <bob@example.org>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
To: Alice <alice@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
|
||||
173
src/e2ee.rs
173
src/e2ee.rs
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{bail, format_err, Result};
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use mailparse::ParsedMail;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::pgp;
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptHelper {
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
force_preference: bool,
|
||||
pub addr: String,
|
||||
pub public_key: SignedPublicKey,
|
||||
}
|
||||
@@ -28,6 +29,7 @@ impl EncryptHelper {
|
||||
let prefer_encrypt =
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
|
||||
.unwrap_or_default();
|
||||
let force_preference = context.get_config_bool(Config::E2eeForce).await?;
|
||||
let addr = match context.get_config(Config::ConfiguredAddr).await? {
|
||||
None => {
|
||||
bail!("addr not configured!");
|
||||
@@ -39,6 +41,7 @@ impl EncryptHelper {
|
||||
|
||||
Ok(EncryptHelper {
|
||||
prefer_encrypt,
|
||||
force_preference,
|
||||
addr,
|
||||
public_key,
|
||||
})
|
||||
@@ -100,11 +103,17 @@ impl EncryptHelper {
|
||||
}
|
||||
}
|
||||
|
||||
// Count number of recipients, including self.
|
||||
// This does not depend on whether we send a copy to self or not.
|
||||
let recipients_count = peerstates.len() + 1;
|
||||
let want_encrypt = if self.force_preference {
|
||||
// Ignore preferences of others.
|
||||
self.prefer_encrypt == EncryptPreference::Mutual
|
||||
} else {
|
||||
// Count number of recipients, including self.
|
||||
// This does not depend on whether we send a copy to self or not.
|
||||
let recipients_count = peerstates.len() + 1;
|
||||
2 * prefer_encrypt_count > recipients_count
|
||||
};
|
||||
|
||||
Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
|
||||
Ok(e2ee_guaranteed || want_encrypt)
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
@@ -179,6 +188,7 @@ pub async fn try_decrypt(
|
||||
// Possibly perform decryption
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
let mut signatures = HashSet::default();
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate
|
||||
@@ -191,17 +201,14 @@ pub async fn try_decrypt(
|
||||
}
|
||||
}
|
||||
|
||||
let (out_mail, signatures) = match decrypt_if_autocrypt_message(
|
||||
let out_mail = decrypt_if_autocrypt_message(
|
||||
context,
|
||||
mail,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
&mut signatures,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some((out_mail, signatures)) => (Some(out_mail), signatures),
|
||||
None => (None, Default::default()),
|
||||
};
|
||||
.await?;
|
||||
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// If message is not encrypted and it is not a read receipt, degrade encryption.
|
||||
@@ -277,7 +284,8 @@ async fn decrypt_if_autocrypt_message(
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail).or_else(|| get_mixed_up_mime(mail)) {
|
||||
None => {
|
||||
// not an autocrypt mime message, abort and ignore
|
||||
@@ -291,60 +299,36 @@ async fn decrypt_if_autocrypt_message(
|
||||
encrypted_data_part,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
ret_valid_signatures,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
|
||||
///
|
||||
/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key
|
||||
/// fingerprints for which there is a valid signature.
|
||||
async fn validate_detached_signature(
|
||||
mail: &ParsedMail<'_>,
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
if mail.ctype.mimetype != "multipart/signed" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
// First part is the content, second part is the signature.
|
||||
let content = first_part.raw_bytes;
|
||||
let signature = second_part.get_body_raw()?;
|
||||
let ret_valid_signatures =
|
||||
pgp::pk_validate(content, &signature, public_keyring_for_validate).await?;
|
||||
|
||||
Ok(Some((content.to_vec(), ret_valid_signatures)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Ok(None) if nothing encrypted was found.
|
||||
async fn decrypt_part(
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let data = mail.get_body_raw()?;
|
||||
|
||||
if has_decrypted_pgp_armor(&data) {
|
||||
let (plain, ret_valid_signatures) =
|
||||
pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate).await?;
|
||||
// we should only have one decryption happening
|
||||
ensure!(ret_valid_signatures.is_empty(), "corrupt signatures");
|
||||
|
||||
// Check for detached signatures.
|
||||
// If decrypted part is a multipart/signed, then there is a detached signature.
|
||||
let decrypted_part = mailparse::parse_mail(&plain)?;
|
||||
if let Some((content, valid_detached_signatures)) =
|
||||
validate_detached_signature(&decrypted_part, &public_keyring_for_validate).await?
|
||||
{
|
||||
return Ok(Some((content, valid_detached_signatures)));
|
||||
} else {
|
||||
// If the message was wrongly or not signed, still return the plain text.
|
||||
// The caller has to check the signatures then.
|
||||
let plain = pgp::pk_decrypt(
|
||||
data,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
Some(ret_valid_signatures),
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(Some((plain, ret_valid_signatures)));
|
||||
}
|
||||
// If the message was wrongly or not signed, still return the plain text.
|
||||
// The caller has to check the signatures then.
|
||||
|
||||
return Ok(Some(plain));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -406,6 +390,7 @@ mod tests {
|
||||
|
||||
use crate::chat;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::Message;
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::ToSave;
|
||||
@@ -416,11 +401,9 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_prexisting() {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
ensure_secret_key_exists(&t).await.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
let t = TestContext::new().await;
|
||||
let test_addr = t.configure_alice().await;
|
||||
assert_eq!(ensure_secret_key_exists(&t).await.unwrap(), test_addr);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -491,7 +474,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
assert!(!msg.was_encrypted());
|
||||
|
||||
// Parsing a message is enough to update peerstate
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
@@ -522,28 +505,28 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message with Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message without Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
@@ -551,7 +534,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset);
|
||||
@@ -629,4 +612,68 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_e2ee_force() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
alice.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
bob.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
|
||||
// Alice does not prefer encryption.
|
||||
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
bob.set_config(Config::E2eeEnabled, Some("1")).await?;
|
||||
|
||||
// Alice sends her key to Bob.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
assert!(!received_msg.get_showpadlock());
|
||||
|
||||
// Bob should not encrypt, because Alice does not prefer encryption.
|
||||
let sent_msg = bob
|
||||
.send_text(bob_chat.id, "This should not be encrypted")
|
||||
.await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
let received_msg = alice.get_last_msg().await;
|
||||
assert!(!received_msg.get_showpadlock());
|
||||
|
||||
// Bob ignores Alice's preference for no encryption.
|
||||
bob.set_config(Config::E2eeForce, Some("1")).await?;
|
||||
let sent_msg = bob.send_text(bob_chat.id, "This should be encrypted").await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
let received_msg = alice.get_last_msg().await;
|
||||
assert!(received_msg.get_showpadlock());
|
||||
|
||||
// Alice switches to MUA without Autocrypt support.
|
||||
dc_receive_imf(
|
||||
&bob,
|
||||
br#"Subject: Hello from MUA
|
||||
Message-ID: foobar@example.com
|
||||
To: Bob <bob@example.net>
|
||||
From: Alice <alice@example.com>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Date: Sun, 14 Mar 2500 00:00:00 +0000
|
||||
|
||||
Hello from MUA."#,
|
||||
"INBOX",
|
||||
100,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Bob can't encrypt now because Alice has no key.
|
||||
let sent_msg = bob
|
||||
.send_text(bob_chat.id, "This should not be encrypted again")
|
||||
.await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
let received_msg = alice.get_last_msg().await;
|
||||
assert!(!received_msg.get_showpadlock());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
186
src/ephemeral.rs
186
src/ephemeral.rs
@@ -52,7 +52,7 @@
|
||||
//! `MsgsChanged` event is emitted when a message deletion is due, to
|
||||
//! make UI reload displayed messages and cause actual deletion.
|
||||
//!
|
||||
//! Server deletion happens by updating the `imap` table based on
|
||||
//! Server deletion happens by generating IMAP deletion jobs based on
|
||||
//! the database entries which are expired either according to their
|
||||
//! ephemeral message timers or global `delete_server_after` setting.
|
||||
|
||||
@@ -73,6 +73,7 @@ use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::job;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::stock_str;
|
||||
@@ -262,7 +263,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
|
||||
impl MsgId {
|
||||
/// Returns ephemeral message timer value for the message.
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value(
|
||||
@@ -278,7 +279,7 @@ impl MsgId {
|
||||
}
|
||||
|
||||
/// Starts ephemeral message timer for the message if it is not started yet.
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> Result<()> {
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
|
||||
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
|
||||
let ephemeral_timestamp = time().saturating_add(duration.into());
|
||||
|
||||
@@ -433,8 +434,11 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules expired IMAP messages for deletion.
|
||||
pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> {
|
||||
/// Returns ID of any expired message that should be deleted from the server.
|
||||
///
|
||||
/// It looks up the trash chat too, to find messages that are already
|
||||
/// deleted locally, but not deleted on the server.
|
||||
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Result<Option<MsgId>> {
|
||||
let now = time();
|
||||
|
||||
let (threshold_timestamp, threshold_timestamp_extended) =
|
||||
@@ -448,21 +452,27 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE EXISTS (
|
||||
SELECT * FROM msgs
|
||||
WHERE rfc724_mid=imap.rfc724_mid
|
||||
AND ((download_state = 0 AND timestamp < ?) OR
|
||||
(download_state != 0 AND timestamp < ?) OR
|
||||
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
|
||||
)",
|
||||
paramsv![threshold_timestamp, threshold_timestamp_extended, now],
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs \
|
||||
WHERE ( \
|
||||
((download_state = 0 AND timestamp < ?) OR (download_state != 0 AND timestamp < ?)) \
|
||||
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
|
||||
) \
|
||||
AND server_uid != 0 \
|
||||
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?)
|
||||
LIMIT 1",
|
||||
paramsv![
|
||||
threshold_timestamp,
|
||||
threshold_timestamp_extended,
|
||||
now,
|
||||
job::Action::DeleteMsgOnImap
|
||||
],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start ephemeral timers for seen messages if they are not started
|
||||
@@ -497,6 +507,9 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::param::Params;
|
||||
use async_std::task::sleep;
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
@@ -712,7 +725,7 @@ mod tests {
|
||||
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
|
||||
/// timer does not result in disabling the timer on the Bob's side.
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_timer_rollback() -> Result<()> {
|
||||
async fn test_ephemeral_timer_rollback() -> anyhow::Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
@@ -786,14 +799,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||
async fn test_ephemeral_delete_msgs() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.get_self_chat().await;
|
||||
|
||||
t.send_text(chat.id, "Saved message, which we delete manually")
|
||||
.await;
|
||||
let msg = t.get_last_msg_in(chat.id).await;
|
||||
msg.id.delete_from_db(&t).await?;
|
||||
msg.id.delete_from_db(&t).await.unwrap();
|
||||
check_msg_was_deleted(&t, &chat, msg.id).await;
|
||||
|
||||
chat.id
|
||||
@@ -804,12 +817,36 @@ mod tests {
|
||||
.send_text(chat.id, "Saved message, disappearing after 1s")
|
||||
.await;
|
||||
|
||||
async_std::task::sleep(Duration::from_millis(1100)).await;
|
||||
sleep(Duration::from_millis(1100)).await;
|
||||
|
||||
// Check that the msg was deleted locally.
|
||||
// Check checks that the msg was deleted locally
|
||||
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
|
||||
|
||||
Ok(())
|
||||
// Check that the msg will be deleted on the server
|
||||
// First of all, set a server_uid so that DC thinks that it's actually possible to delete
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET server_uid=1 WHERE id=?",
|
||||
paramsv![msg.sender_msg_id],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let job = job::load_imap_deletion_job(&t).await.unwrap();
|
||||
assert_eq!(
|
||||
job,
|
||||
Some(job::Job::new(
|
||||
job::Action::DeleteMsgOnImap,
|
||||
msg.sender_msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
))
|
||||
);
|
||||
// Let's assume that executing the job fails on first try and the job is saved to the db
|
||||
job.unwrap().save(&t).await.unwrap();
|
||||
|
||||
// Make sure that we don't get yet another job when loading from db
|
||||
let job2 = job::load_imap_deletion_job(&t).await.unwrap();
|
||||
assert_eq!(job2, None);
|
||||
}
|
||||
|
||||
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
@@ -837,7 +874,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
async fn test_load_imap_deletion_msgid() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
@@ -850,98 +887,42 @@ mod tests {
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
] {
|
||||
let message_id = id.to_string();
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
paramsv![id, message_id, timestamp, ephemeral_timestamp],
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||
paramsv![message_id, id],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||
assert_eq!(
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
paramsv![id.to_string()],
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE rfc724_mid=?",
|
||||
paramsv![id.to_string()],
|
||||
"INSERT INTO msgs (id, server_uid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
paramsv![id, id, timestamp, ephemeral_timestamp],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This should mark message 2000 for deletion.
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 2000).await?;
|
||||
remove_uid(&t, 2000).await?;
|
||||
// No other messages are marked for deletion.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", paramsv![],)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(2000)));
|
||||
|
||||
MsgId::new(2000).delete_from_db(&t).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1000)));
|
||||
|
||||
MsgId::new(1000)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target=folder WHERE rfc724_mid='1000'",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
||||
remove_uid(&t, 1000).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1000))); // delete downloadable anyway
|
||||
|
||||
MsgId::new(1000).delete_from_db(&t).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1010).await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target=folder WHERE rfc724_mid='1010'",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1010)));
|
||||
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
// Keep downloadable for now.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", paramsv![],)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None); // keep downloadable for now
|
||||
|
||||
MsgId::new(1010).delete_from_db(&t).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -955,7 +936,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
To: Alice <alice@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <first@example.com>\n\
|
||||
@@ -963,6 +944,7 @@ mod tests {
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -975,7 +957,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
To: Alice <alice@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <second@example.com>\n\
|
||||
@@ -984,6 +966,7 @@ mod tests {
|
||||
\n\
|
||||
second message\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -1011,7 +994,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
To: Alice <alice@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <third@example.com>\n\
|
||||
@@ -1021,6 +1004,7 @@ mod tests {
|
||||
\n\
|
||||
> hello\n",
|
||||
"INBOX",
|
||||
3,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -33,7 +33,6 @@ pub enum HeaderDef {
|
||||
XMozillaDraftInfo,
|
||||
|
||||
ListId,
|
||||
ListPost,
|
||||
References,
|
||||
InReplyTo,
|
||||
Precedence,
|
||||
|
||||
11
src/html.rs
11
src/html.rs
@@ -440,7 +440,9 @@ test some special html-characters as < > and & but also " 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, "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::No);
|
||||
@@ -466,7 +468,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
// bob: check that bob also got the html-part of the forwarded message
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.com").await;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg_in(chat.get_id()).await;
|
||||
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
|
||||
@@ -489,7 +491,9 @@ test some special html-characters as < > and & but also " 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, "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
|
||||
// forward the message to saved-messages,
|
||||
@@ -556,6 +560,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
&t,
|
||||
include_bytes!("../test-data/message/cp1252-html.eml"),
|
||||
"INBOX",
|
||||
0,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
1060
src/imap.rs
1060
src/imap.rs
File diff suppressed because it is too large
Load Diff
@@ -150,7 +150,7 @@ impl Imap {
|
||||
}
|
||||
if self.config.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break InterruptInfo::new(false);
|
||||
break InterruptInfo::new(false, None);
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
@@ -162,7 +162,7 @@ impl Imap {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
if res {
|
||||
break InterruptInfo::new(false);
|
||||
break InterruptInfo::new(false, None);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ use async_std::prelude::*;
|
||||
use super::{get_folder_meaning, get_folder_meaning_by_name};
|
||||
|
||||
impl Imap {
|
||||
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<()> {
|
||||
pub async fn scan_folders(&mut self, context: &Context) -> Result<()> {
|
||||
// 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 {
|
||||
@@ -29,7 +29,7 @@ impl Imap {
|
||||
let session = self.session.as_mut();
|
||||
let session = session.context("scan_folders(): IMAP No Connection established")?;
|
||||
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
|
||||
let watched_folders = get_watched_folders(context).await?;
|
||||
let watched_folders = get_watched_folders(context).await;
|
||||
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
|
||||
@@ -74,7 +74,7 @@ impl Imap {
|
||||
self.server_sent_unsolicited_exists(context);
|
||||
|
||||
loop {
|
||||
self.fetch_move_delete(context, folder.name())
|
||||
self.fetch_new_messages(context, folder.name(), false)
|
||||
.await
|
||||
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
|
||||
|
||||
@@ -102,21 +102,19 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
|
||||
pub(crate) async fn get_watched_folders(context: &Context) -> Vec<String> {
|
||||
let mut res = Vec::new();
|
||||
if let Some(inbox_folder) = context.get_config(Config::ConfiguredInboxFolder).await? {
|
||||
res.push(inbox_folder);
|
||||
}
|
||||
let folder_watched_configured = &[
|
||||
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),
|
||||
(Config::MvboxMove, Config::ConfiguredMvboxFolder),
|
||||
(Config::MvboxWatch, Config::ConfiguredMvboxFolder),
|
||||
(Config::InboxWatch, Config::ConfiguredInboxFolder),
|
||||
];
|
||||
for (watched, configured) in folder_watched_configured {
|
||||
if context.get_config_bool(*watched).await? {
|
||||
if let Some(folder) = context.get_config(*configured).await? {
|
||||
if context.get_config_bool(*watched).await.unwrap_or_default() {
|
||||
if let Ok(Some(folder)) = context.get_config(*configured).await {
|
||||
res.push(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
res
|
||||
}
|
||||
|
||||
15
src/imex.rs
15
src/imex.rs
@@ -238,7 +238,7 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
msg.param
|
||||
.set(Param::MimeType, "application/autocrypt-setup");
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
|
||||
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
@@ -936,7 +936,9 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_render_setup_file() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = TestContext::new().await;
|
||||
|
||||
t.configure_alice().await;
|
||||
let msg = render_setup_file(&t, "hello").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
// Check some substrings, indicating things got substituted.
|
||||
@@ -953,10 +955,11 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_render_setup_file_newline_replace() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = TestContext::new().await;
|
||||
t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
t.configure_alice().await;
|
||||
let msg = render_setup_file(&t, "pw").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
assert!(msg.contains("<p>hello<br>there</p>"));
|
||||
@@ -1009,13 +1012,15 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_export_and_import_key() {
|
||||
let context = TestContext::new_alice().await;
|
||||
let context = TestContext::new().await;
|
||||
context.configure_alice().await;
|
||||
let blobdir = context.ctx.get_blobdir();
|
||||
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await {
|
||||
panic!("got error on export: {:?}", err);
|
||||
}
|
||||
|
||||
let context2 = TestContext::new_alice().await;
|
||||
let context2 = TestContext::new().await;
|
||||
context2.configure_alice().await;
|
||||
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir).await {
|
||||
panic!("got error on import: {:?}", err);
|
||||
}
|
||||
|
||||
302
src/job.rs
302
src/job.rs
@@ -16,6 +16,7 @@ use crate::config::Config;
|
||||
use crate::contact::{normalize_name, Contact, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
|
||||
use crate::ephemeral::load_imap_deletion_msgid;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::{Imap, ImapActionResult};
|
||||
use crate::location;
|
||||
@@ -95,6 +96,11 @@ pub enum Action {
|
||||
// this is user initiated so it should have a fairly high priority
|
||||
UpdateRecentQuota = 140,
|
||||
|
||||
// Moving message is prioritized lower than deletion so we don't
|
||||
// bother moving message if it is already scheduled for deletion.
|
||||
MoveMsg = 200,
|
||||
DeleteMsgOnImap = 210,
|
||||
|
||||
// This job will download partially downloaded messages completely
|
||||
// and is added when download_full() is called.
|
||||
// Most messages are downloaded automatically on fetch
|
||||
@@ -127,8 +133,10 @@ impl From<Action> for Thread {
|
||||
|
||||
Housekeeping => Thread::Imap,
|
||||
FetchExistingMsgs => Thread::Imap,
|
||||
DeleteMsgOnImap => Thread::Imap,
|
||||
ResyncFolders => Thread::Imap,
|
||||
MarkseenMsgOnImap => Thread::Imap,
|
||||
MoveMsg => Thread::Imap,
|
||||
UpdateRecentQuota => Thread::Imap,
|
||||
DownloadMsg => Thread::Imap,
|
||||
|
||||
@@ -545,6 +553,149 @@ impl Job {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn move_msg(&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 server_folder = &job_try!(msg
|
||||
.server_folder
|
||||
.context("Can't move message out of folder if we don't know the current folder"));
|
||||
|
||||
let move_res = msg.id.needs_move(context, server_folder).await;
|
||||
let dest_folder = match move_res {
|
||||
Err(e) => {
|
||||
warn!(context, "could not load dest folder: {}", e);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!(
|
||||
context,
|
||||
"msg {} does not need to be moved from {}", msg.id, server_folder
|
||||
);
|
||||
return Status::Finished(Ok(()));
|
||||
}
|
||||
Ok(Some(config)) => match context.get_config(config).await {
|
||||
Ok(folder) => folder,
|
||||
Err(err) => {
|
||||
warn!(context, "failed to load config: {}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(dest_folder) = dest_folder {
|
||||
match imap
|
||||
.mv(context, server_folder, msg.server_uid, &dest_folder)
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::Success => {
|
||||
// Rust-Imap provides no target uid on mv, so just set it to 0, update again when precheck_imf() is called for the moved message
|
||||
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0).await;
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
ImapActionResult::Failed => {
|
||||
Status::Finished(Err(format_err!("IMAP action failed")))
|
||||
}
|
||||
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
|
||||
}
|
||||
} else {
|
||||
Status::Finished(Err(format_err!("No mvbox folder configured")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a message on the server.
|
||||
///
|
||||
/// `foreign_id` is a MsgId.
|
||||
///
|
||||
/// If the message is in the trash chat or hidden, this job
|
||||
/// removes database record, otherwise it only clears the
|
||||
/// `server_uid` column. If there are no more records pointing to
|
||||
/// the same message on the server, the job actually removes the
|
||||
/// message on the server.
|
||||
async fn delete_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);
|
||||
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
let cnt = message::rfc724_mid_cnt(context, &msg.rfc724_mid).await;
|
||||
info!(
|
||||
context,
|
||||
"Running delete job for message {} which has {} entries in the database",
|
||||
&msg.rfc724_mid,
|
||||
cnt
|
||||
);
|
||||
if cnt > 1 {
|
||||
info!(
|
||||
context,
|
||||
"The message is deleted from the server when all parts are deleted.",
|
||||
);
|
||||
} else if cnt == 0 {
|
||||
warn!(
|
||||
context,
|
||||
"The message {} has no UID on the server to delete", &msg.rfc724_mid
|
||||
);
|
||||
} else {
|
||||
/* if this is the last existing part of the message,
|
||||
we delete the message from the server */
|
||||
let mid = msg.rfc724_mid;
|
||||
let server_folder = msg.server_folder.as_ref().unwrap();
|
||||
let res = if msg.server_uid == 0 {
|
||||
// Message is already deleted on IMAP server.
|
||||
ImapActionResult::AlreadyDone
|
||||
} else {
|
||||
imap.delete_msg(context, &mid, server_folder, msg.server_uid)
|
||||
.await
|
||||
};
|
||||
match res {
|
||||
ImapActionResult::AlreadyDone | ImapActionResult::Success => {}
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
// If job has failed, for example due to some
|
||||
// IMAP bug, we postpone it instead of failing
|
||||
// immediately. This will prevent adding it
|
||||
// immediately again if user has enabled
|
||||
// automatic message deletion. Without this,
|
||||
// we might waste a lot of traffic constantly
|
||||
// retrying message deletion.
|
||||
return Status::RetryLater;
|
||||
}
|
||||
}
|
||||
}
|
||||
if msg.chat_id.is_trash() || msg.hidden {
|
||||
// Messages are stored in trash chat only to keep
|
||||
// their server UID and Message-ID. Once message is
|
||||
// deleted from the server, database record can be
|
||||
// removed as well.
|
||||
//
|
||||
// Hidden messages are similar to trashed, but are
|
||||
// related to some chat. We also delete their
|
||||
// database records.
|
||||
job_try!(msg.id.delete_from_db(context).await)
|
||||
} else {
|
||||
// Remove server UID from the database record.
|
||||
//
|
||||
// We have either just removed the message from the
|
||||
// server, in which case UID is not valid anymore, or
|
||||
// we have more refernces to the same server UID, so
|
||||
// we remove UID to reduce the number of messages
|
||||
// pointing to the corresponding UID. Once the counter
|
||||
// reaches zero, we will remove the message.
|
||||
job_try!(msg.id.unlink(context).await);
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
} else {
|
||||
/* eg. device messages have no Message-ID */
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the recipients from old emails sent by the user and add them as contacts.
|
||||
/// This way, we can already offer them some email addresses they can write to.
|
||||
///
|
||||
@@ -623,58 +774,55 @@ impl Job {
|
||||
}
|
||||
|
||||
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 => {}
|
||||
}
|
||||
|
||||
let folder = msg.server_folder.as_ref().unwrap();
|
||||
|
||||
let result = if msg.server_uid == 0 {
|
||||
// The message is moved or deleted by us.
|
||||
//
|
||||
// Do not call set_seen with zero UID, as it will return
|
||||
// ImapActionResult::RetryLater, but we do not want to
|
||||
// retry. If the message was moved, we will create another
|
||||
// job to mark the message as seen later. If it was
|
||||
// deleted, there is nothing to do.
|
||||
info!(context, "Can't mark message as seen: No UID");
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Can't mark the message {} as seen on IMAP because there is no known UID",
|
||||
msg.rfc724_mid
|
||||
);
|
||||
}
|
||||
imap.set_seen(context, folder, msg.server_uid).await
|
||||
};
|
||||
|
||||
// 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));
|
||||
match result {
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
|
||||
ImapActionResult::Success | ImapActionResult::Failed => {
|
||||
// XXX the message might just have been moved
|
||||
// we want to send out an MDN anyway
|
||||
// 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(()))
|
||||
}
|
||||
}
|
||||
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,7 +888,7 @@ async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, fold
|
||||
let display_name_normalized = contact
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(|s| normalize_name(s))
|
||||
.map(normalize_name)
|
||||
.unwrap_or_default();
|
||||
|
||||
match Contact::add_or_lookup(
|
||||
@@ -898,6 +1046,13 @@ pub(crate) enum Connection<'a> {
|
||||
Smtp(&'a mut Smtp),
|
||||
}
|
||||
|
||||
pub(crate) async fn load_imap_deletion_job(context: &Context) -> Result<Option<Job>> {
|
||||
let res = load_imap_deletion_msgid(context)
|
||||
.await?
|
||||
.map(|msg_id| Job::new(Action::DeleteMsgOnImap, msg_id.to_u32(), Params::new(), 0));
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for Connection<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
@@ -1006,8 +1161,10 @@ async fn perform_job_action(
|
||||
Action::MaybeSendLocationsEnded => {
|
||||
location::job_maybe_send_locations_ended(context, job).await
|
||||
}
|
||||
Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
|
||||
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
|
||||
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
|
||||
Action::Housekeeping => {
|
||||
sql::housekeeping(context).await.ok_or_log(context);
|
||||
@@ -1084,20 +1241,26 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
match action {
|
||||
Action::Unknown => unreachable!(),
|
||||
Action::Housekeeping
|
||||
| Action::DeleteMsgOnImap
|
||||
| Action::ResyncFolders
|
||||
| Action::MarkseenMsgOnImap
|
||||
| Action::FetchExistingMsgs
|
||||
| Action::MoveMsg
|
||||
| Action::UpdateRecentQuota
|
||||
| Action::DownloadMsg => {
|
||||
info!(context, "interrupt: imap");
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
context
|
||||
.interrupt_inbox(InterruptInfo::new(false, None))
|
||||
.await;
|
||||
}
|
||||
Action::MaybeSendLocations
|
||||
| Action::MaybeSendLocationsEnded
|
||||
| Action::SendMdn
|
||||
| Action::SendMsgToSmtp => {
|
||||
info!(context, "interrupt: smtp");
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
context
|
||||
.interrupt_smtp(InterruptInfo::new(false, None))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1132,9 +1295,20 @@ pub(crate) async fn load_next(
|
||||
let query;
|
||||
let params;
|
||||
let t = time();
|
||||
let m;
|
||||
let thread_i = thread as i64;
|
||||
|
||||
if !info.probe_network {
|
||||
if let Some(msg_id) = info.msg_id {
|
||||
query = r#"
|
||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||
FROM jobs
|
||||
WHERE thread=? AND foreign_id=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
m = msg_id;
|
||||
params = paramsv![thread_i, m];
|
||||
} else if !info.probe_network {
|
||||
// processing for first-try and after backoff-timeouts:
|
||||
// process jobs in the order they were added.
|
||||
query = r#"
|
||||
@@ -1204,6 +1378,12 @@ LIMIT 1;
|
||||
}
|
||||
Thread::Imap => {
|
||||
if let Some(job) = job {
|
||||
if job.action < Action::DeleteMsgOnImap {
|
||||
Ok(load_imap_deletion_job(context).await?.or(Some(job)))
|
||||
} else {
|
||||
Ok(Some(job))
|
||||
}
|
||||
} else if let Some(job) = load_imap_deletion_job(context).await? {
|
||||
Ok(Some(job))
|
||||
} else {
|
||||
Ok(load_housekeeping_job(context).await?)
|
||||
@@ -1229,12 +1409,8 @@ mod tests {
|
||||
VALUES (?, ?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
now,
|
||||
Thread::from(Action::DownloadMsg),
|
||||
if valid {
|
||||
Action::DownloadMsg as i32
|
||||
} else {
|
||||
-1
|
||||
},
|
||||
Thread::from(Action::MoveMsg),
|
||||
if valid { Action::MoveMsg as i32 } else { -1 },
|
||||
foreign_id,
|
||||
Params::new().to_string(),
|
||||
now
|
||||
@@ -1253,8 +1429,8 @@ mod tests {
|
||||
insert_job(&t, 1, false).await; // This can not be loaded into Job struct.
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
)
|
||||
.await?;
|
||||
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
|
||||
@@ -1263,8 +1439,8 @@ mod tests {
|
||||
insert_job(&t, 1, true).await;
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
)
|
||||
.await?;
|
||||
assert!(jobs.is_some());
|
||||
@@ -1279,8 +1455,8 @@ mod tests {
|
||||
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
)
|
||||
.await?;
|
||||
assert!(jobs.is_some());
|
||||
|
||||
30
src/key.rs
30
src/key.rs
@@ -510,7 +510,8 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[async_std::test]
|
||||
async fn test_load_self_existing() {
|
||||
let alice = alice_keypair();
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = TestContext::new().await;
|
||||
t.configure_alice().await;
|
||||
let pubkey = SignedPublicKey::load_self(&t).await.unwrap();
|
||||
assert_eq!(alice.public, pubkey);
|
||||
let seckey = SignedSecretKey::load_self(&t).await.unwrap();
|
||||
@@ -520,7 +521,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_public() {
|
||||
let t = TestContext::new().await;
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = SignedPublicKey::load_self(&t).await;
|
||||
@@ -530,7 +531,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_secret() {
|
||||
let t = TestContext::new().await;
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = SignedSecretKey::load_self(&t).await;
|
||||
@@ -542,7 +543,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
use std::thread;
|
||||
|
||||
let t = TestContext::new().await;
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
let thr0 = {
|
||||
@@ -588,6 +589,27 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
assert_eq!(nrows().await, 1);
|
||||
}
|
||||
|
||||
// Convenient way to create a new key if you need one, run with
|
||||
// `cargo test key::tests::gen_key`.
|
||||
// #[test]
|
||||
// fn gen_key() {
|
||||
// let name = "fiona";
|
||||
// let keypair = crate::pgp::create_keypair(
|
||||
// EmailAddress::new(&format!("{}@example.net", name)).unwrap(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// std::fs::write(
|
||||
// format!("test-data/key/{}-public.asc", name),
|
||||
// keypair.public.to_base64(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// std::fs::write(
|
||||
// format!("test-data/key/{}-secret.asc", name),
|
||||
// keypair.secret.to_base64(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_fingerprint_from_str() {
|
||||
let res = Fingerprint::new(vec![
|
||||
|
||||
@@ -79,7 +79,8 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_keyring_load_self() {
|
||||
// new_self() implies load_self()
|
||||
let t = TestContext::new_alice().await;
|
||||
let t = TestContext::new().await;
|
||||
t.configure_alice().await;
|
||||
let alice = alice_keypair();
|
||||
|
||||
let pub_ring: Keyring<SignedPublicKey> = Keyring::new_self(&t).await.unwrap();
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::eval_order_dependence,
|
||||
clippy::bool_assert_comparison,
|
||||
clippy::manual_split_once
|
||||
clippy::bool_assert_comparison
|
||||
)]
|
||||
|
||||
#[macro_use]
|
||||
|
||||
@@ -223,7 +223,7 @@ pub async fn send_locations_to_chat(
|
||||
.unwrap_or_default();
|
||||
} else if 0 == seconds && is_sending_locations_before {
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
|
||||
chat::add_info_msg(context, chat_id, stock_str, now).await?;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if 0 != seconds {
|
||||
@@ -747,7 +747,7 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
);
|
||||
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
job_try!(chat::add_info_msg(context, chat_id, &stock_str, now).await);
|
||||
job_try!(chat::add_info_msg(context, chat_id, stock_str, now).await);
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,7 +413,7 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let param = LoginParam {
|
||||
addr: "alice@example.org".to_string(),
|
||||
addr: "alice@example.com".to_string(),
|
||||
imap: ServerLoginParam {
|
||||
server: "imap.example.com".to_string(),
|
||||
user: "alice".to_string(),
|
||||
@@ -424,7 +424,7 @@ mod tests {
|
||||
},
|
||||
smtp: ServerLoginParam {
|
||||
server: "smtp.example.com".to_string(),
|
||||
user: "alice@example.org".to_string(),
|
||||
user: "alice@example.com".to_string(),
|
||||
password: "bar".to_string(),
|
||||
port: 456,
|
||||
security: Socket::Ssl,
|
||||
|
||||
394
src/message.rs
394
src/message.rs
@@ -10,6 +10,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, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_INFO,
|
||||
DC_CONTACT_ID_SELF, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
|
||||
@@ -28,7 +29,6 @@ use crate::log::LogExt;
|
||||
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::stock_str;
|
||||
use crate::summary::Summary;
|
||||
|
||||
@@ -83,6 +83,65 @@ impl MsgId {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns Some if the message needs to be moved from `folder`.
|
||||
/// If yes, returns `ConfiguredInboxFolder`, `ConfiguredMvboxFolder` or `ConfiguredSentboxFolder`,
|
||||
/// depending on where the message should be moved
|
||||
pub async fn needs_move(self, context: &Context, folder: &str) -> Result<Option<Config>> {
|
||||
use Config::*;
|
||||
if context.is_mvbox(folder).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
|
||||
if context.is_spam_folder(folder).await? {
|
||||
let msg_unblocked = msg.chat_id != DC_CHAT_ID_TRASH && msg.chat_blocked == Blocked::Not;
|
||||
|
||||
return if msg_unblocked {
|
||||
if self.needs_move_to_mvbox(context, &msg).await? {
|
||||
Ok(Some(ConfiguredMvboxFolder))
|
||||
} else {
|
||||
Ok(Some(ConfiguredInboxFolder))
|
||||
}
|
||||
} else {
|
||||
// Blocked or contact request message in the spam folder, leave it there
|
||||
Ok(None)
|
||||
};
|
||||
}
|
||||
|
||||
if self.needs_move_to_mvbox(context, &msg).await? {
|
||||
Ok(Some(ConfiguredMvboxFolder))
|
||||
} else if msg.state.is_outgoing()
|
||||
&& msg.is_dc_message == MessengerMessage::Yes
|
||||
&& !msg.is_setupmessage()
|
||||
&& msg.to_id != DC_CONTACT_ID_SELF // Leave self-chat-messages in the inbox, not sure about this
|
||||
&& context.is_inbox(folder).await?
|
||||
&& context.get_config_bool(SentboxMove).await?
|
||||
&& context.get_config(ConfiguredSentboxFolder).await?.is_some()
|
||||
{
|
||||
Ok(Some(ConfiguredSentboxFolder))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn needs_move_to_mvbox(self, context: &Context, msg: &Message) -> Result<bool> {
|
||||
if !context.get_config_bool(Config::MvboxMove).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if msg.is_setupmessage() {
|
||||
// do not move setup messages;
|
||||
// there may be a non-delta device that wants to handle it
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match msg.is_dc_message {
|
||||
MessengerMessage::No => Ok(false),
|
||||
MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
|
||||
}
|
||||
}
|
||||
|
||||
/// Put message into trash chat and delete message text.
|
||||
///
|
||||
/// It means the message is deleted locally, but not on the server.
|
||||
@@ -128,6 +187,24 @@ WHERE id=?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes IMAP server UID and folder from the database record.
|
||||
///
|
||||
/// It is used to avoid trying to remove the message from the
|
||||
/// server multiple times when there are multiple message records
|
||||
/// pointing to the same server UID.
|
||||
pub(crate) async fn unlink(self, context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET server_folder='', server_uid=0 \
|
||||
WHERE id=?",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
///
|
||||
/// Avoid using this, eventually types should be cleaned up enough
|
||||
@@ -231,6 +308,8 @@ pub struct Message {
|
||||
pub(crate) subject: String,
|
||||
pub(crate) rfc724_mid: String,
|
||||
pub(crate) in_reply_to: Option<String>,
|
||||
pub(crate) server_folder: Option<String>,
|
||||
pub(crate) server_uid: u32,
|
||||
pub(crate) is_dc_message: MessengerMessage,
|
||||
pub(crate) mime_modified: bool,
|
||||
pub(crate) chat_blocked: Blocked,
|
||||
@@ -250,7 +329,7 @@ impl Message {
|
||||
pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
|
||||
ensure!(
|
||||
!id.is_special(),
|
||||
"Can not load special message ID {} from DB",
|
||||
"Can not load special message ID {} from DB.",
|
||||
id
|
||||
);
|
||||
let msg = context
|
||||
@@ -261,6 +340,8 @@ impl Message {
|
||||
" m.id AS id,",
|
||||
" rfc724_mid AS rfc724mid,",
|
||||
" m.mime_in_reply_to AS mime_in_reply_to,",
|
||||
" m.server_folder AS server_folder,",
|
||||
" m.server_uid AS server_uid,",
|
||||
" m.chat_id AS chat_id,",
|
||||
" m.from_id AS from_id,",
|
||||
" m.to_id AS to_id,",
|
||||
@@ -311,6 +392,8 @@ impl Message {
|
||||
in_reply_to: row
|
||||
.get::<_, Option<String>>("mime_in_reply_to")?
|
||||
.and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
|
||||
server_folder: row.get::<_, Option<String>>("server_folder")?,
|
||||
server_uid: row.get("server_uid")?,
|
||||
chat_id: row.get("chat_id")?,
|
||||
from_id: row.get("from_id")?,
|
||||
to_id: row.get("to_id")?,
|
||||
@@ -796,7 +879,7 @@ impl Message {
|
||||
pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
|
||||
if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
|
||||
if let Some(in_reply_to) = &self.in_reply_to {
|
||||
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
if let Some((_, _, msg_id)) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
return if msg.chat_id.is_trash() {
|
||||
// If message is already moved to trash chat, pretend it does not exist.
|
||||
@@ -810,11 +893,6 @@ impl Message {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Force the message to be sent in plain text.
|
||||
pub fn force_plaintext(&mut self) {
|
||||
self.param.set_int(Param::ForcePlaintext, 1);
|
||||
}
|
||||
|
||||
pub async fn update_param(&self, context: &Context) {
|
||||
context
|
||||
.sql
|
||||
@@ -1084,13 +1162,11 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
|
||||
}
|
||||
let hop_info: Option<String> = context
|
||||
.sql
|
||||
.query_get_value("SELECT hop_info FROM msgs WHERE id=?;", paramsv![msg_id])
|
||||
.await?;
|
||||
|
||||
ret += "\n\n";
|
||||
ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned());
|
||||
if let Some(ref server_folder) = msg.server_folder {
|
||||
if !server_folder.is_empty() {
|
||||
ret += &format!("\nLast seen as: {}/{}", server_folder, msg.server_uid);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
@@ -1207,13 +1283,11 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
.trash(context)
|
||||
.await
|
||||
.with_context(|| format!("Unable to trash message {}", msg_id))?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target='' WHERE rfc724_mid=?",
|
||||
paramsv![msg.rfc724_mid],
|
||||
)
|
||||
.await?;
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::DeleteMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !msg_ids.is_empty() {
|
||||
@@ -1228,9 +1302,6 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Interrupt Inbox loop to start message deletion.
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1535,7 +1606,7 @@ async fn ndn_maybe_add_info_msg(
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
chat_id,
|
||||
&text,
|
||||
text,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
)
|
||||
.await?;
|
||||
@@ -1611,7 +1682,7 @@ pub async fn estimate_deletion_cnt(
|
||||
WHERE m.id > ?
|
||||
AND timestamp < ?
|
||||
AND chat_id != ?
|
||||
AND EXISTS (SELECT * FROM imap WHERE rfc724_mid=m.rfc724_mid);",
|
||||
AND server_uid != 0;",
|
||||
paramsv![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id],
|
||||
)
|
||||
.await?
|
||||
@@ -1637,10 +1708,32 @@ pub async fn estimate_deletion_cnt(
|
||||
Ok(cnt)
|
||||
}
|
||||
|
||||
/// Counts number of database records pointing to specified
|
||||
/// Message-ID.
|
||||
///
|
||||
/// Unlinked messages are excluded.
|
||||
pub async fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> usize {
|
||||
// check the number of messages with the same rfc724_mid
|
||||
match context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0",
|
||||
paramsv![rfc724_mid],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
error!(context, "dc_get_rfc724_mid_cnt() failed. {}", err);
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn rfc724_mid_exists(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
) -> Result<Option<MsgId>> {
|
||||
) -> Result<Option<(String, u32, MsgId)>> {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
if rfc724_mid.is_empty() {
|
||||
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
|
||||
@@ -1650,12 +1743,14 @@ pub(crate) async fn rfc724_mid_exists(
|
||||
let res = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs WHERE rfc724_mid=?",
|
||||
"SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?",
|
||||
paramsv![rfc724_mid],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let server_folder = row.get::<_, Option<String>>(0)?.unwrap_or_default();
|
||||
let server_uid = row.get(1)?;
|
||||
let msg_id: MsgId = row.get(2)?;
|
||||
|
||||
Ok(msg_id)
|
||||
Ok((server_folder, server_uid, msg_id))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -1663,6 +1758,28 @@ pub(crate) async fn rfc724_mid_exists(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn update_server_uid(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
server_folder: &str,
|
||||
server_uid: u32,
|
||||
) {
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET server_folder=?, server_uid=? \
|
||||
WHERE rfc724_mid=?",
|
||||
paramsv![server_folder, server_uid, rfc724_mid],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
warn!(context, "msg: failed to update server_uid: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1681,6 +1798,212 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// chat_msg means that the message was sent by Delta Chat
|
||||
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
|
||||
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
// These are the same as above, but all messages in Spam stay in Spam
|
||||
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "Spam"),
|
||||
("Spam", false, true, "Spam"),
|
||||
("Spam", true, false, "Spam"),
|
||||
("Spam", true, true, "Spam"),
|
||||
];
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_needs_move_incoming_accepted() {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_needs_move_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_needs_move_incoming_request() {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
|
||||
check_needs_move_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_needs_move_outgoing() {
|
||||
for sentbox_move in &[true, false] {
|
||||
// Test outgoing emails
|
||||
for (folder, mvbox_move, chat_msg, mut expected_destination) in
|
||||
COMBINATIONS_ACCEPTED_CHAT
|
||||
{
|
||||
if *folder == "INBOX" && !mvbox_move && *chat_msg && *sentbox_move {
|
||||
expected_destination = "Sent"
|
||||
}
|
||||
check_needs_move_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
*sentbox_move,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_needs_move_setupmsg() {
|
||||
// Test setupmessages
|
||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_needs_move_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn check_needs_move_combination(
|
||||
folder: &str,
|
||||
mvbox_move: bool,
|
||||
chat_msg: bool,
|
||||
expected_destination: &str,
|
||||
accepted_chat: bool,
|
||||
outgoing: bool,
|
||||
setupmessage: bool,
|
||||
sentbox_move: bool,
|
||||
) {
|
||||
println!("Testing: For folder {}, mvbox_move {}, chat_msg {}, accepted {}, outgoing {}, setupmessage {}",
|
||||
folder, mvbox_move, chat_msg, accepted_chat, outgoing, setupmessage);
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredSpamFolder, Some("Spam"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredSentboxFolder, Some("Sent"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config_bool(Config::SentboxMove, sentbox_move)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if accepted_chat {
|
||||
let contact_id = Contact::create(&t.ctx, "", "bob@example.net")
|
||||
.await
|
||||
.unwrap();
|
||||
ChatId::create_for_contact(&t.ctx, contact_id)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let temp;
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
if setupmessage {
|
||||
include_bytes!("../test-data/message/AutocryptSetupMessage.eml")
|
||||
} else {
|
||||
temp = format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
{}\
|
||||
Subject: foo\n\
|
||||
Message-ID: <abc@example.com>\n\
|
||||
{}\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
if outgoing {
|
||||
"From: alice@example.com\nTo: bob@example.net\n"
|
||||
} else {
|
||||
"From: bob@example.net\nTo: alice@example.com\n"
|
||||
},
|
||||
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
|
||||
);
|
||||
temp.as_bytes()
|
||||
},
|
||||
folder,
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let exists = rfc724_mid_exists(&t, "abc@example.com").await.unwrap();
|
||||
let (folder_1, _, msg_id) = exists.unwrap();
|
||||
assert_eq!(folder, folder_1);
|
||||
let actual = if let Some(config) = msg_id.needs_move(&t.ctx, folder).await.unwrap() {
|
||||
t.ctx.get_config(config).await.unwrap()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let expected = if expected_destination == folder {
|
||||
None
|
||||
} else {
|
||||
Some(expected_destination)
|
||||
};
|
||||
assert_eq!(expected, actual.as_deref(), "For folder {}, mvbox_move {}, chat_msg {}, accepted {}, outgoing {}, setupmessage {}: expected {:?}, got {:?}",
|
||||
folder, mvbox_move, chat_msg, accepted_chat, outgoing, setupmessage, expected, actual);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_prepare_message_and_send() {
|
||||
use crate::config::Config;
|
||||
@@ -1864,13 +2187,14 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <123@example.com>\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
123,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
@@ -2072,7 +2396,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <123@example.com>\n\
|
||||
Auto-Submitted: auto-generated\n\
|
||||
@@ -2080,6 +2404,7 @@ mod tests {
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -2091,13 +2416,14 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <456@example.com>\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello again\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use chrono::TimeZone;
|
||||
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
|
||||
|
||||
@@ -154,12 +154,6 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
if chat.is_self_talk() {
|
||||
recipients.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
} else if chat.is_mailing_list() {
|
||||
let list_post = chat
|
||||
.param
|
||||
.get(Param::ListPost)
|
||||
.context("Can't write to mailinglist without ListPost param")?;
|
||||
recipients.push(("".to_string(), list_post.to_string()));
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
@@ -464,45 +458,20 @@ impl<'a> MimeFactory<'a> {
|
||||
self.from_addr.clone(),
|
||||
);
|
||||
|
||||
let undisclosed_recipients = match &self.loaded {
|
||||
Loaded::Message { chat } => chat.typ == Chattype::Broadcast,
|
||||
Loaded::Mdn { .. } => false,
|
||||
};
|
||||
|
||||
let mut to = Vec::new();
|
||||
if undisclosed_recipients {
|
||||
to.push(Address::new_group(
|
||||
"hidden-recipients".to_string(),
|
||||
Vec::new(),
|
||||
));
|
||||
} else {
|
||||
let email_to_remove =
|
||||
if self.msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
self.msg.param.get(Param::Arg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for (name, addr) in self.recipients.iter() {
|
||||
if let Some(email_to_remove) = email_to_remove {
|
||||
if email_to_remove == addr {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if name.is_empty() {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
} else {
|
||||
to.push(Address::new_mailbox_with_name(
|
||||
name.to_string(),
|
||||
addr.clone(),
|
||||
));
|
||||
}
|
||||
for (name, addr) in self.recipients.iter() {
|
||||
if name.is_empty() {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
} else {
|
||||
to.push(Address::new_mailbox_with_name(
|
||||
name.to_string(),
|
||||
addr.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if to.is_empty() {
|
||||
to.push(from.clone());
|
||||
}
|
||||
if to.is_empty() {
|
||||
to.push(from.clone());
|
||||
}
|
||||
|
||||
headers
|
||||
@@ -603,9 +572,20 @@ impl<'a> MimeFactory<'a> {
|
||||
render_rfc724_mid(&rfc724_mid),
|
||||
));
|
||||
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("To".into(), to).unwrap());
|
||||
let undisclosed_recipients = match &self.loaded {
|
||||
Loaded::Message { chat } => chat.typ == Chattype::Broadcast,
|
||||
Loaded::Mdn { .. } => false,
|
||||
};
|
||||
|
||||
if undisclosed_recipients {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("To".into(), "hidden-recipients: ;".to_string()));
|
||||
} else {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("To".into(), to).unwrap());
|
||||
}
|
||||
|
||||
headers
|
||||
.unprotected
|
||||
@@ -1425,10 +1405,6 @@ mod tests {
|
||||
use async_std::prelude::*;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::contact::Origin;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
@@ -1436,7 +1412,6 @@ mod tests {
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
|
||||
use async_std::fs::File;
|
||||
use mailparse::{addrparse_header, MailHeaderMap};
|
||||
|
||||
#[test]
|
||||
fn test_render_email_address() {
|
||||
@@ -1534,7 +1509,7 @@ mod tests {
|
||||
msg_to_subject_str(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: Antw: Chat: hello\n\
|
||||
Message-ID: <2222@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
@@ -1549,7 +1524,7 @@ mod tests {
|
||||
msg_to_subject_str(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: Infos: 42\n\
|
||||
Message-ID: <2222@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
@@ -1568,7 +1543,7 @@ mod tests {
|
||||
msg_to_subject_str(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: Chat: hello\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2223@example.com>\n\
|
||||
@@ -1586,7 +1561,7 @@ mod tests {
|
||||
// 3. Send the first message to a new contact
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
assert_eq!(first_subject_str(t).await, "Message from alice@example.org");
|
||||
assert_eq!(first_subject_str(t).await, "Message from alice@example.com");
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::Displayname, Some("Alice"))
|
||||
@@ -1601,7 +1576,7 @@ mod tests {
|
||||
msg_to_subject_str(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: äääää\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
@@ -1615,7 +1590,7 @@ mod tests {
|
||||
msg_to_subject_str(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: aäääää\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
@@ -1634,7 +1609,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: alice@example.org\n\
|
||||
From: alice@example.com\n\
|
||||
To: bob@example.com\n\
|
||||
Subject: Hello, Bob\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -1643,6 +1618,7 @@ mod tests {
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
@@ -1650,7 +1626,7 @@ mod tests {
|
||||
let new_msg = incoming_msg_to_reply_msg(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: message opened\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -1725,7 +1701,7 @@ mod tests {
|
||||
format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: Different subject\n\
|
||||
In-Reply-To: {}\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
@@ -1736,6 +1712,7 @@ mod tests {
|
||||
)
|
||||
.as_bytes(),
|
||||
"INBOX",
|
||||
5,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -1779,7 +1756,7 @@ mod tests {
|
||||
mf.subject_str(&t).await.unwrap()
|
||||
}
|
||||
|
||||
// In `imf_raw`, From has to be bob@example.com, To has to be alice@example.org
|
||||
// In `imf_raw`, From has to be bob@example.com, To has to be alice@example.com
|
||||
async fn msg_to_subject_str(imf_raw: &[u8]) -> String {
|
||||
let subject_str = msg_to_subject_str_inner(imf_raw, false, false, false).await;
|
||||
|
||||
@@ -1840,13 +1817,14 @@ mod tests {
|
||||
&t,
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: Some other, completely unrelated subject\n\
|
||||
Message-ID: <3cl4@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
Some other, completely unrelated content\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
@@ -1871,7 +1849,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
dc_receive_imf(context, imf_raw, "INBOX", false)
|
||||
dc_receive_imf(context, imf_raw, "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1899,7 +1877,7 @@ mod tests {
|
||||
let msg = incoming_msg_to_reply_msg(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Charlie <charlie@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: Chat: hello\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2223@example.com>\n\
|
||||
@@ -2038,35 +2016,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that removed member address does not go into the `To:` field.
|
||||
#[async_std::test]
|
||||
async fn test_remove_member_bcc() -> Result<()> {
|
||||
// Alice creates a group with Bob and Claire and then removes Bob.
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "Claire", "claire@foo.de").await?;
|
||||
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "Creating a group".to_string()).await?;
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
let remove = alice.pop_sent_msg().await;
|
||||
let remove_payload = remove.payload();
|
||||
let parsed = mailparse::parse_mail(remove_payload.as_bytes())?;
|
||||
let to = parsed
|
||||
.headers
|
||||
.get_first_header("To")
|
||||
.ok_or_else(|| format_err!("No To: header parsed"))?;
|
||||
let to = addrparse_header(to)?;
|
||||
let mailbox = to
|
||||
.extract_single_info()
|
||||
.ok_or_else(|| format_err!("To: field does not contain exactly one address"))?;
|
||||
assert_eq!(mailbox.addr, "bob@example.net");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::blob::BlobObject;
|
||||
use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
|
||||
use crate::contact::addr_normalize;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_get_filemeta, dc_truncate, parse_receive_headers};
|
||||
use crate::dc_tools::{dc_get_filemeta, dc_truncate};
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::e2ee;
|
||||
use crate::events::EventType;
|
||||
@@ -47,7 +47,6 @@ pub struct MimeMessage {
|
||||
/// Addresses are normalized and lowercased:
|
||||
pub recipients: Vec<SingleInfo>,
|
||||
pub from: Vec<SingleInfo>,
|
||||
pub list_post: Option<String>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub decrypting_failed: bool,
|
||||
|
||||
@@ -83,8 +82,6 @@ pub struct MimeMessage {
|
||||
/// This is non-empty only if the message was actually encrypted. It is used
|
||||
/// for e.g. late-parsing HTML.
|
||||
pub decoded_data: Vec<u8>,
|
||||
|
||||
pub(crate) hop_info: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -166,12 +163,10 @@ impl MimeMessage {
|
||||
.get_header_value(HeaderDef::Date)
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.unwrap_or_default();
|
||||
let hop_info = parse_receive_headers(&mail.get_headers());
|
||||
|
||||
let mut headers = Default::default();
|
||||
let mut recipients = Default::default();
|
||||
let mut from = Default::default();
|
||||
let mut list_post = Default::default();
|
||||
let mut chat_disposition_notification_to = None;
|
||||
|
||||
// Parse IMF headers.
|
||||
@@ -180,7 +175,6 @@ impl MimeMessage {
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
&mail.headers,
|
||||
);
|
||||
@@ -254,7 +248,6 @@ impl MimeMessage {
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut throwaway_from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
@@ -282,7 +275,6 @@ impl MimeMessage {
|
||||
parts: Vec::new(),
|
||||
header: headers,
|
||||
recipients,
|
||||
list_post,
|
||||
from,
|
||||
chat_disposition_notification_to,
|
||||
decrypting_failed: false,
|
||||
@@ -302,7 +294,6 @@ impl MimeMessage {
|
||||
footer: None,
|
||||
is_mime_modified: false,
|
||||
decoded_data: Vec::new(),
|
||||
hop_info,
|
||||
};
|
||||
|
||||
match partial {
|
||||
@@ -1101,11 +1092,11 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repl_msg_by_error(&mut self, error_msg: &str) {
|
||||
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
|
||||
self.is_system_message = SystemMessage::Unknown;
|
||||
if let Some(part) = self.parts.first_mut() {
|
||||
part.typ = Viewtype::Text;
|
||||
part.msg = format!("[{}]", error_msg);
|
||||
part.msg = format!("[{}]", error_msg.as_ref());
|
||||
self.parts.truncate(1);
|
||||
}
|
||||
}
|
||||
@@ -1121,7 +1112,6 @@ impl MimeMessage {
|
||||
headers: &mut HashMap<String, String>,
|
||||
recipients: &mut Vec<SingleInfo>,
|
||||
from: &mut Vec<SingleInfo>,
|
||||
list_post: &mut Option<String>,
|
||||
chat_disposition_notification_to: &mut Option<SingleInfo>,
|
||||
fields: &[mailparse::MailHeader<'_>],
|
||||
) {
|
||||
@@ -1152,10 +1142,6 @@ impl MimeMessage {
|
||||
if !from_new.is_empty() {
|
||||
*from = from_new;
|
||||
}
|
||||
let list_post_new = get_list_post(fields);
|
||||
if list_post_new.is_some() {
|
||||
*list_post = list_post_new;
|
||||
}
|
||||
}
|
||||
|
||||
fn process_report(
|
||||
@@ -1173,21 +1159,23 @@ impl MimeMessage {
|
||||
|
||||
// must be present
|
||||
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
|
||||
let original_message_id = report_fields
|
||||
if let Some(original_message_id) = report_fields
|
||||
.get_header_value(HeaderDef::OriginalMessageId)
|
||||
.and_then(|v| parse_message_id(&v).ok());
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
.collect()
|
||||
});
|
||||
.and_then(|v| parse_message_id(&v).ok())
|
||||
{
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
.collect()
|
||||
});
|
||||
|
||||
return Ok(Some(Report {
|
||||
original_message_id,
|
||||
additional_message_ids,
|
||||
}));
|
||||
return Ok(Some(Report {
|
||||
original_message_id,
|
||||
additional_message_ids,
|
||||
}));
|
||||
}
|
||||
}
|
||||
warn!(
|
||||
context,
|
||||
@@ -1343,10 +1331,8 @@ impl MimeMessage {
|
||||
parts: &[Part],
|
||||
) {
|
||||
for report in &self.mdn_reports {
|
||||
for original_message_id in report
|
||||
.original_message_id
|
||||
.iter()
|
||||
.chain(&report.additional_message_ids)
|
||||
for original_message_id in
|
||||
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
|
||||
{
|
||||
match message::handle_mdn(context, from_id, original_message_id, sent_timestamp)
|
||||
.await
|
||||
@@ -1451,10 +1437,7 @@ async fn update_gossip_peerstates(
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Report {
|
||||
/// Original-Message-ID header
|
||||
///
|
||||
/// It MUST be present if the original message has a Message-ID according to RFC 8098, but MS
|
||||
/// Exchange does not add it nevertheless, in which case it is `None`.
|
||||
original_message_id: Option<String>,
|
||||
original_message_id: String,
|
||||
/// Additional-Message-IDs
|
||||
additional_message_ids: Vec<String>,
|
||||
}
|
||||
@@ -1644,14 +1627,6 @@ pub(crate) fn get_from(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||
get_all_addresses_from_header(headers, |header_key| header_key == "from")
|
||||
}
|
||||
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
|
||||
get_all_addresses_from_header(headers, |header_key| header_key == "list-post")
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|s| s.addr)
|
||||
}
|
||||
|
||||
fn get_all_addresses_from_header<F>(headers: &[MailHeader], pred: F) -> Vec<SingleInfo>
|
||||
where
|
||||
F: Fn(String) -> bool,
|
||||
@@ -2371,7 +2346,7 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
|
||||
assert_eq!(message.mdn_reports.len(), 1);
|
||||
assert_eq!(
|
||||
message.mdn_reports[0].original_message_id,
|
||||
Some("foo@example.org".to_string())
|
||||
"foo@example.org"
|
||||
);
|
||||
assert_eq!(
|
||||
&message.mdn_reports[0].additional_message_ids,
|
||||
@@ -2873,6 +2848,7 @@ On 2020-10-25, Bob wrote:
|
||||
&t.ctx,
|
||||
include_bytes!("../test-data/message/subj_with_multimedia_msg.eml"),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
@@ -3021,7 +2997,7 @@ Subject: ...
|
||||
|
||||
Some quote.
|
||||
"###;
|
||||
dc_receive_imf(&t, raw, "INBOX", false).await?;
|
||||
dc_receive_imf(&t, raw, "INBOX", 1, false).await?;
|
||||
|
||||
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
|
||||
let raw = br###"In-Reply-To:
|
||||
@@ -3038,7 +3014,7 @@ Subject: ...
|
||||
Some reply
|
||||
"###;
|
||||
|
||||
dc_receive_imf(&t, raw, "INBOX", false).await?;
|
||||
dc_receive_imf(&t, raw, "INBOX", 2, false).await?;
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.get_text().unwrap(), "Some reply");
|
||||
@@ -3058,21 +3034,21 @@ Some reply
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <foobarbaz@example.org>
|
||||
To: Bob <bob@example.org>
|
||||
From: Alice <alice@example.org>
|
||||
From: Alice <alice@example.com>
|
||||
Subject: subject
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
Chat-Disposition-Notification-To: alice@example.com
|
||||
|
||||
Message.
|
||||
"###;
|
||||
|
||||
// Bob receives message.
|
||||
dc_receive_imf(&bob, raw, "INBOX", false).await?;
|
||||
dc_receive_imf(&bob, raw, "INBOX", 1, 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, "INBOX", 1, 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());
|
||||
@@ -3088,17 +3064,18 @@ Message.
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: alice@example.org\n\
|
||||
From: alice@example.com\n\
|
||||
To: bob@example.net\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: first@example.com\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: alice@example.org\n\
|
||||
Chat-Disposition-Notification-To: alice@example.com\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
.as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -3110,8 +3087,8 @@ Message.
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: alice@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
From: alice@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: message opened\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -3137,6 +3114,7 @@ Message.
|
||||
--SNIPP--"
|
||||
.as_bytes(),
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -3147,18 +3125,4 @@ Message.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test parsing of MDN sent by MS Exchange.
|
||||
///
|
||||
/// It does not have required Original-Message-ID field, so it is useless, but we want to
|
||||
/// recognize it as MDN nevertheless to avoid displaying it in the chat as normal message.
|
||||
#[async_std::test]
|
||||
async fn test_ms_exchange_mdn() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let raw =
|
||||
include_bytes!("../test-data/message/ms_exchange_report_disposition_notification.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await?;
|
||||
assert!(!mimeparser.mdn_reports.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ struct Oauth2 {
|
||||
|
||||
/// OAuth 2 Access Token Response
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Response {
|
||||
// Should always be there according to: <https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/>
|
||||
// but previous code handled its abscense.
|
||||
@@ -59,7 +58,7 @@ pub async fn dc_get_oauth2_url(
|
||||
redirect_uri: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
|
||||
if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await {
|
||||
if let Some(oauth2) = Oauth2::from_address(addr, socks5_enabled).await {
|
||||
context
|
||||
.sql
|
||||
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri))
|
||||
@@ -80,7 +79,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
regenerate: bool,
|
||||
) -> Result<Option<String>> {
|
||||
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
|
||||
if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await {
|
||||
if let Some(oauth2) = Oauth2::from_address(addr, socks5_enabled).await {
|
||||
let lock = context.oauth2_mutex.lock().await;
|
||||
|
||||
// read generated token
|
||||
@@ -226,7 +225,7 @@ pub async fn dc_get_oauth2_addr(
|
||||
code: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
|
||||
let oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await {
|
||||
let oauth2 = match Oauth2::from_address(addr, socks5_enabled).await {
|
||||
Some(o) => o,
|
||||
None => return Ok(None),
|
||||
};
|
||||
@@ -254,13 +253,13 @@ pub async fn dc_get_oauth2_addr(
|
||||
}
|
||||
|
||||
impl Oauth2 {
|
||||
async fn from_address(context: &Context, addr: &str, skip_mx: bool) -> Option<Self> {
|
||||
async fn from_address(addr: &str, skip_mx: bool) -> Option<Self> {
|
||||
let addr_normalized = normalize_addr(addr);
|
||||
if let Some(domain) = addr_normalized
|
||||
.find('@')
|
||||
.map(|index| addr_normalized.split_at(index + 1).1)
|
||||
{
|
||||
if let Some(oauth2_authorizer) = provider::get_provider_info(context, domain, skip_mx)
|
||||
if let Some(oauth2_authorizer) = provider::get_provider_info(domain, skip_mx)
|
||||
.await
|
||||
.and_then(|provider| provider.oauth2_authorizer.as_ref())
|
||||
{
|
||||
@@ -357,39 +356,32 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_oauth_from_address() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
Oauth2::from_address(&t, "hello@gmail.com", false).await,
|
||||
Oauth2::from_address("hello@gmail.com", false).await,
|
||||
Some(OAUTH2_GMAIL)
|
||||
);
|
||||
assert_eq!(
|
||||
Oauth2::from_address(&t, "hello@googlemail.com", false).await,
|
||||
Oauth2::from_address("hello@googlemail.com", false).await,
|
||||
Some(OAUTH2_GMAIL)
|
||||
);
|
||||
assert_eq!(
|
||||
Oauth2::from_address(&t, "hello@yandex.com", false).await,
|
||||
Oauth2::from_address("hello@yandex.com", false).await,
|
||||
Some(OAUTH2_YANDEX)
|
||||
);
|
||||
assert_eq!(
|
||||
Oauth2::from_address(&t, "hello@yandex.ru", false).await,
|
||||
Oauth2::from_address("hello@yandex.ru", false).await,
|
||||
Some(OAUTH2_YANDEX)
|
||||
);
|
||||
assert_eq!(Oauth2::from_address(&t, "hello@web.de", false).await, None);
|
||||
|
||||
assert_eq!(Oauth2::from_address("hello@web.de", false).await, None);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_oauth_from_mx() {
|
||||
// youtube staff seems to use "google workspace with oauth2", figures this out by MX lookup
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
Oauth2::from_address(&t, "hello@youtube.com", false).await,
|
||||
Oauth2::from_address("hello@google.com", false).await,
|
||||
Some(OAUTH2_GMAIL)
|
||||
);
|
||||
// without MX lookup, we would not know as youtube.com is not in our provider-db
|
||||
assert_eq!(
|
||||
Oauth2::from_address(&t, "hello@youtube.com", true).await,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
|
||||
35
src/param.rs
35
src/param.rs
@@ -113,9 +113,6 @@ pub enum Param {
|
||||
/// For Jobs: space-separated list of message recipients
|
||||
Recipients = b'R',
|
||||
|
||||
/// For MDN-sending job
|
||||
MsgId = b'I',
|
||||
|
||||
/// For Groups
|
||||
///
|
||||
/// An unpromoted group has not had any messages sent to it and thus only exists on the
|
||||
@@ -139,17 +136,8 @@ pub enum Param {
|
||||
/// For Chats
|
||||
Devicetalk = b'D',
|
||||
|
||||
/// For Chats: If this is a mailing list chat, contains the List-Post address.
|
||||
/// None if there simply is no `List-Post` header in the mailing list.
|
||||
/// Some("") if the mailing list is using multiple different List-Post headers.
|
||||
///
|
||||
/// The List-Post address is the email address where the user can write to in order to
|
||||
/// post something to the mailing list.
|
||||
ListPost = b'p',
|
||||
|
||||
/// For Contacts: If this is the List-Post address of a mailing list, contains
|
||||
/// the List-Id of the mailing list (which is also used as the group id of the chat).
|
||||
ListId = b's',
|
||||
/// For MDN-sending job
|
||||
MsgId = b'I',
|
||||
|
||||
/// For Contacts: timestamp of status (aka signature or footer) update.
|
||||
StatusTimestamp = b'j',
|
||||
@@ -203,11 +191,6 @@ impl fmt::Display for Params {
|
||||
impl str::FromStr for Params {
|
||||
type Err = Error;
|
||||
|
||||
/// Parse a raw string to Param.
|
||||
///
|
||||
/// Silently ignore unknown keys:
|
||||
/// they may come from a downgrade (when a shortly new version adds a key)
|
||||
/// or from an upgrade (when a key is dropped but was used in the past)
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let mut inner = BTreeMap::new();
|
||||
let mut lines = s.lines().peekable();
|
||||
@@ -227,6 +210,8 @@ impl str::FromStr for Params {
|
||||
|
||||
if let Some(key) = key.as_bytes().first().and_then(|key| Param::from_u8(*key)) {
|
||||
inner.insert(key, value);
|
||||
} else {
|
||||
bail!("Unknown key: {}", key);
|
||||
}
|
||||
} else {
|
||||
bail!("Not a key-value pair: {:?}", line);
|
||||
@@ -429,12 +414,10 @@ impl<'a> ParamsFile<'a> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_std::fs;
|
||||
use async_std::path::Path;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_dc_param() {
|
||||
@@ -537,14 +520,4 @@ mod tests {
|
||||
assert!(p.get_path(Param::File, &t).unwrap().is_none());
|
||||
assert!(p.get_blob(Param::File, &t, false).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_params_unknown_key() -> Result<()> {
|
||||
// 'Z' is used as a key that is known to be unused; these keys should be ignored silently by definition.
|
||||
let p = Params::from_str("w=12\nZ=13\nh=14")?;
|
||||
assert_eq!(p.len(), 2);
|
||||
assert_eq!(p.get(Param::Width), Some("12"));
|
||||
assert_eq!(p.get(Param::Height), Some("14"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ impl Peerstate {
|
||||
|
||||
let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await;
|
||||
|
||||
chat::add_info_msg(context, chat_id, &msg, timestamp).await?;
|
||||
chat::add_info_msg(context, chat_id, msg, timestamp).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
} else {
|
||||
bail!("contact with peerstate.addr {:?} not found", &self.addr);
|
||||
|
||||
125
src/pgp.rs
125
src/pgp.rs
@@ -8,7 +8,7 @@ use anyhow::{bail, ensure, format_err, Result};
|
||||
use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
|
||||
SignedPublicSubKey, SignedSecretKey, StandaloneSignature, SubkeyParamsBuilder,
|
||||
SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder,
|
||||
};
|
||||
use pgp::crypto::{HashAlgorithm, SymmetricKeyAlgorithm};
|
||||
use pgp::types::{
|
||||
@@ -248,7 +248,7 @@ pub async fn pk_encrypt(
|
||||
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
|
||||
.keys()
|
||||
.iter()
|
||||
.filter_map(select_pk_for_encryption)
|
||||
.filter_map(|key| select_pk_for_encryption(key))
|
||||
.collect();
|
||||
let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect();
|
||||
|
||||
@@ -277,17 +277,16 @@ pub async fn pk_encrypt(
|
||||
/// Receiver private keys are provided in
|
||||
/// `private_keys_for_decryption`.
|
||||
///
|
||||
/// Returns decrypted message and fingerprints
|
||||
/// If `ret_signature_fingerprints` is not `None`, stores fingerprints
|
||||
/// of all keys from the `public_keys_for_validation` keyring that
|
||||
/// have valid signatures there.
|
||||
#[allow(clippy::implicit_hasher)]
|
||||
pub async fn pk_decrypt(
|
||||
ctext: Vec<u8>,
|
||||
private_keys_for_decryption: Keyring<SignedSecretKey>,
|
||||
public_keys_for_validation: &Keyring<SignedPublicKey>,
|
||||
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
|
||||
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
|
||||
|
||||
public_keys_for_validation: Keyring<SignedPublicKey>,
|
||||
ret_signature_fingerprints: Option<&mut HashSet<Fingerprint>>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let msgs = async_std::task::spawn_blocking(move || {
|
||||
let cursor = Cursor::new(ctext);
|
||||
let (msg, _) = Message::from_armor_single(cursor)?;
|
||||
@@ -309,54 +308,33 @@ pub async fn pk_decrypt(
|
||||
None => bail!("The decrypted message is empty"),
|
||||
};
|
||||
|
||||
if !public_keys_for_validation.is_empty() {
|
||||
let pkeys = public_keys_for_validation.keys();
|
||||
if let Some(ret_signature_fingerprints) = ret_signature_fingerprints {
|
||||
if !public_keys_for_validation.is_empty() {
|
||||
let fingerprints = async_std::task::spawn_blocking(move || {
|
||||
let pkeys = public_keys_for_validation.keys();
|
||||
|
||||
let mut fingerprints: Vec<Fingerprint> = Vec::new();
|
||||
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
|
||||
for pkey in pkeys {
|
||||
if signed_msg.verify(&pkey.primary_key).is_ok() {
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
fingerprints.push(fp);
|
||||
let mut fingerprints: Vec<Fingerprint> = Vec::new();
|
||||
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
|
||||
for pkey in pkeys {
|
||||
if signed_msg.verify(&pkey.primary_key).is_ok() {
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
fingerprints.push(fp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fingerprints
|
||||
})
|
||||
.await;
|
||||
|
||||
ret_signature_fingerprints.extend(fingerprints);
|
||||
ret_signature_fingerprints.extend(fingerprints);
|
||||
}
|
||||
}
|
||||
Ok((content, ret_signature_fingerprints))
|
||||
Ok(content)
|
||||
} else {
|
||||
bail!("No valid messages found");
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates detached signature.
|
||||
pub async fn pk_validate(
|
||||
content: &[u8],
|
||||
signature: &[u8],
|
||||
public_keys_for_validation: &Keyring<SignedPublicKey>,
|
||||
) -> Result<HashSet<Fingerprint>> {
|
||||
let mut ret: HashSet<Fingerprint> = Default::default();
|
||||
|
||||
let standalone_signature = StandaloneSignature::from_armor_single(Cursor::new(signature))?.0;
|
||||
let pkeys = public_keys_for_validation.keys();
|
||||
|
||||
// Remove trailing CRLF before the delimiter.
|
||||
// According to RFC 3156 it is considered to be part of the MIME delimiter for the purpose of
|
||||
// OpenPGP signature calculation.
|
||||
let content = content
|
||||
.get(..content.len().saturating_sub(2))
|
||||
.ok_or_else(|| format_err!("index is out of range"))?;
|
||||
|
||||
for pkey in pkeys {
|
||||
if standalone_signature.verify(pkey, content).is_ok() {
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
ret.insert(fp);
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Symmetric encryption.
|
||||
pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
|
||||
let lit_msg = Message::new_literal_bytes("", plain);
|
||||
@@ -514,10 +492,12 @@ mod tests {
|
||||
decrypt_keyring.add(KEYS.alice_secret.clone());
|
||||
let mut sig_check_keyring: Keyring<SignedPublicKey> = Keyring::new();
|
||||
sig_check_keyring.add(KEYS.alice_public.clone());
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes().to_vec(),
|
||||
decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| println!("{:?}", err))
|
||||
@@ -530,10 +510,12 @@ mod tests {
|
||||
decrypt_keyring.add(KEYS.bob_secret.clone());
|
||||
let mut sig_check_keyring = Keyring::new();
|
||||
sig_check_keyring.add(KEYS.alice_public.clone());
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes().to_vec(),
|
||||
decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| println!("{:?}", err))
|
||||
@@ -547,10 +529,15 @@ mod tests {
|
||||
let mut keyring = Keyring::new();
|
||||
keyring.add(KEYS.alice_secret.clone());
|
||||
let empty_keyring = Keyring::new();
|
||||
let (plain, valid_signatures) =
|
||||
pk_decrypt(CTEXT_SIGNED.as_bytes().to_vec(), keyring, &empty_keyring)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes().to_vec(),
|
||||
keyring,
|
||||
empty_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
@@ -562,10 +549,12 @@ mod tests {
|
||||
decrypt_keyring.add(KEYS.bob_secret.clone());
|
||||
let mut sig_check_keyring = Keyring::new();
|
||||
sig_check_keyring.add(KEYS.bob_public.clone());
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes().to_vec(),
|
||||
decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -578,14 +567,34 @@ mod tests {
|
||||
let mut decrypt_keyring = Keyring::new();
|
||||
decrypt_keyring.add(KEYS.bob_secret.clone());
|
||||
let sig_check_keyring = Keyring::new();
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_UNSIGNED.as_bytes().to_vec(),
|
||||
decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decrypt_signed_no_sigret() {
|
||||
// Check decrypting signed cyphertext without providing the HashSet for signatures.
|
||||
let mut decrypt_keyring = Keyring::new();
|
||||
decrypt_keyring.add(KEYS.bob_secret.clone());
|
||||
let mut sig_check_keyring = Keyring::new();
|
||||
sig_check_keyring.add(KEYS.alice_public.clone());
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes().to_vec(),
|
||||
decrypt_keyring,
|
||||
sig_check_keyring,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
mod data;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED};
|
||||
use anyhow::Result;
|
||||
use async_std_resolver::{config, resolver, resolver_from_system_conf, AsyncStdResolver};
|
||||
use async_std_resolver::resolver_from_system_conf;
|
||||
use chrono::{NaiveDateTime, NaiveTime};
|
||||
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
|
||||
@@ -83,23 +81,6 @@ pub struct Provider {
|
||||
pub oauth2_authorizer: Option<Oauth2Authorizer>,
|
||||
}
|
||||
|
||||
/// Get resolver to query MX records.
|
||||
///
|
||||
/// We first try resolver_from_system_conf() which reads the system's resolver from `/etc/resolv.conf`.
|
||||
/// This does not work at least on some Androids, therefore we use use ResolverConfig::default()
|
||||
/// which default eg. to google's 8.8.8.8 or 8.8.4.4 as a fallback.
|
||||
async fn get_resolver() -> Result<AsyncStdResolver> {
|
||||
if let Ok(resolver) = resolver_from_system_conf().await {
|
||||
return Ok(resolver);
|
||||
}
|
||||
let resolver = resolver(
|
||||
config::ResolverConfig::default(),
|
||||
config::ResolverOpts::default(),
|
||||
)
|
||||
.await?;
|
||||
Ok(resolver)
|
||||
}
|
||||
|
||||
/// Returns provider for the given domain.
|
||||
///
|
||||
/// This function looks up domain in offline database first. If not
|
||||
@@ -108,11 +89,7 @@ async fn get_resolver() -> Result<AsyncStdResolver> {
|
||||
///
|
||||
/// For compatibility, email address can be passed to this function
|
||||
/// instead of the domain.
|
||||
pub async fn get_provider_info(
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
skip_mx: bool,
|
||||
) -> Option<&'static Provider> {
|
||||
pub async fn get_provider_info(domain: &str, skip_mx: bool) -> Option<&'static Provider> {
|
||||
let domain = domain.rsplitn(2, '@').next()?;
|
||||
|
||||
if let Some(provider) = get_provider_by_domain(domain) {
|
||||
@@ -120,7 +97,7 @@ pub async fn get_provider_info(
|
||||
}
|
||||
|
||||
if !skip_mx {
|
||||
if let Some(provider) = get_provider_by_mx(context, domain).await {
|
||||
if let Some(provider) = get_provider_by_mx(domain).await {
|
||||
return Some(provider);
|
||||
}
|
||||
}
|
||||
@@ -140,8 +117,8 @@ pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> {
|
||||
/// Finds a provider based on MX record for the given domain.
|
||||
///
|
||||
/// For security reasons, only Gmail can be configured this way.
|
||||
pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'static Provider> {
|
||||
if let Ok(resolver) = get_resolver().await {
|
||||
pub async fn get_provider_by_mx(domain: &str) -> Option<&'static Provider> {
|
||||
if let Ok(resolver) = resolver_from_system_conf().await {
|
||||
let mut fqdn: String = domain.to_string();
|
||||
if !fqdn.ends_with('.') {
|
||||
fqdn.push('.');
|
||||
@@ -166,8 +143,6 @@ pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'sta
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(context, "cannot get a resolver to check MX records.");
|
||||
}
|
||||
|
||||
None
|
||||
@@ -194,7 +169,6 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::dc_tools::time;
|
||||
use crate::test_utils::TestContext;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
#[test]
|
||||
@@ -244,13 +218,12 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_provider_info() {
|
||||
let t = TestContext::new().await;
|
||||
assert!(get_provider_info(&t, "", false).await.is_none());
|
||||
assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
|
||||
assert!(get_provider_info("", false).await.is_none());
|
||||
assert!(get_provider_info("google.com", false).await.unwrap().id == "gmail");
|
||||
|
||||
// get_provider_info() accepts email addresses for backwards compatibility
|
||||
assert!(
|
||||
get_provider_info(&t, "example@google.com", false)
|
||||
get_provider_info("example@google.com", false)
|
||||
.await
|
||||
.unwrap()
|
||||
.id
|
||||
@@ -269,10 +242,4 @@ mod tests {
|
||||
assert!(get_provider_update_timestamp() <= time());
|
||||
assert!(get_provider_update_timestamp() > timestamp_past);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_resolver() -> Result<()> {
|
||||
assert!(get_resolver().await.is_ok());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +328,10 @@ static P_DUBBY_ORG: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
key: Config::SentboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
@@ -471,6 +475,10 @@ static P_FIVE_CHAT: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
key: Config::SentboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
@@ -815,6 +823,10 @@ static P_NAUTA_CU: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
key: Config::SentboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
@@ -1089,6 +1101,10 @@ static P_TESTRUN: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
key: Config::SentboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
|
||||
12
src/qr.rs
12
src/qr.rs
@@ -282,7 +282,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
chat.id,
|
||||
&format!("{} verified.", peerstate.addr),
|
||||
format!("{} verified.", peerstate.addr),
|
||||
time(),
|
||||
)
|
||||
.await?;
|
||||
@@ -424,7 +424,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
grpid,
|
||||
..
|
||||
} => {
|
||||
let chat_id = get_chat_id_by_grpid(context, &grpid)
|
||||
let chat_id = get_chat_id_by_grpid(context, grpid)
|
||||
.await?
|
||||
.map(|(chat_id, _protected, _blocked)| chat_id);
|
||||
token::save(
|
||||
@@ -792,12 +792,12 @@ mod tests {
|
||||
async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org")
|
||||
let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.com")
|
||||
.await
|
||||
.context("failed to create contact")?;
|
||||
let pub_key = alice_keypair().public;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.org".to_string(),
|
||||
addr: "alice@example.com".to_string(),
|
||||
last_seen: 1,
|
||||
last_seen_autocrypt: 1,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
@@ -818,7 +818,7 @@ mod tests {
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.com",
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprMismatch { contact_id, .. } = qr {
|
||||
@@ -829,7 +829,7 @@ mod tests {
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
&format!("OPENPGP4FPR:{}#a=alice@example.org", pub_key.fingerprint()),
|
||||
&format!("OPENPGP4FPR:{}#a=alice@example.com", pub_key.fingerprint()),
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprOk { contact_id, .. } = qr {
|
||||
|
||||
@@ -89,23 +89,21 @@ fn inner_generate_secure_join_qr_code(
|
||||
let mut w = tagger::new(&mut svg);
|
||||
|
||||
w.elem("svg", |d| {
|
||||
d.attr("xmlns", "http://www.w3.org/2000/svg")?;
|
||||
d.attr("viewBox", format_args!("0 0 {} {}", width, height))?;
|
||||
Ok(())
|
||||
})?
|
||||
d.attr("xmlns", "http://www.w3.org/2000/svg")
|
||||
.attr("viewBox", format_args!("0 0 {} {}", width, height));
|
||||
})
|
||||
.build(|w| {
|
||||
// White Background apears like a card
|
||||
w.single("rect", |d| {
|
||||
d.attr("x", card_border_size)?;
|
||||
d.attr("y", card_border_size)?;
|
||||
d.attr("rx", card_roundness)?;
|
||||
d.attr("stroke", "#c6c6c6")?;
|
||||
d.attr("stroke-width", card_border_size)?;
|
||||
d.attr("width", width - (card_border_size * 2.0))?;
|
||||
d.attr("height", height - (card_border_size * 2.0))?;
|
||||
d.attr("style", "fill:#f2f2f2")?;
|
||||
Ok(())
|
||||
})?;
|
||||
d.attr("x", card_border_size)
|
||||
.attr("y", card_border_size)
|
||||
.attr("rx", card_roundness)
|
||||
.attr("stroke", "#c6c6c6")
|
||||
.attr("stroke-width", card_border_size)
|
||||
.attr("width", width - (card_border_size * 2.0))
|
||||
.attr("height", height - (card_border_size * 2.0))
|
||||
.attr("style", "fill:#f2f2f2");
|
||||
});
|
||||
// Qrcode
|
||||
w.elem("g", |d| {
|
||||
d.attr(
|
||||
@@ -115,12 +113,12 @@ fn inner_generate_secure_join_qr_code(
|
||||
(width - qr_code_size) / 2.0,
|
||||
((height - qr_code_size) / 2.0) - qr_translate_up
|
||||
),
|
||||
)
|
||||
);
|
||||
// If the qr code should be in the wrong place,
|
||||
// we could also translate and scale the points in the path already,
|
||||
// but that would make the resulting svg way bigger in size and might bring up rounding issues,
|
||||
// so better avoid doing it manually if possible
|
||||
})?
|
||||
})
|
||||
.build(|w| {
|
||||
w.single("path", |d| {
|
||||
let mut path_data = String::with_capacity(0);
|
||||
@@ -134,16 +132,16 @@ fn inner_generate_secure_join_qr_code(
|
||||
}
|
||||
}
|
||||
|
||||
d.attr("style", "fill:#000000")?;
|
||||
d.attr("d", path_data)?;
|
||||
d.attr("transform", format!("scale({})", scale))
|
||||
})
|
||||
})?;
|
||||
d.attr("style", "fill:#000000")
|
||||
.attr("d", path_data)
|
||||
.attr("transform", format!("scale({})", scale));
|
||||
});
|
||||
});
|
||||
|
||||
// Text
|
||||
const BIG_TEXT_CHARS_PER_LINE: usize = 32;
|
||||
const SMALL_TEXT_CHARS_PER_LINE: usize = 38;
|
||||
let chars_per_line = if qrcode_description.len() > SMALL_TEXT_CHARS_PER_LINE * 2 {
|
||||
let chars_per_line = if qrcode_description.len() > SMALL_TEXT_CHARS_PER_LINE*2 {
|
||||
SMALL_TEXT_CHARS_PER_LINE
|
||||
} else {
|
||||
BIG_TEXT_CHARS_PER_LINE
|
||||
@@ -154,27 +152,27 @@ fn inner_generate_secure_join_qr_code(
|
||||
} else {
|
||||
(19.0, -10.0)
|
||||
};
|
||||
for (count, line) in lines.split('\n').enumerate() {
|
||||
for (count, line) in lines.split('\n').enumerate()
|
||||
{
|
||||
w.elem("text", |d| {
|
||||
d.attr(
|
||||
"y",
|
||||
(count as f32 * (text_font_size * 1.2)) + text_y_pos + text_y_shift,
|
||||
)?;
|
||||
d.attr("x", width / 2.0)?;
|
||||
d.attr("text-anchor", "middle")?;
|
||||
d.attr(
|
||||
"style",
|
||||
format!(
|
||||
"font-family:sans-serif;\
|
||||
d.attr("y", (count as f32 * (text_font_size * 1.2)) + text_y_pos + text_y_shift)
|
||||
.attr("x", width / 2.0)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr(
|
||||
"style",
|
||||
format!(
|
||||
"font-family:sans-serif;\
|
||||
font-weight:bold;\
|
||||
font-size:{}px;\
|
||||
fill:#000000;\
|
||||
stroke:none",
|
||||
text_font_size
|
||||
),
|
||||
)
|
||||
})?
|
||||
.build(|w| w.put_raw(line))?;
|
||||
text_font_size
|
||||
),
|
||||
);
|
||||
})
|
||||
.build(|w| {
|
||||
w.put_raw(line);
|
||||
});
|
||||
}
|
||||
// contact avatar in middle of qrcode
|
||||
const LOGO_SIZE: f32 = 94.4;
|
||||
@@ -185,64 +183,68 @@ fn inner_generate_secure_join_qr_code(
|
||||
((height - qr_code_size) / 2.0) - qr_translate_up + logo_position_in_qr;
|
||||
|
||||
w.single("circle", |d| {
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?;
|
||||
d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?;
|
||||
d.attr("r", HALF_LOGO_SIZE + avatar_border_size)?;
|
||||
d.attr("style", "fill:#f2f2f2")
|
||||
})?;
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)
|
||||
.attr("cy", logo_position_y + HALF_LOGO_SIZE)
|
||||
.attr("r", HALF_LOGO_SIZE + avatar_border_size)
|
||||
.attr("style", "fill:#f2f2f2");
|
||||
});
|
||||
|
||||
if let Some(img) = avatar {
|
||||
w.elem("defs", tagger::no_attr())?.build(|w| {
|
||||
w.elem("clipPath", |d| d.attr("id", "avatar-cut"))?
|
||||
.build(|w| {
|
||||
w.single("circle", |d| {
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?;
|
||||
d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?;
|
||||
d.attr("r", HALF_LOGO_SIZE)
|
||||
})
|
||||
})
|
||||
})?;
|
||||
w.elem("defs", |_| {}).build(|w| {
|
||||
w.elem("clipPath", |d| {
|
||||
d.attr("id", "avatar-cut");
|
||||
})
|
||||
.build(|w| {
|
||||
w.single("circle", |d| {
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)
|
||||
.attr("cy", logo_position_y + HALF_LOGO_SIZE)
|
||||
.attr("r", HALF_LOGO_SIZE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
w.single("image", |d| {
|
||||
d.attr("x", logo_position_x)?;
|
||||
d.attr("y", logo_position_y)?;
|
||||
d.attr("width", HALF_LOGO_SIZE * 2.0)?;
|
||||
d.attr("height", HALF_LOGO_SIZE * 2.0)?;
|
||||
d.attr("preserveAspectRatio", "none")?;
|
||||
d.attr("clip-path", "url(#avatar-cut)")?;
|
||||
d.attr(
|
||||
"href", /*might need xlink:href instead if it doesn't work on older devices?*/
|
||||
format!("data:image/jpeg;base64,{}", base64::encode(img)),
|
||||
)
|
||||
})?;
|
||||
d.attr("x", logo_position_x)
|
||||
.attr("y", logo_position_y)
|
||||
.attr("width", HALF_LOGO_SIZE * 2.0)
|
||||
.attr("height", HALF_LOGO_SIZE * 2.0)
|
||||
.attr("preserveAspectRatio", "none")
|
||||
.attr("clip-path", "url(#avatar-cut)")
|
||||
.attr(
|
||||
"href" /*might need xlink:href instead if it doesn't work on older devices?*/,
|
||||
format!("data:image/jpeg;base64,{}", base64::encode(img)),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
w.single("circle", |d| {
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?;
|
||||
d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?;
|
||||
d.attr("r", HALF_LOGO_SIZE)?;
|
||||
d.attr("style", format!("fill:{}", &color))
|
||||
})?;
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)
|
||||
.attr("cy", logo_position_y + HALF_LOGO_SIZE)
|
||||
.attr("r", HALF_LOGO_SIZE)
|
||||
.attr("style", format!("fill:{}", &color));
|
||||
});
|
||||
|
||||
let avatar_font_size = LOGO_SIZE * 0.65;
|
||||
let font_offset = avatar_font_size * 0.1;
|
||||
w.elem("text", |d| {
|
||||
d.attr("y", logo_position_y + HALF_LOGO_SIZE + font_offset)?;
|
||||
d.attr("x", logo_position_x + HALF_LOGO_SIZE)?;
|
||||
d.attr("text-anchor", "middle")?;
|
||||
d.attr("dominant-baseline", "central")?;
|
||||
d.attr("alignment-baseline", "middle")?;
|
||||
d.attr(
|
||||
"style",
|
||||
format!(
|
||||
"font-family:sans-serif;\
|
||||
d.attr("y", logo_position_y + HALF_LOGO_SIZE + font_offset)
|
||||
.attr("x", logo_position_x + HALF_LOGO_SIZE)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dominant-baseline", "central")
|
||||
.attr("alignment-baseline", "middle")
|
||||
.attr(
|
||||
"style",
|
||||
format!(
|
||||
"font-family:sans-serif;\
|
||||
font-weight:400;\
|
||||
font-size:{}px;\
|
||||
fill:#ffffff;",
|
||||
avatar_font_size
|
||||
),
|
||||
)
|
||||
})?
|
||||
.build(|w| w.put_raw(avatar_letter.to_uppercase()))?;
|
||||
avatar_font_size
|
||||
),
|
||||
);
|
||||
})
|
||||
.build(|w| {
|
||||
w.put_raw(avatar_letter.to_uppercase());
|
||||
});
|
||||
}
|
||||
|
||||
// Footer logo
|
||||
@@ -256,10 +258,12 @@ fn inner_generate_secure_join_qr_code(
|
||||
(width - FOOTER_WIDTH) / 2.0,
|
||||
height - logo_offset - FOOTER_HEIGHT - text_y_shift
|
||||
),
|
||||
)
|
||||
})?
|
||||
.build(|w| w.put_raw(include_str!("../assets/qrcode_logo_footer.svg")))
|
||||
})?;
|
||||
);
|
||||
})
|
||||
.build(|w| {
|
||||
w.put_raw(include_str!("../assets/qrcode_logo_footer.svg"));
|
||||
});
|
||||
});
|
||||
|
||||
Ok(svg)
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ impl Context {
|
||||
}
|
||||
|
||||
let quota = if imap.can_check_quota() {
|
||||
let folders = get_watched_folders(self).await?;
|
||||
let folders = get_watched_folders(self).await;
|
||||
get_unique_quota_roots_and_usage(folders, imap).await
|
||||
} else {
|
||||
Err(anyhow!(stock_str::not_supported_by_provider(self).await))
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::context::Context;
|
||||
use crate::dc_tools::maybe_add_time_based_warnings;
|
||||
use crate::imap::Imap;
|
||||
use crate::job::{self, Thread};
|
||||
use crate::message::MsgId;
|
||||
use crate::smtp::Smtp;
|
||||
|
||||
use self::connectivity::ConnectivityStore;
|
||||
@@ -81,15 +82,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
let mut jobs_loaded = 0;
|
||||
let mut info = InterruptInfo::default();
|
||||
loop {
|
||||
let job = match job::load_next(&ctx, Thread::Imap, &info).await {
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed loading job from the database: {:#}.", err);
|
||||
None
|
||||
}
|
||||
Ok(job) => job,
|
||||
};
|
||||
|
||||
match job {
|
||||
match job::load_next(&ctx, Thread::Imap, &info)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
Some(job) if jobs_loaded <= 20 => {
|
||||
jobs_loaded += 1;
|
||||
job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await;
|
||||
@@ -98,15 +95,41 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
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;
|
||||
if ctx
|
||||
.get_config_bool(Config::InboxWatch)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
info!(ctx, "postponing imap-job {} to run fetch...", job);
|
||||
fetch(&ctx, &mut connection).await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
jobs_loaded = 0;
|
||||
|
||||
// Expunge folder if needed, e.g. if some jobs have
|
||||
// deleted messages on the server.
|
||||
if let Err(err) = connection.maybe_close_folder(&ctx).await {
|
||||
warn!(ctx, "failed to close folder: {:?}", err);
|
||||
}
|
||||
|
||||
maybe_add_time_based_warnings(&ctx).await;
|
||||
|
||||
info = fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await;
|
||||
info = if ctx
|
||||
.get_config_bool(Config::InboxWatch)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
|
||||
} else {
|
||||
if let Err(err) = connection.scan_folders(&ctx).await {
|
||||
warn!(ctx, "{}", err);
|
||||
connection.connectivity.set_err(&ctx, err).await;
|
||||
} else {
|
||||
connection.connectivity.set_not_configured(&ctx).await;
|
||||
}
|
||||
connection.fake_idle(&ctx, None).await
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +157,7 @@ async fn fetch(ctx: &Context, connection: &mut Imap) {
|
||||
}
|
||||
|
||||
// fetch
|
||||
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
|
||||
if let Err(err) = connection.fetch(ctx, &watch_folder).await {
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
warn!(ctx, "{:#}", err);
|
||||
}
|
||||
@@ -160,9 +183,13 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
return connection.fake_idle(ctx, Some(watch_folder)).await;
|
||||
}
|
||||
|
||||
// Scan other folders before fetching from watched folder. This may result in the
|
||||
// messages being moved into the watched folder, for example from the Spam folder to
|
||||
// the Inbox folder.
|
||||
// fetch
|
||||
if let Err(err) = connection.fetch(ctx, &watch_folder).await {
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
warn!(ctx, "{:#}", err);
|
||||
return InterruptInfo::new(false, None);
|
||||
}
|
||||
|
||||
if folder == Config::ConfiguredInboxFolder {
|
||||
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
|
||||
if let Err(err) = connection.scan_folders(ctx).await {
|
||||
@@ -172,13 +199,6 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
}
|
||||
}
|
||||
|
||||
// fetch
|
||||
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
warn!(ctx, "{:#}", err);
|
||||
return InterruptInfo::new(false);
|
||||
}
|
||||
|
||||
connection.connectivity.set_connected(ctx).await;
|
||||
|
||||
// idle
|
||||
@@ -188,7 +208,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
Err(err) => {
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
warn!(ctx, "{}", err);
|
||||
InterruptInfo::new(false)
|
||||
InterruptInfo::new(false, None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -333,7 +353,7 @@ impl Scheduler {
|
||||
}))
|
||||
};
|
||||
|
||||
if ctx.get_config_bool(Config::MvboxMove).await? {
|
||||
if ctx.get_config_bool(Config::MvboxWatch).await? {
|
||||
let ctx = ctx.clone();
|
||||
mvbox_handle = Some(task::spawn(async move {
|
||||
simple_imap_loop(
|
||||
@@ -417,10 +437,10 @@ impl Scheduler {
|
||||
return;
|
||||
}
|
||||
|
||||
self.interrupt_inbox(InterruptInfo::new(true))
|
||||
.join(self.interrupt_mvbox(InterruptInfo::new(true)))
|
||||
.join(self.interrupt_sentbox(InterruptInfo::new(true)))
|
||||
.join(self.interrupt_smtp(InterruptInfo::new(true)))
|
||||
self.interrupt_inbox(InterruptInfo::new(true, None))
|
||||
.join(self.interrupt_mvbox(InterruptInfo::new(true, None)))
|
||||
.join(self.interrupt_sentbox(InterruptInfo::new(true, None)))
|
||||
.join(self.interrupt_smtp(InterruptInfo::new(true, None)))
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -429,10 +449,10 @@ impl Scheduler {
|
||||
return;
|
||||
}
|
||||
|
||||
self.interrupt_inbox(InterruptInfo::new(false))
|
||||
.join(self.interrupt_mvbox(InterruptInfo::new(false)))
|
||||
.join(self.interrupt_sentbox(InterruptInfo::new(false)))
|
||||
.join(self.interrupt_smtp(InterruptInfo::new(false)))
|
||||
self.interrupt_inbox(InterruptInfo::new(false, None))
|
||||
.join(self.interrupt_mvbox(InterruptInfo::new(false, None)))
|
||||
.join(self.interrupt_sentbox(InterruptInfo::new(false, None)))
|
||||
.join(self.interrupt_smtp(InterruptInfo::new(false, None)))
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -662,10 +682,14 @@ struct ImapConnectionHandlers {
|
||||
#[derive(Default, Debug)]
|
||||
pub struct InterruptInfo {
|
||||
pub probe_network: bool,
|
||||
pub msg_id: Option<MsgId>,
|
||||
}
|
||||
|
||||
impl InterruptInfo {
|
||||
pub fn new(probe_network: bool) -> Self {
|
||||
Self { probe_network }
|
||||
pub fn new(probe_network: bool, msg_id: Option<MsgId>) -> Self {
|
||||
Self {
|
||||
probe_network,
|
||||
msg_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,17 +362,17 @@ impl Context {
|
||||
[
|
||||
(
|
||||
Config::ConfiguredInboxFolder,
|
||||
None,
|
||||
Config::InboxWatch,
|
||||
inbox.state.connectivity.clone(),
|
||||
),
|
||||
(
|
||||
Config::ConfiguredMvboxFolder,
|
||||
Some(Config::MvboxMove),
|
||||
Config::MvboxWatch,
|
||||
mvbox.state.connectivity.clone(),
|
||||
),
|
||||
(
|
||||
Config::ConfiguredSentboxFolder,
|
||||
Some(Config::SentboxWatch),
|
||||
Config::SentboxWatch,
|
||||
sentbox.state.connectivity.clone(),
|
||||
),
|
||||
],
|
||||
@@ -393,18 +393,10 @@ impl Context {
|
||||
|
||||
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
|
||||
for (folder, watch, state) in &folders_states {
|
||||
let w = if let Some(watch_config) = *watch {
|
||||
self.get_config(watch_config)
|
||||
.await
|
||||
.ok_or_log(self)
|
||||
.flatten()
|
||||
== Some("1".to_string())
|
||||
} else {
|
||||
true
|
||||
};
|
||||
let w = self.get_config(*watch).await.ok_or_log(self);
|
||||
|
||||
let mut folder_added = false;
|
||||
if w {
|
||||
if w.flatten() == Some("1".to_string()) {
|
||||
let f = self.get_config(*folder).await.ok_or_log(self).flatten();
|
||||
|
||||
if let Some(foldername) = f {
|
||||
|
||||
@@ -322,11 +322,10 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
&group_id,
|
||||
&group_name,
|
||||
group_id,
|
||||
group_name,
|
||||
Blocked::Not,
|
||||
ProtectionStatus::Unprotected, // protection is added later as needed
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
@@ -334,7 +333,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
chat::add_to_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
}
|
||||
let msg = stock_str::secure_join_started(context, contact_id).await;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
chat::add_info_msg(context, chat_id, msg, time()).await?;
|
||||
Ok(chat_id)
|
||||
}
|
||||
}
|
||||
@@ -541,7 +540,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
bobstate.chat_id(context).await?,
|
||||
&msg,
|
||||
msg,
|
||||
time(),
|
||||
)
|
||||
.await?;
|
||||
@@ -742,7 +741,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.get_header(HeaderDef::SecureJoinGroup)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| "");
|
||||
if let Err(err) = chat::get_chat_id_by_grpid(context, field_grpid).await {
|
||||
if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid).await {
|
||||
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
|
||||
return Err(
|
||||
err.context(format!("Chat for group {} not found", &field_grpid))
|
||||
@@ -852,7 +851,7 @@ async fn secure_connection_established(
|
||||
) -> Result<(), Error> {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let msg = stock_str::contact_verified(context, contact.get_name_n_addr()).await;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
chat::add_info_msg(context, chat_id, msg, time()).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
Ok(())
|
||||
}
|
||||
@@ -937,29 +936,36 @@ fn encrypted_and_signed(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use async_std::prelude::*;
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::events::Event;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::{LogSink, TestContext};
|
||||
use crate::test_utils::TestContext;
|
||||
use std::time::Duration;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact() -> Result<()> {
|
||||
let (log_tx, _log_sink) = LogSink::create();
|
||||
let alice = TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(log_tx.clone())
|
||||
.build()
|
||||
.await;
|
||||
let bob = TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(log_tx)
|
||||
.build()
|
||||
.await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
|
||||
// Setup JoinerProgress sinks.
|
||||
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
|
||||
bob.add_event_sink(move |event: Event| {
|
||||
let joiner_progress_tx = joiner_progress_tx.clone();
|
||||
async move {
|
||||
if let EventType::SecurejoinJoinerProgress { .. } = event.typ {
|
||||
joiner_progress_tx.try_send(event).unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = dc_get_securejoin_qr(&alice.ctx, None).await?;
|
||||
|
||||
@@ -969,7 +975,7 @@ mod tests {
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!bob.ctx.has_ongoing().await);
|
||||
assert_eq!(sent.recipient(), "alice@example.org".parse().unwrap());
|
||||
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
|
||||
@@ -991,24 +997,28 @@ mod tests {
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
{
|
||||
let evt = joiner_progress_rx
|
||||
.recv()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.await
|
||||
.expect("timeout waiting for JoinerProgress event")
|
||||
.expect("missing JoinerProgress event");
|
||||
match evt.typ {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => panic!("Wrong event type"),
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Check Bob sent the right message.
|
||||
@@ -1085,7 +1095,7 @@ mod tests {
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
@@ -1120,7 +1130,7 @@ mod tests {
|
||||
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
||||
assert!(msg.is_info());
|
||||
let text = msg.get_text().unwrap();
|
||||
assert!(text.contains("alice@example.org verified"));
|
||||
assert!(text.contains("alice@example.com verified"));
|
||||
}
|
||||
|
||||
// Check Bob sent the final message
|
||||
@@ -1143,22 +1153,25 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
let (log_tx, _log_sink) = LogSink::create();
|
||||
let alice = TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(log_tx.clone())
|
||||
.build()
|
||||
.await;
|
||||
let bob = TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(log_tx)
|
||||
.build()
|
||||
.await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Setup JoinerProgress sinks.
|
||||
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
|
||||
bob.add_event_sink(move |event: Event| {
|
||||
let joiner_progress_tx = joiner_progress_tx.clone();
|
||||
async move {
|
||||
if let EventType::SecurejoinJoinerProgress { .. } = event.typ {
|
||||
joiner_progress_tx.try_send(event).unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
// Ensure Bob knows Alice_FP
|
||||
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await?;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.org".into(),
|
||||
addr: "alice@example.com".into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 10,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
@@ -1181,25 +1194,30 @@ mod tests {
|
||||
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
{
|
||||
let evt = joiner_progress_rx
|
||||
.recv()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.await
|
||||
.expect("timeout waiting for JoinerProgress event")
|
||||
.expect("missing JoinerProgress event");
|
||||
match evt.typ {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => panic!("Wrong event type"),
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert!(!bob.ctx.has_ongoing().await);
|
||||
|
||||
// Check Bob sent the right handshake message.
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
@@ -1247,7 +1265,7 @@ mod tests {
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
@@ -1276,17 +1294,8 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact_concurrent_calls() -> Result<()> {
|
||||
let (log_tx, _log_sink) = LogSink::create();
|
||||
let alice = TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(log_tx.clone())
|
||||
.build()
|
||||
.await;
|
||||
let bob = TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(log_tx)
|
||||
.build()
|
||||
.await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// do a scan that is not working as claire is never responding
|
||||
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
|
||||
@@ -1308,27 +1317,30 @@ mod tests {
|
||||
.pop_sent_msg()
|
||||
.await
|
||||
.payload()
|
||||
.contains("alice@example.org"));
|
||||
.contains("alice@example.com"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_secure_join() -> Result<()> {
|
||||
let (log_tx, _log_sink) = LogSink::create();
|
||||
let alice = TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(log_tx.clone())
|
||||
.build()
|
||||
.await;
|
||||
let bob = TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(log_tx)
|
||||
.build()
|
||||
.await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
|
||||
// Setup JoinerProgress sinks.
|
||||
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
|
||||
bob.add_event_sink(move |event: Event| {
|
||||
let joiner_progress_tx = joiner_progress_tx.clone();
|
||||
async move {
|
||||
if let EventType::SecurejoinJoinerProgress { .. } = event.typ {
|
||||
joiner_progress_tx.try_send(event).unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let chatid =
|
||||
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
|
||||
|
||||
@@ -1342,7 +1354,7 @@ mod tests {
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert_eq!(sent.recipient(), "alice@example.org".parse().unwrap());
|
||||
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
|
||||
@@ -1364,24 +1376,28 @@ mod tests {
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
{
|
||||
let evt = joiner_progress_rx
|
||||
.recv()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.await
|
||||
.expect("timeout waiting for JoinerProgress event")
|
||||
.expect("missing JoinerProgress event");
|
||||
match evt.typ {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => panic!("Wrong event type"),
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Check Bob sent the right handshake message.
|
||||
@@ -1426,7 +1442,7 @@ mod tests {
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
|
||||
@@ -71,7 +71,7 @@ impl<'a> BobStateHandle<'a> {
|
||||
pub async fn chat_id(&self, context: &Context) -> Result<ChatId> {
|
||||
match self.bobstate.invite {
|
||||
QrInvite::Group { ref grpid, .. } => {
|
||||
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, grpid).await? {
|
||||
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, &grpid).await? {
|
||||
Ok(chat_id)
|
||||
} else {
|
||||
bail!("chat not found")
|
||||
@@ -422,7 +422,7 @@ impl BobState {
|
||||
BobHandshakeMsg::Request => {
|
||||
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
|
||||
msg.param.set(Param::Arg2, self.invite.invitenumber());
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
}
|
||||
BobHandshakeMsg::RequestWithAuth => {
|
||||
// Sends the Secure-Join-Auth header in mimefactory.rs.
|
||||
|
||||
97
src/sql.rs
97
src/sql.rs
@@ -123,15 +123,6 @@ impl Sql {
|
||||
if !readonly {
|
||||
{
|
||||
let conn = self.get_conn().await?;
|
||||
|
||||
// Try to enable auto_vacuum. This will only be
|
||||
// applied if the database is new or after successful
|
||||
// VACUUM, which usually happens before backup export.
|
||||
// When auto_vacuum is INCREMENTAL, it is possible to
|
||||
// use PRAGMA incremental_vacuum to return unused
|
||||
// database pages to the filesystem.
|
||||
conn.pragma_update(None, "auto_vacuum", &"INCREMENTAL".to_string())?;
|
||||
|
||||
// journal_mode is persisted, it is sufficient to change it only for one handle.
|
||||
conn.pragma_update(None, "journal_mode", &"WAL".to_string())?;
|
||||
|
||||
@@ -613,16 +604,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
|
||||
context.schedule_quota_update().await?;
|
||||
|
||||
// Try to clear the freelist to free some space on the disk. This
|
||||
// only works if auto_vacuum is enabled.
|
||||
if let Err(err) = context
|
||||
.sql
|
||||
.execute("PRAGMA incremental_vacuum", paramsv![])
|
||||
.await
|
||||
{
|
||||
warn!(context, "Failed to run incremental vacuum: {}", err);
|
||||
}
|
||||
|
||||
if let Err(e) = context
|
||||
.set_config(Config::LastHousekeeping, Some(&time().to_string()))
|
||||
.await
|
||||
@@ -685,11 +666,9 @@ async fn maybe_add_from_param(
|
||||
/// have a server UID.
|
||||
async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
sql.execute(
|
||||
"DELETE FROM msgs
|
||||
WHERE (chat_id=? OR hidden)
|
||||
AND NOT EXISTS (
|
||||
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
|
||||
)",
|
||||
"DELETE FROM msgs \
|
||||
WHERE (chat_id = ? OR hidden) \
|
||||
AND server_uid = 0",
|
||||
paramsv![DC_CHAT_ID_TRASH],
|
||||
)
|
||||
.await?;
|
||||
@@ -697,12 +676,11 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_std::channel;
|
||||
mod test {
|
||||
use async_std::fs::File;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::{test_utils::TestContext, EventType};
|
||||
use crate::{test_utils::TestContext, Event, EventType};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -747,22 +725,6 @@ mod tests {
|
||||
assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap());
|
||||
}
|
||||
|
||||
/// Tests that auto_vacuum is enabled for new databases.
|
||||
#[async_std::test]
|
||||
async fn test_auto_vacuum() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let conn = t.sql.get_conn().await?;
|
||||
let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| {
|
||||
let auto_vacuum: i32 = row.get(0)?;
|
||||
Ok(auto_vacuum)
|
||||
})?;
|
||||
|
||||
// auto_vacuum=2 is the same as auto_vacuum=INCREMENTAL
|
||||
assert_eq!(auto_vacuum, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_housekeeping_db_closed() {
|
||||
let t = TestContext::new().await;
|
||||
@@ -779,8 +741,18 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (event_sink, event_source) = channel::unbounded();
|
||||
t.add_event_sender(event_sink).await;
|
||||
t.add_event_sink(move |event: Event| async move {
|
||||
match event.typ {
|
||||
EventType::Info(s) => assert!(
|
||||
!s.contains("Keeping new unreferenced file"),
|
||||
"File {} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)",
|
||||
s
|
||||
),
|
||||
EventType::Error(s) => panic!("{}", s),
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
|
||||
@@ -791,18 +763,6 @@ mod tests {
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
|
||||
|
||||
while let Ok(event) = event_source.try_recv() {
|
||||
match event.typ {
|
||||
EventType::Info(s) => assert!(
|
||||
!s.contains("Keeping new unreferenced file"),
|
||||
"File {} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)",
|
||||
s
|
||||
),
|
||||
EventType::Error(s) => panic!("{}", s),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test.
|
||||
@@ -864,24 +824,17 @@ mod tests {
|
||||
assert!(!disable_server_delete);
|
||||
assert!(!recode_avatar);
|
||||
|
||||
info!(&t, "test_migration_flags: XXX END MARKER");
|
||||
info!(&t, "test_migration_flags: XXX");
|
||||
|
||||
loop {
|
||||
let evt = t
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::Info(_)))
|
||||
.await;
|
||||
match evt {
|
||||
EventType::Info(msg) => {
|
||||
assert!(
|
||||
!msg.contains("[migration]"),
|
||||
"Migrations were run twice, you probably forgot to update the db version"
|
||||
);
|
||||
if msg.contains("test_migration_flags: XXX END MARKER") {
|
||||
break;
|
||||
}
|
||||
if let EventType::Info(info) = t.evtracker.recv().await.unwrap() {
|
||||
assert!(
|
||||
!info.contains("[migration]"),
|
||||
"Migrations were run twice, you probably forgot to update the db version"
|
||||
);
|
||||
if info.contains("test_migration_flags: XXX") {
|
||||
break;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -497,44 +497,6 @@ item TEXT DEFAULT '');"#,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 81 {
|
||||
info!(context, "[migration] v81");
|
||||
sql.execute_migration("ALTER TABLE msgs ADD COLUMN hop_info TEXT;", 81)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 82 {
|
||||
info!(context, "[migration] v82");
|
||||
sql.execute_migration(
|
||||
r#"CREATE TABLE imap (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc724_mid TEXT DEFAULT '', -- Message-ID header
|
||||
folder TEXT DEFAULT '', -- IMAP folder
|
||||
target TEXT DEFAULT '', -- Destination folder, empty to delete.
|
||||
uid INTEGER DEFAULT 0, -- UID
|
||||
uidvalidity INTEGER DEFAULT 0,
|
||||
UNIQUE (folder, uid, uidvalidity)
|
||||
);
|
||||
CREATE INDEX imap_folder ON imap(folder);
|
||||
CREATE INDEX imap_messageid ON imap(rfc724_mid);
|
||||
|
||||
INSERT INTO imap
|
||||
(rfc724_mid, folder, target, uid, uidvalidity)
|
||||
SELECT
|
||||
rfc724_mid,
|
||||
server_folder AS folder,
|
||||
server_folder AS target,
|
||||
server_uid AS uid,
|
||||
(SELECT uidvalidity FROM imap_sync WHERE folder=server_folder) AS uidvalidity
|
||||
FROM msgs
|
||||
WHERE server_uid>0
|
||||
ON CONFLICT (folder, uid, uidvalidity)
|
||||
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
target=excluded.target;
|
||||
"#,
|
||||
82,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
recalc_fingerprints,
|
||||
|
||||
@@ -1229,20 +1229,20 @@ mod tests {
|
||||
async fn test_stock_system_msg_add_member_by_me() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
msg_add_member(&t, "alice@example.org", DC_CONTACT_ID_SELF).await,
|
||||
"Member alice@example.org added by me."
|
||||
msg_add_member(&t, "alice@example.com", DC_CONTACT_ID_SELF).await,
|
||||
"Member alice@example.com added by me."
|
||||
)
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
Contact::create(&t, "Alice", "alice@example.org")
|
||||
Contact::create(&t, "Alice", "alice@example.com")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
assert_eq!(
|
||||
msg_add_member(&t, "alice@example.org", DC_CONTACT_ID_SELF).await,
|
||||
"Member Alice (alice@example.org) added by me."
|
||||
msg_add_member(&t, "alice@example.com", DC_CONTACT_ID_SELF).await,
|
||||
"Member Alice (alice@example.com) added by me."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1250,7 +1250,7 @@ mod tests {
|
||||
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = {
|
||||
Contact::create(&t, "Alice", "alice@example.org")
|
||||
Contact::create(&t, "Alice", "alice@example.com")
|
||||
.await
|
||||
.expect("Failed to create contact Alice");
|
||||
Contact::create(&t, "Bob", "bob@example.com")
|
||||
@@ -1258,8 +1258,8 @@ mod tests {
|
||||
.expect("failed to create bob")
|
||||
};
|
||||
assert_eq!(
|
||||
msg_add_member(&t, "alice@example.org", contact_id,).await,
|
||||
"Member Alice (alice@example.org) added by Bob (bob@example.com)."
|
||||
msg_add_member(&t, "alice@example.com", contact_id,).await,
|
||||
"Member Alice (alice@example.com) added by Bob (bob@example.com)."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,22 +2,20 @@
|
||||
//!
|
||||
//! This private module is only compiled for test runs.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use std::panic;
|
||||
use std::str::FromStr;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{collections::BTreeMap, panic};
|
||||
use std::{fmt, thread};
|
||||
|
||||
use ansi_term::Color;
|
||||
use async_std::channel::{self, Receiver, Sender};
|
||||
use async_std::channel::Receiver;
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{Arc, RwLock};
|
||||
use async_std::task;
|
||||
use async_std::{channel, pin::Pin};
|
||||
use async_std::{future::Future, task};
|
||||
use chat::ChatItem;
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
@@ -31,7 +29,7 @@ use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::events::{Event, EventType};
|
||||
use crate::job::Action;
|
||||
use crate::key::{self, DcKey, KeyPair, KeyPairUse};
|
||||
use crate::key::{self, DcKey};
|
||||
use crate::message::{update_msg_state, Message, MessageState, MsgId};
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::param::{Param, Params};
|
||||
@@ -39,100 +37,41 @@ use crate::param::{Param, Params};
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
|
||||
type EventSink =
|
||||
dyn Fn(Event) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> + Send + Sync + 'static;
|
||||
|
||||
/// Map of [`Context::id`] to names for [`TestContext`]s.
|
||||
static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
|
||||
Lazy::new(|| std::sync::RwLock::new(BTreeMap::new()));
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TestContextBuilder {
|
||||
key_pair: Option<KeyPair>,
|
||||
log_sink: Option<Sender<Event>>,
|
||||
}
|
||||
|
||||
impl TestContextBuilder {
|
||||
/// Configures as alice@example.org with fixed secret key.
|
||||
///
|
||||
/// This is a shortcut for `.with_key_pair(alice_keypair()).
|
||||
pub fn configure_alice(self) -> Self {
|
||||
self.with_key_pair(alice_keypair())
|
||||
}
|
||||
|
||||
/// Configures as bob@example.net with fixed secret key.
|
||||
///
|
||||
/// This is a shortcut for `.with_key_pair(bob_keypair()).
|
||||
pub fn configure_bob(self) -> Self {
|
||||
self.with_key_pair(bob_keypair())
|
||||
}
|
||||
|
||||
/// Configures the new [`TestContext`] with the provided [`KeyPair`].
|
||||
///
|
||||
/// This will extract the email address from the key and configure the context with the
|
||||
/// given identity.
|
||||
pub fn with_key_pair(mut self, key_pair: KeyPair) -> Self {
|
||||
self.key_pair = Some(key_pair);
|
||||
self
|
||||
}
|
||||
|
||||
/// Attaches a [`LogSink`] to this [`TestContext`].
|
||||
///
|
||||
/// This is useful when using multiple [`TestContext`] instances in one test: it allows
|
||||
/// using a single [`LogSink`] for both contexts. This shows the log messages in
|
||||
/// sequence as they occurred rather than all messages from each context in a single
|
||||
/// block.
|
||||
pub fn with_log_sink(mut self, sink: Sender<Event>) -> Self {
|
||||
self.log_sink = Some(sink);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the [`TestContext`].
|
||||
pub async fn build(self) -> TestContext {
|
||||
let name = self.key_pair.as_ref().map(|key| key.addr.local.clone());
|
||||
|
||||
let test_context = TestContext::new_internal(name, self.log_sink).await;
|
||||
|
||||
if let Some(key_pair) = self.key_pair {
|
||||
test_context
|
||||
.configure_addr(&key_pair.addr.to_string())
|
||||
.await;
|
||||
key::store_self_keypair(&test_context, &key_pair, KeyPairUse::Default)
|
||||
.await
|
||||
.expect("Failed to save key");
|
||||
}
|
||||
test_context
|
||||
}
|
||||
}
|
||||
|
||||
/// A Context and temporary directory.
|
||||
///
|
||||
/// The temporary directory can be used to store the SQLite database,
|
||||
/// see e.g. [test_context] which does this.
|
||||
#[derive(Debug)]
|
||||
pub struct TestContext {
|
||||
pub(crate) struct TestContext {
|
||||
pub ctx: Context,
|
||||
pub dir: TempDir,
|
||||
pub evtracker: EventTracker,
|
||||
/// Channels which should receive events from this context.
|
||||
event_senders: Arc<RwLock<Vec<Sender<Event>>>>,
|
||||
pub evtracker: EvTracker,
|
||||
/// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only.
|
||||
recv_idx: RwLock<u32>,
|
||||
/// Functions to call for events received.
|
||||
event_sinks: Arc<RwLock<Vec<Box<EventSink>>>>,
|
||||
/// Receives panics from sinks ("sink" means "event handler" here)
|
||||
poison_receiver: Receiver<String>,
|
||||
/// Reference to implicit [`LogSink`] so it is dropped together with the context.
|
||||
///
|
||||
/// Only used if no explicit `log_sender` is passed into [`TestContext::new_internal`]
|
||||
/// (which is assumed to be the sending end of a [`LogSink`]).
|
||||
///
|
||||
/// This is a convenience in case only a single [`TestContext`] is used to avoid dealing
|
||||
/// with [`LogSink`]. Never read, thus "dead code", since the only purpose is to
|
||||
/// control when Drop is invoked.
|
||||
#[allow(dead_code)]
|
||||
log_sink: Option<LogSink>,
|
||||
poison_receiver: channel::Receiver<String>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for TestContext {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TestContext")
|
||||
.field("ctx", &self.ctx)
|
||||
.field("dir", &self.dir)
|
||||
.field("recv_idx", &self.recv_idx)
|
||||
.field("event_sinks", &String::from("Vec<EventSink>"))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TestContext {
|
||||
/// Returns the builder to have more control over creating the context.
|
||||
pub fn builder() -> TestContextBuilder {
|
||||
TestContextBuilder::default()
|
||||
}
|
||||
|
||||
/// Creates a new [`TestContext`].
|
||||
///
|
||||
/// The [Context] will be created and have an SQLite database named "db.sqlite" in the
|
||||
@@ -141,33 +80,18 @@ impl TestContext {
|
||||
///
|
||||
/// [Context]: crate::context::Context
|
||||
pub async fn new() -> Self {
|
||||
Self::new_internal(None, None).await
|
||||
Self::new_named(None).await
|
||||
}
|
||||
|
||||
/// Creates a new configured [`TestContext`].
|
||||
///
|
||||
/// This is a shortcut which automatically calls [`TestContext::configure_alice`] after
|
||||
/// creating the context.
|
||||
pub async fn new_alice() -> Self {
|
||||
Self::builder().configure_alice().build().await
|
||||
/// Creates a new [`TestContext`] with a set name used in event logging.
|
||||
pub async fn with_name(name: impl Into<String>) -> Self {
|
||||
Self::new_named(Some(name.into())).await
|
||||
}
|
||||
|
||||
/// Creates a new configured [`TestContext`].
|
||||
///
|
||||
/// This is a shortcut which configures bob@example.net with a fixed key.
|
||||
pub async fn new_bob() -> Self {
|
||||
Self::builder().configure_bob().build().await
|
||||
}
|
||||
async fn new_named(name: Option<String>) -> Self {
|
||||
use rand::Rng;
|
||||
pretty_env_logger::try_init().ok();
|
||||
|
||||
/// Internal constructor.
|
||||
///
|
||||
/// `name` is used to identify this context in e.g. log output. This is useful mostly
|
||||
/// when you have multiple [`TestContext`]s in a test.
|
||||
///
|
||||
/// `log_sender` is assumed to be the sender for a [`LogSink`]. If not supplied a new
|
||||
/// [`LogSink`] will be created so that events are logged to this test when the
|
||||
/// [`TestContext`] is dropped.
|
||||
async fn new_internal(name: Option<String>, log_sender: Option<Sender<Event>>) -> Self {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = rand::thread_rng().gen();
|
||||
@@ -175,26 +99,18 @@ impl TestContext {
|
||||
let mut context_names = CONTEXT_NAMES.write().unwrap();
|
||||
context_names.insert(id, name);
|
||||
}
|
||||
let ctx = Context::new(dbfile.into(), id)
|
||||
let ctx = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
.await
|
||||
.expect("failed to create context");
|
||||
|
||||
let events = ctx.get_event_emitter();
|
||||
|
||||
let (log_sender, log_sink) = match log_sender {
|
||||
Some(sender) => (sender, None),
|
||||
None => {
|
||||
let (sender, sink) = LogSink::create();
|
||||
(sender, Some(sink))
|
||||
}
|
||||
};
|
||||
|
||||
let (evtracker_sender, evtracker_receiver) = channel::unbounded();
|
||||
let event_senders = Arc::new(RwLock::new(vec![log_sender, evtracker_sender]));
|
||||
let senders = Arc::clone(&event_senders);
|
||||
let event_sinks: Arc<RwLock<Vec<Box<EventSink>>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
let sinks = Arc::clone(&event_sinks);
|
||||
let (poison_sender, poison_receiver) = channel::bounded(1);
|
||||
let (evtracker_sender, evtracker_receiver) = channel::unbounded();
|
||||
|
||||
task::spawn(async move {
|
||||
async_std::task::spawn(async move {
|
||||
// Make sure that the test fails if there is a panic on this thread here
|
||||
// (but not if there is a panic on another thread)
|
||||
let looptask_id = task::current().id();
|
||||
@@ -210,26 +126,50 @@ impl TestContext {
|
||||
|
||||
while let Some(event) = events.recv().await {
|
||||
{
|
||||
let sinks = senders.read().await;
|
||||
for sender in sinks.iter() {
|
||||
// Don't block because someone wanted to use a oneshot receiver, use
|
||||
// an unbounded channel if you want all events.
|
||||
sender.try_send(event.clone()).ok();
|
||||
log::debug!("{:?}", event);
|
||||
let sinks = sinks.read().await;
|
||||
for sink in sinks.iter() {
|
||||
sink(event.clone()).await;
|
||||
}
|
||||
}
|
||||
receive_event(&event);
|
||||
evtracker_sender.send(event.typ).await.ok();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
ctx,
|
||||
dir,
|
||||
evtracker: EventTracker(evtracker_receiver),
|
||||
event_senders,
|
||||
evtracker: EvTracker(evtracker_receiver),
|
||||
recv_idx: RwLock::new(0),
|
||||
event_sinks,
|
||||
poison_receiver,
|
||||
log_sink,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new configured [`TestContext`].
|
||||
///
|
||||
/// This is a shortcut which automatically calls [`TestContext::configure_alice`] after
|
||||
/// creating the context.
|
||||
pub async fn new_alice() -> Self {
|
||||
let t = Self::with_name("alice").await;
|
||||
t.configure_alice().await;
|
||||
t
|
||||
}
|
||||
|
||||
/// Creates a new configured [`TestContext`].
|
||||
///
|
||||
/// This is a shortcut which configures bob@example.net with a fixed key.
|
||||
pub async fn new_bob() -> Self {
|
||||
let t = Self::with_name("bob").await;
|
||||
let keypair = bob_keypair();
|
||||
t.configure_addr(&keypair.addr.to_string()).await;
|
||||
key::store_self_keypair(&t, &keypair, key::KeyPairUse::Default)
|
||||
.await
|
||||
.expect("Failed to save Bob's key");
|
||||
t
|
||||
}
|
||||
|
||||
/// Sets a name for this [`TestContext`] if one isn't yet set.
|
||||
///
|
||||
/// This will show up in events logged in the test output.
|
||||
@@ -240,12 +180,32 @@ impl TestContext {
|
||||
.or_insert_with(|| name.into());
|
||||
}
|
||||
|
||||
/// Adds a new [`Event`]s sender.
|
||||
/// Add a new callback which will receive events.
|
||||
///
|
||||
/// Once added, all events emitted by this context will be sent to this channel. This
|
||||
/// is useful if you need to wait for events or make assertions on them.
|
||||
pub async fn add_event_sender(&self, sink: Sender<Event>) {
|
||||
self.event_senders.write().await.push(sink)
|
||||
/// The test context runs an async task receiving all events from the [`Context`], which
|
||||
/// are logged to stdout. This allows you to register additional callbacks which will
|
||||
/// receive all events in case your tests need to watch for a specific event.
|
||||
pub async fn add_event_sink<F, R>(&self, sink: F)
|
||||
where
|
||||
// Aka `F: EventSink` but type aliases are not allowed.
|
||||
F: Fn(Event) -> R + Send + Sync + 'static,
|
||||
R: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let mut sinks = self.event_sinks.write().await;
|
||||
sinks.push(Box::new(move |evt| Box::pin(sink(evt))));
|
||||
}
|
||||
|
||||
/// Configure with alice@example.com.
|
||||
///
|
||||
/// The context will be fake-configured as the alice user, with a pre-generated secret
|
||||
/// key. The email address of the user is returned as a string.
|
||||
pub async fn configure_alice(&self) -> String {
|
||||
let keypair = alice_keypair();
|
||||
self.configure_addr(&keypair.addr.to_string()).await;
|
||||
key::store_self_keypair(&self.ctx, &keypair, key::KeyPairUse::Default)
|
||||
.await
|
||||
.expect("Failed to save Alice's key");
|
||||
keypair.addr.to_string()
|
||||
}
|
||||
|
||||
/// Configure as a given email address.
|
||||
@@ -342,11 +302,13 @@ impl TestContext {
|
||||
///
|
||||
/// Receives a message using the `dc_receive_imf()` pipeline.
|
||||
pub async fn recv_msg(&self, msg: &SentMessage) {
|
||||
let mut idx = self.recv_idx.write().await;
|
||||
*idx += 1;
|
||||
let received_msg =
|
||||
"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(), "INBOX", *idx, false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -552,38 +514,6 @@ impl Drop for TestContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// A receiver of [`Event`]s which will log the events to the captured test stdout.
|
||||
///
|
||||
/// Tests redirect the stdout of the test thread and capture this, showing the captured
|
||||
/// stdout if the test fails. This means printing log messages must be done on the thread
|
||||
/// of the test itself and not from a spawned task.
|
||||
///
|
||||
/// This sink achieves this by printing the events, in the order received, at the time it is
|
||||
/// dropped. Thus to use you must only make sure this sink is dropped in the test itself.
|
||||
///
|
||||
/// To use this create an instance using [`LogSink::create`] and then use the
|
||||
/// [`TestContextBuilder::with_log_sink`].
|
||||
#[derive(Debug)]
|
||||
pub struct LogSink {
|
||||
events: Receiver<Event>,
|
||||
}
|
||||
|
||||
impl LogSink {
|
||||
/// Creates a new [`LogSink`] and returns the attached event sink.
|
||||
pub fn create() -> (Sender<Event>, Self) {
|
||||
let (tx, rx) = channel::unbounded();
|
||||
(tx, Self { events: rx })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LogSink {
|
||||
fn drop(&mut self) {
|
||||
while let Ok(event) = self.events.try_recv() {
|
||||
print_event(&event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw message as it was scheduled to be sent.
|
||||
///
|
||||
/// This is a raw message, probably in the shape DC was planning to send it but not having
|
||||
@@ -614,20 +544,19 @@ impl SentMessage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for alice@example.org from disk.
|
||||
/// Load a pre-generated keypair for alice@example.com from disk.
|
||||
///
|
||||
/// This saves CPU cycles by avoiding having to generate a key.
|
||||
///
|
||||
/// The keypair was created using the crate::key::tests::gen_key test.
|
||||
pub fn alice_keypair() -> KeyPair {
|
||||
let addr = EmailAddress::new("alice@example.org").unwrap();
|
||||
|
||||
let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/alice-public.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
pub fn alice_keypair() -> key::KeyPair {
|
||||
let addr = EmailAddress::new("alice@example.com").unwrap();
|
||||
let public =
|
||||
key::SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
|
||||
.unwrap();
|
||||
let secret =
|
||||
key::SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
|
||||
.unwrap();
|
||||
key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
@@ -638,14 +567,12 @@ pub fn alice_keypair() -> KeyPair {
|
||||
/// Load a pre-generated keypair for bob@example.net from disk.
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn bob_keypair() -> KeyPair {
|
||||
pub fn bob_keypair() -> key::KeyPair {
|
||||
let addr = EmailAddress::new("bob@example.net").unwrap();
|
||||
let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/bob-public.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let public =
|
||||
key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap();
|
||||
let secret =
|
||||
key::SignedSecretKey::from_base64(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
||||
key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
@@ -653,43 +580,40 @@ pub fn bob_keypair() -> KeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility to help wait for and retrieve events.
|
||||
///
|
||||
/// This buffers the events in order they are emitted. This allows consuming events in
|
||||
/// order while looking for the right events using the provided methods.
|
||||
///
|
||||
/// The methods only return [`EventType`] rather than the full [`Event`] since it can only
|
||||
/// be attached to a single [`TestContext`] and therefore the context is already known as
|
||||
/// you will be accessing it as [`TestContext::evtracker`].
|
||||
#[derive(Debug)]
|
||||
pub struct EventTracker(Receiver<Event>);
|
||||
pub struct EvTracker(Receiver<EventType>);
|
||||
|
||||
impl EventTracker {
|
||||
/// Consumes emitted events returning the first matching one.
|
||||
///
|
||||
/// If no matching events are ready this will wait for new events to arrive and time out
|
||||
/// after 10 seconds.
|
||||
pub async fn get_matching<F: Fn(&EventType) -> bool>(&self, event_matcher: F) -> EventType {
|
||||
async move {
|
||||
loop {
|
||||
let event = self.0.recv().await.unwrap();
|
||||
if event_matcher(&event.typ) {
|
||||
return event.typ;
|
||||
impl EvTracker {
|
||||
pub async fn get_info_contains(&self, s: &str) -> EventType {
|
||||
loop {
|
||||
let event = self.0.recv().await.unwrap();
|
||||
if let EventType::Info(i) = &event {
|
||||
if i.contains(s) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
}
|
||||
.timeout(Duration::from_secs(10))
|
||||
.await
|
||||
.expect("timeout waiting for event match")
|
||||
}
|
||||
|
||||
/// Consumes events looking for an [`EventType::Info`] with substring matching.
|
||||
pub async fn get_info_contains(&self, s: &str) -> EventType {
|
||||
self.get_matching(|evt| match evt {
|
||||
EventType::Info(ref msg) => msg.contains(s),
|
||||
_ => false,
|
||||
})
|
||||
.await
|
||||
pub async fn get_matching<F: Fn(EventType) -> bool>(&self, event_matcher: F) -> EventType {
|
||||
const TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
loop {
|
||||
let event = async_std::future::timeout(TIMEOUT, self.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
if event_matcher(event.clone()) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EvTracker {
|
||||
type Target = Receiver<EventType>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,7 +640,7 @@ pub(crate) async fn get_chat_msg(
|
||||
/// Pretty-print an event to stdout
|
||||
///
|
||||
/// Done during tests this is captured by `cargo test` and associated with the test itself.
|
||||
fn print_event(event: &Event) {
|
||||
fn receive_event(event: &Event) {
|
||||
let green = Color::Green.normal();
|
||||
let yellow = Color::Yellow.normal();
|
||||
let red = Color::Red.normal();
|
||||
@@ -835,43 +759,3 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
statestr,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// The following three tests demonstrate, when made to fail, the log output being
|
||||
// directed to the correct test output.
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_with_alice() {
|
||||
let alice = TestContext::builder().configure_alice().build().await;
|
||||
alice.ctx.emit_event(EventType::Info("hello".into()));
|
||||
// panic!("Alice fails");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_with_bob() {
|
||||
let bob = TestContext::builder().configure_bob().build().await;
|
||||
bob.ctx.emit_event(EventType::Info("there".into()));
|
||||
// panic!("Bob fails");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_with_both() {
|
||||
let (log_sender, _log_sink) = LogSink::create();
|
||||
let alice = TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(log_sender.clone())
|
||||
.build()
|
||||
.await;
|
||||
let bob = TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(log_sender)
|
||||
.build()
|
||||
.await;
|
||||
alice.ctx.emit_event(EventType::Info("hello".into()));
|
||||
bob.ctx.emit_event(EventType::Info("there".into()));
|
||||
// panic!("Both fail");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: updated subject\n\
|
||||
Message-ID: <msg2@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -100,13 +100,14 @@ mod tests {
|
||||
\n\
|
||||
second message\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: original subject\n\
|
||||
Message-ID: <msg1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -114,6 +115,7 @@ mod tests {
|
||||
\n\
|
||||
first message\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -135,7 +137,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <msg1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde\n\
|
||||
@@ -144,6 +146,7 @@ mod tests {
|
||||
\n\
|
||||
first message\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -154,7 +157,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <msg3@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde\n\
|
||||
@@ -164,13 +167,14 @@ mod tests {
|
||||
\n\
|
||||
third message\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <msg2@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde\n\
|
||||
@@ -180,6 +184,7 @@ mod tests {
|
||||
\n\
|
||||
second message\n",
|
||||
"INBOX",
|
||||
3,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1,13 +1 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcB
|
||||
KAu4m5C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBC5vossj
|
||||
tTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
|
||||
AAoJEGSwj2Gp7ZRDE3oA/i4MCyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pM
|
||||
APkBjOnGOtx2SZpQvJGTa9h804RY6iDrRuI8A/8tEEXAA7g4BF5Ydd0SCisGAQQB
|
||||
l1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeIeAQY
|
||||
FggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRD
|
||||
Lo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyM
|
||||
h2h6cSn/ATn5QJb35pgo+ivp3jsMAg==
|
||||
=t/Qq
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4MCyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDrRuI8A/8tEEXAA7g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeIeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg==
|
||||
@@ -1,14 +1 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
lFgEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcB
|
||||
KAu4m5AAAQDMpCY4sD5/DUR0jRjGC5WstwShz1q+5Vofo5mY9+XRXRA3tBlBbGlj
|
||||
ZSA8YWxpY2VAZXhhbXBsZS5vcmc+iJAEExYIADgWIQQub6LLI7Uy1yhjS1hksI9h
|
||||
qe2UQwUCXlh13QIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBksI9hqe2U
|
||||
QxN6AP4uDAsgzE7I1qg2aEMF/wPxqEztv1dMCj4YyScv/JtqTAD5AYzpxjrcdkma
|
||||
ULyRk2vYfNOEWOog60biPAP/LRBFwAOcXQReWHXdEgorBgEEAZdVAQUBAQdABu3I
|
||||
1stkhQFPCp5bZbm1Vuu6xYsn6dNSa0Xul6stth0DAQgHAAD/X9y9I/JFBeArkgR3
|
||||
U363cWXXxMCWftS+BDwM9zE4PrgQb4h4BBgWCAAgFiEELm+iyyO1MtcoY0tYZLCP
|
||||
YantlEMFAl5Ydd0CGwwACgkQZLCPYantlEMujwEA5sTwaewZXArM2oK8d5aAmyqG
|
||||
NLcLqC9KVXe0Sb1eYXoBANe5wjJV+gHCjIyHaHpxKf8BOflAlvfmmCj6K+neOwwC
|
||||
=gNT4
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
lFgEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5AAAQDMpCY4sD5/DUR0jRjGC5WstwShz1q+5Vofo5mY9+XRXRA3tBlBbGljZSA8YWxpY2VAZXhhbXBsZS5vcmc+iJAEExYIADgWIQQub6LLI7Uy1yhjS1hksI9hqe2UQwUCXlh13QIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBksI9hqe2UQxN6AP4uDAsgzE7I1qg2aEMF/wPxqEztv1dMCj4YyScv/JtqTAD5AYzpxjrcdkmaULyRk2vYfNOEWOog60biPAP/LRBFwAOcXQReWHXdEgorBgEEAZdVAQUBAQdABu3I1stkhQFPCp5bZbm1Vuu6xYsn6dNSa0Xul6stth0DAQgHAAD/X9y9I/JFBeArkgR3U363cWXXxMCWftS+BDwM9zE4PrgQb4h4BBgWCAAgFiEELm+iyyO1MtcoY0tYZLCPYantlEMFAl5Ydd0CGwwACgkQZLCPYantlEMujwEA5sTwaewZXArM2oK8d5aAmyqGNLcLqC9KVXe0Sb1eYXoBANe5wjJV+gHCjIyHaHpxKf8BOflAlvfmmCj6K+neOwwC
|
||||
@@ -1,30 +1 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA
|
||||
5//PjAzbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOe
|
||||
Jw9kohATSqUtsRO0pFJeDvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeT
|
||||
xc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1JzdKTcDWryrSkvmgFdUqJ7pJDk1HFTt+
|
||||
x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBame1BPsE1PA7VzeTSJ
|
||||
R2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS5u
|
||||
ZXQ+wsCJBBABCAAzAhkBBQJeMMduAhsDBAsJCAcGFQgJCgsCAxYCARYhBMzLWqn2
|
||||
4RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSH9oIIALbpmicuVghM3CloiCgJhPEF
|
||||
LFMaQZRDV/KCVVtBcHAhw6d42q8T50mhs+W3Va5E37DN+wcenj8CgeGPQY3kPp2c
|
||||
nZruYtLhLkZ1+VEay5BQFUMb8kY21XrNTQQET8vc0L8cCLQ7RCgm1tGiFVp1nqbj
|
||||
mGUdoru90ksoufWfoqVPjNrW+9eHFvY/Z7PqchCdMnbKOJiwwv4E3NDTySZ1UVZn
|
||||
DztGy95Aa8OZ3cntvbq4JVi7S+N38rRPPPzpZKx+M4DUGfDAoaq7O/Xemyk1sP6C
|
||||
/NgQvS8rri54PgkMgKSS4TyyEzdM+fzeNYFPXFGTbgj4p0pSueQV7/JUfYHRfe3O
|
||||
wE0EXjDHVwEIAKIHgS2yI2niSCN1tqcbLvkhLrEJCVcpGxmA7asl1flwWYrGOBhN
|
||||
JE2sCuZqkofqw6qrgsQ4GFgUU5xmcBCqIZ49jRu+aY38lT4WDFHSbe/mGtaIhb2Z
|
||||
YK6zo9W7Y3r6ud8hbUKJTDfl9qEvJpX/Y0syMjwng8SZNTdYMWgAE4NwcgMgdU3d
|
||||
MA3RT6ePJ4vKs38hmXmInLyZce+GJzmo2tpZyP8viPS7JpqojoCPB3G5h9aHeakp
|
||||
1Y4XKQaExANeWCyBJEhNwtNEOVEpQ0txFYPyDrtxV5y5e79IUP418r/PHsnH6Unx
|
||||
XGzB6LfVbSeEyDyKEl+w0PrlNklySomTZFUAEQEAAcLAdgQYAQgAIAUCXjDHbgIb
|
||||
DBYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSHVCIIAIH694HkQLXR
|
||||
AJlXmi8K/xMVP96ywJovL/B5l4S/vk/iR4P1lYsF55A3Z2PK/iFtwAgVsppcBIPB
|
||||
lqSI0GPDMvEIxj7UFOQfQzVpDes29wG8grHJEJqI/4TlRjOacxTGaJ5fIMsLXJD6
|
||||
nLBuoN5Z6zm3LjqIyOx4ZGrwradPO95OMGT2Xll3YNzUqSWe33RJLqNQ5ea9I7+q
|
||||
vKnW5Z9Yt5nQwOo8yD+f5fql8904B3eAyLqxgkdLmngAWmYhc7KOaKdAsx7TXBAK
|
||||
soeHk51OPk59u7EbX35HWD6snl/phJdUYDXiddyYN/n2ZY9g80ycle2JfgpfrQGl
|
||||
h7oJqgCjZuk=
|
||||
=14Cq
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA5//PjAzbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOeJw9kohATSqUtsRO0pFJeDvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeTxc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1JzdKTcDWryrSkvmgFdUqJ7pJDk1HFTt+x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBame1BPsE1PA7VzeTSJR2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJeMMduAhsDBAsJCAcGFQgJCgsCAxYCARYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSH9oIIALbpmicuVghM3CloiCgJhPEFLFMaQZRDV/KCVVtBcHAhw6d42q8T50mhs+W3Va5E37DN+wcenj8CgeGPQY3kPp2cnZruYtLhLkZ1+VEay5BQFUMb8kY21XrNTQQET8vc0L8cCLQ7RCgm1tGiFVp1nqbjmGUdoru90ksoufWfoqVPjNrW+9eHFvY/Z7PqchCdMnbKOJiwwv4E3NDTySZ1UVZnDztGy95Aa8OZ3cntvbq4JVi7S+N38rRPPPzpZKx+M4DUGfDAoaq7O/Xemyk1sP6C/NgQvS8rri54PgkMgKSS4TyyEzdM+fzeNYFPXFGTbgj4p0pSueQV7/JUfYHRfe3OwE0EXjDHVwEIAKIHgS2yI2niSCN1tqcbLvkhLrEJCVcpGxmA7asl1flwWYrGOBhNJE2sCuZqkofqw6qrgsQ4GFgUU5xmcBCqIZ49jRu+aY38lT4WDFHSbe/mGtaIhb2ZYK6zo9W7Y3r6ud8hbUKJTDfl9qEvJpX/Y0syMjwng8SZNTdYMWgAE4NwcgMgdU3dMA3RT6ePJ4vKs38hmXmInLyZce+GJzmo2tpZyP8viPS7JpqojoCPB3G5h9aHeakp1Y4XKQaExANeWCyBJEhNwtNEOVEpQ0txFYPyDrtxV5y5e79IUP418r/PHsnH6UnxXGzB6LfVbSeEyDyKEl+w0PrlNklySomTZFUAEQEAAcLAdgQYAQgAIAUCXjDHbgIbDBYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSHVCIIAIH694HkQLXRAJlXmi8K/xMVP96ywJovL/B5l4S/vk/iR4P1lYsF55A3Z2PK/iFtwAgVsppcBIPBlqSI0GPDMvEIxj7UFOQfQzVpDes29wG8grHJEJqI/4TlRjOacxTGaJ5fIMsLXJD6nLBuoN5Z6zm3LjqIyOx4ZGrwradPO95OMGT2Xll3YNzUqSWe33RJLqNQ5ea9I7+qvKnW5Z9Yt5nQwOo8yD+f5fql8904B3eAyLqxgkdLmngAWmYhc7KOaKdAsx7TXBAKsoeHk51OPk59u7EbX35HWD6snl/phJdUYDXiddyYN/n2ZY9g80ycle2JfgpfrQGlh7oJqgCjZuk=
|
||||
@@ -1,57 +1 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
7wy7sMVvFBX3zWfjxDU=
|
||||
=gk0q
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
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
|
||||
@@ -1,30 +1 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBF48nc4BCADSWr9fE2K1FcE7eSW/Z0MOuzdozKmQJsrmkb7Abssd1yARZuf3
|
||||
+YYh5WqeKJrSTUFD+mJNUhqtodBqBxFH+JzITMG5qGcAdBwXaeJYFMquezLAuVZs
|
||||
Z2b+Njk9Fw3XF4cUZB58ItO/ViFgDi4r5lqsdiiMvGrLmQD2m2BOB8U52ibFhnSV
|
||||
vvYi6rlsZ1HfqB+efD8InPKfiMKDqu909fgVchGJ7OwtTKanaF3v+rzQvXsnVal1
|
||||
yfc0YsmOM0ekWDhIR5EoSg8pJlBHVc/yBrxQWq9h2e2PUntLK29/qp/k/xsQHN3B
|
||||
Aj/kuQQPzMARcUUvxo9Aq8n4CMzoFmrK2G+bABEBAAHNFTxjaGFybGllQGV4YW1w
|
||||
bGUubmV0PsLAiQQQAQgAMwIZAQUCXjyd5AIbAwQLCQgHBhUICQoLAgMWAgEWIQRm
|
||||
PL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV7/tMCACSeaPalMt/EwsCGxnW
|
||||
NNEWPX7fMSiAZx1bbPYULIroEBVgmObOkDqoB6y9tWQB/HUq44ZIYdhdw7wlzZp9
|
||||
d81dtxGA+QcRkTy8fr/P2KmQhwjV9m22xBCGABPSpFG+/iONEJADEA8Oe3SogI/i
|
||||
PksepLs9Gg9Ix9Qb1ZMt6+GErE7u0aAamW03NQW7SlgLruAKhjKLjP1wxryK4h0M
|
||||
dac6CuGZQH/G3yZX3OQPkE2BGjEefhzj0yEKOGhYM716uswqecjB8HoYh5RaDcE+
|
||||
Shcze/OhSQesqj0RxoCpeN3/6QGcC6DT70Qrqeri0RtA495SS3YTqb2p9U2WISOE
|
||||
JIdXzsBNBF48nc4BCADKwwUPt5jp4yIdVdVdLyXGuJS/pC6t5Q64QlcEzHH03eVJ
|
||||
HYH42shyWPiTE7HU3giqLhnPXjYN8/piONBQ7dQqTkYJtrx0aUBAoQ9p0p6eMGoY
|
||||
4pMxrclwxnvGE+2YplFNzXkcqxkSPy4n5kOPeq55tVHkWG+gTblZdlT9sKM8Ksr8
|
||||
gF728eaWgt+TQCnhuwa9h9BVzLAZquaATm7PvPKamMd7jIVoISXrZKuPBVrSDbx+
|
||||
DElg0HKj8sOxh3lNz2rmRTGWDbURPhqvVyQ2tXryEVfMZRauR9B9YCYEeedRwDrO
|
||||
SME2LaoU483w0j2Vnb4GkOLpg8Wrw+fnIcM32GXxABEBAAHCwHYEGAEIACAFAl48
|
||||
neQCGwwWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV78HXCACyxjrX
|
||||
pr7GKqQOMJGvNKytuVf7gHUbwLq7oAXoM6PixPkfZIArH/EHnt0GaBWq2r08REj2
|
||||
IRQ8t/8zfkWa9L2RxITez3dRkOUjf49i+J9g3oyleJDZVrAhoU2Uzwb+40tTGaEr
|
||||
GpD6m3GqIe5wE7gXFBAf0IwQmIjic64ULCE5j2qaWMxwfvKIDuSD/bN+mSqlQbme
|
||||
keX6bud4rMoGIUnkQthNAQwBQHOjkPZvdjXiDGFxmpDlcJv5e9LYv8kb141JmYIo
|
||||
n9iIGWF3L52+SbvYXFAoPi4qovsdgUXezTRoaiR8Mgft4KUZZIeXUhC7PRuut8Ps
|
||||
bN2P0WCh1CjIJph9
|
||||
=WssU
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
xsBNBF48nc4BCADSWr9fE2K1FcE7eSW/Z0MOuzdozKmQJsrmkb7Abssd1yARZuf3+YYh5WqeKJrSTUFD+mJNUhqtodBqBxFH+JzITMG5qGcAdBwXaeJYFMquezLAuVZsZ2b+Njk9Fw3XF4cUZB58ItO/ViFgDi4r5lqsdiiMvGrLmQD2m2BOB8U52ibFhnSVvvYi6rlsZ1HfqB+efD8InPKfiMKDqu909fgVchGJ7OwtTKanaF3v+rzQvXsnVal1yfc0YsmOM0ekWDhIR5EoSg8pJlBHVc/yBrxQWq9h2e2PUntLK29/qp/k/xsQHN3BAj/kuQQPzMARcUUvxo9Aq8n4CMzoFmrK2G+bABEBAAHNFTxjaGFybGllQGV4YW1wbGUubmV0PsLAiQQQAQgAMwIZAQUCXjyd5AIbAwQLCQgHBhUICQoLAgMWAgEWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV7/tMCACSeaPalMt/EwsCGxnWNNEWPX7fMSiAZx1bbPYULIroEBVgmObOkDqoB6y9tWQB/HUq44ZIYdhdw7wlzZp9d81dtxGA+QcRkTy8fr/P2KmQhwjV9m22xBCGABPSpFG+/iONEJADEA8Oe3SogI/iPksepLs9Gg9Ix9Qb1ZMt6+GErE7u0aAamW03NQW7SlgLruAKhjKLjP1wxryK4h0Mdac6CuGZQH/G3yZX3OQPkE2BGjEefhzj0yEKOGhYM716uswqecjB8HoYh5RaDcE+Shcze/OhSQesqj0RxoCpeN3/6QGcC6DT70Qrqeri0RtA495SS3YTqb2p9U2WISOEJIdXzsBNBF48nc4BCADKwwUPt5jp4yIdVdVdLyXGuJS/pC6t5Q64QlcEzHH03eVJHYH42shyWPiTE7HU3giqLhnPXjYN8/piONBQ7dQqTkYJtrx0aUBAoQ9p0p6eMGoY4pMxrclwxnvGE+2YplFNzXkcqxkSPy4n5kOPeq55tVHkWG+gTblZdlT9sKM8Ksr8gF728eaWgt+TQCnhuwa9h9BVzLAZquaATm7PvPKamMd7jIVoISXrZKuPBVrSDbx+DElg0HKj8sOxh3lNz2rmRTGWDbURPhqvVyQ2tXryEVfMZRauR9B9YCYEeedRwDrOSME2LaoU483w0j2Vnb4GkOLpg8Wrw+fnIcM32GXxABEBAAHCwHYEGAEIACAFAl48neQCGwwWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV78HXCACyxjrXpr7GKqQOMJGvNKytuVf7gHUbwLq7oAXoM6PixPkfZIArH/EHnt0GaBWq2r08REj2IRQ8t/8zfkWa9L2RxITez3dRkOUjf49i+J9g3oyleJDZVrAhoU2Uzwb+40tTGaErGpD6m3GqIe5wE7gXFBAf0IwQmIjic64ULCE5j2qaWMxwfvKIDuSD/bN+mSqlQbmekeX6bud4rMoGIUnkQthNAQwBQHOjkPZvdjXiDGFxmpDlcJv5e9LYv8kb141JmYIon9iIGWF3L52+SbvYXFAoPi4qovsdgUXezTRoaiR8Mgft4KUZZIeXUhC7PRuut8PsbN2P0WCh1CjIJph9
|
||||
@@ -1,57 +1 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
rTWUJ5bdZzZ33OJ9qZNbctFs
|
||||
=x3bv
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
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
|
||||
@@ -1,30 +1 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBF48no8BCADwN4PxXewE4iP3HHn/FE5r0x8x8byo9mJIIEOrjL1k0JWIcz3K
|
||||
vC5evfZ3M/ZK2QPBLhFYOck0+gCAR/eH1zgDZeYXUB1CWUoCKlZ9jH1UbfzE34Zx
|
||||
xiwCZy2ZdMnKxDF/ezyXsVvoEhGQE1+8A9Xy/ZXlplUbyYVacgdK9fQlh50d8FOU
|
||||
+7eplUts/SzXx52T83tB98oS9QVgJ4qIC+fu0xMgqdH7e0ithSc/owfYkKjH+isb
|
||||
D4n9U8KSqb4zzf2Y2gz7h6jdM+PPuWqTSARbH83j7qG8q4IjC7cO3CgSryk3f/ML
|
||||
kquKD8z9ybvrYKXSF04RMEuhgBxXnE0vA3ZpABEBAAHNETxkb21AZXhhbXBsZS5u
|
||||
ZXQ+wsCJBBABCAAzAhkBBQJePJ6rAhsDBAsJCAcGFQgJCgsCAxYCARYhBJaMlJFd
|
||||
jIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaoXGQIAM6KRSMBLKOb0IlXqSCy1Ve9
|
||||
MF02fDNmGFw7xC5v2gKpdDZZJctRLxH3ZFgQmpcSIZ4P/A7XcYnlIxxGxwzyhMma
|
||||
s9eJ1w3sPbvX5vJ4ZYGzooZSFUQXgO6+bm7qQlf7gFcsuTkF5n6cx9tDPcyxy01b
|
||||
7eaexTXoWpYDX110TFbpklprub2QpI2w4gy5y1d/pfOmJYGHgMEALgAEKQCWjQ/j
|
||||
US6pOVEH+pT1ddlv9vq37CmTp00bKbj+Z59mqeUBjE5+RmNWaLwkdZSR1o+LKNw/
|
||||
Gl07gzayRjJyh0Txj4LgHtVSKvGj6GexhddEgqp8qwMMfNbbqWATh9zHNJ+gjSzO
|
||||
wE0EXjyejwEIAKbISj1O986suBXKzHPlEzqetkGEWNV4OHHfhphIxsoW/f3IOdqv
|
||||
0uqwz9AhzXE6YbsFzrc6ZwP+pYTH51T1ugtG0LBMFikh4tyyYjJV0v9gUq2Jqixx
|
||||
HfJ/XfVryKO1OWqs2GtBxaQP5FVf+vQqSPpAz4B/6hqwUvx2XdI6bbGn1fQlTQyf
|
||||
GR6Uclm5kIaY8/VBzuVySaAycfOdBhjoyYhl2eMxdxz4NCb6pwNkl6KbjSfGHVpf
|
||||
R6fbuTihIbZFL1qAfYi6BnvrB9HgnfYItIA9VV2SlLIRfBgxgG9vxKmD83j9r5Dn
|
||||
/e1KC5PoMndMZ5I410H06kjR6dLJbTuy7+MAEQEAAcLAdgQYAQgAIAUCXjyeqwIb
|
||||
DBYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaon5UIAMJLU7EKjCsJ
|
||||
4ldIYYRYE7wzVO2DpiFY5yEVu/Jq2O2C+go7b2oBgUYYB3ExwMUwPnU8H6j+jbzu
|
||||
kxOltKeRZ1QU1d9iwzYVHsP3kw02DceoyMaab0j1DLHPSVPSmsP5/U0eFK++vFvs
|
||||
Y8KuwRUPLK+m7+qFET2gidyRSJGqyiJlEz+wyR3b44Ff1C3RDdRx8EzPVYx+gXRG
|
||||
LIBqsBNuSLhimiwAekMgnpAdsQu8WTCPQSO2Yrz6I6uFVNAr8FnnFoeclwLHbQ++
|
||||
aWhBTuhUp9oJ/388ySkEuMAZrd4YU/RwthM4zsW/VAf936FBkvi8FBJQ0Vdps1xk
|
||||
sQeXK7hQrVw=
|
||||
=eIjV
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
xsBNBF48no8BCADwN4PxXewE4iP3HHn/FE5r0x8x8byo9mJIIEOrjL1k0JWIcz3KvC5evfZ3M/ZK2QPBLhFYOck0+gCAR/eH1zgDZeYXUB1CWUoCKlZ9jH1UbfzE34ZxxiwCZy2ZdMnKxDF/ezyXsVvoEhGQE1+8A9Xy/ZXlplUbyYVacgdK9fQlh50d8FOU+7eplUts/SzXx52T83tB98oS9QVgJ4qIC+fu0xMgqdH7e0ithSc/owfYkKjH+isbD4n9U8KSqb4zzf2Y2gz7h6jdM+PPuWqTSARbH83j7qG8q4IjC7cO3CgSryk3f/MLkquKD8z9ybvrYKXSF04RMEuhgBxXnE0vA3ZpABEBAAHNETxkb21AZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJePJ6rAhsDBAsJCAcGFQgJCgsCAxYCARYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaoXGQIAM6KRSMBLKOb0IlXqSCy1Ve9MF02fDNmGFw7xC5v2gKpdDZZJctRLxH3ZFgQmpcSIZ4P/A7XcYnlIxxGxwzyhMmas9eJ1w3sPbvX5vJ4ZYGzooZSFUQXgO6+bm7qQlf7gFcsuTkF5n6cx9tDPcyxy01b7eaexTXoWpYDX110TFbpklprub2QpI2w4gy5y1d/pfOmJYGHgMEALgAEKQCWjQ/jUS6pOVEH+pT1ddlv9vq37CmTp00bKbj+Z59mqeUBjE5+RmNWaLwkdZSR1o+LKNw/Gl07gzayRjJyh0Txj4LgHtVSKvGj6GexhddEgqp8qwMMfNbbqWATh9zHNJ+gjSzOwE0EXjyejwEIAKbISj1O986suBXKzHPlEzqetkGEWNV4OHHfhphIxsoW/f3IOdqv0uqwz9AhzXE6YbsFzrc6ZwP+pYTH51T1ugtG0LBMFikh4tyyYjJV0v9gUq2JqixxHfJ/XfVryKO1OWqs2GtBxaQP5FVf+vQqSPpAz4B/6hqwUvx2XdI6bbGn1fQlTQyfGR6Uclm5kIaY8/VBzuVySaAycfOdBhjoyYhl2eMxdxz4NCb6pwNkl6KbjSfGHVpfR6fbuTihIbZFL1qAfYi6BnvrB9HgnfYItIA9VV2SlLIRfBgxgG9vxKmD83j9r5Dn/e1KC5PoMndMZ5I410H06kjR6dLJbTuy7+MAEQEAAcLAdgQYAQgAIAUCXjyeqwIbDBYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaon5UIAMJLU7EKjCsJ4ldIYYRYE7wzVO2DpiFY5yEVu/Jq2O2C+go7b2oBgUYYB3ExwMUwPnU8H6j+jbzukxOltKeRZ1QU1d9iwzYVHsP3kw02DceoyMaab0j1DLHPSVPSmsP5/U0eFK++vFvsY8KuwRUPLK+m7+qFET2gidyRSJGqyiJlEz+wyR3b44Ff1C3RDdRx8EzPVYx+gXRGLIBqsBNuSLhimiwAekMgnpAdsQu8WTCPQSO2Yrz6I6uFVNAr8FnnFoeclwLHbQ++aWhBTuhUp9oJ/388ySkEuMAZrd4YU/RwthM4zsW/VAf936FBkvi8FBJQ0Vdps1xksQeXK7hQrVw=
|
||||
@@ -1,57 +1 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
0Vdps1xksQeXK7hQrVw=
|
||||
=LxTs
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
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
|
||||
@@ -1,30 +1 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBF48nxEBCADRa0HRyoj7KdbchkVycHq5jYxYN4NJsRf1bojhuifuLYlJQu7I
|
||||
7Rp862sbvSPN9tM/3dQF6fTyUllFkhoS50fUaf/bJWwi5XWVsLa1+DdOW4As2zkJ
|
||||
slu6Hp/hUFM2FXthRYIwg3c2jparkNtsLyQvtDsG62Acg7dr+NwZ5mxepyJ5WkXc
|
||||
iOPLp9egcrTVoBbcnn4gzj2Wx6MkUByY+bENtUrqsZerWOt+F75DIWkHwzo5Vwfp
|
||||
vH6RrIJabu8KLMLoUTZoovQcDKfk0yv8bdBLEQf84SLY+0BCZ6aZHrqnwEmDBeFG
|
||||
fTcIMFsBxUZfvsMYlAiG6khbZzhQxJ44+YaxABEBAAHNEzxlbGVuYUBleGFtcGxl
|
||||
Lm5ldD7CwIkEEAEIADMCGQEFAl48ny0CGwMECwkIBwYVCAkKCwIDFgIBFiEEuGWG
|
||||
tt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noIpsAgAiP+E48xEhCvEHVIMiQig
|
||||
QvC3kucg2TRk0yrp0ydDeS19l8jXkN8lA9byXVq5VDg8Bs4tN9WR/Gy8x9Xmd6/V
|
||||
yFiquBqGTObO/1u1kegHR0+sp5FxxffZhGoCRBIW4iXww1BzcGUd+TeRlInl5adG
|
||||
pA6vQAGifLsKQQU9/I0bSRBAMGbo0PKZ+EllTLqDeo4D4ELNO6CKyh65FExUIZTt
|
||||
bWZTyLn95ekypUIhgEAKgEs/qE74nJBqcrzkXn3xmDJetYxw2xQzwS+rIwR12ONu
|
||||
b01nm/74Z30zbMViwQF4yTV7VB9uklY6WayCBpT3BkFQiYRPt7lnipBI2PplYXvY
|
||||
Is7ATQRePJ8RAQgAwvvNWB4eABzpUylyhI7q8WNK8GHCGLfqprSLFiAv14psluRW
|
||||
9MezLEFM1N8Az5yqzs3hsuEMiIBPiLrkW8ZkCledlYENorM/6G5+xK9TI4iWnP1L
|
||||
P+qESGGNSF7pXciZE7/XrE/CPL06nXuJRd1qOHvIUnaCQRiqLFPkUG3KhtC+/Ayv
|
||||
z6l2RvSqoTTayxYckgigkEneS/RXMSYnG/A5RJB0fOACp39HzHN/XU7JFLz8WlXg
|
||||
LfWJi7v9uVft9QBCtVseWF+ElKJ5NkVNRrV/SQwFYB91Y+LHqKz6CTQq2Di0vGHj
|
||||
Y9ecai16D/CGUqkJg9t5jnLxZvQeR70o13JqlQARAQABwsB2BBgBCAAgBQJePJ8u
|
||||
AhsMFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noLj2gf9EuLnhKUv
|
||||
0rWW2HShJlKF5oqKmx6tSoNMOd5hbJzL1Z9d7ZjFQJS25jAzl9V6858heAm3HYTl
|
||||
Jfg2RkzHte12k0hr9Svbpadf/OYB2UVsAwGRFGbdq/3H/ZaP5EbCdv8Egcx6pjdg
|
||||
vXb5n6gaMj7LJG4/YvokGLOx3VgNNRdqB0gtKWwN0tzdM/6YfDJTEVDbMUR3aFqY
|
||||
LTSB8KCjNtWcnWO2a17P1Qf6fDhcxpyELkLb1T4sPD4Tz9bFPTJzL87uS4+Ba+Xq
|
||||
2YE7aWa6e+C2HMgH6OghDLqoGpeNy01N3XSikyoFNdEmPYY/RdmuJNSuuyptQHgz
|
||||
h1vIOV7AOoQ+Rw==
|
||||
=THka
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
xsBNBF48nxEBCADRa0HRyoj7KdbchkVycHq5jYxYN4NJsRf1bojhuifuLYlJQu7I7Rp862sbvSPN9tM/3dQF6fTyUllFkhoS50fUaf/bJWwi5XWVsLa1+DdOW4As2zkJslu6Hp/hUFM2FXthRYIwg3c2jparkNtsLyQvtDsG62Acg7dr+NwZ5mxepyJ5WkXciOPLp9egcrTVoBbcnn4gzj2Wx6MkUByY+bENtUrqsZerWOt+F75DIWkHwzo5VwfpvH6RrIJabu8KLMLoUTZoovQcDKfk0yv8bdBLEQf84SLY+0BCZ6aZHrqnwEmDBeFGfTcIMFsBxUZfvsMYlAiG6khbZzhQxJ44+YaxABEBAAHNEzxlbGVuYUBleGFtcGxlLm5ldD7CwIkEEAEIADMCGQEFAl48ny0CGwMECwkIBwYVCAkKCwIDFgIBFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noIpsAgAiP+E48xEhCvEHVIMiQigQvC3kucg2TRk0yrp0ydDeS19l8jXkN8lA9byXVq5VDg8Bs4tN9WR/Gy8x9Xmd6/VyFiquBqGTObO/1u1kegHR0+sp5FxxffZhGoCRBIW4iXww1BzcGUd+TeRlInl5adGpA6vQAGifLsKQQU9/I0bSRBAMGbo0PKZ+EllTLqDeo4D4ELNO6CKyh65FExUIZTtbWZTyLn95ekypUIhgEAKgEs/qE74nJBqcrzkXn3xmDJetYxw2xQzwS+rIwR12ONub01nm/74Z30zbMViwQF4yTV7VB9uklY6WayCBpT3BkFQiYRPt7lnipBI2PplYXvYIs7ATQRePJ8RAQgAwvvNWB4eABzpUylyhI7q8WNK8GHCGLfqprSLFiAv14psluRW9MezLEFM1N8Az5yqzs3hsuEMiIBPiLrkW8ZkCledlYENorM/6G5+xK9TI4iWnP1LP+qESGGNSF7pXciZE7/XrE/CPL06nXuJRd1qOHvIUnaCQRiqLFPkUG3KhtC+/Ayvz6l2RvSqoTTayxYckgigkEneS/RXMSYnG/A5RJB0fOACp39HzHN/XU7JFLz8WlXgLfWJi7v9uVft9QBCtVseWF+ElKJ5NkVNRrV/SQwFYB91Y+LHqKz6CTQq2Di0vGHjY9ecai16D/CGUqkJg9t5jnLxZvQeR70o13JqlQARAQABwsB2BBgBCAAgBQJePJ8uAhsMFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noLj2gf9EuLnhKUv0rWW2HShJlKF5oqKmx6tSoNMOd5hbJzL1Z9d7ZjFQJS25jAzl9V6858heAm3HYTlJfg2RkzHte12k0hr9Svbpadf/OYB2UVsAwGRFGbdq/3H/ZaP5EbCdv8Egcx6pjdgvXb5n6gaMj7LJG4/YvokGLOx3VgNNRdqB0gtKWwN0tzdM/6YfDJTEVDbMUR3aFqYLTSB8KCjNtWcnWO2a17P1Qf6fDhcxpyELkLb1T4sPD4Tz9bFPTJzL87uS4+Ba+Xq2YE7aWa6e+C2HMgH6OghDLqoGpeNy01N3XSikyoFNdEmPYY/RdmuJNSuuyptQHgzh1vIOV7AOoQ+Rw==
|
||||
@@ -1,57 +1 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
1I0FB1+JW+NltRFvfI4DFQ==
|
||||
=ghin
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
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
|
||||
@@ -1,30 +1 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBF48n2MBCAC741/YKzU76pWNijBCYC+hGSKZvBdanm4eJzi/eKPevUypdW2G
|
||||
JzFFJxWjx2wIcV8cLwQvY2Z+ieJaPPwGbUr0nH2S9lmghkCCKjGxWcmrx3sQr7x9
|
||||
431KYOM6+/SAZNjHYjWwcwy2QhE6J7qfC74LjTF4pv+ZRgSZDC+O70NpTWdNdwi/
|
||||
0Kn9gdj5diSwJmnBxQBGp/tnZuu3XGZrgKXlGlPspRMF3Ug2WmvyBhxF8KKoosia
|
||||
Xlumc5gFFuFRnoAVfp/6yehO44l9bYz0zwYWahxmLhShQVhfcH3YBg0l7hMkfC2F
|
||||
p8fDF7Hr1PUOJcBY50VDN0zlV9Bi1iiPofAtABEBAAHNEzxmaW9uYUBleGFtcGxl
|
||||
Lm5ldD7CwIkEEAEIADMCGQEFAl48n3wCGwMECwkIBwYVCAkKCwIDFgIBFiEEyLpQ
|
||||
v0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZUGWQgApCPQYmfa76xcscYBWg3D
|
||||
vMAm0Exk/LvbN74MIqADHIgaNFUHoThTPDVAPeh5ogra/kg4QaLctivC81S2VOPI
|
||||
c/4LGEFJekvythXUgSLNjhlR63Va5ObMV4UegUH9c0O8MGndkQFxiwy98D1bzNyl
|
||||
3mCVOUECVZiJJbxUZwBKj4iowa7kgX/FrdjZIJrz50NMExcDuxvc+MGyDr4YQHrZ
|
||||
TbCjcrJkxufklwsWme0qneuPTxwW2+TnDjHkDaYDdwEnoKRkwlXEy3Lu9ZhswlaW
|
||||
TgdtClpf2geb7tCAPQ0cd2V5m6NnKMzXrnFuJXB6Xkvp32Tyuq4ebILsbCfpLV/H
|
||||
zM7ATQRePJ9jAQgAzQGckmcFjCViSkwGLROhi2Gvb85ABXbkKPMg1x27LraP9YWN
|
||||
Joq2GOy2vv8r8m2q+FpCR25a0NdWlbyiQpPuEWh0udJnNUm+6j5j6PSAmlRRDMhD
|
||||
H4QwzJC+B+lM6NxvSRhxBBtwFAvMy0qeNhv1UCBaeQUHoFCODqJRYOywj7ZGXdQy
|
||||
ttIAlC5PtVw1cV+J8/TGXNrE9DNgFGAm1BPgw/lE1OjVbF8l6NbDdi9YJoOm9mpf
|
||||
fUPlUZMZh3+a5+J6F3KjYwNyi4wi3Y3Pt4avXEo+ib15XNhJcwMslc49La2T8PMn
|
||||
pf3nLClxynJjYDiMuzujcbBWbfVF1383P0KikQARAQABwsB2BBgBCAAgBQJePJ98
|
||||
AhsMFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZX1eAf9GnrA9nCd
|
||||
s3OmtCUpRmVEDar29DfS79B4vBfoYXYb5kRIOxJV6yjfGYRh2IJ0CTfNYkp4AuRC
|
||||
/jEHPXlVUD92Vcb0wSfwO32mMw75FRzIS7/IcwWAauWOtpao3J/tsxHWkSxfh5Zo
|
||||
6vzODQY4k8eHnitxpXT0xSIjnYmVRMuqfb3NELk3PkF52+Fte4zKfBCP80HqO3Bd
|
||||
VCBlQNpZiOMW+yFO/8VqLmB9442nGGhuSfCeBystI3Er0SYSDqNbg5uetBaP1MOo
|
||||
mIZuuxhv3T5jD9DeEDHGOqDjPZ8dMdQYCnYmiVaMb8ECsKG5eMvS8Imss1ho2iz4
|
||||
sjYdVzODl8T0zQ==
|
||||
=2jSq
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
xsBNBF48n2MBCAC741/YKzU76pWNijBCYC+hGSKZvBdanm4eJzi/eKPevUypdW2GJzFFJxWjx2wIcV8cLwQvY2Z+ieJaPPwGbUr0nH2S9lmghkCCKjGxWcmrx3sQr7x9431KYOM6+/SAZNjHYjWwcwy2QhE6J7qfC74LjTF4pv+ZRgSZDC+O70NpTWdNdwi/0Kn9gdj5diSwJmnBxQBGp/tnZuu3XGZrgKXlGlPspRMF3Ug2WmvyBhxF8KKoosiaXlumc5gFFuFRnoAVfp/6yehO44l9bYz0zwYWahxmLhShQVhfcH3YBg0l7hMkfC2Fp8fDF7Hr1PUOJcBY50VDN0zlV9Bi1iiPofAtABEBAAHNEzxmaW9uYUBleGFtcGxlLm5ldD7CwIkEEAEIADMCGQEFAl48n3wCGwMECwkIBwYVCAkKCwIDFgIBFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZUGWQgApCPQYmfa76xcscYBWg3DvMAm0Exk/LvbN74MIqADHIgaNFUHoThTPDVAPeh5ogra/kg4QaLctivC81S2VOPIc/4LGEFJekvythXUgSLNjhlR63Va5ObMV4UegUH9c0O8MGndkQFxiwy98D1bzNyl3mCVOUECVZiJJbxUZwBKj4iowa7kgX/FrdjZIJrz50NMExcDuxvc+MGyDr4YQHrZTbCjcrJkxufklwsWme0qneuPTxwW2+TnDjHkDaYDdwEnoKRkwlXEy3Lu9ZhswlaWTgdtClpf2geb7tCAPQ0cd2V5m6NnKMzXrnFuJXB6Xkvp32Tyuq4ebILsbCfpLV/HzM7ATQRePJ9jAQgAzQGckmcFjCViSkwGLROhi2Gvb85ABXbkKPMg1x27LraP9YWNJoq2GOy2vv8r8m2q+FpCR25a0NdWlbyiQpPuEWh0udJnNUm+6j5j6PSAmlRRDMhDH4QwzJC+B+lM6NxvSRhxBBtwFAvMy0qeNhv1UCBaeQUHoFCODqJRYOywj7ZGXdQyttIAlC5PtVw1cV+J8/TGXNrE9DNgFGAm1BPgw/lE1OjVbF8l6NbDdi9YJoOm9mpffUPlUZMZh3+a5+J6F3KjYwNyi4wi3Y3Pt4avXEo+ib15XNhJcwMslc49La2T8PMnpf3nLClxynJjYDiMuzujcbBWbfVF1383P0KikQARAQABwsB2BBgBCAAgBQJePJ98AhsMFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZX1eAf9GnrA9nCds3OmtCUpRmVEDar29DfS79B4vBfoYXYb5kRIOxJV6yjfGYRh2IJ0CTfNYkp4AuRC/jEHPXlVUD92Vcb0wSfwO32mMw75FRzIS7/IcwWAauWOtpao3J/tsxHWkSxfh5Zo6vzODQY4k8eHnitxpXT0xSIjnYmVRMuqfb3NELk3PkF52+Fte4zKfBCP80HqO3BdVCBlQNpZiOMW+yFO/8VqLmB9442nGGhuSfCeBystI3Er0SYSDqNbg5uetBaP1MOomIZuuxhv3T5jD9DeEDHGOqDjPZ8dMdQYCnYmiVaMb8ECsKG5eMvS8Imss1ho2iz4sjYdVzODl8T0zQ==
|
||||
@@ -1,57 +1 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
WGjaLPiyNh1XM4OXxPTN
|
||||
=C6Iy
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
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
|
||||
@@ -1,10 +1,10 @@
|
||||
Return-Path: <alice@example.org>
|
||||
Delivered-To: alice@example.org
|
||||
Return-Path: <alice@example.com>
|
||||
Delivered-To: alice@example.com
|
||||
Received: from hq5.merlinux.eu
|
||||
by hq5.merlinux.eu with LMTP
|
||||
id gNKpOrrTvF+tVAAAPzvFDg
|
||||
(envelope-from <alice@example.org>)
|
||||
for <alice@example.org>; Tue, 24 Nov 2020 10:34:50 +0100
|
||||
(envelope-from <alice@example.com>)
|
||||
for <alice@example.com>; Tue, 24 Nov 2020 10:34:50 +0100
|
||||
Subject: Autocrypt Setup Message
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=testrun.org;
|
||||
s=testrun; t=1606210490;
|
||||
@@ -21,8 +21,8 @@ Date: Tue, 24 Nov 2020 09:34:48 +0000
|
||||
Chat-Version: 1.0
|
||||
Autocrypt-Setup-Message: v1
|
||||
Message-ID: <abc@example.com>
|
||||
To: <alice@example.org>
|
||||
From: <alice@example.org>
|
||||
To: <alice@example.com>
|
||||
From: <alice@example.com>
|
||||
Content-Type: multipart/mixed; boundary="dKhu3bbmBniQsT8W8w58YRCCiBK2YY"
|
||||
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
Return-Path: <bob@example.org>
|
||||
Delivered-To: alice@example.org
|
||||
Received: from hq5.example.org
|
||||
by hq5.example.org with LMTP
|
||||
id ODfyL7KhyWEANgAAPzvFDg
|
||||
(envelope-from <bob@example.org>)
|
||||
for <bob@example.org>; Mon, 27 Dec 2021 12:21:22 +0100
|
||||
Received: from mout.example.org (mout.example.org [212.227.17.22])
|
||||
by hq5.example.org (Postfix) with ESMTPS id 45BAF27A0001
|
||||
for <bob@example.org>; Mon, 27 Dec 2021 12:21:22 +0100 (CET)
|
||||
Received: from [127.0.0.1] ([217.80.24.163]) by mail.example.org (mrgmx105
|
||||
[212.227.17.168]) with ESMTPSA (Nemesis) id 1MF3HU-1nCnTl33U0-00FXCF; Mon, 27
|
||||
Dec 2021 12:21:21 +0100
|
||||
Subject: ...
|
||||
MIME-Version: 1.0
|
||||
Date: Mon, 27 Dec 2021 13:12:03 +0000
|
||||
Chat-Version: 1.0
|
||||
Autocrypt: addr=bob@example.net; prefer-encrypt=mutual;
|
||||
keydata=xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA5//PjA
|
||||
zbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOeJw9kohATSqUtsRO0pFJe
|
||||
DvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeTxc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1Jz
|
||||
dKTcDWryrSkvmgFdUqJ7pJDk1HFTt+x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBam
|
||||
e1BPsE1PA7VzeTSJR2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS
|
||||
5uZXQ+wsCJBBABCAAzAhkBBQJeMMduAhsDBAsJCAcGFQgJCgsCAxYCARYhBMzLWqn24RQclDFl8dsY
|
||||
sYy89wSHAAoJENsYsYy89wSH9oIIALbpmicuVghM3CloiCgJhPEFLFMaQZRDV/KCVVtBcHAhw6d42q
|
||||
8T50mhs+W3Va5E37DN+wcenj8CgeGPQY3kPp2cnZruYtLhLkZ1+VEay5BQFUMb8kY21XrNTQQET8vc
|
||||
0L8cCLQ7RCgm1tGiFVp1nqbjmGUdoru90ksoufWfoqVPjNrW+9eHFvY/Z7PqchCdMnbKOJiwwv4E3N
|
||||
DTySZ1UVZnDztGy95Aa8OZ3cntvbq4JVi7S+N38rRPPPzpZKx+M4DUGfDAoaq7O/Xemyk1sP6C/NgQ
|
||||
vS8rri54PgkMgKSS4TyyEzdM+fzeNYFPXFGTbgj4p0pSueQV7/JUfYHRfe3OwE0EXjDHVwEIAKIHgS
|
||||
2yI2niSCN1tqcbLvkhLrEJCVcpGxmA7asl1flwWYrGOBhNJE2sCuZqkofqw6qrgsQ4GFgUU5xmcBCq
|
||||
IZ49jRu+aY38lT4WDFHSbe/mGtaIhb2ZYK6zo9W7Y3r6ud8hbUKJTDfl9qEvJpX/Y0syMjwng8SZNT
|
||||
dYMWgAE4NwcgMgdU3dMA3RT6ePJ4vKs38hmXmInLyZce+GJzmo2tpZyP8viPS7JpqojoCPB3G5h9aH
|
||||
eakp1Y4XKQaExANeWCyBJEhNwtNEOVEpQ0txFYPyDrtxV5y5e79IUP418r/PHsnH6UnxXGzB6LfVbS
|
||||
eEyDyKEl+w0PrlNklySomTZFUAEQEAAcLAdgQYAQgAIAUCXjDHbgIbDBYhBMzLWqn24RQclDFl8dsY
|
||||
sYy89wSHAAoJENsYsYy89wSHVCIIAIH694HkQLXRAJlXmi8K/xMVP96ywJovL/B5l4S/vk/iR4P1lY
|
||||
sF55A3Z2PK/iFtwAgVsppcBIPBlqSI0GPDMvEIxj7UFOQfQzVpDes29wG8grHJEJqI/4TlRjOacxTG
|
||||
aJ5fIMsLXJD6nLBuoN5Z6zm3LjqIyOx4ZGrwradPO95OMGT2Xll3YNzUqSWe33RJLqNQ5ea9I7+qvK
|
||||
nW5Z9Yt5nQwOo8yD+f5fql8904B3eAyLqxgkdLmngAWmYhc7KOaKdAsx7TXBAKsoeHk51OPk59u7Eb
|
||||
X35HWD6snl/phJdUYDXiddyYN/n2ZY9g80ycle2JfgpfrQGlh7oJqgCjZuk=
|
||||
Message-ID: <Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net>
|
||||
To: <alice@example.org>
|
||||
From: <bob@example.net>
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
||||
boundary="M1Jorju4VotNmLKKE0sfOXoILeBrPT"
|
||||
|
||||
|
||||
--M1Jorju4VotNmLKKE0sfOXoILeBrPT
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
|
||||
--M1Jorju4VotNmLKKE0sfOXoILeBrPT
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc";
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
wU4D5tq63hTeebASAQdAvnEfGvGoq5gqUvdfaQYTaYEpOGE/PwfwDmoP0dMoAHgg
|
||||
rjw3qVEAlAkvEjr6zZ55GTUFCPL+PTbePTCLXvNeFvjBwEwD49jcm8SO4yIBCACU
|
||||
Xxzv2wWPEXcHv3IC068E1maFYJgjbL4UUqEnepyQeRw6X4hqhivR1t+Sq5jtSB90
|
||||
ywDKf/z3gNytjUYwgWL0wC7hRc9HoctXf/j6pIGMui2FqyzOxmbD1E99lFvexDbo
|
||||
9qx47bFqC47HTc3pyOBHgnCqNsfLwRoBz+BMtpLOU8TeJeA1LanrXDPLxQoExitc
|
||||
CpdmrlpVXmLbgQ7h/tT7dwidQ8xMB5J4h/gXzaSrrPI8E5HVFUEp0nt1G0sRsMFZ
|
||||
HftxsyucK+GSppaU4mPQ5KgLjztY9Hd67f/XFLsJpU0Gxq8aRrMh43WzsM9kgAUB
|
||||
Gj6WW5KH/8gTsjNqMgPC0sHSATYf3TgPs9w2R7ZawiLDfYCRugzWuKwajpMBlt76
|
||||
Sa8XFOg53QPzgK2lIm4jzRCT7bmFXQ+jNn5i8/XsgNohsGCbTxfU37ieAX/RBPtj
|
||||
S4N2RwbMmNr+8PnZaEXz8MKAG1Ptpl3oceqJ8uUXtC2DK8SuGXkTumJYVM1qTNzx
|
||||
Y3T9xFuu56b6sEPQZPrbjdK9hP7KI91vsakbLa9KiNGDFxu/YtO9fM+CT2F+9geN
|
||||
Q4DPYuq/FMLvztjMm27cYT0jPU3sBCkxtb1nsxJViEo5DsFBZA5Xo4pg/waGbCc/
|
||||
u6C7tesb+hf/DgU76UsUKFQGMX6KDNqNiyiWp4nA6c8i5rIh0IXEk97JG6tGLhSc
|
||||
yMdsj59F9vTMFLuFFNCuLGyX9y/2JE2VKfPRbOwspmrbvg9yVLhdyFkxuv+M+cWv
|
||||
tj6E2oL3HhmJXSIbWbWH80c0Q5UUH9Z0tI2cxQZTvQxegnnnJ+VZmQAs2S5nZxds
|
||||
74/Wk0Gf4HHFn2jEDkaMEP4S1W6pvdowkzv7FnQ/3bFdEKGHNrNgZoPNXHh3eM+L
|
||||
HiY8Opx4vsRK5ia/1TbVkzyJtihL5y5LupS7PRXjjLlXjxbrZxQIpMztuC29lgMD
|
||||
Da5+F2hcrQEd2oDv/67s54+IkuBdTTM3YwXy6NJ0NtEVcEfiGILGoNpeBF5ppTgU
|
||||
N4ep54h0PYO/L8xLkzNrvIbJGfquYnKhgRicBNPyrPiDlB/1CmfTIE/K9jJosigV
|
||||
/jEQ2dSlDILFElmGCGRnb/t21PhWPhmiNcSaYdQKhjTf4LgYlBXP57YsEMdo+HAl
|
||||
koaQzcdV8os3PeBeFgQi11B2nOSoq0gHmto6lWZnZC7dIsJwI8cq6A/49WLKUFNR
|
||||
sO+twA==
|
||||
=Dvgk
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
|
||||
--M1Jorju4VotNmLKKE0sfOXoILeBrPT--
|
||||
@@ -1,9 +1,9 @@
|
||||
Return-Path: <alice@example.org>
|
||||
Return-Path: <alice@example.com>
|
||||
Delivered-To: bob@example.org
|
||||
Received: from hq5.merlinux.eu
|
||||
by hq5.merlinux.eu with LMTP
|
||||
id GJ4eNagpFWF5UwAAPzvFDg
|
||||
(envelope-from <alice@example.org>)
|
||||
(envelope-from <alice@example.com>)
|
||||
for <bob@example.org>; Thu, 12 Aug 2021 16:01:12 +0200
|
||||
Received: from mout.gmx.net (mout.gmx.net [212.227.17.22])
|
||||
by hq5.merlinux.eu (Postfix) with ESMTPS id 3033227A0003
|
||||
@@ -24,7 +24,7 @@ Received: from [193.96.224.73] ([193.96.224.73]) by web-mail.gmx.net
|
||||
16:01:11 +0200
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <trinity-18545f24-4f02-4dc8-9f80-8d2646646d03-1628776871644@3c-app-gmx-bap57>
|
||||
From: Alice <alice@example.org>
|
||||
From: Alice <alice@example.com>
|
||||
To: bob@example.org
|
||||
Subject: Fw: subject
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
@@ -60,7 +60,7 @@ X-UI-Out-Filterresults: notjunk:1;V03:K0:pksZU4GoRZI=:jPKwLt7m9sSdgel28Ha/o7
|
||||
<div data-darkreader-inline-border-left="" name="quote" style="margin: 10px 5px 5px 10px; padding: 10px 0px 10px 10px; border-left: 2px solid rgb(195, 217, 229); overflow-wrap: break-word; --darkreader-inline-border-left:#274759;">
|
||||
<div style="margin:0 0 10px 0;"><b>Gesendet:</b> Donnerstag, 12. August 2021 um 15:52 Uhr<br/>
|
||||
<b>Von:</b> "Claire" <claire@example.org><br/>
|
||||
<b>An:</b> alice@example.org<br/>
|
||||
<b>An:</b> alice@example.com<br/>
|
||||
<b>Betreff:</b> subject</div>
|
||||
|
||||
<div name="quoted-content">bodytext</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Return-Path: <paula@example.org>
|
||||
Delivered-To: alice@example.org
|
||||
Delivered-To: alice@example.com
|
||||
Received: from hq5.merlinux.eu
|
||||
by hq5.merlinux.eu with LMTP
|
||||
id t5IeKWSbkV+eZAAAPzvFDg
|
||||
(envelope-from <paula@example.org>)
|
||||
for <alice@example.org>; Thu, 22 Oct 2020 16:47:00 +0200
|
||||
for <alice@example.com>; Thu, 22 Oct 2020 16:47:00 +0200
|
||||
Received: from dd37930.kasserver.com (dd37930.kasserver.com [85.13.154.127])
|
||||
by hq5.merlinux.eu (Postfix) with ESMTPS id E942727A0011
|
||||
for <alice@example.org>; Thu, 22 Oct 2020 16:46:59 +0200 (CEST)
|
||||
for <alice@example.com>; Thu, 22 Oct 2020 16:46:59 +0200 (CEST)
|
||||
Received: from macbook.fritz.box (i59F5C9C2.versanet.de [89.245.201.194])
|
||||
by dd37930.kasserver.com (Postfix) with ESMTPSA id C1EAC53C066B
|
||||
for <alice@example.org>; Thu, 22 Oct 2020 16:46:58 +0200 (CEST)
|
||||
for <alice@example.com>; Thu, 22 Oct 2020 16:46:58 +0200 (CEST)
|
||||
From: paula <paula@example.org>
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="Apple-Mail=_9A2A284B-D732-46ED-9F21-7E32AE214CE9"
|
||||
@@ -22,7 +22,7 @@ Subject: =?utf-8?Q?Anker_SoundCore_2_Bluetooth_Lautsprecher=2C_Fantastisch?=
|
||||
=?utf-8?Q?_=26_HiFi?=
|
||||
Message-Id: <7D32DF54-6498-48A6-B0F9-952499061C19@kadeifalong.de>
|
||||
Date: Thu, 22 Oct 2020 16:46:56 +0200
|
||||
To: alice@example.org
|
||||
To: alice@example.com
|
||||
X-Mailer: Apple Mail (2.3445.104.11)
|
||||
X-Spam: Yes
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
Return-Path: <anonymous@example.org>
|
||||
Delivered-To: anonymous@posteo.de
|
||||
Received: from proxy02.posteo.name ([127.0.0.1])
|
||||
by dovecot16.posteo.name (Dovecot) with LMTP id Cp2uFxP1sWHbCQEAchYRkQ
|
||||
for <anonymous@posteo.de>; Thu, 09 Dec 2021 13:25:38 +0100
|
||||
Received: from proxy02.posteo.de ([127.0.0.1])
|
||||
by proxy02.posteo.name (Dovecot) with LMTP id MWsaCwrvsWG0wgEAGFAyLg
|
||||
; Thu, 09 Dec 2021 13:25:38 +0100
|
||||
Received: from mailin06.posteo.de (unknown [10.0.1.6])
|
||||
by proxy02.posteo.de (Postfix) with ESMTPS id 4J8tXy0KkMz120l
|
||||
for <anonymous@posteo.de>; Thu, 9 Dec 2021 13:25:38 +0100 (CET)
|
||||
Received: from mx04.posteo.de (mailin06.posteo.de [127.0.0.1])
|
||||
by mailin06.posteo.de (Postfix) with ESMTPS id F24DE215B8
|
||||
for <anonymous@posteo.de>; Thu, 9 Dec 2021 13:25:37 +0100 (CET)
|
||||
X-Virus-Scanned: amavisd-new at posteo.de
|
||||
X-Spam-Flag: NO
|
||||
X-Spam-Score: 0.011
|
||||
X-Spam-Level:
|
||||
X-Spam-Status: No, score=0.011 tagged_above=-1000 required=7
|
||||
tests=[HTML_MESSAGE=0.001, T_POSTEO_TLSINY=0.01] autolearn=disabled
|
||||
X-Posteo-Antispam-Signature: v=1; e=base64; a=aes-256-gcm; d=27yedFdXeAzOobR4x685XJ/5e6WQmX8PP5pSnOlGU2a9Ismhk38wb5AS44xh1yeL5PUxla78UEsHwGkPR0IyPRlHWaLMFLd5CJZN3GzFfrj/2CuB+cd1hOLpp9hRmCebc3rchuDr
|
||||
Authentication-Results: posteo.de; dmarc=none (p=none dis=none) header.from=example.org
|
||||
X-Posteo-TLS-Received-Status: TLSv1.2
|
||||
Received: from mail.example.org (mail.example.org [0.0.0.0])
|
||||
by mx04.posteo.de (Postfix) with ESMTPS id 4J8tXx38vRz10yw
|
||||
for <anonymous@posteo.at>; Thu, 9 Dec 2021 13:25:37 +0100 (CET)
|
||||
Received: from [192.168.1.11] (port=22105 helo=mail.example.org)
|
||||
by mail.example.org with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
|
||||
(Exim 4.94.2)
|
||||
(envelope-from <anonymous@example.org>)
|
||||
id 1mvIUG-0007VC-2U
|
||||
for anonymous@posteo.at; Thu, 09 Dec 2021 13:25:24 +0100
|
||||
From: Anonymous <anonymous@example.org>
|
||||
To: Anonymous <anonymous@posteo.at>
|
||||
Subject: Gelesen: Test message
|
||||
Thread-Topic: Test message
|
||||
Thread-Index: AQHX7Dt/+5f88Aokk0KrqG0hbF8dN6wqFvxh
|
||||
Date: Thu, 9 Dec 2021 12:25:24 +0000
|
||||
Message-ID: <1711fc3548cd4b2699ccd4fffac17713@anonymous>
|
||||
In-Reply-To: <75dd051097b02468183707ad0dd62ebd@posteo.de>
|
||||
Accept-Language: de-AT, de-DE, en-US
|
||||
Content-Language: de-DE
|
||||
X-MS-Has-Attach:
|
||||
X-MS-TNEF-Correlator:
|
||||
x-ms-exchange-transport-fromentityheader: Hosted
|
||||
x-originating-ip: [192.168.120.215]
|
||||
Content-Type: multipart/report;
|
||||
boundary="_000_1711fc3548cd4b2699ccd4fffac17713anonymous_";
|
||||
report-type=disposition-notification
|
||||
MIME-Version: 1.0
|
||||
|
||||
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="_002_1711fc3548cd4b2699ccd4fffac17713anonymous_"
|
||||
|
||||
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_
|
||||
Content-Type: text/plain; charset="iso-8859-1"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Ihre Nachricht
|
||||
|
||||
An: Anonymous
|
||||
Betreff: Test message
|
||||
Gesendet: Mittwoch, 8. Dezember 2021 14:57:05 (UTC+01:00) Amsterdam, Ber=
|
||||
lin, Bern, Rom, Stockholm, Wien
|
||||
|
||||
wurde am Donnerstag, 9. Dezember 2021 13:24:34 (UTC+01:00) Amsterdam, Berl=
|
||||
in, Bern, Rom, Stockholm, Wien gelesen.
|
||||
|
||||
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_
|
||||
Content-Type: text/html; charset="iso-8859-1"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Diso-8859-=
|
||||
1">
|
||||
<meta name=3D"Generator" content=3D"Microsoft Exchange Server">
|
||||
<!-- converted from text --><style><!-- .EmailQuote { margin-left: 1pt; pad=
|
||||
ding-left: 4pt; border-left: #800000 2px solid; } --></style>
|
||||
</head>
|
||||
<body>
|
||||
<font size=3D"2"><span style=3D"font-size:10pt;">
|
||||
<div class=3D"PlainText">Ihre Nachricht <br>
|
||||
<br>
|
||||
An: Anonymous<br>
|
||||
Betreff: Test message<br>
|
||||
Gesendet: Mittwoch, 8. Dezember 2021 14:57:05 (UTC+01:00) =
|
||||
Amsterdam, Berlin, Bern, Rom, Stockholm, Wien<br>
|
||||
<br>
|
||||
wurde am Donnerstag, 9. Dezember 2021 13:24:34 (UTC+01:00) Amster=
|
||||
dam, Berlin, Bern, Rom, Stockholm, Wien gelesen.</div>
|
||||
</span></font>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_--
|
||||
|
||||
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_
|
||||
Content-Type: message/disposition-notification
|
||||
|
||||
Final-recipient: RFC822; anonymous@example.org
|
||||
Disposition: automatic-action/MDN-sent-automatically; displayed
|
||||
X-MSExch-Correlation-Key: coNC5vaCQkiAOjek1v1Uew==
|
||||
X-Display-Name: Anonymous
|
||||
|
||||
|
||||
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_--
|
||||
@@ -1,9 +1,9 @@
|
||||
Return-Path: <alice@example.org>
|
||||
Return-Path: <alice@example.com>
|
||||
Delivered-To: bob@example.org
|
||||
Date: Mon, 29 Mar 2021 11:30:57 +0000
|
||||
To: Bob <bob@example.org>
|
||||
From: Alice <alice@example.org>
|
||||
Reply-To: Alice <alice@example.org>
|
||||
From: Alice <alice@example.com>
|
||||
Reply-To: Alice <alice@example.com>
|
||||
Subject: ...
|
||||
Message-ID: <Mr.AkmaxDNOYj0.oNPtoFR8EHC@example.com>
|
||||
In-Reply-To: <Mr.Y1EWG9-FLhN.KUZ4cu74MYR@example.org>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
Return-Path: <alice@example.org>
|
||||
Return-Path: <alice@example.com>
|
||||
Delivered-To: bob@example.org
|
||||
Date: Mon, 29 Mar 2021 11:30:57 +0000
|
||||
To: Bob <bob@example.org>
|
||||
From: Alice <alice@example.org>
|
||||
Reply-To: Alice <alice@example.org>
|
||||
From: Alice <alice@example.com>
|
||||
Reply-To: Alice <alice@example.com>
|
||||
Subject: ...
|
||||
Message-ID: <Mr.AkmaxDNOYj0.oNPtoFR8EHC@example.com>
|
||||
In-Reply-To: <Mr.Y1EWG9-FLhN.KUZ4cu74MYR@example.org>
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
Message-ID: <4718cf7f-67f4-291b-ccb9-a167842729ed@example.org>
|
||||
Date: Sun, 5 Dec 2021 00:00:00 +0000
|
||||
MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101
|
||||
Thunderbird/91.3.2
|
||||
Content-Language: en-US
|
||||
To: Bob <bob@example.net>
|
||||
From: Alice <alice@example.org>
|
||||
Subject: ...
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
boundary="------------68Kl9HSVGFVUMdZIowLUKskt"
|
||||
|
||||
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
|
||||
--------------68Kl9HSVGFVUMdZIowLUKskt
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
--------------68Kl9HSVGFVUMdZIowLUKskt
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc"
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
wV4D5tq63hTeebASAQdAt2c3rVUh+l0Ps7/Je83NaA7M6HsobtfMueqLUBaeancw0rRAo7PbLDLL
|
||||
cVX3SiPw6qqZyD99JZEgxZJFWM2GVILGqdvJFl11OKqXUDbzRgq6wcBMA+PY3JvEjuMiAQf6An2O
|
||||
xxjJsLgY3Ys6Ndqm8Tqp0XxK3gQuj5Vqpgd7Qv+57psL5jLHc46RxUR/txlY3Kay3yITG82iDvi4
|
||||
fbpkes7/t8eWOrtGdyPVokhfekuCLBoF24F4tEYBsumcurkNDqY1l+dxMzGB9goQWiVOUK3n+IV8
|
||||
fWPTazXTxO5o0VbCFU6RklpW07JEQUrmTzc+cwlIMhttU+h9rkfu8lm+9+KpI8GOHGV3RSCfZ1ns
|
||||
PiZL2xgJsTXAb7dF4vaAWozS7BFfxGZ1DknrySGMUBV3nmDjy/na5YiOqe/PWaZE19LcYEUdR6K5
|
||||
AFyifXDAwi0EoMe9w+aFWqnvuOkPWnhTVNLEPAFlODnAMgqeFMfHCiIrRI/UcA/NUNuY/MCFUC17
|
||||
aAw4Gl4v/pGRnVU3H+4KhW7AqNuqXQC0SpqZDuLEfr5DqUtd7at9TJh+n3kACs7sMzj3pLmZwBcg
|
||||
HddQoI35SuiLQwa79Ws/BwwSPKjRNYcKjwrjuG+k0gk+x5vd9PfUIX1ypatyJC5ZeIpFUiqPZYlg
|
||||
RCzYaWkGvvSFKIOrEWHMcUaP1p51L3n4Bc8UjVcvoeXjD2w5/SzbQ9/gp8Pno+lk1F1StDOQcRGw
|
||||
wzlKzw9KyznRCXtBtnGqgjr1gW2c1nt3BDBqq4KKTaf64eorkWOe29Qwk7jWkh+4HOe9uYd4raU3
|
||||
sLSY/LRSbYpJnNVsympMqPYopr7pO5W7sgqU1VFtfdCVZfzgvXi1USgnqQ++2BA253nrN203ZERL
|
||||
sHwWPIjeo5kULPqV7tUfU0goc7uerEFeFjJOg+Z1ZNU9/fhfJYoJTbo+2Kd6v93PPPgGzxeAU+zL
|
||||
in4yDAAJB9yJzkbVL83G7yfJ+3J5h+19aTc6XSlkXzNyLmQvTKFqDdq2SHooAlG7UJoE6vRK+mDz
|
||||
vbND9KbAAtQ4aQp10OYNyb+ZSXiwsKrgxMP3FE3j6Ui7Q9Fp3GgJC5SR0gTcGwqRWODgQau8E26r
|
||||
ukYKlB6XJ9tPAf2BwXeqwiQ3QU1704BzbO5G3tby9TpWqnAdtEfT2LdhllrwQmPWo+lNNWf1oLWu
|
||||
ylhJ1yEWETzeClDFxeyAoehJLZImlISQQsEoEPxCqHZ60o9x6ANto6xv3CIbu0WziA2A6R7tweBi
|
||||
mCAsyZdVCL2gg2nw+UWUyv6baTDpkxtKJOvYZeyzR0TH6KExRgeKjBrWPuHxJ7b+e70/DLvfNg+x
|
||||
Q6pulf+LWDKgZ9bGCZWbutp2uFyvdW+RdJXXXmhSZ3nrhusw/PVdGeQz+3N6LK3yiVOcvLeyNqGW
|
||||
/yYST6Rmqen0/JQPDDdKh4JjmLnJ/SmPTDOCD29uB03tCDDU2mzOUUncJWURE3jmJlKGGoOq4Ar9
|
||||
W03ud3E1ks/ZXk+aqz3jQ354cqSampZcxqX90esibuV/guUI3u0N3ah+FW1IfRhP2xJ36SIzc1lu
|
||||
Bs/jehRDJ9/BSFH+lHRftcYoGjNNFzl7Hx4me8EDdfhzX0HXNUZhVYJlFktdr1cjhPNzlxlnCL8b
|
||||
MgERav2VKFBvW0LR4Mm+trtbFU1ajybVihk7R56yJ/itnTHd3BxR7s8sRsG/6a8d2QiKjfNHBU05
|
||||
KEATHBFwTz3WWBbtBMN8fmIg8g2MrOfjcaHoTAgRJVr0rf+ww+KyaI8ZsraB+KTzXk+iVegNaUe/
|
||||
CiLI+Yl9ePNkFFbi4MyrY0ujXM6zRp7nbUlDewzGpI4LTyyAQ9IUqkCnAi0k7AkM1BIp8z1wxWlW
|
||||
JRAnxGSzxgibYLZ9f/fd9vBAiYA1ZVsuZTN2iUtt2/VJr2K7zPHwgO4j2OLtR4DKazCd7IlrArRH
|
||||
BfawosWYQ7cQJyo/+wxjXccvHVrZRn8vBvmFWdKz9mi1wC1HYyLeMJwYpaPsK79TRedA34pQSuAa
|
||||
QkAO79MxOVnknYS8pEGxrwD9l9vxrlZEllnFtG+QJeXsZgMIjwCaByJs7I3skUAHcuimN1X8htU2
|
||||
ofVNpLp9SUsrtXbFp89Dxiuflj10VvcLGU2AjSsUtjEpPl0nobeJmA3RzFxJZ61RG+E=
|
||||
=dcQr
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--------------68Kl9HSVGFVUMdZIowLUKskt--
|
||||
@@ -1,7 +1,7 @@
|
||||
Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Hocuri <hocuri1@testrun.org>
|
||||
Subject: subj with important info
|
||||
To: alice@example.org
|
||||
To: alice@example.com
|
||||
Message-ID: <6e107266-b2bd-e87f-4996-29b000abcfb0@testrun.org>
|
||||
Date: Thu, 17 Dec 2020 15:38:45 +0100
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Subject: mime-modified test
|
||||
Message-ID: 12345@testrun.org
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||
To: alice@example.org
|
||||
To: alice@example.com
|
||||
From: sender@testrun.org
|
||||
Content-Type: multipart/alternative; boundary="==BREAK=="
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Return-Path: <camping@otherdomain.eu>
|
||||
X-Original-To: alice@example.org
|
||||
X-Original-To: alice@example.com
|
||||
Delivered-To: m045a7e8@dd37930.kasserver.com
|
||||
Received: from mout.kundenserver.de (mout.kundenserver.de [212.227.126.131])
|
||||
by dd37930.kasserver.com (Postfix) with ESMTPS id 271F34B4258C
|
||||
@@ -11,7 +11,7 @@ Received: from oxbsltgw18.schlund.de ([172.19.249.35]) by
|
||||
Date: Thu, 6 Aug 2020 18:40:30 +0200 (CEST)
|
||||
From: Camping <camping@otherdomain.eu>
|
||||
Reply-To: Camping <camping@otherdomain.eu>
|
||||
To: Alice <alice@example.org>
|
||||
To: Alice <alice@example.com>
|
||||
Message-ID: <512278196.1287440.1596732031020@email.ionos.fr>
|
||||
Subject: Re: subj?
|
||||
MIME-Version: 1.0
|
||||
|
||||
Reference in New Issue
Block a user