SmtpBatchMailer.cs :  » RSS-RDF » Aggie » Bitworking » Smtp » C# / CSharp Open Source

Home
C# / CSharp Open Source
1.2.6.4 mono .net core
2.2.6.4 mono core
3.Aspect Oriented Frameworks
4.Bloggers
5.Build Systems
6.Business Application
7.Charting Reporting Tools
8.Chat Servers
9.Code Coverage Tools
10.Content Management Systems CMS
11.CRM ERP
12.Database
13.Development
14.Email
15.Forum
16.Game
17.GIS
18.GUI
19.IDEs
20.Installers Generators
21.Inversion of Control Dependency Injection
22.Issue Tracking
23.Logging Tools
24.Message
25.Mobile
26.Network Clients
27.Network Servers
28.Office
29.PDF
30.Persistence Frameworks
31.Portals
32.Profilers
33.Project Management
34.RSS RDF
35.Rule Engines
36.Script
37.Search Engines
38.Sound Audio
39.Source Control
40.SQL Clients
41.Template Engines
42.Testing
43.UML
44.Web Frameworks
45.Web Service
46.Web Testing
47.Wiki Engines
48.Windows Presentation Foundation
49.Workflows
50.XML Parsers
C# / C Sharp
C# / C Sharp by API
C# / CSharp Tutorial
C# / CSharp Open Source » RSS RDF » Aggie 
Aggie » Bitworking » Smtp » SmtpBatchMailer.cs
// SmtpBatchMailer.cs
//
// This file is based (in parts) on SmtpEmailer code by Steaven Woyan (mailto:swoyan@hotmail.com),
// and by extension on logic/code design from PJ Naughter's C++ SMTP package
// located at http://www.naughter.com/smpt.html.
//
// The original code has been considerably modified for use in Aggie by Ziv Caspi.
//
// Mode modifications made relative to SmtpEmailer:
// - Various name changes
// - Better encapsulation for SmtpAttachment
// - SmtpAttachment can now be used in multi-threaded environments
// - Massive reorg of shared code into functions
// - Added option of several content-transfer-encoding
// - Separated mailer from the mail message
// - Created a batch mailer, to allow multiple messages to be sent
//   to a single host over a single connection
// - Added configuration component

using System;
using System.Collections;
using System.Text;
using System.IO;
using System.Security.Cryptography;
using System.Threading;

using Bitworking.Smtp;

namespace Bitworking.Smtp{
  /// <summary>
  /// How content is encoded before being sent over the SMTP channel.
  /// See RFC 2045, content-transfer-encoding.
  /// </summary>
  public enum ContentTransferEncoding {
    // TODO: Does .NET provide this type of encoding facilities?
    //       If so, use that instead of our own implementation.
    _7bit, _8bit, binary, quoted_printable, base64, ietf_token, x_token
  }

  /// <summary>
  /// This class houses static utility methods.
  /// </summary>
  public class Utils {
    static public string ToString( ContentTransferEncoding encoding ) {
      switch ( encoding ) {
        case ContentTransferEncoding._7bit: return "7bit";
        case ContentTransferEncoding._8bit: return "8bit";
        default: return encoding.ToString();
      }
    }

    public const char InvalidConversionIndicator = '?';
    static public char GetConversionIndicator( ContentTransferEncoding encoding ) {
      switch ( encoding ) {
        case ContentTransferEncoding.quoted_printable: return 'Q';
        case ContentTransferEncoding.base64:           return 'B';
        default: return InvalidConversionIndicator;
      }
    }

    static public string ContentTransferEncode( ContentTransferEncoding how, string what ) {
      return ContentTransferEncode( how, what, 76, "\r\n" );
    } // ContentTransferEncode

    /// <summary>
    /// Encode string according to RFC 2045, section 6
    /// </summary>
    /// <param name="how">Encoding method to use.</param>
    /// <param name="what">Content to encode.</param>
    /// <param name="charsPerLine">After how many characters to break the line.</param>
    /// <param name="lineSeparator">What line break to use.</param>
    /// <returns>The encoded string.</returns>
    static public string ContentTransferEncode( ContentTransferEncoding how, string what, int charsPerLine, string lineSeparator ) {
      switch ( how ) {
        case ContentTransferEncoding._7bit:
          return what.ToString();
        case ContentTransferEncoding._8bit:
          return EncodeAs8bit( what );
        case ContentTransferEncoding.binary:
          return what.ToString();
        case ContentTransferEncoding.quoted_printable:
          return EncodeAsQuotedPrintable( what );
        default:
          throw new ArgumentOutOfRangeException( "how", "Currently this type of encoding not supported" );
      }
    } // ContentTransferEncode

    static public string ContentTransferEncode( ContentTransferEncoding how, byte[] what ) {
      return ContentTransferEncode( how, what, 76, "\r\n" );
    } // ContentTransferEncode

    static public string ContentTransferEncode( ContentTransferEncoding how, byte[] what, int charsPerLine, string lineSeparator  ) {
      switch ( how ) {
        case ContentTransferEncoding._8bit:
          return EncodeAs8bit( what );
        case ContentTransferEncoding.base64:
          return EncodeAsBase64( what, charsPerLine, lineSeparator );
        default:
          throw new ArgumentOutOfRangeException( "how", "Currently this type of encoding not supported" );
      }
    } // ContentTransferEncode

