{
  $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$
}

unit IdPOP4Server;
{
This is an experimental proposal based on Kudzu's idea.
}
interface
uses
  Classes,
  IdAssignedNumbers,
  IdCmdTCPServer,
  IdCommandHandlers,
  IdContext,
  IdCustomTCPServer, //for TIdServerContext
  IdEMailAddress,
  IdException,
  IdExplicitTLSClientServerBase,
  IdReply,
  IdReplyRFC,
  IdReplySMTP,
  IdTCPConnection,
  IdTCPServer,
  IdYarn,
  IdStack;

const
  POP4_PORT = 1970;  //my birthday
  
type
  TIdPOP4ServerContext = class;
  TOnUserLoginEvent = procedure(ASender: TIdPOP4ServerContext; const AUsername, APassword: string;
    var VAuthenticated: Boolean) of object;
  TIdPOP4ServerState = (Auth, Trans, Update);
  TIdPOP4Server = class(TIdExplicitTLSServer)
  protected
    FOnUserLogin : TOnUserLoginEvent;
    procedure CmdBadSequenceError(ASender: TIdCommand);
    procedure CmdAuthFailed(ASender: TIdCommand);
    procedure CmdSyntaxError(ASender: TIdCommand); overload;
    procedure CmdSyntaxError(AContext: TIdContext; ALine: string;
      const AReply: TIdReply=nil);  overload;
    procedure CmdMustUseTLS(ASender: TIdCommand);
    procedure InvalidSyntax(ASender: TIdCommand);

    function DoAuthLogin(ASender: TIdCommand; const Login:string): Boolean;

    procedure InitComponent; override;
    procedure InitializeCommandHandlers; override;
    procedure CommandAUTH(ASender: TIdCommand);
    procedure CommandCAPA(ASender: TIdCommand);
    procedure CommandSTARTTLS(ASender : TIdCommand);
    procedure DoReplyUnknownCommand(AContext: TIdContext; ALine: string); override;

  published
    property OnUserLogin : TOnUserLoginEvent read FOnUserLogin write FOnUserLogin;
    property DefaultPort default IdPORT_POP3;
  end;
  TIdPOP4ServerContext = class(TIdServerContext)
  protected
    FPipeLining : Boolean;
    FState :TIdPOP4ServerState;
    FUser,
    FPassword : String;
    function GetUsingTLS: boolean;
    procedure SetPipeLining(const AValue: Boolean);

  public
    constructor Create(
      AConnection: TIdTCPConnection;
      AYarn: TIdYarn;
      AList: TThreadList = nil
      ); override;
    procedure CheckPipeLine;
    property State    : TIdPOP4ServerState read FState write FState;
    property Username : String read fUser write fUser;
    property Password : String read fPassword write fPassword;
    property UsingTLS:boolean read GetUsingTLS;
    property PipeLining : Boolean read FPipeLining write SetPipeLining;
  end;

implementation
uses IdResourceStringsProtocols, IdCoderMIME, IdGlobal, IdGlobalProtocols, IdSSL, SysUtils;

{ TIdPOP4Server }

procedure TIdPOP4Server.InitializeCommandHandlers;
var LCmd : TIdCommandHandler;
begin
  inherited;
  LCmd := CommandHandlers.Add;

  LCmd.Command :=  'CAPA';  {do not localize}
  LCmd.NormalReply.Code := '211';
  LCmd.OnCommand := CommandCAPA;
  LCmd.Description.Text := 'Syntax: CAPA (get capabilities)';

  //QUIT <CRLF>
  LCmd := CommandHandlers.Add;
  LCmd.Command := 'QUIT';    {Do not Localize}
  LCmd.Disconnect := True;
  LCmd.NormalReply.SetReply(221,RSFTPQuitGoodby);    {Do not Localize}
  LCmd.Description.Text := 'Syntax: QUIT (terminate service)'; {do not localize}

end;

procedure TIdPOP4Server.InitComponent;
begin
  inherited;
  FContextClass := TIdPOP4ServerContext;
  FRegularProtPort := POP4_PORT;
  DefaultPort := POP4_PORT;
  Self.Greeting.Code := '200';
  Self.Greeting.Text.Text := 'Your text goes here!!!';
