Wednesday, September 13, 2006 2:24 AM
bart
Creating a WCF POP3 tunnel - Part 1 - A simple POP3 client in C#
Introduction
In my previous post you learned about this POP3 tunneling idea using WCF. On to the real stuff now, the implementation. In this first implementation-related post, I'm showing you how to create a simple POP3 client in C# based on the POP3 RFC 1939 specification. Others have done this before - I know - but this implementation ...
- resolves a couple of restrictions other implementations have (such as response length limitations);
- lives very close to the POP3 specification and doesn't provide unnecessary abstractions and encapsulations;
- allows for a service-oriented session-based approach using WCF (see later posts).
Where are we?
The diagram below shows the piece of the solution we'll be focusing on in this post:
********************LOCALHOST*******************
* *
* +-------------+ +-------------+ *
* | Mail client | -(TCP:110)-> | POP3 server | *
* +-------------+ +------+------+ *
* | *
* +------+------+ *
* | WCF client | *
* +------+------+ *
* | *
**************************************|*********
|
(HTTP/SOAP:80)
|
******************TUNNEL SERVER*******|*********
* | *
* +--------------+ +------+------+ *
* | POP3 client +-----<==-----+ WCF server | *
* +------+-------+ +-------------+ *
* | *
*********|**************************************
|
(TCP:110)
|
*********|********TARGET SERVER*****************
* | *
* +------+-------+ *
* | POP3 server + *
* +--------------+ *
* *
************************************************
Legend:
Gray - External components
Blue - Implemented in previous posts
Red - Covered in this post
Green - To be covered in further posts
RFC 1939 in simple words
RFC specifications look painful to dive into but for this particular one about the POP3 protocol things are quite easy to understand. Some background information:
- POP3 stands for Post Office Protocol version 3.0 and was submitted to IETF in May 1996. The protocol allows to retrieve mail from a user's mailbox on a mailserver.
By default, POP3 servers listen on port TCP:110.
The basic operation consists of:
- connecting to the server and waiting for a response from the server;
- an authentication phase using two commands (USER and PASS);
- a series of operational commands to obtain statistics, retrieve messages, query administrative data and delete messages;
- disconnecting from the server after sending a QUIT command.
POP3 commands are case-insensitive and are terminated by a CRLF (carriage return line feed) ASCII pair. All communications are in ASCII.
A POP3 server response is and ASCII string. Some commands return a single line (terminated by CRLF), whileas others return multiple lines (each one terminated by CRLF; the entire message terminated by a CRLF.CRLF series).
There are a couple of states: the authorization state (commands USER, PASS, APOP), the transactional state (commands STAT, LIST, RETR, DELE, NOOP, RSET, TOP, UIDL) and the update state (after QUIT).
The implementation - Pop3Client
Our goal is to provide a simple class that allows for POP3 communication (command/response pairs). Therefore, we won't create encapsulations for a retrieved message or perform advanced parsing on the response.
We define our Pop3Client class as follows:
/// <summary>
/// Simple POP3 client implementation on the RFC 1939 commands level.
/// </summary>
public class Pop3Client : IDisposable
{
...
}
Notice the use of the IDisposable pattern which allows consumers of the class to write this:
using
(Pop3Client clnt = new Pop3Client("pop3.mydomain.local"))
{
//use the POP3 client
}
On to the constructors:
///
<summary>
/// Creates a POP3 client object ready to connect to the specified server on the default POP3 port 110.
/// </summary>
/// <param name="server">POP3 server to connect to.</param>
public Pop3Client(string server) : this(server, 110)
{
}
///
<summary>
/// Creates a POP3 client object ready to connect to the specified server on the specified port.
/// </summary>
/// <param name="server">POP3 server to connect to.</param>
/// <param name="port">Port to connect on.</param>
public Pop3Client(string server, int port)
{
this.server = server;
this.port = port;
}
A few private members are required:
private NetworkStream stream;
private StreamReader sr;
private string server;
private int port;
private ASCIIEncoding encoding = new ASCIIEncoding();
The stream and sr variables will be initialized by the Connect method:
///
<summary>
/// Establishes a connection with the POP3 server.
/// </summary>
/// <returns>POP3 response from the server.</returns>
public Pop3Response Connect()
{
TcpClient client = new TcpClient(server, port);
stream = client.GetStream();
sr = new StreamReader(stream);
return new Pop3Response(Receive());
}
Time to introduce a couple of helper methods:
///
<summary>
/// Helper method to check the connection state. Throws an InvalidOperationException if the POP3 client isn't connected to the specified server.
/// </summary>
private void CheckState()
{
if (stream == null)
throw new InvalidOperationException("No connection was made to the server.");
}
/// <summary>
/// Sends one line of data to the POP3 server.
/// </summary>
/// <param name="message">Message to be sent to the POP3 server.</param>
private void Send(string message)
{
byte[] b = encoding.GetBytes(message);
stream.Write(b, 0, b.Length);
}
/// <summary>
/// Receives one line of data from the POP3 server.
/// </summary>
/// <returns>One line of data received from the POP3 server.</returns>
/// <remarks>A line of data ends with \r\n.</remarks>
private string Receive()
{
return sr.ReadLine();
}
To have some basic encapsulation of a POP3 server response, we introduce the Pop3Response class:
/// <summary>
/// Represents a POP3 response received from a POP3 server.
/// </summary>
public class Pop3Response
{
private bool success;
private string message;
/// <summary>
/// Creates a POP3 response object based on the server's response message.
/// </summary>
/// <param name="message">POP3 response message received from a POP3 server.</param>
internal Pop3Response(string message)
{
this.success = message.StartsWith("+OK");
this.message = message;
}
/// <summary>
/// Gets the success state of the POP3 request associated with this POP3 response.
/// </summary>
public bool Success
{
get { return success; }
internal set { success = value; }
}
/// <summary>
/// Gets the full text POP3 response message sent by the server.
/// </summary>
public string Message
{
get { return message; }
internal set { message = value; }
}
}
Back to the Pop3Client class for the Close/Dispose stuff:
///
<summary>
/// Closes the connection to the POP3 server.
/// </summary>
public void Close()
{
if (sr != null)
sr.Close();
if (stream != null)
stream.Close();
}
/// <summary>
/// Closes the connection to the POP3 server.
/// </summary>
/// <remarks>Equivalent to Close.</remarks>
public void Dispose()
{
Close();
}
Time to implement the POP3 commands. For each defined POP3 command, we'll define a separate method with the required arguments. All of these methods will return an instance of the Pop3Response class:
public Pop3Response User(string user)
{
return SendMessage("USER " + user);
}
public Pop3Response Pass(string password)
{
return SendMessage("PASS " + password);
}
public Pop3Response Noop()
{
return SendMessage("NOOP");
}
public Pop3Response Rset()
{
return SendMessage("RSET");
}
public Pop3Response Stat()
{
return SendMessage("STAT");
}
public Pop3Response List()
{
return SendMessage2("LIST");
}
public Pop3Response List(uint msg)
{
return SendMessage("LIST " + msg);
}
public Pop3Response Retr(uint msg)
{
return SendMessage2("RETR " + msg);
}
public Pop3Response Dele(uint msg)
{
return SendMessage("DELE " + msg);
}
public Pop3Response Top(uint msg, uint n)
{
return SendMessage2("TOP " + msg + " " + n);
}
public Pop3Response Uidl()
{
return SendMessage2("UIDL");
}
public Pop3Response Uidl(uint msg)
{
return SendMessage("UIDL " + msg);
}
public Pop3Response Apop(string name, string digest)
{
return SendMessage("APOP " + name + " " + digest);
}
public Pop3Response Quit()
{
return SendMessage("QUIT");
}
These methods rely on two helper methods. The first one, SendMessage, is used for single-line response answers and is defined as follows:
///
<summary>
/// Sends a message to the server and receives the server's one-line response message.
/// </summary>
/// <param name="message">Message to be sent to the server (request).</param>
/// <returns>POP3 response from the server.</returns>
/// <remarks>The message will be terminated with \r\n by the method before sending.</remarks>
private Pop3Response SendMessage(string message)
{
CheckState();
Send(message + "\r\n");
return new Pop3Response(Receive());
}
The second one, SendMessage2, is used for multi-line response answers and is slightly more complex:
/// <summary>
/// Sends a message to the server and receives the server's multiline response message.
/// </summary>
/// <param name="message">Message to be sent to the server (request).</param>
/// <returns>POP3 response from the server.</returns>
/// <remarks>The message will be terminated with \r\n by the method before sending.</remarks>
private Pop3Response SendMessage2(string message)
{
CheckState();
Send(message + "\r\n");
StringBuilder sb = new StringBuilder();
string s;
do
{
s = Receive();
sb.Append(s + (s != "." ? "\r\n" : ""));
} while (s != ".");
return new Pop3Response(sb.ToString());
}
Testing the Pop3Client
A simple test for our Pop3Client implementation is displayed below. It assumes you have access to some server with a given user name and password and you have at least one mail message:
string
server = "foo.mydomain.com";
string user = "bart";
string pass = "somesecret";
using (Pop3Client clnt = new Pop3Client(server))
{
Console.WriteLine(clnt.Connect().Message);
Console.WriteLine(clnt.User(user).Message);
Console.WriteLine(clnt.Pass(pass).Message);
Console.WriteLine(clnt.Noop().Message);
Console.WriteLine(clnt.Stat().Message);
Console.WriteLine(clnt.List().Message);
Console.WriteLine(clnt.List(1).Message);
Console.WriteLine(clnt.Uidl().Message);
Console.WriteLine(clnt.Uidl(1).Message);
Console.WriteLine(clnt.Retr(1).Message);
Console.WriteLine(clnt.Dele(1).Message);
Console.WriteLine(clnt.Rset().Message);
Console.WriteLine(clnt.Quit().Message);
}
The output should be something like this:
+OK Microsoft Exchange Server 2003 POP3 server version 6.5.7638.1 (foo.mydomain.local) ready.
+OK
+OK User successfully logged on.
+OK
+OK 3 22103
+OK 3 22103
1 2387
2 15836
3 3880
.
+OK 1 2387
+OK
1 AAwvNMJAAAA8tsJnRVv8gCE5TwfxKUC1
2 AAQwNMJAAAA8tsJnRVv8gCE5TwfxKUC1
3 AAAwNMJAAAA8tsJnRVv8gCE5TwfxKUC1
.
+OK 1 AAwvNMJAAAA8tsJnRVv8gCE5TwfxKUC1
+OK
Received: from smtp.mydomain.local ([123.234.123.234]) by smtp.mydomain.local with Microsoft SMTPSVC(6.0.3790.1830);
Fri, 8 Sep 2006 13:55:28 +0200
Received: from smtp.baldrick.local ([111.222.100.100])
by smtp.mydomain.local (Postfix) with ESMTP id 6E3C6E0202ED
for <bar@mydomain.local>; Fri, 8 Sep 2006 13:57:35 +0200 (CEST)
Received: from [10.0.0.52] (edmund.baldrick.local [111.222.135.246])
by smtp.baldrick.local (Postfix) with ESMTP id 10C08D418A
for <bar@mydomain.local>; Fri, 8 Sep 2006 13:57:28 +0200 (CEST)
Message-ID: <45015AA9.5020202@baldrick.local>
Date: Fri, 08 Sep 2006 13:57:29 +0200
From: Edmund Blackadder <edmund.blackadder@baldrick.local>
User-Agent: Melchett 1.2.3.4
MIME-Version: 1.0
To: Bar at Foo <bar@mydomain.local>
Subject: The quick brown fox jumps over the lazy dog.
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 8bit
Return-Path: edmund.blackadder@baldrick.local
X-OriginalArrivalTime: 08 Sep 2006 11:55:28.0048 (UTC) FILETIME=[AD4B0B00:01C6D33D]
Bart
We're about as similar as two completely dissimilar things in a pod.
Kind regards,
Edmund
.
+OK
+OK
+OK Microsoft Exchange Server 2003 POP3 server version 6.5.7638.1 signing off.
Download the code
You can download the code for this simple command-oriented POP3 client over here.
All usual disclaimers apply. The writer of this blog doesn't make any warranties or guarantuees about the quality of this software, and isn't responsible for possible information loss under any circumstance whatsoever. The code on this blog post and in the download are made available for demonstration purposes only.
Del.icio.us |
Digg It |
Technorati |
Blinklist |
Furl |
reddit |
DotNetKicks
Filed under: .NET Framework 3.0 (WinFX), Windows Communication Foundation (WCF)