mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +03:00
api: Add list_transports_ex() and set_transport_unpublished() functions
This commit is contained in:
@@ -6784,8 +6784,8 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
* UI should update the list.
|
* UI should update the list.
|
||||||
*
|
*
|
||||||
* The event is emitted when the transports are modified on another device
|
* 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`
|
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`,
|
||||||
* or `set_config(configured_addr)`.
|
* `set_transport_unpublished` or `set_config(configured_addr)`.
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
|
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ use self::types::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
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};
|
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -528,6 +529,7 @@ impl CommandApi {
|
|||||||
/// from a server encoded in a QR code.
|
/// from a server encoded in a QR code.
|
||||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||||
/// - [Self::delete_transport()] to remove a transport.
|
/// - [Self::delete_transport()] to remove a transport.
|
||||||
|
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
|
||||||
async fn add_or_update_transport(
|
async fn add_or_update_transport(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -553,7 +555,23 @@ impl CommandApi {
|
|||||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
/// 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
|
||||||
/// and [Self::delete_transport()] to delete a transport.
|
/// and [Self::delete_transport()] to delete a transport.
|
||||||
|
/// Use [Self::list_transports_ex()] to additionally query
|
||||||
|
/// whether the transports are marked as 'unpublished'.
|
||||||
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let res = ctx
|
||||||
|
.list_transports()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| t.param.into())
|
||||||
|
.collect();
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
async fn list_transports_ex(&self, account_id: u32) -> Result<Vec<Transport>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let res = ctx
|
let res = ctx
|
||||||
.list_transports()
|
.list_transports()
|
||||||
@@ -571,6 +589,26 @@ impl CommandApi {
|
|||||||
ctx.delete_transport(&addr).await
|
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.
|
/// Signal an ongoing process to stop.
|
||||||
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
|
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ use serde::Deserialize;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use yerpc::TypeDef;
|
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.
|
/// Login parameters entered by the user.
|
||||||
///
|
///
|
||||||
/// Usually it will be enough to only set `addr` and `password`,
|
/// Usually it will be enough to only set `addr` and `password`,
|
||||||
@@ -56,6 +66,15 @@ pub struct EnteredLoginParam {
|
|||||||
pub oauth2: Option<bool>,
|
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 {
|
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||||
fn from(param: dc::EnteredLoginParam) -> Self {
|
fn from(param: dc::EnteredLoginParam) -> Self {
|
||||||
let imap_security: Socket = param.imap.security.into();
|
let imap_security: Socket = param.imap.security.into();
|
||||||
|
|||||||
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_id.unarchive_if_not_muted(context, msg.state).await?;
|
||||||
}
|
}
|
||||||
chat.prepare_msg_raw(context, msg, update_msg_id).await?;
|
chat.prepare_msg_raw(context, msg, update_msg_id).await?;
|
||||||
|
|
||||||
let row_ids = create_send_msg_jobs(context, msg)
|
let row_ids = create_send_msg_jobs(context, msg)
|
||||||
.await
|
.await
|
||||||
.context("Failed to create send jobs")?;
|
.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();
|
let lowercase_from = from.to_lowercase();
|
||||||
|
|
||||||
recipients.retain(|x| x.to_lowercase() != lowercase_from);
|
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!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"Message {} has no recipient, skipping smtp-send.", msg.id
|
"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 {
|
if needs_encryption && !rendered_msg.is_encrypted {
|
||||||
/* unrecoverable */
|
/* unrecoverable */
|
||||||
message::set_msg_failed(
|
message::set_msg_failed(
|
||||||
|
|||||||
@@ -837,7 +837,7 @@ impl Context {
|
|||||||
// which only fetches from the primary transport.
|
// which only fetches from the primary transport.
|
||||||
transaction
|
transaction
|
||||||
.execute(
|
.execute(
|
||||||
"UPDATE transports SET add_timestamp=? WHERE addr=?",
|
"UPDATE transports SET add_timestamp=?, is_published=1 WHERE addr=?",
|
||||||
(time(), addr),
|
(time(), addr),
|
||||||
)
|
)
|
||||||
.context(
|
.context(
|
||||||
@@ -974,6 +974,21 @@ impl Context {
|
|||||||
.await
|
.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.
|
/// Returns all secondary self addresses.
|
||||||
pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
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| {
|
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
|
}).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 the primary self address.
|
||||||
/// Returns an error if no self addr is configured.
|
/// Returns an error if no self addr is configured.
|
||||||
pub async fn get_primary_self_addr(&self) -> Result<String> {
|
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::context::Context;
|
||||||
use crate::imap::Imap;
|
use crate::imap::Imap;
|
||||||
use crate::log::warn;
|
use crate::log::warn;
|
||||||
use crate::login_param::EnteredCertificateChecks;
|
|
||||||
pub use crate::login_param::EnteredLoginParam;
|
pub use crate::login_param::EnteredLoginParam;
|
||||||
|
use crate::login_param::{EnteredCertificateChecks, Transport};
|
||||||
use crate::message::Message;
|
use crate::message::Message;
|
||||||
use crate::net::proxy::ProxyConfig;
|
use crate::net::proxy::ProxyConfig;
|
||||||
use crate::oauth2::get_oauth2_addr;
|
use crate::oauth2::get_oauth2_addr;
|
||||||
@@ -110,6 +110,7 @@ impl Context {
|
|||||||
/// from a server encoded in a QR code.
|
/// from a server encoded in a QR code.
|
||||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||||
/// - [Self::delete_transport()] to remove a transport.
|
/// - [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<()> {
|
pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||||
self.stop_io().await;
|
self.stop_io().await;
|
||||||
let result = self.add_transport_inner(param).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.
|
/// 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
|
||||||
/// and [Self::delete_transport()] to delete 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
|
let transports = self
|
||||||
.sql
|
.sql
|
||||||
.query_map_vec("SELECT entered_param FROM transports", (), |row| {
|
.query_map_vec(
|
||||||
let entered_param: String = row.get(0)?;
|
"SELECT entered_param, is_published FROM transports",
|
||||||
let transport: EnteredLoginParam = serde_json::from_str(&entered_param)?;
|
(),
|
||||||
Ok(transport)
|
|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?;
|
.await?;
|
||||||
|
|
||||||
Ok(transports)
|
Ok(transports)
|
||||||
@@ -261,6 +270,40 @@ impl Context {
|
|||||||
Ok(())
|
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<()> {
|
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||||
info!(self, "Configure ...");
|
info!(self, "Configure ...");
|
||||||
|
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option
|
|||||||
.await?
|
.await?
|
||||||
.context("No transports configured")?;
|
.context("No transports configured")?;
|
||||||
let addr = context.get_primary_self_addr().await?;
|
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 =
|
let signed_public_key =
|
||||||
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
|
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
|
||||||
*lock = Some(signed_public_key.clone());
|
*lock = Some(signed_public_key.clone());
|
||||||
|
|||||||
@@ -79,6 +79,16 @@ pub struct EnteredServerLoginParam {
|
|||||||
pub password: String,
|
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.
|
/// Login parameters entered by the user.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct EnteredLoginParam {
|
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 charlie_addr = charlie.get_config(Config::Addr).await?.unwrap();
|
||||||
|
|
||||||
let alice_fp = self_fingerprint(alice).await?;
|
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(
|
test_shared_secret_decryption_ex(
|
||||||
bob,
|
bob,
|
||||||
&charlie_addr,
|
&charlie_addr,
|
||||||
|
|||||||
@@ -701,12 +701,12 @@ pub(crate) async fn add_self_recipients(
|
|||||||
// and connection is frequently lost
|
// and connection is frequently lost
|
||||||
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
|
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
|
||||||
// disabled by default is fine.
|
// 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
|
// 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
|
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
|
||||||
// messages.
|
// messages.
|
||||||
if encrypted {
|
if encrypted {
|
||||||
for addr in context.get_secondary_self_addrs().await? {
|
for addr in context.get_published_secondary_self_addrs().await? {
|
||||||
recipients.push(addr);
|
recipients.push(addr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2343,6 +2343,26 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
|
|||||||
.await?;
|
.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
|
let new_version = sql
|
||||||
.get_raw_config_int(VERSION_CFG)
|
.get_raw_config_int(VERSION_CFG)
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ pub(crate) struct TransportData {
|
|||||||
|
|
||||||
/// Timestamp of when the transport was last time (re)configured.
|
/// Timestamp of when the transport was last time (re)configured.
|
||||||
pub(crate) timestamp: i64,
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ impl TestContextManager {
|
|||||||
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
||||||
(
|
(
|
||||||
new_addr,
|
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}}"#)
|
format!(r#"{{"addr":"{new_addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
|
||||||
),
|
),
|
||||||
).await.unwrap();
|
).await.unwrap();
|
||||||
|
|||||||
@@ -562,7 +562,15 @@ impl ConfiguredLoginParam {
|
|||||||
entered_param: &EnteredLoginParam,
|
entered_param: &EnteredLoginParam,
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
) -> Result<()> {
|
) -> 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,6 +636,7 @@ pub(crate) async fn save_transport(
|
|||||||
entered_param: &EnteredLoginParam,
|
entered_param: &EnteredLoginParam,
|
||||||
configured: &ConfiguredLoginParamJson,
|
configured: &ConfiguredLoginParamJson,
|
||||||
add_timestamp: i64,
|
add_timestamp: i64,
|
||||||
|
is_published: bool,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let addr = addr_normalize(&configured.addr);
|
let addr = addr_normalize(&configured.addr);
|
||||||
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
|
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||||
@@ -635,20 +644,23 @@ pub(crate) async fn save_transport(
|
|||||||
let mut modified = context
|
let mut modified = context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
|
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp, is_published)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (addr)
|
ON CONFLICT (addr)
|
||||||
DO UPDATE SET entered_param=excluded.entered_param,
|
DO UPDATE SET entered_param=excluded.entered_param,
|
||||||
configured_param=excluded.configured_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
|
WHERE entered_param != excluded.entered_param
|
||||||
OR configured_param != excluded.configured_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,
|
&addr,
|
||||||
serde_json::to_string(entered_param)?,
|
serde_json::to_string(entered_param)?,
|
||||||
serde_json::to_string(configured)?,
|
serde_json::to_string(configured)?,
|
||||||
add_timestamp,
|
add_timestamp,
|
||||||
|
is_published,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
@@ -685,7 +697,7 @@ pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
|
|||||||
let transports = context
|
let transports = context
|
||||||
.sql
|
.sql
|
||||||
.query_map_vec(
|
.query_map_vec(
|
||||||
"SELECT entered_param, configured_param, add_timestamp
|
"SELECT entered_param, configured_param, add_timestamp, is_published
|
||||||
FROM transports WHERE id>1",
|
FROM transports WHERE id>1",
|
||||||
(),
|
(),
|
||||||
|row| {
|
|row| {
|
||||||
@@ -694,10 +706,12 @@ pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
|
|||||||
let configured_json: String = row.get(1)?;
|
let configured_json: String = row.get(1)?;
|
||||||
let configured: ConfiguredLoginParamJson = serde_json::from_str(&configured_json)?;
|
let configured: ConfiguredLoginParamJson = serde_json::from_str(&configured_json)?;
|
||||||
let timestamp: i64 = row.get(2)?;
|
let timestamp: i64 = row.get(2)?;
|
||||||
|
let is_published: bool = row.get(3)?;
|
||||||
Ok(TransportData {
|
Ok(TransportData {
|
||||||
configured,
|
configured,
|
||||||
entered,
|
entered,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
is_published,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -736,9 +750,10 @@ pub(crate) async fn sync_transports(
|
|||||||
configured,
|
configured,
|
||||||
entered,
|
entered,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
is_published,
|
||||||
} in transports
|
} in transports
|
||||||
{
|
{
|
||||||
modified |= save_transport(context, entered, configured, *timestamp).await?;
|
modified |= save_transport(context, entered, configured, *timestamp, *is_published).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
context
|
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 (?, ?, ?)",
|
"INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
||||||
(
|
(
|
||||||
addr,
|
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}}"#)
|
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::tools::SystemTime;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::log::LogExt as _;
|
use crate::log::LogExt as _;
|
||||||
use crate::provider::get_provider_by_id;
|
use crate::provider::get_provider_by_id;
|
||||||
use crate::test_utils::TestContext;
|
use crate::test_utils::TestContext;
|
||||||
|
use crate::test_utils::TestContextManager;
|
||||||
use crate::tools::time;
|
use crate::tools::time;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -239,32 +245,7 @@ async fn test_empty_server_list() -> Result<()> {
|
|||||||
|
|
||||||
let addr = format!("alice@{domain}");
|
let addr = format!("alice@{domain}");
|
||||||
|
|
||||||
ConfiguredLoginParam {
|
dummy_configured_login_param(&addr, Some(provider))
|
||||||
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())
|
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -276,3 +257,229 @@ async fn test_empty_server_list() -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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