{ lNet MIME Wrapper

  CopyRight (C) 2007-2008 by Ales Katona

  This library is Free software; you can rediStribute it and/or modify it
  under the terms of the GNU Library General Public License as published by
  the Free Software Foundation; either version 2 of the License, or (at your
  option) any later version.

  This program is diStributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; withOut even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
  for more details.

  You should have received a Copy of the GNU Library General Public License
  along with This library; if not, Write to the Free Software Foundation,
  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

  This license has been modified. See File LICENSE.ADDON for more inFormation.
  Should you find these sources without a LICENSE File, please contact
  me at ales@chello.sk
}

unit lMimeWrapper;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, Classes, Contnrs, lMimeStreams;
  
const
  MIME_VERSION = 'MIME-version: 1.0' + CRLF;

type
  TMimeEncoding = (me8bit, meBase64);
  TMimeDisposition = (mdInline, mdAttachment);

  { TMimeSection }

  TMimeSection = class
   protected
    FContentType: string;
    FEncoding: TMimeEncoding;
    FActivated: Boolean;
    FDescription: string;
    FDisposition: TMimeDisposition;
    FBuffer: string;
    FEncodingStream: TStream;
    FOutputStream: TStream;
    FLocalStream: TBogusStream;
    function RecalculateSize(const OriginalSize: Integer): Integer;
    function GetSize: Integer; virtual; abstract;
    procedure SetDescription(const AValue: string);
    procedure SetDisposition(const AValue: TMimeDisposition);
    procedure SetEncoding(const AValue: TMimeEncoding);
    procedure CreateEncodingStream; virtual;
    function GetHeader: string; virtual;
    function ReadBuffer(const aSize: Integer): string;
    procedure FillBuffer(const aSize: Integer); virtual; abstract;
   public
    constructor Create(aOutputStream: TStream);
    destructor Destroy; override;
    function Read(const aSize: Integer): Integer;
    procedure Reset; virtual;
   public
    property ContentType: string read FContentType write FContentType;
    property Encoding: TMimeEncoding read FEncoding write SetEncoding;
    property Disposition: TMimeDisposition read FDisposition write SetDisposition;
    property Description: string read FDescription write SetDescription;
    property Header: string read GetHeader;
    property Size: Integer read GetSize;
  end;
  
  { TMimeTextSection }

  TMimeTextSection = class(TMimeSection)
   protected
    FOriginalData: string;
    FData: string;
    function GetSize: Integer; override;
    procedure SetData(const AValue: string);
    function GetCharset: string;
    procedure SetCharset(const AValue: string);
    procedure FillBuffer(const aSize: Integer); override;
   public
    constructor Create(aOutputStream: TStream; const aText: string);
    procedure Reset; override;
   public
    property Charset: string read GetCharset write SetCharset;
    property Text: string read FData write SetData;
  end;
  
  { TMimeStreamSection }

  TMimeStreamSection = class(TMimeSection)
   protected
    FStream: TStream;
    FOwnsStreams: Boolean;
    FOriginalPosition: Int64;
    function GetSize: Integer; override;
    procedure SetStream(aValue: TStream);
    procedure FillBuffer(const aSize: Integer); override;
   public
    constructor Create(aOutputStream: TStream; aStream: TStream);
    destructor Destroy; override;
    procedure Reset; override;
   public
    property Stream: TStream read FStream write SetStream;
    property OwnsStreams: Boolean read FOwnsStreams write FOwnsStreams;
  end;
  
  { TMimeFileSection }

  TMimeFileSection = class(TMimeStreamSection)
   protected
    FFileName: string;
    procedure SetFileName(const AValue: string);
    procedure SetContentType(const aFileName: string);
    function GetHeader: string; override;
   public
    constructor Create(aOutputStream: TStream; const aFileName: string);
    property FileName: string read FFileName write SetFileName;
  end;

  { TMimeStream }

  TMimeStream = class(TStream)
   protected
    FSections: TFPObjectList;
    FOutputStream: TMimeOutputStream;
    FBoundary: string;
    FActiveSection: Integer;
    FCalledRead: Boolean;
    FCalledWrite: Boolean;
    function GetBoundarySize: Integer;
    function GetSize: Int64; override;
    function GetCount: Integer;
    function GetBoundary: string;
    function GetSection(i: Integer): TMimeSection;
    function GetMimeHeader: string;
    procedure SetSection(i: Integer; const AValue: TMimeSection);
    procedure ActivateFirstSection;
    procedure ActivateNextSection;
    procedure DoRead(const aSize: Integer);
   public
    constructor Create;
    destructor Destroy; override;
    function Seek(Offset: Longint; Origin: Word): Longint; overload; override;
    function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; overload; override;
    function Read(var Buffer; Count: Longint): Longint; override;
    function Write(const Buffer; Count: Longint): Longint; override;
    procedure AddTextSection(const aText: string; const aCharSet: string = 'UTF-8');
    procedure AddFileSection(const aFileName: string);
    procedure AddStreamSection(aStream: TStream; const FreeStream: Boolean = False);
    procedure Delete(const i: Integer);
    procedure Remove(aSection: TMimeSection);
    procedure Reset;
   public
    property Sections[i: Integer]: TMimeSection read GetSection write SetSection; default;
    property Count: Integer read GetCount;
    property Boundary: string read FBoundary;
  end;
  
  { EAlreadyActivatedException }

  EAlreadyActivatedException = class(Exception)
   public
    constructor Create;
  end;
  
  { EAlreadyCalledReadException }

  EAlreadyCalledReadException = class(Exception)
   public
    constructor Create;
  end;

  { EAlreadyCalledWriteException }

  EAlreadyCalledWriteException = class(Exception)
   public
    constructor Create;
  end;
  