    /// <summary>
    /// Encode byte array as RFC 2045 Base64 (sect 6.8).
    /// Automatically cuts lines using CRLF pairs.
    /// </summary>
    /// <param name="what">Byte stream to encode.</param>
    /// <returns>Encoded byte stream.</returns>
    static public string EncodeAsBase64( byte[] what ) {
      return EncodeAsBase64( what, 76, "\r\n" );
    } // EncodeAsBase64

    static public string EncodeAsBase64( byte[] what, int charsPerLine, string lineSeparator ) {
      // For reasons unknown, CLR's base64 encoder does not provide
      // a way to limit the length of the line. RFC 2045 requires
      // that lines will have no more than 76 characters (after encoding).
      string asSingleLine = Convert.ToBase64String( what );
      return BreakIntoLines( asSingleLine, charsPerLine, lineSeparator );
    } // EncodeAsBase64

    static public string BreakIntoLines( string what, int charsPerLine, string lineSeparator ) {
      int length = what.Length;
      int capacity = length + ((length + charsPerLine) * lineSeparator.Length) / charsPerLine; // Estimated length of new string
      StringBuilder res = new StringBuilder( capacity );
      int pos = 0;
      int remaining = length;
      while ( remaining > 0 ) {
        int howManyToAdd = (remaining > charsPerLine) ? charsPerLine : remaining;
        res.Append( what, pos, howManyToAdd );
        pos += howManyToAdd;
        remaining -= howManyToAdd;
        if ( remaining == 0 )
          break;
        res.Append( lineSeparator );
      }
      return res.ToString();
    } // BreakIntoLines

    /// <summary>
    /// Encode string as RFC 2045 quoted-printable (sect 6.7).
    /// Original code from PJ Naughter (http://www.naughter.com/smtp.html)
    /// </summary>
    /// <param name="what">Content to encode.</param>
    /// <returns>The encoded string.</returns>
    static public string EncodeAsQuotedPrintable_( string what ) {
      int whatLength = what.Length;
      int estimatedLength = whatLength + whatLength / 4;
      StringBuilder buf = new StringBuilder( estimatedLength );

      for ( int x = 0; x < whatLength; ++x ) {
        char  chr   = what[x];
        sbyte byt = (sbyte) chr;

        // RFC 2045 6.7(2), 6.7(3), 6.7(4)
        // TODO: Replace this with a table-driven implementation!
        if ( ((byt>=33) && (byt<=60))  ||
          ((byt>=62) && (byt<=126)) ||
          (byt=='\r')               ||
          (byt=='\n')               ||
          (byt=='\t')               ||
          (byt==' ') ) {
          buf.Append( chr );
        }
        else {
          buf.Append( '=' );
          buf.Append(((sbyte)((byt & 0xF0) >> 4)).ToString("X"));
          buf.Append(((sbyte) (byt & 0x0F)).ToString("X"));
        }
      } // for

      // RFC 2045 6.7(3), 6.7(4), 6.7(5)
      // Soft-break lines larger than 76 chars, no trailiing whitespace
      // at the end of lines, etc.

      int    start = 0;
      string enc   = buf.ToString();
      buf.Length   = 0;

      for ( int x = 0; x < enc.Length; ++x ) {
        sbyte byt = (sbyte) enc[x];
        // TODO: This is inefficient. 1: Use a table. 2: Limit the loop length so no enc.Length check is necessary
        if ( byt == '\n' || byt == '\r' ||  x == (enc.Length - 1) ) {
          buf.Append( enc.Substring( start, x-start+1) );
          start = x + 1;
          continue;
        }
        if ( (x-start) > 76 ) {
          bool inWord = true;
          while ( inWord ) {
            inWord = (!char.IsWhiteSpace( enc, x ) && enc[x-2] != '=');
            if ( inWord ) {
              --x;
              byt = (sbyte) enc[x];
            }
            if ( x == start ) {
              x = start + 76;
              break;
            }

          } // while
          buf.Append(enc.Substring(start, x - start + 1));
          buf.Append("=\r\n");
          start = x + 1;
        } // if on 76
      } // for

      return buf.ToString();
    } // EncodeAsQuotedPrintable_

