{
  $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.85    1/6/05 4:38:30 PM  RLebeau
 Bug fix for decoding Text part headers
}
{
   Rev 1.84    11/30/04 10:44:44 AM  RLebeau
 Bug fix for previous checkin
}
{
   Rev 1.83    11/30/2004 12:10:40 PM  JPMugaas
 Fix for compiler error.
}
{
   Rev 1.82    11/28/04 2:22:04 PM  RLebeau
 Updated a few hard-coded strings to use resource strings instead
}
{
   Rev 1.81    28/11/2004 20:08:14  CCostelloe
 MessagePart.Boundary now (correctly) holds decoded MIME boundary
}
{
   Rev 1.80    11/27/2004 8:58:14 PM  JPMugaas
 Compile errors.
}
{
   Rev 1.79    10/26/2004 10:25:46 PM  JPMugaas
 Updated refs.
}
{
   Rev 1.78    24.09.2004 02:16:48  Andreas Hausladen
 Added ReadTIdBytesFromStream and ReadCharFromStream function to supress .NET
 warnings.
}
{
   Rev 1.77    27.08.2004 22:04:32  Andreas Hausladen
 speed optimization ("const" for string parameters)
 Fixed "blank line multiplication"
}
{
   Rev 1.76    27.08.2004 00:21:32  Andreas Hausladen
 Undo last changes (temporary)
}
{
   Rev 1.75    26.08.2004 22:14:16  Andreas Hausladen
 Fixed last line blank line read/write bug
}
{
   Rev 1.74    7/23/04 7:17:20 PM  RLebeau
 TFileStream access right tweak for ProcessMessage()
}
{
   Rev 1.73    28/06/2004 23:58:12  CCostelloe
 Bug fix
}
{
    Rev 1.72    6/11/2004 9:38:08 AM  DSiders
  Added "Do not Localize" comments.
}
{
   Rev 1.71    2004.06.06 4:53:04 PM  czhower
 Undid 1.70. Not needed, just masked an existing bug and did not fix it.
}
{
   Rev 1.70    06/06/2004 01:23:54  CCostelloe
 OnWork fix
}
{
   Rev 1.69    6/4/04 12:41:56 PM  RLebeau
 ContentTransferEncoding bug fix
}
{
   Rev 1.68    2004.05.20 1:39:08 PM  czhower
 Last of the IdStream updates
}
{
   Rev 1.67    2004.05.20 11:36:52 AM  czhower
 IdStreamVCL
}
{
   Rev 1.66    2004.05.20 11:12:56 AM  czhower
 More IdStream conversions
}
{
   Rev 1.65    2004.05.19 3:06:34 PM  czhower
 IdStream / .NET fix
}
{
   Rev 1.64    19/05/2004 00:54:30  CCostelloe
 Bug fix (though I claim in my defence that it is only a hint fix)
}
{
   Rev 1.63    16/05/2004 18:55:06  CCostelloe
 New TIdText/TIdAttachment processing
}
{
   Rev 1.62    2004.05.03 11:15:16 AM  czhower
 Fixed compile error and added use of constants.
}
{
   Rev 1.61    5/2/04 8:02:12 PM  RLebeau
 Updated TIdIOHandlerStreamMsg to keep track of the last character received
 from the stream so that extra CR LF characters are not added to the end of
 the message data unnecessarily.
}
{
   Rev 1.60    4/23/04 1:54:58 PM  RLebeau
 One more tweak for TIdIOHandlerStreamMsg support
}
{
   Rev 1.59    4/23/04 1:21:16 PM  RLebeau
 Minor tweaks for TIdIOHandlerStreamMsg support
}
{
   Rev 1.58    23/04/2004 20:48:10  CCostelloe
 Added TIdIOHandlerStreamMsg to stop looping if no terminating \r\n.\r\n and
 added support for emails that are attachments only
}
{
   Rev 1.57    2004.04.18 1:39:22 PM  czhower
 Bug fix for .NET with attachments, and several other issues found along the
 way.
}
{
   Rev 1.56    2004.04.16 11:31:00 PM  czhower
 Size fix to IdBuffer, optimizations, and memory leaks


   Rev 1.55    2004.03.07 10:36:08 AM  czhower
 SendMsg now calls OnWork with NoEncode = True


   Rev 1.54    2004.03.04 1:02:58 AM  czhower
 Const removed from arguemtns (1 not needed + 1 incorrect)


   Rev 1.53    2004.03.03 7:18:32 PM  czhower
 Fixed AV bug with ProcessMessage


   Rev 1.52    2004.03.03 11:54:34 AM  czhower
 IdStream change


   Rev 1.51    2/3/04 12:25:50 PM  RLebeau
 Updated WriteTextPart() function inside of SendBody() to write the ContentID
 property is it is assigned.


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


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


   Rev 1.48    1/27/2004 4:04:06 PM  SPerry
 StringStream ->IdStringStream


   Rev 1.47    2004.01.27 12:03:28 AM  czhower
 Properly named a local variable to fix a .net conflict.


   Rev 1.46    1/25/2004 3:52:32 PM  JPMugaas
 Fixes for abstract SSL interface to work in NET.


   Rev 1.45    24/01/2004 19:24:30  CCostelloe
 Cleaned up warnings


   Rev 1.44    1/21/2004 1:30:06 PM  JPMugaas
 InitComponent


   Rev 1.43    16/01/2004 17:39:34  CCostelloe
 Added support for BinHex 4.0 encoding


   Rev 1.42    11/01/2004 19:53:40  CCostelloe
 Revisions for TIdMessage SaveToFile & LoadFromFile for D7 & D8


   Rev 1.40    08/01/2004 23:46:16  CCostelloe
 Changes to ProcessMessage to get TIdMessage.LoadFromFile working in D7


   Rev 1.39    08/01/2004 00:31:06  CCostelloe
 Start of reimplementing LoadFrom/SaveToFile


   Rev 1.38    22/12/2003 00:44:52  CCostelloe
 .NET fixes


   Rev 1.37    11/11/2003 12:06:26 AM  BGooijen
 Did all todo's ( TStream to TIdStream mainly )


   Rev 1.36    2003.10.24 10:43:10 AM  czhower
 TIdSTream to dos


    Rev 1.35    10/17/2003 12:37:36 AM  DSiders
  Added localization comments.
  Added resource string for exception message.


   Rev 1.34    2003.10.14 9:57:12 PM  czhower
 Compile todos


   Rev 1.33    10/12/2003 1:49:56 PM  BGooijen
 Changed comment of last checkin


   Rev 1.32    10/12/2003 1:43:40 PM  BGooijen
 Changed IdCompilerDefines.inc to Core\IdCompilerDefines.inc


   Rev 1.30    10/11/2003 4:21:14 PM  BGooijen
 Compiles in D7 again


   Rev 1.29    10/10/2003 10:42:28 PM  BGooijen
 DotNet


   Rev 1.28    9/10/2003 1:50:52 PM  SGrobety
 DotNet


   Rev 1.27    10/8/2003 9:53:42 PM  GGrieve
 Remove $IFDEFs


   Rev 1.26    05/10/2003 16:39:52  CCostelloe
 Set default ContentType


   Rev 1.25    03/10/2003 21:03:40  CCostelloe
 Bug fixes


   Rev 1.24    2003.10.02 9:27:52 PM  czhower
 DotNet Excludes


   Rev 1.23    01/10/2003 17:58:56  HHariri
 More fixes for Multipart Messages and also fixes for incorrect transfer
 encoding settings


   Rev 1.20    01/10/2003 10:57:56  CCostelloe
 Fixed GenerateTextPartContentType (was ignoring ContentType)


   Rev 1.19    26/09/2003 01:03:48  CCostelloe
 Modified ProcessAttachment in ReceiveBody to update message's Encoding if
 attachment was XX-encoded.  Added decoding of message bodies encoded as
 base64 or quoted-printable.  Added support for nested MIME parts
 (ParentPart).  Added support for TIdText in UU and XX encoding.  Added
 missing base64 and QP support where needed. Rewrote/rearranged most of code.


   Rev 1.18    04/09/2003 20:44:56  CCostelloe
 In SendBody, removed blank line between boundaries and Text part header;
 recoded wDoublePoint


   Rev 1.17    30/08/2003 18:40:44  CCostelloe
 Updated to use IdMessageCoderMIME's new random boundaries


   Rev 1.16    8/8/2003 12:27:18 PM  JPMugaas
 Should now compile.


   Rev 1.15    07/08/2003 00:39:06  CCostelloe
 Modified SendBody to deal with unencoded attachments (otherwise 7bit
 attachments had the attachment header written out as 7bit but was encoded as
 base64)


   Rev 1.14    11/07/2003 01:14:20  CCostelloe
 SendHeader changed to support new IdMessage.GenerateHeader putting generated
 headers in IdMessage.LastGeneratedHeaders.


   Rev 1.13    6/15/2003 01:13:10 PM  JPMugaas
 Minor fixes and cleanups.


   Rev 1.12    5/18/2003 02:31:44 PM  JPMugaas
 Reworked some things so IdSMTP and IdDirectSMTP can share code including
 stuff for pipelining.


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


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


   Rev 1.9    5/8/2003 02:17:58 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.8    3/17/2003 02:16:06 PM  JPMugaas
 Now descends from ExplicitTLS base class.


   Rev 1.7    2/24/2003 07:25:18 PM  JPMugaas
 Now compiles with new code.


   Rev 1.6    12-8-2002 21:12:36  BGooijen
 Changed calls to Writeln to  IOHandler.WriteLn, because the parent classes
 don't provide Writeln, System.Writeln was assumed by the compiler


   Rev 1.5    12-8-2002 21:08:58  BGooijen
 The TIdIOHandlerStream was not Opened before used, fixed that.


   Rev 1.4    12/6/2002 05:30:22 PM  JPMugaas
 Now decend from TIdTCPClientCustom instead of TIdTCPClient.


   Rev 1.3    12/5/2002 02:54:06 PM  JPMugaas
 Updated for new API definitions.


   Rev 1.2    11/23/2002 03:33:44 AM  JPMugaas
 Reverted changes because they were problematic.  Kudzu didn't explain why.


   Rev 1.1    11/19/2002 05:35:30 PM  JPMugaas
 Fixed problem with a line starting with a ".".  A double period should only
 be used if the line is really just one "." and no other cases.


   Rev 1.0    11/13/2002 07:56:58 AM  JPMugaas
}
unit IdMessageClient;

{
  2003-10-04 Ciaran Costelloe (see comments starting CC4)
    If attachment not base64 encoded and has no ContentType, set to text/plain
  2003-Sep-20 Ciaran Costelloe
    Modified ProcessAttachment in ReceiveBody to update message's Encoding
    if attachment was XX-encoded.  Added decoding of message bodies
    encoded as base64 or quoted-printable.  Added support for nested MIME parts
    (ParentPart).  Added support for TIdText in UU and XX encoding.  Added
    missing base64 and QP support where needed.
    Rewrote/rearranged most of code.

  2001-Oct-29 Don Siders
    Modified TIdMessageClient.SendMsg to use AHeadersOnly argument.

  2001-Dec-1  Don Siders
    Save ContentDisposition in TIdMessageClient.ProcessAttachment

  2003-Sep-04 Ciaran Costelloe (CC comments)
    Commented-out IOHandler.WriteLn(''); in SendBody which used to insert a blank line
    between boundary and text attachment header, causing the attachment header to
    be parsed as part of the attachment text (the blank line is the delimiter for
    the end of the header).

  2003-Sep-11 Ciaran Costelloe (CC2 comments)
    Added support in decoding for message body (as distinct from message parts) being
    encoded.
    Added support for generating encoded message body.
}

interface

{$i IdCompilerDefines.inc}

uses
  Classes,
  IdCoderMIME,
  IdExplicitTLSClientServerBase,
  IdGlobal,
  IdHeaderList,
  IdIOHandlerStream,
  IdBaseComponent,
  IdMessage,
  IdTCPClient;

type
  TIdIOHandlerStreamMsg = class(TIdIOHandlerStream)
  protected
    FTerminatorWasRead: Boolean;
    FEscapeLines: Boolean;
    FUnescapeLines: Boolean;
    FLastByteRecv: Byte;
    function ReadDataFromSource(var VBuffer: TIdBytes): Integer; override;
  public
    constructor Create(
      AOwner: TComponent;
      AReceiveStream: TStream;
      ASendStream: TStream = nil
    ); override;  //Should this be reintroduce instead of override?
    function Readable(AMSec: Integer = IdTimeoutDefault): Boolean; override;
    function ReadLn(ATerminator: string; ATimeout: Integer = IdTimeoutDefault;
      AMaxLineLength: Integer = -1; AByteEncoding: IIdTextEncoding = nil
      {$IFDEF STRING_IS_ANSI}; ADestEncoding: IIdTextEncoding = nil{$ENDIF}
      ): string; override;
    procedure WriteLn(const AOut: string; AByteEncoding: IIdTextEncoding = nil
      {$IFDEF STRING_IS_ANSI}; ASrcEncoding: IIdTextEncoding = nil{$ENDIF}
      ); override;
    property EscapeLines: Boolean read FEscapeLines write FEscapeLines;
    property UnescapeLines: Boolean read FUnescapeLines write FUnescapeLines;
  published
    property MaxLineLength default MaxInt;
  end;

  TIdMessageClient = class(TIdExplicitTLSClient)
  protected
    // The length of the folded line
    FMsgLineLength: integer;
    // The string to be pre-pended to the next line
    FMsgLineFold: string;

    procedure ReceiveBody(AMsg: TIdMessage; const ADelim: string = '.'); virtual;    {do not localize}
    function  ReceiveHeader(AMsg: TIdMessage; const AAltTerm: string = ''): string; virtual;
    procedure SendBody(AMsg: TIdMessage); virtual;
    procedure SendHeader(AMsg: TIdMessage); virtual;
    procedure EncodeAndWriteText(const ABody: TStrings; AEncoding: IIdTextEncoding);
    procedure WriteFoldedLine(const ALine : string);
    procedure InitComponent; override;
  public
    {$IFDEF WORKAROUND_INLINE_CONSTRUCTORS}
    constructor Create(AOwner: TComponent); reintroduce; overload;
    {$ENDIF}
    destructor Destroy; override;
    procedure ProcessMessage(AMsg: TIdMessage; AHeaderOnly: Boolean = False); overload;
    procedure ProcessMessage(AMsg: TIdMessage; AStream: TStream; AHeaderOnly: Boolean = False); overload;
    procedure ProcessMessage(AMsg: TIdMessage; const AFilename: string; AHeaderOnly: Boolean = False); overload;
    procedure SendMsg(AMsg: TIdMessage; AHeadersOnly: Boolean = False); overload; virtual;
    //
  //  property Capabilities;
    property MsgLineLength: integer read FMsgLineLength write FMsgLineLength;
    property MsgLineFold: string read FMsgLineFold write FMsgLineFold;
  end;

implementation

uses
  //facilitate inlining only.
  {$IFDEF DOTNET}
  System.IO,
  IdStreamNET,
  {$ELSE}
  IdStreamVCL,
  {$ENDIF}
  //TODO: Remove these references and make it completely pluggable. Check other spots in Indy as well
  IdMessageCoderBinHex4, IdMessageCoderQuotedPrintable, IdMessageCoderMIME,
  IdMessageCoderUUE, IdMessageCoderXXE,
  //
  IdGlobalProtocols,
  IdCoder, IdCoder3to4, IdCoderBinHex4,
  IdCoderHeader, IdHeaderCoderBase, IdMessageCoder, IdComponent, IdException,
  IdResourceStringsProtocols, IdTCPConnection, IdTCPStream, IdIOHandler,
  IdAttachmentFile, IdText, IdAttachment,
  SysUtils;

const
  SContentType = 'Content-Type'; {do not localize}
  SContentTransferEncoding = 'Content-Transfer-Encoding'; {do not localize}
  SThisIsMultiPartMessageInMIMEFormat = 'This is a multi-part message in MIME format'; {do not localize}

