mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +03:00
Merge stable into master
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -4882,9 +4882,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tungstenite"
|
name = "tokio-tungstenite"
|
||||||
version = "0.20.0"
|
version = "0.20.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2"
|
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
@@ -5095,9 +5095,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.20.0"
|
version = "0.20.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649"
|
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
@@ -5063,6 +5063,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
|||||||
* A string containing the verifiers address. If it is the same address as the contact itself,
|
* A string containing the verifiers address. If it is the same address as the contact itself,
|
||||||
* we verified the contact ourself. If it is an empty string, we don't have verifier
|
* we verified the contact ourself. If it is an empty string, we don't have verifier
|
||||||
* information or the contact is not verified.
|
* information or the contact is not verified.
|
||||||
|
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
|
||||||
*/
|
*/
|
||||||
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||||
|
|
||||||
@@ -5075,7 +5076,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
|||||||
* @memberof dc_contact_t
|
* @memberof dc_contact_t
|
||||||
* @param contact The contact object.
|
* @param contact The contact object.
|
||||||
* @return
|
* @return
|
||||||
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
|
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
|
||||||
* we verified the contact ourself. If it is 0, we don't have verifier information or
|
* we verified the contact ourself. If it is 0, we don't have verifier information or
|
||||||
* the contact is not verified.
|
* the contact is not verified.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2533,7 +2533,12 @@ pub unsafe extern "C" fn dc_set_location(
|
|||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
|
block_on(async move {
|
||||||
|
location::set(ctx, latitude, longitude, accuracy)
|
||||||
|
.await
|
||||||
|
.log_err(ctx)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}) as libc::c_int
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -4507,7 +4512,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
|||||||
|
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
|
match block_on(provider::get_provider_info_by_addr(
|
||||||
|
ctx,
|
||||||
|
addr.as_str(),
|
||||||
|
true,
|
||||||
|
))
|
||||||
|
.log_err(ctx)
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
Some(provider) => provider,
|
Some(provider) => provider,
|
||||||
None => ptr::null_mut(),
|
None => ptr::null_mut(),
|
||||||
}
|
}
|
||||||
@@ -4534,11 +4546,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
|||||||
|
|
||||||
match socks5_enabled {
|
match socks5_enabled {
|
||||||
Ok(socks5_enabled) => {
|
Ok(socks5_enabled) => {
|
||||||
match block_on(provider::get_provider_info(
|
match block_on(provider::get_provider_info_by_addr(
|
||||||
ctx,
|
ctx,
|
||||||
addr.as_str(),
|
addr.as_str(),
|
||||||
socks5_enabled,
|
socks5_enabled,
|
||||||
)) {
|
))
|
||||||
|
.log_err(ctx)
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
Some(provider) => provider,
|
Some(provider) => provider,
|
||||||
None => ptr::null_mut(),
|
None => ptr::null_mut(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -903,7 +903,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let latitude = arg1.parse()?;
|
let latitude = arg1.parse()?;
|
||||||
let longitude = arg2.parse()?;
|
let longitude = arg2.parse()?;
|
||||||
|
|
||||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
|
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
|
||||||
if continue_streaming {
|
if continue_streaming {
|
||||||
println!("Success, streaming should be continued.");
|
println!("Success, streaming should be continued.");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3578,6 +3578,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
|||||||
msg.param.remove(Param::ForcePlaintext);
|
msg.param.remove(Param::ForcePlaintext);
|
||||||
msg.param.remove(Param::Cmd);
|
msg.param.remove(Param::Cmd);
|
||||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||||
|
msg.param.remove(Param::WebxdcDocument);
|
||||||
|
msg.param.remove(Param::WebxdcDocumentTimestamp);
|
||||||
msg.param.remove(Param::WebxdcSummary);
|
msg.param.remove(Param::WebxdcSummary);
|
||||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||||
msg.in_reply_to = None;
|
msg.in_reply_to = None;
|
||||||
|
|||||||
@@ -1236,13 +1236,24 @@ impl Contact {
|
|||||||
|
|
||||||
/// Returns the ContactId that verified the contact.
|
/// Returns the ContactId that verified the contact.
|
||||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||||
let verifier_addr = self.get_verifier_addr(context).await?;
|
let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
|
||||||
if let Some(addr) = verifier_addr {
|
return Ok(None);
|
||||||
Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?)
|
};
|
||||||
} else {
|
|
||||||
|
if verifier_addr == self.addr {
|
||||||
|
// Contact is directly verified via QR code.
|
||||||
|
return Ok(Some(ContactId::SELF));
|
||||||
|
}
|
||||||
|
|
||||||
|
match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::AddressBook).await? {
|
||||||
|
Some(contact_id) => Ok(Some(contact_id)),
|
||||||
|
None => {
|
||||||
|
let addr = &self.addr;
|
||||||
|
warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}.");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the number of real (i.e. non-special) contacts in the database.
|
/// Returns the number of real (i.e. non-special) contacts in the database.
|
||||||
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
|
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ impl Context {
|
|||||||
translated_stockstrings: stockstrings,
|
translated_stockstrings: stockstrings,
|
||||||
events,
|
events,
|
||||||
scheduler: SchedulerState::new(),
|
scheduler: SchedulerState::new(),
|
||||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
|
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
|
||||||
quota: RwLock::new(None),
|
quota: RwLock::new(None),
|
||||||
quota_update_request: AtomicBool::new(false),
|
quota_update_request: AtomicBool::new(false),
|
||||||
resync_request: AtomicBool::new(false),
|
resync_request: AtomicBool::new(false),
|
||||||
@@ -820,7 +820,22 @@ impl Context {
|
|||||||
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
|
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
|
||||||
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
|
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
|
||||||
Some(s) => MsgId::new(s.parse()?),
|
Some(s) => MsgId::new(s.parse()?),
|
||||||
None => MsgId::new_unset(),
|
None => {
|
||||||
|
// If `last_msg_id` is not set yet,
|
||||||
|
// subtract 1 from the last id,
|
||||||
|
// so a single message is returned and can
|
||||||
|
// be marked as seen.
|
||||||
|
self.sql
|
||||||
|
.query_row(
|
||||||
|
"SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)",
|
||||||
|
(),
|
||||||
|
|row| {
|
||||||
|
let msg_id: MsgId = row.get(0)?;
|
||||||
|
Ok(msg_id)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let list = self
|
let list = self
|
||||||
|
|||||||
@@ -328,13 +328,13 @@ pub async fn is_sending_locations_to_chat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets current location of the user device.
|
/// Sets current location of the user device.
|
||||||
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
|
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
|
||||||
if latitude == 0.0 && longitude == 0.0 {
|
if latitude == 0.0 && longitude == 0.0 {
|
||||||
return true;
|
return Ok(true);
|
||||||
}
|
}
|
||||||
let mut continue_streaming = false;
|
let mut continue_streaming = false;
|
||||||
|
|
||||||
if let Ok(chats) = context
|
let chats = context
|
||||||
.sql
|
.sql
|
||||||
.query_map(
|
.query_map(
|
||||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||||
@@ -346,10 +346,10 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
{
|
|
||||||
for chat_id in chats {
|
for chat_id in chats {
|
||||||
if let Err(err) = context.sql.execute(
|
context.sql.execute(
|
||||||
"INSERT INTO locations \
|
"INSERT INTO locations \
|
||||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||||
(
|
(
|
||||||
@@ -359,20 +359,16 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
|||||||
time(),
|
time(),
|
||||||
chat_id,
|
chat_id,
|
||||||
ContactId::SELF,
|
ContactId::SELF,
|
||||||
)
|
)).await.context("Failed to store location")?;
|
||||||
).await {
|
|
||||||
warn!(context, "failed to store location {:#}", err);
|
info!(context, "Stored location for chat {chat_id}.");
|
||||||
} else {
|
|
||||||
info!(context, "stored location for chat {}", chat_id);
|
|
||||||
continue_streaming = true;
|
continue_streaming = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if continue_streaming {
|
if continue_streaming {
|
||||||
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
continue_streaming
|
Ok(continue_streaming)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Searches for locations in the given time range, optionally filtering by chat and contact IDs.
|
/// Searches for locations in the given time range, optionally filtering by chat and contact IDs.
|
||||||
@@ -464,7 +460,7 @@ pub async fn delete_all(context: &Context) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `location.kml` contents.
|
/// Returns `location.kml` contents.
|
||||||
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
|
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(String, u32)>> {
|
||||||
let mut last_added_location_id = 0;
|
let mut last_added_location_id = 0;
|
||||||
|
|
||||||
let self_addr = context.get_primary_self_addr().await?;
|
let self_addr = context.get_primary_self_addr().await?;
|
||||||
@@ -534,9 +530,11 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
|||||||
ret += "</Document>\n</kml>";
|
ret += "</Document>\n</kml>";
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure!(location_count > 0, "No locations processed");
|
if location_count > 0 {
|
||||||
|
Ok(Some((ret, last_added_location_id)))
|
||||||
Ok((ret, last_added_location_id))
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_kml_timestamp(utc: i64) -> String {
|
fn get_kml_timestamp(utc: i64) -> String {
|
||||||
@@ -928,4 +926,38 @@ Content-Disposition: attachment; filename="location.kml"
|
|||||||
assert_eq!(locations.len(), 1);
|
assert_eq!(locations.len(), 1);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_send_locations_to_chat() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
let alice_chat = alice.create_chat(&bob).await;
|
||||||
|
send_locations_to_chat(&alice, alice_chat.id, 1000).await?;
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
let msg = bob.recv_msg(&sent).await;
|
||||||
|
assert_eq!(msg.text, "Location streaming enabled by alice@example.org.");
|
||||||
|
let bob_chat_id = msg.chat_id;
|
||||||
|
|
||||||
|
assert_eq!(set(&alice, 10.0, 20.0, 1.0).await?, true);
|
||||||
|
|
||||||
|
// Send image without text.
|
||||||
|
let file_name = "image.png";
|
||||||
|
let bytes = include_bytes!("../test-data/image/logo.png");
|
||||||
|
let file = alice.get_blobdir().join(file_name);
|
||||||
|
tokio::fs::write(&file, bytes).await?;
|
||||||
|
let mut msg = Message::new(Viewtype::Image);
|
||||||
|
msg.set_file(file.to_str().unwrap(), None);
|
||||||
|
let sent = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||||
|
|
||||||
|
let msg = bob.recv_msg_opt(&sent).await.unwrap();
|
||||||
|
assert!(msg.chat_id == bob_chat_id);
|
||||||
|
assert_eq!(msg.msg_ids.len(), 1);
|
||||||
|
|
||||||
|
let bob_msg = Message::load_from_db(&bob, *msg.msg_ids.get(0).unwrap()).await?;
|
||||||
|
assert_eq!(bob_msg.chat_id, bob_chat_id);
|
||||||
|
assert_eq!(bob_msg.viewtype, Viewtype::Image);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -860,9 +860,13 @@ impl<'a> MimeFactory<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns MIME part with a `location.kml` attachment.
|
/// Returns MIME part with a `location.kml` attachment.
|
||||||
async fn get_location_kml_part(&mut self, context: &Context) -> Result<PartBuilder> {
|
async fn get_location_kml_part(&mut self, context: &Context) -> Result<Option<PartBuilder>> {
|
||||||
let (kml_content, last_added_location_id) =
|
let Some((kml_content, last_added_location_id)) =
|
||||||
location::get_kml(context, self.msg.chat_id).await?;
|
location::get_kml(context, self.msg.chat_id).await?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
let part = PartBuilder::new()
|
let part = PartBuilder::new()
|
||||||
.content_type(
|
.content_type(
|
||||||
&"application/vnd.google-earth.kml+xml"
|
&"application/vnd.google-earth.kml+xml"
|
||||||
@@ -878,7 +882,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
// otherwise, the independent location is already filed
|
// otherwise, the independent location is already filed
|
||||||
self.last_added_location_id = Some(last_added_location_id);
|
self.last_added_location_id = Some(last_added_location_id);
|
||||||
}
|
}
|
||||||
Ok(part)
|
Ok(Some(part))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cognitive_complexity)]
|
#[allow(clippy::cognitive_complexity)]
|
||||||
@@ -1177,7 +1181,10 @@ impl<'a> MimeFactory<'a> {
|
|||||||
}
|
}
|
||||||
let flowed_text = format_flowed(final_text);
|
let flowed_text = format_flowed(final_text);
|
||||||
|
|
||||||
let footer = &self.selfstatus;
|
let is_reaction = self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
|
||||||
|
|
||||||
|
let footer = if is_reaction { "" } else { &self.selfstatus };
|
||||||
|
|
||||||
let message_text = format!(
|
let message_text = format!(
|
||||||
"{}{}{}{}{}{}",
|
"{}{}{}{}{}{}",
|
||||||
fwdhint.unwrap_or_default(),
|
fwdhint.unwrap_or_default(),
|
||||||
@@ -1200,7 +1207,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
))
|
))
|
||||||
.body(message_text);
|
.body(message_text);
|
||||||
|
|
||||||
if self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0 {
|
if is_reaction {
|
||||||
main_part = main_part.header(("Content-Disposition", "reaction"));
|
main_part = main_part.header(("Content-Disposition", "reaction"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1239,11 +1246,8 @@ impl<'a> MimeFactory<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await? {
|
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await? {
|
||||||
match self.get_location_kml_part(context).await {
|
if let Some(part) = self.get_location_kml_part(context).await? {
|
||||||
Ok(part) => parts.push(part),
|
parts.push(part);
|
||||||
Err(err) => {
|
|
||||||
warn!(context, "mimefactory: could not send location: {}", err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,15 +1376,16 @@ impl<'a> MimeFactory<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns base64-encoded buffer `buf` split into 78-bytes long
|
/// Returns base64-encoded buffer `buf` split into 76-bytes long
|
||||||
/// chunks separated by CRLF.
|
/// chunks separated by CRLF.
|
||||||
///
|
///
|
||||||
/// This line length limit is an
|
/// [RFC2045 specification of base64 Content-Transfer-Encoding](https://datatracker.ietf.org/doc/html/rfc2045#section-6.8)
|
||||||
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
|
/// says that "The encoded output stream must be represented in lines of no more than 76 characters each."
|
||||||
|
/// Longer lines trigger `BASE64_LENGTH_78_79` rule of SpamAssassin.
|
||||||
pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
|
pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
|
||||||
let base64 = base64::engine::general_purpose::STANDARD.encode(buf);
|
let base64 = base64::engine::general_purpose::STANDARD.encode(buf);
|
||||||
let mut chars = base64.chars();
|
let mut chars = base64.chars();
|
||||||
std::iter::repeat_with(|| chars.by_ref().take(78).collect::<String>())
|
std::iter::repeat_with(|| chars.by_ref().take(76).collect::<String>())
|
||||||
.take_while(|s| !s.is_empty())
|
.take_while(|s| !s.is_empty())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\r\n")
|
.join("\r\n")
|
||||||
@@ -1620,8 +1625,8 @@ mod tests {
|
|||||||
fn test_wrapped_base64_encode() {
|
fn test_wrapped_base64_encode() {
|
||||||
let input = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
let input = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
let output =
|
let output =
|
||||||
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU\r\n\
|
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\r\n\
|
||||||
FBQUFBQUFBQQ==";
|
QUFBQUFBQUFBQQ==";
|
||||||
assert_eq!(wrapped_base64_encode(input), output);
|
assert_eq!(wrapped_base64_encode(input), output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1442,7 +1442,18 @@ impl MimeMessage {
|
|||||||
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
||||||
|
|
||||||
// must be present
|
// must be present
|
||||||
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
|
if report_fields
|
||||||
|
.get_header_value(HeaderDef::Disposition)
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
context,
|
||||||
|
"Ignoring unknown disposition-notification, Message-Id: {:?}.",
|
||||||
|
report_fields.get_header_value(HeaderDef::MessageId)
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
let original_message_id = report_fields
|
let original_message_id = report_fields
|
||||||
.get_header_value(HeaderDef::OriginalMessageId)
|
.get_header_value(HeaderDef::OriginalMessageId)
|
||||||
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
|
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
|
||||||
@@ -1457,18 +1468,10 @@ impl MimeMessage {
|
|||||||
.collect()
|
.collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
return Ok(Some(Report {
|
Ok(Some(Report {
|
||||||
original_message_id,
|
original_message_id,
|
||||||
additional_message_ids,
|
additional_message_ids,
|
||||||
}));
|
}))
|
||||||
}
|
|
||||||
warn!(
|
|
||||||
context,
|
|
||||||
"ignoring unknown disposition-notification, Message-Id: {:?}",
|
|
||||||
report_fields.get_header_value(HeaderDef::MessageId)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_delivery_status(
|
fn process_delivery_status(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use trust_dns_resolver::{config, AsyncResolver, TokioAsyncResolver};
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
|
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
|
||||||
|
use crate::tools::EmailAddress;
|
||||||
|
|
||||||
/// Provider status according to manual testing.
|
/// Provider status according to manual testing.
|
||||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||||
@@ -175,21 +176,30 @@ fn get_resolver() -> Result<TokioAsyncResolver> {
|
|||||||
Ok(resolver)
|
Ok(resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns provider for the given an e-mail address.
|
||||||
|
///
|
||||||
|
/// Returns an error if provided address is not valid.
|
||||||
|
pub async fn get_provider_info_by_addr(
|
||||||
|
context: &Context,
|
||||||
|
addr: &str,
|
||||||
|
skip_mx: bool,
|
||||||
|
) -> Result<Option<&'static Provider>> {
|
||||||
|
let addr = EmailAddress::new(addr)?;
|
||||||
|
|
||||||
|
let provider = get_provider_info(context, &addr.domain, skip_mx).await;
|
||||||
|
Ok(provider)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns provider for the given domain.
|
/// Returns provider for the given domain.
|
||||||
///
|
///
|
||||||
/// This function looks up domain in offline database first. If not
|
/// This function looks up domain in offline database first. If not
|
||||||
/// found, it queries MX record for the domain and looks up offline
|
/// found, it queries MX record for the domain and looks up offline
|
||||||
/// database for MX domains.
|
/// database for MX domains.
|
||||||
///
|
|
||||||
/// For compatibility, email address can be passed to this function
|
|
||||||
/// instead of the domain.
|
|
||||||
pub async fn get_provider_info(
|
pub async fn get_provider_info(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
domain: &str,
|
domain: &str,
|
||||||
skip_mx: bool,
|
skip_mx: bool,
|
||||||
) -> Option<&'static Provider> {
|
) -> Option<&'static Provider> {
|
||||||
let domain = domain.rsplit('@').next()?;
|
|
||||||
|
|
||||||
if let Some(provider) = get_provider_by_domain(domain) {
|
if let Some(provider) = get_provider_by_domain(domain) {
|
||||||
return Some(provider);
|
return Some(provider);
|
||||||
}
|
}
|
||||||
@@ -314,15 +324,25 @@ mod tests {
|
|||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
assert!(get_provider_info(&t, "", false).await.is_none());
|
assert!(get_provider_info(&t, "", false).await.is_none());
|
||||||
assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
|
assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
|
||||||
|
assert!(get_provider_info(&t, "example@google.com", false)
|
||||||
// get_provider_info() accepts email addresses for backwards compatibility
|
|
||||||
assert!(
|
|
||||||
get_provider_info(&t, "example@google.com", false)
|
|
||||||
.await
|
.await
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_get_provider_info_by_addr() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
assert!(get_provider_info_by_addr(&t, "google.com", false)
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
assert!(
|
||||||
|
get_provider_info_by_addr(&t, "example@google.com", false)
|
||||||
|
.await?
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.id
|
.id
|
||||||
== "gmail"
|
== "gmail"
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -464,6 +464,16 @@ Content-Disposition: reaction\n\
|
|||||||
let alice = TestContext::new_alice().await;
|
let alice = TestContext::new_alice().await;
|
||||||
let bob = TestContext::new_bob().await;
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
// Test that the status does not get mixed up into reactions.
|
||||||
|
alice
|
||||||
|
.set_config(
|
||||||
|
Config::Selfstatus,
|
||||||
|
Some("Buy Delta Chat today and make this banner go away!"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
bob.set_config(Config::Selfstatus, Some("Sent from my Delta Chat Pro. 👍"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
let chat_alice = alice.create_chat(&bob).await;
|
let chat_alice = alice.create_chat(&bob).await;
|
||||||
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
|
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
|
||||||
let bob_msg = bob.recv_msg(&alice_msg).await;
|
let bob_msg = bob.recv_msg(&alice_msg).await;
|
||||||
|
|||||||
@@ -113,8 +113,11 @@ pub(crate) async fn receive_imf_inner(
|
|||||||
{
|
{
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(context, "receive_imf: can't parse MIME: {err:#}.");
|
warn!(context, "receive_imf: can't parse MIME: {err:#}.");
|
||||||
let msg_ids;
|
if rfc724_mid.starts_with(GENERATED_PREFIX) {
|
||||||
if !rfc724_mid.starts_with(GENERATED_PREFIX) {
|
// We don't have an rfc724_mid, there's no point in adding a trash entry
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
let row_id = context
|
let row_id = context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
@@ -122,11 +125,7 @@ pub(crate) async fn receive_imf_inner(
|
|||||||
(rfc724_mid, DC_CHAT_ID_TRASH),
|
(rfc724_mid, DC_CHAT_ID_TRASH),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
|
let msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
|
||||||
} else {
|
|
||||||
return Ok(None);
|
|
||||||
// We don't have an rfc724_mid, there's no point in adding a trash entry
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(Some(ReceivedMsg {
|
return Ok(Some(ReceivedMsg {
|
||||||
chat_id: DC_CHAT_ID_TRASH,
|
chat_id: DC_CHAT_ID_TRASH,
|
||||||
@@ -1156,7 +1155,8 @@ async fn add_parts(
|
|||||||
(&part.msg, part.typ)
|
(&part.msg, part.typ)
|
||||||
};
|
};
|
||||||
|
|
||||||
let part_is_empty = part.msg.is_empty() && part.param.get(Param::Quote).is_none();
|
let part_is_empty =
|
||||||
|
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
|
||||||
let mime_modified = save_mime_modified && !part_is_empty;
|
let mime_modified = save_mime_modified && !part_is_empty;
|
||||||
if mime_modified {
|
if mime_modified {
|
||||||
// Avoid setting mime_modified for more than one part.
|
// Avoid setting mime_modified for more than one part.
|
||||||
@@ -1181,7 +1181,8 @@ async fn add_parts(
|
|||||||
|
|
||||||
// If you change which information is skipped if the message is trashed,
|
// If you change which information is skipped if the message is trashed,
|
||||||
// also change `MsgId::trash()` and `delete_expired_messages()`
|
// also change `MsgId::trash()` and `delete_expired_messages()`
|
||||||
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
|
let trash =
|
||||||
|
chat_id.is_trash() || (is_location_kml && msg.is_empty() && typ == Viewtype::Text);
|
||||||
|
|
||||||
let row_id = context
|
let row_id = context
|
||||||
.sql
|
.sql
|
||||||
@@ -1453,7 +1454,10 @@ async fn lookup_chat_by_reply(
|
|||||||
) -> Result<Option<(ChatId, Blocked)>> {
|
) -> Result<Option<(ChatId, Blocked)>> {
|
||||||
// Try to assign message to the same chat as the parent message.
|
// Try to assign message to the same chat as the parent message.
|
||||||
|
|
||||||
if let Some(parent) = parent {
|
let Some(parent) = parent else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
|
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
|
||||||
|
|
||||||
if parent.download_state != DownloadState::Done
|
if parent.download_state != DownloadState::Done
|
||||||
@@ -1484,10 +1488,7 @@ async fn lookup_chat_by_reply(
|
|||||||
// If the parent chat is a 1:1 chat, and the sender is a classical MUA and added
|
// If the parent chat is a 1:1 chat, and the sender is a classical MUA and added
|
||||||
// a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
|
// a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
|
||||||
// newly created ad-hoc group.
|
// newly created ad-hoc group.
|
||||||
if parent_chat.typ == Chattype::Single
|
if parent_chat.typ == Chattype::Single && !mime_parser.has_chat_version() && to_ids.len() > 1 {
|
||||||
&& !mime_parser.has_chat_version()
|
|
||||||
&& to_ids.len() > 1
|
|
||||||
{
|
|
||||||
let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
|
let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
|
||||||
chat_contacts.push(ContactId::SELF);
|
chat_contacts.push(ContactId::SELF);
|
||||||
if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
|
if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
|
||||||
@@ -1499,10 +1500,7 @@ async fn lookup_chat_by_reply(
|
|||||||
context,
|
context,
|
||||||
"Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
|
"Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
|
||||||
);
|
);
|
||||||
return Ok(Some((parent_chat.id, parent_chat.blocked)));
|
Ok(Some((parent_chat.id, parent_chat.blocked)))
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
|
/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
|
||||||
@@ -2058,7 +2056,10 @@ async fn apply_mailinglist_changes(
|
|||||||
mime_parser: &MimeMessage,
|
mime_parser: &MimeMessage,
|
||||||
chat_id: ChatId,
|
chat_id: ChatId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Some(list_post) = &mime_parser.list_post {
|
let Some(list_post) = &mime_parser.list_post else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||||
if chat.typ != Chattype::Mailinglist {
|
if chat.typ != Chattype::Mailinglist {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -2072,8 +2073,7 @@ async fn apply_mailinglist_changes(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (contact_id, _) =
|
let (contact_id, _) = Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
|
||||||
Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
|
|
||||||
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
||||||
if contact.param.get(Param::ListId) != Some(listid) {
|
if contact.param.get(Param::ListId) != Some(listid) {
|
||||||
contact.param.set(Param::ListId, listid);
|
contact.param.set(Param::ListId, listid);
|
||||||
@@ -2091,7 +2091,6 @@ async fn apply_mailinglist_changes(
|
|||||||
chat.param.set(Param::ListPost, list_post);
|
chat.param.set(Param::ListPost, list_post);
|
||||||
chat.update_param(context).await?;
|
chat.update_param(context).await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1012,7 +1012,7 @@ mod tests {
|
|||||||
let instance = send_webxdc_instance(&t, chat_id).await?;
|
let instance = send_webxdc_instance(&t, chat_id).await?;
|
||||||
t.send_webxdc_status_update(
|
t.send_webxdc_status_update(
|
||||||
instance.id,
|
instance.id,
|
||||||
r#"{"info": "foo", "summary":"bar", "payload": 42}"#,
|
r#"{"info": "foo", "summary":"bar", "document":"doc", "payload": 42}"#,
|
||||||
"descr",
|
"descr",
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -1020,7 +1020,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
||||||
.await?,
|
.await?,
|
||||||
r#"[{"payload":42,"info":"foo","summary":"bar","serial":1,"max_serial":1}]"#
|
r#"[{"payload":42,"info":"foo","document":"doc","summary":"bar","serial":1,"max_serial":1}]"#
|
||||||
);
|
);
|
||||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); // instance and info
|
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); // instance and info
|
||||||
let info = Message::load_from_db(&t, instance.id)
|
let info = Message::load_from_db(&t, instance.id)
|
||||||
@@ -1028,6 +1028,7 @@ mod tests {
|
|||||||
.get_webxdc_info(&t)
|
.get_webxdc_info(&t)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(info.summary, "bar".to_string());
|
assert_eq!(info.summary, "bar".to_string());
|
||||||
|
assert_eq!(info.document, "doc".to_string());
|
||||||
|
|
||||||
// forwarding an instance creates a fresh instance; updates etc. are not forwarded
|
// forwarding an instance creates a fresh instance; updates etc. are not forwarded
|
||||||
forward_msgs(&t, &[instance.get_id()], chat_id).await?;
|
forward_msgs(&t, &[instance.get_id()], chat_id).await?;
|
||||||
@@ -1044,6 +1045,7 @@ mod tests {
|
|||||||
.get_webxdc_info(&t)
|
.get_webxdc_info(&t)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(info.summary, "".to_string());
|
assert_eq!(info.summary, "".to_string());
|
||||||
|
assert_eq!(info.document, "".to_string());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user