{
  $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.15    10/26/2004 10:27:42 PM  JPMugaas
  Updated refs.

  Rev 1.14    27.08.2004 22:03:58  Andreas Hausladen
  speed optimization ("const" for string parameters)

  Rev 1.13    8/10/04 1:41:00 PM  RLebeau
  Added FreeSourceStream property to TIdMessageDecoder

  Rev 1.12    7/23/04 6:43:26 PM  RLebeau
  Added extra exception handling to Encode()

  Rev 1.11    29/05/2004 21:22:40  CCostelloe
  Added support for decoding attachments with a Content-Transfer-Encoding of
  binary

  Rev 1.10    2004.05.20 1:39:12 PM  czhower
  Last of the IdStream updates

  Rev 1.9    2004.05.20 11:36:56 AM  czhower
  IdStreamVCL

  Rev 1.8    2004.05.20 11:12:58 AM  czhower
  More IdStream conversions

  Rev 1.7    2004.05.19 3:06:38 PM  czhower
  IdStream / .NET fix

  Rev 1.6    2004.02.03 5:44:02 PM  czhower
  Name changes

  Rev 1.5    1/21/2004 1:17:20 PM  JPMugaas
  InitComponent

  Rev 1.4    10/11/2003 4:40:24 PM  BGooijen
  Fix for DotNet

  Rev 1.3    10/10/2003 10:42:54 PM  BGooijen
  DotNet

  Rev 1.2    26/09/2003 01:04:22  CCostelloe
  Minor change, if any

  Rev 1.1    07/08/2003 00:46:46  CCostelloe
  Function ReadLnSplit added

  Rev 1.0    11/13/2002 07:57:04 AM  JPMugaas
}

unit IdMessageCoder;

interface

{$i IdCompilerDefines.inc}

uses
  Classes,
  IdComponent,
  IdGlobal,
  IdMessage,
  IdBaseComponent;

type
  TIdMessageCoderPartType = (mcptText, mcptAttachment, mcptIgnore, mcptEOF);

  TIdMessageDecoder = class(TIdComponent)
  protected
    FFilename: string;
    FFreeSourceStream: Boolean;
    // Dont use TIdHeaderList for FHeaders - we dont know that they will all be like MIME.
    FHeaders: TStrings;
    FPartType: TIdMessageCoderPartType;
    FSourceStream: TStream;
    procedure InitComponent; override;
  public
    function ReadBody(ADestStream: TStream; var AMsgEnd: Boolean): TIdMessageDecoder; virtual; abstract;
    procedure ReadHeader; virtual;
    //CC: ATerminator param added because Content-Transfer-Encoding of binary needs
    //an ATerminator of EOL...
    function ReadLn(const ATerminator: string = LF; AByteEncoding: IIdTextEncoding = nil
      {$IFDEF STRING_IS_ANSI}; ADestEncoding: IIdTextEncoding = nil{$ENDIF}
      ): string;
    //RLebeau: added for RFC 822 retrieves
    function ReadLnRFC(var VMsgEnd: Boolean; AByteEncoding: IIdTextEncoding = nil
      {$IFDEF STRING_IS_ANSI}; ADestEncoding: IIdTextEncoding = nil{$ENDIF}
      ): String; overload;
    function ReadLnRFC(var VMsgEnd: Boolean; const ALineTerminator: String;
      const ADelim: String = '.'; AByteEncoding: IIdTextEncoding = nil
      {$IFDEF STRING_IS_ANSI}; ADestEncoding: IIdTextEncoding = nil{$ENDIF}
      ): String; overload; {do not localize}
    destructor Destroy; override;
    //
    property Filename: string read FFilename;
    property FreeSourceStream: Boolean read FFreeSourceStream write FFreeSourceStream;
    property Headers: TStrings read FHeaders;
    property PartType: TIdMessageCoderPartType read FPartType;
    property SourceStream: TStream read FSourceStream write FSourceStream;
  end;

  TIdMessageDecoderInfo = class
  public
    function CheckForStart(ASender: TIdMessage; const ALine: string): TIdMessageDecoder; virtual;
     abstract;
    constructor Create; virtual;
  end;

  TIdMessageDecoderList = class
  protected
    FMessageCoders: TStrings;
  public
    class function ByName(const AName: string): TIdMessageDecoderInfo;
    class function CheckForStart(ASender: TIdMessage; const ALine: string): TIdMessageDecoder;
    constructor Create;
    destructor Destroy; override;
    class procedure RegisterDecoder(const AMessageCoderName: string;
     AMessageCoderInfo: TIdMessageDecoderInfo);
  end;

  TIdMessageEncoder = class(TIdComponent)
  protected
    FFilename: string;
    FPermissionCode: integer;
    //
    procedure InitComponent; override;
  public
    procedure Encode(const AFilename: string; ADest: TStream); overload;
    procedure Encode(ASrc: TStream; ADest: TStrings); overload;
    procedure Encode(ASrc: TStream; ADest: TStream); overload; virtual; abstract;
  published
    property Filename: string read FFilename write FFilename;
    property PermissionCode: integer read FPermissionCode write FPermissionCode;
  end;

  TIdMessageEncoderClass = class of TIdMessageEncoder;

  TIdMessageEncoderInfo = class
  protected
    FMessageEncoderClass: TIdMessageEncoderClass;
  public
    constructor Create; virtual;
    procedure InitializeHeaders(AMsg: TIdMessage); virtual;
    //
    property MessageEncoderClass: TIdMessageEncoderClass read FMessageEncoderClass;
  end;

  TIdMessageEncoderList = class
  protected
    FMessageCoders: TStrings;
  public
    class function ByName(const AName: string): TIdMessageEncoderInfo;
    constructor Create;
    destructor Destroy; override;
    class procedure RegisterEncoder(const AMessageEncoderName: string;
     AMessageEncoderInfo: TIdMessageEncoderInfo);
  end;

