de-DEen-GB
 
rss

Just can't get enough of IT

This blog is about mostly anything in IT, but focusses on Microsoft Technologies like Exchange, Office 365, and Azure and covers Cloud Security.

Problem

It might happen that a mobile device running an Android operating system is not being redirected properly by the on-premises AutoDiscover service, when the mailbox has been migrated to Office 365.

If your device is not redirected, the device prefix is not recognized by Exchange Server and therefore not being redirected properly. The new device redirect feature for Android devices was introduced in Exchange Server 2010 SP3 RU9, Exchange Server 2013 CU8, and Exchange Server 2016.

The following device prefixes are known to Exchange by default:

  • Acer, ADR9, Ally, Amazon, Android, ASUS, EasClient, FUJITSU, HTC, HUAWEI, LG, LS, Moto, Mozilla, NEC, Nokia, Palm, PANASONIC, PANTECH, Remoba, Samsung, SEMC, SHARP, SONY-, TOSHIBA, Vortex, VS, ZTE

Solution

If the device prefix of your device is not part of the default list, you can add the prefix to the AutoDiscover web.config file. 

Add the device prefix to the MobileSyncRedirectBypassClientPrefixes key in the appSettings node.

  <appSettings>
    <add key="LiveIdBasicAuthModule.AllowLiveIDOnlyAuth" value="true" />
    <add key="LiveIdBasicAuthModule.ApplicationName" value="Microsoft.Exchange.Autodiscover" />
    <add key="LiveIdBasicAuthModule.RecoverableErrorStatus" value="456" />
    <add key="LiveIdBasicAuthModule.PasswordExpiredErrorStatus" value="457" />
    <add key="ActiveManagerCacheExpirationIntervalSecs" value="5" />
    <add key="ProxyRequestTimeOutInMilliSeconds" value="30000" />
    <add key="LiveIdNegotiateAuxiliaryModule.AllowLiveIDOnlyAuth" value="true" />
    <add key="TrustedClientsForInstanceBasedPerfCounters" value="bes" />
    <add key="InstanceBasedPerfCounterTimeWindowInterval" value="900000" />
    <add key="MobileSyncRedirectBypassEnabled" value="true" />
    <add key="MobileSyncRedirectBypassClientPrefixes" value="Acer,ADR9,Ally,Amazon,Android,ASUS,EasClient,FUJITSU,HTC,HUAWEI,LG,LS,Moto,Mozilla,NEC,Nokia,Palm,PANASONIC,PANTECH,Remoba,Samsung,SEMC,SHARP,SONY-,TOSHIBA,Vortex,VS,ZTE" />
  </appSettings>

File location

%ExchangeInstallPath%\ClientAccess\Autodiscover\web.config

Notes

  • Modify the web.config on each Exchange 2010/2013 Client Access Server and each Exchange 2016 server.
  • After installing an Exchange 2013/2016 CU, the web.config must be modified again.

As always: Be careful when modifying application settings. Test such changes in a test environment first, if possible.

Links

 


You need assistance with your Exchange Server setup? You have questions about your Exchange Server infrastructure and going hybrid? You are interested in what Exchange Server 2016 has to offer for your environment?

Contact me at thomas@mcsmemail.de
Follow at https://twitter.com/stensitzki

Read More »

Problem

I came across an interesting issue when setting up a new Exchange 2013 server in an Exchange organization having the cmdlet extension agent enabled.

As mentioned in my last post Exchange setup checks for the existence of the ScriptingAgentConfig.xml file when agent extenstion is enabled in the Exchange organization. It turned out that this ist not only true when you install an Exchange update using /mode:update, but as well when installing a new Exchange server using /mode:install.

The following error occurs when Exchange Management Tools are provisioned.

Configuring Microsoft Exchange Server

    Preparing Setup                                                               COMPLETED
    Stopping Services                                                             COMPLETED
    Copying Exchange Files                                                        COMPLETED
    Language Files                                                                COMPLETED
    Restoring Services                                                            COMPLETED
    Language Configuration                                                        COMPLETED
    Exchange Management Tools                                                     FAILED
     The following error was generated when "$error.Clear();
          Set-WERRegistryMarkers;
        " was run: "Microsoft.Exchange.Provisioning.ProvisioningBrokerException: Provisioning layer
initialization failed: '"Scripting Agent initialization failed: "File is not found: 'C:\Program File
s\Microsoft\Exchange Server\V15\Bin\CmdletExtensionAgents\ScriptingAgentConfig.xml'.""' ---> Microso
ft.Exchange.Provisioning.ProvisioningException: "Scripting Agent initialization failed: "File is not
 found: 'C:\Program Files\Microsoft\Exchange Server\V15\Bin\CmdletExtensionAgents\ScriptingAgentConf
