You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

803 lines
19 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. /*
  2. ** CWPart.m
  3. **
  4. ** Copyright (c) 2001-2007
  5. **
  6. ** Author: Ludovic Marcotte <ludovic@Sophos.ca>
  7. **
  8. ** This library is free software; you can redistribute it and/or
  9. ** modify it under the terms of the GNU Lesser General Public
  10. ** License as published by the Free Software Foundation; either
  11. ** version 2.1 of the License, or (at your option) any later version.
  12. **
  13. ** This library is distributed in the hope that it will be useful,
  14. ** but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  16. ** Lesser General Public License for more details.
  17. **
  18. ** You should have received a copy of the GNU Lesser General Public
  19. ** License along with this library; if not, write to the Free Software
  20. ** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  21. */
  22. #import "CWPart.h"
  23. #import "CWConstants.h"
  24. #import <PantomimeFramework/CWMessage.h>
  25. #import "CWMIMEMultipart.h"
  26. #import "CWMIMEUtility.h"
  27. #import "NSData+Extensions.h"
  28. #import "Pantomime/NSString+Extensions.h"
  29. #import "Pantomime/CWParser.h"
  30. #import "CWFlags.h"
  31. #import "CWIMAPMessage.h"
  32. #import <Foundation/NSAutoreleasePool.h>
  33. #import <Foundation/NSException.h>
  34. #import <Foundation/NSValue.h>
  35. #import <string.h>
  36. #define LF "\n"
  37. static int currentPartVersion = 2;
  38. //
  39. //
  40. //
  41. @implementation CWPart
  42. - (id) init
  43. {
  44. self = [super init];
  45. [CWPart setVersion: currentPartVersion];
  46. // We initialize our dictionary that will hold all our headers with a capacity of 25.
  47. // This is an empirical number that is used to speedup the addition of headers w/o
  48. // reallocating our array everytime we add a new element.
  49. _headers = [[NSMutableDictionary alloc] initWithCapacity: 25];
  50. _parameters = [[NSMutableDictionary alloc] init];
  51. _line_length = _size = 0;
  52. _content = nil;
  53. return self;
  54. }
  55. //
  56. //
  57. //
  58. - (void) dealloc
  59. {
  60. RELEASE(_defaultCharset);
  61. RELEASE(_parameters);
  62. RELEASE(_headers);
  63. RELEASE(_content);
  64. //[super dealloc];
  65. }
  66. //
  67. //
  68. //
  69. - (id) initWithData: (NSData *) theData
  70. {
  71. NSRange aRange;
  72. aRange = [theData rangeOfCString: "\n\n"];
  73. if (aRange.length == 0)
  74. {
  75. AUTORELEASE_VOID(self);
  76. return nil;
  77. }
  78. // We initialize our message with the headers and the content
  79. self = [self init];
  80. [CWPart setVersion: currentPartVersion];
  81. // We verify if we have an empty body part content like:
  82. // X-UID: 5dc5aa4b82240000
  83. //
  84. // This is a MIME Message
  85. //
  86. // ------=_NextPart_000_007F_01BDF6C7.FABAC1B0
  87. //
  88. //
  89. // ------=_NextPart_000_007F_01BDF6C7.FABAC1B0
  90. // Content-Type: text/html; name="7english.co.kr.htm"
  91. if ([theData length] == 2)
  92. {
  93. [self setContent: [NSData data]];
  94. return self;
  95. }
  96. [self setHeadersFromData:
  97. [theData subdataWithRange: NSMakeRange(0,aRange.location)]];
  98. [CWMIMEUtility setContentFromRawSource:
  99. [theData subdataWithRange:
  100. NSMakeRange(aRange.location + 2,
  101. [theData length]-(aRange.location+2))]
  102. inPart: self];
  103. return self;
  104. }
  105. //
  106. //
  107. //
  108. - (id) initWithData: (NSData *) theData
  109. charset: (NSString *) theCharset
  110. {
  111. [CWPart setVersion: currentPartVersion];
  112. [self setDefaultCharset: theCharset];
  113. return [self initWithData: theData];
  114. }
  115. //
  116. // NSCoding protocol
  117. //
  118. - (void) encodeWithCoder: (NSCoder *) theCoder
  119. {
  120. [CWPart setVersion: currentPartVersion];
  121. [theCoder encodeObject: [self contentType]];
  122. [theCoder encodeObject: [self contentID]];
  123. [theCoder encodeObject: [self contentDescription]];
  124. [theCoder encodeObject: [NSNumber numberWithInt: [self contentDisposition]]];
  125. [theCoder encodeObject: [self filename]];
  126. [theCoder encodeObject: [NSNumber numberWithInteger: [self contentTransferEncoding]]];
  127. [theCoder encodeObject: [NSNumber numberWithInteger: [self format]]];
  128. [theCoder encodeObject: [NSNumber numberWithInteger: _size]];
  129. [theCoder encodeObject: [self boundary]];
  130. [theCoder encodeObject: [self charset]];
  131. [theCoder encodeObject: _defaultCharset];
  132. }
  133. - (id) initWithCoder: (NSCoder *) theCoder
  134. {
  135. self = [super init];
  136. _headers = [[NSMutableDictionary alloc] initWithCapacity: 25];
  137. _parameters = [[NSMutableDictionary alloc] init];
  138. [self setContentType: [theCoder decodeObject]];
  139. [self setContentID: [theCoder decodeObject]];
  140. [self setContentDescription: [theCoder decodeObject]];
  141. [self setContentDisposition: [[theCoder decodeObject] intValue]];
  142. [self setFilename: [theCoder decodeObject]];
  143. [self setContentTransferEncoding: [[theCoder decodeObject] intValue]];
  144. [self setFormat: [[theCoder decodeObject] intValue]];
  145. [self setSize: [[theCoder decodeObject] intValue]];
  146. [self setBoundary: [theCoder decodeObject]];
  147. [self setCharset: [theCoder decodeObject]];
  148. [self setDefaultCharset: [theCoder decodeObject]];
  149. _content = nil;
  150. return self;
  151. }
  152. //
  153. // access / mutation methods
  154. //
  155. - (NSObject *) content
  156. {
  157. return _content;
  158. }
  159. //
  160. //
  161. //
  162. - (void) setContent: (NSObject *) theContent
  163. {
  164. if (theContent && !([theContent isKindOfClass: [NSData class]] ||
  165. [theContent isKindOfClass: [CWMessage class]] ||
  166. [theContent isKindOfClass: [CWMIMEMultipart class]]))
  167. {
  168. [NSException raise: NSInvalidArgumentException
  169. format: @"Invalid argument to CWPart: -setContent: The content MUST be either a NSData, CWMessage or CWMIMEMessage instance."];
  170. }
  171. ASSIGN(_content, theContent);
  172. }
  173. //
  174. //
  175. //
  176. - (NSString *) contentType
  177. {
  178. return [_headers objectForKey: @"Content-Type"];
  179. }
  180. - (void) setContentType: (NSString*) theContentType
  181. {
  182. if (theContentType)
  183. {
  184. [_headers setObject: theContentType forKey: @"Content-Type"];
  185. }
  186. }
  187. //
  188. //
  189. //
  190. - (NSString *) contentID
  191. {
  192. return [_headers objectForKey: @"Content-Id"];
  193. }
  194. - (void) setContentID: (NSString *) theContentID
  195. {
  196. if (theContentID)
  197. {
  198. [_headers setObject: theContentID forKey: @"Content-Id"];
  199. }
  200. }
  201. //
  202. //
  203. //
  204. - (NSString *) contentDescription
  205. {
  206. return [_headers objectForKey: @"Content-Description"];
  207. }
  208. - (void) setContentDescription: (NSString *) theContentDescription
  209. {
  210. if (theContentDescription)
  211. {
  212. [_headers setObject: theContentDescription forKey: @"Content-Description"];
  213. }
  214. }
  215. //
  216. //
  217. //
  218. - (PantomimeContentDisposition) contentDisposition
  219. {
  220. id o;
  221. o = [_headers objectForKey: @"Content-Disposition"];
  222. return (o ? [o intValue] : PantomimeAttachmentDisposition);
  223. }
  224. - (void) setContentDisposition: (PantomimeContentDisposition) theContentDisposition
  225. {
  226. [_headers setObject: [NSNumber numberWithInt: theContentDisposition] forKey: @"Content-Disposition"];
  227. }
  228. //
  229. //
  230. //
  231. - (PantomimeEncoding) contentTransferEncoding
  232. {
  233. id o;
  234. o = [_headers objectForKey: @"Content-Transfer-Encoding"];
  235. if (o)
  236. {
  237. return [o intValue];
  238. }
  239. // Default value for the Content-Transfer-Encoding.
  240. // See RFC2045 - 6.1. Content-Transfer-Encoding Syntax.
  241. return PantomimeEncodingNone;
  242. }
  243. - (void) setContentTransferEncoding: (PantomimeEncoding) theEncoding
  244. {
  245. [_headers setObject: [NSNumber numberWithInt: theEncoding] forKey: @"Content-Transfer-Encoding"];
  246. }
  247. - (NSString *)filename
  248. {
  249. return [_parameters objectForKey: @"filename"];
  250. }
  251. - (void)setFilename:(NSString *)theFilename
  252. {
  253. if (theFilename && ([theFilename length] > 0)) {
  254. [_parameters setObject: theFilename forKey: @"filename"];
  255. } else {
  256. [_parameters setObject: @"unknown" forKey: @"filename"];
  257. }
  258. }
  259. //
  260. //
  261. //
  262. - (PantomimeMessageFormat) format
  263. {
  264. id o;
  265. o = [_parameters objectForKey: @"format"];
  266. if (o)
  267. {
  268. return [o intValue];
  269. }
  270. return PantomimeFormatUnknown;
  271. }
  272. - (void) setFormat: (PantomimeMessageFormat) theFormat
  273. {
  274. [_parameters setObject: [NSNumber numberWithInt: theFormat] forKey: @"format"];
  275. }
  276. //
  277. //
  278. //
  279. - (NSUInteger) lineLength
  280. {
  281. return _line_length;
  282. }
  283. - (void) setLineLength: (int) theLineLength
  284. {
  285. _line_length = theLineLength;
  286. }
  287. //
  288. // This method is used to very if the part is of the following primaryType / subType
  289. //
  290. - (BOOL) isMIMEType: (NSString *) thePrimaryType
  291. subType: (NSString *) theSubType
  292. {
  293. NSString *aString;
  294. if (![self contentType])
  295. {
  296. return NO;//[self setContentType: @"text/plain"];
  297. }
  298. if ([theSubType compare: @"*"] == NSOrderedSame)
  299. {
  300. if ([[self contentType] hasCaseInsensitivePrefix: thePrimaryType])
  301. {
  302. return YES;
  303. }
  304. }
  305. else
  306. {
  307. aString = [NSString stringWithFormat: @"%@/%@", thePrimaryType, theSubType];
  308. if ([aString caseInsensitiveCompare: [self contentType]] == NSOrderedSame)
  309. {
  310. return YES;
  311. }
  312. }
  313. return NO;
  314. }
  315. //
  316. //
  317. //
  318. - (long) size
  319. {
  320. return _size;
  321. }
  322. - (void) setSize: (NSInteger) theSize
  323. {
  324. _size = theSize;
  325. }
  326. - (NSData *)dataValue
  327. {
  328. NSMutableData *dataValue = [[NSMutableData alloc] init];
  329. // We start off by exactring the filename of the part.
  330. NSString *filename;
  331. if ([[self filename] is7bitSafe]) {
  332. filename = [self filename];
  333. } else {
  334. filename = [[NSString alloc] initWithData: [CWMIMEUtility encodeWordUsingQuotedPrintable: [self filename]
  335. prefixLength: 0]
  336. encoding: NSASCIIStringEncoding];
  337. }
  338. // We encode our Content-Transfer-Encoding header.
  339. if ([self contentTransferEncoding] != PantomimeEncodingNone) {
  340. [dataValue appendCFormat: @"Content-Transfer-Encoding: %@%s",
  341. [NSString stringValueOfTransferEncoding: [self contentTransferEncoding]],
  342. LF];
  343. }
  344. // We encode our Content-ID header.
  345. if ([self contentID]) {
  346. [dataValue appendCFormat: @"Content-ID: %@%s", [self contentID], LF];
  347. }
  348. // We encode our Content-Description header.
  349. if ([self contentDescription]) {
  350. [dataValue appendCString: "Content-Description: "];
  351. [dataValue appendData: [CWMIMEUtility encodeWordUsingQuotedPrintable: [self contentDescription]
  352. prefixLength: 21]];
  353. [dataValue appendCString: LF];
  354. }
  355. // We now encode the Content-Type header with its parameters.
  356. [dataValue appendCFormat: @"Content-Type: %@", [self contentType]];
  357. if ([self charset]) {
  358. [dataValue appendCFormat: @"; charset=\"%@\"", [self charset]];
  359. } else {
  360. // Charset unknown, default to UTF-8
  361. [dataValue appendCFormat: @"; charset=\"%@\"", @"UTF-8"];
  362. }
  363. if ([self format] == PantomimeFormatFlowed &&
  364. ([self contentTransferEncoding] == PantomimeEncodingNone || [self contentTransferEncoding] == PantomimeEncoding8bit)) {
  365. [dataValue appendCString: "; format=\"flowed\""];
  366. }
  367. if (filename && [filename length]) {
  368. [dataValue appendCFormat: @"; name=\"%@\"", filename];
  369. }
  370. // Before checking for all other parameters, we check for the boundary one
  371. // If we got a CWMIMEMultipart instance as the content but no boundary
  372. // was set, we create a boundary and we set it.
  373. if ([self boundary] || [_content isKindOfClass: [CWMIMEMultipart class]]) {
  374. if (![self boundary]) {
  375. [self setBoundary: [CWMIMEUtility globallyUniqueBoundary]];
  376. }
  377. [dataValue appendCFormat: @";%s\tboundary=\"",LF];
  378. [dataValue appendData: [self boundary]];
  379. [dataValue appendCString: "\""];
  380. }
  381. // We now check for any other additional parameters. If we have some,
  382. // we add them one per line. We first REMOVE what we have added! We'll
  383. // likely and protocol= here.
  384. NSMutableArray *allKeys = [NSMutableArray arrayWithArray: [_parameters allKeys]];
  385. [allKeys removeObject: @"boundary"];
  386. [allKeys removeObject: @"charset"];
  387. [allKeys removeObject: @"filename"];
  388. [allKeys removeObject: @"format"];
  389. for (int i = 0; i < [allKeys count]; i++) {
  390. [dataValue appendCFormat: @";%s", LF];
  391. [dataValue appendCFormat: @"\t%@=\"%@\"", [allKeys objectAtIndex: i], [_parameters objectForKey: [allKeys objectAtIndex: i]]];
  392. }
  393. [dataValue appendCString: LF];
  394. // We encode our Content-Disposition header. We ignore other parameters
  395. // (other than the filename one) since they are pretty much worthless.
  396. // See RFC2183 for details.
  397. PantomimeContentDisposition disposition = [self contentDisposition];
  398. if (disposition == PantomimeAttachmentDisposition ||
  399. disposition == PantomimeInlineDisposition) {
  400. if (disposition == PantomimeAttachmentDisposition) {
  401. [dataValue appendCString: "Content-Disposition: attachment"];
  402. } else {
  403. [dataValue appendCString: "Content-Disposition: inline"];
  404. }
  405. if (filename && [filename length]) {
  406. [dataValue appendCFormat: @"; filename=\"%@\"", filename];
  407. }
  408. [dataValue appendCString: LF];
  409. }
  410. NSData *dataToSend;
  411. if ([_content isKindOfClass: [CWMessage class]]) {
  412. dataToSend = [(CWMessage *)_content rawSource];
  413. } else if ([_content isKindOfClass: [CWMIMEMultipart class]]) {
  414. CWMIMEMultipart *aMimeMultipart;
  415. NSMutableData *md;
  416. CWPart *aPart;
  417. md = [[NSMutableData alloc] init];
  418. aMimeMultipart = (CWMIMEMultipart *)_content;
  419. NSUInteger count = [aMimeMultipart count];
  420. for (int i = 0; i < count; i++) {
  421. aPart = [aMimeMultipart partAtIndex: i];
  422. if (i > 0) {
  423. [md appendBytes: LF length: strlen(LF)];
  424. }
  425. [md appendBytes: "--" length: 2];
  426. [md appendData: [self boundary]];
  427. [md appendBytes: LF length: strlen(LF)];
  428. [md appendData: [aPart dataValue]];
  429. }
  430. [md appendBytes: "--" length: 2];
  431. [md appendData: [self boundary]];
  432. [md appendBytes: "--" length: 2];
  433. [md appendBytes: LF length: strlen(LF)];
  434. dataToSend = md;
  435. } else {
  436. dataToSend = (NSData *)_content;
  437. }
  438. // We separe our part's headers from the content
  439. [dataValue appendCFormat: @"%s", LF];
  440. // We now encode our content the way it was specified
  441. if ([self contentTransferEncoding] == PantomimeEncodingQuotedPrintable) {
  442. dataToSend = [dataToSend encodeQuotedPrintableWithLineLength: 72 inHeader: NO];
  443. } else if ([self contentTransferEncoding] == PantomimeEncodingBase64) {
  444. dataToSend = [dataToSend encodeBase64WithLineLength: 72];
  445. } else if (([self contentTransferEncoding] == PantomimeEncodingNone || [self contentTransferEncoding] == PantomimeEncoding8bit) &&
  446. [self format] == PantomimeFormatFlowed) {
  447. NSUInteger limit = _line_length;
  448. if (limit < 2 || limit > 998) {
  449. limit = 72;
  450. }
  451. dataToSend = [dataToSend wrapWithLimit: limit];
  452. }
  453. [dataToSend componentsSeparatedByCString:"\n"
  454. block:^(NSData *aLine, NSUInteger count, BOOL isLast) {
  455. if (isLast && [aLine length] == 0) {
  456. return;
  457. }
  458. [dataValue appendData:aLine];
  459. [dataValue appendBytes:LF length:1];
  460. }];
  461. return dataValue;
  462. }
  463. //
  464. //
  465. //
  466. - (NSData *) boundary
  467. {
  468. return [_parameters objectForKey: @"boundary"];
  469. }
  470. - (void) setBoundary: (NSData *) theBoundary
  471. {
  472. if (theBoundary)
  473. {
  474. [_parameters setObject: theBoundary forKey: @"boundary"];
  475. }
  476. }
  477. //
  478. //
  479. //
  480. - (NSData *) protocol
  481. {
  482. return [_parameters objectForKey: @"protocol"];
  483. //return _protocol;
  484. }
  485. - (void) setProtocol: (NSData *) theProtocol
  486. {
  487. //ASSIGN(_protocol, theProtocol);
  488. if (theProtocol)
  489. {
  490. [_parameters setObject: theProtocol forKey: @"protocol"];
  491. }
  492. }
  493. //
  494. //
  495. //
  496. - (NSString *) charset
  497. {
  498. return [_parameters objectForKey: @"charset"];
  499. }
  500. - (void) setCharset: (NSString *) theCharset
  501. {
  502. if (theCharset)
  503. {
  504. [_parameters setObject: theCharset forKey: @"charset"];
  505. }
  506. }
  507. //
  508. //
  509. //
  510. - (NSString *) defaultCharset
  511. {
  512. return _defaultCharset;
  513. }
  514. //
  515. //
  516. //
  517. - (void) setDefaultCharset: (NSString *) theCharset
  518. {
  519. ASSIGN(_defaultCharset, theCharset);
  520. }
  521. //
  522. //
  523. //
  524. - (void) setHeadersFromData: (NSData *) theHeaders
  525. {
  526. //NSAutoreleasePool *pool;
  527. NSArray *allLines;
  528. NSUInteger i, count;
  529. if (!theHeaders || [theHeaders length] == 0)
  530. {
  531. return;
  532. }
  533. // We initialize a local autorelease pool
  534. @autoreleasepool {
  535. // We MUST be sure to unfold all headers properly before
  536. // decoding the headers
  537. theHeaders = [theHeaders unfoldLines];
  538. allLines = [theHeaders componentsSeparatedByCString: "\n"];
  539. count = [allLines count];
  540. for (i = 0; i < count; i++)
  541. {
  542. NSData *aLine = [allLines objectAtIndex: i];
  543. // We stop if we found the header separator. (\n\n) since someone could
  544. // have called this method with the entire rawsource of a message.
  545. if ([aLine length] == 0)
  546. {
  547. break;
  548. }
  549. if ([aLine hasCaseInsensitiveCPrefix: "Content-Description"])
  550. {
  551. [CWParser parseContentDescription: aLine inPart: self];
  552. }
  553. else if ([aLine hasCaseInsensitiveCPrefix: "Content-Disposition"])
  554. {
  555. [CWParser parseContentDisposition: aLine inPart: self];
  556. }
  557. else if ([aLine hasCaseInsensitiveCPrefix: "Content-ID"])
  558. {
  559. [CWParser parseContentID: aLine inPart: self];
  560. }
  561. else if ([aLine hasCaseInsensitiveCPrefix: "Content-Length"])
  562. {
  563. // We just ignore that for now.
  564. }
  565. else if ([aLine hasCaseInsensitiveCPrefix: "Content-Transfer-Encoding"])
  566. {
  567. [CWParser parseContentTransferEncoding: aLine inPart: self];
  568. }
  569. else if ([aLine hasCaseInsensitiveCPrefix: "Content-Type"])
  570. {
  571. [CWParser parseContentType: aLine inPart: self];
  572. }
  573. }
  574. } //RELEASE(pool);
  575. }
  576. //
  577. //
  578. //
  579. - (id) parameterForKey: (NSString *) theKey
  580. {
  581. return [_parameters objectForKey: theKey];
  582. }
  583. - (void) setParameter: (NSString *) theParameter forKey: (NSString *) theKey
  584. {
  585. if (theParameter)
  586. {
  587. [_parameters setObject: theParameter forKey: theKey];
  588. }
  589. else
  590. {
  591. [_parameters removeObjectForKey: theKey];
  592. }
  593. }
  594. //
  595. //
  596. //
  597. - (NSDictionary *) allHeaders
  598. {
  599. return _headers;
  600. }
  601. //
  602. //
  603. //
  604. - (id) headerValueForName: (NSString *) theName
  605. {
  606. NSArray *allKeys;
  607. NSUInteger count;
  608. allKeys = [_headers allKeys];
  609. count = [allKeys count];
  610. while (count--)
  611. {
  612. if ([[allKeys objectAtIndex: count] caseInsensitiveCompare: theName] == NSOrderedSame)
  613. {
  614. return [_headers objectForKey: [allKeys objectAtIndex: count]];
  615. }
  616. }
  617. return nil;
  618. }
  619. //
  620. //
  621. //
  622. - (void) setHeaders: (NSDictionary *) theHeaders
  623. {
  624. if (theHeaders)
  625. {
  626. [_headers addEntriesFromDictionary: theHeaders];
  627. }
  628. else
  629. {
  630. [_headers removeAllObjects];
  631. }
  632. }
  633. - (NSString *)description
  634. {
  635. NSMutableString *str = [NSMutableString
  636. stringWithFormat:@"<CWPart 0x%u Part %ld bytes",
  637. (uint) self, self.size];
  638. if (self.contentType) {
  639. [str appendFormat:@", %@", self.contentType];
  640. }
  641. if (self.filename) {
  642. [str appendFormat:@", %@", self.filename];
  643. }
  644. if ([self isKindOfClass:[CWMessage class]]) {
  645. [str appendFormat:@" msn %lu", (unsigned long) [((CWIMAPMessage *) self) messageNumber]];
  646. }
  647. if ([self isKindOfClass:[CWIMAPMessage class]]) {
  648. [str appendFormat:@" messageID %@", [((CWIMAPMessage *) self) messageID]];
  649. [str appendFormat:@" uid %lu", (unsigned long) [((CWIMAPMessage *) self) UID]];
  650. CWFlags *flags = [((CWMessage *) self) flags];
  651. NSString *flagsString = [flags asString];
  652. if (flagsString.length == 0) {
  653. flagsString = @"NoFlags";
  654. }
  655. [str appendFormat:@" %@", flagsString];
  656. if (((CWMessage *) self).originationDate) {
  657. [str appendFormat:@" %@", ((CWMessage *) self).originationDate];
  658. }
  659. }
  660. [str appendString:@">"];
  661. return str;
  662. }
  663. @end