Compare commits

...

6 Commits

Author SHA1 Message Date
link2xt
31e83160f2 build: update all crates to Rust 2024 edition
Largest change in the FFI crate.
With 2024 (but not 2021) edition unsafe code
inside unsafe functions should be marked separately
so we can mark exactly the code that is unsafe.
Some CFFI functions even have no unsafe code inside.

Most interesting change is that .strdup()
functions are not marked as unsafe anymore.
They are allocating memory and return raw pointers,
but there is nothing unsafe about it.
Only using the returned raw pointers is unsafe.
This way calls to .strdup() don't have to be marked
with unsafe{} blocks.
2026-07-22 20:37:52 +00:00
holger krekel
0e4574cd41 feat: read SMTP recipient limit from relay IMAP metadata
Remove is_chatmail flag, and rely on IMAP metadata
advertising the recipients limit (usually 1000),
falling back to 50 (or fewer in some exceptional cases)
just as before when is_chatmail was false.

Also, server metadata is now keyed per transport,
which in the future eases collecting ICE servers
from all relays (instead of just any first relay).
2026-07-22 20:31:50 +02:00
link2xt
3a913dfb07 feat: enable TLS certificate compression
Enabling brotli does not add new dependencies,
we have it already.
Documentation at <https://docs.rs/rustls/0.23.42/rustls/compress/>
recommends enabling at least brotli.

zlib feature is not enabled as it pulls in duplicate zlib-rs 0.6.6
zlib was even never supported in Chromium (while brotli is).
It might still be interesting to enable it in the future
because it seems to be the only option supported by OpenSSL
and there may be servers that support zlib but not brotli.

RFC 8879 specifies zstd as well, but there is no feature to enable it yet.

This also brings back explicit TLS 1.2 feature that was reverted.
We don't want TLS 1.2 support to be accidentally dropped
if no dependencies explicitly require it anymore.
2026-07-22 14:03:36 +00:00
link2xt
bdd9d96844 feat: accept messages from key contacts with forged From address
From address is not used for key contacts
other than as the address to send replies to.
2026-07-21 19:59:32 +00:00
dependabot[bot]
4e2238a38f chore(deps): bump actions/setup-node from 6 to 7
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-21 19:57:42 +00:00
link2xt
8c53f86fbc chore: bump version to 2.57.0-dev 2026-07-21 19:35:26 +00:00
46 changed files with 1806 additions and 1684 deletions

View File

@@ -514,7 +514,7 @@ jobs:
# Configure Node.js for publishing.
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: 24
registry-url: "https://registry.npmjs.org"

View File

@@ -24,7 +24,7 @@ jobs:
# Configure Node.js for publishing.
# Check <https://docs.npmjs.com/trusted-publishers> for the version requirements.
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: 24
registry-url: "https://registry.npmjs.org"

View File

@@ -21,7 +21,7 @@ jobs:
show-progress: false
persist-credentials: false
- name: Use Node.js 24
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: 24
- name: Add Rust cache

View File

@@ -90,7 +90,7 @@ jobs:
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Use Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: 24
- name: npm install

12
Cargo.lock generated
View File

@@ -1317,7 +1317,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.56.0"
version = "2.57.0-dev"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1425,7 +1425,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.56.0"
version = "2.57.0-dev"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1446,7 +1446,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.56.0"
version = "2.57.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1462,7 +1462,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.56.0"
version = "2.57.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1491,7 +1491,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.56.0"
version = "2.57.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -5222,6 +5222,8 @@ version = "0.23.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"brotli",
"brotli-decompressor",
"log",
"once_cell",
"ring",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.56.0"
version = "2.57.0-dev"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.89"
@@ -100,7 +100,7 @@ tagger = "4.3.4"
textwrap = "0.16.2"
thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "brotli"] }
tokio-stream = { version = "0.1.17", features = ["fs"] }
astral-tokio-tar = { version = "0.6.3", default-features = false }
tokio-util = { workspace = true }

View File

@@ -1,7 +1,7 @@
[package]
name = "deltachat-contact-tools"
version = "0.0.0" # No semver-stable versioning
edition = "2021"
edition = "2024"
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
license = "MPL-2.0"

