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


  Prior revision history:

  Rev 1.17    2/8/05 6:07:16 PM  RLebeau
    Removed AddToInternalBuffer() method, using new AppendString() function
    from IdGlobal instead

  Rev 1.16    10/26/2004 10:29:30 PM  JPMugaas
    Updated refs.

  Rev 1.15    7/16/04 12:02:16 PM  RLebeau
    Reverted FileName fields to not strip off folder paths anymore.

  Rev 1.14    7/5/04 1:19:06 PM  RLebeau
    Updated IdRead() to check the calculated byte count before copying data
    into the caller's buffer.

  Rev 1.13    5/31/04 9:28:58 PM  RLebeau
    Updated FileName fields to strip off folder paths.
    Added "Content-Transfer-Encoding" header to file fields
    Updated "Content-Type" headers to be the appropriate media types when
    applicable

  Rev 1.12    5/30/04 7:39:02 PM  RLebeau
    Moved FormatField() method from TIdMultiPartFormDataStream to
    TIdFormDataField instead
    Misc. tweaks and bug fixes

  Rev 1.11    2004.05.20 11:37:02 AM  czhower
    IdStreamVCL

  Rev 1.10    3/1/04 8:57:34 PM  RLebeau
    Format() fixes for TIdMultiPartFormDataStream.FormatField() and
    TIdFormDataField.GetFieldSize().

  Rev 1.9    2004.02.03 5:44:08 PM  czhower
    Name changes

  Rev 1.8    2004.02.03 2:12:16 PM  czhower
    $I path change

  Rev 1.7    25/01/2004 21:56:42  CCostelloe
    Updated IdSeek to use new IdFromBeginning

  Rev 1.6    24/01/2004 19:26:56  CCostelloe
    Cleaned up warnings

  Rev 1.5    22/11/2003 12:05:26 AM  GGrieve
    Get working on both win32 and DotNet after other DotNet changes

  Rev 1.4    11/10/2003 8:03:54 PM  BGooijen
    Did all todo's ( TStream to TIdStream mainly )

  Rev 1.3    2003.10.24 10:43:12 AM  czhower
    TIdSTream to dos

  Rev 1.2    10/17/2003 12:49:52 AM  DSiders
    Added localization comments.
    Added resource string for unsupported operation exception.

  Rev 1.1    10/7/2003 10:07:06 PM  GGrieve
    Get HTTP compiling for DotNet

  Rev 1.0    11/13/2002 07:57:42 AM  JPMugaas
    Initial version control checkin.

  2001-Nov-23
    changed spelling error from XxxDataFiled to XxxDataField

  2001-Nov Doychin Bondzhev
    Now it descends from TStream and does not do buffering.
    Changes in the way the form parts are added to the stream.
}

unit IdMultipartFormData;

{
  Implementation of the Multipart Form data

  Based on Internet standards outlined in:
    RFC 1867 - Form-based File Upload in HTML
    RFC 2388 - Returning Values from Forms:  multipart/form-data

  Author: Shiv Kumar
}

interface

{$I IdCompilerDefines.inc}

uses
  Classes,
  IdGlobal,
  IdException,
  IdCharsets,
  IdCoderHeader,
  IdResourceStringsProtocols;

const
  sContentTypeFormData = 'multipart/form-data; boundary=';            {do not localize}
  sContentTypeOctetStream = 'application/octet-stream';               {do not localize}
  sContentTypeTextPlain = 'text/plain';                               {do not localize}
  CRLF = #13#10;
  sContentDispositionPlaceHolder = 'Content-Disposition: form-data; name="%s"';  {do not localize}
  sFileNamePlaceHolder = '; filename="%s"';                           {do not localize}
  sContentTypePlaceHolder = 'Content-Type: %s';                       {do not localize}
  sCharsetPlaceHolder = '; charset="%s"';                             {do not localize}
  sContentTransferPlaceHolder = 'Content-Transfer-Encoding: %s';      {do not localize}
  sContentTransferQuotedPrintable = 'quoted-printable';               {do not localize}
  sContentTransferBinary = 'binary';                                  {do not localize}

