Skip to content

What Is Messaging Forensics? A Practitioner's Primer

Messaging forensics explained: acquisition options, a normalized message model, the timestamp zoo, evidence hashing and the pitfalls that break timelines.

Published on 10 min read

Messaging forensics is the collection, parsing, normalization and interpretation of communications, from email and SMS to chat apps and workplace collaboration tools, so that they can answer questions of fact: who said what to whom, when, over which channel, and what was shared, edited or removed along the way.

It sits at the crossroads of mobile forensics, computer forensics, eDiscovery and incident response. A business email compromise case needs mail headers. An insider case needs Slack or Teams. A fraud case needs WhatsApp, Telegram and SMS, often all three for the same people. The hard part is rarely reading a single message. It is combining hundreds of thousands of them, from formats that disagree on almost everything, into one timeline you can defend.

This primer covers the fundamentals that apply to every source: how evidence is acquired, what a normalized message looks like, how timestamps are encoded, how to preserve integrity, how to link identities, and which traps catch experienced examiners.

Acquisition: three routes, three trade-offs

Where the data comes from dictates what it contains. There are broadly three routes.

App exports

Most platforms offer a user-facing export: WhatsApp "Export chat", Telegram Desktop's JSON export, Slack workspace exports, the Discord data package, Meta's "Download your information", Google Takeout, Skype's conversation export, LINE's chat history export.

  • Pros: easy to obtain, often with the owner's cooperation; no device access or root required; structured (JSON, CSV) or at least predictable text.
  • Cons: the vendor decides what is included. Exports tend to drop deleted messages, edit history, read receipts and internal identifiers. Some are text-only with device-local times and no time zone. Many are scoped to what one account can see.

Device and database extraction

The alternative is to pull the application's own store from a device, a backup or a desktop profile: chat.db on macOS, sms.db from an iOS backup, msgstore.db and wa.db on Android, ChatStorage.sqlite on iOS, Signal Desktop's db.sqlite, Viber's viber.db, Skype's legacy main.db, mmssms.db for Android SMS.

  • Pros: richer data. You get internal row IDs, delivery and read state, edit timestamps, attachment metadata, and sometimes remnants of deleted records in free pages or write-ahead logs (-wal files).
  • Cons: access is the problem. Mobile stores sit behind the OS sandbox and often behind encryption: Signal Desktop uses SQLCipher, WhatsApp Android backups are encrypted, and Telegram Desktop's tdata is encrypted at rest. Schemas also change between app versions without notice.

The third route goes through the service operator: a subpoena or court order to the provider, or an administrative export from the tenant you control, such as Microsoft Purview eDiscovery for Teams and Exchange, a Slack Enterprise Grid export, or Google Vault.

  • Pros: complete coverage of the tenant, server-side retention (including items users deleted locally, if retention policies apply), and a clear chain of custody from an authoritative source.
  • Cons: only available to the right party (employer, law enforcement, litigant), slow, and delivered in the provider's format, which may flatten threads or render chats as email-like items. Teams eDiscovery exports, for example, represent chat messages as .msg items of class IPM.SkypeTeams.Message.

In practice you combine routes. A user export fills gaps a device extraction cannot reach, and an eDiscovery export corroborates what was found on a laptop.

A normalized message model

To put an email, a Slack post and a WhatsApp line on the same timeline, map every source onto one schema. The MessagingForensics tool uses the following fields per message; they make a good baseline for any toolkit:

FieldMeaningNotes
timestampWhen the message was sentISO 8601 in UTC, or null when the source has no usable time
platformEmail, Slack, WhatsApp, Teams…Fixed vocabulary, not free text
conversationChannel, chat, thread or mailbox folderGroup names change; keep IDs in extra
sender / senderIdDisplay name and stable identifierNames are cosmetic, IDs are evidence
recipientsAddressees, when the format records themEmail always, most chat exports never
text / subjectBody as plain textHTML reduced to text; subject for email
attachmentsName, MIME type, sizeContent is usually a separate file
editedTime of last editOnly when the source records it
deletedTombstone flag"This message was deleted" is data too
replyToParent message referenceIn-Reply-To for email, thread or quote IDs for chat
directionin or out relative to the ownerOnly meaningful for device stores
format / sourceParser used and evidence pathE.g. archive.zip!/general/2024-02-01.json
extraEverything source-specificRaw headers, IDs, flags kept for the record

