api: Add list_transports_ex() and set_transport_unpublished() functions

Closes https://github.com/chatmail/core/issues/7980.

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.

- This adds `list_transports_ex()` and `set_transport_unpublished()` JsonRPC functions
- By default, transports are published, but when updating, all existing transports except for the primary one become unpublished in order not to break existing users that followed https://delta.chat/legacy-move
- It is not possible to unpublish the primary transport, and setting a transport as primary automatically sets it to published

An alternative would be to change the existing list_transports API rather than adding a new one list_transports_ex. But to be honest, I don't mind the _ex prefix that much, and I am wary about compatibility issues. But maybe it would be fine; see b08ba4bb8 for how this would look.
This commit is contained in:
Hocuri
2026-03-15 17:36:01 +01:00
parent c0cc2ae816
commit 810dab12dc
15 changed files with 453 additions and 61 deletions

View File

@@ -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(&param)?;
let is_published: bool = row.get(1)?;
Ok(Transport {
param,
is_unpublished: !is_published,
})
},
)
.await?;
Ok(transports)
@@ -261,6 +270,44 @@ 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 our new transport addresses yet.
///
/// The default is false, but when the user updates from a version that didn't have this flag,
/// 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<()> {
self.sql
.transaction(|trans| {
let primary_addr: String = trans
.query_row(
"SELECT value FROM config WHERE keyname='configured_addr'",
(),
|row| row.get(0),
)
.context("Select primary address")?;
if primary_addr == addr && unpublished {
bail!("Can't set primary relay as unpublished");
}
// We need to update the timestamp so that the key's timestamp changes
// and is recognized as newer by our peers
trans
.execute(
"UPDATE transports SET is_published=?, add_timestamp=? WHERE addr=? AND is_published!=?1",
(!unpublished, time(), addr),
)
.context("Update transports")?;
Ok(())
})
.await?;
send_sync_transports(self).await?;
Ok(())
}
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
info!(self, "Configure ...");