{*******************************************************} { MiTeC Common Routines } { Simple EML file parser } { } { Copyright (c) 2009-2017 Michal Mutl } { } {*******************************************************} {$INCLUDE Compilers.inc} unit MiTeC_EML; interface uses {$IFDEF RAD9PLUS} WinAPI.Windows, System.SysUtils, System.Classes; {$ELSE} Windows, SysUtils, Classes; {$ENDIF} type TEMLHeader = record Date: string; Timestamp: TDateTime; UserName: string; From: string; Sender: string; ReplyTo: string; ReturnPath: string; SendTo: string; CC: string; BCC: string; Subject: string; Priority: cardinal; ContentTransferEncoding: string; MessageId: string; ContentType: string; Size: Int64; RevisionID, DocumentID: string; Attachments, Read: Boolean; end; function WideToAnsi(const ws: WideString; codePage: Word = CP_ACP): AnsiString; function AnsiToWide(const s: AnsiString; codePage: Word = CP_ACP): WideString; procedure ParseEML(AMessage: TStrings; var AHeader: TEMLHeader; AOnlyMozilla: Boolean = False); function GetEmailAddressParts(const ACompleteEmail: string; var AName, AEmail: string): Boolean; function MailDateToDateTime(const ADateStr: string): TDateTime; implementation {$IFDEF RAD12PLUS}uses System.AnsiStrings;{$ENDIF} function WideToAnsi(const ws: WideString; codePage: Word = CP_ACP): AnsiString; var l: integer; f: Cardinal; begin f:=WC_COMPOSITECHECK or WC_DISCARDNS or WC_SEPCHARS or WC_DEFAULTCHAR; if codepage=CP_UTF8 then f:=0; if ws = '' then Result:='' else begin l:=WideCharToMultiByte(codePage,f,@ws[1],-1,nil,0,nil,nil); SetLength(Result,l-1); if l>1 then WideCharToMultiByte(codePage,f,@ws[1],-1,@Result[1],l-1,nil,nil); end; end; function AnsiToWide(const s: AnsiString; codePage: Word = CP_ACP): WideString; var l: integer; f: Cardinal; begin f:=MB_PRECOMPOSED; if codepage=CP_UTF8 then f:=0; if s = '' then Result:='' else begin l:=MultiByteToWideChar(codePage,f,PAnsiChar(@s[1]),-1,nil,0); SetLength(Result,l-1); if l>1 then MultiByteToWideChar(CodePage,f,PAnsiChar(@s[1]),-1,PWideChar(@Result[1]),l-1); end; end; function IsBitOn (Value: Integer; Bit: Byte): Boolean; begin Result:=(Value and (1 shl Bit))<>0; end; function TrimSpace(const S: string): string; var I, L: Integer; begin L:=Length(S); I:=1; while (I <= L) and {$IFDEF UNICODE}CharInSet(S[I],[#9, #32]){$else}(S[I] in [#9, #32]){$ENDIF} do Inc(I); if I > L then Result:='' else begin while S[L] = ' ' do Dec(L); Result:=Copy(S, I, L - I + 1); end; end; function FastStringReplace(const ASource, APattern, AReplace: string): string; var i: Integer; s: string; begin s:=ASource; Result:=''; repeat i:=Pos(APattern,s); if i>0 then begin Result:=Result+Copy(s,1,i- 1)+AReplace; s:=Copy(s,i+Length(APattern),MaxInt); end else Result:=Result+s; until i<=0; end; function GetTimeZoneBias: Double; var TzInfo: TTimeZoneInformation; begin case GetTimeZoneInformation(TzInfo) of 1: Result:=-(TzInfo.StandardBias+TzInfo.Bias)/(24*60); 2: Result:=-(TzInfo.DaylightBias+TzInfo.Bias)/(24*60); else Result:=-TzInfo.Bias/(24*60); end; end; function MailDateToDateTime(const ADateStr: string): TDateTime; const Months: String = 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec,'; var Field,i: Integer; Hour, Min, Sec, Year, Month, Day: Double; sDate, sHour, sMin, sSec, sYear, sMonth, sDay, sTZ: String; HTZM, MTZM: Word; STZM: Integer; TZM: Double; fdt: Double; begin sHour:=''; sMin:=''; sSec:=''; sYear:=''; sMonth:=''; sDay:=''; sTZ:=''; sDate:=FastStringReplace(Trim(ADateStr),' ',' '); if sDate<>'' then begin if {$IFDEF UNICODE}CharInSet(sDate[1],['0'..'9']){$else}(sDate[1] in ['0'..'9']){$ENDIF} then Field:=1 else Field:=0; for i:=1 to Length(sDate) do begin if {$IFDEF UNICODE}CharInSet(sDate[i],[#32, ':', '/']){$else}(sDate[i] in [#32, ':', '/']){$ENDIF} then begin Inc(Field); if (Field = 6) and (sDate[i] = #32) then Field:=7; end else begin case Field of 1: sDay:=sDay + sDate[i]; 2: sMonth:=sMonth + sDate[i]; 3: sYear:=sYear + sDate[i]; 4: sHour:=sHour + sDate[i]; 5: sMin:=sMin + sDate[i]; 6: sSec:=sSec + sDate[i]; 7: sTZ:=sTZ + sDate[i]; end; end; end; Hour:=StrToIntDef(sHour,0); Min:=StrToIntDef(sMin,0); Sec:=StrToIntDef(sSec,0); Year:=StrToIntDef(sYear,0); Day:=StrToIntDef(sDay,0); if {$IFDEF UNICODE}CharInSet(sMonth[1],['0'..'9']){$else}(sMonth[1] in ['0'..'9']){$ENDIF} then Month:=StrToIntDef(sMonth,0) else Month:=(Pos(sMonth, Months)-1) div 4 + 1; if Year<100 then begin if Year<50 then Year:=2000+Year else Year:=1900+Year; end; if (Year=0) or (Month=0) or (Year=0) then Result:=0 else begin if (sTZ='GMT') or (Length(Trim(sTZ))<>5) then begin STZM:=1; HTZM:=0; MTZM:=0; end else begin STZM:=StrToIntDef(Copy(sTZ,1,1)+'1',1); HTZM:=StrToIntDef(Copy(sTZ,2,2),0); MTZM:=StrToIntDef(Copy(sTZ,4,2),0); end; try TZM:=EncodeTime(HTZM, MTZM,0,0)*STZM; fdt:=EncodeDate(Trunc(Year),Trunc(Month),Trunc(Day)); fdt:=fdt+Hour*(1/24)+Min*(1/24/60)+Sec*(1/24/60/60); fdt:=fdt-TZM+GetTimeZoneBias; Result:=fdt; except Result:=0; end; end; end else Result:=0; end; function QuotedPrintableDecode(const FCurrentData: PAnsiChar): string; { This works if charset="iso-8859-1" ! } var SourceIndex, DecodedIndex, CharCode: Integer; Ch: AnsiChar; CodeHex: string; begin SourceIndex:=0; DecodedIndex:=0; if (FCurrentData<>'' ) and (FCurrentData^<>#0) then begin while True do begin Ch:=FCurrentData[SourceIndex]; if Ch=#0 then Break; if Ch='_' then begin FCurrentData[DecodedIndex]:=' '; Inc(SourceIndex); Inc(DecodedIndex); end else if Ch<>'=' then begin FCurrentData[DecodedIndex]:=Ch; Inc(SourceIndex); Inc(DecodedIndex); end else begin Inc(SourceIndex); Ch:=FCurrentData[SourceIndex]; if (Ch=#13) or (Ch=#10) then begin Inc(SourceIndex); Inc(SourceIndex); end else begin CodeHex:='$'+Ch; Inc(SourceIndex); Ch:=FCurrentData[SourceIndex]; if Ch=#0 then Break; CodeHex:=CodeHex+string(Ch); CharCode:=StrToIntDef(CodeHex,64); case CharCode of $B9: FCurrentData[DecodedIndex]:='š'; $BE: FCurrentData[DecodedIndex]:='ž'; else FCurrentData[DecodedIndex]:=AnsiChar(Chr(CharCode)); end; Inc(SourceIndex); Inc(DecodedIndex); end; end; end; FCurrentData[DecodedIndex]:=#0; end; Result:=string(FCurrentData); end; function DecodeLineBASE64(const Buffer: AnsiString; Decoded: PAnsiChar): Integer; var A1: array[1..4] of Byte; B1: array[1..3] of Byte; I, J: Integer; BytePtr, RealBytes: Integer; begin BytePtr:=0; Result:=0; for J:=1 to Length(Buffer) do begin Inc(BytePtr); case Buffer[J] of 'A'..'Z': A1[BytePtr]:=Ord(Buffer[J])-65; 'a'..'z': A1[BytePtr]:=Ord(Buffer[J])-71; '0'..'9': A1[BytePtr]:=Ord(Buffer[J])+4; '+': A1[BytePtr]:=62; '/': A1[BytePtr]:=63; '=': A1[BytePtr]:=64; end; if BytePtr = 4 then begin BytePtr:=0; RealBytes:=3; if A1[1] = 64 then RealBytes:=0; if A1[3] = 64 then begin A1[3]:=0; A1[4]:=0; RealBytes:=1; end; if A1[4] = 64 then begin A1[4]:=0; RealBytes:=2; end; B1[1]:=A1[1]*4 + (A1[2] div 16); B1[2]:=(A1[2] mod 16)*16+(A1[3] div 4); B1[3]:=(A1[3] mod 4)*64 + A1[4]; for I:=1 to RealBytes do begin Decoded[Result+I-1]:=AnsiChar(Chr(B1[I])); end; Inc(Result, RealBytes); end; end; end; function DecodeQuotedPrintable(const Texto: String): String; var nPos: Integer; nLastPos: Integer; //lFound: Boolean; begin Result:=''; nPos:=1; while nPos <= Length(Texto) do begin if Texto[nPos] = '=' then begin if (Length(Texto)-nPos) >= 2 then begin if (Texto[nPos+1] = #13) and (Texto[nPos+2] = #10) then begin Inc(nPos, 3); end else begin if {$IFDEF UNICODE}CharInSet(Texto[nPos+1],['0'..'9', 'A'..'F']){$else}(Texto[nPos+1] in ['0'..'9', 'A'..'F']){$ENDIF} and {$IFDEF UNICODE}CharInSet(Texto[nPos+2],['0'..'9', 'A'..'F']){$ELSE}(Texto[nPos+2] in ['0'..'9', 'A'..'F']){$ENDIF} then begin Result:=Result + Char(StrToInt('$'+Texto[nPos+1]+Texto[nPos+2])); Inc(nPos, 3) end else begin Inc(nPos, 3); end; end; end else begin Break; end; end else begin nLastPos:=nPos; nPos:=Pos('=', Copy(Texto, nLastPos+1, High(Integer))); if nPos = 0 then nPos:=Length(Texto)+1; Result:=Result + Copy(Texto, nLastPos, nPos); Inc(nPos, nLastPos); end; end; end; procedure DataLinePChar(const Data: PAnsiChar; const TotalLength: Integer; var LinePos, LineLen: Integer; var Line: PAnsiChar; var DataEnd: Boolean); begin if LinePos >= 0 then begin Data[LinePos+LineLen]:=#13; LinePos:=LinePos+LineLen+2; LineLen:=0; end else begin LinePos:=0; LineLen:=0; end; while (LinePos+LineLen) < TotalLength do begin if Data[LinePos+LineLen] = #13 then begin if (LinePos+LineLen+1) < TotalLength then begin if Data[LinePos+LineLen+1] = #10 then begin Data[LinePos+LineLen]:=#0; Line:=@Data[LinePos]; Exit; end; end; end; Inc(LineLen); end; if LinePos < TotalLength then Line:=@Data[LinePos] else DataEnd:=True; end; function DecodeLineUUCODE(const Buffer: AnsiString; Decoded: PAnsiChar): Integer; const CHARS_PER_LINE = 45; Table: AnsiString = '`!"#$%&''()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_'; var A24Bits: array[0..8 * CHARS_PER_LINE] of Boolean; i, j, k, b: Word; LineLen, ActualLen: Byte; function p_ByteFromTable(Ch: AnsiChar): Byte; var ij: Integer; begin ij:=Pos(Ch, Table); if (ij > 64) or (ij = 0) then begin if Ch = #32 then Result:=0 else raise Exception.Create('UUCODE message format error'); end else Result:=ij - 1; end; begin if Buffer = '' then begin Result:=0; Exit; end; try LineLen:=p_ByteFromTable(Buffer[1]); ActualLen:=4 * LineLen div 3; FillChar(A24Bits, 8 * CHARS_PER_LINE + 1, 0); Result:=LineLen; if ActualLen <> (4 * CHARS_PER_LINE div 3) then ActualLen:=Length(Buffer) - 1; k:=0; for i:=2 to ActualLen + 1 do begin b:=p_ByteFromTable(Buffer[i]); for j:=5 downto 0 do begin A24Bits[k]:=b and (1 shl j) > 0; Inc(k); end; end; k:=0; for i:=1 to CHARS_PER_LINE do begin b:=0; for j:=7 downto 0 do begin if A24Bits[k] then b:=b or (1 shl j); Inc(k); end; Decoded[i-1]:=AnsiChar(Char(b)); end; except Result:=0; end; end; function DecodeUUCODE(Encoded: PAnsiChar; Decoded: TMemoryStream): Boolean; var nTL, nPos, nLen: Integer; Line: PAnsiChar; LineDec: array[0..79] of AnsiChar; LineLen: Integer; DataEnd: Boolean; begin Decoded.Clear; DataEnd:=False; nPos:=-1; nTL:={$IFDEF RAD12PLUS}System.AnsiStrings.{$ENDIF}StrLen(Encoded); DataLinePChar(Encoded, nTL, nPos, nLen, Line, DataEnd); while not DataEnd do begin if nLen > 0 then begin LineLen:=DecodeLineUUCODE(AnsiString(Line), LineDec); if LineLen > 0 then Decoded.Write(LineDec[0], LineLen); end; DataLinePChar(Encoded, nTL, nPos, nLen, Line, DataEnd); end; Result:=True; end; function DecodeLine7Bit(Texto: String): String; var Buffer: PAnsiChar; Encoding: Char; Size: Integer; nPos0: Integer; nPos1: Integer; nPos2: Integer; nPos3: Integer; utf,iso8859,Found: Boolean; begin utf:=Pos('=?UTF-8?',Uppercase(Texto))>0; iso8859:=(Pos('=?ISO-8859-1?',Uppercase(Texto))>0) or (Pos('=?ISO-8859-2?',Uppercase(Texto))>0); Result:=TrimSpace(Texto); repeat nPos0:=Pos('=?', Result); Found:=False; if nPos0 > 0 then begin nPos1:=Pos('?', Copy(Result, nPos0+2, Length(Result)))+nPos0+1; nPos2:=Pos('?=', Copy(Result, nPos1+1, Length(Result)))+nPos1; nPos3:=Pos('?', Copy(Result, nPos2+1, Length(Result)))+nPos2; if nPos3 > nPos2 then begin if Length(Result) > nPos3 then begin if Result[nPos3+1] = '=' then begin nPos2:=nPos3; end; end; end; if (nPos1 > nPos0) and (nPos2 > nPos1) then begin Texto:=Copy(Result, nPos1+1, nPos2-nPos1-1); if (Length(Texto) >= 2) and (Texto[2] = '?') and {$IFDEF UNICODE}CharInSet(UpCase(Texto[1]),['B', 'Q', 'U']){$ELSE}(UpCase(Texto[1]) in ['B', 'Q', 'U']){$ENDIF} then begin Encoding:=UpCase(Texto[1]); end else begin Encoding:='Q'; end; Texto:=Copy(Texto, 3, Length(Texto)-2); case Encoding of 'B': begin GetMem(Buffer, Length(Texto)); {$IFDEF UNICODE} Size:=DecodeLineBASE64(WideToAnsi(Texto),Buffer); {$else} Size:=DecodeLineBASE64(Texto, Buffer); {$ENDIF} if Size>0 then Buffer[Size]:=#0; Texto:=string(Buffer); end; 'Q': begin while Pos('_', Texto) > 0 do Texto[Pos('_', Texto)]:=#32; if iso8859 then {$IFDEF UNICODE} Texto:=QuotedPrintableDecode(PAnsiChar(WideToAnsi(Texto))) {$ELSE} Texto:=QuotedPrintableDecode(PAnsiChar(Texto)) {$ENDIF} else Texto:=DecodeQuotedPrintable(Texto); end; 'U': begin GetMem(Buffer, Length(Texto)); {$IFDEF UNICODE} Size:=DecodeLineUUCODE(WideToAnsi(Texto),Buffer); {$else} Size:=DecodeLineUUCODE(Texto,Buffer); {$ENDIF} if Size>0 then Buffer[Size]:=#0; Texto:=string(Buffer); end; end; Result:=Copy(Result, 1, nPos0-1)+Texto+Copy(Result,nPos2+2,Length(Result)); Found:=True; end; end; until not Found; if utf then Result:=string({$IFDEF UNICODE}UTF8String{$ELSE}UTF8Decode{$ENDIF}(Result)); end; function GetFieldValueFromLine(Field, Line: string): string; var p,i: integer; begin Result:=''; p:=Pos(Field,Uppercase(Line)); if p>0 then begin Result:=Trim(Copy(Line,p+Length(Field),Length(Line))); i:=Pos(';',Result); if i>0 then SetLength(Result,i-1); end; end; function GetEmailAddressParts(const ACompleteEmail: string; var AName, AEmail: string): Boolean; function TextPos(const SubStr, Str: string; StartPos: Integer): Integer; var PosRes, StrLen: Integer; s: string; begin Result:=0; StrLen:=Length(Str); if (StartPos < 1) or (StartPos > StrLen) then Exit; s:=system.Copy(Str, StartPos, StrLen); PosRes:=system.Pos(SubStr, s); if (PosRes <> 0) then Result:=StartPos + PosRes - 1; end; function GetEmailAddressPartsByDelimiter(indStart: Integer; ADelimiterEnd: string): Boolean; var indEnd: Integer; begin AName:=Trim(system.Copy(ACompleteEmail, 1, indStart - 1)); indEnd:=TextPos(ADelimiterEnd, ACompleteEmail, indStart + 1); Result:=(indEnd > 0); if Result then AEmail:=Trim(system.Copy(ACompleteEmail, indStart + 1, indEnd - indStart -1)); end; function GetDenormName(const AName: string): string; var i, j: Integer; Len: Integer; SpecSymExpect: Boolean; Sym: Char; begin SpecSymExpect:=False; Len:=Length(AName); SetLength(Result, Len); i:=1; j:=1; while (i <= Length(AName)) do begin Sym:=AName[i]; case Sym of '\': if not SpecSymExpect then begin SpecSymExpect:=True; Inc(i); Continue; end; '"': if not SpecSymExpect then Sym:=' '; end; SpecSymExpect:=False; Result[j]:=Sym; Inc(j); Inc(i); end; SetLength(Result, j - 1); end; var indStart: Integer; begin AName:=ACompleteEmail; AEmail:=ACompleteEmail; indStart:=system.Pos('<', ACompleteEmail); Result:=(indStart > 0); if Result then Result:=GetEmailAddressPartsByDelimiter(indStart, '>') else begin indStart:=system.Pos('(', ACompleteEmail); Result:=(indStart > 0); if Result then Result:=GetEmailAddressPartsByDelimiter(indStart, ')'); end; if Result then begin AName:=Trim(GetDenormName(AName)); if (Length(AName) > 1) and (AName[1] = '''') and (AName[Length(AName)] = '''') then AName:=Copy(AName, 2, Length(AName) - 2); end; end; function FindFieldInHeaders(const Field: string; const Headers: TStrings; var Line: string): Boolean; var s: string; n,i: integer; begin Result:=false; if Headers.Count=0 then Exit; n:=0; while (n'') do begin s:=UpperCase(Headers[n]); if Pos(Field,s)=1 then begin Line:=Headers[n]; i:=n+1; while (i'') and ((Headers[i][1]=' ') or (Headers[i][1]=#9)) do begin Line:=Line+Trim(Headers[i]); inc(i); end; Result:=true; Break; end; inc(n); end; end; procedure ParseEML; var i: integer; s: string; w: Word; d: Cardinal; begin Finalize(AHeader); AHeader.Priority:=0; AHeader.Attachments:=False; AHeader.Read:=False; if (AMessage.Count=0) then Exit; i:=0; while (i