implementation

uses
  IdException, IdResourceStringsProtocols,
  IdTCPStream, IdBuffer, SysUtils;

var
  GMessageDecoderList: TIdMessageDecoderList = nil;
  GMessageEncoderList: TIdMessageEncoderList = nil;

{ TIdMessageDecoderList }

class function TIdMessageDecoderList.ByName(const AName: string): TIdMessageDecoderInfo;
var
  I: Integer;
begin
  Result := nil;
  if GMessageDecoderList <> nil then begin
    I := GMessageDecoderList.FMessageCoders.IndexOf(AName);
    if I <> -1 then begin
      Result := TIdMessageDecoderInfo(GMessageDecoderList.FMessageCoders.Objects[I]);
    end;
  end;
  if Result = nil then begin
    raise EIdException.Create(RSMessageDecoderNotFound + ': ' + AName);    {Do not Localize}
  end;
end;

class function TIdMessageDecoderList.CheckForStart(ASender: TIdMessage; const ALine: string): TIdMessageDecoder;
var
  i: integer;
begin
  Result := nil;
  if GMessageDecoderList <> nil then begin
    for i := 0 to GMessageDecoderList.FMessageCoders.Count - 1 do begin
      Result := TIdMessageDecoderInfo(GMessageDecoderList.FMessageCoders.Objects[i]).CheckForStart(ASender, ALine);
      if Result <> nil then begin
        Break;
      end;
    end;
  end;
end;

constructor TIdMessageDecoderList.Create;
begin
  inherited;
  FMessageCoders := TStringList.Create;
end;

destructor TIdMessageDecoderList.Destroy;
{$IFNDEF USE_OBJECT_ARC}
var
  i: integer;
{$ENDIF}
begin
  {$IFNDEF USE_OBJECT_ARC}
  for i := 0 to FMessageCoders.Count - 1 do begin
    TIdMessageDecoderInfo(FMessageCoders.Objects[i]).Free;
  end;
  {$ENDIF}
  FreeAndNil(FMessageCoders);
  inherited Destroy;
end;

class procedure TIdMessageDecoderList.RegisterDecoder(const AMessageCoderName: string;
 AMessageCoderInfo: TIdMessageDecoderInfo);
begin
  if GMessageDecoderList = nil then begin
    GMessageDecoderList := TIdMessageDecoderList.Create;
  end;
  GMessageDecoderList.FMessageCoders.AddObject(AMessageCoderName, AMessageCoderInfo);
end;

{ TIdMessageDecoderInfo }

constructor TIdMessageDecoderInfo.Create;
begin
  inherited Create;
end;

{ TIdMessageDecoder }

procedure TIdMessageDecoder.InitComponent;
begin
  inherited;
  FFreeSourceStream := True;
  FHeaders := TStringList.Create;
end;

destructor TIdMessageDecoder.Destroy;
begin
  FreeAndNil(FHeaders);
  if FFreeSourceStream then begin
    FreeAndNil(FSourceStream);
  end else begin
    FSourceStream := nil;
  end;
  inherited Destroy;
end;

procedure TIdMessageDecoder.ReadHeader;
begin
end;