function GetLongestLine(var ALine : String; const ADelim : String) : String;
var
  i, fnd, delimLen : Integer;
begin
  Result := '';

  fnd := 0;
  delimLen := Length(ADelim);

  for i := 1 to Length(ALine) do
  begin
    if ALine[i] = ADelim[1] then
    begin
      if Copy(ALine, i, delimLen) = ADelim then
      begin
        fnd := i;
      end;
    end;
  end;

  if fnd > 0 then
  begin
    Result := Copy(ALine, 1, fnd - 1);
    ALine := Copy(ALine, fnd + delimLen, MaxInt);
  end;
end;

procedure RemoveLastBlankLine(Body: TStrings);
var
  Count: Integer;
begin
  if Assigned(Body) then begin
    { Remove the last blank line. The last blank line is added again in
      TIdMessageClient.SendBody(). }
    Count := Body.Count;
    if (Count > 0) and (Body[Count - 1] = '') then begin
      Body.Delete(Count - 1);
    end;
  end;
end;

////////////////////////
// TIdIOHandlerStreamMsg
////////////////////////

constructor TIdIOHandlerStreamMsg.Create(
  AOwner: TComponent;
  AReceiveStream: TStream;
  ASendStream: TStream = nil
  );
begin
  inherited Create(AOwner, AReceiveStream, ASendStream);
  FTerminatorWasRead := False;
  FEscapeLines := False; // do not set this to True! This is for users to set manually...
  FUnescapeLines := False; // do not set this to True! This is for users to set manually...
  FLastByteRecv := 0;
  MaxLineLength := MaxInt;
end;

function TIdIOHandlerStreamMsg.Readable(AMSec: integer = IdTimeoutDefault): Boolean;
begin
  if not FTerminatorWasRead then begin
    Result := inherited Readable(AMSec);
    if Result then begin
      Exit;
    end;
  end;
  Result := ReceiveStream <> nil;
end;

function TIdIOHandlerStreamMsg.ReadDataFromSource(var VBuffer: TIdBytes): Integer;
var
  LTerminator: String;
begin
  if not FTerminatorWasRead then
  begin
    Result := inherited ReadDataFromSource(VBuffer);
    if Result > 0 then begin
      FLastByteRecv := VBuffer[Result-1];
      Exit;
    end;
    // determine whether the stream ended with a line
    // break, adding an extra CR and/or LF if needed...
    if (FLastByteRecv = Ord(LF)) then begin
      // don't add an extra line break
      LTerminator := '.' + EOL;
    end else if (FLastByteRecv = Ord(CR)) then begin
      // add extra LF
      LTerminator := LF + '.' + EOL;
    end else begin
      // add extra CRLF
      LTerminator := EOL + '.' + EOL;
    end;
    FTerminatorWasRead := True;
    // in theory, CopyTIdString() will write the string
    // into the byte array using 1-byte characters even
    // under DotNet where strings are usually Unicode
    // instead of ASCII...
    CopyTIdString(LTerminator, VBuffer, 0);
    Result := Length(LTerminator);
  end else begin
    Result := 0;
  end;
end;

function TIdIOHandlerStreamMsg.ReadLn(ATerminator: string;
  ATimeout: Integer = IdTimeoutDefault; AMaxLineLength: Integer = -1;
  AByteEncoding: IIdTextEncoding = nil
  {$IFDEF STRING_IS_ANSI}; ADestEncoding: IIdTextEncoding = nil{$ENDIF}
  ): string;
begin
  Result := inherited ReadLn(ATerminator, ATimeout, AMaxLineLength,
    AByteEncoding{$IFDEF STRING_IS_ANSI}, ADestEncoding{$ENDIF});
  if FEscapeLines and TextStartsWith(Result, '.') and (not FTerminatorWasRead) then begin {Do not Localize}
    Result := '.' + Result; {Do not Localize}
  end;
end;

procedure TIdIOHandlerStreamMsg.WriteLn(const AOut: string; AByteEncoding: IIdTextEncoding = nil
  {$IFDEF STRING_IS_ANSI}; ASrcEncoding: IIdTextEncoding = nil{$ENDIF}
  );
var
  LOut: String;
begin
  LOut := AOut;
  if FUnescapeLines and TextStartsWith(LOut, '..') then begin {Do not Localize}
    IdDelete(LOut, 1, 1);
  end;
  inherited WriteLn(LOut, AByteEncoding{$IFDEF STRING_IS_ANSI}, ASrcEncoding{$ENDIF});
end;

///////////////////
// TIdMessageClient
///////////////////

{$IFDEF WORKAROUND_INLINE_CONSTRUCTORS}
constructor TIdMessageClient.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
end;
{$ENDIF}

procedure TIdMessageClient.InitComponent;
begin
  inherited InitComponent;
  FMsgLineLength := 79;
  FMsgLineFold := TAB;
end;

procedure TIdMessageClient.WriteFoldedLine(const ALine : string);
var
  ins, s, line, spare : String;
  msgLen, insLen : Word;
begin
  s := ALine;

  // To give an amount of thread-safety
  ins := FMsgLineFold;
  insLen := Length(ins);
  msgLen := FMsgLineLength;

  // Do first line
  if length(s) > FMsgLineLength then
  begin
    spare := Copy(s, 1, msgLen);
    line := GetLongestLine(spare, ' ');    {do not localize}
    s := spare + Copy(s, msgLen + 1, length(s));
    IOHandler.WriteLn(line);

    // continue with the folded lines
    while length(s) > (msgLen - insLen) do
    begin
      spare := Copy(s, 1, (msgLen - insLen));
      line := GetLongestLine(spare, ' ');      {do not localize}
      s := ins + spare + Copy(s, (msgLen - insLen) + 1, length(s));
      IOHandler.WriteLn(line);
    end;

    // complete the output with what's left
    if Trim(s) <> '' then
    begin
      IOHandler.WriteLn(ins + s);
    end;
  end

  else begin
    IOHandler.WriteLn(s);
  end;
end;

