fpc/utils/fppkg/lnet/lmimewrapper.pp
2016-10-02 12:56:59 +00:00

858 lines
22 KiB
ObjectPascal

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