type
  TIdMultiPartFormDataStream = class;

  TIdFormDataField = class(TCollectionItem)
  protected
    FFileName: string;
    FCharset: string;
    FContentType: string;
    FContentTransfer: string;
    FFieldName: string;
    FFieldStream: TStream;
    FFieldValue: String;
    FCanFreeFieldStream: Boolean;
    FHeaderCharSet: string;
    FHeaderEncoding: Char;

    function FormatHeader: string;
    function PrepareDataStream(var VCanFree: Boolean): TStream;

    function GetFieldSize: Int64;
    function GetFieldStream: TStream;
    function GetFieldValue: string;
    procedure SetCharset(const Value: string);
    procedure SetContentType(const Value: string);
    procedure SetContentTransfer(const Value: string);
    procedure SetFieldName(const Value: string);
    procedure SetFieldStream(const Value: TStream);
    procedure SetFieldValue(const Value: string);
    procedure SetFileName(const Value: string);
    procedure SetHeaderCharSet(const Value: string);
    procedure SetHeaderEncoding(const Value: Char);
  public
    constructor Create(Collection: TCollection); override;
    destructor Destroy; override;
    // procedure Assign(Source: TPersistent); override;
    property ContentTransfer: string read FContentTransfer write SetContentTransfer;
    property ContentType: string read FContentType write SetContentType;
    property Charset: string read FCharset write SetCharset;
    property FieldName: string read FFieldName write SetFieldName;
    property FieldStream: TStream read GetFieldStream write SetFieldStream;
    property FileName: string read FFileName write SetFileName;
    property FieldValue: string read GetFieldValue write SetFieldValue;
    property FieldSize: Int64 read GetFieldSize;
    property HeaderCharSet: string read FHeaderCharSet write SetHeaderCharSet;
    property HeaderEncoding: Char read FHeaderEncoding write SetHeaderEncoding;
  end;

  TIdFormDataFields = class(TCollection)
  protected
    FParentStream: TIdMultiPartFormDataStream;
    function GetFormDataField(AIndex: Integer): TIdFormDataField;
  public
    constructor Create(AMPStream: TIdMultiPartFormDataStream);
    function Add: TIdFormDataField;
    property MultipartFormDataStream: TIdMultiPartFormDataStream read FParentStream;
    property Items[AIndex: Integer]: TIdFormDataField read GetFormDataField;
  end;

  TIdMultiPartFormDataStream = class(TIdBaseStream)
  protected
    FInputStream: TStream;
    FFreeInputStream: Boolean;
    FBoundary: string;
    FRequestContentType: string;
    FCurrentItem: integer;
    FInitialized: Boolean;
    FInternalBuffer: TIdBytes;

    FPosition: Int64;
    FSize: Int64;

    FFields: TIdFormDataFields;

    function GenerateUniqueBoundary: string;
    procedure CalculateSize;

    function IdRead(var VBuffer: TIdBytes; AOffset, ACount: Longint): Longint; override;
    function IdWrite(const ABuffer: TIdBytes; AOffset, ACount: Longint): Longint; override;
    function IdSeek(const AOffset: Int64; AOrigin: TSeekOrigin): Int64; override;
    procedure IdSetSize(ASize : Int64); override;
  public
    constructor Create;
    destructor Destroy; override;

    function AddFormField(const AFieldName, AFieldValue: string; const ACharset: string = ''; const AContentType: string = ''; const AFileName: string = ''): TIdFormDataField; overload;
    function AddFormField(const AFieldName, AContentType, ACharset: string; AFieldValue: TStream; const AFileName: string = ''): TIdFormDataField; overload;
    function AddObject(const AFieldName, AContentType, ACharset: string; AFileData: TObject; const AFileName: string = ''): TIdFormDataField; {$IFDEF HAS_DEPRECATED}deprecated{$IFDEF HAS_DEPRECATED_MSG} 'Use overloaded version of AddFormField()'{$ENDIF};{$ENDIF}
    function AddFile(const AFieldName, AFileName: String; const AContentType: string = ''): TIdFormDataField;

    procedure Clear;
    
    property Boundary: string read FBoundary;
    property RequestContentType: string read FRequestContentType;
  end;

  EIdInvalidObjectType = class(EIdException);
  EIdUnsupportedOperation = class(EIdException);
  EIdUnsupportedTransfer = class(EIdException);
  EIdUnsupportedEncoding = class(EIdException);

