Files
ElyOffice/crates/ely-mail-engine/src/net.rs
T
雷电芽衣 1c225f6812 C12.5: mail accounts, add and remove
ElyMail's window lists accounts and adds Gmail, Microsoft, iCloud
(app-specific password), IMAP and POP3 ones; a check logs in to both
servers and reports each. Accounts live in the store as JSON with a
case-insensitive unique address; secrets go to the keychain. Removal
names the cache it deletes, takes the secret first and puts it back
when local removal fails. Originals are set aside until commit and
restored after a crash. Requests are numbered so late results stay
with their own dialog. The account list is keyboard-navigable.
2026-10-19 20:17:19 -04:00

86 lines
2.5 KiB
Rust

use std::{future::Future, sync::Arc, time::Duration};
use rustls::{ClientConfig, crypto::ring, pki_types::ServerName};
use rustls_platform_verifier::BuilderVerifierExt;
use serde::{Deserialize, Serialize};
use tokio::net::TcpStream;
use tokio_rustls::{TlsConnector, client::TlsStream};
use crate::{Error, Result};
pub(crate) type Stream = TlsStream<TcpStream>;
const WAIT: Duration = Duration::from_secs(60);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Server {
pub host: String,
pub port: u16,
pub security: Security,
}
/// No plaintext: every mode ends in TLS.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Security {
Tls,
StartTls,
}
#[derive(Clone)]
pub struct Login {
pub user: String,
pub secret: Secret,
}
#[derive(Clone)]
pub enum Secret {
Password(String),
/// An OAuth access token, sent as XOAUTH2.
Token(String),
}
impl Login {
/// The SASL XOAUTH2 initial response, before base64.
pub(crate) fn xoauth2(&self, token: &str) -> String {
format!("user={}\x01auth=Bearer {token}\x01\x01", self.user)
}
}
/// Verifies servers with the system's trust store.
pub fn system_tls() -> Result<TlsConnector> {
let config = ClientConfig::builder_with_provider(Arc::new(ring::default_provider()))
.with_safe_default_protocol_versions()
.and_then(|builder| builder.with_platform_verifier())
.map_err(|err| Error::Tls(err.to_string()))?
.with_no_client_auth();
Ok(TlsConnector::from(Arc::new(config)))
}
pub(crate) async fn within<T>(step: &'static str, work: impl Future<Output = T>) -> Result<T> {
tokio::time::timeout(WAIT, work)
.await
.map_err(|_| Error::Timeout(step))
}
pub(crate) async fn tcp(server: &Server) -> Result<TcpStream> {
let stream = within(
"connecting",
TcpStream::connect((server.host.as_str(), server.port)),
)
.await?
.map_err(Error::Connect)?;
log::info!("mail: connected to {}:{}", server.host, server.port);
Ok(stream)
}
pub(crate) async fn tls(tcp: TcpStream, server: &Server, tls: &TlsConnector) -> Result<Stream> {
let name = ServerName::try_from(server.host.clone())
.map_err(|err| Error::Tls(format!("{}: {err}", server.host)))?;
let stream = within("starting TLS", tls.connect(name, tcp))
.await?
.map_err(|err| Error::Tls(err.to_string()))?;
log::info!("mail: TLS with {}:{}", server.host, server.port);
Ok(stream)
}