{{error}}
{{(quickSearchResults.length>10)?'10+':(quickSearchResults.length)}} {{(quickSearchResults.length==1)?'result':'results'}}
{{result.title}} {{result.timeStamp | mysql2ymd }}
I am sorry, no such article was written yet.
How to write a simple Light SMTP Server in C#
How to write a simple Light SMTP Server in C#
Sample of a small C# SMTP server with the following features:
  • 17KB source code;
  • five files of code only;
  • able to receive mails;
  • relay (forward) mails to external servers;
  • able to resolve the MX machine IPs for external mails;
  • mails to external recipients (external elaying) requires authentication.
The components of the code are:
  1. "Listener" daemon that takes the incoming SMTP requests;
  2. "MXDiscovery" module able to identify the IP of the external recipient servers;
  3. "Sender" component that is responsible for forwarding the message to external recipients;
  4. "SMTP" library with useful functions;
  5. "SMTPServer" the main application that starts the listener.

I hope this information might be useful for others, too.
SMTPServer.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.IO;
using Common.DNS;

namespace SMTPServer {
    public class SMTPServer {
        static Dictionary<string,string> configuration=new Dictionary<string,string>();
        static void Fatal(string message) {
            Console.WriteLine("FATAL: "+message);
            Console.ReadKey();
            Environment.Exit(1);
        }
        public static void Main(string[] args) {
            foreach(var arg in args) {
                var kv=arg.Split('=');
                configuration[kv[0]]=kv[1];
            }
            Console.WriteLine("IP: "+ListenerIP);
            Console.WriteLine("Port: "+ListenerPort);
            new System.Threading.Thread(new ThreadStart(Listener.Start)).Start();
        }
        public static string ListenerIP {
            get {
                return config("listener-ip");
            }
        }
        public static int ListenerPort {
            get {
                int val;
                var strVal=config("listener-port");
                if(!int.TryParse(strVal,out val))
                    Fatal("e1204142034 - Config listener-port defined as 'listener-port="+strVal+"' in command line must be numeric");
                return val;
            }
        }
        public static string UserPass {
            get {
                return config("user-pass");
            }
        }
        public static DirectoryInfo ReceivedPath {
            get {
                return new DirectoryInfo(config("received-path"));
            }
        }
        private static string config(string name) {
            if(!configuration.ContainsKey(name))
                Fatal("e1204142032 - Config "+name+" not defined as '"+name+"=<value>' in command line");
            return configuration[name];
        }
    }
}
Listener.cs
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.IO;
using Common.SMTP;
using System.Collections;
using System.Collections.Generic;

namespace SMTPServer {
    public class Listener {
        System.IO.StreamReader reader;
        System.IO.StreamWriter writer;
        TcpClient client;

