mirror of
https://github.com/chatmail/core.git
synced 2026-05-03 05:16:28 +03:00
Merge branch 'master' into flub/send-backup
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
0
python/src/deltachat/py.typed
Normal file
0
python/src/deltachat/py.typed
Normal 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`.
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -----------------------
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
120
src/qr.rs
@@ -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,
|
||||||
|
|||||||
@@ -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>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
50
src/sql.rs
50
src/sql.rs
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())),
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user