966bba02bb
Former-commit-id: bb0468d0f257ff100aa895eb5fe583fb5dfbf900
1306 lines
37 KiB
C#
1306 lines
37 KiB
C#
//
|
|
// System.Net.Mail.SmtpClient.cs
|
|
//
|
|
// Author:
|
|
// Tim Coleman (tim@timcoleman.com)
|
|
//
|
|
// Copyright (C) Tim Coleman, 2004
|
|
//
|
|
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining
|
|
// a copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
|
// permit persons to whom the Software is furnished to do so, subject to
|
|
// the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be
|
|
// included in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
//
|
|
|
|
#if SECURITY_DEP
|
|
#if MONO_SECURITY_ALIAS
|
|
extern alias MonoSecurity;
|
|
#endif
|
|
|
|
#if MONO_SECURITY_ALIAS
|
|
using MSI = MonoSecurity::Mono.Security.Interface;
|
|
#else
|
|
using MSI = Mono.Security.Interface;
|
|
#endif
|
|
using System.Security.Cryptography.X509Certificates;
|
|
#endif
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Mime;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Net.Configuration;
|
|
using System.Configuration;
|
|
using System.Net.Security;
|
|
using System.Security.Authentication;
|
|
using System.Threading.Tasks;
|
|
using Mono.Net.Security;
|
|
|
|
namespace System.Net.Mail {
|
|
[Obsolete ("SmtpClient and its network of types are poorly designed, we strongly recommend you use https://github.com/jstedfast/MailKit and https://github.com/jstedfast/MimeKit instead")]
|
|
public class SmtpClient
|
|
: IDisposable
|
|
{
|
|
#region Fields
|
|
|
|
string host;
|
|
int port;
|
|
int timeout = 100000;
|
|
ICredentialsByHost credentials;
|
|
string pickupDirectoryLocation;
|
|
SmtpDeliveryMethod deliveryMethod;
|
|
SmtpDeliveryFormat deliveryFormat;
|
|
bool enableSsl;
|
|
#if SECURITY_DEP
|
|
X509CertificateCollection clientCertificates;
|
|
#endif
|
|
|
|
TcpClient client;
|
|
Stream stream;
|
|
StreamWriter writer;
|
|
StreamReader reader;
|
|
int boundaryIndex;
|
|
MailAddress defaultFrom;
|
|
|
|
MailMessage messageInProcess;
|
|
|
|
BackgroundWorker worker;
|
|
object user_async_state;
|
|
|
|
[Flags]
|
|
enum AuthMechs {
|
|
None = 0,
|
|
Login = 0x01,
|
|
Plain = 0x02,
|
|
}
|
|
|
|
class CancellationException : Exception
|
|
{
|
|
}
|
|
|
|
AuthMechs authMechs;
|
|
Mutex mutex = new Mutex ();
|
|
|
|
#endregion // Fields
|
|
|
|
#region Constructors
|
|
|
|
public SmtpClient ()
|
|
: this (null, 0)
|
|
{
|
|
}
|
|
|
|
public SmtpClient (string host)
|
|
: this (host, 0)
|
|
{
|
|
}
|
|
|
|
public SmtpClient (string host, int port) {
|
|
#if CONFIGURATION_DEP
|
|
SmtpSection cfg = (SmtpSection) ConfigurationManager.GetSection ("system.net/mailSettings/smtp");
|
|
|
|
if (cfg != null) {
|
|
this.host = cfg.Network.Host;
|
|
this.port = cfg.Network.Port;
|
|
this.enableSsl = cfg.Network.EnableSsl;
|
|
TargetName = cfg.Network.TargetName;
|
|
if (this.TargetName == null)
|
|
TargetName = "SMTPSVC/" + (host != null ? host : "");
|
|
|
|
|
|
if (cfg.Network.UserName != null) {
|
|
string password = String.Empty;
|
|
|
|
if (cfg.Network.Password != null)
|
|
password = cfg.Network.Password;
|
|
|
|
Credentials = new CCredentialsByHost (cfg.Network.UserName, password);
|
|
}
|
|
|
|
if (!String.IsNullOrEmpty (cfg.From))
|
|
defaultFrom = new MailAddress (cfg.From);
|
|
}
|
|
#else
|
|
// Just to eliminate the warning, this codepath does not end up in production.
|
|
defaultFrom = null;
|
|
#endif
|
|
|
|
if (!String.IsNullOrEmpty (host))
|
|
this.host = host;
|
|
|
|
if (port != 0)
|
|
this.port = port;
|
|
else if (this.port == 0)
|
|
this.port = 25;
|
|
}
|
|
|
|
#endregion // Constructors
|
|
|
|
#region Properties
|
|
|
|
#if SECURITY_DEP
|
|
[MonoTODO("Client certificates not used")]
|
|
public X509CertificateCollection ClientCertificates {
|
|
get {
|
|
if (clientCertificates == null)
|
|
clientCertificates = new X509CertificateCollection ();
|
|
return clientCertificates;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
public
|
|
string TargetName { get; set; }
|
|
|
|
public ICredentialsByHost Credentials {
|
|
get { return credentials; }
|
|
set {
|
|
CheckState ();
|
|
credentials = value;
|
|
}
|
|
}
|
|
|
|
public SmtpDeliveryMethod DeliveryMethod {
|
|
get { return deliveryMethod; }
|
|
set {
|
|
CheckState ();
|
|
deliveryMethod = value;
|
|
}
|
|
}
|
|
|
|
public bool EnableSsl {
|
|
get { return enableSsl; }
|
|
set {
|
|
CheckState ();
|
|
enableSsl = value;
|
|
}
|
|
}
|
|
|
|
public string Host {
|
|
get { return host; }
|
|
set {
|
|
if (value == null)
|
|
throw new ArgumentNullException ("value");
|
|
if (value.Length == 0)
|
|
throw new ArgumentException ("An empty string is not allowed.", "value");
|
|
CheckState ();
|
|
host = value;
|
|
}
|
|
}
|
|
|
|
public string PickupDirectoryLocation {
|
|
get { return pickupDirectoryLocation; }
|
|
set { pickupDirectoryLocation = value; }
|
|
}
|
|
|
|
public int Port {
|
|
get { return port; }
|
|
set {
|
|
if (value <= 0)
|
|
throw new ArgumentOutOfRangeException ("value");
|
|
CheckState ();
|
|
port = value;
|
|
}
|
|
}
|
|
|
|
public SmtpDeliveryFormat DeliveryFormat {
|
|
get { return deliveryFormat; }
|
|
set {
|
|
CheckState ();
|
|
deliveryFormat = value;
|
|
}
|
|
}
|
|
|
|
[MonoTODO]
|
|
public ServicePoint ServicePoint {
|
|
get { throw new NotImplementedException (); }
|
|
}
|
|
|
|
public int Timeout {
|
|
get { return timeout; }
|
|
set {
|
|
if (value < 0)
|
|
throw new ArgumentOutOfRangeException ("value");
|
|
CheckState ();
|
|
timeout = value;
|
|
}
|
|
}
|
|
|
|
public bool UseDefaultCredentials {
|
|
get { return false; }
|
|
[MonoNotSupported ("no DefaultCredential support in Mono")]
|
|
set {
|
|
if (value)
|
|
throw new NotImplementedException ("Default credentials are not supported");
|
|
CheckState ();
|
|
}
|
|
}
|
|
|
|
#endregion // Properties
|
|
|
|
#region Events
|
|
|
|
public event SendCompletedEventHandler SendCompleted;
|
|
|
|
#endregion // Events
|
|
|
|
#region Methods
|
|
public void Dispose ()
|
|
{
|
|
Dispose (true);
|
|
}
|
|
|
|
[MonoTODO ("Does nothing at the moment.")]
|
|
protected virtual void Dispose (bool disposing)
|
|
{
|
|
// TODO: We should close all the connections and abort any async operations here
|
|
}
|
|
private void CheckState ()
|
|
{
|
|
if (messageInProcess != null)
|
|
throw new InvalidOperationException ("Cannot set Timeout while Sending a message");
|
|
}
|
|
|
|
private static string EncodeAddress(MailAddress address)
|
|
{
|
|
if (!String.IsNullOrEmpty (address.DisplayName)) {
|
|
string encodedDisplayName = MailMessage.EncodeSubjectRFC2047 (address.DisplayName, Encoding.UTF8);
|
|
return "\"" + encodedDisplayName + "\" <" + address.Address + ">";
|
|
}
|
|
return address.ToString ();
|
|
}
|
|
|
|
private static string EncodeAddresses(MailAddressCollection addresses)
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
bool first = true;
|
|
foreach (MailAddress address in addresses) {
|
|
if (!first) {
|
|
sb.Append(", ");
|
|
}
|
|
sb.Append(EncodeAddress(address));
|
|
first = false;
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
private string EncodeSubjectRFC2047 (MailMessage message)
|
|
{
|
|
return MailMessage.EncodeSubjectRFC2047 (message.Subject, message.SubjectEncoding);
|
|
}
|
|
|
|
private string EncodeBody (MailMessage message)
|
|
{
|
|
string body = message.Body;
|
|
Encoding encoding = message.BodyEncoding;
|
|
// RFC 2045 encoding
|
|
switch (message.ContentTransferEncoding) {
|
|
case TransferEncoding.SevenBit:
|
|
return body;
|
|
case TransferEncoding.Base64:
|
|
return Convert.ToBase64String (encoding.GetBytes (body), Base64FormattingOptions.InsertLineBreaks);
|
|
default:
|
|
return ToQuotedPrintable (body, encoding);
|
|
}
|
|
}
|
|
|
|
private string EncodeBody (AlternateView av)
|
|
{
|
|
//Encoding encoding = av.ContentType.CharSet != null ? Encoding.GetEncoding (av.ContentType.CharSet) : Encoding.UTF8;
|
|
|
|
byte [] bytes = new byte [av.ContentStream.Length];
|
|
av.ContentStream.Read (bytes, 0, bytes.Length);
|
|
|
|
// RFC 2045 encoding
|
|
switch (av.TransferEncoding) {
|
|
case TransferEncoding.SevenBit:
|
|
return Encoding.ASCII.GetString (bytes);
|
|
case TransferEncoding.Base64:
|
|
return Convert.ToBase64String (bytes, Base64FormattingOptions.InsertLineBreaks);
|
|
default:
|
|
return ToQuotedPrintable (bytes);
|
|
}
|
|
}
|
|
|
|
|
|
private void EndSection (string section)
|
|
{
|
|
SendData (String.Format ("--{0}--", section));
|
|
SendData (string.Empty);
|
|
}
|
|
|
|
private string GenerateBoundary ()
|
|
{
|
|
string output = GenerateBoundary (boundaryIndex);
|
|
boundaryIndex += 1;
|
|
return output;
|
|
}
|
|
|
|
private static string GenerateBoundary (int index)
|
|
{
|
|
return String.Format ("--boundary_{0}_{1}", index, Guid.NewGuid ().ToString ("D"));
|
|
}
|
|
|
|
private bool IsError (SmtpResponse status)
|
|
{
|
|
return ((int) status.StatusCode) >= 400;
|
|
}
|
|
|
|
protected void OnSendCompleted (AsyncCompletedEventArgs e)
|
|
{
|
|
try {
|
|
if (SendCompleted != null)
|
|
SendCompleted (this, e);
|
|
} finally {
|
|
worker = null;
|
|
user_async_state = null;
|
|
}
|
|
}
|
|
|
|
private void CheckCancellation ()
|
|
{
|
|
if (worker != null && worker.CancellationPending)
|
|
throw new CancellationException ();
|
|
}
|
|
|
|
private SmtpResponse Read () {
|
|
byte [] buffer = new byte [512];
|
|
int position = 0;
|
|
bool lastLine = false;
|
|
|
|
do {
|
|
CheckCancellation ();
|
|
|
|
int readLength = stream.Read (buffer, position, buffer.Length - position);
|
|
if (readLength > 0) {
|
|
int available = position + readLength - 1;
|
|
if (available > 4 && (buffer [available] == '\n' || buffer [available] == '\r'))
|
|
for (int index = available - 3; ; index--) {
|
|
if (index < 0 || buffer [index] == '\n' || buffer [index] == '\r') {
|
|
lastLine = buffer [index + 4] == ' ';
|
|
break;
|
|
}
|
|
}
|
|
|
|
// move position
|
|
position += readLength;
|
|
|
|
// check if buffer is full
|
|
if (position == buffer.Length) {
|
|
byte [] newBuffer = new byte [buffer.Length * 2];
|
|
Array.Copy (buffer, 0, newBuffer, 0, buffer.Length);
|
|
buffer = newBuffer;
|
|
}
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
} while (!lastLine);
|
|
|
|
if (position > 0) {
|
|
Encoding encoding = new ASCIIEncoding ();
|
|
|
|
string line = encoding.GetString (buffer, 0, position - 1);
|
|
|
|
// parse the line to the lastResponse object
|
|
SmtpResponse response = SmtpResponse.Parse (line);
|
|
|
|
return response;
|
|
} else {
|
|
throw new System.IO.IOException ("Connection closed");
|
|
}
|
|
}
|
|
|
|
void ResetExtensions()
|
|
{
|
|
authMechs = AuthMechs.None;
|
|
}
|
|
|
|
void ParseExtensions (string extens)
|
|
{
|
|
string[] parts = extens.Split ('\n');
|
|
|
|
foreach (string part in parts) {
|
|
if (part.Length < 4)
|
|
continue;
|
|
|
|
string start = part.Substring (4);
|
|
if (start.StartsWith ("AUTH ", StringComparison.Ordinal)) {
|
|
string[] options = start.Split (' ');
|
|
for (int k = 1; k < options.Length; k++) {
|
|
string option = options[k].Trim();
|
|
// GSSAPI, KERBEROS_V4, NTLM not supported
|
|
switch (option) {
|
|
/*
|
|
case "CRAM-MD5":
|
|
authMechs |= AuthMechs.CramMD5;
|
|
break;
|
|
case "DIGEST-MD5":
|
|
authMechs |= AuthMechs.DigestMD5;
|
|
break;
|
|
*/
|
|
case "LOGIN":
|
|
authMechs |= AuthMechs.Login;
|
|
break;
|
|
case "PLAIN":
|
|
authMechs |= AuthMechs.Plain;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Send (MailMessage message)
|
|
{
|
|
if (message == null)
|
|
throw new ArgumentNullException ("message");
|
|
|
|
if (deliveryMethod == SmtpDeliveryMethod.Network && (Host == null || Host.Trim ().Length == 0))
|
|
throw new InvalidOperationException ("The SMTP host was not specified");
|
|
else if (deliveryMethod == SmtpDeliveryMethod.PickupDirectoryFromIis)
|
|
throw new NotSupportedException("IIS delivery is not supported");
|
|
|
|
if (port == 0)
|
|
port = 25;
|
|
|
|
// Block while sending
|
|
mutex.WaitOne ();
|
|
try {
|
|
messageInProcess = message;
|
|
if (deliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory)
|
|
SendToFile (message);
|
|
else
|
|
SendInternal (message);
|
|
} catch (CancellationException) {
|
|
// This exception is introduced for convenient cancellation process.
|
|
} catch (SmtpException) {
|
|
throw;
|
|
} catch (Exception ex) {
|
|
throw new SmtpException ("Message could not be sent.", ex);
|
|
} finally {
|
|
// Release the mutex to allow other threads access
|
|
mutex.ReleaseMutex ();
|
|
messageInProcess = null;
|
|
}
|
|
}
|
|
|
|
private void SendInternal (MailMessage message)
|
|
{
|
|
CheckCancellation ();
|
|
|
|
try {
|
|
client = new TcpClient (host, port);
|
|
stream = client.GetStream ();
|
|
// FIXME: this StreamWriter creation is bogus.
|
|
// It expects as if a Stream were able to switch to SSL
|
|
// mode (such behavior is only in Mainsoft Socket API).
|
|
writer = new StreamWriter (stream);
|
|
reader = new StreamReader (stream);
|
|
|
|
SendCore (message);
|
|
} finally {
|
|
if (writer != null)
|
|
writer.Close ();
|
|
if (reader != null)
|
|
reader.Close ();
|
|
if (stream != null)
|
|
stream.Close ();
|
|
if (client != null)
|
|
client.Close ();
|
|
}
|
|
}
|
|
|
|
// FIXME: simple implementation, could be brushed up.
|
|
private void SendToFile (MailMessage message)
|
|
{
|
|
if (!Path.IsPathRooted (pickupDirectoryLocation))
|
|
throw new SmtpException("Only absolute directories are allowed for pickup directory.");
|
|
|
|
string filename = Path.Combine (pickupDirectoryLocation,
|
|
Guid.NewGuid() + ".eml");
|
|
|
|
try {
|
|
writer = new StreamWriter(filename);
|
|
|
|
// FIXME: See how Microsoft fixed the bug about envelope senders, and how it actually represents the info in .eml file headers
|
|
// For all we know, defaultFrom may be the envelope sender
|
|
// For now, we are no worse than some versions of .NET
|
|
MailAddress from = message.From;
|
|
if (from == null)
|
|
from = defaultFrom;
|
|
|
|
string dt = DateTime.Now.ToString("ddd, dd MMM yyyy HH':'mm':'ss zzz", DateTimeFormatInfo.InvariantInfo);
|
|
// remove ':' from time zone offset (e.g. from "+01:00")
|
|
dt = dt.Remove(dt.Length - 3, 1);
|
|
SendHeader(HeaderName.Date, dt);
|
|
|
|
SendHeader (HeaderName.From, EncodeAddress(from));
|
|
SendHeader (HeaderName.To, EncodeAddresses(message.To));
|
|
if (message.CC.Count > 0)
|
|
SendHeader (HeaderName.Cc, EncodeAddresses(message.CC));
|
|
SendHeader (HeaderName.Subject, EncodeSubjectRFC2047 (message));
|
|
|
|
foreach (string s in message.Headers.AllKeys)
|
|
SendHeader (s, message.Headers [s]);
|
|
|
|
AddPriorityHeader (message);
|
|
|
|
boundaryIndex = 0;
|
|
if (message.Attachments.Count > 0)
|
|
SendWithAttachments (message);
|
|
else
|
|
SendWithoutAttachments (message, null, false);
|
|
|
|
|
|
} finally {
|
|
if (writer != null) writer.Close(); writer = null;
|
|
}
|
|
}
|
|
|
|
private void SendCore (MailMessage message)
|
|
{
|
|
SmtpResponse status;
|
|
|
|
status = Read ();
|
|
if (IsError (status))
|
|
throw new SmtpException (status.StatusCode, status.Description);
|
|
|
|
// EHLO
|
|
|
|
// FIXME: parse the list of extensions so we don't bother wasting
|
|
// our time trying commands if they aren't supported.
|
|
|
|
// get the host name (not fully qualified)
|
|
string fqdn = Dns.GetHostName ();
|
|
try {
|
|
// we'll try for the fully qualified name - ref: bug #33551
|
|
fqdn = Dns.GetHostEntry (fqdn).HostName;
|
|
}
|
|
catch (SocketException) {
|
|
// we could not resolve our name but will continue with the partial name
|
|
// IOW we won't fail to send email because of this - ref: bug #37246
|
|
}
|
|
status = SendCommand ("EHLO " + fqdn);
|
|
|
|
if (IsError (status)) {
|
|
status = SendCommand ("HELO " + fqdn);
|
|
|
|
if (IsError (status))
|
|
throw new SmtpException (status.StatusCode, status.Description);
|
|
} else {
|
|
// Parse ESMTP extensions
|
|
string extens = status.Description;
|
|
|
|
if (extens != null)
|
|
ParseExtensions (extens);
|
|
}
|
|
|
|
if (enableSsl) {
|
|
InitiateSecureConnection ();
|
|
ResetExtensions();
|
|
writer = new StreamWriter (stream);
|
|
reader = new StreamReader (stream);
|
|
status = SendCommand ("EHLO " + fqdn);
|
|
|
|
if (IsError (status)) {
|
|
status = SendCommand ("HELO " + fqdn);
|
|
|
|
if (IsError (status))
|
|
throw new SmtpException (status.StatusCode, status.Description);
|
|
} else {
|
|
// Parse ESMTP extensions
|
|
string extens = status.Description;
|
|
if (extens != null)
|
|
ParseExtensions (extens);
|
|
}
|
|
}
|
|
|
|
if (authMechs != AuthMechs.None)
|
|
Authenticate ();
|
|
|
|
// The envelope sender: use 'Sender:' in preference of 'From:'
|
|
MailAddress sender = message.Sender;
|
|
if (sender == null)
|
|
sender = message.From;
|
|
if (sender == null)
|
|
sender = defaultFrom;
|
|
|
|
// MAIL FROM:
|
|
status = SendCommand ("MAIL FROM:<" + sender.Address + '>');
|
|
if (IsError (status)) {
|
|
throw new SmtpException (status.StatusCode, status.Description);
|
|
}
|
|
|
|
// Send RCPT TO: for all recipients
|
|
List<SmtpFailedRecipientException> sfre = new List<SmtpFailedRecipientException> ();
|
|
|
|
for (int i = 0; i < message.To.Count; i ++) {
|
|
status = SendCommand ("RCPT TO:<" + message.To [i].Address + '>');
|
|
if (IsError (status))
|
|
sfre.Add (new SmtpFailedRecipientException (status.StatusCode, message.To [i].Address));
|
|
}
|
|
for (int i = 0; i < message.CC.Count; i ++) {
|
|
status = SendCommand ("RCPT TO:<" + message.CC [i].Address + '>');
|
|
if (IsError (status))
|
|
sfre.Add (new SmtpFailedRecipientException (status.StatusCode, message.CC [i].Address));
|
|
}
|
|
for (int i = 0; i < message.Bcc.Count; i ++) {
|
|
status = SendCommand ("RCPT TO:<" + message.Bcc [i].Address + '>');
|
|
if (IsError (status))
|
|
sfre.Add (new SmtpFailedRecipientException (status.StatusCode, message.Bcc [i].Address));
|
|
}
|
|
|
|
if (sfre.Count >0)
|
|
throw new SmtpFailedRecipientsException ("failed recipients", sfre.ToArray ());
|
|
|
|
// DATA
|
|
status = SendCommand ("DATA");
|
|
if (IsError (status))
|
|
throw new SmtpException (status.StatusCode, status.Description);
|
|
|
|
// Send message headers
|
|
string dt = DateTime.Now.ToString ("ddd, dd MMM yyyy HH':'mm':'ss zzz", DateTimeFormatInfo.InvariantInfo);
|
|
// remove ':' from time zone offset (e.g. from "+01:00")
|
|
dt = dt.Remove (dt.Length - 3, 1);
|
|
SendHeader (HeaderName.Date, dt);
|
|
|
|
MailAddress from = message.From;
|
|
if (from == null)
|
|
from = defaultFrom;
|
|
|
|
SendHeader (HeaderName.From, EncodeAddress (from));
|
|
SendHeader (HeaderName.To, EncodeAddresses (message.To));
|
|
if (message.CC.Count > 0)
|
|
SendHeader (HeaderName.Cc, EncodeAddresses (message.CC));
|
|
SendHeader (HeaderName.Subject, EncodeSubjectRFC2047 (message));
|
|
|
|
string v = "normal";
|
|
|
|
switch (message.Priority){
|
|
case MailPriority.Normal:
|
|
v = "normal";
|
|
break;
|
|
|
|
case MailPriority.Low:
|
|
v = "non-urgent";
|
|
break;
|
|
|
|
case MailPriority.High:
|
|
v = "urgent";
|
|
break;
|
|
}
|
|
SendHeader ("Priority", v);
|
|
if (message.Sender != null)
|
|
SendHeader ("Sender", EncodeAddress (message.Sender));
|
|
if (message.ReplyToList.Count > 0)
|
|
SendHeader ("Reply-To", EncodeAddresses (message.ReplyToList));
|
|
|
|
foreach (string s in message.Headers.AllKeys)
|
|
SendHeader (s, MailMessage.EncodeSubjectRFC2047 (message.Headers [s], message.HeadersEncoding));
|
|
|
|
AddPriorityHeader (message);
|
|
|
|
boundaryIndex = 0;
|
|
if (message.Attachments.Count > 0)
|
|
SendWithAttachments (message);
|
|
else
|
|
SendWithoutAttachments (message, null, false);
|
|
|
|
SendDot ();
|
|
|
|
status = Read ();
|
|
if (IsError (status))
|
|
throw new SmtpException (status.StatusCode, status.Description);
|
|
|
|
try {
|
|
status = SendCommand ("QUIT");
|
|
} catch (System.IO.IOException) {
|
|
// We excuse server for the rude connection closing as a response to QUIT
|
|
}
|
|
}
|
|
|
|
public void Send (string from, string recipients, string subject, string body)
|
|
{
|
|
Send (new MailMessage (from, recipients, subject, body));
|
|
}
|
|
|
|
public Task SendMailAsync (MailMessage message)
|
|
{
|
|
var tcs = new TaskCompletionSource<object> ();
|
|
SendCompletedEventHandler handler = null;
|
|
handler = (s, e) => SendMailAsyncCompletedHandler (tcs, e, handler, this);
|
|
SendCompleted += handler;
|
|
SendAsync (message, tcs);
|
|
return tcs.Task;
|
|
}
|
|
|
|
public Task SendMailAsync (string from, string recipients, string subject, string body)
|
|
{
|
|
return SendMailAsync (new MailMessage (from, recipients, subject, body));
|
|
}
|
|
|
|
static void SendMailAsyncCompletedHandler (TaskCompletionSource<object> source, AsyncCompletedEventArgs e, SendCompletedEventHandler handler, SmtpClient client)
|
|
{
|
|
if (source != e.UserState)
|
|
return;
|
|
|
|
client.SendCompleted -= handler;
|
|
|
|
if (e.Error != null) {
|
|
source.SetException (e.Error);
|
|
return;
|
|
}
|
|
|
|
if (e.Cancelled) {
|
|
source.SetCanceled ();
|
|
return;
|
|
}
|
|
|
|
source.SetResult (null);
|
|
}
|
|
|
|
private void SendDot()
|
|
{
|
|
writer.Write(".\r\n");
|
|
writer.Flush();
|
|
}
|
|
|
|
private void SendData (string data)
|
|
{
|
|
if (String.IsNullOrEmpty (data)) {
|
|
writer.Write("\r\n");
|
|
writer.Flush();
|
|
return;
|
|
}
|
|
|
|
StringReader sr = new StringReader (data);
|
|
string line;
|
|
bool escapeDots = deliveryMethod == SmtpDeliveryMethod.Network;
|
|
while ((line = sr.ReadLine ()) != null) {
|
|
CheckCancellation ();
|
|
|
|
if (escapeDots) {
|
|
if (line.Length > 0 && line[0] == '.') {
|
|
line = "." + line;
|
|
}
|
|
}
|
|
writer.Write (line);
|
|
writer.Write ("\r\n");
|
|
}
|
|
writer.Flush ();
|
|
}
|
|
|
|
public void SendAsync (MailMessage message, object userToken)
|
|
{
|
|
if (worker != null)
|
|
throw new InvalidOperationException ("Another SendAsync operation is in progress");
|
|
|
|
worker = new BackgroundWorker ();
|
|
worker.DoWork += delegate (object o, DoWorkEventArgs ea) {
|
|
try {
|
|
user_async_state = ea.Argument;
|
|
Send (message);
|
|
} catch (Exception ex) {
|
|
ea.Result = ex;
|
|
throw ex;
|
|
}
|
|
};
|
|
worker.WorkerSupportsCancellation = true;
|
|
worker.RunWorkerCompleted += delegate (object o, RunWorkerCompletedEventArgs ea) {
|
|
// Note that RunWorkerCompletedEventArgs.UserState cannot be used (LAMESPEC)
|
|
OnSendCompleted (new AsyncCompletedEventArgs (ea.Error, ea.Cancelled, user_async_state));
|
|
};
|
|
worker.RunWorkerAsync (userToken);
|
|
}
|
|
|
|
public void SendAsync (string from, string recipients, string subject, string body, object userToken)
|
|
{
|
|
SendAsync (new MailMessage (from, recipients, subject, body), userToken);
|
|
}
|
|
|
|
public void SendAsyncCancel ()
|
|
{
|
|
if (worker == null)
|
|
throw new InvalidOperationException ("SendAsync operation is not in progress");
|
|
worker.CancelAsync ();
|
|
}
|
|
|
|
private void AddPriorityHeader (MailMessage message) {
|
|
switch (message.Priority) {
|
|
case MailPriority.High:
|
|
SendHeader (HeaderName.Priority, "Urgent");
|
|
SendHeader (HeaderName.Importance, "high");
|
|
SendHeader (HeaderName.XPriority, "1");
|
|
break;
|
|
case MailPriority.Low:
|
|
SendHeader (HeaderName.Priority, "Non-Urgent");
|
|
SendHeader (HeaderName.Importance, "low");
|
|
SendHeader (HeaderName.XPriority, "5");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void SendSimpleBody (MailMessage message) {
|
|
SendHeader (HeaderName.ContentType, message.BodyContentType.ToString ());
|
|
if (message.ContentTransferEncoding != TransferEncoding.SevenBit)
|
|
SendHeader (HeaderName.ContentTransferEncoding, GetTransferEncodingName (message.ContentTransferEncoding));
|
|
SendData (string.Empty);
|
|
|
|
SendData (EncodeBody (message));
|
|
}
|
|
|
|
private void SendBodylessSingleAlternate (AlternateView av) {
|
|
SendHeader (HeaderName.ContentType, av.ContentType.ToString ());
|
|
if (av.TransferEncoding != TransferEncoding.SevenBit)
|
|
SendHeader (HeaderName.ContentTransferEncoding, GetTransferEncodingName (av.TransferEncoding));
|
|
SendData (string.Empty);
|
|
|
|
SendData (EncodeBody (av));
|
|
}
|
|
|
|
private void SendWithoutAttachments (MailMessage message, string boundary, bool attachmentExists)
|
|
{
|
|
if (message.Body == null && message.AlternateViews.Count == 1)
|
|
SendBodylessSingleAlternate (message.AlternateViews [0]);
|
|
else if (message.AlternateViews.Count > 0)
|
|
SendBodyWithAlternateViews (message, boundary, attachmentExists);
|
|
else
|
|
SendSimpleBody (message);
|
|
}
|
|
|
|
|
|
private void SendWithAttachments (MailMessage message) {
|
|
string boundary = GenerateBoundary ();
|
|
|
|
// first "multipart/mixed"
|
|
ContentType messageContentType = new ContentType ();
|
|
messageContentType.Boundary = boundary;
|
|
messageContentType.MediaType = "multipart/mixed";
|
|
messageContentType.CharSet = null;
|
|
|
|
SendHeader (HeaderName.ContentType, messageContentType.ToString ());
|
|
SendData (String.Empty);
|
|
|
|
// body section
|
|
Attachment body = null;
|
|
|
|
if (message.AlternateViews.Count > 0)
|
|
SendWithoutAttachments (message, boundary, true);
|
|
else {
|
|
body = Attachment.CreateAttachmentFromString (message.Body, null, message.BodyEncoding, message.IsBodyHtml ? "text/html" : "text/plain");
|
|
message.Attachments.Insert (0, body);
|
|
}
|
|
|
|
try {
|
|
SendAttachments (message, body, boundary);
|
|
} finally {
|
|
if (body != null)
|
|
message.Attachments.Remove (body);
|
|
}
|
|
|
|
EndSection (boundary);
|
|
}
|
|
|
|
private void SendBodyWithAlternateViews (MailMessage message, string boundary, bool attachmentExists)
|
|
{
|
|
AlternateViewCollection alternateViews = message.AlternateViews;
|
|
|
|
string inner_boundary = GenerateBoundary ();
|
|
|
|
ContentType messageContentType = new ContentType ();
|
|
messageContentType.Boundary = inner_boundary;
|
|
messageContentType.MediaType = "multipart/alternative";
|
|
|
|
if (!attachmentExists) {
|
|
SendHeader (HeaderName.ContentType, messageContentType.ToString ());
|
|
SendData (String.Empty);
|
|
}
|
|
|
|
// body section
|
|
AlternateView body = null;
|
|
if (message.Body != null) {
|
|
body = AlternateView.CreateAlternateViewFromString (message.Body, message.BodyEncoding, message.IsBodyHtml ? "text/html" : "text/plain");
|
|
alternateViews.Insert (0, body);
|
|
StartSection (boundary, messageContentType);
|
|
}
|
|
|
|
try {
|
|
// alternate view sections
|
|
foreach (AlternateView av in alternateViews) {
|
|
|
|
string alt_boundary = null;
|
|
ContentType contentType;
|
|
if (av.LinkedResources.Count > 0) {
|
|
alt_boundary = GenerateBoundary ();
|
|
contentType = new ContentType ("multipart/related");
|
|
contentType.Boundary = alt_boundary;
|
|
|
|
contentType.Parameters ["type"] = av.ContentType.ToString ();
|
|
StartSection (inner_boundary, contentType);
|
|
StartSection (alt_boundary, av.ContentType, av);
|
|
} else {
|
|
contentType = new ContentType (av.ContentType.ToString ());
|
|
StartSection (inner_boundary, contentType, av);
|
|
}
|
|
|
|
switch (av.TransferEncoding) {
|
|
case TransferEncoding.Base64:
|
|
byte [] content = new byte [av.ContentStream.Length];
|
|
av.ContentStream.Read (content, 0, content.Length);
|
|
SendData (Convert.ToBase64String (content, Base64FormattingOptions.InsertLineBreaks));
|
|
break;
|
|
case TransferEncoding.QuotedPrintable:
|
|
byte [] bytes = new byte [av.ContentStream.Length];
|
|
av.ContentStream.Read (bytes, 0, bytes.Length);
|
|
SendData (ToQuotedPrintable (bytes));
|
|
break;
|
|
case TransferEncoding.SevenBit:
|
|
case TransferEncoding.Unknown:
|
|
content = new byte [av.ContentStream.Length];
|
|
av.ContentStream.Read (content, 0, content.Length);
|
|
SendData (Encoding.ASCII.GetString (content));
|
|
break;
|
|
}
|
|
|
|
if (av.LinkedResources.Count > 0) {
|
|
SendLinkedResources (message, av.LinkedResources, alt_boundary);
|
|
EndSection (alt_boundary);
|
|
}
|
|
|
|
if (!attachmentExists)
|
|
SendData (string.Empty);
|
|
}
|
|
|
|
} finally {
|
|
if (body != null)
|
|
alternateViews.Remove (body);
|
|
}
|
|
EndSection (inner_boundary);
|
|
}
|
|
|
|
private void SendLinkedResources (MailMessage message, LinkedResourceCollection resources, string boundary)
|
|
{
|
|
foreach (LinkedResource lr in resources) {
|
|
StartSection (boundary, lr.ContentType, lr);
|
|
|
|
switch (lr.TransferEncoding) {
|
|
case TransferEncoding.Base64:
|
|
byte [] content = new byte [lr.ContentStream.Length];
|
|
lr.ContentStream.Read (content, 0, content.Length);
|
|
SendData (Convert.ToBase64String (content, Base64FormattingOptions.InsertLineBreaks));
|
|
break;
|
|
case TransferEncoding.QuotedPrintable:
|
|
byte [] bytes = new byte [lr.ContentStream.Length];
|
|
lr.ContentStream.Read (bytes, 0, bytes.Length);
|
|
SendData (ToQuotedPrintable (bytes));
|
|
break;
|
|
case TransferEncoding.SevenBit:
|
|
case TransferEncoding.Unknown:
|
|
content = new byte [lr.ContentStream.Length];
|
|
lr.ContentStream.Read (content, 0, content.Length);
|
|
SendData (Encoding.ASCII.GetString (content));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SendAttachments (MailMessage message, Attachment body, string boundary) {
|
|
foreach (Attachment att in message.Attachments) {
|
|
ContentType contentType = new ContentType (att.ContentType.ToString ());
|
|
if (att.Name != null) {
|
|
contentType.Name = att.Name;
|
|
if (att.NameEncoding != null)
|
|
contentType.CharSet = att.NameEncoding.HeaderName;
|
|
att.ContentDisposition.FileName = att.Name;
|
|
}
|
|
StartSection (boundary, contentType, att, att != body);
|
|
|
|
byte [] content = new byte [att.ContentStream.Length];
|
|
att.ContentStream.Read (content, 0, content.Length);
|
|
switch (att.TransferEncoding) {
|
|
case TransferEncoding.Base64:
|
|
SendData (Convert.ToBase64String (content, Base64FormattingOptions.InsertLineBreaks));
|
|
break;
|
|
case TransferEncoding.QuotedPrintable:
|
|
SendData (ToQuotedPrintable (content));
|
|
break;
|
|
case TransferEncoding.SevenBit:
|
|
case TransferEncoding.Unknown:
|
|
SendData (Encoding.ASCII.GetString (content));
|
|
break;
|
|
}
|
|
|
|
SendData (string.Empty);
|
|
}
|
|
}
|
|
|
|
private SmtpResponse SendCommand (string command)
|
|
{
|
|
writer.Write (command);
|
|
// Certain SMTP servers will reject mail sent with unix line-endings; see http://cr.yp.to/docs/smtplf.html
|
|
writer.Write ("\r\n");
|
|
writer.Flush ();
|
|
return Read ();
|
|
}
|
|
|
|
private void SendHeader (string name, string value)
|
|
{
|
|
SendData (String.Format ("{0}: {1}", name, value));
|
|
}
|
|
|
|
private void StartSection (string section, ContentType sectionContentType)
|
|
{
|
|
SendData (String.Format ("--{0}", section));
|
|
SendHeader ("content-type", sectionContentType.ToString ());
|
|
SendData (string.Empty);
|
|
}
|
|
|
|
private void StartSection (string section, ContentType sectionContentType, AttachmentBase att)
|
|
{
|
|
SendData (String.Format ("--{0}", section));
|
|
SendHeader ("content-type", sectionContentType.ToString ());
|
|
SendHeader ("content-transfer-encoding", GetTransferEncodingName (att.TransferEncoding));
|
|
if (!string.IsNullOrEmpty (att.ContentId))
|
|
SendHeader("content-ID", "<" + att.ContentId + ">");
|
|
SendData (string.Empty);
|
|
}
|
|
|
|
private void StartSection (string section, ContentType sectionContentType, Attachment att, bool sendDisposition) {
|
|
SendData (String.Format ("--{0}", section));
|
|
if (!string.IsNullOrEmpty (att.ContentId))
|
|
SendHeader("content-ID", "<" + att.ContentId + ">");
|
|
SendHeader ("content-type", sectionContentType.ToString ());
|
|
SendHeader ("content-transfer-encoding", GetTransferEncodingName (att.TransferEncoding));
|
|
if (sendDisposition)
|
|
SendHeader ("content-disposition", att.ContentDisposition.ToString ());
|
|
SendData (string.Empty);
|
|
}
|
|
|
|
// use proper encoding to escape input
|
|
private string ToQuotedPrintable (string input, Encoding enc)
|
|
{
|
|
byte [] bytes = enc.GetBytes (input);
|
|
return ToQuotedPrintable (bytes);
|
|
}
|
|
|
|
private string ToQuotedPrintable (byte [] bytes)
|
|
{
|
|
StringWriter writer = new StringWriter ();
|
|
int charsInLine = 0;
|
|
int curLen;
|
|
StringBuilder sb = new StringBuilder("=", 3);
|
|
byte equalSign = (byte)'=';
|
|
char c = (char)0;
|
|
|
|
foreach (byte i in bytes) {
|
|
if (i > 127 || i == equalSign) {
|
|
sb.Length = 1;
|
|
sb.Append(Convert.ToString (i, 16).ToUpperInvariant ());
|
|
curLen = 3;
|
|
} else {
|
|
c = Convert.ToChar (i);
|
|
if (c == '\r' || c == '\n') {
|
|
writer.Write (c);
|
|
charsInLine = 0;
|
|
continue;
|
|
}
|
|
curLen = 1;
|
|
}
|
|
|
|
charsInLine += curLen;
|
|
if (charsInLine > 75) {
|
|
writer.Write ("=\r\n");
|
|
charsInLine = curLen;
|
|
}
|
|
if (curLen == 1)
|
|
writer.Write (c);
|
|
else
|
|
writer.Write (sb.ToString ());
|
|
}
|
|
|
|
return writer.ToString ();
|
|
}
|
|
private static string GetTransferEncodingName (TransferEncoding encoding)
|
|
{
|
|
switch (encoding) {
|
|
case TransferEncoding.QuotedPrintable:
|
|
return "quoted-printable";
|
|
case TransferEncoding.SevenBit:
|
|
return "7bit";
|
|
case TransferEncoding.Base64:
|
|
return "base64";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
private void InitiateSecureConnection () {
|
|
SmtpResponse response = SendCommand ("STARTTLS");
|
|
|
|
if (IsError (response)) {
|
|
throw new SmtpException (SmtpStatusCode.GeneralFailure, "Server does not support secure connections.");
|
|
}
|
|
|
|
#if SECURITY_DEP
|
|
var tlsProvider = MonoTlsProviderFactory.GetProviderInternal ();
|
|
var settings = MSI.MonoTlsSettings.CopyDefaultSettings ();
|
|
settings.UseServicePointManagerCallback = true;
|
|
var sslStream = tlsProvider.CreateSslStream (stream, false, settings);
|
|
CheckCancellation ();
|
|
sslStream.AuthenticateAsClient (Host, this.ClientCertificates, SslProtocols.Default, false);
|
|
stream = sslStream.AuthenticatedStream;
|
|
|
|
#else
|
|
throw new SystemException ("You are using an incomplete System.dll build");
|
|
#endif
|
|
}
|
|
|
|
void Authenticate ()
|
|
{
|
|
string user = null, pass = null;
|
|
|
|
if (UseDefaultCredentials) {
|
|
user = CredentialCache.DefaultCredentials.GetCredential (new System.Uri ("smtp://" + host), "basic").UserName;
|
|
pass = CredentialCache.DefaultCredentials.GetCredential (new System.Uri ("smtp://" + host), "basic").Password;
|
|
} else if (Credentials != null) {
|
|
user = Credentials.GetCredential (host, port, "smtp").UserName;
|
|
pass = Credentials.GetCredential (host, port, "smtp").Password;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
Authenticate (user, pass);
|
|
}
|
|
|
|
void CheckStatus (SmtpResponse status, int i)
|
|
{
|
|
if (((int) status.StatusCode) != i)
|
|
throw new SmtpException (status.StatusCode, status.Description);
|
|
}
|
|
|
|
void ThrowIfError (SmtpResponse status)
|
|
{
|
|
if (IsError (status))
|
|
throw new SmtpException (status.StatusCode, status.Description);
|
|
}
|
|
|
|
void Authenticate (string user, string password)
|
|
{
|
|
if (authMechs == AuthMechs.None)
|
|
return;
|
|
|
|
SmtpResponse status;
|
|
/*
|
|
if ((authMechs & AuthMechs.DigestMD5) != 0) {
|
|
status = SendCommand ("AUTH DIGEST-MD5");
|
|
CheckStatus (status, 334);
|
|
string challenge = Encoding.ASCII.GetString (Convert.FromBase64String (status.Description.Substring (4)));
|
|
Console.WriteLine ("CHALLENGE: {0}", challenge);
|
|
DigestSession session = new DigestSession ();
|
|
session.Parse (false, challenge);
|
|
string response = session.Authenticate (this, user, password);
|
|
status = SendCommand (Convert.ToBase64String (Encoding.UTF8.GetBytes (response)));
|
|
CheckStatus (status, 235);
|
|
} else */
|
|
if ((authMechs & AuthMechs.Login) != 0) {
|
|
status = SendCommand ("AUTH LOGIN");
|
|
CheckStatus (status, 334);
|
|
status = SendCommand (Convert.ToBase64String (Encoding.UTF8.GetBytes (user)));
|
|
CheckStatus (status, 334);
|
|
status = SendCommand (Convert.ToBase64String (Encoding.UTF8.GetBytes (password)));
|
|
CheckStatus (status, 235);
|
|
} else if ((authMechs & AuthMechs.Plain) != 0) {
|
|
string s = String.Format ("\0{0}\0{1}", user, password);
|
|
s = Convert.ToBase64String (Encoding.UTF8.GetBytes (s));
|
|
status = SendCommand ("AUTH PLAIN " + s);
|
|
CheckStatus (status, 235);
|
|
} else {
|
|
throw new SmtpException ("AUTH types PLAIN, LOGIN not supported by the server");
|
|
}
|
|
}
|
|
|
|
#endregion // Methods
|
|
|
|
// The HeaderName struct is used to store constant string values representing mail headers.
|
|
private struct HeaderName {
|
|
public const string ContentTransferEncoding = "Content-Transfer-Encoding";
|
|
public const string ContentType = "Content-Type";
|
|
public const string Bcc = "Bcc";
|
|
public const string Cc = "Cc";
|
|
public const string From = "From";
|
|
public const string Subject = "Subject";
|
|
public const string To = "To";
|
|
public const string MimeVersion = "MIME-Version";
|
|
public const string MessageId = "Message-ID";
|
|
public const string Priority = "Priority";
|
|
public const string Importance = "Importance";
|
|
public const string XPriority = "X-Priority";
|
|
public const string Date = "Date";
|
|
}
|
|
|
|
// This object encapsulates the status code and description of an SMTP response.
|
|
private struct SmtpResponse {
|
|
public SmtpStatusCode StatusCode;
|
|
public string Description;
|
|
|
|
public static SmtpResponse Parse (string line) {
|
|
SmtpResponse response = new SmtpResponse ();
|
|
|
|
if (line.Length < 4)
|
|
throw new SmtpException ("Response is to short " +
|
|
line.Length + ".");
|
|
|
|
if ((line [3] != ' ') && (line [3] != '-'))
|
|
throw new SmtpException ("Response format is wrong.(" +
|
|
line + ")");
|
|
|
|
// parse the response code
|
|
response.StatusCode = (SmtpStatusCode) Int32.Parse (line.Substring (0, 3));
|
|
|
|
// set the raw response
|
|
response.Description = line;
|
|
|
|
return response;
|
|
}
|
|
}
|
|
}
|
|
|
|
class CCredentialsByHost : ICredentialsByHost
|
|
{
|
|
public CCredentialsByHost (string userName, string password) {
|
|
this.userName = userName;
|
|
this.password = password;
|
|
}
|
|
|
|
public NetworkCredential GetCredential (string host, int port, string authenticationType) {
|
|
return new NetworkCredential (userName, password);
|
|
}
|
|
|
|
private string userName;
|
|
private string password;
|
|
}
|
|
}
|
|
|