implementation

uses
  Math, Base64;
  
function EncodingToStr(const Encoding: TMimeEncoding): string;
begin
  Result := '';
  case Encoding of
    me8bit   : Result := '8bit';
    meBase64 : Result := 'base64';
  end;
end;

function DispositionToStr(const Disposition: TMimeDisposition): string;
begin
  Result := '';
  case Disposition of
    mdInline     : Result := 'inline';
    mdAttachment : Result := 'attachment';
  end;
end;

{ TMimeSection }

function TMimeSection.RecalculateSize(const OriginalSize: Integer): Integer;
begin
  Result := 0;

  if OriginalSize = 0 then
    Exit;
    
  case FEncoding of
    me8bit   : Result := OriginalSize;
    meBase64 : if OriginalSize mod 3 = 0 then
                 Result := (OriginalSize div 3) * 4 // this is simple, 4 bytes per 3 bytes
               else
                 Result := ((OriginalSize + 3) div 3) * 4; // add "padding" trupplet
  end;
end;

procedure TMimeSection.SetDescription(const AValue: string);
begin
  if not FActivated then
    FDescription := AValue;
end;

procedure TMimeSection.SetDisposition(const AValue: TMimeDisposition);
begin
  if not FActivated then
    FDisposition := AValue;
end;

procedure TMimeSection.SetEncoding(const AValue: TMimeEncoding);
begin
  if not FActivated then begin
    FEncoding := aValue;
    if Assigned(FEncodingStream) then
      FEncodingStream.Free;
    
    CreateEncodingStream;
  end;
end;

procedure TMimeSection.CreateEncodingStream;
begin
  case FEncoding of
    me8bit   : FEncodingStream := nil;
    meBase64 : FEncodingStream := TBase64EncodingStream.Create(FLocalStream);
  end;
end;

function TMimeSection.GetHeader: string;
begin
  Result := 'Content-Type: ' + FContentType + CRLF;
  Result := Result + 'Content-Transfer-Encoding: ' + EncodingToStr(FEncoding) + CRLF;
  Result := Result + 'Content-Disposition: ' + DispositionToStr(FDisposition) + CRLF;

  if Length(FDescription) > 0 then
    Result := Result + 'Content-Description: ' + FDescription + CRLF;
    
  Result := Result + CRLF;
end;

function TMimeSection.ReadBuffer(const aSize: Integer): string;
begin
  Result := '';

  if aSize >= Length(FBuffer) then
    FillBuffer(aSize);
    
  Result := Copy(FBuffer, 1, aSize);
