Skip to content

Commit 9f15c5a

Browse files
committed
feat: add generic spotlight mail monitor and release v0.1.1
1 parent f429208 commit 9f15c5a

File tree

16 files changed

+574
-212
lines changed

16 files changed

+574
-212
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@
2626

2727
## 项目简介 | Overview
2828

29-
AutoCode 是一个面向 macOS 的验证码助手,自动监听 iMessage、Apple Mail、Outlook 中的新消息并提取验证码,然后按你的策略自动输入或复制到剪贴板。
30-
AutoCode is a macOS desktop app that monitors incoming messages/emails, extracts verification codes, then auto-types or copies them based on your settings.
29+
AutoCode 是一个面向 macOS 的验证码助手,自动监听 iMessage、Apple Mail、Spotlight 邮件源(含 Outlook)中的新消息并提取验证码,然后按你的策略自动输入或复制到剪贴板。
30+
AutoCode is a macOS desktop app that monitors incoming messages/emails from iMessage, Apple Mail, and Spotlight-based mail sources (including Outlook), then auto-types or copies verification codes based on your settings.
3131

3232
## 核心功能 | Features
3333

3434
| 中文 | English |
3535
| --- | --- |
36-
| 多来源监听:iMessage / Apple Mail / Outlook(Spotlight) | Multi-source monitoring: iMessage / Apple Mail / Outlook via Spotlight |
36+
| 多来源监听:iMessage / Apple Mail / Spotlight 邮件(含 Outlook) | Multi-source monitoring: iMessage / Apple Mail / Spotlight mail sources (including Outlook) |
3737
| 多策略提取:模板正则、发件人白名单、HTML 结构、关键词近邻 | Multi-strategy extraction: regex templates, sender whitelist, HTML structure, keyword proximity |
3838
| 粘贴模式:`smart` / `always` / `floating_only` / `clipboard_only` | Paste modes: `smart` / `always` / `floating_only` / `clipboard_only` |
3939
| 前端设置修改后,托盘勾选状态即时同步 | Tray check states sync immediately after settings are changed in UI |
@@ -91,7 +91,7 @@ Without these permissions, the app still runs but some features are degraded.
9191
| --- | --- | --- | --- |
9292
| `listen_imessage` | `true` | 是否监听 iMessage | Enable iMessage monitor |
9393
| `listen_apple_mail` | `true` | 是否监听 Apple Mail | Enable Apple Mail monitor |
94-
| `listen_outlook` | `true` | 是否监听 Outlook | Enable Outlook monitor |
94+
| `listen_outlook` | `true` | 是否监听 Spotlight 邮件源(含 Outlook | Enable Spotlight mail monitor (including Outlook) |
9595
| `paste_mode` | `smart` | 粘贴策略模式 | Paste behavior mode |
9696
| `auto_enter` | `false` | 自动输入后回车 | Press Enter after auto-typing |
9797
| `launch_at_login` | `false` | 开机自启 | Launch at login |
@@ -120,7 +120,7 @@ AutoCode/
120120
├─ src/ # Frontend (Vanilla HTML/CSS/JS)
121121
├─ src-tauri/
122122
│ ├─ src/
123-
│ │ ├─ monitor/ # iMessage / Apple Mail / Outlook monitors
123+
│ │ ├─ monitor/ # iMessage / Apple Mail / Spotlight mail monitors
124124
│ │ ├─ extractor.rs # Multi-strategy code extraction
125125
│ │ ├─ paste.rs # Auto-typing and conflict handling
126126
│ │ ├─ permissions.rs # Permission checks and settings shortcuts
@@ -144,10 +144,10 @@ AutoCode/
144144
- 检查 `辅助功能` 权限
145145
- 如果在 `smart` 模式,可能因为 AutoFill 冲突规避而回退为“仅复制”
146146

147-
### Outlook 识别不稳定
147+
### Spotlight 邮件识别不稳定
148148

149149
- 依赖 Spotlight 索引
150-
- 先确认 `mdfind` 能在 Outlook 数据目录检索到目标邮件
150+
- 先确认 `mdfind` 能在目标客户端数据目录检索到邮件
151151

152152
## 致谢 | Acknowledgement
153153

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "autocode",
33
"private": true,
4-
"version": "0.1.0",
4+
"version": "0.1.1",
55
"type": "module",
66
"scripts": {
77
"tauri": "tauri"

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "autocode"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = "AutoCode - Smart 2FA verification code extractor for macOS"
55
authors = ["you"]
66
edition = "2021"
@@ -29,4 +29,3 @@ dirs = "6"
2929
enigo = "0.3"
3030
anyhow = "1"
3131
thiserror = "2"
32-

src-tauri/src/autostart.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ pub fn enable() -> Result<()> {
5656
let exe_str = exe.to_string_lossy();
5757

5858
let content = generate_plist(&exe_str);
59-
fs::write(&path, &content)
60-
.with_context(|| format!("写入 LaunchAgent 失败: {:?}", path))?;
59+
fs::write(&path, &content).with_context(|| format!("写入 LaunchAgent 失败: {:?}", path))?;
6160

6261
log::info!("已注册开机自启: {:?}", path);
6362
Ok(())
@@ -67,8 +66,7 @@ pub fn enable() -> Result<()> {
6766
pub fn disable() -> Result<()> {
6867
let path = plist_path()?;
6968
if path.exists() {
70-
fs::remove_file(&path)
71-
.with_context(|| format!("删除 LaunchAgent 失败: {:?}", path))?;
69+
fs::remove_file(&path).with_context(|| format!("删除 LaunchAgent 失败: {:?}", path))?;
7270
log::info!("已取消开机自启: {:?}", path);
7371
}
7472
Ok(())

src-tauri/src/clipboard.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::{Context, Result};
2-
use enigo::{Enigo, Keyboard, Key, Settings, Direction};
2+
use enigo::{Direction, Enigo, Key, Keyboard, Settings};
33
use std::process::Command;
44
use std::thread;
55
use std::time::Duration;
@@ -29,11 +29,14 @@ pub fn paste_from_clipboard() -> Result<()> {
2929
// 短暂延时确保剪贴板就绪
3030
thread::sleep(Duration::from_millis(50));
3131

32-
enigo.key(Key::Meta, Direction::Press)
32+
enigo
33+
.key(Key::Meta, Direction::Press)
3334
.map_err(|e| anyhow::anyhow!("按下 Meta 键失败: {}", e))?;
34-
enigo.key(Key::Unicode('v'), Direction::Click)
35+
enigo
36+
.key(Key::Unicode('v'), Direction::Click)
3537
.map_err(|e| anyhow::anyhow!("点击 V 键失败: {}", e))?;
36-
enigo.key(Key::Meta, Direction::Release)
38+
enigo
39+
.key(Key::Meta, Direction::Release)
3740
.map_err(|e| anyhow::anyhow!("释放 Meta 键失败: {}", e))?;
3841

3942
Ok(())
@@ -44,7 +47,8 @@ pub fn type_text(text: &str) -> Result<()> {
4447
let mut enigo = Enigo::new(&Settings::default())
4548
.map_err(|e| anyhow::anyhow!("创建 Enigo 实例失败: {}", e))?;
4649

47-
enigo.text(text)
50+
enigo
51+
.text(text)
4852
.map_err(|e| anyhow::anyhow!("输入文本失败: {}", e))?;
4953

5054
Ok(())
@@ -57,7 +61,8 @@ pub fn press_enter() -> Result<()> {
5761

5862
thread::sleep(Duration::from_millis(100));
5963

60-
enigo.key(Key::Return, Direction::Click)
64+
enigo
65+
.key(Key::Return, Direction::Click)
6166
.map_err(|e| anyhow::anyhow!("按下回车键失败: {}", e))?;
6267

6368
Ok(())

src-tauri/src/config.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub struct AppConfig {
3131
pub listen_imessage: bool,
3232
/// 是否监听 Apple Mail
3333
pub listen_apple_mail: bool,
34-
/// 是否监听 Outlook(通过 Spotlight
34+
/// 是否监听 Spotlight 邮件源(兼容 Outlook 等客户端
3535
pub listen_outlook: bool,
3636
/// 粘贴行为
3737
pub paste_mode: PasteMode,
@@ -146,11 +146,11 @@ impl AppConfig {
146146
return Ok(config);
147147
}
148148

149-
let content = fs::read_to_string(&path)
150-
.with_context(|| format!("读取配置文件失败: {:?}", path))?;
149+
let content =
150+
fs::read_to_string(&path).with_context(|| format!("读取配置文件失败: {:?}", path))?;
151151

152-
let config: Self = toml::from_str(&content)
153-
.with_context(|| "解析配置文件失败,使用默认配置")?;
152+
let config: Self =
153+
toml::from_str(&content).with_context(|| "解析配置文件失败,使用默认配置")?;
154154

155155
Ok(config)
156156
}

src-tauri/src/extractor.rs

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ pub struct VerificationCode {
1414
/// 提取策略 trait
1515
trait ExtractionStrategy: Send + Sync {
1616
fn name(&self) -> &str;
17-
fn extract(&self, text: &str, sender: Option<&str>, config: &AppConfig) -> Option<VerificationCode>;
17+
fn extract(
18+
&self,
19+
text: &str,
20+
sender: Option<&str>,
21+
config: &AppConfig,
22+
) -> Option<VerificationCode>;
1823
fn confidence(&self) -> f32;
1924
}
2025

@@ -26,7 +31,12 @@ impl ExtractionStrategy for TemplateStrategy {
2631
"模板匹配"
2732
}
2833

29-
fn extract(&self, text: &str, _sender: Option<&str>, config: &AppConfig) -> Option<VerificationCode> {
34+
fn extract(
35+
&self,
36+
text: &str,
37+
_sender: Option<&str>,
38+
config: &AppConfig,
39+
) -> Option<VerificationCode> {
3040
for pattern_str in &config.verification_patterns {
3141
match Regex::new(pattern_str) {
3242
Ok(re) => {
@@ -66,7 +76,12 @@ impl ExtractionStrategy for SenderWhitelistStrategy {
6676
"发件人白名单"
6777
}
6878

69-
fn extract(&self, text: &str, sender: Option<&str>, config: &AppConfig) -> Option<VerificationCode> {
79+
fn extract(
80+
&self,
81+
text: &str,
82+
sender: Option<&str>,
83+
config: &AppConfig,
84+
) -> Option<VerificationCode> {
7085
let sender = sender?;
7186
let sender_lower = sender.to_lowercase();
7287

@@ -82,10 +97,7 @@ impl ExtractionStrategy for SenderWhitelistStrategy {
8297
// 对已知发件人使用更宽松的数字提取
8398
let re = Regex::new(r"\b(\d{4,8})\b").ok()?;
8499
// 找到所有数字候选
85-
let candidates: Vec<&str> = re
86-
.find_iter(text)
87-
.map(|m| m.as_str())
88-
.collect();
100+
let candidates: Vec<&str> = re.find_iter(text).map(|m| m.as_str()).collect();
89101

90102
if candidates.is_empty() {
91103
return None;
@@ -119,19 +131,26 @@ impl ExtractionStrategy for KeywordProximityStrategy {
119131
"关键词近邻"
120132
}
121133

122-
fn extract(&self, text: &str, _sender: Option<&str>, config: &AppConfig) -> Option<VerificationCode> {
134+
fn extract(
135+
&self,
136+
text: &str,
137+
_sender: Option<&str>,
138+
config: &AppConfig,
139+
) -> Option<VerificationCode> {
123140
let text_lower = text.to_lowercase();
124141

125142
for keyword in &config.verification_keywords {
126143
let keyword_lower = keyword.to_lowercase();
127144
if let Some(pos) = text_lower.find(&keyword_lower) {
128145
// 在关键词前后 80 字符范围内搜索
129-
let byte_start = text.char_indices()
146+
let byte_start = text
147+
.char_indices()
130148
.rev()
131149
.find(|(i, _)| *i <= pos.saturating_sub(80))
132150
.map(|(i, _)| i)
133151
.unwrap_or(0);
134-
let byte_end = text.char_indices()
152+
let byte_end = text
153+
.char_indices()
135154
.find(|(i, _)| *i >= (pos + keyword.len() + 80).min(text.len()))
136155
.map(|(i, _)| i)
137156
.unwrap_or(text.len());
@@ -142,14 +161,16 @@ impl ExtractionStrategy for KeywordProximityStrategy {
142161
let re = Regex::new(r"\b(\d{4,8})\b").ok()?;
143162
let candidates: Vec<regex::Match> = re.find_iter(window).collect();
144163

145-
if let Some(best) = candidates.iter()
146-
.min_by_key(|m| {
147-
// 距离关键词最近的
148-
let m_center = m.start() + m.len() / 2;
149-
let kw_pos_in_window = if pos >= byte_start { pos - byte_start } else { 0 };
150-
(m_center as i64 - kw_pos_in_window as i64).unsigned_abs()
151-
})
152-
{
164+
if let Some(best) = candidates.iter().min_by_key(|m| {
165+
// 距离关键词最近的
166+
let m_center = m.start() + m.len() / 2;
167+
let kw_pos_in_window = if pos >= byte_start {
168+
pos - byte_start
169+
} else {
170+
0
171+
};
172+
(m_center as i64 - kw_pos_in_window as i64).unsigned_abs()
173+
}) {
153174
let code = best.as_str().to_string();
154175
log::debug!("关键词 '{}' 近邻提取到验证码: {}", keyword, code);
155176
return Some(VerificationCode {
@@ -176,7 +197,12 @@ impl ExtractionStrategy for HtmlStructureStrategy {
176197
"HTML结构"
177198
}
178199

179-
fn extract(&self, text: &str, _sender: Option<&str>, _config: &AppConfig) -> Option<VerificationCode> {
200+
fn extract(
201+
&self,
202+
text: &str,
203+
_sender: Option<&str>,
204+
_config: &AppConfig,
205+
) -> Option<VerificationCode> {
180206
// 检测是否是 HTML 内容
181207
if !text.contains('<') || !text.contains('>') {
182208
return None;

0 commit comments

Comments
 (0)