Merge branch 'master' into flub/send-backup

This commit is contained in:
Floris Bruynooghe
2023-02-16 17:19:31 +01:00
40 changed files with 413 additions and 125 deletions

View File

@@ -4,14 +4,19 @@
### Changes ### Changes
- deltachat-rpc-client: use `dataclass` for `Account`, `Chat`, `Contact` and `Message` #4042 - deltachat-rpc-client: use `dataclass` for `Account`, `Chat`, `Contact` and `Message` #4042
- python: mark bindings as supporting typing according to PEP 561 #4045
- retry filesystem operations during account migration #4043
- ability to send backup over network and QR code to setup second device #4007 - ability to send backup over network and QR code to setup second device #4007
### Fixes ### Fixes
- deltachat-rpc-server: do not block stdin while processing the request. #4041 - deltachat-rpc-server: do not block stdin while processing the request. #4041
deltachat-rpc-server now reads the next request as soon as previous request handler is spawned. deltachat-rpc-server now reads the next request as soon as previous request handler is spawned.
- enable `auto_vacuum` on all SQL connections #2955
### API-Changes ### API-Changes
- Remove `MimeMessage::from_bytes()` public interface. #4033 - Remove `MimeMessage::from_bytes()` public interface. #4033
- BREAKING Types: jsonrpc: `get_messages` now returns a map with `MessageLoadResult` instead of failing completely if one of the requested messages could not be loaded. #4038
## 1.108.0 ## 1.108.0

View File