implementation

uses
  SysUtils,
  IdCoderQuotedPrintable,
  IdCoderMIME,
  IdStream,
  IdGlobalProtocols;

const
  cAllowedContentTransfers: array[0..4] of String = (
    '7bit', '8bit', 'binary', 'quoted-printable', 'base64' {do not localize}
    );

  cAllowedHeaderEncodings: array[0..2] of String = (
    'Q', 'B', '8' {do not localize}
    );

{ TIdMultiPartFormDataStream }

constructor TIdMultiPartFormDataStream.Create;
begin
  inherited Create;
  FSize := 0;
  FInitialized := False;
  FBoundary := GenerateUniqueBoundary;
  FRequestContentType := sContentTypeFormData + FBoundary;
  FFields := TIdFormDataFields.Create(Self);
end;

destructor TIdMultiPartFormDataStream.Destroy;
begin
  FreeAndNil(FFields);
  inherited Destroy;
end;

{$I IdDeprecatedImplBugOff.inc}
function TIdMultiPartFormDataStream.AddObject(const AFieldName,
  AContentType, ACharset: string; AFileData: TObject;
  const AFileName: string = ''): TIdFormDataField;
{$I IdDeprecatedImplBugOn.inc}
begin
  if not (AFileData is TStream) then begin
    raise EIdInvalidObjectType.Create(RSMFDInvalidObjectType);
  end;
  Result := AddFormField(AFieldName, AContentType, ACharset, TStream(AFileData), AFileName);
end;

function TIdMultiPartFormDataStream.AddFile(const AFieldName, AFileName: String;
  const AContentType: string = ''): TIdFormDataField;
var
  LStream: TIdReadFileExclusiveStream;
  LItem: TIdFormDataField;
begin
  LStream := TIdReadFileExclusiveStream.Create(AFileName);
  try
    LItem := FFields.Add;
  except
    FreeAndNil(LStream);
    raise;
  end;

  LItem.FFieldName := AFieldName;
  LItem.FFileName := ExtractFileName(AFileName);
  LItem.FFieldStream := LStream;
  LItem.FCanFreeFieldStream := True;
  if AContentType <> '' then begin
    LItem.ContentType := AContentType;
  end else begin
    LItem.FContentType := GetMIMETypeFromFile(AFileName);
  end;
  LItem.FContentTransfer := sContentTransferBinary;

  Result := LItem;
end;

function TIdMultiPartFormDataStream.AddFormField(const AFieldName, AFieldValue: string;
  const ACharset: string = ''; const AContentType: string = ''; const AFileName: string = ''): TIdFormDataField;
var
  LItem: TIdFormDataField;
begin
  LItem := FFields.Add;

  LItem.FFieldName := AFieldName;
  LItem.FFileName := ExtractFileName(AFileName);
  LItem.FFieldValue := AFieldValue;
  if AContentType <> '' then begin
    LItem.ContentType := AContentType;
  end else begin
    LItem.FContentType := sContentTypeTextPlain;
  end;
  if ACharset <> '' then begin
    LItem.FCharset := ACharset;
  end;
  LItem.FContentTransfer := sContentTransferQuotedPrintable;

  Result := LItem;
end;

function TIdMultiPartFormDataStream.AddFormField(const AFieldName, AContentType, ACharset: string;
  AFieldValue: TStream; const AFileName: string = ''): TIdFormDataField;
var
  LItem: TIdFormDataField;
begin
  if not Assigned(AFieldValue) then begin
    raise EIdInvalidObjectType.Create(RSMFDInvalidObjectType);
  end;

  LItem := FFields.Add;

  LItem.FFieldName := AFieldName;
  LItem.FFileName := ExtractFileName(AFileName);
  LItem.FFieldStream := AFieldValue;
  if AContentType <> '' then begin
    LItem.ContentType := AContentType;
  end else begin
    LItem.FContentType := GetMIMETypeFromFile(AFileName);
  end;
  if ACharset <> '' then begin
    LItem.FCharSet := ACharset;
  end;
  LItem.FContentTransfer := sContentTransferBinary;

  Result := LItem;