        #region Main and Constructor 
        public Listener(TcpClient client) {
            this.client=client;
            NetworkStream stream=client.GetStream();
            reader=new System.IO.StreamReader(stream);
            writer=new System.IO.StreamWriter(stream);
            writer.NewLine="\r\n";
            writer.AutoFlush=true;
        }
        public static void Start() {
            TcpListener listener=new TcpListener(IPAddress.Parse(SMTPServer.ListenerIP),SMTPServer.ListenerPort);
            listener.Start();
            while(true) {
                Listener handler=new Listener(listener.AcceptTcpClient());
                Thread thread=new System.Threading.Thread(new ThreadStart(handler.Run));
                thread.Start();
            }
        }
        #endregion
        public void Run() {
            try {
                wr(220,"sorescu.eu -- Dragos-Matei Sorescu SMTP Server");
                SMTPMessage message=new SMTPMessage();
                bool isUserAuthenticated=false;
                for(;;) {
                    string line=rd();
                    if(line==null)
                        break;
                    string[] tokens=line.Split(' ');
                    bool requiresAuthorization=false;
                    switch(tokens[0].ToUpper()) {
                        case "EHLO":
                            wr(250,"AUTH LOGIN PLAIN");
                            break;
                        case "HELP":
                            wr(250,"OK Success - please contact dragos-matei@sorescu.eu  or http://dragos-matei.sorescu.eu");
                            break;
                        case "HELO":
                            wr(250,"OK Success");
                            break;
                        case "MAIL":
                            if(SMTP.checkRCPTIsInternal(line))
                                requiresAuthorization=true;
                            if(requiresAuthorization&&!isUserAuthenticated) {
                                wr(530,"Authorization requiredw");
                                break;
                            }
                            message.From=line;
                            wr(250,"OK Success");
                            break;
                        case "RCPT":
                            if(!SMTP.checkRCPTIsInternal(line)) {
                                requiresAuthorization=true;
                                //break;
                            }
                            if(requiresAuthorization&&!isUserAuthenticated) {
                                wr(530,"Authorization required");
                                break;
                            }
                            message.To.Add(line);
                            wr(250,"OK ");
                            break;
                        case "AUTH":
                            wr(334,"VXNlcm5hbWU6");
                            string user=rd64();
                            if(user==null)
                                return;
                            if(user.Length==0) {
                                wr(535,"invalid user");
                                break;
                            }
                            wr(334,"UGFzc3dvcmQ6");
                            string pass=rd64();
                            if(pass==null)
                                return;
                            if(pass.Length==0) {
                                wr(535,"invalid password");
                            }
                            if(SMTPServer.UserPass.Equals(user+"-"+pass)) {
                                wr(535,"Authentication failed");
                                break;
                            }
                            isUserAuthenticated=true;
                            wr(235,"Authentication succesful");
                            break;
                        case "DATA":
                            if(requiresAuthorization&&!isUserAuthenticated) {
                                wr(530,"Authorization Required");
                                break;
                            }
                            wr(354,"End data with <CR><LF>.<CR><LF>");
                            message.Data.Add(line);
                            for(;;) {
                                line=rd();
                                message.Data.Add(line);
                                if((line==null)||(line=="."))
                                    break;
                            }
                            wr(250,"Ok: queued");
                            message.SaveAndSend();
                            message=new SMTPMessage();
                            //Console.WriteLine("sd");
                            break;
                        case "RSET":
                            wr(250,"Ok: Reset");
                            message=new SMTPMessage();
                            break;
                        case "QUIT":
                            wr(221,"BYE");
                            message.SaveAndSend();
                            writer.Close();
                            client.Close();
                            return;
                        default:
                            wr(550,"Command not understood");
                            break;
                    }
                }
            } catch(Exception) { }
        }
        private void wr(int code,string c) {
            writer.WriteLine(code+" "+c);
            writer.Flush();
            Console.WriteLine(">> "+code+" "+c);
        }
        private string rd() {
            string result=null;
            try {
                Console.WriteLine(reader.EndOfStream);
                result=reader.ReadLine();
            } catch(Exception e) {
                Console.WriteLine("Exception: "+e.Message);
            }
            if(result==null)
                Console.WriteLine("<< NULL");
            else
                Console.WriteLine("<< "+result);
            return result;
        }
        private string rd64() {
            try {
                string record=rd();
                if(record==null)
                    return null;
                return System.Text.Encoding.ASCII.GetString(Convert.FromBase64String(record));
            } catch(Exception) {
                return "";
            }
        }
    }
}
MXDiscovery.cs
namespace Common.DNS {
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.Runtime.InteropServices;
    using System.Collections.Generic;
    public class DnsMx {
        public DnsMx() { }
        [DllImport("dnsapi",EntryPoint="DnsQuery_W",CharSet=CharSet.Unicode,SetLastError=true,ExactSpelling=true)]
        private static extern int DnsQuery([MarshalAs(UnmanagedType.VBByRefStr)]ref string pszName,
QueryTypes wType,QueryOptions options,int aipServers,ref IntPtr ppQueryResults,int pReserved);
        [DllImport("dnsapi",CharSet=CharSet.Auto,SetLastError=true)]
        private static extern void DnsRecordListFree(IntPtr pRecordList,int FreeType);
        private static string[] GetMXRecords(string domain) {
            IntPtr ptr1=IntPtr.Zero;
            IntPtr ptr2=IntPtr.Zero;
            MXRecord recMx;
            if(Environment.OSVersion.Platform!=PlatformID.Win32NT) {
                throw new NotSupportedException();
            }
            ArrayList list1=new ArrayList();
            int num1=DnsMx.DnsQuery(ref domain,QueryTypes.DNS_TYPE_MX,QueryOptions.DNS_QUERY_BYPASS_CACHE,0,ref ptr1,0);
            if(num1!=0)
                throw new Win32Exception(num1);
            for(ptr2=ptr1;!ptr2.Equals(IntPtr.Zero);
                ptr2=recMx.pNext) {
                recMx=(MXRecord)Marshal.PtrToStructure(ptr2,typeof(MXRecord));
                if(recMx.wType==15) {
                    string text1=Marshal.PtrToStringAuto(recMx.pNameExchange);
                    list1.Add(text1);
                }
            }
            DnsMx.DnsRecordListFree(ptr1,0);
            return (string[])list1.ToArray(typeof(string));
        }
        private enum QueryOptions {
            DNS_QUERY_ACCEPT_TRUNCATED_RESPONSE=1,
            DNS_QUERY_BYPASS_CACHE=8,
            DNS_QUERY_DONT_RESET_TTL_VALUES=0x100000,
            DNS_QUERY_NO_HOSTS_FILE=0x40,
            DNS_QUERY_NO_LOCAL_NAME=0x20,
            DNS_QUERY_NO_NETBT=0x80,
            DNS_QUERY_NO_RECURSION=4,
            DNS_QUERY_NO_WIRE_QUERY=0x10,
            DNS_QUERY_RESERVED=-16777216,
            DNS_QUERY_RETURN_MESSAGE=0x200,
            DNS_QUERY_STANDARD=0,
            DNS_QUERY_TREAT_AS_FQDN=0x1000,
            DNS_QUERY_USE_TCP_ONLY=2,
            DNS_QUERY_WIRE_ONLY=0x100
        }
        private enum QueryTypes { DNS_TYPE_MX=15 }
        [StructLayout(LayoutKind.Sequential)]
        private struct MXRecord {
            public IntPtr pNext;
            public string pName;
            public short wType;
            public short wDataLength;
            public int flags;
            public int dwTtl;
            public int dwReserved;
            public IntPtr pNameExchange;
            public short wPreference;
            public short Pad;
        }
        private Dictionary<string,string[]> cache=new Dictionary<string,string[]>();
        public string[] resolve(string domain) {
            if(!cache.ContainsKey(domain))
                cache.Add(domain,GetMXRecords(domain));
            return cache[domain];
        }
    }
}
Sender.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using Common.SMTP;
namespace SMTPServer {
    public class Sender {
        static Boolean sendMail(string server,string from,string to,List<string> lines,SMTPMessage message) {
            try {
                /*
220 Welcome to AMDOCS ESMTP server
HELO dragos
250 goldmail4a.amdocs.com
MAIL FROM: test@sorescu.eu
250 2.1.0 Ok
RCPT TO: dsorescu@amdocs.com
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Nothing.
Test.
pa.
.
250 2.0.0 Ok: queued as 5FA7E4007E
QUIT
221 2.0.0 Bye
                 */
                TcpClient client=new TcpClient(server,25);
                NetworkStream stream=client.GetStream();
                System.IO.StreamReader reader=new System.IO.StreamReader(stream);
                System.IO.StreamWriter writer=new System.IO.StreamWriter(stream);
                message.Data.Add(reader.ReadLine());
                OutputLine(writer,message,"HELO "+System.Net.Dns.GetHostName());
                message.Data.Add(reader.ReadLine());
                OutputLine(writer,message,from);
                message.Data.Add(reader.ReadLine());
                OutputLine(writer,message,to);
                message.Data.Add(reader.ReadLine());
                OutputLine(writer,message,"DATA");
                message.Data.Add(reader.ReadLine());
                foreach(var line in lines)
                    OutputLine(writer,message,line);
                message.Data.Add(reader.ReadLine());
                OutputLine(writer,message,"QUIT");
                string quitMessage=reader.ReadLine();
                message.Data.Add(quitMessage);
                return quitMessage.StartsWith("221");
            } catch(Exception ex) {
                Console.WriteLine(ex.StackTrace);
                message.Data.Add(ex.ToString());
                return false;
            }
        }
        private static void OutputLine(StreamWriter writer,SMTPMessage log,string p) {
            writer.WriteLine(p);
            log.Data.Add(p);
            writer.Flush();
        }