Two design rules matter. First, never throw away source-specific data just because it does not fit a column; park it in an extra map so a reviewer can go back to it. Second, every row must point back to the exact evidence file and parser that produced it. A timeline entry you cannot trace to its origin is an assertion, not evidence.

The timestamp zoo

Nothing breaks messaging timelines like time. Each platform picks its own epoch, unit and time zone convention, and sometimes several within one database. The table uses the same instant, 2024-02-01 10:15:00 UTC, in each encoding.

EncodingEpoch and unitExample valueWhere you meet it
Unix seconds1970-01-01 UTC, seconds1706782500Slack ts (with a microsecond suffix), Telegram date_unixtime, Skype legacy main.db
Unix milliseconds1970-01-01 UTC, ms1706782500000Android SMS date, WhatsApp Android, Signal Desktop sent_at, Messenger timestamp_ms
Apple Cocoa seconds2001-01-01 UTC, seconds728475300WhatsApp iOS ZMESSAGEDATE, older iMessage chat.db
Apple Cocoa nanoseconds2001-01-01 UTC, ns728475300000000000iMessage chat.db / sms.db on current macOS and iOS
Windows FILETIME1601-01-01 UTC, 100-ns ticks133512561000000000Outlook .msg properties, Teams eDiscovery items
RFC 2822 dateText with explicit offsetThu, 01 Feb 2024 11:15:00 +0100Email Date: and Received: headers
ISO 8601Text, offset optional2024-02-01T10:15:00.000+00:00Discord data packages, Microsoft Graph, Teams JSON
Device-local textNo zone recorded01/02/2024, 11:15WhatsApp .txt export, LINE .txt export

The conversions are simple arithmetic once you know the epoch:

unix_seconds = cocoa_seconds + 978307200
unix_seconds = cocoa_nanoseconds / 1e9 + 978307200
unix_seconds = filetime / 1e7 - 11644473600

The tool applies those rules and auto-scales Unix values: anything above 1e13 is divided down from micro- or nanoseconds, and anything below 1e11 is treated as seconds. Values that land before 1990 or after 2100 are rejected as implausible and the timestamp is left empty rather than invented. That guard matters: a zero, a column mix-up or a Cocoa value read as Unix produces dates in 1970 or 2001 that look real on a chart.

Text formats deserve extra suspicion. WhatsApp and LINE text exports print the phone's local clock with no zone, and the date order (day/month or month/day) depends on the device locale. A value like 03/04/2024 is ambiguous until you see a day above 12 elsewhere in the file. The tool infers the order from the whole file, shows the local time as-is labelled UTC, and emits a warning so nobody mistakes it for true UTC. Likewise, zone-less ISO-style strings such as 2024-01-02 10:00:00 are read as UTC, which is what most exports mean but not all. Record your assumption and the device's time zone setting in your notes.

Email is the opposite problem: every header carries an explicit offset, but the Date: header is written by the sender's client and can be set to anything. The Received: chain, stamped by each relay, is the independent clock to compare against.

Evidence integrity

Messaging evidence is small, text-heavy and trivially editable, which is why integrity practice has to be boring and consistent.

  1. Hash before you parse. Compute a cryptographic hash (SHA-256 is the modern default) of every file as received, before any tool opens it. MessagingForensics does this first: each input file is hashed in the browser before archives are expanded or anything is parsed, and the hash appears in the evidence table and reports.
  2. Work on copies. SQLite in particular will happily modify a file when opened read-write: it can replay and checkpoint a -wal journal into the main database, destroying the pre-checkpoint state. Keep the original, the -wal and the -shm together, and parse copies.
  3. Record the parser and format per file. "Parsed as Slack workspace export" or "Parsed as WhatsApp iOS (ChatStorage.sqlite)" belongs next to the hash. If a file was not recognized, say so explicitly; silent omissions are how gaps go unnoticed.
  4. Keep archive paths. Messages inside a ZIP should cite the member path (export.zip!/Messages/c123/messages.json), not just the archive, so a reviewer can find the exact bytes.
  5. Process locally when you can. Uploading evidence to a third-party service creates a new copy you do not control. An in-browser parser keeps the data on the examiner's machine.

