WWDC Quick Look 💓 By SwiftGGTeam
Build Mail app extensions

Build Mail app extensions

元の動画を見る

ハイライト

macOS Monterey は MailKit を導入し、開発者が App Extension アーキテクチャで Mail に作成支援、受信処理、コンテンツブロック、メールセキュリティ機能を追加できるようにします。従来の Mail プラグインがシステム更新で壊れやすかった問題を解決します。

主要内容

多くのチームでは、メールルールが Mail の中に収まっていません。

企業メールでよくある要件は具体的です。機密プロジェクトのメールでは宛先を検査し、プロジェクト関連メールには自動でマークを付け、マーケティングメール内のリモート画像はブロックし、企業の証明書体系で署名と暗号化を扱う必要があります。以前 Mail を深く拡張するには、開発者は Mail プラグインを書くことがありました。プラグインは Mail の内部実装に依存するため、システム更新後に壊れやすいものでした。

Apple は macOS Monterey で MailKit(Mail 拡張フレームワーク)を提供しました。これは App Extension アーキテクチャの上に構築されており、Safari app extensions や Share Sheet extensions と同じ種類の基盤を使います。拡張は署名済みの Mac app に含められ、既存 app と一緒に App Store で配布できます。

この session では 4 種類の入口が示されます。Compose extension はメール作成中の処理、Action extension は新着メールの処理、Content Blocker extension は WebKit のコンテンツブロックルールの再利用、Message Security extension は署名、暗号化、復号を扱います。

セッション内の例は企業向けの機密保持拡張です。メール作成時にはプロジェクトに基づいて宛先を検査し、受信時には Mars プロジェクトのメールを赤くマークし、メールを開いたときには拡張によって復号済みであることを表示します。すべての操作は Mail のネイティブ UI 内で行われます。

詳細

MailKit 拡張の入口

00:27)MailKit は MEExtension から始まります。拡張の principal class がこのプロトコルを実装し、機能ごとに対応する handler を返します。セッションで触れられた 4 つの機能は、すべてこの入口を通じて Mail から呼び出されます。

import MailKit

class MailExtension: NSObject, MEExtension {
    func handlerForContentBlocker() -> MEContentBlocker {
        return ContentBlocker.shared
    }

    func handlerForMessageActions() -> MEMessageActionHandler {
        return MessageActionHandler.shared
    }

    func handler(for session: MEComposeSession) -> MEComposeSessionHandler {
        return ComposeSessionHandler()
    }

    func handlerForMessageSecurity() -> MEMessageSecurityHandler {
        return MessageSecurityHandler.shared
    }
}

キーポイント:

  • import MailKit は Mail 拡張に関係するプロトコルと型を取り込みます。
  • MailExtensionNSObject を継承し、MEExtension を実装します。
  • handlerForContentBlocker() はコンテンツブロック handler を返し、Mail がメール内容を表示するときにルールを読み込みます。
  • handlerForMessageActions() は受信処理 handler を返し、Mail が新着メールをダウンロードするときに呼び出されます。
  • handler(for:)MEComposeSession を受け取ります。作成ウィンドウごとに独立した session があるため、例では新しい ComposeSessionHandler を返しています。
  • handlerForMessageSecurity() はメールセキュリティ handler を返し、署名、暗号化、復号、証明書ビューを担当します。

Compose extension:メール作成時に宛先を検査する

04:31)Compose extension は作成ウィンドウで 4 つのことができます。宛先アドレスの検証、追加ビューの提供、追加メールヘッダの設定、送信前エラーの表示です。セッションの機密保持拡張は、宛先が現在のプロジェクトに属しているかを検査するために使っています。

import MailKit

class ComposeSessionHandler: NSObject, MEComposeSessionHandler {
    func mailComposeSessionDidBegin(_ session: MEComposeSession) {
        // Perform any setup necessary for handling the compose session.
    }

    func mailComposeSessionDidEnd(_ session: MEComposeSession) {
        SpecialProjectHandler.sharedHandler.selectedSpecialProject = nil
    }

