de-DEen-GB
 
rss

Just can't get enough of IT

This blog is about mostly anything in IT. But the primary focuses are Microsoft Technologies like Exchange, Office 365, Azure and Cloud Security.

Exchange Server 2007Exchange Server 2010Exchange Server 2013Exchange Server 2016Description

The script gathers a lot of Exchange organizational configuration data for documentation purposes.

The data is stored in separate log files. The log files are stored in a separate subfolder located under the script directory.

An exisiting subfolder will be deleted automatically.

Optionally, the log files can automatically be zipped. The zipped archive can be sent by email as an attachment.

 

When the script runs, a progress bar informs about the current step being executed.

Script progress bar

All files are stored in a dedicated subfolder (default name: ExchangeOrgInfo)

Example of exported files

The hash table $infoSources defines the following

  • Types of Exchange configuration data to be gathered
  • Output type for each configuration data entity
  • Optional paramaters for each configuration data entity
  • Attribute name for object sorting within an entity
  • The order of the data to be gathered (long running tasks are at the end)

 

Examples

# EXAMPLE 1
# Gather all data using MYCOMPANY as a prefix
    
.\Get-ExchangeOrganizationDetails.ps1 -Prefix MYCOMPANY

# EXAMPLE 2
# Gather all data using MYCOMPANY as a prefix and save all files as a compressed archive
    
.\Get-ExchangeOrganizationDetails.ps1 -Prefix MYCOMPANY -Zip

Version History

  • 1.0, Initial community release
  • 1.1, Updated and some PowerShell hygiene

Links

 

Follow

 

 

Read More »

Exchange Server 2007Exchange Server 2010Description

This scripts removes or updates users in legacy public folder ACLs. This reduces the likelihood of legacy public folder migration errors due to corrupted ACLs.

When you perform a migration from legacy public folders to modern public folders, you might see the following error as part of the migration reports.

A corrupted item was encountered: Folder ACL

Corrupted items count towards the bad item limit and will not be migrated.

When you take a closer look at the public folder ACLs, you'll see that there will be orphaned users and even users that have not been properly converted during past legacy replications.

In preparation for a modern public folder migration you should cleanup the public folder ACLs from so called zombie users.

Tasks performed by the script:

  • Remove orphaned users listed with SIDs, e.g. NT User:S-1-*
  • Identify ACL user/group with notation NT User:DOMAIN\samAccountName
    • Remove user/group, if object cannot be found in Active Directory
    • Replace user/group, if object can be found in Active Directory

Examples

# EXAMPLE 1
# Validate ACLs on public folder \MYPF and all of it's child public folders on Exchange server EX200701
.\Clean-PublicFolderACL.ps1 -RootPublicFolder "\MYPF" -PublicFolderServer EX200701 -ValidateOnly

# EXAMPLE 2
# Clean ACLs on public folder \MYPF and all of it's child public folders on Exchange server EX200701
.\Clean-PublicFolderACL.ps1 -RootPublicFolder "\MYPF" -PublicFolderServer EX200701

Version History

  • 1.0, Initial community release
  • 1.1, Fixed group replacement logic
  • 1.2, Script optimzation

Links

Last updated: 2016-12-01

Follow

 

Read More »

Public folders are one solution to provide a team collaboration tool for companies. Legacy public folders utilized a proprietary multi master replication mechanism which was not planned to handle todays data volumes. Therefore, Exchange 2013 introduced modern public folders which utilize the robust DAG replication functionality. Due to the technology change between legacy public folders and modern public folders a migration is required.

You can migrate legacy public folders hosted on Exchange 2007 or Exchange 2010 to modern public folders hosted on Exchange 2013. Or you can migrate legacy public folders hosted on Exchange 2010 to modern public folders hosted on Exchange 2016. If a cloud migration is a viable option for your company, you are able to migrate legacy public folders hosted on Exchange 2007 or Exchange 2010 to modern public folders hosted in Exchange Online.

The requirements for legacy Exchange Servers are:

  • Exchange Server 2007 SP3 with Update Rollup 15 or later
  • Exchange Server 2010 SP3 with Update Rollup 8 or later
  • Windows Server hosting Exchange Server 2007 must be upgraded to Windows PowerShell 2.0 and WinRM 2.0 for Windows Server 2008 x64

Since Exchange Server 2013 RTM the public folder migration scripts and the migration guidance have quite often been updated. The information provided at TechNet is very detailed for each migration option and there is no need to repeat each step in this blog post. Please see the link section for all hyperlinks.

Notes

