C12.7: POP3 fetch by UIDL

This commit is contained in:
雷电芽衣 committed 2026-10-19 21:40:25 -04:00
1 parent 890bd80d4d
commit 4d0d64ff54
18 files changed
+394 -47

No files matched your search

+16 -1
View File
@@ -4,7 +4,7 @@ use ely_gpui_component::{
buttons::{Button, ButtonVariant, SegmentedControl},
data_display::DescriptionList,
feedback::{Banner, InlineMessage},
forms::{Input, PasswordInput},
forms::{Input, PasswordInput, Switch},
overlays::Dialog,
primitives::Severity,
};
@@ -207,6 +207,21 @@ impl Mail {
.item(label("mail.add.user"), field(&fields.user))
.item(label("mail.add.password"), password);
}
if adding.kind == Kind::Pop {
let this = this.clone();
let keep = Switch::new("mail-add-keep", adding.keep)
.name(label("mail.add.keep"))
.on_change(move |on, _, cx| {
this.update(cx, |mail, cx| {
if let Some(adding) = &mut mail.adding {
adding.set_keep(on);
cx.notify();
}
})
.ok();
});
rows = rows.item(label("mail.add.keep"), keep);
}
rows.item(label("mail.add.incoming"), field(&fields.incoming))
.item(label("mail.add.port"), field(&fields.incoming_port))
.item(
+11 -2
View File
@@ -42,6 +42,8 @@ pub struct Adding {
pub outgoing_security: Security,
/// Typed-in accounts choose; providers fix it.
pub auth: Auth,
/// POP3 leaves a copy on the server (§12.3).
pub keep: bool,
pub step: Step,
/// The ports last filled in; any other was typed.
filled: [String; 2],
@@ -108,6 +110,7 @@ impl Adding {
incoming_security: Security::Tls,
outgoing_security: Security::StartTls,
auth: Auth::Password,
keep: true,
step: Step::Editing,
filled: Default::default(),
asked: None,
@@ -144,10 +147,16 @@ impl Adding {
self.set_ports(cx);
}
pub fn set_keep(&mut self, keep: bool) {
self.stop();
self.step = Step::Editing;
self.keep = keep;
}
/// The usual ports, unless one was typed.
fn set_ports(&mut self, cx: &mut App) {
let ports = [
kinds::port(Some(self.kind.protocol()), self.incoming_security),
kinds::port(Some(self.kind.protocol(self.keep)), self.incoming_security),
kinds::port(None, self.outgoing_security),
];
let fields = [&self.fields.incoming_port, &self.fields.outgoing_port];
@@ -229,7 +238,7 @@ impl Adding {
address,
user,
auth: self.auth(),
protocol: self.kind.protocol(),
protocol: self.kind.protocol(self.keep),
incoming,
outgoing,
};
+5 -4
View File
@@ -37,9 +37,10 @@ impl Kind {
KINDS.into_iter().find(|kind| kind.key() == key)
}
pub fn protocol(self) -> Protocol {
/// `keep` is POP3's server copy.
pub fn protocol(self, keep: bool) -> Protocol {
match self {
Kind::Pop => Protocol::Pop,
Kind::Pop => Protocol::Pop { keep },
_ => Protocol::Imap,
}
}
@@ -78,8 +79,8 @@ pub fn port(protocol: Option<Protocol>, security: Security) -> u16 {
match (protocol, security) {
(Some(Protocol::Imap), Security::Tls) => 993,
(Some(Protocol::Imap), Security::StartTls) => 143,
(Some(Protocol::Pop), Security::Tls) => 995,
(Some(Protocol::Pop), Security::StartTls) => 110,
(Some(Protocol::Pop { .. }), Security::Tls) => 995,
(Some(Protocol::Pop { .. }), Security::StartTls) => 110,
(None, Security::Tls) => 465,
(None, Security::StartTls) => 587,
}
+5 -5
View File
@@ -1,4 +1,4 @@
//! IMAP accounts sync on open and once added.
//! Accounts sync on open and once added.
use std::{
collections::HashMap,
@@ -26,9 +26,6 @@ impl Mail {
let Some((_, account)) = self.accounts().iter().find(|(each, _)| *each == id) else {
return log::info!("mail: sync asked of a gone account");
};
if account.protocol != Protocol::Imap {
return;
}
if self.syncs.get(&id).is_some_and(|each| each.task.is_some()) {
return log::info!("mail: account {} is already syncing", id.0);
}
@@ -50,7 +47,10 @@ impl Mail {
.map_err(|err| ely_mail_engine::Error::Store(format!("the clock: {err}")))?
.as_secs() as i64;
let secret = sync::secret(&account, &vault).await?;
sync::imap(&account, id, secret, &tls, &store, now).await
match account.protocol {
Protocol::Imap => sync::imap(&account, id, secret, &tls, &store, now).await,
Protocol::Pop { .. } => sync::pop(&account, id, secret, &tls, &store, now).await,
}
};
let task = self.on_runtime(cx, work, move |mail, done, cx| {
let failed = match done {
+40 -15
View File
@@ -1,41 +1,45 @@
use ely_mail_engine::{Protocol, Store};
use gpui::TestAppContext;
use super::{Keys, account, closed_port, settle, window, window_with};
use super::{Keys, account, adding, closed_port, settle, type_in, window, window_with};
use crate::kinds::Kind;
#[test]
fn an_imap_account_syncs_on_open_and_says_why_it_failed() {
fn every_account_syncs_on_open_and_says_why_it_failed() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
let mut ann = account("ann@example.com");
ann.incoming.port = closed_port();
let ann = store.add_account(&ann).unwrap();
let pop = ely_mail_engine::Account {
protocol: Protocol::Pop,
let mut pop = ely_mail_engine::Account {
protocol: Protocol::Pop { keep: true },
..account("bo@example.com")
};
pop.incoming.port = closed_port();
let bo = store.add_account(&pop).unwrap();
drop(store);
let keys = Keys::default();
keys.kept
.lock()
.unwrap()
.insert("account:ann@example.com".into(), "pw".into());
.extend(["ann", "bo"].map(|name| (format!("account:{name}@example.com"), "pw".into())));
let mut cx = TestAppContext::single();
let (mail, cx) = window(&mut cx, dir.path(), keys);
settle(cx, |app| {
mail.read(app)
.syncs
.get(&ann)
.is_some_and(|each| each.task.is_none() && each.synced.is_some())
[ann, bo].iter().all(|id| {
mail.read(app)
.syncs
.get(id)
.is_some_and(|each| each.task.is_none() && each.synced.is_some())
})
});
cx.update(|_, app| {
let mail = mail.read(app);
let syncing = &mail.syncs[&ann];
let failed = syncing.failed.as_deref().unwrap();
assert!(failed.starts_with("cannot connect"), "{failed}");
assert_eq!(syncing.synced.unwrap().at, None);
assert!(!mail.syncs.contains_key(&bo), "POP3 is collected apart");
for id in [ann, bo] {
let syncing = &mail.read(app).syncs[&id];
let failed = syncing.failed.as_deref().unwrap();
assert!(failed.starts_with("cannot connect"), "{failed}");
assert_eq!(syncing.synced.unwrap().at, None);
}
});
}
@@ -88,3 +92,24 @@ fn a_missing_tls_is_the_sync_failure() {
assert!(syncing.task.is_none());
});
}
#[test]
fn a_pop_account_keeps_a_server_copy_unless_told() {
let dir = tempfile::tempdir().unwrap();
let mut cx = TestAppContext::single();
let (mail, cx) = window(&mut cx, dir.path(), Keys::default());
adding(&mail, Kind::Pop, cx);
type_in(&mail, cx, |f| &f.address, "bo@example.com");
type_in(&mail, cx, |f| &f.user, "bo");
type_in(&mail, cx, |f| &f.incoming, "pop.example.com");
type_in(&mail, cx, |f| &f.outgoing, "smtp.example.com");
let protocol = |cx: &mut gpui::VisualTestContext| {
cx.update(|_, app| {
let adding = mail.read(app).adding.as_ref().unwrap();
adding.account(None, app).unwrap().protocol
})
};
assert_eq!(protocol(cx), Protocol::Pop { keep: true });
cx.update(|_, app| mail.update(app, |mail, _| mail.adding.as_mut().unwrap().set_keep(false)));
assert_eq!(protocol(cx), Protocol::Pop { keep: false });
}
+21 -8
View File
@@ -97,7 +97,7 @@ impl Mail {
server(&account.outgoing, cx),
)
.text(label("mail.account.auth"), auth(account.auth, cx));
let (details, failed) = self.sync_rows(*id, details, cx);
let (details, failed) = self.sync_rows(*id, account.protocol, details, cx);
let remove = Button::new("mail-remove", label("mail.remove.open"))
.on_click(|_, window, cx| window.dispatch_action(Box::new(menu::RemoveAccount), cx));
div()
@@ -116,6 +116,7 @@ impl Mail {
fn sync_rows(
&self,
id: AccountId,
protocol: Protocol,
details: DescriptionList,
cx: &App,
) -> (DescriptionList, Option<Banner>) {
@@ -128,12 +129,24 @@ impl Mail {
(None, Some(at)) => local_time(at).into(),
(None, None) => label("mail.sync.never"),
};
let days = sync::BODY_DAYS.to_string();
let mut details = details.text(label("mail.sync.last"), last).text(
label("mail.sync.range"),
text(cx, "mail.sync.range_value", &[("days", &days)]),
);
if let Some(synced) = syncing.synced {
let mut details = details.text(label("mail.sync.last"), last);
if let Protocol::Pop { keep } = protocol {
let server = label(if keep {
"mail.sync.kept"
} else {
"mail.sync.deleted"
});
details = details
.text(label("mail.sync.range"), label("mail.sync.range_pop"))
.text(label("mail.sync.server"), server);
} else {
let days = sync::BODY_DAYS.to_string();
details = details.text(
label("mail.sync.range"),
text(cx, "mail.sync.range_value", &[("days", &days)]),
);
}
if let (Some(synced), Protocol::Imap) = (syncing.synced, protocol) {
for (key, n) in [
("mail.sync.uncached", synced.uncached),
("mail.sync.partial", synced.partial),
@@ -196,7 +209,7 @@ impl Mail {
pub fn protocol_name(protocol: Protocol) -> &'static str {
match protocol {
Protocol::Imap => "IMAP",
Protocol::Pop => "POP3",
Protocol::Pop { .. } => "POP3",
}
}