/////////////////////////////////////////////////////////////////////////////// // // Microsoft Research Singularity // // Copyright (c) Microsoft Corporation. All rights reserved. // // Note: SMTP Server. // //#define SMTPAGENT_VERBOSE using Microsoft.Singularity.Applications; using Microsoft.Singularity.Channels; using Microsoft.Singularity.Diagnostics.Contracts; using Microsoft.Singularity.Endpoint; using Microsoft.Singularity.Directory; using Microsoft.Singularity.V1.Services; using Microsoft.Singularity.Io; using Microsoft.Singularity.Configuration; using Microsoft.Contracts; using Microsoft.SingSharp.Reflection; using System; using System.IO; using System.Collections; using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Diagnostics; using Microsoft.Singularity.Email.Contracts; [assembly: Transform(typeof(ApplicationResourceTransform))] namespace Microsoft.Singularity.Email_NoNet { [ConsoleCategory(HelpMessage="SMTP Mail Transfer Agent", DefaultAction=true)] internal class Parameters { [InputEndpoint("data")] public readonly TRef Stdin; [OutputEndpoint("data")] public readonly TRef Stdout; [StringParameter("cnts", Mandatory=false, Default="/init/contents.txt", HelpMessage="Contents of emails to be sent.")] internal string emailCorpus; [StringParameter("stl", Mandatory=false, Default="/init/stl.txt", HelpMessage="file containing starting loc in corpus for each conn")] internal string startPositionsFile; [Endpoint] public readonly TRef nsRef; [StringParameter("mailstore service", Mandatory=false, Default="/service/mailstore", HelpMessage="Location of mail store service.")] internal string mailstorePath; [LongParameter("nts", Mandatory=false, Default= 1, HelpMessage="Number of emails to send.")] internal long numToSend; [LongParameter("nc", Mandatory=false, Default=1, HelpMessage="Number of concurrent connections.")] internal long numConn; [StringParameter("server", Mandatory=false, Default="0.0.0.0", HelpMessage="Service IP Address to give to clients.")] internal string server; reflective internal Parameters(); internal int AppMain() { return SmtpServer.AppMain(this); } } public class Buffer { private byte[] data; private int size; public static Buffer Allocate(int bytes) { Buffer buffer = new Buffer(bytes); return buffer; } private Buffer(int bytes) { this.data = new byte[bytes]; this.size = 0; } public byte[] Data { get { return data; } } public int Size { get { return size; } } public int Prepare(int bytes) { if (size + bytes > data.Length) { byte[] dst = new byte[data.Length * 2]; Array.Copy(data, dst, size); data = dst; } return size; } public void Save(int bytes) { if (size + bytes + 2 <= data.Length) { size += bytes; data[size++] = (byte)'\r'; data[size++] = (byte)'\n'; } else { throw new Exception("Overflow"); } } public void Release() { data = null; size = 0; } } public class EmailClientError : ApplicationException {} //Baically, instantiating this class //goes through the state machine with the server public class EmailClient { [Conditional("SMTPAGENT_VERBOSE")] public static void DebugWriteLine(string format) { DebugStub.WriteLine(format); } [Conditional("SMTPAGENT_VERBOSE")] public static void DebugWriteLine(string format, __arglist) { DebugStub.WriteLine(format, new ArgIterator(__arglist)); } private static void DumpEmail(string[] email) { DebugStub.WriteLine("Dumping email...\n"); foreach(string line in email) { DebugStub.WriteLine("{0}", __arglist(line)); } } //client side state machine private enum ReceivedState { RECEIVED_NONE = 0, RECEIVED_220 = 1, RECEIVED_250 = 2, RECEIVED_354 = 3, } private enum SentState { SENT_NONE = 0, SENT_EHLO = 1, SENT_NOOP = 2, SENT_MAIL = 3, BEGAN_RCPT = 4, FINISHED_RCPT = 5, BEGAN_DATA = 6, FINISHED_DATA = 7, }; private string[][] corpus; private int nxtEmail; private string[] emailContents; private int dataIndex; private ReceivedState rcvState; private SentState sentState; private int rcptIndex; private string[] rcpts; private static string GetCommand(String line) { int len = line.Length; int i; for (i = 0; i < len && line[i] >= '0' && line[i] <= '9'; i++); return line.Substring(0, i); } [NotDelayed] public EmailClient(String[][] corpus, int startIndex) { this.corpus = corpus; this.nxtEmail = startIndex; DebugStub.WriteLine("Email client starting up startindex {0}\n", __arglist(startIndex)); DebugStub.WriteLine("corpus length is {0}\n", __arglist(corpus.Length)); rcvState = ReceivedState.RECEIVED_NONE; sentState = SentState.SENT_NONE; emailContents = corpus[nxtEmail]; nxtEmail++; if (nxtEmail == corpus.Length) { nxtEmail = 0; } } //Command from SMTP server to client //Advances state machine public void SendLine(string line) { DebugWriteLine("EmailClient.SendLine received {0}\n", __arglist(line)); String command = GetCommand(line); switch(command) { case "220": if (rcvState == ReceivedState.RECEIVED_NONE) { DebugWriteLine("EmailClient received 220 in correct state\n"); rcvState = ReceivedState.RECEIVED_220; return; } DebugStub.WriteLine("EmailClient recieved 220 while in state {0}\n", __arglist(rcvState)); DebugStub.Break(); throw new EmailClientError(); break; case "250": DebugWriteLine("EmailClient received 250\n"); rcvState = ReceivedState.RECEIVED_250; break; case "354": DebugWriteLine("EmailClient received 354\n"); rcvState = ReceivedState.RECEIVED_354; break; case "501": DebugWriteLine("Uhoh! received 501\n"); DebugStub.Break(); break; default: DebugStub.WriteLine("Got unexpected command {0}\n", __arglist(command)); DebugStub.Break(); break; } } //Get line from client to server public string GetLine() { switch(sentState) { case SentState.SENT_NONE: DebugWriteLine("EmailClient GetLine while in state SENT_NONE\n"); string result = "EHLO Nobody\r\n"; sentState = SentState.SENT_EHLO; rcvState = 0; return result; break; case SentState.SENT_EHLO: if (rcvState != ReceivedState.RECEIVED_250) { break; } DebugWriteLine("GetLine while in state SENT_EHLO...sending MAIL\n"); sentState = SentState.SENT_MAIL; rcvState = 0; rcptIndex = 0; string from = FindFrom(emailContents).ToLower(); return "MAIL FROM: <" + from + ">\r\n"; break; case SentState.SENT_MAIL: DebugWriteLine("GetLine while in state SENT_MAIL...sending RCPT\n"); if (rcvState != ReceivedState.RECEIVED_250) { break; } string to = FindTo("To: ", emailContents); string cc = FindTo("Cc: ", emailContents); string bcc = FindTo("Bcc: ", emailContents); if (to == null) { to = cc; cc = null; } if (to == null) { to = bcc; bcc = null; } if (cc != null) { to = to + "," + cc; } if (bcc != null) { to = to + "," + bcc; } if (to == null || to.Length == 0) { //Sometimes the "to" is blank with no cc or bcc because of //an interoffice dl? to = "mail_forward@enron.com"; #if false DebugStub.WriteLine(":: Couldn't find receipients."); DumpEmail(emailContents); DebugStub.Break(); throw new EmailClientError(); #endif } rcpts = to.Split(new Char[]{','}); for (int i = 0; i < rcpts.Length; i++) { rcpts[i] = rcpts[i].Trim().ToLower(); if (rcpts[i].Length == 0) { rcpts[i] = null; } } for (int i = 0; i < rcpts.Length; i++) { for (int j = i + 1; j < rcpts.Length; j++) { if (rcpts[i] == rcpts[j]) { rcpts[j] = null; } } } rcvState = 0; string rcpt = "RCPT TO: <" + rcpts[rcptIndex] + ">\r\n"; rcptIndex++; if (rcptIndex == rcpts.Length) { sentState = SentState.FINISHED_RCPT; } else { sentState = SentState.BEGAN_RCPT; } return rcpt; break; case SentState.BEGAN_RCPT: DebugWriteLine("GetLine while in state BEGAN_RCPT...sending RCPT\n"); if (rcvState != ReceivedState.RECEIVED_250) { break; } rcpt = "RCPT TO: <" + rcpts[rcptIndex] + ">\r\n"; rcptIndex++; rcvState = 0; if (rcptIndex == rcpts.Length) { sentState = SentState.FINISHED_RCPT; } else { sentState = SentState.BEGAN_RCPT; } return rcpt; break; case SentState.FINISHED_RCPT: DebugWriteLine("GetLine while in state FINISHED_RCPT...sending DATA\n"); if (rcvState != ReceivedState.RECEIVED_250) { break; } string dataCommand = "DATA \r\n"; dataIndex = 0; rcvState = 0; sentState = SentState.BEGAN_DATA; return dataCommand; break; case SentState.BEGAN_DATA: DebugWriteLine("GetLine while in state BEGAN_DATA...sending DATA\n"); if (rcvState != ReceivedState.RECEIVED_354) { break; } string data; if (dataIndex == emailContents.Length) { DebugWriteLine("Completed reading email...sending .\n"); data = "."; sentState = SentState.SENT_EHLO; rcvState = 0; emailContents = corpus[nxtEmail]; if(emailContents == null) { DebugStub.WriteLine("null email! nxtEmail {0}\n", __arglist(nxtEmail)); DebugStub.Break(); } nxtEmail++; if (nxtEmail == corpus.Length) { nxtEmail = 0; } } else { data = emailContents[dataIndex]; if ((data.Length == 1) && data[0] == '.') { //prepend another . to avoid false positives that this is the last line data = "." + data; } dataIndex++; //DebugWriteLine("Got {0}\n", __arglist(data)); } return data; break; default: DebugStub.WriteLine("In unexpected state {0}\n", __arglist(sentState)); DebugStub.Break(); break; } DebugStub.WriteLine("EmailClient GetLine in state {0} never got 250\n", __arglist(sentState)); DumpEmail(emailContents); throw new EmailClientError(); } public static String FindFrom(String[] lines) { foreach (string line in lines) { if (line.Length == 0) { return null; // Header ended. } if (line.StartsWith("From: ")) { return line.Substring(6); } } return null; } public static string FindTo(String prefix, String[] lines) { for (int i = 0; i < lines.Length; i++) { if (lines[i].Length == 0) { return null; // Header ended. } if (lines[i].StartsWith(prefix)) { string to = lines[i++].Substring(prefix.Length); while (lines[i].Length > 0 && (lines[i][0] == ' ' || lines[i][0] == '\t')) { to = to + lines[i++]; } return to; } } return null; } } public class ServerSession { private static int idCount = 0; private int id; private string command; private string server; private EmailClient emailClient; private long emailLimit; [Conditional("SMTPAGENT_VERBOSE")] public static void DebugWriteLine(string format) { DebugStub.WriteLine(format); } [Conditional("SMTPAGENT_VERBOSE")] public static void DebugWriteLine(string format, __arglist) { DebugStub.WriteLine(format, new ArgIterator(__arglist)); } public ServerSession(string[][] corpus, long limit, int startIndex) { this.emailClient = new EmailClient(corpus, startIndex); this.id = idCount++; this.server = "0.0.0.0"; this.emailLimit = limit; } public string GetCommand(String line) { int i = line.IndexOf(' '); if (i < 0) { command = line; return null; } command = line.Substring(0, i).ToUpper(); return command; } private enum CurrentState { HAD_NONE = 0, HAD_HELO = 1, HAD_MAIL = 2, HAD_RCPT = 3, }; public void Loop() { String client; String line; CurrentState cstate = CurrentState.HAD_NONE; String from = null; String to = null; //Open a new connection to the mailstore for each smtp connection. //Each client connection stays allive until all emails are sent. DirectoryServiceContract.Imp! dsImp = SmtpServer.dsEp.Acquire(); MailStoreContract.Imp! msImp; MailStoreContract.Exp! msExp; MailStoreContract.NewChannel(out msImp, out msExp); ErrorCode errorCode; if (!SdsUtils.Bind(SmtpServer.mailstorePath, dsImp, msExp, out errorCode)) { DebugWriteLine("Failed to bind to mail store...error {0}\n", __arglist(SdsUtils.ErrorCodeToString(errorCode))); Console.WriteLine("Failed to bind to mail store...error {0}\n", SdsUtils.ErrorCodeToString(errorCode)); DebugStub.Break(); SmtpServer.dsEp.Release(dsImp); delete msImp; return; } SmtpServer.dsEp.Release(dsImp); msImp.RecvMailStoreReady(); emailClient.SendLine("220 " + server + " Singularity Simple SMTP Service Ready"); long numReceived = 0; bool abort = false; DateTime beginEpoch = DateTime.Now; DateTime endEpoch; while (!abort) { line = emailClient.GetLine(); if (line == null) { break; } GetCommand(line); switch (command) { case "EHLO": client = line.Split(new Char[] {' '})[1]; Console.WriteLine(":{0}: EHLO {1}", id, client); emailClient.SendLine("250-" + server + " greets " + client); emailClient.SendLine("250-8BITMIME"); emailClient.SendLine("250 HELP"); from = null; to = null; cstate = CurrentState.HAD_HELO; DebugWriteLine("SMTPServer: Got EHLO\n"); break; case "HELO": client = line.Split(new Char[] {' '})[1]; Console.WriteLine(":{0}: HELO {1}", id, client); emailClient.SendLine("250 " + server + " greets " + client); from = null; to = null; cstate = CurrentState.HAD_HELO; DebugWriteLine("SMTPServer: Got HELO\n"); break; case "MAIL": if (cstate != CurrentState.HAD_HELO) { goto default; } if (line.StartsWith("MAIL FROM:")) { int pos = line.IndexOf("<") + 1; int beg = line.LastIndexOf(':'); if (beg < pos) { beg = pos; } int end = line.IndexOf('>'); from = line.Substring(beg, end - beg).ToLower(); emailClient.SendLine("250 OK"); cstate = CurrentState.HAD_MAIL; DebugWriteLine("SMTPServer: Got MAIL from: {0}\n", __arglist(from)); } else { emailClient.SendLine("501 Syntax error in parameters"); } break; case "RCPT": if (cstate != CurrentState.HAD_MAIL && cstate != CurrentState.HAD_RCPT) { goto default; } DebugWriteLine("Got RCPT\n"); if (line.StartsWith("RCPT TO:")) { int pos = line.IndexOf("<") + 1; int beg = line.LastIndexOf(':'); if (beg < pos) { beg = pos; } int end = line.IndexOf('>'); string address = line.Substring(beg, end - beg).ToLower(); if (SmtpServer.addresses.ContainsKey(address)) { emailClient.SendLine("250 OK"); if (to != null) { to = to + ";" + address; } else { to = address; } cstate = CurrentState.HAD_RCPT; } else { emailClient.SendLine("250 Accepting for forwarding."); cstate = CurrentState.HAD_RCPT; if (to != null) { to = to + ";" + "mail_forward@enron.com"; } else { to = "mail_forward@enron.com"; } } DebugWriteLine("Got address to: {0}\n", __arglist(to)); } else { emailClient.SendLine("501 Syntax error in parameters"); } break; case "DATA": if (cstate != CurrentState.HAD_RCPT) { goto default; } DebugWriteLine("Got DATA\n"); emailClient.SendLine("354 Start mail input; end with ."); Buffer buffer = Buffer.Allocate(65536); bool good = false; for (;;) { int off = buffer.Prepare(1024); string buffLine = emailClient.GetLine(); int len = buffLine.Length; Encoding.ASCII.GetBytes(buffLine, 0, len, buffer.Data, off); if (len < 0) { break; } if (len == 1 && buffer.Data[off] == '.') { DebugWriteLine("Got final . char\n"); good = true; break; } else { DebugWriteLine("S: {0}", __arglist(Encoding.ASCII.GetString(buffer.Data, off, len))); buffer.Save(len); } } if (good) { bool succeeded = false; char[] in ExHeap! addresses = Bitter.FromString(to); byte[] in ExHeap! data = Bitter.FromByteArray(buffer.Data, 0, buffer.Size); // Console.WriteLine("SmtpAgaint: Email from {0} to {1}", from, to); msImp.SendSaveMessage(addresses, data); switch receive { case msImp.SaveAck(): succeeded = true; break; case msImp.SaveNak(error): Console.WriteLine("SmtpAgent: Server dropped email, "+ "error={0}", error); break; case msImp.ChannelClosed(): break; } if (succeeded) { numReceived++; if ((numReceived % 100) == 0) { Console.Write("."); } emailClient.SendLine("250 OK"); } else { emailClient.SendLine("554 Transaction failed"); } } else { emailClient.SendLine("554 Transaction failed"); // Dump(); throw new Exception("554 Transaction failed"); } buffer.Release(); cstate = CurrentState.HAD_HELO; from = null; to = null; if (numReceived == emailLimit) { abort = true; } break; case "NOOP": emailClient.SendLine("250 OK"); break; case "HELP": emailClient.SendLine("250 OK"); break; case "RSET": emailClient.SendLine("250 OK"); from = null; to = null; cstate = CurrentState.HAD_HELO; break; case "QUIT": emailClient.SendLine("221 " + server + " Service closing transmission channel"); abort = true; break; default: emailClient.SendLine("503 Unrecognized command [" + command + "]"); abort = true; break; } } endEpoch = DateTime.Now; Console.WriteLine(":{0}: Session closed", id); TimeSpan delta = endEpoch - beginEpoch; if (delta.TotalSeconds > 0) { Console.WriteLine( "\nDuration(s:ms) {0:f2} emails {1} rate() {2:e3} eps", delta.TotalSeconds, emailLimit, emailLimit/ delta.TotalSeconds ); } if (delta.TotalMilliseconds == 0) { Console.WriteLine("Too fast! < 1 milliseconds to run\n"); } else { Console.WriteLine("Duration ms {0:f2} rate {1:f2} epms {2:e3} eps\n", delta.TotalMilliseconds, (float) ((float)emailLimit / delta.TotalMilliseconds), emailLimit / (delta.TotalMilliseconds / 1000) ); } delete msImp; } } public class SmtpServer { public static String[][] corpus; public static int numCorpusEmails = 12754; public static int[] startPos; public const int _port = 25; public static bool verbose; public static TRef dsEp; public static SortedList addresses; public static string mailstorePath; [Conditional("SMTPAGENT_VERBOSE")] public static void DebugWriteLine(string format) { DebugStub.WriteLine(format); } [Conditional("SMTPAGENT_VERBOSE")] public static void DebugWriteLine(string format, __arglist) { DebugStub.WriteLine(format, new ArgIterator(__arglist)); } //replicating c# Array.Resize that does not appear to be available //in singularity. private static void Resize(ref string[] oldString, int newSize) { if(newSize < 0) { DebugStub.WriteLine("Resize size < 0!!\n"); DebugStub.Break(); throw new EmailClientError(); } string[] newString = new string[newSize]; int length; if (oldString.Length < newSize) { length = oldString.Length; } else { length = newSize; } for(int i = 0; i < length; i++) { newString[i] = oldString[i]; } oldString = newString; } public static void GenerateCorpus(string path, long numberEmails) { DebugWriteLine("Reading test corpus at path {0}\n", __arglist(path)); FileStream fsInput = new FileStream(path, FileMode.Open, FileAccess.Read); StreamReader reader = new StreamReader(fsInput); corpus = new string[numCorpusEmails][]; int arrSize; int numLines; string line; string[] emailContents; //XXX currently we hard code the size of the corpus into the app //we do this because we have generated a list of random //starting positions for each connection that is built against //the corpus... for(int i = 0; i < numCorpusEmails; i++) { arrSize = 1024; emailContents = new string[arrSize]; numLines = 0; while((line = reader.ReadLine()) != null) { if (line.Equals("DEADBEEFDEADBEEFDEADBEEF")) { break; } if (numLines == arrSize) { arrSize = arrSize * 2; Resize(ref emailContents, arrSize); } emailContents[numLines] = line; numLines++; } if(line == null) { DebugStub.WriteLine("line null!\n"); if (numLines == 0 || numLines == 1) { DebugStub.WriteLine("numlines {0}\n", __arglist(numLines)); DebugStub.Break(); } } Resize(ref emailContents, numLines); corpus[i] = emailContents; #if false DebugStub.WriteLine("Parsed email {0} contents:\n", __arglist(i)); foreach (string newline in emailContents) { DebugStub.WriteLine("line {0}\n", __arglist(newline)); } #endif } reader.Close(); DebugStub.WriteLine("Generate corpus complete\n"); } private static int StringToInt(string line) { int result = 0; for (int i = 0; i < line.Length && line[i] >= '0' && line[i] <= '9'; i++) { result = result * 10 + (line[i] - '0'); } return result; } public static void GenerateStartPositions(string path, long numConn) { DebugWriteLine("Reading test corpus at path {0}\n", __arglist(path)); startPos = new int[100]; FileStream fsInput = new FileStream(path, FileMode.Open, FileAccess.Read); StreamReader reader = new StreamReader(fsInput); int cnt = 0; string line; while((line = reader.ReadLine()) != null) { startPos[cnt] = StringToInt(line); if (startPos[cnt] < 0) { DebugStub.Break(); } cnt++; } } internal static int AppMain(Parameters! config) { string server = config.server; if (server[0] >= '0' && server[0] <= '9') { server = "[" + server + "]"; } if (config.numConn < 1 || config.numConn > 100) { Console.WriteLine("numConn must be between [1...100]\n"); return -1; } // Connect to the MailStore. Console.WriteLine("SmtpAgent: Connecting to MailStore."); DirectoryServiceContract.Imp! dsImp = config.nsRef.Acquire(); dsImp.RecvSuccess(); MailStoreContract.Imp! msImp; MailStoreContract.Exp! msExp; MailStoreContract.NewChannel(out msImp, out msExp); mailstorePath = config.mailstorePath; ErrorCode error; if (!SdsUtils.Bind(config.mailstorePath, dsImp, msExp, out error)) { DebugStub.WriteLine("Failed to bind to mail store...error {0}\n", __arglist(SdsUtils.ErrorCodeToString(error))); Console.WriteLine("Failed to bind to mail store...error {0}\n", SdsUtils.ErrorCodeToString(error)); delete dsImp; delete msImp; return -1; } msImp.RecvMailStoreReady(); // Retrieve vaild address list. char[] in ExHeap! buffer; msImp.SendGetAddressList(); msImp.RecvGetAck(out buffer); addresses = ReadUniqueList(Bitter.ToString(buffer)); delete buffer; delete msImp; // Save the endpoint. dsEp = new TRef(dsImp); GenerateCorpus(config.emailCorpus, config.numToSend); GenerateStartPositions(config.startPositionsFile, config.numConn); Console.WriteLine("SmtpAgent: Spawning {0} threads sending {1} emails per connection", config.numConn, config.numToSend); Thread[] workers = new Thread[config.numConn]; ServerSession[] conns = new ServerSession[config.numConn]; for(int i = 0; i < config.numConn; i++) { conns[i] = new ServerSession(corpus, config.numToSend, startPos[i]); workers[i] = new Thread(conns[i].Loop); workers[i].Start(); } for(int i = 0; i < config.numConn; i++) { workers[i].Join(); } // Close the Store engine. dsImp = dsEp.Acquire(); if (dsImp != null) { delete dsImp; } return 0; } public static string PopNextAddress(ref string content) { if (content.Length == 0) { return null; } int i = content.IndexOf(';'); string ret = (i > 0) ? content.Substring(0, i) : content; content = (i > 0) ? content.Substring(i + 1) : ""; return ret; } public static SortedList ReadUniqueList(string content) { SortedList list = new SortedList(); string address; while ((address = PopNextAddress(ref content)) != null) { if (!list.ContainsKey(address)) { list.Add(address, address); } } return list; } } }