Managed AutoResponder Notifications

A long overdue blog on a solution which was created per request of an Exchange fellow. The original scenario is an organization spinning off, thereby changing their primary e-mail domain. They wanted to inform relations and partners using the old e-mail addresses of this change. However, this solution might also be helpful to organizations after merger and acquisitions or rebranding.

For the sake of the example, let us suppose Fabrikam was acquired by Contoso. Targeted mailboxes are migrated from the Fabrikam tenant to the Contoso tenant. Fabrikam will contain Mail-Enabled Users forwarding messages to their Contoso mailbox counterparts. The migrated mailboxes now hosted in Contoso will be configured to send notifications to senders which sent mail to the old Fabrikam address.

While organizations can resort to 3rd party tools to set up an Exchange Transport Rule with a generic message, a little script might also do the job, offering a more granular and controlled solution.

Solution
The scripted solution is available on GitHub here. It will configure an Inbox rule on the targeted Exchange mailbox(es), which would be the new/migrated mailbox. The rule will trigger when messages land in the inbox which were sent to a specific e-mail address, i.e. the previous e-mail address. It will send out an automatic response, which can consist of a custom subject, message body with an optional embedded image. The configuration of the response is defined in an customizable external XML file:

<?xml version="1.0" encoding="ISO-8859-1"?>  
<config>
  <rule>Contoso Autoresponder</rule>
  <subject>Please update your email recipient to Contoso</subject>
  <body>Dear Sender,

Thank you for your message. Fabrikam is now a whole Contoso subsidiary, and the fabrikam.com e-mail address will change to contoso.com. Your e-mail was forwarded to my new e-mail address. 

Please update contact information, distribution lists, etc. to update [OldMail] e-mail references with my new [Identity] e-mail address. 
 
[logo] 
The Contoso Corporation is a multinational business with its headquarters in Paris.</body>
  <logo>Contoso.png</logo>
</config>

The elements that can be used in the config are:

  • <rule> defines the name of the Inbox rule to be created. When the script runs, it will update any existing previously created inbox rules of the same name.
  • <subject> defines the subject of the response.
  • <body> defines the message body. You can use the following macros here:
    • [OldMail] will get replaced with the original e-mail address.
    • [Identity] will get replaced with the new e-mail address.
    • [Logo] will get replaced with the embedded image.
  • Optionally, <logo> refers to the file name of the image to embed.

Requirements
To run the script, you need the following:

  • Exchange Server 2013 SP1 or later, or Exchange Online.
  • Exchange Web Services (EWS) Managed API 2.21 or later (how to, NuGet package exchange.webservices.managed.api).
  • When using modern authentication or OAuth2, the MSAL library is required (NuGet package Microsoft.Identity.Client). Also, you need to have registered an App in Azure Active Directory with sufficient permissions (e.g. full_access_as_app). After registering the app, Tenant ID, Application ID and certificate or secret is what you need to use with the script to run successfully.
  • In addition to installing the NuGet packages, you can also store their DLLs in the same folder as the script for portability.

Usage
The syntax to run the script is as follows:

.\Set-AutoResponderNotification.ps1 -Identity <Identity> -TemplateFile <File> [-TenantId <TenantId> -ClientId <ClientId> [-CertificateThumbprint <ThumbPrint>] [-CertificateFile <File> -CertificatePassword <SecureString>] [-Secret <SecureString>]] [-Credentials <PSCredential>] [-Clear] [-Overwrite] [-Impersonation] [-TrustAll]