end;

procedure TIdPOP4Server.CommandCAPA(ASender: TIdCommand);
begin
  ASender.Reply.SetReply(211, RSPOP3SvrCapaList);
  ASender.SendReply;
  If (IOHandler is TIdServerIOHandlerSSLBase) and
    (FUseTLS in ExplicitTLSVals) Then
  begin
    ASender.Context.Connection.IOHandler.WriteLn('STARTTLS'); {do not localize}
  end;
  if Assigned(FOnUserLogin) then
  begin
    ASender.Context.Connection.IOHandler.WriteLn('AUTH LOGIN');    {Do not Localize}
  end;
  ASender.Context.Connection.IOHandler.WriteLn('.');
  TIdPOP4ServerContext(ASender.Context).CheckPipeLine;
end;

procedure TIdPOP4Server.CommandSTARTTLS(ASender: TIdCommand);
var LIO : TIdSSLIOHandlerSocketBase;
begin
  if (IOHandler is TIdServerIOHandlerSSLBase) and (FUseTLS in ExplicitTLSVals) then begin
    if TIdPOP4ServerContext(ASender.Context).UsingTLS then begin // we are already using TLS
      InvalidSyntax(ASender);
      Exit;
    end;
    if TIdPOP4ServerContext(ASender.Context).State<>Auth then begin //STLS only allowed in auth-state
      ASender.Reply.SetReply(501, RSPOP3SvrNotInThisState);    {Do not Localize}
      Exit;
    end;
    ASender.Reply.SetReply(220, RSPOP3SvrBeginTLSNegotiation);
    //You should never pipeline STARTTLS
    TIdPOP4ServerContext(ASender.Context).PipeLining := False;
    LIO := ASender.Context.Connection.IOHandler as TIdSSLIOHandlerSocketBase;
    LIO.Passthrough := False;
  end else begin
    CmdSyntaxError(ASender);
  end;
end;

procedure TIdPOP4Server.CmdBadSequenceError(ASender: TIdCommand);
begin
  ASender.Reply.SetReply(503, RSSMTPSvrBadSequence);
end;

procedure TIdPOP4Server.CmdSyntaxError(AContext: TIdContext; ALine: string; const AReply: TIdReply = nil);
var
  LTmp : String;
  LReply : TIdReply;
begin
  //First make the first word uppercase
  LTmp := UpCaseFirstWord(ALine);
  try
    if Assigned(AReply) then begin
      LReply := AReply;
    end else begin
      LReply := FReplyClass.Create(nil, ReplyTexts);
      LReply.Assign(ReplyUnknownCommand);
    end;
    LReply.SetReply(500, Sys.Format(RSFTPCmdNotRecognized, [LTmp]));
    AContext.Connection.IOHandler.Write(LReply.FormattedReply);
  finally
    if not Assigned(AReply) then begin
      Sys.FreeAndNil(LReply);
    end;
  end;
end;

procedure TIdPOP4Server.CmdSyntaxError(ASender: TIdCommand);
begin
  CmdSyntaxError(ASender.Context, ASender.RawLine, FReplyUnknownCommand );
  ASender.PerformReply := False;
end;

procedure TIdPOP4Server.CmdMustUseTLS(ASender: TIdCommand);
begin
  ASender.Reply.SetReply(530,RSSMTPSvrReqSTARTTLS);
end;

procedure TIdPOP4Server.InvalidSyntax(ASender: TIdCommand);
begin
  ASender.Reply.SetReply( 501,RSPOP3SvrInvalidSyntax);
end;

procedure TIdPOP4Server.DoReplyUnknownCommand(AContext: TIdContext;
  ALine: string);
begin
  CmdSyntaxError(AContext,ALine);
end;

function TIdPOP4Server.DoAuthLogin(ASender: TIdCommand;
  const Login: string): Boolean;
var
  S: string;
  LUsername, LPassword: string;
  LAuthFailed: Boolean;
  LAccepted: Boolean;
  LS : TIdPOP4ServerContext;
