unit IdMessageBuilder;

interface

{$i IdCompilerDefines.inc}

uses
  Classes, IdMessage;

type
  TIdMessageBuilderAttachment = class(TCollectionItem)
  private
    FContentID: String;
    FContentTransfer: String;
    FContentType: String;
    FData: TStream;
    FFileName: String;
    FName: String;
  public
    procedure Assign(Source: TPersistent); override;
    property ContentID: String read FContentID write FContentID;
    property ContentTransfer: String read FContentTransfer;
    property ContentType: String read FContentType write FContentType;
    property Data: TStream read FData write FData;
    property FileName: String read FFileName write FFileName;
    property Name: String read FName write FName;
  end;

  TIdMessageBuilderAttachments = class(TCollection)
  private
    function GetAttachment(Index: Integer): TIdMessageBuilderAttachment;
    procedure SetAttachment(Index: Integer; Value: TIdMessageBuilderAttachment);
  public
    constructor Create; reintroduce;
    function Add: TIdMessageBuilderAttachment; reintroduce; overload;
    function Add(const AFileName: String; const AContentID: String = ''): TIdMessageBuilderAttachment; overload;
    function Add(AData: TStream; const AContentType: String; const AContentID: String = ''): TIdMessageBuilderAttachment; overload;
    procedure AddToMessage(AMsg: TIdMessage; ParentPart: Integer);
    property Attachment[Index: Integer]: TIdMessageBuilderAttachment
      read GetAttachment write SetAttachment; default;
  end;

  TIdCustomMessageBuilder = class
  protected
    FAttachments: TIdMessageBuilderAttachments;
    FPlainText: TStrings;
    FPlainTextCharSet: String;
    FPlainTextContentTransfer: String;
    procedure AddAttachments(AMsg: TIdMessage);
    procedure FillBody(AMsg: TIdMessage); virtual; abstract;
    procedure FillHeaders(AMsg: TIdMessage); virtual;
    procedure SetPlainText(AValue: TStrings);
    procedure SetAttachments(AValue: TIdMessageBuilderAttachments);
  public
    constructor Create; virtual;
    destructor Destroy; override;
    //
    procedure Clear; virtual;
    procedure FillMessage(AMsg: TIdMessage);
    function NewMessage(AOwner: TComponent = nil): TIdMessage;
    //
    property Attachments: TIdMessageBuilderAttachments read FAttachments write SetAttachments;
    property PlainText: TStrings read FPlainText write SetPlainText;
    property PlainTextCharSet: String read FPlainTextCharSet write FPlainTextCharSet;
    property PlainTextContentTransfer: String read FPlainTextContentTransfer write FPlainTextContentTransfer;
  end;

  TIdMessageBuilderPlain = class(TIdCustomMessageBuilder)
  protected
    procedure FillBody(AMsg: TIdMessage); override;
    procedure FillHeaders(AMsg: TIdMessage); override;
  end;

  TIdMessageBuilderHtml = class(TIdCustomMessageBuilder)
  protected
    FHtml: TStrings;
    FHtmlCharSet: String;
    FHtmlContentTransfer: String;
    FHtmlFiles: TIdMessageBuilderAttachments;
    FHtmlViewerNeededMsg: String;
    procedure FillBody(AMsg: TIdMessage); override;
    procedure FillHeaders(AMsg: TIdMessage); override;
    procedure SetHtml(AValue: TStrings);
    procedure SetHtmlFiles(AValue: TIdMessageBuilderAttachments);
  public
    constructor Create; override;
    destructor Destroy; override;
    //
    procedure Clear; override;
    //
    property Html: TStrings read FHtml write SetHtml;
    property HtmlCharSet: String read FHtmlCharSet write FHtmlCharSet;
    property HtmlContentTransfer: String read FHtmlContentTransfer write FHtmlContentTransfer;
    property HtmlFiles: TIdMessageBuilderAttachments read FHtmlFiles write SetHtmlFiles;
    property HtmlViewerNeededMsg: String read FHtmlViewerNeededMsg write FHtmlViewerNeededMsg;
  end;

  TIdMessageBuilderRtfType = (idMsgBldrRtfMS, idMsgBldrRtfEnriched, idMsgBldrRtfRichtext);

  TIdMessageBuilderRtf = class(TIdCustomMessageBuilder)
  protected
    FRtf: TStrings;
    FRtfType: TIdMessageBuilderRtfType;
    FRtfViewerNeededMsg: String;
    procedure FillBody(AMsg: TIdMessage); override;
    procedure FillHeaders(AMsg: TIdMessage); override;
    procedure SetRtf(AValue: TStrings);
  public
    constructor Create; override;
    destructor Destroy; override;
    //
    procedure Clear; override;
    //
    property Rtf: TStrings read FRtf write SetRtf;
    property RtfType: TIdMessageBuilderRtfType read FRtfType write FRtfType;
    property RtfViewerNeededMsg: String read FRtfViewerNeededMsg write FRtfViewerNeededMsg;
  end;

