Introduction
When yet another site has refused to let me signup with mailinator, I thought "Why I can't have mine ?"
Original Mailinator required two days of working and some refinements (detail here), so the game is not worth the candle if you just don't want to leave your real email on a website.
Fortunately, three years ago I developed an application server just designed to quickly develop web apps and serve them with respectable performance, so why not try it?
(Application server freeware version and documentation can be found here).
Background
Obviously I haven't developed a SMTP server plus the website in five minutes. Mail server come from LumiSoft.Net library.
Differently from the original Mailinator, there is no downloading or email checking from the webapp to the mail server. The mail server is part of the webapp and all the data flow from the ram to the ram, for the moment I don't see any necessity to develop a permanent message store on the disk.
Important: We are using the NetFluid Application Server, where web applications are real applications, so if you change it, you need to recompile.Tools and helping script will be found in the source attached.
Using the code
Step zero: How to run
To run NetFluid Application Server and consequently this code you need the .Net Framework 4 or higher or Mono 2.10 or higher.
Essentially essentially there are two available commands:
- Compile the webapp : Compiler.exe <path to the *.csproj>
- Run the webapp: FluidPlayer.exe <path to the *.csproj>
Correct procedure to apply changes to a NetFluid Webapp:
- Call the Compiler
- Restart the Player
In the attachment you will find helping script to run the code on every platform.
By default NetFluid App Server listen on every ip of running machine on port 8080, you can chage this settings by editing the configuration file <My App>.json (in this case Mailinator/Mailinator.json)
To immediately see the application running :
- Choose the start script corresponding to your platform
- Wait until the [Host Running] message appear
- Open your favorite browser at the address http://localhost:8080
The code is so small and quite stupid, composed by a single page (Mailinator.cs) and three view, so let's see how does it works.
Step one: The STMP server
We need something to recieve the emails
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();
}
Now we a got an empty web application that embed in it self a bottom less mail server, quite useless.
ASPGuy: Static member ? I will not work..
That's NetFluid, not ASP. Static members are really static and kept alive until the server stop.
Step two: Collect the messages
In the real Mailinator there is a bulk of registred domain pointing to the same mailboxes.
Example: netfluid@mailinator.com and netfluid@not-mailinator.com link the same mailbox
So we gonna do the same, whenever a mail
is recieved the server call a delegate which controls each recipient to check if it belong to our domains, if it belongs to us it will be stored in our inmemory collection.
For the memory collection I opted for a ConcurrentDictionary
(key: local name, value : recieved messages) of List instead of ConcurrentBag
because ConcurrentBag
offer a good threading
but it's quite painful if you want to delete elements upon conditions.
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);
}
}
}
}
The nested foreachs above can be simplified into a faster and less readable single LINQ expression.
Step three: Check your inbox !
At this point our system is already working, we a got a running webapp that can recieve and store mails on differents host.
But we can't read them. So let's add a view to our webapp.
Like the real Mailinator, we gonna take mailbox name and message id from the URI, to do this all we need it's to add a method to our page.
[Route("/",true)]
public void Box(string box,string msg)
{
if (string.IsNullOrEmpty(box))
{
render("Mailinator.View.Index");
return;
}
var folder = GetFolder(box);
if (string.IsNullOrEmpty(msg))
{
render("Mailinator.View.Box", box, folder);
return;
}
render("Mailinator.View.Message", box, folder.FirstOrDefault(x=>x.Id==msg));
}
Route attribute defines that the method must be called on that URI, in our case on the URI "/" or rather as "index" of our webapp.
The bool flag indicates that parameters will be taken from URI in base of their position, otherwise they will be taken from the request in base of their name.
At this point our webapp respond to the following schema:
| http://spam.netfluid.org/<mail box>/<message> When an argument is missing
it's replaced by default value, in this case null. |
So, easily we can decide to show the index when both are null and show the message when both are specified.
But when just the mailbox it's specified ? How we can distinguish the mailbox parameter from a public URI ?
style.css could be referred to the stylesheet of our application or to the mailbox style.css@spam.netfluid.org
This because our public folder and our main page points to the same URI (public folder URIs are configurable from the <My Application>.json file)
but the problem persist even if we move the public folders:
/public/style.css could be referred to the stylesheet or to the message
style.css in the folder public
So, decided that we don't want to move our webapp into a subfolder the only available choice is to perform a check.
In this case I opted for a simple if instead of the function is_public_file to keep it simplest as possible.
About the rendering
The render function print out the output of the called page.
In this example I'm using a main page (/View/Index.htm ) and all others pages inherit this one through
the instruction %page inherit overriding the field Content.
Arguments can be passed to the called function via position (like in the exampe)
render("MyPage", arg1, arg2, arg3 ... );
Or via name
render("MyPage", t("mybox",arg1), t("message",arg2) ... );
And recovered in the called via the function args(object)
NOTE: to avoid the use of <> inside the HTML two args functions are defined:
- dynamic args(object)
- T args<T>(object)
When you use the first one you cannot directly call extensions like LINQ (don't blame me,
blame .net dynamics ) and this is the reason of the cast to IEnumerable<MailMessage> in Box.htm
More documentation about page inheritance, overriding fields and template instructions can be found here:
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="http://www.codeproject.com/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="http://www.codeproject.com/{% args(0) %}/{% msg.Id %}">
<span>
{% msg.From %}
</span>
<span>
{% msg.Subject %}
</span>
<span>
{% msg.Date %}
</span>
</a>
</div>
% end foreach
% end if
% end redefine
Step four: Take out the trash
Now we got our fully working own mailinator.
But it will recieve and store into the ram a huge and eternal dose of spam, so let's add a user called and an automated cleaning method.
Allow the user to delete his emails:
A simple form in the message view will trigger the action for delete the current message
<span lang="en" class="short_text" id="result_box"></span><div>
<form method="post">
<input type="submit" name="action" value="Delete" />
</form>
</div>
I didn't specified any action in the form so it will post the data of the message in URL.
And a simple action in the Box method define above:
if (request("action")=="Delete")
{
lock (folder)
{
folder.RemoveAll(x => x.Id == msg);
}
render("Mailinator.View.Box", box, folder);
return;
}
Trigger auto delete on expired mails
static Timer Cleaner;
public override void OnServerStart()
{
Cleaner=new Timer(10*60*1000);
Cleaner.AutoReset = true;
Cleaner.Elapsed += (s, e) =>
{
foreach (var box in Messages)
{
lock (box.Value)
{
box.Value.RemoveAll(x => (DateTime.Now - x.Date) >= TimeSpan.FromMinutes(10));
}
}
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();
}
Like what i did whit the SMTP server, autocleaning on timer is defined by a static timer (common to all instances of the page) that evbery 10 minutes trigger the action
of delete exipired mails.
Initally I used a CuncurrentDictionary of ConcurrentBag but remove expired elements from the bag was quite painful so i preferred a nomal List with a lock.
Points of Interest
I spent thrre hours for finding a good embeddable .NET SMTP server, five minutes to write the webapp and four hours to write this article.
I discovered that I'm very lucky to be a programmer instead of a journalist.
I also heavily refactored the LumiSoft.Net.Mail namespace discovering that
it's a powerful framework with a very incomprehensible structure.
You can find the modified code in the attachments.
History
First version made here.