Preparing a legacy public folder migration is pretty straight forward. The main issue companies are facing is the required downtime for finalizing the public folder migration batch. The required downtime cannot be determined exactly (not as exactly as requested by upper management). Which means that you have to plan for a scheduled maintenance during off hours. In the past a single migration request has been used to migrate legacy public folders. The new batch approach migrates public folder content using multiple requests within a single batch.

Estimated Number Of Concurrent Users

The Create-PublicFolderMailboxesForMigration.ps1 script uses the parameter EstimatedNumberOfConcurrentUsers to determine the overall number of public folder mailboxes serving the hierarchy. The TechNet articles explain this parameter as follows:

The estimated number of simultaneous user connections browsing a public folder hierarchy is usually less than the total number of users in an organization.

Exchange Server 2013 and Exchange Server 2016 currently support 2.000 concurrent connections to a single mailbox. This limit (2.000) is used by the Create-PublicFolderMailboxesForMigration.ps1 in conjunction with EstimatedNumberOfConcurrentUsers to determine the number of public folder mailboxes required to serve the public folder hierarchy. The current version of the script uses a coded limit of max 100 public folder mailboxes. This means that you can only serve 100 x 2.000 = 200.000 concurrent users accessing the public folder hierarchy.

Legacy Public Folder Store

Finalizing the migration request and setting the PublicFolderMigrationComplete attribute requires the legacy public folder information store to be restarted. Otherwise the configuration change will not be picked up the information store in timely fashion. Remember to restart the information store service on all legacy public folder servers.

Interim Migration

If your current public folder infrastructure is based on Exchange 2007 and you want to get rid of that Exchange version, you might think of replicating all content to Exchange 2010. This is not the best approach. Due to known content conversion issues you might encounter data loss when replicating public folder content between Exchange 2007 and Exchange 2010.

The recommended approach is to migrate Exchange 2007 legacy public folders to Exchange 2013 modern public folders directly.

Recommended Reading

A recommended reading on legacy public folder migration from Exchange 2010 to Exchange 2016 is Butch Waller’s blog post “Migration to Modern Public Folders – Notes from the Field

The PowerShell script referenced in that blog post does not work with Exchange 2007. You can use my PowerShell script which utilizes UTF8 encoding and runs with Exchange 2007 and Exchange 2010: https://gallery.technet.microsoft.com/Exchange-2010-Public-315ea9aa

Remark
All limits mentioned in this post reflect the information available at the time of writing.

Links

 

 


You need assistance with your Exchange Server setup? You have questions about your Exchange Server infrastructure and going hybrid with Office 365? Contact us at office365@granikos.eu or visit our website https://www.granikos.eu.

 

 

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 »

Description

This script copies a single receive connector from a source Exchange Server to a single target Exchange server or to all other Exchange servers.

The primary purposes of this script are:

  • Simplify migration of legacy Exchange receive connectors (Exchange 2007 or Exchange2010) to a modern Exchange server (Exchange 2013 or Exchange 2016)
  • Simplify receive connector distribution across multiple Exchange servers (Exchange 2013 or Exchange 2016)

Examples

Copy Exchange 2013/2016 receive connector nikos-one-RC2 from server MBX01 to server MBX2

.\Copy-ReceiveConnector.ps1 -SourceServer MBX01 -ConnectorName nikos-one-RC2 `
-TargetServer MBX2 -DomainController MYDC1.mcsmemail.de

Copy Exchange 2013/2016 receive connector nikos-one-RC2 from server MBX01 to all other Exchange 2013 servers

.\Copy-ReceiveConnector.ps1 -SourceServer MBX01 -ConnectorName nikos-one-RC1 `
-CopyToAllOther -DomainController MYDC1.mcsmemail.de

Copy Exchange 2013/2016 receive connector nikos-two relay from Exchange 2007 server MBX2007 to Exchange 2013 server MBX01 and reset network bindings

.\Copy-ReceiveConnector.ps1 -SourceServer MBX2007 -ConnectorName "nikos-two relay" `
-TargetServer MBX01 -MoveToFrontend -ResetBindings `
-DomainController MYDC1.mcsmemail.de

Version History

  • 1.0, Initial community release
  • 1.1 Domain Controller parameter added, permissions group copy added
  • 1.2 Move to FrontendTransport added, optional permission copy added, reset bindings added
  • 1.3 Update receive connector, if receive connector exists
  • 1.4 Fix to handle connector updates properly
  • 1.41 Minor fixes and update for Exchange 2016

Links

Script last updated: 2016-07-26

Additional Credits

Additional credits go to Jeffery Land, https://jefferyland.wordpress.com

Follow

Read More »