end;

constructor TMimeSection.Create(aOutputStream: TStream);
begin
  FOutputStream := aOutputStream;
  FEncodingStream := nil;
  FLocalStream := TBogusStream.Create;
end;

destructor TMimeSection.Destroy;
begin
  if Assigned(FEncodingStream) then
    FEncodingStream.Free;
    
  FLocalStream.Free;

  inherited Destroy;
end;

function TMimeSection.Read(const aSize: Integer): Integer;
var
  s: string;
begin
  Result := 0;

  if aSize <= 0 then
    Exit;

  if not FActivated then begin
    FActivated := True;
    FBuffer := GetHeader;
  end;
  
  if Length(FBuffer) < aSize then
    FillBuffer(aSize);
    
  s := ReadBuffer(aSize);
  if Length(s) >= aSize then begin
    Result := FOutputStream.Write(s[1], aSize);
    Delete(FBuffer, 1, Result);
  end else if Length(s) > 0 then begin
    Result := FOutputStream.Write(s[1], Length(s));
    Delete(FBuffer, 1, Result);
  end;
end;

procedure TMimeSection.Reset;
begin
  FActivated := False;
  FBuffer := '';
  FLocalStream.Reset;
  SetEncoding(FEncoding);
end;

{ TMimeTextSection }

procedure TMimeTextSection.SetCharset(const aValue: string);
begin
  if not FActivated then begin
    if Length(aValue) > 0 then
      FContentType := 'text/plain; charset="' + aValue + '"'
    else
      FContentType := 'text/plain';
  end;
end;

procedure TMimeTextSection.FillBuffer(const aSize: Integer);
var
  s: string;
  n: Integer;
begin
  s := Copy(FData, 1, aSize);
  
  if Length(s) = 0 then
    Exit;
  
  n := aSize;

  if Assigned(FEncodingStream) then begin
    n := FEncodingStream.Write(s[1], Length(s));
    Delete(FData, 1, n);

    if Length(FData) = 0 then begin
      FEncodingStream.Free; // to fill in the last bit
      CreateEncodingStream;
      FLocalStream.Write(CRLF[1], Length(CRLF));
    end;
    
    SetLength(s, FLocalStream.Size);
    SetLength(s, FLocalStream.Read(s[1], Length(s)));
  end else begin
    Delete(FData, 1, n);
    if Length(FData) = 0 then
      s := s + CRLF;
  end;

  FBuffer := FBuffer + s;
end;

function TMimeTextSection.GetSize: Integer;
begin
  if FActivated then
    Result := Length(FBuffer) + RecalculateSize(Length(FData))
  else
    Result := Length(FBuffer) + Length(GetHeader) + RecalculateSize(Length(FData));

  if not FActivated
  or (Length(FBuffer) > 0)
  or (Length(FData) > 0) then
    if Length(FOriginalData) > 0 then
      Result := Result + Length(CRLF); // CRLF after each msg body
end;

procedure TMimeTextSection.SetData(const AValue: string);
begin
  if not FActivated then begin
    FOriginalData := aValue;
    FData := aValue;
  end;
end;

function TMimeTextSection.GetCharset: string;
var
  n: Integer;
begin
  Result := '';

  n := Pos('=', FContentType);
  if n > 0 then
    Result := StringReplace(Copy(FContentType, n + 1, Length(FContentType)),
                            '"', '', [rfReplaceAll]);
end;

constructor TMimeTextSection.Create(aOutputStream: TStream; const aText: string);
begin
  inherited Create(aOutputStream);

  FContentType := 'text/plain; charset="UTF-8"';
  FOriginalData := aText;
  FData := FOriginalData;
end;

procedure TMimeTextSection.Reset;
begin
  inherited Reset;
  FData := FOriginalData;
end;

{ TMimeStreamSection }

