/[gnustep]/gnustep/core/base/Source/Additions/GSMime.m
ViewVC logotype

Contents of /gnustep/core/base/Source/Additions/GSMime.m

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.80 - (show annotations) (download)
Sat Sep 13 22:42:50 2003 UTC (20 years, 6 months ago) by ayers
Branch: MAIN
Changes since 1.79: +1 -2 lines
	* Headers/Additions/GNUstepBase/GSCategories.h: Move
	declarations
	from Source/Additions/GSCompatibility.h to here.
	([NSObject -compare:]): Deprecate.
	* Source/Additions/GSCompatibility.h: Move contents from here
	to
	Headers/Additions/GNUstepBase/GSCategories.h.

	* Source/Additions/GSCategories.m:
	([NSObject -compare:]): Deprecate.

	* Headers/Foundation/NSObject.h
	([NSObject -compare:]): Deprecate.

	* Source/Additions/GCDictionary.m: Include
	GNUstepBase/GSCategories.h rather than GSCompatibility.h.
	* Source/Additions/GCObject.m: Ditto.
	* Source/Additions/GSMime.m: Ditto.
	* Source/Additions/GSObjCRuntime.m: Ditto.
	* Source/Additions/GSXML.m: Ditto.
	* Source/Additions/Unicode.m: Ditto.

	* macosx/GNUstepBase/preface.h: Update include for new header
	structure.

