IdMessageCoderMIME.pas 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. { $HDR$}
  2. {**********************************************************************}
  3. { Unit archived using Team Coherence }
  4. { Team Coherence is Copyright 2002 by Quality Software Components }
  5. { }
  6. { For further information / comments, visit our WEB site at }
  7. { http://www.TeamCoherence.com }
  8. {**********************************************************************}
  9. {}
  10. { $Log: 10257: IdMessageCoderMIME.pas
  11. {
  12. { Rev 1.12 05/01/2005 17:22:28 CCostelloe
  13. { Randomised MIME boundary.
  14. }
  15. {
  16. { Rev 1.11 8/15/04 5:25:12 PM RLebeau
  17. { Rewrote ReadHeader() to handle attachments similar to how Indy 10 does now
  18. }
  19. {
  20. { Rev 1.10 8/10/04 1:28:18 PM RLebeau
  21. { Updated TIdMessageDecoderMIME to support multi-part form data
  22. }
  23. {
  24. { Rev 1.9 6/4/04 12:38:34 PM RLebeau
  25. { ContentTransferEncoding bug fix
  26. }
  27. {
  28. { Rev 1.8 5/28/04 12:18:42 PM RLebeau
  29. { Fix for compiler error
  30. }
  31. {
  32. { Rev 1.7 25/05/2004 13:57:12 CCostelloe
  33. { Bug fix
  34. }
  35. {
  36. { Rev 1.6 5/1/04 3:04:52 AM RLebeau
  37. { Updated TIdMessageDecoderInfoMIME.CheckForStart() to return nil if no
  38. { boundary is specified in the message
  39. }
  40. {
  41. { Rev 1.5 2003.09.04 5:42:50 PM czhower
  42. { Update to produce lower SpamAsassin scores.
  43. }
  44. {
  45. Rev 1.4 6/14/2003 10:40:36 AM BGooijen
  46. fix for the bug where the attachments are empty
  47. }
  48. {
  49. { Rev 1.2 5/23/03 9:51:04 AM RLebeau
  50. { Minor tweak to previous fix.
  51. }
  52. {
  53. { Rev 1.1 5/23/03 9:43:12 AM RLebeau
  54. { Fixed bugs where message body is parsed incorrectly when MIMEBoundary is
  55. { empty.
  56. }
  57. {
  58. { Rev 1.0 2002.11.12 10:46:04 PM czhower
  59. }
  60. unit IdMessageCoderMIME;
  61. // for all 3 to 4s:
  62. //// TODO: Predict output sizes and presize outputs, then use move on
  63. // presized outputs when possible, or presize only and reposition if stream
  64. interface
  65. uses
  66. Classes,
  67. IdMessageCoder, IdMessage;
  68. type
  69. TIdMessageDecoderMIME = class(TIdMessageDecoder)
  70. protected
  71. FFirstLine: string;
  72. FBodyEncoded: Boolean;
  73. FMIMEBoundary: string;
  74. public
  75. constructor Create(AOwner: TComponent); reintroduce; overload;
  76. constructor Create(AOwner: TComponent; ALine: string); reintroduce; overload;
  77. function ReadBody(ADestStream: TStream;
  78. var VMsgEnd: Boolean): TIdMessageDecoder; override;
  79. procedure ReadHeader; override;
  80. class procedure SetupBoundaries;
  81. class function GenerateRandomChar: Char;
  82. //
  83. property MIMEBoundary: string read FMIMEBoundary write FMIMEBoundary;
  84. property BodyEncoded: Boolean read FBodyEncoded write FBodyEncoded;
  85. end;
  86. TIdMessageDecoderInfoMIME = class(TIdMessageDecoderInfo)
  87. public
  88. function CheckForStart(ASender: TIdMessage; ALine: string): TIdMessageDecoder; override;
  89. end;
  90. TIdMessageEncoderMIME = class(TIdMessageEncoder)
  91. public
  92. procedure Encode(ASrc: TStream; ADest: TStream); override;
  93. end;
  94. TIdMessageEncoderInfoMIME = class(TIdMessageEncoderInfo)
  95. public
  96. constructor Create; override;
  97. procedure InitializeHeaders(AMsg: TIdMessage); override;
  98. end;
  99. var
  100. IndyMIMEBoundary: string;
  101. IndyMultiPartAlternativeBoundary: string;
  102. IndyMultiPartRelatedBoundary: string;
  103. const
  104. {IndyMIMEBoundary = '=_MoreStuf_2zzz1234sadvnqw3nerasdf'; {do not localize}
  105. {IndyMultiPartAlternativeBoundary = '=_MoreStuf_2altzzz1234sadvnqw3nerasdf'; {do not localize}
  106. {IndyMultiPartRelatedBoundary = '=_MoreStuf_2relzzzsadvnq1234w3nerasdf'; {do not localize}
  107. MIMEGenericText = 'text/'; {do not localize}
  108. MIMEGenericMultiPart = 'multipart/'; {do not localize}
  109. MIME7Bit = '7bit'; {do not localize}
  110. implementation
  111. uses
  112. IdCoder, IdCoderMIME, IdException, IdGlobal, IdResourceStrings, IdCoderQuotedPrintable,
  113. SysUtils, IdCoderHeader;
  114. { TIdMessageDecoderInfoMIME }
  115. function TIdMessageDecoderInfoMIME.CheckForStart(ASender: TIdMessage;
  116. ALine: string): TIdMessageDecoder;
  117. begin
  118. if (ASender.MIMEBoundary.Boundary <> '') and AnsiSameText(ALine, '--' + ASender.MIMEBoundary.Boundary) then begin {Do not Localize}
  119. Result := TIdMessageDecoderMIME.Create(ASender);
  120. end else if AnsiSameText(ASender.ContentTransferEncoding, 'base64') or {Do not Localize}
  121. AnsiSameText(ASender.ContentTransferEncoding, 'quoted-printable') then begin {Do not Localize}
  122. Result := TIdMessageDecoderMIME.Create(ASender, ALine);
  123. end else begin
  124. Result := nil;
  125. end;
  126. end;
  127. { TIdCoderMIME }
  128. constructor TIdMessageDecoderMIME.Create(AOwner: TComponent);
  129. begin
  130. inherited;
  131. FBodyEncoded := False;
  132. if AOwner is TIdMessage then begin
  133. FMIMEBoundary := TIdMessage(AOwner).MIMEBoundary.Boundary;
  134. if (TIdMessage(AOwner).ContentTransferEncoding <> '') and
  135. (not AnsiSameText(TIdMessage(AOwner).ContentTransferEncoding, '7bit')) and
  136. (not AnsiSameText(TIdMessage(AOwner).ContentTransferEncoding, '8bit')) and
  137. (not AnsiSameText(TIdMessage(AOwner).ContentTransferEncoding, 'binary')) then
  138. begin
  139. FBodyEncoded := True;
  140. end;
  141. end;
  142. end;
  143. constructor TIdMessageDecoderMIME.Create(AOwner: TComponent; ALine: string);
  144. begin
  145. Create(AOwner);
  146. FFirstLine := ALine;
  147. end;
  148. class function TIdMessageDecoderMIME.GenerateRandomChar: Char;
  149. var
  150. LOrd: integer;
  151. LFloat: Double;
  152. begin
  153. {Allow only digits (ASCII 48-57), uppercase letters (65-90) and lowercase
  154. letters (97-122), which is 62 possible chars...}
  155. LFloat := (Random* 61) + 1.5; //Gives us 1.5 to 62.5
  156. LOrd := Trunc(LFloat)+47; //(1..62) -> (48..109)
  157. if LOrd > 83 then begin
  158. LOrd := LOrd + 13; {Move into lowercase letter range}
  159. end else if LOrd > 57 then begin
  160. LOrd := LOrd + 7; {Move into uppercase letter range}
  161. end;
  162. Result := Chr(LOrd);
  163. end;
  164. class procedure TIdMessageDecoderMIME.SetupBoundaries;
  165. var
  166. LOrd: integer;
  167. LN: integer;
  168. LFloat: Double;
  169. begin
  170. IndyMIMEBoundary := '1234567890123456789012345678901234'; {do not localize}
  171. Randomize;
  172. for LN := 1 to Length(IndyMIMEBoundary) do begin
  173. IndyMIMEBoundary[LN] := GenerateRandomChar;
  174. end;
  175. {CC2: RFC 2045 recommends including "=_" in the boundary, insert in random location...}
  176. LFloat := (Random * (Length(IndyMIMEBoundary)-2)) + 1.5; //Gives us 1.5 to Length-0.5
  177. LN := Trunc(LFloat); // 1 to Length-1 (we are inserting a 2-char string)
  178. IndyMIMEBoundary[LN] := '=';
  179. IndyMIMEBoundary[LN+1] := '_';
  180. {The Alternative boundary is the same with a random lowercase letter added...}
  181. LFloat := (Random* 25) + 1.5; //Gives us 1.5 to 26.5
  182. LOrd := Trunc(LFloat)+96; //(1..26) -> (97..122)
  183. IndyMultiPartAlternativeBoundary := Chr(LOrd) + IndyMIMEBoundary;
  184. {The Related boundary is the same with a random uppercase letter added...}
  185. LFloat := (Random* 25) + 1.5; //Gives us 1.5 to 26.5
  186. LOrd := Trunc(LFloat)+64; //(1..26) -> (65..90)
  187. IndyMultiPartRelatedBoundary := Chr(LOrd) + IndyMultiPartAlternativeBoundary;
  188. end;
  189. function TIdMessageDecoderMIME.ReadBody(ADestStream: TStream; var VMsgEnd: Boolean): TIdMessageDecoder;
  190. var
  191. s: string;
  192. LDecoder: TIdDecoder;
  193. LLine: string;
  194. begin
  195. VMsgEnd := False;
  196. Result := nil;
  197. if FBodyEncoded then begin
  198. s := TIdMessage(Owner).ContentTransferEncoding;
  199. end else begin
  200. s := FHeaders.Values['Content-Transfer-Encoding']; {Do not Localize}
  201. end;
  202. if AnsiSameText(s, 'base64') then begin {Do not Localize}
  203. LDecoder := TIdDecoderMIME.Create(nil);
  204. end else if AnsiSameText(s, 'quoted-printable') then begin {Do not Localize}
  205. LDecoder := TIdDecoderQuotedPrintable.Create(nil);
  206. end else begin
  207. LDecoder := nil;
  208. end;
  209. try
  210. repeat
  211. if FFirstLine = '' then begin // TODO: Improve this. Not very efficient
  212. LLine := ReadLn;
  213. end else begin
  214. LLine := FFirstLine;
  215. FFirstLine := ''; {Do not Localize}
  216. end;
  217. if LLine = '.' then begin // Do not use ADELIM since always ends with . (standard) {Do not Localize}
  218. VMsgEnd := True;
  219. Break;
  220. end;
  221. // New boundary - end self and create new coder
  222. if MIMEBoundary <> '' then begin
  223. if AnsiSameText(LLine, '--' + MIMEBoundary) then begin {Do not Localize}
  224. Result := TIdMessageDecoderMIME.Create(Owner);
  225. Exit;
  226. end;
  227. if AnsiSameText(LLine, '--' + MIMEBoundary + '--') then begin {Do not Localize}
  228. // POP the boundary
  229. if Owner is TIdMessage then begin
  230. TIdMessage(Owner).MIMEBoundary.Pop;
  231. end;
  232. Exit;
  233. end;
  234. end;
  235. if LDecoder = nil then begin
  236. if (Length(LLine) > 0) and (LLine[1] = '.') then begin // Process . in front for no encoding {Do not Localize}
  237. Delete(LLine, 1, 1);
  238. end;
  239. LLine := LLine + EOL;
  240. ADestStream.WriteBuffer(LLine[1], Length(LLine));
  241. end else begin
  242. //for TIdDecoderQuotedPrintable, we have
  243. //to make sure all EOLs are intact
  244. if LDecoder is TIdDecoderQuotedPrintable then begin
  245. LDecoder.DecodeToStream(LLine+EOL, ADestStream);
  246. end else if LLine <> '' then begin
  247. LDecoder.DecodeToStream(LLine, ADestStream);
  248. end;
  249. end;
  250. until False;
  251. finally
  252. FreeAndNil(LDecoder);
  253. end;
  254. end;
  255. procedure TIdMessageDecoderMIME.ReadHeader;
  256. var
  257. ABoundary,
  258. s: string;
  259. LLine: string;
  260. function GetAttachmentFilename(AContentType, AContentDisposition: string): string;
  261. var
  262. LValue: string;
  263. LPos: Cardinal;
  264. begin
  265. LPos := IndyPos('FILENAME=', UpperCase(AContentDisposition)); {Do not Localize}
  266. if LPos > 0 then begin
  267. LValue := Trim(Copy(AContentDisposition, LPos + 9, MaxInt));
  268. end else begin
  269. LValue := ''; //FileName not found
  270. end;
  271. if Length(LValue) = 0 then begin
  272. // Get filename from Content-Type
  273. LPos := IndyPos('NAME=', UpperCase(AContentType)); {Do not Localize}
  274. if LPos > 0 then begin
  275. LValue := Trim(Copy(AContentType, LPos + 5, MaxInt)); {Do not Localize}
  276. end;
  277. end;
  278. if Length(LValue) > 0 then begin
  279. if LValue[1] = '"' then begin {Do not Localize}
  280. // RLebeau - shouldn't this code use AnsiExtractQuotedStr() instead?
  281. Fetch(LValue, '"'); {Do not Localize}
  282. Result := Fetch(LValue, '"'); {Do not Localize}
  283. end else begin
  284. // RLebeau - just in case the name is not the last field in the line
  285. Result := Fetch(LValue, ';'); {Do not Localize}
  286. end;
  287. Result := DecodeHeader(Result);
  288. end else begin
  289. Result := '';
  290. end;
  291. end;
  292. procedure CheckAndSetType(AContentType, AContentDisposition: string);
  293. var
  294. LDisposition, LFileName: string;
  295. begin
  296. LDisposition := Fetch(AContentDisposition, ';'); {Do not Localize}
  297. {The new world order: Indy now defines a TIdAttachment as a part that either has
  298. a filename, or else does NOT have a ContentType starting with text/ or multipart/.
  299. Anything left is a TIdText.}
  300. //WARNING: Attachments may not necessarily have filenames!
  301. LFileName := GetAttachmentFileName(AContentType, AContentDisposition);
  302. // Content-Disposition: inline; - Even this we treat as attachment. It
  303. // can easily contain binary data which text part is not suited for.
  304. if AnsiSameText(LDisposition, 'attachment') or (Length(LFileName) > 0) then {Do not Localize}
  305. begin
  306. FPartType := mcptAttachment;
  307. FFilename := LFileName;
  308. end else begin
  309. {No filename is specified, so see what type the part is...}
  310. if AnsiSameText(Copy(AContentType, 1, 5), MIMEGenericText) or
  311. AnsiSameText(Copy(AContentType, 1, 10), MIMEGenericMultiPart) then
  312. begin
  313. FPartType := mcptText;
  314. end else begin
  315. FPartType := mcptAttachment;
  316. end;
  317. end;
  318. end;
  319. begin
  320. if FBodyEncoded then begin // Read header from the actual message since body parts don't exist {Do not Localize}
  321. CheckAndSetType(TIdMessage(Owner).ContentType, TIdMessage(OWner).ContentDisposition);
  322. end else begin
  323. // Read header
  324. repeat
  325. LLine := ReadLn;
  326. if LLine = '.' then begin // TODO: abnormal situation (Masters!) {Do not Localize}
  327. FPartType := mcptUnknown;
  328. Exit;
  329. end;//if
  330. if LLine = '' then begin
  331. Break;
  332. end;
  333. if LLine[1] in LWS then begin
  334. if FHeaders.Count > 0 then begin
  335. FHeaders[FHeaders.Count - 1] := FHeaders[FHeaders.Count - 1] + ' ' + Copy(LLine, 2, MaxInt); {Do not Localize}
  336. end else begin
  337. FHeaders.Add(StringReplace(Copy(LLine, 2, MaxInt), ': ', '=', [])); {Do not Localize}
  338. end;
  339. end else begin
  340. FHeaders.Add(StringReplace(LLine, ': ', '=', [])); {Do not Localize}
  341. end;
  342. until False;
  343. s := FHeaders.Values['Content-Type']; {Do not Localize}
  344. ABoundary := TIdMIMEBoundary.FindBoundary(s);
  345. if Length(ABoundary) > 0 then begin
  346. if Owner is TIdMessage then begin
  347. TIdMessage(Owner).MIMEBoundary.Push(ABoundary);
  348. // Also update current boundary
  349. FMIMEBoundary := ABoundary;
  350. end;
  351. end;
  352. CheckAndSetType(FHeaders.Values['Content-Type'], {Do not Localize}
  353. FHeaders.Values['Content-Disposition']); {Do not Localize}
  354. end;
  355. end;
  356. { TIdMessageEncoderInfoMIME }
  357. constructor TIdMessageEncoderInfoMIME.Create;
  358. begin
  359. inherited;
  360. FMessageEncoderClass := TIdMessageEncoderMIME;
  361. end;
  362. procedure TIdMessageEncoderInfoMIME.InitializeHeaders(AMsg: TIdMessage);
  363. begin
  364. if AMsg.MessageParts.RelatedPartCount > 0 then begin
  365. AMsg.ContentType := 'multipart/related; type="multipart/alternative"; boundary="' + {do not localize}
  366. IndyMultiPartRelatedBoundary + '"'; {Do not Localize}
  367. end else begin
  368. if AMsg.MessageParts.AttachmentCount > 0 then begin
  369. AMsg.ContentType := 'multipart/mixed; boundary="' {do not localize}
  370. + IndyMIMEBoundary + '"'; {Do not Localize}
  371. end else begin
  372. if AMsg.MessageParts.TextPartCount > 0 then begin
  373. AMsg.ContentType :=
  374. 'multipart/alternative; boundary="' {do not localize}
  375. + IndyMIMEBoundary + '"'; {Do not Localize}
  376. end;
  377. end;
  378. end;
  379. end;
  380. { TIdMessageEncoderMIME }
  381. procedure TIdMessageEncoderMIME.Encode(ASrc, ADest: TStream);
  382. var
  383. s: string;
  384. LEncoder: TIdEncoderMIME;
  385. LSPos, LSSize : Int64;
  386. begin
  387. ASrc.Position := 0;
  388. LSPos := 0;
  389. LSSize := ASrc.Size;
  390. LEncoder := TIdEncoderMIME.Create(nil); try
  391. while LSPos < LSSize do begin
  392. s := LEncoder.Encode(ASrc, 57) + EOL;
  393. Inc(LSPos,57);
  394. ADest.WriteBuffer(s[1], Length(s));
  395. end;
  396. finally FreeAndNil(LEncoder); end;
  397. end;
  398. initialization
  399. TIdMessageDecoderList.RegisterDecoder('MIME', {Do not Localize}
  400. TIdMessageDecoderInfoMIME.Create);
  401. TIdMessageEncoderList.RegisterEncoder('MIME', {Do not Localize}
  402. TIdMessageEncoderInfoMIME.Create);
  403. TIdMessageDecoderMIME.SetupBoundaries;
  404. end.