public
void Start()
{
listener.Start();
tListener = new Thread(new ThreadStart(Listen));
tListener.Start();
}
public void Stop()
{
tListener.Abort();
while (tListener.ThreadState != ThreadState.Aborted)
;
listener.Stop();
}
To do this, we just use some quick-n-dirty threading stuff. A cleaner approach can be taken to stop the server more smoothly in order to close communications with the server in a friendly way. However, for demonstration purposes this approach should be okay. Here's the Listen method that acts as the server's background listening thread:
private void Listen()
{
while (true)
{
TcpClient client = listener.AcceptTcpClient();
new Thread(new ParameterizedThreadStart(StartSession)).Start(client);
}
}
Basically, this method sits and wait for incoming client connections (i.e. connections made from the mail client software to localhost:110). When a connection is made, a background session thread is started, passing in the TcpClient object as a parameter through the ParameterizedThreadStart delegate (new in .NET 2.0):
private void StartSession(object state)
{
TcpClient client = state as TcpClient;
if (client != null)
new Pop3Session(client).Start();
}
Time has come to start the real work: the Pop3Session class. I've declared this class as a nested private class inside the Pop3Server class. Let's start by examining the class' skeleton:
private
class Pop3Session
{
private const string server = "foo.mydomain.local";
private const int port = 110;
private ASCIIEncoding encoding = new ASCIIEncoding();
private NetworkStream stream;
private StreamReader sr;
private localhost.Pop3TunnelServiceClient svc;
public Pop3Session(TcpClient client)
{
this.stream = client.GetStream();
this.sr = new StreamReader(stream);
svc = new localhost.Pop3TunnelServiceClient();
}
}
Notice that we've hardcoded the target server and port that we want our WCF service session to connect to. In reality, you'll obtain this information through configuration (app.config, the registry, whatever you want) and maybe pass it to the Pop3Session from the Pop3Server upon creation of a session.
Basically, this class has two faces. The first one is the TCP socket one, supported by the stream member. The second one is the WCF client one, supported by the svc proxy instance. As long as the local POP3 session lasts, the WCF service session keeps alive too.
On to the Start method which contains the main loop that waits for commands sent by the local mail client software to the localhost:110 local POP3 server:
public void Start()
{
string result = svc.Connect(server, port);
Write(result);
for (; ; )
if (!ExecuteCommand(sr.ReadLine()))
break;
sr.Close();
stream.Close();
}
The first thing the Start method does (which is - for the record - called on a separate thread by the Pop3Server's Listen connection listening loop) is connecting to the target server by calling the svc.Connect method. This method starts a WCF session; recall the WCF service contract defined in part 2:
[OperationContract(IsInitiating = true)]
string Connect(string server, int port);
Next, we start to wait for POP3 commands sent by the local mail client. POP3 commands are straightforward to take in because they come in as single-line strings, terminated by a CRLF (\r\n) pair. Therefore, the ReadLine method of the StreamReader attached to the NetworkStream will just do its job fine.
Every command is then forwarded to the ExecuteCommand helper method, defined below:
private
bool ExecuteCommand(string command)
{
if (command == null || command.Length < 3)
{
SendError();
return true;
}
string[] cmds = command.Split(' ');
if (cmds.Length == 0)
{
SendError();
return true;
}
switch (cmds[0].ToUpper())
{
case "USER":
if (cmds.Length != 2)
SendError();
else
Write(svc.User(cmds[1]));
break;
case "PASS":
if (cmds.Length != 2)
SendError();
else
Write(svc.Pass(cmds[1]));
break;
case "STAT":
if (cmds.Length != 1)
SendError();
else
Write(svc.Stat());
break;
case "LIST":
if (cmds.Length > 2)
SendError();
if (cmds.Length == 1)
Write(svc.List());
else
{
uint msg;
if (!uint.TryParse(cmds[1], out msg))
SendError();
else
Write(svc.List2(msg));
}
break;
case "DELE":
if (cmds.Length != 2)
SendError();
else
{
uint msg;
if (!uint.TryParse(cmds[1], out msg))
SendError();
else
Write(svc.Dele(msg));
}
break;
case "RETR":
if (cmds.Length != 2)
SendError();
else
{
uint msg;
if (!uint.TryParse(cmds[1], out msg))
SendError();
else
Write(svc.Retr(msg));
}
break;
case "TOP":
if (cmds.Length != 3)
SendError();
else
{
uint msg;
uint n;
if (!uint.TryParse(cmds[1], out msg) || !uint.TryParse(cmds[2], out n))
SendError();
else
Write(svc.Top(msg, n));
}
break;
case "QUIT":
if (cmds.Length != 1)
SendError();
else
{
Write(svc.Quit());
return false;
}
break;
case "RSET":
if (cmds.Length != 1)
SendError();
else
Write(svc.Rset());
break;
case "UIDL":
if (cmds.Length > 2)
SendError();
if (cmds.Length == 1)
Write(svc.Uidl());
else
{
uint msg;
if (!uint.TryParse(cmds[1], out msg))
SendError();
else
Write(svc.Uidl2(msg));
}
break;
case "APOP":
if (cmds.Length != 3)
SendError();
else
Write(svc.Apop(cmds[1], cmds[2]));
break;
}
return true;
}
Again, I've omitted all Console.Write* stuff that prints diagnostic information on the screen. This ExecuteCommand method should be very easy to understand. It performs some basic parsing of the command that's received by our local POP3 server and calls the appropriate method on the WCF service through the svc proxy instance. Two helper methods are required:
private
void SendError()
{
Write("-ERR Protocol error.");
}
private void Write(string msg)
{
string s = msg + "\r\n";
byte[] b = encoding.GetBytes(s.ToCharArray());
stream.Write(b, 0, b.Length);
}
As you recall from the service contract, every POP3 equivalent web method takes in all of the parameters required for that command and returns the POP3 response string that was obtained by the target POP3 server. Using the Write method this answer is sent back to the local mail client, with the trailing CRLF pair:
Write(svc.User(cmds[1]));
In order to run this client, we need some simple host code (read: a Main method):
class Program
{
static void Main(string[] args)
{
int port = 110;
string url = new BdsSoft.Net.Mail.localhost.Pop3TunnelServiceClient().Endpoint.Address.Uri.ToString();
Console.WriteLine("WCF POP3 Tunnel Client");
Console.WriteLine("======================");
Console.WriteLine();
Console.WriteLine("Initializing local tunnel endpoint...");
Console.WriteLine("- Local POP3 server listening on TCP port {0}", port);
Console.WriteLine("- Forwarding to {0}", url);
Console.WriteLine();
new Pop3Server(port, url).Start();
Console.ReadLine();
}
}
Configuration
Last but not least, our client needs some configuration. The WCF "Add Service Dialog" will have added an app.config file already but it requires some changes, as shown below. These changes are required to deal with large responses, e.g. in response to a RETR POP3 command that retrieves an entire mail message (the default of 16 KB is not sufficient for our purposes). If you decide to implement this stuff for production purposes, you'll want to change the endpoint address to some remote server of course, not just the local machine which we are using for demo purposes (tunnel server and localhost are the same machine).
<?
xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_IPop3TunnelService" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
maxBufferPoolSize="524288" maxReceivedMessageSize="2147483647"
messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
allowCookies="false">
<readerQuotas maxDepth="32" maxStringContentLength="2147483647" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<reliableSession ordered="true" inactivityTimeout="00:10:00"
enabled="false" />
<security mode="Message">
<transport clientCredentialType="Windows" proxyCredentialType="None"
realm="" />
<message clientCredentialType="Windows" negotiateServiceCredential="true"
algorithmSuite="Default" establishSecurityContext="true" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:8080/BdsSoft.Net.Mail/Pop3TunnelService"
binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IPop3TunnelService"
contract="BdsSoft.Net.Mail.localhost.IPop3TunnelService" name="WSHttpBinding_IPop3TunnelService">
<identity>
<userPrincipalName value="something" />
</identity>
</endpoint>
</client>
</system.serviceModel>
</configuration>
Take it to the test
Right, let's try it. Make sure the target server and port are set correctly (i.e. to some POP3 server you have a mailbox on) in the Pop3Session class:
private const string server = "foo.mydomain.local";
private const int port = 110;
Next, make sure the WCF service from part 2 is up and running. When the server-side has started, you can launch the client-side tool we've been creating in this episode:

Now configure your mail client software to connect to the local POP3 server (you can set the SMTP server to whatever server you want, we won't send mail for now):

Tip: For testing purposes, I recommend to check the "Leave a copy of messages on server" checkbox, to avoid loosing mail.

You should see some result like this if you add some logging code to the client and server applications (note: the code that's available for download has this kind of logging code):

Download the code
You can download the code for the client-side implementation of the POP3 server and WCF client over here. Make sure to check out part 1 and part 2 of this series to download the associated server-side code.
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.
Conclusion
Using WCF it's fairly easy to create a HTTP/SOAP tunnel for an older protocol such as POP3. It won't be difficult at all to tunnel protocols like SMTP and IMAP4 too, although every protocol has its own characteristics and implementation complexities. Luckily, POP3 is a pretty simple protocol that allowed me to illustrate you the principle of all this beauty with a relatively small amount of code.
Time to tunnel?