Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / CUDA

A C# SMTP server (receiver)

5.00/5 (14 votes)
17 Sep 2012CPOL9 min read 114.8K   5.6K  
A C# SMTP server (receiver).

Introduction   

The project (application) I'm presenting here is a relatively simple SMTP server (ok, only the receiver part, no mail routing or the like); while it started just as a tool to help developing SMTP sending applications and diagnosing issues, I then decided to evolve it by adding a number of features so that, as of today, the program can be used for the following purposes

  • Basic SMTP receiver, useful to test/diagnose SMTP sending problems 
  • Plain vanilla SMTP proxy, to receive email and store it inside some other SMTP router "pickup folder" for delivery 
  • Mail collector, to receive junk email/worms/similar stuff and feed email filter or antivirus signatures and help keeping junk out from inboxes
  • Fake MX, helping to reduce the amount of junk and the load on real MX servers 

and then, by the way, it may be used for whatever other purpose you'll brain will suggest (ok, I admit it, I don't think it will be useful as a bottle opener)

Background  

While this isn't the first SMTP application I wrote, this is the first one I wrote using .NET and C# in particular; my previous SMTP receiver was a program I wrote in regular C named "fakeMX", I wrote it after reading some documents dealing with the "MX sandwich" trick (see this, this and this); that application was an SMTP receiver as well and since it worked well, helping me (and a number of others) reducing the amount of junk hitting the mailservers ... I decided that writing a similar application in C# was worth, so I went on and wrote the application I'm presenting here.

Using the code  

The whole project was born after reading this article; I was seeking for a way to create an SMTP listener in C# and that code helped me getting started; so, I fired up VS-2010 and created a new console application, I selected it since it's easier to convert a console app into a window service (see here for infos) if desired (and in this case, it will be the perfect fit for the SMTP listener) and, at the same time, it's easy to debug such an app; anyhow, after creating the new project and having my startup class ready with its default "Main()" method, I left it alone and went on adding an "app.config" to the project; call me a dinosaur, but one of my old habits (since Win 3.x  !) is to start by putting together the configuration file for an application (be it an INI file or an "app.config" in our case) since, from direct experience, this helps thinking about the features you want to implement. In this case, I ended up with the following "app.config" file  

XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <!--
  * HostName        host name used for banner, null = retrieve it from network settings
  
  * ListenAddress:  address to listen on, 0.0.0.0=any (default=127.0.0.1)
  
  * ListenPort:     port # to listen on (default=25 SMTP)
  
  * ReceiveTimeOut  timeout after which a read operation fails (and session is dropped)
                    the default is 8000, that is, 8 seconds, if the client doesn't send
                    in commands or data, it will receive a 4xx "timeout" message and
                    the connection will be dropped; lower the value on very busy boxes,
                    raise it in case you have a slow connection
  
  * MaxSmtpErrors   max number of accepted SMTP errors (invalid commands ...) after reaching
                    this limit, the connection will be dropped with an error message

  * MaxSmtpNoop     max number of "NOOP" commands accepted, same behaviour as for max errors
  
  * MaxSmtpNoop     max number of "VRFY/EXPN" commands accepted, same behaviour as for max errors

  * MaxSmtpRcpt     max number of "RCPT TO" commands accepted, same behaviour as for max errors

  * MaxMessages     max number of messages accepted in a single session (default=10)

  * MaxSessions     max number of parallel sessions; once reached, incoming connections will
                    be rejected (immediately disconnected) until the number of session doesn't
                    drop under this limit, this is useful to avoid being DDoS-ed by a flock of
                    bots issuing a huge number of connection to our box
                    
  * StoreData       if true, the email envelope and DATA are stored into a temporary, unique
                    file, if DoTempFail is true, setting this value will cause the temp fail
                    to be sent out after the DATA has been received (otherwise it will be
                    sent out when receiving the DATA command, see also DoTempFail)

  * StorePath       path used to store the email data (as above), each message will be stored
                    into a file with a unique name, messages headers will the contain some
                    additional headers "X-FakeSMTP-..." containing the session and envelope
                    informations, the folder must be writable, it can be the same path used
                    for LogPath (if blank, will default to %TEMP%)
  
  * MaxDataSize     max size for a mail message (headers and data) only used if StoreData
                    is enabled; in this case, a message bigger than MaxDataSize will cause
                    a 4xx "quota" tempfail message to be returned to the client
                    
  * LogPath         path to store logfiles both related to general program operations and to
                    the sessions/emails; files are named using the current month # so you'll
                    have a max of 12 logfiles, older ones will be automatically overwritten
                    the folder must be writable, it can be the same path used for StorePath
                    (if blank, will default to %TEMP%)
  
  * VerboseLogging  if true, the logfile will also contain the command and replies, this is
                    useful when using the program to test a mail sending application or to
                    diagnose a mail issue (note: that the DATA part won't be logged)

  * BannerDelay     delay (milliseconds) before emitting the SMTP server initial "banner" this may
                    help to slowdown spamsending bots although rising the value too much may cause
                    problems since you may end up with a bunch of sessions waiting for the banner
                    for details see http://wiki.asrg.sp.am/wiki/Early_talker_detection
  
  * ErrorDelay      delay (milliseconds) to emit a response after an error, the delay is multiplied
                    by the errors count so, the more errors the higher the delay (up to max errors)

  * DoTempFail      if true, at or after the DATA command (see StoreData) the server will emit a 
                    4xx tempfail message and drop the connection, this is useful if you want to
                    use the program to setup an "MX sandwich" (aka "nolisting")
                    
  * DoEarlyTalk     if true, enables checking for the so called "early talkers" that is SMTP
                    senders which don't wait for the server banner or reply but keep sending
                    in commands/data; those clients are usually spambots and enabling this check
                    will reject them (checks are performed before both the banner and each reply
                    are sent out to the remote client)
                    
  * RWLproviders    comma separated list of DNS whitelist providers against which the incoming
                    IP is checked; if listed, the blacklist checks will be skipped; set this to
                    null to disable this check
  
  * RBLproviders    comma separated list of DNS blacklist providers against which the incoming
                    IP is checked; if listed and if StoreData is disabled, the connection will
                    be dropped with a tempfail (4xx) error message, set this to null to disable
                    this check
                    
  * LocalDomains    pathname of a text file containing the list of locally handled domains, one
                    on each line; if empty, all domains will be accepted, otherwise the program
                    will emit a "rely denied" error in case an "RCPT TO" targets a domain which
                    isn't included in this list
                    
  * LocalMailBoxes  pathname of a text file containing the list of locall handled email addresses
                    one on each line; if empty all addresses will be accepted, otherwise the program
                    will emit an "invalid address" if an "RCPT TO" targets an address which isn't
                    included in this list
  -->
  <appSettings>
    <add key="HostName" value=""/>
    <add key="ListenAddress" value="127.0.0.1"/>
    <add key="ListenPort" value="25"/>
    <add key="ReceiveTimeOut" value="8000"/>
    <add key="MaxSmtpErrors" value="4"/>
    <add key="MaxSmtpNoop" value="7"/>
    <add key="MaxSmtpVrfy" value="10"/>
    <add key="MaxSmtpRcpt" value="100"/>
    <add key="MaxMessages" value="10"/>
    <add key="MaxSessions" value="16"/>
    <add key="StoreData" value="True"/>
    <add key="StorePath" value=""/>
    <add key="MaxDataSize" value="2097152"/>
    <add key="LogPath" value=""/>
    <add key="VerboseLogging" value="False"/>
    <add key="BannerDelay" value="1000"/>
    <add key="ErrorDelay" value="500"/>
    <add key="DoTempFail" value="False"/>
    <add key="DoEarlyTalk" value="True"/>
    <add key="RWLproviders" value="swl.spamhaus.org,iadb.isipp.com"/>
    <add key="RBLproviders" value="zen.spamhaus.org,bb.barracudacentral.org,
                 ix.dnsbl.manitu.net,bl.spamcop.net,combined.njabl.org"/>
    <add key="LocalDomains" value=""/>
    <add key="LocalMailBoxes" value=""/>
  </appSettings>
</configuration>

The comments I left inside the file should be pretty much explanatory, but since we're just at the beginning, I'll get back on some of those settings later on;  for the moment, least suffice saying that by changing the settings inside the above file you'll be able to tune the program for different purposes (as seen at the beginning).  

Anyhow, once I had the above config ready, I went on and added a new public, static class to the project, it's used to keep the configuration values (so that we'll have them handy) and also contains some common data/code, like, for example, the one used to assign a unique SMTP sessionID or to keep track of the number of active SMTP sessions; here's a snippet from the class code.