@@ -5777,7 +5777,7 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_INCOMING_MSG 2005 #define DC_EVENT_INCOMING_MSG 2005
/** /**
* Downloading a bunch of messages just finished. This is an experimental * Downloading a bunch of messages just finished. This is an
* event to allow the UI to only show one notification per message bunch, * event to allow the UI to only show one notification per message bunch,
* instead of cluttering the user with many notifications. * instead of cluttering the user with many notifications.
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before. * For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.

View File

@@ -45,6 +45,7 @@ use types::message::MessageObject;
use types::provider_info::ProviderInfo; use types::provider_info::ProviderInfo;
use types::webxdc::WebxdcMessageInfo; use types::webxdc::WebxdcMessageInfo;
use self::types::message::MessageLoadResult;
use self::types::{ use self::types::{
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration}, chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
location::JsonrpcLocation, location::JsonrpcLocation,
@@ -465,7 +466,7 @@ impl CommandApi {
Ok(res) => res, Ok(res) => res,
Err(err) => ChatListItemFetchResult::Error { Err(err) => ChatListItemFetchResult::Error {
id: entry.0, id: entry.0,
error: format!("{err:?}"), error: format!("{err:#}"),
}, },
}, },
); );
@@ -945,17 +946,27 @@ impl CommandApi {
MsgId::new(message_id).get_html(&ctx).await MsgId::new(message_id).get_html(&ctx).await
} }
/// get multiple messages in one call,
/// if loading one message fails the error is stored in the result object in it's place.
///
/// this is the batch variant of [get_message]
async fn get_messages( async fn get_messages(
&self, &self,
account_id: u32, account_id: u32,
message_ids: Vec<u32>, message_ids: Vec<u32>,
) -> Result<HashMap<u32, MessageObject>> { ) -> Result<HashMap<u32, MessageLoadResult>> {
let ctx = self.get_context(account_id).await?; let ctx = self.get_context(account_id).await?;
let mut messages: HashMap<u32, MessageObject> = HashMap::new(); let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
for message_id in message_ids { for message_id in message_ids {
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
messages.insert( messages.insert(
message_id, message_id,
MessageObject::from_message_id(&ctx, message_id).await?, match message_result {
Ok(message) => MessageLoadResult::Message(message),
Err(error) => MessageLoadResult::LoadingError {
error: format!("{error:#}"),
},
},
); );
} }
Ok(messages) Ok(messages)

View File

@@ -19,6 +19,13 @@ use super::contact::ContactObject;
use super::reactions::JSONRPCReactions; use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo; use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase", tag = "variant")]
pub enum MessageLoadResult {
Message(MessageObject),
LoadingError { error: String },
}
#[derive(Serialize, TypeDef)] #[derive(Serialize, TypeDef)]
#[serde(rename = "Message", rename_all = "camelCase")] #[serde(rename = "Message", rename_all = "camelCase")]
pub struct MessageObject { pub struct MessageObject {

View File

@@ -81,7 +81,9 @@ async function run() {
messageIds messageIds
); );
for (const [_messageId, message] of Object.entries(messages)) { for (const [_messageId, message] of Object.entries(messages)) {
write($main, `<p>${message.text}</p>`); if (message.variant === "message")
write($main, `<p>${message.text}</p>`);
else write($main, `<p>loading error: ${message.error}</p>`);
} }
} }
} }

View File

@@ -13,13 +13,6 @@ dynamic = [
"version" "version"
] ]
[tool.setuptools]
# We declare the package not-zip-safe so that our type hints are also available
# when checking client code that uses our (installed) package.
# Ref:
# https://mypy.readthedocs.io/en/stable/installed_packages.html?highlight=zip#using-installed-packages-with-mypy-pep-561
zip-safe = false
[tool.setuptools.package-data] [tool.setuptools.package-data]
deltachat_rpc_client = [ deltachat_rpc_client = [
"py.typed" "py.typed"

View File

@@ -36,6 +36,11 @@ dynamic = [
[project.entry-points.pytest11] [project.entry-points.pytest11]
"deltachat.testplugin" = "deltachat.testplugin" "deltachat.testplugin" = "deltachat.testplugin"
[tool.setuptools.package-data]
deltachat = [
"py.typed"
]
[tool.setuptools_scm] [tool.setuptools_scm]
root = ".." root = ".."
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$' tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
@@ -45,7 +50,7 @@ git_describe_command = "git describe --dirty --tags --long --match py-*.*"
line-length = 120 line-length = 120
[tool.ruff] [tool.ruff]
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP032"] select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032"]
line-length = 120 line-length = 120
[tool.isort] [tool.isort]

View File

@@ -1,6 +1,5 @@
"""Account class implementation.""" """Account class implementation."""
from __future__ import print_function
import os import os
from array import array from array import array
@@ -53,7 +52,7 @@ def get_dc_info_as_dict(dc_context):
return info_dict return info_dict
class Account(object): class Account:
"""Each account is tied to a sqlite database file which is fully managed """Each account is tied to a sqlite database file which is fully managed
by the underlying deltachat core library. All public Account methods are by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects. meant to be memory-safe and return memory-safe objects.
@@ -302,7 +301,7 @@ class Account(object):
elif isinstance(obj, str): elif isinstance(obj, str):
displayname, addr = parseaddr(obj) displayname, addr = parseaddr(obj)
else: else:
raise TypeError("don't know how to create chat for %r" % (obj,)) raise TypeError(f"don't know how to create chat for {obj!r}")
if name is None and displayname: if name is None and displayname:
name = displayname name = displayname

View File

@@ -18,7 +18,7 @@ from .cutil import (
from .message import Message from .message import Message
class Chat(object): class Chat:
"""Chat object which manages members and through which you can send and retrieve messages. """Chat object which manages members and through which you can send and retrieve messages.
You obtain instances of it through :class:`deltachat.account.Account`. You obtain instances of it through :class:`deltachat.account.Account`.

View File

@@ -9,7 +9,7 @@ from .chat import Chat
from .cutil import from_dc_charpointer, from_optional_dc_charpointer from .cutil import from_dc_charpointer, from_optional_dc_charpointer
class Contact(object): class Contact:
"""Delta-Chat Contact. """Delta-Chat Contact.
You obtain instances of it through :class:`deltachat.account.Account`. You obtain instances of it through :class:`deltachat.account.Account`.

View File

@@ -12,7 +12,7 @@ from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_char
from .reactions import Reactions from .reactions import Reactions
class Message(object): class Message:
"""Message object. """Message object.
You obtain instances of it through :class:`deltachat.account.Account` or You obtain instances of it through :class:`deltachat.account.Account` or

View File

@@ -8,7 +8,7 @@ class ProviderNotFoundError(Exception):
"""The provider information was not found.""" """The provider information was not found."""
class Provider(object): class Provider:
""" """
Provider information. Provider information.

View File

View File

@@ -4,7 +4,7 @@ from .capi import ffi, lib
from .cutil import from_dc_charpointer, iter_array from .cutil import from_dc_charpointer, iter_array
class Reactions(object): class Reactions:
"""Reactions object. """Reactions object.
You obtain instances of it through :class:`deltachat.message.Message`. You obtain instances of it through :class:`deltachat.message.Message`.

View File

@@ -1,5 +1,3 @@
from __future__ import print_function
import fnmatch import fnmatch
import io import io
import os import os

View File

@@ -14,7 +14,7 @@ def test_db_busy_error(acfactory, tmpdir):
def log(string): def log(string):
with log_lock: with log_lock:
print("%3.2f %s" % (time.time() - starttime, string)) print(f"{time.time() - starttime:3.2f} {string}")
# make a number of accounts # make a number of accounts
accounts = acfactory.get_many_online_accounts(3) accounts = acfactory.get_many_online_accounts(3)

View File

@@ -239,7 +239,7 @@ def test_fetch_existing(acfactory, lp, mvbox_move):
ac1_clone.start_io() ac1_clone.start_io()
assert_folders_configured(ac1_clone) assert_folders_configured(ac1_clone)
lp.sec("check that ac2 contact was fetchted during configure") lp.sec("check that ac2 contact was fetched during configure")
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED") ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
ac2_addr = ac2.get_config("addr") ac2_addr = ac2.get_config("addr")
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts()) assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())

View File

@@ -1,5 +1,3 @@
from __future__ import print_function
import os.path import os.path
import shutil import shutil
from filecmp import cmp from filecmp import cmp

View File

@@ -1,5 +1,3 @@
from __future__ import print_function
import os import os
import time import time
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone

View File

@@ -31,7 +31,7 @@ unset DCC_NEW_TMP_EMAIL
# Try to build wheels for a range of interpreters, but don't fail if they are not available. # Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10 # E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true
echo ----------------------- echo -----------------------

View File

@@ -1,6 +1,7 @@
//! # Account manager module. //! # Account manager module.
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result}; use anyhow::{ensure, Context as _, Result};
@@ -150,27 +151,9 @@ impl Accounts {
if let Some(cfg) = self.config.get_account(id) { if let Some(cfg) = self.config.get_account(id) {
let account_path = self.dir.join(cfg.dir); let account_path = self.dir.join(cfg.dir);
// Spend up to 1 minute trying to remove the files. try_many_times(|| fs::remove_dir_all(&account_path))
// Files may remain locked up to 30 seconds due to r2d2 bug: .await
// https://github.com/sfackler/r2d2/issues/99 .context("failed to remove account data")?;
let mut counter = 0;
loop {
counter += 1;
if let Err(err) = fs::remove_dir_all(&account_path)
.await
.context("failed to remove account data")
{
if counter > 60 {
return Err(err);
}
// Wait 1 second and try again.
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
} else {
break;
}
}
} }
self.config.remove_account(id).await?; self.config.remove_account(id).await?;
@@ -178,6 +161,8 @@ impl Accounts {
} }
/// Migrate an existing account into this structure. /// Migrate an existing account into this structure.
///
/// Returns the ID of new account.
pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> { pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
let blobdir = Context::derive_blobdir(&dbfile); let blobdir = Context::derive_blobdir(&dbfile);
let walfile = Context::derive_walfile(&dbfile); let walfile = Context::derive_walfile(&dbfile);
@@ -202,10 +187,10 @@ impl Accounts {
fs::create_dir_all(self.dir.join(&account_config.dir)) fs::create_dir_all(self.dir.join(&account_config.dir))
.await .await
.context("failed to create dir")?; .context("failed to create dir")?;
fs::rename(&dbfile, &new_dbfile) try_many_times(|| fs::rename(&dbfile, &new_dbfile))
.await .await
.context("failed to rename dbfile")?; .context("failed to rename dbfile")?;
fs::rename(&blobdir, &new_blobdir) try_many_times(|| fs::rename(&blobdir, &new_blobdir))
.await .await
.context("failed to rename blobdir")?; .context("failed to rename blobdir")?;
if walfile.exists() { if walfile.exists() {
@@ -229,11 +214,10 @@ impl Accounts {
Ok(account_config.id) Ok(account_config.id)
} }
Err(err) => { Err(err) => {
// remove temp account let account_path = std::path::PathBuf::from(&account_config.dir);
fs::remove_dir_all(std::path::PathBuf::from(&account_config.dir)) try_many_times(|| fs::remove_dir_all(&account_path))
.await .await
.context("failed to remove account data")?; .context("failed to remove account data")?;
self.config.remove_account(account_config.id).await?; self.config.remove_account(account_config.id).await?;
// set selection back // set selection back
@@ -488,6 +472,33 @@ impl Config {
} }
} }
/// Spend up to 1 minute trying to do the operation.
///
/// Files may remain locked up to 30 seconds due to r2d2 bug:
/// <https://github.com/sfackler/r2d2/issues/99>
async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
where
F: Fn() -> Fut,
Fut: Future<Output = std::result::Result<(), T>>,
{
let mut counter = 0;
loop {
counter += 1;
if let Err(err) = f().await {
if counter > 60 {
return Err(err);
}
// Wait 1 second and try again.
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
} else {
break;
}
}
Ok(())
}
/// Configuration of a single account. /// Configuration of a single account.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct AccountConfig { struct AccountConfig {

View File

@@ -2075,8 +2075,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
Ok(()) Ok(())
} }
/// Prepares a message to be send out /// Prepares a message to be sent out.
/// - Checks if chat can be sent to
async fn prepare_msg_common( async fn prepare_msg_common(
context: &Context, context: &Context,
chat_id: ChatId, chat_id: ChatId,
@@ -2084,6 +2083,8 @@ async fn prepare_msg_common(
change_state_to: MessageState, change_state_to: MessageState,
) -> Result<MsgId> { ) -> Result<MsgId> {
let mut chat = Chat::load_from_db(context, chat_id).await?; let mut chat = Chat::load_from_db(context, chat_id).await?;
// Check if the chat can be sent to.
if let Some(reason) = chat.why_cant_send(context).await? { if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason); bail!("cannot send to {}: {}", chat_id, reason);
} }
@@ -2141,7 +2142,7 @@ pub async fn is_contact_in_chat(
Ok(exists) Ok(exists)
} }
/// Send a message defined by a dc_msg_t object to a chat. /// Sends a message object to a chat.
/// ///
/// Sends the event #DC_EVENT_MSGS_CHANGED on succcess. /// Sends the event #DC_EVENT_MSGS_CHANGED on succcess.
/// However, this does not imply, the message really reached the recipient - /// However, this does not imply, the message really reached the recipient -
@@ -3294,7 +3295,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
Ok(()) Ok(())
} }
/// Set a new profile image for the chat. /// Sets a new profile image for the chat.
/// ///
/// The profile image can only be set when you are a member of the /// The profile image can only be set when you are a member of the
/// chat. To remove the profile image pass an empty string for the /// chat. To remove the profile image pass an empty string for the

View File

@@ -1,7 +1,5 @@
//! # Key-value configuration management. //! # Key-value configuration management.
#![allow(missing_docs)]
use anyhow::{ensure, Context as _, Result}; use anyhow::{ensure, Context as _, Result};
use strum::{EnumProperty, IntoEnumIterator}; use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString}; use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
@@ -195,6 +193,8 @@ pub enum Config {
/// Configured IMAP server security (e.g. TLS, STARTTLS). /// Configured IMAP server security (e.g. TLS, STARTTLS).
ConfiguredMailSecurity, ConfiguredMailSecurity,
/// How to check IMAP server TLS certificates.
ConfiguredImapCertificateChecks, ConfiguredImapCertificateChecks,
/// Configured SMTP server hostname. /// Configured SMTP server hostname.
@@ -208,14 +208,26 @@ pub enum Config {
/// Configured SMTP server port. /// Configured SMTP server port.
ConfiguredSendPort, ConfiguredSendPort,
/// How to check SMTP server TLS certificates.
ConfiguredSmtpCertificateChecks, ConfiguredSmtpCertificateChecks,
/// Whether OAuth 2 is used with configured provider. /// Whether OAuth 2 is used with configured provider.
ConfiguredServerFlags, ConfiguredServerFlags,
/// Configured SMTP server security (e.g. TLS, STARTTLS).
ConfiguredSendSecurity, ConfiguredSendSecurity,
/// Configured folder for incoming messages.
ConfiguredInboxFolder, ConfiguredInboxFolder,
/// Configured folder for chat messages.
ConfiguredMvboxFolder, ConfiguredMvboxFolder,
/// Configured "Sent" folder.
ConfiguredSentboxFolder, ConfiguredSentboxFolder,
/// Unix timestamp of the last successful configuration.
ConfiguredTimestamp, ConfiguredTimestamp,
/// ID of the configured provider from the provider database. /// ID of the configured provider from the provider database.
@@ -228,12 +240,15 @@ pub enum Config {
/// (`addr1@example.org addr2@exapmle.org addr3@example.org`) /// (`addr1@example.org addr2@exapmle.org addr3@example.org`)
SecondaryAddrs, SecondaryAddrs,
/// Read-only core version string.
#[strum(serialize = "sys.version")] #[strum(serialize = "sys.version")]
SysVersion, SysVersion,
/// Maximal recommended attachment size in bytes.
#[strum(serialize = "sys.msgsize_max_recommended")] #[strum(serialize = "sys.msgsize_max_recommended")]
SysMsgsizeMaxRecommended, SysMsgsizeMaxRecommended,
/// Space separated list of all config keys available.
#[strum(serialize = "sys.config_keys")] #[strum(serialize = "sys.config_keys")]
SysConfigKeys, SysConfigKeys,
@@ -419,6 +434,7 @@ impl Context {
Ok(()) Ok(())
} }
/// Set the given config to a boolean value.
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> { pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
self.set_config(key, if value { Some("1") } else { Some("0") }) self.set_config(key, if value { Some("1") } else { Some("0") })
.await?; .await?;

View File

@@ -66,14 +66,20 @@ pub enum KeyGenType {
Ed25519 = 2, Ed25519 = 2,
} }
/// Video chat URL type.
#[derive( #[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)] )]
#[repr(i8)] #[repr(i8)]
pub enum VideochatType { pub enum VideochatType {
/// Unknown type.
#[default] #[default]
Unknown = 0, Unknown = 0,
/// [basicWebRTC](https://github.com/cracker0dks/basicwebrtc) instance.
BasicWebrtc = 1, BasicWebrtc = 1,
/// [Jitsi Meet](https://jitsi.org/jitsi-meet/) instance.
Jitsi = 2, Jitsi = 2,
} }
@@ -109,6 +115,7 @@ pub const DC_CHAT_ID_ALLDONE_HINT: ChatId = ChatId::new(7);
/// larger chat IDs are "real" chats, their messages are "real" messages. /// larger chat IDs are "real" chats, their messages are "real" messages.
pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9); pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
/// Chat type.
#[derive( #[derive(
Debug, Debug,
Default, Default,
@@ -127,11 +134,20 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
)] )]
#[repr(u32)] #[repr(u32)]
pub enum Chattype { pub enum Chattype {
/// Undefined chat type.
#[default] #[default]
Undefined = 0, Undefined = 0,
/// 1:1 chat.
Single = 100, Single = 100,
/// Group chat.
Group = 120, Group = 120,
/// Mailing list.
Mailinglist = 140, Mailinglist = 140,
/// Broadcast list.
Broadcast = 160, Broadcast = 160,
} }

View File

@@ -1,7 +1,5 @@
//! # Events specification. //! # Events specification.
#![allow(missing_docs)]
use std::path::PathBuf; use std::path::PathBuf;
use async_channel::{self as channel, Receiver, Sender, TrySendError}; use async_channel::{self as channel, Receiver, Sender, TrySendError};
@@ -111,6 +109,7 @@ pub struct Event {
pub typ: EventType, pub typ: EventType,
} }
/// Event payload.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum EventType { pub enum EventType {
/// The library-user may write an informational string to the log. /// The library-user may write an informational string to the log.
@@ -171,17 +170,23 @@ pub enum EventType {
/// - Chats created, deleted or archived /// - Chats created, deleted or archived
/// - A draft has been set /// - A draft has been set
/// ///
/// `chat_id` is set if only a single chat is affected by the changes, otherwise 0.
/// `msg_id` is set if only a single message is affected by the changes, otherwise 0.
MsgsChanged { MsgsChanged {
/// Set if only a single chat is affected by the changes, otherwise 0.
chat_id: ChatId, chat_id: ChatId,
/// Set if only a single message is affected by the changes, otherwise 0.
msg_id: MsgId, msg_id: MsgId,
}, },
/// Reactions for the message changed. /// Reactions for the message changed.
ReactionsChanged { ReactionsChanged {
/// ID of the chat which the message belongs to.
chat_id: ChatId, chat_id: ChatId,
/// ID of the message for which reactions were changed.
msg_id: MsgId, msg_id: MsgId,
/// ID of the contact whose reaction set is changed.
contact_id: ContactId, contact_id: ContactId,
}, },
@@ -190,11 +195,16 @@ pub enum EventType {
/// ///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event. /// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
IncomingMsg { IncomingMsg {
/// ID of the chat where the message is assigned.
chat_id: ChatId, chat_id: ChatId,
/// ID of the message.
msg_id: MsgId, msg_id: MsgId,
}, },
/// Downloading a bunch of messages just finished.
IncomingMsgBunch { IncomingMsgBunch {
/// List of incoming message IDs.
msg_ids: Vec<MsgId>, msg_ids: Vec<MsgId>,
}, },
@@ -205,21 +215,30 @@ pub enum EventType {
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to /// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state(). /// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
MsgDelivered { MsgDelivered {
/// ID of the chat which the message belongs to.
chat_id: ChatId, chat_id: ChatId,
/// ID of the message that was successfully sent.
msg_id: MsgId, msg_id: MsgId,
}, },
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to /// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see dc_msg_get_state(). /// DC_STATE_OUT_FAILED, see dc_msg_get_state().
MsgFailed { MsgFailed {
/// ID of the chat which the message belongs to.
chat_id: ChatId, chat_id: ChatId,
/// ID of the message that could not be sent.
msg_id: MsgId, msg_id: MsgId,
}, },
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to /// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state(). /// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
MsgRead { MsgRead {
/// ID of the chat which the message belongs to.
chat_id: ChatId, chat_id: ChatId,
/// ID of the message that was read.
msg_id: MsgId, msg_id: MsgId,
}, },
@@ -234,7 +253,10 @@ pub enum EventType {
/// Chat ephemeral timer changed. /// Chat ephemeral timer changed.
ChatEphemeralTimerModified { ChatEphemeralTimerModified {
/// Chat ID.
chat_id: ChatId, chat_id: ChatId,
/// New ephemeral timer value.
timer: EphemeralTimer, timer: EphemeralTimer,
}, },
@@ -281,15 +303,15 @@ pub enum EventType {
/// ///
/// These events are typically sent after a joiner has scanned the QR code /// These events are typically sent after a joiner has scanned the QR code
/// generated by dc_get_securejoin_qr(). /// generated by dc_get_securejoin_qr().
///
/// @param data1 (int) ID of the contact that wants to join.
/// @param data2 (int) Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
SecurejoinInviterProgress { SecurejoinInviterProgress {
/// ID of the contact that wants to join.
contact_id: ContactId, contact_id: ContactId,
/// Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
progress: usize, progress: usize,
}, },
@@ -297,12 +319,13 @@ pub enum EventType {
/// (Bob, the person who scans the QR code). /// (Bob, the person who scans the QR code).
/// The events are typically sent while dc_join_securejoin(), which /// The events are typically sent while dc_join_securejoin(), which
/// may take some time, is executed. /// may take some time, is executed.
/// @param data1 (int) ID of the inviting contact.
/// @param data2 (int) Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
SecurejoinJoinerProgress { SecurejoinJoinerProgress {
/// ID of the inviting contact.
contact_id: ContactId, contact_id: ContactId,
/// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
progress: usize, progress: usize,
}, },
@@ -312,15 +335,21 @@ pub enum EventType {
/// dc_get_connectivity_html() for details. /// dc_get_connectivity_html() for details.
ConnectivityChanged, ConnectivityChanged,
/// The user's avatar changed.
SelfavatarChanged, SelfavatarChanged,
/// Webxdc status update received.
WebxdcStatusUpdate { WebxdcStatusUpdate {
/// Message ID.
msg_id: MsgId, msg_id: MsgId,
/// Status update ID.
status_update_serial: StatusUpdateSerial, status_update_serial: StatusUpdateSerial,
}, },
/// Inform that a message containing a webxdc instance has been deleted /// Inform that a message containing a webxdc instance has been deleted.
WebxdcInstanceDeleted { WebxdcInstanceDeleted {
/// ID of the deleted message.
msg_id: MsgId, msg_id: MsgId,
}, },
} }

View File

@@ -1,17 +1,18 @@
//! # List of email headers. //! # List of email headers.
#![allow(missing_docs)]
use mailparse::{MailHeader, MailHeaderMap}; use mailparse::{MailHeader, MailHeaderMap};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr)] #[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr)]
#[strum(serialize_all = "kebab_case")] #[strum(serialize_all = "kebab_case")]
#[allow(missing_docs)]
pub enum HeaderDef { pub enum HeaderDef {
MessageId, MessageId,
Subject, Subject,
Date, Date,
From_, From_,
To, To,
/// Carbon copy.
Cc, Cc,
Disposition, Disposition,
@@ -34,11 +35,18 @@ pub enum HeaderDef {
/// header, so it can be used to ignore such messages. /// header, so it can be used to ignore such messages.
XMozillaDraftInfo, XMozillaDraftInfo,
/// Mailing list ID defined in [RFC 2919](https://tools.ietf.org/html/rfc2919).
ListId, ListId,
ListPost, ListPost,
References, References,
/// In-Reply-To header containing Message-ID of the parent message.
InReplyTo, InReplyTo,
/// Used to detect mailing lists if contains "list" value
/// as described in [RFC 3834](https://tools.ietf.org/html/rfc3834)
Precedence, Precedence,
ContentType, ContentType,
ContentId, ContentId,
ChatVersion, ChatVersion,
@@ -52,9 +60,14 @@ pub enum HeaderDef {
ChatGroupMemberRemoved, ChatGroupMemberRemoved,
ChatGroupMemberAdded, ChatGroupMemberAdded,
ChatContent, ChatContent,
/// Duration of the attached media file.
ChatDuration, ChatDuration,
ChatDispositionNotificationTo, ChatDispositionNotificationTo,
ChatWebrtcRoom, ChatWebrtcRoom,
/// [Autocrypt](https://autocrypt.org/) header.
Autocrypt, Autocrypt,
AutocryptSetupMessage, AutocryptSetupMessage,
SecureJoin, SecureJoin,
@@ -63,6 +76,8 @@ pub enum HeaderDef {
SecureJoinInvitenumber, SecureJoinInvitenumber,
SecureJoinAuth, SecureJoinAuth,
Sender, Sender,
/// Ephemeral message timer.
EphemeralTimer, EphemeralTimer,
Received, Received,
@@ -81,8 +96,12 @@ impl HeaderDef {
} }
} }
#[allow(missing_docs)]
pub trait HeaderDefMap { pub trait HeaderDefMap {
/// Returns requested header value if it exists.
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>; fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
/// Returns requested header if it exists.
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>; fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>;
} }

View File

@@ -1,4 +1,4 @@
//! # Delta Chat Core Library. //! # Delta Chat Core Library
#![recursion_limit = "256"] #![recursion_limit = "256"]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -111,7 +111,7 @@ pub mod tools;
pub mod accounts; pub mod accounts;
pub mod reaction; pub mod reaction;
/// if set imap/incoming and smtp/outgoing MIME messages will be printed /// If set IMAP/incoming and SMTP/outgoing MIME messages will be printed.
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG"; pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
#[cfg(test)] #[cfg(test)]

View File

@@ -98,6 +98,8 @@ pub struct RenderedEmail {
/// Message ID (Message in the sense of Email) /// Message ID (Message in the sense of Email)
pub rfc724_mid: String, pub rfc724_mid: String,
/// Message subject.
pub subject: String, pub subject: String,
} }

View File

@@ -1,7 +1,5 @@
//! # MIME message parsing module. //! # MIME message parsing module.
#![allow(missing_docs)]
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
@@ -130,11 +128,13 @@ pub(crate) enum MailinglistType {
None, None,
} }
/// System message type.
#[derive( #[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)] )]
#[repr(u32)] #[repr(u32)]
pub enum SystemMessage { pub enum SystemMessage {
/// Unknown type of system message.
#[default] #[default]
Unknown = 0, Unknown = 0,
@@ -152,8 +152,14 @@ pub enum SystemMessage {
/// Autocrypt Setup Message. /// Autocrypt Setup Message.
AutocryptSetupMessage = 6, AutocryptSetupMessage = 6,
/// Secure-join message.
SecurejoinMessage = 7, SecurejoinMessage = 7,
/// Location streaming is enabled.
LocationStreamingEnabled = 8, LocationStreamingEnabled = 8,
/// Location-only message.
LocationOnly = 9, LocationOnly = 9,
/// Chat ephemeral message timer is changed. /// Chat ephemeral message timer is changed.
@@ -1792,6 +1798,8 @@ pub struct Part {
/// Size of the MIME part in bytes. /// Size of the MIME part in bytes.
pub bytes: usize, pub bytes: usize,
/// Parameters.
pub param: Params, pub param: Params,
/// Attachment filename. /// Attachment filename.

View File

@@ -1,7 +1,5 @@
//! OAuth 2 module. //! OAuth 2 module.
#![allow(missing_docs)]
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::Result; use anyhow::Result;
@@ -56,6 +54,8 @@ struct Response {
scope: Option<String>, scope: Option<String>,
} }
/// Returns URL that should be opened in the browser
/// if OAuth 2 is supported for this address.
pub async fn get_oauth2_url( pub async fn get_oauth2_url(
context: &Context, context: &Context,
addr: &str, addr: &str,
@@ -76,7 +76,7 @@ pub async fn get_oauth2_url(
} }
} }
pub async fn get_oauth2_access_token( pub(crate) async fn get_oauth2_access_token(
context: &Context, context: &Context,
addr: &str, addr: &str,
code: &str, code: &str,
@@ -228,7 +228,11 @@ pub async fn get_oauth2_access_token(
} }
} }
pub async fn get_oauth2_addr(context: &Context, addr: &str, code: &str) -> Result<Option<String>> { pub(crate) async fn get_oauth2_addr(
context: &Context,
addr: &str,
code: &str,
) -> Result<Option<String>> {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?; let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
let oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await { let oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await {
Some(o) => o, Some(o) => o,

View File

@@ -1,7 +1,5 @@
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp). //! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
#![allow(missing_docs)]
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::io; use std::io;
use std::io::Cursor; use std::io::Cursor;
@@ -24,7 +22,10 @@ use crate::key::{DcKey, Fingerprint};
use crate::keyring::Keyring; use crate::keyring::Keyring;
use crate::tools::EmailAddress; use crate::tools::EmailAddress;
#[allow(missing_docs)]
pub const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt"; pub const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
#[allow(missing_docs)]
pub const HEADER_SETUPCODE: &str = "passphrase-begin"; pub const HEADER_SETUPCODE: &str = "passphrase-begin";
/// A wrapper for rPGP public key types /// A wrapper for rPGP public key types

120
src/qr.rs
View File

@@ -1,7 +1,5 @@
//! # QR code module. //! # QR code module.
#![allow(missing_docs)]
mod dclogin_scheme; mod dclogin_scheme;
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -38,34 +36,73 @@ const HTTP_SCHEME: &str = "http://";
const HTTPS_SCHEME: &str = "https://"; const HTTPS_SCHEME: &str = "https://";
pub(crate) const DCBACKUP_SCHEME: &str = "DCBACKUP:"; pub(crate) const DCBACKUP_SCHEME: &str = "DCBACKUP:";
/// Scanned QR code.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Qr { pub enum Qr {
/// Ask the user whether to verify the contact.
///
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
AskVerifyContact { AskVerifyContact {
/// ID of the contact.
contact_id: ContactId, contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint, fingerprint: Fingerprint,
/// Invite number.
invitenumber: String, invitenumber: String,
/// Authentication code.
authcode: String, authcode: String,
}, },
/// Ask the user whether to join the group.
AskVerifyGroup { AskVerifyGroup {
/// Group name.
grpname: String, grpname: String,
/// Group ID.
grpid: String, grpid: String,
/// ID of the contact.
contact_id: ContactId, contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint, fingerprint: Fingerprint,
/// Invite number.
invitenumber: String, invitenumber: String,
/// Authentication code.
authcode: String, authcode: String,
}, },
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
FprOk { FprOk {
/// Contact ID.
contact_id: ContactId, contact_id: ContactId,
}, },
/// Scanned fingerprint does not match the last seen fingerprint.
FprMismatch { FprMismatch {
/// Contact ID.
contact_id: Option<ContactId>, contact_id: Option<ContactId>,
}, },
/// The scanned QR code contains a fingerprint but no e-mail address.
FprWithoutAddr { FprWithoutAddr {
/// Key fingerprint.
fingerprint: String, fingerprint: String,
}, },
/// Ask the user if they want to create an account on the given domain.
Account { Account {
/// Server domain name.
domain: String, domain: String,
}, },
/// Provides a backup that can be retrieve. /// Provides a backup that can be retrieve.
/// ///
/// This contains all the data needed to connect to a device and download a backup from /// This contains all the data needed to connect to a device and download a backup from
@@ -79,52 +116,124 @@ pub enum Qr {
/// The format is somewhat opaque, but `sendme` can deserialise this. /// The format is somewhat opaque, but `sendme` can deserialise this.
ticket: sendme::provider::Ticket, ticket: sendme::provider::Ticket,
}, },
/// Ask the user if they want to use the given service for video chats.
WebrtcInstance { WebrtcInstance {
/// Server domain name.
domain: String, domain: String,
/// URL pattern for video chat rooms.
instance_pattern: String, instance_pattern: String,
}, },
/// Contact address is scanned.
///
/// Optionally, a draft message could be provided.
/// Ask the user if they want to start chatting.
Addr { Addr {
/// Contact ID.
contact_id: ContactId, contact_id: ContactId,
/// Draft message.
draft: Option<String>, draft: Option<String>,
}, },
/// URL scanned.
///
/// Ask the user if they want to open a browser or copy the URL to clipboard.
Url { Url {
/// URL.
url: String, url: String,
}, },
/// Text scanned.
///
/// Ask the user if they want to copy the text to clipboard.
Text { Text {
/// Scanned text.
text: String, text: String,
}, },
/// Ask the user if they want to withdraw their own QR code.
WithdrawVerifyContact { WithdrawVerifyContact {
/// Contact ID.
contact_id: ContactId, contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint, fingerprint: Fingerprint,
/// Invite number.
invitenumber: String, invitenumber: String,
/// Authentication code.
authcode: String, authcode: String,
}, },
/// Ask the user if they want to withdraw their own group invite QR code.
WithdrawVerifyGroup { WithdrawVerifyGroup {
/// Group name.
grpname: String, grpname: String,
/// Group ID.
grpid: String, grpid: String,
/// Contact ID.
contact_id: ContactId, contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint, fingerprint: Fingerprint,
/// Invite number.
invitenumber: String, invitenumber: String,
/// Authentication code.
authcode: String, authcode: String,
}, },
/// Ask the user if they want to revive their own QR code.
ReviveVerifyContact { ReviveVerifyContact {
/// Contact ID.
contact_id: ContactId, contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint, fingerprint: Fingerprint,
/// Invite number.
invitenumber: String, invitenumber: String,
/// Authentication code.
authcode: String, authcode: String,
}, },
/// Ask the user if they want to revive their own group invite QR code.
ReviveVerifyGroup { ReviveVerifyGroup {
/// Group name.
grpname: String, grpname: String,
/// Group ID.
grpid: String, grpid: String,
/// Contact ID.
contact_id: ContactId, contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint, fingerprint: Fingerprint,
/// Invite number.
invitenumber: String, invitenumber: String,
/// Authentication code.
authcode: String, authcode: String,
}, },
/// `dclogin:` scheme parameters. /// `dclogin:` scheme parameters.
///
/// Ask the user if they want to login with the email address.
Login { Login {
/// Email address.
address: String, address: String,
/// Login parameters.
options: LoginOptions, options: LoginOptions,
}, },
} }
@@ -133,7 +242,8 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
string.to_lowercase().starts_with(&pattern.to_lowercase()) string.to_lowercase().starts_with(&pattern.to_lowercase())
} }
/// Check a scanned QR code. /// Checks a scanned QR code.
///
/// The function should be called after a QR code is scanned. /// The function should be called after a QR code is scanned.
/// The function takes the raw text scanned and checks what can be done with it. /// The function takes the raw text scanned and checks what can be done with it.
pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> { pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
@@ -456,6 +566,7 @@ async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
} }
} }
/// Sets configuration values from a QR code.
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
match check_qr(context, qr).await? { match check_qr(context, qr).await? {
Qr::Account { .. } => set_account_from_qr(context, qr).await?, Qr::Account { .. } => set_account_from_qr(context, qr).await?,
@@ -658,6 +769,9 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
} }
impl Qr { impl Qr {
/// Creates a new scanned QR code of a contact address.
///
/// May contain a message draft.
pub async fn from_address( pub async fn from_address(
context: &Context, context: &Context,
name: &str, name: &str,

View File

@@ -9,22 +9,53 @@ use crate::context::Context;
use crate::provider::Socket; use crate::provider::Socket;
use crate::{contact, login_param::CertificateChecks}; use crate::{contact, login_param::CertificateChecks};
/// Options for `dclogin:` scheme.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoginOptions { pub enum LoginOptions {
/// Unsupported version.
UnsuportedVersion(u32), UnsuportedVersion(u32),
/// Version 1.
V1 { V1 {
/// IMAP server password.
///
/// Used for SMTP if separate SMTP password is not provided.
mail_pw: String, mail_pw: String,
/// IMAP host.
imap_host: Option<String>, imap_host: Option<String>,
/// IMAP port.
imap_port: Option<u16>, imap_port: Option<u16>,
/// IMAP username.
imap_username: Option<String>, imap_username: Option<String>,
/// IMAP password.
imap_password: Option<String>, imap_password: Option<String>,
/// IMAP socket security.
imap_security: Option<Socket>, imap_security: Option<Socket>,
/// IMAP certificate checks.
imap_certificate_checks: Option<CertificateChecks>, imap_certificate_checks: Option<CertificateChecks>,
/// SMTP host.
smtp_host: Option<String>, smtp_host: Option<String>,
/// SMTP port.
smtp_port: Option<u16>, smtp_port: Option<u16>,
/// SMTP username.
smtp_username: Option<String>, smtp_username: Option<String>,
/// SMTP password.
smtp_password: Option<String>, smtp_password: Option<String>,
/// SMTP socket security.
smtp_security: Option<Socket>, smtp_security: Option<Socket>,
/// SMTP certificate checks.
smtp_certificate_checks: Option<CertificateChecks>, smtp_certificate_checks: Option<CertificateChecks>,
}, },
} }

View File

@@ -1,4 +1,4 @@
#![allow(missing_docs)] //! # QR code generation module.
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use base64::Engine as _; use base64::Engine as _;
@@ -16,6 +16,10 @@ use crate::{
stock_str::{self, backup_transfer_qr}, stock_str::{self, backup_transfer_qr},
}; };
/// Returns SVG of the QR code to join the group or verify contact.
///
/// If `chat_id` is `None`, returns verification QR code.
/// Otherwise, returns secure join QR code.
pub async fn get_securejoin_qr_svg(context: &Context, chat_id: Option<ChatId>) -> Result<String> { pub async fn get_securejoin_qr_svg(context: &Context, chat_id: Option<ChatId>) -> Result<String> {
if let Some(chat_id) = chat_id { if let Some(chat_id) = chat_id {
generate_join_group_qr_code(context, chat_id).await generate_join_group_qr_code(context, chat_id).await
@@ -56,6 +60,7 @@ async fn generate_verification_qr(context: &Context) -> Result<String> {
) )
} }
/// Renders a [`Qr::Backup`] QR code as an SVG image.
pub async fn generate_backup_qr(context: &Context, qr: &Qr) -> Result<String> { pub async fn generate_backup_qr(context: &Context, qr: &Qr) -> Result<String> {
let ticket = match qr { let ticket = match qr {
Qr::Backup { ticket } => ticket, Qr::Backup { ticket } => ticket,

View File

@@ -1,7 +1,5 @@
//! Internet Message Format reception pipeline. //! Internet Message Format reception pipeline.
#![allow(missing_docs)]
use std::cmp::min; use std::cmp::min;
use std::collections::HashSet; use std::collections::HashSet;
use std::convert::TryFrom; use std::convert::TryFrom;
@@ -48,8 +46,13 @@ use crate::{contact, imap};
/// all have the same chat_id, state and sort_timestamp. /// all have the same chat_id, state and sort_timestamp.
#[derive(Debug)] #[derive(Debug)]
pub struct ReceivedMsg { pub struct ReceivedMsg {
/// Chat the message is assigned to.
pub chat_id: ChatId, pub chat_id: ChatId,
/// Received message state.
pub state: MessageState, pub state: MessageState,
/// Message timestamp for sorting.
pub sort_timestamp: i64, pub sort_timestamp: i64,
/// IDs of inserted rows in messages table. /// IDs of inserted rows in messages table.

View File

@@ -1,7 +1,5 @@
//! # SQLite wrapper. //! # SQLite wrapper.
#![allow(missing_docs)]
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::path::Path; use std::path::Path;
@@ -26,6 +24,7 @@ use crate::peerstate::{deduplicate_peerstates, Peerstate};
use crate::stock_str; use crate::stock_str;
use crate::tools::{delete_file, time}; use crate::tools::{delete_file, time};
#[allow(missing_docs)]
#[macro_export] #[macro_export]
macro_rules! paramsv { macro_rules! paramsv {
() => { () => {
@@ -36,6 +35,7 @@ macro_rules! paramsv {
}; };
} }
#[allow(missing_docs)]
#[macro_export] #[macro_export]
macro_rules! params_iterv { macro_rules! params_iterv {
($($param:expr),+ $(,)?) => { ($($param:expr),+ $(,)?) => {
@@ -55,16 +55,19 @@ pub struct Sql {
/// Database file path /// Database file path
pub(crate) dbfile: PathBuf, pub(crate) dbfile: PathBuf,
/// SQL connection pool.
pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>, pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>,
/// None if the database is not open, true if it is open with passphrase and false if it is /// None if the database is not open, true if it is open with passphrase and false if it is
/// open without a passphrase. /// open without a passphrase.
is_encrypted: RwLock<Option<bool>>, is_encrypted: RwLock<Option<bool>>,
/// Cache of `config` table.
pub(crate) config_cache: RwLock<HashMap<String, Option<String>>>, pub(crate) config_cache: RwLock<HashMap<String, Option<String>>>,
} }
impl Sql { impl Sql {
/// Creates new SQL database.
pub fn new(dbfile: PathBuf) -> Sql { pub fn new(dbfile: PathBuf) -> Sql {
Self { Self {
dbfile, dbfile,
@@ -213,6 +216,17 @@ impl Sql {
Duration::from_secs(10).as_millis() Duration::from_secs(10).as_millis()
))?; ))?;
c.pragma_update(None, "key", passphrase.clone())?; c.pragma_update(None, "key", passphrase.clone())?;
// Try to enable auto_vacuum. This will only be
// applied if the database is new or after successful
// VACUUM, which usually happens before backup export.
// When auto_vacuum is INCREMENTAL, it is possible to
// use PRAGMA incremental_vacuum to return unused
// database pages to the filesystem.
c.pragma_update(None, "auto_vacuum", "INCREMENTAL".to_string())?;
c.pragma_update(None, "journal_mode", "WAL".to_string())?;
// Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode.
c.pragma_update(None, "synchronous", "NORMAL".to_string())?;
Ok(()) Ok(())
}); });
@@ -228,31 +242,12 @@ impl Sql {
async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> { async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> {
*self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?); *self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?);
{
let conn = self.get_conn().await?;
tokio::task::block_in_place(move || -> Result<()> {
// Try to enable auto_vacuum. This will only be
// applied if the database is new or after successful
// VACUUM, which usually happens before backup export.
// When auto_vacuum is INCREMENTAL, it is possible to
// use PRAGMA incremental_vacuum to return unused
// database pages to the filesystem.
conn.pragma_update(None, "auto_vacuum", "INCREMENTAL".to_string())?;
// journal_mode is persisted, it is sufficient to change it only for one handle.
conn.pragma_update(None, "journal_mode", "WAL".to_string())?;
// Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode.
conn.pragma_update(None, "synchronous", "NORMAL".to_string())?;
Ok(())
})?;
}
self.run_migrations(context).await?; self.run_migrations(context).await?;
Ok(()) Ok(())
} }
/// Updates SQL schema to the latest version.
pub async fn run_migrations(&self, context: &Context) -> Result<()> { pub async fn run_migrations(&self, context: &Context) -> Result<()> {
// (1) update low-level database structure. // (1) update low-level database structure.
// this should be done before updates that use high-level objects that // this should be done before updates that use high-level objects that
@@ -397,6 +392,7 @@ impl Sql {
}) })
} }
/// Allocates a connection from the connection pool and returns it.
pub async fn get_conn( pub async fn get_conn(
&self, &self,
) -> Result<r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>> { ) -> Result<r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>> {
@@ -594,22 +590,26 @@ impl Sql {
Ok(value) Ok(value)
} }
/// Sets configuration for the given key to 32-bit signed integer value.
pub async fn set_raw_config_int(&self, key: &str, value: i32) -> Result<()> { pub async fn set_raw_config_int(&self, key: &str, value: i32) -> Result<()> {
self.set_raw_config(key, Some(&format!("{value}"))).await self.set_raw_config(key, Some(&format!("{value}"))).await
} }
/// Returns 32-bit signed integer configuration value for the given key.
pub async fn get_raw_config_int(&self, key: &str) -> Result<Option<i32>> { pub async fn get_raw_config_int(&self, key: &str) -> Result<Option<i32>> {
self.get_raw_config(key) self.get_raw_config(key)
.await .await
.map(|s| s.and_then(|s| s.parse().ok())) .map(|s| s.and_then(|s| s.parse().ok()))
} }
/// Returns 32-bit unsigned integer configuration value for the given key.
pub async fn get_raw_config_u32(&self, key: &str) -> Result<Option<u32>> { pub async fn get_raw_config_u32(&self, key: &str) -> Result<Option<u32>> {
self.get_raw_config(key) self.get_raw_config(key)
.await .await
.map(|s| s.and_then(|s| s.parse().ok())) .map(|s| s.and_then(|s| s.parse().ok()))
} }
/// Returns boolean configuration value for the given key.
pub async fn get_raw_config_bool(&self, key: &str) -> Result<bool> { pub async fn get_raw_config_bool(&self, key: &str) -> Result<bool> {
// Not the most obvious way to encode bool as string, but it is matter // Not the most obvious way to encode bool as string, but it is matter
// of backward compatibility. // of backward compatibility.
@@ -617,27 +617,32 @@ impl Sql {
Ok(res.unwrap_or_default() > 0) Ok(res.unwrap_or_default() > 0)
} }
/// Sets configuration for the given key to boolean value.
pub async fn set_raw_config_bool(&self, key: &str, value: bool) -> Result<()> { pub async fn set_raw_config_bool(&self, key: &str, value: bool) -> Result<()> {
let value = if value { Some("1") } else { None }; let value = if value { Some("1") } else { None };
self.set_raw_config(key, value).await self.set_raw_config(key, value).await
} }
/// Sets configuration for the given key to 64-bit signed integer value.
pub async fn set_raw_config_int64(&self, key: &str, value: i64) -> Result<()> { pub async fn set_raw_config_int64(&self, key: &str, value: i64) -> Result<()> {
self.set_raw_config(key, Some(&format!("{value}"))).await self.set_raw_config(key, Some(&format!("{value}"))).await
} }
/// Returns 64-bit signed integer configuration value for the given key.
pub async fn get_raw_config_int64(&self, key: &str) -> Result<Option<i64>> { pub async fn get_raw_config_int64(&self, key: &str) -> Result<Option<i64>> {
self.get_raw_config(key) self.get_raw_config(key)
.await .await
.map(|s| s.and_then(|r| r.parse().ok())) .map(|s| s.and_then(|r| r.parse().ok()))
} }
/// Returns configuration cache.
#[cfg(feature = "internals")] #[cfg(feature = "internals")]
pub fn config_cache(&self) -> &RwLock<HashMap<String, Option<String>>> { pub fn config_cache(&self) -> &RwLock<HashMap<String, Option<String>>> {
&self.config_cache &self.config_cache
} }
} }
/// Cleanup the account to restore some storage and optimize the database.
pub async fn housekeeping(context: &Context) -> Result<()> { pub async fn housekeeping(context: &Context) -> Result<()> {
if let Err(err) = remove_unused_files(context).await { if let Err(err) = remove_unused_files(context).await {
warn!( warn!(
@@ -696,6 +701,7 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
Ok(()) Ok(())
} }
/// Enumerates used files in the blobdir and removes unused ones.
pub async fn remove_unused_files(context: &Context) -> Result<()> { pub async fn remove_unused_files(context: &Context) -> Result<()> {
let mut files_in_use = HashSet::new(); let mut files_in_use = HashSet::new();
let mut unreferenced_count = 0; let mut unreferenced_count = 0;

View File

@@ -723,14 +723,14 @@ impl Sql {
async fn execute_migration(&self, query: &'static str, version: i32) -> Result<()> { async fn execute_migration(&self, query: &'static str, version: i32) -> Result<()> {
self.transaction(move |transaction| { self.transaction(move |transaction| {
transaction.execute_batch(query)?;
// set raw config inside the transaction // set raw config inside the transaction
transaction.execute( transaction.execute(
"UPDATE config SET value=? WHERE keyname=?;", "UPDATE config SET value=? WHERE keyname=?;",
paramsv![format!("{version}"), VERSION_CFG], paramsv![format!("{version}"), VERSION_CFG],
)?; )?;
transaction.execute_batch(query)?;
Ok(()) Ok(())
}) })
.await .await

View File

@@ -1,7 +1,5 @@
//! Module to work with translatable stock strings. //! Module to work with translatable stock strings.
#![allow(missing_docs)]
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@@ -21,6 +19,7 @@ use crate::message::{Message, Viewtype};
use crate::param::Param; use crate::param::Param;
use crate::tools::timestamp_to_str; use crate::tools::timestamp_to_str;
/// Storage for string translations.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StockStrings { pub struct StockStrings {
/// Map from stock string ID to the translation. /// Map from stock string ID to the translation.
@@ -35,6 +34,7 @@ pub struct StockStrings {
/// See the `stock_*` methods on [Context] to use these. /// See the `stock_*` methods on [Context] to use these.
/// ///
/// [Context]: crate::context::Context /// [Context]: crate::context::Context
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)] #[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)]
#[repr(u32)] #[repr(u32)]
pub enum StockMessage { pub enum StockMessage {
@@ -425,6 +425,7 @@ impl Default for StockStrings {
} }
impl StockStrings { impl StockStrings {
/// Creates a new translated string storage.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
translated_stockstrings: Arc::new(RwLock::new(Default::default())), translated_stockstrings: Arc::new(RwLock::new(Default::default())),

View File

@@ -124,6 +124,7 @@ pub fn timestamp_to_str(wanted: i64) -> String {
} }
} }
/// Converts duration to string representation suitable for logs.
pub fn duration_to_str(duration: Duration) -> String { pub fn duration_to_str(duration: Duration) -> String {
let secs = duration.as_secs(); let secs = duration.as_secs();
let h = secs / 3600; let h = secs / 3600;
@@ -442,6 +443,7 @@ pub(crate) async fn write_file(
}) })
} }
/// Reads the file and returns its context as a byte vector.
pub async fn read_file(context: &Context, path: impl AsRef<Path>) -> Result<Vec<u8>> { pub async fn read_file(context: &Context, path: impl AsRef<Path>) -> Result<Vec<u8>> {
let path_abs = get_abs_path(context, &path); let path_abs = get_abs_path(context, &path);
@@ -530,7 +532,10 @@ pub(crate) fn time() -> i64 {
/// ``` /// ```
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct EmailAddress { pub struct EmailAddress {
/// Local part of the email address.
pub local: String, pub local: String,
/// Email address domain.
pub domain: String, pub domain: String,
} }

View File

@@ -114,12 +114,12 @@ pub struct WebxdcInfo {
pub struct StatusUpdateSerial(u32); pub struct StatusUpdateSerial(u32);
impl StatusUpdateSerial { impl StatusUpdateSerial {
/// Create a new [MsgId]. /// Create a new [StatusUpdateSerial].
pub fn new(id: u32) -> StatusUpdateSerial { pub fn new(id: u32) -> StatusUpdateSerial {
StatusUpdateSerial(id) StatusUpdateSerial(id)
} }
/// Gets StatusUpdateId as untyped integer. /// Gets StatusUpdateSerial as untyped integer.
/// Avoid using this outside ffi. /// Avoid using this outside ffi.
pub fn to_u32(self) -> u32 { pub fn to_u32(self) -> u32 {
self.0 self.0