Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Create-Shortcut.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<#
.Synopsis
Creates a Desktop shortcut for launching Exchange Recipient Admin Center.
.Description
Run this once from wherever this project's files actually live. It resolves
its own folder and the current user's Desktop dynamically, so the same
script works unmodified for any user/location - no hardcoded paths.
#>

$targetFolder = Split-Path -Parent $MyInvocation.MyCommand.Path
$batPath = Join-Path $targetFolder "Launch-ExchangeRecipientAdmin.bat"
$iconPath = Join-Path $targetFolder "images\favicon.ico"
$desktop = [Environment]::GetFolderPath('Desktop')
$shortcutPath = Join-Path $desktop "Exchange Recipient Admin Center.lnk"

$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($shortcutPath)
$shortcut.TargetPath = $batPath
$shortcut.WorkingDirectory = $targetFolder
if (Test-Path $iconPath) {
$shortcut.IconLocation = $iconPath
}
else {
$shortcut.IconLocation = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe,0"
}
$shortcut.Description = "Exchange Recipient Admin Center"
$shortcut.Save()

Write-Host "Shortcut created at: $shortcutPath"
2 changes: 2 additions & 0 deletions Launch-ExchangeRecipientAdmin.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Start-Process powershell.exe -Verb RunAs -ArgumentList '-ExecutionPolicy Bypass -File \"%~dp0Start-ExchangeRecipientAdminCenter.ps1\"'"
209 changes: 198 additions & 11 deletions Start-ExchangeRecipientAdminCenter.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,135 @@ $MIMEHASH = @{".avi" = "video/x-msvideo"; ".crt" = "application/x-x509-ca-cert";
$HTML_SUCCESS = "<div class=`"alert alert-success d-flex align-items-center`" role=`"alert`">{result}</div>"
$HTML_WARN = "<div class=`"alert alert-warning d-flex align-items-center`" role=`"alert`">{result}</div>"

function Get-RemoteMailboxEditPage {
# Renders editremotemailbox.html for a given mailbox identity. Shared by the
# GET (initial view) and POST (after an update) handlers below.
param(
[Parameter(Mandatory)][string]$Identity,
[string]$ResultHtml = ""
)

$Mailbox = Get-RemoteMailbox -Identity $Identity -ErrorAction SilentlyContinue

if (-not $Mailbox) {
return "<!doctype html><html><body>Remote mailbox '$($Identity)' not found</body></html>"
}

# Accepted domain list, used for the "add alias" domain picker
$HTMLROWS_AD = ""
foreach ($Item in (Get-AcceptedDomain)) {
$HTMLROWS_AD += "`n<option value=`"$($Item.Name)`">$($Item.DomainName)</option>"
}

# Proxy address (EmailAddresses) rows, each with a remove button
$HTMLROWS_PROXY = ""
foreach ($Addr in $Mailbox.EmailAddresses) {
$AddrString = $Addr.ToString()
$IsPrimary = $AddrString.StartsWith("SMTP:")
if ($IsPrimary) {
$Badge = "<span class=`"badge text-bg-primary`">Primary</span>"
$RemoveBtn = ""
}
else {
$Badge = "<span class=`"badge text-bg-secondary`">Alias</span>"
$RemoveBtn = "
<form method=`"post`" action=`"/editremotemailbox`" onsubmit=`"return confirm('Remove $($AddrString)?')`">
<input type=`"hidden`" name=`"id`" value=`"$($Mailbox.PrimarySmtpAddress)`">
<input type=`"hidden`" name=`"Action`" value=`"removealias`">
<input type=`"hidden`" name=`"alias`" value=`"$($AddrString)`">
<button type=`"submit`" class=`"btn btn-sm btn-outline-danger`">Remove</button>
</form>"
}
$HTMLROWS_PROXY += "
<tr>
<td>$($AddrString)</td>
<td>$($Badge)</td>
<td>$($RemoveBtn)</td>
</tr>";
}

if ($Mailbox.HiddenFromAddressListsEnabled) {
$HiddenBadgeClass = "text-bg-warning"
$HiddenStatusText = "Hidden"
$HiddenToggleValue = "false"
$HiddenToggleLabel = "Unhide from GAL"
}
else {
$HiddenBadgeClass = "text-bg-success"
$HiddenStatusText = "Visible"
$HiddenToggleValue = "true"
$HiddenToggleLabel = "Hide from GAL"
}

$HTMLRESPONSE = Get-Content -Path "$($BASEDIR)\editremotemailbox.html"
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{DisplayName}", $Mailbox.DisplayName)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{PrimarySmtpAddress}", $Mailbox.PrimarySmtpAddress)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{Alias}", $Mailbox.Alias)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{RemoteRoutingAddress}", $Mailbox.RemoteRoutingAddress)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{id}", $Mailbox.PrimarySmtpAddress)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{hidden_badge_class}", $HiddenBadgeClass)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{hidden_status_text}", $HiddenStatusText)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{hidden_toggle_value}", $HiddenToggleValue)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{hidden_toggle_label}", $HiddenToggleLabel)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("<!-- {row_proxy} -->", $HTMLROWS_PROXY)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("<!-- {row_ad} -->", $HTMLROWS_AD)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("<!-- {result} -->", $ResultHtml)
return $HTMLRESPONSE
}

