2 // System.Net.Mail.SmtpClient.cs
5 // Tim Coleman (tim@timcoleman.com)
7 // Copyright (C) Tim Coleman, 2004
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:
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
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.
34 using System.Collections.Generic;
35 using System.ComponentModel;
38 using System.Net.Mime;
39 using System.Net.Sockets;
40 using System.Security.Cryptography.X509Certificates;
42 using System.Threading;
44 namespace System.Net.Mail {
45 public class SmtpClient
52 ICredentialsByHost credentials;
53 bool useDefaultCredentials;
54 string pickupDirectoryLocation;
55 SmtpDeliveryMethod deliveryMethod;
57 X509CertificateCollection clientCertificates;
65 Mutex mutex = new Mutex ();
67 const string MimeVersion = "1.0 (produced by Mono System.Net.Mail.SmtpClient)";
78 public SmtpClient (string host)
83 [MonoTODO ("Load default settings from configuration.")]
84 public SmtpClient (string host, int port)
86 // FIXME: load from configuration
87 if (String.IsNullOrEmpty (host))
92 // FIXME: load from configuration
98 // FIXME: load credentials from configuration
101 #endregion // Constructors
106 public X509CertificateCollection ClientCertificates {
107 get { return clientCertificates; }
110 public ICredentialsByHost Credentials {
111 get { return credentials; }
112 set { credentials = value; }
115 public SmtpDeliveryMethod DeliveryMethod {
116 get { return deliveryMethod; }
117 set { deliveryMethod = value; }
120 public bool EnableSsl {
121 get { return enableSsl; }
122 set { enableSsl = value; }
127 [MonoTODO ("Check to make sure an email is not being sent.")]
130 throw new ArgumentNullException ();
131 if (value.Length == 0)
132 throw new ArgumentException ();
137 public string PickupDirectoryLocation {
138 get { return pickupDirectoryLocation; }
139 set { pickupDirectoryLocation = value; }
144 [MonoTODO ("Check to make sure an email is not being sent.")]
147 throw new ArgumentOutOfRangeException ();
153 public ServicePoint ServicePoint {
154 get { throw new NotImplementedException (); }
158 get { return timeout; }
159 [MonoTODO ("Check to make sure an email is not being sent.")]
162 throw new ArgumentOutOfRangeException ();
168 public bool UseDefaultCredentials {
169 get { return useDefaultCredentials; }
170 set { useDefaultCredentials = value; }
173 #endregion // Properties
177 public event SendCompletedEventHandler SendCompleted;
183 private void EndSection (string section)
185 SendData (String.Format ("--{0}--", section));
188 private string GenerateBoundary ()
190 string output = GenerateBoundary (boundaryIndex);
195 private static string GenerateBoundary (int index)
197 return String.Format ("--boundary_{0}_{1}", index, Guid.NewGuid ().ToString ("D"));
200 private bool IsError (SmtpResponse status)
202 return ((int) status.StatusCode) >= 400;
205 protected void OnSendCompleted (AsyncCompletedEventArgs e)
207 if (SendCompleted != null)
208 SendCompleted (this, e);
211 private SmtpResponse Read ()
213 SmtpResponse response;
215 char[] buf = new char [3];
216 reader.Read (buf, 0, 3);
219 response.StatusCode = (SmtpStatusCode) Int32.Parse (new String (buf));
220 response.Description = reader.ReadLine ();
225 [MonoTODO ("Need to work on message attachments.")]
226 public void Send (MailMessage message)
228 // Block while sending
233 client = new TcpClient (host, port);
234 stream = client.GetStream ();
235 writer = new StreamWriter (stream);
236 reader = new StreamReader (stream);
238 string boundary = GenerateBoundary ();
240 bool hasAlternateViews = (message.AlternateViews.Count > 0);
241 bool hasAttachments = (message.Attachments.Count > 0);
244 if (IsError (status))
245 throw new SmtpException (status.StatusCode);
248 status = SendCommand (Command.Helo, Dns.GetHostName ());
249 if (IsError (status))
250 throw new SmtpException (status.StatusCode);
253 status = SendCommand (Command.MailFrom, message.From.Address);
254 if (IsError (status))
255 throw new SmtpException (status.StatusCode);
257 // Send RCPT TO: for all recipients
258 List<SmtpFailedRecipientException> sfre = new List<SmtpFailedRecipientException> ();
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 ()));
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 ()));
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 ()));
276 #if TARGET_JVM // List<T>.ToArray () is not supported
277 if (sfre.Count > 0) {
278 SmtpFailedRecipientException[] xs = new SmtpFailedRecipientException[sfre.Count];
280 throw new SmtpFailedRecipientsException ("failed recipients", xs);
284 throw new SmtpFailedRecipientsException ("failed recipients", sfre.ToArray ());
288 status = SendCommand (Command.Data);
289 if (IsError (status))
290 throw new SmtpException (status.StatusCode);
292 // Figure out the message content type
293 ContentType messageContentType = message.BodyContentType;
294 if (hasAttachments || hasAlternateViews) {
295 messageContentType.Boundary = boundary;
298 messageContentType.MediaType = "multipart/mixed";
300 messageContentType.MediaType = "multipart/alternative";
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);
312 foreach (string s in message.Headers.AllKeys)
313 SendHeader (s, message.Headers [s]);
315 SendHeader ("Content-Type", messageContentType.ToString ());
318 if (hasAlternateViews) {
319 string innerBoundary = boundary;
321 // The body is *technically* an alternative view. The body text goes FIRST because
322 // that is most compatible with non-MIME readers.
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
328 // If there are no attachments, then the main content-type is multipart/alternative
329 // and we don't need this subpart.
331 if (hasAttachments) {
332 innerBoundary = GenerateBoundary ();
333 ContentType contentType = new ContentType ("multipart/alternative");
334 contentType.Boundary = innerBoundary;
335 StartSection (boundary, contentType);
338 // Start the section for the body text. This is either section "1" or "0" depending
339 // on whether there are attachments.
341 StartSection (innerBoundary, messageContentType, TransferEncoding.QuotedPrintable);
342 SendData (message.Body);
344 // Send message attachments.
345 SendAttachments (message.Attachments, innerBoundary);
348 EndSection (innerBoundary);
351 // If this is multipart then we need to send a boundary before the body.
352 if (hasAttachments) {
354 ContentType contentType = new ContentType ("multipart/alternative");
355 StartSection (boundary, contentType, TransferEncoding.QuotedPrintable);
357 SendData (message.Body);
361 if (hasAttachments) {
362 string innerBoundary = boundary;
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.
367 if (hasAlternateViews) {
368 innerBoundary = GenerateBoundary ();
369 ContentType contentType = new ContentType ("multipart/mixed");
370 contentType.Boundary = innerBoundary;
371 StartSection (boundary, contentType);
374 SendAttachments (message.Attachments, innerBoundary);
376 if (hasAlternateViews)
377 EndSection (innerBoundary);
383 if (IsError (status))
384 throw new SmtpException (status.StatusCode);
386 status = SendCommand (Command.Quit);
393 // Release the mutex to allow other threads access
394 mutex.ReleaseMutex ();
397 public void Send (string from, string to, string subject, string body)
399 Send (new MailMessage (from, to, subject, body));
402 private void SendData (string data)
404 writer.WriteLine (data);
409 public void SendAsync (MailMessage message, object userToken)
412 OnSendCompleted (new AsyncCompletedEventArgs (null, false, userToken));
415 public void SendAsync (string from, string to, string subject, string body, object userToken)
417 SendAsync (new MailMessage (from, to, subject, body), userToken);
421 public void SendAsyncCancel ()
423 throw new NotImplementedException ();
426 private void SendAttachments (AttachmentCollection attachments, string boundary)
428 for (int i = 0; i < attachments.Count; i += 1) {
430 ContentType contentType = new ContentType ("multipart/alternative");
431 StartSection (boundary, contentType, attachments [i].TransferEncoding);
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);
438 SendData (Convert.ToBase64String (content));
440 SendData (Convert.ToBase64String (content, Base64FormattingOptions.InsertLineBreaks));
443 case TransferEncoding.QuotedPrintable:
444 StreamReader sr = new StreamReader (attachments [i].ContentStream);
445 SendData (ToQuotedPrintable (sr.ReadToEnd ()));
447 //case TransferEncoding.SevenBit:
448 //case TransferEncoding.Unknown:
450 SendData ("TO BE IMPLEMENTED");
456 private SmtpResponse SendCommand (string command, string data)
458 writer.Write (command);
464 private SmtpResponse SendCommand (string command)
466 writer.WriteLine (command);
471 private void SendHeader (string name, string value)
473 SendData (String.Format ("{0}: {1}", name, value));
476 private void StartSection (string section, ContentType sectionContentType)
478 SendData (String.Format ("--{0}", section));
479 SendHeader ("content-type", sectionContentType.ToString ());
483 private void StartSection (string section, ContentType sectionContentType,TransferEncoding transferEncoding)
485 SendData (String.Format ("--{0}", section));
486 SendHeader ("content-type", sectionContentType.ToString ());
487 SendHeader ("content-transfer-encoding", GetTransferEncodingName (transferEncoding));
491 private string ToQuotedPrintable (string input)
493 StringReader reader = new StringReader (input);
494 StringWriter writer = new StringWriter ();
497 while ((i = reader.Read ()) > 0) {
500 writer.Write (Convert.ToString (i, 16).ToUpper ());
503 writer.Write (Convert.ToChar (i));
506 return writer.GetStringBuilder ().ToString ();
509 private static string GetTransferEncodingName (TransferEncoding encoding)
512 case TransferEncoding.QuotedPrintable:
513 return "quoted-printable";
514 case TransferEncoding.SevenBit:
516 case TransferEncoding.Base64:
524 private sealed ContextAwareResult IGetContextAwareResult.GetContextAwareResult ()
526 throw new NotImplementedException ();
529 #endregion // Methods
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:";
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";
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;