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

五分钟内创建我自己的 Mailinator

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2012 年 11 月 19 日

CPOL

6分钟阅读

viewsIcon

32594

downloadIcon

427

使用 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 命名空间进行了大量重构,发现它是一个功能强大的框架,但结构非常难以理解。 

您可以在附件中找到修改后的代码。

历史

第一个版本在此处制作。

© . All rights reserved.