Skip to content

Microsoft Teams Forensics: Purview, Graph and Cache

Acquire Teams chats via Purview eDiscovery, the Microsoft Graph chatMessage API or a Teams (free) export, and know which fields, edits and deletions matter.

Published on 7 min read

Teams is hard to acquire because there is no single place where "the chat" lives. Messages sit in the Teams service. Compliance copies land in Exchange Online mailboxes. Clients keep a partial local cache, and each route gives a different view of the same conversation. This guide covers the three acquisition routes most practitioners use, what the local cache is and is not, and the fields that carry the most evidential weight.

Microsoft renames and reorganizes these products often. Treat the menu paths and licensing notes below as orientation, and check current Microsoft documentation before relying on them in a report.

Route 1: Microsoft Purview eDiscovery

For corporate tenants, Purview eDiscovery is the defensible default. It runs under the tenant's own compliance controls, is auditable, and does not require touching user endpoints.

Background worth knowing: for 1:1 and group chats, Microsoft 365 stores compliance copies of Teams messages in a hidden folder of each participant's Exchange Online mailbox. For channel conversations, the copies go to the team's group mailbox. These are the items eDiscovery searches. Each one has the message class:

IPM.SkypeTeams.Message

Practical consequences:

  • Export shape varies with export options. Depending on the eDiscovery experience and settings, Teams content can come out inside PST files, as individual message items, or as collated conversation transcripts. Decide on the format before exporting, and record the settings you used.
  • Items look like email but are not email. There are no meaningful Received: headers, no SPF or DKIM, and the "subject" is often empty or a chat topic. The sender, recipients (chat members at the time) and times come from MAPI properties.
  • Chat membership matters. Because chat copies are stored per participant mailbox, searching one custodian gives you that person's view. Collect every relevant custodian and deduplicate, rather than assuming one mailbox holds the whole thread.
  • Retention and holds decide what exists. If a retention policy deleted content before a hold was placed, eDiscovery cannot bring it back.

Route 2: Microsoft Graph API chatMessage

For targeted collections, or where eDiscovery is not available to you, the Microsoft Graph API exposes Teams messages as chatMessage resources:

GET /chats/{chat-id}/messages
GET /teams/{team-id}/channels/{channel-id}/messages
GET /teams/{team-id}/channels/{channel-id}/messages/{message-id}/replies

Notes from the field:

  • Channel messages and their replies are fetched separately. Replies carry a replyToId pointing at the root post.
  • Responses are paged. Each page is a JSON object with a value array, and an @odata.nextLink if there is more. Save every page exactly as returned, with the request URL and retrieval time, before transforming anything.
  • Permissions differ between delegated and application access. Tenant-wide export endpoints are protected APIs that Microsoft may require you to request access for, and that may carry licensing or metering requirements. Check the current Graph documentation for the endpoint you use.

A trimmed example of a single message:

{
  "id": "1709561100123",
  "replyToId": null,
  "messageType": "message",
  "createdDateTime": "2024-03-04T14:05:00.123Z",
  "lastModifiedDateTime": "2024-03-04T14:09:41.500Z",
  "lastEditedDateTime": "2024-03-04T14:09:41.500Z",
  "deletedDateTime": null,
  "chatId": "19:abc123@unq.gbl.spaces",
  "from": { "user": { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Alex Doe" } },
  "body": { "contentType": "html", "content": "<p>Updated bank details attached</p>" },
  "attachments": [],
  "reactions": [
    { "reactionType": "like", "createdDateTime": "2024-03-04T14:10:02Z",
      "user": { "user": { "id": "00000000-0000-0000-0000-000000000002" } } }
  ]
}

Route 3: Teams (free) personal data export

Consumer Teams accounts (the free, personal product, not work or school accounts) can request a personal data export through Microsoft's privacy tools. The chat content arrives as a messages.json in the same general shape as a Skype export: a top-level userId, a conversations array, and a MessageList per conversation.

{
  "userId": "8:live:.cid.0123456789abcdef",
  "conversations": [
    {
      "id": "19:example@thread.v2",
      "displayName": "Project chat",
      "MessageList": [
        {
          "id": "1709561100123",
          "originalarrivaltime": "2024-03-04T14:05:00.123Z",
          "messagetype": "RichText/Html",
          "from": "8:live:.cid.0123456789abcdef",
          "displayName": "Alex Doe",
          "content": "<p>see you at 3</p>",
          "conversationid": "19:example@thread.v2",
          "properties": { "edittime": "1709561381000" }
        }
      ]
    }
  ]
}

This export comes from the account holder, so it fits consent-based and civil matters. It reflects what the service still holds for that account at export time.

The local client cache

Teams desktop clients keep a browser-style cache on disk:

Classic Teams (Electron):
%APPDATA%\Microsoft\Teams\IndexedDB\

New Teams (WebView2):
%LOCALAPPDATA%\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\EBWebView\WV2Profile_tfw\IndexedDB\

These folders hold IndexedDB data backed by LevelDB. They can contain fragments of recently viewed conversations, contact records and sometimes content no longer visible in the client. They are partial and structured around what the client needed to render, not a complete record.

MessagingForensics reads them in the browser with its Rust/WebAssembly engine. Drop the whole …indexeddb.leveldb folder (with its .indexeddb.blob sibling if present), or loose .log/.ldb files from a forensic export. The engine:

  • replays the .log write-ahead files and reads the .ldb tables, including Snappy-compressed blocks;
  • keeps every version of every key and labels it live, overwritten, deleted (a later tombstone removed it) or carved;
  • carves CRC-valid log records out of file slack or unallocated space, even at unaligned offsets;
  • decodes IndexedDB keys and database/object-store names, and deserializes values from V8's format, including Chrome's Snappy-compressed and blob-wrapped values.

Message objects are then recognized by their fields: a content body, a compose or arrival time, a sender display name, and a conversation or client message ID. The client's internal schema is undocumented and changes between versions, so treat that mapping as a lead. Check the raw record in the Artifacts tab (every decoded record is listed there with its file, offset and sequence number) and corroborate against eDiscovery or Graph data for the message record itself.

Fields worth examining

Field (Graph)Why it matters
createdDateTimeUTC time the message was created on the service. Your primary timeline anchor.
lastEditedDateTimeSet when the content was edited. The Graph resource holds only the current body, so the previous text is not in the message itself.
lastModifiedDateTimeChanges on any modification, including reactions. Do not read it as "edited".
deletedDateTimePopulated when the message was deleted. Deleted messages may still appear with this set and the content removed. Verify how your collection route represents them.
replyToIdLinks a channel reply to its root post.
messageTypeSeparates user messages from system and event messages, such as members added or calls, which carry context but not user text.
from.user / from.applicationHuman sender versus bot or app. System messages may have no user sender.
reactionsReaction type plus the reacting user and time, which is useful for showing who read or acknowledged a message.
attachmentsReferences to files (often SharePoint or OneDrive URLs), cards and quoted messages. File content is not embedded, so collect it separately from SharePoint or OneDrive.

In the Skype-shaped export, the equivalents are originalarrivaltime, messagetype, properties.edittime and properties.deletetime.

Two cautions. First, display names are not identities. Pivot on the user object ID or MRI (8:orgid:…, 8:live:…) when attributing messages. Second, chats on a Teams call can include event messages that describe the call but not what was said.

Doing this in MessagingForensics

MessagingForensics runs in the browser. Evidence files are SHA-256 hashed on load and never uploaded. For Teams it reads:

  • Purview .msg items. Outlook .msg files whose message class starts with IPM.SkypeTeams are labeled as Teams. The conversation name comes from the conversation topic property, and the time from the client submit time, falling back to delivery and then creation time. The message class is shown in the detail panel. PST exports must first be converted (for example with readpst). The tool refuses PST and OST directly. See email header forensics for the conversion commands.
  • Graph chatMessage JSON. A saved response page ({"value": [...]}) or a bare array of messages. It maps createdDateTime to the timeline, renders HTML bodies as text, and keeps lastEditedDateTime as the edit time. A populated deletedDateTime becomes a deleted flag. It also maps replyToId, messageType, attachment names and URLs, and reactionType values (the reacting user is not kept). Channel messages are grouped by channelIdentity team and channel IDs, and chats by chatId.
  • Teams (free) messages.json. Parsed with the Skype-style reader. It is labeled Teams when the top-level userId looks like a Teams ID or the file path contains "teams". Otherwise it is labeled as a Skype export, so keep "teams" in the folder or file name. properties.edittime and properties.deletetime become the edit time and deleted flag.

Filter by the deleted flag, switch the timeline between UTC and local time, and export CSV, JSON or an HTML report of the filtered set. Open the analyzer to load an export. Related guides: Slack export forensics, Discord data package forensics, and what is messaging forensics.

Related articles

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.
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.
Messaging forensics explained: acquisition options, a normalized message model, the timestamp zoo, evidence hashing and the pitfalls that break timelines.