Click here to Skip to main content
Click here to Skip to main content

My own Mailinator in 5 minutes

, 19 Nov 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
A simple Mailinator clone developed in five minutes with the NetFluid framework

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

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

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

TheQult
Founder Phascode-Xapio
Italy Italy
No Biography provided
Follow on   Twitter

Comments and Discussions

 
QuestionAttachment PinmemberEric Lapouge19-Nov-12 6:32 
AnswerRe: Attachment [modified] PinmemberTheQult19-Nov-12 6:47 
I don't know why it's not showing them, i added the links on top of the article.
Btw here they are :
** OLD VERSION FILES **

modified 19-Nov-12 14:26pm.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.141022.1 | Last Updated 19 Nov 2012
Article Copyright 2012 by TheQult
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid