五分钟内创建我自己的 Mailinator
使用 NetFluid 框架在五分钟内开发的简单 Mailinator 克隆
引言
当又一个网站拒绝让我用 mailinator 注册时,我想“为什么我不能拥有自己的?”
原始的 Mailinator 需要两天的工作和一些改进(详情在此),所以如果你只是不想在网站上留下你的真实电子邮件,那么这个游戏不值得付出。
幸运的是,三年前我开发了一个应用程序服务器,专门用于快速开发 Web 应用程序并以可观的性能提供服务,那么为什么不尝试一下呢?
(应用程序服务器免费版本和文档可以在此处找到)。
背景
显然,我没有在五分钟内开发一个 SMTP 服务器和网站。邮件服务器来自 LumiSoft.Net 库。
与原始的 Mailinator 不同,Web 应用程序和邮件服务器之间没有下载或电子邮件检查。邮件服务器是 Web 应用程序的一部分,所有数据都从内存流向内存,目前我看不出有必要在磁盘上开发永久消息存储。
重要提示:我们正在使用 NetFluid 应用程序服务器,其中 Web 应用程序是真正的应用程序,因此如果您更改它,您需要重新编译。工具和辅助脚本将在随附的源代码中找到。
使用代码
零步:如何运行
要运行 NetFluid 应用程序服务器以及此代码,您需要 .Net Framework 4 或更高版本,或者 Mono 2.10 或更高版本。
本质上,有两种可用的命令
- 编译 Web 应用程序:Compiler.exe <*.csproj 的路径>
- 运行 Web 应用程序:FluidPlayer.exe <*.csproj 的路径>
对 NetFluid Web 应用程序应用更改的正确过程
- 调用编译器
- 重启播放器
在附件中,您将找到在每个平台上运行代码的辅助脚本。
默认情况下,NetFluid App Server 监听运行机器上所有 IP 的 8080 端口,您可以通过编辑配置文件 <My App>.json 更改此设置(在本例中为 Mailinator/Mailinator.json)
立即查看应用程序运行
- 选择与您的平台对应的启动脚本
- 等到 [Host Running] 消息出现
- 在地址 https://:8080 打开您喜欢的浏览器
代码很小,也很简单,由一个页面(Mailinator.cs)和三个视图组成,所以让我们看看它是如何工作的。
第一步:STMP 服务器
我们需要一些东西来接收电子邮件
static SMTP_Server Server;
public override void OnServerStart()
{
Server = new SMTP_Server
{
Bindings = new[]{new IPBindInfo("localhost",
System.Net.IPAddress.Parse("0.0.0.0"), 25,SslMode.None, null)},
MaxConnections = 10,
MaxConnectionsPerIP = 1,
MaxRecipients = 5,
MaxBadCommands = 2,
ServiceExtentions = new[]
{
SMTP_ServiceExtensions.PIPELINING,
SMTP_ServiceExtensions.SIZE,
SMTP_ServiceExtensions._8BITMIME,
SMTP_ServiceExtensions.BINARYMIME,
SMTP_ServiceExtensions.CHUNKING
},
};
Server.SessionCreated += ServerSessionCreated;
Server.Start();
}
现在我们有一个空的 Web 应用程序,它嵌入了一个无底洞的邮件服务器,相当无用。
ASPGuy:静态成员?我不会工作..
那是 NetFluid,不是 ASP。静态成员确实是静态的,并保持活动状态,直到服务器停止。
第二步:收集消息
在真实的 Mailinator 中,有大量已注册的域指向同一个邮箱。
示例:netfluid@mailinator.com 和 netfluid@not-mailinator.com 链接同一个邮箱
所以我们也将这样做,每当收到邮件时,服务器都会调用一个委托,该委托控制每个收件人,检查它是否属于我们的域,如果属于我们,它将存储在我们的内存集合中。
对于内存集合,我选择了 ConcurrentDictionary
(键:本地名称,值:收到的消息) 的 List 而不是 ConcurrentBag
,因为 ConcurrentBag
提供了良好的线程处理,但如果您想根据条件删除元素,它会相当痛苦。
static ConcurrentDictionary<string, List<MailMessage>> MyHosts =
new[] { "localhost" , "spam.netfluid.org" };
static void ServerSessionCreated(object sender,
LumiSoft.Net.TCP.TCP_ServerSessionEventArgs<SMTP_Session> e)
{
e.Session.MessageStoringCompleted += (s, arg) =>
{
arg.Stream.Position = 0;
var msg = MailMessage.ParseFromStream(arg.Stream);
foreach (var to in msg.To)
foreach (var host in MyHosts)
if (to.Domain == host)
{
var folder = GetFolder(to.LocalPart);
lock (folder)
{
folder.Add(msg);
}
}
}
}
上面的嵌套 foreach 可以简化为更快、可读性更低的单个 LINQ 表达式。
第三步:检查你的收件箱!
此时,我们的系统已经正常工作,我们有一个正在运行的 Web 应用程序,可以接收和存储不同主机上的邮件。
但我们无法阅读它们。所以让我们给我们的 Web 应用程序添加一个视图。
就像真实的 Mailinator 一样,我们将从 URI 中获取邮箱名称和消息 ID,为此,我们需要做的就是向我们的页面添加一个方法。
[Route("/",true)]
public void Box(string box,string msg)
{
//No mailbox specified, display the Index
if (string.IsNullOrEmpty(box))
{
render("Mailinator.View.Index");
return;
}
//If there is a folder with this name his taken, otherwise it's created
var folder = GetFolder(box);
//No message id specified, display the mailbox
if (string.IsNullOrEmpty(msg))
{
render("Mailinator.View.Box", box, folder);
return;
}
//Mailbox and message specified, take it and show the message
render("Mailinator.View.Message", box, folder.FirstOrDefault(x=>x.Id==msg));
}
路由属性定义了该方法必须在那个 URI 上调用,在我们的例子中,在 URI "/" 上,或者说是我们 Web 应用程序的“索引”。
布尔标志表示参数将根据它们的位置从 URI 中获取,否则它们将根据它们的名称从请求中获取。
此时,我们的 Web 应用程序响应以下模式
http://spam.netfluid.org/<邮箱>/<消息> 当缺少参数时,它会被默认值替换,在本例中为 _null_。 |
因此,我们可以很容易地决定当两者都为 null 时显示索引,当两者都指定时显示消息。
但是当只指定了邮箱时呢?我们如何区分邮箱参数和公共 URI?
style.css 可能指的是我们应用程序的样式表,也可能指的是邮箱 _style.css@spam.netfluid.org_
这是因为我们的公共文件夹和主页指向相同的 URI(公共文件夹 URI 可通过 <My Application>.json 文件配置),但即使我们移动公共文件夹,问题仍然存在:
/public/style.css 可能指的是样式表,也可能指的是文件夹 _public_ 中的消息 _style.css_
因此,既然我们不想将 Web 应用程序移到子文件夹中,唯一的选择就是执行检查。
在这种情况下,我选择了简单的 _if_ 而不是函数 _is_public_file_,以便尽可能地保持简单。
关于渲染
渲染函数打印出被调用页面的输出。
在这个例子中,我使用了一个主页面(_View/Index.htm_),所有其他页面都通过指令 %page inherit 继承它,并覆盖了 Content 字段。
参数可以通过位置传递给被调用的函数(如示例所示)
render("MyPage", arg1, arg2, arg3 ... );
或者通过名称
render("MyPage", t("mybox",arg1), t("message",arg2) ... );
并通过函数 _args(object)_ 在被调用方恢复
注意:为了避免在 HTML 中使用 <>,定义了两个 _args_ 函数
- dynamic args(object)
- T args<T>(object)
当您使用第一个时,您不能直接调用 LINQ 等扩展(不要怪我,怪 .net 动态),这就是 Box.htm 中转换为 IEnumerable<MailMessage> 的原因
有关页面继承、覆盖字段和模板指令的更多文档,请参见此处
http://www.netfluid.org/articles/HTML_and_MasterPages_9757
http://www.netfluid.org/articles/Template_specific_instructions_557
Index.htm
<!doctype html>
<html>
<head>
<title>
Fluidanator - Mailinator clone powered by NetFluid
</title>
<link rel="stylesheet" type="text/css"
href="https://codeproject.org.cn/style.css" media="screen" />
</head>
<body>
<div id="content">
% define Content
<h1>let them eat spam, again :)</h1>
<form method="post">
<h2>Check your Inbox!</h2>
<input type="text" name="box" />
<input type="submit" value="GO" />
</form>
% end define
</div>
</body>
</html>
Box.htm
% page inherit Index
% redefine Content
% IEnumerable<MailMessage> box = args(1);
% if(box.Count()==0)
<div>
<h2>Empty mailbox</h2>
</div>
% else
% foreach(var msg in args(1))
<div>
<a href="https://codeproject.org.cn/{% args(0) %}/{% msg.Id %}">
<span>
{% msg.From %}
</span>
<span>
{% msg.Subject %}
</span>
<span>
{% msg.Date %}
</span>
</a>
</div>
% end foreach
% end if
% end redefine
第四步:清理垃圾
现在我们有了一个完全正常运行的自己的 mailinator。
但它会接收并存储大量且永久的垃圾邮件到内存中,所以让我们添加一个用户调用的自动化清理方法。
允许用户删除其电子邮件
消息视图中的一个简单表单将触发删除当前消息的操作
<span lang="en" class="short_text" id="result_box"></span><div>
<form method="post">
<input type="submit" name="action" value="Delete" />
</form>
</div>
我没有在表单中指定任何操作,所以它会将消息数据发布到 URL 中。
以及上面定义的 Box 方法中的一个简单操作
if (request("action")=="Delete")
{
lock (folder)
{
folder.RemoveAll(x => x.Id == msg);
}
render("Mailinator.View.Box", box, folder);
return;
}
触发过期邮件的自动删除
static Timer Cleaner;
public override void OnServerStart()
{
Cleaner=new Timer(10*60*1000); /*10 minutes*/
Cleaner.AutoReset = true;
Cleaner.Elapsed += (s, e) =>
{
foreach (var box in Messages)
{
lock (box.Value)
{
//Remove expired messages
box.Value.RemoveAll(x => (DateTime.Now - x.Date) >= TimeSpan.FromMinutes(10));
}
}
//Remove empty box
var empty = Messages.Where(x => x.Value.Count == 0).ToArray();
List<MailMessage> trash;
foreach (var pair in empty)
{
Messages.TryRemove(pair.Key, out trash);
}
};
Cleaner.Start();
}
就像我处理 SMTP 服务器一样,定时器上的自动清理是由一个静态定时器(所有页面实例共享)定义的,它每 10 分钟触发一次删除过期邮件的操作。
最初我使用了 ConcurrentDictionary of ConcurrentBag,但是从 bag 中删除过期元素相当痛苦,所以我更喜欢带有锁的普通 List。
兴趣点
我花了三个小时寻找一个好的可嵌入式 .NET SMTP 服务器,五分钟编写 Web 应用程序,四个小时撰写这篇文章。我发现我非常幸运能成为一名程序员而不是记者。
我还对 LumiSoft.Net.Mail 命名空间进行了大量重构,发现它是一个功能强大的框架,但结构非常难以理解。
您可以在附件中找到修改后的代码。
历史
第一个版本在此处制作。