Build Mail app extensions
System & Services 进阶 23m

构建邮件扩展

Build Mail app extensions

2021年6月9日

在 Apple 官方观看视频

一句话判断

macOS Monterey 的 Mail 扩展框架把邮件客户端的”封闭花园”打开了——你可以拦截、修改、阻止邮件,甚至添加自定义的邮件操作按钮,这在以前需要做整个邮件客户端才能实现。

这场 Session 讲了什么

Session 介绍了 macOS Monterey 中全新的 Mail 扩展框架,允许开发者构建针对 Mail.app 的扩展。支持四种扩展类型:MessageSecurity(邮件加密/签名验证)、Compose(邮件编写拦截和修改)、BlockList(邮件拦截/过滤)、和 ContentExtension(自定义邮件操作按钮)。

这个框架基于 MEExtensionContextMEMessage 构建一套完整的邮件处理管线。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 共享数据。
WWDC 2021