implementation

uses
  IdGlobal, IdGlobalProtocols, IdMessageParts, IdAttachment, IdAttachmentFile,
  IdAttachmentMemory, IdResourceStringsProtocols, IdText, SysUtils;

const
  cTextPlain = 'text/plain'; {do not localize}
  cTextHtml = 'text/html'; {do not localize}
  cTextRtf: array[TIdMessageBuilderRtfType] of String = ('text/rtf', 'text/enriched', 'text/richtext'); {do not localize}
  cMultipartAlternative = 'multipart/alternative'; {do not localize}
  cMultipartMixed = 'multipart/mixed'; {do not localize}
  cMultipartRelatedHtml = 'multipart/related; type="text/html"'; {do not localize}

{ TIdMessageBuilderAttachment }

procedure TIdMessageBuilderAttachment.Assign(Source: TPersistent);
var
  LSource: TIdMessageBuilderAttachment;
begin
  if Source is TIdMessageBuilderAttachment then
  begin
    LSource := TIdMessageBuilderAttachment(Source);
    FContentID := LSource.FContentID;
    FContentTransfer := LSource.FContentTransfer;
    FContentType := LSource.FContentType;
    FData := LSource.FData;
    FFileName := LSource.FFileName;
    FName := LSource.FName;
  end else begin
    inherited Assign(Source);
  end;
end;

{ TIdMessageBuilderAttachments }

constructor TIdMessageBuilderAttachments.Create;
begin
  inherited Create(TIdMessageBuilderAttachment);
end;

function TIdMessageBuilderAttachments.Add: TIdMessageBuilderAttachment;
begin
  // This helps prevent unsupported TIdMessageBuilderAttachment from being added
  Result := nil;
end;

function TIdMessageBuilderAttachments.Add(const AFileName: String;
  const AContentID: String = ''): TIdMessageBuilderAttachment;
begin
  Result := TIdMessageBuilderAttachment(inherited Add);
  Result.FContentID := AContentID;
  Result.FFileName := AFileName;
end;

function TIdMessageBuilderAttachments.Add(AData: TStream; const AContentType: String;
  const AContentID: String = ''): TIdMessageBuilderAttachment;
begin
  Assert(AData <> nil);
  Result := TIdMessageBuilderAttachment(inherited Add);
  Result.FContentID := AContentID;
  Result.FContentType := AContentType;
  Result.FData := AData;
end;

procedure TIdMessageBuilderAttachments.AddToMessage(AMsg: TIdMessage; ParentPart: Integer);
var
  I: Integer;
  LMsgBldrAttachment: TIdMessageBuilderAttachment;
  LMsgAttachment: TIdAttachment;
  LStream: TStream;

  function FormatContentId(Item: TIdMessageBuilderAttachment): String;
  begin
    if Item.ContentID <> '' then begin
      Result := EnsureMsgIDBrackets(Item.ContentID);
    end else begin
      Result := '';
    end;
  end;

  function FormatContentType(Item: TIdMessageBuilderAttachment): String;
  begin
    if Item.ContentType <> '' then begin
      Result := Item.ContentType;
    end else begin
      Result := GetMIMETypeFromFile(Item.FileName);
    end;
  end;

  function FormatName(Item: TIdMessageBuilderAttachment): String;
  begin
    if Item.Name <> '' then begin
      Result := Item.Name;
    end
    else if Item.FileName <> '' then begin
      Result := ExtractFileName(Item.FileName);
    end else begin
      Result := '';
    end;
  end;

begin
  for I := 0 to Count-1 do
  begin
    LMsgBldrAttachment := Attachment[I];
    if Assigned(LMsgBldrAttachment.Data) then
    begin
      LMsgAttachment := TIdAttachmentMemory.Create(AMsg.MessageParts);
      try
        LMsgAttachment.FileName := ExtractFileName(LMsgBldrAttachment.FileName);
        LStream := LMsgAttachment.PrepareTempStream;
        try
          LStream.CopyFrom(LMsgBldrAttachment.Data, 0);
        finally
          LMsgAttachment.FinishTempStream;
        end;
      except
        LMsgAttachment.Free;
        raise;
      end;
    end else
    begin
      LMsgAttachment := TIdAttachmentFile.Create(AMsg.MessageParts, LMsgBldrAttachment.FileName);
    end;
    LMsgAttachment.Name := FormatName(LMsgBldrAttachment);
    LMsgAttachment.ContentId := FormatContentId(LMsgBldrAttachment);
    LMsgAttachment.ContentType := FormatContentType(LMsgBldrAttachment);
    LMsgAttachment.ContentTransfer := LMsgBldrAttachment.ContentTransfer;
    if ParentPart > -1 then
    begin
      if IsHeaderMediaType(LMsgAttachment.ContentType, 'image') then begin {do not localize}
        LMsgAttachment.ContentDisposition := 'inline'; {do not localize}
      end;
      LMsgAttachment.ParentPart := ParentPart;
    end;
  end;
end;

function TIdMessageBuilderAttachments.GetAttachment(Index: Integer): TIdMessageBuilderAttachment;
begin
  Result := TIdMessageBuilderAttachment(inherited GetItem(Index));
end;

procedure TIdMessageBuilderAttachments.SetAttachment(Index: Integer; Value: TIdMessageBuilderAttachment);
begin
  inherited SetItem(Index, Value);
end;

{ TIdCustomMessageBuilder }

constructor TIdCustomMessageBuilder.Create;
begin
  inherited Create;
  FPlainText := TStringList.Create;
  FAttachments := TIdMessageBuilderAttachments.Create;
end;

destructor TIdCustomMessageBuilder.Destroy;
begin
  FPlainText.Free;
  FAttachments.Free;
  inherited Destroy;
end;

procedure TIdCustomMessageBuilder.AddAttachments(AMsg: TIdMessage);
begin
  FAttachments.AddToMessage(AMsg, -1);
end;

procedure TIdCustomMessageBuilder.Clear;
begin
  FAttachments.Clear;
  FPlainText.Clear;
  FPlainTextCharSet := '';
  FPlainTextContentTransfer := '';
end;

procedure TIdCustomMessageBuilder.FillMessage(AMsg: TIdMessage);
begin
  if not Assigned(AMsg) then begin
    Exit;
  end;

  // Clear only the body, ContentType, CharSet, and ContentTransferEncoding here...
  //
  AMsg.ClearBody;
  AMsg.ContentType := '';
  AMsg.CharSet := '';
  AMsg.ContentTransferEncoding := '';

  // let the message decide how to encode itself
  // based on what parts are added in InternalFill()
  //
  AMsg.Encoding := meDefault;

  // fill in type-specific content first
  //
  FillBody(AMsg);

  // Are non-related attachments present?
  //
  AddAttachments(AMsg);

  // Determine the top-level ContentType and
  // ContentTransferEncoding for the message now
  //
  FillHeaders(AMsg);
end;

function TIdCustomMessageBuilder.NewMessage(AOwner: TComponent = nil): TIdMessage;
begin
  Result := TIdMessage.Create(AOwner);
  try
    FillMessage(Result);
  except
    FreeAndNil(Result);
    raise;
  end;
end;

procedure TIdCustomMessageBuilder.SetAttachments(AValue: TIdMessageBuilderAttachments);
begin
  FAttachments.Assign(AValue);
end;

procedure TIdCustomMessageBuilder.FillHeaders(AMsg: TIdMessage);
var
  LPart: TIdMessagePart;
