65.9K
CodeProject 正在变化。 阅读更多。
Home

C# SMTP 服务器(接收器)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2012 年 9 月 10 日

CPOL

9分钟阅读

viewsIcon

116261

downloadIcon

5696

一个 C# SMTP 服务器(接收器)。

引言   

我在此介绍的项目(应用程序)是一个相对简单的 SMTP 服务器(好吧,只有接收部分,没有邮件路由等功能);它最初只是作为一个工具来帮助开发 SMTP 发送应用程序和诊断问题,但我后来决定通过添加许多功能来对其进行改进,因此,截至今天,该程序可用于以下目的:

  • 基本 SMTP 接收器,用于测试/诊断 SMTP 发送问题
  • 普通的 SMTP 代理,用于接收电子邮件并将其存储在其他 SMTP 路由器“取件文件夹”中以供投递
  • 邮件收集器,用于接收垃圾邮件/蠕虫/类似内容,并为电子邮件过滤器或防病毒签名提供数据,有助于将垃圾邮件拒之收件箱之外
  • 虚假 MX,有助于减少垃圾邮件的数量和真实 MX 服务器的负载

此外,它还可以用于您大脑会建议的任何其他目的(好吧,我承认,我不认为它可以用作开瓶器)

背景  

虽然这不是我编写的第一个 SMTP 应用程序,但这是我使用 .NET 和 C# 编写的第一个应用程序;我之前的 SMTP 接收器是我用常规 C 编写的程序,名为“fakeMX”,我是在阅读一些关于“MX 三明治”技巧的文档(参见此处此处此处)后编写的;该应用程序也是一个 SMTP 接收器,由于它运行良好,帮助我(和许多其他人)减少了发送到邮件服务器的垃圾邮件数量……所以我决定用 C# 编写一个类似的应用程序是值得的,于是我就编写了此处介绍的应用程序。

使用代码

整个项目是在阅读这篇文章后诞生的;我当时正在寻找一种在 C# 中创建 SMTP 侦听器的方法,那段代码帮助我入门;于是,我启动了 VS-2010 并创建了一个新的控制台应用程序,我选择它是因为将控制台应用程序转换为 Windows 服务更容易(有关信息,请参阅此处),如果需要(在这种情况下,它将非常适合 SMTP 侦听器),同时,调试此类应用程序也很容易;无论如何,在创建新项目并准备好带有默认“Main()”方法的启动类后,我将其搁置,并继续向项目添加“app.config”;您可以称我为老古董,但我的老习惯之一(自 Win 3.x 以来!)是先为应用程序(无论是 INI 文件还是我们案例中的“app.config”)准备配置文件,因为根据我的直接经验,这有助于思考您想要实现的功能。在这种情况下,我得到了以下“app.config”文件

<?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>

我在文件中留下的注释应该非常清楚,但由于我们才刚刚开始,我稍后会回到其中一些设置;目前,只需说通过更改上述文件中的设置,您就可以根据不同的目的(如开头所见)调整程序。 

无论如何,一旦我准备好上述配置,我便继续向项目添加了一个新的公共静态类,它用于保存配置值(以便我们随时可以使用它们),并且还包含一些常用数据/代码,例如,用于分配唯一 SMTP 会话 ID 或跟踪活动 SMTP 会话数量的代码;以下是该类代码的片段。

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

正如你可能注意到的,上面大多数私有变量都与配置参数匹配,这正是如此,因为它们通过属性公开,例如

// 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; }
}

它允许设置和检索它们的值;至此,有了配置文件和“全局内容”,我继续向我的主类添加了一些代码,用于从文件中加载/解析配置值并填充该类,因此在主类中我们有类似以下内容:

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"]);

上面的“loadConfig()”方法继续从文件中加载各种配置值,检查它们是否正常(如果不是,则将其转换为一些可行的默认值),然后将它们存储到我们的“AppGlobals”类中。

// 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;

完成这些后,我们现在需要设置我们的侦听器并处理传入的 SMTP 连接,并且,由于我想将侦听器和会话处理程序分开,我决定向项目中添加另一个类,即“SMTPsession”类;该类封装了处理客户端和我们的“服务器”之间 SMTP 会话所需的所有逻辑,并负责存储和记录会话信息(如果配置了,还包括收到的消息);该类的主要函数是其构造函数,即

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();
    }
}

它初始化类的实例,并在有传入连接时由“Main()”调用,而真正的工人则是“handleSession()”函数

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();
}

这是真正的主力,它处理所有 SMTP 协议规则,处理命令和响应,实际上,从开始到结束处理整个客户端会话;此功能还负责强制执行我们配置的限制(最大错误、不良客户端等等);该类包含许多辅助函数,用于处理应用程序支持的不同 SMTP 命令,例如,“rcpt_to”命令由以下函数处理

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