    /// <summary>
    /// Encode string as RFC 2045 quoted-printable (sect. 6.7).
    /// Similar to EncodeAsQuotedPrintable_, but has simpler (and slower) algorithm
    /// which is easier to maintain.
    /// </summary>
    /// <param name="what">Content to encode.</param>
    /// <returns>The encoded string.</returns>
    static public string EncodeAsQuotedPrintable( string what ) {
      int whatLength = what.Length;
      int estimatedLength = whatLength + whatLength / 2; // A pessimistic estimation
      StringBuilder str = new StringBuilder( estimatedLength );

      for ( int pos = 0, col = 1; pos < whatLength; ++pos ) {
        char c = what[pos];
        byte b = (byte)c; // TODO: (ZIVC) Hopefully, no one gives us non-7bit code...

        if ( (b >= 33) && (b <= 126) && (b != 61) ) {
          str.Append( c );
          if ( ++col > 70 ) {
            col = 1;
            str.Append( "=\r\n" );
          }
        }
        else if ( (b != '\r') && (b != '\n') ) {
          str.Append( '=' );
          str.Append(((sbyte)((b & 0xF0) >> 4)).ToString("X"));
          str.Append(((sbyte) (b & 0x0F)).ToString("X"));
          col += 3;
          if ( col > 70 ) {
            col = 1;
            str.Append( "=\r\n" );
          }
        }
        else {
          str.Append( c );
          col = 1;
        }
      } // for

      return str.ToString();
    } // EncodeAsQuotedPrintable

    public static string EncodeAs8bit( byte[] what ) {
      // TODO: Dumb test implementation, copied from the function below
      char[] asCharArray = System.Text.Encoding.UTF8.GetChars( what );
      string asStr = new string( asCharArray );
      return EncodeAs8bit( asStr );
    } // EncodeAs8bit
    /// <summary>
    /// Encode string as RFC 2045 8bit.
    /// This means fragmenting lines so that they are not too long.
    /// </summary>
    /// <param name="what">Content to encode.</param>
    /// <returns>The encoded string.</returns>
    public static string EncodeAs8bit( string what ) {
      // We break lines that are 70 characters long at
      // the first whitespace which appears after that point.
      // This is probably only a temporary solution.
      int whatLength = what.Length;
      int estimatedLength = whatLength + whatLength / 2; // A pessimistic estimation
      StringBuilder str = new StringBuilder( estimatedLength );

      for ( int pos = 0, col = 1; pos < whatLength; ++pos ) {
        char c = what[pos];

        if ( c == '\r' ) {
          col = 1;
          str.Append( '\r' );
          continue;
        }

        if ( col < 70 ) {
          str.Append( c );
          col++;
          continue;
        }

        if ( c == ' ' ) {
          str.Append( "\r\n " );
          col = 2;
          continue;
        }

        str.Append( c );
        col++;

      } // for

      return str.ToString();

    } // EncodeAs8bit

    /// <summary>
    /// Ensure that a string exists and is not all whitespace.
    /// </summary>
    /// <param name="what">String to check for substance.</param>
    /// <param name="paramName">The name of the string to check.</param>
    /// <param name="message">The message that accompanies the exception
    /// if the string has no substance.</param>
    /// <returns>The string to check if it has substance.</returns>
    static public string ValidateExistence( string what, string paramName, string message ) {
      if ( what == null || what.Trim().Length == 0 )
        throw new ArgumentNullException( paramName, message );
      return what;
    } // ValidateExistence
  } // class Utils

  /// <summary>
  /// A single SMTP mail message, in its final form.
  /// (Consider using SmtpMailItem instead, which helps
  /// you build the message from logical parts.)
  /// </summary>
  public class SmtpMessage : ICloneable {
    #region Private data
    private string from_;
    private string to_;
    private string message_;
    #endregion

    #region Getters
    public string From {
      get { return from_; }
    }
    public string To {
      get { return to_; }
    }
    public string Message {
      get { return message_; }
    }
    #endregion

    #region Construction and copy
    public SmtpMessage( string from, string to, string message ) {
      from_    = from;
      to_      = to;
      message_ = message;
    }
    public Object Clone() {
      SmtpMessage clone = (SmtpMessage)this.MemberwiseClone();
      return clone;
    } // Clone
    #endregion

  } // class SmtpMessage

  /// <summary>
  /// A single SMTP mail message to send.
  /// </summary>
  public class SmtpMailItem : ICloneable {
    #region AddressList helper class
    public class AddressList : ICloneable {
      private ArrayList list_;
      public AddressList() {
        list_ = new ArrayList();
      }
      public void Add( string address ) {
        string addr = Utils.ValidateExistence( address, "address", "Invalid address." );
        list_.Add( addr );
      }
      public void Remove( string address ) {
        list_.Remove( address );
      }
      public string ToString( string separator ) {
        // TODO: Do we want to estimate the size of this buffer before allocating it?
        StringBuilder buf = new StringBuilder();
        int howMany = list_.Count;
        if ( howMany > 0 ) {
          buf.Append( list_[0] );
          for ( int i = 1; i < howMany; ++i ) { // Start at 1, as we've added item 0
            buf.Append( separator );
            buf.Append( list_[i] );
          }
        }
        return buf.ToString();
      }
      public int Count {
        get { return list_.Count; }
      }
      public IEnumerator GetEnumerator() {
        return list_.GetEnumerator();
      }
      public Object Clone() {
        AddressList clone = new AddressList();
        clone.list_ = (ArrayList)this.list_.Clone();
        return clone;
      }
    } // class AddressList
    #endregion