begin
  if FAttachments.Count > 0 then
  begin
    if AMsg.MessageParts.Count > 1 then
    begin
      // plain text and/or formatting, and at least 1 non-related attachment
      //
      AMsg.ContentType := cMultipartMixed;
      AMsg.CharSet := '';
      AMsg.ContentTransferEncoding := '';
    end else
    begin
      // no plain text or formatting, only 1 non-related attachment
      //
      LPart := AMsg.MessageParts[0];
      AMsg.ContentType := LPart.ContentType;
      AMsg.CharSet := LPart.CharSet;
      AMsg.ContentTransferEncoding := LPart.ContentTransfer;
    end;
  end else
  begin
    AMsg.ContentType := '';
    AMsg.CharSet := '';
    AMsg.ContentTransferEncoding := '';
  end;
end;

procedure TIdCustomMessageBuilder.SetPlainText(AValue: TStrings);
begin
  FPlainText.Assign(AValue);
end;

{ TIdMessageBuilderPlain }

procedure TIdMessageBuilderPlain.FillBody(AMsg: TIdMessage);
var
  LTextPart: TIdText;
begin
  // Is plain text present?
  //
  if FPlainText.Count > 0 then
  begin
    // Should the message contain only plain text?
    //
    if FAttachments.Count = 0 then
    begin
      AMsg.Body.Assign(FPlainText);
    end else
    begin
      // At this point, multiple pieces will be present in the message
      // body, so everything must be stored in the MessageParts collection...
      //
      LTextPart := TIdText.Create(AMsg.MessageParts, FPlainText);
      LTextPart.ContentType := cTextPlain;
      LTextPart.CharSet := FPlainTextCharSet;
      LTextPart.ContentTransfer := FPlainTextContentTransfer;
    end;
  end;
end;

procedure TIdMessageBuilderPlain.FillHeaders(AMsg: TIdMessage);
begin
  if (FPlainText.Count > 0) and (FAttachments.Count = 0) then
  begin
    // plain text only
    //
    AMsg.ContentType := cTextPlain;
    AMsg.CharSet := FPlainTextCharSet;
    AMsg.ContentTransferEncoding := FPlainTextContentTransfer;
  end else
  begin
    inherited FillHeaders(AMsg);
  end;
end;

{ TIdMessageBuilderHtml }

constructor TIdMessageBuilderHtml.Create;
begin
  inherited Create;
  FHtml := TStringList.Create;
  FHtmlFiles := TIdMessageBuilderAttachments.Create;
  FHtmlViewerNeededMsg := rsHtmlViewerNeeded;
end;

destructor TIdMessageBuilderHtml.Destroy;
begin
  FHtml.Free;
  FHtmlFiles.Free;
  inherited Destroy;
end;

procedure TIdMessageBuilderHtml.Clear;
begin
  FHtml.Clear;
  FHtmlCharSet := '';
  FHtmlContentTransfer := '';
  FHtmlFiles.Clear;
  inherited Clear;
end;

procedure TIdMessageBuilderHtml.FillBody(AMsg: TIdMessage);
var
  LUsePlain, LUseHtml, LUseHtmlFiles, LUseAttachments: Boolean;
  LAlternativeIndex, LRelatedIndex: Integer;
  LTextPart: TIdText;
