{
  $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.39    1/7/05 3:29:12 PM  RLebeau
  Fix for AV in Notification()

  Rev 1.38    12/21/04 1:51:42 AM  RLebeau
  Bug fix for Capa() method.

  Rev 1.37    11/27/04 2:48:22 AM  RLebeau
  Fixed bug in ownership of SASLMechanisms property

  Rev 1.36    10/26/2004 10:35:42 PM  JPMugaas
  Updated ref.

  Rev 1.35    2004.04.18 1:39:26 PM  czhower
  Bug fix for .NET with attachments, and several other issues found along the
  way.

  Rev 1.34    2004.04.07 6:02:42 PM  czhower
  Implemented AutoLogin in a better manner.

  Rev 1.33    2004.04.07 5:53:56 PM  czhower
  .NET overload

  Rev 1.32    2004.03.06 1:31:48 PM  czhower
  To match Disconnect changes to core.

  Rev 1.31    2004.02.03 5:44:12 PM  czhower
  Name changes

  Rev 1.30    2004.02.03 2:12:18 PM  czhower
  $I path change

  Rev 1.29    1/25/2004 3:11:36 PM  JPMugaas
  SASL Interface reworked to make it easier for developers to use.
  SSL and SASL reenabled components.

  Rev 1.28    1/21/2004 3:27:06 PM  JPMugaas
  InitComponent

  Rev 1.27    1/12/04 12:22:40 PM  RLebeau
  Updated RetrieveMailboxSize() and RetrieveMsgSize() to support responses that
  contain additional data after the octet values.

  Rev 1.26    22/12/2003 00:45:12  CCostelloe
  .NET fixes

    Rev 1.25    10/19/2003 5:42:36 PM  DSiders
  Added localization comments.

  Rev 1.24    10/11/2003 7:14:34 PM  BGooijen
  Changed IdCompilerDefines.inc path

  Rev 1.23    10/10/2003 11:39:40 PM  BGooijen
  Compiles in DotNet now

  Rev 1.22    6/15/2003 01:17:10 PM  JPMugaas
  Intermediate class no longer used.  We use the SASL functionality right from
  the TIdSASLList.

  Rev 1.21    6/4/2003 04:10:36 PM  JPMugaas
  Removed hacked GetInternelResponse.

  Updated to use Kudzu's new string reply code.

  Rev 1.20    5/26/2003 04:28:16 PM  JPMugaas
  Removed GenerateReply and ParseResponse calls because those functions are
  being removed.

  Rev 1.19    5/26/2003 12:23:58 PM  JPMugaas

  Rev 1.18    5/25/2003 03:54:46 AM  JPMugaas

  Rev 1.17    5/25/2003 03:45:56 AM  JPMugaas

  Rev 1.16    5/22/2003 05:27:52 PM  JPMugaas

  Rev 1.16    5/20/2003 02:29:42 PM  JPMugaas
  Updated with POP3Reply object.

  Rev 1.15    5/10/2003 10:10:46 PM  JPMugaas
  Bug fixes.

  Rev 1.14    5/8/2003 08:44:16 PM  JPMugaas
  Moved some SASL authentication code down to an anscestor for reuse.  WIll
  clean up soon.

  Rev 1.13    5/8/2003 03:18:14 PM  JPMugaas
  Flattened ou the SASL authentication API, made a custom descendant of SASL
  enabled TIdMessageClient classes.

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

  Rev 1.11    5/8/2003 03:03:00 AM  JPMugaas
  Fixed a problem with SASL.  It turns out that what was being processed with
  something from a previous command.  It also turned out that some charactors
  were being removed from the SASL processing when they shouldn't have been.

  Rev 1.10    5/8/2003 02:18:08 AM  JPMugaas
  Fixed an AV in IdPOP3 with SASL list on forms.  Made exceptions for SASL
  mechanisms missing more consistant, made IdPOP3 support feature feature
  negotiation, and consolidated some duplicate code.

  Rev 1.9    5/7/2003 04:58:34 AM  JPMugaas
  We now use the initial greeting message from the server when calculating the
  parameter for the APOP command.  Note that we were calling CAPA before APOP
  and that could throw things off.

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

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

  Rev 1.6    3/19/2003 08:53:40 PM  JPMugaas
  Now should work with new framework.

  Rev 1.5    3/17/2003 02:25:26 PM  JPMugaas
  Updated to use new TLS framework.  Now can require that users use TLS.  Note
  that this setting create an incompatiability with Norton AntiVirus because
  that does act as a "man in the middle" when intercepting E-Mail for virus
  scanning.

  Rev 1.4    3/13/2003 09:49:26 AM  JPMugaas
  Now uses an abstract SSL base class instead of OpenSSL so 3rd-party vendors
  can plug-in their products.

  Rev 1.3    2/24/2003 09:27:58 PM  JPMugaas

  Rev 1.2    12/15/2002 04:27:10 PM  JPMugaas
  POP3 now compiles and works in Indy 10.

  Rev 1.1    12-15-2002 12:57:40  BGooijen
  Added Top-command

  Rev 1.0    11/13/2002 07:58:22 AM  JPMugaas

  2002-08-18 - J. Berg
   - implement SASL, add CAPA and STLS

  02 August 2002 - A. Neillans
   - Bug fix:
     [ 574171 ] TIdMessage not cleared before a retreive

  11-10-2001 - J. Peter Mugaas
    Added suggested code from Andrew P.Rybin that does the following:
    -APOP Authentication Support
    -unrecognized text header now displayed in exception message
    -GetUIDL method

  2001-AUG-31 DSiders
    Changed TIdPOP3.Connect to use ATimeout when calling
    inherited Connect.

  2000-SEPT-28 SG
    Added GetUIDL as from code by

  2000-MAY-10 HH
    Added RetrieveMailBoxSize and renamed RetrieveSize to RetrieveMsgSize.
    Finished Connect.

  2000-MARCH-03 HH
    Converted to Indy
}

