mirror of
https://github.com/chatmail/core.git
synced 2026-10-08 14:10:31 +03:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f41177da1 | ||
|
|
dc63340471 | ||
|
|
ad13477754 | ||
|
|
a2df29515a | ||
|
|
6df1d165dd | ||
|
|
e03e2d9a68 | ||
|
|
8fc6ea19b4 | ||
|
|
c5c947e175 | ||
|
|
6d8dff54a7 |
@@ -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:
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,6 +4,10 @@
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
mod stream;
|
||||
|
||||
pub(crate) use stream::LoggingStream;
|
||||
|
||||
macro_rules! info {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
info!($ctx, $msg,)
|
||||
|
||||
@@ -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
@@ -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
@@ -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!(
|
||||
|
||||
@@ -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
@@ -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
@@ -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) = "a {
|
||||
let recent = if let Some(quota) = "a {
|
||||
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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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)*);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user