ftp.nice.ch/pub/next/connectivity/news/Alexandra.0.82.s.tar.gz#/alex8/NNTP.m

This is NNTP.m in view mode; [Download] [Up]

#import "mapfile.h"
#import "NNTP.h"
#import "next2iso.tab"
#import "iso2next.tab"
#import "Alexandra.h"

#import <string.h>
#import <stdlib.h>
#import <sys/file.h>
#import <sys/uio.h>
#import <sys/types.h>
#import <sys/socket.h>
#import <netdb.h>
#import <netinet/in.h>
#import <time.h>
#import <sys/time.h>
#import <pwd.h>

#import "readline.h"
#import "response_codes.h"
#import "descriptors.h"
#import "headerfields.h"

#import <objc/Storage.h>
#import <misckit/MiscAppDefaults.h>
#include "instr.h"

#define NNTP_LIST_END(s)  ((s)[0]=='.' && ((s)[1]=='\0' || (s)[1]=='\r' || (s)[1]==' '))

@implementation NNTP

int makeTimeTag(char *aTimetag)
{
   struct timeval tp;
   struct timezone tzp;
   struct tm *tv;

   gettimeofday(&tp, &tzp);
   tv = gmtime(&tp.tv_sec);
	
   sprintf(aTimetag,"%02d%02d%02d %02d%02d%02d GMT", tv->tm_year, tv->tm_mon+1, tv->tm_mday, tv->tm_hour,tv->tm_min,tv->tm_sec); 

   return 0;
}


- init
{
    [super init];
    nntpHostName =NULL;
    handling_timeout=FALSE;
    canPost=FALSE;
    statusLine=NULL;

    return self;
}

- writeTimeTagToDefaultDB
{
   char timetag[20];
   char *buf;

   if(nntpHostName!=NULL){
      makeTimeTag(timetag);
      buf= (char *)malloc((strlen(nntpHostName)+15)*sizeof(char));
      sprintf(buf,"NewGroups %s",nntpHostName);
      [NXApp setDefault:buf to:timetag];
      free(buf);
   }
   
   return self;
}

- (const char *)timeTag
{
   char *buf1;
   const char *buf2; 

   buf1=(char *)malloc((strlen(nntpHostName)+15)*sizeof(char));
   sprintf(buf1,"NewGroups %s",nntpHostName);
   buf2=[NXApp defaultValue:buf1];
   free(buf1);
   
   return buf2;
}