    func annotateAddressesForSession(_ session: MEComposeSession) async -> [MEEmailAddress: MEAddressAnnotation] {
        var annotations: [MEEmailAddress: MEAddressAnnotation] = [:]

        for address in session.mailMessage.allRecipientAddresses {
            if !SpecialProjectHandler.verifiedEmails.contains(address) {
                let message = "\(SpecialProjectHandler.bannedDomain) is not a valid domain"
                let annotation = MEAddressAnnotation.error(withLocalizedDescription: message)
                annotations[address] = annotation
            }
        }

        return annotations
    }
}

キーポイント:

  • MEComposeSessionHandler により、拡張は作成ウィンドウのライフサイクルに参加できます。
  • mailComposeSessionDidBegin(_:) は新しい作成ウィンドウが開いたときに呼び出され、現在のウィンドウ状態の初期化に適しています。
  • mailComposeSessionDidEnd(_:) は作成終了時に呼び出され、例では現在選択中のプロジェクトをクリアしています。
  • annotateAddressesForSession(_:) は宛先アドレスが編集されたときに呼び出されます。
  • session.mailMessage.allRecipientAddresses は現在のメールのすべての宛先アドレスを提供します。
  • MEAddressAnnotation.error(withLocalizedDescription:) はエラー注釈を作成し、Mail はそれをアドレス token 上に表示します。
  • 戻り値は [MEEmailAddress: MEAddressAnnotation] で、キーは注釈が必要なアドレス、値は Mail が表示する注釈です。

07:53)Compose extension は作成ウィンドウにカスタム UI を表示することもできます。この UI は MEExtensionViewController のサブクラスから提供する必要があります。

func viewController(for session: MEComposeSession) -> MEExtensionViewController {
    return ComposeSessionViewController(
        nibName: "ComposeSessionViewController",
        bundle: Bundle.main
    )
}

func additionalHeaders(for session: MEComposeSession) -> [String: [String]] {
    guard let project = SpecialProjectHandler.sharedHandler.selectedSpecialProject else {
        return [:]
    }
    return [SpecialProjectHandler.specialProjectsHeader: [project.rawValue]]
}

キーポイント:

  • viewController(for:) は Mail が作成ウィンドウに埋め込むコントローラを返します。
  • ComposeSessionViewController はカスタム UI で、例では特殊プロジェクトの選択に使っています。
  • additionalHeaders(for:) はメール送信時に追加 header を返します。
  • 空の辞書を返すと、現在のメールには追加 header が不要であることを示します。
  • header の値は文字列配列で、例ではプロジェクト名を SpecialProjectHandler.specialProjectsHeader に書き込んでいます。

Action extension:新着メールを自動処理する

08:38)Action extension は Mail が新着メールをダウンロードするときに実行されます。既読状態や flag の変更、Junk、Trash、Archive などのシステムメールボックスへの移動、メール一覧内のメール色設定ができます。

import MailKit

class MessageActionHandler: NSObject, MEMessageActionHandler {
    static let shared = MessageActionHandler()

    func decideAction(for message: MEMessage) async -> MEMessageActionDecision? {
        if SpecialProjectHandler.SpecialProject.allCases.first(where: { message.subject.contains($0.rawValue) }) != nil {
            return MEMessageActionDecision.action(.setBackgroundColor(.yellow))
        }

        if message.rawData == nil {
            return MEMessageActionDecision.invokeAgainWithBody
        }

        if let projects = message.headers?[SpecialProjectHandler.specialProjectsHeader] {
            if projects.contains(SpecialProjectHandler.SpecialProject.marsRemoteOffice.rawValue) {
                return MEMessageActionDecision.action(.flag(.purple))
            } else if projects.contains(SpecialProjectHandler.SpecialProject.apSpaceShuttle.rawValue) {
                return MEMessageActionDecision.action(.flag(.green))
            }
        }

        if let rawData = message.rawData,
           let text = String(data: rawData, encoding: .utf8),
           SpecialProjectHandler.SpecialProject.allCases.first(where: { text.contains($0.rawValue) }) != nil {
            return MEMessageActionDecision.action(.flag(.red))
        }

        return nil
    }
}

