mirror of
https://github.com/chatmail/core.git
synced 2026-06-28 02:26:35 +03:00
Compare commits
29 Commits
1.108.0
...
link2xt/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90087bde39 | ||
|
|
bfd3c1763d | ||
|
|
7586bcf45e | ||
|
|
870527de1e | ||
|
|
a34aeb240a | ||
|
|
6ee165fddc | ||
|
|
c9db41a7f6 | ||
|
|
7a6bfae93b | ||
|
|
ae19c9b331 | ||
|
|
7d2cca8633 | ||
|
|
c52b48b0f5 | ||
|
|
893794f4e7 | ||
|
|
032da4fb1a | ||
|
|
0de5125de8 | ||
|
|
cd3f1fe874 | ||
|
|
4f68e94fb3 | ||
|
|
b3ecbbc8b3 | ||
|
|
01653a881a | ||
|
|
e021a59b87 | ||
|
|
243f28f8a5 | ||
|
|
78577594d0 | ||
|
|
d3e2f38da0 | ||
|
|
05f0fe0a88 | ||
|
|
e11d7c0444 | ||
|
|
a203cde400 | ||
|
|
00c14dd9f6 | ||
|
|
71d9716117 | ||
|
|
2e4f63a290 | ||
|
|
9dee725895 |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -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
61
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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...");
|
||||
}
|
||||
|
||||
@@ -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...");
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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> {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ class ProviderNotFoundError(Exception):
|
||||
"""The provider information was not found."""
|
||||
|
||||
|
||||
class Provider(object):
|
||||
class Provider:
|
||||
"""
|
||||
Provider information.
|
||||
|
||||
|
||||
0
python/src/deltachat/py.typed
Normal file
0
python/src/deltachat/py.typed
Normal file
@@ -4,7 +4,7 @@ from .capi import ffi, lib
|
||||
from .cutil import from_dc_charpointer, iter_array
|
||||
|
||||
|
||||
class Reactions(object):
|
||||
class Reactions:
|
||||
"""Reactions object.
|
||||
|
||||
You obtain instances of it through :class:`deltachat.message.Message`.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import os.path
|
||||
import shutil
|
||||
from filecmp import cmp
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
@@ -31,7 +31,7 @@ unset DCC_NEW_TMP_EMAIL
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# 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 -----------------------
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
17
src/chat.rs
17
src/chat.rs
@@ -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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
119
src/qr.rs
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
129
src/sql.rs
129
src/sql.rs
@@ -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;
|
||||
|
||||
@@ -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
106
src/sql/pool.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user