- openServerNamed:(const char *)serverName
{
    struct servent	*nntpEnt;
    struct protoent	*nntpProtoEnt;
    struct hostent	*nntpHost;
    struct sockaddr_in	nntpServer;
    int 		statusCode;
    char 		inCodeText[BUFFER_SIZE];
    BOOL notUseNov;
    int stype;
	 char *xuser,*xpasswd;
	 const char *buf;
	 
    if(nntpHostName!=NULL){
	    if(serverName!=nntpHostName){
		    free(nntpHostName);
		 	 nntpHostName = NXCopyStringBuffer(serverName);
		 }
	 }
    else
	 	 nntpHostName = NXCopyStringBuffer(serverName);

    // store nntp hostname for reconnecting	
    
    if ((nntpEnt = getservbyname("nntp", "tcp")) == NULL) {
	 NXRunAlertPanel("ALEXANDRA","Cannot find nntp service in 'services' database.",NULL,NULL,NULL);
      return nil;
    }

    if ((nntpProtoEnt = getprotobyname(nntpEnt->s_proto)) == NULL) {
	NXRunAlertPanel("ALEXANDRA","Cannot lookup protocol type.",NULL,NULL,NULL);
	return nil;
    }

    if ((readSocket = socket(AF_INET, SOCK_STREAM, 
                             nntpProtoEnt->p_proto))== -1) {
	NXRunAlertPanel("ALEXANDRA","Cannot create socket to news server.",NULL,NULL,NULL);
	return nil;
    }

    if ((nntpHost = gethostbyname((char *)serverName)) == NULL) {
	NXRunAlertPanel("ALEXANDRA","Cannot find address of host %s.",NULL,NULL,NULL, serverName);
    return nil;
    }

  nntpServer.sin_family = nntpHost->h_addrtype;
  bcopy(nntpHost->h_addr, &nntpServer.sin_addr, nntpHost->h_length);
  nntpServer.sin_port = nntpEnt->s_port;
  if ((connect(readSocket, (struct sockaddr *) &nntpServer,
	sizeof(nntpServer))) == -1) {
    NXRunAlertPanel("ALEXANDRA","Cannot connect to news server on %s.",NULL,NULL,NULL, serverName);
    return nil;
  }
  
  writeSocket=dup(readSocket);
  
  nntpIn = fdopen(readSocket, "r");
  nntpOut = fdopen(writeSocket,"w");

  if(fgets(inCodeText,sizeof(inCodeText),nntpIn)==NULL){
     NXRunAlertPanel("ALEXANDRA","Unable to open server socket.",NULL,NULL,NULL);
     return nil;
  }

  statusCode=atoi(inCodeText);
  switch (statusCode) {
    case OK_CANPOST:
    case OK_NOPOST:
      canPost = (statusCode == OK_CANPOST);
		if(!canPost)
			NXLogError("You are not allowed to post");
      break;
    default:
      NXRunAlertPanel("ALEXANDRA","News server on %s responded incorrectly.",NULL,NULL,NULL, serverName);
      return nil;
      break;
  }
  
  echoSocket=[NXApp defaultBoolValue:"EchoSocket"];

  [self issueCommand:"mode reader"];
  
  statusCode=[self issueCommand:"xover"];
  if(statusCode==-1) return self;
  novSupported=(statusCode==ERR_NCING);
  sprintf(inCodeText,"DoNotUseNov %s",nntpHostName);
  notUseNov=[NXApp defaultBoolValue:inCodeText];
  if((notUseNov==YES)||(novSupported==FALSE))
      novSupported=FALSE;
   else
      novSupported=TRUE;

  sprintf(inCodeText,"DoNotPrefetchFrom %s",nntpHostName);
  doNotPrefetchFROM=[NXApp defaultBoolValue:inCodeText];
  sprintf(inCodeText,"DoNotPrefetchMsgid %s",nntpHostName);
  doNotPrefetchMSGID=[NXApp defaultBoolValue:inCodeText];
  sprintf(inCodeText,"DoNotPrefetchRefs %s",nntpHostName);
  doNotPrefetchREFS=[NXApp defaultBoolValue:inCodeText];
  sprintf(inCodeText,"DoNotPrefetchDate %s",nntpHostName);
  doNotPrefetchDATE=[NXApp defaultBoolValue:inCodeText];
  sprintf(inCodeText,"DoNotPrefetchLines %s",nntpHostName);
  doNotPrefetchLINES=[NXApp defaultBoolValue:inCodeText];

  if(!novSupported){ 
     sprintf(inCodeText,"SortType %s",nntpHostName);
     stype=[NXApp defaultIntValue:inCodeText];
     if(((stype==SORT_BY_DATE)&&doNotPrefetchDATE)||
        ((stype==SORT_BY_REAL_NAME)&&doNotPrefetchFROM)){
           sprintf(inCodeText,"SortType %s",nntpHostName);
           [NXApp setDefault:inCodeText toInt:SORT_BY_NUMBER];
     }
  }

	//AUTHENTICATION
	sprintf(inCodeText,"Authuser %s",serverName);
	buf=[NXApp defaultValue:inCodeText];
	if(!buf)
		buf="";
	xuser=NXCopyStringBuffer(buf);
	
	sprintf(inCodeText,"Authpasswd %s",serverName);
	buf=[NXApp defaultValue:inCodeText];
	if(!buf)
		buf="";
	xpasswd=NXCopyStringBuffer(buf);
	
	if(*xuser && *xpasswd){
		sprintf(inCodeText,"authinfo user %s",xuser);	
		statusCode=[self issueCommand: inCodeText];	
		if(statusCode==NEED_AUTHDATA){
			sprintf(inCodeText,"authinfo pass %s",xpasswd);
			statusCode=[self issueCommand:inCodeText];
			if(statusCode!=OK_AUTH) 
				NXRunAlertPanel("ALEXANDRA","NNTP command failed (status %d). Authorization user %s/pass %s rejected. Access to host %s, if allowed, may be limited", NULL,
						NULL,NULL, statusCode, xuser, xpasswd, nntpHostName);
		} 
		else{
			NXRunAlertPanel("ALEXANDRA","NNTP command failed (status %d). Auth for user %s rejected. Access to host %s, if allowed, may be limited", 
										NULL,NULL,NULL, statusCode, xuser, nntpHostName);

		}
	}
  
  free(xuser);
  free(xpasswd);
  
  return self;
}

- (const char *)serverName
{
   return nntpHostName;
}

- reconnectServer
{	 
    if(nntpIn!=NULL) fclose(nntpIn);
    if(nntpOut!=NULL) fclose(nntpOut);

    if (nntpHostName == NULL) {
	return nil;
    }
    return [self openServerNamed:nntpHostName];
}


- free
{
    if(nntpOut!=NULL){
       if(fprintf(nntpOut,"quit\r\n")==0)
          fflush(nntpOut);
       fclose(nntpOut);
    }
    if(nntpIn!=NULL) fclose(nntpIn);
    free(nntpHostName);
    if(statusLine!=NULL)
       free(statusLine);
    return [super free];
}

- (BOOL)canPost
{
   return canPost;
}

- (BOOL)usesNov
{
   return novSupported;
}

- closeServerDelayed
{
   [mainWindowController perform:@selector(freeAndClose:) with:self afterDelay:0.0 cancelPrevious:FALSE];
   fclose(nntpIn);
   fclose(nntpOut);
   return self;
}

- (char *)getNNTPLine
{
   char *buffer;
   int n;

   buffer=readline(nntpIn);
   if(buffer==NULL){		 
       [self closeServerDelayed];
		 EM_ERROR(ENNTPUnexpectedSocketClose,NULL,NULL);
       return NULL;
    }
    n = strlen(buffer);
	 if(echoSocket)
	 	printf("%s",buffer);
    if (n >= 2 && buffer[n-1] == '\n' && buffer[n-2] == '\r')
	buffer[n-2]='\0';

    return buffer;
}

- (int)getStatus
{
    const char *buf=[self getNNTPLine];
	 
    if(statusLine!=NULL)
       free(statusLine);
	 
    statusLine=NXCopyStringBuffer(buf);
    return atoi(statusLine);
}

- (int)issueCommand:(char *)command
{  
   int status,newStatus;

   // issue command
   fprintf(nntpOut,"%s\r\n",command);
   fflush(nntpOut);
   if(echoSocket)
		printf("%s\n",command);
		
   // get answer
   status=[self getStatus];

   //recover if timeout
   if((status==ERR_FAULT)&& instr(statusLine,"timeout",FALSE)){
       if(handling_timeout==TRUE){
          [self closeServerDelayed];
			 EM_ERROR(ENNTPCouldNotReconnect,NULL,NULL);
          return -1;  //fatal error: could not reconnect
       }
       handling_timeout = TRUE;

       if([self reconnectServer]!=nil){
          if(strncmp(command,"group",5)!=0)
             if(currentGroup!=nil)
                if([self requestGroup:currentGroup]!=OK_GROUP){
                   [self closeServerDelayed];
						 EM_ERROR(ENNTPCouldNotReconnect,NULL,NULL);
                   return -1; //fatal error: could not sync
                } 
       }
       else{
          [self closeServerDelayed];
			 EM_ERROR(ENNTPCouldNotReconnect,NULL,NULL);
          return -1; //fatal error: could not reconnect
       }

       newStatus=[self issueCommand:command];
       handling_timeout = FALSE;
       
       return newStatus;
   }

   return status;
}

- (int)requestGroup:(Newsgroup *)aGroup
{
  char	inCodeText[BUFFER_SIZE];
  long	first, last, numArticles;
  int statusCode;

  sprintf(inCodeText, "group %s", [aGroup stringValue]);
  statusCode=[self issueCommand:inCodeText];

  if(statusCode==OK_GROUP){
      sscanf(statusLine, "%*d %ld %ld %ld", &numArticles, &first, &last);
      [[aGroup setMin:first] setMax:last];
      currentGroup=aGroup;
  }
  else if(statusCode==ERR_COMMAND)
     NXLogError("NNTP server does not recognize GROUP command.");
	  
  return statusCode;
}

- unselectCurrentGroup
{
   currentGroup=nil;

   return self;
}

