Compare commits

...
Author SHA1 Message Date
link2xt 1f41177da1 fix: do not ignore resync_folders 2025-07-19 10:35:57 +00:00
link2xt dc63340471 fix: do not ignore update_recent_quota errors 2025-07-17 20:03:41 +00:00
link2xt ad13477754 fix: bubble up QUOTA errors 2025-07-17 20:01:46 +00:00
l a2df29515a feat: log the number of read/written bytes on IMAP stream read error (#6924) 2025-07-17 20:01:16 +00:00
link2xt 6df1d165dd feat: log when background fetch of all accounts finishes successfully 2025-07-17 15:46:46 +00:00
cliffmccarthy e03e2d9a68 fix: List e-mail contacts in repl listcontacts command
- After the revisions to support key contacts, the 'listcontacts'
  command in the repl only lists key-contacts.  A separate flag now
  has to be passed to Contact::get_all() to list address contacts.
  This makes it difficult to run the example in README.md because we
  need to see the new contact so we can get its ID for the next
  command in the example, that creates a chat by ID.
- Revised 'listcontacts' command to make a second call to
  Contact::get_all() with the DC_GCL_ADDRESS flag, then print the
  e-mail contacts after the key contacts.
- Revised configuration example in top-level README.md to reflect
  current command output.

fixes #7011
2025-07-17 10:27:14 +02:00
iequidoo 8fc6ea19b4 feat: {ensure_and,logged}_debug_assert: Don't evaluate condition twice 2025-07-16 11:41:45 -03:00
iequidoo c5c947e175 feat(repl): Print errors and debug logs to stderr
Follow-up to 545007aca5.
2025-07-16 11:39:56 -03:00
iequidoo 6d8dff54a7 fix: Ignore protected headers in outer message part (#6357)
Delta Chat always adds protected headers to the inner encrypted or signed message, so if a protected
header is only present in the outer part, it should be ignored because it's probably added by the
server or somebody else. The exceptions are Subject and List-ID because there are known cases when
they are only present in the outer message part.

Also treat any Chat-* headers as protected. This fixes e.g. a case when the server injects a
"Chat-Version" IMF header tricking Delta Chat into thinking that it's a chat message.

Also handle "Auto-Submitted" and "Autocrypt-Setup-Message" as protected headers on the receiver
side, this was apparently forgotten.
2025-07-16 11:39:06 -03:00
18 changed files with 401 additions and 82 deletions
+10 -7
View File
@@ -84,26 +84,29 @@ Create a contact:
```
> addcontact yourfriends@email.org
Command executed successfully.
```
List contacts:
```
> listcontacts
Contact#10: <name unset> <yourfriends@email.org>
Contact#1: Me √√ <your@email.org>
Contact#Contact#Self: Me √ <your@email.org>
1 key contacts.
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
1 address contacts.
```
Create a chat with your friend and send a message:
```
> createchat 10
Single#10 created successfully.
> chat 10
Single#10: yourfriends@email.org [yourfriends@email.org]
Single#Chat#12 created successfully.
> chat 12
Selecting chat Chat#12
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
0 messages.
81.252µs to create this list, 123.625µs to mark all messages as noticed.
> send hi
Message sent.
```
List messages when inside a chat:
+9 -6
View File
@@ -87,7 +87,7 @@ async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
let data = read_file(context, filename).await?;
if let Err(err) = receive_imf(context, &data, false).await {
println!("receive_imf errored: {err:?}");
eprintln!("receive_imf errored: {err:?}");
}
Ok(())
}
@@ -621,7 +621,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Location streaming enabled.");
}
println!("{cnt} chats");
println!("{time_needed:?} to create this list");
eprintln!("{time_needed:?} to create this list");
}
"start-realtime" => {
if arg1.is_empty() {
@@ -731,7 +731,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
println!(
eprintln!(
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
);
}
@@ -985,7 +985,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
query,
);
println!("{time_needed:?} to create this list");
eprintln!("{time_needed:?} to create this list");
}
"draft" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -1151,7 +1151,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"listcontacts" | "contacts" => {
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
log_contactlist(&context, &contacts).await?;
println!("{} contacts.", contacts.len());
println!("{} key contacts.", contacts.len());
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
log_contactlist(&context, &addrcontacts).await?;
println!("{} address contacts.", addrcontacts.len());
}
"addcontact" => {
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
@@ -1224,7 +1227,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(&context, arg1).await {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => println!("Cannot set config from QR code: {err:?}"),
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
}
}
"createqrsvg" => {
+3 -3
View File
@@ -311,7 +311,7 @@ impl Validator for DcHelper {}
async fn start(args: Vec<String>) -> Result<(), Error> {
if args.len() < 2 {
println!("Error: Bad arguments, expected [db-name].");
eprintln!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = ContextBuilder::new(args[1].clone().into())
@@ -366,7 +366,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
false
}
Err(err) => {
println!("Error: {err:#}");
eprintln!("Error: {err:#}");
true
}
}
@@ -381,7 +381,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
break;
}
Err(err) => {
println!("Error: {err:#}");
eprintln!("Error: {err:#}");
break;
}
}
+8 -2
View File
@@ -353,11 +353,11 @@ impl Accounts {
/// This is an auxiliary function and not part of public API.
/// Use [Accounts::background_fetch] instead.
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
let n_accounts = accounts.len();
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Starting background fetch for {} accounts.",
accounts.len()
"Starting background fetch for {n_accounts} accounts."
)),
});
let mut set = JoinSet::new();
@@ -369,6 +369,12 @@ impl Accounts {
});
}
set.join_all().await;
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Finished background fetch for {n_accounts} accounts."
)),
});
}
/// Auxiliary function for [Accounts::background_fetch].
+3 -3
View File
@@ -594,9 +594,9 @@ impl Context {
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
.await
{
if let Err(err) = self.update_recent_quota(&mut session).await {
warn!(self, "Failed to update quota: {err:#}.");
}
self.update_recent_quota(&mut session)
.await
.context("Failed to update quota")?;
}
}
+2 -1
View File
@@ -178,7 +178,8 @@ mod tests {
let bob = TestContext::new_bob().await;
receive_imf(&bob, attachment_mime, false).await?;
let msg = bob.get_last_msg().await;
assert_eq!(msg.text, "Hello from Thunderbird!");
// Subject should be prepended because the attachment doesn't have "Chat-Version".
assert_eq!(msg.text, "Hello, Bob! – Hello from Thunderbird!");
Ok(())
}
+31 -13
View File
@@ -8,15 +8,13 @@ use tokio::io::BufWriter;
use super::capabilities::Capabilities;
use crate::context::Context;
use crate::log::{info, warn};
use crate::log::{LoggingStream, info, warn};
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::net::{
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
};
use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history};
use crate::tools::time;
#[derive(Debug)]
@@ -126,12 +124,12 @@ impl Client {
);
let res = match security {
ConnectionSecurity::Tls => {
Client::connect_secure(resolved_addr, host, strict_tls).await
Client::connect_secure(context, resolved_addr, host, strict_tls).await
}
ConnectionSecurity::Starttls => {
Client::connect_starttls(resolved_addr, host, strict_tls).await
Client::connect_starttls(context, resolved_addr, host, strict_tls).await
}
ConnectionSecurity::Plain => Client::connect_insecure(resolved_addr).await,
ConnectionSecurity::Plain => Client::connect_insecure(context, resolved_addr).await,
};
match res {
Ok(client) => {
@@ -202,8 +200,17 @@ impl Client {
}
}
async fn connect_secure(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
async fn connect_secure(
context: &Context,
addr: SocketAddr,
hostname: &str,
strict_tls: bool,
) -> Result<Self> {
let tcp_stream = connect_tcp_inner(addr).await?;
let account_id = context.get_id();
let events = context.events.clone();
let logging_stream = LoggingStream::new(tcp_stream, account_id, events);
let tls_stream = wrap_tls(strict_tls, hostname, alpn(addr.port()), logging_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -214,9 +221,12 @@ impl Client {
Ok(client)
}
async fn connect_insecure(addr: SocketAddr) -> Result<Self> {
async fn connect_insecure(context: &Context, addr: SocketAddr) -> Result<Self> {
let tcp_stream = connect_tcp_inner(addr).await?;
let buffered_stream = BufWriter::new(tcp_stream);
let account_id = context.get_id();
let events = context.events.clone();
let logging_stream = LoggingStream::new(tcp_stream, account_id, events);
let buffered_stream = BufWriter::new(logging_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
let _greeting = client
@@ -226,9 +236,18 @@ impl Client {
Ok(client)
}
async fn connect_starttls(addr: SocketAddr, host: &str, strict_tls: bool) -> Result<Self> {
async fn connect_starttls(
context: &Context,
addr: SocketAddr,
host: &str,
strict_tls: bool,
) -> Result<Self> {
let tcp_stream = connect_tcp_inner(addr).await?;
let account_id = context.get_id();
let events = context.events.clone();
let tcp_stream = LoggingStream::new(tcp_stream, account_id, events);
// Run STARTTLS command and convert the client back into a stream.
let buffered_tcp_stream = BufWriter::new(tcp_stream);
let mut client = async_imap::Client::new(buffered_tcp_stream);
@@ -246,7 +265,6 @@ impl Client {
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);
+4
View File
@@ -4,6 +4,10 @@
use crate::context::Context;
mod stream;
pub(crate) use stream::LoggingStream;
macro_rules! info {
($ctx:expr, $msg:expr) => {
info!($ctx, $msg,)
+160
View File
@@ -0,0 +1,160 @@
//! Stream that logs errors as events.
//!
//! This stream can be used to wrap IMAP,
//! SMTP and HTTP streams so errors
//! that occur are logged before
//! they are processed.
use std::net::SocketAddr;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use anyhow::Result;
use pin_project::pin_project;
use crate::events::{Event, EventType, Events};
use crate::net::session::SessionStream;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
#[derive(Debug)]
struct Metrics {
/// Total number of bytes read.
pub total_read: usize,
/// Total number of bytes written.
pub total_written: usize,
}
impl Metrics {
fn new() -> Self {
Self {
total_read: 0,
total_written: 0,
}
}
}
/// Stream that logs errors to the event channel.
#[derive(Debug)]
#[pin_project]
pub(crate) struct LoggingStream<S: SessionStream> {
#[pin]
inner: S,
/// Account ID for logging.
account_id: u32,
/// Event channel.
events: Events,
/// Metrics for this stream.
metrics: Metrics,
}
impl<S: SessionStream> LoggingStream<S> {
pub fn new(inner: S, account_id: u32, events: Events) -> Self {
Self {
inner,
account_id,
events,
metrics: Metrics::new(),
}
}
}
impl<S: SessionStream> AsyncRead for LoggingStream<S> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let this = self.project();
let peer_addr = this.inner.peer_addr();
let old_remaining = buf.remaining();
let res = this.inner.poll_read(cx, buf);
if let Poll::Ready(Err(ref err)) = res {
debug_assert!(
peer_addr.is_ok(),
"Logging stream should be created over a bound socket"
);
let log_message = match peer_addr {
Ok(peer_addr) => format!(
"Read error on stream {peer_addr:?} after reading {} and writing {} bytes: {err}.",
this.metrics.total_read, this.metrics.total_written
),
Err(_) => {
format!("Read error on a stream that does not have a peer address: {err}.")
}
};
this.events.emit(Event {
id: *this.account_id,
typ: EventType::Warning(log_message),
});
}
let n = old_remaining - buf.remaining();
this.metrics.total_read = this.metrics.total_read.saturating_add(n);
res
}
}
impl<S: SessionStream> AsyncWrite for LoggingStream<S> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
let this = self.project();
let res = this.inner.poll_write(cx, buf);
if let Poll::Ready(Ok(n)) = res {
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
}
res
}
fn poll_flush(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<std::io::Result<()>> {
self.project().inner.poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<std::io::Result<()>> {
self.project().inner.poll_shutdown(cx)
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[std::io::IoSlice<'_>],
) -> Poll<std::io::Result<usize>> {
let this = self.project();
let res = this.inner.poll_write_vectored(cx, bufs);
if let Poll::Ready(Ok(n)) = res {
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
}
res
}
fn is_write_vectored(&self) -> bool {
self.inner.is_write_vectored()
}
}
impl<S: SessionStream> SessionStream for LoggingStream<S> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.inner.set_read_timeout(timeout)
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.inner.peer_addr()
}
}
+18 -4
View File
@@ -368,8 +368,14 @@ impl MimeFactory {
}
}
ensure_and_debug_assert!(member_timestamps.len() >= to.len());
ensure_and_debug_assert!(member_fingerprints.is_empty() || member_fingerprints.len() >= to.len());
ensure_and_debug_assert!(
member_timestamps.len() >= to.len(),
"member_timestamps.len() ({}) < to.len() ({})",
member_timestamps.len(), to.len());
ensure_and_debug_assert!(
member_fingerprints.is_empty() || member_fingerprints.len() >= to.len(),
"member_fingerprints.len() ({}) < to.len() ({})",
member_fingerprints.len(), to.len());
if to.len() > 1 {
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
@@ -448,7 +454,11 @@ impl MimeFactory {
ensure_and_debug_assert!(
member_timestamps.is_empty()
|| to.len() + past_members.len() == member_timestamps.len()
|| to.len() + past_members.len() == member_timestamps.len(),
"to.len() ({}) + past_members.len() ({}) != member_timestamps.len() ({})",
to.len(),
past_members.len(),
member_timestamps.len(),
);
let factory = MimeFactory {
@@ -671,7 +681,11 @@ impl MimeFactory {
ensure_and_debug_assert!(
self.member_timestamps.is_empty()
|| to.len() + past_members.len() == self.member_timestamps.len()
|| to.len() + past_members.len() == self.member_timestamps.len(),
"to.len() ({}) + past_members.len() ({}) != self.member_timestamps.len() ({})",
to.len(),
past_members.len(),
self.member_timestamps.len(),
);
if to.is_empty() {
to.push(hidden_recipients());
+41 -20
View File
@@ -246,6 +246,7 @@ impl MimeMessage {
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut from,
@@ -273,6 +274,7 @@ impl MimeMessage {
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut from,
@@ -446,26 +448,11 @@ impl MimeMessage {
});
if let (Ok(mail), true) = (mail, is_encrypted) {
if !signatures.is_empty() {
// Remove unsigned opportunistically protected headers from messages considered
// Autocrypt-encrypted / displayed with padlock.
// For "Subject" see <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
for h in [
HeaderDef::Subject,
HeaderDef::ChatGroupId,
HeaderDef::ChatGroupName,
HeaderDef::ChatGroupNameChanged,
HeaderDef::ChatGroupNameTimestamp,
HeaderDef::ChatGroupAvatar,
HeaderDef::ChatGroupMemberRemoved,
HeaderDef::ChatGroupMemberAdded,
HeaderDef::ChatGroupMemberTimestamps,
HeaderDef::ChatGroupPastMembers,
HeaderDef::ChatDelete,
HeaderDef::ChatEdit,
HeaderDef::ChatUserAvatar,
] {
remove_header(&mut headers, h.get_headername(), &mut headers_removed);
}
// Unsigned "Subject" mustn't be prepended to messages shown as encrypted
// (<https://github.com/deltachat/deltachat-core-rust/issues/1790>).
// Other headers are removed by `MimeMessage::merge_headers()` except for "List-ID".
remove_header(&mut headers, "subject", &mut headers_removed);
remove_header(&mut headers, "list-id", &mut headers_removed);
}
// let known protected headers from the decrypted
@@ -478,6 +465,7 @@ impl MimeMessage {
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut inner_from,
@@ -1558,6 +1546,7 @@ impl MimeMessage {
fn merge_headers(
context: &Context,
headers: &mut HashMap<String, String>,
headers_removed: &mut HashSet<String>,
recipients: &mut Vec<SingleInfo>,
past_members: &mut Vec<SingleInfo>,
from: &mut Option<SingleInfo>,
@@ -1565,6 +1554,12 @@ impl MimeMessage {
chat_disposition_notification_to: &mut Option<SingleInfo>,
fields: &[mailparse::MailHeader<'_>],
) {
headers.retain(|k, _| {
!is_protected(k) || {
headers_removed.insert(k.to_string());
false
}
});
for field in fields {
// lowercasing all headers is technically not correct, but makes things work better
let key = field.get_key().to_lowercase();
@@ -2005,6 +2000,32 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
}
}
/// Returns whether the outer header value must be ignored if the message contains a signed (and
/// optionally encrypted) part.
///
/// NB: There are known cases when Subject and List-ID only appear in the outer headers of
/// signed-only messages. Such messages are shown as unencrypted anyway.
fn is_protected(key: &str) -> bool {
key.starts_with("chat-")
|| matches!(
key,
"return-path"
| "auto-submitted"
| "autocrypt-setup-message"
| "date"
| "from"
| "sender"
| "reply-to"
| "to"
| "cc"
| "bcc"
| "message-id"
| "in-reply-to"
| "references"
| "secure-join"
)
}
/// Returns if the header is hidden and must be ignored in the IMF section.
pub(crate) fn is_hidden(key: &str) -> bool {
matches!(
+20
View File
@@ -1402,6 +1402,26 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_extra_imf_chat_header() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let chat_id = t.get_self_chat().await.id;
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
let sent_msg = t.pop_sent_msg().await;
// Check removal of some nonexistent "Chat-*" header to protect the code from future breakages.
let payload = sent_msg
.payload
.replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:");
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None)
.await
.unwrap();
assert!(msg.headers.contains_key("chat-version"));
assert!(!msg.headers.contains_key("chat-forty-two"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_long_in_reply_to() -> Result<()> {
let t = TestContext::new_alice().await;
+42 -3
View File
@@ -1,7 +1,10 @@
use anyhow::Result;
use fast_socks5::client::Socks5Stream;
use std::net::SocketAddr;
use std::pin::Pin;
use std::time::Duration;
use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, BufStream, BufWriter};
use tokio::net::TcpStream;
use tokio_io_timeout::TimeoutStream;
pub(crate) trait SessionStream:
@@ -9,54 +12,90 @@ pub(crate) trait SessionStream:
{
/// Change the read timeout on the session stream.
fn set_read_timeout(&mut self, timeout: Option<Duration>);
/// Returns the remote address that this stream is connected to.
fn peer_addr(&self) -> Result<SocketAddr>;
}
impl SessionStream for Box<dyn SessionStream> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.as_mut().set_read_timeout(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.as_ref().peer_addr()
}
}
impl<T: SessionStream> SessionStream for async_native_tls::TlsStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().peer_addr()
}
}
impl<T: SessionStream> SessionStream for tokio_rustls::client::TlsStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().0.set_read_timeout(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().0.peer_addr()
}
}
impl<T: SessionStream> SessionStream for BufStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().peer_addr()
}
}
impl<T: SessionStream> SessionStream for BufWriter<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().peer_addr()
}
}
impl<T: AsyncRead + AsyncWrite + Send + Sync + std::fmt::Debug> SessionStream
for Pin<Box<TimeoutStream<T>>>
{
impl SessionStream for Pin<Box<TimeoutStream<TcpStream>>> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.as_mut().set_read_timeout_pinned(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
Ok(self.get_ref().peer_addr()?)
}
}
impl<T: SessionStream> SessionStream for Socks5Stream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_socket_mut().set_read_timeout(timeout)
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_socket_ref().peer_addr()
}
}
impl<T: SessionStream> SessionStream for shadowsocks::ProxyClientStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout)
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().peer_addr()
}
}
impl<T: SessionStream> SessionStream for async_imap::DeflateStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout)
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().peer_addr()
}
}
/// Session stream with a read buffer.
+10 -5
View File
@@ -124,14 +124,16 @@ impl Context {
/// in case for some providers the quota is always at ~100%
/// and new space is allocated as needed.
pub(crate) async fn update_recent_quota(&self, session: &mut ImapSession) -> Result<()> {
info!(self, "Updating IMAP quota.");
let quota = if session.can_check_quota() {
let folders = get_watched_folders(self).await?;
get_unique_quota_roots_and_usage(session, folders).await
Some(get_unique_quota_roots_and_usage(session, folders).await?)
} else {
Err(anyhow!(stock_str::not_supported_by_provider(self).await))
None
};
if let Ok(quota) = &quota {
let recent = if let Some(quota) = &quota {
match get_highest_usage(quota) {
Ok((highest, _, _)) => {
if needs_quota_warning(
@@ -153,10 +155,13 @@ impl Context {
}
Err(err) => warn!(self, "cannot get highest quota usage: {:#}", err),
}
}
Ok(quota)
} else {
Err(anyhow!(stock_str::not_supported_by_provider(self).await))
};
*self.quota.write().await = Some(QuotaInfo {
recent: quota,
recent: recent.cloned(),
modified: tools::Time::now(),
});
+8 -3
View File
@@ -1457,7 +1457,10 @@ async fn do_chat_assignment(
false => None,
};
if let Some(chat) = chat {
ensure_and_debug_assert!(chat.typ == Chattype::Single);
ensure_and_debug_assert!(
chat.typ == Chattype::Single,
"Chat {chat_id} is not Single",
);
let mut new_protection = match verified_encryption {
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
@@ -2142,7 +2145,7 @@ RETURNING id
// afterwards insert additional parts.
replace_msg_id = None;
ensure_and_debug_assert!(!row_id.is_special());
ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
created_db_entries.push(row_id);
}
@@ -2406,7 +2409,9 @@ async fn lookup_chat_by_reply(
// as we can directly assign the message to the chat
// by its group ID.
ensure_and_debug_assert!(
mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted()
mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
"Encrypted message has group ID {}",
mime_parser.get_chat_group_id().unwrap_or_default(),
);
// Try to assign message to the same chat as the parent message.
+18
View File
@@ -3682,6 +3682,24 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ignore_protected_headers_in_outer_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
send_text_msg(bob, bob_chat_id, "hi all!".to_string()).await?;
let mut sent_msg = bob.pop_sent_msg().await;
sent_msg.payload = sent_msg.payload.replace(
"Chat-Version:",
"Auto-Submitted: auto-generated\r\nChat-Version:",
);
alice.recv_msg(&sent_msg).await;
let ab_contact = alice.add_or_lookup_contact(bob).await;
assert!(!ab_contact.is_bot());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_member_list_on_rejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
+7 -7
View File
@@ -481,17 +481,17 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
// Update quota no more than once a minute.
if ctx.quota_needs_update(60).await {
if let Err(err) = ctx.update_recent_quota(&mut session).await {
warn!(ctx, "Failed to update quota: {:#}.", err);
}
ctx.update_recent_quota(&mut session)
.await
.context("Failed to update quota")?;
}
let resync_requested = ctx.resync_request.swap(false, Ordering::Relaxed);
if resync_requested {
if let Err(err) = session.resync_folders(ctx).await {
warn!(ctx, "Failed to resync folders: {:#}.", err);
ctx.resync_request.store(true, Ordering::Relaxed);
}
session
.resync_folders(ctx)
.await
.context("resync_folders")?;
}
maybe_add_time_based_warnings(ctx).await;
+7 -5
View File
@@ -767,9 +767,10 @@ pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
/// In non-optimized builds, panics instead if so.
#[macro_export]
macro_rules! ensure_and_debug_assert {
($($arg:tt)*) => {
debug_assert!($($arg)*);
anyhow::ensure!($($arg)*);
($cond:expr, $($arg:tt)*) => {
let cond_val = $cond;
debug_assert!(cond_val, $($arg)*);
anyhow::ensure!(cond_val, $($arg)*);
};
}
@@ -806,10 +807,11 @@ macro_rules! ensure_and_debug_assert_ne {
#[macro_export]
macro_rules! logged_debug_assert {
($ctx:expr, $cond:expr, $($arg:tt)*) => {
if !$cond {
let cond_val = $cond;
if !cond_val {
warn!($ctx, $($arg)*);
}
debug_assert!($cond, $($arg)*);
debug_assert!(cond_val, $($arg)*);
};
}