View File

@@ -31,12 +31,12 @@ use std::fmt;
use std::ops::Deref;
use std::sync::LazyLock;
use anyhow::bail;
use anyhow::Result;
use anyhow::bail;
use regex::Regex;
mod vcard;
pub use vcard::{make_vcard, parse_vcard, VcardContact};
pub use vcard::{VcardContact, make_vcard, parse_vcard};
/// Valid contact address.
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@@ -220,7 +220,10 @@ END:VCARD
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
assert_eq!(
contacts[0].profile_image.as_deref().unwrap(),
"/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q=="
);
}
}
@@ -244,7 +247,10 @@ END:VCARD",
assert_eq!(contacts.len(), 1);
assert_eq!(&contacts[0].addr, "alice@example.org");
assert_eq!(&contacts[0].authname, "Alice Wonderland");
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
assert_eq!(
contacts[0].key.as_ref().unwrap(),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[0].profile_image, None);
}
@@ -272,9 +278,15 @@ END:VCARD",
assert_eq!(contacts.len(), 1);
assert_eq!(&contacts[0].addr, "alice@example.org");
assert_eq!(&contacts[0].authname, "Alice");
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
assert_eq!(
contacts[0].key.as_ref().unwrap(),
"xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=="
);
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
assert_eq!(
contacts[0].profile_image.as_ref().unwrap(),
"/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z"
);
}
#[test]

View File

@@ -1,8 +1,8 @@
[package]
name = "deltachat_ffi"
version = "2.56.0"
version = "2.57.0-dev"
description = "Deltachat FFI"
edition = "2018"
edition = "2024"
readme = "README.md"
license = "MPL-2.0"

File diff suppressed because it is too large Load Diff

View File