Correlating identities across platforms

The same person shows up as +1 202 555 0143 in SMS, 12025550143@s.whatsapp.net in WhatsApp, jdoe in Slack, a numeric snowflake in Discord and j.doe@example.test in email. Display names collide and change; identifiers are what you pivot on.

Useful anchors:

  • Phone numbers normalized to E.164 (+12025550143). WhatsApp JIDs, Signal, Viber, iMessage handles and SMS addresses all reduce to them.
  • Email addresses, lower-cased. They link email, iMessage (Apple ID handles), Teams and Slack profiles, and Google Chat.
  • Platform user IDs from extra fields: Slack U… IDs, Discord snowflakes, Telegram user… IDs. These survive renames.
  • Shared indicators in content: the same URL, crypto wallet or IBAN appearing across platforms is often the strongest link between accounts.

MessagingForensics extracts URLs, email addresses, phone numbers in international format, IPv4 addresses, Bitcoin and Ethereum addresses and IBANs from message text and subjects, and counts how many messages contain each. The patterns are deliberately conservative: a phone number must start with +, so local-format numbers are not captured. Treat the list as a starting point for pivots, not an exhaustive inventory. Watchlists (keywords one per line) then filter the combined timeline to the messages that matter.

Common pitfalls

Encrypted stores. Signal Desktop's database is SQLCipher-encrypted; you need the key from the profile to decrypt it before any parser can read it. WhatsApp Android backups (.crypt14, .crypt15) need the key file or the end-to-end backup password. Telegram's desktop cache is encrypted. If you only have the encrypted file, the answer is acquisition, not parsing.

Owner-only exports. Discord's data package contains only the messages the requesting account sent, not the other side of the conversation. Messenger and Instagram exports reflect what that account could see at export time. Always state whose perspective an export represents.

Deleted messages. Exports rarely include them; databases sometimes do, as tombstone rows or in unallocated pages. A "deleted" flag in the normalized model means the source recorded a deletion. The absence of a message proves nothing about whether it existed.

Time zones. Mixing device-local text exports with UTC database timestamps can shift events by hours and reorder a conversation. Normalize everything to UTC, flag the rows that could not be normalized, and note daylight saving transitions in the period.

Duplicates across sources. The same message may appear in a WhatsApp export, in msgstore.db and in the counterpart's phone, or the same email in two mailboxes and an MBOX. Deduplicate on stable identifiers (Message-ID, platform message IDs) where they exist, and on timestamp plus sender plus text hash otherwise, but keep every source reference: a message found on both devices is stronger evidence than one found on either.

Edits. An edited message may carry only its final text. If the source records an edit time, keep it; if the platform keeps history, it lives in another table or file.

Display-name drift. Contacts are renamed, groups are retitled, and exports print whatever name was current at export time, not at send time.

Next steps

Try the MessagingForensics parser: drop exports or databases into the browser, get SHA-256 hashes for every file, a unified UTC timeline, extracted indicators and CSV, JSON or HTML reports, without uploading anything.

Then go deeper by source:

Related articles

What a Slack workspace export contains, how to read ts, thread_ts, edits, tombstones and message markup, what exports leave out, and how to verify integrity.
What the Discord data package contains, why it only holds the owner's messages, how to decode snowflake IDs, and when to use DiscordChatExporter output.
Read Received chains bottom-up, compare Return-Path, From and Reply-To, interpret SPF/DKIM/DMARC results and decode MIME across EML, MBOX, MSG and PST.