Hi!
Here are the smtp classes. They have now a namespace that is SmtpClient and
they should probably be in another namespace. I have designed the classes in
souch a way so that they will be easy to incorperate in the SmtpMail class.
It would be nice if someone could insert them into the cvs or tell me of where
to insert them. I think that a new assembly would be good since the classes
expose more functionallity than the SmtpMail do. SmtpStream ex: provides
direct access to the smtp protocol. but anyhing is ok with me :)
When everything is added it can be used inside SmtpMail.Send like this
----------------------
SmtpClient smtp = new SmtpClient( this.SmtpServer );
smtp.Send( message );
smtp.Close();
----------------------
I think its better if i send a patch later for SmtpMail to manage all the
exceptions in a copatible way with .NET sdk..
Here is the code size in loc:
268 SmtpClient.cs
9 SmtpException.cs
63 SmtpResponse.cs
235 SmtpStream.cs
575 total
Here is the status page: http://p.rsn.bth.se/smtp
Best regards
Per Arneng
CC=mcs
BIN_DIR=../bin
DLL_FILE=$(BIN_DIR)/SmtpClient.dll
SRC_FILES=$(wildcard Smtp*.cs)
all: $(DLL_FILE)
$(DLL_FILE): $(SRC_FILES)
$(CC) -o $(DLL_FILE) --target library -warn:4 -r System.Web.dll $(SRC_FILES)
clean:
rm -f *~ $(DLL_FILE)
using System.IO;
namespace SmtpClient {
public class SmtpException : IOException {
public SmtpException( string message ) : base( message ) {}
}
}
// SmtpClient.cs
// author: Per Arneng <[EMAIL PROTECTED]>
using System;
using System.Net;
using System.IO;
using System.Text;
using System.Collections;
using System.Net.Sockets;
using System.Web.Mail;
namespace SmtpClient {
/// represents a conntection to a smtp server
public class SmtpClient {
private string server;
private TcpClient tcpConnection;
private SmtpStream smtp;
private Encoding encoding;
//Initialise the variables and connect
public SmtpClient( string server ) {
this.server = server;
encoding = new ASCIIEncoding( );
Connect();
}
// make the actual connection
// and HELO handshaking
private void Connect() {
tcpConnection = new TcpClient( server , 25 );
Stream stream = tcpConnection.GetStream();
smtp = new SmtpStream( stream );
// read the server greeting
smtp.ReadResponse();
smtp.CheckForStatusCode( 220 );
// write the HELO command to the server
smtp.WriteHelo( Dns.GetHostName() );
}
public void Send( MailMessage msg ) {
if( ( ! HasData( msg.From ) ) || ( ! HasData( msg.To ) ) )
throw new ArgumentException( "From & To properties must be set." );
// start with a reset incase old data
// is present at the server in this session
smtp.WriteRset();
// write the mail from command
smtp.WriteMailFrom( msg.From );
// write the rcpt to command
smtp.WriteRcptTo( msg.To );
// write the data command and then
// send the email
smtp.WriteData();
if( msg.Attachments.Count == 0 ) {
SendSinglepartMail( msg );
} else {
SendMultipartMail( msg );
}
// write the data end tag "."
smtp.WriteDataEndTag();
}
// sends a single part mail to the server
private void SendSinglepartMail( MailMessage msg ) {
// create the headers
IDictionary headers = CreateHeaders( msg );
smtp.WriteHeaders( headers );
// send the mail body
smtp.WriteLine( msg.Body );
}
// sends a multipart mail to the server
private void SendMultipartMail( MailMessage msg ) {
// create the headers
IDictionary headers = CreateHeaders( msg );
// set the part boundary
string boundary = "NextPart_000_1113_1962_1fe8";
// set the Content-Type header to multipart/mixed
headers[ "Content-Type" ] =
String.Format( "multipart/mixed;\r\n boundary={0}" , boundary );
// write the headers
// and start writing the multipart body
smtp.WriteHeaders( headers );
// write the first part text part
// before the attachments
smtp.WriteBoundary( boundary );
Hashtable partHeaders = new Hashtable();
partHeaders[ "Content-Type" ] = "text/plain";
smtp.WriteHeaders( partHeaders );
smtp.WriteLine( msg.Body );
smtp.WriteBoundary( boundary );
// now start to write the attachments
for( int i=0; i< msg.Attachments.Count ; i++ ) {
MailAttachment a = (MailAttachment)msg.Attachments[ i ];
FileStream file =
new FileStream( a.Filename , FileMode.Open );
Hashtable aHeaders = new Hashtable();
aHeaders[ "Content-Type" ] =
String.Format( "unknown/unknown; name=\"{0}\"", a.Filename );
aHeaders[ "Content-Disposition" ] =
String.Format( "attachment; filename=\"{0}\"" , a.Filename );
aHeaders[ "Content-Transfer-Encoding" ] = "base64";
smtp.WriteHeaders( aHeaders );
smtp.WriteBase64( file );
smtp.WriteLine( "" );
// if it is the last attachment write
// the final boundary otherwise write
// a normal one.
if( i < (msg.Attachments.Count - 1) ) {
smtp.WriteBoundary( boundary );
} else {
smtp.WriteFinalBoundary( boundary );
}
file.Close();
}
}
// send the standard headers
// and the custom in MailMessage
// FIXME: more headers needs to be added so
// that all properties from MailMessage are sent..
// missing: Priority , UrlContentBase,UrlContentLocation
private IDictionary CreateHeaders( MailMessage msg ) {
Hashtable headers = new Hashtable();
headers[ "From" ] = msg.From;
headers[ "To" ] = msg.To;
if( HasData( msg.Cc ) ) headers[ "Cc" ] = msg.Cc;
if( HasData( msg.Bcc ) ) headers[ "Bcc" ] = msg.Bcc;
if( HasData( msg.Subject ) ) headers[ "Subject" ] = msg.Subject;
if( HasData( msg.UrlContentBase ) )
headers[ "Content-Base" ] = msg.UrlContentBase;
if( HasData( msg.UrlContentLocation ) )
headers[ "Content-Location" ] = msg.UrlContentLocation;
// set body the content type
switch( msg.BodyFormat ) {
case MailFormat.Html:
headers[ "Content-Type" ] = "text/html";
break;
case MailFormat.Text:
headers[ "Content-Type" ] = "text/plain";
break;
default:
headers[ "Content-Type" ] = "text/plain";
break;
}
// set the priority as in the same way as .NET sdk does
switch( msg.Priority ) {
case MailPriority.High:
headers[ "Importance" ] = "high";
break;
case MailPriority.Low:
headers[ "Importance" ] = "low";
break;
case MailPriority.Normal:
headers[ "Importance" ] = "normal";
break;
default:
headers[ "Importance" ] = "normal";
break;
}
// .NET sdk allways sets this to normal
headers[ "Priority" ] = "normal";
// add mime version
headers[ "Mime-Version" ] = "1.0";
// set the mailer -- should probably be changed
headers[ "X-Mailer" ] = "Mono (System.Web.Mail.SmtpMail.Send)";
// add the custom headers they will overwrite
// the earlier ones if they are the same
foreach( string key in msg.Headers.Keys )
headers[ key ] = (string)msg.Headers[ key ];
return headers;
}
// returns true if str is not null and not
// empty
private bool HasData( string str ) {
bool hasData = false;
if( str != null ) {
if( str.Length > 0 ) {
hasData = true;
}
}
return hasData;
}
// send quit command and
// closes the connection
public void Close() {
smtp.WriteQuit();
tcpConnection.Close();
}
}
}
// SmtpResponse.cs
// author: Per Arneng <[EMAIL PROTECTED]>
using System;
namespace SmtpClient {
/// this class represents the response from the smtp server
public class SmtpResponse {
private string rawResponse;
private int statusCode;
private string[] parts;
/// use the Parse method to create instances
protected SmtpResponse() {}
/// the smtp status code FIXME: change to Enumeration?
public int StatusCode {
get { return statusCode; }
set { statusCode = value; }
}
/// the response as it was recieved
public string RawResponse {
get { return rawResponse; }
set { rawResponse = value; }
}
/// the response as parts where ; was used as delimiter
public string[] Parts {
get { return parts; }
set { parts = value; }
}
/// parses a new response object from a response string
public static SmtpResponse Parse( string line ) {
SmtpResponse response = new SmtpResponse();
if( line == null )
throw new ArgumentNullException( "Null is not allowed " +
"as a response string.");
if( line.Length < 4 )
throw new FormatException( "Response is to short " +
line.Length + ".");
if( line[ 3 ] != ' ' )
throw new FormatException( "Response format is wrong.");
// parse the response code
response.StatusCode = Int32.Parse( line.Substring( 0 , 3 ) );
// set the rawsponse
response.RawResponse = line;
// set the response parts
response.Parts = line.Substring( 0 , 3 ).Split( ';' );
return response;
}
}
}
// SmtpStream.cs
// author: Per Arneng <[EMAIL PROTECTED]>
using System;
using System.IO;
using System.Collections;
using System.Text;
using System.Security.Cryptography;
namespace SmtpClient {
public class SmtpStream {
protected Stream stream;
protected Encoding encoding;
protected SmtpResponse lastResponse;
protected string command = "";
public SmtpStream( Stream stream ) {
this.stream = stream;
encoding = new ASCIIEncoding();
}
public SmtpResponse LastResponse {
get { return lastResponse; }
}
public void WriteRset() {
command = "RSET";
WriteLine( command );
ReadResponse();
CheckForStatusCode( 250 );
}
public void WriteHelo( string hostName ) {
command = "HELO " + hostName;
WriteLine( command );
ReadResponse();
CheckForStatusCode( 250 );
}
public void WriteMailFrom( string from ) {
command = "MAIL FROM: " + from;
WriteLine( command );
ReadResponse();
CheckForStatusCode( 250 );
}
public void WriteRcptTo( string to ) {
command = "RCPT TO: " + to;
WriteLine( command );
ReadResponse();
CheckForStatusCode( 250 );
}
public void WriteData() {
command = "DATA";
WriteLine( command );
ReadResponse();
CheckForStatusCode( 354 );
}
public void WriteQuit() {
command = "QUIT";
WriteLine( command );
ReadResponse();
CheckForStatusCode( 221 );
}
public void WriteBoundary( string boundary ) {
WriteLine( "--{0}" , boundary );
}
public void WriteFinalBoundary( string boundary ) {
WriteLine( "--{0}--" , boundary );
}
public void WriteDataEndTag() {
command = ".";
WriteLine( command );
ReadResponse();
CheckForStatusCode( 250 );
}
public void WriteHeaders( IDictionary headers ) {
// write the headers
foreach( string key in headers.Keys )
WriteLine( "{0}: {1}" , key , (string)headers[ key ] );
// write the header end tag
WriteLine( "" );
}
public void CheckForStatusCode( int statusCode ) {
if( LastResponse.StatusCode != statusCode ) {
string msg = "" +
"Server reponse: '" + lastResponse.RawResponse + "';" +
"Status code: '" + lastResponse.StatusCode + "';" +
"Expected status code: '" + statusCode + "';" +
"Last command: '" + command + "'";
throw new SmtpException( msg );
}
}
// writes a formatted line to the server
public void WriteLine( string format , params object[] args ) {
WriteLine( String.Format( format , args ) );
}
// writes a line to the server
public void WriteLine( string line ) {
byte[] buffer = encoding.GetBytes( line + "\r\n" );
stream.Write( buffer , 0 , buffer.Length );
#if DEBUG
DebugPrint( line );
#endif
}
// read a line from the server
public void ReadResponse( ) {
string line = null;
byte[] buffer = new byte[ 4096 ];
int readLength = stream.Read( buffer , 0 , buffer.Length );
if( readLength > 0 ) {
line = encoding.GetString( buffer , 0 , readLength );
line = line.TrimEnd( new Char[] { '\r' , '\n' , ' ' } );
}
// parse the line to the lastResponse object
lastResponse = SmtpResponse.Parse( line );
#if DEBUG
DebugPrint( line );
#endif
}
// reads bytes from a stream and writes the encoded
// as base64 encoded characters. ( 60 chars on each row)
public void WriteBase64( Stream inStream ) {
ICryptoTransform base64 = new ToBase64Transform();
ASCIIEncoding encoding = new ASCIIEncoding();
// the buffers
byte[] plainText = new byte[ base64.InputBlockSize ];
byte[] cipherText = new byte[ base64.OutputBlockSize ];
int readLength = 0;
int trLength = 0;
StringBuilder row = new StringBuilder( 60 );
// read through the stream until there
// are no more bytes left
while( true ) {
readLength = inStream.Read( plainText , 0 , plainText.Length );
// break when there is no more data
if( readLength < 1 ) break;
// transfrom and write the blocks. If the block size
// is less than the InputBlockSize then write the final block
if( readLength == plainText.Length ) {
trLength = base64.TransformBlock( plainText , 0 ,
plainText.Length ,
cipherText , 0 );
// trLength must be the same size as the cipherText
// length otherwise something is wrong
if( trLength != cipherText.Length )
throw new Exception( "All of the plaintext bytes where not converted" );
// convert the bytes to a string and then add it to the
// current row
string cipherString = encoding.GetString( cipherText , 0 , trLength );
row.Append( cipherString );
// when a row is full write it and begin
// on the next row
if( row.Length == 60 ) {
WriteLine( row.ToString() );
row.Remove( 0 , 60 );
}
} else {
// convert the final blocks of bytes
cipherText = base64.TransformFinalBlock( plainText , 0 , readLength );
// convert the bytes to a string and then write it
string cipherString = encoding.GetString( cipherText , 0 ,
cipherText.Length );
row.Append( cipherString );
WriteLine( row.ToString() );
}
}
}
/// debug printing
private void DebugPrint( string line ) {
Console.WriteLine( "smtp: {0}" , line );
}
}
}