它对命令执行一系列基本检查,如果需要则增加错误计数,并在任何情况下返回要发送回客户端的响应;此类别还处理记录客户端发送的各种消息的任务,如果配置了,则将其保存到文件中。

至此,应用程序几乎完成,我所需要做的就是编辑“Main()”函数,并添加设置侦听器、接受传入连接以及生成一个运行“SMTPsession”实例的线程以处理给定客户端所需的代码,而“Main()”在填充了这些代码后,最终变成了这样:

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;
}

没什么特别的,不是吗?主方法实际上只是加载配置,创建侦听套接字,并将每个传入连接传递给在自己的线程中运行的 SMTPsession 实例;后者将负责检查我们是否正在处理过多的会话(并丢弃多余的会话),设置超时以避免我们一直等待不发送任何命令的客户端,并在任何情况下处理 SMTP 会话的细节。

兴趣点 

正如我一开始所写,由于配置参数,您可以将应用程序用作普通的 SMTP 接收器,它只会接受来自任何客户端的连接,处理 SMTP 命令,记录会话并存储发送的电子邮件消息,或者,如果您想获得更多乐趣,您可以使用该应用程序来帮助您打击垃圾邮件发送者;为此,您可能希望将其设置为充当“MX 三明治”中的“假 MX”,要设置这样的东西,您需要拥有自己的域,并且至少有一个 MX 服务器处理 SMTP 流量;假设您拥有名为“example.com”的域,并且该域的 DNS 区域如下所示:

$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.

现在,我们暂时不管其他记录,只关注与 MX 相关的记录,即“mx1.example.com”以及指向它的优先级为 20 的 MX 记录;通过这样的设置,如果想实现“MX 三明治”,我们需要另外两个 IP 地址,一个是没有(并且永远不会有)端口 25 上监听的,另一个是我们将使用 FakeSMTP 程序设置我们的“假 MX”的;假设我们使用 192.0.2.1 作为第一个(无 SMTP)IP 地址,使用 192.0.2.254 作为第二个(FakeSMTP 将在此地址监听);然后我们将通过在任何一台计算机上安装该程序并按如下方式更改配置文件来开始:

<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>

通过上述配置,程序一收到 SMTP "DATA" 命令,就会以 ""421 服务暂时不可用,正在关闭传输通道。" 消息回复(如果您愿意,可以将 StoreData 选项设置为 True,在这种情况下,在收到并存储邮件消息后,也会发出相同的消息,这在您希望使用此类垃圾消息来为垃圾邮件过滤器提供数据时可能有用,但请注意,这可能会占用相当大的存储空间);一旦应用程序启动并运行(您可以使用例如 "telnet" 进行测试),您就可以发布它了,因此,在打开防火墙以允许传入连接到 192.0.2.254:25 后,您将继续并按如下方式更改您的 DNS 区域:

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.

也就是说,添加指向“无接收器”和“假接收器”地址的相关“A”和“MX”记录;那么接下来会发生什么很简单,一个正常的“良好”SMTP 发送者只会遵循 RFC,并且想要向您发送电子邮件时,会检索 MX 记录列表并尝试按优先级顺序联系第一个以传递其消息,现在,我们知道第一个 MX 是“过滤过的”,所以发送者会收到“连接错误”,并且遵循 RFC,它会立即重试将消息发送到下一个 MX,即您的真实 MX 服务器,该服务器可能会接受它……一切都很好……但是垃圾邮件发送者/垃圾邮件机器人呢?好吧,他们会尝试耍花招来加快投递速度和/或尝试绕过垃圾邮件过滤器,所以他们要么尝试直接前往优先级较高的 MX,要么前往第一个,如果失败,则前往最后一个……我想您可以想象接下来会发生什么

注释

我承认,在 .NET 和 C# 方面我是一个“新手”,所以代码并不完全是一个好的编程示例,因为我尝试将事情保持简单/清晰(主要是为了我自己),并避免了一些我 otherwise 会使用的技巧(例如在普通的“C”中),所以,整个代码还有很大的改进空间,例如,检查 HELO/EHLO 和电子邮件地址的代码可以使用正则表达式进行改进;代码的其他部分可以重写以进行优化,而且……嗯……更多;我没有继续这样做只是因为程序目前为止达到了我的目的,那就是,汇集一些东西来帮助我学习 C#,同时……一些可能有用的东西,我希望它对其他人也有用。

更新

最新版本(可下载版本)包含一个小代码修复;日志记录代码已更新为在记录到应用程序或会话日志时使用“lock(...)”语句,这有助于避免竞争条件并正确序列化日志写入;更好的方法是添加一个实现队列的独立日志记录类,在两个单独的线程中运行该类的两个实例,然后使用它们来记录消息;这将极大地加快日志记录速度并避免问题(同样的方法也可以应用于用于保存电子邮件文件的代码)

© . All rights reserved.