C12.8: sync now, timed sync, pause, offline folders

This commit is contained in:
雷电芽衣 committed 2026-10-19 22:29:27 -04:00
1 parent 4d0d64ff54
commit 22d913d45e
23 files changed
+763 -66

No files matched your search

+1 -1
View File
@@ -141,7 +141,7 @@
- [x] C12.5 账号(MAIL-01、02):ElyMail 主窗口与添加账号对话框:Gmail、Microsoft、iCloud(应用专用密码向导)、其他 IMAP/SMTP、POP3/SMTP;服务器、端口、TLS 方式、认证方式;连通性检查逐项报结果;多账号;删除账号先写明删去的本地缓存、草稿与凭据,不动服务端。已做:引擎 `Account`(地址、用户名、`Auth` 为密码或 Google、Microsoft 的 OAuth,`Protocol` 为 IMAP 或 POP3,收发两个 `Server`),以 JSON 存在 accounts 表的 settings 列(结构 v1 未发布,直接改,不加升级步);`valid` 照 SMTP 的 Mailbox 核对地址,用户名不空不带换行,主机不空不带空白,端口非零;`check` 同时登录收发两个服务器再退出,各自报结果;`Store::cached` 给该账号的邮件数与只属它的原文字节,`remove_account` 在一个事务里删账号(邮箱、邮件、标记随外键删去)与只属它的原文,原文在提交前删、删不掉即回滚,与别的账号共用的原文留着;凭据库名为 `account:地址`。应用:主窗口左栏列出账号(地址与协议、收信主机),中间为所选账号的地址、用户名、收发服务器、登录方式与“删除账号…”,无账号时为空状态与“添加账号…”;“账号”菜单两项。添加对话框分 Gmail、Microsoft、iCloud、IMAP、POP3:Gmail 与 Microsoft 填可选的地址作登录提示,“以 Google/Microsoft 登录”开浏览器,回来后以 ID 令牌的地址与访问令牌检查两台服务器,构建缺客户端 ID 即提示条写明并禁用按钮;iCloud 写明生成应用专用密码的步骤并可开 Apple 账户,服务器固定(imap.mail.me.com:993 TLS,smtp.mail.me.com:587 STARTTLS,两者用户名都用完整地址,Apple 写明可用);IMAP 与 POP3 填地址、登录方式(密码或两种 OAuth)、用户名、密码、收发主机、端口与加密,改加密时端口随之换成惯用的,手填的端口不动。“检查”逐项报收信与发信服务器(连不上、TLS 失败、拒绝登录及服务器原话等,按界面语言),通过才可“添加账号”,之后改任一字段即回到编辑;后到的检查与登录按编号丢弃,关对话框即中止网络任务;同一地址(不分大小写)不重复添加。保存先写库再存凭据,凭据库拒绝即提示只保留到退出。删除先在后台量出缓存,对话框写明删去的邮件数与大小、草稿与保存的密码或登录、服务器上的邮件不动;先删凭据,删不掉即不删账号并报错。库与网络都在后台(库在锁后,网络在两线程的 tokio 运行时),提示文字不进日志(可能含地址)。测试:引擎 `tests/accounts.rs` 6 个(往返、地址唯一、删去时留下共用原文与量缓存、两台服务器都登录、各自报结果、不合的设置),应用窗口测试 8 个(缺项逐个写明、端口随加密、检查逐项报连不上且改字段回到编辑、iCloud 的服务器、缺客户端 ID、凭据库拒绝仍保存并留在本次、保存与重开读回再删去且凭据随之删、凭据删不掉即留账号),去掉各自的规则即失败;真窗口冒烟(`ELY_SMOKE=1`)画出一帧。未验证:真实的 Gmail、Microsoft、iCloud 与其他服务器(须用户的客户端 ID 与账号)。第 1 轮审查后:删去或替换的原文先改名留在原处,提交后才删、失败即改回,打开库时收拾中断留下的(引擎 `tests/aside.rs`:第二个原文删不掉时两个都还读得到,重开后放回或删去);删除确认按钮带着所删的账号(原先关闭回调先清掉状态,按了什么也不删),测试改按真的按钮;端口以上次自动填入的值判断是否键入(原先把惯用端口当未键入,IMAP 收信填 995 会被冲掉);请求在整个窗口编号,保存的结果只作用于仍在等它的对话框(原先会关掉或改写之后新开的对话框),测试 `a_late_save…`、`a_late_failed_save…`;去掉各修复即失败。第 2 轮审查后:本地删除失败即把读出的凭据放回(凭据库拒绝时只放回本次并写明),账号仍可登录(`a_failed_removal_puts_the_secret_back`:原文的停放处被占,删除失败后凭据、账号与原文都在);地址唯一由库保证(`address` 不分 ASCII 大小写,`add_account` 遇冲突报 `Error::Taken`,对话框写明已添加),两个在途的保存不再都成功(`two_saves_in_flight_add_an_address_once`);左栏账号列表改用组件库 `SelectableList`(一个 Tab 停靠点,上下键选账号,`the_keyboard_picks_another_account`);去掉各修复即失败。第 3 轮审查后:凭据库删不掉时内存里的凭据照留(`Vault::remove` 先删凭据库,成功才清内存),凭据库不可用时添加的账号删除失败后仍能取回凭据(`a_Line truncated
- [x] C12.6 IMAP 同步(MAIL-14,§12.3,QA-18):文件夹列表与特殊用途;邮件头全部、正文只取 90 天内;UIDVALIDITY 变化即重建该邮箱;服务端删除与标记变化随之;后台任务执行,界面显示最后同步时间、下载范围、未缓存的数量与失败原因。已做:引擎 `Imap::folders` 先问 CAPABILITY,有 LIST-EXTENDED 与 SPECIAL-USE 即以 `LIST "" * RETURN (SPECIAL-USE)` 取角色,否则照常 LIST(INBOX 按名,其余按 RFC 6154 的属性;`\Noselect`、`\NonExistent` 不可选);`examine` 读 UIDVALIDITY 与 EXISTS(缺任一即报错,不当作空邮箱);`flags`、`headers`、`bodies`、`structures`、`text` 按 UID 取,同一 UID 的多条 FETCH 回复合并(别的客户端改标记时插来的标记行照收),命令结束后才查所要的段(`\Recent` 属本次会话,不存);`since` 以 UID SEARCH SINCE 找 90 天内的邮件。`sync::imap` 一轮:列出的可选文件夹存为邮箱(带角色),不再列出的连同邮件删去,库里 INBOX 不分大小写(`Store::mailbox`、`set_folders`);每个邮箱 UIDVALIDITY 变了即清空重建;服务端已无的 UID 删去,标记不同即改;更早的邮件只取邮件头;90 天内的先读 BODYSTRUCTURE,无附件即取整封,有附件只取邮件头、各部分的 MIME 头与正文部分(文字,related 里的内嵌图片),按原结构重组,附件内容为空,部分号记在 `missing`(`Fetched::Text`、`Stored::missing`),附件按需(C12.17);已有邮件头的在进入 90 天时补正文;每次 FETCH 至多 100 个 UID;任何一步失败即停下报错,上次同步时间不变,成功才记下;`sync::secret` 取密码,OAuth 以刷新令牌换访问令牌(新的刷新令牌随即存下)。应用:窗口打开时与添加账号后各同步一次 IMAP 账号(POP3 随 C12.7),在应用的 tokio 运行时上;删除账号先中止其同步;账号详情写明上次同步时间(或正在同步、尚未同步)、下载范围、只有邮件头的邮件数与附件未下载的邮件数,失败以提示条写出原因。测试:引擎 `tests/sync.rs` 9 个(首次同步取邮件头与近期正文并记角色与时间;删除、标记与不再列出的文件夹随服务端;新 UIDVALIDITY 重建;失败不记时间;缺 EXISTS 即失败且邮件留着;取信中插来的标记行;INBOX 换大小写不删邮件;扩展 LIST 取角色;有附件的只存正文、附件为空且记部分号),应用 `tests/syncs.rs` 3 个(打开即同步并写明连不上、POP3 不同步;凭据库里没有即写明);去掉各规则即失败。未验证:真实服务器;令牌失效(`SignInAgain`)后重新登录的界面未做,现只写明原因。第 2 轮审查后:结构 v1 照已提交的原样,同步所需的列由 v2 升级加上(`schema::V2`:accounts.synced、mailboxes.role 与 uidvalidity、messages.missing),已存的 v1 库升级后邮件照留并能同步(`a_v1_store_gains_sync_and_keeps_its_mail`);处置为 attachment 的多部分整个是一个按需的实体,部分号记在 `missing`,不再逐个子部分判(`an_attached_multipart_waits_whole`);related 里不看处置:根照常读,带 Content-ID 的其余部分即正文资源,标为 attachment 的内嵌图片照样取下(`a_related_image_marked_attachment_comes_down`、`a_related_start_root_ignores_its_disposition`);本机 TLS 初始化失败即为该账号的同步失败,提示条写出原因(`a_missing_tls_is_the_sync_failure`);部分选择单测在 `imap/parts/tests.rs`(5 个,以真实的 BODYSTRUCTURE 解析后重组再经 MIME 读回);去掉各修复即失败。第 3 轮审查后:邮箱的 UIDVALIDITY 不同于所记(含 v2 升级后尚未记下)时只删去别的 UIDVALIDITY 下的邮件,同一 UIDVALIDITY 的照留,升级后的首次同步不再删去已存的正文(`a_v1_store_gains_sync_and_keeps_its_mail` 接着打开邮箱);related 的 `start` 在 BODYSTRUCTURE 里找不到(多部分不带 Content-ID)而有多部分子项时,各多部分子项照正文读,根的文字与 HTML 照样取下(`a_related_start_may_name_a_multipart`);去掉各修复即失败;三轮已满,未再审查
- [x] C12.7 POP3 收取(MAIL-01,§12.3):按 UIDL 去重下载到本地收件箱,“保留服务端副本”默认开,不做伪文件夹同步。已做:`Protocol::Pop { keep }` 记在账号里(结构未发布,旧的开发库里的 POP3 账号读不出即报错);`Pop::delete` 发 DELE(QUIT 才执行);`Store::uidls` 给收件箱已有的 UIDL;`sync::pop` 一轮:账号只有一个 INBOX 邮箱(角色收件箱),UIDL 不在库里的整封 RETR 存下(`Key::Pop`,无标记),不保留副本时每封在本地存下之后才 DELE(已存下而服务端仍列着的也删),服务端不再列出的本地照留;任何一步失败即停,QUIT 前断开即服务端什么也不删,成功才记同步时间;IMAP 账号拒绝。应用:POP3 账号同 IMAP 一样打开窗口与添加后同步;添加对话框的 POP3 有“在服务器上保留副本”开关(默认开,改动回到编辑);账号详情写明下载方式(每封新邮件整封下载)与服务器上保留副本或下载后删除。测试:引擎 `tests/pop_sync.rs` 4 个(新邮件只取一次、副本留在服务器;不保留时存下即删、已存下的也删;取信被拒即停且不删、不记时间;IMAP 账号拒绝),应用 `tests/syncs.rs`(POP3 账号打开即同步并写明连不上;开关默认开、可关);去掉各规则即失败。未验证:真实的 POP3 服务器;用户在本地删去的邮件(C12.15)下次会再取下,届时须记下删去的 UIDL。第 1 轮审查后:同一次列表里重复的 UIDL(RFC 1939 §7 许可同样的两封)只取一次,存下即记入本轮的集合,不保留副本时每个编号照样 DELE(`a_uidl_listed_twice_comes_down_once`,两种模式;去掉修复即失败);第 2 轮通过
- [ ] C12.8 同步控制(MAIL-14):手动刷新、运行期间定时同步、暂停、按文件夹完整离线下载
- [x] C12.8 同步控制(MAIL-14):手动刷新、运行期间定时同步、暂停、按文件夹完整离线下载。已做:结构 v3 给邮箱加 `offline`(默认关),`Store::set_offline`、`folders` 带此开关;`sync::imap` 对离线的文件夹取每封邮件的整封(不限 90 天,连同附件),只有邮件头或缺附件的照补。应用:“邮件›立即同步”同步全部账号,账号页有“立即同步”(同步中或暂停时禁用)与“暂停同步”/“恢复同步”;暂停即中止进行中的同步、此后手动与定时都不同步(记日志),恢复即同步全部;窗口开着时每 10 分钟同步全部(`syncing::EVERY`);暂停时账号页上方以提示条写明;IMAP 账号页每个文件夹一个“离线保留”开关,打开即存下并同步,写不进即提示;同步进行中打开的,该次结束后再同步一次(`Syncing::again`,暂停即清掉);每次同步有序号(`Mail::runs` 全窗口递增,`Syncing::run` 记当前那次;第 2 轮后:原按账号的条目计数,删账号失败后条目重建、序号从 1 重来,迟到的结束会清掉新的同步),暂停后恢复、删账号失败重新同步时上一次迟到的结束都不碰新的同步;账号页可纵向滚动;“已下载”在有离线文件夹时写明其余文件夹的范围,另有一行“完整保留”列出离线文件夹(连同附件)。测试:引擎 `tests/sync.rs` 的离线文件夹取 90 天以前的整封;应用 `tests/controls.rs` 8 个(定时再同步、暂停期间手动与定时都不同步而恢复即同步、开关存下并触发同步、迟到的结束不清掉恢复后的同步且再暂停能中止它、条目忘掉重建后迟到的结束同样不清掉新的同步、同步中打开开关即再同步一次、40 个文件夹时滚得到最后一个开关、范围按离线文件夹写);去掉各规则即失败。`two_saves_in_flight_add_an_address_once` 原假定先发的保存先到库,定时任务改变了测试执行器的任务次序后另一个先到:两种次序都是正确行为,测试改为认任一胜者(库里一个账号、一个凭据、败者写明已添加)。未验证:真实服务器。
- [ ] C12.9 邮箱视图(MAIL-03):账号与文件夹、邮件列表、阅读区三栏;统一收件箱与账号视图;未读、标记、带附件筛选;列表只画可见行,10 万封滚动不掉帧
- [ ] C12.10 阅读(MAIL-04,§12.4):纯文本;HTML 经 html5ever 解析、净化后按块绘制;远程图片与跟踪资源默认拦截,可对当前邮件或发件人放行;cid 内嵌图片;附件列表;原始邮件头
- [ ] C12.11 会话线程(MAIL-04,§12.3):按 Message-ID、In-Reply-To、References 组织,不按主题合并
+9
View File
@@ -0,0 +1,9 @@
FAIL
- [P1] apps/ely-mail/src/syncing.rs:98 — Pause/resume can lose the resumed sync's abort handle. If the old Tokio task has finished but its GPUI completion callback is still queued, pausing takes its handle and resuming starts another task. The old callback then unconditionally sets `syncing.task = None` at line 74, clearing the new task's handle. A subsequent pause leaves that network task running despite the paused banner. The probe reproduced `resumed handle retained=false; paused task cancelled=false` (.work/C12.8-r1-probes.log). Associate completion callbacks with their particular sync invocation and ignore obsolete completions; add a regression covering completion queued across pause/resume. The existing pause test waits for the first sync to finish before pausing and cannot detect this failure.
- [P2] apps/ely-mail/src/syncing.rs:135 — Enabling offline retention during a running sync can discard the required download. `set_offline` calls `sync`, which returns at line 36 whenever that account already has a task. If INBOX has already been processed and the task is waiting on Sent, enabling INBOX offline saves the flag but never revisits INBOX. A real TLS scripted-server probe finished the sync with `offline=true body_cached=false`: the old message still had only headers (.work/C12.8-r1-probes.log). Keep a pending resync request or safely restart the account so the changed folder policy is applied before reporting completion; test enabling the flag after that folder has been visited.
- [P2] apps/ely-mail/src/controls.rs:75 — The unbounded folder switch column has no scrolling container, and neither the account body nor Shell supplies one. With 40 folders in a 1100×800 window, the rendered folder list starts at y=630 and ends at y=1782; Remove Account is at y=1798. The later offline switches cannot be reached on screen, preventing per-folder offline configuration (.work/C12.8-r1-probes.log, layout selectors added only to the probe copies). Constrain and scroll the folder list or account content, and verify that later folders remain reachable in a bounded window.
- [P2] apps/ely-mail/src/view.rs:150 — The download-range row always says that bodies cover the last 90 days and attachments download only when requested, even when a folder has offline retention enabled. After successfully downloading an offline folder, this contradicts both its older cached bodies and `sync::imap`'s whole-message fetches, which also include attachments. PRD §12.3 requires the displayed download range to reflect the policy. Account for offline folders in this row and test the displayed range after toggling one. Correct the same inaccurate attachment claim in tasks/TASKS.md:144, which currently says offline folders still leave attachments on demand.
+3
View File
@@ -0,0 +1,3 @@
FAIL
- [P1] apps/ely-mail/src/syncing.rs:56 — Sync invocation numbers repeat after `stop_sync` removes the entry. During an account's first sync, a synchronous credential read can block; removing the account aborts that task and discards its `Syncing`, but a refused credential deletion restarts syncing with `run = 1` again. If the original credential read subsequently returns an error, its already-running poll completes normally despite the abort. Its completion passes the generation check, clears the replacement task's handle, and makes Pause leave that network task running. A probe through the actual `remove` failure path reproduced `retained=false, cancelled=false` (.work/C12.8-r2-removal-probe.log); allocating invocation numbers across entry recreation made the same probe pass. Keep invocation numbers unique across `stop_sync` and entry recreation, and add a regression covering a late completion after failed removal. The new pause/resume test retains the same entry and misses this case.
+1
View File
@@ -0,0 +1 @@
PASS