// ***************************************************************
//
// 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");
}
}
}
}