Exchange email adress policies are used to generate addresses since the early days of Exchange. Email address policies are used to automatically generate addresses for various protocols (SMTP, FAX, CCMAIL, MSMAIL). These overall use of address policies is well documented, but there is still some odd behaviour of the policies when it comes to language specific character handling.
The following list describes the common parameters:
All language specific characters and other non RFC 821/822 compliant characters are either translated or removed.
User without any language specific characters
Given Name: John Last Name : Doe Alias : JohnDoe
Email address policy results
%g.%s@mcsmemail.de = John.Doe@mcsmemail.de %m@mcsmemail.de = JohnDoe@mcsmemail.de %1g_%s@mcsmemail.de = J_Doe@mcsmemail.de
User with German language specific characters
Given Name: Michael Last Name : Müller Alias : Michael Mueller
%g.%s@mcsmemail.de = Michael.Mueller@mcsmemail.de %m@mcsmemail.de = Michael-Mueller@mcsmemail.de %2g%s@mcsmemail.de = MiMueller@mcsmemail.de
User with Scottish language specific characters
Given Name: Ian Last Name : O'Connell Alias : IanOConnell
%g.%s@mcsmemail.de = Ian.O'Connell@mcsmemail.de %m@mcsmemail.de = IanOConnell@mcsmemail.de %3g%5s@mcsmemail.de = IanO'Con@mcsmemail.de
As you might have noticed, the apostrophe is not handled as a special character but an RFC compliant character. The address policy is applied without any issues at all.
But a SMTP address containing an apostrophe leads to email transport errors and must be removed.
You can use the replacement parameter to strip all apostrophes from the surname by using the following email address policy.
%g.%r''%s@mcsmemail.de = Ian.OConnell@mcsmemail.de
This example will only remove apostrophes from the surname (%s) as the replacement parameter is placed in front of the surname parameter.
Exchange Server 2019 is the most recent release of the successful email messaging solution, introduced by Microsoft in 1996. Since the early days of the product supported a single primary email address only. The primary email address is used as the sender address when a user composes a new email message and sends the message. A mailbox can have multiple email addresses to receive messages for, but only one so-called reply-address.
But the limitation is not valid anymore.
A recent build of the Exchange Server 2019 Cumulative Update 1 released to VLSC contains a new feature called Multi-Reply Addresses.
This new feature is very helpful in scenarios where a single user sends email messages for multiple companies. Think of a business owner who is responsible for two or more companies. In the past, it was required to configure a mailbox account per primary email address used as a reply address. Such a configuration resulted not only on multiple inboxes but in multiple calendars and contact folders as well.
The new Multi-Reply Addresses feature of Exchange Server 2019 provides a much better solution. Moreover, it is a CEO-safe solution.
After enabling the multi-reply feature in your Exchange Organization the new functionality is available in Exchange Admin Center and Exchange Management Shell.
When you edit the email address properties using the Edit User Mailbox dialogue of an existing mailbox you can add additional reply addresses.
The following screenshot illustrates the steps.
When you close the Edit User Mailbox dialogue the additonal reply addresses and the status are displayed in the recipient list view and the detail pane.
The following screenhot shows how the reply addresses are displayed in the list view and how the status is displyed in the detail pane.
You can verify the updated proxyAddresses Active Directory attibute using ADSIEdit or the Attribute Viewer of the ADUC MMC.
When you compose a new email message using Outlook on the Web, the From selector is displayed automatically. You can select one of the configured reply email addresses as the sender address.
You can configure separate email signatures for each available reply address.
A user can select Options - Mail - Email signature to open the Email signature form. The form provides a new option to set a different email siganture for each reply address.
This is a really exciting new feature.
You can enable the new multi-reply function using the following new Exchange Cmdlet:
# Enable Multi-Reply functionality in Exchange Server 2019 Enable-SmtpMultiReply # Disable Multi-Reply functionality in Exchange Server 2019 Disable-SmtpMultiReply -CleanupPrimarySmtp -Force
When disabling the Multi-Reply feature a all but one primary SMTP address is converted to a legacy proxy smtp address.
You need to be assigned permissions before you can run this cmdlet. It is required to be assigned to the Elevated Exchange Organization Management role.
I do not know if the new feature had been exposed accidentally, but the on-premises version of the Exchange Server 2019 benefits from this new feature. This is a true differentiator to the cloud-based service of Exchange Online.
Enjoy Exchange Server 2019!
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.
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:
Now the mail contact object is a fully routable member or the Exchange organization.
Blame it on SharePoint, not Exchange.
Enjoy Exchange.
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.
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.
Besides the C# class the solution contains the following Powershell script to simplify development and deployment:
The transport agent intercepts a message from a given sender address and performs the following actions:
// 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 }
This blog post focusses on an issue where your Exchange Online users are not able to send emails to other Exchange Online recipients outside of your organization when using a 3rd Party Centralized Email Flow Setup. The term 3rd Party Centralized Email Flow Setup describes a solution where you not follow the preferred hybrid architecture proposed by the Exchange product group, but use a 3rd party software as a centralized email gateway.
You have followed the recommendation to secure the Exchange Online inbound connector for your on-premises email servers by using a certificate name or the remote IP address of your on-premises email gateway.
The on-premises email security gateway utilizes a self-signed certificate to secure TLS connections. The gateway is configured to use two different send connector setups:
In this case Exchange Online Protection (EOP) will not be able to differentiate between tenant internal inbound mail flow and mail flow targeted to other tenants. Therefore, email messages sent from your Exchange Online users to recipients located in other Exchange Online tenants will be discarded.
Interestingly enough, this will happen silently. Your gateway solution will log a successful delivery to Exchange Online Protection. The tenant administrator of the recipient domain will not find an any information in the Exchange Online message tracking logs.
The following diagram illustrates the setup.
The solution for this problem is pretty simple. Just use dedicated certificates for each connector targetting Exchange Online.
Change the Internet Connector to fully trusted 3rd party certificate. In this case you are not required to modify the existing Exchange Online inbound connector setup.
The new connector configurations are:
The following diagram illustrates the new setup:
Enjoy!
This script adds or removes IP addresses or IP address ranges to/from existing Receive Connectors.
The input file can contain more than one IP address (range), one entry per line. The IP address parameter can be used to add a single IP address.
The script creates a new sub directory beneath the current location of the script. The script utilizes the directory as a log directory to store the current remote IP address ranges prior modification.
A log is written to the \log subfolder utilitzing the GlobalFunctions Logger object.
# Example 1 # Add all IP addresses stored in D:\Scripts\ip.txt to a receive connector named RelayConnector .\Set-ReceiveConnectorIpAddress.ps1 -ConnectorName RelayConnector -FileName D:\Scripts\ip.txt -Action Add
# Example 2 # Remove IP address 10.10.10.1 from a receive connector nameds MyConnector from all Exchange Servers in the forest .\Set-ReceiveConnectorIpAddress.ps1 -ConnectorName MyConnector -IpAddress 10.10.10.1 -Action Remove -ViewEntireForest $true
At a recent troubleshooting case I was wondering why the pipeline tracing target directory remained empty after enabling the Exchange 2013 CU12 transport pipeline tracing using
Set-TransportService -PipelineTracingSenderAddress john.doe@mcsmemail.de -PipelineTracingEnabled $true
In this case the sender address itself was John.Doe@MCSMemail.de.
In past scenarios the email address to trace was copied from the original message and therefore this issue never occured.
After heading down the road on why no trace messages got logged in the pipeline tracing folder, and enabling and disabling the feature several times across multiple servers, the sender address made it's way into the cmdlet via Copy&Paste. And voilá... transport started tracing messages.
Set-TransportService -PipelineTracingSenderAddress John.Doe@MCSMemail.de -PipelineTracingEnabled $true
The TechNet article on pipeline tracing does not state anything about the fact that the email address attribute is case sensitive.
If you want to enable or disable pipeline tracing across multiple Exchange 2013 servers, you might want to use the following one-liners:
# One liner to activate Pipeline Tracing on multiple Exchange 2013 servers in a co-ex scenario Get-ExchangeServer | ?{$_.AdminDisplayVersion -ilike "*15*"} | Get-TransportService | Set-TransportService -PipelineTracingEnabled $true -PipelineTracingSenderAddress John.Doe@MCSMemail.de # One liner to deactive Pipeline Tracing across multiple Exchange 2013 servers Get-ExchangeServer | ?{$_.AdminDisplayVersion -ilike "*15*"} | Get-TransportService | Set-TransportService -PipelineTracingEnabled $false -PipelineTracingSenderAddress $null
Not mention that the output stored in the pipeline tracing folders might be sensitive, as all data is stored in a readable format.
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