/////////////////////////////////////////////////////////////////////////////// // // Microsoft Research Singularity // // Copyright (c) Microsoft Corporation. All rights reserved. // // Note: SMTP Server. // 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.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 { [ConsoleCategory(HelpMessage="SMTP Mail Transfer Agent", DefaultAction=true)] internal class Parameters { [InputEndpoint("data")] public readonly TRef Stdin; [OutputEndpoint("data")] public readonly TRef Stdout; #if false [Endpoint] public readonly TRef msRef; #endif [Endpoint] public readonly TRef nsRef; [StringParameter("mailstore service", Mandatory=false, Default="/service/mailstore", HelpMessage="Location of mail store service.")] internal string mailstorePath; [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 ServerSession : SmtpSession { private static int idCount = 0; private int id; private string command; private string server; public ServerSession(Socket socket, String server) : base(socket) { this.id = idCount++; this.server = server; } 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)) { DebugStub.WriteLine("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); // MailStoreContract.Imp! msImp = config.msRef.Acquire(); msImp.RecvMailStoreReady(); WriteLine7("220 ", server, " Singularity Simple SMTP Service Ready"); bool abort = false; while (!abort) { line = ReadLine7(); if (line == null) { break; } GetCommand(line); switch (command) { case "EHLO": client = line.Split(new Char[] {' '})[1]; Console.WriteLine(":{0}: EHLO {1}", id, client); WriteLine7("250-", server, " greets ", client); WriteLine7("250-8BITMIME"); WriteLine7("250 HELP"); from = null; to = null; cstate = CurrentState.HAD_HELO; // DebugStub.WriteLine("Got EHLO\n"); break; case "HELO": client = line.Split(new Char[] {' '})[1]; Console.WriteLine(":{0}: HELO {1}", id, client); WriteLine7("250 ", server, " greets ", client); from = null; to = null; cstate = CurrentState.HAD_HELO; // DebugStub.WriteLine("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(); WriteLine7("250 OK"); cstate = CurrentState.HAD_MAIL; // DebugStub.WriteLine("Got MAIL\n"); } else { WriteLine7("501 Syntax error in parameters"); } break; case "RCPT": if (cstate != CurrentState.HAD_MAIL && cstate != CurrentState.HAD_RCPT) { goto default; } // DebugStub.WriteLine("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)) { WriteLine7("250 OK"); if (to != null) { to = to + ";" + address; } else { to = address; } cstate = CurrentState.HAD_RCPT; } else { WriteLine7("250 Accepting for forwarding."); cstate = CurrentState.HAD_RCPT; if (to != null) { to = to + ";" + "mail_forward@enron.com"; } else { to = "mail_forward@enron.com"; } } } else { WriteLine7("501 Syntax error in parameters"); } break; case "DATA": if (cstate != CurrentState.HAD_RCPT) { goto default; } // DebugStub.WriteLine("Got DATA\n"); WriteLine7("354 Start mail input; end with ."); bool old = Verbose; Verbose = false; Buffer buffer = Buffer.Allocate(65536); bool good = false; for (;;) { int off = buffer.Prepare(1024); int len = ReadLine8(buffer.Data, off); if (len < 0) { break; } if (len == 1 && buffer.Data[off] == '.') { good = true; break; } else { #if false Console.WriteLine("S: {0}", Encoding.ASCII.GetString(buffer.Data, off, len)); #endif buffer.Save(len); } } Verbose = old; if (good) { bool succeeded = false; char[] in ExHeap! addresses = Bitter.FromString(to); byte[] in ExHeap! data = Bitter.FromByteArray(buffer.Data, 0, buffer.Size); // MailStoreContract.Imp msImp = SmtpServer.msEp.Acquire(); // DebugStub.WriteLine("Saving data\n"); 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; } // SmtpServer.msEp.Release(msImp); if (succeeded) { WriteLine7("250 OK"); } else { WriteLine7("554 Transaction failed"); } } else { WriteLine7("554 Transaction failed"); Dump(); throw new Exception("554 Transaction failed"); } buffer.Release(); cstate = CurrentState.HAD_HELO; from = null; to = null; break; case "NOOP": WriteLine7("250 OK"); break; case "HELP": WriteLine7("250 OK"); break; case "RSET": WriteLine7("250 OK"); from = null; to = null; cstate = CurrentState.HAD_HELO; break; case "QUIT": WriteLine7("221 ", server, " Service closing transmission channel"); abort = true; break; default: WriteLine7("503 Unrecognized command [", command, "]"); abort = true; break; } } Console.WriteLine(":{0}: Session closed", id); delete msImp; Close(); } } public class SmtpServer { public const int _port = 25; public static bool verbose; // public static TRef msEp; public static TRef dsEp; public static SortedList addresses; public static string mailstorePath; internal static int AppMain(Parameters! config) { string server = config.server; if (server[0] >= '0' && server[0] <= '9') { server = "[" + server + "]"; } // 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; } // MailStoreContract.Imp! msImp = config.msRef.Acquire(); 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. // msEp = new TRef(msImp); dsEp = new TRef(dsImp); // Connect to the network. Console.WriteLine("SmtpAgent: Opening TCP port {0}", _port); Socket target = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); target.Bind(new IPEndPoint(IPAddress.Any, _port)); target.Listen((int)SocketOptionName.MaxConnections); for (bool stop = false; !stop;) { Console.WriteLine(":: {0} waiting for accept.", server); Socket socket = target.Accept(); // Create the Session and kick it off in its own thread. ServerSession conn = new ServerSession(socket, server); conn.Verbose = verbose; Thread thread = new Thread(conn.Loop); thread.Start(); } target.Close(); // 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; } } }