ig.xml'."" ---> System.IO.FileNotFoundException: "File is not found: 'C:\Program Files\Microsoft\Exc
hange Server\V15\Bin\CmdletExtensionAgents\ScriptingAgentConfig.xml'."
   at Microsoft.Exchange.ProvisioningAgent.ScriptingAgentConfiguration.Initialize(String xmlConfigPa
th)
   at Microsoft.Exchange.ProvisioningAgent.ScriptingAgentConfiguration..ctor(String xmlConfigPath)
   --- End of inner exception stack trace ---
   at Microsoft.Exchange.ProvisioningAgent.ScriptingAgentConfiguration..ctor(String xmlConfigPath)
   at Microsoft.Exchange.ProvisioningAgent.ScriptingAgentClassFactory.get_Configuration()
   at Microsoft.Exchange.ProvisioningAgent.ScriptingAgentClassFactory.GetSupportedCmdlets()
   at Microsoft.Exchange.Provisioning.ProvisioningBroker.BuildHandlerLookupTable(CmdletExtensionAgen
t[] enabledAgents, Exception& ex)
   --- End of inner exception stack trace ---
   at Microsoft.Exchange.Provisioning.ProvisioningLayer.GetProvisioningHandlersImpl(Task task)
   at Microsoft.Exchange.Provisioning.ProvisioningLayer.GetProvisioningHandlers(Task task)
   at Microsoft.Exchange.Configuration.Tasks.Task.<BeginProcessing>b__4()
   at Microsoft.Exchange.Configuration.Tasks.Task.InvokeNonRetryableFunc(Action func, Boolean termin
atePipelineIfFailed)".

As expected a fresh Exchange install contains the sample file only. The following screenshot shows the Exchange Management Shell and the releated folder in the background.

Enabled cmdlet extension agent breaks Exchange setup

 

Solution

The only solution currently known to me is to disable the cmdlet extension agent until the setup of the new Exchange server has finished.

Disable-CmdletExtensionAgent "Scripting Agent"

Having the cmdlet extension agent disabled the setup finishes without any issues. Don't forget to copy the cmdlet extension Xml file to the newly built server and to enable the cmdlet extension agent again.

Enable-CmdletExtensionAgent "Scripting Agent"

 

Links

 


You need assistance with your Exchange Server setup? You have questions about your Exchange Server infrastructure and going hybrid? You are interested in what Exchange Server 2016 has to offer for your environment?

Contact me at thomas@mcsmemail.de
Follow at https://twitter.com/stensitzki

 

Read More »

When you've enabled the Exchange scripting agent extension agents, it is required to copy the configuration file to each Exchange server. Paul Cunningham's script helps you to achive this goal pretty easily.

But if you have installed the Exchange 2013 Management Tools on additonal servers, these servers are not fetched using the Get-ExchangeServer cmdlet. But when you install a Cumulative Update the existence of the extension agent config file is checked. And this even on a server having only the Exchange Management Tools installed.

Therefore the following PowerShell code provides an easy and simple way to add additonal server having the Exchange 2013+ Management Tools installed (aka Admin Servers, Monitoring Servers, Job Servers, etc.). The script uses a filter to select Exchange 2013 servers only, as the script has been extended in an environment having still active Exchange 2007 servers.

The following PowerShell snippet displays only the changes, which need to be added to Paul's original script starting row 68.

# Original PowerShell code
# $exchangeservers = Get-ExchangeServer

# Select all Exchange 2013 servers only, restrict properties to Name and AdminDisplayName
$exchangeservers = Get-ExchangeServer | ?{$_.AdminDisplayVersion -like "Version 15.0*"} | Select Name, AdminDisplayVersion

# Add additional servers as needed

$manualServers = @()
# Copy and modify as needed
$manualServers += (New-Object PSObject -Property @{Name="EXSRV2010";AdminDisplayVersion="Version 14"})
$manualServers += (New-Object PSObject -Property @{Name="EXSRV2013-01";AdminDisplayVersion="Version 15"})
$manualServers += (New-Object PSObject -Property @{Name="EXSRV2013-02";AdminDisplayVersion="Version 15"})

# Combine arrays
$exchangeservers = $exchangeservers + $manualServers

# End Modification

$report = @()

[string]$date = Get-Date -F yyyyMMdd-HHmmss

Enjoy extending the Exchange PowerShell cmdlets.