function Get-RemoteMailboxListPage {
# Renders remotemailboxes.html. Used after disabling a Remote Mailbox, since
# the mailbox no longer exists to redirect back to an edit page for.
param(
[string]$ResultHtml = ""
)

$HTMLROWS_USERS = ""
foreach ($Item in (Get-User -Filter "RecipientType -eq 'User' -and RecipientTypeDetails -ne 'DisabledUser'" | Where { $_.UserPrincipalName })) {
$HTMLROWS_USERS += "`n<option value=`"$($Item.UserPrincipalName)`">$($Item.UserPrincipalName)</option>"
}

$HTMLROWS_AD = ""
foreach ($Item in (Get-AcceptedDomain)) {
if ($Item.Default) {
$HTMLROWS_AD += "`n<option selected value=`"$($Item.Name)`">$($Item.DomainName)</option>"
}
else {
$HTMLROWS_AD += "`n<option value=`"$($Item.Name)`">$($Item.DomainName)</option>"
}
}

$HTMLROWS_RRA = ""
foreach ($Item in (Get-AcceptedDomain)) {
if ($Item.DomainName -like "*.mail.onmicrosoft.com") {
$HTMLROWS_RRA += "`n<option selected value=`"$($Item.Name)`">$($Item.DomainName)</option>"
}
else {
$HTMLROWS_RRA += "`n<option value=`"$($Item.Name)`">$($Item.DomainName)</option>"
}
}

$HTMLROWS_MBX = ""
foreach ($Item in (Get-RemoteMailbox | Select DisplayName, PrimarySMTPAddress, RecipientTypeDetails, WhenChanged)) {
$HTMLROWS_MBX += "
<tr>
<th scope=`"row`">
<a href=`"/editremotemailbox?id=$($Item.PrimarySMTPAddress)`">$($Item.DisplayName)</a></th>
<td>$($Item.PrimarySMTPAddress)</td>
<td>$($Item.RecipientTypeDetails)</td>
<td>$($Item.WhenChanged)</td>
</tr>";
}

$HTMLRESPONSE = Get-Content -Path "$($BASEDIR)\remotemailboxes.html"
$HTMLRESPONSE = $HTMLRESPONSE.Replace("<!-- {row_mbx} -->", $HTMLROWS_MBX)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("<!-- {row_ad} -->", $HTMLROWS_AD)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("<!-- {row_user} -->", $HTMLROWS_USERS)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("<!-- {row_rra} -->", $HTMLROWS_RRA)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("<!-- {result} -->", $ResultHtml)
return $HTMLRESPONSE
}