@@ -17,13 +17,15 @@ use std::ptr;
/// }
/// ```
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
let ret: *mut libc::c_char = if !s.is_null() {
libc::strdup(s)
} else {
libc::calloc(1, 1) as *mut libc::c_char
};
assert!(!ret.is_null());
ret
unsafe {
let ret: *mut libc::c_char = if !s.is_null() {
libc::strdup(s)
} else {
libc::calloc(1, 1) as *mut libc::c_char
};
assert!(!ret.is_null());
ret
}
}
/// Error type for the [OsStrExt] trait
@@ -164,34 +166,40 @@ pub(crate) trait Strdup {
/// This function will panic when the original string contains an
/// interior null byte as this can not be represented in raw C
/// strings.
unsafe fn strdup(&self) -> *mut libc::c_char;
fn strdup(&self) -> *mut libc::c_char;
}
impl Strdup for str {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::new_lossy(self);
dc_strdup(tmp.as_ptr())
fn strdup(&self) -> *mut libc::c_char {
unsafe {
let tmp = CString::new_lossy(self);
dc_strdup(tmp.as_ptr())
}
}
}
impl Strdup for String {
unsafe fn strdup(&self) -> *mut libc::c_char {
fn strdup(&self) -> *mut libc::c_char {
let s: &str = self;
s.strdup()
}
}
impl Strdup for std::path::Path {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
dc_strdup(tmp.as_ptr())
fn strdup(&self) -> *mut libc::c_char {
unsafe {
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
dc_strdup(tmp.as_ptr())
}
}
}
impl Strdup for [u8] {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::new_lossy(self);
dc_strdup(tmp.as_ptr())
fn strdup(&self) -> *mut libc::c_char {
unsafe {
let tmp = CString::new_lossy(self);
dc_strdup(tmp.as_ptr())
}
}
}
@@ -209,15 +217,15 @@ pub(crate) trait OptStrdup {
/// Allocate a new raw C `*char` version of this string, or NULL.
///
/// See [Strdup::strdup] for details.
unsafe fn strdup(&self) -> *mut libc::c_char;
fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> OptStrdup for Option<T> {
unsafe fn strdup(&self) -> *mut libc::c_char {
fn strdup(&self) -> *mut libc::c_char {
match self {
Some(s) => {
let tmp = CString::new_lossy(s.as_ref());
dc_strdup(tmp.as_ptr())
unsafe { dc_strdup(tmp.as_ptr()) }
}
None => ptr::null_mut(),
}
@@ -258,11 +266,9 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
assert!(!s.is_null(), "cannot be used on null pointers");
use std::os::unix::ffi::OsStrExt;
unsafe {
let c_str = std::ffi::CStr::from_ptr(s).to_bytes();
let os_str = std::ffi::OsStr::from_bytes(c_str);
std::path::Path::new(os_str)
}
let c_str = unsafe { std::ffi::CStr::from_ptr(s) }.to_bytes();
let os_str = std::ffi::OsStr::from_bytes(c_str);
std::path::Path::new(os_str)
}
// as_path() implementation for windows, documented above.

View File

@@ -1,8 +1,8 @@
[package]
name = "deltachat-jsonrpc"
version = "2.56.0"
version = "2.57.0-dev"
description = "DeltaChat JSON-RPC API"
edition = "2021"
edition = "2024"
license = "MPL-2.0"
repository = "https://github.com/chatmail/core"

View File

@@ -5,26 +5,27 @@ use std::sync::Arc;
use std::time::Duration;
use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
use anyhow::{Context, Result, anyhow, bail, ensure};
use deltachat::EventEmitter;
pub use deltachat::accounts::Accounts;
use deltachat::blob::BlobObject;
use deltachat::calls::ice_servers;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
get_chat_msgs_ex, markfresh_chat, marknoticed_all_chats, marknoticed_chat,
remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
self, Chat, ChatId, ChatItem, MessageListOptions, add_contact_to_chat, forward_msgs,
forward_msgs_2ctx, get_chat_media, get_chat_msgs, get_chat_msgs_ex, markfresh_chat,
marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::{get_all_ui_config_keys, Config};
use deltachat::config::{Config, get_all_ui_config_keys};
use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::contact::{Contact, ContactId, Origin, may_be_valid_addr};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::{
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
markseen_msgs, Message, MessageState, MsgId, Viewtype,
self, Message, MessageState, MsgId, Viewtype, delete_msgs_ex, get_existing_msg_ids,
get_msg_read_receipt_count, get_msg_read_receipts, markseen_msgs,
};
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
@@ -37,10 +38,9 @@ use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::storage_usage::{get_blobdir_storage_usage, get_storage_usage};
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
use tokio::sync::{Mutex, RwLock, watch};
use types::login_param::EnteredLoginParam;
use yerpc::rpc;
@@ -67,7 +67,7 @@ use self::types::{
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
},
};
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
use crate::api::types::login_param::TransportListEntry;
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
@@ -2295,7 +2295,7 @@ impl CommandApi {
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
let blob = message.get_webxdc_blob(&ctx, &path).await?;
use base64::{engine::general_purpose, Engine as _};
use base64::{Engine as _, engine::general_purpose};
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
}

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result};
use deltachat::calls::{call_state, CallState};
use deltachat::calls::{CallState, call_state};
use deltachat::context::Context;
use deltachat::message::MsgId;
use serde::Serialize;

View File

@@ -1,7 +1,7 @@
use std::time::{Duration, SystemTime};
use anyhow::{bail, Context as _, Result};
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
use anyhow::{Context as _, Result, bail};
use deltachat::chat::{self, ChatVisibility, get_chat_contacts, get_past_chat_contacts};
use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
use deltachat::contact::{Contact, ContactId};

View File

@@ -4,7 +4,7 @@ use deltachat::chatlist::get_last_message_for_chat;
use deltachat::constants::*;
use deltachat::contact::Contact;
use deltachat::{
chat::{get_chat_contacts, ChatVisibility},
chat::{ChatVisibility, get_chat_contacts},
chatlist::Chatlist,
};
use num_traits::cast::ToPrimitive;

View File

@@ -16,7 +16,7 @@ pub struct HttpResponse {
impl From<CoreHttpResponse> for HttpResponse {
fn from(response: CoreHttpResponse) -> Self {
use base64::{engine::general_purpose, Engine as _};
use base64::{Engine as _, engine::general_purpose};
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
let mimetype = response.mimetype;
let encoding = response.encoding;

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.56.0"
"version": "2.57.0-dev"
}

View File

@@ -2,7 +2,7 @@
name = "ratelimit"
version = "1.0.0"
description = "Token bucket implementation"
edition = "2021"
edition = "2024"
license = "MPL-2.0"
[dependencies]

View File

@@ -1,8 +1,8 @@
[package]
name = "deltachat-repl"
version = "2.56.0"
version = "2.57.0-dev"
license = "MPL-2.0"
edition = "2021"
edition = "2024"
repository = "https://github.com/chatmail/core"
[dependencies]

View File

@@ -5,7 +5,7 @@ use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
use anyhow::{bail, ensure, Result};
use anyhow::{Result, bail, ensure};
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
use deltachat::chatlist::*;
use deltachat::constants::*;
@@ -1225,12 +1225,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
let (width, height) = get_filemeta(&buf)?;
println!("width={width}, height={height}");
} else {
let Ok(buf) = read_file(&context, Path::new(arg1)).await else {
bail!("Command failed.");
}
};
let (width, height) = get_filemeta(&buf)?;
println!("width={width}, height={height}");
}
"estimatedeletion" => {
ensure!(!arg1.is_empty(), "Argument <seconds> missing");

View File

@@ -9,12 +9,12 @@ extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned};
use anyhow::{bail, Error};
use anyhow::{Error, bail};
use deltachat::EventType;
use deltachat::chat::ChatId;
use deltachat::context::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::EventType;
use log::{error, info, warn};
use nu_ansi_term::Color;
use rustyline::completion::{Completer, FilenameCompleter, Pair};

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.56.0"
version = "2.57.0-dev"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [

View File

@@ -1,8 +1,8 @@
[package]
name = "deltachat-rpc-server"
version = "2.56.0"
version = "2.57.0-dev"
description = "DeltaChat JSON-RPC server"
edition = "2021"
edition = "2024"
readme = "README.md"
license = "MPL-2.0"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.56.0"
"version": "2.57.0-dev"
}

View File

@@ -6,7 +6,7 @@ use std::env;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use deltachat::constants::DC_VERSION_STR;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;

View File

@@ -2,7 +2,7 @@
name = "deltachat-time"
version = "1.0.0"
description = "Time-related tools"
edition = "2021"
edition = "2024"
license = "MPL-2.0"
[dependencies]

View File

@@ -1,7 +1,7 @@
[package]
name = "deltachat_derive"
version = "2.0.0"
edition = "2018"
edition = "2024"
license = "MPL-2.0"
[lib]

View File

@@ -14,7 +14,7 @@ pub fn to_sql_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
let r#gen = quote! {
impl rusqlite::types::ToSql for #name {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let num = *self as i64;
@@ -24,7 +24,7 @@ pub fn to_sql_derive(input: TokenStream) -> TokenStream {
}
}
};
gen.into()
r#gen.into()
}
#[proc_macro_derive(FromSql)]
@@ -32,7 +32,7 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
let r#gen = quote! {
impl rusqlite::types::FromSql for #name {
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
let inner = rusqlite::types::FromSql::column_result(col)?;
@@ -44,5 +44,5 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
}
}
};
gen.into()
r#gen.into()
}