begin
  // Cache these for better performance
  //
  LUsePlain := FPlainText.Count > 0;
  LUseHtml := FHtml.Count > 0;
  LUseHtmlFiles := LUseHtml and (FHtmlFiles.Count > 0);
  LUseAttachments := FAttachments.Count > 0;

  LAlternativeIndex := -1;
  LRelatedIndex := -1;

  // Is any body data present at all?
  //
  if not (LUsePlain or LUseHtml or LUseHtmlFiles or LUseAttachments) then begin
    Exit;
  end;

  // Should the message contain only plain text?
  //
  if LUsePlain and not (LUseHtml or LUseAttachments) then
  begin
    AMsg.Body.Assign(FPlainText);
    Exit;
  end;

  // Should the message contain only HTML?
  //
  if LUseHtml and not (LUsePlain or LUseHtmlFiles or LUseAttachments) then
  begin
    // TODO: create "multipart/alternative" pieces if FHtmlViewerNeededMsg is not empty...
    AMsg.Body.Assign(FHtml);
    Exit;
  end;

  // At this point, multiple pieces will be present in the message
  // body, so everything must be stored in the MessageParts collection...

  // If the message should contain both plain text and HTML, a
  // "multipart/alternative" piece is needed to wrap them if
  // non-related attachments are also present...
  //
  // RLebeau 5/23/2011: need to output the Alternative piece if
  // the "HTML Viewer is needed" text is going to be used...
  //
  if {LUsePlain and} LUseHtml and LUseAttachments then
  begin
    LTextPart := TIdText.Create(AMsg.MessageParts, nil);
    LTextPart.ContentType := cMultipartAlternative;
    LAlternativeIndex := LTextPart.Index;
  end;

  // Is plain text present?
  //
  if LUsePlain or LUseHtml then
  begin
    LTextPart := TIdText.Create(AMsg.MessageParts, FPlainText);
    begin
      if LUseHtml and (not LUsePlain) then
      begin
        LTextPart.Body.Text := FHtmlViewerNeededMsg;
      end;
      LTextPart.ContentType := cTextPlain;
      LTextPart.CharSet := FPlainTextCharSet;
      LTextPart.ContentTransfer := FPlainTextContentTransfer;
      LTextPart.ParentPart := LAlternativeIndex;
    end;
  end;

  // Is HTML present?
  //
  if LUseHtml then
  begin
    // related attachments can't be referenced by, or used inside
    // of, plain text, so there is no point in wrapping the plain
    // text inside the same "multipart/related" part with the HTML
    // and attachments.  Some email programs don't do that as well.
    // This logic is newer and more accurate than what is described
    // in the "HTML Messages" article found on Indy's website.
    //
    if LUseHtmlFiles then
    begin
      LTextPart := TIdText.Create(AMsg.MessageParts, nil);
      LTextPart.ContentType := cMultipartRelatedHtml;
      LTextPart.ParentPart := LAlternativeIndex;
      LRelatedIndex := LTextPart.Index;
    end;

    // Add HTML
    //
    LTextPart := TIdText.Create(AMsg.MessageParts, FHtml);
    LTextPart.ContentType := cTextHtml;
    LTextPart.CharSet := FHtmlCharSet;
    LTextPart.ContentTransfer := FHtmlContentTransfer;
    if LRelatedIndex <> -1 then begin
      LTextPart.ParentPart := LRelatedIndex; // plain text and related attachments
    end else begin
      LTextPart.ParentPart := LAlternativeIndex; // plain text and optional non-related attachments
    end;

    // Are related attachments present?
    //
    if LUseHtmlFiles then begin
      FHtmlFiles.AddToMessage(AMsg, LRelatedIndex);
    end;
  end;
end;

procedure TIdMessageBuilderHtml.FillHeaders(AMsg: TIdMessage);
begin
  if FAttachments.Count = 0 then
  begin
    if (FPlainText.Count > 0) and (FHtml.Count = 0) then
    begin
      // plain text only
      //
      AMsg.ContentType := cTextPlain;
      AMsg.CharSet := FPlainTextCharSet;
      AMsg.ContentTransferEncoding := FPlainTextContentTransfer;
    end
    else if FHtml.Count > 0 then
    begin
      if (FPlainText.Count = 0) and (FHtmlFiles.Count = 0) then
      begin
        // HTML only
        //
        AMsg.ContentType := cTextHtml;
        AMsg.CharSet := FHtmlCharSet;
        AMsg.ContentTransferEncoding := FHtmlContentTransfer;
      end else
      begin
        // plain text and HTML and no related attachments
        //
        AMsg.ContentType := cMultipartAlternative;
        AMsg.CharSet := '';
        AMsg.ContentTransferEncoding := '';
      end;
    end;
  end else
  begin
    inherited FillHeaders(AMsg);
  end;
end;

procedure TIdMessageBuilderHtml.SetHtml(AValue: TStrings);
begin
  FHtml.Assign(AValue);
end;

procedure TIdMessageBuilderHtml.SetHtmlFiles(AValue: TIdMessageBuilderAttachments);
begin
  FHtmlFiles.Assign(AValue);
end;

{ TIdMessageBuilderRTF }