end;

procedure TIdMultiPartFormDataStream.Clear;
begin
  FInitialized := False;
  FFields.Clear;
  if FFreeInputStream then begin
    FInputStream.Free;
  end;
  FInputStream := nil;
  FFreeInputStream := False;
  FCurrentItem := 0;
  FPosition := 0;
  FSize := 0;
  SetLength(FInternalBuffer, 0);
end;

function TIdMultiPartFormDataStream.GenerateUniqueBoundary: string;
begin
  Result := '--------' + FormatDateTime('mmddyyhhnnsszzz', Now);  {do not localize}
end;

procedure TIdMultiPartFormDataStream.CalculateSize;
var
  I: Integer;
begin
  FSize := 0;
  if FFields.Count > 0 then begin
    for I := 0 to FFields.Count-1 do begin
      FSize := FSize + FFields.Items[I].FieldSize;
    end;
    FSize := FSize + 2{'--'} + Length(Boundary) + 4{'--'+CRLF};
  end;
end;

// RLebeau - IdRead() should wrap multiple files of the same field name
// using a single "multipart/mixed" MIME part, as recommended by RFC 1867

function TIdMultiPartFormDataStream.IdRead(var VBuffer: TIdBytes; AOffset, ACount: Longint): Longint;
var
  LTotalRead, LCount, LBufferCount, LRemaining : Integer;
  LItem: TIdFormDataField;
  LEncoding: IIdTextEncoding;
begin
  if not FInitialized then begin
    FInitialized := True;
    FCurrentItem := 0;
    SetLength(FInternalBuffer, 0);
  end;

  LTotalRead := 0;
  LBufferCount := 0;

  while (LTotalRead < ACount) and ((Length(FInternalBuffer) > 0) or Assigned(FInputStream) or (FCurrentItem < FFields.Count)) do
  begin
    if (Length(FInternalBuffer) = 0) and (not Assigned(FInputStream)) then
    begin
      LItem := FFields.Items[FCurrentItem];
      EnsureEncoding(LEncoding, enc8Bit);
      AppendString(FInternalBuffer, LItem.FormatHeader, -1, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});

      FInputStream := LItem.PrepareDataStream(FFreeInputStream);
      if not Assigned(FInputStream) then begin
        AppendString(FInternalBuffer, CRLF);
        Inc(FCurrentItem);
      end;
    end;

    if Length(FInternalBuffer) > 0 then begin
      LCount := IndyMin(ACount - LBufferCount, Length(FInternalBuffer));
      if LCount > 0 then begin
        LRemaining := Length(FInternalBuffer) - LCount;
        CopyTIdBytes(FInternalBuffer, 0, VBuffer, LBufferCount, LCount);
        if LRemaining > 0 then begin
          CopyTIdBytes(FInternalBuffer, LCount, FInternalBuffer, 0, LRemaining);
        end;
        SetLength(FInternalBuffer, LRemaining);
        LBufferCount := LBufferCount + LCount;
        FPosition := FPosition + LCount;
        LTotalRead := LTotalRead + LCount;
      end;
    end;

    if (LTotalRead < ACount) and (Length(FInternalBuffer) = 0) and Assigned(FInputStream) then begin
      LCount := TIdStreamHelper.ReadBytes(FInputStream, VBuffer, ACount - LTotalRead, LBufferCount);
      if LCount > 0 then begin
        LBufferCount := LBufferCount + LCount;
        LTotalRead := LTotalRead + LCount;
        FPosition := FPosition + LCount;
      end
      else begin
        SetLength(FInternalBuffer, 0);
        if FFreeInputStream then begin
          FInputStream.Free;
        end else begin
          FInputStream.Position := 0;
          AppendString(FInternalBuffer, CRLF);
        end;
        FInputStream := nil;
        FFreeInputStream := False;
        Inc(FCurrentItem);
      end;
    end;

    if (Length(FInternalBuffer) = 0) and (not Assigned(FInputStream)) and (FCurrentItem = FFields.Count) then begin
      AppendString(FInternalBuffer, '--' + Boundary + '--' + CRLF);     {do not localize}
      Inc(FCurrentItem);
    end;
  end;

  Result := LTotalRead;
