Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion crates/app/src/autostart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ pub async fn set_autostart(app: AppHandle, enabled: bool) -> std::result::Result
check_location(&executable)?;
enable(&app, &executable).map_err(setting_error)
} else {
app.autolaunch().disable().map_err(setting_error)
app.autolaunch().disable().map_err(setting_error)?;
// On Windows, an app run as administrator turns it on for every user of the computer,
// which only an administrator can turn off again.
if app.autolaunch().is_enabled().map_err(setting_error)? {
return Err(
"开机自启是以管理员身份开启的。右键 Auto Email Sender 选“以管理员身份运行”,再关掉它。"
.into(),
);
}
Ok(())
}
}

Expand Down
22 changes: 14 additions & 8 deletions crates/fakemail/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,23 @@ pub async fn smtp(pki: &Pki, starttls: bool) -> (u16, Inbox) {
|| std::future::ready(()),
|| std::future::ready(()),
|| std::future::ready(()),
|| std::future::ready(()),
)
.await
}

/// Hooks pause recipient responses, body results, and rejection replies before they reach
/// Hooks pause login, recipient responses, body results, and rejection replies before they reach
/// the client.
pub async fn smtp_with_hooks<R, B, E>(
pub async fn smtp_with_hooks<A, R, B, E>(
pki: &Pki,
starttls: bool,
on_auth: impl Fn() -> A + Send + Sync + 'static,
on_rcpt: impl Fn() -> R + Send + Sync + 'static,
on_body: impl Fn() -> B + Send + Sync + 'static,
on_reject: impl Fn() -> E + Send + Sync + 'static,
) -> (u16, Inbox)
where
A: Future<Output = ()> + Send + 'static,
R: Future<Output = ()> + Send + 'static,
B: Future<Output = ()> + Send + 'static,
E: Future<Output = ()> + Send + 'static,
Expand All @@ -151,6 +154,7 @@ where
let inbox = Inbox::default();
let received = inbox.clone();
let acceptor = tls.clone();
let on_auth = Arc::new(on_auth);
let on_rcpt = Arc::new(on_rcpt);
let on_body = Arc::new(on_body);
let on_reject = Arc::new(on_reject);
Expand All @@ -159,6 +163,7 @@ where
io,
acceptor.clone(),
received.clone(),
on_auth.clone(),
on_rcpt.clone(),
on_body.clone(),
on_reject.clone(),
Expand All @@ -168,15 +173,17 @@ where
(port, inbox)
}

