New tests.
[mono.git] / mcs / class / System / System.Net.Mail / SmtpClient.cs
1 //
2 // System.Net.Mail.SmtpClient.cs
3 //
4 // Author:
5 //      Tim Coleman (tim@timcoleman.com)
6 //
7 // Copyright (C) Tim Coleman, 2004
8 //
9
10 //
11 // Permission is hereby granted, free of charge, to any person obtaining
12 // a copy of this software and associated documentation files (the
13 // "Software"), to deal in the Software without restriction, including
14 // without limitation the rights to use, copy, modify, merge, publish,
15 // distribute, sublicense, and/or sell copies of the Software, and to
16 // permit persons to whom the Software is furnished to do so, subject to
17 // the following conditions:
18 // 
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
21 // 
22 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 //
30
31 #if NET_2_0
32
33 #if SECURITY_DEP
34 extern alias PrebuiltSystem;
35 #endif
36
37 using System;
38 using System.Collections.Generic;
39 using System.ComponentModel;
40 using System.Globalization;
41 using System.IO;
42 using System.Net;
43 using System.Net.Mime;
44 using System.Net.Sockets;
45 using System.Security.Cryptography.X509Certificates;
46 using System.Text;
47 using System.Threading;
48 using System.Reflection;
49 using System.Net.Configuration;
50 using System.Configuration;
51 using System.Net.Security;
52 using System.Security.Authentication;
53
54 #if SECURITY_DEP
55 using X509CertificateCollection = PrebuiltSystem::System.Security.Cryptography.X509Certificates.X509CertificateCollection;
56 #endif
57
58 namespace System.Net.Mail {
59         public class SmtpClient
60         {
61                 #region Fields
62
63                 string host;
64                 int port;
65                 int timeout = 100000;
66                 ICredentialsByHost credentials;
67                 bool useDefaultCredentials = false;
68                 string pickupDirectoryLocation;
69                 SmtpDeliveryMethod deliveryMethod;
70                 bool enableSsl;
71                 X509CertificateCollection clientCertificates;
72
73                 TcpClient client;
74                 Stream stream;
75                 StreamWriter writer;
76                 StreamReader reader;
77                 int boundaryIndex;
78                 MailAddress defaultFrom;
79
80                 MailMessage messageInProcess;
81
82                 BackgroundWorker worker;
83                 object user_async_state;
84
85                 // ESMTP state
86                 [Flags]
87                 enum AuthMechs {
88                         None        = 0,
89                         CramMD5     = 0x01,
90                         DigestMD5   = 0x02,
91                         GssAPI      = 0x04,
92                         Kerberos4   = 0x08,
93                         Login       = 0x10,
94                         Plain       = 0x20,
95                 }
96
97                 class CancellationException : Exception
98                 {
99                 }
100
101                 AuthMechs authMechs = AuthMechs.None;
102                 bool canStartTLS = false;
103
104                 Mutex mutex = new Mutex ();
105
106                 #endregion // Fields
107
108                 #region Constructors
109
110                 public SmtpClient ()
111                         : this (null, 0)
112                 {
113                 }
114
115                 public SmtpClient (string host)
116                         : this (host, 0)
117                 {
118                 }
119
120                 public SmtpClient (string host, int port) {
121 #if CONFIGURATION_DEP
122                         SmtpSection cfg = (SmtpSection) ConfigurationManager.GetSection ("system.net/mailSettings/smtp");
123
124                         if (cfg != null) {
125                                 this.host = cfg.Network.Host;
126                                 this.port = cfg.Network.Port;
127                                 if (cfg.Network.UserName != null) {
128                                         string password = String.Empty;
129
130                                         if (cfg.Network.Password != null)
131                                                 password = cfg.Network.Password;
132
133                                         Credentials = new CCredentialsByHost (cfg.Network.UserName, password);
134                                 }
135
136                                 if (cfg.From != null)
137                                         defaultFrom = new MailAddress (cfg.From);
138                         }
139 #endif
140
141                         if (!String.IsNullOrEmpty (host))
142                                 this.host = host;
143
144                         if (port != 0)
145                                 this.port = port;
146                 }
147
148                 #endregion // Constructors
149
150                 #region Properties
151
152 #if SECURITY_DEP
153                 [MonoTODO("STARTTLS is not supported yet")]
154                 public X509CertificateCollection ClientCertificates {
155                         get {
156                                 if (clientCertificates == null)
157                                         clientCertificates = new X509CertificateCollection ();
158                                 return clientCertificates;
159                         }
160                 }
161 #endif
162
163                 public ICredentialsByHost Credentials {
164                         get { return credentials; }
165                         set {
166                                 CheckState ();
167                                 credentials = value;
168                         }
169                 }
170
171                 public SmtpDeliveryMethod DeliveryMethod {
172                         get { return deliveryMethod; }
173                         set {
174                                 CheckState ();
175                                 deliveryMethod = value;
176                         }
177                 }
178
179                 [MonoTODO("STARTTLS is not supported yet")]
180                 public bool EnableSsl {
181                         // FIXME: So... is this supposed to enable SSL port functionality? or STARTTLS? Or both?
182                         get { return enableSsl; }
183                         set {
184                                 CheckState ();
185                                 enableSsl = value;
186                         }
187                 }
188
189                 public string Host {
190                         get { return host; }
191                         set {
192                                 if (value == null)
193                                         throw new ArgumentNullException ();
194                                 if (value.Length == 0)
195                                         throw new ArgumentException ();
196                                 CheckState ();
197                                 host = value;
198                         }
199                 }
200
201                 public string PickupDirectoryLocation {
202                         get { return pickupDirectoryLocation; }
203                         set { pickupDirectoryLocation = value; }
204                 }
205
206                 public int Port {
207                         get { return port; }
208                         set { 
209                                 if (value <= 0 || value > 65535)
210                                         throw new ArgumentOutOfRangeException ();
211                                 CheckState ();
212                                 port = value;
213                         }
214                 }
215
216                 [MonoTODO]
217                 public ServicePoint ServicePoint {
218                         get { throw new NotImplementedException (); }
219                 }
220
221                 public int Timeout {
222                         get { return timeout; }
223                         set { 
224                                 if (value < 0)
225                                         throw new ArgumentOutOfRangeException ();
226                                 CheckState ();
227                                 timeout = value; 
228                         }
229                 }
230
231                 public bool UseDefaultCredentials {
232                         get { return useDefaultCredentials; }
233                         [MonoNotSupported ("no DefaultCredential support in Mono")]
234                         set {
235                                 if (value)
236                                         throw new NotImplementedException ("Default credentials are not supported");
237                                 CheckState ();
238                         }
239                 }
240
241                 #endregion // Properties
242
243                 #region Events 
244
245                 public event SendCompletedEventHandler SendCompleted;
246
247                 #endregion // Events 
248
249                 #region Methods
250
251                 private void CheckState ()
252                 {
253                         if (messageInProcess != null)
254                                 throw new InvalidOperationException ("Cannot set Timeout while Sending a message");
255                 }
256                 
257                 private static string EncodeAddress(MailAddress address)
258                 {
259                         string encodedDisplayName = ContentType.EncodeSubjectRFC2047 (address.DisplayName, Encoding.UTF8);
260                         return "\"" + encodedDisplayName + "\" <" + address.Address + ">";
261                 }
262
263                 private static string EncodeAddresses(MailAddressCollection addresses)
264                 {
265                         StringBuilder sb = new StringBuilder();
266                         bool first = true;
267                         foreach (MailAddress address in addresses) {
268                                 if (!first) {
269                                         sb.Append(", ");
270                                 }
271                                 sb.Append(EncodeAddress(address));
272                                 first = false;
273                         }
274                         return sb.ToString();
275                 }
276
277                 private string EncodeSubjectRFC2047 (MailMessage message)
278                 {
279                         return ContentType.EncodeSubjectRFC2047 (message.Subject, message.SubjectEncoding);
280                 }
281
282                 private string EncodeBody (MailMessage message)
283                 {
284                         string body = message.Body;
285                         Encoding encoding = message.BodyEncoding;
286                         // RFC 2045 encoding
287                         switch (message.ContentTransferEncoding) {
288                         case TransferEncoding.SevenBit:
289                                 return body;
290                         case TransferEncoding.Base64:
291                                 return Convert.ToBase64String (encoding.GetBytes (body), Base64FormattingOptions.InsertLineBreaks);
292                         default:
293                                 return ToQuotedPrintable (body, encoding);
294                         }
295                 }
296
297                 private string EncodeBody (AlternateView av)
298                 {
299                         //Encoding encoding = av.ContentType.CharSet != null ? Encoding.GetEncoding (av.ContentType.CharSet) : Encoding.UTF8;
300
301                         byte [] bytes = new byte [av.ContentStream.Length];
302                         av.ContentStream.Read (bytes, 0, bytes.Length);
303
304                         // RFC 2045 encoding
305                         switch (av.TransferEncoding) {
306                         case TransferEncoding.SevenBit:
307                                 return Encoding.ASCII.GetString (bytes);
308                         case TransferEncoding.Base64:
309                                 return Convert.ToBase64String (bytes, Base64FormattingOptions.InsertLineBreaks);
310                         default:
311                                 return ToQuotedPrintable (bytes);
312                         }
313                 }
314
315
316                 private void EndSection (string section)
317                 {
318                         SendData (String.Format ("--{0}--", section));
319                         SendData (string.Empty);
320                 }
321
322                 private string GenerateBoundary ()
323                 {
324                         string output = GenerateBoundary (boundaryIndex);
325                         boundaryIndex += 1;
326                         return output;
327                 }
328
329                 private static string GenerateBoundary (int index)
330                 {
331                         return String.Format ("--boundary_{0}_{1}", index, Guid.NewGuid ().ToString ("D"));
332                 }
333
334                 private bool IsError (SmtpResponse status)
335                 {
336                         return ((int) status.StatusCode) >= 400;
337                 }
338
339                 protected void OnSendCompleted (AsyncCompletedEventArgs e)
340                 {
341                         try {
342                                 if (SendCompleted != null)
343                                         SendCompleted (this, e);
344                         } finally {
345                                 worker = null;
346                                 user_async_state = null;
347                         }
348                 }
349
350                 private void CheckCancellation ()
351                 {
352                         if (worker != null && worker.CancellationPending)
353                                 throw new CancellationException ();
354                 }
355
356                 private SmtpResponse Read () {
357                         byte [] buffer = new byte [512];
358                         int position = 0;
359                         bool lastLine = false;
360
361                         do {
362                                 CheckCancellation ();
363
364                                 int readLength = stream.Read (buffer, position, buffer.Length - position);
365                                 if (readLength > 0) {
366                                         int available = position + readLength - 1;
367                                         if (available > 4 && (buffer [available] == '\n' || buffer [available] == '\r'))
368                                                 for (int index = available - 3; ; index--) {
369                                                         if (index < 0 || buffer [index] == '\n' || buffer [index] == '\r') {
370                                                                 lastLine = buffer [index + 4] == ' ';
371                                                                 break;
372                                                         }
373                                                 }
374
375                                         // move position
376                                         position += readLength;
377
378                                         // check if buffer is full
379                                         if (position == buffer.Length) {
380                                                 byte [] newBuffer = new byte [buffer.Length * 2];
381                                                 Array.Copy (buffer, 0, newBuffer, 0, buffer.Length);
382                                                 buffer = newBuffer;
383                                         }
384                                 }
385                                 else {
386                                         break;
387                                 }
388                         } while (!lastLine);
389
390                         if (position > 0) {
391                                 Encoding encoding = new ASCIIEncoding ();
392
393                                 string line = encoding.GetString (buffer, 0, position - 1);
394
395                                 // parse the line to the lastResponse object
396                                 SmtpResponse response = SmtpResponse.Parse (line);
397
398                                 return response;
399                         } else {
400                                 throw new System.IO.IOException ("Connection closed");
401                         }
402                 }
403
404                 void ResetExtensions()
405                 {
406                         canStartTLS = false;
407                         authMechs = AuthMechs.None;
408                 }
409
410                 void ParseExtensions (string extens)
411                 {
412                         char[] delims = new char [1] { ' ' };
413                         string[] parts = extens.Split ('\n');
414
415                         foreach (string part in parts) {
416                                 if (part.Length < 4)
417                                         continue;
418
419                                 string start = part.Substring (4);
420                                 if (start.StartsWith ("AUTH ", StringComparison.Ordinal)) {
421                                         string[] options = start.Split (delims);
422                                         for (int k = 1; k < options.Length; k++) {
423                                                 string option = options[k].Trim();
424                                                 switch (option) {
425                                                 case "CRAM-MD5":
426                                                         authMechs |= AuthMechs.CramMD5;
427                                                         break;
428                                                 case "DIGEST-MD5":
429                                                         authMechs |= AuthMechs.DigestMD5;
430                                                         break;
431                                                 case "GSSAPI":
432                                                         authMechs |= AuthMechs.GssAPI;
433                                                         break;
434                                                 case "KERBEROS_V4":
435                                                         authMechs |= AuthMechs.Kerberos4;
436                                                         break;
437                                                 case "LOGIN":
438                                                         authMechs |= AuthMechs.Login;
439                                                         break;
440                                                 case "PLAIN":
441                                                         authMechs |= AuthMechs.Plain;
442                                                         break;
443                                                 }
444                                         }
445                                 } else if (start.StartsWith ("STARTTLS", StringComparison.Ordinal)) {
446                                         canStartTLS = true;
447                                 }
448                         }
449                 }
450
451                 public void Send (MailMessage message)
452                 {
453                         if (message == null)
454                                 throw new ArgumentNullException ("message");
455
456                         if (deliveryMethod == SmtpDeliveryMethod.Network && String.IsNullOrEmpty (Host))
457                                 throw new InvalidOperationException ("The SMTP host was not specified");
458                         if (deliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory) {
459                                 if (String.IsNullOrEmpty(pickupDirectoryLocation))
460                                         throw new SmtpException("The pickup directory was not specified");
461                                 if (false && pickupDirectoryLocation[0] != '/')
462                                         throw new SmtpException("Only absolute directories are allowed for pickup directory.");
463                         }
464                         if (deliveryMethod == SmtpDeliveryMethod.PickupDirectoryFromIis)
465                                 throw new NotSupportedException("IIS delivery is not supported");
466
467                         if (port == 0)
468                                 port = 25;
469                         
470                         // Block while sending
471                         mutex.WaitOne ();
472                         try {
473                                 messageInProcess = message;
474                                 if (deliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory)
475                                         SendToFile (message, pickupDirectoryLocation + "/" + Guid.NewGuid() + ".eml");
476                                 else
477                                         SendInternal (message);
478                         } catch (CancellationException) {
479                                 // This exception is introduced for convenient cancellation process.
480                         } finally {
481                                 // Release the mutex to allow other threads access
482                                 mutex.ReleaseMutex ();
483                                 messageInProcess = null;
484                         }
485                 }
486
487                 private void SendInternal (MailMessage message)
488                 {
489                         CheckCancellation ();
490
491                         try {
492                                 client = new TcpClient (host, port);
493                                 stream = client.GetStream ();
494                                 // FIXME: this StreamWriter creation is bogus.
495                                 // It expects as if a Stream were able to switch to SSL
496                                 // mode (such behavior is only in Mainsoft Socket API).
497                                 writer = new StreamWriter (stream);
498                                 reader = new StreamReader (stream);
499
500                                 SendCore (message);
501                         } finally {
502                                 if (writer != null)
503                                         writer.Close ();
504                                 if (reader != null)
505                                         reader.Close ();
506                                 if (stream != null)
507                                         stream.Close ();
508                                 if (client != null)
509                                         client.Close ();
510                         }
511                 }
512  
513                 // FIXME: simple implementation, could be brushed up.
514                 private void SendToFile (MailMessage message, String filename)
515                 {
516                         try {
517                                 writer = new StreamWriter(filename);
518
519                                 MailAddress from = message.From;
520
521                                 if (from == null)
522                                         from = defaultFrom;
523                                 
524                                 SendHeader (HeaderName.Date, DateTime.Now.ToString ("ddd, dd MMM yyyy HH':'mm':'ss zzz", DateTimeFormatInfo.InvariantInfo));
525                                 SendHeader (HeaderName.From, from.ToString ());
526                                 SendHeader (HeaderName.To, message.To.ToString ());
527                                 if (message.CC.Count > 0)
528                                         SendHeader (HeaderName.Cc, message.CC.ToString ());
529                                 SendHeader (HeaderName.Subject, EncodeSubjectRFC2047 (message));
530
531                                 foreach (string s in message.Headers.AllKeys)
532                                         SendHeader (s, message.Headers [s]);
533
534                                 AddPriorityHeader (message);
535
536                                 boundaryIndex = 0;
537                                 if (message.Attachments.Count > 0)
538                                         SendWithAttachments (message);
539                                 else
540                                         SendWithoutAttachments (message, null, false);
541
542
543                         } finally {
544                                 if (writer != null) writer.Close(); writer = null;
545                         }
546                 }
547
548                 private void SendCore (MailMessage message)
549                 {
550                         SmtpResponse status;
551
552                         status = Read ();
553                         if (IsError (status))
554                                 throw new SmtpException (status.StatusCode, status.Description);
555
556                         // EHLO
557                         
558                         // FIXME: parse the list of extensions so we don't bother wasting
559                         // our time trying commands if they aren't supported.
560                         status = SendCommand ("EHLO " + Dns.GetHostName ());
561                         
562                         if (IsError (status)) {
563                                 status = SendCommand ("HELO " + Dns.GetHostName ());
564                                 
565                                 if (IsError (status))
566                                         throw new SmtpException (status.StatusCode, status.Description);
567                         } else {
568                                 // Parse ESMTP extensions
569                                 string extens = status.Description;
570                                 
571                                 if (extens != null)
572                                         ParseExtensions (extens);
573                         }
574                         
575                         if (enableSsl) {
576 #if old_comment
577                                 // FIXME: I get the feeling from the docs that EnableSsl is meant
578                                 // for using the SSL-port and not STARTTLS (or, if it includes
579                                 // STARTTLS... only use STARTTLS if the SSL-type in the certificate
580                                 // is TLS and not SSLv2 or SSLv3)
581                                 
582                                 // FIXME: even tho we have a canStartTLS flag... ignore it for now
583                                 // so that the STARTTLS command can throw the appropriate
584                                 // SmtpException if STARTTLS is unavailable
585 #else
586                                 // SmtpClient implements STARTTLS support.
587 #endif
588                                 InitiateSecureConnection ();
589                                 ResetExtensions();
590                                 writer = new StreamWriter (stream);
591                                 reader = new StreamReader (stream);
592                                 status = SendCommand ("EHLO " + Dns.GetHostName ());
593                         
594                                 if (IsError (status)) {
595                                         status = SendCommand ("HELO " + Dns.GetHostName ());
596                                 
597                                         if (IsError (status))
598                                                 throw new SmtpException (status.StatusCode, status.Description);
599                                 } else {
600                                         // Parse ESMTP extensions
601                                         string extens = status.Description;
602                                         if (extens != null)
603                                                 ParseExtensions (extens);
604                                 }
605                         }
606                         
607                         if (authMechs != AuthMechs.None)
608                                 Authenticate ();
609                         
610                         MailAddress from = message.From;
611
612                         if (from == null)
613                                 from = defaultFrom;
614                         
615                         // MAIL FROM:
616                         status = SendCommand ("MAIL FROM:<" + from.Address + '>');
617                         if (IsError (status)) {
618                                 throw new SmtpException (status.StatusCode, status.Description);
619                         }
620
621                         // Send RCPT TO: for all recipients
622                         List<SmtpFailedRecipientException> sfre = new List<SmtpFailedRecipientException> ();
623
624                         for (int i = 0; i < message.To.Count; i ++) {
625                                 status = SendCommand ("RCPT TO:<" + message.To [i].Address + '>');
626                                 if (IsError (status)) 
627                                         sfre.Add (new SmtpFailedRecipientException (status.StatusCode, message.To [i].Address));
628                         }
629                         for (int i = 0; i < message.CC.Count; i ++) {
630                                 status = SendCommand ("RCPT TO:<" + message.CC [i].Address + '>');
631                                 if (IsError (status)) 
632                                         sfre.Add (new SmtpFailedRecipientException (status.StatusCode, message.CC [i].Address));
633                         }
634                         for (int i = 0; i < message.Bcc.Count; i ++) {
635                                 status = SendCommand ("RCPT TO:<" + message.Bcc [i].Address + '>');
636                                 if (IsError (status)) 
637                                         sfre.Add (new SmtpFailedRecipientException (status.StatusCode, message.Bcc [i].Address));
638                         }
639
640 #if TARGET_JVM // List<T>.ToArray () is not supported
641                         if (sfre.Count > 0) {
642                                 SmtpFailedRecipientException[] xs = new SmtpFailedRecipientException[sfre.Count];
643                                 sfre.CopyTo (xs);
644                                 throw new SmtpFailedRecipientsException ("failed recipients", xs);
645                         }
646 #else
647                         if (sfre.Count >0)
648                                 throw new SmtpFailedRecipientsException ("failed recipients", sfre.ToArray ());
649 #endif
650
651                         // DATA
652                         status = SendCommand ("DATA");
653                         if (IsError (status))
654                                 throw new SmtpException (status.StatusCode, status.Description);
655
656                         // Send message headers
657                         string dt = DateTime.Now.ToString ("ddd, dd MMM yyyy HH':'mm':'ss zzz", DateTimeFormatInfo.InvariantInfo);
658                         // remove ':' from time zone offset (e.g. from "+01:00")
659                         dt = dt.Remove (dt.Length - 3, 1);
660                         SendHeader (HeaderName.Date, dt);
661
662                         SendHeader (HeaderName.From, EncodeAddress (from));
663                         SendHeader (HeaderName.To, EncodeAddresses (message.To));
664                         if (message.CC.Count > 0)
665                                 SendHeader (HeaderName.Cc, EncodeAddresses (message.CC));
666                         SendHeader (HeaderName.Subject, EncodeSubjectRFC2047 (message));
667
668                         foreach (string s in message.Headers.AllKeys)
669                                 SendHeader (s, message.Headers [s]);
670
671                         AddPriorityHeader (message);
672
673                         boundaryIndex = 0;
674                         if (message.Attachments.Count > 0)
675                                 SendWithAttachments (message);
676                         else
677                                 SendWithoutAttachments (message, null, false);
678
679                         SendData (".");
680
681                         status = Read ();
682                         if (IsError (status))
683                                 throw new SmtpException (status.StatusCode, status.Description);
684
685                         try {
686                                 status = SendCommand ("QUIT");
687                         } catch (System.IO.IOException) {
688                                 // We excuse server for the rude connection closing as a response to QUIT
689                         }
690                 }
691
692                 public void Send (string from, string to, string subject, string body)
693                 {
694                         Send (new MailMessage (from, to, subject, body));
695                 }
696
697                 private void SendData (string data)
698                 {
699                         CheckCancellation ();
700
701                         // Certain SMTP servers will reject mail sent with unix line-endings; see http://cr.yp.to/docs/smtplf.html
702                         StringBuilder sb = new StringBuilder (data);
703                         sb.Replace ("\r\n", "\n");
704                         sb.Replace ('\r', '\n');
705                         sb.Replace ("\n", "\r\n");
706                         writer.Write (sb.ToString ());
707                         writer.Write ("\r\n");
708                         writer.Flush ();
709                 }
710
711                 public void SendAsync (MailMessage message, object userToken)
712                 {
713                         if (worker != null)
714                                 throw new InvalidOperationException ("Another SendAsync operation is in progress");
715
716                         worker = new BackgroundWorker ();
717                         worker.DoWork += delegate (object o, DoWorkEventArgs ea) {
718                                 try {
719                                         user_async_state = ea.Argument;
720                                         Send (message);
721                                 } catch (Exception ex) {
722                                         ea.Result = ex;
723                                 }
724                         };
725                         worker.WorkerSupportsCancellation = true;
726                         worker.RunWorkerCompleted += delegate (object o, RunWorkerCompletedEventArgs ea) {
727                                 // Note that RunWorkerCompletedEventArgs.UserState cannot be used (LAMESPEC)
728                                 OnSendCompleted (new AsyncCompletedEventArgs (ea.Error, ea.Cancelled, user_async_state));
729                         };
730                         worker.RunWorkerAsync (userToken);
731                 }
732
733                 public void SendAsync (string from, string to, string subject, string body, object userToken)
734                 {
735                         SendAsync (new MailMessage (from, to, subject, body), userToken);
736                 }
737
738                 public void SendAsyncCancel ()
739                 {
740                         if (worker == null)
741                                 throw new InvalidOperationException ("SendAsync operation is not in progress");
742                         worker.CancelAsync ();
743                 }
744
745                 private void AddPriorityHeader (MailMessage message) {
746                         switch (message.Priority) {
747                         case MailPriority.High:
748                                 SendHeader (HeaderName.Priority, "Urgent");
749                                 SendHeader (HeaderName.Importance, "high");
750                                 SendHeader (HeaderName.XPriority, "1");
751                                 break;
752                         case MailPriority.Low:
753                                 SendHeader (HeaderName.Priority, "Non-Urgent");
754                                 SendHeader (HeaderName.Importance, "low");
755                                 SendHeader (HeaderName.XPriority, "5");
756                                 break;
757                         }
758                 }
759
760                 private void SendSimpleBody (MailMessage message) {
761                         SendHeader (HeaderName.ContentType, message.BodyContentType.ToString ());
762                         if (message.ContentTransferEncoding != TransferEncoding.SevenBit)
763                                 SendHeader (HeaderName.ContentTransferEncoding, GetTransferEncodingName (message.ContentTransferEncoding));
764                         SendData (string.Empty);
765
766                         SendData (EncodeBody (message));
767                 }
768
769                 private void SendBodylessSingleAlternate (AlternateView av) {
770                         SendHeader (HeaderName.ContentType, av.ContentType.ToString ());
771                         if (av.TransferEncoding != TransferEncoding.SevenBit)
772                                 SendHeader (HeaderName.ContentTransferEncoding, GetTransferEncodingName (av.TransferEncoding));
773                         SendData (string.Empty);
774
775                         SendData (EncodeBody (av));
776                 }
777
778                 private void SendWithoutAttachments (MailMessage message, string boundary, bool attachmentExists)
779                 {
780                         if (message.Body == null && message.AlternateViews.Count == 1)
781                                 SendBodylessSingleAlternate (message.AlternateViews [0]);
782                         else if (message.AlternateViews.Count > 0)
783                                 SendBodyWithAlternateViews (message, boundary, attachmentExists);
784                         else
785                                 SendSimpleBody (message);
786                 }
787
788
789                 private void SendWithAttachments (MailMessage message) {
790                         string boundary = GenerateBoundary ();
791
792                         // first "multipart/mixed"
793                         ContentType messageContentType = new ContentType ();
794                         messageContentType.Boundary = boundary;
795                         messageContentType.MediaType = "multipart/mixed";
796                         messageContentType.CharSet = null;
797
798                         SendHeader (HeaderName.ContentType, messageContentType.ToString ());
799                         SendData (String.Empty);
800
801                         // body section
802                         Attachment body = null;
803
804                         if (message.AlternateViews.Count > 0)
805                                 SendWithoutAttachments (message, boundary, true);
806                         else {
807                                 body = Attachment.CreateAttachmentFromString (message.Body, null, message.BodyEncoding, message.IsBodyHtml ? "text/html" : "text/plain");
808                                 message.Attachments.Insert (0, body);
809                         }
810
811                         try {
812                                 SendAttachments (message, body, boundary);
813                         } finally {
814                                 if (body != null)
815                                         message.Attachments.Remove (body);
816                         }
817
818                         EndSection (boundary);
819                 }
820
821                 private void SendBodyWithAlternateViews (MailMessage message, string boundary, bool attachmentExists)
822                 {
823                         AlternateViewCollection alternateViews = message.AlternateViews;
824
825                         string inner_boundary = GenerateBoundary ();
826
827                         ContentType messageContentType = new ContentType ();
828                         messageContentType.Boundary = inner_boundary;
829                         messageContentType.MediaType = "multipart/alternative";
830
831                         if (!attachmentExists) {
832                                 SendHeader (HeaderName.ContentType, messageContentType.ToString ());
833                                 SendData (String.Empty);
834                         }
835
836                         // body section
837                         AlternateView body = null;
838                         if (message.Body != null) {
839                                 body = AlternateView.CreateAlternateViewFromString (message.Body, message.BodyEncoding, message.IsBodyHtml ? "text/html" : "text/plain");
840                                 alternateViews.Insert (0, body);
841                                 StartSection (boundary, messageContentType);
842                         }
843
844 try {
845                         // alternate view sections
846                         foreach (AlternateView av in alternateViews) {
847
848                                 string alt_boundary = null;
849                                 ContentType contentType;
850                                 if (av.LinkedResources.Count > 0) {
851                                         alt_boundary = GenerateBoundary ();
852                                         contentType = new ContentType ("multipart/related");
853                                         contentType.Boundary = alt_boundary;
854                                         contentType.Parameters ["type"] = "application/octet-stream";
855                                         StartSection (inner_boundary, contentType);
856                                 } else {
857                                         contentType = new ContentType (av.ContentType.ToString ());
858                                         StartSection (inner_boundary, contentType, av.TransferEncoding);
859                                 }
860
861                                 switch (av.TransferEncoding) {
862                                 case TransferEncoding.Base64:
863                                         byte [] content = new byte [av.ContentStream.Length];
864                                         av.ContentStream.Read (content, 0, content.Length);
865 #if TARGET_JVM
866                                         SendData (Convert.ToBase64String (content));
867 #else
868                                             SendData (Convert.ToBase64String (content, Base64FormattingOptions.InsertLineBreaks));
869 #endif
870                                         break;
871                                 case TransferEncoding.QuotedPrintable:
872                                         byte [] bytes = new byte [av.ContentStream.Length];
873                                         av.ContentStream.Read (bytes, 0, bytes.Length);
874                                         SendData (ToQuotedPrintable (bytes));
875                                         break;
876                                 case TransferEncoding.SevenBit:
877                                 case TransferEncoding.Unknown:
878                                         content = new byte [av.ContentStream.Length];
879                                         SendData (Encoding.ASCII.GetString (content));
880                                         break;
881                                 }
882
883                                 if (av.LinkedResources.Count > 0) {
884                                         SendLinkedResources (message, av.LinkedResources, alt_boundary);
885                                         EndSection (alt_boundary);
886                                 }
887
888                                 if (!attachmentExists)
889                                         SendData (string.Empty);
890                         }
891
892 } finally {
893                         if (body != null)
894                                 alternateViews.Remove (body);
895 }
896                         EndSection (inner_boundary);
897                 }
898
899                 private void SendLinkedResources (MailMessage message, LinkedResourceCollection resources, string boundary)
900                 {
901                         foreach (LinkedResource lr in resources) {
902                                 ContentType contentType = new ContentType (lr.ContentType.ToString ());
903                                 StartSection (boundary, contentType, lr.TransferEncoding);
904
905                                 switch (lr.TransferEncoding) {
906                                 case TransferEncoding.Base64:
907                                         byte [] content = new byte [lr.ContentStream.Length];
908                                         lr.ContentStream.Read (content, 0, content.Length);
909 #if TARGET_JVM
910                                         SendData (Convert.ToBase64String (content));
911 #else
912                                             SendData (Convert.ToBase64String (content, Base64FormattingOptions.InsertLineBreaks));
913 #endif
914                                         break;
915                                 case TransferEncoding.QuotedPrintable:
916                                         byte [] bytes = new byte [lr.ContentStream.Length];
917                                         lr.ContentStream.Read (bytes, 0, bytes.Length);
918                                         SendData (ToQuotedPrintable (bytes));
919                                         break;
920                                 case TransferEncoding.SevenBit:
921                                 case TransferEncoding.Unknown:
922                                         content = new byte [lr.ContentStream.Length];
923                                         SendData (Encoding.ASCII.GetString (content));
924                                         break;
925                                 }
926                         }
927                 }
928
929                 private void SendAttachments (MailMessage message, Attachment body, string boundary) {
930                         foreach (Attachment att in message.Attachments) {
931                                 ContentType contentType = new ContentType (att.ContentType.ToString ());
932                                 if (att.Name != null) {
933                                         contentType.Name = att.Name;
934                                         if (att.NameEncoding != null)
935                                                 contentType.CharSet = att.NameEncoding.HeaderName;
936                                         att.ContentDisposition.FileName = att.Name;
937                                 }
938                                 StartSection (boundary, contentType, att.TransferEncoding, att == body ? null : att.ContentDisposition);
939
940                                 byte [] content = new byte [att.ContentStream.Length];
941                                 att.ContentStream.Read (content, 0, content.Length);
942                                 switch (att.TransferEncoding) {
943                                 case TransferEncoding.Base64:
944 #if TARGET_JVM
945                                         SendData (Convert.ToBase64String (content));
946 #else
947                                         SendData (Convert.ToBase64String (content, Base64FormattingOptions.InsertLineBreaks));
948 #endif
949                                         break;
950                                 case TransferEncoding.QuotedPrintable:
951                                         SendData (ToQuotedPrintable (content));
952                                         break;
953                                 case TransferEncoding.SevenBit:
954                                 case TransferEncoding.Unknown:
955                                         SendData (Encoding.ASCII.GetString (content));
956                                         break;
957                                 }
958
959                                 SendData (string.Empty);
960                         }
961                 }
962
963                 private SmtpResponse SendCommand (string command)
964                 {
965                         writer.Write (command);
966                         // Certain SMTP servers will reject mail sent with unix line-endings; see http://cr.yp.to/docs/smtplf.html
967                         writer.Write ("\r\n");
968                         writer.Flush ();
969                         return Read ();
970                 }
971
972                 private void SendHeader (string name, string value)
973                 {
974                         SendData (String.Format ("{0}: {1}", name, value));
975                 }
976
977                 private void StartSection (string section, ContentType sectionContentType)
978                 {
979                         SendData (String.Format ("--{0}", section));
980                         SendHeader ("content-type", sectionContentType.ToString ());
981                         SendData (string.Empty);
982                 }
983
984                 private void StartSection (string section, ContentType sectionContentType,TransferEncoding transferEncoding)
985                 {
986                         SendData (String.Format ("--{0}", section));
987                         SendHeader ("content-type", sectionContentType.ToString ());
988                         SendHeader ("content-transfer-encoding", GetTransferEncodingName (transferEncoding));
989                         SendData (string.Empty);
990                 }
991
992                 private void StartSection (string section, ContentType sectionContentType, TransferEncoding transferEncoding, ContentDisposition contentDisposition) {
993                         SendData (String.Format ("--{0}", section));
994                         SendHeader ("content-type", sectionContentType.ToString ());
995                         SendHeader ("content-transfer-encoding", GetTransferEncodingName (transferEncoding));
996                         if (contentDisposition != null)
997                                 SendHeader ("content-disposition", contentDisposition.ToString ());
998                         SendData (string.Empty);
999                 }
1000
1001                 // use proper encoding to escape input
1002                 private string ToQuotedPrintable (string input, Encoding enc)
1003                 {
1004                         byte [] bytes = enc.GetBytes (input);
1005                         return ToQuotedPrintable (bytes);
1006                 }
1007
1008                 private string ToQuotedPrintable (byte [] bytes)
1009                 {
1010                         StringWriter writer = new StringWriter ();
1011                         int charsInLine = 0;
1012                         int curLen;
1013                         StringBuilder sb = new StringBuilder("=", 3);
1014                         byte equalSign = (byte)'=';
1015                         char c = (char)0;
1016
1017                         foreach (byte i in bytes) {
1018                                 if (i > 127 || i == equalSign) {
1019                                         sb.Length = 1;
1020                                         sb.Append(Convert.ToString (i, 16).ToUpperInvariant ());
1021                                         curLen = 3;
1022                                 } else {
1023                                         c = Convert.ToChar (i);
1024                                         if (c == '\r' || c == '\n') {
1025                                                 writer.Write (c);
1026                                                 charsInLine = 0;
1027                                                 continue;
1028                                         }
1029                                         curLen = 1;
1030                                 }
1031                                 
1032                                 charsInLine += curLen;
1033                                 if (charsInLine > 75) {
1034                                         writer.Write ("=\r\n");
1035                                         charsInLine = curLen;
1036                                 }
1037                                 if (curLen == 1)
1038                                         writer.Write (c);
1039                                 else
1040                                         writer.Write (sb.ToString ());
1041                         }
1042
1043                         return writer.ToString ();
1044                 }
1045                 private static string GetTransferEncodingName (TransferEncoding encoding)
1046                 {
1047                         switch (encoding) {
1048                         case TransferEncoding.QuotedPrintable:
1049                                 return "quoted-printable";
1050                         case TransferEncoding.SevenBit:
1051                                 return "7bit";
1052                         case TransferEncoding.Base64:
1053                                 return "base64";
1054                         }
1055                         return "unknown";
1056                 }
1057
1058 #if SECURITY_DEP
1059                 RemoteCertificateValidationCallback callback = delegate (object sender,
1060                                                                          X509Certificate certificate,
1061                                                                          X509Chain chain,
1062                                                                          SslPolicyErrors sslPolicyErrors) {
1063                         if (sslPolicyErrors != SslPolicyErrors.None)
1064                                 throw new InvalidOperationException ("SSL authentication error: " + sslPolicyErrors);
1065                         return true;
1066                         };
1067 #endif
1068
1069                 private void InitiateSecureConnection () {
1070                         SmtpResponse response = SendCommand ("STARTTLS");
1071
1072                         if (IsError (response)) {
1073                                 throw new SmtpException (SmtpStatusCode.GeneralFailure, "Server does not support secure connections.");
1074                         }
1075
1076 #if TARGET_JVM
1077                         ((NetworkStream) stream).ChangeToSSLSocket ();
1078 #elif SECURITY_DEP
1079                         SslStream sslStream = new SslStream (stream, false, callback, null);
1080                         CheckCancellation ();
1081                         sslStream.AuthenticateAsClient (Host, this.ClientCertificates, SslProtocols.Default, false);
1082                         stream = sslStream;
1083
1084 #else
1085                         throw new SystemException ("You are using an incomplete System.dll build");
1086 #endif
1087                 }
1088                 
1089                 void Authenticate ()
1090                 {
1091                         string user = null, pass = null;
1092                         
1093                         if (UseDefaultCredentials) {
1094                                 user = CredentialCache.DefaultCredentials.GetCredential (new System.Uri ("smtp://" + host), "basic").UserName;
1095                                 pass =  CredentialCache.DefaultCredentials.GetCredential (new System.Uri ("smtp://" + host), "basic").Password;
1096                         } else if (Credentials != null) {
1097                                 user = Credentials.GetCredential (host, port, "smtp").UserName;
1098                                 pass = Credentials.GetCredential (host, port, "smtp").Password;
1099                         } else {
1100                                 return;
1101                         }
1102                         
1103                         Authenticate (user, pass);
1104                 }
1105
1106                 void Authenticate (string Username, string Password)
1107                 {
1108                         // FIXME: use the proper AuthMech
1109                         SmtpResponse status = SendCommand ("AUTH LOGIN");
1110                         if (((int) status.StatusCode) != 334) {
1111                                 throw new SmtpException (status.StatusCode, status.Description);
1112                         }
1113
1114                         status = SendCommand (Convert.ToBase64String (Encoding.ASCII.GetBytes (Username)));
1115                         if (((int) status.StatusCode) != 334) {
1116                                 throw new SmtpException (status.StatusCode, status.Description);
1117                         }
1118
1119                         status = SendCommand (Convert.ToBase64String (Encoding.ASCII.GetBytes (Password)));
1120                         if (IsError (status)) {
1121                                 throw new SmtpException (status.StatusCode, status.Description);
1122                         }
1123                 }
1124                 
1125                 #endregion // Methods
1126                 
1127                 // The HeaderName struct is used to store constant string values representing mail headers.
1128                 private struct HeaderName {
1129                         public const string ContentTransferEncoding = "Content-Transfer-Encoding";
1130                         public const string ContentType = "Content-Type";
1131                         public const string Bcc = "Bcc";
1132                         public const string Cc = "Cc";
1133                         public const string From = "From";
1134                         public const string Subject = "Subject";
1135                         public const string To = "To";
1136                         public const string MimeVersion = "MIME-Version";
1137                         public const string MessageId = "Message-ID";
1138                         public const string Priority = "Priority";
1139                         public const string Importance = "Importance";
1140                         public const string XPriority = "X-Priority";
1141                         public const string Date = "Date";
1142                 }
1143
1144                 // This object encapsulates the status code and description of an SMTP response.
1145                 private struct SmtpResponse {
1146                         public SmtpStatusCode StatusCode;
1147                         public string Description;
1148
1149                         public static SmtpResponse Parse (string line) {
1150                                 SmtpResponse response = new SmtpResponse ();
1151
1152                                 if (line.Length < 4)
1153                                         throw new SmtpException ("Response is to short " +
1154                                                                  line.Length + ".");
1155
1156                                 if ((line [3] != ' ') && (line [3] != '-'))
1157                                         throw new SmtpException ("Response format is wrong.(" +
1158                                                                  line + ")");
1159
1160                                 // parse the response code
1161                                 response.StatusCode = (SmtpStatusCode) Int32.Parse (line.Substring (0, 3));
1162
1163                                 // set the raw response
1164                                 response.Description = line;
1165
1166                                 return response;
1167                         }
1168                 }
1169         }
1170
1171         class CCredentialsByHost : ICredentialsByHost
1172         {
1173                 public CCredentialsByHost (string userName, string password) {
1174                         this.userName = userName;
1175                         this.password = password;
1176                 }
1177
1178                 public NetworkCredential GetCredential (string host, int port, string authenticationType) {
1179                         return new NetworkCredential (userName, password);
1180                 }
1181
1182                 private string userName;
1183                 private string password;
1184         }
1185 }
1186
1187 #endif // NET_2_0