end;

function TIdMultiPartFormDataStream.IdSeek(const AOffset: Int64; AOrigin: TSeekOrigin): Int64;
begin
  Result := 0;
  case AOrigin of
    soBeginning: begin
      if (AOffset = 0) then begin
        FInitialized := False;
        FPosition := 0;
        Result := 0;
      end else begin
        Result := FPosition;
      end;
    end;
    soCurrent: begin
      Result := FPosition;
    end;
    soEnd: begin
      if (AOffset = 0) then begin
        CalculateSize;
        Result := FSize;
      end else begin
        Result := FPosition;
      end;
    end;
  end;
end;

function TIdMultiPartFormDataStream.IdWrite(const ABuffer: TIdBytes; AOffset, ACount: Longint): Longint;
begin
  raise EIdUnsupportedOperation.Create(RSUnsupportedOperation);
end;

procedure TIdMultiPartFormDataStream.IdSetSize(ASize: Int64);
begin
  raise EIdUnsupportedOperation.Create(RSUnsupportedOperation);
end;

{ TIdFormDataFields }

function TIdFormDataFields.Add: TIdFormDataField;
begin
  Result := TIdFormDataField(inherited Add);
end;

constructor TIdFormDataFields.Create(AMPStream: TIdMultiPartFormDataStream);
begin
  inherited Create(TIdFormDataField);
  FParentStream := AMPStream;
end;

function TIdFormDataFields.GetFormDataField(AIndex: Integer): TIdFormDataField;
begin
  Result := TIdFormDataField(inherited Items[AIndex]);
end;

{ TIdFormDataField }

constructor TIdFormDataField.Create(Collection: TCollection);
var
  LDefCharset: TIdCharSet;
begin
  inherited Create(Collection);
  FFieldStream := nil;
  FFileName := '';
  FFieldName := '';
  FContentType := '';
  FCanFreeFieldStream := False;

  // it's not clear when FHeaderEncoding should be Q not B.
  // Comments welcome on atozedsoftware.indy.general

  LDefCharset := IdGetDefaultCharSet;

  case LDefCharset of
    idcs_ISO_8859_1:
      begin
        FHeaderEncoding := 'Q';     { quoted-printable }    {Do not Localize}
        FHeaderCharSet := IdCharsetNames[LDefCharset];
      end;
    idcs_UNICODE_1_1:
      begin
        FHeaderEncoding := 'B';     { base64 }    {Do not Localize}
        FHeaderCharSet := IdCharsetNames[idcs_UTF_8];
      end;
  else
    begin
      FHeaderEncoding := 'B';     { base64 }    {Do not Localize}
      FHeaderCharSet := IdCharsetNames[LDefCharset];
    end;
  end;
end;

destructor TIdFormDataField.Destroy;
begin
  if Assigned(FFieldStream) then begin
    if FCanFreeFieldStream then begin
      FFieldStream.Free;
    end;
  end;
  inherited Destroy;
end;

function TIdFormDataField.FormatHeader: string;
var
  LBoundary: string;
begin
  LBoundary := '--' + TIdFormDataFields(Collection).MultipartFormDataStream.Boundary; {do not localize}

  // TODO: when STRING_IS_ANSI is defined, provide a way for the user to specify the AnsiString encoding for header values...

  Result := IndyFormat('%s' + CRLF + sContentDispositionPlaceHolder,
    [LBoundary, EncodeHeader(FieldName, '', FHeaderEncoding, FHeaderCharSet)]);       {do not localize}

  if Length(FileName) > 0 then begin
    Result := Result + IndyFormat(sFileNamePlaceHolder,
      [EncodeHeader(FileName, '', FHeaderEncoding, FHeaderCharSet)]);                 {do not localize}
  end;

  Result := Result + CRLF;

  if Length(ContentType) > 0 then begin
    Result := Result + IndyFormat(sContentTypePlaceHolder, [ContentType]);      {do not localize}
    if Length(CharSet) > 0 then begin
      Result := Result + IndyFormat(sCharsetPlaceHolder, [Charset]);            {do not localize}
    end;
    Result := Result + CRLF;
  end;

  if Length(FContentTransfer) > 0 then begin
    Result := Result + IndyFormat(sContentTransferPlaceHolder + CRLF, [FContentTransfer]);
  end;

  Result := Result + CRLF;