async fn smtp_session<R, B, E>(
async fn smtp_session<A, R, B, E>(
mut io: BufReader<Box<dyn Stream>>,
tls: TlsAcceptor,
inbox: Inbox,
on_auth: Arc<impl Fn() -> A + Send + Sync>,
on_rcpt: Arc<impl Fn() -> R + Send + Sync>,
on_body: Arc<impl Fn() -> B + Send + Sync>,
on_reject: Arc<impl Fn() -> E + Send + Sync>,
) -> std::io::Result<()>
where
A: Future<Output = ()> + Send + 'static,
R: Future<Output = ()> + Send + 'static,
B: Future<Output = ()> + Send + 'static,
E: Future<Output = ()> + Send + 'static,
Expand All @@ -198,11 +205,10 @@ where
return Ok(());
}
let words: Vec<&str> = line.split_whitespace().collect();
if words
.first()
.is_some_and(|word| word.eq_ignore_ascii_case("RCPT"))
{
on_rcpt().await;
match words.first().map(|w| w.to_ascii_uppercase()).as_deref() {
Some("AUTH") => on_auth().await,
Some("RCPT") => on_rcpt().await,
_ => {}
}
let reply = match words.first().map(|w| w.to_ascii_uppercase()).as_deref() {
Some("EHLO") if encrypted => "250-fake\r\n250 AUTH PLAIN LOGIN\r\n",
Expand Down
1 change: 1 addition & 0 deletions crates/mailbox/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ workspace = true
[dependencies]
async-imap = { version = "0.11", default-features = false, features = ["runtime-tokio"] }
chrono = { version = "0.4.45", default-features = false, features = ["clock"] }
cloud-api.workspace = true
futures-util = "0.3"
lettre = { version = "0.11", default-features = false, features = ["builder", "smtp-transport", "tokio1", "tokio1-rustls", "ring", "rustls-platform-verifier"] }
net.workspace = true
Expand Down
11 changes: 11 additions & 0 deletions crates/mailbox/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::io;

use crate::pause::{Failure, PauseKind, classify};

/// Why talking to a mail server failed. Displays in English for the log; `message` is for the user.
#[derive(Debug)]
pub enum MailError {
Expand Down Expand Up @@ -112,6 +114,15 @@ impl MailError {
"和 {host} 建立加密连接失败({source})。检查加密方式和端口是否对应:SSL/TLS \
一般用 465、993 端口,STARTTLS 一般用 587、143 端口。"
),
// Checking the code or trying again at once would only keep the limit on.
MailError::Login { reply, .. } | MailError::Refused { reply, .. }
if matches!(classify(self), Failure::Pause(PauseKind::Throttled(_))) =>
{
format!(
"服务商限制了登录或发信的频率(服务器回复:{reply})。过一段时间再试,\
授权码不用改;短时间里反复尝试会让限制更久。"
)
}
MailError::Login { reply, .. } => format!(
"登录被拒绝(服务器回复:{reply})。检查授权码或密码有没有填错,\
邮箱设置里有没有开启 IMAP/SMTP 服务;\
Expand Down
16 changes: 9 additions & 7 deletions crates/mailbox/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub struct Mailbox {
pub needs_secret: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
pub struct Server {
#[ts(type = "string")]
pub host: Cow<'static, str>,
Expand Down Expand Up @@ -86,11 +86,10 @@ pub struct MailboxFields {
}

impl MailboxForm {
/// Trims the text and checks every field; the error says what to fix.
/// Trims the text, folds a full-width address and checks every field; the error says what
/// to fix.
pub fn check(self) -> Result<MailboxFields, String> {
let address = self
.address
.trim()
let address = cloud_api::tidy_email(self.address.trim())
.parse()
.map_err(|_| "邮箱地址格式不正确")?;
let sender_name = self.sender_name.trim();
Expand Down Expand Up @@ -141,7 +140,7 @@ pub fn checked_secret(secret: &str) -> Option<String> {
}

/// What logging in to a mailbox takes.
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub struct Account {
pub address: lettre::Address,
pub sender_name: String,
Expand Down Expand Up @@ -320,7 +319,10 @@ pub async fn secret(
}

/// What logging in to a saved mailbox takes.
pub async fn account(db: &SqlitePool, id: i64) -> Result<Option<Account>, sqlx::Error> {
pub async fn account(
db: impl sqlx::SqliteExecutor<'_>,
id: i64,
) -> Result<Option<Account>, sqlx::Error> {
let Some(r) = sqlx::query!(
r#"SELECT address, sender_name, secret AS "secret!",
smtp_host, smtp_port AS "smtp_port: u16", smtp_security AS "smtp_security: Security",
Expand Down
15 changes: 15 additions & 0 deletions crates/mailbox/tests/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,18 @@ async fn the_test_email_goes_to_the_mailbox_itself() {
"{headers}"
);
}

#[tokio::test]
async fn a_throttled_login_says_to_wait_not_to_change_the_code() {
let pki = fakemail::pki();
let net = Net::new(vec![pki.root.clone()]).unwrap();
let (me, _) = mailbox(&pki, Security::Tls, "throttled").await;
let message = mailbox::check_sending(&net, &me)
.await
.unwrap_err()
.message();
assert!(
message.starts_with("服务商限制了登录或发信的频率"),
"{message}"
);
}
5 changes: 5 additions & 0 deletions crates/mailbox/tests/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ async fn an_address_is_added_once_in_any_letter_case(db: SqlitePool) {
let again = form("me@QQ.com").check().unwrap();
let result = mailbox::create(&db, &again, "s", 0).await;
assert!(matches!(result, Err(SaveError::AddressTaken)), "{result:?}");
// As Chinese input methods type it.
let full_width = form("Me@qq.com").check().unwrap();
assert_eq!(full_width.address.to_string(), "Me@qq.com");
let result = mailbox::create(&db, &full_width, "s", 0).await;
assert!(matches!(result, Err(SaveError::AddressTaken)), "{result:?}");
}

#[sqlx::test(migrator = "db::MIGRATOR")]
Expand Down
15 changes: 14 additions & 1 deletion crates/net/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ pub enum Proxy {

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Endpoint {
/// An IPv6 address without its brackets, as name lookups take it.
pub host: String,
pub port: u16,
}

impl fmt::Display for Endpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.host, self.port)
if self.host.contains(':') {
write!(f, "[{}]:{}", self.host, self.port)
} else {
write!(f, "{}:{}", self.host, self.port)
}
}
}

Expand Down Expand Up @@ -102,6 +107,10 @@ fn system() -> Option<Proxy> {
fn windows_proxy(server: &str) -> Option<Proxy> {
let endpoint = |s: &str| {
let (host, port) = s.trim().rsplit_once(':')?;
let host = host
.strip_prefix('[')
.and_then(|h| h.strip_suffix(']'))
.unwrap_or(host);
Some(Endpoint {
host: host.to_owned(),
port: port.parse().ok()?,
Expand Down Expand Up @@ -147,5 +156,9 @@ mod tests {
);
assert_eq!(windows_proxy("ftp=127.0.0.1:1"), None);
assert_eq!(windows_proxy("127.0.0.1"), None);
// Looked up without the brackets, written with them.
let ipv6 = windows_proxy("http=[::1]:7890;https=[::1]:7890").unwrap();
assert_eq!(ipv6.endpoint().host, "::1");
assert_eq!(ipv6.url(), "http://[::1]:7890");
}
}
4 changes: 4 additions & 0 deletions crates/send/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,10 @@ async fn mark_sending(
.execute(&mut conn)
.await?;
let mut tx = conn.begin_with("BEGIN IMMEDIATE").await?;
// Edited since the login, the mailbox is logged in to again as it is now, next step.
if mailbox::account(&mut *tx, mailbox_id).await?.as_ref() != Some(account) {
return Ok(None);
}
let at = now();
let Some(before) = sqlx::query_as!(
BeforeHandover,
Expand Down
Loading