Links

Questions? Just leave a comment.

Read More »

Description

This scripts creates a new shared mailbox (aka team mailbox) and security groups for full access and and send-as delegation. The security groups are created using a naming convention. If required by your Active Directory team, you can add group prefixes or department abbreviations as well.

The script uses a Xml configuration file to simplify changes for variables unique for your environment.

High level steps executes by the script:

  1. Create a new shared mailbox
  2. Create a new mail enabled security group for full access delegation
  3. Assign full access security group for full access to the shared mailbox
  4. Create a new mail enabled security group for send-as delegation
  5. Assign send-as permissions to send-as security group

 

Examples

Xml settings file

<?xml version="1.0"?>
<Settings>
	<GroupSettings>
		<Prefix>pre_</Prefix>
		<SendAsSuffix>_SA</SendAsSuffix>
		<FullAccessSuffix>_FA</FullAccessSuffix>
<CalendarBookingSuffix>_CB</CalendarBookingSuffix>
		<TargetOU>mcsmemail.de/IT/Groups/Mail</TargetOU>
		<Domain>mcsmemail.de</Domain>
	</GroupSettings>
	<AccountSettings>
		<TargetOU>mcsmemail.de/IT/SharedMailboxes</TargetOU>
	</AccountSettings>
	<GeneralSettings>
		<Sleep>10</Sleep>
	</GeneralSettings>
</Settings>

The following example creates an empty shared mailbox for an internal Exchange Admin team with empty security groups.

.\New-TeamMailbox.ps1 -TeamMailboxName "TM-Exchange Admins" ` 
  -TeamMailboxDisplayName "Exchange Admins" `
  -TeamMailboxAlias "TM-ExchangeAdmins" `
  -TeamMailboxSmtpAddress "ExchangeAdmins@mcsmemail.de" `
  -DepartmentPrefix "IT"

The following Create-TeamMailbox.ps1 script simplifies the process of creating a team mailbox even more.

$teamMailboxName = 'TM-Exchange Admin'
$teamMailboxDisplayName = 'Exchange Admins'
$teamMailboxAlias = 'TM-ExchangeAdmin'
$teamMailboxSmtpAddress = 'ExchangeAdmins@mcsmemails.de'
$departmentPrefix = 'IT'
$groupFullAccessMembers = @('exAdmin1','exAdmin2')
$groupSendAsMember = @('exAdmin1','exAdmin2')

.\New-TeamMailbox.ps1 -TeamMailboxName $teamMailboxName ` 
  -TeamMailboxDisplayName $teamMailboxDisplayName `
  -TeamMailboxAlias $teamMailboxAlias `
  -TeamMailboxSmtpAddress $teamMailboxSmtpAddress `
  -DepartmentPrefix $departmentPrefix `
  -GroupFullAccessMembers $groupFullAccessMembers `
  -GroupSendAsMember $groupSendAsMember -Verbose

Version History

  • 1.0, Initial community release

Links

Follow

Read More »
On July 11, 2016
0 Comment
165 Views

Description

This script helps you to monitor message flow in a NoSpamProxy environment using a PRTG custom PowerShell sensor.

This custom sensor contains the following five channels:

  • In/Out Success
    Total of inbound/outbound successfully delivered messages over the last X minutes
  • Inbound Success
    Number of inbound successfully delivered messages over the last X minutes
  • Outbound Success
    Number of outbound successfully delivered messages over the last X minutes
  • Inbound PermanentlyBlocked
    Number of inbound blocked messages over the last X minutes
  • Outbound DeliveryPending
    Number of outbound messages with pending delivery over the last X minutes

The default interval is five minutes. But you might want to change the interval as needed for your environment.

These channels can easily be modified and additional channels can be added as well.

NoSpamProxy is a powerful anti-spam gateway solution providing additonal functionality like centralized S/MIME and PGP encryption for on-premises and Exchange Online deployments.

PRTG is a industry standard system monitoring solution.

Examples

The script itself does not take any additional attributes and is called by PRTG probe.

To verify your setup, you easily execute the PowerShell script. It returns a Xml result.

PS C:\Scripts> .\Get-NoSpamProxyPrtgData.ps1
<prtg>
  <result>
    <channel>In/Out Success</channel>
    <value>0</value>
    <unit>Count</unit>
  </result>
  <result>
    <channel>Inbound Success</channel>
    <value>0</value>
    <unit>Count</unit>
  </result>
  <result>
    <channel>Outbound Success</channel>
    <value>0</value>
    <unit>Count</unit>
  </result>
  <result>
    <channel>Inbound PermanentlyBlocked</channel>
    <value>0</value>
    <unit>Count</unit>
  </result>
  <result>
    <channel>Inbound DeliveryPending</channel>
    <value>0</value>
    <unit>Count</unit>
    <limitmaxwarning>10</limitmaxwarning>
    <limitmode>1</limitmode>
  </result>