View File

@@ -2,7 +2,7 @@
name = "format-flowed"
version = "1.0.0"
description = "format=flowed support"
edition = "2021"
edition = "2024"
license = "MPL-2.0"
keywords = ["email"]

View File

@@ -224,8 +224,7 @@ mod tests {
fn test_unformat_flowed() {
let text = "this is a very long message that should be wrapped using format=flowed and \n\
unwrapped on the receiver";
let expected =
"this is a very long message that should be wrapped using format=flowed and \
let expected = "this is a very long message that should be wrapped using format=flowed and \
unwrapped on the receiver";
assert_eq!(unformat_flowed(text, false), expected);
@@ -255,8 +254,7 @@ mod tests {
assert_eq!(format_flowed_quote(quote), expected);
let quote = "this is a very long quote that should be wrapped using format=flowed and unwrapped on the receiver";
let expected =
"> this is a very long quote that should be wrapped using format=flowed and \r\n\
let expected = "> this is a very long quote that should be wrapped using format=flowed and \r\n\
> unwrapped on the receiver";
assert_eq!(format_flowed_quote(quote), expected);
}

View File

@@ -2,7 +2,7 @@
name = "deltachat-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
edition = "2024"
license = "MPL-2.0"
[dev-dependencies]

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.56.0"
version = "2.57.0-dev"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"

View File

@@ -770,7 +770,7 @@ pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
/// because it itself cannot utilize DNS. See
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
pub async fn ice_servers(context: &Context) -> Result<String> {
if let Some(ref metadata) = *context.metadata.read().await {
if let Some(metadata) = context.metadata.read().await.values().next() {
let ice_servers = resolve_ice_servers(context, metadata.ice_servers.clone()).await?;
Ok(ice_servers)
} else {

View File

@@ -2969,7 +2969,6 @@ WHERE id=?
)
.await?;
let chunk_size = context.get_max_smtp_rcpt_to().await?;
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new();
@@ -2979,16 +2978,16 @@ WHERE id=?
(),
)?;
}
let mut stmt = t.prepare(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)",
)?;
for recipients_chunk in recipients.chunks(chunk_size) {
let recipients_chunk = recipients_chunk.join(" ");
if !recipients.is_empty() {
let mut stmt = t.prepare(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)",
)?;
let all_recipients = recipients.join(" ");
if let Some(pre_msg) = &rendered_pre_msg {
let row_id = stmt.execute((
&pre_msg.rfc724_mid,
&recipients_chunk,
&all_recipients,
&pre_msg.message,
msg.id,
))?;
@@ -2996,7 +2995,7 @@ WHERE id=?
}
let row_id = stmt.execute((
&rendered_msg.rfc724_mid,
&recipients_chunk,
&all_recipients,
&rendered_msg.message,
msg.id,
))?;

View File

@@ -17,10 +17,9 @@ use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::Provider;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{get_abs_path, time};
use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports};
use crate::transport::{add_pseudo_transport, send_sync_transports};
use crate::{constants, stats};
/// The available configuration keys.
@@ -614,16 +613,6 @@ impl Context {
self.get_config_bool(Config::MdnsEnabled).await
}
/// Gets the configured provider.
///
/// The provider is determined by the current primary transport.
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
let provider = ConfiguredLoginParam::load(self)
.await?
.and_then(|(_transport_id, param)| param.provider);
Ok(provider)
}
/// Gets configured "delete_device_after" value.
///
/// `None` means never delete the message, `Some(x)` means delete