- loadStorageWithGroupList:(Storage *)array
{
  
  char	*inCodeText;
  char group[BUFFER_SIZE];
  newsgroupDesc ngDesc;

  while(((inCodeText=[self getNNTPLine])!=NULL) &&
        (NNTP_LIST_END(inCodeText)==FALSE)){
     sscanf(inCodeText,"%s %ld %ld %c",
            group, &(ngDesc.max), &(ngDesc.min), &(ngDesc.post));
     ngDesc.groupname=NXCopyStringBuffer(group);
     if((ngDesc.post!='y')&&(ngDesc.post!='m')) 
        ngDesc.post='n';
     [array addElement:&ngDesc];
  }

  return self;
}

- scanActive:(Storage *)theArray
{
  int		statusCode;

  [self writeTimeTagToDefaultDB];

  statusCode=[self issueCommand:"list"];

  if (statusCode != OK_GROUPS) {
    EM_ERROR(ENNTPErrorPerformingCommand,"LIST",(void *)atoi(statusLine));
    return self;
  }
  [self loadStorageWithGroupList:theArray];
  return self;
}

- scanNewGroups:(Storage *)theArray
{
  int statusCode;
  char inCodeText[BUFFER_SIZE];

  sprintf(inCodeText,"newgroups %s",[self timeTag]);
  statusCode=[self issueCommand:inCodeText];
  [self writeTimeTagToDefaultDB];

  if (statusCode!= OK_NEWGROUPS){
     [self closeServerDelayed];
	  EM_ERROR(ENNTPErrorPerformingCommand,"NEWGROUPS",(void *)atoi(statusLine));
     return self;
  }
  [self loadStorageWithGroupList:theArray];
  return self;
}

- fetchSubjectHeaders:(Storage *)array from:(long)first to:(long)last
{

   if(novSupported==TRUE)
      [self xover:array from:first to:last];
   else
	   [self xhdr:array from:first to:last];
		
	return self;
}

- xhdr:(Storage *)array from:(long)first to:(long)last
{
   int	statusCode,i;
   char	*inCodeText;
   char	*text,*p;
   char buf[255];
   subjectDesc *defaultDesc;
   subjectDesc *desc;
   BOOL firstHeader=TRUE;
   long number;
   int pos_in_array;

   defaultDesc=(subjectDesc *)calloc(1,sizeof(subjectDesc));
   for(i=0;i<XOVER_COUNT+1;i++){
      if((i==FROM)&&(doNotPrefetchFROM==TRUE)) continue;
      if((i==MSG_ID)&&(doNotPrefetchMSGID==TRUE)) continue;
      if((i==REFS)&&((doNotPrefetchREFS==TRUE)||(doNotPrefetchMSGID==TRUE))) continue;
      if((i==DATE)&&(doNotPrefetchDATE==TRUE)) continue;
      if((i==XOVER_COUNT)&&(doNotPrefetchLINES==TRUE)) continue;

      if(i<XOVER_COUNT)
         sprintf(buf, "xhdr %s %ld-%ld",h_field_name[i], first, last);
      else
         sprintf(buf,"xhdr Lines %ld-%ld",first,last);

      statusCode=[self issueCommand:buf];
      if (statusCode != OK_HEAD){
         if(statusCode == ERR_COMMAND)
				EM_ERROR(ENNTPCommandNotRecognised,"XHDR",NULL);
         else
				EM_ERROR(ENNTPErrorPerformingCommand,"XHDR",(void *)atoi(statusLine));
         return self;
      }
      pos_in_array=0;
      while(((inCodeText=[self getNNTPLine])!=NULL) && (NNTP_LIST_END(inCodeText)==FALSE)){
         sscanf(inCodeText,"%ld",&number);
         text=NXCopyStringBuffer(strchr(inCodeText,' ')+1);
         if(strcmp(text,"(none)")==0)
            text[0]='\0';
         // convert to iso
			p=text;
			while(*p){
				*p=c_iso2next[(unsigned char)(*p)];
				p++;
			}
			
         if(firstHeader==TRUE){
            defaultDesc->number=number;
            defaultDesc->fieldBody=(char **)calloc(XOVER_COUNT,sizeof(char *));
            [array addElement:defaultDesc];
         }
         desc=(subjectDesc *)[array elementAt:(unsigned int)pos_in_array];
         NX_ASSERT(desc!=NULL,"INTERNAL ERROR:XHDR confusion");
         NX_ASSERT(desc->number==number,"INTERNAL ERROR:XHDR mismatch");
         if(i<XOVER_COUNT)
            desc->fieldBody[i]=text;
         else{
            desc->lines=atoi(text);
            free(text);
         }
         pos_in_array++;     
      }
      firstHeader=FALSE;
   }
  free(defaultDesc);
  return self;
}



