mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 23:22:11 +03:00
Compare commits
7 Commits
v2.45.0
...
hoc/unpubl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b08ba4bb8c | ||
|
|
b94d9f95b6 | ||
|
|
39dc495944 | ||
|
|
bf02785a36 | ||
|
|
01b2aa0f66 | ||
|
|
fb46c34b55 | ||
|
|
9393753190 |
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -1300,7 +1300,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1410,7 +1410,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1431,7 +1431,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1447,7 +1447,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1476,7 +1476,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.88"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -6755,6 +6755,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
* @param data1 (int) msg_id ID of the message referring to the call
|
||||
* @param data2 (int) 1 if the call was accepted from this device (process).
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_CALL_ACCEPTED 2560
|
||||
|
||||
@@ -6783,8 +6784,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
* UI should update the list.
|
||||
*
|
||||
* The event is emitted when the transports are modified on another device
|
||||
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`
|
||||
* or `set_config(configured_addr)`.
|
||||
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`,
|
||||
* `set_transport_unpublished` or `set_config(configured_addr)`.
|
||||
*/
|
||||
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
|
||||
|
||||
|
||||
@@ -680,7 +680,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::IncomingCallAccepted { .. }
|
||||
| EventType::OutgoingCallAccepted { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
| EventType::EventChannelOverflow { .. }
|
||||
@@ -703,6 +702,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
EventType::IncomingCall { has_video, .. } => *has_video as libc::c_int,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device, ..
|
||||
} => *from_this_device as libc::c_int,
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -68,6 +68,7 @@ use self::types::{
|
||||
},
|
||||
};
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::login_param::Transport;
|
||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -528,6 +529,7 @@ impl CommandApi {
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
|
||||
async fn add_or_update_transport(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -551,9 +553,10 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport,
|
||||
/// [Self::set_transport_unpublished()] to publish or unpublish a transport,
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
||||
async fn list_transports(&self, account_id: u32) -> Result<Vec<Transport>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let res = ctx
|
||||
.list_transports()
|
||||
@@ -571,6 +574,26 @@ impl CommandApi {
|
||||
ctx.delete_transport(&addr).await
|
||||
}
|
||||
|
||||
/// Change whether the transport is unpublished.
|
||||
///
|
||||
/// Unpublished transports are not advertised to contacts,
|
||||
/// and self-sent messages are not sent there,
|
||||
/// so that we don't cause extra messages to the corresponding inbox,
|
||||
/// but can still receive messages from contacts who don't know the new relay addresses yet.
|
||||
///
|
||||
/// The default is true, but when updating,
|
||||
/// existing secondary transports are set to unpublished,
|
||||
/// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
|
||||
async fn set_transport_unpublished(
|
||||
&self,
|
||||
account_id: u32,
|
||||
addr: String,
|
||||
unpublished: bool,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.set_transport_unpublished(&addr, unpublished).await
|
||||
}
|
||||
|
||||
/// Signal an ongoing process to stop.
|
||||
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -441,6 +441,8 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
/// The call was accepted from this device (process).
|
||||
from_this_device: bool,
|
||||
},
|
||||
|
||||
/// Outgoing call accepted.
|
||||
@@ -634,9 +636,14 @@ impl From<CoreEventType> for EventType {
|
||||
place_call_info,
|
||||
has_video,
|
||||
},
|
||||
CoreEventType::IncomingCallAccepted { msg_id, chat_id } => IncomingCallAccepted {
|
||||
CoreEventType::IncomingCallAccepted {
|
||||
msg_id,
|
||||
chat_id,
|
||||
from_this_device,
|
||||
} => IncomingCallAccepted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
from_this_device,
|
||||
},
|
||||
CoreEventType::OutgoingCallAccepted {
|
||||
msg_id,
|
||||
|
||||
@@ -4,6 +4,16 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use yerpc::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Transport {
|
||||
/// The login data entered by the user.
|
||||
pub param: EnteredLoginParam,
|
||||
/// Whether this transport is set to 'unpublished'.
|
||||
/// See `set_transport_unpublished` / `setTransportUnpublished` for details.
|
||||
pub is_unpublished: bool,
|
||||
}
|
||||
|
||||
/// Login parameters entered by the user.
|
||||
///
|
||||
/// Usually it will be enough to only set `addr` and `password`,
|
||||
@@ -56,6 +66,15 @@ pub struct EnteredLoginParam {
|
||||
pub oauth2: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<dc::Transport> for Transport {
|
||||
fn from(transport: dc::Transport) -> Self {
|
||||
Transport {
|
||||
param: transport.param.into(),
|
||||
is_unpublished: transport.is_unpublished,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||
fn from(param: dc::EnteredLoginParam) -> Self {
|
||||
let imap_security: Socket = param.imap.security.into();
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.45.0"
|
||||
"version": "2.46.0-dev"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -99,7 +99,7 @@ class ACFactory:
|
||||
ac.remove()
|
||||
ac_clone = self.get_unconfigured_account()
|
||||
for transport in transports:
|
||||
ac_clone.add_or_update_transport(transport)
|
||||
ac_clone.add_or_update_transport(transport["param"])
|
||||
ac_clone.bring_online()
|
||||
return ac_clone
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ def test_add_second_address(acfactory) -> None:
|
||||
account.add_transport_from_qr(qr)
|
||||
assert len(account.list_transports()) == 3
|
||||
|
||||
first_addr = account.list_transports()[0]["addr"]
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
first_addr = account.list_transports()[0]["param"]["addr"]
|
||||
second_addr = account.list_transports()[1]["param"]["addr"]
|
||||
|
||||
# Cannot delete the first address.
|
||||
with pytest.raises(JsonRpcError):
|
||||
@@ -90,7 +90,7 @@ def test_change_address(acfactory) -> None:
|
||||
assert old_alice_addr in alice_vcard
|
||||
qr = acfactory.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
new_alice_addr = alice.list_transports()[1]["param"]["addr"]
|
||||
with pytest.raises(JsonRpcError):
|
||||
# Cannot use the address that is not
|
||||
# configured for any transport.
|
||||
@@ -179,7 +179,7 @@ def test_reconfigure_transport(acfactory) -> None:
|
||||
account.set_config("mvbox_move", "1")
|
||||
|
||||
[transport] = account.list_transports()
|
||||
account.add_or_update_transport(transport)
|
||||
account.add_or_update_transport(transport["param"])
|
||||
|
||||
# Reconfiguring the transport should not reset
|
||||
# the settings as if when configuring the first transport.
|
||||
@@ -215,15 +215,15 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
|
||||
log.section("ac1 clone removes second transport")
|
||||
[transport1, transport2, transport3] = ac1_clone.list_transports()
|
||||
addr3 = transport3["addr"]
|
||||
ac1_clone.delete_transport(transport2["addr"])
|
||||
addr3 = transport3["param"]["addr"]
|
||||
ac1_clone.delete_transport(transport2["param"]["addr"])
|
||||
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1)
|
||||
[transport1, transport3] = ac1.list_transports()
|
||||
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport3["addr"])
|
||||
ac1.set_config("configured_addr", transport3["param"]["addr"])
|
||||
|
||||
# One event for updated `add_timestamp` of the new primary transport,
|
||||
# one event for the `configured_addr` update.
|
||||
@@ -233,12 +233,12 @@ def test_transport_synchronization(acfactory, log) -> None:
|
||||
assert ac1_clone.get_config("configured_addr") == addr3
|
||||
|
||||
log.section("ac1 removes the first transport")
|
||||
ac1.delete_transport(transport1["addr"])
|
||||
ac1.delete_transport(transport1["param"]["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1_clone)
|
||||
[transport3] = ac1_clone.list_transports()
|
||||
assert transport3["addr"] == addr3
|
||||
assert transport3["param"]["addr"] == addr3
|
||||
assert ac1_clone.get_config("configured_addr") == addr3
|
||||
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
@@ -262,13 +262,13 @@ def test_transport_sync_new_as_primary(acfactory, log) -> None:
|
||||
[transport1, transport2] = ac1_transports
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert len(ac1_clone.list_transports()) == 2
|
||||
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
||||
assert ac1_clone.get_config("configured_addr") == transport1["param"]["addr"]
|
||||
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport2["addr"])
|
||||
ac1.set_config("configured_addr", transport2["param"]["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert ac1_clone.get_config("configured_addr") == transport2["addr"]
|
||||
assert ac1_clone.get_config("configured_addr") == transport2["param"]["addr"]
|
||||
|
||||
log.section("ac1_clone receives a message via the new primary transport")
|
||||
ac1_chat = ac1.create_chat(bob)
|
||||
@@ -288,7 +288,7 @@ def test_recognize_self_address(acfactory) -> None:
|
||||
qr = acfactory.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
new_alice_addr = alice.list_transports()[1]["param"]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
@@ -311,7 +311,7 @@ def test_transport_limit(acfactory) -> None:
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
second_addr = account.list_transports()[1]["param"]["addr"]
|
||||
account.delete_transport(second_addr)
|
||||
|
||||
# test that adding a transport after deleting one works again
|
||||
@@ -337,13 +337,13 @@ def test_message_info_imap_urls(acfactory) -> None:
|
||||
bob_chat = bob.create_chat(alice)
|
||||
|
||||
# Alice switches to another transport and removes the rest of the transports.
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
new_alice_addr = alice.list_transports()[1]["param"]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
removed_addrs = []
|
||||
for transport in alice.list_transports():
|
||||
if transport["addr"] != new_alice_addr:
|
||||
alice.delete_transport(transport["addr"])
|
||||
removed_addrs.append(transport["addr"])
|
||||
if transport["param"]["addr"] != new_alice_addr:
|
||||
alice.delete_transport(transport["param"]["addr"])
|
||||
removed_addrs.append(transport["param"]["addr"])
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
|
||||
@@ -370,14 +370,14 @@ def test_remove_primary_transport(acfactory) -> None:
|
||||
|
||||
# Alice changes the transport.
|
||||
[transport1, transport2] = alice.list_transports()
|
||||
alice.set_config("configured_addr", transport2["addr"])
|
||||
alice.set_config("configured_addr", transport2["param"]["addr"])
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
msg1 = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg1.text == "Hello!"
|
||||
|
||||
# Alice deletes the first transport.
|
||||
alice.delete_transport(transport1["addr"])
|
||||
alice.delete_transport(transport1["param"]["addr"])
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ def test_lowercase_address(acfactory) -> None:
|
||||
assert account.is_configured()
|
||||
assert addr_upper != addr
|
||||
assert account.get_config("configured_addr") == addr
|
||||
assert account.list_transports()[0]["addr"] == addr
|
||||
assert account.list_transports()[0]["param"]["addr"] == addr
|
||||
|
||||
param = account.get_info()["used_transport_settings"]
|
||||
assert addr in param
|
||||
@@ -138,7 +138,7 @@ def test_list_transports(acfactory) -> None:
|
||||
)
|
||||
transports = account.list_transports()
|
||||
assert len(transports) == 1
|
||||
params = transports[0]
|
||||
params = transports[0]["param"]
|
||||
assert params["addr"] == addr
|
||||
assert params["password"] == password
|
||||
assert params["imapUser"] == addr
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.45.0"
|
||||
"version": "2.46.0-dev"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.45.0"
|
||||
version = "2.46.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::context::{Context, WeakContext};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::message::{Message, MsgId, Viewtype, markseen_msgs};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::net::dns::lookup_host_with_cache;
|
||||
use crate::param::Param;
|
||||
@@ -249,6 +249,7 @@ impl Context {
|
||||
if chat.is_contact_request() {
|
||||
chat.id.accept(self).await?;
|
||||
}
|
||||
markseen_msgs(self, vec![call_id]).await?;
|
||||
|
||||
// send an acceptance message around: to the caller as well as to the other devices of the callee
|
||||
let mut msg = Message {
|
||||
@@ -265,6 +266,7 @@ impl Context {
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
from_this_device: true,
|
||||
});
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
Ok(())
|
||||
@@ -283,6 +285,7 @@ impl Context {
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
call.mark_as_ended(self).await?;
|
||||
markseen_msgs(self, vec![call_id]).await?;
|
||||
let declined_call_str = stock_str::declined_call(self).await;
|
||||
call.update_text(self, &declined_call_str).await?;
|
||||
} else {
|
||||
@@ -430,6 +433,7 @@ impl Context {
|
||||
self.emit_event(EventType::IncomingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
from_this_device: false,
|
||||
});
|
||||
} else {
|
||||
let accept_call_info = mime_message
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use crate::chat::forward_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::message::MessageState;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
@@ -115,9 +116,28 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
// Bob accepts the incoming call
|
||||
bob.accept_incoming_call(bob_call.id, ACCEPT_INFO.to_string())
|
||||
.await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Incoming video call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device: true,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let sent2 = bob.pop_sent_msg().await;
|
||||
let info = bob
|
||||
@@ -131,7 +151,15 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
bob2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming video call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::IncomingCallAccepted {
|
||||
from_this_device: false,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let info = bob2
|
||||
.load_call_by_id(bob2_call.id)
|
||||
@@ -200,9 +228,20 @@ async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
bob2_call,
|
||||
..
|
||||
} = accept_call().await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
|
||||
// Bob has accepted the call and also ends it
|
||||
bob.end_call(bob_call.id).await?;
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Incoming video call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -328,8 +367,18 @@ async fn test_callee_rejects_call() -> Result<()> {
|
||||
} = setup_call().await?;
|
||||
|
||||
// Bob has accepted Alice before, but does not want to talk with Alice
|
||||
bob_call.chat_id.accept(&bob).await?;
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_eq!(bob_call.id.get_state(&bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN to Alice.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, bob_call.from_id)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_text(&bob, bob_call.id, "Declined call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -370,6 +419,35 @@ async fn test_callee_rejects_call() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_callee_sees_contact_request_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
let bob_call = bob.recv_msg(&sent1).await;
|
||||
// Bob can't end_call() because the contact request isn't accepted, but he can mark the call as
|
||||
// seen.
|
||||
markseen_msgs(bob, vec![bob_call.id]).await?;
|
||||
assert_eq!(bob_call.id.get_state(bob).await?, MessageState::InSeen);
|
||||
// Bob sends an MDN only to self so that an unaccepted contact can't know anything.
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE msg_id=? AND from_id=?",
|
||||
(bob_call.id, ContactId::SELF)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_caller_cancels_call() -> Result<()> {
|
||||
// Alice calls Bob
|
||||
|
||||
24
src/chat.rs
24
src/chat.rs
@@ -2737,7 +2737,6 @@ async fn prepare_send_msg(
|
||||
chat_id.unarchive_if_not_muted(context, msg.state).await?;
|
||||
}
|
||||
chat.prepare_msg_raw(context, msg, update_msg_id).await?;
|
||||
|
||||
let row_ids = create_send_msg_jobs(context, msg)
|
||||
.await
|
||||
.context("Failed to create send jobs")?;
|
||||
@@ -2844,19 +2843,12 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
recipients.retain(|x| x.to_lowercase() != lowercase_from);
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage
|
||||
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out:
|
||||
if (msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden)
|
||||
// This may happen eg. for groups with only SELF and bcc_self disabled:
|
||||
|| (!context.get_config_bool(Config::BccSelf).await? && recipients.is_empty())
|
||||
{
|
||||
smtp::add_self_recipients(context, &mut recipients, needs_encryption).await?;
|
||||
}
|
||||
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out
|
||||
if msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden {
|
||||
recipients.clear();
|
||||
}
|
||||
|
||||
if recipients.is_empty() {
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
context,
|
||||
"Message {} has no recipient, skipping smtp-send.", msg.id
|
||||
@@ -2895,6 +2887,12 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
);
|
||||
}
|
||||
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage
|
||||
{
|
||||
smtp::add_self_recipients(context, &mut recipients, rendered_msg.is_encrypted).await?;
|
||||
}
|
||||
|
||||
if needs_encryption && !rendered_msg.is_encrypted {
|
||||
/* unrecoverable */
|
||||
message::set_msg_failed(
|
||||
|
||||
@@ -4733,9 +4733,10 @@ async fn test_sync_broadcast_and_send_message() -> Result<()> {
|
||||
vec![a2b_contact_id]
|
||||
);
|
||||
|
||||
// alice2's smeared clock may be behind alice's one, so "hi" from alice2 may appear before "You
|
||||
// joined the channel." for bob.
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
// alice2's smeared clock may be behind alice1's one, so we need to work around "hi" appearing
|
||||
// before "You joined the channel." for bob. alice1 makes 3 more calls of
|
||||
// create_smeared_timestamp() than alice2 does as of 2026-03-10.
|
||||
SystemTime::shift(Duration::from_secs(3));
|
||||
tcm.section("Alice's second device sends a message to the channel");
|
||||
let sent_msg = alice2.send_text(a2_broadcast_id, "hi").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
@@ -837,7 +837,7 @@ impl Context {
|
||||
// which only fetches from the primary transport.
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE transports SET add_timestamp=? WHERE addr=?",
|
||||
"UPDATE transports SET add_timestamp=?, is_published=1 WHERE addr=?",
|
||||
(time(), addr),
|
||||
)
|
||||
.context(
|
||||
@@ -974,6 +974,21 @@ impl Context {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all published self addresses, newest first.
|
||||
/// See `[Context::set_transport_unpublished]`
|
||||
pub(crate) async fn get_published_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql
|
||||
.query_map_vec(
|
||||
"SELECT addr FROM transports WHERE is_published=1 ORDER BY add_timestamp DESC",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all secondary self addresses.
|
||||
pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql.query_map_vec("SELECT addr FROM transports WHERE addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')", (), |row| {
|
||||
@@ -982,6 +997,23 @@ impl Context {
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Returns all published secondary self addresses.
|
||||
/// See `[Context::set_transport_unpublished]`
|
||||
pub(crate) async fn get_published_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql
|
||||
.query_map_vec(
|
||||
"SELECT addr FROM transports
|
||||
WHERE is_published=1
|
||||
AND addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the primary self address.
|
||||
/// Returns an error if no self addr is configured.
|
||||
pub async fn get_primary_self_addr(&self) -> Result<String> {
|
||||
|
||||
@@ -28,8 +28,8 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::warn;
|
||||
use crate::login_param::EnteredCertificateChecks;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::login_param::{EnteredCertificateChecks, Transport};
|
||||
use crate::message::Message;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
@@ -110,6 +110,7 @@ impl Context {
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
|
||||
pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
let result = self.add_transport_inner(param).await;
|
||||
@@ -188,14 +189,22 @@ impl Context {
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
|
||||
pub async fn list_transports(&self) -> Result<Vec<Transport>> {
|
||||
let transports = self
|
||||
.sql
|
||||
.query_map_vec("SELECT entered_param FROM transports", (), |row| {
|
||||
let entered_param: String = row.get(0)?;
|
||||
let transport: EnteredLoginParam = serde_json::from_str(&entered_param)?;
|
||||
Ok(transport)
|
||||
})
|
||||
.query_map_vec(
|
||||
"SELECT entered_param, is_published FROM transports",
|
||||
(),
|
||||
|row| {
|
||||
let param: String = row.get(0)?;
|
||||
let param: EnteredLoginParam = serde_json::from_str(¶m)?;
|
||||
let is_published: bool = row.get(1)?;
|
||||
Ok(Transport {
|
||||
param,
|
||||
is_unpublished: !is_published,
|
||||
})
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(transports)
|
||||
@@ -261,6 +270,40 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change whether the transport is unpublished.
|
||||
///
|
||||
/// Unpublished transports are not advertised to contacts,
|
||||
/// and self-sent messages are not sent there,
|
||||
/// so that we don't cause extra messages to the corresponding inbox,
|
||||
/// but can still receive messages from contacts who don't know the new relay addresses yet.
|
||||
///
|
||||
/// The default is true, but when updating,
|
||||
/// existing secondary transports are set to unpublished,
|
||||
/// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
|
||||
pub async fn set_transport_unpublished(&self, addr: &str, unpublished: bool) -> Result<()> {
|
||||
// We need to update the timestamp so that the key's timestamp changes
|
||||
// and is recognized as newer by our peers
|
||||
self.sql
|
||||
.transaction(|trans| {
|
||||
let primary_addr: String = trans.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
(),
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if primary_addr == addr && unpublished {
|
||||
bail!("Can't set primary relay as unpublished");
|
||||
}
|
||||
trans.execute(
|
||||
"UPDATE transports SET is_published=?, add_timestamp=? WHERE addr=?",
|
||||
(!unpublished, time(), addr),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
send_sync_transports(self).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
|
||||
@@ -397,6 +397,8 @@ pub enum EventType {
|
||||
msg_id: MsgId,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
/// The call was accepted from this device (process).
|
||||
from_this_device: bool,
|
||||
},
|
||||
|
||||
/// Outgoing call accepted.
|
||||
|
||||
@@ -296,7 +296,7 @@ pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option
|
||||
.await?
|
||||
.context("No transports configured")?;
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let all_addrs = context.get_all_self_addrs().await?.join(",");
|
||||
let all_addrs = context.get_published_self_addrs().await?.join(",");
|
||||
let signed_public_key =
|
||||
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
|
||||
*lock = Some(signed_public_key.clone());
|
||||
|
||||
@@ -79,6 +79,16 @@ pub struct EnteredServerLoginParam {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// A transport, as shown in the "relays" list in the UI.
|
||||
#[derive(Debug)]
|
||||
pub struct Transport {
|
||||
/// The login data entered by the user.
|
||||
pub param: EnteredLoginParam,
|
||||
/// Whether this transport is set to 'unpublished'.
|
||||
/// See [`Context::set_transport_unpublished`] for details.
|
||||
pub is_unpublished: bool,
|
||||
}
|
||||
|
||||
/// Login parameters entered by the user.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EnteredLoginParam {
|
||||
|
||||
@@ -204,7 +204,7 @@ async fn test_qr_code_security() -> Result<()> {
|
||||
let charlie_addr = charlie.get_config(Config::Addr).await?.unwrap();
|
||||
|
||||
let alice_fp = self_fingerprint(alice).await?;
|
||||
let secret_for_encryption = dbg!(format!("securejoin/{alice_fp}/{authcode}"));
|
||||
let secret_for_encryption = format!("securejoin/{alice_fp}/{authcode}");
|
||||
test_shared_secret_decryption_ex(
|
||||
bob,
|
||||
&charlie_addr,
|
||||
|
||||
@@ -701,12 +701,12 @@ pub(crate) async fn add_self_recipients(
|
||||
// and connection is frequently lost
|
||||
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
|
||||
// disabled by default is fine.
|
||||
if context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty() {
|
||||
if (context.get_config_delete_server_after().await? != Some(0)) || !recipients.is_empty() {
|
||||
// Avoid sending unencrypted messages to all transports, chatmail relays won't accept
|
||||
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
|
||||
// messages.
|
||||
if encrypted {
|
||||
for addr in context.get_secondary_self_addrs().await? {
|
||||
for addr in context.get_published_secondary_self_addrs().await? {
|
||||
recipients.push(addr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2343,6 +2343,26 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Add an `is_published` flag to transports.
|
||||
// Unpublished transports are not advertised to contacts,
|
||||
// and self-sent messages are not sent there,
|
||||
// so that we don't cause extra messages to the corresponding inbox,
|
||||
// but can still receive messages from contacts who don't know the new relay addresses yet.
|
||||
// The default is true, but when updating,
|
||||
// existing secondary transports are set to unpublished,
|
||||
// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
|
||||
inc_and_check(&mut migration_version, 149)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE transports ADD COLUMN is_published INTEGER DEFAULT 1 NOT NULL;
|
||||
UPDATE transports SET is_published=0 WHERE addr!=(
|
||||
SELECT value FROM config WHERE keyname='configured_addr'
|
||||
)",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -65,6 +65,10 @@ pub(crate) struct TransportData {
|
||||
|
||||
/// Timestamp of when the transport was last time (re)configured.
|
||||
pub(crate) timestamp: i64,
|
||||
|
||||
/// Whether the transport is published.
|
||||
/// See [`Context::set_transport_unpublished`] for details.
|
||||
pub(crate) is_published: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -211,7 +211,7 @@ impl TestContextManager {
|
||||
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
||||
(
|
||||
new_addr,
|
||||
serde_json::to_string(&EnteredLoginParam::default()).unwrap(),
|
||||
serde_json::to_string(&EnteredLoginParam{addr: new_addr.to_string(), ..Default::default()}).unwrap(),
|
||||
format!(r#"{{"addr":"{new_addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
|
||||
),
|
||||
).await.unwrap();
|
||||
|
||||
312
src/transport.rs
312
src/transport.rs
@@ -562,7 +562,15 @@ impl ConfiguredLoginParam {
|
||||
entered_param: &EnteredLoginParam,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
save_transport(context, entered_param, &self.into(), timestamp).await?;
|
||||
let is_published = true;
|
||||
save_transport(
|
||||
context,
|
||||
entered_param,
|
||||
&self.into(),
|
||||
timestamp,
|
||||
is_published,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -628,6 +636,7 @@ pub(crate) async fn save_transport(
|
||||
entered_param: &EnteredLoginParam,
|
||||
configured: &ConfiguredLoginParamJson,
|
||||
add_timestamp: i64,
|
||||
is_published: bool,
|
||||
) -> Result<bool> {
|
||||
let addr = addr_normalize(&configured.addr);
|
||||
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
@@ -635,20 +644,23 @@ pub(crate) async fn save_transport(
|
||||
let mut modified = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp, is_published)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT (addr)
|
||||
DO UPDATE SET entered_param=excluded.entered_param,
|
||||
configured_param=excluded.configured_param,
|
||||
add_timestamp=excluded.add_timestamp
|
||||
add_timestamp=excluded.add_timestamp,
|
||||
is_published=excluded.is_published
|
||||
WHERE entered_param != excluded.entered_param
|
||||
OR configured_param != excluded.configured_param
|
||||
OR add_timestamp < excluded.add_timestamp",
|
||||
OR add_timestamp < excluded.add_timestamp
|
||||
OR is_published != excluded.is_published",
|
||||
(
|
||||
&addr,
|
||||
serde_json::to_string(entered_param)?,
|
||||
serde_json::to_string(configured)?,
|
||||
add_timestamp,
|
||||
is_published,
|
||||
),
|
||||
)
|
||||
.await?
|
||||
@@ -685,7 +697,7 @@ pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
|
||||
let transports = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT entered_param, configured_param, add_timestamp
|
||||
"SELECT entered_param, configured_param, add_timestamp, is_published
|
||||
FROM transports WHERE id>1",
|
||||
(),
|
||||
|row| {
|
||||
@@ -694,10 +706,12 @@ pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
|
||||
let configured_json: String = row.get(1)?;
|
||||
let configured: ConfiguredLoginParamJson = serde_json::from_str(&configured_json)?;
|
||||
let timestamp: i64 = row.get(2)?;
|
||||
let is_published: bool = row.get(3)?;
|
||||
Ok(TransportData {
|
||||
configured,
|
||||
entered,
|
||||
timestamp,
|
||||
is_published,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -736,9 +750,10 @@ pub(crate) async fn sync_transports(
|
||||
configured,
|
||||
entered,
|
||||
timestamp,
|
||||
is_published,
|
||||
} in transports
|
||||
{
|
||||
modified |= save_transport(context, entered, configured, *timestamp).await?;
|
||||
modified |= save_transport(context, entered, configured, *timestamp, *is_published).await?;
|
||||
}
|
||||
|
||||
context
|
||||
@@ -784,7 +799,7 @@ pub(crate) async fn add_pseudo_transport(context: &Context, addr: &str) -> Resul
|
||||
"INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
||||
(
|
||||
addr,
|
||||
serde_json::to_string(&EnteredLoginParam::default())?,
|
||||
serde_json::to_string(&EnteredLoginParam{addr: addr.to_string(), ..Default::default()})?,
|
||||
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
|
||||
),
|
||||
)
|
||||
@@ -793,283 +808,4 @@ pub(crate) async fn add_pseudo_transport(context: &Context, addr: &str) -> Resul
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::log::LogExt as _;
|
||||
use crate::provider::get_provider_by_id;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::tools::time;
|
||||
|
||||
#[test]
|
||||
fn test_configured_certificate_checks_display() {
|
||||
use std::string::ToString;
|
||||
|
||||
assert_eq!(
|
||||
"accept_invalid_certificates".to_string(),
|
||||
ConfiguredCertificateChecks::AcceptInvalidCertificates.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_load_login_param() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let param = ConfiguredLoginParam {
|
||||
addr: "alice@example.org".to_string(),
|
||||
imap: vec![ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "imap.example.com".to_string(),
|
||||
port: 123,
|
||||
security: ConnectionSecurity::Starttls,
|
||||
},
|
||||
user: "alice".to_string(),
|
||||
}],
|
||||
imap_user: "".to_string(),
|
||||
imap_password: "foo".to_string(),
|
||||
smtp: vec![ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "smtp.example.com".to_string(),
|
||||
port: 456,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: "alice@example.org".to_string(),
|
||||
}],
|
||||
smtp_user: "".to_string(),
|
||||
smtp_password: "bar".to_string(),
|
||||
provider: None,
|
||||
certificate_checks: ConfiguredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
};
|
||||
|
||||
param
|
||||
.clone()
|
||||
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
|
||||
.await?;
|
||||
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#;
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.query_get_value::<String>("SELECT configured_param FROM transports", ())
|
||||
.await?
|
||||
.unwrap(),
|
||||
expected_param
|
||||
);
|
||||
assert_eq!(t.is_configured().await?, true);
|
||||
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(param, loaded);
|
||||
|
||||
// Legacy ConfiguredImapCertificateChecks config is ignored
|
||||
t.set_config(Config::ConfiguredImapCertificateChecks, Some("999"))
|
||||
.await?;
|
||||
assert!(ConfiguredLoginParam::load(&t).await.is_ok());
|
||||
|
||||
// Test that we don't panic on unknown ConfiguredImapCertificateChecks values.
|
||||
let wrong_param = expected_param.replace("Strict", "Stricct");
|
||||
assert_ne!(expected_param, wrong_param);
|
||||
t.sql
|
||||
.execute("UPDATE transports SET configured_param=?", (wrong_param,))
|
||||
.await?;
|
||||
assert!(ConfiguredLoginParam::load(&t).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_posteo_alias() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let user = "alice@posteo.de";
|
||||
|
||||
// Alice has old config with "alice@posteo.at" address
|
||||
// and "alice@posteo.de" username.
|
||||
t.set_config(Config::Configured, Some("1")).await?;
|
||||
t.set_config(Config::ConfiguredProvider, Some("posteo"))
|
||||
.await?;
|
||||
t.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredMailServer, Some("posteo.de"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredMailPort, Some("993"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredMailSecurity, Some("1"))
|
||||
.await?; // TLS
|
||||
t.set_config(Config::ConfiguredMailUser, Some(user)).await?;
|
||||
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredSendServer, Some("posteo.de"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSendPort, Some("465"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSendSecurity, Some("1"))
|
||||
.await?; // TLS
|
||||
t.set_config(Config::ConfiguredSendUser, Some(user)).await?;
|
||||
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredServerFlags, Some("0"))
|
||||
.await?;
|
||||
|
||||
let param = ConfiguredLoginParam {
|
||||
addr: "alice@posteo.at".to_string(),
|
||||
imap: vec![
|
||||
ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "posteo.de".to_string(),
|
||||
port: 993,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: user.to_string(),
|
||||
},
|
||||
ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "posteo.de".to_string(),
|
||||
port: 143,
|
||||
security: ConnectionSecurity::Starttls,
|
||||
},
|
||||
user: user.to_string(),
|
||||
},
|
||||
],
|
||||
imap_user: "alice@posteo.de".to_string(),
|
||||
imap_password: "foobarbaz".to_string(),
|
||||
smtp: vec![
|
||||
ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "posteo.de".to_string(),
|
||||
port: 465,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: user.to_string(),
|
||||
},
|
||||
ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "posteo.de".to_string(),
|
||||
port: 587,
|
||||
security: ConnectionSecurity::Starttls,
|
||||
},
|
||||
user: user.to_string(),
|
||||
},
|
||||
],
|
||||
smtp_user: "alice@posteo.de".to_string(),
|
||||
smtp_password: "foobarbaz".to_string(),
|
||||
provider: get_provider_by_id("posteo"),
|
||||
certificate_checks: ConfiguredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
};
|
||||
|
||||
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
|
||||
assert_eq!(loaded, param);
|
||||
|
||||
migrate_configured_login_param(&t).await;
|
||||
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(loaded, param);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_empty_server_list_legacy() -> Result<()> {
|
||||
// Find a provider that does not have server list set.
|
||||
//
|
||||
// There is at least one such provider in the provider database.
|
||||
let (domain, provider) = crate::provider::data::PROVIDER_DATA
|
||||
.iter()
|
||||
.find(|(_domain, provider)| provider.server.is_empty())
|
||||
.unwrap();
|
||||
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let addr = format!("alice@{domain}");
|
||||
|
||||
t.set_config(Config::Configured, Some("1")).await?;
|
||||
t.set_config(Config::ConfiguredProvider, Some(provider.id))
|
||||
.await?;
|
||||
t.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredServerFlags, Some("0"))
|
||||
.await?;
|
||||
|
||||
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
|
||||
assert_eq!(loaded.provider, Some(*provider));
|
||||
assert_eq!(loaded.imap.is_empty(), false);
|
||||
assert_eq!(loaded.smtp.is_empty(), false);
|
||||
|
||||
migrate_configured_login_param(&t).await;
|
||||
|
||||
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(loaded.provider, Some(*provider));
|
||||
assert_eq!(loaded.imap.is_empty(), false);
|
||||
assert_eq!(loaded.smtp.is_empty(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn migrate_configured_login_param(t: &TestContext) {
|
||||
t.sql.execute("DROP TABLE transports;", ()).await.unwrap();
|
||||
t.sql.set_raw_config_int("dbversion", 130).await.unwrap();
|
||||
t.sql.run_migrations(t).await.log_err(t).ok();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_empty_server_list() -> Result<()> {
|
||||
// Find a provider that does not have server list set.
|
||||
//
|
||||
// There is at least one such provider in the provider database.
|
||||
let (domain, provider) = crate::provider::data::PROVIDER_DATA
|
||||
.iter()
|
||||
.find(|(_domain, provider)| provider.server.is_empty())
|
||||
.unwrap();
|
||||
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let addr = format!("alice@{domain}");
|
||||
|
||||
ConfiguredLoginParam {
|
||||
addr: addr.clone(),
|
||||
imap: vec![ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "example.org".to_string(),
|
||||
port: 100,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: addr.clone(),
|
||||
}],
|
||||
imap_user: addr.clone(),
|
||||
imap_password: "foobarbaz".to_string(),
|
||||
smtp: vec![ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "example.org".to_string(),
|
||||
port: 100,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: addr.clone(),
|
||||
}],
|
||||
smtp_user: addr.clone(),
|
||||
smtp_password: "foobarbaz".to_string(),
|
||||
provider: Some(provider),
|
||||
certificate_checks: ConfiguredCertificateChecks::Automatic,
|
||||
oauth2: false,
|
||||
}
|
||||
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
|
||||
.await?;
|
||||
|
||||
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(loaded.provider, Some(*provider));
|
||||
assert_eq!(loaded.imap.is_empty(), false);
|
||||
assert_eq!(loaded.smtp.is_empty(), false);
|
||||
assert_eq!(t.get_configured_provider().await?, Some(*provider));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
mod transport_tests;
|
||||
|
||||
485
src/transport/transport_tests.rs
Normal file
485
src/transport/transport_tests.rs
Normal file
@@ -0,0 +1,485 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
use super::*;
|
||||
use crate::log::LogExt as _;
|
||||
use crate::provider::get_provider_by_id;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::time;
|
||||
|
||||
#[test]
|
||||
fn test_configured_certificate_checks_display() {
|
||||
use std::string::ToString;
|
||||
|
||||
assert_eq!(
|
||||
"accept_invalid_certificates".to_string(),
|
||||
ConfiguredCertificateChecks::AcceptInvalidCertificates.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_load_login_param() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let param = ConfiguredLoginParam {
|
||||
addr: "alice@example.org".to_string(),
|
||||
imap: vec![ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "imap.example.com".to_string(),
|
||||
port: 123,
|
||||
security: ConnectionSecurity::Starttls,
|
||||
},
|
||||
user: "alice".to_string(),
|
||||
}],
|
||||
imap_user: "".to_string(),
|
||||
imap_password: "foo".to_string(),
|
||||
smtp: vec![ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "smtp.example.com".to_string(),
|
||||
port: 456,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: "alice@example.org".to_string(),
|
||||
}],
|
||||
smtp_user: "".to_string(),
|
||||
smtp_password: "bar".to_string(),
|
||||
provider: None,
|
||||
certificate_checks: ConfiguredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
};
|
||||
|
||||
param
|
||||
.clone()
|
||||
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
|
||||
.await?;
|
||||
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#;
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.query_get_value::<String>("SELECT configured_param FROM transports", ())
|
||||
.await?
|
||||
.unwrap(),
|
||||
expected_param
|
||||
);
|
||||
assert_eq!(t.is_configured().await?, true);
|
||||
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(param, loaded);
|
||||
|
||||
// Legacy ConfiguredImapCertificateChecks config is ignored
|
||||
t.set_config(Config::ConfiguredImapCertificateChecks, Some("999"))
|
||||
.await?;
|
||||
assert!(ConfiguredLoginParam::load(&t).await.is_ok());
|
||||
|
||||
// Test that we don't panic on unknown ConfiguredImapCertificateChecks values.
|
||||
let wrong_param = expected_param.replace("Strict", "Stricct");
|
||||
assert_ne!(expected_param, wrong_param);
|
||||
t.sql
|
||||
.execute("UPDATE transports SET configured_param=?", (wrong_param,))
|
||||
.await?;
|
||||
assert!(ConfiguredLoginParam::load(&t).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_posteo_alias() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let user = "alice@posteo.de";
|
||||
|
||||
// Alice has old config with "alice@posteo.at" address
|
||||
// and "alice@posteo.de" username.
|
||||
t.set_config(Config::Configured, Some("1")).await?;
|
||||
t.set_config(Config::ConfiguredProvider, Some("posteo"))
|
||||
.await?;
|
||||
t.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredMailServer, Some("posteo.de"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredMailPort, Some("993"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredMailSecurity, Some("1"))
|
||||
.await?; // TLS
|
||||
t.set_config(Config::ConfiguredMailUser, Some(user)).await?;
|
||||
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredSendServer, Some("posteo.de"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSendPort, Some("465"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSendSecurity, Some("1"))
|
||||
.await?; // TLS
|
||||
t.set_config(Config::ConfiguredSendUser, Some(user)).await?;
|
||||
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredServerFlags, Some("0"))
|
||||
.await?;
|
||||
|
||||
let param = ConfiguredLoginParam {
|
||||
addr: "alice@posteo.at".to_string(),
|
||||
imap: vec![
|
||||
ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "posteo.de".to_string(),
|
||||
port: 993,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: user.to_string(),
|
||||
},
|
||||
ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "posteo.de".to_string(),
|
||||
port: 143,
|
||||
security: ConnectionSecurity::Starttls,
|
||||
},
|
||||
user: user.to_string(),
|
||||
},
|
||||
],
|
||||
imap_user: "alice@posteo.de".to_string(),
|
||||
imap_password: "foobarbaz".to_string(),
|
||||
smtp: vec![
|
||||
ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "posteo.de".to_string(),
|
||||
port: 465,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: user.to_string(),
|
||||
},
|
||||
ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "posteo.de".to_string(),
|
||||
port: 587,
|
||||
security: ConnectionSecurity::Starttls,
|
||||
},
|
||||
user: user.to_string(),
|
||||
},
|
||||
],
|
||||
smtp_user: "alice@posteo.de".to_string(),
|
||||
smtp_password: "foobarbaz".to_string(),
|
||||
provider: get_provider_by_id("posteo"),
|
||||
certificate_checks: ConfiguredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
};
|
||||
|
||||
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
|
||||
assert_eq!(loaded, param);
|
||||
|
||||
migrate_configured_login_param(&t).await;
|
||||
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(loaded, param);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_empty_server_list_legacy() -> Result<()> {
|
||||
// Find a provider that does not have server list set.
|
||||
//
|
||||
// There is at least one such provider in the provider database.
|
||||
let (domain, provider) = crate::provider::data::PROVIDER_DATA
|
||||
.iter()
|
||||
.find(|(_domain, provider)| provider.server.is_empty())
|
||||
.unwrap();
|
||||
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let addr = format!("alice@{domain}");
|
||||
|
||||
t.set_config(Config::Configured, Some("1")).await?;
|
||||
t.set_config(Config::ConfiguredProvider, Some(provider.id))
|
||||
.await?;
|
||||
t.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredServerFlags, Some("0"))
|
||||
.await?;
|
||||
|
||||
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
|
||||
assert_eq!(loaded.provider, Some(*provider));
|
||||
assert_eq!(loaded.imap.is_empty(), false);
|
||||
assert_eq!(loaded.smtp.is_empty(), false);
|
||||
|
||||
migrate_configured_login_param(&t).await;
|
||||
|
||||
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(loaded.provider, Some(*provider));
|
||||
assert_eq!(loaded.imap.is_empty(), false);
|
||||
assert_eq!(loaded.smtp.is_empty(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn migrate_configured_login_param(t: &TestContext) {
|
||||
t.sql.execute("DROP TABLE transports;", ()).await.unwrap();
|
||||
t.sql.set_raw_config_int("dbversion", 130).await.unwrap();
|
||||
t.sql.run_migrations(t).await.log_err(t).ok();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_empty_server_list() -> Result<()> {
|
||||
// Find a provider that does not have server list set.
|
||||
//
|
||||
// There is at least one such provider in the provider database.
|
||||
let (domain, provider) = crate::provider::data::PROVIDER_DATA
|
||||
.iter()
|
||||
.find(|(_domain, provider)| provider.server.is_empty())
|
||||
.unwrap();
|
||||
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let addr = format!("alice@{domain}");
|
||||
|
||||
dummy_configured_login_param(&addr, Some(provider))
|
||||
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
|
||||
.await?;
|
||||
|
||||
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(loaded.provider, Some(*provider));
|
||||
assert_eq!(loaded.imap.is_empty(), false);
|
||||
assert_eq!(loaded.smtp.is_empty(), false);
|
||||
assert_eq!(t.get_configured_provider().await?, Some(*provider));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dummy_configured_login_param(
|
||||
addr: &str,
|
||||
provider: Option<&'static Provider>,
|
||||
) -> ConfiguredLoginParam {
|
||||
ConfiguredLoginParam {
|
||||
addr: addr.to_string(),
|
||||
imap: vec![ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "example.org".to_string(),
|
||||
port: 100,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: addr.to_string(),
|
||||
}],
|
||||
imap_user: addr.to_string(),
|
||||
imap_password: "foobarbaz".to_string(),
|
||||
smtp: vec![ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "example.org".to_string(),
|
||||
port: 100,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: addr.to_string(),
|
||||
}],
|
||||
smtp_user: addr.to_string(),
|
||||
smtp_password: "foobarbaz".to_string(),
|
||||
provider,
|
||||
certificate_checks: ConfiguredCertificateChecks::Automatic,
|
||||
oauth2: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_is_published_flag() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
for a in [alice, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
a.set_config_bool(Config::BccSelf, true).await?;
|
||||
}
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
check_addrs(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
Addresses {
|
||||
primary: "alice@example.org",
|
||||
secondary_published: &[],
|
||||
secondary_unpublished: &[],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
dummy_configured_login_param("alice@otherprovider.com", None)
|
||||
.save_to_transports_table(
|
||||
alice,
|
||||
&EnteredLoginParam {
|
||||
addr: "alice@otherprovider.com".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
time(),
|
||||
)
|
||||
.await?;
|
||||
send_sync_transports(alice).await?;
|
||||
sync_and_check_recipients(alice, alice2, "alice@otherprovider.com alice@example.org").await;
|
||||
|
||||
check_addrs(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
Addresses {
|
||||
primary: "alice@example.org",
|
||||
secondary_published: &["alice@otherprovider.com"],
|
||||
secondary_unpublished: &[],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
alice
|
||||
.set_transport_unpublished("alice@example.org", true)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"Can't set primary relay as unpublished"
|
||||
);
|
||||
|
||||
// Make sure that the newly generated key has a newer timestamp,
|
||||
// so that it is recognized by Bob:
|
||||
SystemTime::shift(Duration::from_secs(2));
|
||||
|
||||
alice
|
||||
.set_transport_unpublished("alice@otherprovider.com", true)
|
||||
.await?;
|
||||
sync_and_check_recipients(alice, alice2, "alice@example.org").await;
|
||||
|
||||
check_addrs(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
Addresses {
|
||||
primary: "alice@example.org",
|
||||
secondary_published: &[],
|
||||
secondary_unpublished: &["alice@otherprovider.com"],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(2));
|
||||
|
||||
alice
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@otherprovider.com"))
|
||||
.await?;
|
||||
sync_and_check_recipients(alice, alice2, "alice@example.org alice@otherprovider.com").await;
|
||||
|
||||
check_addrs(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
Addresses {
|
||||
primary: "alice@otherprovider.com",
|
||||
secondary_published: &["alice@example.org"],
|
||||
secondary_unpublished: &[],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Addresses {
|
||||
primary: &'static str,
|
||||
secondary_published: &'static [&'static str],
|
||||
secondary_unpublished: &'static [&'static str],
|
||||
}
|
||||
|
||||
async fn check_addrs(
|
||||
alice: &TestContext,
|
||||
alice2: &TestContext,
|
||||
bob: &TestContext,
|
||||
addresses: Addresses,
|
||||
) {
|
||||
fn assert_eq(left: Vec<String>, right: Vec<&'static str>) {
|
||||
assert_eq!(
|
||||
left.iter().map(|s| s.as_str()).collect::<BTreeSet<_>>(),
|
||||
right.into_iter().collect::<BTreeSet<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
let published_self_addrs = concat(&[addresses.secondary_published, &[addresses.primary]]);
|
||||
for a in [alice2, alice] {
|
||||
assert_eq(
|
||||
a.get_all_self_addrs().await.unwrap(),
|
||||
concat(&[
|
||||
addresses.secondary_published,
|
||||
addresses.secondary_unpublished,
|
||||
&[addresses.primary],
|
||||
]),
|
||||
);
|
||||
assert_eq(
|
||||
a.get_published_self_addrs().await.unwrap(),
|
||||
published_self_addrs.clone(),
|
||||
);
|
||||
assert_eq(
|
||||
a.get_secondary_self_addrs().await.unwrap(),
|
||||
concat(&[
|
||||
addresses.secondary_published,
|
||||
addresses.secondary_unpublished,
|
||||
]),
|
||||
);
|
||||
assert_eq(
|
||||
a.get_published_secondary_self_addrs().await.unwrap(),
|
||||
concat(&[addresses.secondary_published]),
|
||||
);
|
||||
for transport in a.list_transports().await.unwrap() {
|
||||
if addresses.primary == transport.param.addr
|
||||
|| addresses
|
||||
.secondary_published
|
||||
.contains(&transport.param.addr.as_str())
|
||||
{
|
||||
assert_eq!(transport.is_unpublished, false);
|
||||
} else if addresses
|
||||
.secondary_unpublished
|
||||
.contains(&transport.param.addr.as_str())
|
||||
{
|
||||
assert_eq!(transport.is_unpublished, true);
|
||||
} else {
|
||||
panic!("Unexpected transport {transport:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let alice_bob_chat_id = a.create_chat_id(bob).await;
|
||||
let sent = a.send_text(alice_bob_chat_id, "hi").await;
|
||||
assert_eq!(
|
||||
sent.recipients,
|
||||
format!("bob@example.net {}", published_self_addrs.join(" ")),
|
||||
"{} is sending to the wrong set of recipients",
|
||||
a.name()
|
||||
);
|
||||
let bob_alice_chat_id = bob.recv_msg(&sent).await.chat_id;
|
||||
bob_alice_chat_id.accept(bob).await.unwrap();
|
||||
let answer = bob.send_text(bob_alice_chat_id, "hi back").await;
|
||||
assert_eq(
|
||||
answer.recipients.split(' ').map(Into::into).collect(),
|
||||
concat(&[&published_self_addrs, &["bob@example.net"]]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn concat(slices: &[&[&'static str]]) -> Vec<&'static str> {
|
||||
let mut res = vec![];
|
||||
for s in slices {
|
||||
res.extend(*s);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn sync_and_check_recipients(from: &TestContext, to: &TestContext, recipients: &str) {
|
||||
from.send_sync_msg().await.unwrap();
|
||||
let sync_msg = from.pop_sent_msg().await;
|
||||
assert_eq!(sync_msg.recipients, recipients);
|
||||
to.recv_msg_trash(&sync_msg).await;
|
||||
}
|
||||
Reference in New Issue
Block a user