Compare commits

...

29 Commits

Author SHA1 Message Date
link2xt
90087bde39 sql: release memory when returning connection to the pool 2023-02-17 22:09:24 +00:00
link2xt
bfd3c1763d Replace r2d2 with an own connection pool
New connection pool does not use threads
and does not remove idle connections
or create new connections at runtime.
2023-02-17 21:12:17 +00:00
link2xt
7586bcf45e Update rusqlite to 0.28 2023-02-17 14:48:33 +00:00
link2xt
870527de1e Remove r2d2_sqlite dependency 2023-02-17 11:06:17 +00:00
link2xt
a34aeb240a Increase database timeout to 60 seconds 2023-02-16 18:37:47 +00:00
link2xt
6ee165fddc python: type annotations for testplugin.py 2023-02-16 17:48:29 +00:00
link2xt
c9db41a7f6 python: build Python 3.11 wheels 2023-02-16 14:36:23 +00:00
link2xt
7a6bfae93b Fix typo 2023-02-16 11:42:59 +00:00
link2xt
ae19c9b331 Add more documentation 2023-02-15 21:56:33 +00:00
link2xt
7d2cca8633 sql: enable auto_vacuum on all connections 2023-02-15 18:59:47 +00:00
link2xt
c52b48b0f5 sql: update version first in migration transactions
In execute_migration transaction first update the version, and only
then execute the rest of the migration. This ensures that transaction
is immediately recognized as write transaction and there is no need
to promote it from read transaction to write transaction later, e.g.
in the case of "DROP TABLE IF EXISTS" that is a read only operation if
the table does not exist. Promoting a read transaction to write
transaction may result in an error if database is locked.
2023-02-15 18:58:44 +00:00
link2xt
893794f4e7 accounts: retry filesystem operations in migrate_account() 2023-02-15 18:57:37 +00:00
link2xt
032da4fb1a python: add py.typed file
It marks the package as supporting typing according to PEP 561 <https://peps.python.org/pep-0561/>

Also remove zip-safe option from setuptools configuration for deltachat-rpc-client.
It is deprecated in setuptools and not used for wheel package format according to <https://setuptools.pypa.io/en/latest/deprecated/zip_safe.html>
2023-02-15 18:56:34 +00:00
Simon Laux
0de5125de8 jsonrpc: get_messages now returns a map with MessageLoadResult instead of failing completely if one of the requested messages could not be loaded. (#4038)
* jsonrpc: `get_messages` now returns a map with `MessageLoadResult`
instead of failing completely if one of the requested messages could not be loaded.

* add pr number to changelog

* format errors with causes instead of debug output

also for chatlistitemfetchresult
2023-02-15 18:46:34 +00:00
link2xt
cd3f1fe874 python: autoformat with black 2023-02-15 17:15:43 +00:00
link2xt
4f68e94fb3 python: use f-strings instead of percent-encoding 2023-02-15 16:33:05 +00:00
link2xt
b3ecbbc8b3 python: do not import print function in tests 2023-02-15 16:32:19 +00:00
link2xt
01653a881a python: do not import print function 2023-02-15 16:28:16 +00:00
link2xt
e021a59b87 python: do not inherit from object 2023-02-15 16:27:34 +00:00
link2xt
243f28f8a5 python: use dataclasses to reduce boilerplate 2023-02-15 12:35:52 +00:00
link2xt
78577594d0 deltachat-rpc-server: spawn request handlers 2023-02-15 12:19:03 +00:00
link2xt
d3e2f38da0 deltachat-jsonrpc: fix "prettier" arguments
**.ts finds .ts files only in the current directory and no *.js files.
2023-02-15 11:18:58 +00:00
link2xt
05f0fe0a88 Derive Default where possible 2023-02-14 21:33:02 +00:00
link2xt
e11d7c0444 Remove unnecessary .into_iter() 2023-02-14 21:04:29 +00:00
link2xt
a203cde400 Remove TestHeader from non-test builds 2023-02-14 11:00:04 +00:00
link2xt
00c14dd9f6 Move changelog entry to unreleased section 2023-02-14 10:58:43 +00:00
link2xt
71d9716117 Remove MimeMessage::from_bytes()
It was not used anywhere except the tests.
2023-02-14 10:57:57 +00:00
link2xt
2e4f63a290 Add Unreleased changelog section 2023-02-14 01:54:22 +00:00
link2xt
9dee725895 Fix header levels for 1.108.0 changelog 2023-02-14 01:53:57 +00:00
67 changed files with 833 additions and 540 deletions

View File

@@ -1,20 +1,39 @@
# Changelog
## 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
### 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
## Changes
### Changes
- Use read/write timeouts instead of per-command timeouts for SMTP #3985
- Cache DNS results for SMTP connections #3985
- Prefer TLS over STARTTLS during autoconfiguration #4021
- Use SOCKS5 configuration for HTTP requests #4017
- Show non-deltachat emails by default for new installations #4019
## Fixes
### Fixes
- Fix Securejoin for multiple devices on a joining side #3982
- python: handle NULL value returned from `dc_get_msg()` #4020
Account.`get_message_by_id` may return `None` in this case.
## API-Changes
### API-Changes
- Remove bitflags from `get_chat_msgs()` interface #4022
C interface is not changed.
Rust and JSON-RPC API have `flags` integer argument

61
Cargo.lock generated
View File

@@ -928,14 +928,13 @@ 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",
@@ -1667,15 +1666,6 @@ 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"
@@ -1687,11 +1677,11 @@ dependencies = [
[[package]]
name = "hashlink"
version = "0.7.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
dependencies = [
"hashbrown 0.11.2",
"hashbrown",
]
[[package]]
@@ -1924,7 +1914,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"hashbrown",
]
[[package]]
@@ -2083,9 +2073,9 @@ checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565"
[[package]]
name = "libsqlite3-sys"
version = "0.24.2"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"
checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
dependencies = [
"cc",
"openssl-sys",
@@ -2805,27 +2795,6 @@ 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"
@@ -3069,16 +3038,15 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.27.0"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a"
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"memchr",
"smallvec",
]
@@ -3192,15 +3160,6 @@ 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"
@@ -3977,7 +3936,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown 0.12.3",
"hashbrown",
"regex",
]

