iOS 簡訊過濾筆記|Message Filter App Extension

前陣子在研究 iOS 的簡訊過濾功能,想說是不是也能做出像 Whoscall 那樣能幫忙擋垃圾簡訊的東西。結果研究下來才發現,表面看似簡單,實際上被蘋果設了不少限制😂
這篇就當我的研究筆記,順便記錄一些實作心得,給未來的自己和有興趣研究這功能的開發者參考。
那就開始吧!
Message Filter 是什麼?

Message Filter App Extension 是蘋果在 iOS 11 之後提供的 API,讓開發者可以協助系統過濾「簡訊」。但這裡的「簡訊」只限於 SMS,不是 iMessage。簡單說,就是幫系統判斷這封訊息要不要歸類成垃圾或促銷。
系統分類機制
如果你有看過「未知與垃圾訊息」的分頁,那就是這個機制在運作。當簡訊進來時,系統會詢問 Extension 該如何處理,依照回傳的分類決定是否要顯示在主要收件匣中。
開發前的準備
建立主 App 與 Extension
首先要知道,Message Filter 不能單獨存在。它必須掛在一個主 App 底下,所以得先建一個主 App 專案,再加上 Message Filter Extension。
共用資料與設定
App 和 Extension 要共用資料,必須設定 App Group。這樣才能共用像 Realm 資料庫或設定檔等內容。
如果還想讓它連線到後端 API(例如把未知簡訊送去伺服器判斷),就要設定 Shared Web Credentials 與 apple-app-site-association 檔案,這個檔案要放在後端的根目錄或 .well-known 資料夾裡。
iOS 14+ 取得 AASA 的變更與快取
- CDN 快取:自 iOS 14 / macOS 11 起,裝置不再直接向你的主機抓取 AASA 檔案,而是透過 Apple 的 CDN 取得並快取。
- Developer alternate mode:開發調試時若想繞過 CDN 直接連你的主機,可在 Associated Domains entitlement 中於網域後加上查詢參數
?mode=developer,例如:
applinks:example.com?mode=developer webcredentials:example.com?mode=developer- 這能讓裝置直接抓取你伺服器上的最新版 AASA,降低等待 CDN 更新的時間。完成上線前,請移除該參數,避免非預期行為。
簡訊過濾的原理
本地端分類
簡訊收到後,系統會先詢問 Extension:這封要分類成什麼?
func handle(_ queryRequest: ILMessageFilterQueryRequest,
context: ILMessageFilterExtensionContext,
completion: @escaping (ILMessageFilterQueryResponse) -> Void) {
let response = ILMessageFilterQueryResponse()
response.action = .junk // 或 .promotion、.transaction 等
completion(response)
}這段就是最基本的邏輯:決定訊息的去向。
後端分類
若想交給後端判斷,可用 deferQueryRequestToNetwork() 讓系統自行呼叫 API。但要注意:
- API 的 URL 必須寫死在 Info.plist 中,不能動態變更。
<key>NSExtension</key>
<dict>
<key>NSExtensionPrincipalClass</key>
<string>MessageFilterExtension</string>
<key>NSExtensionAttributes</key>
<dict>
<key>ILMessageFilterExtensionNetworkURL</key>
<string>https://www.example-sms-filter-application.com/api</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.identitylookup.message-filter</string>
</dict>- 請求格式固定,開發者無法自訂。
POST /server-endpoint HTTP/1.1
Accept: */*
Content-Type: application/json; charset=utf-8
Content-Length: 148
{
"_version": 1,
"query": {
"sender": "14085550001",
"message": {
"text": "This is a message"
}
},
"app": {
"version": "1.1"
}
}能過濾什麼?
其實這個功能限制很多:
資料取得限制
- 只能處理 未知聯絡人 的 SMS,iMessage 無法過濾。
- 無法讀取舊簡訊,只能處理「當下收到」的內容。
可用分類
可用分類是系統定義好的:
transaction(交易)promotion(促銷)junk(垃圾)
自 iOS 16 起,蘋果在原本三大分類下(transaction、promotion、junk)新增了更多子分類,總共有 12 個:
- 交易 (transaction):財務 (finance)、訂單 (orders)、提醒 (reminders)、健康 (health)、公共服務 (public services)
- 促銷 (promotion):天氣 (weather)、電信營運商 (carrier)、獎勵 (rewards)、其他促銷內容 (other promotions)
- 垃圾 (junk):優惠券 (coupons)、優惠 (offers)、其他垃圾內容 (other junk)
這些子分類能讓訊息分類更精細,但開發者仍無法自訂新的分類或修改系統既有分類。
延伸閱讀
iOS 16 之後的更新重點
- 子分類(Subcategories):除了三大類外,可進一步標註至細部子分類,方便使用者在「未知與垃圾訊息」頁面瀏覽與檢索。實作上,子分類屬於官方定義的列舉(IdentityLookup.framework),需回傳系統支援的值。
- 能力查詢(Capabilities Query):在 iOS 16 後,系統會透過
ILMessageFilterCapabilitiesQueryRequest詢問 Extension 支援哪些分類/子分類與行為,你需要在對應的handle(capabilitiesQueryRequest:...)中回報(例如宣告支援網路過濾、支援的分類集合)。 - 文件與範例:建議搭配 WWDC22〈Explore SMS message filters〉與最新的 IdentityLookup 文件核對子分類常數與可用行為,避免使用到已廢棄或區域限定的值。
補充:部份分類與子分類在特定市場才會預設出現;請以官方最新文件為準,並以實機在目標地區版本測試。
使用者啟用設定
就算 Extension 寫好了,用戶也要手動開啟這功能:
路徑: 設定 → 訊息 → 未知與垃圾訊息 → SMS 過濾 → 選擇你的 App。
無法直接導向設定頁
若想從 App 跳轉,蘋果未提供直接 API。只能開啟 App 自身設定頁:
if let appSettings = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(appSettings)
}這只能帶使用者到 App 的設定頁,無法直接開到訊息設定。
審核與上架注意事項
Bundle 與 Target 設定
帶有 Extension 的 App 是兩個 Target,需確保 Bundle ID 前綴一致。App Store Connect 新增 App 時只需主 App 的 ID。
上架測試
上架前一定要測試主 App 與 Extension 是否正常,並符合蘋果的安全標準,否則容易被退件。
實際使用心得
這功能適合用於研究與實驗,不太適合商業防詐應用。限制太多、權限太嚴格,也無法存取歷史訊息。蘋果明顯不希望開發者攔截太多使用者資料。
從另一個角度看,這也體現了蘋果對隱私與安全的高標準。若任何 App 都能讀簡訊內容,後果會非常嚴重。
這次研究 Message Filter App Extension 算是一次有趣的挖坑經驗。過程中不斷發現「欸?原來這不能做」、「這又被鎖起來」😂
不過至少對整個系統機制有更深的了解,感覺如果真的想做防詐功能,還是得靠後端配合,而不是全靠 iOS 端。



