feat: add option to trust any TLS certificate for IMAP connections

This commit is contained in:
rustmailer
2025-11-27 00:57:43 +08:00
parent 78b33f8994
commit 7c7e353114
33 changed files with 291 additions and 65 deletions
+96 -9
View File
@@ -55,7 +55,7 @@ use crate::modules::rest::response::DataPage;
use crate::modules::token::AccessToken;
use crate::raise_error;
pub type AccountModel = AccountV1;
pub type AccountModel = AccountV2;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Enum)]
pub enum AccountType {
@@ -86,11 +86,41 @@ pub struct AccountV1 {
pub updated_at: i64,
pub use_proxy: Option<u64>,
}
impl AccountV1 {
fn pk(&self) -> String {
format!("{}_{}", self.created_at, self.id)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
#[native_model(id = 4, version = 2, from = AccountV1)]
#[native_db(primary_key(pk -> String))]
pub struct AccountV2 {
#[secondary_key(unique)]
pub id: u64,
pub imap: Option<ImapConfig>,
pub enabled: bool,
#[oai(validator(custom = "crate::modules::common::validator::EmailValidator"))]
pub email: String,
pub name: Option<String>,
pub capabilities: Option<Vec<String>>,
pub date_since: Option<DateSince>,
pub folder_limit: Option<u32>,
pub sync_folders: Option<Vec<String>>,
pub account_type: AccountType,
pub sync_interval_min: Option<i64>,
pub known_folders: Option<BTreeSet<String>>,
pub created_at: i64,
pub updated_at: i64,
pub use_proxy: Option<u64>,
pub use_dangerous: bool,
pub pgp_key: Option<String>,
}
impl AccountV2 {
fn pk(&self) -> String {
format!("{}_{}", self.created_at, self.id)
}
pub fn new(request: AccountCreateRequest) -> BichonResult<Self> {
Ok(Self {
@@ -109,12 +139,14 @@ impl AccountV1 {
updated_at: utc_now!(),
use_proxy: request.use_proxy,
folder_limit: request.folder_limit,
use_dangerous: request.use_dangerous,
pgp_key: request.pgp_key,
})
}
pub async fn check_account_exists(account_id: u64) -> BichonResult<AccountModel> {
let account =
secondary_find_impl::<AccountModel>(DB_MANAGER.meta_db(), AccountV1Key::id, account_id)
secondary_find_impl::<AccountModel>(DB_MANAGER.meta_db(), AccountV2Key::id, account_id)
.await?
.ok_or_else(|| {
raise_error!(
@@ -144,7 +176,7 @@ impl AccountV1 {
}
pub async fn find(account_id: u64) -> BichonResult<Option<AccountModel>> {
secondary_find_impl::<AccountModel>(DB_MANAGER.meta_db(), AccountV1Key::id, account_id)
secondary_find_impl::<AccountModel>(DB_MANAGER.meta_db(), AccountV2Key::id, account_id)
.await
}
@@ -198,7 +230,7 @@ impl AccountV1 {
async fn delete_account(account_id: u64) -> BichonResult<()> {
delete_impl(DB_MANAGER.meta_db(), move|rw|{
rw.get().secondary::<AccountModel>(AccountV1Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
rw.get().secondary::<AccountModel>(AccountV2Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
.ok_or_else(||raise_error!(format!("The account entity with id={account_id} that you want to delete was not found."), ErrorCode::ResourceNotFound))
}).await
}
@@ -228,7 +260,7 @@ impl AccountV1 {
sync_folders: Vec<String>,
) -> BichonResult<()> {
update_impl(DB_MANAGER.meta_db(), move |rw| {
rw.get().secondary::<AccountModel>(AccountV1Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
rw.get().secondary::<AccountModel>(AccountV2Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
.ok_or_else(|| raise_error!(format!("When trying to update account sync_folders, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound))
}, |current|{
let mut updated = current.clone();
@@ -243,7 +275,7 @@ impl AccountV1 {
known_folders: BTreeSet<String>,
) -> BichonResult<()> {
update_impl(DB_MANAGER.meta_db(), move |rw| {
rw.get().secondary::<AccountModel>(AccountV1Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
rw.get().secondary::<AccountModel>(AccountV2Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
.ok_or_else(|| raise_error!(format!("When trying to update account known_folders, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound))
}, |current|{
let mut updated = current.clone();
@@ -258,7 +290,7 @@ impl AccountV1 {
capabilities: Vec<String>,
) -> BichonResult<()> {
update_impl(DB_MANAGER.meta_db(), move |rw| {
rw.get().secondary::<AccountModel>(AccountV1Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
rw.get().secondary::<AccountModel>(AccountV2Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
.ok_or_else(|| raise_error!(format!("When trying to update account capabilities, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound))
}, |current|{
let mut updated = current.clone();
@@ -287,7 +319,7 @@ impl AccountV1 {
}
pub async fn count() -> BichonResult<usize> {
count_by_unique_secondary_key_impl::<AccountModel>(DB_MANAGER.meta_db(), AccountV1Key::id)
count_by_unique_secondary_key_impl::<AccountModel>(DB_MANAGER.meta_db(), AccountV2Key::id)
.await
}
@@ -355,7 +387,62 @@ impl AccountV1 {
if let Some(enabled) = request.enabled {
new.enabled = enabled;
}
if let Some(use_dangerous) = request.use_dangerous {
new.use_dangerous = use_dangerous;
}
if let Some(pgp_key) = request.pgp_key {
new.pgp_key = Some(pgp_key);
}
new.updated_at = utc_now!();
Ok(new)
}
}
impl From<AccountV1> for AccountV2 {
fn from(value: AccountV1) -> Self {
Self {
id: value.id,
imap: value.imap,
enabled: value.enabled,
email: value.email,
name: value.name,
capabilities: value.capabilities,
date_since: value.date_since,
folder_limit: value.folder_limit,
sync_folders: value.sync_folders,
account_type: value.account_type,
sync_interval_min: value.sync_interval_min,
known_folders: value.known_folders,
created_at: value.created_at,
updated_at: value.updated_at,
use_proxy: value.use_proxy,
use_dangerous: false,
pgp_key: None,
}
}
}
impl From<AccountV2> for AccountV1 {
fn from(value: AccountV2) -> Self {
Self {
id: value.id,
imap: value.imap,
enabled: value.enabled,
email: value.email,
name: value.name,
capabilities: value.capabilities,
date_since: value.date_since,
folder_limit: value.folder_limit,
sync_folders: value.sync_folders,
account_type: value.account_type,
sync_interval_min: value.sync_interval_min,
known_folders: value.known_folders,
created_at: value.created_at,
updated_at: value.updated_at,
use_proxy: value.use_proxy,
}
}
}
+6 -1
View File
@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use std::collections::BTreeSet;
use crate::modules::account::entity::ImapConfig;
@@ -43,6 +42,8 @@ pub struct AccountCreateRequest {
#[oai(validator(minimum(value = "10"), maximum(value = "480")))]
pub sync_interval_min: Option<i64>,
pub use_proxy: Option<u64>,
pub use_dangerous: bool,
pub pgp_key: Option<String>,
}
impl AccountCreateRequest {
@@ -132,6 +133,10 @@ pub struct AccountUpdateRequest {
/// - If `None` or not provided, the client will connect directly to the API server.
/// - If `Some(proxy_id)`, the client will use the pre-configured proxy with the given ID for API requests.
pub use_proxy: Option<u64>,
pub use_dangerous: Option<bool>,
pub pgp_key: Option<String>,
}
impl AccountUpdateRequest {
+3 -1
View File
@@ -16,7 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::modules::account::migration::AccountModel;
use crate::modules::cache::imap::MAILBOX_MODELS;
use crate::modules::error::{code::ErrorCode, BichonError};
use crate::modules::settings::cli::SETTINGS;
@@ -71,6 +71,8 @@ impl DatabaseManager {
let rw = database
.rw_transaction()
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
rw.migrate::<AccountModel>()
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
rw.commit()
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
+2 -2
View File
@@ -16,8 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::modules::account::migration::AccountV1;
use crate::modules::account::migration::{AccountV1, AccountV2};
use crate::modules::autoconfig::CachedMailSettings;
use crate::modules::error::code::ErrorCode;
use crate::modules::error::BichonResult;
@@ -63,6 +62,7 @@ impl ModelsAdapter {
self.register_model::<SystemSetting>();
self.register_model::<CachedMailSettings>();
self.register_model::<AccountV1>();
self.register_model::<AccountV2>();
self.register_model::<OAuth2>();
self.register_model::<OAuth2PendingEntity>();
self.register_model::<OAuth2AccessToken>();
+15 -7
View File
@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::modules::account::entity::Encryption;
use crate::modules::error::code::ErrorCode;
use crate::modules::error::BichonResult;
@@ -100,15 +99,17 @@ impl Client {
encryption: &Encryption,
port: u16,
use_proxy: Option<u64>,
dangerous: bool,
) -> BichonResult<Self> {
let resolved_addr = Self::resolve_to_socket_addr(domain, port)?;
debug!("Attempting IMAP connection to {domain} ({resolved_addr}).");
match encryption {
Encryption::Ssl => {
Self::establish_secure_connection(resolved_addr, domain, use_proxy).await
Self::establish_secure_connection(resolved_addr, domain, use_proxy, dangerous).await
}
Encryption::StartTls => {
Self::establish_starttls_connection(resolved_addr, domain, use_proxy).await
Self::establish_starttls_connection(resolved_addr, domain, use_proxy, dangerous)
.await
}
Encryption::None => Self::establish_insecure_connection(resolved_addr, use_proxy).await,
}
@@ -118,11 +119,17 @@ impl Client {
address: SocketAddr,
server_hostname: &str,
use_proxy: Option<u64>,
dangerous: bool,
) -> BichonResult<Self> {
// Establish the TLS connection with the specified parameters
let tls_stream =
establish_tls_connection(address, server_hostname, alpn(address.port()), use_proxy)
.await?;
let tls_stream = establish_tls_connection(
address,
server_hostname,
alpn(address.port()),
use_proxy,
dangerous,
)
.await?;
let stats_stream = StatsWrapper::new(tls_stream);
// Wrap the TLS stream in a buffered writer for efficient IO
let buffered_stream = BufWriter::new(stats_stream);
@@ -180,6 +187,7 @@ impl Client {
address: SocketAddr,
server_hostname: &str,
use_proxy: Option<u64>,
dangerous: bool,
) -> BichonResult<Self> {
// Establish the initial TCP connection
let tcp_stream = establish_tcp_connection_with_timeout(address, use_proxy).await?;
@@ -217,7 +225,7 @@ impl Client {
let buffered_tcp_stream = client.into_inner();
let tcp_stream = buffered_tcp_stream.into_inner();
// Wrap the TCP stream in TLS encryption
let tls_stream = establish_tls_stream(server_hostname, &[], tcp_stream).await?;
let tls_stream = establish_tls_stream(server_hostname, &[], tcp_stream, dangerous).await?;
// Wrap the TLS stream in a buffered writer
let buffered_stream = BufWriter::new(tls_stream);
// Create a SessionStream trait object for further communication
+8 -2
View File
@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::modules::account::dispatcher::STATUS_DISPATCHER;
use crate::modules::account::entity::AuthType;
use crate::modules::account::migration::{AccountModel, AccountType};
@@ -51,7 +50,14 @@ impl ImapConnectionManager {
async fn create_client(&self, account: &AccountModel) -> BichonResult<Client> {
assert_eq!(account.account_type, AccountType::IMAP);
let imap = account.imap.as_ref().unwrap();
Client::connection(&imap.host, &imap.encryption, imap.port, imap.use_proxy).await
Client::connection(
&imap.host,
&imap.encryption,
imap.port,
imap.use_proxy,
account.use_dangerous,
)
.await
}
async fn authenticate(
+6 -4
View File
@@ -16,19 +16,21 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use mail_parser::MessageParser;
use crate::{base64_encode_url_safe, modules::{account::entity::Encryption, imap::client::Client}};
use crate::{
base64_encode_url_safe,
modules::{account::entity::Encryption, imap::client::Client},
};
#[tokio::test]
async fn testxx() {
rustls::crypto::CryptoProvider::install_default(rustls::crypto::ring::default_provider())
.unwrap();
let client = Client::connection("imap.zoho.com".into(), &Encryption::Ssl, 993, None)
let client = Client::connection("imap.zoho.com".into(), &Encryption::Ssl, 993, None, false)
.await
.unwrap();
let mut session = client.login("pollybase@zohomail.com", "xxx").await.unwrap();
let mut session = client.login("xx@zohomail.com", "xxx").await.unwrap();
session.select("INBOX").await.unwrap();
let result = session.uid_search("LARGER 1024").await.unwrap();
println!("{:#?}", result);
+4 -3
View File
@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::modules::error::code::ErrorCode;
use crate::modules::settings::proxy::Proxy;
use crate::modules::utils::tls::establish_tls_stream;
@@ -56,17 +55,19 @@ pub(crate) async fn establish_tcp_connection_with_timeout(
Ok(Box::pin(timeout_stream))
}
pub(crate) async fn establish_tls_connection(
pub async fn establish_tls_connection(
address: SocketAddr,
server_hostname: &str,
alpn_protocols: &[&str],
use_proxy: Option<u64>,
dangerous: bool,
) -> BichonResult<impl SessionStream> {
// Establish the TCP connection with timeout
let tcp_stream = establish_tcp_connection_with_timeout(address, use_proxy).await?;
// Wrap the TCP stream with TLS encryption
let tls_stream = establish_tls_stream(server_hostname, alpn_protocols, tcp_stream).await?;
let tls_stream =
establish_tls_stream(server_hostname, alpn_protocols, tcp_stream, dangerous).await?;
// Return the TLS stream wrapped in a SessionStream
Ok(tls_stream)
+72 -14
View File
@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::{
modules::{
error::{code::ErrorCode, BichonResult},
@@ -24,38 +23,44 @@ use crate::{
},
raise_error,
};
use rustls::RootCertStore;
use rustls::{
client::danger::{ServerCertVerified, ServerCertVerifier},
RootCertStore,
};
use std::sync::Arc;
pub async fn establish_tls_stream(
server_hostname: &str,
alpn_protocols: &[&str],
stream: impl SessionStream + 'static,
dangerous: bool,
) -> BichonResult<impl SessionStream> {
let tls_stream = establish_rustls_stream(server_hostname, alpn_protocols, stream).await?;
let tls_stream =
establish_rustls_stream(server_hostname, alpn_protocols, stream, dangerous).await?;
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
Ok(boxed_stream)
}
pub async fn establish_rustls_stream(
async fn establish_rustls_stream(
server_hostname: &str,
alpn_protocols: &[&str],
stream: impl SessionStream,
dangerous: bool,
) -> BichonResult<impl SessionStream> {
// Create a root certificate store and add default trusted roots
let root_store = RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.into(),
};
// Configure the Rustls client with the root certs and no client authentication
let mut config = rustls::ClientConfig::builder()
//builder_with_provider(
// rustls::crypto::ring::default_provider().into(),
// )
// .with_protocol_versions(&[&rustls::version::TLS13])
// .unwrap()
.with_root_certificates(root_store)
.with_no_client_auth();
let mut config = if dangerous {
rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(SkipCertVerification))
.with_no_client_auth()
} else {
rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth()
};
// Set the ALPN protocols
config.alpn_protocols = alpn_protocols
@@ -76,3 +81,56 @@ pub async fn establish_rustls_stream(
Ok(tls_stream)
}
#[derive(Debug)]
struct SkipCertVerification;
impl ServerCertVerifier for SkipCertVerification {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
// Always return a valid certificate verification result
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::RSA_PKCS1_SHA1,
rustls::SignatureScheme::ECDSA_SHA1_Legacy,
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::ED448,
]
}
}