using System; using System.Collections.Generic; using System.IO; using System.Net.Sockets; using System.Text; using DateTimeStyles = System.Globalization.DateTimeStyles; using System.Security.Cryptography; namespace Com.Brian_Web.Net { public class POP3 { private static readonly int PORT = 110; private static readonly Encoding ENCODING = new ASCIIEncoding(); private static readonly int INSANE_MESSAGE_NUMBER = 100000; private static readonly int PREVIEW_LINES = 2; private static readonly int DEFAULT_TIMEOUT = 30*1000; private TcpClient sock; private StreamReader inStream; private StreamWriter outStream; private bool authenticated; private string apopTimestamp; internal Message activeMessageReader; private List cachedList; private string[] cachedCapa; private CommandLog cl; /* * Public Interface */ public enum UseTLS { TLS_OFF, TLS_AUTO, TLS_ON }; public POP3(string host) : this(host,UseTLS.TLS_OFF) { } public POP3(string host, UseTLS tls) : this(host,tls,null) { } public POP3(string host, UseTLS tls, CommandLog cl) { this.cl = cl; int port = PORT; if(host.IndexOf(':') != -1) { try { port = NonNegInt(host.Substring(host.IndexOf(':')+1)); host = host.Substring(0,host.IndexOf(':')); } catch(FormatException) { /* fall though */ } } SSL ssl; if(tls == UseTLS.TLS_OFF) { sock = new TcpClient(host,port); ssl = null; } else { sock = ssl = new SSL(host,port,false); } //Timeout = DEFAULT_TIMEOUT; Stream stream = sock.GetStream(); //stream = new BufferedStream(stream); inStream = new StreamReader(stream,ENCODING); outStream = new StreamWriter(stream,ENCODING); string greeting = ReadOKResult(); if(greeting.IndexOf('<') != -1 && greeting.IndexOf('>') != -1) apopTimestamp = greeting.Substring(greeting.IndexOf('<'),greeting.IndexOf('>')-greeting.IndexOf('<')+1); if(tls != UseTLS.TLS_OFF) { bool support = SupportsSSL(); if(!support && tls == UseTLS.TLS_ON) throw new POP3Exn("Server does not support SSL"); if(support) { P("STLS"); ReadOKResult(); ssl.negotiate(); cachedCapa = null; } } } public bool TlsEnabled { get { SSL ssl = sock as SSL; return ssl != null && ssl.IsNegotiated; } } public int Timeout { get { return sock.ReceiveTimeout; } set { sock.SendTimeout = value; sock.ReceiveTimeout = value; } } public string Login(string user, string pass) { if(SupportsSaslMech("CRAM-MD5")) { try { CramMD5Login(user,pass); return "CRAM-MD5"; } catch(CmdFailedExn) { /* fall though */ } } if(apopTimestamp != null) { try { ApopLogin(user,pass); } catch(CmdFailedExn) { /* fall though */ } } ClearLogin(user,pass); return "CLEAR"; } public void ClearLogin(string user, string pass) { if(authenticated) Pe("Already authenticated"); P("USER " + Clean(user)); P("PASS " + Clean(pass)); ReadOKResult(); authenticated = true; } public void ApopLogin(string user, string pass) { if(authenticated) Pe("Already authenticated"); if(apopTimestamp == null) Pe("Server does not support APOP"); P("APOP " + Clean(user) + " " + Md5String(apopTimestamp + pass)); ReadOKResult(); authenticated = true; } public void CramMD5Login(string user, string pass) { if(authenticated) Pe("Already authenticated"); if(!SupportsSaslMech("CRAM-MD5")) Pe("Server does not support CRAM-MD5"); P("AUTH CRAM-MD5"); string b64timestamp = ReadLine(); if(b64timestamp == null) Pe("Hit EOF Early"); if(!b64timestamp.StartsWith("+ ")) Pe("Invalid Response"); byte[] timestamp = Base64Decode(b64timestamp.Substring(2)); byte[] key = GetBytes(pass); string digest = BytesToString(HmacMd5(key,timestamp)); string response = Clean(user) + " " + digest; P(Base64Encode(response)); ReadOKResult(); authenticated = true; } public bool IsAuthenticated { get { return authenticated; } } public string[] Capa() { if(cachedCapa != null) return cachedCapa; if(activeMessageReader != null) Pe("Message reader active"); P("CAPA"); string res = ReadResult(); if(!IsOK(res)) return cachedCapa = new string[0]; List list = new List(); string line; while((line = ReadLineDotEOF()) != null) list.Add(line); return cachedCapa = list.ToArray(); } public string[] SaslMechs() { string[] caps = Capa(); int i; for(i=0;i Stat() { string res = SimpleCommand("STAT"); string[] split = res.Split(new char[] { ' ', '\t' }); if(split.Length < 3) Pe("Invalid Response",true); try { return new Pair(NonNegInt(split[1]),NonNegInt(split[2])); } catch(FormatException) { Pe("Invalid Response",true); return new Pair(0,0); /* not reached */ } } public int FirstNumber { get { return NextNumber(0); } } public int NextNumber(int prev) { if(cachedList == null) CacheList(); if(prev < 0) Pe("Invalid message number"); int i = prev + 1; int end = cachedList.Count; while(i < end && cachedList[i] == -1) i++; return i >= end ? 0 : i; } public int MessageSize(int i) { if(cachedList == null) CacheList(); if(i < 0 || i >= cachedList.Count) Pe("Invalid MessageID"); return cachedList[i]; } public Headers MessageHeaders(int n) { if(n < 0) Pe("Invalid message number"); if(cachedList == null) CacheList(); SimpleCommand("TOP " + n + " " + PREVIEW_LINES); Headers h = ReadHeaders(n); string line; int i=1; while((line = ReadLineDotEOF()) != null) if(line.Length != 0) h["PREVIEW",i++] = line; return h; } public Message MessageBody(int n) { if(n < 0) Pe("Invalid Message Number"); if(cachedList == null) CacheList(); SimpleCommand("RETR " + n); Message m = new Message(this,ReadHeaders(n)); return activeMessageReader = m; } public void DeleteMessage(int n) { if(n < 0) Pe("Invalid Message Number"); SimpleCommand("DELE " + n); cachedList = null; } public void Noop() { if(activeMessageReader != null) Pe("Message reader active"); P("NOOP"); ReadOKResult(); } public void Reset() { SimpleCommand("RSET"); } public void Quit() { SimpleCommand("QUIT"); } public string SimpleCommand(string s) { if(!authenticated) Pe("Not Authenticated"); if(activeMessageReader != null) Pe("Message reader active"); P(s); return ReadOKResult(); } public void Close() { sock.Close(); } private void CacheList() { SimpleCommand("LIST"); List list = new List(); for(;;) { string line = ReadLine(); if(line == null) Pe("Hit EOF Early",true); if(line.Equals(".")) break; string[] split = line.Split(new char[] { ' ','\t' }); if(split.Length < 2) Pe("Invalid response",true); try { int n = NonNegInt(split[0]); if(n == 0 || n >= INSANE_MESSAGE_NUMBER) Pe("Insane message number"); while(list.Count < n) list.Add(-1); list.Add(NonNegInt(split[1])); } catch(FormatException) { Pe("Invalid Response",true); } } cachedList = list; } private Headers ReadHeaders(int number) { Headers h = new Headers(number,0); string line; for(line=ReadLineDotEOF();line != null && line.Length != 0;) { int p = line.IndexOf(':'); if(p == -1) { line = ReadLineDotEOF(); continue; } string name = line.Substring(0,p); StringBuilder sb = new StringBuilder(line.Substring(p+1).Trim()); for(;;) { line = ReadLineDotEOF(); if(line == null || !(line.StartsWith(" ") || line.StartsWith("\t"))) break; sb.Append(" ").Append(line.Trim()); } h.Add(name,sb.ToString()); } if(number < cachedList.Count) h.size = cachedList[number]; return h; } private bool IsOK(string s) { return s.Equals("+OK") || s.StartsWith("+OK "); } private string ReadOKResult() { string res = ReadResult(); if(!IsOK(res)) throw new CmdFailedExn("Server fail: " + res); return res; } private string ReadResult() { string s = ReadLine(); if(s == null) Pe("Remote closed connection early",true); return s; } private string ReadLineDotEOF() { string s = ReadLine(); if(s == null || s.Equals(".")) return null; return s.StartsWith("..") ? s.Substring(1) : s; } private string ReadLine() { string s = inStream.ReadLine(); if(cl != null) cl.Server(s); return s; } private static readonly char[] NEWLINE = new char[] {'\r','\n'}; private void P(string s) { if(cl != null) { if(s.StartsWith("PASSS ")) cl.Client("PASS *password hidden*"); else cl.Client(s); } outStream.Write(s); outStream.Write(NEWLINE); outStream.Flush(); } private static string Clean(string s) { if(s.IndexOf(' ') == -1 && s.IndexOf('\n') == -1 && s.IndexOf('\r') == -1 && s.IndexOf('\t') == -1) return s; StringBuilder sb = new StringBuilder(s.Length); for(int i=0;i= 4 ? int.Parse(args[3]) : -1; Console.WriteLine("Connecting to " + host); POP3 pop3 = new POP3(host,UseTLS.TLS_OFF,new ConsoleCommandLog()); pop3.Login(user,pass); Pair stat = pop3.Stat(); Console.WriteLine("" + stat.a + " messages - " + stat.b + " octets"); for(int n = pop3.FirstNumber;n!=0;n=pop3.NextNumber(n)) Console.WriteLine("Message " + n + " is " + pop3.MessageSize(n) + " octets"); if(message >= 0) { POP3.Message msg = pop3.MessageBody(message); Console.WriteLine("Displaying message {0}",message); Console.WriteLine("===="); Console.WriteLine("To: {0}",msg["to"]); Console.WriteLine("From: {0}",msg.Headers.FromName); Console.WriteLine("Subject: {0}",msg["subject"]); Console.WriteLine("Date: {0}",msg.Headers.Date); string line; while((line = msg.ReadLine()) != null) Console.WriteLine(line); } pop3.Quit(); } public struct Pair { public readonly T a; public readonly U b; public Pair(T a, U b) { this.a = a; this.b = b; } } public static byte[] Md5(byte[] b) { return new MD5CryptoServiceProvider().ComputeHash(b); } public static byte[] HmacMd5(byte[] k, byte[] data) { if(k.Length > 64) k = Md5(k); if(k.Length < 64) { byte[] k2 = new byte[64]; k.CopyTo(k2,0); k = k2; } byte[] k_ipad_and_buf = new byte[64+data.Length]; byte[] k_opad_and_buf = new byte[64+16]; for(int i=0;i<64;i++) { k_ipad_and_buf[i] = (byte) (k[i] ^ 0x36); k_opad_and_buf[i] = (byte) (k[i] ^ 0x5c); } data.CopyTo(k_ipad_and_buf,64); byte[] digest = Md5(k_ipad_and_buf); digest.CopyTo(k_opad_and_buf,64); return Md5(k_opad_and_buf); } private static byte[] Base64Decode(string s) { return Convert.FromBase64String(s); } private static string Base64Encode(byte[] b) { return Convert.ToBase64String(b); } private static byte[] GetBytes(string s) { return ENCODING.GetBytes(s); } private static string BytesToString(byte[] b) { StringBuilder sb = new StringBuilder(b.Length*2); for(int i=0;i>4]); sb.Append("0123456789abcdef"[(b[i]&0x0f)>>0]); } return sb.ToString(); } private static string Base64Encode(string s) { return Base64Encode(GetBytes(s));} private static string Md5String(string s) { return BytesToString(Md5(GetBytes(s))); } } public class RFC822Headers : Hash { public string GetString(string key) { string s = (String) this[key]; return s == null ? "" : s; } public bool Exists(string key) { return this[key] != null; } public bool Add(string name, string val) { name = name.ToLower(); if(val.IndexOf("=?") != -1) val = Rfc2047Decode(val); if(this[name] != null) { int n = 1; while(this[name,n] != null) n++; this[name,n] = val; return true; } else { this[name] = val; return false; } } public string FromName { get { string cached = this["from",typeof(RFC822Headers)] as string; if(cached != null) return cached; string rfc822Addr = GetString("from"); int l = rfc822Addr.IndexOf('<'); int r = rfc822Addr.IndexOf('>'); string s = rfc822Addr; if(l != -1 && r != -1 && r > l && r == rfc822Addr.Length - 1) { if(l == 0) { s = rfc822Addr.Substring(l+1,r-(l+1)); } else { r = l-1; l = 0; while(l < r && s[l] == ' ') l++; while(r > l && s[r] == ' ') r--; if(s[l] == '"') l++; if(s[r] == '"') r--; if(r >= l) s = rfc822Addr.Substring(l,r+1-l); } } else { l = rfc822Addr.IndexOf('('); r = rfc822Addr.IndexOf(')'); if(l != -1 && r != -1 && r > l && rfc822Addr.IndexOf("(",r+1) == -1) s = rfc822Addr.Substring(l+1,r-(l+1)); } this["from",typeof(RFC822Headers)] = s; return s; } } private static readonly DateTime nullDate = new DateTime(); private static readonly String[] dateFormats = { "ddd, dd MMM yyyy HH':mm':'ss z", "ddd, dd MMM yyyy HH':'mm':'ss '\"'z'\"'", "dd MM yyyy HH':'mm':'ss z" }; public DateTime Date { get { Object cached = this["date",typeof(RFC822Headers)]; if(cached != null) return (DateTime) cached; string rfc822Date = GetString("date"); if(rfc822Date.EndsWith(" UT")) rfc822Date += "C"; DateTime? dt = null; for(int i=0;i dict; public Hash() { dict = new Dictionary(); } public object this[object k1] { get { return this[k1,null]; } set { this[k1,null] = value; } } public object this[object k1, object k2] { get { Key k = new Key(k1,k2); return dict.ContainsKey(k) ? dict[k] : null; } set { dict[new Key(k1,k2)] = value; } } } public class SSL : TcpClient { public SSL(string host, int port, bool negotiate) : base(host,port) { if(negotiate) this.negotiate(); } public void negotiate() { throw new IOException("SSL Not supported yet"); } public bool IsNegotiated { get { return false; } } } }