View File

@@ -180,13 +180,10 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
/// usage by UIs.
pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting
// `max_smtp_rcpt_to` in the provider db.
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
/// Same as `DEFAULT_MAX_SMTP_RCPT_TO`, but for chatmail relays.
pub(crate) const DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO: usize = 999;
// Fallback for the maximum number of recipients in SMTP's `RCPT TO:`;
// recipient lists exceeding the limit are sent in chunks.
// Relays typically advertise their limit via IMAP METADATA.
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: u32 = 50;
/// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours

View File

@@ -264,8 +264,8 @@ pub struct InnerContext {
/// <https://datatracker.ietf.org/doc/html/rfc2971>
pub(crate) server_id: RwLock<Option<HashMap<String, String>>>,
/// IMAP METADATA.
pub(crate) metadata: RwLock<Option<ServerMetadata>>,
/// IMAP METADATA, per transport id.
pub(crate) metadata: RwLock<BTreeMap<u32, ServerMetadata>>,
/// ID for this `Context` in the current process.
///
@@ -492,7 +492,7 @@ impl Context {
quota: RwLock::new(BTreeMap::new()),
new_msgs_notify,
server_id: RwLock::new(None),
metadata: RwLock::new(None),
metadata: RwLock::new(BTreeMap::new()),
creation_time: tools::Time::now(),
last_error: parking_lot::RwLock::new("".to_string()),
migration_error: parking_lot::RwLock::new(None),
@@ -572,20 +572,24 @@ impl Context {
self.get_config_bool(Config::IsChatmail).await
}
/// Returns maximum number of recipients the provider allows to send a single email to.
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<usize> {
let is_chatmail = self.is_chatmail().await?;
let val = self
.get_configured_provider()
.await?
/// Returns maximum number of recipients a single email can be sent to.
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<u32> {
let Some((transport_id, param)) = ConfiguredLoginParam::load(self).await? else {
bail!("Not configured");
};
let metadata_limit = self
.metadata
.read()
.await
.get(&transport_id)
.and_then(|metadata| metadata.max_smtp_rcpt_to);
if let Some(limit) = metadata_limit {
return Ok(limit);
}
let val = param
.provider
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or_else(
|| match is_chatmail {
true => constants::DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO,
false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
},
usize::from,
);
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, u32::from);
Ok(val)
}
@@ -915,7 +919,7 @@ impl Context {
.unwrap_or_else(|| "<unset>".to_string()),
);
if let Some(metadata) = &*self.metadata.read().await {
if let Some(metadata) = self.metadata.read().await.values().next() {
if let Some(comment) = &metadata.comment {
res.insert("imap_server_comment", format!("{comment:?}"));
}

View File

@@ -9,6 +9,7 @@ use std::{
collections::{BTreeMap, HashMap},
iter::Peekable,
mem::take,
str::FromStr,
sync::atomic::Ordering,
time::{Duration, UNIX_EPOCH},
};
@@ -124,6 +125,9 @@ pub(crate) struct ServerMetadata {
pub iroh_relay: Option<Url>,
/// Maximum number of recipients for SMTP `RCPT TO:`.
pub max_smtp_rcpt_to: Option<u32>,
/// ICE servers for WebRTC calls.
pub ice_servers: Vec<UnresolvedIceServer>,
@@ -1324,11 +1328,12 @@ impl Session {
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
let mut lock = context.metadata.write().await;
let transport_id = self.transport_id();
if !self.can_metadata() {
*lock = Some(Default::default());
lock.entry(transport_id).or_default();
}
if let Some(ref mut old_metadata) = *lock {
if let Some(old_metadata) = lock.get_mut(&transport_id) {
let now = time();
// Refresh TURN server credentials if they expire in 12 hours.
@@ -1378,6 +1383,7 @@ impl Session {
let mut comment = None;
let mut admin = None;
let mut iroh_relay = None;
let mut max_smtp_rcpt_to = None;
let mut ice_servers = None;
let mut ice_servers_expiration_timestamp = 0;
@@ -1387,7 +1393,7 @@ impl Session {
.get_metadata(
mailbox,
options,
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn /shared/vendor/deltachat/maxsmtprecipients)",
)
.await?;
for m in metadata {
@@ -1423,6 +1429,18 @@ impl Session {
}
}
}
"/shared/vendor/deltachat/maxsmtprecipients" => {
if let Some(value) = m.value {
if let Ok(limit) = u32::from_str(&value) {
max_smtp_rcpt_to = Some(limit);
} else {
warn!(
context,
"Got invalid maxsmtprecipients metadata: {:?}.", value
);
}
}
}
_ => {}
}
}
@@ -1434,13 +1452,17 @@ impl Session {
create_fallback_ice_servers()
};
*lock = Some(ServerMetadata {
comment,
admin,
iroh_relay,
ice_servers,
ice_servers_expiration_timestamp,
});
lock.insert(
transport_id,
ServerMetadata {
comment,
admin,
iroh_relay,
max_smtp_rcpt_to,
ice_servers,
ice_servers_expiration_timestamp,
},
);
Ok(())
}

View File

@@ -525,8 +525,6 @@ impl MimeMessage {
// let known protected headers from the decrypted
// part override the unencrypted top-level
// Signature was checked for original From, so we
// do not allow overriding it.
let mut inner_from = None;
MimeMessage::merge_headers(
@@ -558,19 +556,22 @@ impl MimeMessage {
// This _might_ be because the sender's mail server
// replaced the sending address, e.g. in a mailing list.
// Or it's because someone is doing some replay attack.
// Resending encrypted messages via mailing lists
// without reencrypting is not useful anyway,
// so we return an error below.
warn!(
context,
"From header in encrypted part doesn't match the outer one",
);
// Return an error from the parser.
// This will result in creating a tombstone
// and no further message processing
// as if the MIME structure is broken.
bail!("From header is forged");
// If there are no valid signatures,
// possibly because we don't have the public key,
// the message will be associated with the address-contact.
// If the address is possibly forged, we trash the message.
if signatures.is_empty() {
// Return an error from the parser.
// This will result in creating a tombstone
// and no further message processing
// as if the MIME structure is broken.
bail!("From header is forged");
}
}
from = inner_from;
}

View File

@@ -242,7 +242,8 @@ impl Context {
.metadata
.read()
.await
.as_ref()
.values()
.next()
.and_then(|conf| conf.iroh_relay.clone())
{
RelayMode::Custom(RelayUrl::from(relay_url).into())

View File

@@ -549,11 +549,13 @@ pub(crate) async fn receive_imf_inner(
// It sometimes happens that a slow server (usually a classical email server)
// receives a message via SMTP,
// but then the connection to the server dies before it sends the OK response.
// In order to handle this case, we delete the SMTP send jobs if we receive our own message via IMAP.
// In order to handle this case, we delete the SMTP send job
// if we receive our own message via IMAP.
//
// Now, messages with long recipient lists are split into multiple SMTP jobs.
// In this case, we only want to delete the SMTP job that was sent to self
// because this is the only chunk we can be sure was sent out.
// Note that messages with long recipient lists are sent out in chunks,
// removing already sent recipients from the job after each chunk.
// Self recipients are added at the end so removing the job
// removes the last chunk which apparently went out fine.
let self_addr = context.get_primary_self_addr().await?;
context
.sql

View File

@@ -3456,6 +3456,8 @@ async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> {
Ok(())
}
/// Tests reception of encrypted and signed message with forged From header
/// when the signature cannot be checked because the public key is not available.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forged_from_and_no_valid_signatures() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -4718,6 +4720,10 @@ async fn test_no_op_member_added_is_trash() -> Result<()> {
Ok(())
}
/// Tests reception of a message with a valid signature and forged From header.
///
/// The message is accepted because the sender contact is associated with the key
/// rather than the address.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forged_from() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -4732,8 +4738,16 @@ async fn test_forged_from() -> Result<()> {
.payload
.replace("bob@example.net", "notbob@example.net");
let msg = alice.recv_msg_opt(&sent_msg).await;
assert!(msg.is_none());
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.text, "hi!");
assert!(msg.get_showpadlock());
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
assert!(contact.is_key_contact());
// We take the address from the encrypted part
// and send replies there.
assert_eq!(contact.get_addr(), "bob@example.net");
Ok(())
}

View File

@@ -401,7 +401,28 @@ pub(crate) async fn send_msg_to_smtp(
)
.collect::<Vec<_>>();
let status = smtp_send(context, &recipients_list, body.as_str(), smtp, Some(msg_id)).await;
let chunk_size = context.get_max_smtp_rcpt_to().await?.max(1);
let mut unsent = recipients_list.as_slice();
let status = loop {
let unsent_len = u32::try_from(unsent.len()).context("Too many SMTP recipients")?;
let split_index = usize::try_from(chunk_size.min(unsent_len))
.context("Failed to convert SMTP chunk size")?;
let (chunk, rest) = unsent.split_at(split_index);
let status = smtp_send(context, chunk, body.as_str(), smtp, Some(msg_id)).await;
if !matches!(status, SendResult::Success) || rest.is_empty() {
break status;
}
let rest_str = rest
.iter()
.map(|a| a.as_ref())
.collect::<Vec<_>>()
.join(" ");
context
.sql
.execute("UPDATE smtp SET recipients=? WHERE id=?", (rest_str, rowid))
.await?;
unsent = rest;
};
match status {
SendResult::Retry => {}

View File

@@ -253,7 +253,6 @@ async fn test_empty_server_list() -> Result<()> {
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
assert_eq!(t.get_configured_provider().await?, Some(*provider));
Ok(())
}