Merge pull request #995 from yjoly/master
[mono.git] / mcs / class / System / System.Net.Mail / SmtpClient.cs
index 8fe2b6ae83df9d688041155c05132b316e178951..c6abb68630e6eb9f60d043b808294012f0849c93 100644 (file)
 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 //
 
-#if NET_2_0
-
 #if SECURITY_DEP
+
+#if MONOTOUCH || MONODROID
+using System.Security.Cryptography.X509Certificates;
+#else
 extern alias PrebuiltSystem;
+using X509CertificateCollection = PrebuiltSystem::System.Security.Cryptography.X509Certificates.X509CertificateCollection;
+using System.Security.Cryptography.X509Certificates;
+#endif
+
 #endif
 
 using System;
@@ -42,7 +48,6 @@ using System.IO;
 using System.Net;
 using System.Net.Mime;
 using System.Net.Sockets;
-using System.Security.Cryptography.X509Certificates;
 using System.Text;
 using System.Threading;
 using System.Reflection;
@@ -50,13 +55,15 @@ using System.Net.Configuration;
 using System.Configuration;
 using System.Net.Security;
 using System.Security.Authentication;
-
-#if SECURITY_DEP
-using X509CertificateCollection = PrebuiltSystem::System.Security.Cryptography.X509Certificates.X509CertificateCollection;
+#if NET_4_5
+using System.Threading.Tasks;
 #endif
 
 namespace System.Net.Mail {
        public class SmtpClient
+#if NET_4_0
+       : IDisposable
+#endif
        {
                #region Fields
 
@@ -83,23 +90,18 @@ namespace System.Net.Mail {
                BackgroundWorker worker;
                object user_async_state;
 
-               // ESMTP state
                [Flags]
                enum AuthMechs {
                        None        = 0,
-                       CramMD5     = 0x01,
-                       DigestMD5   = 0x02,
-                       GssAPI      = 0x04,
-                       Kerberos4   = 0x08,
-                       Login       = 0x10,
-                       Plain       = 0x20,
+                       Login       = 0x01,
+                       Plain       = 0x02,
                }
 
                class CancellationException : Exception
                {
                }
 
-               AuthMechs authMechs = AuthMechs.None;
+               AuthMechs authMechs;
                Mutex mutex = new Mutex ();
 
                #endregion // Fields
@@ -123,6 +125,9 @@ namespace System.Net.Mail {
                        if (cfg != null) {
                                this.host = cfg.Network.Host;
                                this.port = cfg.Network.Port;
+#if NET_4_0
+                               this.enableSsl = cfg.Network.EnableSsl;
+#endif
                                TargetName = cfg.Network.TargetName;
                                if (this.TargetName == null)
                                        TargetName = "SMTPSVC/" + (host != null ? host : "");
@@ -137,7 +142,7 @@ namespace System.Net.Mail {
                                        Credentials = new CCredentialsByHost (cfg.Network.UserName, password);
                                }
 
-                               if (cfg.From != null)
+                               if (!String.IsNullOrEmpty (cfg.From))
                                        defaultFrom = new MailAddress (cfg.From);
                        }
 #else
@@ -150,6 +155,8 @@ namespace System.Net.Mail {
 
                        if (port != 0)
                                this.port = port;
+                       else if (this.port == 0)
+                               this.port = 25;
                }
 
                #endregion // Constructors
@@ -257,7 +264,18 @@ namespace System.Net.Mail {
                #endregion // Events 
 
                #region Methods
+#if NET_4_0
+               public void Dispose ()
+               {
+                       Dispose (true);
+               }
 
+               [MonoTODO ("Does nothing at the moment.")]
+               protected virtual void Dispose (bool disposing)
+               {
+                       // TODO: We should close all the connections and abort any async operations here
+               }
+#endif
                private void CheckState ()
                {
                        if (messageInProcess != null)
@@ -266,8 +284,11 @@ namespace System.Net.Mail {
                
                private static string EncodeAddress(MailAddress address)
                {
-                       string encodedDisplayName = ContentType.EncodeSubjectRFC2047 (address.DisplayName, Encoding.UTF8);
-                       return "\"" + encodedDisplayName + "\" <" + address.Address + ">";
+                       if (!String.IsNullOrEmpty (address.DisplayName)) {
+                               string encodedDisplayName = ContentType.EncodeSubjectRFC2047 (address.DisplayName, Encoding.UTF8);
+                               return "\"" + encodedDisplayName + "\" <" + address.Address + ">";
+                       }
+                       return address.ToString ();
                }
 
                private static string EncodeAddresses(MailAddressCollection addresses)
@@ -418,7 +439,6 @@ namespace System.Net.Mail {
 
                void ParseExtensions (string extens)
                {
-                       char[] delims = new char [1] { ' ' };
                        string[] parts = extens.Split ('\n');
 
                        foreach (string part in parts) {
@@ -427,22 +447,19 @@ namespace System.Net.Mail {
 
                                string start = part.Substring (4);
                                if (start.StartsWith ("AUTH ", StringComparison.Ordinal)) {
-                                       string[] options = start.Split (delims);
+                                       string[] options = start.Split (' ');
                                        for (int k = 1; k < options.Length; k++) {
                                                string option = options[k].Trim();
+                                               // GSSAPI, KERBEROS_V4, NTLM not supported
                                                switch (option) {
+                                               /*
                                                case "CRAM-MD5":
                                                        authMechs |= AuthMechs.CramMD5;
                                                        break;
                                                case "DIGEST-MD5":
                                                        authMechs |= AuthMechs.DigestMD5;
                                                        break;
-                                               case "GSSAPI":
-                                                       authMechs |= AuthMechs.GssAPI;
-                                                       break;
-                                               case "KERBEROS_V4":
-                                                       authMechs |= AuthMechs.Kerberos4;
-                                                       break;
+                                               */
                                                case "LOGIN":
                                                        authMechs |= AuthMechs.Login;
                                                        break;
@@ -527,16 +544,22 @@ namespace System.Net.Mail {
                        try {
                                writer = new StreamWriter(filename);
 
+                               // FIXME: See how Microsoft fixed the bug about envelope senders, and how it actually represents the info in .eml file headers
+                               //        For all we know, defaultFrom may be the envelope sender
+                               // For now, we are no worse than some versions of .NET
                                MailAddress from = message.From;
-
                                if (from == null)
                                        from = defaultFrom;
-                               
-                               SendHeader (HeaderName.Date, DateTime.Now.ToString ("ddd, dd MMM yyyy HH':'mm':'ss zzz", DateTimeFormatInfo.InvariantInfo));
-                               SendHeader (HeaderName.From, from.ToString ());
-                               SendHeader (HeaderName.To, message.To.ToString ());
+
+                               string dt = DateTime.Now.ToString("ddd, dd MMM yyyy HH':'mm':'ss zzz", DateTimeFormatInfo.InvariantInfo);
+                               // remove ':' from time zone offset (e.g. from "+01:00")
+                               dt = dt.Remove(dt.Length - 3, 1);
+                               SendHeader(HeaderName.Date, dt);
+
+                               SendHeader (HeaderName.From, EncodeAddress(from));
+                               SendHeader (HeaderName.To, EncodeAddresses(message.To));
                                if (message.CC.Count > 0)
-                                       SendHeader (HeaderName.Cc, message.CC.ToString ());
+                                       SendHeader (HeaderName.Cc, EncodeAddresses(message.CC));
                                SendHeader (HeaderName.Subject, EncodeSubjectRFC2047 (message));
 
                                foreach (string s in message.Headers.AllKeys)
@@ -605,14 +628,16 @@ namespace System.Net.Mail {
                        
                        if (authMechs != AuthMechs.None)
                                Authenticate ();
-                       
-                       MailAddress from = message.From;
 
-                       if (from == null)
-                               from = defaultFrom;
+                       // The envelope sender: use 'Sender:' in preference of 'From:'
+                       MailAddress sender = message.Sender;
+                       if (sender == null)
+                               sender = message.From;
+                       if (sender == null)
+                               sender = defaultFrom;
                        
                        // MAIL FROM:
-                       status = SendCommand ("MAIL FROM:<" + from.Address + '>');
+                       status = SendCommand ("MAIL FROM:<" + sender.Address + '>');
                        if (IsError (status)) {
                                throw new SmtpException (status.StatusCode, status.Description);
                        }
@@ -658,6 +683,10 @@ namespace System.Net.Mail {
                        dt = dt.Remove (dt.Length - 3, 1);
                        SendHeader (HeaderName.Date, dt);
 
+                       MailAddress from = message.From;
+                       if (from == null)
+                               from = defaultFrom;
+
                        SendHeader (HeaderName.From, EncodeAddress (from));
                        SendHeader (HeaderName.To, EncodeAddresses (message.To));
                        if (message.CC.Count > 0)
@@ -719,6 +748,43 @@ namespace System.Net.Mail {
                        Send (new MailMessage (from, to, subject, body));
                }
 
+#if NET_4_5
+               public Task SendMailAsync (MailMessage message)
+               {
+                       var tcs = new TaskCompletionSource<object> ();
+                       SendCompletedEventHandler handler = null;
+                       handler = (s, e) => SendMailAsyncCompletedHandler (tcs, e, handler, this);
+                       SendCompleted += handler;
+                       SendAsync (message, tcs);
+                       return tcs.Task;
+               }
+
+               public Task SendMailAsync (string from, string recipients, string subject, string body)
+               {
+                       return SendMailAsync (new MailMessage (from, recipients, subject, body));
+               }
+
+               static void SendMailAsyncCompletedHandler (TaskCompletionSource<object> source, AsyncCompletedEventArgs e, SendCompletedEventHandler handler, SmtpClient client)
+               {
+                       if (handler != e.UserState)
+                               return;
+
+                       client.SendCompleted -= handler;
+
+                       if (e.Error != null) {
+                               source.SetException (e.Error);
+                               return;
+                       }
+
+                       if (e.Cancelled) {
+                               source.SetCanceled ();
+                               return;
+                       }
+
+                       source.SetResult (null);
+               }
+#endif
+
                private void SendDot()
                {
                        writer.Write(".\r\n");
@@ -902,10 +968,10 @@ try {
                                        
                                        contentType.Parameters ["type"] = av.ContentType.ToString ();
                                        StartSection (inner_boundary, contentType);
-                                       StartSection (alt_boundary, av.ContentType, av.TransferEncoding);
+                                       StartSection (alt_boundary, av.ContentType, av);
                                } else {
                                        contentType = new ContentType (av.ContentType.ToString ());
-                                       StartSection (inner_boundary, contentType, av.TransferEncoding);
+                                       StartSection (inner_boundary, contentType, av);
                                }
 
                                switch (av.TransferEncoding) {
@@ -950,7 +1016,7 @@ try {
                private void SendLinkedResources (MailMessage message, LinkedResourceCollection resources, string boundary)
                {
                        foreach (LinkedResource lr in resources) {
-                               StartSection (boundary, lr.ContentType, lr.TransferEncoding, lr);
+                               StartSection (boundary, lr.ContentType, lr);
 
                                switch (lr.TransferEncoding) {
                                case TransferEncoding.Base64:
@@ -986,7 +1052,7 @@ try {
                                                contentType.CharSet = att.NameEncoding.HeaderName;
                                        att.ContentDisposition.FileName = att.Name;
                                }
-                               StartSection (boundary, contentType, att.TransferEncoding, att == body ? null : att.ContentDisposition);
+                               StartSection (boundary, contentType, att, att != body);
 
                                byte [] content = new byte [att.ContentStream.Length];
                                att.ContentStream.Read (content, 0, content.Length);
@@ -1032,35 +1098,27 @@ try {
                        SendData (string.Empty);
                }
 
-               private void StartSection (string section, ContentType sectionContentType,TransferEncoding transferEncoding)
+               private void StartSection (string section, ContentType sectionContentType, AttachmentBase att)
                {
                        SendData (String.Format ("--{0}", section));
                        SendHeader ("content-type", sectionContentType.ToString ());
-                       SendHeader ("content-transfer-encoding", GetTransferEncodingName (transferEncoding));
-                       SendData (string.Empty);
-               }
-
-               private void StartSection(string section, ContentType sectionContentType, TransferEncoding transferEncoding, LinkedResource lr)
-               {
-                       SendData (String.Format("--{0}", section));
-                       SendHeader ("content-type", sectionContentType.ToString ());
-                       SendHeader ("content-transfer-encoding", GetTransferEncodingName (transferEncoding));
-
-                       if (lr.ContentId != null && lr.ContentId.Length > 0)
-                               SendHeader("content-ID", "<" + lr.ContentId + ">");
-
+                       SendHeader ("content-transfer-encoding", GetTransferEncodingName (att.TransferEncoding));
+                       if (!string.IsNullOrEmpty (att.ContentId))
+                               SendHeader("content-ID", "<" + att.ContentId + ">");
                        SendData (string.Empty);
                }
 
-               private void StartSection (string section, ContentType sectionContentType, TransferEncoding transferEncoding, ContentDisposition contentDisposition) {
+               private void StartSection (string section, ContentType sectionContentType, Attachment att, bool sendDisposition) {
                        SendData (String.Format ("--{0}", section));
+                       if (!string.IsNullOrEmpty (att.ContentId))
+                               SendHeader("content-ID", "<" + att.ContentId + ">");
                        SendHeader ("content-type", sectionContentType.ToString ());
-                       SendHeader ("content-transfer-encoding", GetTransferEncodingName (transferEncoding));
-                       if (contentDisposition != null)
-                               SendHeader ("content-disposition", contentDisposition.ToString ());
+                       SendHeader ("content-transfer-encoding", GetTransferEncodingName (att.TransferEncoding));
+                       if (sendDisposition)
+                               SendHeader ("content-disposition", att.ContentDisposition.ToString ());
                        SendData (string.Empty);
                }
-
+               
                // use proper encoding to escape input
                private string ToQuotedPrintable (string input, Encoding enc)
                {
@@ -1170,25 +1228,53 @@ try {
                        Authenticate (user, pass);
                }
 
-               void Authenticate (string Username, string Password)
+               void CheckStatus (SmtpResponse status, int i)
                {
-                       // FIXME: use the proper AuthMech
-                       SmtpResponse status = SendCommand ("AUTH LOGIN");
-                       if (((int) status.StatusCode) != 334) {
+                       if (((int) status.StatusCode) != i)
                                throw new SmtpException (status.StatusCode, status.Description);
-                       }
+               }
 
-                       status = SendCommand (Convert.ToBase64String (Encoding.ASCII.GetBytes (Username)));
-                       if (((int) status.StatusCode) != 334) {
+               void ThrowIfError (SmtpResponse status)
+               {
+                       if (IsError (status))
                                throw new SmtpException (status.StatusCode, status.Description);
-                       }
+               }
 
-                       status = SendCommand (Convert.ToBase64String (Encoding.ASCII.GetBytes (Password)));
-                       if (IsError (status)) {
-                               throw new SmtpException (status.StatusCode, status.Description);
+               void Authenticate (string user, string password)
+               {
+                       if (authMechs == AuthMechs.None)
+                               return;
+
+                       SmtpResponse status;
+                       /*
+                       if ((authMechs & AuthMechs.DigestMD5) != 0) {
+                               status = SendCommand ("AUTH DIGEST-MD5");
+                               CheckStatus (status, 334);
+                               string challenge = Encoding.ASCII.GetString (Convert.FromBase64String (status.Description.Substring (4)));
+                               Console.WriteLine ("CHALLENGE: {0}", challenge);
+                               DigestSession session = new DigestSession ();
+                               session.Parse (false, challenge);
+                               string response = session.Authenticate (this, user, password);
+                               status = SendCommand (Convert.ToBase64String (Encoding.UTF8.GetBytes (response)));
+                               CheckStatus (status, 235);
+                       } else */
+                       if ((authMechs & AuthMechs.Login) != 0) {
+                               status = SendCommand ("AUTH LOGIN");
+                               CheckStatus (status, 334);
+                               status = SendCommand (Convert.ToBase64String (Encoding.UTF8.GetBytes (user)));
+                               CheckStatus (status, 334);
+                               status = SendCommand (Convert.ToBase64String (Encoding.UTF8.GetBytes (password)));
+                               CheckStatus (status, 235);
+                       } else if ((authMechs & AuthMechs.Plain) != 0) {
+                               string s = String.Format ("\0{0}\0{1}", user, password);
+                               s = Convert.ToBase64String (Encoding.UTF8.GetBytes (s));
+                               status = SendCommand ("AUTH PLAIN " + s);
+                               CheckStatus (status, 235);
+                       } else {
+                               throw new SmtpException ("AUTH types PLAIN, LOGIN not supported by the server");
                        }
                }
-               
+
                #endregion // Methods
                
                // The HeaderName struct is used to store constant string values representing mail headers.
@@ -1251,4 +1337,3 @@ try {
        }
 }
 
-#endif // NET_2_0