        internal static void Send(string externalMail,List<string> Data,string MAIL_FROM) {
            Common.DNS.DnsMx dns=new Common.DNS.DnsMx();
            SMTPMessage message=new SMTPMessage();
            message.From="SMTP administrator";
            message.To.Add(MAIL_FROM);
            bool sent=false;
            try {
                string domain=externalMail.Split('@')[1];
                string[] servers=dns.resolve(domain);
                foreach(var serverName in servers) {
                    if(!sendMail(serverName,MAIL_FROM,externalMail,Data,message))
                        continue;
                    sent=true;
                    break;
                }
            } catch(Exception e) {
                message.Data.Add("Error: "+e.ToString());
                message.Data.Add("Message: "+e.Message);
                message.Data.Add("Source: "+e.Source);
                message.Data.Add("StackTrace: "+e.StackTrace);
                sent=false;
            }
            if(!sent)
                message.Save();
        }
    }
}
SMTP.cs
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

namespace Common.SMTP {
    class SMTP {
        public static string[] extractMails(string text) {
            Regex emailRegex=new Regex(@"\w+([-+.]\w+)*@\w+([-.]\w+)*",
            RegexOptions.IgnoreCase);
            MatchCollection emailMatches=emailRegex.Matches(text);
            List<string> result=new List<string>();
            foreach(Match emailMatch in emailMatches)
                result.Add(emailMatch.Value);
            return result.ToArray();
        }
        public static bool checkRCPTIsInternal(string text) {
            string[] mails=extractMails(text);
            foreach(string mail in mails)
                if(!isInternal(mail))
                    return false;
            return true;
        }
        public static bool isInternal(string email) {
            if(email.EndsWith("@sorescu.eu"))
                return true;
            if(email.EndsWith("@localhost"))
                return true;
            if(email.EndsWith("@localhost.com"))
                return true;
            return false;
        }
    }
    class SMTPMessage {
        public string From;
        public List<string> To=new List<string>();
        public List<string> Data=new List<string>();
        internal void SaveAndSend() {
            Save();
            foreach(var to in this.To) {
                var mail=SMTP.extractMails(to);
                if(!SMTP.isInternal(mail[0]))
                    SMTPServer.Sender.Send(mail[0],Data,From);
            }
        }
        internal void Save() {
            DirectoryInfo directory=SMTPServer.SMTPServer.ReceivedPath;
            directory.Create();
            string uniqueFileName=Path.Combine(directory.FullName,System.Guid.NewGuid().ToString()+".smtp");
            File.WriteAllLines(uniqueFileName,To);
            File.AppendAllLines(uniqueFileName,Data);
        }
    }
}