Handling basic NNTP commands





4.00/5 (7 votes)
Aug 6, 2003
4 min read

50092

404
A C# class to wrap around some of the basic NNTP commands.
Introduction
This article describes a component that supports operations such as connecting to a news server, retrieving a list of newsgroups, and retrieving message headers from newsgroups using the Network News Transfer Protocol (NNTP).
Background
My friends and I have newsgroups on several different newsgroup servers. From time to time (especially at the end of each month), we would like to find out how many people have been posting in our newsgroups and how many messages did each of them post. We were looking at a simple program that can poll the newsgroup we specified, collect information on messages available and generate some report or table. Finally, I've decided to write the program myself and the very first thing that I ran into is the NNTP.
Using the code
There is another article on Code Project by Ly Hoang Hai on retrieving newsgroups from a news server. Since the protocol is the same, I am skipping some duplicate material such as how to send commands to the server via network stream, which has already been covered by him. What is different is that I packaged the functionalities I need into a NNTP
class that inherits from the Component
class. By doing so, I could simply drag the NNTP component from the toolbox and reuse it in my later projects. The NNTP
class also publishes a few events to notify others of the connection/header retrieval process. The NNTP
class and the Newsgroup
class (which contains information about the current selected newsgroup) has the following fields:
public class NNTP : System.ComponentModel.Component
{
// TCP Connection to the Newsgroup Server
protected TcpClient tcpServer;
// Name for the current connected news server
private string serverName = string.Empty;
// Boolean variable indicating whether the connection is active
private bool isConnected = false;
// Number of Millisecond for server timeout, default = 30 sec
private int timeout = 30000;
// whether posting to the news server is allowed
private bool isPostingAllowed = false;
// Current selected newsgroup
protected Newsgroup newsgroup;
// whether events are fired
private bool enableEvents = true;
...
}
public class Newsgroup
{
// Name of the Newsgroup
private string groupName;
// Estimated Number of posts in the newsgroup
private int numberPosts;
// Article numbers of the first available article
private int lowWatermark;
// Article numbers of the last available article
private int highWatermark;
...
}
Each of the private fields in each class has a public property corresponding to them.
Many of the functions of the NNTP
class such as Connect()
and ListNewsgroup()
are self-explanatory. Rather then going through each of them, I would like to draw your attention to the DownloadHeaders
function that retrieves message headers from the servers. First, a simple data structure, ArticleHeader
, is needed to hold the retrieved information. The following code is part of its definition. Notice that some of the information retrieved from the server such as Message-ID and References header are not included. You can modify this class and the corresponding sections in DownloadHeaders
to suit your project needs.
public class ArticleHeader
{
protected string articleID; // Article ID of the newsgroup message
protected string from; // Sender of the message
protected string subject; // Subject of the message
protected string dateString; // Date the message was sent.
...
}
The private version of DownloadHeaders
is listed below. In the real world, you only have to call one of its public version and pass to the function, the range of article IDs that you would like to retrieve. To retrieve message headers from the newsgroup, I used the XOVER
command in the function. This will just give me the header information, not the whole article, but that's exactly what I need. Here comes the tricky part. If you look at the source code of the function, you will notice that the DownloadHeaders
function only returns a value indicating whether the retrieval succeeded (int.Max
), failed (-1), or stalled due to a null response. The actual header information is returned to the caller one by one through the OnDownLoadHeaderOne
event. Therefore, you have to have an event handler attached to this event to get the headers.
private int DownloadHeaders(string XOVERCommand)
{
// Setup the network stream
NetworkStream ns = this.tcpServer.GetStream();
StreamReader sr = new StreamReader(ns, System.Text.Encoding.Default);
StreamWriter sw = new StreamWriter(ns);
sw.AutoFlush = true;
sw.WriteLine(XOVERCommand); // Execute the XOVER command
string response = sr.ReadLine(); // Retrieve Response
int headerCount = 0;
if ( response == null ) // NULL response
{
if ( this.OnDownloadHeaderFailed != null && this.enableEvents )
this.OnDownloadHeaderFailed( this, new EventArgs() );
return headerCount;
}
switch( response.Substring(0,3) )
{
case "224": // Valid command, article list to follow
bool done = false;
string article;
do
{
article = sr.ReadLine(); // Read the Header of the article
if ( article == null ) // NULL response
{
if ( this.OnDownloadHeaderFailed != null
&& this.enableEvents )
this.OnDownloadHeaderFailed
( this, new EventArgs() );
return headerCount;
}
if ( article == "." ) // Check for finish
done = true;
else
{
// take care of empty fields
article.Replace("\t\t", "\t<empty>\t");
// break up the server response
string[] fields = article.Split( '\t' );
// fill in header
ArticleHeader h = new ArticleHeader();
h.ArticleID = fields[0];
h.Subject = Utils.DecodeHeaderString(fields[1]);
h.From = Utils.DecodeHeaderString(fields[2]);
h.DateString = fields[3];
headerCount++;
if ( this.enableEvents &&
this.OnDownLoadHeaderOne != null )
this.OnDownLoadHeaderOne(this, h);
}
} while (!done);
if ( this.OnDownloadHeaderSucceeded != null
&& this.enableEvents )
this.OnDownloadHeaderSucceeded
( this, new EventArgs() );
headerCount = int.MaxValue;
break;
case "412": // 412 No newsgroup selected
case "420": // 420 Current article number is invalid
case "423": // 423 No articles in that range
if (this.OnDownloadHeaderFailed != null
&& this.enableEvents )
this.OnDownloadHeaderFailed
( this, new EventArgs() );
headerCount = -1;
break;
default:
throw new Exception
("XOVER : Unexpected response from server. Received: "
+ response);
}
return headerCount;
}
Reminder: If you modify the ArticleHeader
class shown earlier, you will also have to modify this function to match your changes.
The NNTP
also publishes quite a few number of events such as OnConnectStart
and OnListNewsgroupDone
. By attaching different event handlers to these events, you would be able to let the user know exactly what is going on with the undergoing connection/retrieval operation.
Tips
2 tips I learned from my later projects. First, be careful of what you ask for, when calling DownloadHeaders()
with no argument. Since the function will then attempt to retrieve all the available headers from the server, it might take some time for the process to finish depending on the number of messages and your connection speed. Also check the return value to see if your StreamReader
read a null value. If so, you can use ReconnectNewsgroup()
to reestablish the connection to the newsgroup.
Second, you can setup a nice progress bar to indicate the retrieval process. You already know how many messages you are retrieving. All you have to do next is increment the progress bar's value in the event handler of the OnDownLoadHeaderOne
event. This way, your user will know that something is indeed going on.
About the demo
The demo shows this NNTP component in action. You enter a news server, hit Connect, select a newsgroup and click Retrieve. The result is that you get the 5 most recent messages available on the newsgroup you specified. If you get less than 5, it may be because some of the messages have been removed from the newsgroup. If they aren't there, we can't retrieve them.