Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
3145768fed Use the message text as the initial subject 2023-02-14 11:01:56 +00:00
65 changed files with 451 additions and 745 deletions

View File

@@ -3,20 +3,12 @@
## Unreleased
### Changes
- 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
- replace `r2d2` and `r2d2_sqlite` dependencies with an own connection pool #4050 #4053
- Use message text as the initial subject for 1:1 contacts. #4036
### Fixes
- 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.
- enable `auto_vacuum` on all SQL connections #2955
### API-Changes
- 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

61
Cargo.lock generated
View File

@@ -928,13 +928,14 @@ dependencies = [
"num-traits",
"num_cpus",
"once_cell",
"parking_lot",
"percent-encoding",
"pgp",
"pretty_env_logger",
"proptest",
"qrcodegen",
"quick-xml",
"r2d2",
"r2d2_sqlite",
"rand 0.8.5",
"ratelimit",
"regex",
@@ -1666,6 +1667,15 @@ version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1677,11 +1687,11 @@ dependencies = [
[[package]]
name = "hashlink"
version = "0.8.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
dependencies = [
"hashbrown",
"hashbrown 0.11.2",
]
[[package]]
@@ -1914,7 +1924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
@@ -2073,9 +2083,9 @@ checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565"
[[package]]
name = "libsqlite3-sys"
version = "0.25.2"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"
dependencies = [
"cc",
"openssl-sys",
@@ -2795,6 +2805,27 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb"
[[package]]
name = "r2d2"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
dependencies = [
"log",
"parking_lot",
"scheduled-thread-pool",
]
[[package]]
name = "r2d2_sqlite"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fdc8e4da70586127893be32b7adf21326a4c6b1aba907611edf467d13ffe895"
dependencies = [
"r2d2",
"rusqlite",
]
[[package]]
name = "radix_trie"
version = "0.2.1"
@@ -3038,15 +3069,16 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.28.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"memchr",
"smallvec",
]
@@ -3160,6 +3192,15 @@ dependencies = [
"windows-sys 0.36.1",
]
[[package]]
name = "scheduled-thread-pool"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf"
dependencies = [
"parking_lot",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@@ -3936,7 +3977,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown",
"hashbrown 0.12.3",
"regex",
]

View File

@@ -54,14 +54,15 @@ num_cpus = "1.15"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.17.0"
parking_lot = "0.12"
percent-encoding = "2.2"
pgp = { version = "0.9", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.27"
r2d2 = "0.8"
r2d2_sqlite = "0.20"
rand = "0.8"
regex = "1.7"
rusqlite = { version = "0.28", features = ["sqlcipher", "release_memory"] }
rusqlite = { version = "0.27", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.4"
serde_json = "1.0"

View File

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

View File

@@ -22,15 +22,20 @@ pub enum Lot {
}
#[repr(u8)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Meaning {
#[default]
None = 0,
Text1Draft = 1,
Text1Username = 2,
Text1Self = 3,
}
impl Default for Meaning {
fn default() -> Self {
Meaning::None
}
}
impl Lot {
pub fn get_text1(&self) -> Option<&str> {
match self {
@@ -146,9 +151,9 @@ impl Lot {
}
#[repr(u32)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LotState {
#[default]
// Default
Undefined = 0,
// Qr States
@@ -210,6 +215,12 @@ pub enum LotState {
MsgOutMdnRcvd = 28,
}
impl Default for LotState {
fn default() -> Self {
LotState::Undefined
}
}
impl From<MessageState> for LotState {
fn from(s: MessageState) -> Self {
use MessageState::*;

View File

@@ -45,7 +45,6 @@ use types::message::MessageObject;
use types::provider_info::ProviderInfo;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::MessageLoadResult;
use self::types::{
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
location::JsonrpcLocation,
@@ -86,11 +85,6 @@ impl CommandApi {
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
/// Test function.
async fn sleep(&self, delay: f64) {
tokio::time::sleep(std::time::Duration::from_secs_f64(delay)).await
}
// ---------------------------------------------
// Misc top level functions
// ---------------------------------------------
@@ -466,7 +460,7 @@ impl CommandApi {
Ok(res) => res,
Err(err) => ChatListItemFetchResult::Error {
id: entry.0,
error: format!("{err:#}"),
error: format!("{err:?}"),
},
},
);
@@ -946,27 +940,17 @@ impl CommandApi {
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(
&self,
account_id: u32,
message_ids: Vec<u32>,
) -> Result<HashMap<u32, MessageLoadResult>> {
) -> Result<HashMap<u32, MessageObject>> {
let ctx = self.get_context(account_id).await?;
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
let mut messages: HashMap<u32, MessageObject> = HashMap::new();
for message_id in message_ids {
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
messages.insert(
message_id,
match message_result {
Ok(message) => MessageLoadResult::Message(message),
Err(error) => MessageLoadResult::LoadingError {
error: format!("{error:#}"),
},
},
MessageObject::from_message_id(&ctx, message_id).await?,
);
}
Ok(messages)

View File

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

View File

@@ -68,7 +68,10 @@ async function run() {
null
);
for (const [chatId, _messageId] of chats) {
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
const chat = await client.rpc.getFullChatById(
selectedAccount,
chatId
);
write($main, `<h3>${chat.name}</h3>`);
const messageIds = await client.rpc.getMessageIds(
selectedAccount,
@@ -81,9 +84,7 @@ async function run() {
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
if (message.variant === "message")
write($main, `<p>${message.text}</p>`);
else write($main, `<p>loading error: ${message.error}</p>`);
write($main, `<p>${message.text}</p>`);
}
}
}

View File

@@ -3,27 +3,24 @@ import { DeltaChat } from "../dist/deltachat.js";
run().catch(console.error);
async function run() {
const delta = new DeltaChat("ws://localhost:20808/ws");
const delta = new DeltaChat('ws://localhost:20808/ws');
delta.on("event", (event) => {
console.log("event", event.data);
});
const email = process.argv[2];
const password = process.argv[3];
if (!email || !password)
throw new Error(
"USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>"
);
console.log(`creating acccount for ${email}`);
const id = await delta.rpc.addAccount();
console.log(`created account id ${id}`);
const email = process.argv[2]
const password = process.argv[3]
if (!email || !password) throw new Error('USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>')
console.log(`creating acccount for ${email}`)
const id = await delta.rpc.addAccount()
console.log(`created account id ${id}`)
await delta.rpc.setConfig(id, "addr", email);
await delta.rpc.setConfig(id, "mail_pw", password);
console.log("configuration updated");
await delta.rpc.configure(id);
console.log("account configured!");
console.log('configuration updated')
await delta.rpc.configure(id)
console.log('account configured!')
const accounts = await delta.rpc.getAllAccounts();
console.log("accounts", accounts);
console.log("waiting for events...");
console.log("waiting for events...")
}

View File

@@ -10,5 +10,5 @@ async function run() {
const accounts = await delta.rpc.getAllAccounts();
console.log("accounts", accounts);
console.log("waiting for events...");
console.log("waiting for events...")
}

View File

@@ -38,8 +38,8 @@
"example:start": "http-server .",
"extract-constants": "node ./scripts/generate-constants.js",
"generate-bindings": "cargo test",
"prettier:check": "prettier --check .",
"prettier:fix": "prettier --write .",
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
"test:prepare": "cargo build --package deltachat-rpc-server --bin deltachat-rpc-server",
"test:report-coverage": "node report_api_coverage.mjs",
@@ -49,4 +49,4 @@
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.108.0"
}
}

View File

@@ -43,7 +43,7 @@ export class BaseDeltaChat<
const method = request.method;
if (method === "event") {
const event = request.params! as DCWireEvent<Event>;
//@ts-ignore
//@ts-ignore
this.emit(event.event.type, event.contextId, event.event as any);
this.emit("ALL", event.contextId, event.event as any);

View File

@@ -4,7 +4,10 @@ import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import { StdioDeltaChat as DeltaChat } from "../deltachat.js";
import { RpcServerHandle, startServer } from "./test_base.js";
import {
RpcServerHandle,
startServer,
} from "./test_base.js";
describe("basic tests", () => {
let serverHandle: RpcServerHandle;
@@ -12,9 +15,9 @@ describe("basic tests", () => {
before(async () => {
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout)
// dc.on("ALL", (event) => {
//console.log("event", event);
//console.log("event", event);
// });
});
@@ -108,8 +111,8 @@ describe("basic tests", () => {
assert((await dc.rpc.getConfig(accountId, "addr")) == "valid@email");
});
it("set invalid key should throw", async function () {
await expect(dc.rpc.setConfig(accountId, "invalid_key", "some value")).to
.be.eventually.rejected;
await expect(dc.rpc.setConfig(accountId, "invalid_key", "some value")).to.be
.eventually.rejected;
});
it("get invalid key should throw", async function () {
await expect(dc.rpc.getConfig(accountId, "invalid_key")).to.be.eventually
@@ -122,10 +125,7 @@ describe("basic tests", () => {
it("set and retrive (batch)", async function () {
const config = { addr: "valid@email", mail_pw: "1234" };
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
it("set and retrive ui.* (batch)", async function () {
@@ -134,10 +134,7 @@ describe("basic tests", () => {
"ui.enter_key_sends": "true",
};
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
it("set and retrive mixed(ui and core) (batch)", async function () {
@@ -148,10 +145,7 @@ describe("basic tests", () => {
mail_pw: "123456",
};
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
});

View File

@@ -59,13 +59,13 @@ export async function startServer(): Promise<RpcServerHandle> {
export async function createTempUser(url: string) {
const response = await fetch(url, {
method: "POST",
method: "POST",
headers: {
"cache-control": "no-cache",
},
});
if (!response.ok) throw new Error("Received invalid response");
return response.json();
if (!response.ok) throw new Error('Received invalid response')
return response.json();
}
function getTargetDir(): Promise<string> {
@@ -89,3 +89,4 @@ function getTargetDir(): Promise<string> {
);
});
}

View File

@@ -10,7 +10,7 @@ deltachat = { path = "..", features = ["internals"]}
dirs = "4"
log = "0.4.16"
pretty_env_logger = "0.4"
rusqlite = "0.28"
rusqlite = "0.27"
rustyline = "10"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }

View File

@@ -13,6 +13,13 @@ dynamic = [
"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]
deltachat_rpc_client = [
"py.typed"

View File

@@ -1,5 +1,4 @@
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from dataclasses import dataclass
from ._utils import AttrDict
from .chat import Chat
@@ -12,17 +11,28 @@ if TYPE_CHECKING:
from .deltachat import DeltaChat
@dataclass
class Account:
"""Delta Chat account."""
manager: "DeltaChat"
id: int
def __init__(self, manager: "DeltaChat", account_id: int) -> None:
self.manager = manager
self.id = account_id
@property
def _rpc(self) -> Rpc:
return self.manager.rpc
def __eq__(self, other) -> bool:
if not isinstance(other, Account):
return False
return self.id == other.id and self.manager == other.manager
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Account id={self.id}>"
async def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it."""
return AttrDict(await self._rpc.wait_for_event(self.id))

View File

@@ -1,7 +1,6 @@
import calendar
from datetime import datetime
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from dataclasses import dataclass
from ._utils import AttrDict
from .const import ChatVisibility
@@ -13,17 +12,28 @@ if TYPE_CHECKING:
from .account import Account
@dataclass
class Chat:
"""Chat object which manages members and through which you can send and retrieve messages."""
account: "Account"
id: int
def __init__(self, account: "Account", chat_id: int) -> None:
self.account = account
self.id = chat_id
@property
def _rpc(self) -> Rpc:
return self.account._rpc
def __eq__(self, other) -> bool:
if not isinstance(other, Chat):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Chat id={self.id} account={self.account.id}>"
async def delete(self) -> None:
"""Delete this chat and all its messages.

View File

@@ -1,5 +1,4 @@
from typing import TYPE_CHECKING
from dataclasses import dataclass
from ._utils import AttrDict
from .rpc import Rpc
@@ -9,7 +8,6 @@ if TYPE_CHECKING:
from .chat import Chat
@dataclass
class Contact:
"""
Contact API.
@@ -17,8 +15,20 @@ class Contact:
Essentially a wrapper for RPC, account ID and a contact ID.
"""
account: "Account"
id: int
def __init__(self, account: "Account", contact_id: int) -> None:
self.account = account
self.id = contact_id
def __eq__(self, other) -> bool:
if not isinstance(other, Contact):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Contact id={self.id} account={self.account.id}>"
@property
def _rpc(self) -> Rpc:

View File

@@ -1,6 +1,5 @@
import json
from typing import TYPE_CHECKING, Union
from dataclasses import dataclass
from ._utils import AttrDict
from .contact import Contact
@@ -10,12 +9,23 @@ if TYPE_CHECKING:
from .account import Account
@dataclass
class Message:
"""Delta Chat Message object."""
account: "Account"
id: int
def __init__(self, account: "Account", msg_id: int) -> None:
self.account = account
self.id = msg_id
def __eq__(self, other) -> bool:
if not isinstance(other, Message):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Message id={self.id} account={self.account.id}>"
@property
def _rpc(self) -> Rpc:

View File

@@ -1,7 +1,6 @@
from unittest.mock import MagicMock
import pytest
import asyncio
from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError
@@ -14,17 +13,6 @@ async def test_system_info(rpc) -> None:
assert "deltachat_core_version" in system_info
@pytest.mark.asyncio()
async def test_sleep(rpc) -> None:
"""Test that long-running task does not block short-running task from completion."""
sleep_5_task = asyncio.create_task(rpc.sleep(5.0))
sleep_3_task = asyncio.create_task(rpc.sleep(3.0))
done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED)
assert sleep_3_task in done
assert sleep_5_task in pending
sleep_5_task.cancel()
@pytest.mark.asyncio()
async def test_email_address_validity(rpc) -> None:
valid_addresses = [

View File

@@ -51,10 +51,7 @@ async fn main() -> Result<()> {
let mut lines = BufReader::new(stdin).lines();
while let Some(message) = lines.next_line().await? {
log::trace!("RPC recv {}", message);
let session = session.clone();
tokio::spawn(async move {
session.handle_incoming(&message).await;
});
session.handle_incoming(&message).await;
}
log::info!("EOF reached on stdin");
Ok(())

View File

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

View File

@@ -1,12 +1,13 @@
"""Account class implementation."""
from __future__ import print_function
import os
from array import array
from contextlib import contextmanager
from email.utils import parseaddr
from threading import Event
from typing import Any, Dict, Generator, List, Optional, Union, TYPE_CHECKING
from typing import Any, Dict, Generator, List, Optional, Union
from . import const, hookspec
from .capi import ffi, lib
@@ -22,9 +23,6 @@ from .cutil import (
from .message import Message
from .tracker import ConfigureTracker, ImexTracker
if TYPE_CHECKING:
from .events import FFIEventTracker
class MissingCredentials(ValueError):
"""Account is missing `addr` and `mail_pw` config values."""
@@ -55,7 +53,7 @@ def get_dc_info_as_dict(dc_context):
return info_dict
class Account:
class Account(object):
"""Each account is tied to a sqlite database file which is fully managed
by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects.
@@ -63,9 +61,6 @@ class Account:
MissingCredentials = MissingCredentials
_logid: str
_evtracker: "FFIEventTracker"
def __init__(self, db_path, os_name=None, logging=True, closed=False) -> None:
from .events import EventThread
@@ -307,7 +302,7 @@ class Account:
elif isinstance(obj, str):
displayname, addr = parseaddr(obj)
else:
raise TypeError(f"don't know how to create chat for {obj!r}")
raise TypeError("don't know how to create chat for %r" % (obj,))
if name is None and displayname:
name = displayname

View File

@@ -18,7 +18,7 @@ from .cutil import (
from .message import Message
class Chat:
class Chat(object):
"""Chat object which manages members and through which you can send and retrieve messages.
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
class Contact:
class Contact(object):
"""Delta-Chat Contact.
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
class Message:
class Message(object):
"""Message object.
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."""
class Provider:
class Provider(object):
"""
Provider information.

View File

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

View File

@@ -1,3 +1,5 @@
from __future__ import print_function
import fnmatch
import io
import os
@@ -275,8 +277,6 @@ class ACSetup:
CONFIGURED = "CONFIGURED"
IDLEREADY = "IDLEREADY"
_configured_events: Queue
def __init__(self, testprocess, init_time):
self._configured_events = Queue()
self._account2state = {}
@@ -378,13 +378,8 @@ class ACSetup:
class ACFactory:
"""Account factory"""
init_time: float
_finalizers: List[Callable[[], None]]
_accounts: List[Account]
_acsetup: ACSetup
_preconfigured_keys: List[str]
def __init__(self, request, testprocess, tmpdir, data) -> None:
self.init_time = time.time()
@@ -436,15 +431,14 @@ class ACFactory:
assert "addr" in configdict and "mail_pw" in configdict
return configdict
def _get_cached_account(self, addr) -> Optional[Account]:
def _get_cached_account(self, addr):
if addr in self.testprocess._addr2files:
return self._getaccount(addr)
return None
def get_unconfigured_account(self, closed=False) -> Account:
def get_unconfigured_account(self, closed=False):
return self._getaccount(closed=closed)
def _getaccount(self, try_cache_addr=None, closed=False) -> Account:
def _getaccount(self, try_cache_addr=None, closed=False):
logid = f"ac{len(self._accounts) + 1}"
# we need to use fixed database basename for maybe_cache_* functions to work
path = self.tmpdir.mkdir(logid).join("dc.db")
@@ -458,10 +452,10 @@ class ACFactory:
self._accounts.append(ac)
return ac
def set_logging_default(self, logging) -> None:
def set_logging_default(self, logging):
self._logging = bool(logging)
def remove_preconfigured_keys(self) -> None:
def remove_preconfigured_keys(self):
self._preconfigured_keys = []
def _preconfigure_key(self, account, addr):
@@ -499,7 +493,7 @@ class ACFactory:
self._acsetup.init_logging(ac)
return ac
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs) -> Account:
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs):
if cloned_from is None:
configdict = self.get_next_liveconfig()
else:
@@ -521,7 +515,7 @@ class ACFactory:
self._acsetup.start_configure(ac)
return ac
def prepare_account_from_liveconfig(self, configdict) -> Account:
def prepare_account_from_liveconfig(self, configdict):
ac = self.get_unconfigured_account()
assert "addr" in configdict and "mail_pw" in configdict, configdict
configdict.setdefault("bcc_self", False)
@@ -531,11 +525,11 @@ class ACFactory:
self._preconfigure_key(ac, configdict["addr"])
return ac
def wait_configured(self, account) -> None:
def wait_configured(self, account):
"""Wait until the specified account has successfully completed configure."""
self._acsetup.wait_one_configured(account)
def bring_accounts_online(self) -> None:
def bring_accounts_online(self):
print("bringing accounts online")
self._acsetup.bring_online()
print("all accounts online")

View File

@@ -14,7 +14,7 @@ def test_db_busy_error(acfactory, tmpdir):
def log(string):
with log_lock:
print(f"{time.time() - starttime:3.2f} {string}")
print("%3.2f %s" % (time.time() - starttime, string))
# make a number of accounts
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()
assert_folders_configured(ac1_clone)
lp.sec("check that ac2 contact was fetched during configure")
lp.sec("check that ac2 contact was fetchted during configure")
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
ac2_addr = ac2.get_config("addr")
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())

View File

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

View File

@@ -1,3 +1,5 @@
from __future__ import print_function
import os
import time
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.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true
echo -----------------------

View File

@@ -38,9 +38,6 @@ by the [Protected Headers](https://tools.ietf.org/id/draft-autocrypt-lamps-prote
# Outgoing messages
Messengers MUST add a `Chat-Version: 1.0` header to outgoing messages.
For filtering and smart appearance of the messages in normal MUAs,
the `Subject` header SHOULD be `Message from <sender name>`.
Replies to messages MAY follow the typical `Re:`-format.
The body MAY contain text which MUST have the content type `text/plain`
or `mulipart/alternative` containing `text/plain`.

View File

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

View File

@@ -11,15 +11,20 @@ use anyhow::{bail, Context as _, Error, Result};
use crate::key::{DcKey, SignedPublicKey};
/// Possible values for encryption preference
#[derive(PartialEq, Eq, Debug, Default, Clone, Copy, FromPrimitive, ToPrimitive)]
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum EncryptPreference {
#[default]
NoPreference = 0,
Mutual = 1,
Reset = 20,
}
impl Default for EncryptPreference {
fn default() -> Self {
EncryptPreference::NoPreference
}
}
impl fmt::Display for EncryptPreference {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match *self {

View File

@@ -61,7 +61,6 @@ pub enum ChatItem {
/// Chat protection status.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -78,7 +77,6 @@ pub enum ChatItem {
#[repr(u32)]
pub enum ProtectionStatus {
/// Chat is not protected.
#[default]
Unprotected = 0,
/// Chat is protected.
@@ -87,6 +85,12 @@ pub enum ProtectionStatus {
Protected = 1,
}
impl Default for ProtectionStatus {
fn default() -> Self {
ProtectionStatus::Unprotected
}
}
/// The reason why messages cannot be sent to the chat.
///
/// The reason is mainly for logging and displaying in debug REPL, thus not translated.
@@ -2075,7 +2079,8 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
Ok(())
}
/// Prepares a message to be sent out.
/// Prepares a message to be send out
/// - Checks if chat can be sent to
async fn prepare_msg_common(
context: &Context,
chat_id: ChatId,
@@ -2083,8 +2088,6 @@ async fn prepare_msg_common(
change_state_to: MessageState,
) -> Result<MsgId> {
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? {
bail!("cannot send to {}: {}", chat_id, reason);
}
@@ -2142,7 +2145,7 @@ pub async fn is_contact_in_chat(
Ok(exists)
}
/// Sends a message object to a chat.
/// Send a message defined by a dc_msg_t object to a chat.
///
/// Sends the event #DC_EVENT_MSGS_CHANGED on succcess.
/// However, this does not imply, the message really reached the recipient -
@@ -3295,7 +3298,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
Ok(())
}
/// Sets a new profile image for the chat.
/// Set a new profile image for the chat.
///
/// 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

View File

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

View File

@@ -12,7 +12,6 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -27,62 +26,81 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
)]
#[repr(i8)]
pub enum Blocked {
#[default]
Not = 0,
Yes = 1,
Request = 2,
}
impl Default for Blocked {
fn default() -> Self {
Blocked::Not
}
}
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)]
pub enum ShowEmails {
Off = 0,
AcceptedContacts = 1,
#[default] // also change Config.ShowEmails props(default) on changes
All = 2,
}
impl Default for ShowEmails {
fn default() -> Self {
ShowEmails::All // also change Config.ShowEmails props(default) on changes
}
}
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)]
pub enum MediaQuality {
#[default] // also change Config.MediaQuality props(default) on changes
Balanced = 0,
Worse = 1,
}
impl Default for MediaQuality {
fn default() -> Self {
MediaQuality::Balanced // also change Config.MediaQuality props(default) on changes
}
}
/// Type of the key to generate.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)]
pub enum KeyGenType {
#[default]
Default = 0,
Rsa2048 = 1,
Ed25519 = 2,
}
/// Video chat URL type.
impl Default for KeyGenType {
fn default() -> Self {
KeyGenType::Default
}
}
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(i8)]
pub enum VideochatType {
/// Unknown type.
#[default]
Unknown = 0,
/// [basicWebRTC](https://github.com/cracker0dks/basicwebrtc) instance.
BasicWebrtc = 1,
/// [Jitsi Meet](https://jitsi.org/jitsi-meet/) instance.
Jitsi = 2,
}
impl Default for VideochatType {
fn default() -> Self {
VideochatType::Unknown
}
}
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
@@ -115,10 +133,8 @@ pub const DC_CHAT_ID_ALLDONE_HINT: ChatId = ChatId::new(7);
/// larger chat IDs are "real" chats, their messages are "real" messages.
pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
/// Chat type.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -134,23 +150,19 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
)]
#[repr(u32)]
pub enum Chattype {
/// Undefined chat type.
#[default]
Undefined = 0,
/// 1:1 chat.
Single = 100,
/// Group chat.
Group = 120,
/// Mailing list.
Mailinglist = 140,
/// Broadcast list.
Broadcast = 160,
}
impl Default for Chattype {
fn default() -> Self {
Chattype::Undefined
}
}
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;

View File

@@ -223,24 +223,12 @@ pub struct Contact {
/// Possible origins of a contact.
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u32)]
pub enum Origin {
/// Unknown origin. Can be used as a minimum origin to specify that the caller does not care
/// about origin of the contact.
#[default]
Unknown = 0,
/// The contact is a mailing list address, needed to unblock mailing lists
@@ -299,6 +287,12 @@ pub enum Origin {
ManuallyCreated = 0x0400_0000,
}
impl Default for Origin {
fn default() -> Self {
Origin::Unknown
}
}
impl Origin {
/// Contacts that are known, i. e. they came in via accepted contacts or
/// themselves an accepted contact. Known contacts are shown in the
@@ -1521,6 +1515,7 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
book.lines()
.collect::<Vec<&str>>()
.chunks(2)
.into_iter()
.filter_map(|chunk| {
let name = chunk.first()?;
let addr = chunk.get(1)?;

View File

@@ -35,7 +35,6 @@ pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
/// Download state of the message.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -51,7 +50,6 @@ pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
#[repr(u32)]
pub enum DownloadState {
/// Message is fully downloaded.
#[default]
Done = 0,
/// Message is partially downloaded and can be fully downloaded at request.
@@ -64,6 +62,12 @@ pub enum DownloadState {
InProgress = 1000,
}
impl Default for DownloadState {
fn default() -> Self {
DownloadState::Done
}
}
impl Context {
// Returns validated download limit or `None` for "no limit".
pub(crate) async fn download_limit(&self) -> Result<Option<u32>> {

View File

@@ -1,5 +1,7 @@
//! # Events specification.
#![allow(missing_docs)]
use std::path::PathBuf;
use async_channel::{self as channel, Receiver, Sender, TrySendError};
@@ -109,7 +111,6 @@ pub struct Event {
pub typ: EventType,
}
/// Event payload.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum EventType {
/// The library-user may write an informational string to the log.
@@ -170,23 +171,17 @@ pub enum EventType {
/// - Chats created, deleted or archived
/// - 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 {
/// Set if only a single chat is affected by the changes, otherwise 0.
chat_id: ChatId,
/// Set if only a single message is affected by the changes, otherwise 0.
msg_id: MsgId,
},
/// Reactions for the message changed.
ReactionsChanged {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message for which reactions were changed.
msg_id: MsgId,
/// ID of the contact whose reaction set is changed.
contact_id: ContactId,
},
@@ -195,16 +190,11 @@ pub enum EventType {
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
IncomingMsg {
/// ID of the chat where the message is assigned.
chat_id: ChatId,
/// ID of the message.
msg_id: MsgId,
},
/// Downloading a bunch of messages just finished.
IncomingMsgBunch {
/// List of incoming message IDs.
msg_ids: Vec<MsgId>,
},
@@ -215,30 +205,21 @@ pub enum EventType {
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
MsgDelivered {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that was successfully sent.
msg_id: MsgId,
},
/// 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().
MsgFailed {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that could not be sent.
msg_id: MsgId,
},
/// 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().
MsgRead {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that was read.
msg_id: MsgId,
},
@@ -253,10 +234,7 @@ pub enum EventType {
/// Chat ephemeral timer changed.
ChatEphemeralTimerModified {
/// Chat ID.
chat_id: ChatId,
/// New ephemeral timer value.
timer: EphemeralTimer,
},
@@ -303,15 +281,15 @@ pub enum EventType {
///
/// These events are typically sent after a joiner has scanned the QR code
/// 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 {
/// ID of the contact that wants to join.
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,
},
@@ -319,13 +297,12 @@ pub enum EventType {
/// (Bob, the person who scans the QR code).
/// The events are typically sent while dc_join_securejoin(), which
/// 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 {
/// ID of the inviting contact.
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,
},
@@ -335,21 +312,15 @@ pub enum EventType {
/// dc_get_connectivity_html() for details.
ConnectivityChanged,
/// The user's avatar changed.
SelfavatarChanged,
/// Webxdc status update received.
WebxdcStatusUpdate {
/// Message ID.
msg_id: MsgId,
/// Status update ID.
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 {
/// ID of the deleted message.
msg_id: MsgId,
},
}

View File

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

View File

@@ -1,4 +1,4 @@
//! # Delta Chat Core Library
//! # Delta Chat Core Library.
#![recursion_limit = "256"]
#![forbid(unsafe_code)]
@@ -111,7 +111,7 @@ pub mod tools;
pub mod accounts;
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";
#[cfg(test)]

View File

@@ -981,7 +981,6 @@ impl Message {
/// For outgoing message, the message could be pending, already delivered or confirmed.
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
@@ -998,7 +997,6 @@ impl Message {
#[repr(u32)]
pub enum MessageState {
/// Undefined message state.
#[default]
Undefined = 0,
/// Incoming *fresh* message. Fresh messages are neither noticed
@@ -1041,6 +1039,12 @@ pub enum MessageState {
OutMdnRcvd = 28,
}
impl Default for MessageState {
fn default() -> Self {
MessageState::Undefined
}
}
impl std::fmt::Display for MessageState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
@@ -1910,7 +1914,6 @@ pub(crate) async fn rfc724_mid_exists(
/// How a message is primarily displayed.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -1926,7 +1929,6 @@ pub(crate) async fn rfc724_mid_exists(
#[repr(u32)]
pub enum Viewtype {
/// Unknown message type.
#[default]
Unknown = 0,
/// Text message.
@@ -1980,6 +1982,12 @@ pub enum Viewtype {
Webxdc = 80,
}
impl Default for Viewtype {
fn default() -> Self {
Viewtype::Unknown
}
}
impl Viewtype {
/// Whether a message with this [`Viewtype`] should have a file attachment.
pub fn has_file(&self) -> bool {

View File

@@ -28,7 +28,7 @@ use crate::stock_str;
use crate::tools::IsNoneOrEmpty;
use crate::tools::{
create_outgoing_rfc724_mid, create_smeared_timestamp, get_filebytes, remove_subject_prefix,
time,
time, truncate,
};
// attachments of 25 mb brutto should work on the majority of providers
@@ -98,8 +98,6 @@ pub struct RenderedEmail {
/// Message ID (Message in the sense of Email)
pub rfc724_mid: String,
/// Message subject.
pub subject: String,
}
@@ -437,11 +435,7 @@ impl<'a> MimeFactory<'a> {
}
}
let self_name = &match context.get_config(Config::Displayname).await? {
Some(name) => name,
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
};
stock_str::subject_for_new_contact(context, self_name).await
truncate(self.msg.text.as_deref().unwrap_or_default().trim(), 40).to_string()
}
Loaded::Mdn { .. } => stock_str::read_rcpt(context).await,
};
@@ -1671,13 +1665,13 @@ mod tests {
// 3. Send the first message to a new contact
let t = TestContext::new_alice().await;
assert_eq!(first_subject_str(t).await, "Message from alice@example.org");
assert_eq!(first_subject_str(t).await, "Hi");
let t = TestContext::new_alice().await;
t.set_config(Config::Displayname, Some("Alice"))
.await
.unwrap();
assert_eq!(first_subject_str(t).await, "Message from Alice");
assert_eq!(first_subject_str(t).await, "Hi");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -1,5 +1,7 @@
//! # MIME message parsing module.
#![allow(missing_docs)]
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
@@ -128,14 +130,11 @@ pub(crate) enum MailinglistType {
None,
}
/// System message type.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
#[repr(u32)]
pub enum SystemMessage {
/// Unknown type of system message.
#[default]
Unknown = 0,
/// Group name changed.
@@ -152,14 +151,8 @@ pub enum SystemMessage {
/// Autocrypt Setup Message.
AutocryptSetupMessage = 6,
/// Secure-join message.
SecurejoinMessage = 7,
/// Location streaming is enabled.
LocationStreamingEnabled = 8,
/// Location-only message.
LocationOnly = 9,
/// Chat ephemeral message timer is changed.
@@ -184,6 +177,12 @@ pub enum SystemMessage {
WebxdcInfoMessage = 32,
}
impl Default for SystemMessage {
fn default() -> Self {
SystemMessage::Unknown
}
}
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
impl MimeMessage {
@@ -1798,8 +1797,6 @@ pub struct Part {
/// Size of the MIME part in bytes.
pub bytes: usize,
/// Parameters.
pub param: Params,
/// Attachment filename.
@@ -2365,7 +2362,7 @@ mod tests {
assert_eq!(of, "no");
// unknown headers do not bubble upwards
let of = mimeparser.get_header(HeaderDef::TestHeader).unwrap();
let of = mimeparser.get_header(HeaderDef::_TestHeader).unwrap();
assert_eq!(of, "Bar");
// the following fields would bubble up

View File

@@ -1,5 +1,7 @@
//! OAuth 2 module.
#![allow(missing_docs)]
use std::collections::HashMap;
use anyhow::Result;
@@ -54,8 +56,6 @@ struct Response {
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(
context: &Context,
addr: &str,
@@ -76,7 +76,7 @@ pub async fn get_oauth2_url(
}
}
pub(crate) async fn get_oauth2_access_token(
pub async fn get_oauth2_access_token(
context: &Context,
addr: &str,
code: &str,
@@ -228,11 +228,7 @@ pub(crate) async fn get_oauth2_access_token(
}
}
pub(crate) async fn get_oauth2_addr(
context: &Context,
addr: &str,
code: &str,
) -> Result<Option<String>> {
pub 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 oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await {
Some(o) => o,

View File

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

View File

@@ -37,11 +37,10 @@ pub enum Protocol {
}
/// Socket security.
#[derive(Debug, Default, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Socket {
/// Unspecified socket security, select automatically.
#[default]
Automatic = 0,
/// TLS connection.
@@ -54,6 +53,12 @@ pub enum Socket {
Plain = 3,
}
impl Default for Socket {
fn default() -> Self {
Socket::Automatic
}
}
/// Pattern used to construct login usernames from email addresses.
#[derive(Debug, PartialEq, Eq, Clone)]
#[repr(u8)]

119
src/qr.rs
View File

@@ -1,5 +1,7 @@
//! # QR code module.
#![allow(missing_docs)]
mod dclogin_scheme;
use std::collections::BTreeMap;
@@ -35,190 +37,80 @@ const SMTP_SCHEME: &str = "SMTP:";
const HTTP_SCHEME: &str = "http://";
const HTTPS_SCHEME: &str = "https://";
/// Scanned QR code.
#[derive(Debug, Clone, PartialEq, Eq)]
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 {
/// ID of the contact.
contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user whether to join the group.
AskVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// ID of the contact.
contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
FprOk {
/// Contact ID.
contact_id: ContactId,
},
/// Scanned fingerprint does not match the last seen fingerprint.
FprMismatch {
/// Contact ID.
contact_id: Option<ContactId>,
},
/// The scanned QR code contains a fingerprint but no e-mail address.
FprWithoutAddr {
/// Key fingerprint.
fingerprint: String,
},
/// Ask the user if they want to create an account on the given domain.
Account {
/// Server domain name.
domain: String,
},
/// Ask the user if they want to use the given service for video chats.
WebrtcInstance {
/// Server domain name.
domain: String,
/// URL pattern for video chat rooms.
instance_pattern: String,
},
/// Contact address is scanned.
///
/// Optionally, a draft message could be provided.
/// Ask the user if they want to start chatting.
Addr {
/// Contact ID.
contact_id: ContactId,
/// Draft message.
draft: Option<String>,
},
/// URL scanned.
///
/// Ask the user if they want to open a browser or copy the URL to clipboard.
Url {
/// URL.
url: String,
},
/// Text scanned.
///
/// Ask the user if they want to copy the text to clipboard.
Text {
/// Scanned text.
text: String,
},
/// Ask the user if they want to withdraw their own QR code.
WithdrawVerifyContact {
/// Contact ID.
contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to withdraw their own group invite QR code.
WithdrawVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// Contact ID.
contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to revive their own QR code.
ReviveVerifyContact {
/// Contact ID.
contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to revive their own group invite QR code.
ReviveVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// Contact ID.
contact_id: ContactId,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// `dclogin:` scheme parameters.
///
/// Ask the user if they want to login with the email address.
Login {
/// Email address.
address: String,
/// Login parameters.
options: LoginOptions,
},
}
@@ -227,8 +119,7 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
string.to_lowercase().starts_with(&pattern.to_lowercase())
}
/// Checks a scanned QR code.
///
/// Check a scanned QR code.
/// 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.
pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
@@ -524,7 +415,6 @@ 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<()> {
match check_qr(context, qr).await? {
Qr::Account { .. } => set_account_from_qr(context, qr).await?,
@@ -727,9 +617,6 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
}
impl Qr {
/// Creates a new scanned QR code of a contact address.
///
/// May contain a message draft.
pub async fn from_address(
context: &Context,
name: &str,

View File

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

View File

@@ -1,4 +1,4 @@
//! # QR code generation module.
#![allow(missing_docs)]
use anyhow::Result;
use base64::Engine as _;
@@ -14,10 +14,6 @@ use crate::{
securejoin, stock_str,
};
/// 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> {
if let Some(chat_id) = chat_id {
generate_join_group_qr_code(context, chat_id).await

View File

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

View File

@@ -27,10 +27,9 @@ pub enum Connectivity {
// the top) take priority. This means that e.g. if any folder has an error - usually
// because there is no internet connection - the connectivity for the whole
// account will be `Notconnected`.
#[derive(Debug, Default, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
enum DetailedConnectivity {
Error(String),
#[default]
Uninitialized,
Connecting,
Working,
@@ -41,6 +40,12 @@ enum DetailedConnectivity {
NotConfigured,
}
impl Default for DetailedConnectivity {
fn default() -> Self {
DetailedConnectivity::Uninitialized
}
}
impl DetailedConnectivity {
fn to_basic(&self) -> Option<Connectivity> {
match self {

View File

@@ -1,12 +1,15 @@
//! # SQLite wrapper.
#![allow(missing_docs)]
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use anyhow::{bail, Context as _, Result};
use rusqlite::{self, config::DbConfig, Connection, OpenFlags};
use rusqlite::{config::DbConfig, Connection, OpenFlags};
use tokio::sync::RwLock;
use crate::blob::BlobObject;
@@ -23,7 +26,6 @@ use crate::peerstate::{deduplicate_peerstates, Peerstate};
use crate::stock_str;
use crate::tools::{delete_file, time};
#[allow(missing_docs)]
#[macro_export]
macro_rules! paramsv {
() => {
@@ -34,7 +36,6 @@ macro_rules! paramsv {
};
}
#[allow(missing_docs)]
#[macro_export]
macro_rules! params_iterv {
($($param:expr),+ $(,)?) => {
@@ -47,9 +48,6 @@ pub(crate) fn params_iter(iter: &[impl crate::ToSql]) -> impl Iterator<Item = &d
}
mod migrations;
mod pool;
use pool::{Pool, PooledConnection};
/// A wrapper around the underlying Sqlite3 object.
#[derive(Debug)]
@@ -57,19 +55,16 @@ pub struct Sql {
/// Database file path
pub(crate) dbfile: PathBuf,
/// SQL connection pool.
pool: RwLock<Option<Pool>>,
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
/// open without a passphrase.
is_encrypted: RwLock<Option<bool>>,
/// Cache of `config` table.
pub(crate) config_cache: RwLock<HashMap<String, Option<String>>>,
}
impl Sql {
/// Creates new SQL database.
pub fn new(dbfile: PathBuf) -> Sql {
Self {
dbfile,
@@ -194,27 +189,70 @@ impl Sql {
})
}
/// Creates a new connection pool.
fn new_pool(dbfile: &Path, passphrase: String) -> Result<Pool> {
let mut connections = Vec::new();
for _ in 0..3 {
let connection = new_connection(dbfile, &passphrase)?;
connections.push(connection);
}
fn new_pool(
dbfile: &Path,
passphrase: String,
) -> Result<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>> {
let mut open_flags = OpenFlags::SQLITE_OPEN_NO_MUTEX;
open_flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE);
open_flags.insert(OpenFlags::SQLITE_OPEN_CREATE);
let pool = Pool::new(connections);
// this actually creates min_idle database handles just now.
// therefore, with_init() must not try to modify the database as otherwise
// we easily get busy-errors (eg. table-creation, journal_mode etc. should be done on only one handle)
let mgr = r2d2_sqlite::SqliteConnectionManager::file(dbfile)
.with_flags(open_flags)
.with_init(move |c| {
c.execute_batch(&format!(
"PRAGMA cipher_memory_security = OFF; -- Too slow on Android
PRAGMA secure_delete=on;
PRAGMA busy_timeout = {};
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
PRAGMA foreign_keys=on;
",
Duration::from_secs(10).as_millis()
))?;
c.pragma_update(None, "key", passphrase.clone())?;
Ok(())
});
let pool = r2d2::Pool::builder()
.min_idle(Some(2))
.max_size(10)
.connection_timeout(Duration::from_secs(60))
.build(mgr)
.context("Can't build SQL connection pool")?;
Ok(pool)
}
async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> {
*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?;
Ok(())
}
/// Updates SQL schema to the latest version.
pub async fn run_migrations(&self, context: &Context) -> Result<()> {
// (1) update low-level database structure.
// this should be done before updates that use high-level objects that
@@ -359,11 +397,12 @@ impl Sql {
})
}
/// Allocates a connection from the connection pool and returns it.
pub(crate) async fn get_conn(&self) -> Result<PooledConnection> {
pub async fn get_conn(
&self,
) -> Result<r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>> {
let lock = self.pool.read().await;
let pool = lock.as_ref().context("no SQL connection")?;
let conn = pool.get();
let conn = pool.get()?;
Ok(conn)
}
@@ -555,26 +594,22 @@ impl Sql {
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<()> {
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>> {
self.get_raw_config(key)
.await
.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>> {
self.get_raw_config(key)
.await
.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> {
// Not the most obvious way to encode bool as string, but it is matter
// of backward compatibility.
@@ -582,68 +617,27 @@ impl Sql {
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<()> {
let value = if value { Some("1") } else { None };
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<()> {
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>> {
self.get_raw_config(key)
.await
.map(|s| s.and_then(|r| r.parse().ok()))
}
/// Returns configuration cache.
#[cfg(feature = "internals")]
pub fn config_cache(&self) -> &RwLock<HashMap<String, Option<String>>> {
&self.config_cache
}
}
/// Creates a new SQLite connection.
///
/// `path` is the database path.
///
/// `passphrase` is the SQLCipher database passphrase.
/// Empty string if database is not encrypted.
fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
let mut flags = OpenFlags::SQLITE_OPEN_NO_MUTEX;
flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE);
flags.insert(OpenFlags::SQLITE_OPEN_CREATE);
let conn = Connection::open_with_flags(path, flags)?;
conn.execute_batch(
"PRAGMA cipher_memory_security = OFF; -- Too slow on Android
PRAGMA secure_delete=on;
PRAGMA busy_timeout = 60000; -- 60 seconds
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
PRAGMA foreign_keys=on;
",
)?;
conn.pragma_update(None, "key", passphrase)?;
// 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())?;
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(conn)
}
/// Cleanup the account to restore some storage and optimize the database.
pub async fn housekeeping(context: &Context) -> Result<()> {
if let Err(err) = remove_unused_files(context).await {
warn!(
@@ -702,7 +696,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
Ok(())
}
/// Enumerates used files in the blobdir and removes unused ones.
pub async fn remove_unused_files(context: &Context) -> Result<()> {
let mut files_in_use = HashSet::new();
let mut unreferenced_count = 0;

View File

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

View File

@@ -1,106 +0,0 @@
//! Connection pool.
use std::fmt;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, Weak};
use parking_lot::{Condvar, Mutex};
use rusqlite::Connection;
/// Inner connection pool.
struct InnerPool {
/// Available connections.
connections: Mutex<Vec<Connection>>,
/// Conditional variable to notify about added connections.
///
/// Used to wait for available connection when the pool is empty.
cond: Condvar,
}
impl InnerPool {
/// Puts a connection into the pool.
///
/// The connection could be new or returned back.
fn put(&self, connection: Connection) {
let mut connections = self.connections.lock();
connections.push(connection);
drop(connections);
self.cond.notify_one();
}
}
/// Pooled connection.
pub struct PooledConnection {
/// Weak reference to the pool used to return the connection back.
pool: Weak<InnerPool>,
/// Only `None` right after moving the connection back to the pool.
conn: Option<Connection>,
}
impl Drop for PooledConnection {
fn drop(&mut self) {
// Put the connection back unless the pool is already dropped.
if let Some(pool) = self.pool.upgrade() {
if let Some(conn) = self.conn.take() {
conn.release_memory().ok();
pool.put(conn);
}
}
}
}
impl Deref for PooledConnection {
type Target = Connection;
fn deref(&self) -> &Connection {
self.conn.as_ref().unwrap()
}
}
impl DerefMut for PooledConnection {
fn deref_mut(&mut self) -> &mut Connection {
self.conn.as_mut().unwrap()
}
}
/// Connection pool.
#[derive(Clone)]
pub struct Pool {
/// Reference to the actual connection pool.
inner: Arc<InnerPool>,
}
impl fmt::Debug for Pool {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "Pool")
}
}
impl Pool {
/// Creates a new connection pool.
pub fn new(connections: Vec<Connection>) -> Self {
let inner = Arc::new(InnerPool {
connections: Mutex::new(connections),
cond: Condvar::new(),
});
Pool { inner }
}
/// Retrieves a connection from the pool.
pub fn get(&self) -> PooledConnection {
let mut connections = self.inner.connections.lock();
loop {
if let Some(conn) = connections.pop() {
return PooledConnection {
pool: Arc::downgrade(&self.inner),
conn: Some(conn),
};
}
self.inner.cond.wait(&mut connections);
}
}
}

View File

@@ -1,5 +1,7 @@
//! Module to work with translatable stock strings.
#![allow(missing_docs)]
use std::collections::HashMap;
use std::sync::Arc;
@@ -19,7 +21,6 @@ use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::tools::timestamp_to_str;
/// Storage for string translations.
#[derive(Debug, Clone)]
pub struct StockStrings {
/// Map from stock string ID to the translation.
@@ -34,7 +35,6 @@ pub struct StockStrings {
/// See the `stock_*` methods on [Context] to use these.
///
/// [Context]: crate::context::Context
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)]
#[repr(u32)]
pub enum StockMessage {
@@ -422,7 +422,6 @@ impl Default for StockStrings {
}
impl StockStrings {
/// Creates a new translated string storage.
pub fn new() -> Self {
Self {
translated_stockstrings: Arc::new(RwLock::new(Default::default())),
@@ -869,14 +868,6 @@ pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
translated(context, StockMessage::UnknownSenderForChat).await
}
/// Stock string: `Message from %1$s`.
// TODO: This can compute `self_name` itself instead of asking the caller to do this.
pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
translated(context, StockMessage::SubjectForNewContact)
.await
.replace1(self_name)
}
/// Stock string: `Failed to send message to %1$s.`.
pub(crate) async fn failed_sending_to(context: &Context, name: &str) -> String {
translated(context, StockMessage::FailedSendingTo)

View File

@@ -13,16 +13,21 @@ use crate::tools::{create_id, time};
/// Token namespace
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
#[repr(u32)]
pub enum Namespace {
#[default]
Unknown = 0,
Auth = 110,
InviteNumber = 100,
}
impl Default for Namespace {
fn default() -> Self {
Namespace::Unknown
}
}
/// Saves a token to the database.
pub async fn save(
context: &Context,

View File

@@ -124,7 +124,6 @@ pub fn timestamp_to_str(wanted: i64) -> String {
}
}
/// Converts duration to string representation suitable for logs.
pub fn duration_to_str(duration: Duration) -> String {
let secs = duration.as_secs();
let h = secs / 3600;
@@ -443,7 +442,6 @@ 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>> {
let path_abs = get_abs_path(context, &path);
@@ -532,10 +530,7 @@ pub(crate) fn time() -> i64 {
/// ```
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct EmailAddress {
/// Local part of the email address.
pub local: String,
/// Email address domain.
pub domain: String,
}

View File

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