The available parameters and switches are as follows:

  • Identity specifies one or more e-mail addresses of mailboxes to process. Identity supports the pipeline (see examples).
  • OldMail specifies one or more old e-mail addresses to use when configuring the autoresponder message. When specifying multiple identities,
    the number of OldMail entries need to match the number of identities.
  • Server specifies the Exchange Web Services endpoint, for example outlook.office365.com for Exchange Online. When omitted, Autodiscover will be used.
  • Impersonation to use impersonation when accessing the mailbox. When using modern authentication, impersonation is mandatory.
  • TrustAll to accept all certificates including self-signed certificates.
  • TenantId specifies the ID of the Tenant when using a mailbox hosted in Exchange Online.
  • ClientId to specify the Application ID of the registered application in Azure Active Directory.
  • Credentials to specify the Basic Authentication credentials for on-premises usage or against Exchange Online when modern authentication is not an option.
  • CertificateThumbprint is the thumbprint of the certificate to use for modern authentication. The certificate with the public key needs to stored with the registered application for authentication. The certificate with the private key should be present in the local certificate store.
  • CertificateFile and CertificatePassword can be used to specify a certificate file to use. The file should contain the private key; the password protecting the certificate file can be specified using CertificatePassword as a secure string.
  • Secret can be used to specify the secret to authenticate using the registered application. The secret needs to be provided as a secure string.
  • Template
    specifies the XML template file to use when configuring or clearing the autoresponder inbox rule. The format has explained above.
  • Clear specifies if any you want to remove the inbox rules with name specified in the template. Use this when you want to remove autoresponder rules from mailboxes. When using Clear, you don’t need to specify OldMail.
  • Overwrite specifies if any existing inbox rules with name specified in the template should be overwritten. When omitted, the script will skip processing
    mailboxes with inbox rules with conflicting names. Use this when you want to configure the autoresponder only on mailboxes which do not have the rule.

Note that usage of the Verbose, Confirm and WhatIf parameters are supported.

Examples
Nothing more explanatory than an example. When we want to configure the autoresponder on a single mailbox, we can use something like:

image

Here, the autoresponder will get configured on the specified mailbox, triggering when mail has been sent to michel@fabrikam.com using the configuration defined in template.xml. Modern authentication will be used for authentication, using variables for Tenant, Client and in this case secret.

When you want to configure autoresponder for multiple mailboxes, you can for example use a CSV file. It needs to contain two elements which will be passed through pipeline, Identity and OldMail:

Identity,OldMail
philip@contoso.com,p.mortimer@fabrikam.com
francis@contoso.com,f.blake@fabrikam.com

After defining the response in a file template.xml, we can use this CSV to configure inbox rules on the Contoso mailboxes identified by Identity, triggering when mail is sent to their OldMail addresses:

Import-CSV -Path Users.csv | .\Set-AutoResponderNotification.ps1 -Server outlook.office365.com -Impersonation -TemplateFile .\Template.xml -TenantId <TenantId> -ClientId <ClientId> -Overwrite -CertificateThumbprint <Thumbprint>

What will happen is that for every set of Identity and OldMail, the mailbox specified by Identity will be configured with an inbox rule. When an existing rule is found, which is determined using the <rule> element in the XML template, it will get overwritten. The specified certificate will be picked from the local certificate store to authenticate against Tenant with TenantId, as application specified by ClientId.

Note that when the user opens Manage Rules & Alerts in Outlook, the configured inbox rule will be visible. This allows the user to remove it when it is no longer required. After a certain period, administrators can centrally remove these rules as well running the script using the Clear switch.

image

And finally, an example of how this may looks to senders when they receive an autoresponse message, using the sample configuration from the beginning of this article.

image

Application Access Policy
When using the script with modern authentication, you can leverage features such as conditional access to set boundaries for script usage. Also, you can configure application access policies to scope the registered app (script) to a subset of mailboxes. To accomplish this, assign an ApplicationAccessPolicy to the app.

To be more convenient in managing permissions, you can define a distribution group and assign the Application Access Policy with group scope (PolicyScopeGroupId). You then only need to add/remove members as you need to configure mailboxes.

More information about Application Access Policies here. Note that the permissions mentioned to not include full_access_as_app as permission, but it works.

EWS, why not Graph?
Microsoft already announced back in 2018 that development on Exchange Web Services will halt in and focus will shift to Graph. As part of this move, a more recent statement announced deprecation of some least-used API per March 2022, stimulating organizations to switch to using Graph. However, the organization looking for a solution wished for an automatic response with HTML as well as an embedded logo. Because of this, I had to use a template as a reply action.

But where Exchange Web Services supports reply with a template, Graph does not offer this functionality. So, until there is more feature parity between Exchange Web Services and Graph, or EWS goes completely out of service, solutions may still be forced to have a look at EWS for certain tasks.

Script Updates

powershellA small heads-up for those not following me on Twitter of one of the other social media channels. Last week I made updates to the following three scripts:

Install-Exchange2013.ps1, version 1.72

  • Added CU5 support
  • Added KB2971467 (CU5 Disable Shared Cache Service Managed Availability probes)

Remove-DuplicateItems.ps1, version 1.3

  • Changed parameter Mailbox, you can now use an e-mail address as well.
  • Added parameter Credentials.
  • Added item class and size for certain duplication checks.
  • Changed item removal process
  • Remove items after, not while processing folder. Avoids asynchronous deletion issues.
  • Works against Office 365.

Remove-MessageClassItems.ps1, version 1.3

  • Changed parameter Mailbox, you can now use an e-mail address as well
  • Added parameter Credentials
  • Added parameter PartialMatching for partial class name matching.
  • Changed item removal process. Remove items after, not while processing folder. Avoids asynchronous deletion issues.
  • Works against Office 365.
  • Deleted Items folder will be processed, unless MoveToDeletedItems is used.
  • Changed EWS DLL loading, can now be in current folder as well.

Be advised I keep am overview of the scripts and their current versions with publish dates here.

 

Jetstress 2013

Ex2013 LogoIn the list of expected products to accompany Exchange 2013, Microsoft today released JetStress 2013, version 15.0.651.0.

Jetstress is your tool of choice to check the performance and stability of your storage design under load. It simulates Exchange I/O behaviour using Exchange 2013 patterns allowing you to verify dimensioning and validate performance expectations from a database perspective.

Capture

To run JetStress 2013 you need:

  • .NET Framework 4.5
  • ESE.DLL, ESEPerf.dll, ESEPerf.ini, ESEPerf.hxx (copy these from an Exchange 2013 installation source)

Note: The installer currently installs a shortcut named “Exchange JetStress 2010”, but it really is JetStress 2013.

You can download the Jetstress 2013 here.

NGN Exchange Event, Tips & Tricks Presentation

On October 31st, the NGN – a Dutch society for IT professionals – held its 3rd Exchange themed event, this time at The Reehorst in Ede (NL). Because of the recently released Exchange 2013 and all the news and related questions, we planned for a whole day of sessions and it was nice to see the turn up was nearly 100 IT professionals.

Since all people would still be on pre-2013 versions of Exchange, I figured a presentation using real-world Exchange 2010 Tips and Tricks might be more appropriate. I was glad a quick poll amongst the attendees showed a significant increase in Exchange 2010 deployments (around 80%) when compared to last year’s event, but as expected there’s still some Exchange 2007 and few Exchange 2003 out there.

I decided to stick with two deep-dive topics, which were Message Trackings Logs and Cmdlet Extension Agents. On those topics I went from basics to more advanced examples, hoping it would ignite people with no experience and people with experience could still pick up a thing or two.I’m still waiting for evaluation results, the only way to get feedback from these sessions apart from the occasional e-mail or tweet.

(picture by Dave Stork)

You can find my presentation here (partially Dutch) and the accompanying sample script on Message Tracking Logs here and the one on Cmdlet Extension Agents here (script); the ScriptingAgent.xml file can be downloaded here.

As always, these events are also a time to catch up with fellow Exchange people and discuss topics with attendees during the breaks. There were even Exchange fellows present who didn’t have a session, like Johan Veldhuis (MVP) and Maarten Piederiet (MCM); they did join in on the Q&A Panel.

The sessions and speakers were:

  • Introduction (Jaap Wesselius, MVP)
  • Building with Exchange 2013: Architecture (Dave Stork)
  • Exchange and Virtualisation (Jetze Mellema)
  • Exchange 2010 Tips & Tricks (Ashley Flentge, MCM & Michel de Rooij)
  • Exchange 2013 Coexistence and Migrations (Kay Sellenrode, MCM and MCA)
  • Exchange and Load Balancing (Jetze Mellema)
  • Q&A Panel

The NGN published all presentations in a single ZIP file which can be downloaded here. Unfortunately, NGN didn’t record the sessions so I can‚Äôt share those with you. They did record the Q&A Panel session; you can view it here (in Dutch):


PS: When you see references to “exchangedag”, like in the Twitter hashtag, you need to know “dag” means day in Dutch; it’s no form of professional deformation.