function TMimeStreamSection.GetSize: Integer;
begin
  if FActivated then
    Result := Length(FBuffer) + RecalculateSize(FStream.Size - FStream.Position)
  else
    Result := Length(FBuffer) + Length(GetHeader) + RecalculateSize(FStream.Size - FStream.Position);
    
  if not FActivated
  or (Length(FBuffer) > 0)
  or (FStream.Size - FStream.Position > 0) then
    if FStream.Size - FOriginalPosition > 0 then
      Result := Result + Length(CRLF); // CRLF after each msg body
end;

procedure TMimeStreamSection.SetStream(aValue: TStream);
begin
  if Assigned(FStream)
  and FOwnsStreams then begin
    FStream.Free;
    FStream := nil;
  end;
  
  FStream := aValue;
  FOriginalPosition := FStream.Position;
end;

procedure TMimeStreamSection.FillBuffer(const aSize: Integer);
var
  s: string;
  n: Integer;
begin
  SetLength(s, aSize);
  SetLength(s, FStream.Read(s[1], aSize));
  
  if Length(s) <= 0 then
    Exit;
  
  if Assigned(FEncodingStream) then begin
    n := FEncodingStream.Write(s[1], Length(s));
    
    if n < Length(s) then
      FStream.Position := FStream.Position - (n - Length(s));
      
    if FStream.Size - FStream.Position = 0 then begin
      FEncodingStream.Free; // to fill in the last bit
      CreateEncodingStream;
      FLocalStream.Write(CRLF[1], Length(CRLF));
    end;
      
    SetLength(s, FLocalStream.Size);
    SetLength(s, FLocalStream.Read(s[1], FLocalStream.Size));
  end else if FStream.Size - FStream.Position = 0 then
    s := s + CRLF;

  FBuffer := FBuffer + s;
end;

constructor TMimeStreamSection.Create(aOutputStream: TStream; aStream: TStream);
begin
  inherited Create(aOutputStream);
  
  FDisposition := mdAttachment;
  FStream := aStream;
  FOriginalPosition := FStream.Position;
  FContentType := 'application/octet-stream';
end;

destructor TMimeStreamSection.Destroy;
begin
  if FOwnsStreams then
    FStream.Free;

  inherited Destroy;
end;

procedure TMimeStreamSection.Reset;
begin
  inherited Reset;
  FStream.Position := FOriginalPosition;
end;

{ TMimeStream }

function TMimeStream.GetBoundarySize: Integer;
var
  n: Integer;
begin
  Result := 0;

  if FSections.Count > 1 then begin
    n := Max(FActiveSection, 0);
    Result := (Length(FBoundary) + 4) * (FSections.Count - n) + 2;
    // # sections * (boundarylength + --CRLF +) ending --
  end;
end;

function TMimeStream.GetSize: Int64;
var
  i: Integer;
begin
  Result := 0;
  
  if FActiveSection > -2 then
    for i := 0 to Count - 1 do
      Result := Result + TMimeSection(FSections[i]).Size;
      
  if FActiveSection = -1 then // not yet active, must add header info
    Result := Result + Length(GetMimeHeader) + GetBoundarySize;
    
  Result := Result + FOutputStream.Size;
end;

function TMimeStream.GetCount: Integer;
begin
  Result := FSections.Count;
end;

function TMimeStream.GetBoundary: string;
var
  i: Integer;
begin
  Result := '';
  for i := 1 to 25 + Random(15) do
    Result := Result + Char(Random(Ord('9') - Ord('0') + 1) + Ord('0'));
end;

function TMimeStream.GetSection(i: Integer): TMimeSection;
begin
  Result := nil;
  
  if  (i >= 0)
  and (i < FSections.Count) then
    Result := TMimeSection(FSections[i]);
end;

function TMimeStream.GetMimeHeader: string;
const
  MIME_HEADER = 'Content-type: multipart/mixed; boundary="';
begin
  Result := MIME_VERSION;
  
  if FSections.Count > 1 then
    Result := Result + MIME_HEADER + FBoundary + '"' + CRLF + CRLF +
         'This is a multi-part message in MIME format.' + CRLF +
         '--' + FBoundary + CRLF;
end;

procedure TMimeStream.SetSection(i: Integer; const AValue: TMimeSection);
begin
  if  (i >= 0)
  and (i < FSections.Count) then
    FSections[i] := aValue;