begin
  LS := ASender.Context as TIdPOP4ServerContext;
  Result := False;
  LAuthFailed := False;
  TIdPOP4ServerContext(ASender.Context).PipeLining := False;
  if UpperCase(Login) = 'LOGIN' then    {Do not Localize}
  begin // LOGIN USING THE LOGIN AUTH - BASE64 ENCODED
    s := TIdEncoderMIME.EncodeString('Username:');     {Do not Localize}
    //  s := SendRequest( '334 ' + s );    {Do not Localize}
    ASender.Reply.SetReply (334, s);    {Do not Localize}
    ASender.SendReply;
    s := Trim(ASender.Context.Connection.IOHandler.ReadLn);
    if s <> '' then    {Do not Localize}
    begin
      try
        s := TIdDecoderMIME.DecodeString(s);

        LUsername := s;
        // What? Endcode this string literal?
        s := TIdEncoderMIME.EncodeString('Password:');    {Do not Localize}
        //    s := SendRequest( '334 ' + s );    {Do not Localize}
        ASender.Reply.SetReply(334, s);    {Do not Localize}
        ASender.SendReply;
        s := Trim(ASender.Context.Connection.IOHandler.ReadLn);
        if Length(s) = 0 then
        begin
          LAuthFailed := True;
        end
        else
        begin
          LPassword := TIdDecoderMIME.DecodeString(s);
        end;
      // when TIdDecoderMime.DecodeString(s) raise a exception,catch it and set AuthFailed as true
      except
        LAuthFailed := true;
      end;
    end
    else
    begin
      LAuthFailed := True;
    end;
  end;

  // Add other login units here

  if LAuthFailed then
  begin
    Result := False;
    CmdAuthFailed(ASender);
  end
  else
  begin
    LAccepted := False;
    if Assigned(fOnUserLogin) then
    begin
      fOnUserLogin(LS,  LUsername, LPassword, LAccepted);
    end
    else
    begin
      LAccepted := True;
    end;
    if LAccepted then
    begin
      LS.FState := Trans;
    end;
    LS.Username := LUsername;
    if not LAccepted then
    begin
      CmdAuthFailed(ASender);
    end
    else
    begin
      ASender.Reply.SetReply(235,' welcome ' + Trim(LUsername));    {Do not Localize}
      ASender.SendReply;
    end;
  end;
end;

procedure TIdPOP4Server.CmdAuthFailed(ASender: TIdCommand);
begin
  ASender.Reply.SetReply(535,RSSMTPSvrAuthFailed);
  ASender.SendReply;
end;

procedure TIdPOP4Server.CommandAUTH(ASender: TIdCommand);
var
  Login: string;
begin
  //Note you can not use PIPELINING with AUTH
  TIdPOP4ServerContext(ASender.Context).PipeLining := False;
  if not ((FUseTLS=utUseRequireTLS) and not TIdSMTPServerContext(ASender.Context).UsingTLS) then
  begin
     Login := ASender.UnparsedParams;
     DoAuthLogin(ASender, Login);
  end
  else
  begin
    MustUseTLS(ASender);
  end;
end;

{ TIdPOP4ServerContext }

procedure TIdPOP4ServerContext.CheckPipeLine;
begin
  if Connection.IOHandler.InputBufferIsEmpty=False then
  begin
    PipeLining := True;
  end;
end;

constructor TIdPOP4ServerContext.Create(AConnection: TIdTCPConnection;
  AYarn: TIdYarn; AList: TThreadList);
begin
  inherited;
  FState := Auth;
  FUser := '';
  fPassword := '';
end;

function TIdPOP4ServerContext.GetUsingTLS: boolean;
begin
  Result:=Connection.IOHandler is TIdSSLIOHandlerSocketBase;
  if result then
  begin
    Result:=not TIdSSLIOHandlerSocketBase(Connection.IOHandler).PassThrough;
  end;
end;

procedure TIdPOP4ServerContext.SetPipeLining(const AValue: Boolean);
begin
  if AValue and (not PipeLining) then
  begin
    Connection.IOHandler.WriteBufferOpen;
  end
  else if (not AValue) and PipeLining then
  begin
    Connection.IOHandler.WriteBufferClose;
  end;
  FPipeLining := AValue;
end;

end.
