C# SMTP 服务器(接收器)





5.00/5 (13投票s)
一个 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(...)”语句,这有助于避免竞争条件并正确序列化日志写入;更好的方法是添加一个实现队列的独立日志记录类,在两个单独的线程中运行该类的两个实例,然后使用它们来记录消息;这将极大地加快日志记录速度并避免问题(同样的方法也可以应用于用于保存电子邮件文件的代码)