Guide
Parse and Extract Data from Emails in PowerShell
According to a 2023 McKinsey report, knowledge workers spend 28% of their workday reading and responding to email. Much of that is extracting data -- order confirmations, ticket numbers, shipping updates -- and typing it into another system. This guide shows how to automate that with PowerShell and Nylas CLI. Read emails, extract structured data with regex, process orders and tickets, and build inbox monitoring scripts. Works across all major email providers.
Written by Hazik Director of Product Management
Reviewed by Caleb Geene
Email reading methods compared
PowerShell can read email programmatically through four methods: the EWS Managed API, the Microsoft Graph SDK, raw IMAP via MailKit, and the Nylas CLI. Each method differs in provider coverage, output format, and setup complexity. According to Litmus email client market share data, Gmail and Outlook together account for over 80% of business email, so cross-provider support matters for most automation projects.
| Method | Read inbox | Search | JSON output | Providers |
|---|---|---|---|---|
| EWS Managed API | FindItems + LoadPropertiesForItems | SearchFilter + AQS | Manual serialization | Exchange only (deprecated Oct 2026) |
| Microsoft Graph SDK | Get-MgUserMessage (paginated) | $filter OData query | ConvertTo-Json from objects | Microsoft 365 only |
| Raw IMAP (MailKit) | ImapClient.Inbox.Fetch | SearchQuery builder | Manual serialization | Any IMAP server |
| Nylas CLI | nylas email list --json | nylas email search "query" | Native JSON, pipe to ConvertFrom-Json | Gmail, Outlook, Exchange, Yahoo, iCloud, IMAP |
The Graph SDK requires Mail.Read permission and returns paginated results you must loop through. EWS is deprecated for Exchange Online effective October 2026. Raw IMAP requires MailKit from NuGet and manual MIME parsing. The CLI returns structured JSON that PowerShell's ConvertFrom-Json turns directly into objects.
Setup
Setting up Nylas CLI for email parsing on Windows takes under 2 minutes. The PowerShell install script downloads the latest release binary, verifies a SHA-256 checksum, and places the executable in ~/.config/nylas/bin. After install, a single nylas auth config command stores your API key locally for all subsequent calls.
Install with irm https://cli.nylas.com/install.ps1 | iex and run nylas auth config to paste your API key. See the getting started guide for other install methods.
Extract order numbers from confirmation emails
Order confirmation emails from e-commerce platforms follow predictable patterns that PowerShell regex can parse reliably. The script below searches for order confirmations, reads each message body, and extracts order numbers, dollar amounts, and tracking codes using three regex patterns. According to Statista, global e-commerce generated over $6.3 trillion in revenue in 2024, which means most business inboxes contain dozens of parseable order confirmations per week.
The extraction pipeline calls nylas email search "order confirmation" to find matching messages, then reads each message body with nylas email read. Three regex patterns handle common order ID formats (e.g., #12345, ORD-12345), dollar amounts after "total" or "charged" labels, and alphanumeric tracking numbers. Results are collected into PSCustomObject instances and exported to CSV for use in spreadsheets or downstream scripts.
# extract-orders.ps1 -- Parse order confirmations
$orderEmails = nylas email search "order confirmation" --json --limit 20 |
ConvertFrom-Json
$orders = $orderEmails | ForEach-Object {
$detail = nylas email read $_.id --json | ConvertFrom-Json
$body = $detail.body
# Extract order number (patterns: #12345, Order-12345, ORD-12345)
$orderNum = if ($body -match '(?:order|#|ORD)[-\s]?(\d{4,10})') {
$matches[1]
} else { "unknown" }
# Extract total amount ($123.45 or USD 123.45)
$amount = if ($body -match '(?:total|amount|charged)[\s:]*\$?([\d,]+\.?\d{0,2})') {
$matches[1]
} else { "unknown" }
# Extract tracking number if present
$tracking = if ($body -match '(?:tracking|shipment)[\s:#]*([A-Z0-9]{10,25})') {
$matches[1]
} else { $null }
[PSCustomObject]@{
Date = $_.date
From = $_.from
OrderNum = $orderNum
Amount = $amount
Tracking = $tracking
Subject = $_.subject
}
}
$orders | Format-Table Date, OrderNum, Amount, Tracking -AutoSize
$orders | Export-Csv "orders-extracted.csv" -NoTypeInformation
Write-Host "Extracted $($orders.Count) orders"Parse support ticket notifications
Support platforms like Zendesk, Freshdesk, and Jira Service Management send email notifications with ticket IDs embedded in the subject line and priority levels in the body. Parsing these notifications with PowerShell creates a local tracking dashboard without logging into each platform. Zendesk alone processes over 5.6 million tickets daily across its customer base, according to Zendesk's 2024 CX Trends report.
The script below filters emails from a Zendesk notification address, extracts the ticket number from subject lines formatted as [Ticket #12345], and parses priority and status from the email body. Results are grouped by priority so you can see at a glance how many urgent tickets arrived. The same regex patterns work for Freshdesk and Jira with minor format adjustments.
# parse-tickets.ps1 -- Extract ticket data from help desk emails
$ticketEmails = nylas email search "from:notifications@yourcompany.zendesk.com" `
--json --limit 50 | ConvertFrom-Json
$tickets = $ticketEmails | ForEach-Object {
$detail = nylas email read $_.id --json | ConvertFrom-Json
$body = $detail.body
$subject = $_.subject
# Zendesk format: "[Ticket #12345] Subject here"
$ticketId = if ($subject -match '\[(?:Ticket\s)?#?(\d+)\]') {
$matches[1]
} else { "unknown" }
# Extract priority
$priority = if ($body -match '(?:priority|urgency)[\s:]*(\w+)') {
$matches[1]
} elseif ($subject -match '\b(urgent|high|critical)\b') {
$matches[1]
} else { "normal" }
# Extract status
$status = if ($body -match '(?:status)[\s:]*(\w+(?:\s\w+)?)') {
$matches[1]
} else { "open" }
[PSCustomObject]@{
TicketId = $ticketId
Subject = ($subject -replace '\[.*?\]\s*', '')
Priority = $priority
Status = $status
Date = $_.date
}
}
Write-Host "Ticket Summary:" -ForegroundColor Cyan
$tickets | Group-Object Priority | Sort-Object Count -Descending |
ForEach-Object { Write-Host " $($_.Name): $($_.Count)" }
$tickets | Format-Table TicketId, Priority, Status, Subject -AutoSizeContinuous inbox monitoring
Continuous inbox monitoring polls for new unread emails at a fixed interval and routes each message to a handler based on sender or subject pattern. The default 30-second polling interval balances responsiveness with API rate limits. The script tracks seen message IDs in a hashtable and caps it at 1,000 entries to prevent unbounded memory growth during long-running sessions.
The polling loop below calls nylas email list --unread --json --limit 20 on each cycle, skips any message ID already in the $seenIds hashtable, and routes new messages with a switch -Regex block. At 30-second intervals, the script makes roughly 2,880 API calls per day. Schedule it via Windows Task Scheduler with the -NeverExpire option on a startup trigger for persistent monitoring.
# inbox-processor.ps1 -- Process new emails as they arrive
param([int]$PollIntervalSeconds = 30)
$seenIds = @{}
Write-Host "Monitoring inbox... (poll every $($PollIntervalSeconds)s)" -ForegroundColor Cyan
while ($true) {
try {
$emails = nylas email list --unread --json --limit 20 | ConvertFrom-Json
foreach ($email in $emails) {
if ($seenIds.ContainsKey($email.id)) { continue }
$detail = nylas email read $email.id --json | ConvertFrom-Json
# Route based on sender
switch -Regex ($email.from) {
'orders@' { Write-Host "ORDER: $($email.subject)" -ForegroundColor Green }
'alerts@' { Write-Host "ALERT: $($email.subject)" -ForegroundColor Red }
'zendesk.com' { Write-Host "TICKET: $($email.subject)" -ForegroundColor Yellow }
default { Write-Host "NEW: $($email.subject)" -ForegroundColor Gray }
}
$seenIds[$email.id] = $true
}
# Prevent unbounded memory growth
if ($seenIds.Count -gt 1000) {
$keep = $seenIds.Keys | Select-Object -Last 500
$seenIds = @{}
$keep | ForEach-Object { $seenIds[$_] = $true }
}
} catch {
Write-Host "Error: $_" -ForegroundColor Red
}
Start-Sleep -Seconds $PollIntervalSeconds
}Extract shipping tracking updates
Shipping notification emails from UPS, FedEx, and USPS each use a distinct tracking number format that regex can identify reliably. UPS tracking numbers start with 1Z followed by 16 alphanumeric characters. FedEx uses 12- to 22-digit numeric codes. USPS tracking numbers begin with 92, 93, or 94 followed by 20-22 digits. According to Pitney Bowes, the US alone processed 21.2 billion parcels in 2023, making shipping email parsing one of the most common automation use cases.
The script below defines a carrier lookup table with regex patterns and sender domains, then scans the last 100 emails for matches. When a carrier sender is detected, the message body is parsed for a tracking number and estimated delivery date. Each match produces a PSCustomObject with carrier name, tracking number, and delivery estimate.
# shipping-tracker.ps1 -- Extract tracking info from carrier emails
$carriers = @{
'ups' = @{ Pattern = '1Z[A-Z0-9]{16}'; Sender = '*@ups.com' }
'fedex' = @{ Pattern = '\b\d{12,22}\b'; Sender = '*@fedex.com' }
'usps' = @{ Pattern = '\b(9[234]\d{20,22})\b'; Sender = '*@usps.gov' }
}
$emails = nylas email list --json --limit 100 | ConvertFrom-Json
$shipments = foreach ($email in $emails) {
foreach ($carrier in $carriers.GetEnumerator()) {
if ($email.from -like $carrier.Value.Sender) {
$detail = nylas email read $email.id --json | ConvertFrom-Json
if ($detail.body -match $carrier.Value.Pattern) {
$deliveryDate = if ($detail.body -match '(?:deliver|expected).*?(\d{1,2}/\d{1,2}/\d{2,4})') {
$matches[1]
} else { "TBD" }
[PSCustomObject]@{
Carrier = $carrier.Key.ToUpper()
Tracking = $matches[0]
DeliveryEst = $deliveryDate
Subject = $email.subject
}
}
}
}
}
$shipments | Format-Table Carrier, Tracking, DeliveryEst, Subject -AutoSizeParse financial transaction emails
Financial transaction emails from banks and payment processors follow a consistent pattern: a dollar amount, a merchant name, and a debit or credit indicator. Parsing these notifications with PowerShell builds a local transaction ledger without scraping bank portals. A 2023 Javelin Strategy & Research survey found that 72% of US consumers receive email alerts for every card transaction, creating a reliable data source for automated financial tracking.
The script below searches for emails from a bank alert address, extracts dollar amounts using a regex that matches $123.45 patterns, identifies the merchant name after keywords like "at" or "payee", and classifies each transaction as debit or credit. Results are summed by type and sorted by amount for a quick spending overview.
# financial-parser.ps1 -- Extract transaction data from bank emails
$bankEmails = nylas email search "from:alerts@yourbank.com" --json --limit 50 |
ConvertFrom-Json
$transactions = $bankEmails | ForEach-Object {
$detail = nylas email read $_.id --json | ConvertFrom-Json
$body = $detail.body
$amount = if ($body -match '\$\s?([\d,]+\.\d{2})') { $matches[1] -replace ',' }
else { $null }
$merchant = if ($body -match "(?:at|merchant|payee)[\s:]+([A-Za-z0-9\s&']+?)(?:\.|,|\n)") {
$matches[1].Trim()
} else { "Unknown" }
$type = if ($body -match 'debit|purchase|withdrawal') { "Debit" }
elseif ($body -match 'credit|deposit|refund') { "Credit" }
else { "Unknown" }
if ($amount) {
[PSCustomObject]@{
Date=$_.date; Type=$type; Amount=[decimal]$amount; Merchant=$merchant
}
}
}
$totalDebits = ($transactions | Where-Object { $_.Type -eq "Debit" } |
Measure-Object -Property Amount -Sum).Sum
Write-Host "Transactions: $($transactions.Count), Total debits: $totalDebits"
$transactions | Sort-Object Amount -Descending | Format-Table -AutoSizeRoute emails to different handlers
An email router dispatches incoming messages to different processing functions based on sender address or subject pattern. This pattern separates routing logic from parsing logic, making it straightforward to add new handlers without modifying existing ones. A typical SaaS team receives notifications from 10-15 different platforms (PagerDuty, GitHub, Jira, form builders), each with its own email format.
The script below defines a hashtable where each key is a sender address and each value is a script block that receives the email metadata and full message body. The router iterates over unread emails, matches the sender against the handler table using -like wildcards, and invokes the matching handler. Adding a new handler is a single hashtable entry with a param($email, $detail) signature.
# email-router.ps1 -- Route emails to processing handlers
$handlers = @{
'alerts@pagerduty.com' = {
param($email, $detail)
if ($detail.body -match 'Incident #(\d+)') {
Write-Host "PagerDuty incident #$($matches[1])" -ForegroundColor Red
}
}
'noreply@github.com' = {
param($email, $detail)
if ($email.subject -match '\[(.+?)\] (.+?) #(\d+)') {
Write-Host "GitHub: $($matches[1]) $($matches[2]) #$($matches[3])" -ForegroundColor Cyan
}
}
'forms@company.com' = {
param($email, $detail)
$fields = @{}
[regex]::Matches($detail.body, '(\w[\w\s]+?):\s*(.+?)(?:\n|$)') |
ForEach-Object { $fields[$_.Groups[1].Value.Trim()] = $_.Groups[2].Value.Trim() }
Write-Host "Form: $($fields.Count) fields" -ForegroundColor Yellow
$fields.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
}
}
$emails = nylas email list --unread --json --limit 30 | ConvertFrom-Json
foreach ($email in $emails) {
$handler = $handlers.GetEnumerator() |
Where-Object { $email.from -like "*$($_.Key)*" } | Select-Object -First 1
if ($handler) {
$detail = nylas email read $email.id --json | ConvertFrom-Json
& $handler.Value $email $detail
}
}Export parsed data to SQL Server
Exporting parsed email data to SQL Server transforms ephemeral inbox content into queryable, persistent records. The Invoke-Sqlcmd cmdlet, included in the SqlServer PowerShell module, executes parameterized queries directly from the pipeline. Using parameterized queries with -Variable prevents SQL injection -- a risk that OWASP ranks as the #3 web application vulnerability in its 2021 Top 10.
The script below searches for order confirmation emails, extracts order numbers and amounts with regex, and inserts each record into a SQL Server table. An IF NOT EXISTS guard prevents duplicate inserts when the script runs on overlapping email batches. Each insert uses parameterized variables (@OrderNum, @Amount, @EmailDate) rather than string interpolation, keeping the query safe from injection.
# email-to-sql.ps1 -- Parse emails and insert into database
$emails = nylas email search "order confirmation" --json --limit 50 |
ConvertFrom-Json
foreach ($email in $emails) {
$detail = nylas email read $email.id --json | ConvertFrom-Json
$orderNum = if ($detail.body -match 'order\s*#?\s*(\d{4,10})') { $matches[1] } else { continue }
$amount = if ($detail.body -match '\$([\d,]+\.\d{2})') { $matches[1] -replace ',' } else { "0" }
$query = "IF NOT EXISTS (SELECT 1 FROM Orders WHERE OrderNumber=@OrderNum) " +
"INSERT INTO Orders (OrderNumber,Amount,EmailDate) VALUES (@OrderNum,@Amount,@EmailDate)"
try {
Invoke-Sqlcmd -ServerInstance "db.company.local" -Database "OrderTracking" -Query $query -Variable "OrderNum=$orderNum","Amount=$amount","EmailDate=$($email.date)"
Write-Host "Inserted #$orderNum" -ForegroundColor Green
} catch {
Write-Host "Failed #$orderNum : $_" -ForegroundColor Red
}
}Troubleshooting
Three issues account for most email-parsing failures in PowerShell: HTML tags breaking regex matches, single-item JSON arrays being unwrapped by the pipeline, and server-side search indexing delays. According to Email on Acid, over 95% of marketing and transactional emails are sent as HTML, which means HTML-aware parsing is the default case, not the exception.
Regex returns no matches on HTML email bodies
HTML emails wrap text in tags like <span>Order #12345</span>. Your regex pattern Order #(\d+) still matches because -match searches the full string. But if the value is split across tags (Order <b>#12345</b>), strip HTML first: $body -replace '<[^>]+>', ''.
ConvertFrom-Json returns a single object instead of an array
When the CLI returns only one email, PowerShell unwraps the array into a single object. Wrap the result to force an array: @(nylas email list --json | ConvertFrom-Json). This ensures ForEach-Object and .Count work consistently.
Search returns zero results for emails you know exist
The CLI search uses the provider's server-side search. Gmail and Outlook index messages asynchronously — a message received 30 seconds ago may not appear in search yet. For very recent messages, use nylas email list --limit 5 --json instead of search.
Frequently asked questions
These questions cover the most common patterns developers ask about when parsing email in PowerShell: extracting structured data with regex, automating inbox processing with polling loops, and handling HTML message bodies. PowerShell's -match operator and $matches automatic variable handle 90% of email parsing tasks without installing additional modules.
How do I extract data from emails in PowerShell?
Use nylas email read <id> --json | ConvertFrom-Json to get the full email body. Then apply regex with -match to extract order numbers, tracking codes, amounts, or any structured data. PowerShell's $matches automatic variable captures the matched groups.
Can I process incoming emails automatically with PowerShell?
Yes. Build a polling loop that calls nylas email list --unread --json every 30-60 seconds. Track seen message IDs in a hashtable. Process new arrivals with your extraction logic. Schedule the script with Task Scheduler for persistent monitoring.
How do I parse HTML email bodies in PowerShell?
For simple extraction, use regex with -match on the raw HTML. For complex HTML, install HtmlAgilityPack from NuGet and query DOM elements. In most cases, regex is faster since you're extracting specific values, not navigating the full document tree.
Next steps
- Download email attachments in PowerShell -- filter by type, batch download, auto-save
- Generate and email reports -- CSV attachments, HTML tables, weekly digests
- Send email from PowerShell -- compose, reply, and send with attachments
- Full command reference -- every flag and subcommand
- RFC 5322 -- Internet Message Format — canonical header and body grammar parsers must respect
- RFC 2045 -- MIME Part One: Format of Message Bodies — multipart, encoding, and content-type rules
- Microsoft Learn: PSCustomObject deep dive — the structure most parsing pipelines emit