</prtg>

The PRTG channel configuration

PRTG channel using a custom sensor

The following screenshot shows PRTG example graphs.

NoSpamProxy monitored using a PRTG custom sensor

Notes

The custom PowerShell script must be saved to the following location of the PRTG probe:

[INSTALLPATH]\PRTG Network Monitor\Custom Sensors\EXEXML

Ensure to have the PowerShell execution policy set correctly. Otherwise the PRTG service won't be able to execute the PowerShell script.

Ensure that the service account used by the PRTG probe has access to the script and is a member of the NoSpamProxy Monitoring Administrators security group.

Version History

  • 1.0, Initial community release

Links

Additional Credits

Additional credits go to Brian Addicks, https://github.com/brianaddicks/prtgshell

Follow

Read More »

Description

This script imports multiple PST files located in a single directory into a user mailbox or a user mailbox archive. 

Due to some filename limitations of the New-MailboxImportRequest cmdlet in reagards to the UNC path, the PST filenames are sanitized. Any unsupported (unwanted) character is removed. You can modify the replacement function as needed. This might be necessary as the PST filenames can be used as target folder names during import.

Original filenames:

Filenames before renaming

Renamed filenames:

Filenames after renaming

When using the FilenameAsTargetFolder switch each PST file is imported into a separate target folder.

PST filenames used as target folder

NOTE:
This script utilizes the GlobalFunctions PowerShell module for logging. Please prepare your system for the use of the GlobalFunctions module first.

Steps performed:

  1. Sanitize PST filenames
  2. Create new mailbox import request
  3. Monitor status of created import request
    1. When still running, continue monitoring status
    2. When successfully finished, save import request statistics report to log file and continiue with next PST
    3. When aborted, continue with next PST or cancel script

Examples

 

Example PowerShell Output

.\Start-MailboxImport.ps1 -Identity JohnDoe -Archive -FilePath "\\ROBERTKWEISS\e$\PSTImport\JohnDoe" -FilenameAsTargetFolder -BadItemLimit 10 -ContinueOnError -SecondsToWait 90
Note: Script will wait 90s between each status check!
Create New-MailboxImportRequest for user: JohnDoe and file: \\ROBERTKWEISS\e$\PSTImport\JohnDoe\Myoldarchive.pst into the archive. Targetfolder:"Myoldarchive".
Waiting for import JohnDoe-Myoldarchive.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive.pst to be completed. Status: InProgress
Waiting for import JohnDoe-Myoldarchive.pst to be completed. Status: InProgress
Waiting for import JohnDoe-Myoldarchive.pst to be completed. Status: InProgress
Import request JohnDoe-Myoldarchive.pst completed successfully.
Import request JohnDoe-Myoldarchive.pst deleted.
Create New-MailboxImportRequest for user: JohnDoe and file: \\ROBERTKWEISS\e$\PSTImport\JohnDoe\Myoldarchive1.pst into the archive. Targetfolder:"Myoldarchive1".
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: Queued
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: InProgress
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: InProgress
Waiting for import JohnDoe-Myoldarchive1.pst to be completed. Status: InProgress
Import request JohnDoe-Myoldarchive1.pst completed successfully.
Import request JohnDoe-Myoldarchive1.pst deleted.
Script finished.

Version History

  • 1.0, Initial community release
  • 1.1, log will now be stored in a subfolder (name equals Identity)
  • 1.2, PST file renaming added
  • 1.3, Module ActiveDirectory removed
  • 1.4, AcceptLargeDatalost would now be added if BadItemLimit is over 51
  • 1.5, Parameter IncludeFodlers added
  • 1.6, Parameter TargetFolder added

Links

Last updated: 2016-08-18

Follow me

Additional Note

This Powershell script has been optimized using the ISESteroids™ add-on. Learn more about ISESteroids™ here.

Read More »

Issue

When a SharePoint list is being email enabled the Central Administration service accounts creates a new contact object in Active Directory. The object attributes configured by SharePoint are fully sufficient for the Exchange transport routing engine when addressed directly. Which means that a new email sent to the email address of the email enabled list is routed to SharePoint.

However, when the contact has been added to an Exchange distribution list, the Exchange routing engine will not resolve the contact object. You won't see any information on this situation in the Exchange message tracking log.