Adding Exchange Shell items to PowerShell ISE

I’ve become a fan of using the PowerShell Integrated Scripting Environment (PowerShell ISE) for creating, testing and debugging scripts, using breakpoints and step-by-step execution; features found in many development environments. Depending on the script I’m working on and for what customer or environment, I may need to add snap-ins or switch contexts, like connecting to Exchange Online.

One of the powerful features of ISE is that it allows customizing through the ISE object model. For example, you can explore ISE through the $psise object:

image

To add custom menu options to ISE, we‚Äôre going to add items to the submenu of $psISE.CurrentPowerShellTab.AddOnsMenu, which is ‚ÄúAdd-ons‚ÄĚ. An item needs to consist of:

  • Display Name, which is used for displaying the menu item;
  • Action, which can be a PowerShell cmdlet, scriptblock or function;
  • Optionally, you can assign a keyboard shortcut to the menu option.

To automatically load the custom entries after starting up ISE, we’re going to define the entries in our default ISE profile file, Microsoft.PowerShellISE_profile.ps1, which location is stored in $profile. The file doesn’t exist by default, so when required you can simply create the file in the proper location using notepad $profile.

In our example, we’ll add three entries:

  • Implicit Remoting to connect to Exchange using a static FQDN;
  • Loading the Exchange 2010 Snap-in and connecting to Exchange using Autodiscover (unsupported, will bypass RBAC);
  • Connecting to Exchange Online.

Note that the example won’t be using stored credentials and will let ISE prompt the user for credentials when required, which is perfectly fine if you need to access different Office 365 tenants for example.

Now, in the Microsoft.PowerShellISE_profile.ps1 file, add the following contents:

$psISE.CurrentPowerShellTab.AddOnsMenu.SubMenus.Add(
    "Connect to Exchange @ Contoso", {
        $ExSession= New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://exserver.contoso.com/PowerShell/ -Authentication Kerberos
        Import-PSSession $ExSession
    },
    "Control+Alt+1"
)
$psISE.CurrentPowerShellTab.AddOnsMenu.SubMenus.Add(
    "Connect to Exchange On-Premise", {
        Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010
        . $env:ExchangeInstallPath\bin\RemoteExchange.ps1
¬†¬†¬†¬†¬†¬†¬† Connect-ExchangeServer ‚Äďauto
            },
    "Control+Alt+2"
)
$psISE.CurrentPowerShellTab.AddOnsMenu.SubMenus.Add(
    "Connect to Exchange Online", {
        $o365Cred= Get-Credential
        $o365Session= New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $o365Cred -Authentication Basic -AllowRedirection
        Import-PSSession $o365Session
    },
    "Control+Alt+3"
)

After starting ISE you’ll see the Add-ons menu now contains three extra items:

image

When selecting ‚ÄúConnect to Exchange Online‚ÄĚ (or pressing the configured keyboard shortcut), ISE will execute the associated code block; the progress is displayed in the output window. You will be prompted for credentials after which ISE will connect to Exchange Online and import the remote session.

image

After the session has been imported, you have the additional commands at your disposal in ISE and you can work on your scripts since they’ll be running in the context of the environment you’ve connected to.

Of course, this is just an example of what you can customize in ISE (pink background anyone?). For more information on customizing PowerShell ISE check here. If you’re new to PowerShell ISE, check here.

Retrieving DCs functional capabilities

While constructing a page for the forest and domain functional levels, and the maximum functional level for domain controllers, I wanted to show an example of how to retrieve this Active Directory attribute for all domain controllers. You could for example incorporate this in your automated procedures to check for any Windows 2003 servers and take further actions when needed.

The script is below. It outputs object so you use the pipe for further processing. For information on the possible values for msDS-Behavior-Version check out the new AD Functional Levels page here.

#--------------------------------------------------------------------------------
# Name         : Get-DCMSDSBehaviorVersion.ps1
# Created By   : Michel de Rooij
# E-mail       : michel@eightwone.com
# Date         : 20120307
# Source       : http://eightwone.com
# Version      : 1.0
#
# THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK
# OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER.
#--------------------------------------------------------------------------------

