{
  $Project$
  $Workfile$
  $Revision$
  $DateUTC$
  $Id$

  This file is part of the Indy (Internet Direct) project, and is offered
  under the dual-licensing agreement described on the Indy website.
  (http://www.indyproject.org/)

  Copyright:
   (c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
}
{
  $Log$
}
{
  Rev 1.29    3/1/2005 3:35:48 PM  BGooijen
  Auth

  Rev 1.28    1/11/2005 3:09:06 AM  JPMugaas
  Fix.  A NNTP banner should not be obtained after STARTTLS succeded.

  Rev 1.27    10/26/2004 10:33:46 PM  JPMugaas
  Updated refs.

  Rev 1.26    2004.05.20 11:37:02 AM  czhower
  IdStreamVCL

  Rev 1.25    16/05/2004 14:30:42  CCostelloe
  ReceiveHeader checks added in case message has no body

  Rev 1.24    3/7/2004 11:21:50 PM  JPMugaas
  Compiler warnings.

  Rev 1.23    2004.03.06 1:31:46 PM  czhower
  To match Disconnect changes to core.

  Rev 1.22    2004.02.03 5:44:10 PM  czhower
  Name changes

  Rev 1.21    2004.01.28 9:36:32 PM  czhower
  Fixed search and replace error

  Rev 1.20    2004.01.27 1:13:36 PM  czhower
  T --> TId
  var --> out

  Rev 1.19    1/26/2004 1:16:46 PM  JPMugaas
  SSL Reenabled.

  Rev 1.18    2004.01.22 9:28:44 PM  czhower
  DotNetExclude for TLS.

  Rev 1.17    1/21/2004 3:26:50 PM  JPMugaas
  InitComponent

  Rev 1.16    1/5/2004 8:22:18 PM  JMJacobson
  Updated TIdNNTP.GetCapability to handle empty LIST EXTENSIONS response
  (response 215)

  Rev 1.15    11/11/03 11:06:18 AM  RLebeau
  Updated SendCmd() to test for a 281 response when issuing an AUTHINFO USER
  command, as per RFC 2980

  Rev 1.14    2003.10.24 10:33:22 AM  czhower
  Saved first this time.

  Rev 1.12    10/19/2003 5:31:52 PM  DSiders
  Added localization comments.

  Rev 1.11    2003.10.14 9:57:16 PM  czhower
  Compile todos

  Rev 1.10    2003.10.12 4:04:00 PM  czhower
  compile todos

  Rev 1.9    9/10/2003 03:26:12 AM  JPMugaas
  Updated GetArticle(), GetBody(), and GetHeader() to use new
  EnsureMsgIDBrackets() function in IdGlobal.  Checked in on behalf of Remy
  Lebeau

  Rev 1.8    6/9/2003 05:14:58 AM  JPMugaas
  Fixed crical error.
  Supports HDR and OVER commands defined in
  http://www.ietf.org/internet-drafts/draft-ietf-nntpext-base-18.txt if feature
  negotiation indicates that they are supported.
  Added XHDR data parsing routine.
  Added events for when we receive a line of data with XOVER or XHDR as per
  John Jacobson's request.

  Rev 1.7    6/9/2003 01:09:40 AM  JPMugaas
  Host wasn't published when it should have been published.

  Rev 1.6    6/5/2003 04:54:00 AM  JPMugaas
  Reworkings and minor changes for new Reply exception framework.

  Rev 1.5    5/8/2003 11:28:06 AM  JPMugaas
  Moved feature negoation properties down to the ExplicitTLSClient level as
  feature negotiation goes hand in hand with explicit TLS support.

  Rev 1.4    4/5/2003 02:06:20 PM  JPMugaas
  TLS handshake itself can now be handled.

  Rev 1.3    3/27/2003 05:46:36 AM  JPMugaas
  Updated framework with an event if the TLS negotiation command fails.
  Cleaned up some duplicate code in the clients.

  Rev 1.2    3/26/2003 04:18:22 PM  JPMugaas
  Now supports implicit and explicit TLS.

  Rev 1.1    2/24/2003 09:25:16 PM  JPMugaas

  Rev 1.0    11/13/2002 07:57:52 AM  JPMugaas

  2001-Dec - Chad Z. Hower a.k.a. Kudzu
    -Continued modifications

  2001-Oct - Chad Z. Hower a.k.a. Kudzu
    -Massive reworking to fit the Indy 9 model and update a lot of outdated code
     that was left over from Delphi 4 days. Updates now use overloaded functions.
    There were also several problems with message number accounting.

  2000-Jun-23 J. Peter Mugaas
    -GetNewGroupsList, GetNewGroupsList, and GetNewNewsList No longer require
     an Event handler if you provide a TStrings to those procedures
    -ParseXOVER was added so that you could parse XOVER data
    -ParseNewsGroup was ripped from GetNewGroupsList so that newsgroups can
     be parsed while not downloading newsgroups
    -Moved some duplicate code into a separate procedure
    -The IdNNTP now uses the Indy exceptions and IdResourceStrings to facilitate
     internationalization

  2000-Apr-28 Mark L. Holmes
    -Ported to Indy

  2000-Apr-28
    -Final Version

  1999-Dec-29 MTL
    -Moved to new Palette Scheme (Winshoes Servers)
}

unit IdNNTP;

interface

{$i IdCompilerDefines.inc}

uses
  Classes,
  IdAssignedNumbers, IdExplicitTLSClientServerBase, IdException, IdGlobal,
  IdMessage, IdMessageClient, IdReplyRFC, 
  IdTCPServer, IdTCPConnection;

{
  Original Author: Chad Z. Hower a.k.a. Kudzu
  Amended and modified by: AHeid, Mark Holmes
}

type
  // Most users of this component should use "mtReader"
  TIdModeType = (mtStream, mtIHAVE, mtReader);
  TIdNNTPPermission = (crCanPost, crNoPost, crAuthRequired, crTempUnavailable);
  TIdModeSetResult = (mrCanStream, mrNoStream, mrCanIHAVE, mrNoIHAVE, mrCanPost, mrNoPost);
  TIdEventStreaming = procedure (AMesgID: string; var AAccepted: Boolean)of object;
  TIdNewsTransporTIdEvent = procedure (AMsg: TStrings) of object;
   //AMsg can be an index number or a message ID depending upon the parameters of XHDR
  TIdEvenTIdNewsgroupList = procedure(ANewsgroup: string; ALow, AHigh: Int64;
   AType: string; var ACanContinue: Boolean) of object;
  TIdEventXOVER = procedure(AArticleIndex : Int64; ASubject,
     AFrom : String; ADate : TDateTime; AMsgId, AReferences : String; AByteCount,
     ALineCount : Integer; AExtraData : String; var VCanContinue : Boolean) of object;
  TIdEventNewNewsList = procedure(AMsgID: string; var ACanContinue: Boolean) of object;
  TIdEventXHDREntry = procedure(AHeader : String; AMsg, AHeaderData : String; var ACanContinue: Boolean) of object;

  //TODO: Add a TranslateRFC822 Marker - probably need to do it in TCPConnection and modify Capture
  // Better yet, make capture an object
  TIdNNTP = class(TIdMessageClient)
  protected
    FGreetingCode : Integer;
    FMsgHigh: Int64;
    FMsgLow: Int64;
    FMsgCount: Int64;
    FNewsAgent: string;
    FOnNewsgroupList,
    FOnNewGroupsList: TIdEvenTIdNewsgroupList;
    FOnNewNewsList: TIdEventNewNewsList;
    FOnXHDREntry : TIdEventXHDREntry;
    FOnXOVER : TIdEventXOVER;
    FModeType: TIdModeType;
    FModeResult: TIdModeSetResult;
    FPermission: TIdNNTPPermission;
    FForceAuth: boolean;

    FHDRSupported : Boolean;
    FOVERSupported : Boolean;
    //
    procedure AfterConnect;
    procedure GetCapability;
    function ConvertDateTimeDist(ADate: TDateTime; AGMT: boolean;
     ADistributions: string): string;
    function GetSupportsTLS : boolean; override;
    procedure InitComponent; override;
    procedure ProcessGroupList(ACmd: string; AResponse: integer;
     ALisTIdEvent: TIdEvenTIdNewsgroupList);
    procedure XHDRCommon(AHeader, AParam : String);
    procedure XOVERCommon(AParam : String);
    procedure StartTLS;
  public
    procedure Check(AMsgIDs: TStrings; AResponses: TStrings);
    procedure Connect; override;
    destructor Destroy; override;
    procedure DisconnectNotifyPeer; override;
    function GetArticle(AMsg: TIdMessage): Boolean; overload;
    function GetArticle(AMsgNo: Int64; AMsg: TIdMessage): Boolean; overload;
    function GetArticle(AMsgID: string; AMsg: TIdMessage): Boolean; overload;
    function GetArticle(AMsg: TStrings): Boolean; overload;
    function GetArticle(AMsgNo: Int64; AMsg: TStrings): Boolean; overload;
    function GetArticle(AMsgID: string; AMsg: TStrings): Boolean; overload;
    function GetArticle(AMsg: TStream): Boolean; overload;
    function GetArticle(AMsgNo: Int64; AMsg: TStream): Boolean; overload;
    function GetArticle(AMsgID: string; AMsg: TStream): Boolean; overload;
    function GetBody(AMsg: TIdMessage): Boolean; overload;
    function GetBody(AMsgNo: Int64; AMsg: TIdMessage): Boolean; overload;
    function GetBody(AMsgID: string; AMsg: TIdMessage): Boolean; overload;
    function GetBody(AMsg: TStrings): Boolean; overload;
    function GetBody(AMsgNo: Int64; AMsg: TStrings): Boolean; overload;
    function GetBody(AMsgID: string; AMsg: TStrings): Boolean; overload;
    function GetBody(AMsg: TStream): Boolean; overload;
    function GetBody(AMsgNo: Int64; AMsg: TStream): Boolean; overload;
    function GetBody(AMsgID: string; AMsg: TStream): Boolean; overload;
    function GetHeader(AMsg: TIdMessage): Boolean; overload;
    function GetHeader(AMsgNo: Int64; AMsg: TIdMessage): Boolean; overload;
    function GetHeader(AMsgID: string; AMsg: TIdMessage): Boolean; overload;
    function GetHeader(AMsg: TStrings): Boolean; overload;
    function GetHeader(AMsgNo: Int64; AMsg: TStrings): Boolean; overload;
    function GetHeader(AMsgID: string; AMsg: TStrings): Boolean; overload;
    function GetHeader(AMsg: TStream): Boolean; overload;
    function GetHeader(AMsgNo: Int64; AMsg: TStream): Boolean; overload;
    function GetHeader(AMsgID: string; AMsg: TStream): Boolean; overload;
    procedure GetNewsgroupList; overload;
    procedure GetNewsgroupList(AList: TStrings); overload;
    procedure GetNewsgroupList(AStream: TStream); overload;
    procedure GetNewGroupsList(ADate: TDateTime; AGMT: boolean;
     ADistributions: string); overload;
    procedure GetNewGroupsList(ADate: TDateTime; AGMT: boolean;
     ADistributions: string; AList : TStrings); overload;
    procedure GetNewNewsList(ANewsgroups: string;
      ADate: TDateTime; AGMT: boolean; ADistributions: string); overload;
    procedure GetNewNewsList(ANewsgroups: string; ADate: TDateTime;
      AGMT: boolean; ADistributions: string; AList : TStrings); overload;
    procedure GetOverviewFMT(AResponse: TStrings);
    function IsExtCmdSupported(AExtension : String) : Boolean;
    procedure IHAVE(AMsg: TStrings);
    function Next: Boolean;
    function Previous: Boolean;
    procedure ParseXOVER(Aline: String; var AArticleIndex : Int64; var ASubject,
     AFrom : String; var ADate : TDateTime; var AMsgId, AReferences : String; var AByteCount,
     ALineCount : Integer; var AExtraData : String);
    procedure ParseNewsGroup(ALine : String; out ANewsGroup: string; out AHi, ALo : Int64;
     out AStatus : String);
    procedure ParseXHDRLine(ALine : String; out AMsg : String; out AHeaderData : String);
    procedure Post(AMsg: TIdMessage); overload;
    procedure Post(AStream: TStream); overload;
    function SendCmd(AOut: string; const AResponse: array of Int16;
      AEncoding: IIdTextEncoding = nil): Int16; override;
    function SelectArticle(AMsgNo: Int64): Boolean;
    procedure SelectGroup(AGroup: string);
    function TakeThis(AMsgID: string; AMsg: TStream): string;
    procedure XHDR(AHeader: string; AParam: string; AResponse: TStrings); overload;
    procedure XHDR(AHeader: string; AParam: string); overload;
    procedure XOVER(AParam: string; AResponse: TStrings); overload;
    procedure XOVER(AParam: string; AResponse: TStream); overload;
    procedure XOVER(AParam: string); overload;
    procedure SendAuth;
    //
    property ModeResult: TIdModeSetResult read FModeResult write FModeResult;
    property MsgCount: Int64 read FMsgCount;
    property MsgHigh: Int64 read FMsgHigh;
    property MsgLow: Int64 read FMsgLow;
    property Permission: TIdNNTPPermission read FPermission;
  published
    property NewsAgent: string read FNewsAgent write FNewsAgent;
    property Mode: TIdModeType read FModeType write FModeType default mtReader;
    property Password;
    property Username;
    property OnNewsgroupList: TIdEvenTIdNewsgroupList read FOnNewsgroupList write FOnNewsgroupList;
    property OnNewGroupsList: TIdEvenTIdNewsGroupList read FOnNewGroupsList write FOnNewGroupsList;
    property OnNewNewsList: TIdEventNewNewsList read FOnNewNewsList write FOnNewNewsList;
    property OnXHDREntry : TIdEventXHDREntry read FOnXHDREntry write FOnXHDREntry;
    property OnXOVER : TIdEventXOVER read FOnXOVER write FOnXOVER;
    property OnTLSNotAvailable;
    property Port default IdPORT_NNTP;
    property Host;
    property UseTLS;
    property ForceAuth:boolean read FForceAuth write FForceAuth default false;
  end;

  EIdNNTPException = class(EIdException);
  EIdNNTPNoOnNewGroupsList = class(EIdNNTPException);
  EIdNNTPNoOnNewNewsList = class(EIdNNTPException);
  EIdNNTPNoOnNewsgroupList = class(EIdNNTPException);
  EIdNNTPNoOnXHDREntry = class(EIdNNTPException);
  EIdNNTPNoOnXOVER = class(EIdNNTPException);
  EIdNNTPStringListNotInitialized = class(EIdNNTPException);

  EIdNNTPConnectionRefused = class (EIdReplyRFCError);

implementation

uses
  IdComponent,
  IdGlobalProtocols,
  IdResourceStringsProtocols,
  IdSSL, SysUtils;

procedure TIdNNTP.ParseXOVER(Aline : String;
  var AArticleIndex : Int64;
  var ASubject,
      AFrom : String;
  var ADate : TDateTime;
  var AMsgId,
      AReferences : String;
  var AByteCount,
      ALineCount : Integer;
  var AExtraData : String);

begin
  {Strip backspace and tab junk sequences which occur after a tab separator so they don't throw off any code}
  ALine := ReplaceAll(ALine, #9#8#9, #9);
  {Article Index}
  AArticleIndex := IndyStrToInt64(Fetch(ALine, #9), 0);
  {Subject}
  ASubject := Fetch(ALine, #9);
  {From}
  AFrom := Fetch(ALine, #9);
  {Date}
  ADate := GMTToLocalDateTime(Fetch(Aline, #9));
  {Message ID}
  AMsgId := Fetch(Aline, #9);
  {References}
  AReferences := Fetch(ALine, #9);
  {Byte Count}
  AByteCount := IndyStrToInt(Fetch(ALine, #9), 0);
  {Line Count}
  ALineCount := IndyStrToInt(Fetch(ALine, #9), 0);
  {Extra data}
  AExtraData := ALine;
end;

procedure TIdNNTP.ParseNewsGroup(ALine : String; out ANewsGroup : String;
  out AHi, ALo : Int64; out AStatus : String);
begin
  ANewsgroup := Fetch(ALine, ' ');
  AHi := IndyStrToInt64(Fetch(Aline, ' '), 0);
  ALo := IndyStrToInt64(Fetch(ALine, ' '), 0);
  AStatus := ALine;
end;

procedure TIdNNTP.InitComponent;
begin
  inherited InitComponent;
  Mode := mtReader;
  Port := IdPORT_NNTP;
  ForceAuth := false;

  FRegularProtPort := IdPORT_NNTP;
  FImplicitTLSProtPort := IdPORT_SNEWS;
end;

function TIdNNTP.SendCmd(AOut: string; const AResponse: Array of Int16;
  AEncoding: IIdTextEncoding = nil): Int16;
begin
  // NOTE: Responses must be passed as arrays so that the proper inherited SendCmd is called
  // and a stack overflow is not caused.
  Result := inherited SendCmd(AOut, [], AEncoding);
  if (Result = 480) or (Result = 450) then
  begin
    SendAuth;
    Result := inherited SendCmd(AOut, AResponse, AEncoding);
  end else begin
    CheckResponse(Result, AResponse);
  end;
end;

procedure TIdNNTP.Connect;
begin
  inherited Connect;
  try
    FGreetingCode := GetResponse;
    AfterConnect;
    StartTLS;
    if ForceAuth then begin
      SendAuth;
    end;
  except
    Disconnect(False);
    raise;
  end;
end;

{ This procedure gets the overview format as suported by the server }
procedure TIdNNTP.GetOverviewFMT(AResponse: TStrings);
var
  LEncoding: IIdTextEncoding;
begin
  SendCmd('LIST OVERVIEW.FMT', 215);  {do not localize}
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AResponse, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

{ Send the XOVER Command.  XOVER [Range]
  Range can be of the form: Article Number i.e. 1
                            Article Number followed by a dash
                            Article Number followed by a dash and aother number
  Remember to select a group first and to issue a GetOverviewFMT so that you
  can interpret the information sent by the server corectly. }
procedure TIdNNTP.XOVER(AParam: string; AResponse: TStrings);
var
  LEncoding: IIdTextEncoding;
begin
  XOVERCommon(AParam);
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AResponse, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

procedure TIdNNTP.XOVER(AParam: string; AResponse: TStream);
var
  LEncoding: IIdTextEncoding;
begin
  XOVERCommon(AParam);
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AResponse, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

{ Send the XHDR Command.  XHDR Header (Range | Message-ID)
  Range can be of the form: Article Number i.e. 1
                            Article Number followed by a dash
                            Article Number followed by a dash and aother number
  Parm is either the Range or the MessageID of the articles you want. They
  are Mutually Exclusive}
procedure TIdNNTP.XHDR(AHeader: string; AParam: String; AResponse: TStrings);
var
  LEncoding: IIdTextEncoding;
begin
  { This method will send the XHDR command.
  The programmer is responsible for choosing the correct header. Headers
  that should always work as per RFC 1036 are:

      From
      Date
      Newsgroups
      Subject
      Message-ID
      Path

    These Headers may work... They are optional per RFC1036 and new headers can
    be added at any time as server implementation changes

      Reply-To
      Sender
      Followup-To
      Expires
      References
      Control
      Distribution
      Organization
      Keywords
      Summary
      Approved
      Lines
      Xref
    }
  XHDRCommon(AHeader,AParam);
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AResponse, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

procedure TIdNNTP.SelectGroup(AGroup: string);
var
  s: string;
begin
  SendCmd('GROUP ' + AGroup, [211]);  {do not localize}
  s := LastCmdResult.Text[0];
  FMsgCount := IndyStrToInt64(Fetch(s), 0);
  FMsgLow := IndyStrToInt64(Fetch(s), 0);
  FMsgHigh := IndyStrToInt64(Fetch(s), 0);
end;

{ This method will send messages via the IHAVE command.
The IHAVE command first sends the message ID and waits for a response from the
server prior to sending the header and body. This command is of no practical
use for NNTP client readers as readers are generally denied the privelege
to execute the IHAVE command. this is a news transport command. So use this
when you are implementing a NNTP server send unit }

procedure TIdNNTP.IHAVE(AMsg: TStrings);
var
  i     : Integer;
  MsgID : string;
begin
//TODO: Im not sure this fucntion works properly - needs checked
// Why is it not using a TIdMessage?
  // Since we are merely forwarding messages we have already received
  // it is assumed that the required header fields and body are already in place

  // We need to get the message ID from the stringlist because it's required
  // that we send it s part of the IHAVE command
  for i := 0 to AMsg.Count - 1 do
  begin
    if IndyPos('Message-ID', AMsg.Strings[i]) > 0 then begin  {do not localize}
      MsgID := AMsg.Strings[i];
      Fetch(MsgID,':');
      Break;
    end;
  end;
  SendCmd('IHAVE ' + MsgID, 335); {do not localize}
  WriteRFCStrings(AMsg);
  // Why is the response ignored? What is it?
  Readln;
end;

(*
1.1.1  The CHECK command

   CHECK <message-id>

   CHECK is used by a peer to discover if the article with the specified
   message-id should be sent to the server using the TAKETHIS command.
   The peer does not have to wait for a response from the server before
   sending the next command.

   From using the responses to the sequence of CHECK commands, a list of
   articles to be sent can be constructed for subsequent use by the
   TAKETHIS command.

   The use of the CHECK command for streaming is optional.  Some
   implementations will directly use the TAKETHIS command and send all
   articles in the send queue on that peer for the server.

   On some implementations, the use of the CHECK command is not
   permitted when the server is in slave mode (via the SLAVE command).

   Responses that are of the form X3X must specify the message-id in the
   response.

1.1.2.  Responses

      238 no such article found, please send it to me
      400 not accepting articles
      431 try sending it again later
      438 already have it, please don't send it to me
      480 Transfer permission denied
      500 Command not understood
*)
procedure TIdNNTP.Check(AMsgIDs: TStrings; AResponses: TStrings);
var
  i: Integer;
begin
  if not Assigned(AResponses) then begin
    raise EIdNNTPStringListNotInitialized.Create(RSNNTPStringListNotInitialized);
  end;
  for i := 0 to AMsgIDs.Count - 1 do begin
    IOHandler.WriteLn('CHECK '+ AMsgIDs.Strings[i]);  {do not localize}
  end;
  for i := 0 to AMsgIDs.Count - 1 do begin
    AResponses.Add(IOHandler.ReadLn)
  end;
end;

(*
1.3.1  The TAKETHIS command

   TAKETHIS <message-id>

   TAKETHIS is used to send articles to a server when in streaming mode.
   The entire article (header and body, in that sequence) is sent
   immediately after the peer sends the TAKETHIS command.  The peer does
   not have to wait for a response from the server before sending the
   next command and the associated article.

   During transmission of the article, the peer should send the entire
   article, including header and body, in the manner specified for text
   transmission from the server.  See RFC 977, Section 2.4.1 for
   details.

   Responses that are of the form X3X must specify the message-id in the
   response.

1.3.2.  Responses

      239 article transferred ok
      400 not accepting articles
      439 article transfer failed
      480 Transfer permission denied
      500 Command not understood
*)
function TIdNNTP.TakeThis(AMsgID: string; AMsg: TStream): string;
// This message assumes AMsg is "raw" and has already taken care of . to ..
begin
  SendCmd('TAKETHIS ' + AMsgID, 239); {do not localize}
  IOHandler.Write(AMsg);
  IOHandler.WriteLn('.');
end;

(*
3.10.  The POST command

3.10.1.  POST

   POST

   If posting is allowed, response code 340 is returned to indicate that
   the article to be posted should be sent. Response code 440 indicates
   that posting is prohibited for some installation-dependent reason.

   If posting is permitted, the article should be presented in the
   format specified by RFC850, and should include all required header
   lines. After the article's header and body have been completely sent
   by the client to the server, a further response code will be returned
   to indicate success or failure of the posting attempt.

   The text forming the header and body of the message to be posted
   should be sent by the client using the conventions for text received
   from the news server:  A single period (".") on a line indicates the
   end of the text, with lines starting with a period in the original
   text having that period doubled during transmission.

   No attempt shall be made by the server to filter characters, fold or
   limit lines, or otherwise process incoming text.  It is our intent
   that the server just pass the incoming message to be posted to the
   server installation's news posting software, which is separate from
   this specification.  See RFC850 for more details.

   Since most installations will want the client news program to allow
   the user to prepare his message using some sort of text editor, and
   transmit it to the server for posting only after it is composed, the
   client program should take note of the herald message that greeted it
   when the connection was first established. This message indicates
   whether postings from that client are permitted or not, and can be
   used to caution the user that his access is read-only if that is the
   case. This will prevent the user from wasting a good deal of time
   composing a message only to find posting of the message was denied.
   The method and determination of which clients and hosts may post is
   installation dependent and is not covered by this specification.

3.10.2.  Responses

   240 article posted ok
   340 send article to be posted. End with <CR-LF>.<CR-LF>
   440 posting not allowed
   441 posting failed

   (for reference, one of the following codes will be sent upon initial
   connection; the client program should determine whether posting is
   generally permitted from these:) 200 server ready - posting allowed
   201 server ready - no posting allowed
*)
procedure TIdNNTP.Post(AMsg: TIdMessage);
begin
  SendCmd('POST', 340); {do not localize}
  //Header
  if Length(NewsAgent) > 0 then begin
    AMsg.ExtraHeaders.Values['X-Newsreader'] := NewsAgent;  {do not localize}
  end;
  SendMsg(AMsg);
  SendCmd('.', 240);
end;

procedure TIdNNTP.Post(AStream: TStream);
begin
  SendCmd('POST', 340); {do not localize}
  IOHandler.Write(AStream);
  SendCmd('.', 240);
end;

procedure TIdNNTP.ProcessGroupList(ACmd: string; AResponse: integer;
 ALisTIdEvent: TIdEvenTIdNewsgroupList);
var
  s1, sNewsgroup: string;
  lLo, lHi: Int64;
  sStatus: string;
  LCanContinue: Boolean;
begin
  BeginWork(wmRead, 0); try
    SendCmd(ACmd, AResponse);
    s1 := IOHandler.ReadLn;
    LCanContinue := True;
    while (s1 <> '.') and LCanContinue do
    begin
      ParseNewsGroup(s1, sNewsgroup, lHi, lLo, sStatus);
      ALisTIdEvent(sNewsgroup, lLo, lHi, sStatus, LCanContinue);
      s1 := IOHandler.ReadLn;
    end;
  finally
    EndWork(wmRead);
  end;
end;

procedure TIdNNTP.GetNewsgroupList;
begin
  if not Assigned(FOnNewsgroupList) then begin
    raise EIdNNTPNoOnNewsgroupList.Create(RSNNTPNoOnNewsgroupList);
  end;
  ProcessGroupList('LIST', 215, FOnNewsgroupList);  {do not localize}
end;

procedure TIdNNTP.GetNewGroupsList(ADate: TDateTime; AGMT: boolean;
 ADistributions: string);
begin
  if not Assigned(FOnNewGroupsList) then begin
    raise EIdNNTPNoOnNewGroupsList.Create(RSNNTPNoOnNewGroupsList);
  end;
  ProcessGroupList('NEWGROUPS ' + ConvertDateTimeDist(ADate, AGMT, ADistributions), {do not localize}
    231, FOnNewGroupsList);
end;

procedure TIdNNTP.GetNewNewsList(ANewsgroups: string;
 ADate: TDateTime; AGMT: boolean; ADistributions: string);
var
  s1: string;
  CanContinue: Boolean;
begin
  if not Assigned(FOnNewNewsList) then begin
    raise EIdNNTPNoOnNewNewsList.Create(RSNNTPNoOnNewNewsList);
  end;

  BeginWork(wmRead,0); try
    SendCmd('NEWNEWS ' + ANewsgroups + ' ' + ConvertDateTimeDist(ADate, AGMT, ADistributions), 230);  {do not localize}
    s1 := IOHandler.ReadLn;
    CanContinue := True;
    while (s1 <> '.') and CanContinue do begin
      FOnNewNewsList(s1, CanContinue);
      s1 := IOHandler.ReadLn;
    end;
  finally
    EndWork(wmRead);
  end;
end;

(*
3.9.  The NEXT command

3.9.1.  NEXT

   NEXT

   The internally maintained "current article pointer" is advanced to
   the next article in the current newsgroup.  If no more articles
   remain in the current group, an error message is returned and the
   current article remains selected.

   The internally-maintained "current article pointer" is set by this
   command.

   A response indicating the current article number, and the message-id
   string will be returned.  No text is sent in response to this
   command.

3.9.2.  Responses

   223 n a article retrieved - request text separately
           (n = article number, a = unique article id)
   412 no newsgroup selected
   420 no current article has been selected
   421 no next article in this group
*)
function TIdNNTP.Next: Boolean;
begin
  Result := SendCmd('NEXT', [223, 421]) = 223;  {do not localize}
end;

(*
3.5.  The LAST command

3.5.1.  LAST

   LAST

   The internally maintained "current article pointer" is set to the
   previous article in the current newsgroup.  If already positioned at
   the first article of the newsgroup, an error message is returned and
   the current article remains selected.

   The internally-maintained "current article pointer" is set by this
   command.

   A response indicating the current article number, and a message-id
   string will be returned.  No text is sent in response to this
   command.

3.5.2.  Responses

   223 n a article retrieved - request text separately
           (n = article number, a = unique article id)
   412 no newsgroup selected
   420 no current article has been selected
   422 no previous article in this group
*)
function TIdNNTP.Previous: Boolean;
begin
  Result := SendCmd('LAST', [223, 422]) = 223;  {do not localize}
end;

function TIdNNTP.SelectArticle(AMsgNo: Int64): Boolean;
begin
  Result := SendCmd('STAT ' + IntToStr(AMsgNo), [223, 423]) = 223;  {do not localize}
end;

procedure TIdNNTP.GetNewsgroupList(AList: TStrings);
var
  LEncoding: IIdTextEncoding;
begin
  SendCmd('LIST', 215); {do not localize}
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AList, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

procedure TIdNNTP.GetNewGroupsList(ADate: TDateTime; AGMT: boolean;
 ADistributions: string; AList: TStrings);
var
  LEncoding: IIdTextEncoding;
begin
  SendCmd('NEWGROUPS ' + ConvertDateTimeDist(ADate, AGMT, ADistributions), 231);  {do not localize}
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AList, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

procedure TIdNNTP.GetNewNewsList(ANewsgroups: string; ADate: TDateTime;
 AGMT: boolean; ADistributions: string; AList: TStrings);
var
  LEncoding: IIdTextEncoding;
begin
  SendCmd('NEWNEWS ' + ANewsgroups + ' ' + ConvertDateTimeDist(ADate, AGMT, ADistributions), 230);  {do not localize}
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AList, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

function TIdNNTP.ConvertDateTimeDist(ADate: TDateTime; AGMT: boolean;
 ADistributions: string): string;
begin
  Result := FormatDateTime('yymmdd hhnnss', ADate); {do not localize}
  if AGMT then begin
    Result:= Result + ' GMT'; {do not localize}
  end;
  if Length(ADistributions) > 0 then begin
    Result := ' <' + ADistributions + '>';
  end;
end;

(*
3.1.  The ARTICLE, BODY, HEAD, and STAT commands

   There are two forms to the ARTICLE command (and the related BODY,
   HEAD, and STAT commands), each using a different method of specifying
   which article is to be retrieved.  When the ARTICLE command is
   followed by a message-id in angle brackets ("<" and ">"), the first
   form of the command is used; when a numeric parameter or no parameter
   is supplied, the second form is invoked.

   The text of the article is returned as a textual response, as
   described earlier in this document.

   The HEAD and BODY commands are identical to the ARTICLE command
   except that they respectively return only the header lines or text
   body of the article.

   The STAT command is similar to the ARTICLE command except that no
   text is returned.  When selecting by message number within a group,
   the STAT command serves to set the current article pointer without
   sending text. The returned acknowledgement response will contain the
   message-id, which may be of some value.  Using the STAT command to
   select by message-id is valid but of questionable value, since a
   selection by message-id does NOT alter the "current article pointer".

3.1.1.  ARTICLE (selection by message-id)

   ARTICLE <message-id>

   Display the header, a blank line, then the body (text) of the
   specified article.  Message-id is the message id of an article as
   shown in that article's header.  It is anticipated that the client
   will obtain the message-id from a list provided by the NEWNEWS
   command, from references contained within another article, or from
   the message-id provided in the response to some other commands.

   Please note that the internally-maintained "current article pointer"
   is NOT ALTERED by this command. This is both to facilitate the
   presentation of articles that may be referenced within an article
   being read, and because of the semantic difficulties of determining
   the proper sequence and membership of an article which may have been
   posted to more than one newsgroup.

3.1.2.  ARTICLE (selection by number)

   ARTICLE [nnn]

   Displays the header, a blank line, then the body (text) of the
   current or specified article.  The optional parameter nnn is the

   numeric id of an article in the current newsgroup and must be chosen
   from the range of articles provided when the newsgroup was selected.
   If it is omitted, the current article is assumed.

   The internally-maintained "current article pointer" is set by this
   command if a valid article number is specified.

   [the following applies to both forms of the article command.] A
   response indicating the current article number, a message-id string,
   and that text is to follow will be returned.

   The message-id string returned is an identification string contained
   within angle brackets ("<" and ">"), which is derived from the header
   of the article itself.  The Message-ID header line (required by
   RFC850) from the article must be used to supply this information. If
   the message-id header line is missing from the article, a single
   digit "0" (zero) should be supplied within the angle brackets.

   Since the message-id field is unique with each article, it may be
   used by a news reading program to skip duplicate displays of articles
   that have been posted more than once, or to more than one newsgroup.

3.1.3.  Responses

   220 n <a> article retrieved - head and body follow
           (n = article number, <a> = message-id)
   221 n <a> article retrieved - head follows
   222 n <a> article retrieved - body follows
   223 n <a> article retrieved - request text separately
   412 no newsgroup has been selected
   420 no current article has been selected
   423 no such article number in this group
   430 no such article found
*)
function TIdNNTP.GetArticle(AMsg: TIdMessage): Boolean;
begin
  Result := True;
  SendCmd('ARTICLE', 220);  {do not localize}
  AMsg.Clear;
  //Don't call ReceiveBody if the message ended at the end of the headers
  //(ReceiveHeader() would have returned '.' in that case)...
  if ReceiveHeader(AMsg) = '' then begin
    ReceiveBody(AMsg);
  end;
end;

function TIdNNTP.GetArticle(AMsgNo: Int64; AMsg: TIdMessage): Boolean;
begin
  Result := SendCmd('ARTICLE ' + IntToStr(AMsgNo), [220, 423]) = 220; {do not localize}
  if Result then begin
    AMsg.Clear;
    //Don't call ReceiveBody if the message ended at the end of the headers
    //(ReceiveHeader() would have returned '.' in that case)...
    if ReceiveHeader(AMsg) = '' then begin
      ReceiveBody(AMsg);
    end;
  end;
end;

function TIdNNTP.GetArticle(AMsgID: string; AMsg: TIdMessage): Boolean;
begin
  Result := SendCmd('ARTICLE ' + EnsureMsgIDBrackets(AMsgID), [220, 430]) = 220; {do not localize}
  if Result then begin
    AMsg.Clear;
    //Don't call ReceiveBody if the message ended at the end of the headers
    //(ReceiveHeader() would have returned '.' in that case)...
    if ReceiveHeader(AMsg) = '' then begin
      ReceiveBody(AMsg);
    end;
  end;
end;

function TIdNNTP.GetArticle(AMsg: TStrings): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := True;
  SendCmd('ARTICLE', 220);  {do not localize}
  AMsg.Clear;
  // per RFC 3977, headers should be in UTF-8, but are not required to,
  // so lets read them as 8-bit...
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

function TIdNNTP.GetArticle(AMsgNo: Int64; AMsg: TStrings): Boolean;
begin
  Result := SendCmd('ARTICLE ' + IntToStr(AMsgNo), [220, 423]) = 220; {do not localize}
  if Result then begin
    AMsg.Clear;
    // per RFC 3977, headers should be in UTF-8, but are not required to,
    // so lets read them as 8-bit...
    IOHandler.Capture(AMsg, IndyTextEncoding_8Bit);
  end;
end;

function TIdNNTP.GetArticle(AMsgID: string; AMsg: TStrings): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('ARTICLE ' + EnsureMsgIDBrackets(AMsgID), [220, 430]) = 220; {do not localize}
  if Result then begin
    AMsg.Clear;
    // per RFC 3977, headers should be in UTF-8, but are not required to,
    // so lets read them as 8-bit...
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdNNTP.GetArticle(AMsg: TStream): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := True;
  SendCmd('ARTICLE', 220);  {do not localize}
  // per RFC 3977, headers should be in UTF-8, but are not required to,
  // so lets read them as 8-bit...
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

function TIdNNTP.GetArticle(AMsgNo: Int64; AMsg: TStream): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('ARTICLE ' + IntToStr(AMsgNo), [220, 423]) = 220; {do not localize}
  if Result then begin
    // per RFC 3977, headers should be in UTF-8, but are not required to,
    // so lets read them as 8-bit...
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdNNTP.GetArticle(AMsgID: string; AMsg: TStream): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('ARTICLE ' + EnsureMsgIDBrackets(AMsgID), [220, 430]) = 220; {do not localize}
  if Result then begin
    // per RFC 3977, headers should be in UTF-8, but are not required to,
    // so lets read them as 8-bit...
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdNNTP.GetBody(AMsg: TIdMessage): Boolean;
begin
  Result := SendCmd('BODY', [222, 420]) = 222; {do not localize}
  if Result then begin
    AMsg.Clear;
    ReceiveBody(AMsg);
  end;
end;

function TIdNNTP.GetBody(AMsgNo: Int64; AMsg: TIdMessage): Boolean;
begin
  Result := SendCmd('BODY ' + IntToStr(AMsgNo), [222, 423]) = 222;  {do not localize}
  if Result then begin
    AMsg.Clear;
    ReceiveBody(AMsg);
  end;
end;

function TIdNNTP.GetBody(AMsgID: string; AMsg: TIdMessage): Boolean;
begin
  Result := SendCmd('BODY ' + EnsureMsgIDBrackets(AMsgID), [222, 430]) = 222;  {do not localize}
  if Result then begin
    AMsg.Clear;
    ReceiveBody(AMsg);
  end;
end;

function TIdNNTP.GetBody(AMsg: TStrings): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := True;
  SendCmd('BODY', 222); {do not localize}
  AMsg.Clear;
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

function TIdNNTP.GetBody(AMsgNo: Int64; AMsg: TStrings): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('BODY ' + IntToStr(AMsgNo), [222, 423]) = 222;  {do not localize}
  if Result then begin
    AMsg.Clear;
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdNNTP.GetBody(AMsgID: string; AMsg: TStrings): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('BODY ' + EnsureMsgIDBrackets(AMsgID), [222, 430]) = 222;  {do not localize}
  if Result then begin
    AMsg.Clear;
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdNNTP.GetBody(AMsg: TStream): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := True;
  SendCmd('BODY', 222); {do not localize}
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

function TIdNNTP.GetBody(AMsgNo: Int64; AMsg: TStream): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('BODY ' + IntToStr(AMsgNo), [222, 423]) = 222;  {do not localize}
  if Result then begin
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdNNTP.GetBody(AMsgID: string; AMsg: TStream): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('BODY ' + EnsureMsgIDBrackets(AMsgID), [222, 430]) = 222;  {do not localize}
  if Result then begin
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdNNTP.GetHeader(AMsg: TIdMessage): Boolean;
begin
  Result := True;
  SendCmd('HEAD', 221); {do not localize}
  AMsg.Clear;
  ReceiveHeader(AMsg);
end;

function TIdNNTP.GetHeader(AMsgNo: Int64; AMsg: TIdMessage): Boolean;
begin
  Result := SendCmd('HEAD ' + IntToStr(AMsgNo), [221, 423]) = 221;  {do not localize}
  if Result then begin
    AMsg.Clear;
    ReceiveHeader(AMsg);
  end;
end;

function TIdNNTP.GetHeader(AMsgID: string; AMsg: TIdMessage): Boolean;
begin
  Result := SendCmd('HEAD ' + EnsureMsgIDBrackets(AMsgID), [221, 430]) = 221;  {do not localize}
  if Result then begin
    AMsg.Clear;
    ReceiveHeader(AMsg);
  end;
end;

function TIdNNTP.GetHeader(AMsg: TStrings): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := True;
  SendCmd('HEAD', 221); {do not localize}
  AMsg.Clear;
  // per RFC 3977, headers should be in UTF-8, but are not required to,
  // so lets read them as 8-bit...
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

function TIdNNTP.GetHeader(AMsgNo: Int64; AMsg: TStrings): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('HEAD ' + IntToStr(AMsgNo), [221, 423]) = 221;  {do not localize}
  if Result then begin
    AMsg.Clear;
    // per RFC 3977, headers should be in UTF-8, but are not required to,
    // so lets read them as 8-bit...
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdNNTP.GetHeader(AMsgID: string; AMsg: TStrings): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('HEAD ' + EnsureMsgIDBrackets(AMsgID), [221, 430]) = 221;  {do not localize}
  if Result then begin
    AMsg.Clear;
    // per RFC 3977, headers should be in UTF-8, but are not required to,
    // so lets read them as 8-bit...
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdNNTP.GetHeader(AMsg: TStream): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := True;
  SendCmd('HEAD', 221); {do not localize}
  // per RFC 3977, headers should be in UTF-8, but are not required to,
  // so lets read them as 8-bit...
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

function TIdNNTP.GetHeader(AMsgNo: Int64; AMsg: TStream): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('HEAD ' + IntToStr(AMsgNo), [221, 423]) = 221;  {do not localize}
  if Result then begin
    // per RFC 3977, headers should be in UTF-8, but are not required to,
    // so lets read them as 8-bit...
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdNNTP.GetHeader(AMsgID: string; AMsg: TStream): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := SendCmd('HEAD ' + EnsureMsgIDBrackets(AMsgID), [221, 430]) = 221;  {do not localize}
  if Result then begin
    // per RFC 3977, headers should be in UTF-8, but are not required to,
    // so lets read them as 8-bit...
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(AMsg, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

procedure TIdNNTP.GetNewsgroupList(AStream: TStream);
var
  LEncoding: IIdTextEncoding;
begin
  SendCmd('LIST', 215); {do not localize}
  LEncoding := IndyTextEncoding_8Bit;
  IOHandler.Capture(AStream, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
end;

procedure TIdNNTP.AfterConnect;
begin
  try
   
    // Here lets check to see what condition we are in after being greeted by
    // the server. The application utilizing NNTPWinshoe should check the value
    // of GreetingResult to determine if further action is warranted.

    case FGreetingCode of
      200: FPermission := crCanPost;
      201: FPermission := crNoPost;
      400: FPermission := crTempUnavailable;
      // This should never happen because the server should immediately close
      // the connection but just in case ....
      // Kudzu: Changed this to an exception, otherwise it produces non-standard usage by the
      // users code
      502: raise EIdNNTPConnectionRefused.CreateError(502, RSNNTPConnectionRefused);
    end;
    // here we call SeTIdMode on the value stored in mode to make sure we can
    // use the mode we have selected
    case Mode of
      mtStream: begin
        SendCmd('MODE STREAM'); {do not localize}
        if LastCmdResult.NumericCode <> 203 then begin
          ModeResult := mrNoStream
        end else begin
          ModeResult := mrCanStream;
        end;
      end;
      mtReader: begin
        // We should get the same info we got in the greeting
        // result but we set mode to reader anyway since the
        // server may want to do some internal reconfiguration
        // if it knows that a reader has connected
        SendCmd('MODE READER'); {do not localize}
        if LastCmdResult.NumericCode <> 200 then begin
          ModeResult := mrNoPost;
        end else begin
          ModeResult := mrCanPost;
        end;
      end;
    end;
    GetCapability;
  except
    Disconnect;
    Raise;
  end;
end;

destructor TIdNNTP.Destroy;
begin
  inherited Destroy;
end;

procedure TIdNNTP.GetCapability;
var
  i: Integer;
begin
  FCapabilities.Clear;
  // try CAPABILITIES first, as it is a standard command introduced in RFC 3977
  if SendCmd('CAPABILITIES') = 101 then {do not localize}
  begin
    IOHandler.Capture(FCapabilities, '.'); {do not localize}
  end
  // fall back to the previous non-standard approach
  else if SendCmd('LIST EXTENSIONS') in [202, 215] then  {do not localize}
  begin
    IOHandler.Capture(FCapabilities, '.'); {do not localize}
  end;
  //flatten everything out for easy processing
  for i := 0 to FCapabilities.Count-1 do
  begin
    FCapabilities[i] := Trim(UpperCase(FCapabilities[i]));
  end;
  FOVERSupported := IsExtCmdSupported('OVER');  {do not localize}
  FHDRSupported := IsExtCmdSupported('HDR');  {do not localize}
//  Self.FStartTLSSupported := IsExtCmdSupported('STARTTLS');
end;

function TIdNNTP.IsExtCmdSupported(AExtension: String): Boolean;
begin
  Result := FCapabilities.IndexOf(Trim(UpperCase(AExtension))) > -1;
end;

procedure TIdNNTP.StartTLS;
var
  LIO : TIdSSLIOHandlerSocketBase;
begin
  if (IOHandler is TIdSSLIOHandlerSocketBase) and (FUseTLS <> utNoTLSSupport) then
  begin
    LIO := TIdSSLIOHandlerSocketBase(IOHandler);
    //we check passthrough because we can either be using TLS currently with
    //implicit TLS support or because STARTLS was issued previously.
    if LIO.PassThrough then
    begin
      if IsExtCmdSupported('STARTTLS') then  {do not localize}
      begin
        if SendCmd('STARTTLS') = 382 then {do not localize}
        begin
          TLSHandshake;
          AfterConnect;
        end else begin
          ProcessTLSNegCmdFailed;
        end;
      end else begin
        ProcessTLSNotAvail;
      end;
    end;
  end;
end;

function TIdNNTP.GetSupportsTLS: boolean;
begin
  Result := IsExtCmdSupported('STARTTLS') {do not localize}
end;

procedure TIdNNTP.XHDR(AHeader, AParam: string);
var
  LLine : String;
  LMsg, LHeaderData : String;
  LCanContinue : Boolean;
begin
  if Assigned(FOnXHDREntry) then
  begin
    XHDRCommon(AHeader,AParam);
    BeginWork(wmRead, 0);
    try
      LLine := IOHandler.ReadLn;
      LCanContinue := True;
      while (LLine <> '.') and LCanContinue do
      begin
        ParseXHDRLine(LLine,LMsg,LHeaderData);
        FOnXHDREntry(AHeader,LMsg,LHeaderData,LCanContinue);
        LLine := IOHandler.ReadLn;
      end;
    finally
      EndWork(wmRead);
    end;
  end else
  begin
    raise EIdNNTPNoOnXHDREntry.Create(RSNNTPNoOnXHDREntry);
  end;
end;

procedure TIdNNTP.XOVER(AParam: string);
var
  LLine : String;
  //for our XOVER data
  LArticleIndex : Int64;
  LSubject,
  LFrom : String;
  LDate : TDateTime;
  LMsgId, LReferences : String;
  LByteCount,
  LLineCount : Integer;
  LExtraData : String;
  LCanContinue : Boolean;
begin
  if Assigned( FOnXOVER) then
  begin
    XOVERCommon(AParam);
    BeginWork(wmRead, 0);
    try
      LLine := IOHandler.ReadLn;
      LCanContinue := True;
      while (LLine <> '.') and LCanContinue do
      begin
        ParseXOVER(LLine,LArticleIndex,LSubject,LFrom,LDate,
          LMsgId,LReferences,LByteCount,LLineCount,LExtraData);
        FOnXOVER(LArticleIndex,LSubject,LFrom,LDate,LMsgId,LReferences,LByteCount,LLineCount,LExtraData,LCanContinue);
        LLine := IOHandler.ReadLn;
      end;
    finally
      EndWork(wmRead);
    end;
  end else
  begin
    raise EIdNNTPNoOnXOVER.Create(RSNNTPNoOnXOVER);
  end;
end;

procedure TIdNNTP.ParseXHDRLine(ALine: String; out AMsg,
  AHeaderData: String);
begin
//from:  RFC 2890
//Each line
//containing matched headers returned by the server has an article
//number (or message ID, if a message ID was specified in the command),
//then one or more spaces, then the value of the requested header in
//that article.

//from: http://www.ietf.org/internet-drafts/draft-ietf-nntpext-base-18.txt
// describing HDR
// The line consists
//   of the article number, a space, and then the contents of the header
//   (without the header name or the colon and space that follow it) or
//   metadata item. If the article is specified by message-id rather than
//   by article range, the article number is given as "0".
  AMsg := Fetch(ALine);
  AHeaderData := ALine;
end;

procedure TIdNNTP.XHDRCommon(AHeader, AParam : String);
begin
  if FHDRSupported then
  begin
  //http://www.ietf.org/internet-drafts/draft-ietf-nntpext-base-18.txt
  //says the correct reply code is 225 but RFC 2980 specifies 221 for the
  //XHDR command so we should accept both to CYA.
    SendCmd('HDR '+ AHeader + ' ' + AParam, [225, 221]);  {do not localize}
  end else
  begin
    SendCmd('XHDR ' + AHeader + ' ' + AParam, 221); {do not localize}
  end;
end;

procedure TIdNNTP.XOVERCommon(AParam: String);
begin
  if FOVERSupported then begin
    SendCmd('OVER '+ AParam, 224);  {do not localize}
  end else begin
    SendCmd('XOVER ' + AParam, 224);  {do not localize}
  end;
end;

procedure TIdNNTP.DisconnectNotifyPeer;
begin
  inherited DisconnectNotifyPeer;
  SendCmd('QUIT', 205);  {do not localize}
end;

procedure TIdNNTP.SendAuth;
begin
  // RLebeau - RFC 2980 says that if the password is not required,
  // then 281 will be returned for the username request, not 381.
  if (inherited SendCmd('AUTHINFO USER ' + Username, [281, 381]) = 381) then begin {do not localize}
    inherited SendCmd('AUTHINFO PASS ' + Password, 281);  {do not localize}
  end;
end;

end.