View File

@@ -54,15 +54,14 @@ 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.27", features = ["sqlcipher"] }
rusqlite = { version = "0.28", features = ["sqlcipher", "release_memory"] }
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 experimental
* Downloading a bunch of messages just finished. This is an
* 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,20 +22,15 @@ pub enum Lot {
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Default, 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 {
@@ -151,9 +146,9 @@ impl Lot {
}
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum LotState {
// Default
#[default]
Undefined = 0,
// Qr States
@@ -215,12 +210,6 @@ 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,6 +45,7 @@ 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,
@@ -85,6 +86,11 @@ 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
// ---------------------------------------------
@@ -460,7 +466,7 @@ impl CommandApi {
Ok(res) => res,
Err(err) => ChatListItemFetchResult::Error {
id: entry.0,
error: format!("{err:?}"),
error: format!("{err:#}"),
},
},
);
@@ -940,17 +946,27 @@ 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, MessageObject>> {
) -> Result<HashMap<u32, MessageLoadResult>> {
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 {
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
messages.insert(
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)

View File

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

View File

@@ -3,24 +3,27 @@ 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 **.ts",
"prettier:fix": "prettier --write **.ts",
"prettier:check": "prettier --check .",
"prettier:fix": "prettier --write .",
"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,10 +4,7 @@ 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;
@@ -15,9 +12,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);
// });
});
@@ -111,8 +108,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
@@ -125,7 +122,10 @@ 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,7 +134,10 @@ 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 () {
@@ -145,7 +148,10 @@ 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,4 +89,3 @@ 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.27"
rusqlite = "0.28"
rustyline = "10"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }

View File

@@ -13,13 +13,6 @@ 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,4 +1,5 @@
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from dataclasses import dataclass
from ._utils import AttrDict
from .chat import Chat
@@ -11,28 +12,17 @@ if TYPE_CHECKING:
from .deltachat import DeltaChat
@dataclass
class Account:
"""Delta Chat account."""
def __init__(self, manager: "DeltaChat", account_id: int) -> None:
self.manager = manager
self.id = account_id
manager: "DeltaChat"
id: int
@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,6 +1,7 @@
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
@@ -12,28 +13,17 @@ if TYPE_CHECKING:
from .account import Account
@dataclass
class Chat:
"""Chat object which manages members and through which you can send and retrieve messages."""
def __init__(self, account: "Account", chat_id: int) -> None:
self.account = account
self.id = chat_id
account: "Account"
id: int
@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,4 +1,5 @@
from typing import TYPE_CHECKING
from dataclasses import dataclass
from ._utils import AttrDict
from .rpc import Rpc
@@ -8,6 +9,7 @@ if TYPE_CHECKING:
from .chat import Chat
@dataclass
class Contact:
"""
Contact API.
@@ -15,20 +17,8 @@ class Contact:
Essentially a wrapper for RPC, account ID and a contact ID.
"""
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}>"
account: "Account"
id: int
@property
def _rpc(self) -> Rpc:

View File

@@ -1,5 +1,6 @@
import json
from typing import TYPE_CHECKING, Union
from dataclasses import dataclass
from ._utils import AttrDict
from .contact import Contact
@@ -9,23 +10,12 @@ if TYPE_CHECKING:
from .account import Account
@dataclass
class Message:
"""Delta Chat Message object."""
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}>"
account: "Account"
id: int
@property
def _rpc(self) -> Rpc:

View File