end;

procedure TMimeStream.ActivateFirstSection;
var
  s: string;
begin
  if FActiveSection = -1 then
    if FSections.Count > 0 then begin
      FActiveSection := 0;
      s := GetMimeHeader;
      FOutputStream.Write(s[1], Length(s));
    end;
end;

procedure TMimeStream.ActivateNextSection;
var
  s: string;
begin
  Inc(FActiveSection);

  if FSections.Count > 1 then begin
    if FActiveSection >= FSections.Count then
      s := '--' + FBoundary + '--' + CRLF
    else
      s := '--' + FBoundary + CRLF;
      
    FOutputStream.Write(s[1], Length(s));
  end;

  if FActiveSection >= FSections.Count then
    FActiveSection := -2;
end;

procedure TMimeStream.DoRead(const aSize: Integer);
begin
  ActivateFirstSection;
  
  if FActiveSection < 0 then
    Exit;
    
  TMimeSection(FSections[FActiveSection]).Read(aSize);
  
  if TMimeSection(FSections[FActiveSection]).Size = 0 then
    ActivateNextSection;
end;

constructor TMimeStream.Create;
begin
  Randomize;
  
  FActiveSection := -1;
  FBoundary := GetBoundary;
  FSections := TFPObjectList.Create(True);
  FOutputStream := TMimeOutputStream.Create(@DoRead);
end;

destructor TMimeStream.Destroy;
begin
  FSections.Free;
  FOutputStream.Free;

  inherited Destroy;
end;

function TMimeStream.Seek(Offset: Longint; Origin: Word): Longint;
begin
  Result := Offset;
end;

function TMimeStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;
begin
  Result := Offset;
end;

function TMimeStream.Read(var Buffer; Count: Longint): Longint;
begin
  if Count <= 0 then
    Exit(0);
    
  if FCalledWrite then
    raise EAlreadyCalledWriteException.Create;

  FCalledRead := True;
  Result := FOutputStream.Read(Buffer, Count);
end;

function TMimeStream.Write(const Buffer; Count: Longint): Longint;
begin
  if Count <= 0 then
    Exit(0);

  if FCalledRead then
    raise EAlreadyCalledReadException.Create;

  Result := 0;
  FCalledWrite := True;
  raise Exception.Create('Not yet implemented');
end;

procedure TMimeStream.AddTextSection(const aText: string; const aCharSet: string = 'UTF-8');
var
  s: TMimeTextSection;
begin
  if FActiveSection >= 0 then
    raise EAlreadyActivatedException.Create;
    
  s := TMimeTextSection.Create(FOutputStream, aText);
  
  s.Charset := aCharSet;
  FSections.Add(s);
end;

procedure TMimeStream.AddFileSection(const aFileName: string);
begin
  if FActiveSection >= 0 then
    raise EAlreadyActivatedException.Create;

  FSections.Add(TMimeFileSection.Create(FOutputStream, aFileName));
end;

procedure TMimeStream.AddStreamSection(aStream: TStream; const FreeStream: Boolean
  );
var
  s: TMimeStreamSection;
begin
  if FActiveSection >= 0 then
    raise EAlreadyActivatedException.Create;

  s := TMimeStreamSection.Create(FOutputStream, aStream);
  if FreeStream then
    s.OwnsStreams := True;
  FSections.Add(s);
end;

procedure TMimeStream.Delete(const i: Integer);
begin
  if (i >= 0) and (i < Count) then
    FSections.Delete(i);
end;

procedure TMimeStream.Remove(aSection: TMimeSection);
begin
  FSections.Remove(aSection);
end;

procedure TMimeStream.Reset;
var
  i: Integer;
begin
  FCalledRead := False;
  FCalledWrite := False;
  
  for i := 0 to FSections.Count - 1 do
    TMimeSection(FSections[i]).Reset;
    
  FOutputStream.Reset;
  FActiveSection := -1;
end;

{ TMimeFileSection }

procedure TMimeFileSection.SetFileName(const AValue: string);
begin
  if not FActivated then begin
    FFileName := aValue;
    Stream := TFileStream.Create(aValue, fmOpenRead);
    SetContentType(aValue);
  end;