end;

function TIdFormDataField.GetFieldSize: Int64;
var
  LStream: TStream;
  LOldPos: TIdStreamSize;
  {$IFDEF STRING_IS_ANSI}
  LBytes: TIdBytes;
  {$ENDIF}
  I: Integer;
begin
  {$IFDEF STRING_IS_ANSI}
  LBytes := nil; // keep the compiler happy
  {$ENDIF}
  Result := Length(FormatHeader);
  if Assigned(FFieldStream) then begin
    I := PosInStrArray(ContentTransfer, cAllowedContentTransfers, False);
    if I <= 2 then begin
      // need to include an explicit CRLF at the end of the data
      Result := Result + FFieldStream.Size + 2{CRLF};
    end else
    begin
      LStream := TIdCalculateSizeStream.Create;
      try
        LOldPos := FFieldStream.Position;
        try
          if I = 3 then begin
            TIdEncoderQuotedPrintable.EncodeStream(FFieldStream, LStream);
            // the encoded text always includes a CRLF at the end...
            Result := Result + LStream.Size {+2};
          end else begin
            TIdEncoderMime.EncodeStream(FFieldStream, LStream);
            // the encoded text does not include a CRLF at the end...
            Result := Result + LStream.Size + 2;
          end;
        finally
          FFieldStream.Position := LOldPos;
        end;
      finally
        LStream.Free;
      end;
    end;
  end
  else if Length(FFieldValue) > 0 then begin
    I := PosInStrArray(FContentTransfer, cAllowedContentTransfers, False);
    if I <= 0 then begin
      // 7bit
      {$IFDEF STRING_IS_UNICODE}
      I := IndyTextEncoding_ASCII.GetByteCount(FFieldValue);
      {$ELSE}
      // the methods useful for calculating a length without actually
      // encoding are protected, so have to actually encode the
      // string to find out the final length...
      LBytes := RawToBytes(FFieldValue[1], Length(FFieldValue));
      CheckByteEncoding(LBytes, CharsetToEncoding(FCharset), IndyTextEncoding_ASCII);
      I := Length(LBytes);
      {$ENDIF}
      // need to include an explicit CRLF at the end of the data
      Result := Result + I + 2{CRLF};
    end
    else if (I = 1) or (I = 2) then begin
      // 8bit/binary
      {$IFDEF STRING_IS_UNICODE}
      I := CharsetToEncoding(FCharset).GetByteCount(FFieldValue);
      {$ELSE}
      I := Length(FFieldValue);
      {$ENDIF}
      // need to include an explicit CRLF at the end of the data
      Result := Result + I + 2{CRLF};
    end else
    begin
      LStream := TIdCalculateSizeStream.Create;
      try
        {$IFNDEF STRING_IS_UNICODE}
        LBytes := RawToBytes(FFieldValue[1], Length(FFieldValue));
        {$ENDIF}
        if I = 3 then begin
          // quoted-printable
          {$IFDEF STRING_IS_UNICODE}
          TIdEncoderQuotedPrintable.EncodeString(FFieldValue, LStream, CharsetToEncoding(FCharset));
          {$ELSE}
          TIdEncoderQuotedPrintable.EncodeBytes(LBytes, LStream);
          {$ENDIF}
          // the encoded text always includes a CRLF at the end...
          Result := Result + LStream.Size {+2};
        end else begin
          // base64
          {$IFDEF STRING_IS_UNICODE}
          TIdEncoderMIME.EncodeString(FFieldValue, LStream, CharsetToEncoding(FCharset){$IFDEF STRING_IS_ANSI}, IndyTextEncoding_OSDefault{$ENDIF});
          {$ELSE}
          TIdEncoderMIME.EncodeBytes(LBytes, LStream);
          {$ENDIF}
          // the encoded text does not include a CRLF at the end...
          Result := Result + LStream.Size + 2;
        end;
      finally
        LStream.Free;
      end;
    end;
  end else begin
    // need to include an explicit CRLF at the end of blank text
    Result := Result + 2{CRLF};
  end;
