diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4455ec2c6..f0659c359 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,7 +120,7 @@ jobs: - name: Upload C library uses: actions/upload-artifact@v3 with: - name: ${{ matrix.os }}-${{matrix.rust}}-libdeltachat.a + name: ${{ matrix.os }}-libdeltachat.a path: target/debug/libdeltachat.a retention-days: 1 @@ -142,7 +142,7 @@ jobs: - name: Upload deltachat-rpc-server uses: actions/upload-artifact@v3 with: - name: ${{ matrix.os }}-${{matrix.rust}}-deltachat-rpc-server + name: ${{ matrix.os }}-deltachat-rpc-server path: target/debug/deltachat-rpc-server retention-days: 1 @@ -196,7 +196,7 @@ jobs: - name: Download libdeltachat.a uses: actions/download-artifact@v3 with: - name: ${{ matrix.os }}-${{matrix.rust}}-libdeltachat.a + name: ${{ matrix.os }}-libdeltachat.a path: target/debug - name: Install python @@ -255,7 +255,7 @@ jobs: - name: Download deltachat-rpc-server uses: actions/download-artifact@v3 with: - name: ${{ matrix.os }}-${{matrix.rust}}-deltachat-rpc-server + name: ${{ matrix.os }}-deltachat-rpc-server path: target/debug - name: Make deltachat-rpc-server executable diff --git a/CHANGELOG.md b/CHANGELOG.md index 024008417..21a540326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,37 @@ - Remove upper limit on the attachment size. #4253 - Update rPGP to 0.10.1. #4236 - Compress `mime_headers` column with HTML emails stored in database +- Strip BIDI characters in system messages, files, group names and contact names #3479 +- maybe_add_time_based_warnings(): Use release date instead of the provider DB update one +- Remove confusing log line "ignoring unsolicited response Recent(…)" #3934 +- Cleanly terminate deltachat-rpc-server. + Also terminate on ctrl-c. +- Refactorings #4317 +- Add JSON-RPC API `can_send()`. +- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs. + New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API. + These APIs can be used by bots to get all unprocessed messages + in the order of their arrival and wait for them without relying on events. +- Async Python API `get_fresh_messages_in_arrival_order()` is deprecated + in favor of `get_next_msgs()` and `wait_next_msgs()`. +- New Python bindings API `Account.wait_next_incoming_message()`. +- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`. ### Fixes - Fix python bindings README documentation on installing the bindings from source. - Show a warning if quota list is empty #4261 +- Update "accounts.toml" atomically +- Don't let blocking be bypassed using groups #4316 + +## [1.112.6] - 2023-04-04 + +### Changes + +- Add a device message after backup transfer #4301 + +### Fixed + +- Updated `iroh` from 0.4.0 to 0.4.1 to fix transfer of large accounts with many blob files. ## [1.112.5] - 2023-04-02 @@ -32,8 +59,8 @@ ### Changes - Update iroh, remove `default-net` from `[patch.crates-io]` section. -- transfer backup: Connect to mutliple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240 -- Make sure BackupProvider is cancelled on drop (or dc_backup_provider_unref). The BackupProvider will now alaway finish with an IMEX event of 1000 or 0, previoulsy it would sometimes finishe with 1000 (success) when it really was 0 (failure). #4242 +- transfer backup: Connect to multiple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240 +- Make sure BackupProvider is cancelled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242 ### Fixes - Do not return media from trashed messages in the "All media" view. #4247 @@ -207,7 +234,6 @@ - Do not treat invalid email addresses as an exception #3942 - Add timeouts to HTTP requests #3948 - ## 1.105.0 ### Changes @@ -293,7 +319,6 @@ - Disable read timeout during IMAP IDLE #3826 - Bots automatically accept mailing lists #3831 - ## 1.102.0 ### Changes diff --git a/Cargo.lock b/Cargo.lock index b324be423..89b75936b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -905,9 +905,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1157,7 +1157,7 @@ dependencies = [ [[package]] name = "deltachat" -version = "1.112.5" +version = "1.112.6" dependencies = [ "ansi_term", "anyhow", @@ -1231,7 +1231,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "1.112.5" +version = "1.112.6" dependencies = [ "anyhow", "async-channel", @@ -1254,7 +1254,7 @@ dependencies = [ [[package]] name = "deltachat-repl" -version = "1.112.5" +version = "1.112.6" dependencies = [ "ansi_term", "anyhow", @@ -1269,7 +1269,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.112.5" +version = "1.112.6" dependencies = [ "anyhow", "deltachat", @@ -1280,6 +1280,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tokio-util", "yerpc", ] @@ -1293,7 +1294,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "1.112.5" +version = "1.112.6" dependencies = [ "anyhow", "deltachat", @@ -2176,9 +2177,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" +checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" dependencies = [ "bytes", "fnv", diff --git a/Cargo.toml b/Cargo.toml index 81e1f7a34..281aeeb41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "1.112.5" +version = "1.112.6" edition = "2021" license = "MPL-2.0" rust-version = "1.65" diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index bdac367da..f6ed211e4 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.112.5" +version = "1.112.6" description = "Deltachat FFI" edition = "2018" readme = "README.md" diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 3203d865d..c76a5891a 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -181,12 +181,17 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t; * and check it in the event loop thread * every time before calling dc_get_next_event(). * To terminate the event loop, main thread should: - * 1. Notify event loop that it should terminate by atomically setting the - * boolean flag in the memory shared between the main thread and event loop. + * 1. Notify background threads, + * such as event loop (blocking in dc_get_next_event()) + * and message processing loop (blocking in dc_wait_next_msgs()), + * that they should terminate by atomically setting the + * boolean flag in the memory + * shared between the main thread and background loop threads. * 2. Call dc_stop_io() or dc_accounts_stop_io(), depending * on whether a single account or account manager is used. * Stopping I/O is guaranteed to emit at least one event * and interrupt the event loop even if it was blocked on dc_get_next_event(). + * Stopping I/O is guaranteed to interrupt a single dc_wait_next_msgs(). * 3. Wait until the event loop thread notices the flag, * exits the event loop and terminates. * 4. Call dc_context_unref() or dc_accounts_unref(). @@ -457,6 +462,16 @@ char* dc_get_blobdir (const dc_context_t* context); * Prevents adding the "Device messages" and "Saved messages" chats, * adds Auto-Submitted header to outgoing messages * and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots). + * - `last_msg_id` = database ID of the last message processed by the bot. + * This ID and IDs below it are guaranteed not to be returned + * by dc_get_next_msgs() and dc_wait_next_msgs(). + * The value is updated automatically + * when dc_markseen_msgs() is called, + * but the bot can also set it manually if it processed + * the message but does not want to mark it as seen. + * For most bots calling `dc_markseen_msgs()` is the + * recommended way to update this value + * even for self-sent messages. * - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default), * 0=do not fetch existing messages on configure. * In both cases, existing recipients are added to the contact database. @@ -1343,6 +1358,56 @@ int dc_estimate_deletion_cnt (dc_context_t* context, int from_ser dc_array_t* dc_get_fresh_msgs (dc_context_t* context); +/** + * Returns the message IDs of all messages of any chat + * with a database ID higher than `last_msg_id` config value. + * + * This function is intended for use by bots. + * Self-sent messages, device messages, + * messages from contact requests + * and muted chats are included, + * but messages from explicitly blocked contacts + * and chats are ignored. + * + * This function may be called as a part of event loop + * triggered by DC_EVENT_INCOMING_MSG if you are only interested + * in the incoming messages. + * Otherwise use a separate message processing loop + * calling dc_wait_next_msgs() in a separate thread. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @return An array of message IDs, must be dc_array_unref()'d when no longer used. + * On errors, the list is empty. NULL is never returned. + */ +dc_array_t* dc_get_next_msgs (dc_context_t* context); + + +/** + * Waits for notification of new messages + * and returns an array of new message IDs. + * See the documentation for dc_get_next_msgs() + * for the details of return value. + * + * This function waits for internal notification of + * a new message in the database and returns afterwards. + * Notification is also sent when I/O is started + * to allow processing new messages + * and when I/O is stopped using dc_stop_io() or dc_accounts_stop_io() + * to allow for manual interruption of the message processing loop. + * The function may return an empty array if there are + * no messages after notification, + * which may happen on start or if the message is quickly deleted + * after adding it to the database. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @return An array of message IDs, must be dc_array_unref()'d when no longer used. + * On errors, the list is empty. NULL is never returned. + */ +dc_array_t* dc_wait_next_msgs (dc_context_t* context); + + /** * Mark all messages in a chat as _noticed_. * _Noticed_ messages are no longer _fresh_ and do not count as being unseen @@ -1942,6 +2007,11 @@ int dc_resend_msgs (dc_context_t* context, const uint3 * Moreover, timer is started for incoming ephemeral messages. * This also happens for contact requests chats. * + * This function updates last_msg_id configuration value + * to the maximum of the current value and IDs passed to this function. + * Bots which mark messages as seen can rely on this side effect + * to avoid updating last_msg_id value manually. + * * One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat. * * @memberof dc_context_t @@ -7069,6 +7139,11 @@ void dc_event_unref(dc_event_t* event); /// `%1$s` will be replaced by name and address of the account. #define DC_STR_BACKUP_TRANSFER_QR 162 +/// "Account transferred to your second device." +/// +/// Used as a device message after a successful backup transfer. +#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163 + /** * @} */ diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 7daa9d195..6872c6d38 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -1280,6 +1280,50 @@ pub unsafe extern "C" fn dc_get_fresh_msgs( }) } +#[no_mangle] +pub unsafe extern "C" fn dc_get_next_msgs(context: *mut dc_context_t) -> *mut dc_array::dc_array_t { + if context.is_null() { + eprintln!("ignoring careless call to dc_get_next_msgs()"); + return ptr::null_mut(); + } + let ctx = &*context; + + let msg_ids = block_on(ctx.get_next_msgs()) + .context("failed to get next messages") + .log_err(ctx) + .unwrap_or_default(); + let arr = dc_array_t::from( + msg_ids + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>(), + ); + Box::into_raw(Box::new(arr)) +} + +#[no_mangle] +pub unsafe extern "C" fn dc_wait_next_msgs( + context: *mut dc_context_t, +) -> *mut dc_array::dc_array_t { + if context.is_null() { + eprintln!("ignoring careless call to dc_wait_next_msgs()"); + return ptr::null_mut(); + } + let ctx = &*context; + + let msg_ids = block_on(ctx.wait_next_msgs()) + .context("failed to wait for next messages") + .log_err(ctx) + .unwrap_or_default(); + let arr = dc_array_t::from( + msg_ids + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>(), + ); + Box::into_raw(Box::new(arr)) +} + #[no_mangle] pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id: u32) { if context.is_null() { diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 82b29c3bc..77787abd3 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "1.112.5" +version = "1.112.6" description = "DeltaChat JSON-RPC API" edition = "2021" default-run = "deltachat-jsonrpc-server" diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index e821a5b68..cc994ed63 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -453,6 +453,49 @@ impl CommandApi { ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await } + /// Gets messages to be processed by the bot and returns their IDs. + /// + /// Only messages with database ID higher than `last_msg_id` config value + /// are returned. After processing the messages, the bot should + /// update `last_msg_id` by calling [`markseen_msgs`] + /// or manually updating the value to avoid getting already + /// processed messages. + /// + /// [`markseen_msgs`]: Self::markseen_msgs + async fn get_next_msgs(&self, account_id: u32) -> Result> { + let ctx = self.get_context(account_id).await?; + let msg_ids = ctx + .get_next_msgs() + .await? + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect(); + Ok(msg_ids) + } + + /// Waits for messages to be processed by the bot and returns their IDs. + /// + /// This function is similar to [`get_next_msgs`], + /// but waits for internal new message notification before returning. + /// New message notification is sent when new message is added to the database, + /// on initialization, when I/O is started and when I/O is stopped. + /// This allows bots to use `wait_next_msgs` in a loop to process + /// old messages after initialization and during the bot runtime. + /// To shutdown the bot, stopping I/O can be used to interrupt + /// pending or next `wait_next_msgs` call. + /// + /// [`get_next_msgs`]: Self::get_next_msgs + async fn wait_next_msgs(&self, account_id: u32) -> Result> { + let ctx = self.get_context(account_id).await?; + let msg_ids = ctx + .wait_next_msgs() + .await? + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect(); + Ok(msg_ids) + } + /// Estimate the number of messages that will be deleted /// by the set_config()-options `delete_device_after` or `delete_server_after`. /// This is typically used to show the estimated impact to the user @@ -944,6 +987,11 @@ impl CommandApi { /// Moreover, timer is started for incoming ephemeral messages. /// This also happens for contact requests chats. /// + /// This function updates `last_msg_id` configuration value + /// to the maximum of the current value and IDs passed to this function. + /// Bots which mark messages as seen can rely on this side effect + /// to avoid updating `last_msg_id` value manually. + /// /// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat. async fn markseen_msgs(&self, account_id: u32, msg_ids: Vec) -> Result<()> { let ctx = self.get_context(account_id).await?; @@ -1701,6 +1749,15 @@ impl CommandApi { Ok(msg_id) } + /// Checks if messages can be sent to a given chat. + async fn can_send(&self, account_id: u32, chat_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + let chat_id = ChatId::new(chat_id); + let chat = Chat::load_from_db(&ctx, chat_id).await?; + let can_send = chat.can_send(&ctx).await?; + Ok(can_send) + } + // --------------------------------------------- // functions for the composer // the composer is the message input field diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 5121a87e2..e2743bba7 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -55,5 +55,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.112.5" + "version": "1.112.6" } diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index 0fcf6347f..3eca44d6a 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-repl" -version = "1.112.5" +version = "1.112.6" license = "MPL-2.0" edition = "2021" diff --git a/deltachat-rpc-client/examples/echobot_no_hooks.py b/deltachat-rpc-client/examples/echobot_no_hooks.py index fadf2e560..77fda86e7 100644 --- a/deltachat-rpc-client/examples/echobot_no_hooks.py +++ b/deltachat-rpc-client/examples/echobot_no_hooks.py @@ -6,7 +6,7 @@ import asyncio import logging import sys -from deltachat_rpc_client import DeltaChat, EventType, Rpc +from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId async def main(): @@ -30,9 +30,9 @@ async def main(): await deltachat.start_io() async def process_messages(): - for message in await account.get_fresh_messages_in_arrival_order(): + for message in await account.get_next_messages(): snapshot = await message.get_snapshot() - if not snapshot.is_bot and not snapshot.is_info: + if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info: await snapshot.chat.send_text(snapshot.text) await snapshot.message.mark_seen() diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py index 94edad50e..727c51c80 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py @@ -3,7 +3,7 @@ from ._utils import AttrDict, run_bot_cli, run_client_cli from .account import Account from .chat import Chat from .client import Bot, Client -from .const import EventType +from .const import EventType, SpecialContactId from .contact import Contact from .deltachat import DeltaChat from .message import Message @@ -19,6 +19,7 @@ __all__ = [ "DeltaChat", "EventType", "Message", + "SpecialContactId", "Rpc", "run_bot_cli", "run_client_cli", diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index 6434ee966..4c44079f7 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, List, Optional, Tuple, Union +from warnings import warn from ._utils import AttrDict from .chat import Chat @@ -239,7 +240,22 @@ class Account: fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id) return [Message(self, msg_id) for msg_id in fresh_msg_ids] + async def get_next_messages(self) -> List[Message]: + """Return a list of next messages.""" + next_msg_ids = await self._rpc.get_next_msgs(self.id) + return [Message(self, msg_id) for msg_id in next_msg_ids] + + async def wait_next_messages(self) -> List[Message]: + """Wait for new messages and return a list of them.""" + next_msg_ids = await self._rpc.wait_next_msgs(self.id) + return [Message(self, msg_id) for msg_id in next_msg_ids] + async def get_fresh_messages_in_arrival_order(self) -> List[Message]: """Return fresh messages list sorted in the order of their arrival, with ascending IDs.""" + warn( + "get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.", + DeprecationWarning, + stacklevel=2, + ) fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id)) return [Message(self, msg_id) for msg_id in fresh_msg_ids] diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index 65063a6f3..a9cfad8ab 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -105,6 +105,10 @@ class Chat: info = await self._rpc.get_full_chat_by_id(self.account.id, self.id) return AttrDict(chat=self, **info) + async def can_send(self) -> bool: + """Return true if messages can be sent to the chat.""" + return await self._rpc.can_send(self.account.id, self.id) + async def send_message( self, text: Optional[str] = None, diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py index 6f816e5de..5205a1ed9 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py @@ -20,7 +20,7 @@ from ._utils import ( parse_system_image_changed, parse_system_title_changed, ) -from .const import COMMAND_PREFIX, EventType, SystemMessageType +from .const import COMMAND_PREFIX, EventType, SpecialContactId, SystemMessageType from .events import ( EventFilter, GroupImageChanged, @@ -189,9 +189,10 @@ class Client: async def _process_messages(self) -> None: if self._should_process_messages: - for message in await self.account.get_fresh_messages_in_arrival_order(): + for message in await self.account.get_next_messages(): snapshot = await message.get_snapshot() - await self._on_new_msg(snapshot) + if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]: + await self._on_new_msg(snapshot) if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE: await self._handle_info_msg(snapshot) await snapshot.message.mark_seen() diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index fc05854d6..d17c28a16 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -98,8 +98,8 @@ async def test_account(acfactory) -> None: assert await alice.get_chatlist() assert await alice.get_chatlist(snapshot=True) assert await alice.get_qr_code() - await alice.get_fresh_messages() - await alice.get_fresh_messages_in_arrival_order() + assert await alice.get_fresh_messages() + assert await alice.get_next_messages() group = await alice.create_group("test group") await group.add_contact(alice_contact_bob) @@ -147,7 +147,9 @@ async def test_chat(acfactory) -> None: assert alice_chat_bob != bob_chat_alice assert repr(alice_chat_bob) await alice_chat_bob.delete() + assert not await bob_chat_alice.can_send() await bob_chat_alice.accept() + assert await bob_chat_alice.can_send() await bob_chat_alice.block() bob_chat_alice = await snapshot.sender.create_chat() await bob_chat_alice.mute() @@ -303,3 +305,29 @@ async def test_bot(acfactory) -> None: await acfactory.process_message(from_account=user, to_client=bot, text="hello") event = await acfactory.process_message(from_account=user, to_client=bot, text="/help") mock.hook.assert_called_once_with(event.msg_id) + + +@pytest.mark.asyncio() +async def test_wait_next_messages(acfactory) -> None: + alice = await acfactory.new_configured_account() + + # Create a bot account so it does not receive device messages in the beginning. + bot = await acfactory.new_preconfigured_account() + await bot.set_config("bot", "1") + await bot.configure() + + # There are no old messages and the call returns immediately. + assert not await bot.wait_next_messages() + + # Bot starts waiting for messages. + next_messages_task = asyncio.create_task(bot.wait_next_messages()) + + bot_addr = await bot.get_config("addr") + alice_contact_bot = await alice.create_contact(bot_addr, "Bob") + alice_chat_bot = await alice_contact_bot.create_chat() + await alice_chat_bot.send_text("Hello!") + + next_messages = await next_messages_task + assert len(next_messages) == 1 + snapshot = await next_messages[0].get_snapshot() + assert snapshot.text == "Hello!" diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index 9a9353432..f66223ade 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "1.112.5" +version = "1.112.6" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" @@ -20,6 +20,7 @@ log = "0.4" serde_json = "1.0.95" serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.27.0", features = ["io-std"] } +tokio-util = "0.7.7" yerpc = { version = "0.4.0", features = ["anyhow_expose"] } [features] diff --git a/deltachat-rpc-server/src/main.rs b/deltachat-rpc-server/src/main.rs index 1e957a38b..87df5fdf4 100644 --- a/deltachat-rpc-server/src/main.rs +++ b/deltachat-rpc-server/src/main.rs @@ -3,6 +3,7 @@ use std::env; ///! ///! It speaks JSON Lines over stdio. use std::path::PathBuf; +use std::sync::Arc; use anyhow::{anyhow, Context as _, Result}; use deltachat::constants::DC_VERSION_STR; @@ -10,7 +11,9 @@ use deltachat_jsonrpc::api::events::event_to_json_rpc_notification; use deltachat_jsonrpc::api::{Accounts, CommandApi}; use futures_lite::stream::StreamExt; use tokio::io::{self, AsyncBufReadExt, BufReader}; +use tokio::sync::RwLock; use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; use yerpc::{RpcClient, RpcSession}; #[tokio::main(flavor = "multi_thread")] @@ -40,24 +43,38 @@ async fn main() -> Result<()> { let events = accounts.get_event_emitter(); log::info!("Creating JSON-RPC API."); - let state = CommandApi::new(accounts); + let accounts = Arc::new(RwLock::new(accounts)); + let state = CommandApi::from_arc(accounts.clone()); let (client, mut out_receiver) = RpcClient::new(); - let session = RpcSession::new(client.clone(), state); + let session = RpcSession::new(client.clone(), state.clone()); + let canceler = CancellationToken::new(); // Events task converts core events to JSON-RPC notifications. let events_task: JoinHandle> = tokio::spawn(async move { + let mut r = Ok(()); while let Some(event) = events.recv().await { + if r.is_err() { + continue; + } let event = event_to_json_rpc_notification(event); - client.send_notification("event", Some(event)).await?; + r = client.send_notification("event", Some(event)).await; } + r?; Ok(()) }); // Send task prints JSON responses to stdout. + let cancelable = canceler.clone(); let send_task: JoinHandle> = tokio::spawn(async move { - while let Some(message) = out_receiver.next().await { - let message = serde_json::to_string(&message)?; + loop { + let message = tokio::select! { + _ = cancelable.cancelled() => break, + message = out_receiver.next() => match message { + None => break, + Some(message) => serde_json::to_string(&message)?, + } + }; log::trace!("RPC send {}", message); println!("{message}"); } @@ -68,23 +85,41 @@ async fn main() -> Result<()> { let recv_task: JoinHandle> = tokio::spawn(async move { let stdin = io::stdin(); let mut lines = BufReader::new(stdin).lines(); - while let Some(message) = lines.next_line().await? { + loop { + let message = tokio::select! { + _ = tokio::signal::ctrl_c() => { + log::info!("got ctrl-c event"); + break; + } + message = lines.next_line() => match message? { + None => { + log::info!("EOF reached on stdin"); + break; + } + Some(message) => message, + } + }; log::trace!("RPC recv {}", message); let session = session.clone(); tokio::spawn(async move { session.handle_incoming(&message).await; }); } - log::info!("EOF reached on stdin"); Ok(()) }); - // Wait for the end of stdin. - recv_task.await??; + // Wait for the end of stdin / ctrl-c. + recv_task.await?.ok(); - // Shutdown the server. - send_task.abort(); - events_task.abort(); + // See "Thread safety" section in deltachat-ffi/deltachat.h for explanation. + // NB: Events are drained by events_task. + canceler.cancel(); + accounts.read().await.stop_io().await; + drop(state); + let (r0, r1) = tokio::join!(events_task, send_task); + for r in [r0, r1] { + r??; + } Ok(()) } diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 0fff96832..e9033c1cf 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -55,7 +55,7 @@ checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ "getrandom 0.2.8", "once_cell", - "version_check 0.9.4", + "version_check", ] [[package]] @@ -73,6 +73,21 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -118,7 +133,7 @@ dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom 7.1.1", + "nom", "num-traits", "rusticata-macros", "thiserror", @@ -133,7 +148,7 @@ checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "synstructure", ] @@ -145,7 +160,7 @@ checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -174,18 +189,19 @@ dependencies = [ [[package]] name = "async-imap" -version = "0.6.0" -source = "git+https://github.com/async-email/async-imap?branch=master#85ff7a3d9d71a3715354fabf2fc1a8d047b5710e" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8379e2f1cdeb79afd2006932d7e8f64993fc0f7386d0ebc37231c90b05968c25" dependencies = [ "async-channel", "async-native-tls 0.4.0", - "base64 0.13.1", + "base64 0.21.0", "byte-pool", "chrono", "futures", "imap-proto", "log", - "nom 7.1.1", + "nom", "once_cell", "ouroboros", "pin-utils", @@ -229,7 +245,7 @@ dependencies = [ "futures", "hostname", "log", - "nom 7.1.1", + "nom", "pin-project", "thiserror", "tokio", @@ -243,14 +259,14 @@ checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] name = "async_zip" -version = "0.0.11" +version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50d29ab7e2f9e808cca1a69ea56a36f4ff216f54a41a23aae1fd4afc05cc020" +checksum = "b2105142db9c6203b9dadc83b0553394589a6cb31b1449a3b46b42f47c3434d0" dependencies = [ "async-compression", "crc32fast", @@ -281,18 +297,18 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base-x" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" - [[package]] name = "base16ct" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.11.0" @@ -460,7 +476,7 @@ checksum = "3b3c57c2a0967ad1a09ba4c2bf8f1c6b6db2f71e8c0db4fa280c65a0f6c249c3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -492,10 +508,41 @@ dependencies = [ ] [[package]] -name = "buf_redux" -version = "0.8.4" +name = "brotli" +version = "3.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "buffer-redux" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2886ea01509598caac116942abd33ab5a88fa32acdf7e4abfa0fc489ca520c9" dependencies = [ "memchr", "safemem", @@ -531,9 +578,19 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "camellia" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30" +dependencies = [ + "byteorder", + "cipher", +] [[package]] name = "cast5" @@ -600,43 +657,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clap" -version = "4.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098" -dependencies = [ - "bitflags 2.0.2", - "clap_derive", - "clap_lex", - "is-terminal", - "once_cell", - "strsim", - "termcolor", -] - -[[package]] -name = "clap_derive" -version = "4.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" -dependencies = [ - "os_str_bytes", -] - [[package]] name = "cobs" version = "0.2.3" @@ -668,24 +688,11 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.42.0", -] - [[package]] name = "const-oid" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cec318a675afcb6a1ea1d4340e2d377e56e47c266f28043ceccbf4412ddfdd3b" +checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" [[package]] name = "const_format" @@ -790,6 +797,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2538c4e68e52548bacb3e83ac549f903d44f011ac9d5abb5e132e67d0808f7" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -813,6 +832,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "4.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" +dependencies = [ + "cfg-if", + "digest 0.10.6", + "fiat-crypto", + "packed_simd_2", + "platforms", + "subtle", + "zeroize", +] + [[package]] name = "cxx" version = "1.0.85" @@ -837,7 +871,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 1.0.107", ] [[package]] @@ -854,7 +888,7 @@ checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -878,7 +912,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 1.0.107", ] [[package]] @@ -889,7 +923,7 @@ checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -898,41 +932,26 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" -[[package]] -name = "data-encoding-macro" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86927b7cd2fe88fa698b87404b287ab98d1a0063a34071d92e575b72d3029aca" -dependencies = [ - "data-encoding", - "data-encoding-macro-internal", -] - -[[package]] -name = "data-encoding-macro-internal" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5bbed42daaa95e780b60a50546aa345b8413a1e46f9a40a12907d3598f038db" -dependencies = [ - "data-encoding", - "syn", -] - [[package]] name = "default-net" -version = "0.13.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cdeb2fba22cc965edfe42933910a84d8202417302f734ac98d34a7ee5ead51" +checksum = "a4898b43aed56499fad6b294d15b3e76a51df68079bf492e5daae38ca084e003" dependencies = [ + "dlopen2", "libc", "memalloc", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "once_cell", "system-configuration", "windows", ] [[package]] name = "deltachat" -version = "1.112.0" +version = "1.112.6" dependencies = [ "anyhow", "async-channel", @@ -942,7 +961,7 @@ dependencies = [ "async_zip", "backtrace", "base64 0.21.0", - "bitflags 1.3.2", + "brotli", "chrono", "deltachat_derive", "email", @@ -990,6 +1009,7 @@ dependencies = [ "tokio-io-timeout", "tokio-stream", "tokio-tar", + "tokio-util", "toml", "trust-dns-resolver", "url", @@ -1011,7 +1031,7 @@ name = "deltachat_derive" version = "2.0.0" dependencies = [ "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -1022,7 +1042,18 @@ checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ "const-oid", "der_derive", - "pem-rfc7468", + "pem-rfc7468 0.6.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b10af9f9f9f2134a42d3f8aa74658660f2e0234b0eb81bd171df8aa32779ed" +dependencies = [ + "const-oid", + "pem-rfc7468 0.7.0", "zeroize", ] @@ -1034,7 +1065,7 @@ checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" dependencies = [ "asn1-rs", "displaydoc", - "nom 7.1.1", + "nom", "num-bigint", "num-traits", "rusticata-macros", @@ -1049,38 +1080,38 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] name = "derive_builder" -version = "0.11.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.11.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] name = "derive_builder_macro" -version = "0.11.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" dependencies = [ "derive_builder_core", - "syn", + "syn 1.0.107", ] [[package]] @@ -1093,7 +1124,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.107", ] [[package]] @@ -1155,7 +1186,30 @@ checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", +] + +[[package]] +name = "dlopen2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b121caccfc363e4d9a4589528f3bef7c71b83c6ed01c8dc68cbeeb7fd29ec698" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a09ac8bb8c16a282264c379dffba707b9c998afc7506009137f3c6136888078" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", ] [[package]] @@ -1164,10 +1218,23 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a48e5d537b8a30c0b023116d981b16334be1485af7ca68db3a2b7024cbc957fd" +dependencies = [ + "der 0.7.3", + "digest 0.10.6", + "elliptic-curve 0.13.4", + "rfc6979 0.4.0", + "signature 2.1.0", ] [[package]] @@ -1177,7 +1244,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ "serde", - "signature", + "signature 1.6.4", +] + +[[package]] +name = "ed25519" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.1.0", ] [[package]] @@ -1186,8 +1263,8 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ - "curve25519-dalek", - "ed25519", + "curve25519-dalek 3.2.0", + "ed25519 1.5.2", "rand 0.7.3", "serde", "serde_bytes", @@ -1195,6 +1272,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "2.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798f704d128510932661a3489b08e3f4c934a01d61c5def59ae7b8e48f19665a" +dependencies = [ + "curve25519-dalek 4.0.0-rc.2", + "ed25519 2.2.1", + "serde", + "sha2 0.10.6", + "zeroize", +] + [[package]] name = "educe" version = "0.4.20" @@ -1204,7 +1294,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1219,15 +1309,36 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", - "crypto-bigint", - "der", + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", "digest 0.10.6", - "ff", + "ff 0.12.1", "generic-array", - "group", + "group 0.12.1", "rand_core 0.6.4", - "sec1", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c71eaa367f2e5d556414a8eea812bc62985c879748d6403edabd9cb03f16e7" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.1", + "digest 0.10.6", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "hkdf", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.1", "subtle", "zeroize", ] @@ -1244,15 +1355,9 @@ dependencies = [ "lazy_static", "rand 0.7.3", "time 0.1.45", - "version_check 0.9.4", + "version_check", ] -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "encoded-words" version = "0.2.0" @@ -1355,7 +1460,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1369,7 +1474,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.107", ] [[package]] @@ -1461,6 +1566,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + [[package]] name = "filetime" version = "0.2.19" @@ -1601,7 +1722,7 @@ checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1641,7 +1762,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", - "version_check 0.9.4", + "version_check", + "zeroize", ] [[package]] @@ -1670,9 +1792,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.11.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" dependencies = [ "color_quant", "weezl", @@ -1690,16 +1812,27 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] [[package]] name = "h2" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" dependencies = [ "bytes", "fnv", @@ -1753,6 +1886,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1813,7 +1955,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756" dependencies = [ - "libm", + "libm 0.2.6", ] [[package]] @@ -1877,6 +2019,15 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "idea" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" +dependencies = [ + "cipher", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1906,9 +2057,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.5" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" dependencies = [ "bytemuck", "byteorder", @@ -1926,7 +2077,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f73b1b63179418b20aa81002d616c5f21b4ba257da9bca6989ea64dc573933e0" dependencies = [ - "nom 7.1.1", + "nom", ] [[package]] @@ -1939,19 +2090,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "indicatif" -version = "0.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" -dependencies = [ - "console", - "number_prefix", - "portable-atomic 0.3.19", - "tokio", - "unicode-width", -] - [[package]] name = "inout" version = "0.1.3" @@ -2000,28 +2138,24 @@ checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" [[package]] name = "iroh" -version = "0.3.0" -source = "git+https://github.com/n0-computer/iroh?branch=main#bf22ee2c311cf2a845bd7ef9425dc773d05502a2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4fb9858c8cd3dd924a5da5bc511363845a9bcfdfac066bb2ef8454eb6111546" dependencies = [ "abao", "anyhow", "base64 0.21.0", "blake3", "bytes", - "clap", - "console", - "data-encoding", "default-net", - "der", + "der 0.6.1", "derive_more", "dirs-next", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "futures", "hex", - "indicatif", - "multibase", "num_cpus", - "portable-atomic 1.1.0", + "portable-atomic", "postcard", "quic-rpc", "quinn", @@ -2046,18 +2180,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "is-terminal" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" -dependencies = [ - "hermit-abi", - "io-lifetimes", - "rustix", - "windows-sys 0.42.0", -] - [[package]] name = "itoa" version = "1.0.5" @@ -2136,6 +2258,12 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + [[package]] name = "libm" version = "0.2.6" @@ -2144,9 +2272,9 @@ checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" [[package]] name = "libsqlite3-sys" -version = "0.25.2" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "cc", "openssl-sys", @@ -2300,17 +2428,6 @@ dependencies = [ "windows-sys 0.42.0", ] -[[package]] -name = "multibase" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" -dependencies = [ - "base-x", - "data-encoding", - "data-encoding-macro", -] - [[package]] name = "mutate_once" version = "0.1.1" @@ -2345,13 +2462,52 @@ dependencies = [ ] [[package]] -name = "nom" -version = "4.2.3" +name = "netlink-packet-core" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +checksum = "7e5cf0b54effda4b91615c40ff0fd12d0d4c9a6e0f5116874f03941792ff535a" dependencies = [ - "memchr", - "version_check 0.1.5", + "anyhow", + "byteorder", + "libc", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea993e32c77d87f01236c38f572ecb6c311d592e56a06262a007fd2a6e31253c" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror", +] + +[[package]] +name = "netlink-sys" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" +dependencies = [ + "bytes", + "libc", + "log", ] [[package]] @@ -2402,7 +2558,7 @@ checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" dependencies = [ "byteorder", "lazy_static", - "libm", + "libm 0.2.6", "num-integer", "num-iter", "num-traits", @@ -2420,7 +2576,7 @@ checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2462,7 +2618,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", - "libm", + "libm 0.2.6", ] [[package]] @@ -2475,12 +2631,6 @@ dependencies = [ "libc", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "object" version = "0.30.0" @@ -2534,7 +2684,7 @@ checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2566,12 +2716,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "os_str_bytes" -version = "6.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" - [[package]] name = "ouroboros" version = "0.15.5" @@ -2592,7 +2736,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2607,8 +2751,20 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.6", +] + +[[package]] +name = "p256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc7304b8213f8597952b2f4c3d7f09947d46bb677801df8f287ffd2c4e26f9da" +dependencies = [ + "ecdsa 0.16.6", + "elliptic-curve 0.13.4", + "primeorder", "sha2 0.10.6", ] @@ -2618,11 +2774,33 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", "sha2 0.10.6", ] +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa 0.16.6", + "elliptic-curve 0.13.4", + "primeorder", + "sha2 0.10.6", +] + +[[package]] +name = "packed_simd_2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" +dependencies = [ + "cfg-if", + "libm 0.1.4", +] + [[package]] name = "parking" version = "2.0.0" @@ -2652,6 +2830,12 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + [[package]] name = "pem" version = "1.1.1" @@ -2670,6 +2854,15 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -2678,17 +2871,19 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pgp" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991e3f098483f52c454c7cb16720adc010c2966a8845d3c34aad589cb86d3196" +checksum = "37a79d6411154d1a9908e7a2c4bac60a5742f6125823c2c30780c7039aef02f0" dependencies = [ "aes", - "base64 0.13.1", + "base64 0.21.0", "bitfield", "block-padding", "blowfish", - "buf_redux", + "bstr", + "buffer-redux", "byteorder", + "camellia", "cast5", "cfb-mode", "chrono", @@ -2697,23 +2892,27 @@ dependencies = [ "derive_builder", "des", "digest 0.10.6", - "ed25519-dalek", + "ed25519-dalek 2.0.0-rc.2", + "elliptic-curve 0.13.4", "flate2", "generic-array", "hex", + "idea", "log", "md-5", - "nom 4.2.3", + "nom", "num-bigint-dig", "num-derive", "num-traits", + "p256 0.13.1", + "p384 0.13.0", "rand 0.8.5", "ripemd", - "rsa", + "rsa 0.9.0-pre.1", "sha1", "sha2 0.10.6", "sha3", - "signature", + "signature 2.1.0", "smallvec", "thiserror", "twofish", @@ -2738,7 +2937,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2759,9 +2958,21 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.6.1", + "pkcs8 0.9.0", + "spki 0.6.0", + "zeroize", +] + +[[package]] +name = "pkcs1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575fd6eebed721a2929faa1ee1383a49788378083bbbd7f299af75dd84195cee" +dependencies = [ + "der 0.7.3", + "pkcs8 0.10.2", + "spki 0.7.1", "zeroize", ] @@ -2771,8 +2982,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.3", + "spki 0.7.1", ] [[package]] @@ -2781,6 +3002,12 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +[[package]] +name = "platforms" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" + [[package]] name = "png" version = "0.17.7" @@ -2793,12 +3020,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "portable-atomic" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" - [[package]] name = "portable-atomic" version = "1.1.0" @@ -2825,7 +3046,7 @@ checksum = "fc4b01218787dd4420daf63875163a787a78294ad48a24e9f6fa8c6507759a79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2840,6 +3061,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" +[[package]] +name = "primeorder" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8d3875361e28f7753baefef104386e7aa47642c93023356d97fdef4003bfb5" +dependencies = [ + "elliptic-curve 0.13.4", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2849,8 +3079,8 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", - "version_check 0.9.4", + "syn 1.0.107", + "version_check", ] [[package]] @@ -2861,14 +3091,14 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "version_check 0.9.4", + "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.49" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -2905,9 +3135,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.27.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" dependencies = [ "memchr", ] @@ -2964,9 +3194,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -3112,9 +3342,9 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "reqwest" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" +checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" dependencies = [ "base64 0.21.0", "bytes", @@ -3163,11 +3393,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.4.9", "hmac", "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -3204,22 +3444,43 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "pkcs1", - "pkcs8", + "pkcs1 0.4.1", + "pkcs8 0.9.0", "rand_core 0.6.4", - "signature", + "signature 1.6.4", "smallvec", "subtle", "zeroize", ] [[package]] -name = "rusqlite" -version = "0.28.0" +name = "rsa" +version = "0.9.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" +checksum = "f16504cc31b04d2a5ec729f0c7e1b62e76634a9537f089df0ca1981dc8208a89" dependencies = [ - "bitflags 1.3.2", + "byteorder", + "const-oid", + "digest 0.10.6", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1 0.7.2", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "signature 2.1.0", + "subtle", + "zeroize", +] + +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags 2.0.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3260,7 +3521,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom 7.1.1", + "nom", ] [[package]] @@ -3384,10 +3645,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", - "der", + "base16ct 0.1.1", + "der 0.6.1", "generic-array", - "pkcs8", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48518a2b5775ba8ca5b46596aae011caa431e6ce7e4a67ead66d92f08884220e" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.3", + "generic-array", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -3456,7 +3731,7 @@ checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -3575,6 +3850,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.6", + "rand_core 0.6.4", +] + [[package]] name = "slab" version = "0.4.7" @@ -3628,7 +3913,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a5be806ab6f127c3da44b7378837ebf01dadca8510a0e572460216b228bd0e" +dependencies = [ + "base64ct", + "der 0.7.3", ] [[package]] @@ -3638,7 +3933,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19cfdc32e0199062113edf41f344fbf784b8205a94600233c84eb838f45191e1" dependencies = [ "base64ct", - "pem-rfc7468", + "pem-rfc7468 0.6.0", "sha2 0.10.6", ] @@ -3648,14 +3943,14 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "288d8f5562af5a3be4bda308dd374b2c807b940ac370b5efa1c99311da91d9a1" dependencies = [ - "ed25519-dalek", - "p256", - "p384", + "ed25519-dalek 1.0.1", + "p256 0.11.1", + "p384 0.11.2", "rand_core 0.6.4", - "rsa", - "sec1", + "rsa 0.7.2", + "sec1 0.3.0", "sha2 0.10.6", - "signature", + "signature 1.6.4", "ssh-encoding", "zeroize", ] @@ -3700,7 +3995,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 1.0.107", ] [[package]] @@ -3720,6 +4015,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -3728,7 +4034,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "unicode-xid", ] @@ -3809,7 +4115,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -3913,7 +4219,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -3969,9 +4275,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" dependencies = [ "bytes", "futures-core", @@ -4041,7 +4347,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -4250,12 +4556,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" - [[package]] name = "version_check" version = "0.9.4" @@ -4327,7 +4627,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-shared", ] @@ -4361,7 +4661,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4589,12 +4889,12 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "1.1.1" +version = "2.0.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" dependencies = [ - "curve25519-dalek", - "rand_core 0.5.1", + "curve25519-dalek 3.2.0", + "rand_core 0.6.4", "zeroize", ] @@ -4609,7 +4909,7 @@ dependencies = [ "data-encoding", "der-parser", "lazy_static", - "nom 7.1.1", + "nom", "oid-registry", "rusticata-macros", "thiserror", @@ -4651,6 +4951,6 @@ checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "synstructure", ] diff --git a/package.json b/package.json index a06a0c3f5..6716b8c01 100644 --- a/package.json +++ b/package.json @@ -60,5 +60,5 @@ "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit" }, "types": "node/dist/index.d.ts", - "version": "1.112.5" + "version": "1.112.6" } diff --git a/python/README.rst b/python/README.rst index c2b4d87e2..da1b25c7e 100644 --- a/python/README.rst +++ b/python/README.rst @@ -122,7 +122,7 @@ Build and install the bindings: export DCC_RS_DEV="$PWD" export DCC_RS_TARGET=release - python -m pip install python + python -m pip install ./python `DCC_RS_DEV` environment variable specifies the location of the core development tree. If this variable is not set, diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index 9d80e35f7..6d067f4fb 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -376,6 +376,22 @@ class Account: dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref) return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None) + def _wait_next_message_ids(self) -> List[int]: + """Return IDs of all next messages from all chats.""" + dc_array = ffi.gc(lib.dc_wait_next_msgs(self._dc_context), lib.dc_array_unref) + return [lib.dc_array_get_id(dc_array, i) for i in range(lib.dc_array_get_cnt(dc_array))] + + def wait_next_incoming_message(self) -> Message: + """Waits until the next incoming message + with ID higher than given is received and returns it.""" + while True: + message_ids = self._wait_next_message_ids() + for msg_id in message_ids: + message = Message.from_db(self, msg_id) + if message and not message.is_from_self() and not message.is_from_device(): + self.set_config("last_msg_id", str(msg_id)) + return message + def create_chat(self, obj) -> Chat: """Create a 1:1 chat with Account, Contact or e-mail address.""" return self.create_contact(obj).create_chat() diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py index 1c2986728..e58d97d05 100644 --- a/python/src/deltachat/message.py +++ b/python/src/deltachat/message.py @@ -344,6 +344,16 @@ class Message: contact_id = lib.dc_msg_get_from_id(self._dc_msg) return Contact(self.account, contact_id) + def is_from_self(self): + """Return true if the message is sent by self.""" + contact_id = lib.dc_msg_get_from_id(self._dc_msg) + return contact_id == const.DC_CONTACT_ID_SELF + + def is_from_device(self): + """Return true if the message is sent by the device.""" + contact_id = lib.dc_msg_get_from_id(self._dc_msg) + return contact_id == const.DC_CONTACT_ID_DEVICE + # # Message State query methods # diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index cfa5655a1..85e1774bd 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -44,21 +44,21 @@ def test_configure_generate_key(acfactory, lp): lp.sec("ac1: send unencrypted message to ac2") chat.send_text("message1") lp.sec("ac2: waiting for message from ac1") - msg_in = ac2._evtracker.wait_next_incoming_message() + msg_in = ac2.wait_next_incoming_message() assert msg_in.text == "message1" assert not msg_in.is_encrypted() lp.sec("ac2: send encrypted message to ac1") msg_in.chat.send_text("message2") lp.sec("ac1: waiting for message from ac2") - msg2_in = ac1._evtracker.wait_next_incoming_message() + msg2_in = ac1.wait_next_incoming_message() assert msg2_in.text == "message2" assert msg2_in.is_encrypted() lp.sec("ac1: send encrypted message to ac2") msg2_in.chat.send_text("message3") lp.sec("ac2: waiting for message from ac1") - msg3_in = ac2._evtracker.wait_next_incoming_message() + msg3_in = ac2.wait_next_incoming_message() assert msg3_in.text == "message3" assert msg3_in.is_encrypted() diff --git a/release-date.in b/release-date.in new file mode 100644 index 000000000..5dc1be6ba --- /dev/null +++ b/release-date.in @@ -0,0 +1 @@ +2023-04-04 \ No newline at end of file diff --git a/scripts/create-provider-data-rs.py b/scripts/create-provider-data-rs.py index 57f92428e..824e3354d 100755 --- a/scripts/create-provider-data-rs.py +++ b/scripts/create-provider-data-rs.py @@ -233,7 +233,7 @@ if __name__ == "__main__": else: now = datetime.datetime.fromisoformat(sys.argv[2]) out_all += ( - "pub static PROVIDER_UPDATED: Lazy = " + "pub static _PROVIDER_UPDATED: Lazy = " "Lazy::new(|| chrono::NaiveDate::from_ymd_opt(" + str(now.year) + ", " diff --git a/scripts/run-python-test.sh b/scripts/run-python-test.sh index 93acd07cc..544e1e278 100755 --- a/scripts/run-python-test.sh +++ b/scripts/run-python-test.sh @@ -22,4 +22,5 @@ export PYTHONDONTWRITEBYTECODE=1 # run python tests (tox invokes pytest to run tests in python/tests) #TOX_PARALLEL_NO_SPINNER=1 tox -e lint,doc tox -e lint -tox -e doc,py +tox -e doc +tox -e py -- "$@" diff --git a/scripts/set_core_version.py b/scripts/set_core_version.py index 63e3ec205..cd6303cc9 100755 --- a/scripts/set_core_version.py +++ b/scripts/set_core_version.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import datetime import json import os import pathlib @@ -91,15 +92,25 @@ def main(): ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml") assert core_toml == ffi_toml, (core_toml, ffi_toml) + today = datetime.date.today().isoformat() + if "alpha" not in newversion: - for line in open("CHANGELOG.md"): + changelog_name = "CHANGELOG.md" + changelog_tmpname = changelog_name + ".tmp" + changelog_tmp = open(changelog_tmpname, "w") + found = False + for line in open(changelog_name): ## 1.25.0 - if line.startswith("## [") and line[4:].strip().startswith(newversion): - break - else: + if line == f"## [{newversion}]\n": + line = f"## [{newversion}] - {today}\n" + found = True + changelog_tmp.write(line) + if not found: raise SystemExit( - f"CHANGELOG.md contains no entry for version: {newversion}" + f"{changelog_name} contains no entry for version: {newversion}" ) + changelog_tmp.close() + os.rename(changelog_tmpname, changelog_name) for toml_filename in toml_list: replace_toml_version(toml_filename, newversion) @@ -107,6 +118,9 @@ def main(): for json_filename in json_list: update_package_json(json_filename, newversion) + with open("release-date.in", "w") as f: + f.write(today) + print("running cargo check") subprocess.call(["cargo", "check"]) diff --git a/src/accounts.rs b/src/accounts.rs index f81c9a7ca..082057595 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use anyhow::{ensure, Context as _, Result}; use serde::{Deserialize, Serialize}; use tokio::fs; +use tokio::io::AsyncWriteExt; use uuid::Uuid; use crate::context::Context; @@ -301,7 +302,7 @@ pub const DB_NAME: &str = "dc.db"; /// Account manager configuration file. #[derive(Debug, Clone, PartialEq)] -pub struct Config { +struct Config { file: PathBuf, inner: InnerConfig, } @@ -325,10 +326,8 @@ impl Config { selected_account: 0, next_id: 1, }; - let cfg = Config { - file: dir.join(CONFIG_NAME), - inner, - }; + let file = dir.join(CONFIG_NAME); + let mut cfg = Self { file, inner }; cfg.sync().await?; @@ -336,10 +335,24 @@ impl Config { } /// Sync the inmemory representation to disk. - async fn sync(&self) -> Result<()> { - fs::write(&self.file, toml::to_string_pretty(&self.inner)?) + /// Takes a mutable reference because the saved file is a part of the `Config` state. This + /// protects from parallel calls resulting to a wrong file contents. + async fn sync(&mut self) -> Result<()> { + let tmp_path = self.file.with_extension("toml.tmp"); + let mut file = fs::File::create(&tmp_path) .await - .context("failed to write config") + .context("failed to create a tmp config")?; + file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes()) + .await + .context("failed to write a tmp config")?; + file.sync_data() + .await + .context("failed to sync a tmp config")?; + drop(file); + fs::rename(&tmp_path, &self.file) + .await + .context("failed to rename config")?; + Ok(()) } /// Read a configuration from the given file into memory. @@ -359,7 +372,7 @@ impl Config { } } - let config = Self { file, inner }; + let mut config = Self { file, inner }; if modified { config.sync().await?; } @@ -503,17 +516,19 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts1"); - let mut accounts1 = Accounts::new(p.clone()).await.unwrap(); - accounts1.add_account().await.unwrap(); + { + let mut accounts = Accounts::new(p.clone()).await.unwrap(); + accounts.add_account().await.unwrap(); - let accounts2 = Accounts::open(p).await.unwrap(); + assert_eq!(accounts.accounts.len(), 1); + assert_eq!(accounts.config.get_selected_account(), 1); + } + { + let accounts = Accounts::open(p).await.unwrap(); - assert_eq!(accounts1.accounts.len(), 1); - assert_eq!(accounts1.config.get_selected_account(), 1); - - assert_eq!(accounts1.dir, accounts2.dir); - assert_eq!(accounts1.config, accounts2.config,); - assert_eq!(accounts1.accounts.len(), accounts2.accounts.len()); + assert_eq!(accounts.accounts.len(), 1); + assert_eq!(accounts.config.get_selected_account(), 1); + } } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/src/authres.rs b/src/authres.rs index 41e45e8cd..cc684e544 100644 --- a/src/authres.rs +++ b/src/authres.rs @@ -308,7 +308,7 @@ async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result, ) -> Result { + let grpname = strip_rtlo_characters(grpname); let row_id = context.sql.insert( "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);", - paramsv![ + ( chattype, - grpname, + &grpname, grpid, create_blocked, create_smeared_timestamp(context), create_protected, param.unwrap_or_default(), - ], + ), ).await?; let chat_id = ChatId::new(u32::try_from(row_id)?); info!( context, "Created group/mailinglist '{}' grpid={} as {}, blocked={}.", - grpname, + &grpname, grpid, chat_id, create_blocked, @@ -303,7 +304,7 @@ impl ChatId { "UPDATE contacts SET selfavatar_sent=? WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);", - paramsv![timestamp, self], + (timestamp, self), ) .await?; Ok(()) @@ -320,7 +321,7 @@ impl ChatId { .sql .execute( "UPDATE chats SET blocked=?1 WHERE id=?2 AND blocked != ?1", - paramsv![new_blocked, self], + (new_blocked, self), ) .await?; Ok(count > 0) @@ -434,10 +435,7 @@ impl ChatId { context .sql - .execute( - "UPDATE chats SET protected=? WHERE id=?;", - paramsv![protect, self], - ) + .execute("UPDATE chats SET protected=? WHERE id=?;", (protect, self)) .await?; context.emit_event(EventType::ChatModified(self)); @@ -523,12 +521,12 @@ impl ChatId { if visibility == ChatVisibility::Archived { transaction.execute( "UPDATE msgs SET state=? WHERE chat_id=? AND state=?;", - paramsv![MessageState::InNoticed, self, MessageState::InFresh], + (MessageState::InNoticed, self, MessageState::InFresh), )?; } transaction.execute( "UPDATE chats SET archived=? WHERE id=?;", - paramsv![visibility, self], + (visibility, self), )?; Ok(()) }) @@ -557,7 +555,7 @@ impl ChatId { .execute( "UPDATE chats SET archived=0 WHERE id=? AND archived=1 \ AND NOT(muted_until=-1 OR muted_until>?)", - paramsv![self, time()], + (self, time()), ) .await?; return Ok(()); @@ -575,7 +573,7 @@ impl ChatId { WHERE state=? AND hidden=0 AND chat_id=?", - paramsv![MessageState::InFresh, self], + (MessageState::InFresh, self), ) .await?; if unread_cnt == 1 { @@ -586,7 +584,7 @@ impl ChatId { } context .sql - .execute("UPDATE chats SET archived=0 WHERE id=?", paramsv![self]) + .execute("UPDATE chats SET archived=0 WHERE id=?", (self,)) .await?; Ok(()) } @@ -615,26 +613,23 @@ impl ChatId { .sql .execute( "DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);", - paramsv![self], + (self,), ) .await?; context .sql - .execute("DELETE FROM msgs WHERE chat_id=?;", paramsv![self]) + .execute("DELETE FROM msgs WHERE chat_id=?;", (self,)) .await?; context .sql - .execute( - "DELETE FROM chats_contacts WHERE chat_id=?;", - paramsv![self], - ) + .execute("DELETE FROM chats_contacts WHERE chat_id=?;", (self,)) .await?; context .sql - .execute("DELETE FROM chats WHERE id=?;", paramsv![self]) + .execute("DELETE FROM chats WHERE id=?;", (self,)) .await?; context.emit_msgs_changed_without_ids(); @@ -690,7 +685,7 @@ impl ChatId { .sql .query_get_value( "SELECT id FROM msgs WHERE chat_id=? AND state=?;", - paramsv![self, MessageState::OutDraft], + (self, MessageState::OutDraft), ) .await?; Ok(msg_id) @@ -772,14 +767,14 @@ impl ChatId { "UPDATE msgs SET timestamp=?,type=?,txt=?, param=?,mime_in_reply_to=? WHERE id=?;", - paramsv![ + ( time(), msg.viewtype, msg.text.as_deref().unwrap_or(""), msg.param.to_string(), msg.in_reply_to.as_deref().unwrap_or_default(), - msg.id - ], + msg.id, + ), ) .await?; return Ok(true); @@ -803,7 +798,7 @@ impl ChatId { hidden, mime_in_reply_to) VALUES (?,?,?, ?,?,?,?,?,?);", - paramsv![ + ( self, ContactId::SELF, time(), @@ -813,7 +808,7 @@ impl ChatId { msg.param.to_string(), 1, msg.in_reply_to.as_deref().unwrap_or_default(), - ], + ), ) .await?; msg.id = MsgId::new(row_id.try_into()?); @@ -826,7 +821,7 @@ impl ChatId { .sql .count( "SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id=?", - paramsv![self], + (self,), ) .await?; Ok(count) @@ -869,7 +864,7 @@ impl ChatId { WHERE state=? AND hidden=0 AND chat_id=?;", - paramsv![MessageState::InFresh, self], + (MessageState::InFresh, self), ) .await? }; @@ -879,7 +874,7 @@ impl ChatId { pub(crate) async fn get_param(self, context: &Context) -> Result { let res: Option = context .sql - .query_get_value("SELECT param FROM chats WHERE id=?", paramsv![self]) + .query_get_value("SELECT param FROM chats WHERE id=?", (self,)) .await?; Ok(res .map(|s| s.parse().unwrap_or_default()) @@ -924,13 +919,13 @@ impl ChatId { let row = sql .query_row_optional( &query, - paramsv![ + ( self, MessageState::OutPreparing, MessageState::OutDraft, MessageState::OutPending, - MessageState::OutFailed - ], + MessageState::OutFailed, + ), f, ) .await?; @@ -1052,10 +1047,7 @@ impl ChatId { pub async fn get_gossiped_timestamp(self, context: &Context) -> Result { let timestamp: Option = context .sql - .query_get_value( - "SELECT gossiped_timestamp FROM chats WHERE id=?;", - paramsv![self], - ) + .query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,)) .await?; Ok(timestamp.unwrap_or_default()) } @@ -1078,7 +1070,7 @@ impl ChatId { .sql .execute( "UPDATE chats SET gossiped_timestamp=? WHERE id=?;", - paramsv![timestamp, self], + (timestamp, self), ) .await?; @@ -1174,7 +1166,7 @@ impl Chat { c.blocked, c.locations_send_until, c.muted_until, c.protected FROM chats c WHERE c.id=?;", - paramsv![chat_id], + (chat_id,), |row| { let c = Chat { id: chat_id, @@ -1288,7 +1280,7 @@ impl Chat { .sql .execute( "UPDATE chats SET param=? WHERE id=?", - paramsv![self.param.to_string(), self.id], + (self.param.to_string(), self.id), ) .await?; Ok(()) @@ -1470,7 +1462,7 @@ impl Chat { .sql .query_get_value( "SELECT contact_id FROM chats_contacts WHERE chat_id=?;", - paramsv![self.id], + (self.id,), ) .await? { @@ -1551,13 +1543,13 @@ impl Chat { "INSERT INTO locations \ (timestamp,from_id,chat_id, latitude,longitude,independent)\ VALUES (?,?,?, ?,?,1);", - paramsv![ + ( timestamp, ContactId::SELF, self.id, msg.param.get_float(Param::SetLatitude).unwrap_or_default(), msg.param.get_float(Param::SetLongitude).unwrap_or_default(), - ], + ), ) .await { @@ -1603,7 +1595,7 @@ impl Chat { mime_headers=?, mime_compressed=1, location_id=?, ephemeral_timer=?, ephemeral_timestamp=? WHERE id=?;", - paramsv![ + params_slice![ new_rfc724_mid, self.id, ContactId::SELF, @@ -1652,7 +1644,7 @@ impl Chat { ephemeral_timer, ephemeral_timestamp) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);", - paramsv![ + params_slice![ new_rfc724_mid, self.id, ContactId::SELF, @@ -1674,6 +1666,7 @@ impl Chat { ], ) .await?; + context.new_msgs_notify.notify_one(); msg.id = MsgId::new(u32::try_from(raw_id)?); maybe_set_logging_xdc(context, msg, self.id).await?; @@ -1863,7 +1856,7 @@ async fn update_special_chat_name( .sql .execute( "UPDATE chats SET name=? WHERE id=? AND name!=?", - paramsv![name, chat_id, name], + (&name, chat_id, &name), ) .await?; } @@ -1926,7 +1919,7 @@ impl ChatIdBlocked { WHERE c.type=100 -- 100 = Chattype::Single AND c.id>9 -- 9 = DC_CHAT_ID_LAST_SPECIAL AND j.contact_id=?;", - paramsv![contact_id], + (contact_id,), |row| { let id: ChatId = row.get(0)?; let blocked: Blocked = row.get(1)?; @@ -1977,13 +1970,13 @@ impl ChatIdBlocked { "INSERT INTO chats (type, name, param, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?)", - params![ + ( Chattype::Single, chat_name, params.to_string(), create_blocked as u8, - create_smeared_timestamp(context) - ], + create_smeared_timestamp(context), + ), )?; let chat_id = ChatId::new( transaction @@ -1996,7 +1989,7 @@ impl ChatIdBlocked { "INSERT INTO chats_contacts (chat_id, contact_id) VALUES((SELECT last_insert_rowid()), ?)", - params![contact_id], + (contact_id,), )?; Ok(chat_id) @@ -2156,7 +2149,7 @@ pub async fn is_contact_in_chat( .sql .exists( "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;", - paramsv![chat_id, contact_id], + (chat_id, contact_id), ) .await?; Ok(exists) @@ -2208,6 +2201,13 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message } async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result { + // protect all system messages againts RTLO attacks + if msg.is_system_message() { + if let Some(text) = &msg.text { + msg.text = Some(strip_rtlo_characters(text.as_ref())); + } + } + if prepare_send_msg(context, chat_id, msg).await?.is_some() { context.emit_msgs_changed(msg.chat_id, msg.id); @@ -2370,12 +2370,12 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result Result<()> .sql .exists( "SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;", - paramsv![MessageState::InFresh, chat_id], + (MessageState::InFresh, chat_id), ) .await?; if !exists { @@ -2637,7 +2637,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> WHERE state=? AND hidden=0 AND chat_id=?;", - paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id], + (MessageState::InNoticed, MessageState::InFresh, chat_id), ) .await?; } @@ -2686,12 +2686,12 @@ pub(crate) async fn mark_old_messages_as_noticed( AND hidden=0 AND chat_id=? AND timestamp<=?;", - paramsv![ + ( MessageState::InNoticed, MessageState::InFresh, msg.chat_id, - msg.sort_timestamp - ], + msg.sort_timestamp, + ), )?; if changed_rows > 0 { changed_chats.push(msg.chat_id); @@ -2739,7 +2739,7 @@ pub async fn get_chat_media( AND (type=? OR type=? OR type=?) AND hidden=0 ORDER BY timestamp, id;", - paramsv![ + ( chat_id.is_none(), chat_id.unwrap_or_else(|| ChatId::new(0)), DC_CHAT_ID_TRASH, @@ -2754,7 +2754,7 @@ pub async fn get_chat_media( } else { msg_type }, - ], + ), |row| row.get::<_, MsgId>(0), |ids| Ok(ids.flatten().collect()), ) @@ -2832,7 +2832,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result(0), |ids| ids.collect::, _>>().map_err(Into::into), ) @@ -2858,12 +2858,12 @@ pub async fn create_group_chat( "INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);", - paramsv![ + ( Chattype::Group, chat_name, grpid, create_smeared_timestamp(context), - ], + ), ) .await?; @@ -2896,7 +2896,7 @@ async fn find_unused_broadcast_list_name(context: &Context) -> Result { .sql .exists( "SELECT COUNT(*) FROM chats WHERE type=? AND name=?;", - paramsv![Chattype::Broadcast, better_name], + (Chattype::Broadcast, &better_name), ) .await? { @@ -2916,12 +2916,12 @@ pub async fn create_broadcast_list(context: &Context) -> Result { "INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);", - paramsv![ + ( Chattype::Broadcast, chat_name, grpid, create_smeared_timestamp(context), - ], + ), ) .await?; let chat_id = ChatId::new(u32::try_from(row_id)?); @@ -2942,7 +2942,7 @@ pub(crate) async fn add_to_chat_contacts_table( for contact_id in contact_ids { transaction.execute( "INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)", - paramsv![chat_id, contact_id], + (chat_id, contact_id), )?; } Ok(()) @@ -2962,7 +2962,7 @@ pub(crate) async fn remove_from_chat_contacts_table( .sql .execute( "DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?", - paramsv![chat_id, contact_id], + (chat_id, contact_id), ) .await?; Ok(()) @@ -3080,7 +3080,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) FROM chats_contacts cc LEFT JOIN contacts c ON c.id=cc.contact_id WHERE cc.chat_id=? AND cc.contact_id!=?;", - paramsv![chat_id, ContactId::SELF], + (chat_id, ContactId::SELF), |row| Ok(row.get::<_, i64>(0)), |rows| { let mut needs_attach = false; @@ -3153,7 +3153,7 @@ pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuratio .sql .execute( "UPDATE chats SET muted_until=? WHERE id=?;", - paramsv![duration, chat_id], + (duration, chat_id), ) .await .context(format!("Failed to set mute duration for {chat_id}"))?; @@ -3240,10 +3240,7 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()> if !is_group_explicitly_left(context, grpid).await? { context .sql - .execute( - "INSERT INTO leftgrps (grpid) VALUES(?);", - paramsv![grpid.to_string()], - ) + .execute("INSERT INTO leftgrps (grpid) VALUES(?);", (grpid,)) .await?; } @@ -3253,10 +3250,7 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()> pub(crate) async fn is_group_explicitly_left(context: &Context, grpid: &str) -> Result { let exists = context .sql - .exists( - "SELECT COUNT(*) FROM leftgrps WHERE grpid=?;", - paramsv![grpid], - ) + .exists("SELECT COUNT(*) FROM leftgrps WHERE grpid=?;", (grpid,)) .await?; Ok(exists) } @@ -3288,7 +3282,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) - .sql .execute( "UPDATE chats SET name=? WHERE id=?;", - paramsv![new_name.to_string(), chat_id], + (new_name.to_string(), chat_id), ) .await?; if chat.is_promoted() @@ -3541,7 +3535,7 @@ pub(crate) async fn get_chat_id_by_grpid( .sql .query_row_optional( "SELECT id, blocked, protected FROM chats WHERE grpid=?;", - paramsv![grpid], + (grpid,), |row| { let chat_id = row.get::<_, ChatId>(0)?; @@ -3595,7 +3589,7 @@ pub async fn add_device_msg_with_importance( .sql .query_get_value( "SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", - paramsv![chat_id], + (chat_id,), ) .await? { @@ -3620,7 +3614,7 @@ pub async fn add_device_msg_with_importance( param, rfc724_mid) VALUES (?,?,?,?,?,?,?,?,?,?,?);", - paramsv![ + ( chat_id, ContactId::DEVICE, ContactId::SELF, @@ -3632,9 +3626,10 @@ pub async fn add_device_msg_with_importance( msg.text.as_ref().cloned().unwrap_or_default(), msg.param.to_string(), rfc724_mid, - ], + ), ) .await?; + context.new_msgs_notify.notify_one(); msg_id = MsgId::new(u32::try_from(row_id)?); if !msg.hidden { @@ -3645,10 +3640,7 @@ pub async fn add_device_msg_with_importance( if let Some(label) = label { context .sql - .execute( - "INSERT INTO devmsglabels (label) VALUES (?);", - paramsv![label.to_string()], - ) + .execute("INSERT INTO devmsglabels (label) VALUES (?);", (label,)) .await?; } @@ -3675,7 +3667,7 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result .sql .exists( "SELECT COUNT(label) FROM devmsglabels WHERE label=?", - paramsv![label], + (label,), ) .await?; @@ -3692,10 +3684,7 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<()> { context .sql - .execute( - "DELETE FROM msgs WHERE from_id=?;", - paramsv![ContactId::DEVICE], - ) + .execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,)) .await?; context.sql.execute("DELETE FROM devmsglabels;", ()).await?; @@ -3738,7 +3727,7 @@ pub(crate) async fn add_info_msg_with_cmd( context.sql.insert( "INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,rfc724_mid,ephemeral_timer, param,mime_in_reply_to) VALUES (?,?,?, ?,?,?,?,?, ?,?,?, ?,?);", - paramsv![ + ( chat_id, from_id.unwrap_or(ContactId::INFO), ContactId::INFO, @@ -3752,8 +3741,9 @@ pub(crate) async fn add_info_msg_with_cmd( ephemeral_timer, param.to_string(), parent.map(|msg|msg.rfc724_mid.clone()).unwrap_or_default() - ] + ) ).await?; + context.new_msgs_notify.notify_one(); let msg_id = MsgId::new(row_id.try_into()?); context.emit_msgs_changed(chat_id, msg_id); @@ -3792,7 +3782,7 @@ pub(crate) async fn update_msg_text_and_timestamp( .sql .execute( "UPDATE msgs SET txt=?, timestamp=? WHERE id=?;", - paramsv![text, timestamp, msg_id], + (text, timestamp, msg_id), ) .await?; context.emit_msgs_changed(chat_id, msg_id); @@ -3808,6 +3798,7 @@ mod tests { use crate::message::delete_msgs; use crate::receive_imf::receive_imf; use crate::test_utils::TestContext; + use tokio::fs; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_chat_info() { @@ -6107,4 +6098,30 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_blob_renaming() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + add_contact_to_chat( + &alice, + chat_id, + Contact::create(&alice, "bob", "bob@example.net").await?, + ) + .await?; + let dir = tempfile::tempdir()?; + let file = dir.path().join("harmless_file.\u{202e}txt.exe"); + fs::write(&file, "aaa").await?; + let mut msg = Message::new(Viewtype::File); + msg.set_file(file.to_str().unwrap(), None); + let msg = bob.recv_msg(&alice.send_msg(chat_id, &mut msg).await).await; + + // the file bob receives should not contain BIDI-control characters + assert_eq!( + Some("$BLOBDIR/harmless_file.txt.exe"), + msg.param.get(Param::File), + ); + Ok(()) + } } diff --git a/src/chatlist.rs b/src/chatlist.rs index 038c061a6..ddd60c072 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -137,7 +137,7 @@ impl Chatlist { AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2) GROUP BY c.id ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - paramsv![MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned], + (MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned), process_row, process_rows, ).await? @@ -164,7 +164,7 @@ impl Chatlist { AND c.archived=1 GROUP BY c.id ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - paramsv![MessageState::OutDraft], + (MessageState::OutDraft,), process_row, process_rows, ) @@ -198,7 +198,7 @@ impl Chatlist { AND c.name LIKE ?3 GROUP BY c.id ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - paramsv![MessageState::OutDraft, skip_id, str_like_cmd], + (MessageState::OutDraft, skip_id, str_like_cmd), process_row, process_rows, ) @@ -228,7 +228,7 @@ impl Chatlist { AND NOT c.archived=?4 GROUP BY c.id ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - paramsv![MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned], + (MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned), process_row, process_rows, ).await?; @@ -356,7 +356,7 @@ pub async fn get_archived_cnt(context: &Context) -> Result { .sql .count( "SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;", - paramsv![Blocked::Yes, ChatVisibility::Archived], + (Blocked::Yes, ChatVisibility::Archived), ) .await?; Ok(count) diff --git a/src/config.rs b/src/config.rs index 035caf314..3527e0096 100644 --- a/src/config.rs +++ b/src/config.rs @@ -308,6 +308,9 @@ pub enum Config { /// This value is used internally to remember the MsgId of the logging xdc #[strum(props(default = "0"))] DebugLogging, + + /// Last message processed by the bot. + LastMsgId, } impl Context { @@ -358,6 +361,11 @@ impl Context { Ok(self.get_config_parsed(key).await?.unwrap_or_default()) } + /// Returns 32-bit unsigned integer configuration value for the given key. + pub async fn get_config_u32(&self, key: Config) -> Result { + Ok(self.get_config_parsed(key).await?.unwrap_or_default()) + } + /// Returns 64-bit signed integer configuration value for the given key. pub async fn get_config_i64(&self, key: Config) -> Result { Ok(self.get_config_parsed(key).await?.unwrap_or_default()) @@ -459,6 +467,12 @@ impl Context { Ok(()) } + /// Set the given config to an unsigned 32-bit integer value. + pub async fn set_config_u32(&self, key: Config, value: u32) -> Result<()> { + self.set_config(key, Some(&value.to_string())).await?; + 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") }) diff --git a/src/contact.rs b/src/contact.rs index 07430d308..78269f003 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -32,7 +32,10 @@ use crate::mimeparser::AvatarAction; use crate::param::{Param, Params}; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::sql::{self, params_iter}; -use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, EmailAddress}; +use crate::tools::{ + duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time, + EmailAddress, +}; use crate::{chat, stock_str}; /// Time during which a contact is considered as seen recently. @@ -344,7 +347,7 @@ impl Contact { c.authname, c.param, c.status FROM contacts c WHERE c.id=?;", - paramsv![contact_id], + (contact_id,), |row| { let name: String = row.get(0)?; let addr: String = row.get(1)?; @@ -457,7 +460,7 @@ impl Contact { .sql .execute( "UPDATE msgs SET state=? WHERE from_id=? AND state=?;", - paramsv![MessageState::InNoticed, id, MessageState::InFresh], + (MessageState::InNoticed, id, MessageState::InFresh), ) .await?; Ok(()) @@ -490,7 +493,7 @@ impl Contact { "SELECT id FROM contacts \ WHERE addr=?1 COLLATE NOCASE \ AND id>?2 AND origin>=?3 AND blocked=0;", - paramsv![addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32,], + (&addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32), ) .await?; Ok(id) @@ -536,7 +539,7 @@ impl Contact { return Ok((ContactId::SELF, sth_modified)); } - let mut name = name; + let mut name = strip_rtlo_characters(name); #[allow(clippy::collapsible_if)] if origin <= Origin::OutgoingTo { // The user may accidentally have written to a "noreply" address with another MUA: @@ -551,7 +554,7 @@ impl Contact { // For these kind of email addresses, sender and address often don't belong together // (like hocuri ). In this example, hocuri shouldn't // be saved as the displayname for notifications@github.com. - name = ""; + name = "".to_string(); } } @@ -605,7 +608,7 @@ impl Contact { transaction .execute( "UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;", - paramsv![ + ( new_name, if update_addr { addr.to_string() @@ -623,7 +626,7 @@ impl Contact { row_authname }, row_id - ], + ), )?; if update_name || update_authname { @@ -631,7 +634,7 @@ impl Contact { // This is one of the few duplicated data, however, getting the chat list is easier this way. let chat_id: Option = transaction.query_row( "SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)", - params![Chattype::Single, isize::try_from(row_id)?], + (Chattype::Single, isize::try_from(row_id)?), |row| { let chat_id: ChatId = row.get(0)?; Ok(chat_id) @@ -645,7 +648,7 @@ impl Contact { "SELECT addr, name, authname FROM contacts WHERE id=?", - params![contact_id], + (contact_id,), |row| { let addr: String = row.get(0)?; let name: String = row.get(1)?; @@ -663,7 +666,7 @@ impl Contact { let count = transaction.execute( "UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1", - params![chat_name, chat_id])?; + (chat_name, chat_id))?; if count > 0 { // Chat name updated @@ -681,7 +684,7 @@ impl Contact { .execute( "INSERT INTO contacts (name, addr, origin, authname) VALUES (?, ?, ?, ?);", - params![ + ( if update_name { name.to_string() } else { @@ -694,7 +697,7 @@ impl Contact { } else { "".to_string() } - ], + ), )?; sth_modified = Modifier::Created; @@ -795,12 +798,12 @@ impl Contact { ORDER BY c.last_seen DESC, c.id DESC;", sql::repeat_vars(self_addrs.len()) ), - rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![ + rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![ ContactId::LAST_SPECIAL, Origin::IncomingReplyTo, s3str_like_cmd, s3str_like_cmd, - if flag_verified_only { 0i32 } else { 1i32 }, + if flag_verified_only { 0i32 } else { 1i32 } ])), |row| row.get::<_, ContactId>(0), |ids| { @@ -847,7 +850,7 @@ impl Contact { ORDER BY last_seen DESC, id DESC;", sql::repeat_vars(self_addrs.len()) ), - rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![ + rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![ ContactId::LAST_SPECIAL, Origin::IncomingReplyTo ])), @@ -880,7 +883,7 @@ impl Contact { .transaction(move |transaction| { let mut stmt = transaction .prepare("SELECT name, grpid FROM chats WHERE type=? AND blocked=?")?; - let rows = stmt.query_map(params![Chattype::Mailinglist, Blocked::Yes], |row| { + let rows = stmt.query_map((Chattype::Mailinglist, Blocked::Yes), |row| { let name: String = row.get(0)?; let grpid: String = row.get(1)?; Ok((name, grpid)) @@ -902,7 +905,7 @@ impl Contact { // Always do an update in case the blocking is reset or name is changed. transaction.execute( "UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?", - params![&name, Origin::MailinglistAddress, &grpid], + (&name, Origin::MailinglistAddress, &grpid), )?; } Ok(()) @@ -917,7 +920,7 @@ impl Contact { .sql .count( "SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0", - paramsv![ContactId::LAST_SPECIAL], + (ContactId::LAST_SPECIAL,), ) .await?; Ok(count) @@ -933,7 +936,7 @@ impl Contact { .sql .query_map( "SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;", - paramsv![ContactId::LAST_SPECIAL], + (ContactId::LAST_SPECIAL,), |row| row.get::<_, ContactId>(0), |ids| { ids.collect::, _>>() @@ -1027,12 +1030,12 @@ impl Contact { let deleted_contacts = transaction.execute( "DELETE FROM contacts WHERE id=? AND (SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?)=0;", - paramsv![contact_id, contact_id], + (contact_id, contact_id), )?; if deleted_contacts == 0 { transaction.execute( "UPDATE contacts SET origin=? WHERE id=?;", - paramsv![Origin::Hidden, contact_id], + (Origin::Hidden, contact_id), )?; } Ok(()) @@ -1060,7 +1063,7 @@ impl Contact { .sql .execute( "UPDATE contacts SET param=? WHERE id=?", - paramsv![self.param.to_string(), self.id], + (self.param.to_string(), self.id), ) .await?; Ok(()) @@ -1072,7 +1075,7 @@ impl Contact { .sql .execute( "UPDATE contacts SET status=? WHERE id=?", - paramsv![self.status, self.id], + (&self.status, self.id), ) .await?; Ok(()) @@ -1192,9 +1195,7 @@ impl Contact { if peerstate.verified_key.is_some() { return Ok(VerifiedStatus::BidirectVerified); } - } - - if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? { + } else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? { if peerstate.verified_key.is_some() { return Ok(VerifiedStatus::BidirectVerified); } @@ -1230,7 +1231,7 @@ impl Contact { .sql .count( "SELECT COUNT(*) FROM contacts WHERE id>?;", - paramsv![ContactId::LAST_SPECIAL], + (ContactId::LAST_SPECIAL,), ) .await?; Ok(count) @@ -1244,10 +1245,7 @@ impl Contact { let exists = context .sql - .exists( - "SELECT COUNT(*) FROM contacts WHERE id=?;", - paramsv![contact_id], - ) + .exists("SELECT COUNT(*) FROM contacts WHERE id=?;", (contact_id,)) .await?; Ok(exists) } @@ -1262,7 +1260,7 @@ impl Contact { .sql .execute( "UPDATE contacts SET origin=? WHERE id=? AND origin (String, String) { if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) { ( if name.is_empty() { - captures - .get(1) - .map_or("".to_string(), |m| normalize_name(m.as_str())) + strip_rtlo_characters( + &captures + .get(1) + .map_or("".to_string(), |m| normalize_name(m.as_str())), + ) } else { - name.to_string() + strip_rtlo_characters(name) }, captures .get(2) .map_or("".to_string(), |m| m.as_str().to_string()), ) } else { - (name.to_string(), addr.to_string()) + (strip_rtlo_characters(name), addr.to_string()) } } @@ -1324,7 +1324,7 @@ async fn set_block_contact( .sql .execute( "UPDATE contacts SET blocked=? WHERE id=?;", - paramsv![i32::from(new_blocking), contact_id], + (i32::from(new_blocking), contact_id), ) .await?; @@ -1343,7 +1343,7 @@ WHERE type=? AND id IN ( SELECT chat_id FROM chats_contacts WHERE contact_id=? ); "#, - paramsv![new_blocking, Chattype::Single, contact_id], + (new_blocking, Chattype::Single, contact_id), ) .await .is_ok() @@ -1460,7 +1460,7 @@ pub(crate) async fn update_last_seen( .sql .execute( "UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2", - paramsv![timestamp, contact_id], + (timestamp, contact_id), ) .await? > 0 @@ -1489,7 +1489,7 @@ pub fn normalize_name(full_name: &str) -> String { match full_name.as_bytes() { [b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name .get(1..full_name.len() - 1) - .map_or("".to_string(), |s| s.trim().into()), + .map_or("".to_string(), |s| s.trim().to_string()), _ => full_name.to_string(), } } @@ -1575,7 +1575,7 @@ impl RecentlySeenLoop { .query_map( "SELECT id, last_seen FROM contacts WHERE last_seen > ?", - paramsv![time() - SEEN_RECENTLY_SECONDS], + (time() - SEEN_RECENTLY_SECONDS,), |row| { let contact_id: ContactId = row.get("id")?; let last_seen: i64 = row.get("last_seen")?; diff --git a/src/context.rs b/src/context.rs index 59879a179..351a8898f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -14,7 +14,7 @@ use std::time::{Duration, Instant, SystemTime}; use anyhow::{bail, ensure, Context as _, Result}; use async_channel::Sender; use ratelimit::Ratelimit; -use tokio::sync::{oneshot, Mutex, RwLock}; +use tokio::sync::{oneshot, Mutex, Notify, RwLock}; use tokio::task; use crate::chat::{get_chat_cnt, ChatId}; @@ -221,6 +221,11 @@ pub struct InnerContext { /// IMAP UID resync request. pub(crate) resync_request: AtomicBool, + /// Notify about new messages. + /// + /// This causes [`Context::wait_next_msgs`] to wake up. + pub(crate) new_msgs_notify: Notify, + /// Server ID response if ID capability is supported /// and the server returned non-NIL on the inbox connection. /// @@ -251,8 +256,8 @@ pub(crate) struct DebugLogging { pub(crate) msg_id: MsgId, /// Handle to the background task responsible for sending pub(crate) loop_handle: task::JoinHandle<()>, - /// Channel that log events should be send to - /// A background loop will receive and handle them + /// Channel that log events should be sent to. + /// A background loop will receive and handle them. pub(crate) sender: Sender, } @@ -366,6 +371,11 @@ impl Context { blobdir.display() ); + let new_msgs_notify = Notify::new(); + // Notify once immediately to allow processing old messages + // without starting I/O. + new_msgs_notify.notify_one(); + let inner = InnerContext { id, blobdir, @@ -382,6 +392,7 @@ impl Context { quota: RwLock::new(None), quota_update_request: AtomicBool::new(false), resync_request: AtomicBool::new(false), + new_msgs_notify, server_id: RwLock::new(None), creation_time: std::time::SystemTime::now(), last_full_folder_scan: Mutex::new(None), @@ -782,6 +793,10 @@ impl Context { "debug_logging", self.get_config_int(Config::DebugLogging).await?.to_string(), ); + res.insert( + "last_msg_id", + self.get_config_int(Config::LastMsgId).await?.to_string(), + ); let elapsed = self.creation_time.elapsed(); res.insert("uptime", duration_to_str(elapsed.unwrap_or_default())); @@ -814,7 +829,7 @@ impl Context { " AND NOT(c.muted_until=-1 OR c.muted_until>?)", " ORDER BY m.timestamp DESC,m.id DESC;" ), - paramsv![MessageState::InFresh, time()], + (MessageState::InFresh, time()), |row| row.get::<_, MsgId>(0), |rows| { let mut list = Vec::new(); @@ -828,6 +843,66 @@ impl Context { Ok(list) } + /// Returns a list of messages with database ID higher than requested. + /// + /// Blocked contacts and chats are excluded, + /// but self-sent messages and contact requests are included in the results. + pub async fn get_next_msgs(&self) -> Result> { + let last_msg_id = match self.get_config(Config::LastMsgId).await? { + Some(s) => MsgId::new(s.parse()?), + None => MsgId::new_unset(), + }; + + let list = self + .sql + .query_map( + "SELECT m.id + FROM msgs m + LEFT JOIN contacts ct + ON m.from_id=ct.id + LEFT JOIN chats c + ON m.chat_id=c.id + WHERE m.id>? + AND m.hidden=0 + AND m.chat_id>9 + AND ct.blocked=0 + AND c.blocked!=1 + ORDER BY m.id ASC", + ( + last_msg_id.to_u32(), // Explicitly convert to u32 because 0 is allowed. + ), + |row| { + let msg_id: MsgId = row.get(0)?; + Ok(msg_id) + }, + |rows| { + let mut list = Vec::new(); + for row in rows { + list.push(row?); + } + Ok(list) + }, + ) + .await?; + Ok(list) + } + + /// Returns a list of messages with database ID higher than last marked as seen. + /// + /// This function is supposed to be used by bot to request messages + /// that are not processed yet. + /// + /// Waits for notification and returns a result. + /// Note that the result may be empty if the message is deleted + /// shortly after notification or notification is manually triggered + /// to interrupt waiting. + /// Notification may be manually triggered by calling [`Self::stop_io`]. + pub async fn wait_next_msgs(&self) -> Result> { + self.new_msgs_notify.notified().await; + let list = self.get_next_msgs().await?; + Ok(list) + } + /// Searches for messages containing the query string. /// /// If `chat_id` is provided this searches only for messages in this chat, if `chat_id` @@ -839,24 +914,10 @@ impl Context { } let str_like_in_text = format!("%{real_query}%"); - let do_query = |query, params| { - self.sql.query_map( - query, - params, - |row| row.get::<_, MsgId>("id"), - |rows| { - let mut ret = Vec::new(); - for id in rows { - ret.push(id?); - } - Ok(ret) - }, - ) - }; - let list = if let Some(chat_id) = chat_id { - do_query( - "SELECT m.id AS id + self.sql + .query_map( + "SELECT m.id AS id FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id @@ -865,9 +926,17 @@ impl Context { AND ct.blocked=0 AND txt LIKE ? ORDER BY m.timestamp,m.id;", - paramsv![chat_id, str_like_in_text], - ) - .await? + (chat_id, str_like_in_text), + |row| row.get::<_, MsgId>("id"), + |rows| { + let mut ret = Vec::new(); + for id in rows { + ret.push(id?); + } + Ok(ret) + }, + ) + .await? } else { // For performance reasons results are sorted only by `id`, that is in the order of // message reception. @@ -879,8 +948,9 @@ impl Context { // of unwanted results that are discarded moments later, we added `LIMIT 1000`. // According to some tests, this limit speeds up eg. 2 character searches by factor 10. // The limit is documented and UI may add a hint when getting 1000 results. - do_query( - "SELECT m.id AS id + self.sql + .query_map( + "SELECT m.id AS id FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id @@ -892,9 +962,17 @@ impl Context { AND ct.blocked=0 AND m.txt LIKE ? ORDER BY m.id DESC LIMIT 1000", - paramsv![str_like_in_text], - ) - .await? + (str_like_in_text,), + |row| row.get::<_, MsgId>("id"), + |rows| { + let mut ret = Vec::new(); + for id in rows { + ret.push(id?); + } + Ok(ret) + }, + ) + .await? }; Ok(list) @@ -1152,7 +1230,7 @@ mod tests { t.sql .execute( "UPDATE chats SET muted_until=? WHERE id=?;", - paramsv![time() - 3600, bob.id], + (time() - 3600, bob.id), ) .await .unwrap(); @@ -1169,10 +1247,7 @@ mod tests { // to test get_fresh_msgs() with invalid mute_until (everything < -1), // that results in "muted forever" by definition. t.sql - .execute( - "UPDATE chats SET muted_until=-2 WHERE id=?;", - paramsv![bob.id], - ) + .execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,)) .await .unwrap(); let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); @@ -1521,4 +1596,38 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_get_next_msgs() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let alice_chat = alice.create_chat(&bob).await; + + assert!(alice.get_next_msgs().await?.is_empty()); + assert!(bob.get_next_msgs().await?.is_empty()); + + let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await; + let received_msg = bob.recv_msg(&sent_msg).await; + + let bob_next_msg_ids = bob.get_next_msgs().await?; + assert_eq!(bob_next_msg_ids.len(), 1); + assert_eq!(bob_next_msg_ids.get(0), Some(&received_msg.id)); + + bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32()) + .await?; + assert!(bob.get_next_msgs().await?.is_empty()); + + // Next messages include self-sent messages. + let alice_next_msg_ids = alice.get_next_msgs().await?; + assert_eq!(alice_next_msg_ids.len(), 1); + assert_eq!(alice_next_msg_ids.get(0), Some(&sent_msg.sender_msg_id)); + + alice + .set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32()) + .await?; + assert!(alice.get_next_msgs().await?.is_empty()); + + Ok(()) + } } diff --git a/src/download.rs b/src/download.rs index 89c99f011..cc60e873d 100644 --- a/src/download.rs +++ b/src/download.rs @@ -101,7 +101,7 @@ impl MsgId { .sql .execute( "UPDATE msgs SET download_state=? WHERE id=?;", - paramsv![download_state, self], + (download_state, self), ) .await?; context.emit_event(EventType::MsgsChanged { @@ -134,7 +134,7 @@ impl Job { .sql .query_row_optional( "SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target=folder", - paramsv![msg.rfc724_mid], + (&msg.rfc724_mid,), |row| { let server_uid: u32 = row.get(0)?; let server_folder: String = row.get(1)?; diff --git a/src/ephemeral.rs b/src/ephemeral.rs index 55bc6b76d..22c3b6303 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -174,10 +174,7 @@ impl ChatId { pub async fn get_ephemeral_timer(self, context: &Context) -> Result { let timer = context .sql - .query_get_value( - "SELECT ephemeral_timer FROM chats WHERE id=?;", - paramsv![self], - ) + .query_get_value("SELECT ephemeral_timer FROM chats WHERE id=?;", (self,)) .await?; Ok(timer.unwrap_or_default()) } @@ -199,7 +196,7 @@ impl ChatId { "UPDATE chats SET ephemeral_timer=? WHERE id=?;", - paramsv![timer, self], + (timer, self), ) .await?; @@ -291,10 +288,7 @@ impl MsgId { pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result { let res = match context .sql - .query_get_value( - "SELECT ephemeral_timer FROM msgs WHERE id=?", - paramsv![self], - ) + .query_get_value("SELECT ephemeral_timer FROM msgs WHERE id=?", (self,)) .await? { None | Some(0) => Timer::Disabled, @@ -314,7 +308,7 @@ impl MsgId { "UPDATE msgs SET ephemeral_timestamp = ? \ WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \ AND id = ?", - paramsv![ephemeral_timestamp, ephemeral_timestamp, self], + (ephemeral_timestamp, ephemeral_timestamp, self), ) .await?; context.scheduler.interrupt_ephemeral_task().await; @@ -338,8 +332,8 @@ pub(crate) async fn start_ephemeral_timers_msgids( sql::repeat_vars(msg_ids.len()) ), rusqlite::params_from_iter( - std::iter::once(&now as &dyn crate::ToSql) - .chain(std::iter::once(&now as &dyn crate::ToSql)) + std::iter::once(&now as &dyn crate::sql::ToSql) + .chain(std::iter::once(&now as &dyn crate::sql::ToSql)) .chain(params_iter(msg_ids)), ), ) @@ -369,7 +363,7 @@ WHERE AND ephemeral_timestamp <= ? AND chat_id != ? "#, - paramsv![now, DC_CHAT_ID_TRASH], + (now, DC_CHAT_ID_TRASH), |row| { let id: MsgId = row.get("id")?; let chat_id: ChatId = row.get("chat_id")?; @@ -402,12 +396,12 @@ WHERE AND chat_id != ? AND chat_id != ? "#, - paramsv![ + ( threshold_timestamp, DC_CHAT_ID_LAST_SPECIAL, self_chat_id, - device_chat_id - ], + device_chat_id, + ), |row| { let id: MsgId = row.get("id")?; let chat_id: ChatId = row.get("chat_id")?; @@ -449,7 +443,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu SET chat_id=?, txt='', subject='', txt_raw='', mime_headers='', from_id=0, to_id=0, param='' WHERE id=?", - params![DC_CHAT_ID_TRASH, msg_id], + (DC_CHAT_ID_TRASH, msg_id), )?; msgs_changed.push((chat_id, msg_id)); @@ -494,7 +488,7 @@ async fn next_delete_device_after_timestamp(context: &Context) -> Result Option { WHERE ephemeral_timestamp != 0 AND chat_id != ?; "#, - paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them + (DC_CHAT_ID_TRASH,), // Trash contains already deleted messages, skip them ) .await { @@ -605,12 +599,12 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<() (download_state != 0 AND timestamp < ?) OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?)) )", - paramsv![ - target, + ( + &target, threshold_timestamp, threshold_timestamp_extended, now, - ], + ), ) .await?; @@ -635,12 +629,12 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> { WHERE ephemeral_timer > 0 \ AND ephemeral_timestamp = 0 \ AND state NOT IN (?, ?, ?)", - paramsv![ + ( time(), MessageState::InFresh, MessageState::InNoticed, - MessageState::OutDraft - ], + MessageState::OutDraft, + ), ) .await?; @@ -1106,7 +1100,7 @@ mod tests { assert!(msg.text.is_none_or_empty(), "{:?}", msg.text); let rawtxt: Option = t .sql - .query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id]) + .query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,)) .await .unwrap(); assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}"); @@ -1131,13 +1125,13 @@ mod tests { t.sql .execute( "INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);", - paramsv![id, message_id, timestamp, ephemeral_timestamp], + (id, &message_id, timestamp, ephemeral_timestamp), ) .await?; t.sql .execute( "INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');", - paramsv![message_id, id], + (&message_id, id), ) .await?; } @@ -1148,7 +1142,7 @@ mod tests { .sql .count( "SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?", - paramsv![id.to_string()], + (id.to_string(),), ) .await?, 1 @@ -1159,10 +1153,7 @@ mod tests { async fn remove_uid(context: &Context, id: u32) -> Result<()> { context .sql - .execute( - "DELETE FROM imap WHERE rfc724_mid=?", - paramsv![id.to_string()], - ) + .execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),)) .await?; Ok(()) } diff --git a/src/events.rs b/src/events.rs index ebb1d299f..ade9fe8db 100644 --- a/src/events.rs +++ b/src/events.rs @@ -32,7 +32,9 @@ impl Events { Self { receiver, sender } } - /// Emits an event. + /// Emits an event into event channel. + /// + /// If the channel is full, deletes the oldest event first. pub fn emit(&self, event: Event) { match self.sender.try_send(event) { Ok(()) => {} @@ -49,7 +51,7 @@ impl Events { } } - /// Retrieve the event emitter. + /// Creates an event emitter. pub fn get_emitter(&self) -> EventEmitter { EventEmitter(self.receiver.clone()) } diff --git a/src/imap.rs b/src/imap.rs index 0e5b15492..a82b45a0a 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -550,7 +550,7 @@ impl Imap { context .sql .transaction(move |transaction| { - transaction.execute("DELETE FROM imap WHERE folder=?", params![folder])?; + transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?; for (uid, (rfc724_mid, target)) in &msgs { // This may detect previously undetected moved // messages, so we update server_folder too. @@ -560,7 +560,7 @@ impl Imap { ON CONFLICT(folder, uid, uidvalidity) DO UPDATE SET rfc724_mid=excluded.rfc724_mid, target=excluded.target", - params![rfc724_mid, folder, uid, uid_validity, target], + (rfc724_mid, folder, uid, uid_validity, target), )?; } Ok(()) @@ -676,7 +676,7 @@ impl Imap { .sql .execute( "DELETE FROM imap WHERE folder=? AND uidvalidity!=?", - paramsv![folder, new_uid_validity], + (&folder, new_uid_validity), ) .await?; @@ -759,7 +759,7 @@ impl Imap { ON CONFLICT(folder, uid, uidvalidity) DO UPDATE SET rfc724_mid=excluded.rfc724_mid, target=excluded.target", - paramsv![message_id, folder, uid, uid_validity, &target], + (&message_id, &folder, uid, uid_validity, &target), ) .await?; @@ -1050,7 +1050,7 @@ impl Session { WHERE folder = ? AND target != folder ORDER BY target, uid", - paramsv![folder], + (folder,), |row| { let rowid: i64 = row.get(0)?; let uid: u32 = row.get(1)?; @@ -1740,17 +1740,37 @@ impl Session { /// If this returns `true`, this means that new emails arrived and you should /// fetch again, even if you just fetched. fn server_sent_unsolicited_exists(&self, context: &Context) -> Result { + use async_imap::imap_proto::Response; + use async_imap::imap_proto::ResponseCode; + use UnsolicitedResponse::*; + let mut unsolicited_exists = false; while let Ok(response) = self.unsolicited_responses.try_recv() { match response { - UnsolicitedResponse::Exists(_) => { + Exists(_) => { info!( context, "Need to fetch again, got unsolicited EXISTS {:?}", response ); unsolicited_exists = true; } - _ => info!(context, "ignoring unsolicited response {:?}", response), + + // We are not interested in the following responses and they are are + // sent quite frequently, so, we ignore them without logging them + Expunge(_) | Recent(_) => {} + Other(response_data) + if matches!( + response_data.parsed(), + Response::Fetch { .. } + | Response::Done { + code: Some(ResponseCode::CopyUid(_, _, _)), + .. + } + ) => {} + + _ => { + info!(context, "got unsolicited response {:?}", response) + } } } Ok(unsolicited_exists) @@ -2168,7 +2188,7 @@ async fn mark_seen_by_uid( AND uid=?3 LIMIT 1 )", - paramsv![&folder, uid_validity, uid], + (&folder, uid_validity, uid), |row| { let msg_id: MsgId = row.get(0)?; let chat_id: ChatId = row.get(1)?; @@ -2184,12 +2204,12 @@ async fn mark_seen_by_uid( "UPDATE msgs SET state=?1 WHERE (state=?2 OR state=?3) AND id=?4", - paramsv![ + ( MessageState::InSeen, MessageState::InFresh, MessageState::InNoticed, - msg_id - ], + msg_id, + ), ) .await .with_context(|| format!("failed to update msg {msg_id} state"))? @@ -2219,7 +2239,7 @@ pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) .execute( "INSERT OR IGNORE INTO imap_markseen (id) SELECT id FROM imap WHERE rfc724_mid=?", - paramsv![message_id], + (message_id,), ) .await?; context @@ -2239,7 +2259,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) .execute( "INSERT INTO imap_sync (folder, uid_next) VALUES (?,?) ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next", - paramsv![folder, uid_next], + (folder, uid_next), ) .await?; Ok(()) @@ -2253,10 +2273,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) async fn get_uid_next(context: &Context, folder: &str) -> Result { Ok(context .sql - .query_get_value( - "SELECT uid_next FROM imap_sync WHERE folder=?;", - paramsv![folder], - ) + .query_get_value("SELECT uid_next FROM imap_sync WHERE folder=?;", (folder,)) .await? .unwrap_or(0)) } @@ -2271,7 +2288,7 @@ pub(crate) async fn set_uidvalidity( .execute( "INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?) ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity", - paramsv![folder, uidvalidity], + (folder, uidvalidity), ) .await?; Ok(()) @@ -2282,7 +2299,7 @@ async fn get_uidvalidity(context: &Context, folder: &str) -> Result { .sql .query_get_value( "SELECT uidvalidity FROM imap_sync WHERE folder=?;", - paramsv![folder], + (folder,), ) .await? .unwrap_or(0)) @@ -2294,7 +2311,7 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) -> .execute( "INSERT INTO imap_sync (folder, modseq) VALUES (?,?) ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq", - paramsv![folder, modseq], + (folder, modseq), ) .await?; Ok(()) @@ -2303,10 +2320,7 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) -> async fn get_modseq(context: &Context, folder: &str) -> Result { Ok(context .sql - .query_get_value( - "SELECT modseq FROM imap_sync WHERE folder=?;", - paramsv![folder], - ) + .query_get_value("SELECT modseq FROM imap_sync WHERE folder=?;", (folder,)) .await? .unwrap_or(0)) } diff --git a/src/imex.rs b/src/imex.rs index d74907242..a61a51142 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -762,14 +762,11 @@ async fn export_database(context: &Context, dest: &Path, passphrase: String) -> context .sql .call_write(|conn| { - conn.execute("VACUUM;", params![]) + conn.execute("VACUUM;", ()) .map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}")) .ok(); - conn.execute( - "ATTACH DATABASE ? AS backup KEY ?", - paramsv![dest, passphrase], - ) - .context("failed to attach backup database")?; + conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase)) + .context("failed to attach backup database")?; let res = conn .query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(())) .context("failed to export to attached backup database"); diff --git a/src/imex/transfer.rs b/src/imex/transfer.rs index 220bb0344..285281faf 100644 --- a/src/imex/transfer.rs +++ b/src/imex/transfer.rs @@ -46,9 +46,11 @@ use tokio_stream::wrappers::ReadDirStream; use tokio_util::sync::CancellationToken; use crate::blob::BlobDirContents; -use crate::chat::delete_and_reset_all_device_msgs; +use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs}; use crate::context::{Context, OngoingGuard}; +use crate::message::{Message, Viewtype}; use crate::qr::Qr; +use crate::stock_str::backup_transfer_msg_body; use crate::{e2ee, EventType}; use super::{export_database, DBFILE_BACKUP_NAME}; @@ -261,7 +263,12 @@ impl BackupProvider { } }; match &res { - Ok(_) => context.emit_event(SendProgress::Completed.into()), + Ok(_) => { + context.emit_event(SendProgress::Completed.into()); + let mut msg = Message::new(Viewtype::Text); + msg.text = Some(backup_transfer_msg_body(context).await); + add_device_msg(context, None, Some(&mut msg)).await?; + } Err(err) => { error!(context, "Backup transfer failure: {err:#}"); context.emit_event(SendProgress::Failed.into()) diff --git a/src/job.rs b/src/job.rs index f6fc19809..f739f4f63 100644 --- a/src/job.rs +++ b/src/job.rs @@ -99,7 +99,7 @@ impl Job { if self.job_id != 0 { context .sql - .execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32]) + .execute("DELETE FROM jobs WHERE id=?;", (self.job_id as i32,)) .await?; } @@ -117,22 +117,22 @@ impl Job { .sql .execute( "UPDATE jobs SET desired_timestamp=?, tries=? WHERE id=?;", - paramsv![ + ( self.desired_timestamp, i64::from(self.tries), self.job_id as i32, - ], + ), ) .await?; } else { context.sql.execute( "INSERT INTO jobs (added_timestamp, action, foreign_id, desired_timestamp) VALUES (?,?,?,?);", - paramsv![ + ( self.added_timestamp, self.action, self.foreign_id, self.desired_timestamp - ] + ) ).await?; } @@ -277,7 +277,7 @@ WHERE desired_timestamp<=? ORDER BY action DESC, added_timestamp LIMIT 1; "#; - params = paramsv![t]; + params = vec![t]; } else { // processing after call to dc_maybe_network(): // process _all_ pending jobs that failed before @@ -289,13 +289,13 @@ WHERE tries>0 ORDER BY desired_timestamp, action DESC LIMIT 1; "#; - params = paramsv![]; + params = vec![]; }; loop { let job_res = context .sql - .query_row_optional(query, params.clone(), |row| { + .query_row_optional(query, rusqlite::params_from_iter(params.clone()), |row| { let job = Job { job_id: row.get("id")?, action: row.get("action")?, @@ -318,12 +318,14 @@ LIMIT 1; // TODO: improve by only doing a single query let id = context .sql - .query_row(query, params.clone(), |row| row.get::<_, i32>(0)) + .query_row(query, rusqlite::params_from_iter(params.clone()), |row| { + row.get::<_, i32>(0) + }) .await .context("failed to retrieve invalid job ID from the database")?; context .sql - .execute("DELETE FROM jobs WHERE id=?;", paramsv![id]) + .execute("DELETE FROM jobs WHERE id=?;", (id,)) .await .with_context(|| format!("failed to delete invalid job {id}"))?; } @@ -344,7 +346,7 @@ mod tests { "INSERT INTO jobs (added_timestamp, action, foreign_id, desired_timestamp) VALUES (?, ?, ?, ?);", - paramsv![ + ( now, if valid { Action::DownloadMsg as i32 @@ -352,8 +354,8 @@ mod tests { -1 }, foreign_id, - now - ], + now, + ), ) .await .unwrap(); diff --git a/src/key.rs b/src/key.rs index 74fb997cf..2965cfb55 100644 --- a/src/key.rs +++ b/src/key.rs @@ -100,7 +100,7 @@ impl DcKey for SignedPublicKey { WHERE addr=? AND is_default=1; "#, - paramsv![addr], + (addr,), |row| { let bytes: Vec = row.get(0)?; Ok(bytes) @@ -240,7 +240,7 @@ pub(crate) async fn load_keypair( WHERE addr=?1 AND is_default=1; "#, - paramsv![addr], + (addr,), |row| { let pub_bytes: Vec = row.get(0)?; let sec_bytes: Vec = row.get(1)?; @@ -297,7 +297,7 @@ pub async fn store_self_keypair( transaction .execute( "DELETE FROM keypairs WHERE public_key=? OR private_key=?;", - paramsv![public_key, secret_key], + (&public_key, &secret_key), ) .context("failed to remove old use of key")?; if default == KeyPairUse::Default { @@ -317,7 +317,7 @@ pub async fn store_self_keypair( .execute( "INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);", - paramsv![addr, is_default, public_key, secret_key, t], + (addr, is_default, &public_key, &secret_key, t), ) .context("failed to insert keypair")?; diff --git a/src/lib.rs b/src/lib.rs index 51a72a1d5..38c8d0d47 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,11 +35,6 @@ extern crate rusqlite; #[macro_use] extern crate strum_macros; -#[allow(missing_docs)] -pub trait ToSql: rusqlite::ToSql + Send + Sync {} - -impl ToSql for T {} - #[macro_use] pub mod log; @@ -71,6 +66,7 @@ pub mod ephemeral; mod http; mod imap; pub mod imex; +pub mod release; mod scheduler; #[macro_use] mod job; diff --git a/src/location.rs b/src/location.rs index 28864263d..cae938cf8 100644 --- a/src/location.rs +++ b/src/location.rs @@ -271,11 +271,11 @@ pub async fn send_locations_to_chat( SET locations_send_begin=?, \ locations_send_until=? \ WHERE id=?", - paramsv![ + ( if 0 != seconds { now } else { 0 }, if 0 != seconds { now + seconds } else { 0 }, chat_id, - ], + ), ) .await?; if 0 != seconds && !is_sending_locations_before { @@ -310,7 +310,7 @@ pub async fn is_sending_locations_to_chat( .sql .exists( "SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;", - paramsv![chat_id, time()], + (chat_id, time()), ) .await? } @@ -319,7 +319,7 @@ pub async fn is_sending_locations_to_chat( .sql .exists( "SELECT COUNT(id) FROM chats WHERE locations_send_until>?;", - paramsv![time()], + (time(),), ) .await? } @@ -338,7 +338,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64 .sql .query_map( "SELECT id FROM chats WHERE locations_send_until>?;", - paramsv![time()], + (time(),), |row| row.get::<_, i32>(0), |chats| { chats @@ -352,14 +352,14 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64 if let Err(err) = context.sql.execute( "INSERT INTO locations \ (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);", - paramsv![ + ( latitude, longitude, accuracy, time(), chat_id, ContactId::SELF, - ] + ) ).await { warn!(context, "failed to store location {:#}", err); } else { @@ -404,14 +404,14 @@ pub async fn get_range( AND (? OR l.from_id=?) \ AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \ ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;", - paramsv![ + ( disable_chat_id, chat_id, disable_contact_id, contact_id as i32, timestamp_from, timestamp_to, - ], + ), |row| { let msg_id = row.get(6)?; let txt: String = row.get(9)?; @@ -471,7 +471,7 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32) let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row( "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;", - paramsv![chat_id], |row| { + (chat_id,), |row| { let send_begin: i64 = row.get(0)?; let send_until: i64 = row.get(1)?; let last_sent: i64 = row.get(2)?; @@ -500,12 +500,12 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32) AND independent=0 \ GROUP BY timestamp \ ORDER BY timestamp;", - paramsv![ + ( ContactId::SELF, locations_send_begin, locations_last_sent, ContactId::SELF - ], + ), |row| { let location_id: i32 = row.get(0)?; let latitude: f64 = row.get(1)?; @@ -575,7 +575,7 @@ pub async fn set_kml_sent_timestamp( .sql .execute( "UPDATE chats SET locations_last_sent=? WHERE id=?;", - paramsv![timestamp, chat_id], + (timestamp, chat_id), ) .await?; Ok(()) @@ -587,7 +587,7 @@ pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id: .sql .execute( "UPDATE msgs SET location_id=? WHERE id=?;", - paramsv![location_id, msg_id], + (location_id, msg_id), ) .await?; @@ -629,10 +629,10 @@ pub(crate) async fn save( .prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?; let mut stmt_insert = conn.prepare_cached(stmt_insert)?; - let exists = stmt_test.exists(paramsv![timestamp, contact_id])?; + let exists = stmt_test.exists((timestamp, contact_id))?; if independent || !exists { - stmt_insert.execute(paramsv![ + stmt_insert.execute(( timestamp, contact_id, chat_id, @@ -640,7 +640,7 @@ pub(crate) async fn save( longitude, accuracy, independent, - ])?; + ))?; if timestamp > newest_timestamp { // okay to drop, as we use cached prepared statements @@ -729,7 +729,7 @@ async fn maybe_send_locations(context: &Context) -> Result> { AND timestamp>=? \ AND timestamp>? \ AND independent=0", - paramsv![ContactId::SELF, locations_send_begin, locations_last_sent,], + (ContactId::SELF, locations_send_begin, locations_last_sent), ) .await?; @@ -781,7 +781,7 @@ async fn maybe_send_locations(context: &Context) -> Result> { "UPDATE chats \ SET locations_send_begin=0, locations_send_until=0 \ WHERE id=?", - paramsv![chat_id], + (chat_id,), ) .await .context("failed to disable location streaming")?; diff --git a/src/message.rs b/src/message.rs index 3ec5a99ae..ea72a8c99 100644 --- a/src/message.rs +++ b/src/message.rs @@ -77,7 +77,7 @@ impl MsgId { pub async fn get_state(self, context: &Context) -> Result { let result = context .sql - .query_get_value("SELECT state FROM msgs WHERE id=?", paramsv![self]) + .query_get_value("SELECT state FROM msgs WHERE id=?", (self,)) .await? .unwrap_or_default(); Ok(result) @@ -106,7 +106,7 @@ SET param='' WHERE id=?; "#, - paramsv![chat_id, self], + (chat_id, self), ) .await?; @@ -119,22 +119,19 @@ WHERE id=?; // sure they are not left while the message is deleted. context .sql - .execute("DELETE FROM smtp WHERE msg_id=?", paramsv![self]) + .execute("DELETE FROM smtp WHERE msg_id=?", (self,)) .await?; context .sql - .execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsv![self]) + .execute("DELETE FROM msgs_mdns WHERE msg_id=?;", (self,)) .await?; context .sql - .execute( - "DELETE FROM msgs_status_updates WHERE msg_id=?;", - paramsv![self], - ) + .execute("DELETE FROM msgs_status_updates WHERE msg_id=?;", (self,)) .await?; context .sql - .execute("DELETE FROM msgs WHERE id=?;", paramsv![self]) + .execute("DELETE FROM msgs WHERE id=?;", (self,)) .await?; Ok(()) } @@ -143,7 +140,7 @@ WHERE id=?; update_msg_state(context, self, MessageState::OutDelivered).await?; let chat_id: ChatId = context .sql - .query_get_value("SELECT chat_id FROM msgs WHERE id=?", paramsv![self]) + .query_get_value("SELECT chat_id FROM msgs WHERE id=?", (self,)) .await? .unwrap_or_default(); context.emit_event(EventType::MsgDelivered { @@ -328,7 +325,7 @@ impl Message { " FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id", " WHERE m.id=?;" ), - paramsv![id], + (id,), |row| { let text = match row.get_ref("txt")? { rusqlite::types::ValueRef::Text(buf) => { @@ -949,7 +946,7 @@ impl Message { .sql .execute( "UPDATE msgs SET param=? WHERE id=?;", - paramsv![self.param.to_string(), self.id], + (self.param.to_string(), self.id), ) .await?; Ok(()) @@ -960,7 +957,7 @@ impl Message { .sql .execute( "UPDATE msgs SET subject=? WHERE id=?;", - paramsv![self.subject, self.id], + (&self.subject, self.id), ) .await?; Ok(()) @@ -1094,7 +1091,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { let msg = Message::load_from_db(context, msg_id).await?; let rawtxt: Option = context .sql - .query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id]) + .query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,)) .await?; let mut ret = String::new(); @@ -1144,7 +1141,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { .sql .query_map( "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;", - paramsv![msg_id], + (msg_id,), |row| { let contact_id: ContactId = row.get(0)?; let ts: i64 = row.get(1)?; @@ -1225,7 +1222,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { .sql .query_map( "SELECT folder, uid FROM imap WHERE rfc724_mid=?", - paramsv![msg.rfc724_mid], + (msg.rfc724_mid,), |row| { let folder: String = row.get("folder")?; let uid: u32 = row.get("uid")?; @@ -1245,7 +1242,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { } let hop_info: Option = context .sql - .query_get_value("SELECT hop_info FROM msgs WHERE id=?;", paramsv![msg_id]) + .query_get_value("SELECT hop_info FROM msgs WHERE id=?;", (msg_id,)) .await?; ret += "\n\n"; @@ -1353,7 +1350,7 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result Result ensure!(rows_updated <= 1), Err(e) => { @@ -1421,7 +1418,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> { .sql .execute( "UPDATE imap SET target=? WHERE rfc724_mid=?", - paramsv![target, msg.rfc724_mid], + (target, msg.rfc724_mid), ) .await?; @@ -1459,7 +1456,7 @@ async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()> .sql .execute( "DELETE FROM locations WHERE independent = 1 AND id=?;", - paramsv![location_id as i32], + (location_id as i32,), ) .await?; Ok(()) @@ -1471,6 +1468,12 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec) -> Result<()> return Ok(()); } + let old_last_msg_id = MsgId::new(context.get_config_u32(Config::LastMsgId).await?); + let last_msg_id = msg_ids.iter().fold(&old_last_msg_id, std::cmp::max); + context + .set_config_u32(Config::LastMsgId, last_msg_id.to_u32()) + .await?; + let msgs = context .sql .query_map( @@ -1558,7 +1561,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec) -> Result<()> .sql .execute( "INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)", - paramsv![id, curr_from_id, curr_rfc724_mid], + (id, curr_from_id, curr_rfc724_mid), ) .await .context("failed to insert into smtp_mdns")?; @@ -1586,10 +1589,7 @@ pub(crate) async fn update_msg_state( ) -> Result<()> { context .sql - .execute( - "UPDATE msgs SET state=? WHERE id=?;", - paramsv![state, msg_id], - ) + .execute("UPDATE msgs SET state=? WHERE id=?;", (state, msg_id)) .await?; Ok(()) } @@ -1609,7 +1609,7 @@ pub(crate) async fn exists(context: &Context, msg_id: MsgId) -> Result { let chat_id: Option = context .sql - .query_get_value("SELECT chat_id FROM msgs WHERE id=?;", paramsv![msg_id]) + .query_get_value("SELECT chat_id FROM msgs WHERE id=?;", (msg_id,)) .await?; if let Some(chat_id) = chat_id { @@ -1635,7 +1635,7 @@ pub(crate) async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str .sql .execute( "UPDATE msgs SET state=?, error=? WHERE id=?;", - paramsv![msg.state, error, msg_id], + (msg.state, error, msg_id), ) .await { @@ -1680,7 +1680,7 @@ pub async fn handle_mdn( " WHERE rfc724_mid=? AND from_id=1", " ORDER BY m.id;" ), - paramsv![rfc724_mid], + (&rfc724_mid,), |row| { Ok(( row.get::<_, MsgId>("msg_id")?, @@ -1706,7 +1706,7 @@ pub async fn handle_mdn( .sql .exists( "SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;", - paramsv![msg_id, from_id], + (msg_id, from_id), ) .await? { @@ -1714,7 +1714,7 @@ pub async fn handle_mdn( .sql .execute( "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);", - paramsv![msg_id, from_id, timestamp_sent], + (msg_id, from_id, timestamp_sent), ) .await?; } @@ -1754,7 +1754,7 @@ pub(crate) async fn handle_ndn( " FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id", " WHERE rfc724_mid=? AND from_id=1", ), - paramsv![failed.rfc724_mid], + (&failed.rfc724_mid,), |row| { Ok(( row.get::<_, MsgId>("msg_id")?, @@ -1894,7 +1894,7 @@ pub async fn estimate_deletion_cnt( AND timestamp < ? AND chat_id != ? AND EXISTS (SELECT * FROM imap WHERE rfc724_mid=m.rfc724_mid);", - paramsv![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id], + (DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id), ) .await? } else { @@ -1907,12 +1907,12 @@ pub async fn estimate_deletion_cnt( AND timestamp < ? AND chat_id != ? AND chat_id != ? AND hidden = 0;", - paramsv![ + ( DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id, - DC_CHAT_ID_TRASH - ], + DC_CHAT_ID_TRASH, + ), ) .await? }; @@ -1933,7 +1933,7 @@ pub(crate) async fn rfc724_mid_exists( .sql .query_row_optional( "SELECT id FROM msgs WHERE rfc724_mid=?", - paramsv![rfc724_mid], + (rfc724_mid,), |row| { let msg_id: MsgId = row.get(0)?; diff --git a/src/mimefactory.rs b/src/mimefactory.rs index ce4bac4c1..24a22de0e 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -166,7 +166,7 @@ impl<'a> MimeFactory<'a> { FROM chats_contacts cc \ LEFT JOIN contacts c ON cc.contact_id=c.id \ WHERE cc.chat_id=? AND cc.contact_id>9;", - paramsv![msg.chat_id], + (msg.chat_id,), |row| { let authname: String = row.get(0)?; let addr: String = row.get(1)?; @@ -195,7 +195,7 @@ impl<'a> MimeFactory<'a> { .sql .query_row( "SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", - paramsv![msg.id], + (msg.id,), |row| { let in_reply_to: String = row.get(0)?; let references: String = row.get(1)?; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index de5009ce4..455c5733f 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -33,7 +33,7 @@ use crate::peerstate::Peerstate; use crate::simplify::{simplify, SimplifiedText}; use crate::stock_str; use crate::sync::SyncItems; -use crate::tools::{get_filemeta, parse_receive_headers, truncate_by_lines}; +use crate::tools::{get_filemeta, parse_receive_headers, strip_rtlo_characters, truncate_by_lines}; use crate::{location, tools}; /// A parsed MIME message. @@ -1668,10 +1668,7 @@ impl MimeMessage { { context .sql - .query_get_value( - "SELECT timestamp FROM msgs WHERE rfc724_mid=?", - paramsv![field], - ) + .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,)) .await? } else { None @@ -1952,6 +1949,8 @@ fn get_attachment_filename( }; } + let desired_filename = desired_filename.map(|filename| strip_rtlo_characters(&filename)); + Ok(desired_filename) } @@ -2350,7 +2349,7 @@ mod tests { .sql .execute( "INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)", - paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp], + ("Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp), ) .await .expect("Failed to write to the database"); diff --git a/src/net.rs b/src/net.rs index a0f3a3313..c6cc6f539 100644 --- a/src/net.rs +++ b/src/net.rs @@ -75,7 +75,7 @@ async fn lookup_host_with_cache( VALUES (?, ?, ?) ON CONFLICT (hostname, address) DO UPDATE SET timestamp=excluded.timestamp", - paramsv![hostname, ip_string, now], + (hostname, ip_string, now), ) .await?; } @@ -89,7 +89,7 @@ async fn lookup_host_with_cache( WHERE hostname = ? AND ? < timestamp + 30 * 24 * 3600 ORDER BY timestamp DESC", - paramsv![hostname, now], + (hostname, now), |row| { let address: String = row.get(0)?; Ok(address) @@ -157,7 +157,7 @@ pub(crate) async fn connect_tcp( "UPDATE dns_cache SET timestamp = ? WHERE address = ?", - paramsv![time(), resolved_addr.ip().to_string()], + (time(), resolved_addr.ip().to_string()), ) .await?; break; diff --git a/src/peerstate.rs b/src/peerstate.rs index 16378c918..47b9cda13 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -144,7 +144,7 @@ impl Peerstate { verified_key, verified_key_fingerprint, verifier \ FROM acpeerstates \ WHERE addr=? COLLATE NOCASE LIMIT 1;"; - Self::from_stmt(context, query, paramsv![addr]).await + Self::from_stmt(context, query, (addr,)).await } /// Loads peerstate corresponding to the given fingerprint from the database. @@ -160,7 +160,7 @@ impl Peerstate { OR gossip_key_fingerprint=? \ ORDER BY public_key_fingerprint=? DESC LIMIT 1;"; let fp = fingerprint.hex(); - Self::from_stmt(context, query, paramsv![fp, fp, fp]).await + Self::from_stmt(context, query, (&fp, &fp, &fp)).await } /// Loads peerstate by address or verified fingerprint. @@ -180,7 +180,7 @@ impl Peerstate { OR addr=? COLLATE NOCASE \ ORDER BY verified_key_fingerprint=? DESC, last_seen DESC LIMIT 1;"; let fp = fingerprint.hex(); - Self::from_stmt(context, query, paramsv![fp, addr, fp]).await + Self::from_stmt(context, query, (&fp, &addr, &fp)).await } async fn from_stmt( @@ -471,7 +471,7 @@ impl Peerstate { verified_key = excluded.verified_key, verified_key_fingerprint = excluded.verified_key_fingerprint, verifier = excluded.verifier", - paramsv![ + ( self.last_seen, self.last_seen_autocrypt, self.prefer_encrypt as i64, @@ -482,9 +482,9 @@ impl Peerstate { self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()), self.verified_key.as_ref().map(|k| k.to_bytes()), self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()), - self.addr, + &self.addr, self.verifier.as_deref().unwrap_or(""), - ], + ), ) .await?; Ok(()) @@ -524,7 +524,7 @@ impl Peerstate { .sql .query_get_value( "SELECT id FROM contacts WHERE addr=? COLLATE NOCASE;", - paramsv![self.addr], + (&self.addr,), ) .await? .with_context(|| format!("contact with peerstate.addr {:?} not found", &self.addr))?; @@ -554,7 +554,7 @@ impl Peerstate { .sql .query_get_value( "SELECT created_timestamp FROM chats WHERE id=?;", - paramsv![chat_id], + (chat_id,), ) .await? .unwrap_or(0) @@ -851,7 +851,7 @@ mod tests { // can be loaded without errors. ctx.ctx .sql - .execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr]) + .execute("INSERT INTO acpeerstates (addr) VALUES(?)", (addr,)) .await .expect("Failed to write to the database"); diff --git a/src/provider.rs b/src/provider.rs index 2d37b1347..58e1cd517 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -3,12 +3,11 @@ mod data; use anyhow::Result; -use chrono::{NaiveDateTime, NaiveTime}; use trust_dns_resolver::{config, AsyncResolver, TokioAsyncResolver}; use crate::config::Config; use crate::context::Context; -use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED}; +use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS}; /// Provider status according to manual testing. #[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)] @@ -258,22 +257,12 @@ pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> { } } -/// Returns update timestamp as a unix timestamp compatible for comparison with time() and database times. -pub fn get_provider_update_timestamp() -> i64 { - NaiveDateTime::new(*PROVIDER_UPDATED, NaiveTime::from_hms_opt(0, 0, 0).unwrap()) - .timestamp_millis() - / 1_000 -} - #[cfg(test)] mod tests { #![allow(clippy::indexing_slicing)] - use chrono::NaiveDate; - use super::*; use crate::test_utils::TestContext; - use crate::tools::time; #[test] fn test_get_provider_by_domain_unexistant() { @@ -336,18 +325,6 @@ mod tests { ); } - #[test] - fn test_get_provider_update_timestamp() { - let timestamp_past = NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(), - NaiveTime::from_hms_opt(0, 0, 0).unwrap(), - ) - .timestamp_millis() - / 1_000; - assert!(get_provider_update_timestamp() <= time()); - assert!(get_provider_update_timestamp() > timestamp_past); - } - #[test] fn test_get_resolver() -> Result<()> { assert!(get_resolver().is_ok()); diff --git a/src/provider/data.rs b/src/provider/data.rs index 8ca9d91e9..32b1ff465 100644 --- a/src/provider/data.rs +++ b/src/provider/data.rs @@ -1945,5 +1945,5 @@ pub(crate) static PROVIDER_IDS: Lazy> = ]) }); -pub static PROVIDER_UPDATED: Lazy = +pub static _PROVIDER_UPDATED: Lazy = Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 2, 20).unwrap()); diff --git a/src/qr.rs b/src/qr.rs index 920776b5e..f40b3786e 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -10,7 +10,7 @@ use percent_encoding::percent_decode_str; use serde::Deserialize; use self::dclogin_scheme::configure_from_login_qr; -use crate::chat::{self, get_chat_id_by_grpid, ChatIdBlocked}; +use crate::chat::{get_chat_id_by_grpid, ChatIdBlocked}; use crate::config::Config; use crate::constants::Blocked; use crate::contact::{ @@ -21,7 +21,6 @@ use crate::key::Fingerprint; use crate::message::Message; use crate::peerstate::Peerstate; use crate::socks::Socks5Config; -use crate::tools::time; use crate::{token, EventType}; const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase @@ -435,16 +434,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { Contact::add_or_lookup(context, &name, peerstate_addr, Origin::UnhandledQrScan) .await .context("add_or_lookup")?; - let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request) + ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request) .await .context("Failed to create (new) chat for contact")?; - chat::add_info_msg( - context, - chat.id, - &format!("{} verified.", peerstate.addr), - time(), - ) - .await?; Ok(Qr::FprOk { contact_id }) } else { let contact_id = Contact::lookup_id_by_addr(context, &addr, Origin::Unknown) diff --git a/src/reaction.rs b/src/reaction.rs index 832304b87..d8ad4b5bb 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -156,7 +156,7 @@ async fn set_msg_id_reaction( "DELETE FROM reactions WHERE msg_id = ?1 AND contact_id = ?2", - paramsv![msg_id, contact_id], + (msg_id, contact_id), ) .await?; } else { @@ -167,7 +167,7 @@ async fn set_msg_id_reaction( VALUES (?1, ?2, ?3) ON CONFLICT(msg_id, contact_id) DO UPDATE SET reaction=excluded.reaction", - paramsv![msg_id, contact_id, reaction.as_str()], + (msg_id, contact_id, reaction.as_str()), ) .await?; } @@ -247,7 +247,7 @@ async fn get_self_reaction(context: &Context, msg_id: MsgId) -> Result "SELECT reaction FROM reactions WHERE msg_id=? AND contact_id=?", - paramsv![msg_id, ContactId::SELF], + (msg_id, ContactId::SELF), ) .await?; Ok(reaction_str @@ -262,7 +262,7 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result Blocked::Not, + Some(ChatIdBlocked { id: _, blocked }) => blocked, + None => Blocked::Request, + }; + if chat_id.is_none() { // try to create a group - let create_blocked = match test_normal_chat { - Some(ChatIdBlocked { - id: _, - blocked: Blocked::Not, - }) => Blocked::Not, - _ if is_bot => Blocked::Not, - _ => Blocked::Request, - }; - if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group( context, mime_parser, @@ -579,13 +582,15 @@ async fn add_parts( { chat_id = Some(new_chat_id); chat_id_blocked = new_chat_id_blocked; + } + } - // if the chat is somehow blocked but we want to create a non-blocked chat, - // unblock the chat - if chat_id_blocked != Blocked::Not && create_blocked == Blocked::Not { - new_chat_id.unblock(context).await?; - chat_id_blocked = Blocked::Not; - } + // if the chat is somehow blocked but we want to create a non-blocked chat, + // unblock the chat + if chat_id_blocked != Blocked::Not && create_blocked != Blocked::Yes { + if let Some(chat_id) = chat_id { + chat_id.set_blocked(context, create_blocked).await?; + chat_id_blocked = create_blocked; } } @@ -1077,7 +1082,7 @@ async fn add_parts( let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len()); - for part in &mime_parser.parts { + for part in &mut mime_parser.parts { if part.is_reaction { set_msg_reaction( context, @@ -1093,6 +1098,7 @@ async fn add_parts( if is_system_message != SystemMessage::Unknown { param.set_int(Param::Cmd, is_system_message as i32); } + if let Some(replace_msg_id) = replace_msg_id { let placeholder = Message::load_from_db(context, replace_msg_id).await?; for key in [ @@ -1362,7 +1368,7 @@ async fn calc_sort_timestamp( .sql .query_get_value( "SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?", - paramsv![chat_id, MessageState::InFresh], + (chat_id, MessageState::InFresh), ) .await?; @@ -1681,7 +1687,7 @@ async fn apply_group_changes( .sql .execute( "UPDATE chats SET name=? WHERE id=?;", - paramsv![grpname.to_string(), chat_id], + (strip_rtlo_characters(grpname), chat_id), ) .await?; send_event_chat_modified = true; @@ -1751,10 +1757,7 @@ async fn apply_group_changes( // start from scratch. context .sql - .execute( - "DELETE FROM chats_contacts WHERE chat_id=?;", - paramsv![chat_id], - ) + .execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,)) .await?; members_to_add.push(ContactId::SELF); diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index ed3791fe7..cf64ee3b5 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -374,7 +374,7 @@ async fn test_no_message_id_header() { .sql .exists( "SELECT COUNT(*) FROM msgs WHERE chat_id=?;", - paramsv![DC_CHAT_ID_TRASH], + (DC_CHAT_ID_TRASH,), ) .await .unwrap()); @@ -3046,6 +3046,54 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> { Ok(()) } +/// Regression test for two bugs: +/// +/// 1. If you blocked some spammer using DC, the 1:1 messages with that contact +/// are not received, but they could easily bypass this restriction creating +/// a new group with only you two as member. +/// 2. A blocked group was sometimes not unblocked when when an unblocked +/// contact sent a message into it. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_blocked_contact_creates_group() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + + let chat = alice.create_chat(&bob).await; + chat.id.block(&alice).await?; + + let group_id = bob + .create_group_with_members( + ProtectionStatus::Unprotected, + "group name", + &[&alice, &fiona], + ) + .await; + + let sent = bob.send_text(group_id, "Heyho, I'm a spammer!").await; + let rcvd = alice.recv_msg(&sent).await; + // Alice blocked Bob, so she shouldn't get the message + assert_eq!(rcvd.chat_blocked, Blocked::Yes); + + // Fiona didn't block Bob, though, so she gets the message + let rcvd = fiona.recv_msg(&sent).await; + assert_eq!(rcvd.chat_blocked, Blocked::Request); + + // Fiona writes to the group + rcvd.chat_id.accept(&fiona).await?; + let sent = fiona.send_text(rcvd.chat_id, "Hello from Fiona").await; + + // The group is unblocked now that Fiona sent a message to it + let rcvd = alice.recv_msg(&sent).await; + assert_eq!(rcvd.chat_blocked, Blocked::Request); + // In order not to lose context, Bob's message should also be shown in the group + let msgs = chat::get_chat_msgs(&alice, rcvd.chat_id).await?; + assert_eq!(msgs.len(), 2); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_thunderbird_autocrypt() -> Result<()> { let t = TestContext::new_bob().await; diff --git a/src/release.rs b/src/release.rs new file mode 100644 index 000000000..0545079ec --- /dev/null +++ b/src/release.rs @@ -0,0 +1,10 @@ +//! DC release info. + +use chrono::NaiveDate; +use once_cell::sync::Lazy; + +const DATE_STR: &str = include_str!("../release-date.in"); + +/// Last release date. +pub static DATE: Lazy = + Lazy::new(|| NaiveDate::parse_from_str(DATE_STR, "%Y-%m-%d").unwrap()); diff --git a/src/scheduler.rs b/src/scheduler.rs index 2404d7e22..840824f09 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -62,6 +62,11 @@ impl SchedulerState { /// Starts the scheduler if it is not yet started. async fn do_start(mut inner: RwLockWriteGuard<'_, InnerSchedulerState>, context: Context) { info!(context, "starting IO"); + + // Notify message processing loop + // to allow processing old messages after restart. + context.new_msgs_notify.notify_one(); + let ctx = context.clone(); match Scheduler::start(context).await { Ok(scheduler) => *inner = InnerSchedulerState::Started(scheduler), @@ -95,6 +100,11 @@ impl SchedulerState { // to terminate on receiving the next event and then call stop_io() // which will emit the below event(s) info!(context, "stopping IO"); + + // Wake up message processing loop even if there are no messages + // to allow for clean shutdown. + context.new_msgs_notify.notify_one(); + if let Some(debug_logging) = context.debug_logging.read().await.as_ref() { debug_logging.loop_handle.abort(); } diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs index b6584f6e1..04b1edd33 100644 --- a/src/securejoin/bobstate.rs +++ b/src/securejoin/bobstate.rs @@ -135,21 +135,21 @@ impl BobState { // rows that we will delete. So start with a dummy UPDATE. transaction.execute( r#"UPDATE bobstate SET next_step=?;"#, - params![SecureJoinStep::Terminated], + (SecureJoinStep::Terminated,), )?; let mut stmt = transaction.prepare("SELECT id FROM bobstate;")?; let mut aborted = Vec::new(); - for id in stmt.query_map(params![], |row| row.get::<_, i64>(0))? { + for id in stmt.query_map((), |row| row.get::<_, i64>(0))? { let id = id?; let state = BobState::from_db_id(transaction, id)?; aborted.push(state); } // Finally delete everything and insert new row. - transaction.execute("DELETE FROM bobstate;", params![])?; + transaction.execute("DELETE FROM bobstate;", ())?; transaction.execute( "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);", - params![invite, next, chat_id], + (invite, next, chat_id), )?; let id = transaction.last_insert_rowid(); Ok((id, aborted)) @@ -180,7 +180,7 @@ impl BobState { fn from_db_id(connection: &Connection, id: i64) -> rusqlite::Result { connection.query_row( "SELECT invite, next_step, chat_id FROM bobstate WHERE id=?;", - params![id], + (id,), |row| { let s = BobState { id, @@ -217,12 +217,12 @@ impl BobState { SecureJoinStep::AuthRequired | SecureJoinStep::ContactConfirm => { sql.execute( "UPDATE bobstate SET next_step=? WHERE id=?;", - paramsv![next, self.id], + (next, self.id), ) .await?; } SecureJoinStep::Terminated | SecureJoinStep::Completed => { - sql.execute("DELETE FROM bobstate WHERE id=?;", paramsv!(self.id)) + sql.execute("DELETE FROM bobstate WHERE id=?;", (self.id,)) .await?; } } diff --git a/src/smtp.rs b/src/smtp.rs index 26402b325..bc6e29e90 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -520,10 +520,7 @@ pub(crate) async fn send_msg_to_smtp( // database. context .sql - .execute( - "UPDATE smtp SET retries=retries+1 WHERE id=?", - paramsv![rowid], - ) + .execute("UPDATE smtp SET retries=retries+1 WHERE id=?", (rowid,)) .await .context("failed to update retries count")?; @@ -531,7 +528,7 @@ pub(crate) async fn send_msg_to_smtp( .sql .query_row( "SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?", - paramsv![rowid], + (rowid,), |row| { let mime: String = row.get(0)?; let recipients: String = row.get(1)?; @@ -545,7 +542,7 @@ pub(crate) async fn send_msg_to_smtp( message::set_msg_failed(context, msg_id, "Number of retries exceeded the limit.").await; context .sql - .execute("DELETE FROM smtp WHERE id=?", paramsv![rowid]) + .execute("DELETE FROM smtp WHERE id=?", (rowid,)) .await .context("failed to remove message with exceeded retry limit from smtp table")?; bail!("Number of retries exceeded the limit"); @@ -588,7 +585,7 @@ pub(crate) async fn send_msg_to_smtp( SendResult::Success | SendResult::Failure(_) => { context .sql - .execute("DELETE FROM smtp WHERE id=?", paramsv![rowid]) + .execute("DELETE FROM smtp WHERE id=?", (rowid,)) .await?; } }; @@ -637,7 +634,7 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp) .sql .query_map( "SELECT id FROM smtp ORDER BY id ASC", - paramsv![], + (), |row| { let rowid: i64 = row.get(0)?; Ok(rowid) @@ -691,7 +688,7 @@ async fn send_mdn_msg_id( "SELECT msg_id, rfc724_mid FROM smtp_mdns WHERE from_id=? AND msg_id!=?", - paramsv![contact_id, msg_id], + (contact_id, msg_id), |row| { let msg_id: MsgId = row.get(0)?; let rfc724_mid: String = row.get(1)?; @@ -718,7 +715,7 @@ async fn send_mdn_msg_id( info!(context, "Successfully sent MDN for {}", msg_id); context .sql - .execute("DELETE FROM smtp_mdns WHERE msg_id = ?", paramsv![msg_id]) + .execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,)) .await?; if !additional_msg_ids.is_empty() { let q = format!( @@ -779,7 +776,7 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result { .sql .execute( "UPDATE smtp_mdns SET retries=retries+1 WHERE msg_id=?", - paramsv![msg_id], + (msg_id,), ) .await .context("failed to update MDN retries count")?; @@ -789,7 +786,7 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result { // database, do not try to send this MDN again. context .sql - .execute("DELETE FROM smtp_mdns WHERE msg_id = ?", paramsv![msg_id]) + .execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,)) .await?; Err(err) } else { diff --git a/src/sql.rs b/src/sql.rs index 641b352ec..eab857af4 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -23,27 +23,28 @@ use crate::peerstate::{deduplicate_peerstates, Peerstate}; use crate::stock_str; use crate::tools::{delete_file, time}; -#[allow(missing_docs)] +/// Extension to [`rusqlite::ToSql`] trait +/// which also includes [`Send`] and [`Sync`]. +pub trait ToSql: rusqlite::ToSql + Send + Sync {} + +impl ToSql for T {} + +/// Constructs a slice of trait object references `&dyn ToSql`. +/// +/// One of the uses is passing more than 16 parameters +/// to a query, because [`rusqlite::Params`] is only implemented +/// for tuples of up to 16 elements. #[macro_export] -macro_rules! paramsv { - () => { - rusqlite::params_from_iter(Vec::<&dyn $crate::ToSql>::new()) - }; - ($($param:expr),+ $(,)?) => { - rusqlite::params_from_iter(vec![$(&$param as &dyn $crate::ToSql),+]) +macro_rules! params_slice { + ($($param:expr),+) => { + [$(&$param as &dyn $crate::sql::ToSql),+] }; } -#[allow(missing_docs)] -#[macro_export] -macro_rules! params_iterv { - ($($param:expr),+ $(,)?) => { - vec![$(&$param as &dyn $crate::ToSql),+] - }; -} - -pub(crate) fn params_iter(iter: &[impl crate::ToSql]) -> impl Iterator { - iter.iter().map(|item| item as &dyn crate::ToSql) +pub(crate) fn params_iter( + iter: &[impl crate::sql::ToSql], +) -> impl Iterator { + iter.iter().map(|item| item as &dyn crate::sql::ToSql) } mod migrations; @@ -139,11 +140,8 @@ impl Sql { let res = self .call_write(move |conn| { // Check that backup passphrase is correct before resetting our database. - conn.execute( - "ATTACH DATABASE ? AS backup KEY ?", - paramsv![path_str, passphrase], - ) - .context("failed to attach backup database")?; + conn.execute("ATTACH DATABASE ? AS backup KEY ?", (path_str, passphrase)) + .context("failed to attach backup database")?; if let Err(err) = conn .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) .context("backup passphrase is not correct") @@ -556,27 +554,21 @@ impl Sql { let mut lock = self.config_cache.write().await; if let Some(value) = value { let exists = self - .exists( - "SELECT COUNT(*) FROM config WHERE keyname=?;", - paramsv![key], - ) + .exists("SELECT COUNT(*) FROM config WHERE keyname=?;", (key,)) .await?; if exists { - self.execute( - "UPDATE config SET value=? WHERE keyname=?;", - paramsv![value, key], - ) - .await?; + self.execute("UPDATE config SET value=? WHERE keyname=?;", (value, key)) + .await?; } else { self.execute( "INSERT INTO config (keyname, value) VALUES (?, ?);", - paramsv![key, value], + (key, value), ) .await?; } } else { - self.execute("DELETE FROM config WHERE keyname=?;", paramsv![key]) + self.execute("DELETE FROM config WHERE keyname=?;", (key,)) .await?; } lock.insert(key.to_string(), value.map(|s| s.to_string())); @@ -597,7 +589,7 @@ impl Sql { let mut lock = self.config_cache.write().await; let value = self - .query_get_value("SELECT value FROM config WHERE keyname=?;", paramsv![key]) + .query_get_value("SELECT value FROM config WHERE keyname=?;", (key,)) .await .context(format!("failed to fetch raw config: {key}"))?; lock.insert(key.to_string(), value.clone()); @@ -980,7 +972,7 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> { AND NOT EXISTS ( SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!='' )", - paramsv![DC_CHAT_ID_TRASH], + (DC_CHAT_ID_TRASH,), ) .await?; Ok(()) @@ -1163,12 +1155,12 @@ mod tests { sql.open(&t, "".to_string()).await?; sql.execute( "INSERT INTO config (keyname, value) VALUES (?, ?);", - paramsv!("foo", "bar"), + ("foo", "bar"), ) .await?; let value: Option = sql - .query_get_value("SELECT value FROM config WHERE keyname=?;", paramsv!("foo")) + .query_get_value("SELECT value FROM config WHERE keyname=?;", ("foo",)) .await?; assert_eq!(value.unwrap(), "bar"); diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 55c979a87..9493164ea 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -30,7 +30,7 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool // set raw config inside the transaction transaction.execute( "INSERT INTO config (keyname, value) VALUES (?, ?);", - paramsv![VERSION_CFG, format!("{dbversion_before_update}")], + (VERSION_CFG, format!("{dbversion_before_update}")), )?; Ok(()) }) @@ -746,7 +746,7 @@ impl Sql { fn set_db_version_trans(transaction: &mut rusqlite::Transaction, version: i32) -> Result<()> { transaction.execute( "UPDATE config SET value=? WHERE keyname=?;", - params![format!("{version}"), VERSION_CFG], + (format!("{version}"), VERSION_CFG), )?; Ok(()) } diff --git a/src/stock_str.rs b/src/stock_str.rs index 6a1f3ccf6..d41e43057 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -407,6 +407,9 @@ pub enum StockMessage { #[strum(props(fallback = "Scan to set up second device for %1$s"))] BackupTransferQr = 162, + + #[strum(props(fallback = "ℹ️ Account transferred to your second device."))] + BackupTransferMsgBody = 163, } impl StockMessage { @@ -1261,6 +1264,10 @@ pub(crate) async fn backup_transfer_qr(context: &Context) -> Result { .replace1(&full_name)) } +pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String { + translated(context, StockMessage::BackupTransferMsgBody).await +} + impl Context { /// Set the stock string for the [StockMessage]. /// diff --git a/src/sync.rs b/src/sync.rs index ea10cbce9..008c450fd 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -65,10 +65,7 @@ impl Context { let item = SyncItem { timestamp, data }; let item = serde_json::to_string(&item)?; self.sql - .execute( - "INSERT INTO multi_device_sync (item) VALUES(?);", - paramsv![item], - ) + .execute("INSERT INTO multi_device_sync (item) VALUES(?);", (item,)) .await?; Ok(()) diff --git a/src/test_utils.rs b/src/test_utils.rs index 07fbef649..003b3578c 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -18,7 +18,10 @@ use tokio::runtime::Handle; use tokio::sync::RwLock; use tokio::task; -use crate::chat::{self, Chat, ChatId, MessageListOptions}; +use crate::chat::{ + self, add_to_chat_contacts_table, create_group_chat, Chat, ChatId, MessageListOptions, + ProtectionStatus, +}; use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::Chattype; @@ -425,7 +428,7 @@ impl TestContext { }; self.ctx .sql - .execute("DELETE FROM smtp WHERE id=?;", paramsv![rowid]) + .execute("DELETE FROM smtp WHERE id=?;", (rowid,)) .await .expect("failed to remove job"); update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered) @@ -702,6 +705,25 @@ impl TestContext { ); } } + + pub async fn create_group_with_members( + &self, + protect: ProtectionStatus, + chat_name: &str, + members: &[&TestContext], + ) -> ChatId { + let chat_id = create_group_chat(self, protect, chat_name).await.unwrap(); + let mut to_add = vec![]; + for member in members { + let contact = self.add_or_lookup_contact(member).await; + to_add.push(contact.id); + } + add_to_chat_contacts_table(self, chat_id, &to_add) + .await + .unwrap(); + + chat_id + } } impl Deref for TestContext { diff --git a/src/token.rs b/src/token.rs index 30acb81a0..2db815864 100644 --- a/src/token.rs +++ b/src/token.rs @@ -35,7 +35,7 @@ pub async fn save( .sql .execute( "INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);", - paramsv![namespace, foreign_id, token, time()], + (namespace, foreign_id, token, time()), ) .await?, None => { @@ -43,7 +43,7 @@ pub async fn save( .sql .execute( "INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);", - paramsv![namespace, token, time()], + (namespace, token, time()), ) .await? } @@ -71,7 +71,7 @@ pub async fn lookup( .sql .query_get_value( "SELECT token FROM tokens WHERE namespc=? AND foreign_id=? ORDER BY timestamp DESC LIMIT 1;", - paramsv![namespace, chat_id], + (namespace, chat_id), ) .await? } @@ -81,7 +81,7 @@ pub async fn lookup( .sql .query_get_value( "SELECT token FROM tokens WHERE namespc=? AND foreign_id=0 ORDER BY timestamp DESC LIMIT 1;", - paramsv![namespace], + (namespace,), ) .await? } @@ -108,7 +108,7 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> boo .sql .exists( "SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;", - paramsv![namespace, token], + (namespace, token), ) .await .unwrap_or_default() @@ -119,7 +119,7 @@ pub async fn delete(context: &Context, namespace: Namespace, token: &str) -> Res .sql .execute( "DELETE FROM tokens WHERE namespc=? AND token=?;", - paramsv![namespace, token], + (namespace, token), ) .await?; Ok(()) diff --git a/src/tools.rs b/src/tools.rs index 7ce5eacde..23b66c934 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -13,7 +13,7 @@ use std::time::{Duration, SystemTime}; use anyhow::{bail, Context as _, Result}; use base64::Engine as _; -use chrono::{Local, TimeZone}; +use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone}; use futures::{StreamExt, TryStreamExt}; use mailparse::dateparse; use mailparse::headers::Headers; @@ -26,7 +26,6 @@ use crate::constants::{DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS}; use crate::context::Context; use crate::events::EventType; use crate::message::{Message, Viewtype}; -use crate::provider::get_provider_update_timestamp; use crate::stock_str; /// Shortens a string to a specified length and adds "[...]" to the @@ -163,12 +162,23 @@ pub(crate) fn create_smeared_timestamps(context: &Context, count: usize) -> i64 context.smeared_timestamp.create_n(now, count as i64) } +/// Returns the last release timestamp as a unix timestamp compatible for comparison with time() and +/// database times. +pub fn get_release_timestamp() -> i64 { + NaiveDateTime::new( + *crate::release::DATE, + NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + ) + .timestamp_millis() + / 1_000 +} + // if the system time is not plausible, once a day, add a device message. // for testing we're using time() as that is also used for message timestamps. // moreover, add a warning if the app is outdated. pub(crate) async fn maybe_add_time_based_warnings(context: &Context) { - if !maybe_warn_on_bad_time(context, time(), get_provider_update_timestamp()).await { - maybe_warn_on_outdated(context, time(), get_provider_update_timestamp()).await; + if !maybe_warn_on_bad_time(context, time(), get_release_timestamp()).await { + maybe_warn_on_outdated(context, time(), get_release_timestamp()).await; } } @@ -550,9 +560,11 @@ impl rusqlite::types::ToSql for EmailAddress { } } -/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines. +/// Sanitizes user input +/// - strip newlines +/// - strip malicious bidi characters pub(crate) fn improve_single_line_input(input: &str) -> String { - input.replace(['\n', '\r'], " ").trim().to_string() + strip_rtlo_characters(input.replace(['\n', '\r'], " ").trim()) } pub(crate) trait IsNoneOrEmpty { @@ -691,6 +703,13 @@ pub(crate) fn buf_decompress(buf: &[u8]) -> Result> { Ok(mem::take(decompressor.get_mut())) } +const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}']; +/// This method strips all occurances of the RTLO Unicode character. +/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)? +pub(crate) fn strip_rtlo_characters(input_str: &str) -> String { + input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "") +} + #[cfg(test)] mod tests { #![allow(clippy::indexing_slicing)] @@ -1140,17 +1159,17 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true"; / 1_000; // a correct time must not add a device message - maybe_warn_on_bad_time(&t, timestamp_now, get_provider_update_timestamp()).await; + maybe_warn_on_bad_time(&t, timestamp_now, get_release_timestamp()).await; let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); // we cannot find out if a date in the future is wrong - a device message is not added - maybe_warn_on_bad_time(&t, timestamp_future, get_provider_update_timestamp()).await; + maybe_warn_on_bad_time(&t, timestamp_future, get_release_timestamp()).await; let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 0); // a date in the past must add a device message - maybe_warn_on_bad_time(&t, timestamp_past, get_provider_update_timestamp()).await; + maybe_warn_on_bad_time(&t, timestamp_past, get_release_timestamp()).await; let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); let device_chat_id = chats.get_chat_id(0).unwrap(); @@ -1158,31 +1177,21 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true"; assert_eq!(msgs.len(), 1); // the message should be added only once a day - test that an hour later and nearly a day later - maybe_warn_on_bad_time( - &t, - timestamp_past + 60 * 60, - get_provider_update_timestamp(), - ) - .await; + maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60, get_release_timestamp()).await; let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); assert_eq!(msgs.len(), 1); maybe_warn_on_bad_time( &t, timestamp_past + 60 * 60 * 24 - 1, - get_provider_update_timestamp(), + get_release_timestamp(), ) .await; let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); assert_eq!(msgs.len(), 1); // next day, there should be another device message - maybe_warn_on_bad_time( - &t, - timestamp_past + 60 * 60 * 24, - get_provider_update_timestamp(), - ) - .await; + maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60 * 24, get_release_timestamp()).await; let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 1); assert_eq!(device_chat_id, chats.get_chat_id(0).unwrap()); @@ -1200,7 +1209,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true"; maybe_warn_on_outdated( &t, timestamp_now + 180 * 24 * 60 * 60, - get_provider_update_timestamp(), + get_release_timestamp(), ) .await; let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); @@ -1210,7 +1219,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true"; maybe_warn_on_outdated( &t, timestamp_now + 365 * 24 * 60 * 60, - get_provider_update_timestamp(), + get_release_timestamp(), ) .await; let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); @@ -1224,13 +1233,13 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true"; maybe_warn_on_outdated( &t, timestamp_now + (365 + 1) * 24 * 60 * 60, - get_provider_update_timestamp(), + get_release_timestamp(), ) .await; maybe_warn_on_outdated( &t, timestamp_now + (365 + 2) * 24 * 60 * 60, - get_provider_update_timestamp(), + get_release_timestamp(), ) .await; let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); @@ -1245,7 +1254,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true"; maybe_warn_on_outdated( &t, timestamp_now + (365 + 33) * 24 * 60 * 60, - get_provider_update_timestamp(), + get_release_timestamp(), ) .await; let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); @@ -1255,6 +1264,18 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true"; assert_eq!(msgs.len(), test_len + 1); } + #[test] + fn test_get_release_timestamp() { + let timestamp_past = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + ) + .timestamp_millis() + / 1_000; + assert!(get_release_timestamp() <= time()); + assert!(get_release_timestamp() > timestamp_past); + } + #[test] fn test_remove_subject_prefix() { assert_eq!(remove_subject_prefix("Subject"), "Subject"); diff --git a/src/update_helper.rs b/src/update_helper.rs index ed140dfdd..539ad85c2 100644 --- a/src/update_helper.rs +++ b/src/update_helper.rs @@ -31,7 +31,7 @@ impl Context { if update { transaction.execute( "UPDATE contacts SET param=? WHERE id=?", - params![param.to_string(), contact_id], + (param.to_string(), contact_id), )?; } Ok(update) @@ -61,7 +61,7 @@ impl ChatId { if update { transaction.execute( "UPDATE chats SET param=? WHERE id=?", - params![param.to_string(), self], + (param.to_string(), self), )?; } Ok(update) diff --git a/src/webxdc.rs b/src/webxdc.rs index 07d7da54a..cf143da12 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -20,6 +20,7 @@ use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::param::Params; use crate::scheduler::InterruptInfo; +use crate::tools::strip_rtlo_characters; use crate::tools::{create_smeared_timestamp, get_abs_path}; use crate::{chat, EventType}; @@ -262,7 +263,7 @@ impl Context { FROM msgs WHERE chat_id=?1 AND hidden=0 ORDER BY timestamp DESC, id DESC LIMIT 1"#, - paramsv![instance.chat_id], + (instance.chat_id,), |row| { let last_msg_id: MsgId = row.get(0)?; let last_from_id: ContactId = row.get(1)?; @@ -293,13 +294,13 @@ impl Context { can_info_msg: bool, from_id: ContactId, ) -> Result { - let update_str = update_str.trim(); + let update_str = strip_rtlo_characters(update_str.trim()); if update_str.is_empty() { bail!("create_status_update_record: empty update."); } let status_update_item: StatusUpdateItem = - if let Ok(item) = serde_json::from_str::(update_str) { + if let Ok(item) = serde_json::from_str::(&update_str) { item } else { bail!("create_status_update_record: no valid update item."); @@ -351,7 +352,9 @@ impl Context { .param .update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)? { - instance.param.set(Param::WebxdcSummary, summary); + instance + .param + .set(Param::WebxdcSummary, strip_rtlo_characters(summary)); param_changed = true; } } @@ -384,7 +387,7 @@ impl Context { .sql .insert( "INSERT INTO msgs_status_updates (msg_id, update_item) VALUES(?, ?);", - paramsv![instance_id, serde_json::to_string(&status_update_item)?], + (instance_id, serde_json::to_string(&status_update_item)?), ) .await?; let status_update_serial = StatusUpdateSerial(u32::try_from(rowid)?); @@ -432,7 +435,7 @@ impl Context { "INSERT INTO smtp_status_updates (msg_id, first_serial, last_serial, descr) VALUES(?, ?, ?, ?) ON CONFLICT(msg_id) DO UPDATE SET last_serial=excluded.last_serial, descr=excluded.descr", - paramsv![instance.id, status_update_serial, status_update_serial, descr], + (instance.id, status_update_serial, status_update_serial, descr), ).await?; self.scheduler .interrupt_smtp(InterruptInfo::new(false)) @@ -576,7 +579,7 @@ impl Context { .sql .query_map( "SELECT update_item, id FROM msgs_status_updates WHERE msg_id=? AND id>? ORDER BY id", - paramsv![instance_msg_id, last_known_serial], + (instance_msg_id, last_known_serial), |row| { let update_item_str = row.get::<_, String>(0)?; let serial = row.get::<_, StatusUpdateSerial>(1)?; @@ -628,11 +631,11 @@ impl Context { .sql .query_map( "SELECT update_item FROM msgs_status_updates WHERE msg_id=? AND id>=? AND id<=? ORDER BY id", - paramsv![ + ( instance_msg_id, range.map(|r|r.0).unwrap_or(StatusUpdateSerial(0)), range.map(|r|r.1).unwrap_or(StatusUpdateSerial(u32::MAX)), - ], + ), |row| row.get::<_, String>(0), |rows| { let mut json = String::default();