* Makefile: Don't build make-map.exe.
[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 using System;
34 using System.Collections.Generic;
35 using System.ComponentModel;
36 using System.IO;
37 using System.Net;
38 using System.Net.Mime;
39 using System.Net.Sockets;
40 using System.Security.Cryptography.X509Certificates;
41 using System.Text;
42 using System.Threading;
43
44 namespace System.Net.Mail {
45         public class SmtpClient
46         {
47                 #region Fields
48
49                 string host;
50                 int port;
51                 int timeout = 100000;
52                 ICredentialsByHost credentials;
53                 bool useDefaultCredentials;
54                 string pickupDirectoryLocation;
55                 SmtpDeliveryMethod deliveryMethod;
56                 bool enableSsl;
57                 X509CertificateCollection clientCertificates;
58
59                 TcpClient client;
60                 NetworkStream stream;
61                 StreamWriter writer;
62                 StreamReader reader;
63                 int boundaryIndex;
64
65                 Mutex mutex = new Mutex ();
66
67                 const string MimeVersion = "1.0 (produced by Mono System.Net.Mail.SmtpClient)";
68
69                 #endregion // Fields
70
71                 #region Constructors
72
73                 public SmtpClient ()
74                         : this (null, 0)
75                 {
76                 }
77
78                 public SmtpClient (string host)
79                         : this (host, 0)
80                 {
81                 }
82
83                 [MonoTODO ("Load default settings from configuration.")]
84                 public SmtpClient (string host, int port)
85                 {
86                         // FIXME: load from configuration
87                         if (String.IsNullOrEmpty (host))
88                                 Host = "127.0.0.1";
89                         else
90                                 Host = host;
91                         
92                         // FIXME: load from configuration
93                         if (port == 0)
94                                 Port = 25;
95                         else
96                                 Port = port;
97
98                         // FIXME: load credentials from configuration
99                 }
100
101                 #endregion // Constructors
102
103                 #region Properties
104
105                 [MonoTODO]
106                 public X509CertificateCollection ClientCertificates {
107                         get { return clientCertificates; }
108                 }
109
110                 public ICredentialsByHost Credentials {
111                         get { return credentials; }
112                         set { credentials = value; }
113                 }
114
115                 public SmtpDeliveryMethod DeliveryMethod {
116                         get { return deliveryMethod; }
117                         set { deliveryMethod = value; }
118                 }
119
120                 public bool EnableSsl {
121                         get { return enableSsl; }
122                         set { enableSsl = value; }
123                 }
124
125                 public string Host {
126                         get { return host; }
127                         [MonoTODO ("Check to make sure an email is not being sent.")]
128                         set {
129                                 if (value == null)
130                                         throw new ArgumentNullException ();
131                                 if (value.Length == 0)
132                                         throw new ArgumentException ();
133                                 host = value;
134                         }
135                 }
136
137                 public string PickupDirectoryLocation {
138                         get { return pickupDirectoryLocation; }
139                         set { pickupDirectoryLocation = value; }
140                 }
141
142                 public int Port {
143                         get { return port; }
144                         [MonoTODO ("Check to make sure an email is not being sent.")]
145                         set { 
146                                 if (value <= 0)
147                                         throw new ArgumentOutOfRangeException ();
148                                 port = value; 
149                         }
150                 }
151
152                 [MonoTODO]
153                 public ServicePoint ServicePoint {
154                         get { throw new NotImplementedException (); }
155                 }
156
157                 public int Timeout {
158                         get { return timeout; }
159                         [MonoTODO ("Check to make sure an email is not being sent.")]
160                         set { 
161                                 if (value < 0)
162                                         throw new ArgumentOutOfRangeException ();
163                                 timeout = value; 
164                         }
165                 }
166
167                 [MonoTODO]
168                 public bool UseDefaultCredentials {
169                         get { return useDefaultCredentials; }
170                         set { useDefaultCredentials = value; }
171                 }
172
173                 #endregion // Properties
174
175                 #region Events 
176
177                 public event SendCompletedEventHandler SendCompleted;
178
179                 #endregion // Events 
180
181                 #region Methods
182
183                 private void EndSection (string section)
184                 {
185                         SendData (String.Format ("--{0}--", section));
186                 }
187
188                 private string GenerateBoundary ()
189                 {
190                         string output = GenerateBoundary (boundaryIndex);
191                         boundaryIndex += 1;
192                         return output;
193                 }
194
195                 private static string GenerateBoundary (int index)
196                 {
197                         return String.Format ("--boundary_{0}_{1}", index, Guid.NewGuid ().ToString ("D"));
198                 }
199
200                 private bool IsError (SmtpResponse status)
201                 {
202                         return ((int) status.StatusCode) >= 400;
203                 }
204
205                 protected void OnSendCompleted (AsyncCompletedEventArgs e)
206                 {
207                         if (SendCompleted != null)
208                                 SendCompleted (this, e);
209                 }
210
211                 private SmtpResponse Read ()
212                 {
213                         SmtpResponse response;
214
215                         char[] buf = new char [3];
216                         reader.Read (buf, 0, 3);
217                         reader.Read ();
218
219                         response.StatusCode = (SmtpStatusCode) Int32.Parse (new String (buf));
220                         response.Description = reader.ReadLine ();
221
222                         return response;
223                 }
224
225                 [MonoTODO ("Need to work on message attachments.")]
226                 public void Send (MailMessage message)
227                 {
228                         // Block while sending
229                         mutex.WaitOne ();
230
231                         SmtpResponse status;
232
233                         client = new TcpClient (host, port);
234                         stream = client.GetStream ();
235                         writer = new StreamWriter (stream);
236                         reader = new StreamReader (stream);
237                         boundaryIndex = 0;
238                         string boundary = GenerateBoundary ();
239
240                         bool hasAlternateViews = (message.AlternateViews.Count > 0);
241                         bool hasAttachments = (message.Attachments.Count > 0);
242
243                         status = Read ();
244                         if (IsError (status))
245                                 throw new SmtpException (status.StatusCode);
246
247                         // HELO
248                         status = SendCommand (Command.Helo, Dns.GetHostName ());
249                         if (IsError (status))
250                                 throw new SmtpException (status.StatusCode);
251
252                         // MAIL FROM:
253                         status = SendCommand (Command.MailFrom, message.From.Address);
254                         if (IsError (status))
255                                 throw new SmtpException (status.StatusCode);
256
257                         // Send RCPT TO: for all recipients
258                         List<SmtpFailedRecipientException> sfre = new List<SmtpFailedRecipientException> ();
259
260                         for (int i = 0; i < message.To.Count; i ++) {
261                                 status = SendCommand (Command.RcptTo, message.To [i].Address);
262                                 if (IsError (status)) 
263                                         sfre.Add (new SmtpFailedRecipientException (status.StatusCode, message.To [i].Address.ToString ()));
264                         }
265                         for (int i = 0; i < message.CC.Count; i ++) {
266                                 status = SendCommand (Command.RcptTo, message.CC [i].Address);
267                                 if (IsError (status)) 
268                                         sfre.Add (new SmtpFailedRecipientException (status.StatusCode, message.CC [i].Address.ToString ()));
269                         }
270                         for (int i = 0; i < message.Bcc.Count; i ++) {
271                                 status = SendCommand (Command.RcptTo, message.Bcc [i].Address);
272                                 if (IsError (status)) 
273                                         sfre.Add (new SmtpFailedRecipientException (status.StatusCode, message.Bcc [i].Address.ToString ()));
274                         }
275
276 #if TARGET_JVM // List<T>.ToArray () is not supported
277                         if (sfre.Count > 0) {
278                                 SmtpFailedRecipientException[] xs = new SmtpFailedRecipientException[sfre.Count];
279                                 sfre.CopyTo (xs);
280                                 throw new SmtpFailedRecipientsException ("failed recipients", xs);
281                         }
282 #else
283                         if (sfre.Count >0)
284                                 throw new SmtpFailedRecipientsException ("failed recipients", sfre.ToArray ());
285 #endif
286
287                         // DATA
288                         status = SendCommand (Command.Data);
289                         if (IsError (status))
290                                 throw new SmtpException (status.StatusCode);
291
292                         // Figure out the message content type
293                         ContentType messageContentType = message.BodyContentType;
294                         if (hasAttachments || hasAlternateViews) {
295                                 messageContentType.Boundary = boundary;
296
297                                 if (hasAttachments)
298                                         messageContentType.MediaType = "multipart/mixed";
299                                 else
300                                         messageContentType.MediaType = "multipart/alternative";
301                         }
302
303                         // Send message headers
304                         SendHeader (HeaderName.From, message.From.ToString ());
305                         SendHeader (HeaderName.To, message.To.ToString ());
306                         if (message.CC.Count > 0)
307                                 SendHeader (HeaderName.Cc, message.CC.ToString ());
308                         if (message.Bcc.Count > 0)
309                                 SendHeader (HeaderName.Bcc, message.Bcc.ToString ());
310                         SendHeader (HeaderName.Subject, message.Subject);
311
312                         foreach (string s in message.Headers.AllKeys)
313                                 SendHeader (s, message.Headers [s]);
314
315                         SendHeader ("Content-Type", messageContentType.ToString ());
316                         SendData ("");
317
318                         if (hasAlternateViews) {
319                                 string innerBoundary = boundary;
320
321                                 // The body is *technically* an alternative view.  The body text goes FIRST because
322                                 // that is most compatible with non-MIME readers.
323                                 //
324                                 // If there are attachments, then the main content-type is multipart/mixed and
325                                 // the subpart has type multipart/alternative.  Then all of the views have their
326                                 // own types.  
327                                 //
328                                 // If there are no attachments, then the main content-type is multipart/alternative
329                                 // and we don't need this subpart.
330
331                                 if (hasAttachments) {
332                                         innerBoundary = GenerateBoundary ();
333                                         ContentType contentType = new ContentType ("multipart/alternative");
334                                         contentType.Boundary = innerBoundary;
335                                         StartSection (boundary, contentType);
336                                 }
337                                 
338                                 // Start the section for the body text.  This is either section "1" or "0" depending
339                                 // on whether there are attachments.
340
341                                 StartSection (innerBoundary, messageContentType, TransferEncoding.QuotedPrintable);
342                                 SendData (message.Body);
343
344                                 // Send message attachments.
345                                 SendAttachments (message.Attachments, innerBoundary);
346
347                                 if (hasAttachments) 
348                                         EndSection (innerBoundary);
349                         }
350                         else {
351                                 // If this is multipart then we need to send a boundary before the body.
352                                 if (hasAttachments) {
353                                         // FIXME: check this
354                                         ContentType contentType = new ContentType ("multipart/alternative");
355                                         StartSection (boundary, contentType, TransferEncoding.QuotedPrintable);
356                                 }
357                                 SendData (message.Body);
358                         }
359
360                         // Send attachments
361                         if (hasAttachments) {
362                                 string innerBoundary = boundary;
363
364                                 // If we have alternate views and attachments then we need to nest this part inside another
365                                 // boundary.  Otherwise, we are cool with the boundary we have.
366
367                                 if (hasAlternateViews) {
368                                         innerBoundary = GenerateBoundary ();
369                                         ContentType contentType = new ContentType ("multipart/mixed");
370                                         contentType.Boundary = innerBoundary;
371                                         StartSection (boundary, contentType);
372                                 }
373
374                                 SendAttachments (message.Attachments, innerBoundary);
375
376                                 if (hasAlternateViews)
377                                         EndSection (innerBoundary);
378                         }
379
380                         SendData (".");
381
382                         status = Read ();
383                         if (IsError (status))
384                                 throw new SmtpException (status.StatusCode);
385
386                         status = SendCommand (Command.Quit);
387
388                         writer.Close ();
389                         reader.Close ();
390                         stream.Close ();
391                         client.Close ();
392
393                         // Release the mutex to allow other threads access
394                         mutex.ReleaseMutex ();
395                 }
396
397                 public void Send (string from, string to, string subject, string body)
398                 {
399                         Send (new MailMessage (from, to, subject, body));
400                 }
401
402                 private void SendData (string data)
403                 {
404                         writer.WriteLine (data);
405                         writer.Flush ();
406                 }
407
408                 [MonoTODO]
409                 public void SendAsync (MailMessage message, object userToken)
410                 {
411                         Send (message);
412                         OnSendCompleted (new AsyncCompletedEventArgs (null, false, userToken));
413                 }
414
415                 public void SendAsync (string from, string to, string subject, string body, object userToken)
416                 {
417                         SendAsync (new MailMessage (from, to, subject, body), userToken);
418                 }
419
420                 [MonoTODO]
421                 public void SendAsyncCancel ()
422                 {
423                         throw new NotImplementedException ();
424                 }
425
426                 private void SendAttachments (AttachmentCollection attachments, string boundary)
427                 {
428                         for (int i = 0; i < attachments.Count; i += 1) {
429                                 // FIXME: check this
430                                 ContentType contentType = new ContentType ("multipart/alternative");
431                                 StartSection (boundary, contentType, attachments [i].TransferEncoding);
432
433                                 switch (attachments [i].TransferEncoding) {
434                                 case TransferEncoding.Base64:
435                                         byte[] content = new byte [attachments [i].ContentStream.Length];
436                                         attachments [i].ContentStream.Read (content, 0, content.Length);
437 #if TARGET_JVM
438                                         SendData (Convert.ToBase64String (content));
439 #else
440                                         SendData (Convert.ToBase64String (content, Base64FormattingOptions.InsertLineBreaks));
441 #endif
442                                         break;
443                                 case TransferEncoding.QuotedPrintable:
444                                         StreamReader sr = new StreamReader (attachments [i].ContentStream);
445                                         SendData (ToQuotedPrintable (sr.ReadToEnd ()));
446                                         break;
447                                 //case TransferEncoding.SevenBit:
448                                 //case TransferEncoding.Unknown:
449                                 default:
450                                         SendData ("TO BE IMPLEMENTED");
451                                         break;
452                                 }
453                         }
454                 }
455
456                 private SmtpResponse SendCommand (string command, string data)
457                 {
458                         writer.Write (command);
459                         writer.Write (" ");
460                         SendData (data);
461                         return Read ();
462                 }
463
464                 private SmtpResponse SendCommand (string command)
465                 {
466                         writer.WriteLine (command);
467                         writer.Flush ();
468                         return Read ();
469                 }
470
471                 private void SendHeader (string name, string value)
472                 {
473                         SendData (String.Format ("{0}: {1}", name, value));
474                 }
475
476                 private void StartSection (string section, ContentType sectionContentType)
477                 {
478                         SendData (String.Format ("--{0}", section));
479                         SendHeader ("content-type", sectionContentType.ToString ());
480                         SendData ("");
481                 }
482
483                 private void StartSection (string section, ContentType sectionContentType,TransferEncoding transferEncoding)
484                 {
485                         SendData (String.Format ("--{0}", section));
486                         SendHeader ("content-type", sectionContentType.ToString ());
487                         SendHeader ("content-transfer-encoding", GetTransferEncodingName (transferEncoding));
488                         SendData ("");
489                 }
490
491                 private string ToQuotedPrintable (string input)
492                 {
493                         StringReader reader = new StringReader (input);
494                         StringWriter writer = new StringWriter ();
495                         int i;
496
497                         while ((i = reader.Read ()) > 0) {
498                                 if (i > 127) {
499                                         writer.Write ("=");
500                                         writer.Write (Convert.ToString (i, 16).ToUpper ());
501                                 }
502                                 else
503                                         writer.Write (Convert.ToChar (i));
504                         }
505
506                         return writer.GetStringBuilder ().ToString ();
507                 }
508
509                 private static string GetTransferEncodingName (TransferEncoding encoding)
510                 {
511                         switch (encoding) {
512                         case TransferEncoding.QuotedPrintable:
513                                 return "quoted-printable";
514                         case TransferEncoding.SevenBit:
515                                 return "7bit";
516                         case TransferEncoding.Base64:
517                                 return "base64";
518                         }
519                         return "unknown";
520                 }
521
522 /*
523                 [MonoTODO]
524                 private sealed ContextAwareResult IGetContextAwareResult.GetContextAwareResult ()
525                 {
526                         throw new NotImplementedException ();
527                 }
528 */
529                 #endregion // Methods
530
531                 // The Command struct is used to store constant string values representing SMTP commands.
532                 private struct Command {
533                         public const string Data = "DATA";
534                         public const string Helo = "HELO";
535                         public const string MailFrom = "MAIL FROM:";
536                         public const string Quit = "QUIT";
537                         public const string RcptTo = "RCPT TO:";
538                 }
539
540                 // The HeaderName struct is used to store constant string values representing mail headers.
541                 private struct HeaderName {
542                         public const string ContentTransferEncoding = "Content-Transfer-Encoding";
543                         public const string ContentType = "Content-Type";
544                         public const string Bcc = "Bcc";
545                         public const string Cc = "Cc";
546                         public const string From = "From";
547                         public const string Subject = "Subject";
548                         public const string To = "To";
549                         public const string MimeVersion = "MIME-Version";
550                         public const string MessageId = "Message-ID";
551                 }
552
553                 // This object encapsulates the status code and description of an SMTP response.
554                 private struct SmtpResponse {
555                         public SmtpStatusCode StatusCode;
556                         public string Description;
557                 }
558         }
559 }
560
561 #endif // NET_2_0