// this is copied from TIdIOHandler.ReadLn() and then adjusted to read from
// a TStream, with the same sematics as Idglobal.ReadLnFromStream() but with
// support for searching for a caller-specified terminator.
function DoReadLnFromStream(AStream: TStream; ATerminator: string;
  AMaxLineLength: Integer = -1; AByteEncoding: IIdTextEncoding = nil
  {$IFDEF STRING_IS_ANSI}; ADestEncoding: IIdTextEncoding = nil{$ENDIF}
  ): string;
const
  LBUFMAXSIZE = 2048;
var
  LBuffer: TIdBuffer;
  LSize: Integer;
  LStartPos: Integer;
  LTermPos: Integer;
  LTerm, LTemp: TIdBytes;
  LStrmStartPos, LStrmPos, LStrmSize: TIdStreamSize;
begin
  Assert(AStream<>nil);
  LTerm := nil; // keep the compiler happy

  { we store the stream size for the whole routine to prevent
  so do not incur a performance penalty with TStream.Size.  It has
  to use something such as Seek each time the size is obtained}
  {4 seek vs 3 seek}
  LStrmStartPos := AStream.Position;
  LStrmPos := LStrmStartPos;
  LStrmSize := AStream.Size;

  if LStrmPos >= LStrmSize then begin
    Result := '';
    Exit;
  end;

  SetLength(LTemp, LBUFMAXSIZE);
  LBuffer := TIdBuffer.Create;
  try
    EnsureEncoding(AByteEncoding);
    {$IFDEF STRING_IS_ANSI}
    EnsureEncoding(ADestEncoding, encOSDefault);
    {$ENDIF}
    if AMaxLineLength < 0 then begin
      AMaxLineLength := MaxInt;
    end;
    // User may pass '' if they need to pass arguments beyond the first.
    if ATerminator = '' then begin
      ATerminator := LF;
    end;
    LTerm := ToBytes(ATerminator, AByteEncoding
      {$IFDEF STRING_IS_ANSI}, ADestEncoding{$ENDIF}
      );
    LTermPos := -1;
    LStartPos := 0;
    repeat
      LSize := IndyMin(LStrmSize - LStrmPos, LBUFMAXSIZE);
      LSize := ReadTIdBytesFromStream(AStream, LTemp, LSize);
      if LSize < 1 then begin
        LStrmPos := LStrmStartPos + LBuffer.Size;
        Break;
      end;
      Inc(LStrmPos, LSize);
      LBuffer.Write(LTemp, LSize, 0);

      LTermPos := LBuffer.IndexOf(LTerm, LStartPos);
      if LTermPos > -1 then begin
        if (AMaxLineLength > 0) and (LTermPos > AMaxLineLength) then begin
          LStrmPos := LStrmStartPos + AMaxLineLength;
          LTermPos := AMaxLineLength;
        end else begin
          LStrmPos := LStrmStartPos + LTermPos + Length(LTerm);
        end;
        Break;
      end;

      LStartPos := IndyMax(LBuffer.Size-(Length(LTerm)-1), 0);
      if (AMaxLineLength > 0) and (LStartPos >= AMaxLineLength) then begin
        LStrmPos := LStrmStartPos + AMaxLineLength;
        LTermPos := AMaxLineLength;
        Break;
      end;
    until LStrmPos >= LStrmSize;

    // Extract actual data
    if (ATerminator = LF) and (LTermPos > 0) and (LTermPos < LBuffer.Size) then begin
      if (LBuffer.PeekByte(LTermPos) = Ord(LF)) and
         (LBuffer.PeekByte(LTermPos-1) = Ord(CR)) then begin
        Dec(LTermPos);
      end;
    end;

    AStream.Position := LStrmPos;
    Result := LBuffer.ExtractToString(LTermPos, AByteEncoding
      {$IFDEF STRING_IS_ANSI}, ADestEncoding{$ENDIF}
      );
  finally
    LBuffer.Free;
  end;
end;

function TIdMessageDecoder.ReadLn(const ATerminator: string = LF;
  AByteEncoding: IIdTextEncoding = nil
  {$IFDEF STRING_IS_ANSI}; ADestEncoding: IIdTextEncoding = nil{$ENDIF}
  ): string;
begin
  if SourceStream is TIdTCPStream then begin
    Result := TIdTCPStream(SourceStream).Connection.IOHandler.ReadLn(
      ATerminator, IdTimeoutDefault, -1, AByteEncoding{$IFDEF STRING_IS_ANSI}, ADestEncoding{$ENDIF}
      );
  end else begin
    Result := DoReadLnFromStream(SourceStream, ATerminator, -1, AByteEncoding
      {$IFDEF STRING_IS_ANSI}, ADestEncoding{$ENDIF}
      );
  end;