$RootDSE= [ADSI]'LDAP://RootDSE'
$objSearchRoot= New-Object DirectoryServices.DirectoryEntry( "LDAP://"+ $RootDSE.configurationNamingContext)
$objSearch= New-Object DirectoryServices.DirectorySearcher
$objSearch.SearchRoot= $objSearchRoot
$objSearch.Filter= "(objectClass=nTDSDSA)"
$objSearch.SearchScope = "SubTree"
$Results= @()
$objSearch.FindAll() | ForEach {
    $objItem = $_.getDirectoryEntry()
    $obj= New-Object PSObject -Property @{
        Servername= $objParent = $objItem.psbase.parent.DNSHostName.ToString()
        MSDSBehaviorVersion= $objItem.Get("MSDS-Behavior-Version")
    }
    $Results+= $obj
}
$Results

Note that if you have over 1000 domain controllers, you need to set the $objSearch.Pagesize, e.g. 1000. It may also encounter problems in forests with multiple roots.

Exchange PST Capture Tool released

It took a while, but today the Exchange Team released the long awaited Microsoft Exchange PST Capture Tool (initial version 14.3.16.4). The tool can be used to discover and inject PST files in an Exchange 2010 Exchange Online mailbox or archive.

The tool was originally from Red Gate and known as PST Importer. It’s architecture consists of three components: the central service, (optional) agents for PST discovery, registration and collecting PST files and an administrative console (image by Red Gate):

The online documentation can be found here.

Note that although it’s only supported for Exchange 2010 and Exchange Online, you can use it with Exchange 2007; it’s only untested (and probably unsupported) with that product.

You can read the official announcement here; you can download the tool and the agents here.

Exchange Environment Report

A quick post on Exchange fellow Steve Goodman who created a nice PowerShell script which generates a basic HTML report on your Exchange environment. When required, you can also e-mail the report, which is nice if you want to schedule the script to run on a daily basis for example.

The script is provided as-is so you can tailor it to your needs. It’s still work in progress, so if you got any requests just send Steve a message.

You can find the post and script here.

Bulk configuring & enabling OCS users

Not Exchange related, but something I’d like to share with you is a script to bulk configure and enable users for OCS with enterprise voice. In the process, the telephone numbers are also changed, since often the customer is moving to a new range of numbers as well.

The information was provided by the customer in an Excel sheet, which I exported to a CSV file. Since the script was to be run on an Windows Server 2003 box, I opted for a simple VB script, which you can find below.

A short explanation:

  • When the telephone number has a value, the user is configured for enterprise voice (intOptionFlags 896). If the telephone number is empty, the user is configured for IM and presence (intOptionFlags 256);
  • You can expand the sheet (CSV) with extra columns. When you need to create colums before the current ones, don’t forget to modify the index of the arrFields() references accordingly;
  • Change the OCSHomeServer to the proper pool value;
  • Change the OCSLocationProfile to the proper value;
  • Change the OCSPhoneContext to the proper value;
  • If you want to see what it will do first, set TestMode to True;
  • Use it in a lab environment first; test, test, test!

Note: If you have problems finding out the value of the OCSHomeServer, OCSLocationProfile or OCSPhoneContext settings, configure one user with the proper settings using ADUC, and inspect the values of those settings by using ADSIEdit or LDP.

users.csv

name;samaccountname;telephonenumber
Francis Blake;francis.blake;+31 (0) 30 123 45 11
Philip Mortimer;philip.mortimer;+31 (0) 30 123 45 22

OCSEnableUsers.vbs

'*--------------------------------------------------------------------------------
'* Name         : OCSEnableUsers
'* Created By   : Michel de Rooij
'* E-mail       : michel@eightwone.com
'* Date         : 20101118
'* Version      : 0.1
'*--------------------------------------------------------------------------------
'* Changes:
'* 0.1 Initial version
'*--------------------------------------------------------------------------------

On Error Resume Next

dim oConn, strQry, rs, objUser, strVal, objFSO, objFile, strLine, arrFields, i, line
dim strSAM, strTel
dim strServerURI, strLineURI, strSIP, strExtension, intOptionFlags

Const ADS_PROPERTY_APPEND = 3