end;

function TIdFormDataField.PrepareDataStream(var VCanFree: Boolean): TStream;
var
  I: Integer;
  {$IFDEF STRING_IS_ANSI}
  LBytes: TIdBytes;
  {$ENDIF}
begin
  {$IFDEF STRING_IS_ANSI}
  LBytes := nil; // keep the compiler happy
  {$ENDIF}
  Result := nil;
  VCanFree := False;

  if Assigned(FFieldStream) then begin
    FFieldStream.Position := 0;
    I := PosInStrArray(FContentTransfer, cAllowedContentTransfers, False);
    if I <= 2 then begin
      Result := FFieldStream;
    end else begin
      Result := TMemoryStream.Create;
      try
        if I = 3 then begin
          TIdEncoderQuotedPrintable.EncodeStream(FFieldStream, Result);
          // the encoded text always includes a CRLF at the end...
        end else begin
          TIdEncoderMime.EncodeStream(FFieldStream, Result);
          // the encoded text does not include a CRLF at the end...
          WriteStringToStream(Result, CRLF);
        end;
        Result.Position := 0;
      except
        FreeAndNil(Result);
        raise;
      end;
      VCanFree := True;
    end;
  end
  else if Length(FFieldValue) > 0 then begin
    Result := TMemoryStream.Create;
    try
      {$IFDEF STRING_IS_ANSI}
      LBytes := RawToBytes(FFieldValue[1], Length(FFieldValue));
      {$ENDIF}
      I := PosInStrArray(FContentTransfer, cAllowedContentTransfers, False);
      if I <= 0 then begin
        // 7bit
        {$IFDEF STRING_IS_UNICODE}
        WriteStringToStream(Result, FFieldValue, IndyTextEncoding_ASCII);
        {$ELSE}
        CheckByteEncoding(LBytes, CharsetToEncoding(FCharset), IndyTextEncoding_ASCII);
        WriteTIdBytesToStream(Result, LBytes);
        {$ENDIF}
        // need to include an explicit CRLF at the end of the data
        WriteStringToStream(Result, CRLF);
      end
      else if (I = 1) or (I = 2) then begin
        // 8bit/binary
        {$IFDEF STRING_IS_UNICODE}
        WriteStringToStream(Result, FFieldValue, CharsetToEncoding(FCharset));
        {$ELSE}
        WriteTIdBytesToStream(Result, LBytes);
        {$ENDIF}
        // need to include an explicit CRLF at the end of the data
        WriteStringToStream(Result, CRLF);
      end else
      begin
        if I = 3 then begin
          // quoted-printable
          {$IFDEF STRING_IS_UNICODE}
          TIdEncoderQuotedPrintable.EncodeString(FFieldValue, Result, CharsetToEncoding(FCharset));
          {$ELSE}
          TIdEncoderQuotedPrintable.EncodeBytes(LBytes, Result);
          {$ENDIF}
          // the encoded text always includes a CRLF at the end...
        end else begin
          // base64
          {$IFDEF STRING_IS_UNICODE}
          TIdEncoderMIME.EncodeString(FFieldValue, Result, CharsetToEncoding(FCharset));
          {$ELSE}
          TIdEncoderMIME.EncodeBytes(LBytes, Result);
          {$ENDIF}
          // the encoded text does not include a CRLF at the end...
          WriteStringToStream(Result, CRLF);
        end;
      end;
    except
      FreeAndNil(Result);
      raise;
    end;
    Result.Position := 0;
    VCanFree := True;
  end;
end;