キーポイント:

  • MEMessageActionHandler は Mail がダウンロードした新着メールを受け取ります。
  • decideAction(for:)MEMessageActionDecision? を返し、実行すべき動作を Mail に伝えます。
  • 最初のブロックでは message.subject で件名を検査し、プロジェクト名に一致したら黄色の背景を設定します。
  • message.rawData == nil は、現在は一部の header だけがあり、完全な本文がないことを示します。
  • MEMessageActionDecision.invokeAgainWithBody は、Mail に完全な本文と header を取得させた後、handler をもう一度呼び出すよう要求します。
  • message.headers は Compose extension が書き込んだカスタム header を読み取ります。
  • .flag(.purple).flag(.green).flag(.red) はメールに異なる flag を設定します。
  • nil を返すと、このメールを処理しないことを示します。

Content Blocker:Safari のコンテンツブロックルールを再利用する

11:23)Mail のメール内容ビューは WebKit ベースです。Content Blocker extension はコンテンツルールを Mail の WebKit 構成に接続し、HTML 内の URL などの条件に基づいてリソース読み込みをブロックできます。セッションでは、ルール構文が Safari content blockers と同じであることが明示されています。

import MailKit

class ContentBlocker: NSObject, MEContentBlocker {
    static let shared = ContentBlocker()

    func contentRulesJSON() -> Data {
        guard let url = Bundle.main.url(
            forResource: "ContentBlockerRules",
            withExtension: "json"
        ) else { return Data() }

        guard let data = try? Data(contentsOf: url) else { return Data() }

        return data
    }
}

キーポイント:

  • MEContentBlocker は Mail のコンテンツブロックルールを提供します。
  • contentRulesJSON() は JSON の Data エンコードを返します。
  • 例では app bundle から ContentBlockerRules.json を読み取ります。
  • ファイルが見つからない、または読み取りに失敗した場合は空の Data を返し、Mail では利用できるルールがなくなります。
  • ルール形式は Safari content blockers を引き継ぐため、既存の Safari ルールを Mail extension に移行できます。

ルールファイル自体は JSON です。

[
    {
        "action": {
            "type": "block"
        },
        "trigger": {
            "url-filter": "webkit.svg",
            "resource-type": ["image", "style-sheet"],
            "if-domain": "example.com"
        }
    }
]

キーポイント:

  • action.typeblock に設定すると、一致した場合に読み込みをブロックします。
  • trigger.url-filter は URL ルールでリソースを照合します。
  • resource-type はリソース種別を限定し、ここでは画像とスタイルシートに一致します。
  • if-domain はページのドメインを限定し、ここでは example.com だけに適用されます。

Message Security:署名、暗号化、復号を担う

12:57)Message Security extension はメールセキュリティを処理します。送信時には、現在のメールを署名または暗号化できるかを Mail に伝えます。送信ボタンが押された後、RFC822 message data を受け取り、署名または暗号化後の RFC822 data を返します。受信時には、Mail がエンコード済み RFC822 data を拡張へ渡し、拡張はデコード済みメールを返します。

import MailKit

class MessageSecurityHandler: NSObject, MEMessageSecurityHandler {
    static let shared = MessageSecurityHandler()

    func encodingStatus(
        for message: MEMessage,
        composeContext: MEComposeContext
    ) async -> MEOutgoingMessageEncodingStatus {
        let invalidRecipients = message.allRecipientAddresses.filter({ address in
            return !SpecialProjectHandler.verifiedEmails.contains(address)
        })

        if !invalidRecipients.isEmpty {
            return MEOutgoingMessageEncodingStatus(
                canSign: false,
                canEncrypt: false,
                securityError: MessageSecurityError.unverifiedEmails(emailAdresses: invalidRecipients),
                addressesFailingEncryption: invalidRecipients
            )
        } else {
            let encoder = MockEncoder.sharedInstance
            let encodingStatus = encoder.securityStatus(for: message)
            return encodingStatus
        }
    }

    func encode(_ message: MEMessage, composeContext: MEComposeContext) async -> MEMessageEncodingResult {
        return MockEncoder.sharedInstance.encodedMessage(for: message, context: composeContext)
    }
}

キーポイント:

  • MEMessageSecurityHandler はメールセキュリティ handler プロトコルです。
  • encodingStatus(for:composeContext:) は送信者または受信者が変化したときに呼び出され、鍵と証明書アイコンの状態を駆動します。
  • message.allRecipientAddresses は現在の宛先一覧を提供します。
  • MEOutgoingMessageEncodingStatus は、署名可能か、暗号化可能か、エラー情報、暗号化に失敗したアドレスを返します。
  • encode(_:composeContext:) はメール送信時に呼び出されます。
  • 例では実際のエンコード処理を MockEncoder.sharedInstance.encodedMessage(for:context:) に委ねています。