end;

function TIdMessageDecoder.ReadLnRFC(var VMsgEnd: Boolean;
  AByteEncoding: IIdTextEncoding = nil
  {$IFDEF STRING_IS_ANSI}; ADestEncoding: IIdTextEncoding = nil{$ENDIF}
  ): String;
begin
  Result := ReadLnRFC(VMsgEnd, LF, '.', AByteEncoding{$IFDEF STRING_IS_ANSI}, ADestEncoding{$ENDIF}); {do not localize}
end;

function TIdMessageDecoder.ReadLnRFC(var VMsgEnd: Boolean; const ALineTerminator: String;
  const ADelim: String = '.'; AByteEncoding: IIdTextEncoding = nil
  {$IFDEF STRING_IS_ANSI}; ADestEncoding: IIdTextEncoding = nil{$ENDIF}
  ): String;
begin
  Result := ReadLn(ALineTerminator, AByteEncoding{$IFDEF STRING_IS_ANSI}, ADestEncoding{$ENDIF});
  // Do not use ATerminator since always ends with . (standard)
  if Result = ADelim then {do not localize}
  begin
    VMsgEnd := True;
    Exit;
  end;
  if TextStartsWith(Result, '..') then begin {do not localize}
    IdDelete(Result, 1, 1);
  end;
  VMsgEnd := False;
end;


{ TIdMessageEncoderInfo }

constructor TIdMessageEncoderInfo.Create;
begin
  inherited Create;
end;

procedure TIdMessageEncoderInfo.InitializeHeaders(AMsg: TIdMessage);
begin
//
end;

{ TIdMessageEncoderList }

class function TIdMessageEncoderList.ByName(const AName: string): TIdMessageEncoderInfo;
var
  I: Integer;
begin
  Result := nil;
  if GMessageEncoderList <> nil then begin
    I := GMessageEncoderList.FMessageCoders.IndexOf(AName);
    if I <> -1 then begin
      Result := TIdMessageEncoderInfo(GMessageEncoderList.FMessageCoders.Objects[I]);
    end;
  end;
  if Result = nil then begin
    raise EIdException.Create(RSMessageEncoderNotFound + ': ' + AName);    {Do not Localize}
  end;
end;

constructor TIdMessageEncoderList.Create;
begin
  inherited;
  FMessageCoders := TStringList.Create;
end;

destructor TIdMessageEncoderList.Destroy;
{$IFNDEF USE_OBJECT_ARC}
var
  i: integer;
{$ENDIF}
begin
  {$IFNDEF USE_OBJECT_ARC}
  for i := 0 to FMessageCoders.Count - 1 do begin
    TIdMessageEncoderInfo(FMessageCoders.Objects[i]).Free;
  end;
  {$ENDIF}
  FreeAndNil(FMessageCoders);
  inherited Destroy;
end;

class procedure TIdMessageEncoderList.RegisterEncoder(const AMessageEncoderName: string;
 AMessageEncoderInfo: TIdMessageEncoderInfo);
begin
  if GMessageEncoderList = nil then begin
    GMessageEncoderList := TIdMessageEncoderList.Create;
  end;
  GMessageEncoderList.FMessageCoders.AddObject(AMessageEncoderName, AMessageEncoderInfo);
end;

{ TIdMessageEncoder }

procedure TIdMessageEncoder.Encode(const AFilename: string; ADest: TStream);
var
  LSrcStream: TStream;
begin
  LSrcStream := TIdReadFileExclusiveStream.Create(AFileName); try
    Encode(LSrcStream, ADest);
  finally FreeAndNil(LSrcStream); end;
end;

procedure TIdMessageEncoder.Encode(ASrc: TStream; ADest: TStrings);
var
  LDestStream: TStream;
begin
  // TODO: provide an Encode() implementation that can save its output directly
  // to ADest without having to waste memory encoding the data entirely to
  // memory first. In Delphi 2009+ in particular, TStrings.LoadFromStream()
  // wastes a lot of memory handling large streams...
  LDestStream := TMemoryStream.Create; try
    Encode(ASrc, LDestStream);
    LDestStream.Position := 0;
    ADest.LoadFromStream(LDestStream);
  finally FreeAndNil(LDestStream); end;
end;

procedure TIdMessageEncoder.InitComponent;
begin
  inherited InitComponent;
  FPermissionCode := 660;
end;

initialization
finalization
  FreeAndNil(GMessageDecoderList);
  FreeAndNil(GMessageEncoderList);
end.