Solution

The solution is pretty simple but require the intervention of an Exchange administrator. Execute the following on the mail contact object:

Get-Contact CONTACTNAME | Set-Contact

The use of Set-Contact without any additonal parameters forces Exchange to validate the currently exisiting attributes and the follow four attributes:

  • legacyExchangeDN
  • msExchUMDtmfMap
  • proxyAddresses
  • textEncodedORAddress

Now the mail contact object is a fully routable member or the Exchange organization.

Blame it on SharePoint, not Exchange.

Enjoy Exchange.

 

Read More »

Description

This script fetches the disk volume (Win32_Volume) information via WMI and shows the results in the PowerShell command line window. Optionally, you can have the report sent as an Html email to a recipient of your choice.

The switch -AllExchangeServer simplifies gathering the disk volume information across all Exchange servers in your environment.

The following screenshot shows the command line output when using

.\Get-Diskpace.ps1 -ComputerName MYSERVER

Get-Diskspace command line output

The following screenshot shows an example of the html email output when using

.\Get-Diskpace.ps1 -ComputerName MYSERVER -SendMail -MailFrom postmaster@sedna-inc.com -MailTo exchangeadmin@sedna-inc.com -MailServer mail.sedna-inc.com

Get-Diskspace email example

Examples

# EXAMPLE 1
# Get disk information from computer MYSERVER in MB

Get-Diskpace.ps1 -ComputerName MYSERVER -Unit MB

# EXAMPLE 2
# Get disk information from all Exchange servers and send html email

Get-Diskpace.ps1 -AllExchangeServer -SendMail -MailFrom postmaster@sedna-inc.com -MailTo exchangeadmin@sedna-inc.com -MailServer mail.sedna-inc.com

Version History

  • 1.0, Initial community release
  • 1.1,  Email reports added
  • 1.11, Send email issue fixed
  • 1.12, PowerShell hygiene applied

Links

Follow

Additional Note

This Powershell script has been optimized using the ISESteroids™ add-on. Learn more about ISESteroids™ here.

Read More »

Troubleshooting Outlook connectivity issues with Office 365 is tricky. Administrators can use two valuable tools provided by Microsoft to identify and even fix client related connectivity issues.

1. Outlook Account Test Page

Start with the Outlook account problems test page in the Office 365 portal. You need to log on as the Office 365 user having issues.

SARA Server

The site tests for the following:

  • You cannot create an Outlook profile or you are asked for your password repeatedly when creating one.
  • You cannot connect to your mailbox or receive an error that a mailbox cannot be found.
  • You are getting invalid license errors or messages that Office cannot verify the license.

If no issues are identified after you've logged on to Office 365, move to the next step.

2. Support and Recovery Assistant

The Microsoft Support and Recovery Assistant (SARA) for Office 365 is click to run tool that is installed and executed locally.

Support and Recovery Assistant (SARA)

These two tools fix most of the Outlook connectivity issues you are facing as an Office 365 administrator.

Links

 

Enjoy Office 365

Read More »

This is a wrap-up of an older post that had originally been published on my former website.

Even though that this post focusses on Exchange 2010 transport agents, you will get an understand on what is required to create an Exchange 2013/2016 aka Version 15 transport agent.

Visual Studio Project

Writing your own transport agent for Exchange 2010 is not really complicated. With a Visual Studio C# Class project you are ready to go.

The follow picture shows the Visual Studio Solution as it has been used for the Message Modifier Solution.

Visual Studio Solution

Besides the C# class the solution contains the following Powershell script to simplify development and deployment:

  • Add-TransportAgent.ps1
    Installs the transport agent on the productive Exchange Server
  • Remove-TransportAgent.ps1
    Uninstalls the transport agent on the productive Exchange Server
    See Technet Gallery https://gallery.technet.microsoft.com/Remove-a-custom-Exchange-1cd30e92
  • Build-DeploymentPackage.ps1
    Copy all required DLLs, Powershell scripts and the deployment configuration file to a dedicated folder
  • install.ps1
    Installs the transport agent on the development Exchange Server
  • uninstall.ps1
    Uninstalls the transport agent on the development Exchange Server

The transport agent intercepts a message from a given sender address and performs the following actions:

  • If the message has attachments with file names starting with "WORKBOOK_" the attachments are renamed to the following format:

    [yyyyMMdd] [EMAIL SUBJECT]-[NUMBER].[ORIGINAL EXTENSION]
     
  • The subject is rewritten from the format

    [dd.MM.yyyy] [SUBJECT TEXT]
    to
    [yyyyMMdd] [SUBJECT TEXT]

 