constructor TIdMessageBuilderRtf.Create;
begin
  inherited Create;
  FRtf := TStringList.Create;
  FRtfType := idMsgBldrRtfMS;
  FRtfViewerNeededMsg := rsRtfViewerNeeded;
end;

destructor TIdMessageBuilderRtf.Destroy;
begin
  FRtf.Free;
  inherited Destroy;
end;

procedure TIdMessageBuilderRtf.Clear;
begin
  FRtf.Clear;
  inherited Clear;
end;

procedure TIdMessageBuilderRtf.FillBody(AMsg: TIdMessage);
var
  LUsePlain, LUseRtf, LUseAttachments: Boolean;
  LAlternativeIndex: Integer;
  LTextPart: TIdText;
begin
  // Cache these for better performance
  //
  LUsePlain := FPlainText.Count > 0;
  LUseRtf := FRtf.Count > 0;
  LUseAttachments := FAttachments.Count > 0;
  LAlternativeIndex := -1;

  // Is any body data present at all?
  //
  if not (LUsePlain or LUseRtf or LUseAttachments) then begin
    Exit;
  end;

  // Should the message contain only plain text?
  //
  if LUsePlain and not (LUseRtf or LUseAttachments) then
  begin
    AMsg.Body.Assign(FPlainText);
    Exit;
  end;

  // Should the message contain only RTF?
  //
  if LUseRtf and not (LUsePlain or LUseAttachments) then
  begin
    // TODO: create "multipart/alternative" pieces if FRtfViewerNeededMsg is not empty...
    AMsg.Body.Assign(FRtf);
    Exit;
  end;

  // At this point, multiple pieces will be present in the message
  // body, so everything must be stored in the MessageParts collection...

  // If the message should contain both plain text and RTF, a
  // "multipart/alternative" piece is needed to wrap them if
  // attachments are also present...
  //
  // RLebeau 11/11/2013: need to output the Alternative piece if
  // the "RTF Viewer is needed" text is going to be used...
  //
  if {LUsePlain and} LUseRtf and LUseAttachments then
  begin
    LTextPart := TIdText.Create(AMsg.MessageParts, nil);
    LTextPart.ContentType := cMultipartAlternative;
    LAlternativeIndex := LTextPart.Index;
  end;

  // Is plain text present?
  //
  if LUsePlain or LUseRtf then
  begin
    LTextPart := TIdText.Create(AMsg.MessageParts, FPlainText);
    if LUseRtf and (not LUsePlain) then
    begin
      LTextPart.Body.Text := FRtfViewerNeededMsg;
    end;
    LTextPart.ContentType := cTextPlain;
    LTextPart.CharSet := FPlainTextCharSet;
    LTextPart.ContentTransfer := FPlainTextContentTransfer;
    LTextPart.ParentPart := LAlternativeIndex;
  end;

  // Is RTF present?
  //
  if LUseRtf then
  begin
    // Add RTF
    //
    LTextPart := TIdText.Create(AMsg.MessageParts, FRtf);
    LTextPart.ContentType := cTextRtf[FRtfType];
    LTextPart.ParentPart := LAlternativeIndex; // plain text and optional non-related attachments
  end;
end;

procedure TIdMessageBuilderRtf.FillHeaders(AMsg: TIdMessage);
begin
  if FAttachments.Count = 0 then
  begin
    if (FPlainText.Count > 0) and (FRtf.Count = 0) then
    begin
      // plain text only
      //
      AMsg.ContentType := cTextPlain;
      AMsg.CharSet := FPlainTextCharSet;
      AMsg.ContentTransferEncoding := FPlainTextContentTransfer;
    end
    else if (FRtf.Count > 0) and (FPlainText.Count = 0) then
    begin
      // RTF only
      //
      AMsg.ContentType := cTextRtf[FRtfType];
      AMsg.CharSet := '';
      AMsg.ContentTransferEncoding := '';
    end else
    begin
      // plain text and RTF and no non-related attachments
      //
      AMsg.ContentType := cMultipartAlternative;
      AMsg.CharSet := '';
      AMsg.ContentTransferEncoding := '';
    end;
  end else
  begin
    inherited FillHeaders(AMsg);
  end;
end;

procedure TIdMessageBuilderRtf.SetRtf(AValue: TStrings);
begin
  FRtf.Assign(AValue);
end;

end.
