// 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
}
|