1 /** Implementation for GSMIME
2
3 Copyright (C) 2000,2001 Free Software Foundation, Inc.
4
5 Written by: Richard frith-Macdonald <rfm@gnu.org>
6 Date: October 2000
7
8 This file is part of the GNUstep Base Library.
9
10 This library is free software; you can redistribute it and/or
11 modify it under the terms of the GNU Library General Public
12 License as published by the Free Software Foundation; either
13 version 2 of the License, or (at your option) any later version.
14
15 This library is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 Library General Public License for more details.
19
20 You should have received a copy of the GNU Library General Public
21 License along with this library; if not, write to the Free
22 Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA.
23
24 <title>The MIME parsing system</title>
25 <chapter>
26 <heading>Mime Parser</heading>
27 <p>
28 The GNUstep Mime parser. This is collection Objective-C classes
29 for representing MIME (and HTTP) documents and managing conversions
30 to and from convenient internal formats.
31 </p>
32 <p>
33 The idea is to center round two classes -
34 </p>
35 <deflist>
36 <term>document</term>
37 <desc>
38 A container for the actual data (and headers) of a mime/http
39 document, this is also used to create raw MIME data for sending.
40 </desc>
41 <term>parser</term>
42 <desc>
43 An object that can be fed data and will parse it into a document.
44 This object also provides various utility methods and an API
45 that permits overriding in order to extend the functionality to
46 cope with new document types.
47 </desc>
48 </deflist>
49 </chapter>
50 $Date: 2003/07/31 23:49:32 $ $Revision: 1.79 $
51 */
52
53 #include "config.h"
54 #include <Foundation/Foundation.h>
55 #include "GNUstepBase/GSMime.h"
56 #include "GNUstepBase/GSCategories.h"
57 #include <string.h>
58 #include <ctype.h>
59
60 static NSCharacterSet *whitespace = nil;
61 static NSCharacterSet *rfc822Specials = nil;
62 static NSCharacterSet *rfc2045Specials = nil;
63
64 /*
65 * Name - decodebase64()
66 * Purpose - Convert 4 bytes in base64 encoding to 3 bytes raw data.
67 */
68 static void
69 decodebase64(unsigned char *dst, const char *src)
70 {
71 dst[0] = (src[0] << 2) | ((src[1] & 0x30) >> 4);
72 dst[1] = ((src[1] & 0x0F) << 4) | ((src[2] & 0x3C) >> 2);
73 dst[2] = ((src[2] & 0x03) << 6) | (src[3] & 0x3F);
74 }
75
76 static char b64[]
77 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
78
79 static int
80 encodebase64(char *dst, const unsigned char *src, int length)
81 {
82 int dIndex = 0;
83 int sIndex;
84
85 for (sIndex = 0; sIndex < length; sIndex += 3)
86 {
87 int c0 = src[sIndex];
88 int c1 = src[sIndex+1];
89 int c2 = src[sIndex+2];
90
91 dst[dIndex++] = b64[(c0 >> 2) & 077];
92 dst[dIndex++] = b64[((c0 << 4) & 060) | ((c1 >> 4) & 017)];
93 dst[dIndex++] = b64[((c1 << 2) & 074) | ((c2 >> 6) & 03)];
94 dst[dIndex++] = b64[c2 & 077];
95 }
96
97 /* If len was not a multiple of 3, then we have encoded too
98 * many characters. Adjust appropriately.
99 */
100 if (sIndex == length + 1)
101 {
102 /* There were only 2 bytes in that last group */
103 dst[dIndex - 1] = '=';
104 }
105 else if (sIndex == length + 2)
106 {
107 /* There was only 1 byte in that last group */
108 dst[dIndex - 1] = '=';
109 dst[dIndex - 2] = '=';
110 }
111 dst[dIndex] = '\0';
112 return dIndex;
113 }
114
115 typedef enum {
116 WE_QUOTED,
117 WE_BASE64
118 } WE;
119
120 /*
121 * Name - decodeWord()
122 * Params - dst destination
123 * src where to start decoding from
124 * end where to stop decoding (or NULL if end of buffer).
125 * enc content-transfer-encoding
126 * Purpose - Decode text with BASE64 or QUOTED-PRINTABLE codes.
127 */
128 static unsigned char*
129 decodeWord(unsigned char *dst, unsigned char *src, unsigned char *end, WE enc)
130 {
131 int c;
132
133 if (enc == WE_QUOTED)
134 {
135 while (*src && (src != end))
136 {
137 if (*src == '=')
138 {
139 src++;
140 if (*src == '\0')
141 {
142 break;
143 }
144 if (('\n' == *src) || ('\r' == *src))
145 {
146 break;
147 }
148 c = isdigit(*src) ? (*src - '0') : (*src - 55);
149 c <<= 4;
150 src++;
151 if (*src == '\0')
152 {
153 break;
154 }
155 c += isdigit(*src) ? (*src - '0') : (*src - 55);
156 *dst = c;
157 }
158 else if (*src == '_')
159 {
160 *dst = '\040';
161 }
162 else
163 {
164 *dst = *src;
165 }
166 dst++;
167 src++;
168 }
169 *dst = '\0';
170 return dst;
171 }
172 else if (enc == WE_BASE64)
173 {
174 unsigned char buf[4];
175 unsigned pos = 0;
176
177 while (*src && (src != end))
178 {
179 c = *src++;
180 if (isupper(c))
181 {
182 c -= 'A';
183 }
184 else if (islower(c))
185 {
186 c = c - 'a' + 26;
187 }
188 else if (isdigit(c))
189 {
190 c = c - '0' + 52;
191 }
192 else if (c == '/')
193 {
194 c = 63;
195 }
196 else if (c == '+')
197 {
198 c = 62;
199 }
200 else if (c == '=')
201 {
202 c = -1;
203 }
204 else if (c == '-')
205 {
206 break; /* end */
207 }
208 else
209 {
210 c = -1; /* ignore */
211 }
212
213 if (c >= 0)
214 {
215 buf[pos++] = c;
216 if (pos == 4)
217 {
218 pos = 0;
219 decodebase64(dst, buf);
220 dst += 3;
221 }
222 }
223 }
224
225 if (pos > 0)
226 {
227 unsigned i;
228
229 for (i = pos; i < 4; i++)
230 buf[i] = '\0';
231 pos--;
232 }
233 decodebase64(dst, buf);
234 dst += pos;
235 *dst = '\0';
236 return dst;
237 }
238 else
239 {
240 NSLog(@"Unsupported encoding type");
241 return end;
242 }
243 }
244
245 static NSString *
246 selectCharacterSet(NSString *str, NSData **d)
247 {
248 if ([str length] == 0)
249 {
250 *d = [NSData data];
251 return @"us-ascii"; // Default character set.
252 }
253 if ((*d = [str dataUsingEncoding: NSASCIIStringEncoding]) != nil)
254 return @"us-ascii"; // Default character set.
255 if ((*d = [str dataUsingEncoding: NSISOLatin1StringEncoding]) != nil)
256 return @"iso-8859-1";
257 if ((*d = [str dataUsingEncoding: NSISOLatin2StringEncoding]) != nil)
258 return @"iso-8859-2";
259 if ((*d = [str dataUsingEncoding: NSISOLatin3StringEncoding]) != nil)
260 return @"iso-8859-3";
261 if ((*d = [str dataUsingEncoding: NSISOLatin4StringEncoding]) != nil)
262 return @"iso-8859-4";
263 if ((*d = [str dataUsingEncoding: NSISOCyrillicStringEncoding]) != nil)
264 return @"iso-8859-5";
265 if ((*d = [str dataUsingEncoding: NSISOArabicStringEncoding]) != nil)
266 return @"iso-8859-6";
267 if ((*d = [str dataUsingEncoding: NSISOGreekStringEncoding]) != nil)
268 return @"iso-8859-7";
269 if ((*d = [str dataUsingEncoding: NSISOHebrewStringEncoding]) != nil)
270 return @"iso-8859-8";
271 if ((*d = [str dataUsingEncoding: NSISOLatin5StringEncoding]) != nil)
272 return @"iso-8859-9";
273 if ((*d = [str dataUsingEncoding: NSISOLatin6StringEncoding]) != nil)
274 return @"iso-8859-10";
275 if ((*d = [str dataUsingEncoding: NSISOLatin7StringEncoding]) != nil)
276 return @"iso-8859-13";
277 if ((*d = [str dataUsingEncoding: NSISOLatin8StringEncoding]) != nil)
278 return @"iso-8859-14";
279 if ((*d = [str dataUsingEncoding: NSISOLatin9StringEncoding]) != nil)
280 return @"iso-8859-15";
281 if ((*d = [str dataUsingEncoding: NSWindowsCP1250StringEncoding]) != nil)
282 return @"windows-1250";
283 if ((*d = [str dataUsingEncoding: NSWindowsCP1251StringEncoding]) != nil)
284 return @"windows-1251";
285 if ((*d = [str dataUsingEncoding: NSWindowsCP1252StringEncoding]) != nil)
286 return @"windows-1252";
287 if ((*d = [str dataUsingEncoding: NSWindowsCP1253StringEncoding]) != nil)
288 return @"windows-1253";
289 if ((*d = [str dataUsingEncoding: NSWindowsCP1254StringEncoding]) != nil)
290 return @"windows-1254";
291
292 *d = [str dataUsingEncoding: NSUTF8StringEncoding];
293 return @"utf-8"; // Catch-all character set.
294 }
295
296 /**
297 * Encode a word in a header according to RFC2047 if necessary.
298 * For an ascii word, we just return the data.
299 */
300 static NSData*
301 wordData(NSString *word)
302 {
303 NSData *d = nil;
304 NSString *charset;
305
306 charset = selectCharacterSet(word, &d);
307 if ([charset isEqualToString: @"us-ascii"] == YES)
308 {
309 return d;
310 }
311 else
312 {
313 int len = [charset cStringLength];
314 char buf[len+1];
315 NSMutableData *md;
316
317 [charset getCString: buf];
318 md = [NSMutableData dataWithCapacity: [d length]*4/3 + len + 8];
319 d = [GSMimeDocument encodeBase64: d];
320 [md appendBytes: "=?" length: 2];
321 [md appendBytes: buf length: len];
322 [md appendBytes: "?b?" length: 3];
323 [md appendData: d];
324 [md appendBytes: "?=" length: 2];
325 return md;
326 }
327 }
328
329 /**
330 * Coding contexts are objects used by the parser to store the state of
331 * decoding incoming data while it is being incrementally parsed.<br />
332 * The most rudimentary context ... this is used for decoding plain
333 * text and binary data (ie data which is not really decoded at all)
334 * and all other decoding work is done by a subclass.
335 */
336 @implementation GSMimeCodingContext
337 /**
338 * Returns the current value of the 'atEnd' flag.
339 */
340 - (BOOL) atEnd
341 {
342 return atEnd;
343 }
344
345 /**
346 * Copying is implemented as a simple retain.
347 */
348 - (id) copyWithZone: (NSZone*)z
349 {
350 return RETAIN(self);
351 }
352
353 /**
354 * Decode length bytes of data from sData and append the results to dData.<br />
355 * Return YES on succes, NO if there is an error.
356 */
357 - (BOOL) decodeData: (const void*)sData
358 length: (unsigned)length
359 intoData: (NSMutableData*)dData
360 {
361 unsigned size = [dData length];
362
363 [dData setLength: size + length];
364 memcpy([dData mutableBytes] + size, sData, length);
365 return YES;
366 }
367
368 /**
369 * Sets the current value of the 'atEnd' flag.
370 */
371 - (void) setAtEnd: (BOOL)flag
372 {
373 atEnd = flag;
374 }
375 @end
376
377 @interface GSMimeBase64DecoderContext : GSMimeCodingContext
378 {
379 @public
380 unsigned char buf[4];
381 unsigned pos;
382 }
383 @end
384 @implementation GSMimeBase64DecoderContext
385 - (BOOL) decodeData: (const void*)sData
386 length: (unsigned)length
387 intoData: (NSMutableData*)dData
388 {
389 unsigned size = [dData length];
390 unsigned char *src = (unsigned char*)sData;
391 unsigned char *end = src + length;
392 unsigned char *beg;
393 unsigned char *dst;
394
395 /*
396 * Expand destination data buffer to have capacity to handle info.
397 */
398 [dData setLength: size + (3 * (end + 8 - src))/4];
399 dst = (unsigned char*)[dData mutableBytes];
400 beg = dst;
401
402 /*
403 * Now decode data into buffer, keeping count and temporary
404 * data in context.
405 */
406 while (src < end)
407 {
408 int cc = *src++;
409
410 if (isupper(cc))
411 {
412 cc -= 'A';
413 }
414 else if (islower(cc))
415 {
416 cc = cc - 'a' + 26;
417 }
418 else if (isdigit(cc))
419 {
420 cc = cc - '0' + 52;
421 }
422 else if (cc == '+')
423 {
424 cc = 62;
425 }
426 else if (cc == '/')
427 {
428 cc = 63;
429 }
430 else if (cc == '=')
431 {
432 [self setAtEnd: YES];
433 cc = -1;
434 }
435 else if (cc == '-')
436 {
437 [self setAtEnd: YES];
438 break;
439 }
440 else
441 {
442 cc = -1; /* ignore */
443 }
444
445 if (cc >= 0)
446 {
447 buf[pos++] = cc;
448 if (pos == 4)
449 {
450 pos = 0;
451 decodebase64(dst, buf);
452 dst += 3;
453 }
454 }
455 }
456
457 /*
458 * Odd characters at end of decoded data need to be added separately.
459 */
460 if ([self atEnd] == YES && pos > 0)
461 {
462 unsigned len = pos - 1;;
463
464 while (pos < 4)
465 {
466 buf[pos++] = '\0';
467 }
468 pos = 0;
469 decodebase64(dst, buf);
470 size += len;
471 }
472 [dData setLength: size + dst - beg];
473 return YES;
474 }
475 @end
476
477 @interface GSMimeQuotedDecoderContext : GSMimeCodingContext
478 {
479 @public
480 unsigned char buf[4];
481 unsigned pos;
482 }
483 @end
484 @implementation GSMimeQuotedDecoderContext
485 - (BOOL) decodeData: (const void*)sData
486 length: (unsigned)length
487 intoData: (NSMutableData*)dData
488 {
489 unsigned size = [dData length];
490 unsigned char *src = (unsigned char*)sData;
491 unsigned char *end = src + length;
492 unsigned char *beg;
493 unsigned char *dst;
494
495 /*
496 * Expand destination data buffer to have capacity to handle info.
497 */
498 [dData setLength: size + (end - src)];
499 dst = (unsigned char*)[dData mutableBytes];
500 beg = dst;
501
502 while (src < end)
503 {
504 if (pos > 0)
505 {
506 if ((*src == '\n') || (*src == '\r'))
507 {
508 pos = 0;
509 }
510 else
511 {
512 buf[pos++] = *src;
513 if (pos == 3)
514 {
515 int c;
516 int val;
517
518 pos = 0;
519 c = buf[1];
520 val = isdigit(c) ? (c - '0') : (c - 55);
521 val *= 0x10;
522 c = buf[2];
523 val += isdigit(c) ? (c - '0') : (c - 55);
524 *dst++ = val;
525 }
526 }
527 }
528 else if (*src == '=')
529 {
530 buf[pos++] = '=';
531 }
532 else
533 {
534 *dst++ = *src;
535 }
536 src++;
537 }
538 [dData setLength: size + dst - beg];
539 return YES;
540 }
541 @end
542
543 @interface GSMimeChunkedDecoderContext : GSMimeCodingContext
544 {
545 @public
546 unsigned char buf[8];
547 unsigned pos;
548 enum {
549 ChunkSize, // Reading chunk size
550 ChunkExt, // Reading chunk extensions
551 ChunkEol1, // Reading end of line after size;ext
552 ChunkData, // Reading chunk data
553 ChunkEol2, // Reading end of line after data
554 ChunkFoot, // Reading chunk footer after newline
555 ChunkFootA // Reading chunk footer
556 } state;
557 unsigned size; // Size of buffer required.
558 NSMutableData *data;
559 }
560 @end
561 @implementation GSMimeChunkedDecoderContext
562 - (void) dealloc
563 {
564 RELEASE(data);
565 [super dealloc];
566 }
567 - (id) init
568 {
569 self = [super init];
570 if (self != nil)
571 {
572 data = [NSMutableData new];
573 }
574 return self;
575 }
576 @end
577
578
579
580 @interface GSMimeParser (Private)
581 - (BOOL) _decodeBody: (NSData*)data;
582 - (NSString*) _decodeHeader;
583 - (BOOL) _unfoldHeader;
584 - (BOOL) _scanHeaderParameters: (NSScanner*)scanner into: (GSMimeHeader*)info;
585 @end
586
587 /**
588 * <p>
589 * This class provides support for parsing MIME messages
590 * into GSMimeDocument objects. Each parser object maintains
591 * an associated document into which data is stored.
592 * </p>
593 * <p>
594 * You supply the document to be parsed as one or more data
595 * items passed to the -parse: method, and (if
596 * the method always returns YES, you give it
597 * a final nil argument to mark the end of the
598 * document.
599 * </p>
600 * <p>
601 * On completion of parsing a valid document, the
602 * [GSMimeParser-mimeDocument] method returns the
603 * resulting parsed document.
604 * </p>
605 */
606 @implementation GSMimeParser
607
608 /**
609 * Convenience method to parse a single data item as a MIME message
610 * and return the resulting document.
611 */
612 + (GSMimeDocument*) documentFromData: (NSData*)mimeData
613 {
614 GSMimeDocument *newDocument = nil;
615 GSMimeParser *parser = [GSMimeParser new];
616
617 if ([parser parse: mimeData] == YES)
618 {
619 [parser parse: nil];
620 }
621 if ([parser isComplete] == YES)
622 {
623 newDocument = [parser mimeDocument];
624 RETAIN(newDocument);
625 }
626 RELEASE(parser);
627 return AUTORELEASE(newDocument);
628 }
629
630 /**
631 * Create and return a parser.
632 */
633 + (GSMimeParser*) mimeParser
634 {
635 return AUTORELEASE([[self alloc] init]);
636 }
637
638 /**
639 * Return a coding context object to be used for decoding data
640 * according to the scheme specified in the header.
641 * <p>
642 * The default implementation supports the following transfer
643 * encodings specified in either a <code>transfer-encoding</code>
644 * of <code>content-transfer-encoding</code> header -
645 * </p>
646 * <list>
647 * <item>base64</item>
648 * <item>quoted-printable</item>
649 * <item>binary (no coding actually performed)</item>
650 * <item>7bit (no coding actually performed)</item>
651 * <item>8bit (no coding actually performed)</item>
652 * <item>chunked (for HTTP/1.1)</item>
653 * </list>
654 * To add new coding schemes to the parser, you need to ovrride
655 * this method to return a new coding context for your scheme
656 * when the info argument indicates that this is appropriate.
657 */
658 - (GSMimeCodingContext*) contextFor: (GSMimeHeader*)info
659 {
660 NSString *name;
661 NSString *value;
662
663 if (info == nil)
664 {
665 return AUTORELEASE([GSMimeCodingContext new]);
666 }
667
668 name = [info name];
669 if ([name isEqualToString: @"content-transfer-encoding"] == YES
670 || [name isEqualToString: @"transfer-encoding"] == YES)
671 {
672 value = [[info value] lowercaseString];
673 if ([value length] == 0)
674 {
675 NSLog(@"Bad value for %@ header - assume binary encoding", name);
676 return AUTORELEASE([GSMimeCodingContext new]);
677 }
678 if ([value isEqualToString: @"base64"] == YES)
679 {
680 return AUTORELEASE([GSMimeBase64DecoderContext new]);
681 }
682 else if ([value isEqualToString: @"quoted-printable"] == YES)
683 {
684 return AUTORELEASE([GSMimeQuotedDecoderContext new]);
685 }
686 else if ([value isEqualToString: @"binary"] == YES)
687 {
688 return AUTORELEASE([GSMimeCodingContext new]);
689 }
690 else if ([value characterAtIndex: 0] == '7')
691 {
692 return AUTORELEASE([GSMimeCodingContext new]);
693 }
694 else if ([value characterAtIndex: 0] == '8')
695 {
696 return AUTORELEASE([GSMimeCodingContext new]);
697 }
698 else if ([value isEqualToString: @"chunked"] == YES)
699 {
700 return AUTORELEASE([GSMimeChunkedDecoderContext new]);
701 }
702 }
703
704 NSLog(@"contextFor: - unknown header (%@) ... assumed binary encoding", name);
705 return AUTORELEASE([GSMimeCodingContext new]);
706 }
707
708 /**
709 * Return the data accumulated in the parser. If the parser is
710 * still parsing headers, this will be the header data read so far.
711 * If the parse has parsed the body of the message, this will be
712 * the data of the body, with any transfer encoding removed.
713 */
714 - (NSData*) data
715 {
716 return data;
717 }
718
719 - (void) dealloc
720 {
721 RELEASE(data);
722 RELEASE(child);
723 RELEASE(context);
724 RELEASE(boundary);
725 RELEASE(document);
726 [super dealloc];
727 }
728
729 /**
730 * <p>
731 * Decodes the raw data from the specified range in the source
732 * data object and appends it to the destination data object.
733 * The context object provides information about the content
734 * encoding type in use, and the state of the decoding operation.
735 * </p>
736 * <p>
737 * This method may be called repeatedly to incrementally decode
738 * information as it arrives on some communications channel.
739 * It should be called with a nil source data item (or with
740 * the atEnd flag of the context set to YES) in order to flush
741 * any information held in the context to the output data
742 * object.
743 * </p>
744 * <p>
745 * You may override this method in order to implement additional
746 * coding schemes, but usually it should be enough for you to
747 * implement a custom GSMimeCodingContext subclass fotr this method
748 * to use.
749 * </p>
750 */
751 - (BOOL) decodeData: (NSData*)sData
752 fromRange: (NSRange)aRange
753 intoData: (NSMutableData*)dData
754 withContext: (GSMimeCodingContext*)con
755 {
756 unsigned len = [sData length];
757 BOOL result = YES;
758
759 if (dData == nil || [con isKindOfClass: [GSMimeCodingContext class]] == NO)
760 {
761 [NSException raise: NSInvalidArgumentException
762 format: @"[%@ -%@] bad destination data for decode",
763 NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
764 }
765 GS_RANGE_CHECK(aRange, len);
766
767 /*
768 * Chunked decoding is relatively complex ... it makes sense to do it
769 * here, in order to make use of parser facilities, rather than having
770 * the decoding context do the work. In this case the context is used
771 * solely to store state information.
772 */
773 if ([con class] == [GSMimeChunkedDecoderContext class])
774 {
775 GSMimeChunkedDecoderContext *ctxt;
776 unsigned size = [dData length];
777 unsigned char *beg;
778 unsigned char *dst;
779 const char *src;
780 const char *end;
781 const char *footers;
782
783 ctxt = (GSMimeChunkedDecoderContext*)con;
784
785 /*
786 * Get pointers into source data buffer.
787 */
788 src = (const char *)[sData bytes];
789 footers = src;
790 src += aRange.location;
791 end = src + aRange.length;
792 beg = 0;
793 /*
794 * Make sure buffer is big enough, and set up output pointers.
795 */
796 [dData setLength: ctxt->size];
797 dst = (unsigned char*)[dData mutableBytes];
798 dst = dst + size;
799 beg = dst;
800
801 while ([ctxt atEnd] == NO && src < end)
802 {
803 switch (ctxt->state)
804 {
805 case ChunkSize:
806 if (isxdigit(*src) && ctxt->pos < sizeof(ctxt->buf))
807 {
808 ctxt->buf[ctxt->pos++] = *src;
809 }
810 else if (*src == ';')
811 {
812 ctxt->state = ChunkExt;
813 }
814 else if (*src == '\r')
815 {
816 ctxt->state = ChunkEol1;
817 }
818 else if (*src == '\n')
819 {
820 ctxt->state = ChunkData;
821 }
822 src++;
823 if (ctxt->state != ChunkSize)
824 {
825 unsigned int val = 0;
826 unsigned int index;
827
828 for (index = 0; index < ctxt->pos; index++)
829 {
830 val *= 16;
831 if (isdigit(ctxt->buf[index]))
832 {
833 val += ctxt->buf[index] - '0';
834 }
835 else if (isupper(ctxt->buf[index]))
836 {
837 val += ctxt->buf[index] - 'A' + 10;
838 }
839 else
840 {
841 val += ctxt->buf[index] - 'a' + 10;
842 }
843 }
844 ctxt->pos = val;
845 /*
846 * If we have read a chunk already, make sure that our
847 * destination size is updated correctly before growing
848 * the buffer for another chunk.
849 */
850 size += (dst - beg);
851 ctxt->size = size + val;
852 [dData setLength: ctxt->size];
853 dst = (unsigned char*)[dData mutableBytes];
854 dst += size;
855 beg = dst;
856 }
857 break;
858
859 case ChunkExt:
860 if (*src == '\r')
861 {
862 ctxt->state = ChunkEol1;
863 }
864 else if (*src == '\n')
865 {
866 ctxt->state = ChunkData;
867 }
868 src++;
869 break;
870
871 case ChunkEol1:
872 if (*src == '\n')
873 {
874 ctxt->state = ChunkData;
875 }
876 src++;
877 break;
878
879 case ChunkData:
880 /*
881 * If the pos is non-zero, we have a data chunk to read.
882 * otherwise, what we actually want is to read footers.
883 */
884 if (ctxt->pos > 0)
885 {
886 *dst++ = *src++;
887 if (--ctxt->pos == 0)
888 {
889 ctxt->state = ChunkEol2;
890 }
891 }
892 else
893 {
894 footers = src; // Record start position.
895 ctxt->state = ChunkFoot;
896 }
897 break;
898
899 case ChunkEol2:
900 if (*src == '\n')
901 {
902 ctxt->state = ChunkSize;
903 }
904 src++;
905 break;
906
907 case ChunkFoot:
908 if (*src == '\r')
909 {
910 src++;
911 }
912 else if (*src == '\n')
913 {
914 [ctxt setAtEnd: YES];
915 }
916 else
917 {
918 ctxt->state = ChunkFootA;
919 }
920 break;
921
922 case ChunkFootA:
923 if (*src == '\n')
924 {
925 ctxt->state = ChunkFootA;
926 }
927 src++;
928 break;
929 }
930 }
931 if (ctxt->state == ChunkFoot || ctxt->state == ChunkFootA)
932 {
933 [ctxt->data appendBytes: footers length: src - footers];
934 if ([ctxt atEnd] == YES)
935 {
936 NSMutableData *old;
937
938 /*
939 * Pretend we are back parsing the original headers ...
940 */
941 old = data;
942 data = ctxt->data;
943 bytes = (unsigned char*)[data mutableBytes];
944 dataEnd = [data length];
945 flags.inBody = 0;
946
947 /*
948 * Duplicate the normal header parsing process for our footers.
949 */
950 while (flags.inBody == 0)
951 {
952 if ([self _unfoldHeader] == NO)
953 {
954 break;
955 }
956 if (flags.inBody == 0)
957 {
958 NSString *header;
959
960 header = [self _decodeHeader];
961 if (header == nil)
962 {
963 break;
964 }
965 if ([self parseHeader: header] == NO)
966 {
967 flags.hadErrors = 1;
968 break;
969 }
970 }
971 }
972
973 /*
974 * restore original data.
975 */
976 ctxt->data = data;
977 data = old;
978 bytes = (unsigned char*)[data mutableBytes];
979 dataEnd = [data length];
980 flags.inBody = 1;
981 }
982 }
983 /*
984 * Correct size of output buffer.
985 */
986 [dData setLength: size + dst - beg];
987 }
988 else
989 {
990 result = [con decodeData: [sData bytes] + aRange.location
991 length: aRange.length
992 intoData: dData];
993 }
994
995 /*
996 * A nil data item as input represents end of data.
997 */
998 if (sData == nil)
999 {
1000 [con setAtEnd: YES];
1001 }
1002
1003 return result;
1004 }
1005
1006 - (NSString*) description
1007 {
1008 NSMutableString *desc;
1009
1010 desc = [NSMutableString stringWithFormat: @"GSMimeParser <%0x> -\n", self];
1011 [desc appendString: [document description]];
1012 return desc;
1013 }
1014
1015 /**
1016 * <deprecated />
1017 * Returns the object into which raw mime data is being parsed.
1018 */
1019 - (id) document
1020 {
1021 return document;
1022 }
1023
1024 /**
1025 * This method may be called to tell the parser that it should not expect
1026 * to parse any headers, and that the data it will receive is body data.<br />
1027 * If the parse is already in the body, or is complete, this method has
1028 * no effect.<br />
1029 * This is for use when some other utility has been used to parse headers,
1030 * and you have set the headers of the document owned by the parser
1031 * accordingly. You can then use the GSMimeParser to read the body data
1032 * into the document.
1033 */
1034 - (void) expectNoHeaders
1035 {
1036 if (flags.complete == 0)
1037 {
1038 flags.inBody = 1;
1039 }
1040 }
1041
1042 /**
1043 * Returns YES if the document parsing is known to be completed successfully.
1044 * Returns NO if either more data is needed, or if the parser encountered an
1045 * error.
1046 */
1047 - (BOOL) isComplete
1048 {
1049 if (flags.hadErrors == 1)
1050 {
1051 return NO;
1052 }
1053 return (flags.complete == 1) ? YES : NO;
1054 }
1055
1056 /**
1057 * Returns YES if the parser is parsing an HTTP document rather than
1058 * a true MIME document.
1059 */
1060 - (BOOL) isHttp
1061 {
1062 return (flags.isHttp == 1) ? YES : NO;
1063 }
1064
1065 /**
1066 * Returns YES if all the document headers have been parsed but
1067 * the document body parsing may not yet be complete.
1068 */
1069 - (BOOL) isInBody
1070 {
1071 return (flags.inBody == 1) ? YES : NO;
1072 }
1073
1074 /**
1075 * Returns YES if parsing of the document headers has not yet
1076 * been completed.
1077 */
1078 - (BOOL) isInHeaders
1079 {
1080 if (flags.inBody == 1)
1081 return NO;
1082 if (flags.complete == 1)
1083 return NO;
1084 return YES;
1085 }
1086
1087 - (id) init
1088 {
1089 self = [super init];
1090 if (self != nil)
1091 {
1092 data = [[NSMutableData alloc] init];
1093 document = [[GSMimeDocument alloc] init];
1094 }
1095 return self;
1096 }
1097
1098 /**
1099 * Returns the GSMimeDocument instance into which data is being parsed
1100 * or has been parsed.
1101 */
1102 - (GSMimeDocument*) mimeDocument
1103 {
1104 return document;
1105 }
1106
1107 /**
1108 * <p>
1109 * This method is called repeatedly to pass raw mime data into
1110 * the parser. It returns <code>YES</code> as long as it wants
1111 * more data to complete parsing of a document, and <code>NO</code>
1112 * if parsing is complete, either due to having reached the end of
1113 * a document or due to an error.
1114 * </p>
1115 * <p>
1116 * Since it is not always possible to determine if the end of a
1117 * MIME document has been reached from its content, the method
1118 * may need to be called with a nil or empty argument after you have
1119 * passed all the data to it ... this tells it that the data
1120 * is complete.
1121 * </p>
1122 * <p>
1123 * The parser attempts to be as flexible as possible and to continue
1124 * parsing wherever it can. If an error occurs in parsing, the
1125 * -isComplete method will always return NO, even after the -parse:
1126 * method has been called with a nil argument.
1127 * </p>
1128 * <p>
1129 * A multipart document will be parsed to content consisting of an
1130 * NSArray of GSMimeDocument instances representing each part.<br />
1131 * Otherwise, a document will become content of type NSData, unless
1132 * it is of content type <em>text</em>, in which case it will be an
1133 * NSString.<br />
1134 * If a document has no content type specified, it will be treated as
1135 * <em>text</em>, unless it is identifiable as a <em>file</em>
1136 * (eg. t has a content-disposition header containing a filename parameter).
1137 * </p>
1138 */
1139 - (BOOL) parse: (NSData*)d
1140 {
1141 unsigned l = [d length];
1142
1143 if (flags.complete == 1)
1144 {
1145 return NO; /* Already completely parsed! */
1146 }
1147 if (l > 0)
1148 {
1149 NSDebugMLLog(@"GSMime", @"Parse %u bytes - '%*.*s'", l, l, l, [d bytes]);
1150 if (flags.inBody == 0)
1151 {
1152 [data appendBytes: [d bytes] length: [d length]];
1153 bytes = (unsigned char*)[data mutableBytes];
1154 dataEnd = [data length];
1155
1156 while (flags.inBody == 0)
1157 {
1158 if ([self _unfoldHeader] == NO)
1159 {
1160 return YES; /* Needs more data to fill line. */
1161 }
1162 if (flags.inBody == 0)
1163 {
1164 NSString *header;
1165
1166 header = [self _decodeHeader];
1167 if (header == nil)
1168 {
1169 return NO; /* Couldn't handle words. */
1170 }
1171 if ([self parseHeader: header] == NO)
1172 {
1173 flags.hadErrors = 1;
1174 return NO; /* Header not parsed properly. */
1175 }
1176 }
1177 else
1178 {
1179 NSDebugMLLog(@"GSMime", @"Parsed end of headers", "");
1180 }
1181 }
1182 /*
1183 * All headers have been parsed, so we empty our internal buffer
1184 * (which we will now use to store decoded data) and place unused
1185 * information back in the incoming data object to act as input.
1186 */
1187 d = AUTORELEASE([data copy]);
1188 [data setLength: 0];
1189
1190 /*
1191 * If we have finished parsing the headers, we may have http
1192 * continuation header(s), in which case, we must start parsing
1193 * headers again.
1194 */
1195 if (flags.inBody == 1)
1196 {
1197 NSDictionary *info;
1198
1199 info = [[document headersNamed: @"http"] lastObject];
1200 if (info != nil)
1201 {
1202 NSString *val;
1203
1204 val = [info objectForKey: NSHTTPPropertyStatusCodeKey];
1205 if (val != nil)
1206 {
1207 int v = [val intValue];
1208
1209 if (v >= 100 && v < 200)
1210 {
1211 /*
1212 * This is an intermediary response ... so we have
1213 * to restart the parsing operation!
1214 */
1215 NSDebugMLLog(@"GSMime", @"Parsed http continuation", "");
1216 flags.inBody = 0;
1217 }
1218 }
1219 }
1220 }
1221 }
1222
1223 if ([d length] > 0)
1224 {
1225 if (flags.inBody == 1)
1226 {
1227 /*
1228 * We can't just re-call -parse: ...
1229 * that would lead to recursion.
1230 */
1231 return [self _decodeBody: d];
1232 }
1233 else
1234 {
1235 return [self parse: d];
1236 }
1237 }
1238
1239 return YES; /* Want more data for body */
1240 }
1241 else
1242 {
1243 BOOL result;
1244
1245 if (flags.inBody == 1)
1246 {
1247 result = [self _decodeBody: d];
1248 }
1249 else
1250 {
1251 /*
1252 * If still parsing headers, add CR-LF sequences to terminate
1253 * the headers.
1254 */
1255 result = [self parse: [NSData dataWithBytes: @"\r\n\r\n" length: 4]];
1256 }
1257 flags.inBody = 0;
1258 flags.complete = 1; /* Finished parsing */
1259 return result;
1260 }
1261 }
1262
1263 /**
1264 * <p>
1265 * This method is called to parse a header line <em>for the
1266 * current document</em>, split its contents into a GSMimeHeader
1267 * object, and add that information to the document.<br />
1268 * The method is normally used internally by the -parse: method,
1269 * but you may also call it to parse an entire header line and
1270 * add it to the document (this may be useful in conjunction
1271 * with the -expectNoHeaders method, to parse a document body data
1272 * into a document where the headers are available from a
1273 * separate source).
1274 * </p>
1275 * <example>
1276 * GSMimeParser *parser = [GSMimeParser mimeParser];
1277 *
1278 * [parser parseHeader: @"content-type: text/plain"];
1279 * [parser expectNoHeaders];
1280 * [parser parse: bodyData];
1281 * [parser parse: nil];
1282 * </example>
1283 * <p>
1284 * The standard implementation of this method scans the header
1285 * name and then calls -scanHeaderBody:into: to complete the
1286 * parsing of the header.
1287 * </p>
1288 * <p>
1289 * This method also performs consistency checks on headers scanned
1290 * so it is recommended that it is not overridden, but that
1291 * subclasses override -scanHeaderBody:into: to
1292 * implement custom scanning.
1293 * </p>
1294 * <p>
1295 * As a special case, for HTTP support, this method also parses
1296 * lines in the format of HTTP responses as if they were headers
1297 * named <code>http</code>. The resulting header object contains
1298 * additional object values -
1299 * </p>
1300 * <deflist>
1301 * <term>HttpMajorVersion</term>
1302 * <desc>The first part of the version number</desc>
1303 * <term>HttpMinorVersion</term>
1304 * <desc>The second part of the version number</desc>
1305 * <term>NSHTTPPropertyServerHTTPVersionKey</term>
1306 * <desc>The full HTTP protocol version number</desc>
1307 * <term>NSHTTPPropertyStatusCodeKey</term>
1308 * <desc>The HTTP status code</desc>
1309 * <term>NSHTTPPropertyStatusReasonKey</term>
1310 * <desc>The text message (if any) after the status code</desc>
1311 * </deflist>
1312 */
1313 - (BOOL) parseHeader: (NSString*)aHeader
1314 {
1315 NSScanner *scanner = [NSScanner scannerWithString: aHeader];
1316 NSString *name;
1317 NSString *value;
1318 GSMimeHeader *info;
1319
1320 NSDebugMLLog(@"GSMime", @"Parse header - '%@'", aHeader);
1321 info = AUTORELEASE([GSMimeHeader new]);
1322
1323 /*
1324 * Special case - permit web response status line to act like a header.
1325 */
1326 if ([scanner scanString: @"HTTP" intoString: &name] == NO
1327 || [scanner scanString: @"/" intoString: 0] == NO)
1328 {
1329 if ([scanner scanUpToString: @":" intoString: &name] == NO)
1330 {
1331 NSLog(@"Not a valid header (%@)", [scanner string]);
1332 return NO;
1333 }
1334 /*
1335 * Position scanner after colon and any white space.
1336 */
1337 if ([scanner scanString: @":" intoString: 0] == NO)
1338 {
1339 NSLog(@"No colon terminating name in header (%@)", [scanner string]);
1340 return NO;
1341 }
1342 }
1343
1344 /*
1345 * Set the header name.
1346 */
1347 [info setName: name];
1348 name = [info name];
1349
1350 /*
1351 * Break header fields out into info dictionary.
1352 */
1353 if ([self scanHeaderBody: scanner into: info] == NO)
1354 {
1355 return NO;
1356 }
1357
1358 /*
1359 * Check validity of broken-out header fields.
1360 */
1361 if ([name isEqualToString: @"mime-version"] == YES)
1362 {
1363 int majv = 0;
1364 int minv = 0;
1365
1366 value = [info value];
1367 if ([value length] == 0)
1368 {
1369 NSLog(@"Missing value for mime-version header");
1370 return NO;
1371 }
1372 if (sscanf([value lossyCString], "%d.%d", &majv, &minv) != 2)
1373 {
1374 NSLog(@"Bad value for mime-version header (%@)", value);
1375 return NO;
1376 }
1377 [document deleteHeaderNamed: name]; // Should be unique
1378 }
1379 else if ([name isEqualToString: @"content-type"] == YES)
1380 {
1381 NSString *tmp = [info parameterForKey: @"boundary"];
1382 NSString *type;
1383 NSString *subtype;
1384 BOOL supported = NO;
1385
1386 DESTROY(boundary);
1387 if (tmp != nil)
1388 {
1389 unsigned int l = [tmp cStringLength] + 2;
1390 unsigned char *b = NSZoneMalloc(NSDefaultMallocZone(), l + 1);
1391
1392 b[0] = '-';
1393 b[1] = '-';
1394 [tmp getCString: &b[2]];
1395 boundary = [[NSData alloc] initWithBytesNoCopy: b length: l];
1396 }
1397
1398 type = [info objectForKey: @"Type"];
1399 if ([type length] == 0)
1400 {
1401 NSLog(@"Missing Mime content-type");
1402 return NO;
1403 }
1404 subtype = [info objectForKey: @"Subtype"];
1405
1406 if ([type isEqualToString: @"text"] == YES)
1407 {
1408 if (subtype == nil)
1409 {
1410 subtype = @"plain";
1411 }
1412 }
1413 else if ([type isEqualToString: @"multipart"] == YES)
1414 {
1415 if (subtype == nil)
1416 {
1417 subtype = @"mixed";
1418 }
1419 supported = YES;
1420 if (boundary == nil)
1421 {
1422 NSLog(@"multipart message without boundary");
1423 return NO;
1424 }
1425 }
1426 else
1427 {
1428 if (subtype == nil)
1429 {
1430 subtype = @"octet-stream";
1431 }
1432 }
1433
1434 [document deleteHeaderNamed: name]; // Should be unique
1435 }
1436
1437 NS_DURING
1438 [document addHeader: info];
1439 NS_HANDLER
1440 return NO;
1441 NS_ENDHANDLER
1442 NSDebugMLLog(@"GSMime", @"Header parsed - %@", info);
1443
1444 return YES;
1445 }
1446
1447 /**
1448 * <p>
1449 * This method is called to parse a header line and split its
1450 * contents into an info dictionary.
1451 * </p>
1452 * <p>
1453 * On entry, the dictionary is already partially filled,
1454 * the name argument is a lowercase representation of the
1455 * header name, and the scanner is set to a scan location
1456 * immediately after the colon in the header string.
1457 * </p>
1458 * <p>
1459 * If the header is parsed successfully, the method should
1460 * return YES, otherwise NO.
1461 * </p>
1462 * <p>
1463 * You should not call this method directly yourself, but may
1464 * override it to support parsing of new headers.
1465 * </p>
1466 * <p>
1467 * You should be aware of the parsing that the standard
1468 * implementation performs, and that <em>needs</em> to be
1469 * done for certain headers in order to permit the parser to
1470 * work generally -
1471 * </p>
1472 * <deflist>
1473 * <term>content-disposition</term>
1474 * <desc>
1475 * <deflist>
1476 * <term>Value</term>
1477 * <desc>
1478 * The content disposition (excluding parameters) as a
1479 * lowercase string.
1480 * </desc>
1481 * </deflist>
1482 * </desc>
1483 * <term>content-type</term>
1484 * <desc>
1485 * <deflist>
1486 * <term>Subtype</term>
1487 * <desc>The MIME subtype lowercase</desc>
1488 * <term>Type</term>
1489 * <desc>The MIME type lowercase</desc>
1490 * <term>value</term>
1491 * <desc>The full MIME type (xxx/yyy) in lowercase</desc>
1492 * </deflist>
1493 * </desc>
1494 * <term>content-transfer-encoding</term>
1495 * <desc>
1496 * <deflist>
1497 * <term>Value</term>
1498 * <desc>The transfer encoding type in lowercase</desc>
1499 * </deflist>
1500 * </desc>
1501 * <term>http</term>
1502 * <desc>
1503 * <deflist>
1504 * <term>HttpVersion</term>
1505 * <desc>The HTTP protocol version number</desc>
1506 * <term>HttpMajorVersion</term>
1507 * <desc>The first component of the version number</desc>
1508 * <term>HttpMinorVersion</term>
1509 * <desc>The second component of the version number</desc>
1510 * <term>HttpStatus</term>
1511 * <desc>The response status value (numeric code)</desc>
1512 * <term>Value</term>
1513 * <desc>The text message (if any)</desc>
1514 * </deflist>
1515 * </desc>
1516 * <term>transfer-encoding</term>
1517 * <desc>
1518 * <deflist>
1519 * <term>Value</term>
1520 * <desc>The transfer encoding type in lowercase</desc>
1521 * </deflist>
1522 * </desc>
1523 * </deflist>
1524 */
1525 - (BOOL) scanHeaderBody: (NSScanner*)scanner
1526 into: (GSMimeHeader*)info
1527 {
1528 NSString *name = [info name];
1529 NSString *value = nil;
1530
1531 [self scanPastSpace: scanner];
1532
1533 /*
1534 * Now see if we are interested in any of it.
1535 */
1536 if ([name isEqualToString: @"http"] == YES)
1537 {
1538 int loc = [scanner scanLocation];
1539 int major;
1540 int minor;
1541 int status;
1542 unsigned count;
1543 NSArray *hdrs;
1544
1545 if ([scanner scanInt: &major] == NO || major < 0)
1546 {
1547 NSLog(@"Bad value for http major version");
1548 return NO;
1549 }
1550 if ([scanner scanString: @"." intoString: 0] == NO)
1551 {
1552 NSLog(@"Bad format for http version");
1553 return NO;
1554 }
1555 if ([scanner scanInt: &minor] == NO || minor < 0)
1556 {
1557 NSLog(@"Bad value for http minor version");
1558 return NO;
1559 }
1560 if ([scanner scanInt: &status] == NO || status < 0)
1561 {
1562 NSLog(@"Bad value for http status");
1563 return NO;
1564 }
1565 [info setObject: [NSString stringWithFormat: @"%d", minor]
1566 forKey: @"HttpMinorVersion"];
1567 [info setObject: [NSString stringWithFormat: @"%d.%d", major, minor]
1568 forKey: @"HttpVersion"];
1569 [info setObject: [NSString stringWithFormat: @"%d", major]
1570 forKey: NSHTTPPropertyServerHTTPVersionKey];
1571 [info setObject: [NSString stringWithFormat: @"%d", status]
1572 forKey: NSHTTPPropertyStatusCodeKey];
1573 [self scanPastSpace: scanner];
1574 value = [[scanner string] substringFromIndex: [scanner scanLocation]];
1575 [info setObject: value
1576 forKey: NSHTTPPropertyStatusReasonKey];
1577 value = [[scanner string] substringFromIndex: loc];
1578 /*
1579 * Get rid of preceeding headers in case this is a continuation.
1580 */
1581 hdrs = [document allHeaders];
1582 for (count = 0; count < [hdrs count]; count++)
1583 {
1584 GSMimeHeader *h = [hdrs objectAtIndex: count];
1585
1586 [document deleteHeader: h];
1587 }
1588 /*
1589 * Mark to say we are parsing HTTP content
1590 */
1591 [self setIsHttp];
1592 }
1593 else if ([name isEqualToString: @"content-transfer-encoding"] == YES
1594 || [name isEqualToString: @"transfer-encoding"] == YES)
1595 {
1596 value = [self scanToken: scanner];
1597 if ([value length] == 0)
1598 {
1599 NSLog(@"Bad value for content-transfer-encoding header");
1600 return NO;
1601 }
1602 value = [value lowercaseString];
1603 }
1604 else if ([name isEqualToString: @"content-type"] == YES)
1605 {
1606 NSString *type;
1607 NSString *subtype = nil;
1608
1609 type = [self scanName: scanner];
1610 if ([type length] == 0)
1611 {
1612 NSLog(@"Invalid Mime content-type");
1613 return NO;
1614 }
1615 type = [type lowercaseString];
1616 [info setObject: type forKey: @"Type"];
1617 if ([scanner scanString: @"/" intoString: 0] == YES)
1618 {
1619 subtype = [self scanName: scanner];
1620 if ([subtype length] == 0)
1621 {
1622 NSLog(@"Invalid Mime content-type (subtype)");
1623 return NO;
1624 }
1625 subtype = [subtype lowercaseString];
1626 [info setObject: subtype forKey: @"Subtype"];
1627 value = [NSString stringWithFormat: @"%@/%@", type, subtype];
1628 }
1629 else
1630 {
1631 value = type;
1632 }
1633
1634 [self _scanHeaderParameters: scanner into: info];
1635 }
1636 else if ([name isEqualToString: @"content-disposition"] == YES)
1637 {
1638 value = [self scanName: scanner];
1639 value = [value lowercaseString];
1640 /*
1641 * Concatenate slash separated parts of field.
1642 */
1643 while ([scanner scanString: @"/" intoString: 0] == YES)
1644 {
1645 NSString *sub = [self scanName: scanner];
1646
1647 if ([sub length] > 0)
1648 {
1649 sub = [sub lowercaseString];
1650 value = [NSString stringWithFormat: @"%@/%@", value, sub];
1651 }
1652 }
1653
1654 /*
1655 * Expect anything else to be 'name=value' parameters.
1656 */
1657 [self _scanHeaderParameters: scanner into: info];
1658 }
1659 else
1660 {
1661 int loc;
1662
1663 [self scanPastSpace: scanner];
1664 loc = [scanner scanLocation];
1665 value = [[scanner string] substringFromIndex: loc];
1666 }
1667
1668 if (value != nil)
1669 {
1670 [info setValue: value];
1671 }
1672
1673 return YES;
1674 }
1675
1676 /**
1677 * A convenience method to use a scanner (that is set up to scan a
1678 * header line) to scan a name - a simple word.
1679 * <list>
1680 * <item>Leading whitespace is ignored.</item>
1681 * </list>
1682 */
1683 - (NSString*) scanName: (NSScanner*)scanner
1684 {
1685 NSString *value;
1686
1687 [self scanPastSpace: scanner];
1688
1689 /*
1690 * Scan value terminated by any MIME special character.
1691 */
1692 if ([scanner scanUpToCharactersFromSet: rfc2045Specials
1693 intoString: &value] == NO)
1694 {
1695 return nil;
1696 }
1697 return value;
1698 }
1699
1700 /**
1701 * A convenience method to scan past any whitespace in the scanner
1702 * in preparation for scanning something more interesting that
1703 * comes after it. Returns YES if any space was read, NO otherwise.
1704 */
1705 - (BOOL) scanPastSpace: (NSScanner*)scanner
1706 {
1707 NSCharacterSet *skip;
1708 BOOL scanned;
1709
1710 skip = RETAIN([scanner charactersToBeSkipped]);
1711 [scanner setCharactersToBeSkipped: nil];
1712 scanned = [scanner scanCharactersFromSet: whitespace intoString: 0];
1713 [scanner setCharactersToBeSkipped: skip];
1714 RELEASE(skip);
1715 return scanned;
1716 }
1717
1718 /**
1719 * A convenience method to use a scanner (that is set up to scan a
1720 * header line) to scan in a special character that terminated a
1721 * token previously scanned. If the token was terminated by
1722 * whitespace and no other special character, the string returned
1723 * will contain a single space character.
1724 */
1725 - (NSString*) scanSpecial: (NSScanner*)scanner
1726 {
1727 NSCharacterSet *specials;
1728 unsigned location;
1729 unichar c;
1730
1731 [self scanPastSpace: scanner];
1732
1733 if (flags.isHttp == 1)
1734 {
1735 specials = rfc822Specials;
1736 }
1737 else
1738 {
1739 specials = rfc2045Specials;
1740 }
1741 /*
1742 * Now return token delimiter (may be whitespace)
1743 */
1744 location = [scanner scanLocation];
1745 c = [[scanner string] characterAtIndex: location];
1746
1747 if ([specials characterIsMember: c] == YES)
1748 {
1749 [scanner setScanLocation: location + 1];
1750 return [NSString stringWithCharacters: &c length: 1];
1751 }
1752 else
1753 {
1754 return @" ";
1755 }
1756 }
1757
1758 /**
1759 * A convenience method to use a scanner (that is set up to scan a
1760 * header line) to scan a header token - either a quoted string or
1761 * a simple word.
1762 * <list>
1763 * <item>Leading whitespace is ignored.</item>
1764 * <item>Backslash escapes in quoted text are converted</item>
1765 * </list>
1766 */
1767 - (NSString*) scanToken: (NSScanner*)scanner
1768 {
1769 [self scanPastSpace: scanner];
1770 if ([scanner scanString: @"\"" intoString: 0] == YES) // Quoted
1771 {
1772 NSString *string = [scanner string];
1773 unsigned length = [string length];
1774 unsigned start = [scanner scanLocation];
1775 NSRange r = NSMakeRange(start, length - start);
1776 BOOL done = NO;
1777
1778 while (done == NO)
1779 {
1780 r = [string rangeOfString: @"\""
1781 options: NSLiteralSearch
1782 range: r];
1783 if (r.length == 0)
1784 {
1785 NSLog(@"Parsing header value - found unterminated quoted string");
1786 return nil;
1787 }
1788 if ([string characterAtIndex: r.location - 1] == '\\')
1789 {
1790 int p;
1791
1792 /*
1793 * Count number of escape ('\') characters ... if it's odd
1794 * then the quote has been escaped and is not a closing
1795 * quote.
1796 */
1797 p = r.location;
1798 while (p > 0 && [string characterAtIndex: p - 1] == '\\')
1799 {
1800 p--;
1801 }
1802 p = r.location - p;
1803 if (p % 2 == 1)
1804 {
1805 r.location++;
1806 r.length = length - r.location;
1807 }
1808 else
1809 {
1810 done = YES;
1811 }
1812 }
1813 else
1814 {
1815 done = YES;
1816 }
1817 }
1818 [scanner setScanLocation: r.location + 1];
1819 length = r.location - start;
1820 if (length == 0)
1821 {
1822 return nil;
1823 }
1824 else
1825 {
1826 unichar buf[length];
1827 unichar *src = buf;
1828 unichar *dst = buf;
1829
1830 [string getCharacters: buf range: NSMakeRange(start, length)];
1831 while (src < &buf[length])
1832 {
1833 if (*src == '\\')
1834 {
1835 src++;
1836 if (flags.buggyQuotes == 1 && *src != '\\' && *src != '"')
1837 {
1838 *dst++ = '\\'; // Buggy use of escape in quotes.
1839 }
1840 }
1841 *dst++ = *src++;
1842 }
1843 return [NSString stringWithCharacters: buf length: dst - buf];
1844 }
1845 }
1846 else // Token
1847 {
1848 NSCharacterSet *specials;
1849 NSString *value;
1850
1851 if (flags.isHttp == 1)
1852 {
1853 specials = rfc822Specials;
1854 }
1855 else
1856 {
1857 specials = rfc2045Specials;
1858 }
1859
1860 /*
1861 * Move past white space.
1862 */
1863 [self scanPastSpace: scanner];
1864
1865 /*
1866 * Scan value terminated by any special character.
1867 */
1868 if ([scanner scanUpToCharactersFromSet: specials
1869 intoString: &value] == NO)
1870 {
1871 return nil;
1872 }
1873 return value;
1874 }
1875 }
1876
1877 /**
1878 * Method to inform the parser that the data it is parsing is likely to
1879 * contain fields with buggy use of backslash quotes ... and it should
1880 * try to be tolerant of them and treat them as is they were escaped
1881 * backslashes. This is for use with things like microsoft internet
1882 * explorer, which puts the backslashes used as file path separators
1883 * in parameters without quoting them.
1884 */
1885 - (void) setBuggyQuotes: (BOOL)flag
1886 {
1887 if (flag)
1888 {
1889 flags.buggyQuotes = 1;
1890 }
1891 else
1892 {
1893 flags.buggyQuotes = 0;
1894 }
1895 }
1896
1897 /**
1898 * Method to inform the parser that the data it is parsing is an HTTP
1899 * document rather than true MIME. This method is called internally
1900 * if the parser detects an HTTP response line at the start of the
1901 * headers it is parsing.
1902 */
1903 - (void) setIsHttp
1904 {
1905 flags.isHttp = 1;
1906 }
1907 @end
1908
1909 @implementation GSMimeParser (Private)
1910 /*
1911 * This method takes the raw data of an unfolded header line, and handles
1912 * Method to inform the parser that the data it is parsing is an HTTP
1913 * document rather than true MIME. This method is called internally
1914 * if the parser detects an HTTP response line at the start of the
1915 * headers it is parsing.
1916 * RFC2047 word encoding in the header is handled by creating a
1917 * string containing the decoded words.
1918 */
1919 - (NSString*) _decodeHeader
1920 {
1921 NSStringEncoding enc;
1922 NSString *charset;
1923 WE encoding;
1924 unsigned char c;
1925 unsigned char *src, *dst, *beg;
1926 NSMutableString *hdr = [NSMutableString string];
1927 CREATE_AUTORELEASE_POOL(arp);
1928
1929 /*
1930 * Remove any leading or trailing space - there shouldn't be any.
1931 */
1932 while (lineStart < lineEnd && isspace(bytes[lineStart]))
1933 {
1934 lineStart++;
1935 }
1936 while (lineEnd > lineStart && isspace(bytes[lineEnd-1]))
1937 {
1938 lineEnd--;
1939 }
1940
1941 /*
1942 * Perform quoted text substitution.
1943 */
1944 bytes[lineEnd] = '\0';
1945 dst = src = beg = &bytes[lineStart];
1946 while (*src != 0)
1947 {
1948 if ((src[0] == '=') && (src[1] == '?'))
1949 {
1950 unsigned char *tmp;
1951
1952 if (dst > beg)
1953 {
1954 NSData *d = [NSData dataWithBytes: beg length: dst - beg];
1955 NSString *s;
1956
1957 s = [[NSString alloc] initWithData: d
1958 encoding: NSASCIIStringEncoding];
1959 [hdr appendString: s];
1960 RELEASE(s);
1961 dst = beg;
1962 }
1963
1964 if (src[3] == '\0')
1965 {
1966 dst[0] = '=';
1967 dst[1] = '?';
1968 dst[2] = '\0';
1969 NSLog(@"Bad encoded word - character set missing");
1970 break;
1971 }
1972
1973 src += 2;
1974 tmp = src;
1975 src = (unsigned char*)strchr((char*)src, '?');
1976 if (src == 0)
1977 {
1978 NSLog(@"Bad encoded word - character set terminator missing");
1979 break;
1980 }
1981 *src = '\0';
1982 charset = [NSString stringWithCString: tmp];
1983 enc = [GSMimeDocument encodingFromCharset: charset];
1984 src++;
1985 if (*src == 0)
1986 {
1987 NSLog(@"Bad encoded word - content type missing");
1988 break;
1989 }
1990 c = tolower(*src);
1991 if (c == 'b')
1992 {
1993 encoding = WE_BASE64;
1994 }
1995 else if (c == 'q')
1996 {
1997 encoding = WE_QUOTED;
1998 }
1999 else
2000 {
2001 NSLog(@"Bad encoded word - content type unknown");
2002 break;
2003 }
2004 src = (unsigned char*)strchr((char*)src, '?');
2005 if (src == 0)
2006 {
2007 NSLog(@"Bad encoded word - content type terminator missing");
2008 break;
2009 }
2010 src++;
2011 if (*src == 0)
2012 {
2013 NSLog(@"Bad encoded word - data missing");
2014 break;
2015 }
2016 tmp = (unsigned char*)strchr((char*)src, '?');
2017 if (tmp == 0)
2018 {
2019 NSLog(@"Bad encoded word - data terminator missing");
2020 break;
2021 }
2022 dst = decodeWord(dst, src, tmp, encoding);
2023 tmp++;
2024 if (*tmp != '=')
2025 {
2026 NSLog(@"Bad encoded word - encoded word terminator missing");
2027 break;
2028 }
2029 src = tmp;
2030 if (dst > beg)
2031 {
2032 NSData *d = [NSData dataWithBytes: beg length: dst - beg];
2033 NSString *s;
2034
2035 s = [[NSString alloc] initWithData: d encoding: enc];
2036 [hdr appendString: s];
2037 RELEASE(s);
2038 dst = beg;
2039 }
2040 }
2041 else
2042 {
2043 *dst++ = *src;
2044 }
2045 src++;
2046 }
2047 if (dst > beg)
2048 {
2049 NSData *d = [NSData dataWithBytes: beg length: dst - beg];
2050 NSString *s;
2051
2052 s = [[NSString alloc] initWithData: d
2053 encoding: NSASCIIStringEncoding];
2054 [hdr appendString: s];
2055 RELEASE(s);
2056 dst = beg;
2057 }
2058 RELEASE(arp);
2059 return hdr;
2060 }
2061
2062 - (BOOL) _decodeBody: (NSData*)d
2063 {
2064 unsigned l = [d length];
2065 BOOL result = NO;
2066
2067 rawBodyLength += l;
2068
2069 if (context == nil)
2070 {
2071 GSMimeHeader *hdr;
2072
2073 expect = 0;
2074 /*
2075 * Check for expected content length.
2076 */
2077 hdr = [document headerNamed: @"content-length"];
2078 if (hdr != nil)
2079 {
2080 expect = [[hdr value] intValue];
2081 }
2082
2083 /*
2084 * Set up context for decoding data.
2085 */
2086 hdr = [document headerNamed: @"transfer-encoding"];
2087 if (hdr == nil)
2088 {
2089 hdr = [document headerNamed: @"content-transfer-encoding"];
2090 }
2091 else if ([[[hdr value] lowercaseString] isEqualToString: @"chunked"])
2092 {
2093 /*
2094 * Chunked transfer encoding overrides any content length spec.
2095 */
2096 expect = 0;
2097 }
2098 context = [self contextFor: hdr];
2099 RETAIN(context);
2100 NSDebugMLLog(@"GSMime", @"Parse body expects %u bytes", expect);
2101 }
2102
2103 NSDebugMLLog(@"GSMime", @"Parse %u bytes - '%*.*s'", l, l, l, [d bytes]);
2104 // NSDebugMLLog(@"GSMime", @"Boundary - '%*.*s'", [boundary length], [boundary length], [boundary bytes]);
2105
2106 if ([context atEnd] == YES)
2107 {
2108 flags.inBody = 0;
2109 flags.complete = 1;
2110 if ([d length] > 0)
2111 {
2112 NSLog(@"Additional data (%*.*s) ignored after parse complete",
2113 [d length], [d length], [d bytes]);
2114 }
2115 result = YES; /* Nothing more to do */
2116 }
2117 else if (boundary == nil)
2118 {
2119 GSMimeHeader *typeInfo;
2120 NSString *type;
2121
2122 typeInfo = [document headerNamed: @"content-type"];
2123 type = [typeInfo objectForKey: @"Type"];
2124 if ([type isEqualToString: @"multipart"] == YES)
2125 {
2126 NSLog(@"multipart decode attempt without boundary");
2127 flags.inBody = 0;
2128 flags.complete = 1;
2129 result = NO;
2130 }
2131 else
2132 {
2133 [self decodeData: d
2134 fromRange: NSMakeRange(0, [d length])
2135 intoData: data
2136 withContext: context];
2137
2138 if ([context atEnd] == YES
2139 || (expect > 0 && rawBodyLength >= expect))
2140 {
2141 flags.inBody = 0;
2142 flags.complete = 1;
2143
2144 NSDebugMLLog(@"GSMime", @"Parse body complete", "");
2145 /*
2146 * If no content type is supplied, we assume text ... unless
2147 * we have something that's known to be a file.
2148 */
2149 if (type == nil)
2150 {
2151 if ([document contentFile] != nil)
2152 {
2153 type = @"application";
2154 }
2155 else
2156 {
2157 type = @"text";
2158 }
2159 }
2160
2161 if ([type isEqualToString: @"text"] == YES)
2162 {
2163 NSString *charset;
2164 NSStringEncoding stringEncoding;
2165 NSString *string;
2166
2167 /*
2168 * Assume that content type is best represented as NSString.
2169 */
2170 charset = [typeInfo parameterForKey: @"charset"];
2171 stringEncoding
2172 = [GSMimeDocument encodingFromCharset: charset];
2173 string = [[NSString alloc] initWithData: data
2174 encoding: stringEncoding];
2175 [document setContent: string];
2176 RELEASE(string);
2177 }
2178 else
2179 {
2180 /*
2181 * Assume that any non-text content type is best
2182 * represented as NSData.
2183 */
2184 [document setContent: data];
2185 }
2186 }
2187 result = YES;
2188 }
2189 }
2190 else
2191 {
2192 unsigned int bLength = [boundary length];
2193 unsigned char *bBytes = (unsigned char*)[boundary bytes];
2194 unsigned char bInit = bBytes[0];
2195 BOOL done = NO;
2196 BOOL endedFinalPart = NO;
2197
2198 [data appendBytes: [d bytes] length: [d length]];
2199 bytes = (unsigned char*)[data mutableBytes];
2200 dataEnd = [data length];
2201
2202 while (done == NO)
2203 {
2204 /*
2205 * Search our data for the next boundary.
2206 */
2207 while (dataEnd - lineStart >= bLength)
2208 {
2209 if (bytes[lineStart] == bInit
2210 && memcmp(&bytes[lineStart], bBytes, bLength) == 0)
2211 {
2212 if (lineStart == 0 || bytes[lineStart-1] == '\r'
2213 || bytes[lineStart-1] == '\n')
2214 {
2215 lineEnd = lineStart + bLength;
2216 if (lineEnd + 2 < dataEnd && bytes[lineEnd] == '-'
2217 && bytes[lineEnd+1] == '-')
2218 {
2219 endedFinalPart = YES;
2220 }
2221 break;
2222 }
2223 }
2224 lineStart++;
2225 }
2226 if (dataEnd - lineStart < bLength)
2227 {
2228 done = YES; /* Needs more data. */
2229 }
2230 else if (child == nil)
2231 {
2232 /*
2233 * Found boundary at the start of the first section.
2234 * Set sectionStart to point immediately after boundary.
2235 */
2236 lineStart += bLength;
2237 sectionStart = lineStart;
2238 child = [GSMimeParser new];
2239 if (flags.buggyQuotes == 1)
2240 {
2241 [child setBuggyQuotes: YES];
2242 }
2243 }
2244 else
2245 {
2246 NSData *d;
2247 unsigned pos;
2248
2249 /*
2250 * Found boundary at the end of a section.
2251 * Skip past line terminator for boundary at start of section
2252 * or past marker for end of multipart document.
2253 */
2254 if (bytes[sectionStart] == '-' && sectionStart < dataEnd
2255 && bytes[sectionStart+1] == '-')
2256 {
2257 sectionStart += 2;
2258 }
2259 if (bytes[sectionStart] == '\r')
2260 {
2261 sectionStart++;
2262 }
2263 if (bytes[sectionStart] == '\n')
2264 {
2265 sectionStart++;
2266 }
2267
2268 /*
2269 * Create data object for this section and pass it to the
2270 * child parser to deal with. NB. As lineStart points to
2271 * the start of the end boundary, we need to step back to
2272 * before the end of line introducing it in order to have
2273 * the correct length of body data for the child document.
2274 */
2275 pos = lineStart;
2276 if (pos > 0 && bytes[pos-1] == '\n')
2277 {
2278 pos--;
2279 }
2280 if (pos > 0 && bytes[pos-1] == '\r')
2281 {
2282 pos--;
2283 }
2284 d = [NSData dataWithBytes: &bytes[sectionStart]
2285 length: pos - sectionStart];
2286 if ([child parse: d] == YES)
2287 {
2288 /*
2289 * The parser wants more data, so pass a nil data item
2290 * to tell it that it has had all there is.
2291 */
2292 [child parse: nil];
2293 }
2294 if ([child isComplete] == YES)
2295 {
2296 GSMimeDocument *doc;
2297
2298 /*
2299 * Store the document produced by the child, and
2300 * create a new parser for the next section.
2301 */
2302 doc = [child mimeDocument];
2303 if (doc != nil)
2304 {
2305 [document addContent: doc];
2306 }
2307 RELEASE(child);
2308 child = [GSMimeParser new];
2309 if (flags.buggyQuotes == 1)
2310 {
2311 [child setBuggyQuotes: YES];
2312 }
2313 }
2314 else
2315 {
2316 /*
2317 * Section failed to decode properly!
2318 */
2319 NSLog(@"Failed to decode section of multipart");
2320 RELEASE(child);
2321 child = [GSMimeParser new];
2322 if (flags.buggyQuotes == 1)
2323 {
2324 [child setBuggyQuotes: YES];
2325 }
2326 }
2327
2328 /*
2329 * Update parser data.
2330 */
2331 lineStart += bLength;
2332 sectionStart = lineStart;
2333 memcpy(bytes, &bytes[sectionStart], dataEnd - sectionStart);
2334 dataEnd -= sectionStart;
2335 [data setLength: dataEnd];
2336 bytes = (unsigned char*)[data mutableBytes];
2337 lineStart -= sectionStart;
2338 sectionStart = 0;
2339 if (endedFinalPart == YES)
2340 {
2341 done = YES;
2342 }
2343 }
2344 }
2345 /*
2346 * Check to see if we have reached content length or ended multipart
2347 * document.
2348 */
2349 if (endedFinalPart == YES || (expect > 0 && rawBodyLength >= expect))
2350 {
2351 flags.complete = 1;
2352 flags.inBody = 0;
2353 result = NO;
2354 }
2355 else
2356 {
2357 result = YES;
2358 }
2359 }
2360 return result;
2361 }
2362
2363 - (BOOL) _unfoldHeader
2364 {
2365 char c;
2366 BOOL unwrappingComplete = NO;
2367
2368 lineStart = lineEnd = input;
2369 NSDebugMLLog(@"GSMimeH", @"entry: input:%u dataEnd:%u lineStart:%u '%*.*s'",
2370 input, dataEnd, lineStart, dataEnd - input, dataEnd - input, &bytes[input]);
2371 /*
2372 * RFC822 lets header fields break across lines, with continuation
2373 * lines beginning with whitespace. This is called folding - and the
2374 * first thing we need to do is unfold any folded lines into a single
2375 * unfolded line (lineStart to lineEnd).
2376 */
2377 while (input < dataEnd && unwrappingComplete == NO)
2378 {
2379 if ((c = bytes[input]) != '\r' && c != '\n')
2380 {
2381 input++;
2382 }
2383 else
2384 {
2385 lineEnd = input++;
2386 if (input < dataEnd && c == '\r' && bytes[input] == '\n')
2387 {
2388 c = bytes[input++];
2389 }
2390 if (input < dataEnd || (c == '\n' && lineEnd == lineStart))
2391 {
2392 unsigned length = lineEnd - lineStart;
2393
2394 if (length == 0)
2395 {
2396 /* An empty line cannot be folded. */
2397 unwrappingComplete = YES;
2398 }
2399 else if ((c = bytes[input]) != '\r' && c != '\n' && isspace(c))
2400 {
2401 unsigned diff = input - lineEnd;
2402
2403 memmove(&bytes[lineStart + diff], &bytes[lineStart], length);
2404 lineStart += diff;
2405 lineEnd += diff;
2406 }
2407 else
2408 {
2409 /* No folding ... done. */
2410 unwrappingComplete = YES;
2411 }
2412 }
2413 }
2414 }
2415
2416 if (unwrappingComplete == YES)
2417 {
2418 if (lineEnd == lineStart)
2419 {
2420 unsigned lengthRemaining;
2421
2422 /*
2423 * Overwrite the header data with the body, ready to start
2424 * parsing the body data.
2425 */
2426 lengthRemaining = dataEnd - input;
2427 if (lengthRemaining > 0)
2428 {
2429 memcpy(bytes, &bytes[input], lengthRemaining);
2430 }
2431 dataEnd = lengthRemaining;
2432 [data setLength: lengthRemaining];
2433 bytes = (unsigned char*)[data mutableBytes];
2434 sectionStart = 0;
2435 lineStart = 0;
2436 lineEnd = 0;
2437 input = 0;
2438 flags.inBody = 1;
2439 }
2440 }
2441 else
2442 {
2443 input = lineStart; /* Reset to try again with more data. */
2444 }
2445
2446 NSDebugMLLog(@"GSMimeH", @"exit: inBody:%d unwrappingComplete: %d "
2447 @"input:%u dataEnd:%u lineStart:%u '%*.*s'", flags.inBody,
2448 unwrappingComplete,
2449 input, dataEnd, lineStart, lineEnd - lineStart, lineEnd - lineStart,
2450 &bytes[lineStart]);
2451 return unwrappingComplete;
2452 }
2453
2454 - (BOOL) _scanHeaderParameters: (NSScanner*)scanner into: (GSMimeHeader*)info
2455 {
2456 [self scanPastSpace: scanner];
2457 while ([scanner scanString: @";" intoString: 0] == YES)
2458 {
2459 NSString *paramName;
2460
2461 paramName = [self scanName: scanner];
2462 if ([paramName length] == 0)
2463 {
2464 NSLog(@"Invalid Mime %@ field (parameter name)", [info name]);
2465 return NO;
2466 }
2467
2468 [self scanPastSpace: scanner];
2469 if ([scanner scanString: @"=" intoString: 0] == YES)
2470 {
2471 NSString *paramValue;
2472
2473 paramValue = [self scanToken: scanner];
2474 [self scanPastSpace: scanner];
2475 if (paramValue == nil)
2476 {
2477 paramValue = @"";
2478 }
2479 [info setParameter: paramValue forKey: paramName];
2480 }
2481 else
2482 {
2483 NSLog(@"Ignoring Mime %@ field parameter (%@)",
2484 [info name], paramName);
2485 }
2486 }
2487 return YES;
2488 }
2489
2490 @end
2491
2492
2493
2494 @implementation GSMimeHeader
2495
2496 static NSCharacterSet *nonToken = nil;
2497 static NSCharacterSet *tokenSet = nil;
2498
2499 + (void) initialize
2500 {
2501 if (nonToken == nil)
2502 {
2503 NSMutableCharacterSet *ms;
2504
2505 ms = [NSMutableCharacterSet new];
2506 [ms addCharactersInRange: NSMakeRange(33, 126-32)];
2507 [ms removeCharactersInString: @"()<>@,;:\\\"/[]?="];
2508 tokenSet = [ms copy];
2509 RELEASE(ms);
2510 nonToken = RETAIN([tokenSet invertedSet]);
2511 }
2512 }
2513
2514 /**
2515 * Makes the value into a quoted string if necessary (ie if it contains
2516 * any special / non-token characters). If flag is YES then the value
2517 * is made into a quoted string even if it does not contain special characters.
2518 */
2519 + (NSString*) makeQuoted: (NSString*)v always: (BOOL)flag
2520 {
2521 NSRange r;
2522 unsigned pos = 0;
2523 unsigned l = [v length];
2524
2525 r = [v rangeOfCharacterFromSet: nonToken
2526 options: NSLiteralSearch
2527 range: NSMakeRange(pos, l - pos)];
2528 if (flag == YES || r.length > 0)
2529 {
2530 NSMutableString *m = [NSMutableString new];
2531
2532 [m appendString: @"\""];
2533 while (r.length > 0)
2534 {
2535 unichar c;
2536
2537 if (r.location > pos)
2538 {
2539 [m appendString:
2540 [v substringWithRange: NSMakeRange(pos, r.location - pos)]];
2541 }
2542 pos = r.location + 1;
2543 c = [v characterAtIndex: r.location];
2544 if (c < 128)
2545 {
2546 if (c == '\\' || c == '"')
2547 {
2548 [m appendFormat: @"\\%c", c];
2549 }
2550 else
2551 {
2552 [m appendFormat: @"%c", c];
2553 }
2554 }
2555 else
2556 {
2557 NSLog(@"NON ASCII characters not yet implemented");
2558 }
2559 r = [v rangeOfCharacterFromSet: nonToken
2560 options: NSLiteralSearch
2561 range: NSMakeRange(pos, l - pos)];
2562 }
2563 if (l > pos)
2564 {
2565 [m appendString:
2566 [v substringWithRange: NSMakeRange(pos, l - pos)]];
2567 }
2568 [m appendString: @"\""];
2569 v = AUTORELEASE(m);
2570 }
2571 return v;
2572 }
2573
2574 /**
2575 * Convert the supplied string to a standardized token by making it
2576 * lowercase and removing all illegal characters.
2577 */
2578 + (NSString*) makeToken: (NSString*)t
2579 {
2580 NSRange r;
2581
2582 t = [t lowercaseString];
2583 r = [t rangeOfCharacterFromSet: nonToken];
2584 if (r.length > 0)
2585 {
2586 NSMutableString *m = [t mutableCopy];
2587
2588 while (r.length > 0)
2589 {
2590 [m deleteCharactersInRange: r];
2591 r = [m rangeOfCharacterFromSet: nonToken];
2592 }
2593 t = AUTORELEASE(m);
2594 }
2595 return t;
2596 }
2597
2598 - (id) copyWithZone: (NSZone*)z
2599 {
2600 GSMimeHeader *c = [GSMimeHeader allocWithZone: z];
2601 NSEnumerator *e;
2602 NSString *k;
2603
2604 c = [c initWithName: [self name]
2605 value: [self value]
2606 parameters: [self parameters]];
2607 e = [objects keyEnumerator];
2608 while ((k = [e nextObject]) != nil)
2609 {
2610 [c setObject: [self objectForKey: k] forKey: k];
2611 }
2612 return c;
2613 }
2614
2615 - (void) dealloc
2616 {
2617 RELEASE(name);
2618 RELEASE(value);
2619 RELEASE(objects);
2620 RELEASE(params);
2621 [super dealloc];
2622 }
2623
2624 - (NSString*) description
2625 {
2626 NSMutableString *desc;
2627
2628 desc = [NSMutableString stringWithFormat: @"GSMimeHeader <%0x> -\n", self];
2629 [desc appendFormat: @" name: %@\n", [self name]];
2630 [desc appendFormat: @" value: %@\n", [self value]];
2631 [desc appendFormat: @" params: %@\n", [self parameters]];
2632 return desc;
2633 }
2634
2635 - (id) init
2636 {
2637 return [self initWithName: @"unknown" value: @"none" parameters: nil];
2638 }
2639
2640 /**
2641 * Convenience method calling -initWithName:value:parameters: with the
2642 * supplied argument and nil parameters.
2643 */
2644 - (id) initWithName: (NSString*)n
2645 value: (NSString*)v
2646 {
2647 return [self initWithName: n value: v parameters: nil];
2648 }
2649
2650 /**
2651 * <init />
2652 * Initialise a GSMimeHeader supplying a name, a value and a dictionary
2653 * of any parameters occurring after the value.
2654 */
2655 - (id) initWithName: (NSString*)n
2656 value: (NSString*)v
2657 parameters: (NSDictionary*)p
2658 {
2659 objects = [NSMutableDictionary new];
2660 params = [NSMutableDictionary new];
2661 [self setName: n];
2662 [self setValue: v];
2663 [self setParameters: p];
2664 return self;
2665 }
2666
2667 /**
2668 * Returns the name of this header ... a lowercase string.
2669 */
2670 - (NSString*) name
2671 {
2672 return name;
2673 }
2674
2675 /**
2676 * Return extra information specific to a particular header type.
2677 */
2678 - (id) objectForKey: (NSString*)k
2679 {
2680 return [objects objectForKey: k];
2681 }
2682
2683 /**
2684 * Returns a dictionary of all the additional objects for the header.
2685 */
2686 - (NSDictionary*) objects
2687 {
2688 return AUTORELEASE([objects copy]);
2689 }
2690
2691 /**
2692 * Return the named parameter value.
2693 */
2694 - (NSString*) parameterForKey: (NSString*)k
2695 {
2696 NSString *p = [params objectForKey: k];
2697
2698 if (p == nil)
2699 {
2700 k = [GSMimeHeader makeToken: k];
2701 p = [params objectForKey: k];
2702 }
2703 return p;
2704 }
2705
2706 /**
2707 * Returns the parameters of this header ... a dictionary whose keys
2708 * are all lowercase strings, and whose values are strings which may
2709 * contain mixed case.
2710 */
2711 - (NSDictionary*) parameters
2712 {
2713 return AUTORELEASE([params copy]);
2714 }
2715
2716 /**
2717 * Returns the full text of the header, built from its component parts,
2718 * and including a terminating CR-LF
2719 */
2720 - (NSMutableData*) rawMimeData
2721 {
2722 NSMutableData *md = [NSMutableData dataWithCapacity: 128];
2723 NSEnumerator *e = [params keyEnumerator];
2724 NSString *k;
2725 NSData *d = [[self name] dataUsingEncoding: NSASCIIStringEncoding];
2726 unsigned l = [d length];
2727 char buf[l];
2728 unsigned int i = 0;
2729 BOOL conv = YES;
2730
2731 #define LIM 120
2732 /*
2733 * Capitalise the header name. However, the version header is a special
2734 * case - it is defined as being literally 'MIME-Version'
2735 */
2736 memcpy(buf, [d bytes], l);
2737 if (l == 12 && memcmp(buf, "mime-version", 12) == 0)
2738 {
2739 memcpy(buf, "MIME-Version", 12);
2740 }
2741 else
2742 {
2743 while (i < l)
2744 {
2745 if (conv == YES)
2746 {
2747 if (islower(buf[i]))
2748 {
2749 buf[i] = toupper(buf[i]);
2750 }
2751 }
2752 if (buf[i++] == '-')
2753 {
2754 conv = YES;
2755 }
2756 else
2757 {
2758 conv = NO;
2759 }
2760 }
2761 }
2762 [md appendBytes: buf length: l];
2763 d = wordData(value);
2764 if ([md length] + [d length] + 2 > LIM)
2765 {
2766 [md appendBytes: ":\r\n\t" length: 4];
2767 [md appendData: d];
2768 l = [md length] + 8;
2769 }
2770 else
2771 {
2772 [md appendBytes: ": " length: 2];
2773 [md appendData: d];
2774 l = [md length];
2775 }
2776
2777 while ((k = [e nextObject]) != nil)
2778 {
2779 NSString *v;
2780 NSData *kd;
2781 NSData *vd;
2782 unsigned kl;
2783 unsigned vl;
2784
2785 v = [GSMimeHeader makeQuoted: [params objectForKey: k] always: NO];
2786 kd = wordData(k);
2787 vd = wordData(v);
2788 kl = [kd length];
2789 vl = [vd length];
2790
2791 if ((l + kl + vl + 3) > LIM)
2792 {
2793 [md appendBytes: ";\r\n\t" length: 4];
2794 [md appendData: kd];
2795 [md appendBytes: "=" length: 1];
2796 [md appendData: vd];
2797 l = kl + vl + 9;
2798 }
2799 else
2800 {
2801 [md appendBytes: "; " length: 2];
2802 [md appendData: kd];
2803 [md appendBytes: "=" length: 1];
2804 [md appendData: vd];
2805 l += kl + vl + 3;
2806 }
2807 }
2808 [md appendBytes: "\r\n" length: 2];
2809
2810 return md;
2811 }
2812
2813 /**
2814 * Sets the name of this header ... converts to lowercase and removes
2815 * illegal characters. If given a nil or empty string argument,
2816 * sets the name to 'unknown'.
2817 */
2818 - (void) setName: (NSString*)s
2819 {
2820 s = [GSMimeHeader makeToken: s];
2821 if ([s length] == 0)
2822 {
2823 s = @"unknown";
2824 }
2825 ASSIGN(name, s);
2826 }
2827
2828 /**
2829 * Method to store specific information for particular types of
2830 * header. This is used for non-standard parts of headers.<br />
2831 * Setting a nil value for o will remove any existing value set
2832 * using the k as its key.
2833 */
2834 - (void) setObject: (id)o forKey: (NSString*)k
2835 {
2836 if (o == nil)
2837 {
2838 [objects removeObjectForKey: k];
2839 }
2840 else
2841 {
2842 [objects setObject: o forKey: k];
2843 }
2844 }
2845
2846 /**
2847 * Sets a parameter of this header ... converts name to lowercase and
2848 * removes illegal characters.<br />
2849 * If a nil parameter name is supplied, removes any parameter with the
2850 * specified key.
2851 */
2852 - (void) setParameter: (NSString*)v forKey: (NSString*)k
2853 {
2854 k = [GSMimeHeader makeToken: k];
2855 if (v == nil)
2856 {
2857 [params removeObjectForKey: k];
2858 }
2859 else
2860 {
2861 [params setObject: v forKey: k];
2862 }
2863 }
2864
2865 /**
2866 * Sets all parameters of this header ... converts names to lowercase
2867 * and removes illegal characters from them.
2868 */
2869 - (void) setParameters: (NSDictionary*)d
2870 {
2871 NSMutableDictionary *m = [NSMutableDictionary new];
2872 NSEnumerator *e = [d keyEnumerator];
2873 NSString *k;
2874
2875 while ((k = [e nextObject]) != nil)
2876 {
2877 [m setObject: [d objectForKey: k] forKey: [GSMimeHeader makeToken: k]];
2878 }
2879 DESTROY(params);
2880 params = m;
2881 }
2882
2883 /**
2884 * Sets the value of this header (without changing parameters)<br />
2885 * If given a nil argument, set an empty string value.
2886 */
2887 - (void) setValue: (NSString*)s
2888 {
2889 if (s == nil)
2890 {
2891 s = @"";
2892 }
2893 ASSIGN(value, s);
2894 }
2895
2896 /**
2897 * Returns the full text of the header, built from its component parts,
2898 * and including a terminating CR-LF
2899 */
2900 - (NSString*) text
2901 {
2902 NSString *s = [NSString alloc];
2903
2904 s = [s initWithData: [self rawMimeData] encoding: NSASCIIStringEncoding];
2905 return AUTORELEASE(s);
2906 }
2907
2908 /**
2909 * Returns the value of this header (excluding any parameters)
2910 */
2911 - (NSString*) value
2912 {
2913 return value;
2914 }
2915 @end
2916
2917
2918
2919 @interface GSMimeDocument (Private)
2920 - (unsigned) _indexOfHeaderNamed: (NSString*)name;
2921 @end
2922
2923 /**
2924 * <p>
2925 * This class is intended to provide a wrapper for MIME messages
2926 * permitting easy access to the contents of a message and
2927 * providing a basis for parsing an unparsing messages that
2928 * have arrived via email or as a web document.
2929 * </p>
2930 * <p>
2931 * The class keeps track of all the document headers, and provides
2932 * methods for modifying and examining the headers that apply to a
2933 * document.
2934 * </p>
2935 */
2936 @implementation GSMimeDocument
2937
2938 /**
2939 * Return the MIME characterset name corresponding to the
2940 * specified string encoding.
2941 */
2942 + (NSString*) charsetFromEncoding: (NSStringEncoding)enc
2943 {
2944 if (enc == NSASCIIStringEncoding)
2945 return @"us-ascii"; // Default character set.
2946 if (enc == NSISOLatin1StringEncoding)
2947 return @"iso-8859-1";
2948 if (enc == NSISOLatin2StringEncoding)
2949 return @"iso-8859-2";
2950 if (enc == NSISOLatin3StringEncoding)
2951 return @"iso-8859-3";
2952 if (enc == NSISOLatin4StringEncoding)
2953 return @"iso-8859-4";
2954 if (enc == NSISOCyrillicStringEncoding)
2955 return @"iso-8859-5";
2956 if (enc == NSISOArabicStringEncoding)
2957 return @"iso-8859-6";
2958 if (enc == NSISOGreekStringEncoding)
2959 return @"iso-8859-7";
2960 if (enc == NSISOHebrewStringEncoding)
2961 return @"iso-8859-8";
2962 if (enc == NSISOLatin5StringEncoding)
2963 return @"iso-8859-9";
2964 if (enc == NSISOLatin6StringEncoding)
2965 return @"iso-8859-10";
2966 if (enc == NSISOLatin7StringEncoding)
2967 return @"iso-8859-13";
2968 if (enc == NSISOLatin8StringEncoding)
2969 return @"iso-8859-14";
2970 if (enc == NSISOLatin9StringEncoding)
2971 return @"iso-8859-15";
2972 if (enc == NSWindowsCP1250StringEncoding)
2973 return @"windows-1250";
2974 if (enc == NSWindowsCP1251StringEncoding)
2975 return @"windows-1251";
2976 if (enc == NSWindowsCP1252StringEncoding)
2977 return @"windows-1252";
2978 if (enc == NSWindowsCP1253StringEncoding)
2979 return @"windows-1253";
2980 if (enc == NSWindowsCP1254StringEncoding)
2981 return @"windows-1254";
2982 return @"utf-8";
2983 }
2984
2985 /**
2986 * Decode the source data from base64 encoding and return the result.
2987 */
2988 + (NSData*) decodeBase64: (NSData*)source
2989 {
2990 int length;
2991 int declen ;
2992 const signed char *src;
2993 const signed char *end;
2994 unsigned char *result;
2995 unsigned char *dst;
2996 unsigned char buf[4];
2997 unsigned pos = 0;
2998
2999 if (source == nil)
3000 {
3001 return nil;
3002 }
3003 length = [source length];
3004 if (length == 0)
3005 {
3006 return [NSData data];
3007 }
3008 declen = ((length + 3) * 3)/4;
3009 src = (const char*)[source bytes];
3010 end = &src[length];
3011
3012 result = (unsigned char*)NSZoneMalloc(NSDefaultMallocZone(), declen);
3013 dst = result;
3014
3015 while (*src && (src != end))
3016 {
3017 int c = *src++;
3018
3019 if (isupper(c))
3020 {
3021 c -= 'A';
3022 }
3023 else if (islower(c))
3024 {
3025 c = c - 'a' + 26;
3026 }
3027 else if (isdigit(c))
3028 {
3029 c = c - '0' + 52;
3030 }
3031 else if (c == '/')
3032 {
3033 c = 63;
3034 }
3035 else if (c == '+')
3036 {
3037 c = 62;
3038 }
3039 else if (c == '=')
3040 {
3041 c = -1;
3042 }
3043 else if (c == '-')
3044 {
3045 break; /* end */
3046 }
3047 else
3048 {
3049 c = -1; /* ignore */
3050 }
3051
3052 if (c >= 0)
3053 {
3054 buf[pos++] = c;
3055 if (pos == 4)
3056 {
3057 pos = 0;
3058 decodebase64(dst, buf);
3059 dst += 3;
3060 }
3061 }
3062 }
3063
3064 if (pos > 0)
3065 {
3066 unsigned i;
3067
3068 for (i = pos; i < 4; i++)
3069 buf[i] = '\0';
3070 pos--;
3071 }
3072 decodebase64(dst, buf);
3073 dst += pos;
3074 return AUTORELEASE([[NSData allocWithZone: NSDefaultMallocZone()]
3075 initWithBytesNoCopy: result length: dst - result]);
3076 }
3077
3078 /**
3079 * Converts the base64 encoded data in source to a decoded ASCII string
3080 * using the +decodeBase64: method. If the encoded data does not represent
3081 * an ASCII string, you should use the +decodeBase64: method directly.
3082 */
3083 + (NSString*) decodeBase64String: (NSString*)source
3084 {
3085 NSData *d = [source dataUsingEncoding: NSASCIIStringEncoding];
3086 NSString *r = nil;
3087
3088 d = [self decodeBase64: d];
3089 if (d != nil)
3090 {
3091 r = [[NSString alloc] initWithData: d encoding: NSASCIIStringEncoding];
3092 AUTORELEASE(r);
3093 }
3094 return r;
3095 }
3096
3097 /**
3098 * Convenience method to return an autoreleased document using the
3099 * specified content, type, and name value. This calls the
3100 * -setContent:type:name: method to set up the document.
3101 */
3102 + (GSMimeDocument*) documentWithContent: (id)newContent
3103 type: (NSString*)type
3104 name: (NSString*)name
3105 {
3106 GSMimeDocument *doc = AUTORELEASE([self new]);
3107
3108 [doc setContent: newContent type: type name: name];
3109 return doc;
3110 }
3111
3112 /**
3113 * Encode the source data to base64 encoding and return the result.
3114 */
3115 + (NSData*) encodeBase64: (NSData*)source
3116 {
3117 int length;
3118 int destlen;
3119 unsigned char *sBuf;
3120 unsigned char *dBuf;
3121
3122 if (source == nil)
3123 {
3124 return nil;
3125 }
3126 length = [source length];
3127 if (length == 0)
3128 {
3129 return [NSData data];
3130 }
3131 destlen = 4 * ((length - 1) / 3) + 5;
3132 sBuf = (unsigned char*)[source bytes];
3133 dBuf = NSZoneMalloc(NSDefaultMallocZone(), destlen);
3134 dBuf[destlen - 1] = '\0';
3135
3136 destlen = encodebase64(dBuf, sBuf, length);
3137
3138 return AUTORELEASE([[NSData allocWithZone: NSDefaultMallocZone()]
3139 initWithBytesNoCopy: dBuf length: destlen]);
3140 }
3141
3142 /**
3143 * Converts the ASCII string source into base64 encoded data using the
3144 * +encodeBase64: method. If the original data is not an ASCII string,
3145 * you should use the +encodeBase64: method directly.
3146 */
3147 + (NSString*) encodeBase64String: (NSString*)source
3148 {
3149 NSData *d = [source dataUsingEncoding: NSASCIIStringEncoding];
3150 NSString *r = nil;
3151
3152 d = [self encodeBase64: d];
3153 if (d != nil)
3154 {
3155 r = [[NSString alloc] initWithData: d encoding: NSASCIIStringEncoding];
3156 AUTORELEASE(r);
3157 }
3158 return r;
3159 }
3160
3161 /**
3162 * Return the string encoding corresponding to the specified MIME
3163 * characterset name.
3164 */
3165 + (NSStringEncoding) encodingFromCharset: (NSString*)charset
3166 {
3167 if (charset == nil)
3168 {
3169 return NSASCIIStringEncoding; // Default character set.
3170 }
3171
3172 charset = [charset lowercaseString];
3173
3174 /*
3175 * Try the three most popular charactersets first - for efficiency.
3176 */
3177 if ([charset isEqualToString: @"us-ascii"] == YES)
3178 return NSASCIIStringEncoding;
3179 if ([charset isEqualToString: @"iso-8859-1"] == YES)
3180 return NSISOLatin1StringEncoding;
3181 if ([charset isEqualToString: @"utf-8"] == YES)
3182 return NSUTF8StringEncoding;
3183
3184 /*
3185 * Now try all remaining character sets in alphabetical order.
3186 */
3187 if ([charset isEqualToString: @"ascii"] == YES)
3188 return NSASCIIStringEncoding;
3189 if ([charset isEqualToString: @"iso-8859-2"] == YES)
3190 return NSISOLatin2StringEncoding;
3191 if ([charset isEqualToString: @"iso-8859-3"] == YES)
3192 return NSISOLatin3StringEncoding;
3193 if ([charset isEqualToString: @"iso-8859-4"] == YES)
3194 return NSISOLatin4StringEncoding;
3195 if ([charset isEqualToString: @"iso-8859-5"] == YES)
3196 return NSISOCyrillicStringEncoding;
3197 if ([charset isEqualToString: @"iso-8859-6"] == YES)
3198 return NSISOArabicStringEncoding;
3199 if ([charset isEqualToString: @"iso-8859-7"] == YES)
3200 return NSISOGreekStringEncoding;
3201 if ([charset isEqualToString: @"iso-8859-8"] == YES)
3202 return NSISOHebrewStringEncoding;
3203 if ([charset isEqualToString: @"iso-8859-9"] == YES)
3204 return NSISOLatin5StringEncoding;
3205 if ([charset isEqualToString: @"iso-8859-10"] == YES)
3206 return NSISOLatin6StringEncoding;
3207 if ([charset isEqualToString: @"iso-8859-13"] == YES)
3208 return NSISOLatin7StringEncoding;
3209 if ([charset isEqualToString: @"iso-8859-14"] == YES)
3210 return NSISOLatin8StringEncoding;
3211 if ([charset isEqualToString: @"iso-8859-15"] == YES)
3212 return NSISOLatin9StringEncoding;
3213 if ([charset isEqualToString: @"windows-1250"] == YES)
3214 return NSWindowsCP1250StringEncoding;
3215 if ([charset isEqualToString: @"windows-1251"] == YES)
3216 return NSWindowsCP1251StringEncoding;
3217 if ([charset isEqualToString: @"windows-1252"] == YES)
3218 return NSWindowsCP1252StringEncoding;
3219 if ([charset isEqualToString: @"windows-1253"] == YES)
3220 return NSWindowsCP1253StringEncoding;
3221 if ([charset isEqualToString: @"windows-1254"] == YES)
3222 return NSWindowsCP1254StringEncoding;
3223
3224 return NSASCIIStringEncoding; // Default character set.
3225 }
3226
3227 + (void) initialize
3228 {
3229 if (self == [GSMimeDocument class])
3230 {
3231 NSMutableCharacterSet *m = [[NSMutableCharacterSet alloc] init];
3232
3233 [m formUnionWithCharacterSet:
3234 [NSCharacterSet characterSetWithCharactersInString:
3235 @".()<>@,;:[]\"\\"]];
3236 [m formUnionWithCharacterSet:
3237 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
3238 [m formUnionWithCharacterSet:
3239 [NSCharacterSet controlCharacterSet]];
3240 [m formUnionWithCharacterSet:
3241 [NSCharacterSet illegalCharacterSet]];
3242 rfc822Specials = [m copy];
3243 [m formUnionWithCharacterSet:
3244 [NSCharacterSet characterSetWithCharactersInString:
3245 @"/?="]];
3246 [m removeCharactersInString: @"."];
3247 rfc2045Specials = [m copy];
3248 whitespace = RETAIN([NSCharacterSet whitespaceAndNewlineCharacterSet]);
3249 }
3250 }
3251
3252 /**
3253 * Adds a part to a multipart document
3254 */
3255 - (void) addContent: (id)newContent
3256 {
3257 if (content == nil)
3258 {
3259 content = [NSMutableArray new];
3260 }
3261 if ([content isKindOfClass: [NSMutableArray class]] == YES)
3262 {
3263 [content addObject: newContent];
3264 }
3265 else
3266 {
3267 [NSException raise: NSInvalidArgumentException
3268 format: @"[%@ -%@] passed bad content",
3269 NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
3270 }
3271 }
3272
3273 /**
3274 * <p>
3275 * This method may be called to add a header to the document.
3276 * The header must be a mutable dictionary object that contains
3277 * at least the fields that are standard for all headers.
3278 * </p>
3279 * <p>
3280 * Certain well-known headers are restricted to one occurrance in
3281 * an email, and when extra copies are added they replace originals.
3282 * </p>
3283 * <p>
3284 * The mime-version header is special ... it is inserted before any
3285 * other mime headers rather than being added at the end.
3286 * </p>
3287 */
3288 - (void) addHeader: (GSMimeHeader*)info
3289 {
3290 NSString *name = [info name];
3291
3292 if (name == nil || [name isEqualToString: @"unknown"] == YES)
3293 {
3294 [NSException raise: NSInvalidArgumentException
3295 format: @"[%@ -%@] header with invalid name",
3296 NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
3297 }
3298 if ([name isEqualToString: @"mime-version"] == YES
3299 || [name isEqualToString: @"content-disposition"] == YES
3300 || [name isEqualToString: @"content-transfer-encoding"] == YES
3301 || [name isEqualToString: @"content-type"] == YES
3302 || [name isEqualToString: @"subject"] == YES)
3303 {
3304 unsigned index = [self _indexOfHeaderNamed: name];
3305
3306 if (index != NSNotFound)
3307 {
3308 [headers replaceObjectAtIndex: index withObject: info];
3309 }
3310 else if ([name isEqualToString: @"mime-version"] == YES)
3311 {
3312 unsigned tmp;
3313
3314 index = [headers count];
3315 tmp = [self _indexOfHeaderNamed: @"content-disposition"];
3316 if (tmp != NSNotFound && tmp < index)
3317 {
3318 index = tmp;
3319 }
3320 tmp = [self _indexOfHeaderNamed: @"content-transfer-encoding"];
3321 if (tmp != NSNotFound && tmp < index)
3322 {
3323 index = tmp;
3324 }
3325 tmp = [self _indexOfHeaderNamed: @"content-type"];
3326 if (tmp != NSNotFound && tmp < index)
3327 {
3328 index = tmp;
3329 }
3330 [headers insertObject: info atIndex: index];
3331 }
3332 else
3333 {
3334 [headers addObject: info];
3335 }
3336 }
3337 else
3338 {
3339 [headers addObject: info];
3340 }
3341 }
3342
3343 /**
3344 * <p>
3345 * This method returns an array containing GSMimeHeader objects
3346 * representing the headers associated with the document.
3347 * </p>
3348 * <p>
3349 * The order of the headers in the array is the order of the
3350 * headers in the document.
3351 * </p>
3352 */
3353 - (NSArray*) allHeaders
3354 {
3355 return [NSArray arrayWithArray: headers];
3356 }
3357
3358 /**
3359 * This returns the content data of the document in the same format in
3360 * which the data was placed in the document. This may be one of -
3361 * <deflist>
3362 * <term>text</term>
3363 * <desc>an NSString object</desc>
3364 * <term>binary</term>
3365 * <desc>an NSData object</desc>
3366 * <term>multipart</term>
3367 * <desc>an NSArray object containing GSMimeDocument objects</desc>
3368 * </deflist>
3369 * If you want to be sure that you get a particular type of data, use the
3370 * -convertToData or -convertToText method.
3371 */
3372 - (id) content
3373 {
3374 return content;
3375 }
3376
3377 /**
3378 * Search the content of this document to locate a part whose content ID
3379 * matches the specified key. Recursively descend into other documents.<br />
3380 * Return nil if no match is found, the matching GSMimeDocument otherwise.
3381 */
3382 - (id) contentByID: (NSString*)key
3383 {
3384 if ([key hasPrefix: @"<"] == NO)
3385 {
3386 key = [NSString stringWithFormat: @"<%@>", key];
3387 }
3388 if ([content isKindOfClass: [NSArray class]] == YES)
3389 {
3390 NSEnumerator *e = [content objectEnumerator];
3391 GSMimeDocument *d;
3392
3393 while ((d = [e nextObject]) != nil)
3394 {
3395 if ([[d contentID] isEqualToString: key] == YES)
3396 {
3397 return d;
3398 }
3399 d = [d contentByID: key];
3400 if (d != nil)
3401 {
3402 return d;
3403 }
3404 }
3405 }
3406 return nil;
3407 }
3408
3409 /**
3410 * Search the content of this document to locate a part whose content-type
3411 * name or content-disposition name matches the specified key.
3412 * Recursively descend into other documents.<br />
3413 * Return nil if no match is found, the matching GSMimeDocument otherwise.
3414 */
3415 - (id) contentByName: (NSString*)key
3416 {
3417
3418 if ([content isKindOfClass: [NSArray class]] == YES)
3419 {
3420 NSEnumerator *e = [content objectEnumerator];
3421 GSMimeDocument *d;
3422
3423 while ((d = [e nextObject]) != nil)
3424 {
3425 GSMimeHeader *hdr;
3426
3427 hdr = [d headerNamed: @"content-type"];
3428 if ([[hdr parameterForKey: @"name"] isEqualToString: key] == YES)
3429 {
3430 return d;
3431 }
3432 hdr = [d headerNamed: @"content-disposition"];
3433 if ([[hdr parameterForKey: @"name"] isEqualToString: key] == YES)
3434 {
3435 return d;
3436 }
3437 d = [d contentByName: key];
3438 if (d != nil)
3439 {
3440 return d;
3441 }
3442 }
3443 }
3444 return nil;
3445 }
3446
3447 /**
3448 * Convenience method to fetch the content file name from the header.
3449 */
3450 - (NSString*) contentFile
3451 {
3452 GSMimeHeader *hdr = [self headerNamed: @"content-disposition"];
3453
3454 return [hdr parameterForKey: @"filename"];
3455 }
3456
3457 /**
3458 * Convenience method to fetch the content ID from the header.
3459 */
3460 - (NSString*) contentID
3461 {
3462 GSMimeHeader *hdr = [self headerNamed: @"content-id"];
3463
3464 return [hdr value];
3465 }
3466
3467 /**
3468 * Convenience method to fetch the content name from the header.
3469 */
3470 - (NSString*) contentName
3471 {
3472 GSMimeHeader *hdr = [self headerNamed: @"content-type"];
3473
3474 return [hdr parameterForKey: @"name"];
3475 }
3476
3477 /**
3478 * Convenience method to fetch the content sub-type from the header.
3479 */
3480 - (NSString*) contentSubtype
3481 {
3482 GSMimeHeader *hdr = [self headerNamed: @"content-type"];
3483
3484 return [hdr objectForKey: @"Subtype"];
3485 }
3486
3487 /**
3488 * Convenience method to fetch the content type from the header.
3489 */
3490 - (NSString*) contentType
3491 {
3492 GSMimeHeader *hdr = [self headerNamed: @"content-type"];
3493
3494 return [hdr objectForKey: @"Type"];
3495 }
3496
3497 /**
3498 * Search the content of this document to locate all parts whose content-type
3499 * name or content-disposition name matches the specified key.
3500 * Do <em>NOT</em> recurse into other documents.<br />
3501 * Return nil if no match is found, an array of matching GSMimeDocument
3502 * instances otherwise.
3503 */
3504 - (NSArray*) contentsByName: (NSString*)key
3505 {
3506 NSMutableArray *a = nil;
3507
3508 if ([content isKindOfClass: [NSArray class]] == YES)
3509 {
3510 NSEnumerator *e = [content objectEnumerator];
3511 GSMimeDocument *d;
3512
3513 while ((d = [e nextObject]) != nil)
3514 {
3515 GSMimeHeader *hdr;
3516 BOOL match = YES;
3517
3518 hdr = [d headerNamed: @"content-type"];
3519 if ([[hdr parameterForKey: @"name"] isEqualToString: key] == NO)
3520 {
3521 hdr = [d headerNamed: @"content-disposition"];
3522 if ([[hdr parameterForKey: @"name"] isEqualToString: key] == NO)
3523 {
3524 match = NO;
3525 }
3526 }
3527 if (match == YES)
3528 {
3529 if (a == nil)
3530 {
3531 a = [NSMutableArray arrayWithCapacity: 4];
3532 }
3533 [a addObject: d];
3534 }
3535 }
3536 }
3537 return a;
3538 }
3539
3540 /**
3541 * Return the content as an NSData object (unless it is multipart)<br />
3542 * Perform conversion from text to data using the charset specified in
3543 * the content-type header, or infer the charset, and update the header
3544 * accordingly.<br />
3545 * If the content can not be represented as a plain NSData object, this
3546 * method returns nil.
3547 */
3548 - (NSData*) convertToData
3549 {
3550 NSData *d = nil;
3551
3552 if ([content isKindOfClass: [NSString class]] == YES)
3553 {
3554 GSMimeHeader *hdr = [self headerNamed: @"content-type"];
3555 NSString *charset = [hdr parameterForKey: @"charset"];
3556 NSStringEncoding enc;
3557
3558 enc = [GSMimeDocument encodingFromCharset: charset];
3559 d = [content dataUsingEncoding: enc];
3560 if (d == nil)
3561 {
3562 charset = selectCharacterSet(content, &d);
3563 [hdr setParameter: charset forKey: @"charset"];
3564 }
3565 }
3566 else if ([content isKindOfClass: [NSData class]] == YES)
3567 {
3568 d = content;
3569 }
3570 return d;
3571 }
3572
3573 /**
3574 * Return the content as an NSString object (unless it is multipart)
3575 * If the content cannot be represented as text, this returns nil.
3576 */
3577 - (NSString*) convertToText
3578 {
3579 NSString *s = nil;
3580
3581 if ([content isKindOfClass: [NSString class]] == YES)
3582 {
3583 s = content;
3584 }
3585 else if ([content isKindOfClass: [NSData class]] == YES)
3586 {
3587 GSMimeHeader *hdr = [self headerNamed: @"content-type"];
3588 NSString *charset = [hdr parameterForKey: @"charset"];
3589 NSStringEncoding enc;
3590
3591 enc = [GSMimeDocument encodingFromCharset: charset];
3592 s = [[NSString alloc] initWithData: content encoding: enc];
3593 AUTORELEASE(s);
3594 }
3595 return s;
3596 }
3597
3598 /**
3599 * Returns a copy of the receiver.
3600 */
3601 - (id) copyWithZone: (NSZone*)z
3602 {
3603 GSMimeDocument *c = [GSMimeDocument allocWithZone: z];
3604
3605 c->headers = [[NSMutableArray allocWithZone: z] initWithArray: headers
3606 copyItems: YES];
3607
3608 if ([content isKindOfClass: [NSArray class]] == YES)
3609 {
3610 c->content = [[NSMutableArray allocWithZone: z] initWithArray: content
3611 copyItems: YES];
3612 }
3613 else
3614 {
3615 c->content = [content copyWithZone: z];
3616 }
3617 return c;
3618 }
3619
3620 - (void) dealloc
3621 {
3622 RELEASE(headers);
3623 RELEASE(content);
3624 [super dealloc];
3625 }
3626
3627 /**
3628 * This method removes all occurrances of header objects identical to
3629 * the one supplied as an argument.
3630 */
3631 - (void) deleteHeader: (GSMimeHeader*)aHeader
3632 {
3633 unsigned count = [headers count];
3634
3635 while (count-- > 0)
3636 {
3637 if ([aHeader isEqual: [headers objectAtIndex: count]] == YES)
3638 {
3639 [headers removeObjectAtIndex: count];
3640 }
3641 }
3642 }
3643
3644 /**
3645 * This method removes all occurrances of headers whose name
3646 * matches the supplied string.
3647 */
3648 - (void) deleteHeaderNamed: (NSString*)name
3649 {
3650 unsigned count = [headers count];
3651
3652 name = [name lowercaseString];
3653 while (count-- > 0)
3654 {
3655 GSMimeHeader *info = [headers objectAtIndex: count];
3656
3657 if ([name isEqualToString: [info name]] == YES)
3658 {
3659 [headers removeObjectAtIndex: count];
3660 }
3661 }
3662 }
3663
3664 - (NSString*) description
3665 {
3666 NSMutableString *desc;
3667 NSDictionary *locale;
3668
3669 desc = [NSMutableString stringWithFormat: @"GSMimeDocument <%0x> -\n", self];
3670 locale = [[NSUserDefaults standardUserDefaults] dictionaryRepresentation];
3671 [desc appendString: [headers descriptionWithLocale: locale]];
3672 [desc appendFormat: @"\nDocument content -\n%@", content];
3673 return desc;
3674 }
3675
3676 /**
3677 * This method returns the first header whose name equals the supplied argument.
3678 */
3679 - (GSMimeHeader*) headerNamed: (NSString*)name
3680 {
3681 NSArray *a = [self headersNamed: name];
3682
3683 if ([a count] > 0)
3684 {
3685 return [a objectAtIndex: 0];
3686 }
3687 return nil;
3688 }
3689
3690 /**
3691 * This method returns an array of GSMimeHeader objects for all headers
3692 * whose names equal the supplied argument.
3693 */
3694 - (NSArray*) headersNamed: (NSString*)name
3695 {
3696 unsigned count = [headers count];
3697 unsigned index;
3698 NSMutableArray *array;
3699
3700 name = [GSMimeHeader makeToken: name];
3701 array = [NSMutableArray array];
3702 for (index = 0; index < count; index++)
3703 {
3704 GSMimeHeader *info = [headers objectAtIndex: index];
3705
3706 if ([name isEqualToString: [info name]] == YES)
3707 {
3708 [array addObject: info];
3709 }
3710 }
3711 return array;
3712 }
3713
3714 - (id) init
3715 {
3716 if ((self = [super init]) != nil)
3717 {
3718 headers = [NSMutableArray new];
3719 }
3720 return self;
3721 }
3722
3723 /**
3724 * <p>Make a probably unique string suitable for use as the
3725 * boundary parameter in the content of a multipart document.
3726 * </p>
3727 * <p>This implementation provides base64 encoded data
3728 * consisting of an MD5 digest of some pseudo random stuff,
3729 * plus an incrementing counter. The inclusion of the counter
3730 * guarantees that we won't produce two identical strings in
3731 * the same run of the program.
3732 * </p>
3733 */
3734 - (NSString*) makeBoundary
3735 {
3736 static int count = 0;
3737 unsigned char output[20];
3738 NSMutableData *md;
3739 NSString *result;
3740 NSData *source;
3741 NSData *digest;
3742 int sequence = ++count;
3743
3744 source = [[[NSProcessInfo processInfo] globallyUniqueString]
3745 dataUsingEncoding: NSUTF8StringEncoding];
3746 digest = [source md5Digest];
3747 memcpy(output, [digest bytes], 16);
3748 output[16] = (sequence >> 24) & 0xff;
3749 output[17] = (sequence >> 16) & 0xff;
3750 output[18] = (sequence >> 8) & 0xff;
3751 output[19] = sequence & 0xff;
3752
3753 md = [[NSMutableData alloc] initWithLength: 40];
3754 [md setLength: encodebase64([md mutableBytes], output, 20)];
3755 result = [[NSString alloc] initWithData: md encoding: NSASCIIStringEncoding];
3756 return AUTORELEASE(result);
3757 }
3758
3759 /**
3760 * Create new content ID header, set it as the content ID of the document
3761 * and return it.<br />
3762 * This is a convenience method which simply places angle brackets around
3763 * an [NSProcessInfo-globallyUniqueString] to form the header value.
3764 */
3765 - (GSMimeHeader*) makeContentID
3766 {
3767 GSMimeHeader *hdr;
3768 NSString *str = [[NSProcessInfo processInfo] globallyUniqueString];
3769
3770 str = [NSString stringWithFormat: @"<%@>", str];
3771 hdr = [[GSMimeHeader alloc] initWithName: @"content-id"
3772 value: str
3773 parameters: nil];
3774 [self setHeader: hdr];
3775 RELEASE(hdr);
3776 return hdr;
3777 }
3778
3779 /**
3780 * Convenience method to create a new header and add it to the receiver
3781 * replacing any existing header of the same name.<br />
3782 * Returns the newly created header.<br />
3783 * See [GSMimeHeader-initWithName:value:parameters:] and -setHeader: methods.
3784 */
3785 - (GSMimeHeader*) makeHeader: (NSString*)name
3786 value: (NSString*)value
3787 parameters: (NSDictionary*)parameters
3788 {
3789 GSMimeHeader *hdr;
3790
3791 hdr = [[GSMimeHeader alloc] initWithName: name
3792 value: value
3793 parameters: parameters];
3794 [self setHeader: hdr];
3795 RELEASE(hdr);
3796 return hdr;
3797 }
3798
3799 /**
3800 * Create new message ID header, set it as the message ID of the document
3801 * and return it.<br />
3802 * This is a convenience method which simply places angle brackets around
3803 * an [NSProcessInfo-globallyUniqueString] to form the header value.
3804 */
3805 - (GSMimeHeader*) makeMessageID
3806 {
3807 GSMimeHeader *hdr;
3808 NSString *str = [[NSProcessInfo processInfo] globallyUniqueString];
3809
3810 str = [NSString stringWithFormat: @"<%@>", str];
3811 hdr = [[GSMimeHeader alloc] initWithName: @"message-id"
3812 value: str
3813 parameters: nil];
3814 [self setHeader: hdr];
3815 RELEASE(hdr);
3816 return hdr;
3817 }
3818
3819 /**
3820 * Return an NSData object representing the MIME document as raw data
3821 * ready to be sent via an email system.<br />
3822 * Calls -rawMimeData: with the isOuter flag set to YES.
3823 */
3824 - (NSMutableData*) rawMimeData
3825 {
3826 return [self rawMimeData: YES];
3827 }
3828
3829 /**
3830 * <p>Return an NSData object representing the MIME document as raw data
3831 * ready to be sent via an email system.
3832 * </p>
3833 * <p>The isOuter flag denotes whether this document is the outermost
3834 * part of a MIME message, or is a part of a multipart message.
3835 * </p>
3836 * <p>During generation of the document this method will perform some
3837 * consistency checks and try to automatically generate missing header
3838 * information needed to build the mime data (eg. filling in the boundary
3839 * parameter in the content-type header for multipart documents).<br />
3840 * However, you should not depend on automatic behaviors but should
3841 * fill in as much detail as possible before generating data.
3842 * </p>
3843 */
3844 - (NSMutableData*) rawMimeData: (BOOL)isOuter
3845 {
3846 NSMutableArray *partData = nil;
3847 NSMutableData *md = [NSMutableData dataWithCapacity: 1024];
3848 NSData *d = nil;
3849 NSEnumerator *enumerator;
3850 GSMimeHeader *type;
3851 GSMimeHeader *enc;
3852 GSMimeHeader *hdr;
3853 NSData *boundary = 0;
3854 BOOL contentIsBinary = NO;
3855 BOOL contentIs7bit = YES;
3856 unsigned int count;
3857 unsigned int i;
3858 CREATE_AUTORELEASE_POOL(arp);
3859
3860 if (isOuter == YES)
3861 {
3862 /*
3863 * Ensure there is a mime version header.
3864 */
3865 hdr = [self headerNamed: @"mime-version"];
3866 if (hdr == nil)
3867 {
3868 hdr = [GSMimeHeader alloc];
3869 hdr = [hdr initWithName: @"mime-version"
3870 value: @"1.0"
3871 parameters: nil];
3872 [self addHeader: hdr];
3873 RELEASE(hdr);
3874 }
3875 }
3876
3877 if ([content isKindOfClass: [NSArray class]] == YES)
3878 {
3879 count = [content count];
3880 partData = [NSMutableArray arrayWithCapacity: count];
3881 for (i = 0; i < count; i++)
3882 {
3883 GSMimeDocument *part = [content objectAtIndex: i];
3884
3885 [partData addObject: [part rawMimeData: NO]];
3886
3887 /*
3888 * If any part of a multipart document is not 7bit then
3889 * the document as a whole must not be 7bit either.
3890 * It is important to check this *after* the part has been
3891 * processed by -rawMimeData:, so we know that the encoding
3892 * set for the part is valid.
3893 */
3894 if (contentIs7bit == YES)
3895 {
3896 NSString *v;
3897
3898 enc = [part headerNamed: @"content-transfer-encoding"];
3899 v = [enc value];
3900 if ([v isEqualToString: @"8bit"] == YES
3901 || [v isEqualToString: @"binary"] == YES)
3902 {
3903 contentIs7bit = NO;
3904 if ([v isEqualToString: @"binary"] == YES)
3905 {
3906 contentIsBinary = YES;
3907 }
3908 }
3909 }
3910 }
3911 }
3912
3913 type = [self headerNamed: @"content-type"];
3914 if (type == nil)
3915 {
3916 /*
3917 * Attempt to infer the content type from the content.
3918 */
3919 if (partData != nil)
3920 {
3921 [self setContent: content type: @"multipart/mixed" name: nil];
3922 }
3923 else if ([content isKindOfClass: [NSString class]] == YES)
3924 {
3925 [self setContent: content type: @"text/plain" name: nil];
3926 }
3927 else if ([content isKindOfClass: [NSData class]] == YES)
3928 {
3929 [self setContent: content
3930 type: @"application/octet-stream"
3931 name: nil];
3932 }
3933 else
3934 {
3935 [NSException raise: NSInternalInconsistencyException
3936 format: @"[%@ -%@] with bad content",
3937 NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
3938 }
3939 type = [self headerNamed: @"content-type"];
3940 }
3941
3942 if (partData != nil)
3943 {
3944 NSString *v;
3945 BOOL shouldSet;
3946
3947 enc = [self headerNamed: @"content-transfer-encoding"];
3948 v = [enc value];
3949 if ([v isEqualToString: @"binary"])
3950 {
3951 /*
3952 * For binary encoding, we can just accept the setting.
3953 */
3954 shouldSet = NO;
3955 }
3956 else if ([v isEqualToString: @"8bit"])
3957 {
3958 if (contentIsBinary == YES)
3959 {
3960 shouldSet = YES; // Need to promote from 8bit to binary
3961 }
3962 else
3963 {
3964 shouldSet = NO;
3965 }
3966 }
3967 else if (v == nil || [v isEqualToString: @"7bit"] == YES)
3968 {
3969 /*
3970 * For 7bit encoding, we can accept the setting if the content
3971 * is all 7bit data, otherwise we must change it to 8bit so
3972 * that the content can be handled properly.
3973 */
3974 if (contentIs7bit == YES)
3975 {
3976 shouldSet = NO;
3977 }
3978 else
3979 {
3980 shouldSet = YES;
3981 }
3982 }
3983 else
3984 {
3985 /*
3986 * A multipart document can't have any other encoding, so we need
3987 * to fix it.
3988 */
3989 shouldSet = YES;
3990 }
3991
3992 if (shouldSet == YES)
3993 {
3994 NSString *encoding;
3995
3996 /*
3997 * Force a change to the current transfer encoding setting.
3998 */
3999 if (contentIs7bit == YES)
4000 {
4001 encoding = @"7bit";
4002 }
4003 else if (contentIsBinary == YES)
4004 {
4005 encoding = @"binary";
4006 }
4007 else
4008 {
4009 encoding = @"8bit";
4010 }
4011 if (enc == nil)
4012 {
4013 enc = [GSMimeHeader alloc];
4014 enc = [enc initWithName: @"content-transfer-encoding"
4015 value: encoding
4016 parameters: nil];
4017 [self setHeader: enc];
4018 RELEASE(enc);
4019 }
4020 else
4021 {
4022 [enc setValue: encoding];
4023 }
4024 }
4025
4026 v = [type parameterForKey: @"boundary"];
4027 if (v == nil)
4028 {
4029 v = [self makeBoundary];
4030 [type setParameter: v forKey: @"boundary"];
4031 }
4032 boundary = [v dataUsingEncoding: NSASCIIStringEncoding];
4033
4034 v = [type objectForKey: @"Subtype"];
4035 if ([v isEqualToString: @"related"] == YES)
4036 {
4037 GSMimeDocument *start;
4038
4039 v = [type parameterForKey: @"start"];
4040 if (v == nil)
4041 {
4042 start = [content objectAtIndex: 0];
4043 #if 0
4044 /*
4045 * The 'start' parameter is not compulsory ... should we
4046 * force it to be set anyway in case some dumb software
4047 * doesn't default to the first part of the message?
4048 */
4049 v = [start contentID];
4050 if (v == nil)
4051 {
4052 hdr = [start makeContentID];
4053 v = [hdr value];
4054 }
4055 [type setParameter: v forKey: @"start"];
4056 #endif
4057 }
4058 else
4059 {
4060 start = [self contentByID: v];
4061 }
4062 hdr = [start headerNamed: @"content-type"];
4063 v = [hdr value];
4064 /*
4065 * If there is no 'type' parameter, we can fill it in automatically.
4066 */
4067 if ([type parameterForKey: @"type"] == nil)
4068 {
4069 [type setParameter: v forKey: @"type"];
4070 }
4071 if ([v isEqual: [type parameterForKey: @"type"]] == NO)
4072 {
4073 [NSException raise: NSInvalidArgumentException
4074 format: @"multipart/related 'type' (%@) does not match "
4075 @"that of the 'start' part (%@)",
4076 [type parameterForKey: @"type"], v];
4077 }
4078 }
4079 }
4080 else
4081 {
4082 NSString *encoding;
4083
4084 d = [self convertToData];
4085 enc = [self headerNamed: @"content-transfer-encoding"];
4086 encoding = [enc value];
4087 if (encoding == nil)
4088 {
4089 if ([[type objectForKey: @"Type"] isEqualToString: @"text"] == YES)
4090 {
4091 NSString *charset = [type parameterForKey: @"charset"];
4092
4093 if (charset != nil
4094 && [charset isEqualToString: @"ascii"] == NO
4095 && [charset isEqualToString: @"us-ascii"] == NO)
4096 {
4097 encoding = @"8bit";
4098 enc = [GSMimeHeader alloc];
4099 enc = [enc initWithName: @"content-transfer-encoding"
4100 value: encoding
4101 parameters: nil];
4102 [self addHeader: enc];
4103 RELEASE(enc);
4104 }
4105 }
4106 else
4107 {
4108 enc = [GSMimeHeader alloc];
4109 enc = [enc initWithName: @"content-transfer-encoding"
4110 value: @"base64"
4111 parameters: nil];
4112 [self addHeader: enc];
4113 RELEASE(enc);
4114 }
4115 }
4116
4117 if (encoding == nil
4118 || [encoding isEqualToString: @"7bit"] == YES
4119 || [encoding isEqualToString: @"8bit"] == YES)
4120 {
4121 unsigned char *bytes = (unsigned char*)[d bytes];
4122 unsigned length = [d length];
4123 BOOL hadCarriageReturn = NO;
4124 unsigned lineLength = 0;
4125 unsigned i;
4126
4127 for (i = 0; i < length; i++)
4128 {
4129 unsigned char c = bytes[i];
4130
4131 if (hadCarriageReturn == YES)
4132 {
4133 if (c != '\n')
4134 {
4135 encoding = @"binary"; // CR not part of CRLF
4136 break;
4137 }
4138 hadCarriageReturn = NO;
4139 lineLength = 0;
4140 }
4141 else if (c == '\n')
4142 {
4143 encoding = @"binary"; // LF not part of CRLF
4144 break;
4145 }
4146 else if (c == '\r')
4147 {
4148 hadCarriageReturn = YES;
4149 }
4150 else if (++lineLength > 998)
4151 {
4152 encoding = @"binary"; // Line of more than 998
4153 break;
4154 }
4155
4156 if (c == 0)
4157 {
4158 encoding = @"binary";
4159 break;
4160 }
4161 else if (c > 127)
4162 {
4163 encoding = @"8bit"; // Not 7bit data
4164 }
4165 }
4166
4167 if (encoding != nil)
4168 {
4169 if (enc == nil)
4170 {
4171 enc = [GSMimeHeader alloc];
4172 enc = [enc initWithName: @"content-transfer-encoding"
4173 value: encoding
4174 parameters: nil];
4175 [self addHeader: enc];
4176 RELEASE(enc);
4177 }
4178 else
4179 {
4180 [enc setValue: encoding];
4181 }
4182 }
4183 }
4184 }
4185
4186 /*
4187 * Add all the headers.
4188 */
4189 enumerator = [headers objectEnumerator];
4190 while ((hdr = [enumerator nextObject]) != nil)
4191 {
4192 [md appendData: [hdr rawMimeData]];
4193 }
4194
4195 if (partData != nil)
4196 {
4197 count = [content count];
4198 for (i = 0; i < count; i++)
4199 {
4200 GSMimeDocument *part = [content objectAtIndex: i];
4201 NSMutableData *rawPart = [partData objectAtIndex: i];
4202
4203 if (contentIs7bit == YES)
4204 {
4205 NSString *v;
4206
4207 enc = [part headerNamed: @"content-transport-encoding"];
4208 v = [enc value];
4209 if (v != nil && ([v isEqualToString: @"8bit"]
4210 || [v isEqualToString: @"binary"]))
4211 {
4212 [NSException raise: NSInternalInconsistencyException
4213 format: @"[%@ -%@] bad part encoding for 7bit container",
4214 NSStringFromClass([self class]),
4215 NSStringFromSelector(_cmd)];
4216 }
4217 }
4218 /*
4219 * For a multipart document, insert the boundary before each part.
4220 */
4221 [md appendBytes: "\r\n--" length: 4];
4222 [md appendData: boundary];
4223 [md appendBytes: "\r\n" length: 2];
4224 [md appendData: rawPart];
4225 }
4226 [md appendBytes: "\r\n--" length: 4];
4227 [md appendData: boundary];
4228 [md appendBytes: "--\r\n" length: 4];
4229 }
4230 else
4231 {
4232 /*
4233 * Separate headers from body.
4234 */
4235 [md appendBytes: "\r\n" length: 2];
4236
4237 if ([[enc value] isEqualToString: @"base64"] == YES)
4238 {
4239 const char *ptr;
4240 unsigned len;
4241 unsigned pos = 0;
4242
4243 d = [GSMimeDocument encodeBase64: d];
4244 ptr = [d bytes];
4245 len = [d length];
4246
4247 while (len - pos > 76)
4248 {
4249 [md appendBytes: &ptr[pos] length: 76];
4250 [md appendBytes: "\r\n" length: 2];
4251 pos += 76;
4252 }
4253 [md appendBytes: &ptr[pos] length: len-pos];
4254 }
4255 else
4256 {
4257 [md appendData: d];
4258 }
4259 }
4260 RELEASE(arp);
4261 return md;
4262 }
4263
4264 /**
4265 * Sets a new value for the content of the document.
4266 */
4267 - (void) setContent: (id)newContent
4268 {
4269 if ([newContent isKindOfClass: [NSString class]] == YES)
4270 {
4271 if (newContent != content)
4272 {
4273 ASSIGNCOPY(content, newContent);
4274 }
4275 }
4276 else if ([newContent isKindOfClass: [NSData class]] == YES)
4277 {
4278 if (newContent != content)
4279 {
4280 ASSIGNCOPY(content, newContent);
4281 }
4282 }
4283 else if ([newContent isKindOfClass: [NSArray class]] == YES)
4284 {
4285 if (newContent != content)
4286 {
4287 newContent = [newContent mutableCopy];
4288 ASSIGN(content, newContent);
4289 RELEASE(newContent);
4290 }
4291 }
4292 else
4293 {
4294 [NSException raise: NSInvalidArgumentException
4295 format: @"[%@ -%@] passed bad content",
4296 NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
4297 }
4298 }
4299
4300 /**
4301 * Convenience method calling -setContent:type:name: to set document
4302 * content and type with a nil value for name ... useful for top-level
4303 * documents rather than parts within a document (parts should really
4304 * be named).
4305 */
4306 - (void) setContent: (id)newContent
4307 type: (NSString*)type
4308 {
4309 [self setContent: newContent type: type name: nil];
4310 }
4311
4312 /**
4313 * <p>Convenience method to set the content of the document along with
4314 * creating a content-type header for it.
4315 * </p>
4316 * <p>The type parameter may be a simple common content type (text,
4317 * multipart, or application), in which case the default subtype for
4318 * that type is used. Alternatively it may be full detail of a
4319 * content type header value, which will be parsed into 'type', 'subtype'
4320 * and 'parameters'.<br />
4321 * NB. In this case, if the parsed data contains a 'name' parameter
4322 * and the name argument is non-nil, the argument value will
4323 * override the parsed value.
4324 * </p>
4325 * <p>You can get the same effect by calling -setContent: to set the document
4326 * content, then creating a [GSMimeHeader] instance, initialising it with
4327 * the content type information you want using
4328 * [GSMimeHeader-initWithName:value:parameters:], and calling the
4329 * -setHeader: method to attach it to the document.
4330 * </p>
4331 * <p>Using this method imposes a few extra checks and restrictions on the
4332 * combination of content and type/subtype you may use ... so you may want
4333 * to use the more primitive methods in order to bypass these checks if
4334 * you are using unusual type/subtype information or if you need to provide
4335 * additional parameters in the header.
4336 * </p>
4337 */
4338 - (void) setContent: (id)newContent
4339 type: (NSString*)type
4340 name: (NSString*)name
4341 {
4342 CREATE_AUTORELEASE_POOL(arp);
4343 NSString *subtype = nil;
4344 GSMimeHeader *hdr = nil;
4345
4346 if (type == nil)
4347 {
4348 type = @"text";
4349 }
4350
4351 if ([type isEqualToString: @"text"] == YES)
4352 {
4353 subtype = @"plain";
4354 }
4355 else if ([type isEqualToString: @"multipart"] == YES)
4356 {
4357 subtype = @"mixed";
4358 }
4359 else if ([type isEqualToString: @"application"] == YES)
4360 {
4361 subtype = @"octet-stream";
4362 }
4363 else
4364 {
4365 GSMimeParser *p = AUTORELEASE([GSMimeParser new]);
4366 NSScanner *scanner = [NSScanner scannerWithString: type];
4367
4368 hdr = AUTORELEASE([GSMimeHeader new]);
4369 [hdr setName: @"content-type"];
4370 if ([p scanHeaderBody: scanner into: hdr] == NO)
4371 {
4372 [NSException raise: NSInvalidArgumentException
4373 format: @"Unable to parse type information"];
4374 }
4375 }
4376
4377 if (hdr == nil)
4378 {
4379 NSString *val;
4380
4381 val = [NSString stringWithFormat: @"%@/%@", type, subtype];
4382 hdr = [GSMimeHeader alloc];
4383 hdr = [hdr initWithName: @"content-type" value: val parameters: nil];
4384 [hdr setObject: type forKey: @"Type"];
4385 [hdr setObject: subtype forKey: @"Subtype"];
4386 AUTORELEASE(hdr);
4387 }
4388 else
4389 {
4390 type = [hdr objectForKey: @"Type"];
4391 subtype = [hdr objectForKey: @"Subtype"];
4392 }
4393
4394 if (name != nil)
4395 {
4396 [hdr setParameter: name forKey: @"name"];
4397 }
4398
4399 if ([type isEqualToString: @"multipart"] == NO
4400 && [type isEqualToString: @"application"] == NO
4401 && [content isKindOfClass: [NSArray class]] == YES)
4402 {
4403 [NSException raise: NSInvalidArgumentException
4404 format: @"[%@ -%@] content doesn't match content-type",
4405 NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
4406 }
4407
4408 [self setContent: newContent];
4409 [self setHeader: hdr];
4410 RELEASE(arp);
4411 }
4412
4413 /**
4414 * This method may be called to set a header in the document.
4415 * Any other headers with the same name will be removed from
4416 * the document.
4417 */
4418 - (void) setHeader: (GSMimeHeader*)info
4419 {
4420 NSString *name = [info name];
4421
4422 if (name != nil)
4423 {
4424 unsigned count = [headers count];
4425
4426 /*
4427 * Remove any existing headers with this name.
4428 */
4429 while (count-- > 0)
4430 {
4431 GSMimeHeader *tmp = [headers objectAtIndex: count];
4432
4433 if ([name isEqualToString: [tmp name]] == YES)
4434 {
4435 [headers removeObjectAtIndex: count];
4436 }
4437 }
4438 }
4439 [self addHeader: info];
4440 }
4441
4442 @end
4443
4444 @implementation GSMimeDocument (Private)
4445 /**
4446 * Returns the index of the first header matching the specified name
4447 * or NSNotFound if no match is found.<br />
4448 * NB. The supplied name <em>must</em> be lowercase.<br />
4449 * This method is for internal use
4450 */
4451 - (unsigned) _indexOfHeaderNamed: (NSString*)name
4452 {
4453 unsigned count = [headers count];
4454 unsigned index;
4455
4456 for (index = 0; index < count; index++)
4457 {
4458 GSMimeHeader *hdr = [headers objectAtIndex: index];
4459
4460 if ([name isEqualToString: [hdr name]] == YES)
4461 {
4462 return index;
4463 }
4464 }
4465 return NSNotFound;
4466 }
4467
4468 @end
4469

savannah-hackers-public@gnu.org
ViewVC Help
Powered by ViewVC 1.1.26