- xover:(Storage *)array from:(long)first to:(long)last
{
  int	statusCode;
  char	inCodeText[BUFFER_SIZE];
  char  *line_buffer;
  int i,j,a;
  subjectDesc subDesc;

  sprintf(inCodeText, "xover %ld-%ld", first, last);
  statusCode=[self issueCommand:inCodeText];
  if (statusCode != OK_XOVER){
     if(statusCode == ERR_COMMAND)
	     EM_ERROR(ENNTPCommandNotRecognised,"XOVER",NULL);
    else
		  EM_ERROR(ENNTPErrorPerformingCommand,"XOVER",(void *)atoi(statusLine));
    return self;
  }
  
  while(((line_buffer=[self getNNTPLine])!=NULL) &&(NNTP_LIST_END(line_buffer)==FALSE)){
     subDesc.fieldBody=(char **)calloc(XOVER_COUNT,sizeof(char *));
     sscanf(line_buffer,"%ld",&(subDesc.number));
     i=0;j=0;a=0;
     while(j<XOVER_COUNT+2){
        a=i;
        while((line_buffer[i]!='\0')&&(line_buffer[i]!='\t')){
		     char *c=line_buffer+i;
			  *c=c_iso2next[(unsigned char)(*c)];
           i++;
		  }
        if((j>0)&&(j<XOVER_COUNT+1)){
           subDesc.fieldBody[j-1]=NULL;
           if(i>a){
              subDesc.fieldBody[j-1]=(char *)malloc((i-a+1)*sizeof(char));
              strncpy(subDesc.fieldBody[j-1],line_buffer+a,i-a);
              subDesc.fieldBody[j-1][i-a]='\0';
           }
        }
        j++;
        i++;
     }
	  subDesc.artsize=0; subDesc.lines=0;
     sscanf(line_buffer+a,"%d\t%d",&(subDesc.artsize),&(subDesc.lines));
     [array addElement:&subDesc];
  }

  return self;
}

- (int)loadArticleHeader:(Article *)article toString:(char **)aString
{
	int	statusCode;
	char	inCodeText[BUFFER_SIZE];
	long i;
	char *buf,*buf2;
	int len,maxlen;
  	NXStream *theStream;

  //Read HEAD
  sprintf(inCodeText,"head %ld", [article number]);
  statusCode=[self issueCommand:inCodeText];
  if(statusCode==ERR_NOARTIG)
     return statusCode;
  if(statusCode!=OK_HEAD){
	  EM_ERROR(ENNTPErrorPerformingCommand,"HEAD",NULL);
     return statusCode;
  }

  theStream=NXOpenMemory(NULL,0,NX_WRITEONLY);
  if(MapNntpToStream(nntpIn,theStream,echoSocket)==-1){
     [self closeServerDelayed];
     EM_ERROR(ENNTPUnexpectedSocketClose,NULL,NULL);
     return -1;
  }
  
  [article parseHeader:theStream];
  
  NXSeek(theStream,0,NX_FROMEND);
  NXPutc(theStream,(int)'\0');
  // convert to iso
  NXGetMemoryBuffer(theStream,&buf,&len,&maxlen);
  buf2=buf;
  for(i=0;i<len;i++){
     *buf2=(char)c_iso2next[(unsigned char)(*buf2)];
     buf2++;
  }
  
  *aString=NXCopyStringBuffer(buf);
  NXCloseMemory(theStream,NX_FREEBUFFER);
 
  return statusCode;
}