unit IdPOP3;

{ POP 3 (Post Office Protocol Version 3) }

interface

{$i IdCompilerDefines.inc}

uses
  Classes,
  IdAssignedNumbers,
  IdGlobal,
  IdException,
  IdExplicitTLSClientServerBase,
  IdGlobalProtocols,
  IdMessage,
  IdMessageClient,
  IdReply,
  IdSASL,
  IdSASLCollection,
  IdBaseComponent,
  IdUserPassProvider;

type
  TIdPOP3AuthenticationType = (patUserPass, patAPOP, patSASL);

const
  DEF_POP3USE_IMPLICIT_TLS = False;
  DEF_ATYPE = patUserPass;

type
  TIdPOP3 = class(TIdMessageClient)
  protected
    FAuthType : TIdPOP3AuthenticationType;
    FAutoLogin: Boolean;
    FAPOPToken : String; 
    FHasAPOP: Boolean;
    FHasCAPA: Boolean;
    FSASLMechanisms : TIdSASLEntries;
    //
    function GetReplyClass:TIdReplyClass; override;
    function GetSupportsTLS: Boolean; override;
    procedure SetSASLMechanisms(AValue: TIdSASLEntries);
    procedure InitComponent; override;
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
  public
    function CheckMessages: Integer;
    procedure Connect; override;
    procedure Login; virtual;
    destructor Destroy; override;
    function Delete(const MsgNum: Integer): Boolean;
    procedure DisconnectNotifyPeer; override;
    procedure KeepAlive;
    function List(const ADest: TStrings; const AMsgNum: Integer = -1): Boolean;
    procedure ParseLIST(ALine: String; var VMsgNum, VMsgSize: Integer);
    procedure ParseUIDL(ALine: String; var VMsgNum: Integer; var VUidl: String);
    function Reset: Boolean;
    function Retrieve(const MsgNum: Integer; AMsg: TIdMessage): Boolean;
    function RetrieveHeader(const MsgNum: Integer; AMsg: TIdMessage): Boolean;
    function RetrieveMsgSize(const MsgNum: Integer): Integer;
    function RetrieveMailBoxSize: Int64;
    function RetrieveRaw(const aMsgNo: Integer; const aDest: TStrings): boolean; overload;
    function RetrieveRaw(const aMsgNo: Integer; const aDest: TStream): boolean; overload;
    function RetrieveStats(var VMsgCount: Integer; var VMailBoxSize: Int64): Boolean;
    function UIDL(const ADest: TStrings; const AMsgNum: Integer = -1): Boolean;
    function Top(const AMsgNum: Integer; const ADest: TStrings; const AMaxLines: Integer = 0): boolean;
    function CAPA: Boolean;
    property HasAPOP: boolean read FHasAPOP;
    property HasCAPA: boolean read FHasCAPA;
  published
    property AuthType : TIdPOP3AuthenticationType read FAuthType write FAuthType default DEF_ATYPE;
    property AutoLogin: Boolean read FAutoLogin write FAutoLogin;
    property Host;
    property Username;
    property UseTLS;
    property Password;
    property Port default IdPORT_POP3;
    property SASLMechanisms : TIdSASLEntries read FSASLMechanisms write SetSASLMechanisms;
  end;

