Madrid is a Swift package that provides read-only access to
your iMessage® chat.db database.
It comprises the following two modules:
- iMessage: Core functionality for querying an iMessage database.
- TypedStream:
A Swift implementation for decoding Apple's
typedstreamformat, adapted from Christopher Sardegna's work on imessage-exporter.
- Xcode 16+
- Swift 6.0+
- macOS 13.0+
Add Madrid as a dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/mattt/Madrid.git", from: "0.4.0")
]Then add the modules you need to your target's dependencies:
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "iMessage", package: "Madrid"),
.product(name: "TypedStream", package: "Madrid")
]
)
]import iMessage
// Create a database (uses `~/Library/Messages/chat.db` by default)
let db = try iMessage.Database()
// Build a predicate-style request
let pastWeek = Date.now.addingTimeInterval(-7 * 24 * 60 * 60) ..< Date.now
let request = FetchRequest<Message>(
predicate: .and([
.participantHandles([
"johnny.appleseed@mac.com",
"+18002752273",
]),
.dateRange(pastWeek),
]),
sortDescriptors: [.date(.descending)],
limit: 10
)
// Execute request
for message in try db.fetch(request) {
print("From: \(message.sender)")
print("Content: \(message.text)")
print("Sent at: \(message.date)")
print("Chat: \(message.chatID)")
}Note
participantHandles matches messages sent by those handles
and messages you sent in chats they take part in.
Messages you send from another device (iPhone, iPad)
are synced without a sender handle,
so they can only be tied to a conversation through their chat —
which Message.chatID exposes.
// Look a chat up by identifier, e.g. from a message's `chatID`
let request = FetchRequest<Chat>(predicate: .id("iMessage;-;+18002752273"))
if let chat = try db.fetch(request).first {
print("Name: \(chat.displayName ?? "-")")
print("Participants: \(chat.participants)")
}Tip
Legacy convenience APIs
(fetchMessages(for:with:in:limit:), fetchChats(with:in:limit:))
are still available,
but are deprecated in favor of fetch(_:).
import TypedStream
let decoder = TypedStreamDecoder()
let data = // ... your typedstream data ...
let result = try decoder.decode(data)
print(result.stringValue)Messages keeps chat.db in WAL mode:
a new message is committed to chat.db-wal
and only copied into chat.db at the next checkpoint,
every few MB of writes — often hours later.
Database(path:) reads the log when chat.db-wal and chat.db-shm are readable
(mode: .automatic, the default)
and otherwise falls back to SQLite's immutable=1,
which sees the main file alone.
db.accessMode tells which one you got.
Pass mode: .live to get an error instead of stale data,
or mode: .immutable when a sandbox grant covers only chat.db
and staleness is acceptable.
If you get the error message "database disk image is malformed" when attempting to connect to your iMessage database, it typically indicates corruption in the SQLite file. The error most often occurs when attempting to read the database while another process (like the Messages app) is actively using it.
To check if the database file is corrupt, you can use SQLite's built-in integrity check`:
sqlite3 ~/Library/Messages/chat.db "PRAGMA integrity_check;If the original database file is corrupt, restore from a Time Machine backup or other backup source.
The most reliable way to prevent this error is to operate on a copy of the iMessage database:
-
Quit Messages: Ensure the Messages app is completely closed.
-
Copy All Database Files:
# Create destination directory mkdir -p ~/imessage_db_copy # Copy main database and supporting files cp -p ~/Library/Messages/chat.db ~/imessage_db_copy/ # Copy WAL and shared memory files if they exist cp -p ~/Library/Messages/chat.db-* ~/imessage_db_copy/ 2>/dev/null || true
Always include the
-shmand-walfiles when copying a SQLite database using WAL mode -
Use the Copied Database:
let homeURL = FileManager.default.homeDirectoryForCurrentUser let dbURL = homeURL.appendingPathComponent("imessage_db_copy/chat.db") let db = try iMessage.Database(path: dbURL.path)
- Christopher Sardegna
(@ReagentX)
for reverse-engineering the
typedstreamformat.
iMessage® is a registered trademark of Apple Inc. This project is not affiliated with, endorsed, or sponsored by Apple Inc.
This project is available under the MIT license. See the LICENSE file for more info.