| name | analyzing-outlook-pst-for-email-forensics |
| description | Parse Microsoft Outlook PST and OST files using libpff and pst-utils to extract message content, headers, attachments, deleted items, and MAPI metadata, including recovery of items from the Recoverable Items folder. Use when conducting email forensic investigations, legal e-discovery, or incident response that requires reconstructing communication patterns or tracing message routing from Outlook archives. |
| 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 |
| nist_ai_rmf | ["MANAGE-2.4","MANAGE-3.1","MEASURE-3.1"] |
| nist_csf | ["RS.AN-03","DE.AE-02","RS.MA-01"] |
| mitre_attack | ["T1114.001","T1564.008","T1070.008"] |
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.
When to Use
- When investigating security incidents that require analyzing outlook pst for email forensics
- When building detection rules or threat hunting queries for this domain
- When SOC analysts need structured procedures for this analysis type
- When validating security monitoring coverage for related attack techniques
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
json
hashlib
email
sys
datetime datetime
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()