Links

Code Sample

// AttachmentModify  
// ----------------------------------------------------------  
// Example for intercepting email messages in an Exchange 2010 transport queue  
//   
// The example intercepts messages sent from a configurable email address(es)  
// and checks the mail message for attachments have filename in to format  
//   
//      WORKBOOK_{GUID}  
//  
// Changing the filename of the attachments makes it easier for the information worker  
// to identify the reports in the emails and in the file system as well.  
// Copyright (c) Thomas Stensitzki
// ----------------------------------------------------------  
  
using System;  
using System.Collections.Generic;  
using System.Diagnostics;  
using System.Globalization;  
using System.IO;  
using System.Reflection;  
using System.Text;  
using System.Text.RegularExpressions;  
using System.Threading;  
using System.Xml;  
  
// the lovely Exchange   
using Microsoft.Exchange.Data.Transport;  
using Microsoft.Exchange.Data.Transport.Smtp;  
using Microsoft.Exchange.Data.Transport.Email;  
using Microsoft.Exchange.Data.Transport.Routing;  
  
namespace SFTools.Messaging.AttachmentModify  
{  
    #region Message Modifier Factory  
  
    /// <summary>  
    /// Message Modifier Factory  
    /// </summary>  
    public class MessageModifierFactory : RoutingAgentFactory  
    {  
        /// <summary>  
        /// Instance of our transport agent configuration  
        /// This is for a later implementation  
        /// </summary>  
        private MessageModifierConfig messageModifierConfig = new MessageModifierConfig();  
  
        /// <summary>  
        /// Returns an instance of the agent  
        /// </summary>  
        /// <param name="server">The SMTP Server</param>  
        /// <returns>The Transport Agent</returns>  
        public override RoutingAgent CreateAgent(SmtpServer server)  
        {  
            return new MessageModifier(messageModifierConfig);  
        }  
    }  
 
    #endregion  
 
    #region Message Modifier Routing Agent  
      
    /// <summary>  
    /// The Message Modifier Routing Agent for modifying an email message  
    /// </summary>  
    public class MessageModifier : RoutingAgent  
    {  
        // The agent uses the fileLock object to synchronize access to the log file  
        private object fileLock = new object();  
  
        /// <summary>  
        /// The current MailItem the transport agent is handling  
        /// </summary>  
        private MailItem mailItem;  
  
        /// <summary>  
        /// This context to allow Exchange to continue processing a message  
        /// </summary>  
        private AgentAsyncContext agentAsyncContext;  
  
        /// <summary>  
        /// Transport agent configuration  
        /// </summary>  
        private MessageModifierConfig messageModifierConfig;  
  
        /// <summary>  
        /// Constructor for the MessageModifier class  
        /// </summary>  
        /// <param name="messageModifierConfig">Transport Agent configuration</param>  
        public MessageModifier(MessageModifierConfig messageModifierConfig)  
        {  
            // Set configuration  
            this.messageModifierConfig = messageModifierConfig;  
  
            // Register an OnRoutedMessage event handler  
            this.OnRoutedMessage += OnRoutedMessageHandler;  
        }  
  
