// *************************************************************** // // CatchAllAgent // // An Exchange 2007 Transport Protocol agent that implements // catch all. // // HowTo: // // 1) Copy both the assembly and config.xml file at the same // location on yourt Exchange 2007 Edge or (Internet facing) // Hub role. // // 2) Add entries to the config.xml for the domains for which // you want catch all to be working. Entries look like this: // // // // // Note: The messages will be redirected to the specified // 'address' and it's important that those addresses really // exist. Otherwise the message will NDR, or Recipient // Filtering will reject the message. // // 3) Use the install-transportagent task to install the agent // // 4) Use the enable-transportagent task to enable the agent. // It's important to specify a priority which is higher than // the priority of the Recipient Filtering agent. // // Features: // // 1. Changes to the configuration will be picked up // automatically. // // 2. If the new configuration is invalid, it will ignore the new // configuration. // // 3. It's possible to dump traces to a file (traces.log in the // example below) by adding the following to the application // configuration file (edgetransport.exe.config): // // // // // // // // // // // // // *************************************************************** namespace CatchAll { using System; using System.Collections.Generic; using System.Reflection; using System.IO; using System.Xml; using System.Globalization; using System.Diagnostics; using System.Threading; using Microsoft.Exchange.Data.Transport; using Microsoft.Exchange.Data.Transport.Smtp; /// /// CatchAllFactory: The agent factory for the CatchAllAgent. /// public class CatchAllFactory : SmtpReceiveAgentFactory { private CatchAllConfig catchAllConfig = new CatchAllConfig(); /// /// Creates a CatchAllAgent instance. /// /// The SMTP server. /// An agent instance. public override SmtpReceiveAgent CreateAgent(SmtpServer server) { return new CatchAllAgent(catchAllConfig, server.AddressBook); } } /// /// CatchAllAgent: An SmtpReceiveAgent implementing catch-all. /// public class CatchAllAgent : SmtpReceiveAgent { /// /// The address book to be used for lookups. /// private AddressBook addressBook; /// /// The configuration /// private CatchAllConfig catchAllConfig; /// /// Constructor /// /// The configuration. /// The address book to perform lookups. public CatchAllAgent(CatchAllConfig catchAllConfig, AddressBook addressBook) { // Save the address book and configuration this.addressBook = addressBook; this.catchAllConfig = catchAllConfig; // Register an OnRcptCommand event handler this.OnRcptCommand += new RcptCommandEventHandler(this.RcptToHandler); } /// /// Handles the "RCPT TO:" SMTP command /// /// The event source. /// The event arguments. public void RcptToHandler(ReceiveEventSource source, RcptCommandEventArgs rcptArgs) { RoutingAddress catchAllAddress; // Check whether to handle the recipient's domain if (this.catchAllConfig.AddressMap.TryGetValue( rcptArgs.RecipientAddress.DomainPart.ToLower(), out catchAllAddress)) { // Rewrite the (envelope) recipient address if 'not found' if ((this.addressBook != null) && (this.addressBook.Find(rcptArgs.RecipientAddress) == null)) { rcptArgs.RecipientAddress = catchAllAddress; } } return; } } /// /// CatchAllConfig: The configuration for the CatchAllAgent. /// public class CatchAllConfig { /// /// The name of the configuration file. /// private static readonly string configFileName = "config.xml"; /// /// Point out the directory with the configuration file (= assembly location) /// private string configDirectory; /// /// The filesystem watcher to monitor configuration file updates. /// private FileSystemWatcher configFileWatcher; /// /// The (domain to) catchall address map /// private Dictionary addressMap; /// /// Whether reloading is ongoing /// private int reLoading = 0; /// /// Constructor. /// public CatchAllConfig() { // 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 = "config.xml"; this.configFileWatcher.Changed += new FileSystemEventHandler(this.OnChanged); // Create an initially empty map this.addressMap = new Dictionary(); // Load the configuration this.Load(); // Now start monitoring this.configFileWatcher.EnableRaisingEvents = true; } /// /// The mapping between domain to catchall address. /// public Dictionary AddressMap { get { return this.addressMap; } } /// /// Configuration changed handler. /// /// Event source. /// Event arguments. 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; } /// /// Load the configuration file. If any errors occur, does nothing. /// private void Load() { // Load the configuration XmlDocument doc = new XmlDocument(); bool docLoaded = false; string fileName = Path.Combine( this.configDirectory, CatchAllConfig.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 map = new Dictionary(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>(ref this.addressMap, map); Trace.WriteLine("accepted configuration"); } } } }