Repository navigation
Expand file tree
/
Copy pathGet-DirectSendReport.ps1
More file actions
189 lines (170 loc) · 6.11 KB
/
Copy pathGet-DirectSendReport.ps1
File metadata and controls
189 lines (170 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
<#
.SYNOPSIS
Direct Send (SMTP) report via Unified Audit Log, up to 30 days, with live per‐window updates.
.DESCRIPTION
- Splits the full range into N-hour windows (default 6h)
- Displays a live progress bar, status line, ETA, and per-window counts
- Automatically halves any window on failure down to 1h
- Robustly parses JSON and skips bad entries
- Exports CSV and sortable HTML
.PARAMETER DaysBack
Days back to report (1–30). Mutually exclusive with StartDate/EndDate.
.PARAMETER StartDate
Custom start date (≤ 30 days ago). Mutually exclusive with DaysBack.
.PARAMETER EndDate
Custom end date (≥ StartDate and ≤ now).
.PARAMETER WindowHours
Size of each retrieval window in hours (1–720). Default is 6.
.EXAMPLE
# Last 7 days in 6h windows
.\Get-DirectSendReport.ps1 -DaysBack 7
.EXAMPLE
# Last 30 days in 12h windows
.\Get-DirectSendReport.ps1 -DaysBack 30 -WindowHours 12
#>
param(
[Parameter(ParameterSetName='ByDays')][ValidateRange(1,30)][int]$DaysBack,
[Parameter(ParameterSetName='ByDate')][datetime]$StartDate,
[Parameter(ParameterSetName='ByDate')][datetime]$EndDate,
[ValidateRange(1,720)][int]$WindowHours = 6
)
# 1) Compute StartDate/EndDate
if ($PSCmdlet.ParameterSetName -eq 'ByDays') {
$EndDate = Get-Date
$StartDate = $EndDate.AddDays(-$DaysBack)
} elseif ($PSCmdlet.ParameterSetName -eq 'ByDate') {
if (-not ($StartDate -and $EndDate)) { throw "Both -StartDate and -EndDate are required." }
if ($EndDate -le $StartDate) { throw "EndDate must be later than StartDate." }
if (($EndDate - $StartDate).TotalDays -gt 30) { throw "Range cannot exceed 30 days." }
} else {
throw "Specify either -DaysBack or both -StartDate and -EndDate."
}
# 2) Build windows
$windows = New-Object System.Collections.Generic.List[psobject]
$cursor = $StartDate
while ($cursor -lt $EndDate) {
$next = $cursor.AddHours($WindowHours)
if ($next -gt $EndDate) { $next = $EndDate }
$windows.Add([pscustomobject]@{ Start=$cursor; End=$next })
$cursor = $next
}
$total = $windows.Count
# 3) Connect once
Import-Module ExchangeOnlineManagement -Force
Write-Host "`nConnecting to Exchange Online…" -ForegroundColor Cyan
Connect-ExchangeOnline -ShowBanner:$false
# 4) Recursive fetch helper
function FetchWindow($s,$e) {
try {
return Search-UnifiedAuditLog -StartDate $s -EndDate $e -Operations Send -ResultSize 5000 `
-ErrorAction Stop -WarningAction SilentlyContinue
} catch {
$dur = ($e - $s).TotalHours
if ($dur -le 1) {
Write-Warning "Window $s→$e failed: $($_.Exception.Message)"
return @()
}
$mid = [datetime](($s.Ticks + $e.Ticks)/2)
return (FetchWindow $s $mid) + (FetchWindow $mid $e)
}
}
# 5) Loop windows with live updates
Write-Host "`nRetrieving audit entries in $WindowHours-hour windows…" -ForegroundColor Green
$all = @(); $i = 0
$sw = [Diagnostics.Stopwatch]::StartNew()
foreach ($win in $windows) {
$i++
$pct = [math]::Round(($i/$total)*100)
$elapsed = $sw.Elapsed.TotalSeconds
$remaining = if ($i -gt 0) { ($elapsed/$i)*($total-$i) } else { 0 }
$eta = [TimeSpan]::FromSeconds($remaining)
Write-Progress `
-Activity "Overall Progress" `
-Status "Window $i of $total" `
-PercentComplete $pct `
-SecondsRemaining ([math]::Round($remaining))
Write-Host ("[{0}/{1}] Fetching {2:yyyy-MM-dd HH:mm} → {3:yyyy-MM-dd HH:mm} ETA: {4:hh\:mm\:ss}" `
-f $i,$total,$win.Start,$win.End,$eta)
$slice = FetchWindow $win.Start $win.End
Write-Host (" → Retrieved {0} entries" -f $slice.Count)
if ($slice) { $all += $slice }
}
$sw.Stop()
Write-Progress -Activity "Overall Progress" -Completed
Disconnect-ExchangeOnline -Confirm:$false
# 6) Parse & filter JSON
$bad = 0
$logs = foreach ($e in $all) {
if ($e.AuditData -match '"Protocol":"SMTP"' -and $e.AuditData -match '"ClientIP":') {
try {
$j = $e.AuditData | ConvertFrom-Json
[PSCustomObject]@{
Timestamp = $e.CreationDate
Sender = $j.Sender
Recipients = $j.Recipients -join ', '
Subject = $j.Subject
ClientIP = $j.ClientIP
Protocol = $j.Protocol
MessageID = $j.MessageId
Connector = $j.ConnectorId
AuthType = $j.AuthenticationType
MailboxGUID = $j.MailboxGuid
}
} catch {
$bad++
}
}
}
if ($bad) { Write-Warning "$bad entries skipped due to JSON errors." }
# 7) Export CSV
$ts = Get-Date -Format yyyyMMdd_HHmmss
$csv = "DirectSend_Report_$ts.csv"
$logs | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csv
Write-Host "`n✅ CSV: $csv" -ForegroundColor Yellow
# 8) Export HTML
$cols = 'Timestamp','Sender','Recipients','Subject','ClientIP','Protocol','AuthType','Connector','MessageID','MailboxGUID'
$htmlRows = $logs | ForEach-Object {
$cells = $cols | ForEach-Object { "<td>$($_.$_)</td>" }
"<tr>$($cells -join '')</tr>"
}
$tbody = $htmlRows -join "`n"
$thead = "<tr>" + ($cols | ForEach-Object { "<th onclick=`"sortTable('$_')`">$_</th>" }) -join "" + "</tr>"
$html = @"
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'><title>Direct Send Report</title>
<style>
body{font-family:Arial}
table{border-collapse:collapse;width:100%}
th,td{border:1px solid #ddd;padding:8px}
th{background:#4CAF50;color:#fff;cursor:pointer}
tr:nth-child(even){background:#f2f2f2}
</style>
<script>
function sortTable(col){
const tbl=document.getElementById('tbl'),
hdrs=tbl.tHead.rows[0].cells,
idx=Array.from(hdrs).findIndex(h=>h.innerText===col),
dir=tbl.getAttribute('dir')==='asc'?'desc':'asc';
tbl.setAttribute('dir',dir);
Array.from(tbl.tBodies[0].rows)
.sort((a,b)=>(dir==='asc'?1:-1)*a.cells[idx].innerText.localeCompare(b.cells[idx].innerText))
.forEach(r=>tbl.tBodies[0].appendChild(r));
}
</script>
</head>
<body>
<h2>Direct Send Report</h2>
<p><strong>Range:</strong> $StartDate → $EndDate</p>
<table id='tbl' dir='asc'>
<thead>$thead</thead>
<tbody>
$tbody
</tbody>
</table>
</body>
</html>
"@
$htmlFile = "DirectSend_Report_$ts.html"
$html | Set-Content -Encoding UTF8 -Path $htmlFile