Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions deltachat-ffi/deltachat.h
Original file line number Diff line number Diff line change
Expand Up @@ -6784,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

Expand Down
38 changes: 38 additions & 0 deletions deltachat-jsonrpc/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
Expand All @@ -553,7 +555,23 @@ 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
/// 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>> {
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 res = ctx
.list_transports()
Expand All @@ -571,6 +589,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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default is false

/// 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?;
Expand Down
19 changes: 19 additions & 0 deletions deltachat-jsonrpc/src/api/types/login_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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();
Expand Down
24 changes: 11 additions & 13 deletions src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")?;
Expand Down Expand Up @@ -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
{
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
// 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())
{
info!(
context,
"Message {} has no recipient, skipping smtp-send.", msg.id
Expand Down Expand Up @@ -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?;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change actually fixes an unrelated bug, and is necessary for the tests to pass: We need to pass is_encrypted to add_self_recipients(), not needs_encryption

}

if needs_encryption && !rendered_msg.is_encrypted {
/* unrecoverable */
message::set_msg_failed(
Expand Down
34 changes: 33 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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| {
Expand All @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
WHERE is_published=1
WHERE is_published

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> {
Expand Down
57 changes: 50 additions & 7 deletions src/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default is false, because it is "unpublished", not "published"

/// 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),
)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs .context() so we know what query fails if we at some point do something about "primary relay" (failover) and remove configured_addr. Or just someone calls this function on unconfigured context, this will fail because of no rows.

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 ...");

Expand Down
2 changes: 1 addition & 1 deletion src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
10 changes: 10 additions & 0 deletions src/login_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/mimeparser/shared_secret_decryption_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something unrelated

test_shared_secret_decryption_ex(
bob,
&charlie_addr,
Expand Down
2 changes: 1 addition & 1 deletion src/smtp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ pub(crate) async fn add_self_recipients(
// 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);
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/sql/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
4 changes: 4 additions & 0 deletions src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading