[Mono-list] SMTP into CVS

Per Arneng pt99par@student.bth.se
Sun, 23 Feb 2003 01:41:00 +0100


--------------Boundary-00=_CKKQ6KQ4H56G5CMEJPMK
Content-Type: text/plain;
  charset="us-ascii"
Content-Transfer-Encoding: quoted-printable

Hi!

Here are the smtp classes. They have now a namespace that is SmtpClient a=
nd=20
they should probably be in another namespace. I have designed the classes=
 in=20
souch a way so that they will be easy to incorperate in the SmtpMail clas=
s.=20

It would be nice if someone could insert them into the cvs or tell me of =
where=20
to insert them. I think that a new assembly would be good since the class=
es=20
expose more functionallity than the SmtpMail do. SmtpStream ex: provides=20
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 =3D 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=
=20
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
=20
Best regards
   Per Arneng

--------------Boundary-00=_CKKQ6KQ4H56G5CMEJPMK
Content-Type: text/x-makefile;
  charset="us-ascii";
  name="Makefile"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="Makefile"


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)
--------------Boundary-00=_CKKQ6KQ4H56G5CMEJPMK
Content-Type: text/x-c++src;
  charset="us-ascii";
  name="SmtpException.cs"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="SmtpException.cs"

using System.IO;

namespace SmtpClient {

    public class SmtpException : IOException {
	public SmtpException( string message ) : base( message ) {}
    }

}

--------------Boundary-00=_CKKQ6KQ4H56G5CMEJPMK
Content-Type: text/x-c++src;
  charset="us-ascii";
  name="SmtpClient.cs"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="SmtpClient.cs"

// SmtpClient.cs
// author: Per Arneng <pt99par@student.bth.se>
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();
	
	}
	
		
    }

}

--------------Boundary-00=_CKKQ6KQ4H56G5CMEJPMK
Content-Type: text/x-c++src;
  charset="us-ascii";
  name="SmtpResponse.cs"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="SmtpResponse.cs"

// SmtpResponse.cs
// author: Per Arneng <pt99par@student.bth.se>
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;
	}
    }

}

--------------Boundary-00=_CKKQ6KQ4H56G5CMEJPMK
Content-Type: text/x-c++src;
  charset="us-ascii";
  name="SmtpStream.cs"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="SmtpStream.cs"

// SmtpStream.cs
// author: Per Arneng <pt99par@student.bth.se>
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 );
	}

    }


}

--------------Boundary-00=_CKKQ6KQ4H56G5CMEJPMK--