| name | analyzing-outlook-pst-for-email-forensics |
| description | Analyze Microsoft Outlook PST and OST files for email forensic evidence including message content, headers, attachments, deleted items, and metadata using libpff, pst-utils, and forensic email analysis tools for legal investigations and incident response. |
| domain | cybersecurity |
| subdomain | digital-forensics |
| tags | ["email-forensics","pst","ost","outlook","mapi","email-headers","attachments","deleted-emails","libpff","eml-extraction"] |
| version | 1.0 |
| author | mahipal |
| license | Apache-2.0 |
Analyzing Outlook PST for Email Forensics
Overview
Microsoft Outlook PST (Personal Storage Table) and OST (Offline Storage Table) files are critical evidence sources in digital forensics investigations. PST files store email messages, calendar events, contacts, tasks, and notes in a proprietary binary format based on the MAPI (Messaging Application Programming Interface) property system. Forensic analysis of these files enables recovery of deleted emails (from the Recoverable Items folder), extraction of email headers for tracing message routes, analysis of attachments for malware or exfiltrated data, and reconstruction of communication patterns. Modern PST files use Unicode format with 4KB pages and can grow up to 50GB, while legacy ANSI format is limited to 2GB.
Prerequisites
- libpff/pffexport (open-source PST parser)
- Python 3.8+ with pypff or libratom libraries
- MailXaminer, Forensic Email Collector, or SysTools PST Forensics (commercial)
- Microsoft Outlook (optional, for native PST access)
- Sufficient disk space for extracted content
PST File Locations
| Source | Path |
|---|
| Outlook 2016+ Default | %USERPROFILE%\Documents\Outlook Files*.pst |
| Outlook Legacy | %LOCALAPPDATA%\Microsoft\Outlook*.pst |
| OST Cache | %LOCALAPPDATA%\Microsoft\Outlook*.ost |
| Archive | %USERPROFILE%\Documents\Outlook Files\archive.pst |
Analysis with Open-Source Tools
libpff / pffexport
pffexport -m all evidence.pst -t exported_pst
pffexport -m items evidence.pst -t exported_emails
pffexport -m recovered evidence.pst -t recovered_items
pffinfo evidence.pst
Python PST Analysis
import pypff
import os
import json
import hashlib
import email
import sys
from datetime import datetime
from collections defaultdict
:
():
.pst_path = pst_path
.output_dir = output_dir
os.makedirs(output_dir, exist_ok=)
.pst = pypff.file()
.pst.(pst_path)
.messages = []
.attachments = []
.stats = defaultdict()
():
folder_name = folder.name
current_path = folder_path folder_name
i (folder.number_of_sub_messages):
:
message = folder.get_sub_message(i)
msg_data = .extract_message(message, current_path)
msg_data:
.messages.append(msg_data)
.stats[] +=
Exception e:
.stats[] +=
i (folder.number_of_sub_folders):
:
subfolder = folder.get_sub_folder(i)
.process_folder(subfolder, current_path)
Exception:
() -> :
msg_data = {
: folder_path,
: message.subject ,
: message.sender_name ,
: ,
: (message.creation_time) message.creation_time ,
: (message.delivery_time) message.delivery_time ,
: (message.modification_time) message.modification_time ,
: message.number_of_attachments > ,
: message.number_of_attachments,
: (message.plain_text_body ),
: (message.html_body ),
}
headers = message.transport_headers
headers:
msg_data[] =
msg_data[] = (headers)
parsed = email.message_from_string(headers)
msg_data[] = parsed.get(, )
msg_data[] = parsed.get(, )
msg_data[] = parsed.get(, )
msg_data[] = parsed.get(, )
msg_data[] = parsed.get(, )
msg_data[] = parsed.get_all(, [])
j (message.number_of_attachments):
:
attachment = message.get_attachment(j)
att_data = {
: msg_data[],
: attachment.name ,
: attachment.size,
: ,
}
.attachments.append(att_data)
.stats[] +=
Exception:
msg_data
():
att_dir = os.path.join(.output_dir, )
os.makedirs(att_dir, exist_ok=)
root = .pst.get_root_folder()
._save_attachments_recursive(root, att_dir, max_size_mb)
():
i (folder.number_of_sub_messages):
:
message = folder.get_sub_message(i)
j (message.number_of_attachments):
att = message.get_attachment(j)
att.size att.size < max_size_mb * * :
name = att.name
safe_name = .join(c c.isalnum() c c name)
path = os.path.join(att_dir, safe_name)
:
data = att.read_buffer(att.size)
(path, ) f:
f.write(data)
Exception:
Exception:
i (folder.number_of_sub_folders):
:
._save_attachments_recursive(folder.get_sub_folder(i), att_dir, max_size_mb)
Exception:
() -> :
root = .pst.get_root_folder()
.process_folder(root)
report = {
: datetime.now().isoformat(),
: .pst_path,
: os.path.getsize(.pst_path),
: (.stats),
: .messages[:],
: .attachments[:],
}
report_path = os.path.join(.output_dir, )
(report_path, ) f:
json.dump(report, f, indent=, default=)
()
()
()
report_path
():
.pst.close()
():
(sys.argv) < :
()
sys.exit()
analyzer = PSTForensicAnalyzer(sys.argv[], sys.argv[])
analyzer.generate_report()
analyzer.close()
__name__ == :
main()