mirror of
https://github.com/chatmail/core.git
synced 2026-06-29 02:56:36 +03:00
Compare commits
9 Commits
1.102.0
...
feat_encry
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41a87a0626 | ||
|
|
d5faf75f56 | ||
|
|
e3ff786eb7 | ||
|
|
540cc8b205 | ||
|
|
7533f863d1 | ||
|
|
bc4eea0c28 | ||
|
|
d8844a0524 | ||
|
|
a714a4c2da | ||
|
|
cb03e93570 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -6,23 +6,9 @@
|
||||
|
||||
### API-Changes
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
## 1.102.0
|
||||
|
||||
### Changes
|
||||
|
||||
- If an email has multiple From addresses, handle this as if there was
|
||||
no From address, to prevent from forgery attacks. Also, improve
|
||||
handling of emails with invalid From addresses in general #3667
|
||||
|
||||
### API-Changes
|
||||
|
||||
### Fixes
|
||||
- fix detection of "All mail", "Trash", "Junk" etc folders. #3760
|
||||
- fetch messages sequentially to fix reactions on partially downloaded messages #3688
|
||||
- Fix a bug where one malformed message blocked receiving any further messages #3769
|
||||
|
||||
|
||||
## 1.101.0
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -826,7 +826,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.102.0"
|
||||
version = "1.101.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -898,7 +898,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.102.0"
|
||||
version = "1.101.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -920,7 +920,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.102.0"
|
||||
version = "1.101.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat-jsonrpc",
|
||||
@@ -943,7 +943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.102.0"
|
||||
version = "1.101.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.102.0"
|
||||
version = "1.101.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.102.0"
|
||||
version = "1.101.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.102.0"
|
||||
version = "1.101.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -47,7 +47,7 @@ use types::provider_info::ProviderInfo;
|
||||
use types::webxdc::WebxdcMessageInfo;
|
||||
|
||||
use self::types::{
|
||||
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
|
||||
chat::{BasicChat, JSONRPCChatVisibility, JSONRPCEncryptionModus, MuteDuration},
|
||||
location::JsonrpcLocation,
|
||||
message::{
|
||||
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
@@ -528,6 +528,8 @@ impl CommandApi {
|
||||
ChatId::new(chat_id).get_encryption_info(&ctx).await
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
|
||||
/// The QR code is compatible to the OPENPGP4FPR format
|
||||
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
|
||||
@@ -557,6 +559,32 @@ impl CommandApi {
|
||||
))
|
||||
}
|
||||
|
||||
async fn set_chat_encryption_modus(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
encryption_modus: JSONRPCEncryptionModus
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat = ChatId::new(chat_id);
|
||||
Ok(chat.set_encryption_modus(&ctx, encryption_modus.into_core_type()).await?)
|
||||
}
|
||||
|
||||
async fn get_chat_encryption_modus(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32
|
||||
) -> Result<Option<JSONRPCEncryptionModus>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat = ChatId::new(chat_id);
|
||||
Ok(
|
||||
match chat.encryption_modus(&ctx).await? {
|
||||
Some(encryption_modus) => Some(JSONRPCEncryptionModus::from_core_type(encryption_modus)),
|
||||
None => None,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
/// started on another device with `get_chat_securejoin_qr_code_svg()`.
|
||||
/// This function is typically called when `check_qr()` returns
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::chat::{Chat, ChatId, EncryptionModus};
|
||||
use deltachat::constants::Chattype;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
use deltachat::context::Context;
|
||||
@@ -211,3 +211,33 @@ impl JSONRPCChatVisibility {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
||||
#[serde(rename = "EncryptionModus")]
|
||||
pub enum JSONRPCEncryptionModus {
|
||||
Opportunistic = 0,
|
||||
ForcePlaintext = 1,
|
||||
ForceEncrypted = 2,
|
||||
ForceVerified = 3,
|
||||
}
|
||||
|
||||
impl JSONRPCEncryptionModus {
|
||||
pub fn into_core_type(self) -> EncryptionModus {
|
||||
match self {
|
||||
JSONRPCEncryptionModus::Opportunistic => EncryptionModus::Opportunistic,
|
||||
JSONRPCEncryptionModus::ForcePlaintext => EncryptionModus::ForcePlaintext,
|
||||
JSONRPCEncryptionModus::ForceEncrypted => EncryptionModus::ForceEncrypted,
|
||||
JSONRPCEncryptionModus::ForceVerified => EncryptionModus::ForceVerified
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_core_type(core_encryption_modus: EncryptionModus) -> Self {
|
||||
match core_encryption_modus {
|
||||
EncryptionModus::Opportunistic => JSONRPCEncryptionModus::Opportunistic,
|
||||
EncryptionModus::ForcePlaintext => JSONRPCEncryptionModus::ForcePlaintext,
|
||||
EncryptionModus::ForceEncrypted => JSONRPCEncryptionModus::ForceEncrypted,
|
||||
EncryptionModus::ForceVerified => JSONRPCEncryptionModus::ForceVerified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +331,16 @@ export class RawClient {
|
||||
return (this._transport.request('get_chat_securejoin_qr_code_svg', [accountId, chatId] as RPC.Params)) as Promise<[string,string]>;
|
||||
}
|
||||
|
||||
|
||||
public setChatEncryptionModus(accountId: T.U32, chatId: T.U32, encryptionModus: T.EncryptionModus): Promise<null> {
|
||||
return (this._transport.request('set_chat_encryption_modus', [accountId, chatId, encryptionModus] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getChatEncryptionModus(accountId: T.U32, chatId: T.U32): Promise<(T.EncryptionModus|null)> {
|
||||
return (this._transport.request('get_chat_encryption_modus', [accountId, chatId] as RPC.Params)) as Promise<(T.EncryptionModus|null)>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
* started on another device with `get_chat_securejoin_qr_code_svg()`.
|
||||
@@ -730,8 +740,8 @@ export class RawClient {
|
||||
}
|
||||
|
||||
|
||||
public deleteContact(accountId: T.U32, contactId: T.U32): Promise<boolean> {
|
||||
return (this._transport.request('delete_contact', [accountId, contactId] as RPC.Params)) as Promise<boolean>;
|
||||
public deleteContact(accountId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('delete_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export type BasicChat=
|
||||
* used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
*/
|
||||
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
|
||||
export type EncryptionModus=("Opportunistic"|"ForcePlaintext"|"ForceEncrypted"|"ForceVerified");
|
||||
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
|
||||
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
|
||||
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
|
||||
@@ -195,4 +196,4 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag
|
||||
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
|
||||
export type F64=number;
|
||||
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
|
||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,string,null,U32,Record<string,(string)[]>,U32,U32,string,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
|
||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,U32,EncryptionModus,null,U32,U32,(EncryptionModus|null),U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,null,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,string,null,U32,Record<string,(string)[]>,U32,U32,string,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
|
||||
|
||||
@@ -48,5 +48,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.102.0"
|
||||
"version": "1.101.0"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.102.0"
|
||||
version = "1.101.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -4,7 +4,7 @@ This document gives a quick overview about the Webxdc specification,
|
||||
It is meant for both, developing Webxdc apps
|
||||
and developing Webxdc implementations.
|
||||
|
||||
The [Webxdc guidebook](https://docs.webxdc.org/) shows more detailed information
|
||||
The [Webxdc guidebook](https://deltachat.github.io/webxdc_docs/) shows more detailed information
|
||||
when developing Webxdc apps.
|
||||
|
||||
|
||||
|
||||
@@ -60,5 +60,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.102.0"
|
||||
"version": "1.101.0"
|
||||
}
|
||||
@@ -901,34 +901,6 @@ def test_dont_show_emails(acfactory, lp):
|
||||
ac1.get_config("configured_addr")
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: unknown.address@junk.org, unkwnown.add@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message2@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Spam",
|
||||
"""
|
||||
From: alice@example.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message3@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Actually interesting message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
"Junk",
|
||||
"""
|
||||
@@ -954,9 +926,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
ac1._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
assert msg.text == "subj – message in Sent"
|
||||
chat_msgs = msg.chat.get_messages()
|
||||
assert len(chat_msgs) == 2
|
||||
assert any(msg.text == "subj – Actually interesting message in Spam" for msg in chat_msgs)
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
|
||||
ac1.direct_imap.select_folder("Spam")
|
||||
@@ -972,7 +942,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
msg2 = ac1._evtracker.wait_next_messages_changed()
|
||||
|
||||
assert msg2.text == "subj – message in Drafts that is moved to Sent later"
|
||||
assert len(msg.chat.get_messages()) == 3
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
|
||||
|
||||
def test_no_old_msg_is_fresh(acfactory, lp):
|
||||
|
||||
@@ -13,8 +13,6 @@ use once_cell::sync::Lazy;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::mimeparser;
|
||||
use crate::mimeparser::ParserErrorExt;
|
||||
use crate::tools::time;
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
@@ -32,23 +30,20 @@ pub(crate) async fn handle_authres(
|
||||
mail: &ParsedMail<'_>,
|
||||
from: &str,
|
||||
message_time: i64,
|
||||
) -> mimeparser::ParserResult<DkimResults> {
|
||||
) -> Result<DkimResults> {
|
||||
let from_domain = match EmailAddress::new(from) {
|
||||
Ok(email) => email.domain,
|
||||
Err(e) => {
|
||||
warn!(context, "invalid email {:#}", e);
|
||||
// This email is invalid, but don't return an error, we still want to
|
||||
// add a stub to the database so that it's not downloaded again
|
||||
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e)).map_err_malformed();
|
||||
return Ok(DkimResults::default());
|
||||
}
|
||||
};
|
||||
|
||||
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
|
||||
update_authservid_candidates(context, &authres)
|
||||
.await
|
||||
.map_err_sql()?;
|
||||
compute_dkim_results(context, authres, &from_domain, message_time)
|
||||
.await
|
||||
.map_err_sql()
|
||||
update_authservid_candidates(context, &authres).await?;
|
||||
compute_dkim_results(context, authres, &from_domain, message_time).await
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -579,7 +574,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
file.read_to_end(&mut bytes).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
|
||||
let from = &mimeparser::get_from(&mail.headers)[0].addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from, time()).await?;
|
||||
assert!(res.allow_keychange);
|
||||
@@ -591,7 +586,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
file.read_to_end(&mut bytes).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
|
||||
let from = &mimeparser::get_from(&mail.headers)[0].addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from, time()).await?;
|
||||
if !res.allow_keychange {
|
||||
@@ -642,10 +637,9 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
|
||||
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
|
||||
// return an Err because this would prevent the message from being added
|
||||
// to the database and downloaded again and again
|
||||
let bytes = b"From: invalid@from.com
|
||||
Authentication-Results: dkim=";
|
||||
let bytes = b"Authentication-Results: dkim=";
|
||||
let mail = mailparse::parse_mail(bytes).unwrap();
|
||||
handle_authres(&t, &mail, "invalid@rom.com", time())
|
||||
handle_authres(&t, &mail, "invalidfrom.com", time())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -687,7 +681,7 @@ Authentication-Results: dkim=";
|
||||
.await;
|
||||
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
|
||||
|
||||
let received = alice.recv_msg(&sent).await;
|
||||
|
||||
@@ -723,7 +717,7 @@ Authentication-Results: dkim=";
|
||||
loop {
|
||||
if let Some(mut sent) = bob2.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
|
||||
alice.recv_msg(&sent).await;
|
||||
} else if let Some(sent) = alice.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
bob2.recv_msg(&sent).await;
|
||||
|
||||
86
src/chat.rs
86
src/chat.rs
@@ -75,6 +75,29 @@ pub enum ProtectionStatus {
|
||||
Protected = 1,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
IntoStaticStr,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum EncryptionModus {
|
||||
Opportunistic = 0,
|
||||
ForcePlaintext = 1,
|
||||
ForceEncrypted = 2,
|
||||
ForceVerified = 3,
|
||||
}
|
||||
|
||||
impl Default for ProtectionStatus {
|
||||
fn default() -> Self {
|
||||
ProtectionStatus::Unprotected
|
||||
@@ -898,6 +921,38 @@ impl ChatId {
|
||||
Ok(ret.trim().to_string())
|
||||
}
|
||||
|
||||
/// This sets a protection modus for the chat and enforces that messages are only send if they
|
||||
/// meet the encryption modus (ForcePlaintext, Opportunistic, ForceEncrypted, ForceVerified)
|
||||
pub async fn set_encryption_modus(
|
||||
self,
|
||||
context: &Context,
|
||||
modus: EncryptionModus,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET encryption_modus=? WHERE id=?;",
|
||||
paramsv![modus, self],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This sets a protection modus for the chat and enforces that messages are only send if they
|
||||
/// meet the encryption modus (ForcePlaintext, Opportunistic, ForceEncrypted, ForceVerified)
|
||||
pub async fn encryption_modus(self, context: &Context) -> Result<Option<EncryptionModus>> {
|
||||
let encryption_modus: Option<EncryptionModus> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT encryption_modus FROM chats WHERE id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(encryption_modus)
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
///
|
||||
/// Avoid using this, eventually types should be cleaned up enough
|
||||
@@ -1944,6 +1999,14 @@ pub async fn is_contact_in_chat(
|
||||
// the caller can get it from msg.chat_id. Forwards would need to
|
||||
// be fixed for this somehow too.
|
||||
pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
// Propagate same encryption_mode of chat to message in case messages doesn't yet have an
|
||||
// encryption_mode
|
||||
if let None = msg.encryption_modus(&context).await? {
|
||||
if let Some(encryption_mode) = chat_id.encryption_modus(&context).await? {
|
||||
msg.set_encryption_modus(&context, encryption_mode).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if chat_id.is_unset() {
|
||||
let forwards = msg.param.get(Param::PrepForwards);
|
||||
if let Some(forwards) = forwards {
|
||||
@@ -5649,4 +5712,27 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_encryption_modus() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let contact_fiona = Contact::create(&t, "", "fiona@example.net").await?;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "Group").await?;
|
||||
|
||||
assert_eq!(
|
||||
chat_id.encryption_modus(&t).await?,
|
||||
Some(EncryptionModus::Opportunistic)
|
||||
);
|
||||
|
||||
chat_id
|
||||
.set_encryption_modus(&t, EncryptionModus::ForceEncrypted)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
chat_id.encryption_modus(&t).await?,
|
||||
Some(EncryptionModus::ForceEncrypted)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::collections::HashSet;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use mailparse::ParsedMail;
|
||||
use mailparse::SingleInfo;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::authres;
|
||||
@@ -13,7 +14,6 @@ use crate::context::Context;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::log::LogExt;
|
||||
use crate::mimeparser::{self, ParserErrorExt};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::pgp;
|
||||
|
||||
@@ -56,12 +56,18 @@ pub async fn try_decrypt(
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn prepare_decryption(
|
||||
pub async fn prepare_decryption(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
from: &str,
|
||||
from: &[SingleInfo],
|
||||
message_time: i64,
|
||||
) -> mimeparser::ParserResult<DecryptionInfo> {
|
||||
) -> Result<DecryptionInfo> {
|
||||
let from = if let Some(f) = from.first() {
|
||||
&f.addr
|
||||
} else {
|
||||
return Ok(DecryptionInfo::default());
|
||||
};
|
||||
|
||||
let autocrypt_header = Aheader::from_headers(from, &mail.headers)
|
||||
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
|
||||
.flatten();
|
||||
@@ -76,8 +82,7 @@ pub(crate) async fn prepare_decryption(
|
||||
// Disallowing keychanges is disabled for now:
|
||||
true, // dkim_results.allow_keychange,
|
||||
)
|
||||
.await
|
||||
.map_err_sql()?;
|
||||
.await?;
|
||||
|
||||
Ok(DecryptionInfo {
|
||||
from: from.to_string(),
|
||||
|
||||
38
src/imap.rs
38
src/imap.rs
@@ -56,8 +56,6 @@ use session::Session;
|
||||
|
||||
use self::select_folder::NewlySelected;
|
||||
|
||||
pub(crate) const GENERATED_PREFIX: &str = "GEN_";
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImapActionResult {
|
||||
Failed,
|
||||
@@ -797,7 +795,7 @@ impl Imap {
|
||||
};
|
||||
|
||||
// Get the Message-ID or generate a fake one to identify the message in the database.
|
||||
let message_id = prefetch_get_or_create_message_id(&headers);
|
||||
let message_id = prefetch_get_message_id(&headers).unwrap_or_else(create_id);
|
||||
|
||||
let target = match target_folder(context, folder, is_spam_folder, &headers).await? {
|
||||
Some(config) => match context.get_config(config).await? {
|
||||
@@ -1280,7 +1278,7 @@ impl Imap {
|
||||
let msg = fetch?;
|
||||
match get_fetch_headers(&msg) {
|
||||
Ok(headers) => {
|
||||
if let Some(from) = mimeparser::get_from(&headers) {
|
||||
if let Some(from) = mimeparser::get_from(&headers).first() {
|
||||
if context.is_self_addr(&from.addr).await? {
|
||||
result.extend(mimeparser::get_recipients(&headers));
|
||||
}
|
||||
@@ -1450,7 +1448,7 @@ impl Imap {
|
||||
.context("we checked that message has body right above, but it has vanished")?;
|
||||
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
|
||||
|
||||
let rfc724_mid = if let Some(rfc724_mid) = uid_message_ids.get(&server_uid) {
|
||||
let rfc724_mid = if let Some(rfc724_mid) = &uid_message_ids.get(&server_uid) {
|
||||
rfc724_mid
|
||||
} else {
|
||||
warn!(
|
||||
@@ -1458,7 +1456,7 @@ impl Imap {
|
||||
"No Message-ID corresponding to UID {} passed in uid_messsage_ids",
|
||||
server_uid
|
||||
);
|
||||
continue;
|
||||
""
|
||||
};
|
||||
match receive_imf_inner(
|
||||
&context,
|
||||
@@ -1754,13 +1752,9 @@ async fn should_move_out_of_spam(
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
let from = match mimeparser::get_from(headers) {
|
||||
Some(f) => f,
|
||||
None => return Ok(false),
|
||||
};
|
||||
// No chat found.
|
||||
let (from_id, blocked_contact, _origin) =
|
||||
from_field_to_contact_id(context, &from, true).await?;
|
||||
from_field_to_contact_id(context, &mimeparser::get_from(headers), true).await?;
|
||||
if blocked_contact {
|
||||
// Contact is blocked, leave the message in spam.
|
||||
return Ok(false);
|
||||
@@ -1980,15 +1974,13 @@ fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>>
|
||||
}
|
||||
|
||||
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
|
||||
headers
|
||||
.get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
|
||||
.or_else(|| headers.get_header_value(HeaderDef::MessageId))
|
||||
.and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
|
||||
}
|
||||
|
||||
pub(crate) fn prefetch_get_or_create_message_id(headers: &[mailparse::MailHeader]) -> String {
|
||||
prefetch_get_message_id(headers)
|
||||
.unwrap_or_else(|| format!("{}{}", GENERATED_PREFIX, create_id()))
|
||||
if let Some(message_id) = headers.get_header_value(HeaderDef::XMicrosoftOriginalMessageId) {
|
||||
crate::mimeparser::parse_message_id(&message_id).ok()
|
||||
} else if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId) {
|
||||
crate::mimeparser::parse_message_id(&message_id).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns chat by prefetched headers.
|
||||
@@ -2045,12 +2037,8 @@ pub(crate) async fn prefetch_should_download(
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||
.is_some();
|
||||
|
||||
let from = match mimeparser::get_from(headers) {
|
||||
Some(f) => f,
|
||||
None => return Ok(false),
|
||||
};
|
||||
let (_from_id, blocked_contact, origin) =
|
||||
from_field_to_contact_id(context, &from, true).await?;
|
||||
from_field_to_contact_id(context, &mimeparser::get_from(headers), true).await?;
|
||||
// prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact.
|
||||
// (prevent_rename is the last argument of from_field_to_contact_id())
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use deltachat_derive::{FromSql, ToSql};
|
||||
use rusqlite::types::ValueRef;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::EncryptionModus;
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
@@ -302,6 +303,7 @@ impl Message {
|
||||
" m.hidden AS hidden,",
|
||||
" m.location_id AS location,",
|
||||
" c.blocked AS blocked",
|
||||
" m.encryption_modus as encryption_modus",
|
||||
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
|
||||
" WHERE m.id=?;"
|
||||
),
|
||||
@@ -848,10 +850,39 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Force the message to be sent in plain text.
|
||||
/// Deprecated: use Message::set_encryption_modus(EncryptionModus::ForcePlaintext)
|
||||
pub fn force_plaintext(&mut self) {
|
||||
self.param.set_int(Param::ForcePlaintext, 1);
|
||||
}
|
||||
|
||||
pub async fn set_encryption_modus(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
encryption_modus: EncryptionModus,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET encryption_modus=? WHERE id=?;",
|
||||
paramsv![encryption_modus, self.id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn encryption_modus(&self, context: &Context) -> Result<Option<EncryptionModus>> {
|
||||
let encryption_modus: Option<EncryptionModus> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT encryption_modus FROM msgs WHERE id=?;",
|
||||
paramsv![self.id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(encryption_modus)
|
||||
}
|
||||
|
||||
pub async fn update_param(&self, context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
|
||||
@@ -9,6 +9,7 @@ use tokio::fs;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::Chat;
|
||||
use crate::chat::EncryptionModus;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||
use crate::contact::Contact;
|
||||
@@ -324,7 +325,7 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_force_plaintext(&self) -> bool {
|
||||
fn should_force_plaintext(&self, encryption_modus: &EncryptionModus) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
if chat.is_protected() {
|
||||
@@ -333,6 +334,8 @@ impl<'a> MimeFactory<'a> {
|
||||
// encryption may disclose recipients;
|
||||
// this is probably a worse issue than not opportunistically (!) encrypting
|
||||
true
|
||||
} else if encryption_modus == &EncryptionModus::ForcePlaintext {
|
||||
true
|
||||
} else {
|
||||
self.msg
|
||||
.param
|
||||
@@ -602,7 +605,11 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
let min_verified = self.min_verified();
|
||||
let grpimage = self.grpimage();
|
||||
let force_plaintext = self.should_force_plaintext();
|
||||
let encryption_modus = match self.msg.encryption_modus(context).await? {
|
||||
Some(encryption_modus) => encryption_modus,
|
||||
None => EncryptionModus::Opportunistic,
|
||||
};
|
||||
let force_plaintext = self.should_force_plaintext(&encryption_modus);
|
||||
let skip_autocrypt = self.should_skip_autocrypt();
|
||||
let e2ee_guaranteed = self.is_e2ee_guaranteed();
|
||||
let encrypt_helper = EncryptHelper::new(context).await?;
|
||||
@@ -644,6 +651,34 @@ impl<'a> MimeFactory<'a> {
|
||||
encrypt_helper.should_encrypt(context, e2ee_guaranteed, &peerstates)?;
|
||||
let is_encrypted = should_encrypt && !force_plaintext;
|
||||
|
||||
// Ensure we fulfill encryption_modus
|
||||
match encryption_modus {
|
||||
EncryptionModus::Opportunistic => {}
|
||||
EncryptionModus::ForcePlaintext => {
|
||||
ensure!(
|
||||
!is_encrypted,
|
||||
"EncryptionModus is ForcePlaintext but message is encrypted"
|
||||
);
|
||||
}
|
||||
EncryptionModus::ForceEncrypted => {
|
||||
ensure!(
|
||||
is_encrypted,
|
||||
"EncryptionModus is ForceEncrypted but message is unencrypted"
|
||||
);
|
||||
}
|
||||
EncryptionModus::ForceVerified => {
|
||||
let chat_is_protected = if let Loaded::Message { chat } = &self.loaded {
|
||||
chat.is_protected()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
ensure!(
|
||||
is_encrypted && chat_is_protected,
|
||||
"EncryptionModus is ForceVerified but chat is not protected"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let message = if parts.is_empty() {
|
||||
// Single part, render as regular message.
|
||||
main_part
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::events::EventType;
|
||||
use crate::format_flowed::unformat_flowed;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::Fingerprint;
|
||||
use crate::location;
|
||||
use crate::message::{self, Viewtype};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
@@ -28,7 +29,6 @@ use crate::simplify::{simplify, SimplifiedText};
|
||||
use crate::stock_str;
|
||||
use crate::sync::SyncItems;
|
||||
use crate::tools::{get_filemeta, parse_receive_headers, truncate_by_lines};
|
||||
use crate::{location, tools};
|
||||
|
||||
/// A parsed MIME message.
|
||||
///
|
||||
@@ -46,7 +46,7 @@ pub struct MimeMessage {
|
||||
|
||||
/// Addresses are normalized and lowercased:
|
||||
pub recipients: Vec<SingleInfo>,
|
||||
pub from: SingleInfo,
|
||||
pub from: Vec<SingleInfo>,
|
||||
/// Whether the From address was repeated in the signed part
|
||||
/// (and we know that the signer intended to send from this address)
|
||||
pub from_is_signed: bool,
|
||||
@@ -157,50 +157,21 @@ impl Default for SystemMessage {
|
||||
|
||||
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum ParserError {
|
||||
#[error("{}", _0)]
|
||||
Malformed(anyhow::Error),
|
||||
|
||||
#[error("{:#}", _0)]
|
||||
Sql(anyhow::Error),
|
||||
}
|
||||
|
||||
pub(crate) type ParserResult<T> = std::result::Result<T, ParserError>;
|
||||
|
||||
pub(crate) trait ParserErrorExt<T, E>
|
||||
where
|
||||
Self: std::marker::Sized,
|
||||
{
|
||||
fn map_err_malformed(self) -> ParserResult<T>;
|
||||
fn map_err_sql(self) -> ParserResult<T>;
|
||||
}
|
||||
|
||||
impl<T, E: Into<anyhow::Error>> ParserErrorExt<T, E> for Result<T, E> {
|
||||
fn map_err_malformed(self) -> ParserResult<T> {
|
||||
self.map_err(|e| ParserError::Malformed(e.into()))
|
||||
}
|
||||
|
||||
fn map_err_sql(self) -> ParserResult<T> {
|
||||
self.map_err(|e| ParserError::Sql(e.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl MimeMessage {
|
||||
pub async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
|
||||
Ok(MimeMessage::from_bytes_with_partial(context, body, None).await?)
|
||||
MimeMessage::from_bytes_with_partial(context, body, None).await
|
||||
}
|
||||
|
||||
/// Parse a mime message.
|
||||
///
|
||||
/// If `partial` is set, it contains the full message size in bytes
|
||||
/// and `body` contains the header only.
|
||||
pub(crate) async fn from_bytes_with_partial(
|
||||
pub async fn from_bytes_with_partial(
|
||||
context: &Context,
|
||||
body: &[u8],
|
||||
partial: Option<u32>,
|
||||
) -> ParserResult<Self> {
|
||||
let mail = mailparse::parse_mail(body).map_err_malformed()?;
|
||||
) -> Result<Self> {
|
||||
let mail = mailparse::parse_mail(body)?;
|
||||
|
||||
let message_time = mail
|
||||
.headers
|
||||
@@ -227,7 +198,7 @@ impl MimeMessage {
|
||||
);
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>().map_err_malformed()?;
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" {
|
||||
if let Some(part) = mail.subparts.first() {
|
||||
for field in &part.headers {
|
||||
@@ -245,14 +216,11 @@ impl MimeMessage {
|
||||
headers.remove("secure-join-fingerprint");
|
||||
headers.remove("chat-verified");
|
||||
|
||||
let from = from.context("No from in message").map_err_malformed()?;
|
||||
let mut decryption_info =
|
||||
prepare_decryption(context, &mail, &from.addr, message_time).await?;
|
||||
|
||||
// Memory location for a possible decrypted message.
|
||||
let mut mail_raw = Vec::new();
|
||||
let mut gossiped_addr = Default::default();
|
||||
let mut from_is_signed = false;
|
||||
let mut decryption_info = prepare_decryption(context, &mail, &from, message_time).await?;
|
||||
hop_info += "\n\n";
|
||||
hop_info += &decryption_info.dkim_results.to_string();
|
||||
|
||||
@@ -265,7 +233,7 @@ impl MimeMessage {
|
||||
// autocrypt message.
|
||||
|
||||
mail_raw = raw;
|
||||
let decrypted_mail = mailparse::parse_mail(&mail_raw).map_err_malformed()?;
|
||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(context, "decrypted message mime-body:");
|
||||
println!("{}", String::from_utf8_lossy(&mail_raw));
|
||||
@@ -279,8 +247,7 @@ impl MimeMessage {
|
||||
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossiped_addr =
|
||||
update_gossip_peerstates(context, message_time, &mail, gossip_headers)
|
||||
.await
|
||||
.map_err_sql()?;
|
||||
.await?;
|
||||
}
|
||||
|
||||
// let known protected headers from the decrypted
|
||||
@@ -288,7 +255,7 @@ impl MimeMessage {
|
||||
|
||||
// Signature was checked for original From, so we
|
||||
// do not allow overriding it.
|
||||
let mut signed_from = None;
|
||||
let mut signed_from = Vec::new();
|
||||
|
||||
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
|
||||
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
|
||||
@@ -303,21 +270,23 @@ impl MimeMessage {
|
||||
&mut chat_disposition_notification_to,
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
if let Some(signed_from) = signed_from {
|
||||
if addr_cmp(&signed_from.addr, &from.addr) {
|
||||
from_is_signed = true;
|
||||
} else {
|
||||
// There is a From: header in the encrypted &
|
||||
// signed part, but it doesn't match the outer one.
|
||||
// This _might_ be because the sender's mail server
|
||||
// replaced the sending address, e.g. in a mailing list.
|
||||
// Or it's because someone is doing some replay attack
|
||||
// - OTOH, I can't come up with an attack scenario
|
||||
// where this would be useful.
|
||||
warn!(
|
||||
context,
|
||||
"From header in signed part does't match the outer one"
|
||||
);
|
||||
if let Some(signed_from) = signed_from.first() {
|
||||
if let Some(from) = from.first() {
|
||||
if addr_cmp(&signed_from.addr, &from.addr) {
|
||||
from_is_signed = true;
|
||||
} else {
|
||||
// There is a From: header in the encrypted &
|
||||
// signed part, but it doesn't match the outer one.
|
||||
// This _might_ be because the sender's mail server
|
||||
// replaced the sending address, e.g. in a mailing list.
|
||||
// Or it's because someone is doing some replay attack
|
||||
// - OTOH, I can't come up with an attack scenario
|
||||
// where this would be useful.
|
||||
warn!(
|
||||
context,
|
||||
"From header in signed part does't match the outer one"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,10 +302,7 @@ impl MimeMessage {
|
||||
// && decryption_info.dkim_results.allow_keychange
|
||||
{
|
||||
peerstate.degrade_encryption(message_time);
|
||||
peerstate
|
||||
.save_to_db(&context.sql, false)
|
||||
.await
|
||||
.map_err_sql()?;
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
}
|
||||
(Ok(mail), HashSet::new(), false)
|
||||
@@ -380,15 +346,11 @@ impl MimeMessage {
|
||||
Some(org_bytes) => {
|
||||
parser
|
||||
.create_stub_from_partial_download(context, org_bytes)
|
||||
.await
|
||||
.map_err_sql()?;
|
||||
.await?;
|
||||
}
|
||||
None => match mail {
|
||||
Ok(mail) => {
|
||||
parser
|
||||
.parse_mime_recursive(context, &mail, false)
|
||||
.await
|
||||
.map_err_malformed()?;
|
||||
parser.parse_mime_recursive(context, &mail, false).await?;
|
||||
}
|
||||
Err(err) => {
|
||||
let msg_body = stock_str::cant_decrypt_msg_body(context).await;
|
||||
@@ -409,7 +371,7 @@ impl MimeMessage {
|
||||
parser.maybe_remove_bad_parts();
|
||||
parser.maybe_remove_inline_mailinglist_footer();
|
||||
parser.heuristically_parse_ndn(context).await;
|
||||
parser.parse_headers(context).await.map_err_malformed()?;
|
||||
parser.parse_headers(context).await?;
|
||||
|
||||
// Disallowing keychanges is disabled for now
|
||||
// if !decryption_info.dkim_results.allow_keychange {
|
||||
@@ -427,14 +389,11 @@ impl MimeMessage {
|
||||
parser.decoded_data = mail_raw;
|
||||
}
|
||||
|
||||
crate::peerstate::maybe_do_aeap_transition(context, &mut decryption_info, &parser)
|
||||
.await
|
||||
.map_err_sql()?;
|
||||
crate::peerstate::maybe_do_aeap_transition(context, &mut decryption_info, &parser).await?;
|
||||
if let Some(peerstate) = decryption_info.peerstate {
|
||||
peerstate
|
||||
.handle_fingerprint_change(context, message_time)
|
||||
.await
|
||||
.map_err_sql()?;
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(parser)
|
||||
@@ -623,18 +582,21 @@ impl MimeMessage {
|
||||
// See if an MDN is requested from the other side
|
||||
if !self.decrypting_failed && !self.parts.is_empty() {
|
||||
if let Some(ref dn_to) = self.chat_disposition_notification_to {
|
||||
// Check that the message is not outgoing.
|
||||
let from = &self.from.addr;
|
||||
if !context.is_self_addr(from).await? {
|
||||
if from.to_lowercase() == dn_to.addr.to_lowercase() {
|
||||
if let Some(part) = self.parts.last_mut() {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
if let Some(from) = self.from.get(0) {
|
||||
// Check that the message is not outgoing.
|
||||
if !context.is_self_addr(&from.addr).await? {
|
||||
if from.addr.to_lowercase() == dn_to.addr.to_lowercase() {
|
||||
if let Some(part) = self.parts.last_mut() {
|
||||
part.param.set_int(Param::WantsMdn, 1);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"{} requested a read receipt to {}, ignoring",
|
||||
from.addr,
|
||||
dn_to.addr
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"{} requested a read receipt to {}, ignoring", from, dn_to.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1264,7 +1226,7 @@ impl MimeMessage {
|
||||
context: &Context,
|
||||
headers: &mut HashMap<String, String>,
|
||||
recipients: &mut Vec<SingleInfo>,
|
||||
from: &mut Option<SingleInfo>,
|
||||
from: &mut Vec<SingleInfo>,
|
||||
list_post: &mut Option<String>,
|
||||
chat_disposition_notification_to: &mut Option<SingleInfo>,
|
||||
fields: &[mailparse::MailHeader<'_>],
|
||||
@@ -1293,7 +1255,7 @@ impl MimeMessage {
|
||||
*recipients = recipients_new;
|
||||
}
|
||||
let from_new = get_from(fields);
|
||||
if from_new.is_some() {
|
||||
if !from_new.is_empty() {
|
||||
*from = from_new;
|
||||
}
|
||||
let list_post_new = get_list_post(fields);
|
||||
@@ -1838,9 +1800,8 @@ pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||
}
|
||||
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
|
||||
let all = get_all_addresses_from_header(headers, |header_key| header_key == "from");
|
||||
tools::single_value(all)
|
||||
pub(crate) fn get_from(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||
get_all_addresses_from_header(headers, |header_key| header_key == "from")
|
||||
}
|
||||
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
@@ -1916,35 +1877,35 @@ mod tests {
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, None);
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, None);
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, None);
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Goetz C".to_string()));
|
||||
|
||||
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Goetz C".to_string()));
|
||||
|
||||
@@ -1952,7 +1913,7 @@ mod tests {
|
||||
MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Götz C".to_string()));
|
||||
|
||||
@@ -1962,7 +1923,7 @@ mod tests {
|
||||
MimeMessage::from_bytes(&ctx, b"From: \"=?utf-8?q?G=C3=B6tz?= C\" <g@c.de>\n\nhi")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = mimemsg.from;
|
||||
let contact = mimemsg.from.first().unwrap();
|
||||
assert_eq!(contact.addr, "g@c.de");
|
||||
assert_eq!(contact.display_name, Some("Götz C".to_string()));
|
||||
}
|
||||
@@ -2194,9 +2155,14 @@ mod tests {
|
||||
test1\n\
|
||||
";
|
||||
|
||||
let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await;
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(mimeparser, Err(ParserError::Malformed(_))));
|
||||
let of = &mimeparser.from[0];
|
||||
assert_eq!(of.addr, "hello@one.org");
|
||||
|
||||
assert!(mimeparser.chat_disposition_notification_to.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -2235,7 +2201,7 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mimeparser_with_context() {
|
||||
let context = TestContext::new().await;
|
||||
let raw = b"From: hello@example.org\n\
|
||||
let raw = b"From: hello\n\
|
||||
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
|
||||
Subject: outer-subject\n\
|
||||
Secure-Join-Group: no\n\
|
||||
|
||||
@@ -51,6 +51,7 @@ pub enum Param {
|
||||
ErroneousE2ee = b'e',
|
||||
|
||||
/// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
|
||||
/// Deprecated: Use EncryptionModus::ForcePlaintext
|
||||
ForcePlaintext = b'u',
|
||||
|
||||
/// For Messages: do not include Autocrypt header.
|
||||
|
||||
@@ -617,9 +617,10 @@ pub async fn maybe_do_aeap_transition(
|
||||
mime_parser: &crate::mimeparser::MimeMessage,
|
||||
) -> Result<()> {
|
||||
if let Some(peerstate) = &mut info.peerstate {
|
||||
// If the from addr is different from the peerstate address we know,
|
||||
// we may want to do an AEAP transition.
|
||||
if !addr_cmp(&peerstate.addr, &mime_parser.from.addr)
|
||||
if let Some(from) = mime_parser.from.first() {
|
||||
// If the from addr is different from the peerstate address we know,
|
||||
// we may want to do an AEAP transition.
|
||||
if !addr_cmp(&peerstate.addr, &from.addr)
|
||||
// Check if it's a chat message; we do this to avoid
|
||||
// some accidental transitions if someone writes from multiple
|
||||
// addresses with an MUA.
|
||||
@@ -635,30 +636,31 @@ pub async fn maybe_do_aeap_transition(
|
||||
// to the attacker's address, allowing for easier phishing.
|
||||
&& mime_parser.from_is_signed
|
||||
&& info.message_time > peerstate.last_seen
|
||||
{
|
||||
// Add info messages to chats with this (verified) contact
|
||||
//
|
||||
peerstate
|
||||
.handle_setup_change(
|
||||
context,
|
||||
info.message_time,
|
||||
PeerstateChange::Aeap(info.from.clone()),
|
||||
)
|
||||
.await?;
|
||||
{
|
||||
// Add info messages to chats with this (verified) contact
|
||||
//
|
||||
peerstate
|
||||
.handle_setup_change(
|
||||
context,
|
||||
info.message_time,
|
||||
PeerstateChange::Aeap(info.from.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
peerstate.addr = info.from.clone();
|
||||
let header = info.autocrypt_header.as_ref().context(
|
||||
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
|
||||
)?;
|
||||
peerstate.apply_header(header, info.message_time);
|
||||
peerstate.to_save = Some(ToSave::All);
|
||||
peerstate.addr = info.from.clone();
|
||||
let header = info.autocrypt_header.as_ref().context(
|
||||
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
|
||||
)?;
|
||||
peerstate.apply_header(header, info.message_time);
|
||||
peerstate.to_save = Some(ToSave::All);
|
||||
|
||||
// We don't know whether a peerstate with this address already existed, or a
|
||||
// new one should be created, so just try both create=false and create=true,
|
||||
// and if this fails, create=true, one will succeed (this is a very cold path,
|
||||
// so performance doesn't really matter).
|
||||
peerstate.save_to_db(&context.sql, true).await?;
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
// We don't know whether a peerstate with this address already existed, or a
|
||||
// new one should be created, so just try both create=false and create=true,
|
||||
// and if this fails, create=true, one will succeed (this is a very cold path,
|
||||
// so performance doesn't really matter).
|
||||
peerstate.save_to_db(&context.sql, true).await?;
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use regex::Regex;
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
|
||||
use crate::contact;
|
||||
use crate::contact::{
|
||||
may_be_valid_addr, normalize_name, Contact, ContactId, Origin, VerifiedStatus,
|
||||
};
|
||||
@@ -21,14 +22,14 @@ use crate::download::DownloadState;
|
||||
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
|
||||
use crate::imap::markseen_on_imap_table;
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{
|
||||
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
|
||||
};
|
||||
use crate::mimeparser::{
|
||||
parse_message_ids, AvatarAction, MailinglistType, MimeMessage, ParserError, SystemMessage,
|
||||
parse_message_id, parse_message_ids, AvatarAction, MailinglistType, MimeMessage, SystemMessage,
|
||||
};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
||||
@@ -36,8 +37,7 @@ use crate::reaction::{set_msg_reaction, Reaction};
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{extract_grpid_from_rfc724_mid, smeared_time};
|
||||
use crate::{contact, imap};
|
||||
use crate::tools::{create_id, extract_grpid_from_rfc724_mid, smeared_time};
|
||||
|
||||
/// This is the struct that is returned after receiving one email (aka MIME message).
|
||||
///
|
||||
@@ -66,7 +66,11 @@ pub async fn receive_imf(
|
||||
seen: bool,
|
||||
) -> Result<Option<ReceivedMsg>> {
|
||||
let mail = parse_mail(imf_raw).context("can't parse mail")?;
|
||||
let rfc724_mid = imap::prefetch_get_or_create_message_id(&mail.headers);
|
||||
let rfc724_mid = mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::MessageId)
|
||||
.and_then(|msgid| parse_message_id(&msgid).ok())
|
||||
.unwrap_or_else(create_id);
|
||||
receive_imf_inner(context, &rfc724_mid, imf_raw, seen, None, false).await
|
||||
}
|
||||
|
||||
@@ -101,33 +105,10 @@ pub(crate) async fn receive_imf_inner(
|
||||
|
||||
let mut mime_parser =
|
||||
match MimeMessage::from_bytes_with_partial(context, imf_raw, is_partial_download).await {
|
||||
Err(ParserError::Malformed(err)) => {
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf: can't parse MIME: {}", err);
|
||||
|
||||
let msg_ids;
|
||||
if !rfc724_mid.starts_with(GENERATED_PREFIX) {
|
||||
let row_id = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
paramsv![rfc724_mid, DC_CHAT_ID_TRASH],
|
||||
)
|
||||
.await?;
|
||||
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 {
|
||||
chat_id: DC_CHAT_ID_TRASH,
|
||||
state: MessageState::Undefined,
|
||||
sort_timestamp: 0,
|
||||
msg_ids,
|
||||
needs_delete_job: false,
|
||||
}));
|
||||
return Ok(None);
|
||||
}
|
||||
Err(ParserError::Sql(err)) => return Err(err),
|
||||
Ok(mime_parser) => mime_parser,
|
||||
};
|
||||
|
||||
@@ -160,6 +141,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
None
|
||||
};
|
||||
|
||||
// the function returns the number of created messages in the database
|
||||
let prevent_rename =
|
||||
mime_parser.is_mailinglist_message() || mime_parser.get_header(HeaderDef::Sender).is_some();
|
||||
|
||||
@@ -186,6 +168,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
} else {
|
||||
Origin::IncomingUnknownTo
|
||||
},
|
||||
prevent_rename,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -370,25 +353,28 @@ pub(crate) async fn receive_imf_inner(
|
||||
/// * `prevent_rename`: passed through to `add_or_lookup_contacts_by_address_list()`
|
||||
pub async fn from_field_to_contact_id(
|
||||
context: &Context,
|
||||
from: &SingleInfo,
|
||||
from_address_list: &[SingleInfo],
|
||||
prevent_rename: bool,
|
||||
) -> Result<(ContactId, bool, Origin)> {
|
||||
let display_name = if prevent_rename {
|
||||
Some("")
|
||||
} else {
|
||||
from.display_name.as_deref()
|
||||
};
|
||||
let from_id = add_or_lookup_contact_by_addr(
|
||||
let from_ids = add_or_lookup_contacts_by_address_list(
|
||||
context,
|
||||
display_name,
|
||||
&from.addr,
|
||||
from_address_list,
|
||||
Origin::IncomingUnknownFrom,
|
||||
prevent_rename,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if from_id == ContactId::SELF {
|
||||
if from_ids.contains(&ContactId::SELF) {
|
||||
Ok((ContactId::SELF, false, Origin::OutgoingBcc))
|
||||
} else {
|
||||
} else if !from_ids.is_empty() {
|
||||
if from_ids.len() > 1 {
|
||||
warn!(
|
||||
context,
|
||||
"mail has more than one From address, only using first: {:?}", from_address_list
|
||||
);
|
||||
}
|
||||
let from_id = from_ids.get(0).cloned().unwrap_or_default();
|
||||
|
||||
let mut from_id_blocked = false;
|
||||
let mut incoming_origin = Origin::Unknown;
|
||||
if let Ok(contact) = Contact::load_from_db(context, from_id).await {
|
||||
@@ -396,6 +382,13 @@ pub async fn from_field_to_contact_id(
|
||||
incoming_origin = contact.origin;
|
||||
}
|
||||
Ok((from_id, from_id_blocked, incoming_origin))
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"mail has an empty From header: {:?}", from_address_list
|
||||
);
|
||||
|
||||
Ok((ContactId::UNDEFINED, false, Origin::Unknown))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,10 +570,9 @@ async fn add_parts(
|
||||
if chat.is_protected() {
|
||||
let s = stock_str::unknown_sender_for_chat(context).await;
|
||||
mime_parser.repl_msg_by_error(&s);
|
||||
} else {
|
||||
} else if let Some(from) = mime_parser.from.first() {
|
||||
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
|
||||
// to the sender's name, indicating to the user that he/she is not part of the group.
|
||||
let from = &mime_parser.from;
|
||||
let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
|
||||
for part in mime_parser.parts.iter_mut() {
|
||||
part.param.set(Param::OverrideSenderDisplayname, name);
|
||||
@@ -645,9 +637,11 @@ async fn add_parts(
|
||||
// if contact renaming is prevented (for mailinglists and bots),
|
||||
// we use name from From:-header as override name
|
||||
if prevent_rename {
|
||||
if let Some(name) = &mime_parser.from.display_name {
|
||||
for part in mime_parser.parts.iter_mut() {
|
||||
part.param.set(Param::OverrideSenderDisplayname, name);
|
||||
if let Some(from) = mime_parser.from.first() {
|
||||
if let Some(name) = &from.display_name {
|
||||
for part in mime_parser.parts.iter_mut() {
|
||||
part.param.set(Param::OverrideSenderDisplayname, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1807,8 +1801,10 @@ async fn create_or_lookup_mailinglist(
|
||||
// a usable name for these lists is in the `From` header
|
||||
// and we can detect these lists by a unique `ListId`-suffix.
|
||||
if listid.ends_with(".list-id.mcsv.net") {
|
||||
if let Some(display_name) = &mime_parser.from.display_name {
|
||||
name = display_name.clone();
|
||||
if let Some(from) = mime_parser.from.first() {
|
||||
if let Some(display_name) = &from.display_name {
|
||||
name = display_name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1828,15 +1824,18 @@ async fn create_or_lookup_mailinglist(
|
||||
//
|
||||
// this pattern is similar to mailchimp above, however,
|
||||
// with weaker conditions and does not overwrite existing names.
|
||||
if name.is_empty()
|
||||
&& (mime_parser.from.addr.contains("noreply")
|
||||
|| mime_parser.from.addr.contains("no-reply")
|
||||
|| mime_parser.from.addr.starts_with("notifications@")
|
||||
|| mime_parser.from.addr.starts_with("newsletter@")
|
||||
|| listid.ends_with(".xt.local"))
|
||||
{
|
||||
if let Some(display_name) = &mime_parser.from.display_name {
|
||||
name = display_name.clone();
|
||||
if name.is_empty() {
|
||||
if let Some(from) = mime_parser.from.first() {
|
||||
if from.addr.contains("noreply")
|
||||
|| from.addr.contains("no-reply")
|
||||
|| from.addr.starts_with("notifications@")
|
||||
|| from.addr.starts_with("newsletter@")
|
||||
|| listid.ends_with(".xt.local")
|
||||
{
|
||||
if let Some(display_name) = &from.display_name {
|
||||
name = display_name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2234,6 +2233,7 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
context: &Context,
|
||||
address_list: &[SingleInfo],
|
||||
origin: Origin,
|
||||
prevent_rename: bool,
|
||||
) -> Result<Vec<ContactId>> {
|
||||
let mut contact_ids = HashSet::new();
|
||||
for info in address_list.iter() {
|
||||
@@ -2241,7 +2241,11 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
if !may_be_valid_addr(addr) {
|
||||
continue;
|
||||
}
|
||||
let display_name = info.display_name.as_deref();
|
||||
let display_name = if prevent_rename {
|
||||
Some("")
|
||||
} else {
|
||||
info.display_name.as_deref()
|
||||
};
|
||||
contact_ids
|
||||
.insert(add_or_lookup_contact_by_addr(context, display_name, addr, origin).await?);
|
||||
}
|
||||
@@ -2284,7 +2288,7 @@ mod tests {
|
||||
async fn test_grpid_simple() {
|
||||
let context = TestContext::new().await;
|
||||
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: hello@example.org\n\
|
||||
From: hello\n\
|
||||
Subject: outer-subject\n\
|
||||
In-Reply-To: <lqkjwelq123@123123>\n\
|
||||
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
|
||||
@@ -2298,25 +2302,11 @@ mod tests {
|
||||
assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_bad_from() {
|
||||
let context = TestContext::new().await;
|
||||
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: hello\n\
|
||||
Subject: outer-subject\n\
|
||||
In-Reply-To: <lqkjwelq123@123123>\n\
|
||||
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
|
||||
\n\
|
||||
hello\x00";
|
||||
let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await;
|
||||
assert!(matches!(mimeparser, Err(ParserError::Malformed(_))));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_grpid_from_multiple() {
|
||||
let context = TestContext::new().await;
|
||||
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: hello@example.org\n\
|
||||
From: hello\n\
|
||||
Subject: outer-subject\n\
|
||||
In-Reply-To: <Gr.HcxyMARjyJy.9-qweqwe@asd.net>\n\
|
||||
References: <qweqweqwe>, <Gr.HcxyMARjyJy.9-uvzWPTLtV@nau.ca>\n\
|
||||
@@ -2603,55 +2593,8 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
// Check that the message is not shown to the user:
|
||||
assert!(chats.is_empty());
|
||||
|
||||
// Check that the message was added to the db:
|
||||
assert!(message::rfc724_mid_exists(context, "3924@example.com")
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
|
||||
/// If there is no Message-Id header, we generate a random id.
|
||||
/// But there is no point in adding a trash entry in the database
|
||||
/// if the email is malformed (e.g. because `From` is missing)
|
||||
/// with this random id we just generated.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_message_id_header() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert!(chats.get_msg_id(0).is_err());
|
||||
|
||||
let received = receive_imf(
|
||||
&t,
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
To: bob@example.com\n\
|
||||
Subject: foo\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
dbg!(&received);
|
||||
assert!(received.is_none());
|
||||
|
||||
assert!(!t
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM msgs WHERE chat_id=?;",
|
||||
paramsv![DC_CHAT_ID_TRASH],
|
||||
)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
// Check that the message is not shown to the user:
|
||||
assert!(chats.is_empty());
|
||||
// Check that the message was added to the database:
|
||||
assert!(chats.get_msg_id(0).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::provider::get_provider_by_domain;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
const DBVERSION: i32 = 68;
|
||||
const DBVERSION: i32 = 69;
|
||||
const VERSION_CFG: &str = "dbversion";
|
||||
const TABLES: &str = include_str!("./tables.sql");
|
||||
|
||||
@@ -616,6 +616,15 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 94 {
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN encryption_modus INTEGER DEFAULT 0;
|
||||
ALTER TABLE msgs ADD COLUMN encryption_modus INTEGER DEFAULT 0;"#,
|
||||
94,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
|
||||
12
src/tools.rs
12
src/tools.rs
@@ -670,18 +670,6 @@ pub(crate) fn parse_receive_headers(headers: &Headers) -> String {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// If `collection` contains exactly one element, return this element.
|
||||
/// Otherwise, return None.
|
||||
pub(crate) fn single_value<T>(collection: impl IntoIterator<Item = T>) -> Option<T> {
|
||||
let mut iter = collection.into_iter();
|
||||
if let Some(value) = iter.next() {
|
||||
if iter.next().is_none() {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
Reference in New Issue
Block a user