构建邮件扩展
Build Mail app extensions
2021年6月9日
一句话判断
macOS Monterey 的 Mail 扩展框架把邮件客户端的”封闭花园”打开了——你可以拦截、修改、阻止邮件,甚至添加自定义的邮件操作按钮,这在以前需要做整个邮件客户端才能实现。
这场 Session 讲了什么
Session 介绍了 macOS Monterey 中全新的 Mail 扩展框架,允许开发者构建针对 Mail.app 的扩展。支持四种扩展类型:MessageSecurity(邮件加密/签名验证)、Compose(邮件编写拦截和修改)、BlockList(邮件拦截/过滤)、和 ContentExtension(自定义邮件操作按钮)。
这个框架基于 MEExtensionContext 和 MEMessage 构建一套完整的邮件处理管线。MEMessage 封装了邮件的所有信息(发件人、收件人、主题、正文、附件),扩展可以读取和修改这些内容。MEComposeContext 提供了编写邮件时的拦截能力——你可以在用户点击”发送”后自动修改邮件内容(比如添加免责声明、附件加密)。BlockList 扩展可以根据规则自动将邮件标记为垃圾邮件。Session 还展示了 ContentExtension 如何在邮件详情界面添加自定义按钮(比如”添加到 CRM”、“创建工单”)。
值得深挖的点
Compose 扩展的拦截时机
Compose 扩展有两个拦截时机:canHandle 决定是否要处理这封邮件,handle 执行实际的处理逻辑。系统会在用户点击发送后先调用 canHandle,如果返回 true,再调用 handle。在 handle 中你可以修改收件人列表、主题行、正文内容和附件。一个实际用例:企业合规场景下,自动检测邮件中是否包含敏感信息(信用卡号、社保号),如果发现就阻止发送或要求确认。
ContentExtension 的交互设计
ContentExtension 在邮件详情界面的工具栏中添加自定义按钮。按钮点击后触发你的 App Extension 代码,可以访问当前邮件的内容。Session 建议按钮设计要克制——最多添加 2-3 个按钮,每个按钮有明确的图标和简短标题。过多的按钮会挤占 Mail 自身的操作空间,用户体验反而变差。
代码片段
// Compose 扩展:在邮件发送前自动添加免责声明
class DisclaimerComposeExtension: MEComposeExtension {
override func canHandle(_ context: MEComposeContext) -> Bool {
// 只处理外发邮件(不是草稿或回复)
return context.action == .compose
}
override func handle(_ context: MEComposeContext,
completion: @escaping (MEComposeExtensionResult) -> Void) {
// 读取当前邮件内容
guard let message = context.message else {
completion(.noChange)
return
}
// 检查是否已经包含免责声明
let disclaimer = "\n\n---\n此邮件为机密信息,仅供指定收件人阅读。"
if message.body?.contains(disclaimer) == true {
completion(.noChange)
return
}
// 在邮件正文末尾追加免责声明
var modifiedBody = message.body ?? ""
modifiedBody += disclaimer
message.body = modifiedBody
// 返回修改后的邮件
completion(.updatedMessage(message))
}
}
// BlockList 扩展:根据规则过滤垃圾邮件
class SpamBlockExtension: MEBlockListExtension {
override func handle(_ message: MEMessage,
completion: @escaping (MEBlockListExtensionResult) -> Void) {
// 检查发件人是否在黑名单中
if isSenderBlocked(message.from) {
completion(.moveToTrash)
return
}
// 检查邮件内容是否匹配垃圾邮件模式
let spamPatterns = ["免费领取", "中奖通知", "点击领取优惠"]
let subject = message.subject ?? ""
let body = message.body ?? ""
for pattern in spamPatterns {
if subject.contains(pattern) || body.contains(pattern) {
completion(.moveToJunk)
return
}
}
// 正常邮件,不做处理
completion(.none)
}
private func isSenderBlocked(_ sender: MEEmailAddress?) -> Bool {
let blockedDomains = ["spam-domain.com", "phishing.net"]
guard let domain = sender?.address?.components(separatedBy: "@").last else {
return false
}
return blockedDomains.contains(domain)
}
}
// ContentExtension:在邮件详情中添加自定义按钮
class CRMContentExtension: MEContentExtension {
override func handle(_ action: String,
for message: MEMessage,
completion: @escaping (MEContentExtensionResult) -> Void) {
switch action {
case "add-to-crm":
// 将发件人添加到 CRM 系统
Task {
let contact = CRMContact(
name: message.from?.displayName ?? "",
email: message.from?.address ?? "",
subject: message.subject ?? ""
)
try await CRMService.shared.addContact(contact)
completion(.success)
}
case "create-ticket":
// 根据邮件创建工单
Task {
let ticket = SupportTicket(
title: message.subject ?? "无主题",
description: message.body ?? "",
reporter: message.from?.address ?? ""
)
try await TicketService.shared.createTicket(ticket)
completion(.success)
}
default:
completion(.noChange)
}
}
}
最佳实践
- Compose 扩展的处理逻辑要快——用户点击发送后等待时间超过 1 秒就会有明显感知。复杂的内容分析(比如 NLP 处理)应该放在后台进行,Compose 扩展只做简单的文本匹配和修改。
- BlockList 规则定期从你的服务器更新,而不是硬编码在扩展中。垃圾邮件的模式变化很快,静态规则很快就会过时。
- ContentExtension 的按钮文案要简短(2-4 个字),配合系统风格的 SF Symbols 图标。避免使用自定义图标——它们在 Light/Dark 模式下可能显示异常。
还有什么值得关注
- Mail 扩展需要在 macOS Monterey 上运行,iOS 上的 Mail 目前不支持第三方扩展。
Info.plist中需要声明扩展类型(MEExtensionPointIdentifier),每种类型有独立的权限范围。- Mail 扩展运行在独立的沙盒进程中,无法直接访问宿主 App 的数据,需要通过 App Group 共享数据。