    #region AttachmentList helper class
    public class AttachmentList : ICloneable {
      private ArrayList list_;
      public AttachmentList() {
        list_ = new ArrayList();
      }
      public void Add( SmtpAttachment attachment ) {
        list_.Add( attachment );
      }
      public void Remove( SmtpAttachment attachment ) {
        list_.Remove( attachment );
      }
      public int Count {
        get { return list_.Count; }
      }
      public IEnumerator GetEnumerator() {
        return list_.GetEnumerator();
      }
      public Object Clone() {
        AttachmentList clone = new AttachmentList();
        clone.list_ = (ArrayList)this.list_.Clone();
        return clone;
      }
    } // class AttachmentList
    #endregion

    #region Construction and copy
    /// <summary>
    /// Empty constructor.
    /// </summary>
    public SmtpMailItem() {
    }

    /// <summary>
    /// Clone an existing item.
    /// </summary>
    /// <returns></returns>
    public Object Clone() {
      // Do a shallow copy, and then fix all the fields
      // which must be deep-copied:
      SmtpMailItem clone = (SmtpMailItem)this.MemberwiseClone();
      clone.recipients_  = (AddressList)this.recipients_.Clone();
      clone.attachments_ = (AttachmentList)this.attachments_.Clone();
      return clone;
    } // Clone
    #endregion

    #region Properties
    /// <summary>
    /// The originator of this mail message.
    /// </summary>
    public string From {
      get { return from_; }
      set { from_ = Utils.ValidateExistence( value, "From", "Invalid mail originator (from) field." ); }
    }

    /// <summary>
    /// The item's subject line.
    /// </summary>
    public string Subject {
      get { return subject_; }
      set { subject_ = Utils.ValidateExistence( value, "Subject", "Invalid mail subject field." ); }
    }

    /// <summary>
    /// The message itself. Empty messages are allowed.
    /// </summary>
    public string Body {
      get { return body_; }
      set { body_ = ((value != null) ? value : ""); }
    }

    /// <summary>
    /// The character-set to use for sending the body.
    /// </summary>
    public Encoding Charset {
      get { return charset_; }
      set { charset_ = value; }
    }
    /// <summary>
    /// The encoding to use when sending the message body.
    /// </summary>
    public ContentTransferEncoding ContentTransferEncoding {
      get { return contentTransferEncoding_; }
      set { contentTransferEncoding_ = value; }
    }

    /// <summary>
    /// Whether to send the body as HTML or not.
    /// </summary>
    public bool SendAsHtml {
      get { return sendAsHtml_; }
      set { sendAsHtml_ = value; }
    }

    /// <summary>
    /// Whom to send the message to.
    /// </summary>
    public SmtpMailItem.AddressList Recipients {
      get { return recipients_; }
    }

    /// <summary>
    /// List of attachments to send along with the message.
    /// </summary>
    public SmtpMailItem.AttachmentList Attachments {
      get { return attachments_; }
    }

    /// <summary>
    /// How many attachments are to be sent inline.
    /// </summary>
    public int InlineAttachmentsCount {
      get {
        return CountAttachments( HowAttached.Inlined );
      }
    }
    public int ExternalAttachmentsCount {
      get {
        return CountAttachments( HowAttached.Externally );
      }
    }
    #endregion

    #region Private members
    private string                  from_;
    private string                  subject_;
    private string                  body_;
    private Encoding                charset_;
    private ContentTransferEncoding contentTransferEncoding_;
    private bool                    sendAsHtml_;
    private AddressList             recipients_ = new AddressList();
    private AttachmentList          attachments_ = new AttachmentList();
    #endregion

    #region Private methods
    private int CountAttachments( HowAttached howAttached ) {
      int count = 0;
      foreach ( object item in attachments_ ) {
        SmtpAttachment attachment = (SmtpAttachment)item;
        if ( attachment.HowAttached == howAttached )
          count++;
      }
      return count;
    } // CountAttachments
    #endregion
  } // class SmtpMailItem

  /// <summary>
  /// A class for sending multiple email messages with attachments
  /// via a single SMTP connection to a single host.
  /// </summary>
  public class SmtpBatchMailer {
    /* A word on how this class works:
     * This class contains a private thread that it uses to send mail messages.
     * Clients that want to send mail create instances of the SmtpMailItem class,
     * and submit them for sending by this class. Each mail item is then duplicated,
     * and put on an internal queue to be picked up by the sending thread.
     * Note that the sending thread awaits a special command to stop -- failure
     * to call this command currently means the mailer will block the "process"
     * from quitting.
     */
    #region Helper class to indicate a request to stop background activities
    public class PleaseStop {
    }
    #endregion

    #region Construction and Destruction
    // TODO: Add a true dispose pattern here.
    public SmtpBatchMailer( string host )
      : this( host, 25, false ) {
    }
    public SmtpBatchMailer( string host, int port )
      : this( host, port, false ) {
    }
    public SmtpBatchMailer( SmtpConfigInfo config ) {
      if ( config != null )
        Init( config.host, config.port, config.offline, config.username, config.password );
      else
        Init( "", 0, true, "", "" );
    }
    public SmtpBatchMailer( string host, int port, bool offline ) {
      Init( host, port, offline, "", "" );
    }
    private void Init( string host, int port, bool offline, string username, string password ) {
      this.offline = offline;

      if ( !offline ) {
        if ( host == null || host.Trim().Length == 0 )
          throw new ArgumentNullException( "Invalid host name.", "host" );

        if ( port <= 0 )
          throw new ArgumentException( "Invalid port number.", "port" );

        host_ = host;
        port_ = port;
      }
      else {
        // TODO: Is there any reason to cling to the supplied host/port here?
        host_ = "";
        port_ = 25;
      }

      username_ = username;
      password_ = password;

      commandsUnsafe_ = new Queue();
      commands_       = Queue.Synchronized( commandsUnsafe_ );
      hasCommands_    = new ManualResetEvent( false );

      consumerThread_ = new Thread( new ThreadStart( MailThread ) );
      consumerThread_.Start();
    } // SmtpBatchMailer