type
  EIdPOP3Exception = class(EIdException);
  EIdDoesNotSupportAPOP = class(EIdPOP3Exception);
  EIdUnrecognizedReply = class(EIdPOP3Exception);

implementation

uses
  IdFIPS,
  IdHash,
  IdHashMessageDigest,
  IdTCPConnection,
  IdSSL,
  IdResourceStringsProtocols,
  IdReplyPOP3,
  IdCoderMIME,
  SysUtils;

{ TIdPOP3 }

function TIdPOP3.CheckMessages: Integer;
var
  LIgnore: Int64;
begin
  // RLebeau: for backwards compatibility, raise an exception if STAT fails
  if not RetrieveStats(Result, LIgnore) then begin
    RaiseExceptionForLastCmdResult;
  end;
  // Only gets here if exception is not raised
end;

procedure TIdPOP3.Login;
var
  S: String;
  LMD5: TIdHashMessageDigest5;
begin
  if UseTLS in ExplicitTLSVals then begin
    if SupportsTLS then begin
      if SendCmd('STLS','') = ST_OK then begin {Do not translate}
        TLSHandshake;
      end else begin
        ProcessTLSNegCmdFailed;
      end;
    end else begin
      ProcessTLSNotAvail;
    end;
  end;

  case FAuthType of
    patAPOP:  //APR
      begin
        if FHasAPOP then begin
          CheckMD5Permitted;
          LMD5 := TIdHashMessageDigest5.Create;
          try
            S := LowerCase(LMD5.HashStringAsHex(FAPOPToken+Password));
          finally
            LMD5.Free;
          end;//try
          SendCmd('APOP ' + Username + ' ' + S, ST_OK);    {Do not Localize}
        end else begin
          raise EIdDoesNotSupportAPOP.Create(RSPOP3ServerDoNotSupportAPOP);
        end;
      end;
    patUserPass:
      begin //classic method
        SendCmd('USER ' + Username, ST_OK);    {Do not Localize}
        SendCmd('PASS ' + Password, ST_OK);    {Do not Localize}
      end;//if APOP
    patSASL:
      begin
        // SASL in POP3 did not originally support Initial-Response. It was added
        // in RFC 2449 along with the CAPA command. If a server supports the CAPA
        // command then it *should* also support Initial-Response as well, however
        // many POP3 servers support CAPA but do not support Initial-Response
        // (which was formalized in RFC 5034). So, until we can handle that
        // descrepency better, we will simply disable Initial-Response for now.

        FSASLMechanisms.LoginSASL('AUTH', FHost, IdGSKSSN_pop, [ST_OK], [ST_SASLCONTINUE], Self, Capabilities, 'SASL', False); {do not localize}
      end;
  end;
end;

procedure TIdPOP3.InitComponent;
begin
  inherited;
  FAutoLogin := True;
  FSASLMechanisms := TIdSASLEntries.Create(Self);
  FRegularProtPort := IdPORT_POP3;
  FImplicitTLSProtPort := IdPORT_POP3S;
  Port := IdPORT_POP3;
  FAuthType := DEF_ATYPE;
end;

function TIdPOP3.Delete(const MsgNum: Integer): Boolean;
begin
  Result := (SendCmd('DELE ' + IntToStr(MsgNum), '') = ST_OK);   {do not localize}
end;

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

function TIdPOP3.GetReplyClass:TIdReplyClass;
begin
  Result := TIdReplyPOP3;