procedure TIdMessageClient.ReceiveBody(AMsg: TIdMessage; const ADelim: string = '.');   {do not localize}
var
  LMsgEnd: Boolean;
  LActiveDecoder: TIdMessageDecoder;
  LLine: string;
  LParentPart: integer;
  LPreviousParentPart: integer;
  LEncoding, LCharsetEncoding: IIdTextEncoding;
  LContentTransferEncoding: string;
  LUnknownContentTransferEncoding: Boolean;

  // TODO - move this procedure into TIdIOHandler as a new Capture method?
  procedure CaptureAndDecodeCharset;
  var
    LMStream: TMemoryStream;
  begin
    LMStream := TMemoryStream.Create;
    try
      IOHandler.Capture(LMStream, ADelim, True, IndyTextEncoding_8Bit{$IFDEF STRING_IS_ANSI}, IndyTextEncoding_8Bit{$ENDIF});
      LMStream.Position := 0;
      // TODO: when String is AnsiString, TIdIMAP4 uses 8bit as the destination
      // encoding, should this be doing the same? Otherwise, we could just use
      // AMsg.Body.LoadFromStream() instead...
      ReadStringsAsCharSet(LMStream, AMsg.Body, AMsg.CharSet{$IFDEF STRING_IS_ANSI}, CharsetToEncoding(AMsg.CharSet){$ENDIF});
    finally
      FreeAndNil(LMStream);
    end;
  end;

  // RLebeau 11/2/2013: TIdMessage.Headers is a TIdHeaderList, but
  // TIdMessageDecoder.Headers is a plain TStringList.  Although TIdHeaderList
  // is a TStrings descendant, but it reintroduces its own Values[] property
  // instead of implementing the TStrings.Values[] property, so we cannot
  // access TIdMessage.Headers using a TStrings pointer or else the wrong
  // property will be invoked and we won't get the right value when accessing
  // TIdMessage.Headers since TStrings and TIdHeaderList use different
  // NameValueSeparator implementations, so we have to access them separately...
  function GetHeaderValue(const AName: string): string;
  begin
    if AMsg.IsMsgSinglePartMime then begin
      Result := AMsg.Headers.Values[AName];
    end else begin
      Result := LActiveDecoder.Headers.Values[AName];
    end;
  end;

  {Only set AUseBodyAsTarget to True if you want the input stream stored in TIdMessage.Body
  instead of TIdText.Body: this happens with some single-part messages.}
  procedure ProcessTextPart(var VDecoder: TIdMessageDecoder; AUseBodyAsTarget: Boolean);
  var
    LMStream: TMemoryStream;
    i: integer;
    LTxt : TIdText;
    LNewDecoder: TIdMessageDecoder;
    {$IFDEF STRING_IS_ANSI}
    LAnsiEncoding: IIdTextEncoding;
    {$ENDIF}
  begin
    LMStream := TMemoryStream.Create;
    try
      LParentPart := AMsg.MIMEBoundary.ParentPart;
      LNewDecoder := VDecoder.ReadBody(LMStream, LMsgEnd);
      try
        LMStream.Position := 0;
        if AUseBodyAsTarget then begin
          if AMsg.IsMsgSinglePartMime then begin
            {$IFDEF STRING_IS_ANSI}
            LAnsiEncoding := CharsetToEncoding(AMsg.CharSet);
            {$ENDIF}
            ReadStringsAsCharSet(LMStream, AMsg.Body, AMsg.CharSet{$IFDEF STRING_IS_ANSI}, LAnsiEncoding{$ENDIF});
          end else begin
            {$IFDEF STRING_IS_ANSI}
            LAnsiEncoding := ContentTypeToEncoding(VDecoder.Headers.Values[SContentType], QuoteMIME);
            {$ENDIF}
            ReadStringsAsContentType(LMStream, AMsg.Body, VDecoder.Headers.Values[SContentType], QuoteMIME{$IFDEF STRING_IS_ANSI}, LAnsiEncoding{$ENDIF});
          end;
        end else begin
          LTxt := TIdText.Create(AMsg.MessageParts);
          try
            {$IFDEF STRING_IS_ANSI}
            LAnsiEncoding := ContentTypeToEncoding(GetHeaderValue(SContentType), QuoteMIME);
            {$ENDIF}
            ReadStringsAsContentType(LMStream, LTxt.Body, GetHeaderValue(SContentType), QuoteMIME{$IFDEF STRING_IS_ANSI}, LAnsiEncoding{$ENDIF});
            RemoveLastBlankLine(LTxt.Body);
            LTxt.ContentType := LTxt.ResolveContentType(GetHeaderValue(SContentType));
            LTxt.CharSet := LTxt.GetCharSet(GetHeaderValue(SContentType));       {do not localize}
            LTxt.ContentTransfer := GetHeaderValue(SContentTransferEncoding);    {do not localize}
            LTxt.ContentID := GetHeaderValue('Content-ID');  {do not localize}
            LTxt.ContentLocation := GetHeaderValue('Content-Location');  {do not localize}
            LTxt.ContentDescription := GetHeaderValue('Content-Description');  {do not localize}
            LTxt.ContentDisposition := GetHeaderValue('Content-Disposition');  {do not localize}
            if not AMsg.IsMsgSinglePartMime then begin
              for i := 0 to VDecoder.Headers.Count-1 do begin
                if LTxt.Headers.IndexOfName(VDecoder.Headers.Names[i]) < 0 then begin
                  LTxt.ExtraHeaders.AddValue(
                    VDecoder.Headers.Names[i],
                    IndyValueFromIndex(VDecoder.Headers, i)
                  );
                end;
              end;
            end;
            LTxt.Filename := VDecoder.Filename;
            if IsHeaderMediaType(LTxt.ContentType, 'multipart') then begin {do not localize}
              LTxt.ParentPart := LPreviousParentPart;

              // RLebeau 08/17/09 - According to RFC 2045 Section 6.4:
              // "If an entity is of type "multipart" the Content-Transfer-Encoding is not
              // permitted to have any value other than "7bit", "8bit" or "binary"."
              //
              // However, came across one message where the "Content-Type" was set to
              // "multipart/related" and the "Content-Transfer-Encoding" was set to
              // "quoted-printable".  Outlook and Thunderbird were apparently able to parse
              // the message correctly, but Indy was not.  So let's check for that scenario
              // and ignore illegal "Content-Transfer-Encoding" values if present...

              if LTxt.ContentTransfer <> '' then begin
                if PosInStrArray(LTxt.ContentTransfer, ['7bit', '8bit', 'binary'], False) = -1 then begin {do not localize}
                  LTxt.ContentTransfer := '';
                end;
              end;
            end else begin
              LTxt.ParentPart := LParentPart;
            end;
          except
            LTxt.Free;
            raise;
          end;
        end;
      except
        LNewDecoder.Free;
        raise;
      end;
      VDecoder.Free;
      VDecoder := LNewDecoder;
    finally
      FreeAndNil(LMStream);
    end;
  end;

  procedure ProcessAttachment(var VDecoder: TIdMessageDecoder);
  var
    LDestStream: TStream;
    i: integer;
    LAttachment: TIdAttachment;
    LNewDecoder: TIdMessageDecoder;
  begin
    LParentPart := AMsg.MIMEBoundary.ParentPart;
    AMsg.DoCreateAttachment(VDecoder.Headers, LAttachment);
    Assert(Assigned(LAttachment), 'Attachment must not be unassigned here!'); {Do not localize}
    try
      LNewDecoder := nil;
      try
        LDestStream := LAttachment.PrepareTempStream;
        try
          LNewDecoder := VDecoder.ReadBody(LDestStream, LMsgEnd);
        finally
          LAttachment.FinishTempStream;
        end;
        LAttachment.ContentType := LAttachment.ResolveContentType(GetHeaderValue(SContentType));
        LAttachment.CharSet := LAttachment.GetCharSet(GetHeaderValue(SContentType));
        if VDecoder is TIdMessageDecoderUUE then begin
          LAttachment.ContentTransfer := TIdMessageDecoderUUE(VDecoder).CodingType;  {do not localize}
        end else begin
          //Watch out for BinHex 4.0 encoding: no ContentTransfer is specified
          //in the header, but we need to set it to something meaningful for us...
          if IsHeaderMediaType(LAttachment.ContentType, 'application/mac-binhex40') then begin {do not localize}
            LAttachment.ContentTransfer := 'binhex40'; {do not localize}
          end else begin
            LAttachment.ContentTransfer := GetHeaderValue(SContentTransferEncoding);
          end;
        end;
        LAttachment.ContentDisposition := GetHeaderValue('Content-Disposition'); {do not localize}
        LAttachment.ContentID := GetHeaderValue('Content-ID');                   {do not localize}
        LAttachment.ContentLocation := GetHeaderValue('Content-Location');       {do not localize}
        LAttachment.ContentDescription := GetHeaderValue('Content-Description'); {do not localize}
        if not AMsg.IsMsgSinglePartMime then begin
          for i := 0 to VDecoder.Headers.Count-1 do begin
            if LAttachment.Headers.IndexOfName(VDecoder.Headers.Names[i]) < 0 then begin
              LAttachment.ExtraHeaders.AddValue(
                VDecoder.Headers.Names[i],
                IndyValueFromIndex(VDecoder.Headers, i)
              );
            end;
          end;
        end;
        LAttachment.Filename := VDecoder.Filename;
        if IsHeaderMediaType(LAttachment.ContentType, 'multipart') then begin  {do not localize}
          LAttachment.ParentPart := LPreviousParentPart;

          // RLebeau 08/17/09 - According to RFC 2045 Section 6.4:
          // "If an entity is of type "multipart" the Content-Transfer-Encoding is not
          // permitted to have any value other than "7bit", "8bit" or "binary"."
          //
          // However, came across one message where the "Content-Type" was set to
          // "multipart/related" and the "Content-Transfer-Encoding" was set to
          // "quoted-printable".  Outlook and Thunderbird were apparently able to parse
          // the message correctly, but Indy was not.  So let's check for that scenario
          // and ignore illegal "Content-Transfer-Encoding" values if present...

          if LAttachment.ContentTransfer <> '' then begin
            if PosInStrArray(LAttachment.ContentTransfer, ['7bit', '8bit', 'binary'], False) = -1 then begin {do not localize}
              LAttachment.ContentTransfer := '';
            end;
          end;
        end else begin
          LAttachment.ParentPart := LParentPart;
        end;
      except
        LNewDecoder.Free;
        raise;
      end;
      VDecoder.Free;
      VDecoder := LNewDecoder;
    except
      //This should also remove the Item from the TCollection.
      //Note that Delete does not exist in the TCollection.
      LAttachment.Free;
      raise;
    end;
  end;