        /// <summary>  
        /// Event handler for OnRoutedMessage event  
        /// </summary>  
        /// <param name="source">Routed Message Event Source</param>  
        /// <param name="args">Queued Message Event Arguments</param>  
        void OnRoutedMessageHandler(RoutedMessageEventSource source, QueuedMessageEventArgs args)  
        {  
            lock (fileLock) {  
                try {  
                    this.mailItem = args.MailItem;  
                    this.agentAsyncContext = this.GetAgentAsyncContext();  
  
                    // Get the folder for accessing the config file  
                    string dllDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);  
  
                    // Fetch the from address from the current mail item  
                    RoutingAddress fromAddress = this.mailItem.FromAddress;  
  
                    Boolean boWorkbookFound = false;    // We just want to modifiy subjects when we modified an attachement first  
 
                    #region External Receive Connector Example  
  
                    // CHeck first, if the mail item does have a ReceiveConnectorName property first to prevent ugly things to happen  
                    if (mailItem.Properties.ContainsKey("Microsoft.Exchange.Transport.ReceiveConnectorName")) {  
                        // This is just an example, if you want to do something with a mail item which has been received via a named external receive connector  
                        if (mailItem.Properties["Microsoft.Exchange.Transport.ReceiveConnectorName"].ToString().ToLower() == "externalreceiveconnectorname")  
                        {  
                            // do something fancy with the email  
                        }  
                    }  
 
                    #endregion  
  
                    RoutingAddress catchAddress;  
  
                    // Check, if we have any email addresses configured to look for  
                    if (this.messageModifierConfig.AddressMap.Count > 0) {  
                        // Now lets check, if the sender address can be found in the dictionary  
                        if (this.messageModifierConfig.AddressMap.TryGetValue(fromAddress.ToString().ToLower(), out catchAddress)) {  
                            // Sender address found, now check if we have attachments to handle  
                            if (this.mailItem.Message.Attachments.Count != 0) {  
                                // Get all attachments  
                                AttachmentCollection attachments = this.mailItem.Message.Attachments;  
  
                                // Modify each attachment  
                                for (int count = 0; count < this.mailItem.Message.Attachments.Count; count++) {  
                                    // Get attachment  
                                    Attachment attachment = this.mailItem.Message.Attachments[count];  
  
                                    // We will only transform attachments which start with "WORKBOOK_"  
                                    if (attachment.FileName.StartsWith("WORKBOOK_")) {  
                                        // Create a new filename for the attachment  
                                        // [MODIFIED SUBJECT]-[NUMBER].[FILEEXTENSION]  
                                        String newFileName = MakeValidFileName(string.Format("{0}-{1}{2}", ModifiySubject(this.mailItem.Message.Subject.Trim()), count + 1, Path.GetExtension(attachment.FileName)));  
  
                                        // Change the filename of the attachment  
                                        this.mailItem.Message.Attachments[count].FileName = newFileName;  
  
                                        // Yes we have changed the attachment. Therefore we want to change the subject as well.  
                                        boWorkbookFound = true;  
                                    }  
                                }  
  
                                // Have changed any attachments?  
                                if (boWorkbookFound) {  
                                    // Then let's change the subject as well  
                                    this.mailItem.Message.Subject = ModifiySubject(this.mailItem.Message.Subject);  
                                }  
                            }  
                        }  
                    }  
                }  
                catch (System.IO.IOException ex) {  
                    // oops  
                    Debug.WriteLine(ex.ToString());  
                    this.agentAsyncContext.Complete();  
                }  
                finally {  
                    // We are done  
                    this.agentAsyncContext.Complete();  
                }  
            }  
  
            // Return to pipeline  
            return;  
        }  
  