- (int)loadArticleBody:(Article *)article toString:(char **)aString
{
	int	statusCode;
	char	inCodeText[BUFFER_SIZE];
	long i;
	char *buf,*buf2;
	int len,maxlen;
  	NXStream *theStream;

  //Read HEAD
  sprintf(inCodeText,"body %ld", [article number]);
  statusCode=[self issueCommand:inCodeText];
  if(statusCode==ERR_NOARTIG)
     return statusCode;
  if(statusCode!=OK_BODY){
	  EM_ERROR(ENNTPErrorPerformingCommand,"HEAD",NULL);
     return statusCode;
  }

  theStream=NXOpenMemory(NULL,0,NX_WRITEONLY);
  if(MapNntpToStream(nntpIn,theStream,echoSocket)==-1){
     [self closeServerDelayed];
     EM_ERROR(ENNTPUnexpectedSocketClose,NULL,NULL);
     return -1;
  }
  NXSeek(theStream,0,NX_FROMEND);
  NXPutc(theStream,(int)'\0');
  
  // convert to iso
  NXGetMemoryBuffer(theStream,&buf,&len,&maxlen);
  buf2=buf;
  for(i=0;i<len-1;i++){
     *buf2=(char)c_iso2next[(unsigned char)(*buf2)];
     buf2++;
  }
  
  *aString=NXCopyStringBuffer(buf);
  NXCloseMemory(theStream,NX_FREEBUFFER);
  
  return statusCode;
}

- (int)postArticle:(NXStream *)theStream
{
    int statusCode;

    const char	*streambuf;
    char *buf,*copiedbuf;
    int		max, len,i;
    int		bytesSent;

    NXGetMemoryBuffer(theStream, &streambuf, &len, &max);
    buf=(char *)malloc((len+4)*sizeof(char));
    strncpy(buf,streambuf,len);

    statusCode=[self issueCommand:"post"];
    switch (statusCode) {
    case CONT_POST:
        break; 
    case ERR_NOPOST:
        NXRunAlertPanel("ALEXANDRA","NNTP POST command failed.  %d  posting not allowed.",
	NULL,NULL,NULL, statusCode);
        return statusCode;
    default:
        // This should never occur!
        NXRunAlertPanel("ALEXANDRA",
	"NNTP POST command failed. Status code %d.",NULL,NULL,NULL, statusCode);
        return statusCode;
    }

    // Convert to iso
    for(i=0;i<len;i++)
      buf[i]=(char)c_next2iso[(unsigned char)buf[i]];

    // append .
    if(buf[len-1]!='\n'){
       buf[len]='\n';
       len++;
    }
    buf[len]='.';
    buf[len+1]='\n';
    len+=2;
    buf[len]='\0';
	 
    // send
    copiedbuf=buf;
    while (len > 0) {
        bytesSent = send(writeSocket, buf, len, 0);
        buf += bytesSent;
        len -= bytesSent;
    }
	 
    if(echoSocket)
	 	printf("%s",copiedbuf);
		
    free(copiedbuf);

    statusCode=[self getStatus];
    switch (statusCode) {
    case OK_POSTED:
       return statusCode;
    case ERR_POSTFAIL:
       NXRunAlertPanel("ALEXANDRA","%d  posting failed. %s.",
		NULL,NULL,NULL, statusCode,statusLine);
        return statusCode;
    default:
        // This should never occur!
        NXRunAlertPanel("ALEXANDRA","NNTP POST command failed. Status Code %d.",NULL,NULL,NULL, statusCode);
        return statusCode;
    }
}

- (BOOL)slowLink
{
   return isSlowLink;
}

- setSlowLink:(BOOL)v
{
   isSlowLink=v;
   return self;
}

- killFile
{
   return killFile;
}

- (BOOL)doesPrefetchFrom
{
   return (!doNotPrefetchFROM);
}

- (BOOL)doesPrefetchDate
{
   return (!doNotPrefetchDATE);
}

- (BOOL)findArticle:(const char *)msgid inGroups:(char ***)groups
{
	char buf[BUFFER_SIZE];
	int numGroups,status;
	char *buff,*aString;
	
	sprintf(buf,"XHDR Newsgroups %s",msgid);
	status=[self issueCommand:buf];

	if(status==OK_HEAD){
		char *gstring=strchr([self getNNTPLine],' ')+1;
		
		// count the number of groups
		numGroups=0;
		buff=gstring;
		while(*buff!='\r'){
			if(*buff==',')
				numGroups++;
			buff++;
		}
		
		// make list
		*groups=calloc(numGroups+3,sizeof(void *));
		numGroups=0;
		for(aString=strtok(gstring,",");aString;aString=strtok(NULL,",")){
			(*groups)[numGroups]=NXCopyStringBuffer(aString);
			numGroups++;
		}
		
		//remove the rest until .
		[self getNNTPLine];
		
		return TRUE;
	}
	
	return FALSE;
}

@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.