begin
  LMsgEnd := False;

  // RLebeau 08/09/09 - TIdNNTP.GetBody() calls TIdMessage.Clear() before then
  // calling ReceiveBody(), thus the TIdMessage.ContentTransferEncoding value
  // is not available for use below.  What is the best way to detect that so
  // the user could be allowed to set up the IOHandler.DefStringEncoding
  // beforehand?

  LUnknownContentTransferEncoding := False;

  if AMsg.NoDecode then begin
    LEncoding := IndyTextEncoding_8Bit;
  end else
  begin
    LContentTransferEncoding := AMsg.ContentTransferEncoding;
    if LContentTransferEncoding = '' then begin
      // RLebeau 04/08/2014: According to RFC 2045 Section 6.1:
      // "Content-Transfer-Encoding: 7BIT" is assumed if the
      // Content-Transfer-Encoding header field is not present."
      if IsHeaderMediaType(AMsg.ContentType, 'application/mac-binhex40') then begin  {Do not Localize}
        LContentTransferEncoding := 'binhex40'; {do not localize}
      end
      else if (AMsg.Encoding = meMIME) and (AMsg.MIMEBoundary.Count > 0) then begin
        LContentTransferEncoding := '7bit'; {do not localize}
      end;
    end
    else if IsHeaderMediaType(AMsg.ContentType, 'multipart') then {do not localize}
    begin
      // RLebeau 08/17/09 - According to RFC 2045 Section 6.4:
      // "If an entity is of type "multipart" the Content-Transfer-Encoding is not
      // permitted to have any value other than "7bit", "8bit" or "binary"."
      //
      // However, came across one message where the "Content-Type" was set to
      // "multipart/related" and the "Content-Transfer-Encoding" was set to
      // "quoted-printable".  Outlook and Thunderbird were apparently able to parse
      // the message correctly, but Indy was not.  So let's check for that scenario
      // and ignore illegal "Content-Transfer-Encoding" values if present...
      if PosInStrArray(LContentTransferEncoding, ['7bit', '8bit', 'binary'], False) = -1 then begin {do not localize}
        LContentTransferEncoding := '';
        //LUnknownContentTransferEncoding := True;
      end;
    end;

    if LContentTransferEncoding <> '' then begin
      case PosInStrArray(LContentTransferEncoding, ['7bit', 'quoted-printable', 'base64', '8bit', 'binary'], False) of {do not localize}
        0..2: LEncoding := IndyTextEncoding_ASCII;
        3..4: LEncoding := IndyTextEncoding_8Bit;
      else
        // According to RFC 2045 Section 6.4:
        // "Any entity with an unrecognized Content-Transfer-Encoding must be
        // treated as if it has a Content-Type of "application/octet-stream",
        // regardless of what the Content-Type header field actually says."
        LEncoding := IndyTextEncoding_8Bit;
        LContentTransferEncoding := '';
        LUnknownContentTransferEncoding := True;
      end;
    end else begin
      LEncoding := IndyTextEncoding_8Bit;
    end;
  end;

  BeginWork(wmRead);
  try
    if AMsg.NoDecode then begin
      CaptureAndDecodeCharset;
    end else begin
      LActiveDecoder := nil;
      try
        if ((not LUnknownContentTransferEncoding) and
         ((AMsg.Encoding = meMIME) and (AMsg.MIMEBoundary.Count > 0)) or
         ((AMsg.Encoding = mePlainText) and (PosInStrArray(AMsg.ContentTransferEncoding, ['base64', 'quoted-printable'], False) = -1))  {do not localize}
         ) then begin
          {NOTE: You hit this code path with multipart MIME messages and with
          plain-text messages (which may have UUE or XXE attachments embedded).}
          LCharsetEncoding := CharsetToEncoding(AMsg.CharSet);
          repeat
            {CC: This code assumes the preamble text (before the first boundary)
            is plain text.  I cannot imagine it not being, but if it arises, lines
            will have to be decoded.}

            // TODO: need to figure out a way to handle both transfer encoding
            // and charset encoding together!  Need to read the raw bytes into
            // an intermediate buffer of some kind using the transfer encoding,
            // and then decode the characters using the charset afterwards...
            //
            // Need to do this anyway because ReadLnRFC() processes the LF and
            // ADelim values in terms of the charset specified, which is wrong.
            // EBCDIC-based charsets totally break that logic! For example, cp1026
            // converts #10 (LF) to $25 instead of $0A during encoding, and converts
            // $0A (LF) and $2E ('.') to #$83 and #6 during decoding, etc. And what
            // if the charset is UTF-16 instead?  So we need to read raw bytes into
            // a buffer, checking it for handling of line breaks, dot-transparency,
            // and message termination, and THEN decode whatever is left using the
            // charset...

            LLine := IOHandler.ReadLnRFC(LMsgEnd, LF, ADelim, IndyTextEncoding_8Bit{$IFDEF STRING_IS_ANSI}, IndyTextEncoding_8Bit{$ENDIF});
            if LMsgEnd then begin
              Break;
            end;
            if LActiveDecoder = nil then begin
              LActiveDecoder := TIdMessageDecoderList.CheckForStart(AMsg, LLine);
            end;
            // Check again, the if above can set it.
            if LActiveDecoder = nil then begin
              LLine := LCharsetEncoding.GetString(ToBytes(LLine, IndyTextEncoding_8Bit{$IFDEF STRING_IS_ANSI}, IndyTextEncoding_8Bit{$ENDIF}));
              AMsg.Body.Add(LLine);
            end else begin
              RemoveLastBlankLine(AMsg.Body);
              while LActiveDecoder <> nil do begin
                LActiveDecoder.SourceStream := TIdTCPStream.Create(Self);
                LPreviousParentPart := AMsg.MIMEBoundary.ParentPart;
                LActiveDecoder.ReadHeader;
                case LActiveDecoder.PartType of
                  mcptText:       ProcessTextPart(LActiveDecoder, False);
                  mcptAttachment: ProcessAttachment(LActiveDecoder);
                  mcptIgnore:     FreeAndNil(LActiveDecoder);
                  mcptEOF:        begin FreeAndNil(LActiveDecoder); LMsgEnd := True; end;
                end;
              end;
            end;
          until LMsgEnd;
          RemoveLastBlankLine(AMsg.Body);
        end else begin
          {These are single-part MIMEs, or else mePlainTexts with the body encoded QP/base64}
          AMsg.IsMsgSinglePartMime := True;
          LActiveDecoder := TIdMessageDecoderMime.Create(AMsg);
          LActiveDecoder.SourceStream := TIdTCPStream.Create(Self);
          // RLebeau: override what TIdMessageDecoderMime.InitComponent() assigns
          TIdMessageDecoderMime(LActiveDecoder).BodyEncoded := True;
          TIdMessageDecoderMime(LActiveDecoder).ReadHeader;
          case LActiveDecoder.PartType of
            mcptText: begin
              if LUnknownContentTransferEncoding then begin
                ProcessAttachment(LActiveDecoder);
              end else begin
                ProcessTextPart(LActiveDecoder, True); //Put the text into TIdMessage.Body
              end;
            end;
            mcptAttachment: ProcessAttachment(LActiveDecoder);
            mcptIgnore:     FreeAndNil(LActiveDecoder);
            mcptEOF:        FreeAndNil(LActiveDecoder);
          end;
        end;
      finally
        FreeAndNil(LActiveDecoder);
      end;
    end;
  finally
    EndWork(wmRead);
  end;
end;