        /// <summary>  
        /// Build a new subject, if the first 10 chars of the original subject are a valid date.  
        /// We muste transform the de-DE format dd.MM.yyyy to yyyyMMdd for better sortability in the email client.  
        /// </summary>  
        /// <param name="MessageSubject">The original subject string</param>  
        /// <returns>The modified subject string, if modification was possible</returns>  
        private static string ModifiySubject(string MessageSubject)  
        {  
            string newSubject = String.Empty;  
  
            if (MessageSubject.Length >= 10) {  
                string dateCheck = MessageSubject.Substring(0, 10);  
                DateTime dt = new DateTime();  
                try {  
                    // Check if we can parse the datetime  
                    if (DateTime.TryParse(dateCheck, out dt)) {  
                        // lets fetch the subject starting at the 10th character  
                        string subjectRight = MessageSubject.Substring(10).Trim();  
                        // build a new subject  
                        newSubject = string.Format("{0:yyyyMMdd} {1}", dt, subjectRight);  
                    }  
                }  
                finally {  
                    // do nothing  
                }  
            }  
  
            return newSubject;  
        }  
  
  
        /// <summary>  
        /// Replace invalid filename chars with an underscore  
        /// </summary>  
        /// <param name="name">The filename to be checked</param>  
        /// <returns>The sanitized filename</returns>  
        private static string MakeValidFileName(string name)  
        {  
            string invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()));  
            string invalidRegExStr = string.Format(@"[{0}]+", invalidChars);  
            return Regex.Replace(name, invalidRegExStr, "_");  
        }  
  
    }  
 
    #endregion  
 
    #region Message Modifier Configuration  
  
    /// <summary>  
    /// Message Modifier Configuration class  
    /// </summary>  
    public class MessageModifierConfig  
    {  
        /// <summary>  
        ///  The name of the configuration file.  
        /// </summary>  
        private static readonly string configFileName = "SFTools.MessageModify.Config.xml";  
  
        /// <summary>  
        /// Point out the directory with the configuration file (= assembly location)  
        /// </summary>  
        private string configDirectory;  
  
        /// <summary>  
        /// The filesystem watcher to monitor configuration file updates.  
        /// </summary>  
        private FileSystemWatcher configFileWatcher;  
  
        /// <summary>  
        /// The from address  
        /// </summary>  
        private Dictionary<string, RoutingAddress> addressMap;  
  
        /// <summary>  
        /// Whether reloading is ongoing  
        /// </summary>  
        private int reLoading = 0;  
  
        /// <summary>  
        /// The mapping between domain to catchall address.  
        /// </summary>  
        public Dictionary<string, RoutingAddress> AddressMap  
        {  
            get { return this.addressMap; }  
        }  
  
        /// <summary>  
        /// Constructor  
        /// </summary>  
        public MessageModifierConfig()  
        {  
            // Setup a file system watcher to monitor the configuration file  
            this.configDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);  
            this.configFileWatcher = new FileSystemWatcher(this.configDirectory);  
            this.configFileWatcher.NotifyFilter = NotifyFilters.LastWrite;  
            this.configFileWatcher.Filter = configFileName;  
            this.configFileWatcher.Changed += new FileSystemEventHandler(this.OnChanged);  
  
            // Create an initially empty map  
            this.addressMap = new Dictionary<string, RoutingAddress>();  
  
            // Load the configuration  
            this.Load();  
  
            // Now start monitoring  
            this.configFileWatcher.EnableRaisingEvents = true;  
        }  
  
        /// <summary>  
        /// Configuration changed handler.  
        /// </summary>  
        /// <param name="source">Event source.</param>  
        /// <param name="e">Event arguments.</param>  
        private void OnChanged(object source, FileSystemEventArgs e)  
        {  
            // Ignore if load ongoing  
            if (Interlocked.CompareExchange(ref this.reLoading, 1, 0) != 0) {  
                Trace.WriteLine("load ongoing: ignore");  
                return;  
            }  
  
            // (Re) Load the configuration  
            this.Load();  
  
            // Reset the reload indicator  
            this.reLoading = 0;  
        }  
  
        /// <summary>  
        /// Load the configuration file. If any errors occur, does nothing.  
        /// </summary>  
        private void Load()  
        {  
            // Load the configuration  
            XmlDocument doc = new XmlDocument();  
            bool docLoaded = false;  
            string fileName = Path.Combine(this.configDirectory, MessageModifierConfig.configFileName);  
  
            try {  
                doc.Load(fileName);  
                docLoaded = true;  
            }  
            catch (FileNotFoundException) {  
                Trace.WriteLine("Configuration file not found: {0}", fileName);  
            }  
            catch (XmlException e) {  
                Trace.WriteLine("XML error: {0}", e.Message);  
            }  
            catch (IOException e) {  
                Trace.WriteLine("IO error: {0}", e.Message);  
            }  
  
            // If a failure occured, ignore and simply return  
            if (!docLoaded || doc.FirstChild == null) {  
                Trace.WriteLine("Configuration error: either no file or an XML error");  
                return;  
            }  
  
            // Create a dictionary to hold the mappings  
            Dictionary<string, RoutingAddress> map = new Dictionary<string, RoutingAddress>(100);  
  
            // Track whether there are invalid entries  
            bool invalidEntries = false;  
  
            // Validate all entries and load into a dictionary  
            foreach (XmlNode node in doc.FirstChild.ChildNodes) {  
                if (string.Compare(node.Name, "domain", true, CultureInfo.InvariantCulture) != 0) {  
                    continue;  
                }  
  
                XmlAttribute domain = node.Attributes["name"];  
                XmlAttribute address = node.Attributes["address"];  
  
                // Validate the data  
                if (domain == null || address == null) {  
                    invalidEntries = true;  
                    Trace.WriteLine("Reject configuration due to an incomplete entry. (Either or both domain and address missing.)");  
                    break;  
                }  
  
                if (!RoutingAddress.IsValidAddress(address.Value)) {  
                    invalidEntries = true;  
                    Trace.WriteLine(String.Format("Reject configuration due to an invalid address ({0}).", address));  
                    break;  
                }  
  
                // Add the new entry  
                string lowerDomain = domain.Value.ToLower();  
                map[lowerDomain] = new RoutingAddress(address.Value);  
  
                Trace.WriteLine(String.Format("Added entry ({0} -> {1})", lowerDomain, address.Value));  
            }  
  
            // If there are no invalid entries, swap in the map  
            if (!invalidEntries) {  
                Interlocked.Exchange<Dictionary<string, RoutingAddress>>(ref this.addressMap, map);  
                Trace.WriteLine("Accepted configuration");  
            }  
        }  
    }  
     #endregion  
} 

 

 

Read More »