    public void Close() {
      PleaseStop command = new PleaseStop();
      EnqueueCommand( command );

      // Wait for the consumer thread to actually stop
      consumerThread_.Join();
    } // Close
    #endregion

    #region Public interface
    /// <summary>
    /// Adds the mail item to a list of messages to send in the background.
    /// </summary>
    /// <param name="item">Mail item to send in the background.</param>
    public void SubmitItemForSending( SmtpMailItem item ) {
      // Duplicate the mail item, so we'll have our own copy to pass
      // to the worker thread
      SmtpMailItem myItem = (SmtpMailItem)item.Clone();

      // Push the mail item on the queue
      // TODO: Currently, we have no optimizations: the consumer thread
      // is always running and looking for mail to consume. A better
      // implementation would be to delay its creation until necessary,
      // close it down when idle, etc.
      EnqueueCommand( myItem );
    } // SubmitItemForSending

    public void SubmitMessageForSending( SmtpMessage message ) {
      SmtpMessage myMessage = (SmtpMessage)message.Clone();
      EnqueueCommand( myMessage );
    } // SubmitMessageForSending
    #endregion

    #region Private members
    private bool offline;
    private string host_ = "";
    private int    port_ = 25;
    private string username_ = "";
    private string password_ = "";
    private Thread consumerThread_;
    // Must not be touched outside of the consumer thread.
    // TODO: We might want to enforce this by removing it out
    // of the members and make it a local var in the thread's function,
    // but then we'll have to pass it around many functions. Ugly.
    // Another way would be to move a lot of this functionality into
    // the SmtpConnection class itself -- this sounds like a better idea.
    private SmtpConnection connection_;
    private StreamWriter debugWriter_;
    // commandsUnsafe, and hasCommands_ are sync'ed. Must only be used by
    // the constructor, consumer thread, and the EnqueueCommand() method.
    private Queue            commandsUnsafe_;
    private Queue            commands_;
    private ManualResetEvent hasCommands_;
    #endregion

    #region Private thread
    private void EnqueueCommand( object command ) {
      // NOTE: The order of these two operations is crucial,
      //       or we might get the "lost event" syndrom.
      commands_.Enqueue( command );
      hasCommands_.Set();
    } // EnqueueCommand

    /// <summary>
    /// Go into a loop in which we consume all mail items
    /// put on our queue by mailing them to the target.
    /// Do not stop until told to.
    /// </summary>
    private void MailThread() {
      // The consumer thread waits for the hasCommands_ event to
      // be raised, indicating that we have a command waiting
      // in our command queue (commands_). It then wakes up
      // and checks what the command is, performs it, and
      // goes back to sleep.

      bool wantsToStop = false; // When set, thread quits

      while ( !wantsToStop ) {
        // Here is how items are removed from the command queue:
        // 1. We remove all items from the queue and act on them
        // 2. We reset the event that indicates there are items
        // 3. We test to see if there are any MORE items on the queue
        // That third step is necessary to handle
        // an enqueue operation being done between steps 1 and 2.
        while ( commands_.Count > 0 )
          MailThreadConsumeCommands( ref wantsToStop );
        hasCommands_.Reset();
        while ( commands_.Count > 0 )
          MailThreadConsumeCommands( ref wantsToStop );

        // No need to go waiting if we've been asked to stop
        if ( wantsToStop )
          break;

        // At this point we've either consumed all commands
        // and the event is clear, or there are more commands
        // and the event is set.
        int timeout_ms = ((connection_ == null) ? Timeout.Infinite : 10000); // 10 seconds
        bool ok = hasCommands_.WaitOne( timeout_ms, false );
        if ( !ok ) {
          // We've timed-out. Might as well stop the connection
          MailThreadDropConnection();
        }
      } // while
    } // MailThread

    private void MailThreadConsumeCommands( ref bool wantsToStop ) {
      // Precondition: We're called when there's at least one command to consume
      Object command = commands_.Dequeue();
      Type commandType = command.GetType();
      if ( commandType == typeof( SmtpMailItem ) ) {
        MailThreadSendMail( command as SmtpMailItem );
      }
      else if ( commandType == typeof( SmtpMessage ) )
        MailThreadSendMail( command as SmtpMessage );
      else if ( commandType == typeof( SmtpBatchMailer.PleaseStop ) ) {
        MailThreadDropConnection();
        wantsToStop = true;
      }

    } // MailThreadConsumeCommands

