mirror of
https://github.com/chatmail/core.git
synced 2026-05-04 05:46:29 +03:00
Merge branch 'master' into fix3782
This commit is contained in:
51
src/chat.rs
51
src/chat.rs
@@ -782,17 +782,35 @@ impl ChatId {
|
||||
// the times are average, no matter if there are fresh messages or not -
|
||||
// and have to be multiplied by the number of items shown at once on the chatlist,
|
||||
// so savings up to 2 seconds are possible on older devices - newer ones will feel "snappier" :)
|
||||
let count = context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
let count = if self.is_archived_link() {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(DISTINCT(m.chat_id))
|
||||
FROM msgs m
|
||||
LEFT JOIN chats c ON m.chat_id=c.id
|
||||
WHERE m.state=10
|
||||
and m.hidden=0
|
||||
AND m.chat_id>9
|
||||
AND c.blocked=0
|
||||
AND c.archived=1
|
||||
",
|
||||
paramsv![],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
paramsv![MessageState::InFresh, self],
|
||||
)
|
||||
.await?;
|
||||
paramsv![MessageState::InFresh, self],
|
||||
)
|
||||
.await?
|
||||
};
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
@@ -1216,6 +1234,10 @@ impl Chat {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
}
|
||||
} else if self.id.is_archived_link() {
|
||||
if let Ok(image_rel) = get_archive_icon(context).await {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
}
|
||||
} else if self.typ == Chattype::Single {
|
||||
let contacts = get_chat_contacts(context, self.id).await?;
|
||||
if let Some(contact_id) = contacts.first() {
|
||||
@@ -1710,6 +1732,21 @@ pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<String> {
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_archive_icon(context: &Context) -> Result<String> {
|
||||
if let Some(icon) = context.sql.get_raw_config("icon-archive").await? {
|
||||
return Ok(icon);
|
||||
}
|
||||
|
||||
let icon = include_bytes!("../assets/icon-archive.png");
|
||||
let blob = BlobObject::create(context, "icon-archive.png", icon).await?;
|
||||
let icon = blob.as_name().to_string();
|
||||
context
|
||||
.sql
|
||||
.set_raw_config("icon-archive", Some(&icon))
|
||||
.await?;
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
async fn update_special_chat_name(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
|
||||
@@ -92,8 +92,6 @@ impl Chatlist {
|
||||
let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
|
||||
let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;
|
||||
|
||||
let mut add_archived_link_item = false;
|
||||
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let msg_id: Option<MsgId> = row.get(1)?;
|
||||
@@ -123,7 +121,7 @@ impl Chatlist {
|
||||
//
|
||||
// The query shows messages from blocked contacts in
|
||||
// groups. Otherwise it would be hard to follow conversations.
|
||||
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
||||
let ids = if let Some(query_contact_id) = query_contact_id {
|
||||
// show chats shared with a given contact
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
@@ -216,7 +214,7 @@ impl Chatlist {
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
let ids = context.sql.query_map(
|
||||
let mut ids = context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -236,19 +234,15 @@ impl Chatlist {
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?;
|
||||
if !flag_no_specials {
|
||||
add_archived_link_item = true;
|
||||
if !flag_no_specials && get_archived_cnt(context).await? > 0 {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
|
||||
}
|
||||
ids.insert(0, (DC_CHAT_ID_ARCHIVED_LINK, None));
|
||||
}
|
||||
ids
|
||||
};
|
||||
|
||||
if add_archived_link_item && get_archived_cnt(context).await? > 0 {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
|
||||
}
|
||||
ids.push((DC_CHAT_ID_ARCHIVED_LINK, None));
|
||||
}
|
||||
|
||||
Ok(Chatlist { ids })
|
||||
}
|
||||
|
||||
|
||||
@@ -579,10 +579,10 @@ async fn try_imap_one_param(
|
||||
|
||||
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
info!(context, "failure: {:#}", err);
|
||||
return Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
msg: format!("{:#}", err),
|
||||
});
|
||||
}
|
||||
Ok(imap) => imap,
|
||||
@@ -590,10 +590,10 @@ async fn try_imap_one_param(
|
||||
|
||||
match imap.connect(context).await {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
info!(context, "failure: {:#}", err);
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
msg: format!("{:#}", err),
|
||||
})
|
||||
}
|
||||
Ok(()) => {
|
||||
@@ -634,7 +634,7 @@ async fn try_smtp_one_param(
|
||||
info!(context, "failure: {}", err);
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: err.to_string(),
|
||||
msg: format!("{:#}", err),
|
||||
})
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
|
||||
@@ -62,7 +62,7 @@ fn parse_server<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
server_event: &BytesStart,
|
||||
) -> Result<Option<Server>, quick_xml::Error> {
|
||||
let end_tag = String::from_utf8_lossy(server_event.name())
|
||||
let end_tag = String::from_utf8_lossy(server_event.name().as_ref())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
|
||||
@@ -70,12 +70,17 @@ fn parse_server<B: BufRead>(
|
||||
.attributes()
|
||||
.find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type")
|
||||
.map(|a| {
|
||||
String::from_utf8_lossy(a.key.as_ref())
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
== "type"
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.map(|typ| {
|
||||
typ.unwrap()
|
||||
.unescape_and_decode_value(reader)
|
||||
.decode_and_unescape_value(reader)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
})
|
||||
@@ -89,25 +94,23 @@ fn parse_server<B: BufRead>(
|
||||
let mut tag_config = MozConfigTag::Undefined;
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Start(ref event) => {
|
||||
tag_config = String::from_utf8_lossy(event.name())
|
||||
tag_config = String::from_utf8_lossy(event.name().as_ref())
|
||||
.parse()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Event::End(ref event) => {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
let tag = String::from_utf8_lossy(event.name().as_ref())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
|
||||
if tag == end_tag {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Text(ref event) => {
|
||||
let val = event
|
||||
.unescape_and_decode(reader)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_owned();
|
||||
let val = event.unescape().unwrap_or_default().trim().to_owned();
|
||||
|
||||
match tag_config {
|
||||
MozConfigTag::Hostname => hostname = Some(val),
|
||||
@@ -150,9 +153,11 @@ fn parse_xml_reader<B: BufRead>(
|
||||
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Start(ref event) => {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
let tag = String::from_utf8_lossy(event.name().as_ref())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
|
||||
if tag == "incomingserver" {
|
||||
if let Some(incoming_server) = parse_server(reader, event)? {
|
||||
|
||||
@@ -59,12 +59,18 @@ fn parse_protocol<B: BufRead>(
|
||||
|
||||
let mut current_tag: Option<String> = None;
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Start(ref event) => {
|
||||
current_tag = Some(String::from_utf8_lossy(event.name()).trim().to_lowercase());
|
||||
current_tag = Some(
|
||||
String::from_utf8_lossy(event.name().as_ref())
|
||||
.trim()
|
||||
.to_lowercase(),
|
||||
);
|
||||
}
|
||||
Event::End(ref event) => {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
let tag = String::from_utf8_lossy(event.name().as_ref())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
if tag == "protocol" {
|
||||
break;
|
||||
}
|
||||
@@ -73,7 +79,7 @@ fn parse_protocol<B: BufRead>(
|
||||
}
|
||||
}
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape_and_decode(reader).unwrap_or_default();
|
||||
let val = e.unescape().unwrap_or_default();
|
||||
|
||||
if let Some(ref tag) = current_tag {
|
||||
match tag.as_str() {
|
||||
@@ -115,9 +121,9 @@ fn parse_redirecturl<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
) -> Result<String, quick_xml::Error> {
|
||||
let mut buf = Vec::new();
|
||||
match reader.read_event(&mut buf)? {
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape_and_decode(reader).unwrap_or_default();
|
||||
let val = e.unescape().unwrap_or_default();
|
||||
Ok(val.trim().to_string())
|
||||
}
|
||||
_ => Ok("".to_string()),
|
||||
@@ -131,9 +137,11 @@ fn parse_xml_reader<B: BufRead>(
|
||||
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Start(ref e) => {
|
||||
let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase();
|
||||
let tag = String::from_utf8_lossy(e.name().as_ref())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
|
||||
if tag == "protocol" {
|
||||
if let Some(protocol) = parse_protocol(reader)? {
|
||||
|
||||
@@ -88,18 +88,30 @@ fn dehtml_quick_xml(buf: &str) -> String {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
loop {
|
||||
match reader.read_event(&mut buf) {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(quick_xml::events::Event::Start(ref e)) => {
|
||||
dehtml_starttag_cb(e, &mut dehtml, &reader)
|
||||
}
|
||||
Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::CData(e)) => dehtml_text_cb(&e.escape(), &mut dehtml),
|
||||
Ok(quick_xml::events::Event::CData(e)) => match e.escape() {
|
||||
Ok(e) => dehtml_text_cb(&e, &mut dehtml),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"CDATA escape error at position {}: {:?}",
|
||||
reader.buffer_position(),
|
||||
e,
|
||||
);
|
||||
}
|
||||
},
|
||||
Ok(quick_xml::events::Event::Empty(ref e)) => {
|
||||
// Handle empty tags as a start tag immediately followed by end tag.
|
||||
// For example, `<p/>` is treated as `<p></p>`.
|
||||
dehtml_starttag_cb(e, &mut dehtml, &reader);
|
||||
dehtml_endtag_cb(&BytesEnd::borrowed(e.name()), &mut dehtml);
|
||||
dehtml_endtag_cb(
|
||||
&BytesEnd::new(String::from_utf8_lossy(e.name().as_ref())),
|
||||
&mut dehtml,
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
@@ -121,7 +133,7 @@ fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|
||||
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
|
||||
{
|
||||
let last_added = escaper::decode_html_buf_sloppy(event.escaped()).unwrap_or_default();
|
||||
let last_added = escaper::decode_html_buf_sloppy(event as &[_]).unwrap_or_default();
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
|
||||
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
|
||||
@@ -135,7 +147,9 @@ fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
}
|
||||
|
||||
fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
let tag = String::from_utf8_lossy(event.name().as_ref())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
|
||||
match tag.as_str() {
|
||||
"p" | "table" | "td" | "style" | "script" | "title" | "pre" => {
|
||||
@@ -176,7 +190,9 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
dehtml: &mut Dehtml,
|
||||
reader: &quick_xml::Reader<B>,
|
||||
) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
let tag = String::from_utf8_lossy(event.name().as_ref())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
|
||||
match tag.as_str() {
|
||||
"p" | "table" | "td" => {
|
||||
@@ -206,10 +222,15 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
if let Some(href) = event
|
||||
.html_attributes()
|
||||
.filter_map(|attr| attr.ok())
|
||||
.find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "href")
|
||||
.find(|attr| {
|
||||
String::from_utf8_lossy(attr.key.as_ref())
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
== "href"
|
||||
})
|
||||
{
|
||||
let href = href
|
||||
.unescape_and_decode_value(reader)
|
||||
.decode_and_unescape_value(reader)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
@@ -258,7 +279,7 @@ fn maybe_push_tag(
|
||||
fn tag_contains_attr(event: &BytesStart, reader: &Reader<impl BufRead>, name: &str) -> bool {
|
||||
event.attributes().any(|r| {
|
||||
r.map(|a| {
|
||||
a.unescape_and_decode_value(reader)
|
||||
a.decode_and_unescape_value(reader)
|
||||
.map(|v| v == name)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! # Download large messages manually.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -33,6 +31,7 @@ const MIN_DOWNLOAD_LIMIT: u32 = 32768;
|
||||
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
|
||||
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
|
||||
|
||||
/// Download state of the message.
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
@@ -49,9 +48,16 @@ pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum DownloadState {
|
||||
/// Message is fully downloaded.
|
||||
Done = 0,
|
||||
|
||||
/// Message is partially downloaded and can be fully downloaded at request.
|
||||
Available = 10,
|
||||
|
||||
/// Failed to fully download the message.
|
||||
Failure = 20,
|
||||
|
||||
/// Full download of the message is in progress.
|
||||
InProgress = 1000,
|
||||
}
|
||||
|
||||
|
||||
12
src/fuzzing.rs
Normal file
12
src/fuzzing.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! # Fuzzing module.
|
||||
//!
|
||||
//! This module exposes private APIs for fuzzing.
|
||||
|
||||
/// Fuzzing target for simplify().
|
||||
///
|
||||
/// Calls simplify() and panics if simplify() panics.
|
||||
/// Does not return any vaule to avoid exposing internal crate types.
|
||||
#[cfg(fuzzing)]
|
||||
pub fn simplify(input: String, is_chat_message: bool) {
|
||||
crate::simplify::simplify(input, is_chat_message);
|
||||
}
|
||||
155
src/imap.rs
155
src/imap.rs
@@ -297,6 +297,7 @@ impl Imap {
|
||||
|
||||
let oauth2 = self.config.lp.oauth2;
|
||||
|
||||
info!(context, "Connecting to IMAP server");
|
||||
let connection_res: Result<Client> = if self.config.lp.security == Socket::Starttls
|
||||
|| self.config.lp.security == Socket::Plain
|
||||
{
|
||||
@@ -304,22 +305,23 @@ impl Imap {
|
||||
let imap_server: &str = config.lp.server.as_ref();
|
||||
let imap_port = config.lp.port;
|
||||
|
||||
let connection = if let Some(socks5_config) = &config.socks5_config {
|
||||
Client::connect_insecure_socks5((imap_server, imap_port), socks5_config.clone())
|
||||
if let Some(socks5_config) = &config.socks5_config {
|
||||
if config.lp.security == Socket::Starttls {
|
||||
Client::connect_starttls_socks5(
|
||||
imap_server,
|
||||
imap_port,
|
||||
socks5_config.clone(),
|
||||
config.strict_tls,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Client::connect_insecure_socks5((imap_server, imap_port), socks5_config.clone())
|
||||
.await
|
||||
}
|
||||
} else if config.lp.security == Socket::Starttls {
|
||||
Client::connect_starttls(imap_server, imap_port, config.strict_tls).await
|
||||
} else {
|
||||
Client::connect_insecure((imap_server, imap_port)).await
|
||||
};
|
||||
|
||||
match connection {
|
||||
Ok(client) => {
|
||||
if config.lp.security == Socket::Starttls {
|
||||
client.secure(imap_server, config.strict_tls).await
|
||||
} else {
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
} else {
|
||||
let config = &self.config;
|
||||
@@ -328,8 +330,8 @@ impl Imap {
|
||||
|
||||
if let Some(socks5_config) = &config.socks5_config {
|
||||
Client::connect_secure_socks5(
|
||||
(imap_server, imap_port),
|
||||
imap_server,
|
||||
imap_port,
|
||||
config.strict_tls,
|
||||
socks5_config.clone(),
|
||||
)
|
||||
@@ -345,6 +347,7 @@ impl Imap {
|
||||
let imap_pw: &str = config.lp.password.as_ref();
|
||||
|
||||
let login_res = if oauth2 {
|
||||
info!(context, "Logging into IMAP server with OAuth 2");
|
||||
let addr: &str = config.addr.as_ref();
|
||||
|
||||
let token = get_oauth2_access_token(context, addr, imap_pw, true)
|
||||
@@ -356,6 +359,7 @@ impl Imap {
|
||||
};
|
||||
client.authenticate("XOAUTH2", auth).await
|
||||
} else {
|
||||
info!(context, "Logging into IMAP server with LOGIN");
|
||||
client.login(imap_user, imap_pw).await
|
||||
};
|
||||
|
||||
@@ -371,6 +375,7 @@ impl Imap {
|
||||
"IMAP-LOGIN as {}",
|
||||
self.config.lp.user
|
||||
)));
|
||||
info!(context, "Successfully logged into IMAP server");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -378,7 +383,7 @@ impl Imap {
|
||||
let imap_user = self.config.lp.user.to_owned();
|
||||
let message = stock_str::cannot_login(context, &imap_user).await;
|
||||
|
||||
warn!(context, "{} ({})", message, err);
|
||||
warn!(context, "{} ({:#})", message, err);
|
||||
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
@@ -386,7 +391,7 @@ impl Imap {
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
|
||||
{
|
||||
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
|
||||
warn!(context, "{}", e);
|
||||
warn!(context, "{:#}", e);
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
@@ -396,13 +401,13 @@ impl Imap {
|
||||
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
|
||||
.await
|
||||
{
|
||||
warn!(context, "{}", e);
|
||||
warn!(context, "{:#}", e);
|
||||
}
|
||||
} else {
|
||||
self.login_failed_once = true;
|
||||
}
|
||||
|
||||
Err(format_err!("{}\n\n{}", message, err))
|
||||
Err(format_err!("{}\n\n{:#}", message, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -672,7 +677,10 @@ impl Imap {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let new_emails = self.select_with_uidvalidity(context, folder).await?;
|
||||
let new_emails = self
|
||||
.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to select folder {}", folder))?;
|
||||
|
||||
if !new_emails && !fetch_existing_msgs {
|
||||
info!(context, "No new emails in folder {}", folder);
|
||||
@@ -832,9 +840,15 @@ impl Imap {
|
||||
}
|
||||
self.prepare(context).await.context("could not connect")?;
|
||||
|
||||
add_all_recipients_as_contacts(context, self, Config::ConfiguredSentboxFolder).await;
|
||||
add_all_recipients_as_contacts(context, self, Config::ConfiguredMvboxFolder).await;
|
||||
add_all_recipients_as_contacts(context, self, Config::ConfiguredInboxFolder).await;
|
||||
add_all_recipients_as_contacts(context, self, Config::ConfiguredSentboxFolder)
|
||||
.await
|
||||
.context("failed to get recipients from the sentbox")?;
|
||||
add_all_recipients_as_contacts(context, self, Config::ConfiguredMvboxFolder)
|
||||
.await
|
||||
.context("failed to ge recipients from the movebox")?;
|
||||
add_all_recipients_as_contacts(context, self, Config::ConfiguredInboxFolder)
|
||||
.await
|
||||
.context("failed to get recipients from the inbox")?;
|
||||
|
||||
if context.get_config_bool(Config::FetchExistingMsgs).await? {
|
||||
for config in &[
|
||||
@@ -843,17 +857,18 @@ impl Imap {
|
||||
Config::ConfiguredSentboxFolder,
|
||||
] {
|
||||
if let Some(folder) = context.get_config(*config).await? {
|
||||
info!(
|
||||
context,
|
||||
"Fetching existing messages from folder \"{}\"", folder
|
||||
);
|
||||
self.fetch_new_messages(context, &folder, false, true)
|
||||
.await
|
||||
.context("could not fetch messages")?;
|
||||
.context("could not fetch existing messages")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Done fetching existing messages.");
|
||||
context
|
||||
.set_config_bool(Config::FetchedExistingMsgs, true)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1202,8 +1217,10 @@ impl Imap {
|
||||
/// Prefetch all messages greater than or equal to `uid_next`. Returns a list of fetch results
|
||||
/// in the order of ascending delivery time to the server (INTERNALDATE).
|
||||
async fn prefetch(&mut self, uid_next: u32) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
|
||||
let session = self.session.as_mut();
|
||||
let session = session.context("fetch_after(): IMAP No Connection established")?;
|
||||
let session = self
|
||||
.session
|
||||
.as_mut()
|
||||
.context("no IMAP connection established")?;
|
||||
|
||||
// fetch messages with larger UID than the last one seen
|
||||
let set = format!("{}:*", uid_next);
|
||||
@@ -1228,7 +1245,6 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(list);
|
||||
|
||||
Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect())
|
||||
}
|
||||
@@ -2301,49 +2317,58 @@ impl std::fmt::Display for UidRange {
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) {
|
||||
let mailbox = if let Ok(Some(m)) = context.get_config(folder).await {
|
||||
async fn add_all_recipients_as_contacts(
|
||||
context: &Context,
|
||||
imap: &mut Imap,
|
||||
folder: Config,
|
||||
) -> Result<()> {
|
||||
let mailbox = if let Some(m) = context.get_config(folder).await? {
|
||||
m
|
||||
} else {
|
||||
return;
|
||||
info!(
|
||||
context,
|
||||
"Folder {} is not configured, skipping fetching contacts from it.", folder
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
if let Err(e) = imap.select_with_uidvalidity(context, &mailbox).await {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
warn!(context, "Could not select {}: {:#}", mailbox, e);
|
||||
return;
|
||||
}
|
||||
match imap.get_all_recipients(context).await {
|
||||
Ok(contacts) => {
|
||||
let mut any_modified = false;
|
||||
for contact in contacts {
|
||||
let display_name_normalized = contact
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(|s| normalize_name(s))
|
||||
.unwrap_or_default();
|
||||
imap.select_with_uidvalidity(context, &mailbox)
|
||||
.await
|
||||
.with_context(|| format!("could not select {}", mailbox))?;
|
||||
|
||||
match Contact::add_or_lookup(
|
||||
context,
|
||||
&display_name_normalized,
|
||||
&contact.addr,
|
||||
Origin::OutgoingTo,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((_, modified)) => {
|
||||
if modified != Modifier::None {
|
||||
any_modified = true;
|
||||
}
|
||||
}
|
||||
Err(e) => warn!(context, "Could not add recipient: {}", e),
|
||||
let contacts = imap
|
||||
.get_all_recipients(context)
|
||||
.await
|
||||
.context("could not get recipients")?;
|
||||
|
||||
let mut any_modified = false;
|
||||
for contact in contacts {
|
||||
let display_name_normalized = contact
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(|s| normalize_name(s))
|
||||
.unwrap_or_default();
|
||||
|
||||
match Contact::add_or_lookup(
|
||||
context,
|
||||
&display_name_normalized,
|
||||
&contact.addr,
|
||||
Origin::OutgoingTo,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((_, modified)) => {
|
||||
if modified != Modifier::None {
|
||||
any_modified = true;
|
||||
}
|
||||
}
|
||||
if any_modified {
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
}
|
||||
Err(e) => warn!(context, "Could not add recipient: {}", e),
|
||||
}
|
||||
Err(e) => warn!(context, "Could not add recipients: {}", e),
|
||||
};
|
||||
}
|
||||
if any_modified {
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -8,13 +8,13 @@ use anyhow::{Context as _, Result};
|
||||
use async_imap::Client as ImapClient;
|
||||
use async_imap::Session as ImapSession;
|
||||
|
||||
use tokio::net::{self, TcpStream};
|
||||
use tokio::time::timeout;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
use tokio::io::BufWriter;
|
||||
use tokio::net::ToSocketAddrs;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use super::session::Session;
|
||||
use crate::login_param::build_tls;
|
||||
use crate::net::connect_tcp;
|
||||
use crate::socks::Socks5Config;
|
||||
|
||||
use super::session::SessionStream;
|
||||
@@ -24,7 +24,6 @@ pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Client {
|
||||
is_secure: bool,
|
||||
inner: ImapClient<Box<dyn SessionStream>>,
|
||||
}
|
||||
|
||||
@@ -93,108 +92,131 @@ impl Client {
|
||||
}
|
||||
|
||||
pub async fn connect_secure(hostname: &str, port: u16, strict_tls: bool) -> Result<Self> {
|
||||
let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect((hostname, port))).await??;
|
||||
let mut timeout_stream = TimeoutStream::new(tcp_stream);
|
||||
timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT));
|
||||
timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT));
|
||||
let timeout_stream = Box::pin(timeout_stream);
|
||||
|
||||
let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?;
|
||||
let tls = build_tls(strict_tls);
|
||||
let tls_stream: Box<dyn SessionStream> =
|
||||
Box::new(tls.connect(hostname, timeout_stream).await?);
|
||||
let mut client = ImapClient::new(tls_stream);
|
||||
let tls_stream = tls.connect(hostname, tcp_stream).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")?;
|
||||
.context("failed to read greeting")??;
|
||||
|
||||
Ok(Client {
|
||||
is_secure: true,
|
||||
inner: client,
|
||||
})
|
||||
Ok(Client { inner: client })
|
||||
}
|
||||
|
||||
pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> Result<Self> {
|
||||
let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect(addr)).await??;
|
||||
let mut timeout_stream = TimeoutStream::new(tcp_stream);
|
||||
timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT));
|
||||
timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT));
|
||||
let timeout_stream = Box::pin(timeout_stream);
|
||||
let stream: Box<dyn SessionStream> = Box::new(timeout_stream);
|
||||
|
||||
let mut client = ImapClient::new(stream);
|
||||
pub async fn connect_insecure(addr: impl ToSocketAddrs) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp(addr, IMAP_TIMEOUT).await?;
|
||||
let buffered_stream = BufWriter::new(tcp_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")?;
|
||||
.context("failed to read greeting")??;
|
||||
|
||||
Ok(Client {
|
||||
is_secure: false,
|
||||
inner: client,
|
||||
})
|
||||
Ok(Client { inner: client })
|
||||
}
|
||||
|
||||
pub async fn connect_starttls(hostname: &str, port: u16, strict_tls: bool) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(tcp_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
client
|
||||
.run_command_and_check_ok("STARTTLS", None)
|
||||
.await
|
||||
.context("STARTTLS command failed")?;
|
||||
let tcp_stream = client.into_inner();
|
||||
|
||||
let tls = build_tls(strict_tls);
|
||||
let tls_stream = tls
|
||||
.connect(hostname, 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 = ImapClient::new(session_stream);
|
||||
|
||||
Ok(Client { inner: client })
|
||||
}
|
||||
|
||||
pub async fn connect_secure_socks5(
|
||||
target_addr: impl net::ToSocketAddrs,
|
||||
domain: &str,
|
||||
port: u16,
|
||||
strict_tls: bool,
|
||||
socks5_config: Socks5Config,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream: Box<dyn SessionStream> =
|
||||
Box::new(socks5_config.connect(target_addr, IMAP_TIMEOUT).await?);
|
||||
|
||||
let socks5_stream = socks5_config.connect((domain, port), IMAP_TIMEOUT).await?;
|
||||
let tls = build_tls(strict_tls);
|
||||
let tls_stream: Box<dyn SessionStream> =
|
||||
Box::new(tls.connect(domain, socks5_stream).await?);
|
||||
let mut client = ImapClient::new(tls_stream);
|
||||
|
||||
let tls_stream = tls.connect(domain, socks5_stream).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")?;
|
||||
.context("failed to read greeting")??;
|
||||
|
||||
Ok(Client {
|
||||
is_secure: true,
|
||||
inner: client,
|
||||
})
|
||||
Ok(Client { inner: client })
|
||||
}
|
||||
|
||||
pub async fn connect_insecure_socks5(
|
||||
target_addr: impl net::ToSocketAddrs,
|
||||
target_addr: impl ToSocketAddrs,
|
||||
socks5_config: Socks5Config,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream: Box<dyn SessionStream> =
|
||||
Box::new(socks5_config.connect(target_addr, IMAP_TIMEOUT).await?);
|
||||
|
||||
let mut client = ImapClient::new(socks5_stream);
|
||||
let socks5_stream = socks5_config.connect(target_addr, IMAP_TIMEOUT).await?;
|
||||
let buffered_stream = BufWriter::new(socks5_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")?;
|
||||
.context("failed to read greeting")??;
|
||||
|
||||
Ok(Client {
|
||||
is_secure: false,
|
||||
inner: client,
|
||||
})
|
||||
Ok(Client { inner: client })
|
||||
}
|
||||
|
||||
pub async fn secure(self, domain: &str, strict_tls: bool) -> Result<Self> {
|
||||
if self.is_secure {
|
||||
Ok(self)
|
||||
} else {
|
||||
let Client { mut inner, .. } = self;
|
||||
let tls = build_tls(strict_tls);
|
||||
inner.run_command_and_check_ok("STARTTLS", None).await?;
|
||||
pub async fn connect_starttls_socks5(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
socks5_config: Socks5Config,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream = socks5_config
|
||||
.connect((hostname, port), IMAP_TIMEOUT)
|
||||
.await?;
|
||||
|
||||
let stream = inner.into_inner();
|
||||
let ssl_stream = tls.connect(domain, stream).await?;
|
||||
let boxed: Box<dyn SessionStream> = Box::new(ssl_stream);
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(socks5_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
client
|
||||
.run_command_and_check_ok("STARTTLS", None)
|
||||
.await
|
||||
.context("STARTTLS command failed")?;
|
||||
let socks5_stream = client.into_inner();
|
||||
|
||||
Ok(Client {
|
||||
is_secure: true,
|
||||
inner: ImapClient::new(boxed),
|
||||
})
|
||||
}
|
||||
let tls = build_tls(strict_tls);
|
||||
let tls_stream = tls
|
||||
.connect(hostname, socks5_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 = ImapClient::new(session_stream);
|
||||
|
||||
Ok(Client { inner: client })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use async_imap::types::Mailbox;
|
||||
use async_imap::Session as ImapSession;
|
||||
use async_native_tls::TlsStream;
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
use tokio::io::BufWriter;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
@@ -33,12 +34,17 @@ pub(crate) trait SessionStream:
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>);
|
||||
}
|
||||
|
||||
impl SessionStream for TlsStream<Box<dyn SessionStream>> {
|
||||
impl SessionStream for Box<dyn SessionStream> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.as_mut().set_read_timeout(timeout);
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for TlsStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
}
|
||||
impl SessionStream for TlsStream<Pin<Box<TimeoutStream<TcpStream>>>> {
|
||||
impl<T: SessionStream> SessionStream for BufWriter<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
@@ -48,7 +54,7 @@ impl SessionStream for Pin<Box<TimeoutStream<TcpStream>>> {
|
||||
self.as_mut().set_read_timeout_pinned(timeout);
|
||||
}
|
||||
}
|
||||
impl SessionStream for Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>> {
|
||||
impl<T: SessionStream> SessionStream for Socks5Stream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_socket_mut().set_read_timeout(timeout)
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ mod dehtml;
|
||||
mod authres;
|
||||
mod color;
|
||||
pub mod html;
|
||||
mod net;
|
||||
pub mod plaintext;
|
||||
mod ratelimit;
|
||||
pub mod summary;
|
||||
@@ -117,3 +118,6 @@ pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
|
||||
mod test_utils;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(fuzzing)]
|
||||
pub mod fuzzing;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Location handling.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -20,32 +18,63 @@ use crate::mimeparser::SystemMessage;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{duration_to_str, time};
|
||||
|
||||
/// Location record
|
||||
/// Location record.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Location {
|
||||
/// Row ID of the location.
|
||||
pub location_id: u32,
|
||||
|
||||
/// Location latitude.
|
||||
pub latitude: f64,
|
||||
|
||||
/// Location longitude.
|
||||
pub longitude: f64,
|
||||
|
||||
/// Nonstandard `accuracy` attribute of the `coordinates` tag.
|
||||
pub accuracy: f64,
|
||||
|
||||
/// Location timestamp in seconds.
|
||||
pub timestamp: i64,
|
||||
|
||||
/// Contact ID.
|
||||
pub contact_id: ContactId,
|
||||
|
||||
/// Message ID.
|
||||
pub msg_id: u32,
|
||||
|
||||
/// Chat ID.
|
||||
pub chat_id: ChatId,
|
||||
|
||||
/// A marker string, such as an emoji, to be displayed on top of the location.
|
||||
pub marker: Option<String>,
|
||||
|
||||
/// Whether location is independent, i.e. not part of the path.
|
||||
pub independent: u32,
|
||||
}
|
||||
|
||||
impl Location {
|
||||
/// Creates a new empty location.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// KML document.
|
||||
///
|
||||
/// See <https://www.ogc.org/standards/kml/> for the standard and
|
||||
/// <https://developers.google.com/kml> for documentation.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Kml {
|
||||
/// Nonstandard `addr` attribute of the `Document` tag storing the user email address.
|
||||
pub addr: Option<String>,
|
||||
|
||||
/// Placemarks.
|
||||
pub locations: Vec<Location>,
|
||||
|
||||
/// Currently parsed XML tag.
|
||||
tag: KmlTag,
|
||||
|
||||
/// Currently parsed placemark.
|
||||
pub curr: Location,
|
||||
}
|
||||
|
||||
@@ -62,10 +91,12 @@ bitflags! {
|
||||
}
|
||||
|
||||
impl Kml {
|
||||
/// Creates a new empty KML document.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Parses a KML document.
|
||||
pub fn parse(to_parse: &[u8]) -> Result<Self> {
|
||||
ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
|
||||
|
||||
@@ -78,7 +109,7 @@ impl Kml {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
loop {
|
||||
match reader.read_event(&mut buf).with_context(|| {
|
||||
match reader.read_event_into(&mut buf).with_context(|| {
|
||||
format!(
|
||||
"location parsing error at position {}",
|
||||
reader.buffer_position()
|
||||
@@ -86,7 +117,7 @@ impl Kml {
|
||||
})? {
|
||||
quick_xml::events::Event::Start(ref e) => kml.starttag_cb(e, &reader),
|
||||
quick_xml::events::Event::End(ref e) => kml.endtag_cb(e),
|
||||
quick_xml::events::Event::Text(ref e) => kml.text_cb(e, &reader),
|
||||
quick_xml::events::Event::Text(ref e) => kml.text_cb(e),
|
||||
quick_xml::events::Event::Eof => break,
|
||||
_ => (),
|
||||
}
|
||||
@@ -96,9 +127,9 @@ impl Kml {
|
||||
Ok(kml)
|
||||
}
|
||||
|
||||
fn text_cb<B: std::io::BufRead>(&mut self, event: &BytesText, reader: &quick_xml::Reader<B>) {
|
||||
fn text_cb(&mut self, event: &BytesText) {
|
||||
if self.tag.contains(KmlTag::WHEN) || self.tag.contains(KmlTag::COORDINATES) {
|
||||
let val = event.unescape_and_decode(reader).unwrap_or_default();
|
||||
let val = event.unescape().unwrap_or_default();
|
||||
|
||||
let val = val.replace(['\n', '\r', '\t', ' '], "");
|
||||
|
||||
@@ -127,7 +158,9 @@ impl Kml {
|
||||
}
|
||||
|
||||
fn endtag_cb(&mut self, event: &BytesEnd) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
let tag = String::from_utf8_lossy(event.name().as_ref())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
|
||||
if tag == "placemark" {
|
||||
if self.tag.contains(KmlTag::PLACEMARK)
|
||||
@@ -147,14 +180,20 @@ impl Kml {
|
||||
event: &BytesStart,
|
||||
reader: &quick_xml::Reader<B>,
|
||||
) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
let tag = String::from_utf8_lossy(event.name().as_ref())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
if tag == "document" {
|
||||
if let Some(addr) = event
|
||||
.attributes()
|
||||
.filter_map(|a| a.ok())
|
||||
.find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "addr")
|
||||
{
|
||||
self.addr = addr.unescape_and_decode_value(reader).ok();
|
||||
if let Some(addr) = event.attributes().filter_map(|a| a.ok()).find(|attr| {
|
||||
String::from_utf8_lossy(attr.key.as_ref())
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
== "addr"
|
||||
}) {
|
||||
self.addr = addr
|
||||
.decode_and_unescape_value(reader)
|
||||
.ok()
|
||||
.map(|a| a.into_owned());
|
||||
}
|
||||
} else if tag == "placemark" {
|
||||
self.tag = KmlTag::PLACEMARK;
|
||||
@@ -172,12 +211,17 @@ impl Kml {
|
||||
self.tag = KmlTag::PLACEMARK | KmlTag::POINT | KmlTag::COORDINATES;
|
||||
if let Some(acc) = event.attributes().find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "accuracy")
|
||||
.map(|a| {
|
||||
String::from_utf8_lossy(a.key.as_ref())
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
== "accuracy"
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}) {
|
||||
let v = acc
|
||||
.unwrap()
|
||||
.unescape_and_decode_value(reader)
|
||||
.decode_and_unescape_value(reader)
|
||||
.unwrap_or_default();
|
||||
|
||||
self.curr.accuracy = v.trim().parse().unwrap_or_default();
|
||||
@@ -259,6 +303,7 @@ pub async fn is_sending_locations_to_chat(
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Sets current location of the user device.
|
||||
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
|
||||
if latitude == 0.0 && longitude == 0.0 {
|
||||
return true;
|
||||
@@ -306,6 +351,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
||||
continue_streaming
|
||||
}
|
||||
|
||||
/// Searches for locations in the given time range, optionally filtering by chat and contact IDs.
|
||||
pub async fn get_range(
|
||||
context: &Context,
|
||||
chat_id: Option<ChatId>,
|
||||
@@ -396,6 +442,7 @@ pub async fn delete_all(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `location.kml` contents.
|
||||
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
|
||||
let mut last_added_location_id = 0;
|
||||
|
||||
@@ -481,6 +528,7 @@ fn get_kml_timestamp(utc: i64) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Returns a KML document containing a single location with the given timestamp and coordinates.
|
||||
pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String {
|
||||
format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||
@@ -498,6 +546,7 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets the timestamp of the last time location was sent in the chat.
|
||||
pub async fn set_kml_sent_timestamp(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
@@ -513,6 +562,7 @@ pub async fn set_kml_sent_timestamp(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the location of the message.
|
||||
pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
|
||||
@@ -1168,6 +1168,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||||
"3gp" => (Viewtype::Video, "video/3gpp"),
|
||||
"aac" => (Viewtype::Audio, "audio/aac"),
|
||||
"avi" => (Viewtype::Video, "video/x-msvideo"),
|
||||
"avif" => (Viewtype::File, "image/avif"), // supported since Android 12 / iOS 16
|
||||
"doc" => (Viewtype::File, "application/msword"),
|
||||
"docx" => (
|
||||
Viewtype::File,
|
||||
@@ -1176,6 +1177,8 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||||
"epub" => (Viewtype::File, "application/epub+zip"),
|
||||
"flac" => (Viewtype::Audio, "audio/flac"),
|
||||
"gif" => (Viewtype::Gif, "image/gif"),
|
||||
"heic" => (Viewtype::File, "image/heic"), // supported since Android 10 / iOS 11
|
||||
"heif" => (Viewtype::File, "image/heif"), // supported since Android 10 / iOS 11
|
||||
"html" => (Viewtype::File, "text/html"),
|
||||
"htm" => (Viewtype::File, "text/html"),
|
||||
"ico" => (Viewtype::File, "image/vnd.microsoft.icon"),
|
||||
@@ -1200,10 +1203,15 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||||
"oga" => (Viewtype::Audio, "audio/ogg"),
|
||||
"ogg" => (Viewtype::Audio, "audio/ogg"),
|
||||
"ogv" => (Viewtype::File, "video/ogg"),
|
||||
"opus" => (Viewtype::File, "audio/ogg"), // not supported eg. on Android 4
|
||||
"opus" => (Viewtype::File, "audio/ogg"), // supported since Android 10
|
||||
"otf" => (Viewtype::File, "font/otf"),
|
||||
"pdf" => (Viewtype::File, "application/pdf"),
|
||||
"png" => (Viewtype::Image, "image/png"),
|
||||
"ppt" => (Viewtype::File, "application/vnd.ms-powerpoint"),
|
||||
"pptx" => (
|
||||
Viewtype::File,
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
),
|
||||
"rar" => (Viewtype::File, "application/vnd.rar"),
|
||||
"rtf" => (Viewtype::File, "application/rtf"),
|
||||
"spx" => (Viewtype::File, "audio/ogg"), // Ogg Speex Profile
|
||||
@@ -1212,6 +1220,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||||
"tiff" => (Viewtype::File, "image/tiff"),
|
||||
"tif" => (Viewtype::File, "image/tiff"),
|
||||
"ttf" => (Viewtype::File, "font/ttf"),
|
||||
"txt" => (Viewtype::File, "text/plain"),
|
||||
"vcard" => (Viewtype::File, "text/vcard"),
|
||||
"vcf" => (Viewtype::File, "text/vcard"),
|
||||
"wav" => (Viewtype::File, "audio/wav"),
|
||||
@@ -1221,11 +1230,12 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||||
"wmv" => (Viewtype::Video, "video/x-ms-wmv"),
|
||||
"xdc" => (Viewtype::Webxdc, "application/webxdc+zip"),
|
||||
"xhtml" => (Viewtype::File, "application/xhtml+xml"),
|
||||
"xls" => (Viewtype::File, "application/vnd.ms-excel"),
|
||||
"xlsx" => (
|
||||
Viewtype::File,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
),
|
||||
"xml" => (Viewtype::File, "application/vnd.ms-excel"),
|
||||
"xml" => (Viewtype::File, "application/xml"),
|
||||
"zip" => (Viewtype::File, "application/zip"),
|
||||
_ => {
|
||||
return None;
|
||||
|
||||
@@ -799,6 +799,7 @@ impl<'a> MimeFactory<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns MIME part with a `message.kml` attachment.
|
||||
fn get_message_kml_part(&self) -> Option<PartBuilder> {
|
||||
let latitude = self.msg.param.get_float(Param::SetLatitude)?;
|
||||
let longitude = self.msg.param.get_float(Param::SetLongitude)?;
|
||||
@@ -818,6 +819,7 @@ impl<'a> MimeFactory<'a> {
|
||||
Some(part)
|
||||
}
|
||||
|
||||
/// Returns MIME part with a `location.kml` attachment.
|
||||
async fn get_location_kml_part(&mut self, context: &Context) -> Result<PartBuilder> {
|
||||
let (kml_content, last_added_location_id) =
|
||||
location::get_kml(context, self.msg.chat_id).await?;
|
||||
|
||||
33
src/net.rs
Normal file
33
src/net.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
///! # Common network utilities.
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
use tokio::time::timeout;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
/// Returns a TCP connection stream with read/write timeouts set
|
||||
/// and Nagle's algorithm disabled with `TCP_NODELAY`.
|
||||
///
|
||||
/// `TCP_NODELAY` ensures writing to the stream always results in immediate sending of the packet
|
||||
/// to the network, which is important to reduce the latency of interactive protocols such as IMAP.
|
||||
pub(crate) async fn connect_tcp(
|
||||
addr: impl ToSocketAddrs,
|
||||
timeout_val: Duration,
|
||||
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
|
||||
let tcp_stream = timeout(timeout_val, TcpStream::connect(addr))
|
||||
.await
|
||||
.context("connection timeout")?
|
||||
.context("connection failure")?;
|
||||
|
||||
// Disable Nagle's algorithm.
|
||||
tcp_stream.set_nodelay(true)?;
|
||||
|
||||
let mut timeout_stream = TimeoutStream::new(tcp_stream);
|
||||
timeout_stream.set_write_timeout(Some(timeout_val));
|
||||
timeout_stream.set_read_timeout(Some(timeout_val));
|
||||
let pinned_stream = Box::pin(timeout_stream);
|
||||
|
||||
Ok(pinned_stream)
|
||||
}
|
||||
@@ -141,8 +141,19 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
match ctx.get_config_bool(Config::FetchedExistingMsgs).await {
|
||||
Ok(fetched_existing_msgs) => {
|
||||
if !fetched_existing_msgs {
|
||||
// Consider it done even if we fail.
|
||||
//
|
||||
// This operation is not critical enough to retry,
|
||||
// especially if the error is persistent.
|
||||
if let Err(err) =
|
||||
ctx.set_config_bool(Config::FetchedExistingMsgs, true).await
|
||||
{
|
||||
warn!(ctx, "Can't set Config::FetchedExistingMsgs: {:#}", err);
|
||||
}
|
||||
|
||||
if let Err(err) = connection.fetch_existing_msgs(&ctx).await {
|
||||
warn!(ctx, "Failed to fetch existing messages: {:#}", err);
|
||||
connection.trigger_reconnect(&ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,8 +209,8 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
|
||||
.await
|
||||
.context("prepare IMAP connection")
|
||||
{
|
||||
connection.trigger_reconnect(ctx);
|
||||
warn!(ctx, "{:#}", err);
|
||||
connection.trigger_reconnect(ctx);
|
||||
return connection.fake_idle(ctx, Some(watch_folder)).await;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! # Simplify incoming plaintext.
|
||||
|
||||
// protect lines starting with `--` against being treated as a footer.
|
||||
// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
|
||||
// this should be invisible on most systems and there is no need to unescape it again
|
||||
// (which won't be done by non-deltas anyway)
|
||||
//
|
||||
// this escapes a bit more than actually needed by delta (eg. also lines as "-- footer"),
|
||||
// but for non-delta-compatibility, that seems to be better.
|
||||
// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
|
||||
/// Protects lines starting with `--` against being treated as a footer.
|
||||
/// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
|
||||
/// this should be invisible on most systems and there is no need to unescape it again
|
||||
/// (which won't be done by non-deltas anyway).
|
||||
///
|
||||
/// This escapes a bit more than actually needed by delta (e.g. also lines as "-- footer"),
|
||||
/// but for non-delta-compatibility, that seems to be better.
|
||||
/// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
|
||||
pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
if let Some(text) = text.strip_prefix("--") {
|
||||
"-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-")
|
||||
@@ -74,6 +74,7 @@ pub(crate) fn split_lines(buf: &str) -> Vec<&str> {
|
||||
/// Simplified text and some additional information gained from the input.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct SimplifiedText {
|
||||
/// The text itself.
|
||||
pub text: String,
|
||||
|
||||
/// True if the message is forwarded.
|
||||
|
||||
16
src/socks.rs
16
src/socks.rs
@@ -4,10 +4,10 @@ use std::fmt;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use crate::net::connect_tcp;
|
||||
use anyhow::Result;
|
||||
pub use async_smtp::ServerAddress;
|
||||
use tokio::net::{self, TcpStream};
|
||||
use tokio::time::timeout;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
use crate::context::Context;
|
||||
@@ -59,14 +59,7 @@ impl Socks5Config {
|
||||
target_addr: impl net::ToSocketAddrs,
|
||||
timeout_val: Duration,
|
||||
) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
|
||||
let tcp_stream = timeout(timeout_val, TcpStream::connect(target_addr))
|
||||
.await
|
||||
.context("connection timeout")?
|
||||
.context("connection failure")?;
|
||||
let mut timeout_stream = TimeoutStream::new(tcp_stream);
|
||||
timeout_stream.set_write_timeout(Some(timeout_val));
|
||||
timeout_stream.set_read_timeout(Some(timeout_val));
|
||||
let timeout_stream = Box::pin(timeout_stream);
|
||||
let tcp_stream = connect_tcp(target_addr, timeout_val).await?;
|
||||
|
||||
let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
|
||||
{
|
||||
@@ -78,8 +71,7 @@ impl Socks5Config {
|
||||
None
|
||||
};
|
||||
let socks_stream =
|
||||
Socks5Stream::use_stream(timeout_stream, authentication_method, Config::default())
|
||||
.await?;
|
||||
Socks5Stream::use_stream(tcp_stream, authentication_method, Config::default()).await?;
|
||||
|
||||
Ok(socks_stream)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! # Message summary for chatlist.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use crate::chat::Chat;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
@@ -54,6 +52,8 @@ pub struct Summary {
|
||||
}
|
||||
|
||||
impl Summary {
|
||||
/// Constucts chatlist summary
|
||||
/// from the provided message, chat and message author contact snapshots.
|
||||
pub async fn new(
|
||||
context: &Context,
|
||||
msg: &Message,
|
||||
|
||||
Reference in New Issue
Block a user