Const OCSHomeServer     = "CN=LC Services,CN=Microsoft,CN=OCSPOOL1,CN=Pools,CN=RTC Service,CN=Services,CN=Configuration,DC=contoso,DC=com"
Const OCSLocationProfile= "CN={820ADF85-B64C-4F32-92F0-E4AA37267677},CN=Location Profiles,CN=RTC Service,CN=Services,CN=Configuration,DC=contoso,DC=com"
Const OCSPhoneContext   = "L0CDP.L1UDP@OCS2.contoso.com"

Const TestMode        = False

set oConn= createObject("Adodb.Connection")
oConn.provider = "AdsDSOObject"
oConn.open "ADs Provider"

set objFSO= createObject("Scripting.FileSystemObject")
set objFile= objFSO.OpenTextFile("users.csv", 1, True)

wscript.echo "RUNNING IN TESTMODE IS "& TestMode

line= 1
while not objFile.AtEndOfStream
 strLine= trim(objFile.readline)
 if Line> 1 Then
 arrFields= split(strLine, ";")
 strSAM= arrFields(1)
 strTel= normalizePhone( arrFields(2))
 strTelNew= replace( replace( arrFields(3), "(0)", ""), "  ", " ")

 wscript.echo strSAM&" "& strTel
 strQry= "<LDAP://dc=contoso,dc=com>;(samAccountName="& strSAM& ");adspath;subtree"
 set rs= oConn.execute( strQry)
 if rs.recordCount > 0 Then
 while not rs.EOF
 set objUser= getObject( rs.fields(0).value)
 wscript.echo "User found: "& objUser.distinguishedName
 wscript.echo "Previous Phone No: "& objUser.TelephoneNumber

 strSIP= "sip:"& objUser.mail
 strExtension= right( strTel, 3)
 strLineURI= "tel:"& strTel& ";ext="& strExtension
 strServerURI= "sip:"& strExtension& ";phone-context="& OCSPhoneContext

 If strTel= "" Then
 intOptionFlags= 256
 Else
 setAttr objUser, "msRTCSIP-Line", strLineURI
 setAttr objUser, "msRTCSIP-LineServer", strServerURI
 intOptionFlags= 896
 End If

' Set AD fields
 setAttr objUser, "telephoneNumber", strTelNew

 ' Set OCS props
 setAttr objUser, "msRTCSIP-UserEnabled", True
 setAttr objUser, "msRTCSIP-PrimaryHomeServer", OCSHomeServer
 setAttr objUser, "msRTCSIP-PrimaryUserAddress", strSIP
 setAttr objUser, "msRTCSIP-UserLocationProfile", OCSLocationProfile
 setAttr objUser, "msRTCSIP-OptionFlags", intOptionFlags

 addAttr objUser, "proxyAddresses", strSIP

 If Not TestMode Then
 objUser.SetInfo
 End If

 rs.moveNext

 wend
 Else
 wscript.echo "*** WARN: User not found in AD: "& strSAM
 End If

 Else
 ' Skip header
 End If
 line= line+ 1
wend 

objFile.close
set objFSO= Nothing

Function setAttr( objUser, strAttr, strVal)
 wscript.echo "Setting "& strAttr& " to "& strVal
 If TestMode Then
 ' ...
 Else
 objUser.put strAttr, strVal
 End If
End Function

Function addAttr( objUser, strAttr, strVal)
 wscript.echo "Adding "& strVal& " to "& strAttr
 If TestMode Then
 ' ...
 Else
 objUser.PutEx ADS_PROPERTY_APPEND, strAttr, array(strVal)
 End If
End Function

Function NormalizePhone( Tel)
 NormalizePhone= replace( replace( tel, " ", ""), "(0)", "")
End Function

JetStress updated

A quick note to inform you the the JetStress tool has been updated to version 14.1.225.17.

The online documentation on TechNet is currently being revised (article 706601). Currently on the following sentence has been removed from the article:

“You should test Jetstress 2010 with Exchange 2010 ESE binaries, and use Jetstress 2007 for testing legacy ESE versions of Exchange 2007 and Exchange 2003”

The JetStress tool can be used to simulate disk I/O load on a test server running Exchange to test and validate performance and stability of the disk subsystem, prior to moving them into a production environment.

The toolkit page has been updated accordingly.

You can download it here; x86 version is located here.