16:05)デコード処理はメール表示時に行われます。拡張がこのメールを処理できる場合は MEDecodedMessage を返し、処理不要な場合はすばやく nil を返します。

func decodedMessage(forMessageData data: Data) -> MEDecodedMessage? {
    let decoder = ExampleDecoder.sharedInstance

    if decoder.shouldDecodeMessage(withData: data) {
        return decoder.decodedMessage(from: data)
    }

    return nil
}

func extensionViewController(signers messageSigners: [MEMessageSigner]) -> MEExtensionViewController? {
    let controller = ExampleSigningViewController.sharedInstance
    controller.signers = messageSigners
    return controller
}

キーポイント:

  • decodedMessage(forMessageData:) はエンコード済み RFC822 データを受け取ります。
  • shouldDecodeMessage(withData:) はまず拡張が処理する必要があるかを判断し、無関係なメールが遅くなるのを避けます。
  • decodedMessage(from:) は復号または署名除去後のメールデータと、署名および暗号化状態を返します。
  • extensionViewController(signers:) は証明書情報 UI を返します。
  • messageSigners は拡張のデコード時に返された signer 情報で、Mail はそれを証明書ビューへ渡します。

重要ポイント

  1. 何をするか:企業メールにプロジェクト単位の宛先検証を追加します。 取り組む価値:Compose extension はユーザーがアドレスを編集したときに MEAddressAnnotation を返せるため、エラーを Mail のアドレス token 上に直接表示できます。 始め方:Mail Extension target を作成し、Compose Session Handler を有効にして、annotateAddressesForSession(_:)session.mailMessage.allRecipientAddresses を読み取り、企業の権限表から注釈を生成します。

  2. 何をするか:プロジェクトメールを自動で異なる色または flag に分類します。 取り組む価値:Action extension は新着メールが受信箱に入る前に実行されるため、ユーザーが Mail を開いた時点で分類結果を確認できます。 始め方MEMessageActionHandler.decideAction(for:) を実装し、まず message.subjectmessage.headers を検査し、本文が必要な場合は MEMessageActionDecision.invokeAgainWithBody を返します。

  3. 何をするか:メール内のリモート追跡画像をブロックします。 取り組む価値:Content Blocker extension は Safari content blockers のルール形式を使うため、既存の WebKit ルールを再利用できます。 始め方MEContentBlocker.contentRulesJSON() を実装し、リモート画像ドメインをブロックする JSON ルールを app bundle に入れます。

  4. 何をするか:企業の証明書体系を Mail の送信フローに接続します。 取り組む価値:Message Security extension は現在の宛先に基づいて署名と暗号化の状態を返せるため、Mail は鍵と証明書アイコンでユーザーへフィードバックできます。 始め方encodingStatus(for:composeContext:) を実装して証明書の利用可否を検査し、encode(_:composeContext:) でエンコード済み RFC822 データを返します。

  5. 何をするか:暗号化メール向けにカスタム証明書詳細ページを提供します。 取り組む価値:Mail は拡張が MEExtensionViewController を返して signer や message context を表示することを許可しており、企業は内部証明書フィールドを表示できます。 始め方:メールのデコード時に signer 情報を返し、extensionViewController(signers:) を実装して signer データをカスタムビューに入れます。

関連セッション

  • Meet Safari Web Extensions on iOS — Safari Web Extensions のデバイス横断拡張モデルを理解し、WebKit コンテンツブロックルールを Safari と Mail で再利用する方法を比較できます。
  • Sync files to the cloud with FileProvider on macOS — FileProvider も extension アーキテクチャを使い、サードパーティサービスをシステムレベルの体験へ統合します。
  • Meet TestFlight on Mac — Mail extension は Mac app と一緒に配布され、TestFlight on Mac は拡張の外部テストに使えます。
  • Distribute apps in Xcode with cloud signing — Mail extension は署名済みの Mac app に含める必要があり、クラウド署名フローは配布前の証明書設定コストを減らせます。

コメント

GitHub Issues · utterances