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:
- "Listener" daemon that takes the incoming SMTP requests;
- "MXDiscovery" module able to identify the IP of the external recipient servers;
- "Sender" component that is responsible for forwarding the message to external recipients;
- "SMTP" library with useful functions;
- "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;
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;
}
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();
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 {
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);
}
}
}