procedure TIdMessageClient.SendHeader(AMsg: TIdMessage);
begin
  AMsg.GenerateHeader;
  IOHandler.Write(AMsg.LastGeneratedHeaders);
end;

procedure TIdMessageClient.SendBody(AMsg: TIdMessage);
var
  i: Integer;
  LAttachment: TIdAttachment;
  LBoundary: string;
  LDestStream: TStream;
  LStrStream: TStream;
  ISOCharset: string;
  HeaderEncoding: Char;  { B | Q }
  LEncoder: TIdMessageEncoder;
  LLine: string;

  procedure EncodeStrings(AStrings: TStrings; AEncoderClass: TIdMessageEncoderClass; AByteEncoding: IIdTextEncoding
    {$IFDEF STRING_IS_ANSI}; AAnsiEncoding: IIdTextEncoding{$ENDIF});
  var
    LStrings: TStringList;
  begin
    {$IFDEF STRING_IS_ANSI}
    EnsureEncoding(AAnsiEncoding, encOSDefault);
    {$ENDIF}
    LStrings := TStringList.Create; try
      LEncoder := AEncoderClass.Create(Self); try
        LStrStream := TMemoryStream.Create; try
          // RLebeau 10/06/2010: not using TStrings.SaveToStream() in D2009+
          // anymore, as it may save a BOM which we do not want here...
          WriteStringToStream(LStrStream, AStrings.Text, AByteEncoding{$IFDEF STRING_IS_ANSI}, AAnsiEncoding{$ENDIF});
          LStrStream.Position := 0;
          LEncoder.Encode(LStrStream, LStrings);
        finally FreeAndNil(LStrStream); end;
      finally FreeAndNil(LEncoder); end;
      IOHandler.WriteRFCStrings(LStrings, False);
    finally FreeAndNil(LStrings); end;
  end;

  procedure EncodeAttachment(AAttachment: TIdAttachment; AEncoderClass: TIdMessageEncoderClass);
  var
    LAttachStream: TStream;
  begin
    LDestStream := TIdTCPStream.Create(Self, 8192); try
      LEncoder := AEncoderClass.Create(Self); try
        LEncoder.Filename := AAttachment.Filename;
        LAttachStream := AAttachment.OpenLoadStream; try
          LEncoder.Encode(LAttachStream, LDestStream);
        finally AAttachment.CloseLoadStream; end;
      finally FreeAndNil(LEncoder); end;
    finally FreeAndNil(LDestStream); end;
  end;

  procedure WriteTextPart(ATextPart: TIdText);
  var
    LEncoding: IIdTextEncoding;
    LFileName: String;
  begin
    if ATextPart.ContentType = '' then begin
      ATextPart.ContentType := 'text/plain'; {do not localize}
    end;

    // RLebeau 08/17/09 - According to RFC 2045 Section 6.4:
    // "If an entity is of type "multipart" the Content-Transfer-Encoding is not
    // permitted to have any value other than "7bit", "8bit" or "binary"."
    //
    // However, came across one message where the "Content-Type" was set to
    // "multipart/related" and the "Content-Transfer-Encoding" was set to
    // "quoted-printable".  Outlook and Thunderbird were apparently able to parse
    // the message correctly, but Indy was not.  So let's check for that scenario
    // and ignore illegal "Content-Transfer-Encoding" values if present...

    if IsHeaderMediaType(ATextPart.ContentType, 'multipart') then begin {do not localize}
      if ATextPart.ContentTransfer <> '' then begin
        if PosInStrArray(ATextPart.ContentTransfer, ['7bit', '8bit', 'binary'], False) = -1 then begin {do not localize}
          ATextPart.ContentTransfer := '';
        end;
      end;
    end
    else if ATextPart.ContentTransfer = '' then begin
      ATextPart.ContentTransfer := 'quoted-printable'; {do not localize}
    end
    else if (PosInStrArray(ATextPart.ContentTransfer, ['quoted-printable', 'base64'], False) = -1) {do not localize}
      and ATextPart.IsBodyEncodingRequired then
    begin
      ATextPart.ContentTransfer := '8bit';                    {do not localize}
    end;

    if ATextPart.ContentDisposition = '' then begin
      ATextPart.ContentDisposition := 'inline'; {do not localize}
    end;

    // TODO: when STRING_IS_ANSI is defined, provide a way for the user to specify the AnsiString encoding for header values...
    LFileName := EncodeHeader(ExtractFileName(ATextPart.FileName), '', HeaderEncoding, ISOCharSet); {do not localize}

    if ATextPart.ContentType <> '' then begin
      IOHandler.Write('Content-Type: ' + ATextPart.ContentType); {do not localize}
      if ATextPart.CharSet <> '' then begin
        IOHandler.Write('; charset="' + ATextPart.CharSet + '"'); {do not localize}
      end;
      if LFileName <> '' then begin
        IOHandler.WriteLn(';');  {do not localize}
        IOHandler.Write(TAB + 'name="' + LFileName + '"'); {do not localize}
      end;
      IOHandler.WriteLn;
    end;

    if ATextPart.ContentTransfer <> '' then begin
      IOHandler.WriteLn(SContentTransferEncoding + ': ' + ATextPart.ContentTransfer); {do not localize}
    end;

    IOHandler.Write('Content-Disposition: ' + ATextPart.ContentDisposition); {do not localize}
    if LFileName <> '' then begin
      IOHandler.WriteLn(';'); {do not localize}
      IOHandler.Write(TAB + 'filename="' + LFileName + '"'); {do not localize}
    end;
    IOHandler.WriteLn;

    if ATextPart.ContentID <> '' then begin
      IOHandler.WriteLn('Content-ID: ' + ATextPart.ContentID);  {do not localize}
    end;

    if ATextPart.ContentDescription <> '' then begin
      IOHandler.WriteLn('Content-Description: ' + ATextPart.ContentDescription); {do not localize}
    end;

    IOHandler.Write(ATextPart.ExtraHeaders);
    IOHandler.WriteLn;

    LEncoding := CharsetToEncoding(ATextPart.CharSet);
    if TextIsSame(ATextPart.ContentTransfer, 'quoted-printable') then begin {do not localize}
      EncodeStrings(ATextPart.Body, TIdMessageEncoderQuotedPrintable, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
    end
    else if TextIsSame(ATextPart.ContentTransfer, 'base64') then begin  {do not localize}
      EncodeStrings(ATextPart.Body, TIdMessageEncoderMIME, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
    end else
    begin
      IOHandler.WriteRFCStrings(ATextPart.Body, False, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
      { No test for last line break necessary because IOHandler.WriteRFCStrings() uses WriteLn(). }
    end;
  end;

var
  LFileName: String;
  LTextPart: TIdText;
  LAddedTextPart: Boolean;
  LLastPart: Integer;
  LEncoding: IIdTextEncoding;
  LAttachStream: TStream;
begin
  LBoundary := '';
  AMsg.InitializeISO(HeaderEncoding, ISOCharSet);
  BeginWork(wmWrite);
  try
    if (not AMsg.IsMsgSinglePartMime) and
      (PosInStrArray(AMsg.ContentTransferEncoding, ['base64', 'quoted-printable'], False) <> -1) then {do not localize}
    begin
      //CC2: The user wants the body encoded.
      if AMsg.MessageParts.Count > 0 then begin
        //CC2: We cannot deal with parts within a body encoding (user has to do
        //this manually, if the user really wants to). Note this should have been trapped in TIdMessage.GenerateHeader.
        raise EIdException.Create(RSMsgClientInvalidForTransferEncoding);
      end;
      IOHandler.WriteLn;     //This is the blank line after the headers
      DoStatus(hsStatusText, [RSMsgClientEncodingText]);
      LEncoding := CharsetToEncoding(AMsg.CharSet);
      //CC2: Now output AMsg.Body in the chosen encoding...
      if TextIsSame(AMsg.ContentTransferEncoding, 'base64') then begin  {do not localize}
        EncodeStrings(AMsg.Body, TIdMessageEncoderMIME, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
      end else begin  {'quoted-printable'}
        EncodeStrings(AMsg.Body, TIdMessageEncoderQuotedPrintable, LEncoding{$IFDEF STRING_IS_ANSI}, LEncoding{$ENDIF});
      end;
    end
    else if AMsg.Encoding = mePlainText then begin
      IOHandler.WriteLn;     //This is the blank line after the headers
      //CC2: It is NOT Mime.  It is a body followed by optional attachments
      DoStatus(hsStatusText, [RSMsgClientEncodingText]);
      // Write out Body first
      LEncoding := CharsetToEncoding(AMsg.CharSet);
      EncodeAndWriteText(AMsg.Body, LEncoding);
      IOHandler.WriteLn;
      if AMsg.MessageParts.Count > 0 then begin
        //The message has attachments.
        for i := 0 to AMsg.MessageParts.Count - 1 do begin
          //CC: Added support for TIdText...
          if AMsg.MessageParts.Items[i] is TIdText then begin
            IOHandler.WriteLn;
            IOHandler.WriteLn('------- Start of text attachment -------'); {do not localize}
            DoStatus(hsStatusText,  [RSMsgClientEncodingText]);
            WriteTextPart(TIdText(AMsg.MessageParts.Items[i]));
            IOHandler.WriteLn('------- End of text attachment -------');   {do not localize}
          end
          else if AMsg.MessageParts.Items[i] is TIdAttachment then begin
            LAttachment := TIdAttachment(AMsg.MessageParts[i]);
            DoStatus(hsStatusText, [RSMsgClientEncodingAttachment]);
            if LAttachment.ContentTransfer = '' then begin
              //The user has nothing specified: see has he set a preference in
              //TIdMessage.AttachmentEncoding (AttachmentEncoding is really an
              //old and somewhat deprecated property, but we can still support it)...
              if PosInStrArray(AMsg.AttachmentEncoding, ['UUE', 'XXE']) <> -1 then begin  {do not localize}
                LAttachment.ContentTransfer := AMsg.AttachmentEncoding;
              end else begin
                //We default to UUE (rather than XXE)...
                LAttachment.ContentTransfer := 'UUE';  {do not localize}
              end;
            end;
            case PosInStrArray(LAttachment.ContentTransfer, ['UUE', 'XXE'], False) of  {do not localize}
              0: EncodeAttachment(LAttachment, TIdMessageEncoderUUE);
              1: EncodeAttachment(LAttachment, TIdMessageEncoderXXE);
            end;
          end;
          IOHandler.WriteLn;
        end;
      end;
    end
    else begin
      //CC2: It is MIME-encoding...
      LAddedTextPart := False;
      //######### OUTPUT THE PREAMBLE TEXT ########
      {For single-part MIME messages, we want the message part headers to be appended
      to the message headers.  Otherwise, add the blank separator between header and
      body...}
      if not AMsg.IsMsgSinglePartMime then begin
        IOHandler.WriteLn;     //This is the blank line after the headers
        //if AMsg.Body.Count > 0 then begin
        if not AMsg.IsBodyEmpty then begin
          //CC2: The message has a body text.  There are now a few possibilities.
          //First up, if ConvertPreamble is False then the user explicitly does not want us
          //to convert the .Body since he had to change it from the default False.
          //Secondly, if AMsg.MessageParts.TextPartCount > 0, he may have put the
          //message text in the part, so don't convert the body.
          //Thirdly, if AMsg.MessageParts.Count = 0, then it has no other parts
          //anyway: in this case, output it without boundaries.
          //if (AMsg.ConvertPreamble and (AMsg.MessageParts.TextPartCount = 0)) then begin
          if AMsg.ConvertPreamble and (AMsg.MessageParts.TextPartCount = 0) and (AMsg.MessageParts.Count > 0) then begin
            //CC2: There is no text part, the user has not changed ConvertPreamble from
            //its default of True, so the user has probably put his message into
            //the body by mistake instead of putting it in a TIdText part.
            //Create a TIdText part from the .Body text...
            LTextPart := TIdText.Create(AMsg.MessageParts, AMsg.Body);
            LTextPart.CharSet := AMsg.CharSet;
            LTextPart.ContentType := 'text/plain';  {do not localize}
            LTextPart.ContentTransfer := 'quoted-printable';    {do not localize}
            //Have to remember that we added a text part, which is the last part
            //in the collection, because we need it to be outputted first...
            LAddedTextPart := True;
            //CC2: Insert our standard preamble text...
            IOHandler.WriteLn(SThisIsMultiPartMessageInMIMEFormat);
          end else begin
            //CC2: Hopefully the user has put suitable text in the preamble, or this
            //is an already-received message which already has a preamble text...
            LEncoding := CharsetToEncoding(AMsg.CharSet);
            EncodeAndWriteText(AMsg.Body, LEncoding);
          end;
        end
        else begin
          //CC2: The user has specified no body text: he presumably has the message in
          //a TIdText part, but it may have no text at all (a message consisting only
          //of headers, which is allowed under the RFC, which will have a parts count
          //of 0).
          if AMsg.MessageParts.Count <> 0 then begin
            //Add the "standard" MIME preamble text for non-html email clients...
            IOHandler.WriteLn(SThisIsMultiPartMessageInMIMEFormat);
          end;
        end;
        IOHandler.WriteLn;
        //######### SET UP THE BOUNDARY STACK ########
        LBoundary := AMsg.MIMEBoundary.Boundary;
        if LBoundary = '' then begin
          LBoundary := TIdMIMEBoundaryStrings.GenerateBoundary;
          AMsg.MIMEBoundary.Push(LBoundary, -1);  //-1 is "top level"
        end;
      end;
      //######### OUTPUT THE PARTS ########
      //CC2: Write the text parts in their order, if you change the order you
      //can mess up mutipart sequences.
      //The exception is due to ConvertPreamble, which may have added a text
      //part at the end (the only place a TIdText part can be added), but it
      //needs to be outputted first...
      LLastPart := AMsg.MessageParts.Count - 1;
      if LAddedTextPart then begin
        IOHandler.WriteLn('--' + LBoundary);       {do not localize}
        DoStatus(hsStatusText, [RSMsgClientEncodingText]);
        WriteTextPart(AMsg.MessageParts.Items[LLastPart] as TIdText);
        IOHandler.WriteLn;
        Dec(LLastPart);  //Don't output it again in the following "for" loop
      end;
      for i := 0 to LLastPart do begin
        LLine := AMsg.MessageParts.Items[i].ContentType;
        if IsHeaderMediaType(LLine, 'multipart') then begin  {do not localize}
          //A multipart header.  Write out the CURRENT boundary first...
          IOHandler.WriteLn('--' + LBoundary);      {do not localize}
          //Make the current boundary and this part number active...
          //Now need to generate a new boundary...
          LBoundary := TIdMIMEBoundaryStrings.GenerateBoundary;
          AMsg.MIMEBoundary.Push(LBoundary, i);
          //Make sure the header does not already have a pre-existing
          //boundary since we just generated a new one...
          IOHandler.WriteLn('Content-Type: ' + RemoveHeaderEntry(LLine, 'boundary', QuoteMIME) + ';');            {do not localize}
          IOHandler.WriteLn(TAB + 'boundary="' + LBoundary + '"');  {do not localize}
          IOHandler.WriteLn;
        end
        else begin
          //Not a multipart header, see if it is a part change...
          if not AMsg.IsMsgSinglePartMime then begin
            while AMsg.MessageParts.Items[i].ParentPart <> AMsg.MIMEBoundary.ParentPart do begin
              IOHandler.WriteLn('--' + LBoundary + '--');  {do not localize}
              IOHandler.WriteLn;
              AMsg.MIMEBoundary.Pop;  //This also pops AMsg.MIMEBoundary.ParentPart
              LBoundary := AMsg.MIMEBoundary.Boundary;
            end;
            IOHandler.WriteLn('--' + LBoundary);  {do not localize}
          end;
          if AMsg.MessageParts.Items[i] is TIdText then begin
            DoStatus(hsStatusText,  [RSMsgClientEncodingText]);
            WriteTextPart(AMsg.MessageParts.Items[i] as TIdText);
            IOHandler.WriteLn;
          end
          else if AMsg.MessageParts.Items[i] is TIdAttachment then begin
            LAttachment := TIdAttachment(AMsg.MessageParts[i]);
            DoStatus(hsStatusText, [RSMsgClientEncodingAttachment]);
            if LAttachment.ContentTransfer = '' then begin
              LAttachment.ContentTransfer := 'base64'; {do not localize}
            end;
            if LAttachment.ContentDisposition = '' then begin
              LAttachment.ContentDisposition := 'attachment'; {do not localize}
            end;
            if LAttachment.ContentType = '' then begin
              if TextIsSame(LAttachment.ContentTransfer, 'base64') then begin {do not localize}
                LAttachment.ContentType := 'application/octet-stream'; {do not localize}
              end else begin
                {CC4: Set default type if not base64 encoded...}
                LAttachment.ContentType := 'text/plain'; {do not localize}
              end;
            end;

            // TODO: when STRING_IS_ANSI is defined, provide a way for the user to specify the AnsiString encoding for header values...
            LFileName := EncodeHeader(ExtractFileName(LAttachment.FileName), '', HeaderEncoding, ISOCharSet); {do not localize}

            if TextIsSame(LAttachment.ContentTransfer, 'binhex40') then begin   {do not localize}
              //This is special - you do NOT write out any Content-Transfer-Encoding
              //header!  We also have to write a Content-Type specified in RFC 1741
              //(overriding any ContentType present, if necessary).
              LAttachment.ContentType := 'application/mac-binhex40';            {do not localize}
              IOHandler.Write('Content-Type: ' + LAttachment.ContentType); {do not localize}
              if LAttachment.CharSet <> '' then begin
                IOHandler.Write('; charset="' + LAttachment.CharSet + '"'); {do not localize}
              end;
              if LFileName <> '' then begin
                IOHandler.WriteLn(';'); {do not localize}
                IOHandler.Write(TAB + 'name="' + LFileName + '"'); {do not localize}
              end;
              IOHandler.WriteLn;
            end
            else begin
              IOHandler.Write('Content-Type: ' + LAttachment.ContentType); {do not localize}
              if LAttachment.CharSet <> '' then begin
                IOHandler.Write('; charset="' + LAttachment.CharSet + '"'); {do not localize}
              end;
              if LFileName <> '' then begin
                IOHandler.WriteLn(';');
                IOHandler.Write(TAB + 'name="' + LFileName + '"'); {do not localize}
              end;
              IOHandler.WriteLn;
              IOHandler.WriteLn('Content-Transfer-Encoding: ' + LAttachment.ContentTransfer); {do not localize}
              IOHandler.Write('Content-Disposition: ' + LAttachment.ContentDisposition); {do not localize}
              if LFileName <> '' then begin
                IOHandler.WriteLn(';');
                IOHandler.Write(TAB + 'filename="' + LFileName + '"'); {do not localize}
              end;
              IOHandler.WriteLn;
            end;
            if LAttachment.ContentID <> '' then begin
              IOHandler.WriteLn('Content-ID: '+ LAttachment.ContentID); {Do not Localize}
            end;
            if LAttachment.ContentDescription <> '' then begin
              IOHandler.WriteLn('Content-Description: ' + LAttachment.ContentDescription); {Do not localize}
            end;

            IOHandler.Write(LAttachment.ExtraHeaders);
            IOHandler.WriteLn;

            case PosInStrArray(LAttachment.ContentTransfer, ['base64', 'quoted-printable', 'binhex40'], False) of {do not localize}
              0: EncodeAttachment(LAttachment, TIdMessageEncoderMIME);
              1: EncodeAttachment(LAttachment, TIdMessageEncoderQuotedPrintable);
              2: EncodeAttachment(LAttachment, TIdMessageEncoderBinHex4);
              else
              begin
                LEncoding := CharsetToEncoding(LAttachment.Charset);
                LAttachStream := LAttachment.OpenLoadStream;
                try
                  while ReadLnFromStream(LAttachStream, LLine, -1, LEncoding) do begin
                    IOHandler.WriteLnRFC(LLine, LEncoding);
                  end;
                finally
                  LAttachment.CloseLoadStream;
                end;
              end;
            end;
            IOHandler.WriteLn;
          end;
        end;
      end;
      if AMsg.MessageParts.Count > 0 then begin
        for i := 0 to AMsg.MIMEBoundary.Count - 1 do begin
          if not AMsg.IsMsgSinglePartMime then begin
            IOHandler.WriteLn('--' + AMsg.MIMEBoundary.Boundary + '--');
            IOHandler.WriteLn;
          end;
          AMsg.MIMEBoundary.Pop;
        end;
      end;
    end;
  finally
    EndWork(wmWrite);
  end;
end;

procedure TIdMessageClient.SendMsg(AMsg: TIdMessage; AHeadersOnly: Boolean = False);
begin
  BeginWork(wmWrite);
  try
    if AMsg.NoEncode then begin
      IOHandler.Write(AMsg.Headers);
      IOHandler.WriteLn;
      if not AHeadersOnly then begin
        IOHandler.WriteRFCStrings(AMsg.Body, False);
      end;
    end else begin
      SendHeader(AMsg);
      if (not AHeadersOnly) then begin
        SendBody(AMsg);
      end;
    end;
  finally
    EndWork(wmWrite);
  end;
end;

function TIdMessageClient.ReceiveHeader(AMsg: TIdMessage; const AAltTerm: string = ''): string;
var
  LMsgEnd: Boolean;
begin
  BeginWork(wmRead);
  try
    repeat
      Result := IOHandler.ReadLnRFC(LMsgEnd);
      // Exchange Bug: Exchange sometimes returns . when getting a message instead of
      // '' then a . - That is there is no seperation between the header and the message for an
      // empty message.
      if ((Length(AAltTerm) = 0) and LMsgEnd) or  {do not localize}
         ({APR: why? (Length(AAltTerm) > 0) and }(Result = AAltTerm)) then begin
        Break;
      end else if Result <> '' then begin
        AMsg.Headers.Append(Result);
      end;
    until False;
    AMsg.ProcessHeaders;
  finally
    EndWork(wmRead);
  end;
end;

procedure TIdMessageClient.ProcessMessage(AMsg: TIdMessage; AHeaderOnly: Boolean = False);
begin
  if IOHandler <> nil then begin
    //Don't call ReceiveBody if the message ended at the end of the headers
    //(ReceiveHeader() would have returned '.' in that case)...
    BeginWork(wmRead);
    try
      if ReceiveHeader(AMsg) = '' then begin
        if not AHeaderOnly then begin
          ReceiveBody(AMsg);
        end;
      end;
    finally
      EndWork(wmRead);
    end;
  end;
end;

procedure TIdMessageClient.ProcessMessage(AMsg: TIdMessage; AStream: TStream; AHeaderOnly: Boolean = False);
var
  LIOHandler: TIdIOHandlerStreamMsg;
begin
  LIOHandler := TIdIOHandlerStreamMsg.Create(nil, AStream);
  try
    LIOHandler.FreeStreams := False;
    IOHandler := LIOHandler;
    try
      IOHandler.Open;
      ProcessMessage(AMsg, AHeaderOnly);
    finally
      IOHandler := nil;
    end;
  finally
    LIOHandler.Free;
  end;
end;

procedure TIdMessageClient.ProcessMessage(AMsg: TIdMessage; const AFilename: string; AHeaderOnly: Boolean = False);
var
  LStream: TStream;
begin
  LStream := TIdReadFileExclusiveStream.Create(AFileName); try
    ProcessMessage(AMsg, LStream, AHeaderOnly);
  finally FreeAndNil(LStream); end;
end;

procedure TIdMessageClient.EncodeAndWriteText(const ABody: TStrings; AEncoding: IIdTextEncoding);
begin
  Assert(ABody<>nil);
  Assert(IOHandler<>nil);
  // TODO: encode the text...
  IOHandler.WriteRFCStrings(ABody, False, AEncoding);
end;

destructor TIdMessageClient.Destroy;
begin
  inherited Destroy;
end;

end.
