/source/Delphi/Protocols/IdReplyIMAP4.pas

# · Pascal · 559 lines · 263 code · 30 blank · 266 comment · 35 complexity · 1ad8f193fdd9cac74a0c0b58d4c1f2c2 MD5 · raw file

  1. {
  2. $Project$
  3. $Workfile$
  4. $Revision$
  5. $DateUTC$
  6. $Id$
  7. This file is part of the Indy (Internet Direct) project, and is offered
  8. under the dual-licensing agreement described on the Indy website.
  9. (http://www.indyproject.org/)
  10. Copyright:
  11. (c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
  12. }
  13. {
  14. $Log$
  15. }
  16. {
  17. Rev 1.26 3/23/2005 3:01:56 PM DSiders
  18. Modified TIdReplyIMAP4.Destroy to call inherited destructor.
  19. }
  20. {
  21. { Rev 1.25 20/01/2005 11:02:00 CCostelloe
  22. { Now compiles, also updated to suit change in IdReply
  23. }
  24. {
  25. { Rev 1.24 1/19/05 5:21:52 PM RLebeau
  26. { added Destructor to free the FExtra object
  27. {
  28. { Removed label from SetFormattedReply()
  29. }
  30. {
  31. { Rev 1.23 10/26/2004 10:39:54 PM JPMugaas
  32. { Updated refs.
  33. }
  34. {
  35. Rev 1.22 6/11/2004 9:38:30 AM DSiders
  36. Added "Do not Localize" comments.
  37. }
  38. {
  39. { Rev 1.21 5/17/04 9:53:00 AM RLebeau
  40. { Changed TIdRepliesIMAP4 constructor to use 'reintroduce' instead
  41. }
  42. {
  43. { Rev 1.20 5/16/04 5:31:24 PM RLebeau
  44. { Added constructor to TIdRepliesIMAP4 class
  45. }
  46. {
  47. { Rev 1.19 03/03/2004 01:16:56 CCostelloe
  48. { Yet another check-in as part of continuing development
  49. }
  50. {
  51. { Rev 1.18 26/02/2004 02:02:22 CCostelloe
  52. { A few updates to support IdIMAP4Server development
  53. }
  54. {
  55. { Rev 1.17 05/02/2004 00:26:06 CCostelloe
  56. { Changes to support TIdIMAP4Server
  57. }
  58. {
  59. { Rev 1.16 2/3/2004 4:12:34 PM JPMugaas
  60. { Fixed up units so they should compile.
  61. }
  62. {
  63. { Rev 1.15 2004.01.29 12:07:52 AM czhower
  64. { .Net constructor problem fix.
  65. }
  66. {
  67. { Rev 1.14 1/3/2004 8:05:48 PM JPMugaas
  68. { Bug fix: Sometimes, replies will appear twice due to the way functionality
  69. { was enherited.
  70. }
  71. {
  72. { Rev 1.13 22/12/2003 00:45:40 CCostelloe
  73. { .NET fixes
  74. }
  75. {
  76. { Rev 1.12 03/12/2003 09:48:34 CCostelloe
  77. { IsItANumber and IsItAValidSequenceNumber made public for use by TIdIMAP4.
  78. }
  79. {
  80. { Rev 1.11 28/11/2003 21:02:46 CCostelloe
  81. { Fixes for Courier IMAP
  82. }
  83. {
  84. { Rev 1.10 22/10/2003 12:18:06 CCostelloe
  85. { Split out DoesLineHaveExpectedResponse for use by other functions in IdIMAP4.
  86. }
  87. {
  88. Rev 1.9 10/19/2003 5:57:12 PM DSiders
  89. Added localization comments.
  90. }
  91. {
  92. { Rev 1.8 18/10/2003 22:33:00 CCostelloe
  93. { RemoveUnsolicitedResponses added.
  94. }
  95. {
  96. { Rev 1.7 20/09/2003 19:36:42 CCostelloe
  97. { Multiple changes to clear up older issues
  98. }
  99. {
  100. { Rev 1.6 2003.09.20 10:38:40 AM czhower
  101. { Bug fix to allow clearing code field (Return to default value)
  102. }
  103. {
  104. { Rev 1.5 18/06/2003 21:57:00 CCostelloe
  105. { Rewrote SetFormattedReply. Compiles and works. Needs tidying up, as does
  106. { IdIMAP4.
  107. }
  108. {
  109. { Rev 1.4 17/06/2003 01:38:12 CCostelloe
  110. { Updated to suit LoginSASL changes. Compiles OK.
  111. }
  112. {
  113. { Rev 1.3 15/06/2003 08:41:48 CCostelloe
  114. { Bug fix: i was undefined in SetFormattedReply in posted version, changed to LN
  115. }
  116. {
  117. { Rev 1.2 12/06/2003 10:26:14 CCostelloe
  118. { Unfinished but compiles. Checked in to show problem with Get/SetNumericCode.
  119. }
  120. {
  121. { Rev 1.1 6/5/2003 04:54:26 AM JPMugaas
  122. { Reworkings and minor changes for new Reply exception framework.
  123. }
  124. {
  125. { Rev 1.0 5/27/2003 03:03:54 AM JPMugaas
  126. }
  127. unit IdReplyIMAP4;
  128. {
  129. 2003-Sep-26: CC2: Added Extra property.
  130. 2003-Oct-18: CC3: Added RemoveUnsolicitedResponses function.
  131. 2003-Nov-28: CC4: Fixes for Courier IMAP server.
  132. }
  133. interface
  134. uses
  135. IdReply,
  136. IdReplyRFC,
  137. IdObjs;
  138. const
  139. IMAP_OK = 'OK'; {Do not Localize}
  140. IMAP_NO = 'NO'; {Do not Localize}
  141. IMAP_BAD = 'BAD'; {Do not Localize}
  142. IMAP_PREAUTH = 'PREAUTH'; {Do not Localize}
  143. IMAP_BYE = 'BYE'; {Do not Localize}
  144. IMAP_CONT = '+'; {Do not Localize}
  145. VALID_TAGGEDREPLIES : array [0..5] of string =
  146. (IMAP_OK, IMAP_NO, IMAP_BAD, IMAP_PREAUTH, IMAP_BYE, IMAP_CONT);
  147. type
  148. TIdReplyIMAP4 = class(TIdReply)
  149. protected
  150. {CC: A tagged IMAP response is 'C41 OK Completed', where C41 is the
  151. command sequence number identifying the command you sent to get that
  152. response. An untagged one is '* OK Bad parameter'. The codes are
  153. the same, some just start with *.
  154. FSequenceNumber is either a *, C41 or '' (if the response line starts with
  155. a valid response code like OK)...}
  156. FSequenceNumber: string;
  157. {IMAP servers can send extra info after a command like "BAD Bad parameter".
  158. Keep these for error messages (may be more than one).
  159. Unsolicited responses from the server will also be put here.}
  160. FExtra: TIdStrings;
  161. function GetExtra: TIdStrings; //Added to get over .NET not calling TIdReplyIMAP4's constructor
  162. {You would think that we need to override IdReply's Get/SetNumericCode
  163. because they assume the code is like '32' whereas IMAP codes are text like
  164. 'OK' (when IdReply's StrToIntDef always returns 0), but Indy 10 has switched
  165. from numeric codes to string codes (i.e. we use 'OK' and never a
  166. numeric equivalent like 4).}
  167. {function GetNumericCode: Integer;
  168. procedure SetNumericCode(const AValue: Integer);}
  169. {Get/SetFormattedReply need to be overriden for IMAP4}
  170. function GetFormattedReply: TIdStrings; override;
  171. procedure SetFormattedReply(const AValue: TIdStrings); override;
  172. {CC: Need this also, otherwise the virtual one in IdReply uses
  173. TIdReplyRFC.CheckIfCodeIsValid which will only convert numeric
  174. codes like '22' to integer 22.}
  175. function CheckIfCodeIsValid(const ACode: string): Boolean; override;
  176. public
  177. constructor Create(
  178. ACollection: TIdCollection = nil;
  179. AReplyTexts: TIdReplies = nil
  180. ); override;
  181. destructor Destroy; override;
  182. procedure Clear; override;
  183. //
  184. //CLIENT-SIDE (TIdIMAP4) FUNCTIONS...
  185. procedure RaiseReplyError; override;
  186. procedure DoReplyError(ADescription: string; AnOffendingLine: string = ''); reintroduce;
  187. procedure RemoveUnsolicitedResponses(AExpectedResponses: array of String);
  188. function DoesLineHaveExpectedResponse(ALine: string; AExpectedResponses: array of string): Boolean;
  189. {CC: The following decides if AValue is a valid command sequence number
  190. like C41...}
  191. function IsItAValidSequenceNumber(const AValue: string): Boolean;
  192. {CC2: The following determines if AText consists only of digits...}
  193. function IsItANumber(const AValue: string): Boolean;
  194. //
  195. //SERVER-SIDE (TIdIMAP4Server) FUNCTIONS...
  196. function ParseRequest(ARequest: string): Boolean;
  197. //
  198. property NumericCode: Integer read GetNumericCode write SetNumericCode;
  199. property Extra: TIdStrings read GetExtra;
  200. property SequenceNumber: string read FSequenceNumber;
  201. //
  202. end;
  203. TIdRepliesIMAP4 = class(TIdReplies)
  204. public
  205. constructor Create(AOwner: TIdPersistent); reintroduce;
  206. end;
  207. //This error method came from the POP3 Protocol reply exceptions
  208. // SendCmd / GetResponse
  209. EIdReplyIMAP4Error = class(EIdReplyError)
  210. public
  211. constructor CreateError(const AReplyMessage: string); {reintroduce; virtual;}
  212. end;
  213. implementation
  214. uses IdGlobal, IdGlobalProtocols, IdSys;
  215. { TIdReplyIMAP4 }
  216. function TIdReplyIMAP4.ParseRequest(ARequest: string): Boolean;
  217. begin
  218. FSequenceNumber := Fetch(ARequest);
  219. Result := IsItAValidSequenceNumber(FSequenceNumber);
  220. end;
  221. function TIdReplyIMAP4.GetExtra: TIdStrings;
  222. begin
  223. if not Assigned(FExtra) then begin
  224. FExtra := TIdStringList.Create;
  225. end;
  226. Result := FExtra;
  227. end;
  228. constructor TIdReplyIMAP4.Create(
  229. ACollection: TIdCollection = nil;
  230. AReplyTexts: TIdReplies = nil
  231. );
  232. begin
  233. inherited;
  234. FExtra := TIdStringList.Create;
  235. Clear;
  236. end;
  237. destructor TIdReplyIMAP4.Destroy;
  238. begin
  239. Sys.FreeAndNil(FExtra);
  240. inherited;
  241. end;
  242. procedure TIdReplyIMAP4.Clear;
  243. begin
  244. inherited Clear;
  245. FSequenceNumber := '';
  246. Extra.Clear;
  247. end;
  248. procedure TIdReplyIMAP4.RaiseReplyError;
  249. begin
  250. raise EIdReplyIMAP4Error.CreateError('Default RaiseReply error'); {do not localize}
  251. end;
  252. function TIdReplyIMAP4.IsItANumber(const AValue: string): Boolean;
  253. var
  254. LN: integer;
  255. begin
  256. Result := False;
  257. for LN := 1 to Length(AValue) do begin
  258. if ( (Ord(AValue[LN]) < Ord('0')) or (Ord(AValue[LN]) > Ord('9')) ) then begin {Do not Localize}
  259. Exit;
  260. end;
  261. end;
  262. Result := True;
  263. end;
  264. function TIdReplyIMAP4.IsItAValidSequenceNumber(const AValue: string): Boolean;
  265. {CC: The following decides if AValue is a valid command sequence number
  266. like C41...}
  267. begin
  268. Result := False;
  269. {CC: Cannot be a C or a digit on its own...}
  270. if Length(AValue) >= 2 then begin
  271. {CC: Must start with a C...}
  272. if AValue[1] = 'C' then begin {Do not Localize}
  273. {CC: Check if other characters are digits...}
  274. Result := IsItANumber(Copy(AValue, 2, MaxInt));
  275. end;
  276. end;
  277. end;
  278. function TIdReplyIMAP4.CheckIfCodeIsValid(const ACode: string): Boolean;
  279. var
  280. LOrd : Integer;
  281. begin
  282. LOrd := PosInStrArray(ACode, VALID_TAGGEDREPLIES, False);
  283. Result := (LOrd <> -1) or (Sys.Trim(ACode) = '');
  284. end;
  285. function TIdReplyIMAP4.GetFormattedReply: TIdStrings;
  286. begin
  287. {Used by TIdIMAP4Server to assemble a string reply from our fields...}
  288. FFormattedReply.Clear;
  289. Result := FFormattedReply;
  290. end;
  291. procedure TIdReplyIMAP4.SetFormattedReply(const AValue: TIdStrings);
  292. {CC: AValue may be in one of a few formats:
  293. 1) Many commands just give a simple result to the command issued:
  294. C41 OK Completed
  295. 2) Some commands give you data first, then the result:
  296. * LIST (\UnMarked) "/" INBOX
  297. * LIST (\UnMarked) "/" Junk
  298. * LIST (\UnMarked) "/" Junk/Subbox1
  299. C42 OK Completed
  300. 3) Some responses have a result but * instead of a command number (like C42):
  301. * OK CommuniGate Pro IMAP Server 3.5.7 ready
  302. 4) Some have neither a * nor command number, but start with a result:
  303. + Send the additional command text
  304. or:
  305. BAD Bad parameter
  306. Because you may get data first, which you need to put into Text, you need to
  307. accept all the above possibilities.
  308. In this function, we can assume that the last line of AValues has previously been
  309. identified (by GetResponse).
  310. For the Text parameter, data lines are added with the starting * stripped off.
  311. The last Text line is the response line (the OK, BAD, etc., line) with any *
  312. and response (OK, BAD) stripped out - this is usually just Completed or the
  313. error message.
  314. Set FSequenceNumber to C41 for cases (1) and (2) above, * for case (3), and
  315. empty '' for case 4. This tells the caller the context of the reply.
  316. }
  317. var
  318. LWord: string;
  319. LPos: integer;
  320. LBuf : String;
  321. LN: integer;
  322. LLine: string;
  323. begin
  324. Clear;
  325. LWord := '';
  326. if AValue.Count <= 0 then begin
  327. {Throw an exception. Something is badly messed up if we were called with
  328. an empty string list.}
  329. DoReplyError('Unexpected: Logic error, SetFormattedReply called with an empty list of parameters'); {do not localize}
  330. end;
  331. {CC: Any lines before the last one should be data lines, which begin with a * ...}
  332. for LN := 0 to AValue.Count - 2 do begin
  333. LLine := AValue[LN];
  334. if LLine <> '' then begin
  335. LWord := Sys.Trim(Fetch(LLine));
  336. LLine := Sys.Trim(LLine);
  337. if (LLine = '') then begin
  338. {Throw an exception: this line is a single word, not a valid data
  339. line since it does not have a * plus at least one word of data.}
  340. DoReplyError('Unexpected: Non-last response line (i.e. a data line) only contained one word, instead of a * followed by one or more words', AValue[LN]); {do not localize}
  341. end;
  342. if (LWord <> '*') then begin {Do not Localize}
  343. //Throw an exception: No * as first word of a data line.
  344. DoReplyError('Unexpected: Non-last response line (i.e. a data line) did not start with a *', AValue[LN]); {do not localize}
  345. end;
  346. Text.Add(LLine);
  347. end;
  348. end;
  349. {The response (OK, BAD, etc.) is in the LAST line received (or else the
  350. function that got the response, such as GetResponse, is broken).}
  351. LLine := AValue[AValue.Count-1];
  352. if LLine = '' then begin
  353. {Throw an exception: The previous function (GetResponse, or whatever)
  354. messed up and passed an empty line as the response (last) line...}
  355. DoReplyError('Unexpected: Response (last) line was empty instead of containing a line with a response code like OK, NO, BAD, etc'); {do not localize}
  356. end;
  357. LBuf := LLine;
  358. LWord := Sys.Trim(Fetch(LBuf));
  359. LBuf := Sys.Trim(LBuf);
  360. {We can assume, if the previous function (GetResponse) did its
  361. job, that either the first or the second word (if it exists) is the
  362. response code...}
  363. LPos := PosInStrArray(LWord, VALID_TAGGEDREPLIES); {Do not Localize}
  364. if LPos > -1 then begin
  365. {The first word is a valid response. Leave FSequenceNumber as ''
  366. because there was nothing before it.}
  367. FCode := LWord;
  368. Text.Add(LBuf);
  369. end else if LWord = '*' then begin {Do not Localize}
  370. if LBuf = '' then begin
  371. {Throw an exception: it is a line that is just '*'}
  372. DoReplyError('Unexpected: Response (last) line contained only a *'); {do not localize}
  373. end;
  374. FSequenceNumber := LWord; {Record that it is a * line}
  375. {The next word had better be a response...}
  376. LWord := Sys.Trim(Fetch(LBuf));
  377. LBuf := Sys.Trim(LBuf);
  378. if (LBuf = '') then begin
  379. {Should never get to here: LBuf should have been ''. Might as
  380. well throw an exception since we are down here anyway.}
  381. DoReplyError('Unexpected: Response (last) line contained only a * (type 2)'); {do not localize}
  382. end;
  383. LPos := PosInStrArray(LWord, VALID_TAGGEDREPLIES);
  384. if LPos = -1 then begin
  385. {A line beginning with * but no valid response code as the 2nd
  386. word. It is invalid, but maybe a data line that GetResponse
  387. missed. Throw an exception anyway.}
  388. DoReplyError('Unexpected: Response (last) line started with a * but next word was not a valid response like OK, BAD, etc', LLine); {do not localize}
  389. end;
  390. {A valid resonse code...}
  391. FCode := LWord;
  392. Text.Add(LBuf);
  393. end else if IsItAValidSequenceNumber(LWord) then begin
  394. if LBuf = '' then begin
  395. {Throw an exception: it is a line that is just 'C41' or whatever}
  396. DoReplyError('Unexpected: Response (last) line started with a command reference (like C41) but nothing else', LLine); {do not localize}
  397. end;
  398. FSequenceNumber := LWord; {Record that it is a C41 line}
  399. {The next word had better be a response...}
  400. LWord := Sys.Trim(Fetch(LBuf));
  401. LBuf := Sys.Trim(LBuf);
  402. if LBuf = '' then begin
  403. {Should never get to here: LBuf should have been ''. Might as
  404. well throw an exception since we are down here anyway.}
  405. DoReplyError('Unexpected: Logic error, line starts with a command reference (like C41) but nothing else, why was an exception not thrown earlier?', LLine); {do not localize}
  406. end;
  407. LPos := PosInStrArray(LWord, VALID_TAGGEDREPLIES);
  408. if LPos = -1 then begin
  409. {A line beginning with C41 but no valid response code as the 2nd
  410. word. Throw an exception.}
  411. DoReplyError('Unexpected: Line starts with a command reference (like C41) but next word was not a valid response like OK, BAD, etc', LLine); {do not localize}
  412. end;
  413. {A valid response code...}
  414. FCode := LWord;
  415. //CC4: LBuf will contain "SEARCH completed" if LLine was "C64 OK SEARCH completed".
  416. //Ditch LBuf, otherwise we will confuse the later parser that checks for
  417. //"expected response" keywords.
  418. Extra.Add(LBuf);
  419. end else begin
  420. {Not a response, * or command (e.g. C41). Throw an exception, as usual.}
  421. DoReplyError('Unexpected: Line does not start with a command reference (like C41), a *, or a valid response like OK, BAD, etc', LLine); {do not localize}
  422. end;
  423. if FCode = '' then begin
  424. {Did not get a valid response line, copy ALL of the last line we received
  425. into Text[] for error display. This is paranoid programming, we probably
  426. would have thrown an exception by now.}
  427. Text.Add(AValue[AValue.Count-1]);
  428. end;
  429. end;
  430. procedure TIdReplyIMAP4.RemoveUnsolicitedResponses(AExpectedResponses: array of String);
  431. {CC3: This goes through the lines in Text and moves any that are not "expected" into
  432. Extra. Lines that are "expected" are those that have a command in one of the
  433. strings in AExpectedResponses, which has entries like "FETCH", "UID", "LIST".
  434. Unsolicited responses are typically lines like "* RECENT 3", which are sent by
  435. the server to tell you that new messages arrived. The problem is that they can
  436. be anywhere in a reply from the server, the RFC does not stipulate where, or
  437. what their format may be, but they wont be expected by the caller and will cause
  438. the caller's parsing to fail.
  439. The Text variable also has the bits stripped off from the final response, i.e.
  440. it will have "Completed" as the last entry, stripped from "C62 OK Completed".}
  441. var
  442. LLine: string;
  443. LN, LIndex: integer;
  444. LLast: integer; {Need to calculate this outside the loop}
  445. begin
  446. {The (valid) lines are of one of two formats:
  447. * LIST BlahBlah
  448. * 53 FETCH BlahBlah
  449. The "53" arises with commands that reference a specific email, the server returns
  450. the relative message number in that case.
  451. Note the * has been stripped off before this procedure is called.}
  452. LLast := Text.Count-1;
  453. LIndex := 0;
  454. for LN := 0 to LLast do begin
  455. LLine := Text[LIndex];
  456. if LLine = '' then begin
  457. {Unlikely to happen, but paranoia is always a better approach...}
  458. Text.Delete(LIndex);
  459. end else begin
  460. if DoesLineHaveExpectedResponse(LLine, AExpectedResponses) then begin
  461. {We were expecting this word, so don't remove this line.}
  462. Inc(LIndex);
  463. continue;
  464. end;
  465. {We were not expecting this response, it is an unsolicited response or
  466. something else we are not interested in. Transfer the UNSTRIPPED
  467. line to Extra (i.e. not LLine).}
  468. Extra.Add(Text[LIndex]);
  469. Text.Delete(LIndex);
  470. end;
  471. end;
  472. end;
  473. function TIdReplyIMAP4.DoesLineHaveExpectedResponse(ALine: string; AExpectedResponses: array of string): Boolean;
  474. var
  475. LWord: string;
  476. LPos: integer;
  477. begin
  478. Result := False;
  479. {Get the first word, it may be a relative message number like "53".
  480. CC4: Note the line may only consist of a single word, e.g. "SEARCH" with some
  481. servers (e.g. Courier) where there were no matches to the search.}
  482. LPos := Pos(' ', ALine); {Do not Localize}
  483. if LPos > 0 then begin
  484. if IsItANumber(Copy(ALine, 1, LPos-1)) then begin
  485. ALine := Copy(ALine, LPos+1, MAXINT);
  486. end;
  487. {If there was a relative message number, it is now stripped from LLine.}
  488. {The first word in LLine is the one that may hold our expected response.}
  489. LPos := Pos(' ', ALine); {Do not Localize}
  490. if LPos > 0 then begin
  491. LWord := Copy(ALine, 1, LPos-1);
  492. end else begin
  493. LWord := ALine;
  494. end;
  495. end else begin
  496. LWord := ALine;
  497. end;
  498. if PosInStrArray(LWord, AExpectedResponses) > -1 then begin
  499. {We were expecting this word...}
  500. Result := True;
  501. end;
  502. end;
  503. procedure TIdReplyIMAP4.DoReplyError(ADescription: string; AnOffendingLine: string);
  504. var
  505. LMsg: string;
  506. begin
  507. LMsg := ADescription;
  508. if AnOffendingLine <> '' then begin
  509. LMsg := LMsg + ', offending line: ' + AnOffendingLine; {do not localize}
  510. end;
  511. raise EIdReplyIMAP4Error.CreateError(LMsg);
  512. end;
  513. { TIdRepliesIMAP4 }
  514. constructor TIdRepliesIMAP4.Create(AOwner: TIdPersistent);
  515. begin
  516. inherited Create(AOwner, TIdReplyIMAP4);
  517. end;
  518. { EIdReplyIMAP4Error }
  519. constructor EIdReplyIMAP4Error.CreateError(const AReplyMessage: string);
  520. begin
  521. inherited Create(AReplyMessage);
  522. end;
  523. end.