end;

procedure TMimeFileSection.SetContentType(const aFileName: string);
var
  s: string;
begin
  s := StringReplace(ExtractFileExt(aFileName), '.', '', [rfReplaceAll]);

  if (s = 'txt')
  or (s = 'pas')
  or (s = 'pp')
  or (s = 'pl')
  or (s = 'cpp')
  or (s = 'cc')
  or (s = 'h')
  or (s = 'hh')
  or (s = 'rb')
  or (s = 'pod')
  or (s = 'php')
  or (s = 'php3')
  or (s = 'php4')
  or (s = 'php5')
  or (s = 'c++') then FContentType := 'text/plain';
  
  if (s = 'html')
  or (s = 'shtml') then FContentType := 'text/html';
  if s = 'css' then FContentType := 'text/css';
  
  if s = 'png' then FContentType := 'image/x-png';
  if s = 'xpm' then FContentType := 'image/x-pixmap';
  if s = 'xbm' then FContentType := 'image/x-bitmap';
  if (s = 'tif')
  or (s = 'tiff') then FContentType := 'image/tiff';
  if s = 'mng' then FContentType := 'image/x-mng';
  if s = 'gif' then FContentType := 'image/gif';
  if s = 'rgb' then FContentType := 'image/rgb';
  if (s = 'jpg')
  or (s = 'jpeg') then FContentType := 'image/jpeg';
  if s = 'bmp' then FContentType := 'image/x-ms-bmp';
    
  if s = 'wav' then FContentType := 'audio/x-wav';
  if s = 'mp3' then FContentType := 'audio/x-mp3';
  if s = 'ogg' then FContentType := 'audio/x-ogg';
  if s = 'avi' then FContentType := 'video/x-msvideo';
  if (s = 'qt')
  or (s = 'mov') then FContentType := 'video/quicktime';
  if (s = 'mpg')
  or (s = 'mpeg') then FContentType := 'video/mpeg';
  
  if s = 'pdf' then FContentType := 'application/pdf';
  if s = 'rtf' then FContentType := 'application/rtf';
  if s = 'tex' then FContentType := 'application/x-tex';
  if s = 'latex' then FContentType := 'application/x-latex';
  if s = 'doc' then FContentType := 'application/msword';
  if s = 'gz' then FContentType := 'application/x-gzip';
  if s = 'zip' then FContentType := 'application/zip';
  if s = '7z' then FContentType := 'application/x-7zip';
  if s = 'rar' then FContentType := 'application/rar';
  if s = 'tar' then FContentType := 'application/x-tar';
  if s = 'arj' then FContentType := 'application/arj';
end;

function TMimeFileSection.GetHeader: string;
begin
  Result := 'Content-Type: ' + FContentType + CRLF;
  Result := Result + 'Content-Transfer-Encoding: ' + EncodingToStr(FEncoding) + CRLF;
  Result := Result + 'Content-Disposition: ' + DispositionToStr(FDisposition) +
            '; filename="' + FFileName + '"' + CRLF;

  if Length(FDescription) > 0 then
    Result := Result + 'Content-Description: ' + FDescription + CRLF;
    
  Result := Result + CRLF;
end;

constructor TMimeFileSection.Create(aOutputStream: TStream; const aFileName: string);
begin
  inherited Create(aOutputStream, TFileStream.Create(aFileName, fmOpenRead));

  SetContentType(aFileName);
  FDescription := ExtractFileName(aFileName);
  Encoding := meBase64;
  FFileName := ExtractFileName(aFileName);
  FOwnsStreams := True;
end;

{ EAlreadyActivatedException }

constructor EAlreadyActivatedException.Create;
begin
  inherited Create('The stream or section has already been activated (by Read() or Write())');
end;

{ EAlreadyCalledReadException }

constructor EAlreadyCalledReadException.Create;
begin
  inherited Create('The stream has already been used for reading');
end;

{ EAlreadyCalledWriteException }

constructor EAlreadyCalledWriteException.Create;
begin
  inherited Create('The stream has already been used for writing');
end;

end.