    private void MailThreadSendMail( SmtpMessage message ) {
      // TODO: Validation
      if ( connection_ == null )
        EstablishConnection();
      SendMailItem( message );
    } // MailThreadSendMail

    private void MailThreadSendMail( SmtpMailItem item ) {
      // Validate some fields
      Utils.ValidateExistence( item.From, "From", "Mail item has an invalid From field" );
      if ( item.Recipients.Count == 0 )
        throw new ArgumentException( "Mail item has no recipients set" );

      // If the connection is down, this is the time to bring it up
      // (this is a small optimization that delays opening the connection
      // until we really need to)
      if ( connection_ == null )
        EstablishConnection();

      SendMailItem( item );
    } // MailThreadSendMail

    private void MailThreadDropConnection() {
      if ( connection_ == null )
        return;
      TeardownConnection();
    }
    #endregion

    #region Private utility functions to send/receive data over the SMTP connection
    private void SendRequestGetReply( ref StringBuilder buf, string format, params object[] args ) {
      string response;
      int    code;
      SendRequestGetReply( out response, out code, ref buf, format, args );
    } // SendRequestGetReply

    private void SendRequestGetReply( out string response, out int code, ref StringBuilder buf, string format, params object[] args ) {
      SendRequest( ref buf, format, args );
      connection_.GetReply( out response, out code );
    } // SendRequestGetReply

    private void SendRequest( ref StringBuilder buf, string format, params object[] args ) {
      buf.Length = 0;
      buf.AppendFormat( format, args );
      string what = buf.ToString();
      if ( debugWriter_ != null )
        debugWriter_.WriteLine( what );
      connection_.SendCommand( what );
      buf.Length = 0;
    } // SendRequest

    private void SendRequest( string what ) {
      if ( debugWriter_ != null )
        debugWriter_.WriteLine( what );
      connection_.SendCommand( what );
    } // SendRequest
    #endregion

    #region Private utility functions to bring up and tear down the SMTP connection
    private void EstablishConnection() {
      // Open connection to the server
      if ( connection_ != null )
        throw new Exception( "Attempted to establish a connection with an SMTP server" +
          " while a connection already exists" ); // TODO: We need a better exception class here
      connection_ = new SmtpConnection();
      connection_.Offline = offline;
      connection_.Open( host_, port_ );

      // Exchange greetings with the server
      StringBuilder buf = new StringBuilder( 256 );

      // Server announces itself...
      string response;
      int code;
      connection_.GetReply( out response, out code );

      // If we need to do authentication, use ESMTP. Otherwise,
      // can simply using RFC 821.
      if ( username_ != "" || password_ != "" ) {
        // RFC 1869 EHLO
        SendRequestGetReply( out response, out code, ref buf, "EHLO {0}", host_ );
        if ( code != 250 ) {
          // TODO: How do we report errors here?
          throw new Exception( "SMTP server responded " + response + " to EHLO. Expecting 250" );
        }

        Authenticate();
      }
      else {
        // RFC 821 HELO
        SendRequestGetReply( ref buf, "HELO {0}", host_ );
      }
    } // EstablishConnection

    private void Authenticate() {
      int code;
      string response;
      StringBuilder buf = new StringBuilder( 256 );
      
      // C: "AUTH LOGIN"
      // S: "334 " base64("Username:")
      SendRequestGetReply( out response, out code, ref buf, "AUTH LOGIN" );
      if ( code != 334 ) {
        throw new Exception( "SMTP server responded " + response + " to AUTH LOGIN. Expecting 334" );
      }

      // For debugging only:
      response = response.Substring( 4, response.Length - 4 );
      byte[] asciiResponse = Convert.FromBase64String( response );
      response = Encoding.ASCII.GetString( asciiResponse );

      // C: base64(username)
      // S: "334 " base64("Password:")
      // TODO: What encoding does the server expect us to use?
      byte[] username = Encoding.ASCII.GetBytes( username_ );
      SendRequestGetReply( out response, out code, ref buf, Convert.ToBase64String( username ) );
      if ( code != 334 ) {
        throw new Exception( "SMTP server responses " + response + " to our username. Expecting 334" );
      }

      // For debugging only:
      response = response.Substring( 4, response.Length - 4 );
      asciiResponse = Convert.FromBase64String( response );
      response = Encoding.ASCII.GetString( asciiResponse );

      // C: base64(password)
      // S: "235"
      // TODO: What encoding does the server expect us to use?
      byte[] password = Encoding.ASCII.GetBytes( password_ );
      SendRequestGetReply( out response, out code, ref buf, Convert.ToBase64String( password ) );
      if ( code != 235 ) {
        throw new Exception( "SMTP server responses " + response + " to our username. Expecting 235" );
      }

    } // Authenticate

    private void TeardownConnection() {
      StringBuilder buf = new StringBuilder( 10 );
      SendRequestGetReply( ref buf, "QUIT" );
      connection_.Close();
      connection_ = null;
    }
    #endregion