function TIdFormDataField.GetFieldStream: TStream;
begin
  if not Assigned(FFieldStream) then begin
    raise EIdInvalidObjectType.Create(RSMFDInvalidObjectType);
  end;
  Result := FFieldStream;
end;

function TIdFormDataField.GetFieldValue: string;
begin
  if Assigned(FFieldStream) then begin
    raise EIdInvalidObjectType.Create(RSMFDInvalidObjectType);
  end;
  Result := FFieldValue;
end;

procedure TIdFormDataField.SetCharset(const Value: string);
begin
  FCharset := Value;
end;

procedure TIdFormDataField.SetContentTransfer(const Value: string);
begin
  if Length(Value) > 0 then begin
    if PosInStrArray(Value, cAllowedContentTransfers, False) = -1 then begin
      raise EIdUnsupportedTransfer.Create(RSMFDInvalidTransfer);
    end;
  end;
  FContentTransfer := Value;
end;

procedure TIdFormDataField.SetContentType(const Value: string);
var
  LContentType, LCharSet: string;
begin
  if Length(Value) > 0 then begin
    LContentType := Value;
  end
  else if Length(FFileName) > 0 then begin
    LContentType := GetMIMETypeFromFile(FFileName);
  end
  else begin
    LContentType := sContentTypeOctetStream;
  end;

  FContentType := RemoveHeaderEntry(LContentType, 'charset', LCharSet, QuoteMIME); {do not localize}

  // RLebeau: per RFC 2045 Section 5.2:
  //
  // Default RFC 822 messages without a MIME Content-Type header are taken
  // by this protocol to be plain text in the US-ASCII character set,
  // which can be explicitly specified as:
  //
  //   Content-type: text/plain; charset=us-ascii
  //
  // This default is assumed if no Content-Type header field is specified.
  // It is also recommend that this default be assumed when a
  // syntactically invalid Content-Type header field is encountered. In
  // the presence of a MIME-Version header field and the absence of any
  // Content-Type header field, a receiving User Agent can also assume
  // that plain US-ASCII text was the sender's intent.  Plain US-ASCII
  // text may still be assumed in the absence of a MIME-Version or the
  // presence of an syntactically invalid Content-Type header field, but
  // the sender's intent might have been otherwise.
  if (LCharSet = '') and IsHeaderMediaType(FContentType, 'text') then begin {do not localize}
    LCharSet := 'us-ascii'; {do not localize}
  end;
  {RLebeau: override the current CharSet only if the header specifies a new value}
  if LCharSet <> '' then begin
    FCharSet := LCharSet;
  end;
end;

procedure TIdFormDataField.SetFieldName(const Value: string);
begin
  FFieldName := Value;
end;

procedure TIdFormDataField.SetFieldStream(const Value: TStream);
begin
  if not Assigned(Value) then begin
    raise EIdInvalidObjectType.Create(RSMFDInvalidObjectType);
  end;

  if Assigned(FFieldStream) and FCanFreeFieldStream then begin
    FFieldStream.Free;
  end;

  FFieldValue := '';
  FFieldStream := Value;
  FCanFreeFieldStream := False;
end;

procedure TIdFormDataField.SetFieldValue(const Value: string);
begin
  if Assigned(FFieldStream) then begin
    if FCanFreeFieldStream then begin
      FFieldStream.Free;
    end;
    FFieldStream := nil;
    FCanFreeFieldStream := False;
  end;
  FFieldValue := Value;
end;

procedure TIdFormDataField.SetFileName(const Value: string);
begin
  FFileName := ExtractFileName(Value);
end;

procedure TIdFormDataField.SetHeaderCharSet(const Value: string);
begin
  FHeaderCharset := Value;
end;

procedure TIdFormDataField.SetHeaderEncoding(const Value: Char);
begin
  if FHeaderEncoding <> Value then begin
    if PosInStrArray(Value, cAllowedHeaderEncodings, False) = -1 then begin
      raise EIdUnsupportedEncoding.Create(RSMFDInvalidEncoding);
    end;
    FHeaderEncoding := Value;
  end;
end;

end.