# Starting the powershell webserver
"$(Get-Date -Format s) Starting Exchange Recipient Admin Webserver at: $($BINDING)"
$LISTENER = New-Object System.Net.HttpListener
Expand All @@ -48,6 +177,7 @@ try {
"$(Get-Date -Format s) Powershell webserver started."
$WEBLOG = "$(Get-Date -Format s) Powershell webserver started.`n"
while ($LISTENER.IsListening) {
try {
# analyze incoming request
$CONTEXT = $LISTENER.GetContext()
$REQUEST = $CONTEXT.Request
Expand Down Expand Up @@ -137,14 +267,9 @@ try {

"GET /editremotemailbox" {
# Edit Remote Mailbox Section
$id = $REQUEST.Url.Query.Split("=")[1]
$remotemailbox = Get-RemoteMailbox -Identity $id

$HTMLRESPONSE = (Get-Content -Path "$($BASEDIR)\editremotemailbox.html")
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{DisplayName}", $remotemailbox.DisplayName)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{PrimarySmtpAddress}", $remotemailbox.PrimarySmtpAddress)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{Alias}", $remotemailbox.Alias)
$HTMLRESPONSE = $HTMLRESPONSE.Replace("{RemoteRoutingAddress}", $remotemailbox.RemoteRoutingAddress)
$id = [URI]::UnescapeDataString($REQUEST.Url.Query.TrimStart('?').Split("=")[1])

$HTMLRESPONSE = Get-RemoteMailboxEditPage -Identity $id
break
}

Expand All @@ -161,15 +286,68 @@ try {
$params[$key] = [System.Web.HttpUtility]::UrlDecode($value)
}

# "id" is set by the alias/hidden/disable mini-forms; the main
# properties form has no id field and keys off PrimarySmtpAddress.
$Identity = if ($params.ContainsKey('id')) { $params['id'] } else { $params['PrimarySmtpAddress'] }
$Disabled = $false

try {
Set-RemoteMailbox -Identity $params['PrimarySmtpAddress'] -DisplayName $params['DisplayName'] -Alias $params['Alias'] -RemoteRoutingAddress $params['RemoteRoutingAddress']
$HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Remote Mailbox updated successfully")
switch ($params['Action']) {
"addalias" {
$NewAlias = "$($params['alias_local'])@$($params['alias_accepteddomain'])"
Set-RemoteMailbox -Identity $Identity -EmailAddresses @{Add = $NewAlias }
$HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Added alias $NewAlias")
break
}
"removealias" {
Set-RemoteMailbox -Identity $Identity -EmailAddresses @{Remove = $params['alias'] }
$HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Removed alias $($params['alias'])")
break
}
"togglehidden" {
$NewHidden = $params['hidden'] -eq 'true'
Set-RemoteMailbox -Identity $Identity -HiddenFromAddressListsEnabled $NewHidden
$HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Set 'Hidden from address lists' to $NewHidden")
break
}
"disable" {
# Require the confirmation text to match the mailbox's actual
# PrimarySmtpAddress - checked server-side too, not just via the
# UI's type-to-confirm JS, since that's trivially bypassed.
$MailboxToDisable = Get-RemoteMailbox -Identity $Identity -ErrorAction SilentlyContinue
if (-not $MailboxToDisable) {
$HTML_RESULT = $HTML_WARN.Replace("{result}", "Remote mailbox '$($Identity)' not found")
}
elseif ($params['confirmAddress'] -ne $MailboxToDisable.PrimarySmtpAddress) {
$HTML_RESULT = $HTML_WARN.Replace("{result}", "Confirmation text didn't match $($MailboxToDisable.PrimarySmtpAddress) - no changes were made")
}
else {
Disable-RemoteMailbox -Identity $Identity -Confirm:$false
$HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Remote Mailbox for $($MailboxToDisable.PrimarySmtpAddress) has been disabled")
$Disabled = $true
}
break
}
default {
Set-RemoteMailbox -Identity $Identity -DisplayName $params['DisplayName'] -Alias $params['Alias'] -RemoteRoutingAddress $params['RemoteRoutingAddress']
$HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Remote Mailbox updated successfully")
# DisplayName/Alias changes don't affect PrimarySmtpAddress, so it still identifies the mailbox below
$Identity = $params['PrimarySmtpAddress']
}
}
}
catch {
$HTML_RESULT = $HTML_WARN.Replace("{result}", $Error -join "<br />")
}

$HTMLRESPONSE = (Get-Content -Path "$($BASEDIR)\remotemailboxes.html").Replace("<!-- {result} -->", $HTML_RESULT)
# A disabled mailbox no longer exists to render an edit page for -
# send the user back to the list instead.
if ($Disabled) {
$HTMLRESPONSE = Get-RemoteMailboxListPage -ResultHtml $HTML_RESULT
}
else {
$HTMLRESPONSE = Get-RemoteMailboxEditPage -Identity $Identity -ResultHtml $HTML_RESULT
}
break
}

Expand Down Expand Up @@ -515,6 +693,15 @@ try {
"$(Get-Date -Format s) Stopping powershell webserver..."
break;
}
}
catch {
# A single request failing (e.g. client closed the connection before the
# response finished writing - "network name is no longer available") should
# not take down the whole webserver. Log it and keep listening.
"$(Get-Date -Format s) Request error: $($_.Exception.Message)"
$WEBLOG += "$(Get-Date -Format s) Request error: $($_.Exception.Message)`n"
try { $RESPONSE.Close() } catch { }
}
}
}
finally {
Expand Down
1 change: 1 addition & 0 deletions web/accepteddomains.html
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ <h5 class="modal-title" id="addAcceptedDomainLabel">Add Accepted
</div>
</div>

<script src="table-tools.js"></script>
<script>
feather.replace()
</script>
Expand Down
1 change: 1 addition & 0 deletions web/contacts.html
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ <h5 class="modal-title" id="addContactLabel">Add New Contact</h5>
</div>
</div>

<script src="table-tools.js"></script>
<script>
feather.replace()
</script>
Expand Down
1 change: 1 addition & 0 deletions web/distributiongroups.html
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ <h5 class="modal-title" id="mailEnableGroupLabel">Mail-Enable a
</div>
</div>

<script src="table-tools.js"></script>
<script>
feather.replace()
</script>
Expand Down
Loading