    private void SendMailItem( SmtpMailItem item ) {
      // Preconditions: Connection established, and belongs to us

      // TODO: (ZIVC) Push this uglyness out of this function. It is too long as it is.
      bool sendAsHtml = item.SendAsHtml;
      bool hasInlines = (item.InlineAttachmentsCount > 0);
      bool hasAttachments = (item.ExternalAttachmentsCount > 0 );

      if ( item.Charset == null )
        item.Charset = System.Text.Encoding.UTF8;

      // contentTypeBodyLine and contentTransferEncodingLine go with
      // the actual body text ONLY.
      string contentTypeBodyLine
        = "Content-Type: text/"
        + (sendAsHtml ? "html" : "plain" )
        + "; charset="
        + item.Charset.WebName;
      string contentTransferEncodingLine
        = "Content-Transfer-Encoding: "
        + Utils.ToString( item.ContentTransferEncoding ) + "\r\n";

      // Convert body (a string) into a byte array according to the text
      // encoding (here called "charset", and then encode it for transfer
      byte[] bodyEncoded = item.Charset.GetBytes( item.Body );
      //string body = Utils.ContentTransferEncode( item.ContentTransferEncoding, item.Body );
      string body = Utils.ContentTransferEncode( item.ContentTransferEncoding, bodyEncoded );

      StringBuilder buf = new StringBuilder( body.Length + 256 );
      HeaderFieldEncoder header = new HeaderFieldEncoder( item.Charset, item.ContentTransferEncoding );

      // TODO: Currently, no linebreaks are allowed in the following fields
      //       (Are there any other evil chars we should remove?)
      // TODO: Other fields we might like to do this:
      item.Subject = System.Text.RegularExpressions.Regex.Replace( item.Subject, "\\r|\\n", " " );

      // NOTE: Currently we don't encode RFC 821 fields. Only RFC 822 fields.
      // RFC 821 MAIL
      SendRequestGetReply( ref buf, "MAIL FROM:<{0}>", item.From );

      // RFC 821 RCPT
      foreach( object obj in item.Recipients ) {
        SendRequestGetReply( ref buf, "RCPT TO:<{0}>", obj );
      }

      // RFC 821 DATA
      SendRequest( "DATA" );
      // TODO: Server should answer here 354. We need to eat it! (ZivC)

      // Debugging support: Uncomment the next line for the mailer
      //   to leave an email file in the current directory of each mail sent
      //debugWriter_ = new StreamWriter( System.Guid.NewGuid().ToString() + ".eml", true );
      using ( debugWriter_ ) {

        // RFC 822 headers
        SendRequest( "X-Mailer: Aggie SmtpBatchMailer" );
        SendRequest( ref buf, header.Encode( "DATE",     "{0}", DateTime.Now.ToLongDateString() ) );
        SendRequest( ref buf, header.Encode( "FROM",     "{0}", item.From ) );
        SendRequest( ref buf, header.Encode( "TO",       "{0}", item.Recipients.ToString( ";" ) ) );
        SendRequest( ref buf, header.Encode( "REPLY-TO", "{0}", item.From ) );
        SendRequest( ref buf, header.Encode( "SUBJECT",  "{0}", item.Subject ) );

        // MIME...
        // We always declare ourselves as a MIME message, with multipart contents.
        // The body itself is encoded, and so requires Content-Transfer-Encoding header.
        SendRequest( "MIME-Version: 1.0" );

        if ( !sendAsHtml
          ||
          ( sendAsHtml
          &&
          ( hasInlines || hasAttachments )
          )
          ) {
          SendRequest( "Content-Type: multipart/mixed; boundary=\"#SEPERATOR1#\"\r\n" );
          SendRequest( "This is a multi-part message.\r\n\r\n--#SEPERATOR1#");
        }

        if ( sendAsHtml ) {
          SendRequest( "Content-Type: multipart/related; boundary=\"#SEPERATOR2#\"");
          SendRequest( "Content-Transfer-Encoding: 7bit\r\n" );
          SendRequest( "--#SEPERATOR2#" );
        }

        if ( sendAsHtml && hasInlines ) {
          SendRequest( "Content-Type: multipart/alternative; boundary=\"#SEPERATOR3#\"" );
          SendRequest( "Content-Transfer-Encoding: 7bit\r\n" );
          SendRequest( "--#SEPERATOR3#" );
          SendRequest( contentTypeBodyLine );
          SendRequest( contentTransferEncodingLine );
          SendRequest( body );
          SendRequest( "--#SEPERATOR3#" );
          SendRequest( "Content-Type: text/plain; charset=iso-8859-1" );
          SendRequest( "\r\nIf you can see this, then your email client does not support MHTML messages." );
          SendRequest( "--#SEPERATOR3#--\r\n" );
          SendRequest( "--#SEPERATOR2#\r\n" );
          SendMailAttachments( ref buf, item, HowAttached.Inlined );
        }
        else {
          SendRequest( contentTypeBodyLine );
          SendRequest( contentTransferEncodingLine );
          SendRequest( body );
        }

        if ( sendAsHtml ) {
          SendRequest( "\r\n--#SEPERATOR2#--" );
        }

        if ( hasAttachments ) {
          SendMailAttachments( ref buf, item, HowAttached.Externally );
        }

        // Wrapup
        SendRequest( "" );
        if ( hasInlines  || hasAttachments ) {
          SendRequest( "--#SEPERATOR1#--" );
        }
        SendRequestGetReply( ref buf, "." );
      }
      debugWriter_ = null;

    } // SendMailItem