end;

procedure TIdPOP3.KeepAlive;
begin
  SendCmd('NOOP', ST_OK);    {Do not Localize}
end;

function TIdPOP3.Reset: Boolean;
begin
  Result := (SendCmd('RSET', '') = ST_OK);    {Do not Localize}
end;

function TIdPOP3.RetrieveRaw(const aMsgNo: Integer; const aDest: TStrings): boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := (SendCmd('RETR ' + IntToStr(aMsgNo), '') = ST_OK);    {Do not Localize}
  if Result then begin
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(aDest, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdPOP3.RetrieveRaw(const aMsgNo: Integer; const aDest: TStream): boolean;
var
  LEncoding: IIdTextEncoding;
begin
  Result := (SendCmd('RETR ' + IntToStr(aMsgNo), '') = ST_OK);    {Do not Localize}
  if Result then begin
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(aDest, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

function TIdPOP3.Retrieve(const MsgNum: Integer; AMsg: TIdMessage): Boolean;
begin
  Result := (SendCmd('RETR ' + IntToStr(MsgNum), '') = ST_OK);   {Do not Localize}
  if Result then begin
    AMsg.Clear;
    // This is because of a bug in Exchange? with empty messages. See comment in ReceiveHeader
    if ReceiveHeader(AMsg) = '' then begin
      // Only retreive the body if we do not already have a full RFC
      ReceiveBody(AMsg);
    end;
  end;
end;

function TIdPOP3.RetrieveHeader(const MsgNum: Integer; AMsg: TIdMessage): Boolean;
begin
//  Result := False;
  AMsg.Clear;
  SendCmd('TOP ' + IntToStr(MsgNum) + ' 0', ST_OK);    {Do not Localize}
  // Only gets here if no exception is raised
  ReceiveHeader(AMsg,'.');
  Result := True;
end;

function TIdPOP3.RetrieveMailBoxSize: Int64;
var
  LIgnore: Integer;
begin
  // RLebeau: for backwards compatibility, return -1 if STAT fails
  try
    if not RetrieveStats(LIgnore, Result) then begin
      RaiseExceptionForLastCmdResult;
    end;
  except
    Result := -1;
  end;
end;

function TIdPOP3.RetrieveMsgSize(const MsgNum: Integer): Integer;
var
  s: string;
begin
  Result := -1;
  // Returns the size of the message. if an error ocurrs, returns -1.
  SendCmd('LIST ' + IntToStr(MsgNum), ST_OK);    {Do not Localize}
  s := LastCmdResult.Text[0];
  if Length(s) > 0 then begin
    // RL - ignore the message number, grab just the octets,
    // and ignore everything else that may be present
    Fetch(s);
    Result := IndyStrToInt(Fetch(s), -1);
  end;
end;

function TIdPOP3.RetrieveStats(var VMsgCount: Integer; var VMailBoxSize: Int64): Boolean;
var
  s: string;
begin
  VMsgCount := 0;
  VMailBoxSize := 0;
  Result := (SendCmd('STAT', '') = ST_OK);    {Do not Localize}
  if Result then begin
    s := LastCmdResult.Text[0];
    if Length(s) > 0 then begin
      VMsgCount := IndyStrToInt(Fetch(s));
      VMailBoxSize := IndyStrToInt64(Fetch(s));
    end;
  end;
end;

function TIdPOP3.List(const ADest: TStrings; const AMsgNum: Integer = -1): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  if AMsgNum >= 0 then begin
    Result := (SendCmd('LIST ' + IntToStr(AMsgNum), '') = ST_OK);    {Do not Localize}
    if Result then begin
      ADest.Assign(LastCmdResult.Text);
    end;
  end
  else begin
    Result := (SendCmd('LIST', '') = ST_OK);    {Do not Localize}
    if Result then begin
      LEncoding := IndyTextEncoding_8Bit;
      IOHandler.Capture(ADest, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
    end;
  end;
end;

procedure TIdPOP3.ParseLIST(ALine: String; var VMsgNum, VMsgSize: Integer);
begin
  VMsgNum := IndyStrToInt(Fetch(ALine), -1);
  VMsgSize := IndyStrToInt(Fetch(ALine), -1);
end;

function TIdPOP3.UIDL(const ADest: TStrings; const AMsgNum: Integer = -1): Boolean;
var
  LEncoding: IIdTextEncoding;
begin
  if AMsgNum >= 0 then begin
    Result := (SendCmd('UIDL ' + IntToStr(AMsgNum), '') = ST_OK);    {Do not Localize}
    if Result then begin
      ADest.Assign(LastCmdResult.Text);
    end;
  end
  else begin
    Result := (SendCmd('UIDL', '') = ST_OK);    {Do not Localize}
    if Result then begin
      LEncoding := IndyTextEncoding_8Bit;
      IOHandler.Capture(ADest, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
    end;
  end;
end;

procedure TIdPOP3.ParseUIDL(ALine: String; var VMsgNum: Integer; var VUidl: String);
begin
  VMsgNum := IndyStrToInt(Fetch(ALine), -1);
  VUidl := Fetch(ALine);
end;

function TIdPOP3.Top(const AMsgNum: Integer; const ADest: TStrings; const AMaxLines: Integer = 0): boolean;
var
  Cmd: String;
  LEncoding: IIdTextEncoding;
begin
  Cmd := 'TOP ' + IntToStr(AMsgNum); {Do not Localize}
  if AMaxLines <> 0 then begin
    Cmd := Cmd + ' ' + IntToStr(AMaxLines); {Do not Localize}
  end;
  Result := (SendCmd(Cmd,'') = ST_OK);
  if Result then begin
    LEncoding := IndyTextEncoding_8Bit;
    IOHandler.Capture(ADest, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
  end;
end;

destructor TIdPOP3.Destroy;
begin
  FreeAndNil(FSASLMechanisms);
  inherited;
end;

function TIdPOP3.CAPA: Boolean;
begin
  FCapabilities.Clear;
  Result := (SendCmd('CAPA','') = ST_OK);    {Do not Localize}
  if Result then begin
    IOHandler.Capture(FCapabilities);
  end;
// RLebeau - do not delete here!  The +OK reply line is handled
// by SendCmd() above.  Deleting here is removing an actual capability entry.
{
  if FCapabilities.Count >0 then
  begin
    //dete the initial OK reply line
    FCapabilities.Delete(0);
  end;
  FHasCapa := Result;
}
  FHasCapa := (FCapabilities.Count > 0);
 // ParseCapaReply(FCapabilities,'SASL');
end;

procedure TIdPOP3.Notification(AComponent: TComponent; Operation: TOperation);
begin
  if (Operation = opRemove) and (FSASLMechanisms <> nil) then begin
    FSASLMechanisms.RemoveByComp(AComponent);
  end;
  inherited Notification(AComponent,Operation);
end;

function TIdPOP3.GetSupportsTLS: Boolean;
begin
   Result := ( FCapabilities.IndexOf('STLS') > -1 ); //do not localize
end;

procedure TIdPOP3.SetSASLMechanisms(AValue: TIdSASLEntries);
begin
  FSASLMechanisms.Assign(AValue);
end;

procedure TIdPOP3.Connect;
var
  S: String;
  I: Integer;
begin
  FHasAPOP := False;
  FHasCAPA := False;
  FAPOPToken := '';

  if (IOHandler is TIdSSLIOHandlerSocketBase) then begin
    (IOHandler as TIdSSLIOHandlerSocketBase).PassThrough := (FUseTLS <> utUseImplicitTLS);
  end;

  inherited Connect;
  try
    GetResponse(ST_OK);

    // the initial greeting text is needed to determine APOP availability
    S := LastCmdResult.Text.Strings[0];  //read response
    I := Pos('<', S);    {Do not Localize}
    if i > 0 then begin
      S := Copy(S, I, MaxInt); //?: System.Delete(S,1,i-1);
      I := Pos('>', S);    {Do not Localize}
      if I > 0 then begin
        FAPOPToken := Copy(S, 1, I);
      end;
    end;
    FHasAPOP := (Length(FAPOPToken) > 0);
    CAPA;
    if FAutoLogin then begin
      Login;
    end;
  except
    Disconnect(False);
    raise;
  end;
end;

end.