C#
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.IO;

namespace FakeSMTP
{
    public static class AppGlobals
    {
        #region "privateData"
        private static IPAddress    _listenIP = IPAddress.Loopback;

        private static string       _listenAddress = null;
        private static int          _listenPort = 0;
        private static int          _receiveTimeout = 0;
        private static string       _hostName = null;
        private static bool         _doTempFail = false;
        private static string       _logPath = null;
        private static bool         _verboseLog = false;
        private static long         _maxSessions = 0;
        private static int          _maxMessages = 0;
        private static bool         _storeData = false;
        private static long         _maxDataSize = 0;
        private static string       _storePath = null;
        private static bool         _earlyTalk = false;
        private static string[]     _whiteLists = null;
        private static string[]     _blackLists = null;
        private static int          _maxSmtpErr = 0;
        private static int          _maxSmtpNoop = 0;
        private static int          _maxSmtpVrfy = 0;
        private static int          _maxSmtpRcpt = 0;
        private static int          _bannerDelay = 0;
        private static int          _errorDelay = 0;

        private static List<string> _localDomains = null;
        private static List<string> _localMailBoxes = null;        

        // sessions count
        private static object       _lkSessions = new object();
        private static long         _sessions = 0;

        private static object       _lkSessID = new object();
        private static long         _sessID = 0;
        #endregion

as you probably noticed, most private vars above match the configuration parameters and that's exactly the case, since they're exposed through properties like

C#
// listen IP
public static IPAddress listenIP
{
    get { return _listenIP; }
    set { _listenIP = value; }
}

// listen address (as a string)
public static string listenAddress
{
    get { return _listenAddress; }
    set { _listenAddress = value; }
}

// listen port
public static int listenPort
{
    get { return _listenPort; }
    set { _listenPort = value; }
}

// timeout for receiving commands/data (milliseconds)
public static int receiveTimeout
{
    get { return _receiveTimeout; }
    set { _receiveTimeout = value; }
}

// host name (used for banner, if blank retrieved from network settings)
public static string hostName
{
    get { return _hostName; }
    set { _hostName = value; }
}

which allow to set and retrieve their values; at this point, having the config file and "global stuff" in place, I went on and added to my main class some code to load/parse the configuration values from file and populate the class, so in the main class we have something like

C#
static void loadConfig()
{
    // listen address
    IPAddress listenIP = IPAddress.Loopback;
    string listenAddress = ConfigurationManager.AppSettings["ListenAddress"];
    if (String.IsNullOrEmpty(listenAddress)) listenAddress = "127.0.0.1";
    if (false == IPAddress.TryParse(listenAddress, out listenIP))
    {
        listenAddress = "127.0.0.1";
        listenIP = IPAddress.Loopback;
    }

    // listen port
    int listenPort = int.Parse(ConfigurationManager.AppSettings["ListenPort"]);
    if ((listenPort < 1) || (listenPort > 65535))
        listenPort = 25;

    // receive timeout
    int receiveTimeout = int.Parse(ConfigurationManager.AppSettings["ReceiveTimeOut"]);
    if (receiveTimeout < 0)
        receiveTimeout = 0;

    // hostname (for the banner)
    string hostName = ConfigurationManager.AppSettings["HostName"];
    if (string.IsNullOrEmpty(hostName))
        hostName = System.Net.Dns.GetHostEntry("").HostName;

    // true=emits a "tempfail" when receiving the DATA command
    bool doTempFail = bool.Parse(ConfigurationManager.AppSettings["DoTempFail"]);

the "loadConfig()" method above proceeds loading the various config values from file, checking if they're ok (and if not turning them into some viable defaults) and then storing them into our "AppGlobals" class

C#
// set the global values
AppGlobals.listenIP = listenIP;
AppGlobals.listenAddress = listenAddress;
AppGlobals.listenPort = listenPort;
AppGlobals.receiveTimeout = receiveTimeout;
AppGlobals.hostName = hostName.ToLower();
AppGlobals.doTempFail = doTempFail;
AppGlobals.storeData = storeData;
AppGlobals.maxDataSize = storeSize;

done that, we'll now need to setup our listener and handle incoming SMTP connections and, since I wanted to keep the listener and the session handler separate, I decided to add another class to the project, namely the "SMTPsession" one; this class encapsulates all the logic needed to handle an SMTP session between the client and our "server" and takes care of storing and logging the session informations (and, if so configured, the received messages); the main functions of this class are its constructor, that is

C#
public SMTPsession(TcpClient client)
{
    try
    {
        this._sessCount = AppGlobals.addSession();
        this._sessionID = AppGlobals.sessionID();
        this._hostName = AppGlobals.hostName;

        if (null != AppGlobals.LocalDomains)
            this._mailDomains = AppGlobals.LocalDomains;
        if (null != AppGlobals.LocalMailBoxes)
            this._mailBoxes = AppGlobals.LocalMailBoxes;

        this._client = client;
        this._clientIP = this._client.Client.RemoteEndPoint.ToString();
        int i = this._clientIP.IndexOf(':');
        if (-1 != i) this._clientIP = this._clientIP.Substring(0, i);
        this._client.ReceiveTimeout = AppGlobals.receiveTimeout;

        this._stream = this._client.GetStream();
        this._reader = new StreamReader(this._stream);
        this._writer = new StreamWriter(this._stream);
        this._writer.NewLine = "\r\n";
        this._writer.AutoFlush = true;

        AppGlobals.writeConsole("client {0} connected, sess={1}, ID={2}.", 
                   this._clientIP, this._sessCount, this._sessionID);
        this._initOk = true;
    }
    catch (Exception ex)
    {
        AppGlobals.writeConsole("SMTPsession::Exception: " + ex.Message);
        closeSession();
    }
}

which initializes the instance of the class and is called by the "Main()" whenever there's an incoming connection and the real worker, that is the "handleSession()" function

C#
public void handleSession()
{
    string cmdLine = "?";
    string response = cmd_ok(null);
    cmdID  currCmd = cmdID.invalid;
    bool   connOk = true;

    if (false == this._initOk)
    {
        closeSession();
        return;
    }

    // sessions limit reached, reject session
    if (this._sessCount > AppGlobals.maxSessions)
    {
        if (connOk) sendLine(TEMPFAIL_MSG);
        closeSession();
        return;
    }

    // if the remote IP isn't a private one
    if (!isPrivateIP(this._clientIP))
    {
        // checks the incoming IP against whitelists, if listed skip blacklist checks
        bool isDnsListed = isListed(this._clientIP, AppGlobals.whiteLists, "white");
        if (!isDnsListed)
        {
            // check the IP against blacklists
            isDnsListed = isListed(this._clientIP, AppGlobals.blackLists, "black");
            if ((isDnsListed) && (!AppGlobals.storeData))
            {
                // if blacklisted and NOT storing messages
                sendLine(string.Format(DNSBL_MSG, this._clientIP, this._dnsListName));
                closeSession();
                return;
            }
        }
    }

    // add a short delay before banner and check for early talker
    // see http://wiki.asrg.sp.am/wiki/Early_talker_detection
    sleepDown(AppGlobals.bannerDelay);
    this._earlyTalker = isEarlyTalker();
    if (this._earlyTalker)
    {
        sendLine(ETALKER_MSG);
        closeSession();
        return;
    }

    // all ok, send out our banner            
    connOk = sendLine(cmd_banner(null));
    while ((null != cmdLine) && (true == connOk))
    {
        if (this._lastCmd == cmdID.data)
        {
            string mailMsg = recvData();
            if (this._timedOut)
            {
                // got a receive timeout during the DATA phase
                if (connOk) sendLine(TIMEOUT_MSG);
                closeSession();
                return;
            }
            response = cmd_dot(null);
            if (String.IsNullOrEmpty(mailMsg))
                response = "422 Recipient mailbox exceeded quota limit.";
            else
            {
                storeMailMsg(mailMsg);
                if (AppGlobals.doTempFail) 
                {
                    // emit a tempfail AFTER storing the mail DATA
                    if (connOk) sendLine(TEMPFAIL_MSG);
                    closeSession();
                    return;
                }
            }
            resetSession();
        }
        else
        {
            // read an SMTP command line and deal with the command
            cmdLine = recvLine();
            if (null != cmdLine)
            {
                logCmdAndResp(DIR_RX, cmdLine);
                currCmd = getCommandID(cmdLine);
                switch (currCmd)
                {
                    case cmdID.helo:            // HELO
                        response = cmd_helo(cmdLine);
                        break;
                    case cmdID.ehlo:            // EHLO
                        response = cmd_helo(cmdLine);
                        break;
                    case cmdID.mailFrom:        // MAIL FROM:
                        response = cmd_mail(cmdLine);
                        break;
                    case cmdID.rcptTo:          // RCPT TO:
                        response = cmd_rcpt(cmdLine);
                        break;
                    case cmdID.data:            // DATA
                        if ((AppGlobals.doTempFail) && (!AppGlobals.storeData))
                        {
                            // emit a tempfail upon receiving the DATA command
                            response = TEMPFAIL_MSG;
                            cmdLine = null;
                            this._lastCmd = currCmd = cmdID.quit;
                        }
                        else
                            response = cmd_data(cmdLine);
                        break;
                    case cmdID.rset:            // RSET
                        response = cmd_rset(cmdLine);
                        break;
                    case cmdID.quit:            // QUIT
                        response = cmd_quit(cmdLine);
                        cmdLine = null; // force closing
                        break;
                    case cmdID.vrfy:            // VRFY
                        response = cmd_vrfy(cmdLine);
                        break;
                    case cmdID.expn:            // EXPN
                        response = cmd_vrfy(cmdLine);
                        break;
                    case cmdID.help:            // HELP
                        response = cmd_help(cmdLine);
                        break;
                    case cmdID.noop:            // NOOP
                        response = cmd_noop(cmdLine);
                        break;
                    default:                    // unkown/unsupported
                        response = cmd_unknown(cmdLine);
                        break;
                }
            }
            else
            {
                // the read timed out (or we got an error), emit a message and drop the connection
                response = TIMEOUT_MSG;
                currCmd = cmdID.quit;
            }
        }

        // send response
        if ((this._errCount > 0) && (cmdID.quit != currCmd))
        {
            // tarpit a bad client, time increases with error count
            sleepDown(AppGlobals.errorDelay * this._errCount);
        }
        else
        {
            // add a short delay
            sleepDown(25);
        }

        // checks for early talkers
        this._earlyTalker = isEarlyTalker();

        // send out the response
        connOk = sendLine(response);

        // check/enforce hard limits (errors, vrfy ...)
        if ((cmdID.quit != currCmd) && (connOk))
        {
            string errMsg = null;
            if (this._msgCount > AppGlobals.maxMessages)
            {
                // above max # of message in a single session
                errMsg = "451 Session messages count exceeded";
            } 
            else if (this._errCount > AppGlobals.maxSmtpErr)
            {
                // too many errors
                errMsg = "550 Max errors exceeded";
            }
            else if (this._vrfyCount > AppGlobals.maxSmtpVrfy)
            {
                // tried to VRFY/EXPN too many addresses
                errMsg = "451 Max recipient verification exceeded";
            }
            else if (this._noopCount > AppGlobals.maxSmtpNoop)
            {
                // entered too many NOOP commands
                errMsg = "451 Max NOOP count exceeded";
            }
            else if (this._rcptTo.Count > AppGlobals.maxSmtpRcpt)
            {
                // too many recipients for a single message
                errMsg = "452 Too many recipients";
            }
            else if (this._earlyTalker)
            {
                // early talker
                errMsg = ETALKER_MSG;
            }
            if (null != errMsg)
            {
                if (connOk) connOk = sendLine(errMsg);
                cmdLine = null; // force closing
            }
        }

        // check if connection Ok
        if (connOk) connOk = this._client.Connected;
    } // while null...

    // close/reset this session
    closeSession();
}

Which is the real workhorse, it deals with all the SMTP protocol rules, handing commands and responses and, in practice, handing the whole client session from its start to its end; this functions also takes care of enforcing the limits we configured (max errors, bad clients and so on); the class then contains a number of helper functions used to handle the different SMTP commands supported by the application, for example, the "rcpt_to" command is handled by this function

C#
// RCPT TO:
private string cmd_rcpt(string cmdLine)
{
    if (string.IsNullOrEmpty(this._mailFrom))
    {
        this._errCount++;
        return "503 Need MAIL before RCPT";
    }
    List<string> parts = parseCmdLine(cmdID.rcptTo, cmdLine);
    if (2 != parts.Count)
    {
        this._errCount++;
        return String.Format("501 {0} needs argument", parts[0]);
    }
    if (!checkMailAddr(parts[1]))
    {
        this._errCount++;
        return String.Format("553 Invalid address {0}", parts[1]);
    }

    if (!isLocalDomain(this._mailDom))
    {
        // relaying not allowed...
        this._errCount++;
        return "530 Relaying not allowed for policy reasons";
    }
    else if (!isLocalBox(this._mailBox, this._mailDom))
    {
        // unkown/invalid recipient
        this._errCount++;
        return String.Format("553 Unknown email address {0}", parts[1]);
    }

    this._rcptTo.Add(parts[1]);
    this._lastCmd = cmdID.rcptTo;
    return string.Format("250 {0}... Recipient ok", parts[1]);
}

which performs a number of basic checks on the command, increases the error count if needed and, in any case, returns the response to be sent back to the client; this class also handles the tasks of logging the various messages sent by the client and, if so configured, of saving them to files.

At this point, the app was almost complete, all I needed was editing the "Main()" function and adding the code needed to setup the listener, accept incoming connections and spawn a thread running an instance of the "SMTPsession" to handle that given client, and the "Main()", once filled up with such code, ended up like this

C#
static int Main(string[] args)
{
    // our internal stuff
    IPAddress listenAddr = IPAddress.Loopback;
    int listenPort = 25;
    int retCode = 0;

    // load the config
    loadConfig();

    // tell we're starting up and, if verbose, dump config parameters
    AppGlobals.writeConsole("{0} {1} starting up (NET {2})", 
       AppGlobals.appName, AppGlobals.appVersion, AppGlobals.appRuntime);
    if (AppGlobals.logVerbose)
        dumpSettings();            

    // setup the listening IP:port
    listenAddr = AppGlobals.listenIP;
    listenPort = AppGlobals.listenPort;

    // try starting the listener
    try
    {
        listener = new TcpListener(listenAddr, listenPort);
        listener.Start();
    }
    catch (Exception ex)
    {
        AppGlobals.writeConsole("Listener::Error: " + ex.Message);
        return 1;
    }

    // tell we're ready to accept connections
    AppGlobals.writeConsole("Listening for connections on {0}:{1}", listenAddr, listenPort);

    // run until interrupted (Ctrl-C in our case)
    while (!timeToStop)
    {
        try
        {
            // wait for an incoming connection, accept it and spawn a thread to handle it
            SMTPsession handler = new SMTPsession(listener.AcceptTcpClient());
            Thread thread = new System.Threading.Thread(new ThreadStart(handler.handleSession));
            thread.Start();
        }
        catch (Exception ex)
        {
            // we got an error
            retCode = 2;
            AppGlobals.writeConsole("Handler::Error: " + ex.Message);
            timeToStop = true;
        }
    }

    // finalize
    if (null != listener)
    {
        try { listener.Stop(); }
        catch { }
    }
    return retCode;
}

Nothing special, isn't it ? The main method really does little more than loading the configuration, creating the listening socket and passing each incoming connection to an instance of SMTPsession running inside its own thread; the latter will then take care of checking if we are dealing with too many sessions (and drop the exceeding ones), of setting up a timeout so that we won't keep waiting for a client which doesn't send us any command and, in any case, of dealing with the nuts and bolts of the SMTP session.

Points of Interest 

As I wrote at the beginning, thanks to the configuration parameters, you may use the application as a vanilla SMTP receiver which will just accept connections from whatever client, deal with the SMTP commands, log the session and store the sent email messages, or, if you want to have some more fun, you may use the application to help you fighting spammers; to do so, you may want to set it up to act as a "fake MX" sitting inside an "MX sandwich", to setup such a thing, you'll need to have your own domain and, at least, an MX server dealing with the SMTP traffic; let's say that you own the domain named "example.com" and that the DNS zone for such a domain looks this way

$ORIGIN example.com.

@            IN SOA ns1.example.com. root.example.com. (
                    2012080968 ; serial
                    7200       ; refresh (2 hours)
                    3600       ; retry (1 hour)
                    1209600    ; expire (2 weeks)
                    3600       ; minimum (1 hour)
                    )

@            IN NS      ns1.example.com
@            IN NS      ns2.example.com

@            IN A       192.0.2.25
mx1          IN A       192.0.2.25
www          IN A       192.0.2.80
ftp          IN CNAME   www.example.com.

@            IN TXT     "v=spf1 a mx -all"

@            IN MX 20   mx1.example.com.

Now, let's leave the various records alone and focus on the ones related to MX, that is, the "mx1.example.com" and the MX record with preference 20 pointing to it; with such a setup, willing to implement the "MX sandwich" we'll need to have a couple of additional IP addresses, one on which there isn't (and there will never be) something listening on port 25 and a second one on which we'll setup our "Fake MX" using the FakeSMTP program; let's say we will use 192.0.2.1 as the first (no SMTP) IP address and 192.0.2.254 as the second one, on which the FakeSMTP will listen; we'll then start by installing the program on whatever box and changing the config file as follows:

XML
<appSettings>
    <add key="HostName" value=""/>
    <add key="ListenAddress" value="0.0.0.0"/>
    <add key="ListenPort" value="25"/>
    <add key="ReceiveTimeOut" value="8000"/>
    <add key="MaxSmtpErrors" value="4"/>
    <add key="MaxSmtpNoop" value="7"/>
    <add key="MaxSmtpVrfy" value="10"/>
    <add key="MaxSmtpRcpt" value="100"/>
    <add key="MaxMessages" value="10"/>
    <add key="MaxSessions" value="32"/>
    <add key="StoreData" value="False"/>
    <add key="StorePath" value="X:\FakeSMTP\Data"/>
    <add key="MaxDataSize" value="2097152"/>
    <add key="LogPath" value="X:\FakeSMTP\Logs"/>
    <add key="VerboseLogging" value="False"/>
    <add key="BannerDelay" value="1500"/>
    <add key="ErrorDelay" value="750"/>
    <add key="DoTempFail" value="True"/>
    <add key="DoEarlyTalk" value="True"/>
    <add key="RWLproviders" value="swl.spamhaus.org,iadb.isipp.com"/>
    <add key="RBLproviders" value="zen.spamhaus.org,bb.barracudacentral.org,ix.dnsbl.manitu.net,bl.spamcop.net,combined.njabl.org"/>
    <add key="LocalDomains" value=""/>
    <add key="LocalMailBoxes" value=""/>
</appSettings>

with the above configuration, the program will answer with a ""421 Service temporarily unavailable, closing transmission channel." message as soon as it receives the SMTP "DATA"  command (if you prefer, you may set the StoreData option to True, in such a case, the same message will be emitted after receiving and storing the mail message, this may be useful in case you want to use such junk messages to feed a spamfilter, but be warned, those may take up quite a bunch of storage space); once the app will be up and running (you may test it by using e.g. "telnet") you'll be ready to publish it, so, after opening your firewall to allow incoming connections to 192.0.2.254:25, you'll go on and change your DNS zone as follows:

mx0          IN A       192.0.2.1
mx1          IN A       192.0.2.25
mx9          IN A       192.0.2.254

@            IN MX 10   mx0.example.com.
@            IN MX 20   mx1.example.com.
@            IN MX 50   mx9.example.com.

That is, adding the relevant "A" and "MX" records pointing to the "no receiver" and to the "fake receiver" addresses; what will happen then is simple, a regular, "good" SMTP sender will just follow the RFCs and, willing to send you an email, will retrieve the list of MX records and try contacting the first one in preference order to deliver its message, now, we know that the first MX is "filtered" so the sender will get back a "connection error" and, following the RFCs it will immediately retry sending the message to the next MX in order, that is, to your REAL MX server which will possibly accept it... all's well... but what about spammers/spambots ? Well, those try playing tricks to speed up delivery and/or to try bypassing spam filters so, they either try going straight to the higher MX in preference order or to go to the first and, if that fails, to the last... and I think you can imagine what happens then  

Notes

I'll admit, I'm a "rookie" when it comes to .NET and C#, so the code isn't exactly a good example of programming since I tried to keep things simple/clear (mostly for myself) and avoiding some tricks which I'd otherwise use (e.g. in vanilla "C") so, the whole code has quite some room for improvement, for example, the code checking the HELO/EHLO and the email addresses may be improved using regular expressions; other portions of the code may be rewritten to optimize them and... well ... more;  I didn't go on and do it just since the program, as is served my purpose, that is, putting together something to help me learning C# and at the same time... something which may be useful and, I hope, it will be useful for someone else too. 

Updates

The latest version (the downloadable one) contains a small code fix; the logging code was updated to use a "lock(...)" statement whenever logging to the application or session logs, this helps avoiding race conditions and properly serializing log writes; a better approach would have been adding a separate logging class implementing a queue, running a couple instances of such a class in two separate threads and then using them to log messages; this would dramatically speed up logging and avoid problems (the same may be applied to the code used to save email files)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)