@@ -1,6 +1,7 @@
from unittest.mock import MagicMock
import pytest
import asyncio
from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError
@@ -13,6 +14,17 @@ 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,7 +51,10 @@ async fn main() -> Result<()> {
let mut lines = BufReader::new(stdin).lines();
while let Some(message) = lines.next_line().await? {
log::trace!("RPC recv {}", message);
session.handle_incoming(&message).await;
let session = session.clone();
tokio::spawn(async move {
session.handle_incoming(&message).await;
});
}
log::info!("EOF reached on stdin");
Ok(())

View File

@@ -36,6 +36,11 @@ 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>.*)?$'
@@ -45,7 +50,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", "UP032"]
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032"]
line-length = 120
[tool.isort]

View File

@@ -1,13 +1,12 @@
"""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
from typing import Any, Dict, Generator, List, Optional, Union, TYPE_CHECKING
from . import const, hookspec
from .capi import ffi, lib
@@ -23,6 +22,9 @@ 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."""
@@ -53,7 +55,7 @@ def get_dc_info_as_dict(dc_context):
return info_dict
class Account(object):
class Account:
"""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.
@@ -61,6 +63,9 @@ class Account(object):
MissingCredentials = MissingCredentials
_logid: str
_evtracker: "FFIEventTracker"
def __init__(self, db_path, os_name=None, logging=True, closed=False) -> None:
from .events import EventThread
@@ -302,7 +307,7 @@ class Account(object):
elif isinstance(obj, str):
displayname, addr = parseaddr(obj)
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:
name = displayname

View File

@@ -18,7 +18,7 @@ from .cutil import (
from .message import Message
class Chat(object):
class Chat:
"""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(object):
class Contact:
"""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(object):
class Message:
"""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(object):
class Provider:
"""
Provider information.

View File

View File

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

View File

@@ -1,5 +1,3 @@
from __future__ import print_function
import fnmatch
import io
import os
@@ -277,6 +275,8 @@ class ACSetup:
CONFIGURED = "CONFIGURED"
IDLEREADY = "IDLEREADY"
_configured_events: Queue
def __init__(self, testprocess, init_time):
self._configured_events = Queue()
self._account2state = {}
@@ -378,8 +378,13 @@ 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()
@@ -431,14 +436,15 @@ class ACFactory:
assert "addr" in configdict and "mail_pw" in configdict
return configdict
def _get_cached_account(self, addr):
def _get_cached_account(self, addr) -> Optional[Account]:
if addr in self.testprocess._addr2files:
return self._getaccount(addr)
return None
def get_unconfigured_account(self, closed=False):
def get_unconfigured_account(self, closed=False) -> Account:
return self._getaccount(closed=closed)
def _getaccount(self, try_cache_addr=None, closed=False):
def _getaccount(self, try_cache_addr=None, closed=False) -> Account:
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")
@@ -452,10 +458,10 @@ class ACFactory:
self._accounts.append(ac)
return ac
def set_logging_default(self, logging):
def set_logging_default(self, logging) -> None:
self._logging = bool(logging)
def remove_preconfigured_keys(self):
def remove_preconfigured_keys(self) -> None:
self._preconfigured_keys = []
def _preconfigure_key(self, account, addr):
@@ -493,7 +499,7 @@ class ACFactory:
self._acsetup.init_logging(ac)
return ac
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs):
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs) -> Account:
if cloned_from is None:
configdict = self.get_next_liveconfig()
else:
@@ -515,7 +521,7 @@ class ACFactory:
self._acsetup.start_configure(ac)
return ac
def prepare_account_from_liveconfig(self, configdict):
def prepare_account_from_liveconfig(self, configdict) -> Account:
ac = self.get_unconfigured_account()
assert "addr" in configdict and "mail_pw" in configdict, configdict
configdict.setdefault("bcc_self", False)
@@ -525,11 +531,11 @@ class ACFactory:
self._preconfigure_key(ac, configdict["addr"])
return ac
def wait_configured(self, account):
def wait_configured(self, account) -> None:
"""Wait until the specified account has successfully completed configure."""
self._acsetup.wait_one_configured(account)
def bring_accounts_online(self):
def bring_accounts_online(self) -> None:
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("%3.2f %s" % (time.time() - starttime, string))
print(f"{time.time() - starttime:3.2f} {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 fetchted during configure")
lp.sec("check that ac2 contact was fetched 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,5 +1,3 @@
from __future__ import print_function
import os.path
import shutil
from filecmp import cmp

View File

@@ -1,5 +1,3 @@
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,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 -----------------------

View File

@@ -1,6 +1,7 @@
//! # Account manager module.
use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
@@ -150,27 +151,9 @@ impl Accounts {
if let Some(cfg) = self.config.get_account(id) {
let account_path = self.dir.join(cfg.dir);
// 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;
}
}
try_many_times(|| fs::remove_dir_all(&account_path))
.await
.context("failed to remove account data")?;
}
self.config.remove_account(id).await?;
@@ -178,6 +161,8 @@ 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);
@@ -202,10 +187,10 @@ impl Accounts {
fs::create_dir_all(self.dir.join(&account_config.dir))
.await
.context("failed to create dir")?;
fs::rename(&dbfile, &new_dbfile)
try_many_times(|| fs::rename(&dbfile, &new_dbfile))
.await
.context("failed to rename dbfile")?;
fs::rename(&blobdir, &new_blobdir)
try_many_times(|| fs::rename(&blobdir, &new_blobdir))
.await
.context("failed to rename blobdir")?;
if walfile.exists() {
@@ -229,11 +214,10 @@ impl Accounts {
Ok(account_config.id)
}
Err(err) => {
// remove temp account
fs::remove_dir_all(std::path::PathBuf::from(&account_config.dir))
let account_path = std::path::PathBuf::from(&account_config.dir);
try_many_times(|| fs::remove_dir_all(&account_path))
.await
.context("failed to remove account data")?;
self.config.remove_account(account_config.id).await?;
// 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.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct AccountConfig {

View File

@@ -11,20 +11,15 @@ use anyhow::{bail, Context as _, Error, Result};
use crate::key::{DcKey, SignedPublicKey};
/// Possible values for encryption preference
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
#[derive(PartialEq, Eq, Debug, Default, 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,6 +61,7 @@ pub enum ChatItem {
/// Chat protection status.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -77,6 +78,7 @@ pub enum ChatItem {
#[repr(u32)]
pub enum ProtectionStatus {
/// Chat is not protected.
#[default]
Unprotected = 0,
/// Chat is protected.
@@ -85,12 +87,6 @@ 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.
@@ -2079,8 +2075,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
Ok(())
}
/// Prepares a message to be send out
/// - Checks if chat can be sent to
/// Prepares a message to be sent out.
async fn prepare_msg_common(
context: &Context,
chat_id: ChatId,
@@ -2088,6 +2083,8 @@ 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);
}
@@ -2145,7 +2142,7 @@ pub async fn is_contact_in_chat(
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.
/// However, this does not imply, the message really reached the recipient -
@@ -3298,7 +3295,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
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
/// chat. To remove the profile image pass an empty string for the

View File

@@ -1,7 +1,5 @@
//! # 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};
@@ -195,6 +193,8 @@ 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,14 +208,26 @@ 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.
@@ -228,12 +240,15 @@ 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,
@@ -419,6 +434,7 @@ 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,6 +12,7 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -26,79 +27,60 @@ 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, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Default, 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, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Default, 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, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)]
pub enum KeyGenType {
#[default]
Default = 0,
Rsa2048 = 1,
Ed25519 = 2,
}
impl Default for KeyGenType {
fn default() -> Self {
KeyGenType::Default
}
}
/// Video chat URL type.
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(i8)]
pub enum VideochatType {
/// Unknown type.
#[default]
Unknown = 0,
BasicWebrtc = 1,
Jitsi = 2,
}
impl Default for VideochatType {
fn default() -> Self {
VideochatType::Unknown
}
/// [basicWebRTC](https://github.com/cracker0dks/basicwebrtc) instance.
BasicWebrtc = 1,
/// [Jitsi Meet](https://jitsi.org/jitsi-meet/) instance.
Jitsi = 2,
}
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
@@ -133,8 +115,10 @@ 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,
@@ -150,17 +134,21 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
)]
#[repr(u32)]
pub enum Chattype {
/// Undefined chat type.
#[default]
Undefined = 0,
Single = 100,
Group = 120,
Mailinglist = 140,
Broadcast = 160,
}
impl Default for Chattype {
fn default() -> Self {
Chattype::Undefined
}
/// 1:1 chat.
Single = 100,
/// Group chat.
Group = 120,
/// Mailing list.
Mailinglist = 140,
/// Broadcast list.
Broadcast = 160,
}
pub const DC_MSG_ID_DAYMARKER: u32 = 9;

View File

@@ -223,12 +223,24 @@ pub struct Contact {
/// Possible origins of a contact.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug,
Default,
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
@@ -287,12 +299,6 @@ 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
@@ -1515,7 +1521,6 @@ 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,6 +35,7 @@ pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
/// Download state of the message.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -50,6 +51,7 @@ 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.
@@ -62,12 +64,6 @@ 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,7 +1,5 @@
//! # Events specification.
#![allow(missing_docs)]
use std::path::PathBuf;
use async_channel::{self as channel, Receiver, Sender, TrySendError};
@@ -111,6 +109,7 @@ 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.
@@ -171,17 +170,23 @@ 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,
},
@@ -190,11 +195,16 @@ 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>,
},
@@ -205,21 +215,30 @@ 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,
},
@@ -234,7 +253,10 @@ pub enum EventType {
/// Chat ephemeral timer changed.
ChatEphemeralTimerModified {
/// Chat ID.
chat_id: ChatId,
/// New ephemeral timer value.
timer: EphemeralTimer,
},
@@ -281,15 +303,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,
},
@@ -297,12 +319,13 @@ 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,
},
@@ -312,15 +335,21 @@ 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,17 +1,18 @@
//! # 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,
@@ -34,11 +35,18 @@ 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,
@@ -52,9 +60,14 @@ pub enum HeaderDef {
ChatGroupMemberRemoved,
ChatGroupMemberAdded,
ChatContent,
/// Duration of the attached media file.
ChatDuration,
ChatDispositionNotificationTo,
ChatWebrtcRoom,
/// [Autocrypt](https://autocrypt.org/) header.
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
@@ -63,6 +76,8 @@ pub enum HeaderDef {
SecureJoinInvitenumber,
SecureJoinAuth,
Sender,
/// Ephemeral message timer.
EphemeralTimer,
Received,
@@ -70,7 +85,8 @@ pub enum HeaderDef {
/// See <https://datatracker.ietf.org/doc/html/rfc8601>
AuthenticationResults,
_TestHeader,
#[cfg(test)]
TestHeader,
}
impl HeaderDef {
@@ -80,8 +96,12 @@ 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>;
}
@@ -103,7 +123,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,6 +981,7 @@ impl Message {
/// For outgoing message, the message could be pending, already delivered or confirmed.
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
@@ -997,6 +998,7 @@ impl Message {
#[repr(u32)]
pub enum MessageState {
/// Undefined message state.
#[default]
Undefined = 0,
/// Incoming *fresh* message. Fresh messages are neither noticed
@@ -1039,12 +1041,6 @@ 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!(
@@ -1914,6 +1910,7 @@ pub(crate) async fn rfc724_mid_exists(
/// How a message is primarily displayed.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -1929,6 +1926,7 @@ pub(crate) async fn rfc724_mid_exists(
#[repr(u32)]
pub enum Viewtype {
/// Unknown message type.
#[default]
Unknown = 0,
/// Text message.
@@ -1982,12 +1980,6 @@ 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

@@ -98,6 +98,8 @@ pub struct RenderedEmail {
/// Message ID (Message in the sense of Email)
pub rfc724_mid: String,
/// Message subject.
pub subject: String,
}
@@ -2009,7 +2011,7 @@ mod tests {
"1.0"
);
let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes())
let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes(), None)
.await
.unwrap();
}

View File

@@ -1,7 +1,5 @@
//! # MIME message parsing module.
#![allow(missing_docs)]
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
@@ -48,7 +46,7 @@ use crate::{location, tools};
/// It is created by parsing the raw data of an actual MIME message
/// using the [MimeMessage::from_bytes] constructor.
#[derive(Debug)]
pub struct MimeMessage {
pub(crate) struct MimeMessage {
/// Parsed MIME parts.
pub parts: Vec<Part>,
@@ -130,11 +128,14 @@ pub(crate) enum MailinglistType {
None,
}
/// System message type.
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
Debug, Default, 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.
@@ -151,8 +152,14 @@ 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.
@@ -177,24 +184,14 @@ 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 {
pub async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
MimeMessage::from_bytes_with_partial(context, body, None).await
}
/// Parse a mime message.
///
/// If `partial` is set, it contains the full message size in bytes
/// and `body` contains the header only.
pub(crate) async fn from_bytes_with_partial(
pub(crate) async fn from_bytes(
context: &Context,
body: &[u8],
partial: Option<u32>,
@@ -1801,6 +1798,8 @@ pub struct Part {
/// Size of the MIME part in bytes.
pub bytes: usize,
/// Parameters.
pub param: Params,
/// Attachment filename.
@@ -2012,35 +2011,35 @@ mod tests {
async fn test_mimeparser_fromheader() {
let ctx = TestContext::new_alice().await;
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi")
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi", None)
.await
.unwrap();
let contact = mimemsg.from;
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, None);
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi")
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi", None)
.await
.unwrap();
let contact = mimemsg.from;
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, None);
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: <g@c.de>\n\nhi")
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: <g@c.de>\n\nhi", None)
.await
.unwrap();
let contact = mimemsg.from;
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, None);
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C <g@c.de>\n\nhi")
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C <g@c.de>\n\nhi", None)
.await
.unwrap();
let contact = mimemsg.from;
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, Some("Goetz C".to_string()));
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" <g@c.de>\n\nhi")
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" <g@c.de>\n\nhi", None)
.await
.unwrap();
let contact = mimemsg.from;
@@ -2048,7 +2047,7 @@ mod tests {
assert_eq!(contact.display_name, Some("Goetz C".to_string()));
let mimemsg =
MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C <g@c.de>\n\nhi")
MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C <g@c.de>\n\nhi", None)
.await
.unwrap();
let contact = mimemsg.from;
@@ -2057,10 +2056,13 @@ mod tests {
// although RFC 2047 says, encoded-words shall not appear inside quoted-string,
// this combination is used in the wild eg. by MailMate
let mimemsg =
MimeMessage::from_bytes(&ctx, b"From: \"=?utf-8?q?G=C3=B6tz?= C\" <g@c.de>\n\nhi")
.await
.unwrap();
let mimemsg = MimeMessage::from_bytes(
&ctx,
b"From: \"=?utf-8?q?G=C3=B6tz?= C\" <g@c.de>\n\nhi",
None,
)
.await
.unwrap();
let contact = mimemsg.from;
assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, Some("Götz C".to_string()));
@@ -2070,7 +2072,7 @@ mod tests {
async fn test_mimeparser_crash() {
let context = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
@@ -2082,7 +2084,7 @@ mod tests {
async fn test_get_rfc724_mid_exists() {
let context = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/mail_with_message_id.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
@@ -2096,7 +2098,7 @@ mod tests {
async fn test_get_rfc724_mid_not_exists() {
let context = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(mimeparser.get_rfc724_mid(), None);
@@ -2293,7 +2295,7 @@ mod tests {
test1\n\
";
let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await;
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await;
assert!(mimeparser.is_err());
}
@@ -2308,7 +2310,7 @@ mod tests {
\n\
Some reply\n\
";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(
@@ -2354,7 +2356,7 @@ mod tests {
--==break==--\n\
\n";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
@@ -2363,7 +2365,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
@@ -2388,26 +2390,26 @@ mod tests {
let t = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/mail_attach_txt.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
assert_eq!(mimeparser.user_avatar, None);
assert_eq!(mimeparser.group_avatar, None);
let raw = include_bytes!("../test-data/message/mail_with_user_avatar.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert!(mimeparser.user_avatar.unwrap().is_change());
assert_eq!(mimeparser.group_avatar, None);
let raw = include_bytes!("../test-data/message/mail_with_user_avatar_deleted.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete));
assert_eq!(mimeparser.group_avatar, None);
let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert!(mimeparser.user_avatar.unwrap().is_change());
@@ -2417,7 +2419,9 @@ mod tests {
let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml");
let raw = String::from_utf8_lossy(raw).to_string();
let raw = raw.replace("Chat-User-Avatar:", "Xhat-Xser-Xvatar:");
let mimeparser = MimeMessage::from_bytes(&t, raw.as_bytes()).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t, raw.as_bytes(), None)
.await
.unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Image);
assert_eq!(mimeparser.user_avatar, None);
@@ -2429,7 +2433,7 @@ mod tests {
let t = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/videochat_invitation.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::VideochatInvitation);
assert_eq!(
@@ -2476,7 +2480,7 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\
--==break==--\n\
;";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(
@@ -2525,7 +2529,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(
@@ -2605,7 +2609,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
--outer--\n\
";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(
@@ -2652,7 +2656,7 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(
@@ -2699,7 +2703,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
------=_Part_25_46172632.1581201680436--
"#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(
@@ -2743,7 +2747,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
------=_Part_25_46172632.1581201680436--
"#;
let message = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
let message = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::File);
@@ -2797,7 +2801,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
----11019878869865180--
"#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(message.get_subject(), Some("example".to_string()));
@@ -2869,7 +2873,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
--------------779C1631600DF3DB8C02E53A--"#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(message.get_subject(), Some("Test subject".to_string()));
@@ -2940,7 +2944,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
------=_NextPart_000_0003_01D622B3.CA753E60--
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(
@@ -3038,7 +3042,7 @@ From: alice <alice@example.org>
Reply
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(
@@ -3070,7 +3074,7 @@ From: alice <alice@example.org>
> Just a quote.
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(
@@ -3104,7 +3108,7 @@ On 2020-10-25, Bob wrote:
> A quote.
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(message.get_subject(), Some("Re: top posting".to_string()));
@@ -3122,7 +3126,7 @@ On 2020-10-25, Bob wrote:
async fn test_attachment_quote() {
let context = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/quote_attach.eml");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
@@ -3140,7 +3144,7 @@ On 2020-10-25, Bob wrote:
async fn test_quote_div() {
let t = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/gmx-quote.eml");
let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t, raw, None).await.unwrap();
assert_eq!(mimeparser.parts[0].msg, "YIPPEEEEEE\n\nMulti-line");
assert_eq!(mimeparser.parts[0].param.get(Param::Quote).unwrap(), "Now?");
}
@@ -3150,7 +3154,7 @@ On 2020-10-25, Bob wrote:
// all-inkl.com puts quotes into `<blockquote> </blockquote>`.
let t = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/allinkl-quote.eml");
let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t, raw, None).await.unwrap();
assert!(mimeparser.parts[0].msg.starts_with("It's 1.0."));
assert_eq!(
mimeparser.parts[0].param.get(Param::Quote).unwrap(),
@@ -3194,7 +3198,7 @@ On 2020-10-25, Bob wrote:
async fn test_mime_modified_plain() {
let t = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap();
assert!(!mimeparser.is_mime_modified);
assert_eq!(
mimeparser.parts[0].msg,
@@ -3206,7 +3210,7 @@ On 2020-10-25, Bob wrote:
async fn test_mime_modified_alt_plain_html() {
let t = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap();
assert!(mimeparser.is_mime_modified);
assert_eq!(
mimeparser.parts[0].msg,
@@ -3218,7 +3222,7 @@ On 2020-10-25, Bob wrote:
async fn test_mime_modified_alt_plain() {
let t = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/text_alt_plain.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap();
assert!(!mimeparser.is_mime_modified);
assert_eq!(
mimeparser.parts[0].msg,
@@ -3233,7 +3237,7 @@ On 2020-10-25, Bob wrote:
async fn test_mime_modified_alt_html() {
let t = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap();
assert!(mimeparser.is_mime_modified);
assert_eq!(
mimeparser.parts[0].msg,
@@ -3245,7 +3249,7 @@ On 2020-10-25, Bob wrote:
async fn test_mime_modified_html() {
let t = TestContext::new_alice().await;
let raw = include_bytes!("../test-data/message/text_html.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap();
assert!(mimeparser.is_mime_modified);
assert_eq!(
mimeparser.parts[0].msg,
@@ -3261,7 +3265,7 @@ On 2020-10-25, Bob wrote:
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_DESIRED_TEXT_LEN
let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT));
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref())
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None)
.await
.unwrap();
assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT);
@@ -3288,7 +3292,7 @@ On 2020-10-25, Bob wrote:
MIME-Version: 1.0\n\
\n\
Does it work with outlook now?\n\
")
", None)
.await
.unwrap();
assert_eq!(
@@ -3453,7 +3457,7 @@ Message.
// 1. Test mimeparser directly
let mdn =
include_bytes!("../test-data/message/ms_exchange_report_disposition_notification.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn).await?;
let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn, None).await?;
assert_eq!(mimeparser.mdn_reports.len(), 1);
assert_eq!(
mimeparser.mdn_reports[0].original_message_id.as_deref(),
@@ -3479,6 +3483,7 @@ Message.
let mime_message = MimeMessage::from_bytes(
&alice,
include_bytes!("../test-data/message/attached-eml.eml"),
None,
)
.await?;
@@ -3521,6 +3526,7 @@ Content-Disposition: reaction\n\
\n\
\u{1F44D}"
.as_bytes(),
None,
)
.await?;

View File

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

View File

@@ -1,7 +1,5 @@
//! 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;
@@ -24,7 +22,10 @@ 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,10 +37,11 @@ pub enum Protocol {
}
/// Socket security.
#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[derive(Debug, Default, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Socket {
/// Unspecified socket security, select automatically.
#[default]
Automatic = 0,
/// TLS connection.
@@ -53,12 +54,6 @@ 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,7 +1,5 @@
//! # QR code module.
#![allow(missing_docs)]
mod dclogin_scheme;
use std::collections::BTreeMap;
@@ -37,80 +35,190 @@ 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,
},
}
@@ -119,7 +227,8 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
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 takes the raw text scanned and checks what can be done with it.
pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
@@ -415,6 +524,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<()> {
match check_qr(context, qr).await? {
Qr::Account { .. } => set_account_from_qr(context, qr).await?,
@@ -617,6 +727,9 @@ 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,22 +9,53 @@ 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 @@
#![allow(missing_docs)]
//! # QR code generation module.
use anyhow::Result;
use base64::Engine as _;
@@ -14,6 +14,10 @@ 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,7 +1,5 @@
//! Internet Message Format reception pipeline.
#![allow(missing_docs)]
use std::cmp::min;
use std::collections::HashSet;
use std::convert::TryFrom;
@@ -48,8 +46,13 @@ 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.
@@ -103,35 +106,35 @@ pub(crate) async fn receive_imf_inner(
);
}
let mut mime_parser =
match MimeMessage::from_bytes_with_partial(context, imf_raw, is_partial_download).await {
Err(err) => {
warn!(context, "receive_imf: can't parse MIME: {:#}", err);
let msg_ids;
if !rfc724_mid.starts_with(GENERATED_PREFIX) {
let row_id = context
.sql
.execute(
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
paramsv![rfc724_mid, DC_CHAT_ID_TRASH],
)
.await?;
msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
} else {
return Ok(None);
// We don't have an rfc724_mid, there's no point in adding a trash entry
}
return Ok(Some(ReceivedMsg {
chat_id: DC_CHAT_ID_TRASH,
state: MessageState::Undefined,
sort_timestamp: 0,
msg_ids,
needs_delete_job: false,
}));
let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw, is_partial_download).await
{
Err(err) => {
warn!(context, "receive_imf: can't parse MIME: {:#}", err);
let msg_ids;
if !rfc724_mid.starts_with(GENERATED_PREFIX) {
let row_id = context
.sql
.execute(
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
paramsv![rfc724_mid, DC_CHAT_ID_TRASH],
)
.await?;
msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
} else {
return Ok(None);
// We don't have an rfc724_mid, there's no point in adding a trash entry
}
Ok(mime_parser) => mime_parser,
};
return Ok(Some(ReceivedMsg {
chat_id: DC_CHAT_ID_TRASH,
state: MessageState::Undefined,
sort_timestamp: 0,
msg_ids,
needs_delete_job: false,
}));
}
Ok(mime_parser) => mime_parser,
};
// we can not add even an empty record if we have no info whatsoever
if !mime_parser.has_headers() {

View File

@@ -20,7 +20,7 @@ async fn test_grpid_simple() {
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
\n\
hello\x00";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None);
@@ -38,7 +38,7 @@ async fn test_bad_from() {
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
\n\
hello\x00";
let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await;
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await;
assert!(mimeparser.is_err());
}
@@ -52,7 +52,7 @@ async fn test_grpid_from_multiple() {
References: <qweqweqwe>, <Gr.HcxyMARjyJy.9-uvzWPTLtV@nau.ca>\n\
\n\
hello\x00";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
.await
.unwrap();
let grpid = Some("HcxyMARjyJy");
@@ -2614,7 +2614,7 @@ References: <second@example.net> <nonexistent@example.net> <first@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Message with references."#;
let mime_parser = MimeMessage::from_bytes(&t, &mime[..]).await?;
let mime_parser = MimeMessage::from_bytes(&t, &mime[..], None).await?;
let parent = get_parent_message(&t, &mime_parser).await?.unwrap();
assert_eq!(parent.id, first.id);

View File

@@ -27,9 +27,10 @@ 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, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
#[derive(Debug, Default, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
enum DetailedConnectivity {
Error(String),
#[default]
Uninitialized,
Connecting,
Working,
@@ -40,12 +41,6 @@ enum DetailedConnectivity {
NotConfigured,
}
impl Default for DetailedConnectivity {
fn default() -> Self {
DetailedConnectivity::Uninitialized
}
}
impl DetailedConnectivity {
fn to_basic(&self) -> Option<Connectivity> {
match self {

View File

@@ -236,7 +236,7 @@ impl BobState {
/// stage is returned. Once [`BobHandshakeStage::Completed`] or
/// [`BobHandshakeStage::Terminated`] are reached this [`BobState`] should be destroyed,
/// further calling it will just result in the messages being unused by this handshake.
pub async fn handle_message(
pub(crate) async fn handle_message(
&mut self,
context: &Context,
mime_message: &MimeMessage,

View File

@@ -1,15 +1,12 @@
//! # 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::{config::DbConfig, Connection, OpenFlags};
use rusqlite::{self, config::DbConfig, Connection, OpenFlags};
use tokio::sync::RwLock;
use crate::blob::BlobObject;
@@ -26,6 +23,7 @@ use crate::peerstate::{deduplicate_peerstates, Peerstate};
use crate::stock_str;
use crate::tools::{delete_file, time};
#[allow(missing_docs)]
#[macro_export]
macro_rules! paramsv {
() => {
@@ -36,6 +34,7 @@ macro_rules! paramsv {
};
}
#[allow(missing_docs)]
#[macro_export]
macro_rules! params_iterv {
($($param:expr),+ $(,)?) => {
@@ -48,6 +47,9 @@ 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)]
@@ -55,16 +57,19 @@ pub struct Sql {
/// Database file path
pub(crate) dbfile: PathBuf,
pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>,
/// SQL connection pool.
pool: RwLock<Option<Pool>>,
/// 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,
@@ -189,70 +194,27 @@ impl Sql {
})
}
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);
/// 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);
}
// 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")?;
let pool = Pool::new(connections);
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
@@ -397,12 +359,11 @@ impl Sql {
})
}
pub async fn get_conn(
&self,
) -> Result<r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>> {
/// Allocates a connection from the connection pool and returns it.
pub(crate) async fn get_conn(&self) -> Result<PooledConnection> {
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)
}
@@ -594,22 +555,26 @@ 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.
@@ -617,27 +582,68 @@ 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!(
@@ -696,6 +702,7 @@ 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

106
src/sql/pool.rs Normal file
View File

@@ -0,0 +1,106 @@
//! 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,7 +1,5 @@
//! Module to work with translatable stock strings.
#![allow(missing_docs)]
use std::collections::HashMap;
use std::sync::Arc;
@@ -21,6 +19,7 @@ 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.
@@ -35,6 +34,7 @@ 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,6 +422,7 @@ 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())),

View File

@@ -441,8 +441,8 @@ impl TestContext {
/// peerstates will be updated. Later receiving the message using [recv_msg] is
/// unlikely to be affected as the peerstate would be processed again in exactly the
/// same way.
pub async fn parse_msg(&self, msg: &SentMessage<'_>) -> MimeMessage {
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes())
pub(crate) async fn parse_msg(&self, msg: &SentMessage<'_>) -> MimeMessage {
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes(), None)
.await
.unwrap()
}

View File

@@ -13,21 +13,16 @@ use crate::tools::{create_id, time};
/// Token namespace
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
Debug, Default, 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,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 {
let secs = duration.as_secs();
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>> {
let path_abs = get_abs_path(context, &path);
@@ -530,7 +532,10 @@ 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 [MsgId].
/// Create a new [StatusUpdateSerial].
pub fn new(id: u32) -> StatusUpdateSerial {
StatusUpdateSerial(id)
}
/// Gets StatusUpdateId as untyped integer.
/// Gets StatusUpdateSerial as untyped integer.
/// Avoid using this outside ffi.
pub fn to_u32(self) -> u32 {
self.0