    private void SendMailItem( SmtpMessage message ) {
      // Preconditions: Connection established, and belongs to us
      StringBuilder buf = new StringBuilder( message.Message.Length + 256 );

      // RFC 821 MAIL
      SendRequestGetReply( ref buf, "MAIL FROM:<{0}>", message.From );

      // RFC 821 RCPT
      SendRequestGetReply( ref buf, "RCPT TO:<{0}>", message.To );

      // RFC 821 DATA
      SendRequest( "DATA" );
      // TODO: Isn't the server supposed to send a response after DATA?
      //       If so, we don't consume it here, which means that we might
      //       consume it in the wrong place!

      SendRequest( message.Message );
      /*
      X-Mailer: Aggie SmtpBatchMailer
      DATE: 26 Aug 76 1429 EDT
      FROM: Aggie SmtpBatchMailer SmtpMessage <zivca@netvision.net.il>
      TO: Ziv Caspi <zivca@netvision.net.il>
      SUBJECT: A test
      MIME-Version: 1.0
      Content-Type: text/plain; charset=us-ascii
      Content-Transfer-Encoding: 7bit
      
      This is a test text
      */
      // Wrapup
      SendRequest( "" );
      SendRequestGetReply( ref buf, "." );

    } // SendMailItem


    private void SendMailAttachments( ref StringBuilder buf, SmtpMailItem mail, HowAttached type ) {
      // TODO: (ZIVC) As I have already indicated, I have not yet touched this function.
      buf.Length = 0;
      byte[] fbuf = new byte[2048];
      int num;
      SmtpAttachment attachment;
      string seperator = type == HowAttached.Externally ? "\r\n--#SEPERATOR1#" : "\r\n--#SEPERATOR2#";
      buf.Length = 0;
      foreach(object o in mail.Attachments) {
        attachment = (SmtpAttachment) o;
        if(attachment.HowAttached != type) {
          continue;
        }
        CryptoStream cs = new CryptoStream(new FileStream(attachment.Filename, FileMode.Open, FileAccess.Read, FileShare.Read), new ToBase64Transform(), CryptoStreamMode.Read);

        SendRequest( seperator );
        SendRequest( ref buf, "Content-Type: {0}; name={1}",
          attachment.ContentType, Path.GetFileName( attachment.Filename) );

        SendRequest( "Content-Transfer-Encoding: base64" );
        SendRequest( ref buf, "Content-Disposition: attachment; filename={0}",
          Path.GetFileName(attachment.Filename) );
        SendRequest( ref buf, "Content-ID: {0}\r\n",
          Path.GetFileNameWithoutExtension( attachment.Filename) );
        num = cs.Read( fbuf, 0, 2048 );
        while ( num > 0 ) {
          connection_.SendData( Encoding.ASCII.GetChars(fbuf, 0, num), 0, num );
          num = cs.Read( fbuf, 0, 2048 );
        }
        cs.Close();
        connection_.SendCommand("");
      }

    } // SendMailAttachments

  } // class SmtpBatchMailer

  /// <summary>
  /// Utility class to encode RFC 822 header fields.
  /// </summary>
  public class HeaderFieldEncoder {
    private ContentTransferEncoding encoding;
    private Encoding                charset; // Charset is of type encoding. Confusing, heh?

    public HeaderFieldEncoder( Encoding charset, ContentTransferEncoding encoding ) {
      this.charset  = charset;
      this.encoding = encoding;
    } // HeaderFieldEncodeer

    public string Encode( string header, string format, params object[] args ) {
      // Combine the format string and the arguments to create
      // a header field string
      StringBuilder buff = new StringBuilder();
      buff.AppendFormat( format, args );
      string result;

      // If the string has only characters less than 128, we're done.
      int pos;
      for ( pos = 0; pos < buff.Length; pos++ ) {
        if ( 127 < (int)buff[pos] ) {
          break;
        }
      }
      if ( pos == buff.Length ) {
        result = header + ": " + buff.ToString();
        return result;
      }

      // There are non-7bit chars in the string. Encode it
      //   FIELD ": =?" CHARSET "?" CONVERSION-INDICATOR "?" ENCODED-STRING "?="
      char conversionIndicator = Utils.GetConversionIndicator( encoding );
      if ( conversionIndicator == Utils.InvalidConversionIndicator ) {
        // The conversion cannot be done. We could throw, but it seems
        // better just to push the characters forward and let the receiver
        // use what it can
        result = header + ": " + buff.ToString();
        return result;
      }

      byte[] encodedBytes = charset.GetBytes( buff.ToString() );
      string encoded = Utils.ContentTransferEncode( encoding, encodedBytes, int.MaxValue, "" );
      result = header + ": =?" + charset.WebName + "?" + conversionIndicator + "?"
        + encoded + "?=";

      // TODO: We currently don't break the line if it's too long
      return result;
    } // Encode

  } // class HeaderFieldEncoder
}
www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.