/[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.82 - (show annotations) (download)
Thu Oct 2 16:50:49 2003 UTC (20 years, 8 months ago) by CaS
Branch: MAIN
Changes since 1.81: +2 -1 lines
Fixed minor memory leak.

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/09/30 17:47:35 $ $Revision: 1.81 $
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 * Wraps the supplied key in angle brackets if they are not present.<br />
3381 * Return nil if no match is found, the matching GSMimeDocument otherwise.
3382 */
3383 - (id) contentByID: (NSString*)key
3384 {
3385 if ([key hasPrefix: @"<"] == NO)
3386 {
3387 key = [NSString stringWithFormat: @"<%@>", key];
3388 }
3389 if ([content isKindOfClass: [NSArray class]] == YES)
3390 {
3391 NSEnumerator *e = [content objectEnumerator];
3392 GSMimeDocument *d;
3393
3394 while ((d = [e nextObject]) != nil)
3395 {
3396 if ([[d contentID] isEqualToString: key] == YES)
3397 {
3398 return d;
3399 }
3400 d = [d contentByID: key];
3401 if (d != nil)
3402 {
3403 return d;
3404 }
3405 }
3406 }
3407 return nil;
3408 }
3409
3410 /**
3411 * Search the content of this document to locate a part whose content-type
3412 * name or content-disposition name matches the specified key.
3413 * Recursively descend into other documents.<br />
3414 * Return nil if no match is found, the matching GSMimeDocument otherwise.
3415 */
3416 - (id) contentByName: (NSString*)key
3417 {
3418
3419 if ([content isKindOfClass: [NSArray class]] == YES)
3420 {
3421 NSEnumerator *e = [content objectEnumerator];
3422 GSMimeDocument *d;
3423
3424 while ((d = [e nextObject]) != nil)
3425 {
3426 GSMimeHeader *hdr;
3427
3428 hdr = [d headerNamed: @"content-type"];
3429 if ([[hdr parameterForKey: @"name"] isEqualToString: key] == YES)
3430 {
3431 return d;
3432 }
3433 hdr = [d headerNamed: @"content-disposition"];
3434 if ([[hdr parameterForKey: @"name"] isEqualToString: key] == YES)
3435 {
3436 return d;
3437 }
3438 d = [d contentByName: key];
3439 if (d != nil)
3440 {
3441 return d;
3442 }
3443 }
3444 }
3445 return nil;
3446 }
3447
3448 /**
3449 * Convenience method to fetch the content file name from the header.
3450 */
3451 - (NSString*) contentFile
3452 {
3453 GSMimeHeader *hdr = [self headerNamed: @"content-disposition"];
3454
3455 return [hdr parameterForKey: @"filename"];
3456 }
3457
3458 /**
3459 * Convenience method to fetch the content ID from the header.
3460 */
3461 - (NSString*) contentID
3462 {
3463 GSMimeHeader *hdr = [self headerNamed: @"content-id"];
3464
3465 return [hdr value];
3466 }
3467
3468 /**
3469 * Convenience method to fetch the content name from the header.
3470 */
3471 - (NSString*) contentName
3472 {
3473 GSMimeHeader *hdr = [self headerNamed: @"content-type"];
3474
3475 return [hdr parameterForKey: @"name"];
3476 }
3477
3478 /**
3479 * Convenience method to fetch the content sub-type from the header.
3480 */
3481 - (NSString*) contentSubtype
3482 {
3483 GSMimeHeader *hdr = [self headerNamed: @"content-type"];
3484
3485 return [hdr objectForKey: @"Subtype"];
3486 }
3487
3488 /**
3489 * Convenience method to fetch the content type from the header.
3490 */
3491 - (NSString*) contentType
3492 {
3493 GSMimeHeader *hdr = [self headerNamed: @"content-type"];
3494
3495 return [hdr objectForKey: @"Type"];
3496 }
3497
3498 /**
3499 * Search the content of this document to locate all parts whose content-type
3500 * name or content-disposition name matches the specified key.
3501 * Do <em>NOT</em> recurse into other documents.<br />
3502 * Return nil if no match is found, an array of matching GSMimeDocument
3503 * instances otherwise.
3504 */
3505 - (NSArray*) contentsByName: (NSString*)key
3506 {
3507 NSMutableArray *a = nil;
3508
3509 if ([content isKindOfClass: [NSArray class]] == YES)
3510 {
3511 NSEnumerator *e = [content objectEnumerator];
3512 GSMimeDocument *d;
3513
3514 while ((d = [e nextObject]) != nil)
3515 {
3516 GSMimeHeader *hdr;
3517 BOOL match = YES;
3518
3519 hdr = [d headerNamed: @"content-type"];
3520 if ([[hdr parameterForKey: @"name"] isEqualToString: key] == NO)
3521 {
3522 hdr = [d headerNamed: @"content-disposition"];
3523 if ([[hdr parameterForKey: @"name"] isEqualToString: key] == NO)
3524 {
3525 match = NO;
3526 }
3527 }
3528 if (match == YES)
3529 {
3530 if (a == nil)
3531 {
3532 a = [NSMutableArray arrayWithCapacity: 4];
3533 }
3534 [a addObject: d];
3535 }
3536 }
3537 }
3538 return a;
3539 }
3540
3541 /**
3542 * Return the content as an NSData object (unless it is multipart)<br />
3543 * Perform conversion from text to data using the charset specified in
3544 * the content-type header, or infer the charset, and update the header
3545 * accordingly.<br />
3546 * If the content can not be represented as a plain NSData object, this
3547 * method returns nil.
3548 */
3549 - (NSData*) convertToData
3550 {
3551 NSData *d = nil;
3552
3553 if ([content isKindOfClass: [NSString class]] == YES)
3554 {
3555 GSMimeHeader *hdr = [self headerNamed: @"content-type"];
3556 NSString *charset = [hdr parameterForKey: @"charset"];
3557 NSStringEncoding enc;
3558
3559 enc = [GSMimeDocument encodingFromCharset: charset];
3560 d = [content dataUsingEncoding: enc];
3561 if (d == nil)
3562 {
3563 charset = selectCharacterSet(content, &d);
3564 [hdr setParameter: charset forKey: @"charset"];
3565 }
3566 }
3567 else if ([content isKindOfClass: [NSData class]] == YES)
3568 {
3569 d = content;
3570 }
3571 return d;
3572 }
3573
3574 /**
3575 * Return the content as an NSString object (unless it is multipart)
3576 * If the content cannot be represented as text, this returns nil.
3577 */
3578 - (NSString*) convertToText
3579 {
3580 NSString *s = nil;
3581
3582 if ([content isKindOfClass: [NSString class]] == YES)
3583 {
3584 s = content;
3585 }
3586 else if ([content isKindOfClass: [NSData class]] == YES)
3587 {
3588 GSMimeHeader *hdr = [self headerNamed: @"content-type"];
3589 NSString *charset = [hdr parameterForKey: @"charset"];
3590 NSStringEncoding enc;
3591
3592 enc = [GSMimeDocument encodingFromCharset: charset];
3593 s = [[NSString alloc] initWithData: content encoding: enc];
3594 AUTORELEASE(s);
3595 }
3596 return s;
3597 }
3598
3599 /**
3600 * Returns a copy of the receiver.
3601 */
3602 - (id) copyWithZone: (NSZone*)z
3603 {
3604 GSMimeDocument *c = [GSMimeDocument allocWithZone: z];
3605
3606 c->headers = [[NSMutableArray allocWithZone: z] initWithArray: headers
3607 copyItems: YES];
3608
3609 if ([content isKindOfClass: [NSArray class]] == YES)
3610 {
3611 c->content = [[NSMutableArray allocWithZone: z] initWithArray: content
3612 copyItems: YES];
3613 }
3614 else
3615 {
3616 c->content = [content copyWithZone: z];
3617 }
3618 return c;
3619 }
3620
3621 - (void) dealloc
3622 {
3623 RELEASE(headers);
3624 RELEASE(content);
3625 [super dealloc];
3626 }
3627
3628 /**
3629 * This method removes all occurrances of header objects identical to
3630 * the one supplied as an argument.
3631 */
3632 - (void) deleteHeader: (GSMimeHeader*)aHeader
3633 {
3634 unsigned count = [headers count];
3635
3636 while (count-- > 0)
3637 {
3638 if ([aHeader isEqual: [headers objectAtIndex: count]] == YES)
3639 {
3640 [headers removeObjectAtIndex: count];
3641 }
3642 }
3643 }
3644
3645 /**
3646 * This method removes all occurrances of headers whose name
3647 * matches the supplied string.
3648 */
3649 - (void) deleteHeaderNamed: (NSString*)name
3650 {
3651 unsigned count = [headers count];
3652
3653 name = [name lowercaseString];
3654 while (count-- > 0)
3655 {
3656 GSMimeHeader *info = [headers objectAtIndex: count];
3657
3658 if ([name isEqualToString: [info name]] == YES)
3659 {
3660 [headers removeObjectAtIndex: count];
3661 }
3662 }
3663 }
3664
3665 - (NSString*) description
3666 {
3667 NSMutableString *desc;
3668 NSDictionary *locale;
3669
3670 desc = [NSMutableString stringWithFormat: @"GSMimeDocument <%0x> -\n", self];
3671 locale = [[NSUserDefaults standardUserDefaults] dictionaryRepresentation];
3672 [desc appendString: [headers descriptionWithLocale: locale]];
3673 [desc appendFormat: @"\nDocument content -\n%@", content];
3674 return desc;
3675 }
3676
3677 /**
3678 * This method returns the first header whose name equals the supplied argument.
3679 */
3680 - (GSMimeHeader*) headerNamed: (NSString*)name
3681 {
3682 NSArray *a = [self headersNamed: name];
3683
3684 if ([a count] > 0)
3685 {
3686 return [a objectAtIndex: 0];
3687 }
3688 return nil;
3689 }
3690
3691 /**
3692 * This method returns an array of GSMimeHeader objects for all headers
3693 * whose names equal the supplied argument.
3694 */
3695 - (NSArray*) headersNamed: (NSString*)name
3696 {
3697 unsigned count = [headers count];
3698 unsigned index;
3699 NSMutableArray *array;
3700
3701 name = [GSMimeHeader makeToken: name];
3702 array = [NSMutableArray array];
3703 for (index = 0; index < count; index++)
3704 {
3705 GSMimeHeader *info = [headers objectAtIndex: index];
3706
3707 if ([name isEqualToString: [info name]] == YES)
3708 {
3709 [array addObject: info];
3710 }
3711 }
3712 return array;
3713 }
3714
3715 - (id) init
3716 {
3717 if ((self = [super init]) != nil)
3718 {
3719 headers = [NSMutableArray new];
3720 }
3721 return self;
3722 }
3723
3724 /**
3725 * <p>Make a probably unique string suitable for use as the
3726 * boundary parameter in the content of a multipart document.
3727 * </p>
3728 * <p>This implementation provides base64 encoded data
3729 * consisting of an MD5 digest of some pseudo random stuff,
3730 * plus an incrementing counter. The inclusion of the counter
3731 * guarantees that we won't produce two identical strings in
3732 * the same run of the program.
3733 * </p>
3734 */
3735 - (NSString*) makeBoundary
3736 {
3737 static int count = 0;
3738 unsigned char output[20];
3739 NSMutableData *md;
3740 NSString *result;
3741 NSData *source;
3742 NSData *digest;
3743 int sequence = ++count;
3744
3745 source = [[[NSProcessInfo processInfo] globallyUniqueString]
3746 dataUsingEncoding: NSUTF8StringEncoding];
3747 digest = [source md5Digest];
3748 memcpy(output, [digest bytes], 16);
3749 output[16] = (sequence >> 24) & 0xff;
3750 output[17] = (sequence >> 16) & 0xff;
3751 output[18] = (sequence >> 8) & 0xff;
3752 output[19] = sequence & 0xff;
3753
3754 md = [[NSMutableData alloc] initWithLength: 40];
3755 [md setLength: encodebase64([md mutableBytes], output, 20)];
3756 result = [[NSString alloc] initWithData: md encoding: NSASCIIStringEncoding];
3757 RELEASE(md);
3758 return AUTORELEASE(result);
3759 }
3760
3761 /**
3762 * Create new content ID header, set it as the content ID of the document
3763 * and return it.<br />
3764 * This is a convenience method which simply places angle brackets around
3765 * an [NSProcessInfo-globallyUniqueString] to form the header value.
3766 */
3767 - (GSMimeHeader*) makeContentID
3768 {
3769 GSMimeHeader *hdr;
3770 NSString *str = [[NSProcessInfo processInfo] globallyUniqueString];
3771
3772 str = [NSString stringWithFormat: @"<%@>", str];
3773 hdr = [[GSMimeHeader alloc] initWithName: @"content-id"
3774 value: str
3775 parameters: nil];
3776 [self setHeader: hdr];
3777 RELEASE(hdr);
3778 return hdr;
3779 }
3780
3781 /**
3782 * Convenience method to create a new header and add it to the receiver
3783 * replacing any existing header of the same name.<br />
3784 * Returns the newly created header.<br />
3785 * See [GSMimeHeader-initWithName:value:parameters:] and -setHeader: methods.
3786 */
3787 - (GSMimeHeader*) makeHeader: (NSString*)name
3788 value: (NSString*)value
3789 parameters: (NSDictionary*)parameters
3790 {
3791 GSMimeHeader *hdr;
3792
3793 hdr = [[GSMimeHeader alloc] initWithName: name
3794 value: value
3795 parameters: parameters];
3796 [self setHeader: hdr];
3797 RELEASE(hdr);
3798 return hdr;
3799 }
3800
3801 /**
3802 * Create new message ID header, set it as the message ID of the document
3803 * and return it.<br />
3804 * This is a convenience method which simply places angle brackets around
3805 * an [NSProcessInfo-globallyUniqueString] to form the header value.
3806 */
3807 - (GSMimeHeader*) makeMessageID
3808 {
3809 GSMimeHeader *hdr;
3810 NSString *str = [[NSProcessInfo processInfo] globallyUniqueString];
3811
3812 str = [NSString stringWithFormat: @"<%@>", str];
3813 hdr = [[GSMimeHeader alloc] initWithName: @"message-id"
3814 value: str
3815 parameters: nil];
3816 [self setHeader: hdr];
3817 RELEASE(hdr);
3818 return hdr;
3819 }
3820
3821 /**
3822 * Return an NSData object representing the MIME document as raw data
3823 * ready to be sent via an email system.<br />
3824 * Calls -rawMimeData: with the isOuter flag set to YES.
3825 */
3826 - (NSMutableData*) rawMimeData
3827 {
3828 return [self rawMimeData: YES];
3829 }
3830
3831 /**
3832 * <p>Return an NSData object representing the MIME document as raw data
3833 * ready to be sent via an email system.
3834 * </p>
3835 * <p>The isOuter flag denotes whether this document is the outermost
3836 * part of a MIME message, or is a part of a multipart message.
3837 * </p>
3838 * <p>During generation of the document this method will perform some
3839 * consistency checks and try to automatically generate missing header
3840 * information needed to build the mime data (eg. filling in the boundary
3841 * parameter in the content-type header for multipart documents).<br />
3842 * However, you should not depend on automatic behaviors but should
3843 * fill in as much detail as possible before generating data.
3844 * </p>
3845 */
3846 - (NSMutableData*) rawMimeData: (BOOL)isOuter
3847 {
3848 NSMutableArray *partData = nil;
3849 NSMutableData *md = [NSMutableData dataWithCapacity: 1024];
3850 NSData *d = nil;
3851 NSEnumerator *enumerator;
3852 GSMimeHeader *type;
3853 GSMimeHeader *enc;
3854 GSMimeHeader *hdr;
3855 NSData *boundary = 0;
3856 BOOL contentIsBinary = NO;
3857 BOOL contentIs7bit = YES;
3858 unsigned int count;
3859 unsigned int i;
3860 CREATE_AUTORELEASE_POOL(arp);
3861
3862 if (isOuter == YES)
3863 {
3864 /*
3865 * Ensure there is a mime version header.
3866 */
3867 hdr = [self headerNamed: @"mime-version"];
3868 if (hdr == nil)
3869 {
3870 hdr = [GSMimeHeader alloc];
3871 hdr = [hdr initWithName: @"mime-version"
3872 value: @"1.0"
3873 parameters: nil];
3874 [self addHeader: hdr];
3875 RELEASE(hdr);
3876 }
3877 }
3878
3879 if ([content isKindOfClass: [NSArray class]] == YES)
3880 {
3881 count = [content count];
3882 partData = [NSMutableArray arrayWithCapacity: count];
3883 for (i = 0; i < count; i++)
3884 {
3885 GSMimeDocument *part = [content objectAtIndex: i];
3886
3887 [partData addObject: [part rawMimeData: NO]];
3888
3889 /*
3890 * If any part of a multipart document is not 7bit then
3891 * the document as a whole must not be 7bit either.
3892 * It is important to check this *after* the part has been
3893 * processed by -rawMimeData:, so we know that the encoding
3894 * set for the part is valid.
3895 */
3896 if (contentIs7bit == YES)
3897 {
3898 NSString *v;
3899
3900 enc = [part headerNamed: @"content-transfer-encoding"];
3901 v = [enc value];
3902 if ([v isEqualToString: @"8bit"] == YES
3903 || [v isEqualToString: @"binary"] == YES)
3904 {
3905 contentIs7bit = NO;
3906 if ([v isEqualToString: @"binary"] == YES)
3907 {
3908 contentIsBinary = YES;
3909 }
3910 }
3911 }
3912 }
3913 }
3914
3915 type = [self headerNamed: @"content-type"];
3916 if (type == nil)
3917 {
3918 /*
3919 * Attempt to infer the content type from the content.
3920 */
3921 if (partData != nil)
3922 {
3923 [self setContent: content type: @"multipart/mixed" name: nil];
3924 }
3925 else if ([content isKindOfClass: [NSString class]] == YES)
3926 {
3927 [self setContent: content type: @"text/plain" name: nil];
3928 }
3929 else if ([content isKindOfClass: [NSData class]] == YES)
3930 {
3931 [self setContent: content
3932 type: @"application/octet-stream"
3933 name: nil];
3934 }
3935 else
3936 {
3937 [NSException raise: NSInternalInconsistencyException
3938 format: @"[%@ -%@] with bad content",
3939 NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
3940 }
3941 type = [self headerNamed: @"content-type"];
3942 }
3943
3944 if (partData != nil)
3945 {
3946 NSString *v;
3947 BOOL shouldSet;
3948
3949 enc = [self headerNamed: @"content-transfer-encoding"];
3950 v = [enc value];
3951 if ([v isEqualToString: @"binary"])
3952 {
3953 /*
3954 * For binary encoding, we can just accept the setting.
3955 */
3956 shouldSet = NO;
3957 }
3958 else if ([v isEqualToString: @"8bit"])
3959 {
3960 if (contentIsBinary == YES)
3961 {
3962 shouldSet = YES; // Need to promote from 8bit to binary
3963 }
3964 else
3965 {
3966 shouldSet = NO;
3967 }
3968 }
3969 else if (v == nil || [v isEqualToString: @"7bit"] == YES)
3970 {
3971 /*
3972 * For 7bit encoding, we can accept the setting if the content
3973 * is all 7bit data, otherwise we must change it to 8bit so
3974 * that the content can be handled properly.
3975 */
3976 if (contentIs7bit == YES)
3977 {
3978 shouldSet = NO;
3979 }
3980 else
3981 {
3982 shouldSet = YES;
3983 }
3984 }
3985 else
3986 {
3987 /*
3988 * A multipart document can't have any other encoding, so we need
3989 * to fix it.
3990 */
3991 shouldSet = YES;
3992 }
3993
3994 if (shouldSet == YES)
3995 {
3996 NSString *encoding;
3997
3998 /*
3999 * Force a change to the current transfer encoding setting.
4000 */
4001 if (contentIs7bit == YES)
4002 {
4003 encoding = @"7bit";
4004 }
4005 else if (contentIsBinary == YES)
4006 {
4007 encoding = @"binary";
4008 }
4009 else
4010 {
4011 encoding = @"8bit";
4012 }
4013 if (enc == nil)
4014 {
4015 enc = [GSMimeHeader alloc];
4016 enc = [enc initWithName: @"content-transfer-encoding"
4017 value: encoding
4018 parameters: nil];
4019 [self setHeader: enc];
4020 RELEASE(enc);
4021 }
4022 else
4023 {
4024 [enc setValue: encoding];
4025 }
4026 }
4027
4028 v = [type parameterForKey: @"boundary"];
4029 if (v == nil)
4030 {
4031 v = [self makeBoundary];
4032 [type setParameter: v forKey: @"boundary"];
4033 }
4034 boundary = [v dataUsingEncoding: NSASCIIStringEncoding];
4035
4036 v = [type objectForKey: @"Subtype"];
4037 if ([v isEqualToString: @"related"] == YES)
4038 {
4039 GSMimeDocument *start;
4040
4041 v = [type parameterForKey: @"start"];
4042 if (v == nil)
4043 {
4044 start = [content objectAtIndex: 0];
4045 #if 0
4046 /*
4047 * The 'start' parameter is not compulsory ... should we
4048 * force it to be set anyway in case some dumb software
4049 * doesn't default to the first part of the message?
4050 */
4051 v = [start contentID];
4052 if (v == nil)
4053 {
4054 hdr = [start makeContentID];
4055 v = [hdr value];
4056 }
4057 [type setParameter: v forKey: @"start"];
4058 #endif
4059 }
4060 else
4061 {
4062 start = [self contentByID: v];
4063 }
4064 hdr = [start headerNamed: @"content-type"];
4065 v = [hdr value];
4066 /*
4067 * If there is no 'type' parameter, we can fill it in automatically.
4068 */
4069 if ([type parameterForKey: @"type"] == nil)
4070 {
4071 [type setParameter: v forKey: @"type"];
4072 }
4073 if ([v isEqual: [type parameterForKey: @"type"]] == NO)
4074 {
4075 [NSException raise: NSInvalidArgumentException
4076 format: @"multipart/related 'type' (%@) does not match "
4077 @"that of the 'start' part (%@)",
4078 [type parameterForKey: @"type"], v];
4079 }
4080 }
4081 }
4082 else
4083 {
4084 NSString *encoding;
4085
4086 d = [self convertToData];
4087 enc = [self headerNamed: @"content-transfer-encoding"];
4088 encoding = [enc value];
4089 if (encoding == nil)
4090 {
4091 if ([[type objectForKey: @"Type"] isEqualToString: @"text"] == YES)
4092 {
4093 NSString *charset = [type parameterForKey: @"charset"];
4094
4095 if (charset != nil
4096 && [charset isEqualToString: @"ascii"] == NO
4097 && [charset isEqualToString: @"us-ascii"] == NO)
4098 {
4099 encoding = @"8bit";
4100 enc = [GSMimeHeader alloc];
4101 enc = [enc initWithName: @"content-transfer-encoding"
4102 value: encoding
4103 parameters: nil];
4104 [self addHeader: enc];
4105 RELEASE(enc);
4106 }
4107 }
4108 else
4109 {
4110 enc = [GSMimeHeader alloc];
4111 enc = [enc initWithName: @"content-transfer-encoding"
4112 value: @"base64"
4113 parameters: nil];
4114 [self addHeader: enc];
4115 RELEASE(enc);
4116 }
4117 }
4118
4119 if (encoding == nil
4120 || [encoding isEqualToString: @"7bit"] == YES
4121 || [encoding isEqualToString: @"8bit"] == YES)
4122 {
4123 unsigned char *bytes = (unsigned char*)[d bytes];
4124 unsigned length = [d length];
4125 BOOL hadCarriageReturn = NO;
4126 unsigned lineLength = 0;
4127 unsigned i;
4128
4129 for (i = 0; i < length; i++)
4130 {
4131 unsigned char c = bytes[i];
4132
4133 if (hadCarriageReturn == YES)
4134 {
4135 if (c != '\n')
4136 {
4137 encoding = @"binary"; // CR not part of CRLF
4138 break;
4139 }
4140 hadCarriageReturn = NO;
4141 lineLength = 0;
4142 }
4143 else if (c == '\n')
4144 {
4145 encoding = @"binary"; // LF not part of CRLF
4146 break;
4147 }
4148 else if (c == '\r')
4149 {
4150 hadCarriageReturn = YES;
4151 }
4152 else if (++lineLength > 998)
4153 {
4154 encoding = @"binary"; // Line of more than 998
4155 break;
4156 }
4157
4158 if (c == 0)
4159 {
4160 encoding = @"binary";
4161 break;
4162 }
4163 else if (c > 127)
4164 {
4165 encoding = @"8bit"; // Not 7bit data
4166 }
4167 }
4168
4169 if (encoding != nil)
4170 {
4171 if (enc == nil)
4172 {
4173 enc = [GSMimeHeader alloc];
4174 enc = [enc initWithName: @"content-transfer-encoding"
4175 value: encoding
4176 parameters: nil];
4177 [self addHeader: enc];
4178 RELEASE(enc);
4179 }
4180 else
4181 {
4182 [enc setValue: encoding];
4183 }
4184 }
4185 }
4186 }
4187
4188 /*
4189 * Add all the headers.
4190 */
4191 enumerator = [headers objectEnumerator];
4192 while ((hdr = [enumerator nextObject]) != nil)
4193 {
4194 [md appendData: [hdr rawMimeData]];
4195 }
4196
4197 if (partData != nil)
4198 {
4199 count = [content count];
4200 for (i = 0; i < count; i++)
4201 {
4202 GSMimeDocument *part = [content objectAtIndex: i];
4203 NSMutableData *rawPart = [partData objectAtIndex: i];
4204
4205 if (contentIs7bit == YES)
4206 {
4207 NSString *v;
4208
4209 enc = [part headerNamed: @"content-transport-encoding"];
4210 v = [enc value];
4211 if (v != nil && ([v isEqualToString: @"8bit"]
4212 || [v isEqualToString: @"binary"]))
4213 {
4214 [NSException raise: NSInternalInconsistencyException
4215 format: @"[%@ -%@] bad part encoding for 7bit container",
4216 NSStringFromClass([self class]),
4217 NSStringFromSelector(_cmd)];
4218 }
4219 }
4220 /*
4221 * For a multipart document, insert the boundary before each part.
4222 */
4223 [md appendBytes: "\r\n--" length: 4];
4224 [md appendData: boundary];
4225 [md appendBytes: "\r\n" length: 2];
4226 [md appendData: rawPart];
4227 }
4228 [md appendBytes: "\r\n--" length: 4];
4229 [md appendData: boundary];
4230 [md appendBytes: "--\r\n" length: 4];
4231 }
4232 else
4233 {
4234 /*
4235 * Separate headers from body.
4236 */
4237 [md appendBytes: "\r\n" length: 2];
4238
4239 if ([[enc value] isEqualToString: @"base64"] == YES)
4240 {
4241 const char *ptr;
4242 unsigned len;
4243 unsigned pos = 0;
4244
4245 d = [GSMimeDocument encodeBase64: d];
4246 ptr = [d bytes];
4247 len = [d length];
4248
4249 while (len - pos > 76)
4250 {
4251 [md appendBytes: &ptr[pos] length: 76];
4252 [md appendBytes: "\r\n" length: 2];
4253 pos += 76;
4254 }
4255 [md appendBytes: &ptr[pos] length: len-pos];
4256 }
4257 else
4258 {
4259 [md appendData: d];
4260 }
4261 }
4262 RELEASE(arp);
4263 return md;
4264 }
4265
4266 /**
4267 * Sets a new value for the content of the document.
4268 */
4269 - (void) setContent: (id)newContent
4270 {
4271 if ([newContent isKindOfClass: [NSString class]] == YES)
4272 {
4273 if (newContent != content)
4274 {
4275 ASSIGNCOPY(content, newContent);
4276 }
4277 }
4278 else if ([newContent isKindOfClass: [NSData class]] == YES)
4279 {
4280 if (newContent != content)
4281 {
4282 ASSIGNCOPY(content, newContent);
4283 }
4284 }
4285 else if ([newContent isKindOfClass: [NSArray class]] == YES)
4286 {
4287 if (newContent != content)
4288 {
4289 newContent = [newContent mutableCopy];
4290 ASSIGN(content, newContent);
4291 RELEASE(newContent);
4292 }
4293 }
4294 else
4295 {
4296 [NSException raise: NSInvalidArgumentException
4297 format: @"[%@ -%@] passed bad content",
4298 NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
4299 }
4300 }
4301
4302 /**
4303 * Convenience method calling -setContent:type:name: to set document
4304 * content and type with a nil value for name ... useful for top-level
4305 * documents rather than parts within a document (parts should really
4306 * be named).
4307 */
4308 - (void) setContent: (id)newContent
4309 type: (NSString*)type
4310 {
4311 [self setContent: newContent type: type name: nil];
4312 }
4313
4314 /**
4315 * <p>Convenience method to set the content of the document along with
4316 * creating a content-type header for it.
4317 * </p>
4318 * <p>The type parameter may be a simple common content type (text,
4319 * multipart, or application), in which case the default subtype for
4320 * that type is used. Alternatively it may be full detail of a
4321 * content type header value, which will be parsed into 'type', 'subtype'
4322 * and 'parameters'.<br />
4323 * NB. In this case, if the parsed data contains a 'name' parameter
4324 * and the name argument is non-nil, the argument value will
4325 * override the parsed value.
4326 * </p>
4327 * <p>You can get the same effect by calling -setContent: to set the document
4328 * content, then creating a [GSMimeHeader] instance, initialising it with
4329 * the content type information you want using
4330 * [GSMimeHeader-initWithName:value:parameters:], and calling the
4331 * -setHeader: method to attach it to the document.
4332 * </p>
4333 * <p>Using this method imposes a few extra checks and restrictions on the
4334 * combination of content and type/subtype you may use ... so you may want
4335 * to use the more primitive methods in order to bypass these checks if
4336 * you are using unusual type/subtype information or if you need to provide
4337 * additional parameters in the header.
4338 * </p>
4339 */
4340 - (void) setContent: (id)newContent
4341 type: (NSString*)type
4342 name: (NSString*)name
4343 {
4344 CREATE_AUTORELEASE_POOL(arp);
4345 NSString *subtype = nil;
4346 GSMimeHeader *hdr = nil;
4347
4348 if (type == nil)
4349 {
4350 type = @"text";
4351 }
4352
4353 if ([type isEqualToString: @"text"] == YES)
4354 {
4355 subtype = @"plain";
4356 }
4357 else if ([type isEqualToString: @"multipart"] == YES)
4358 {
4359 subtype = @"mixed";
4360 }
4361 else if ([type isEqualToString: @"application"] == YES)
4362 {
4363 subtype = @"octet-stream";
4364 }
4365 else
4366 {
4367 GSMimeParser *p = AUTORELEASE([GSMimeParser new]);
4368 NSScanner *scanner = [NSScanner scannerWithString: type];
4369
4370 hdr = AUTORELEASE([GSMimeHeader new]);
4371 [hdr setName: @"content-type"];
4372 if ([p scanHeaderBody: scanner into: hdr] == NO)
4373 {
4374 [NSException raise: NSInvalidArgumentException
4375 format: @"Unable to parse type information"];
4376 }
4377 }
4378
4379 if (hdr == nil)
4380 {
4381 NSString *val;
4382
4383 val = [NSString stringWithFormat: @"%@/%@", type, subtype];
4384 hdr = [GSMimeHeader alloc];
4385 hdr = [hdr initWithName: @"content-type" value: val parameters: nil];
4386 [hdr setObject: type forKey: @"Type"];
4387 [hdr setObject: subtype forKey: @"Subtype"];
4388 AUTORELEASE(hdr);
4389 }
4390 else
4391 {
4392 type = [hdr objectForKey: @"Type"];
4393 subtype = [hdr objectForKey: @"Subtype"];
4394 }
4395
4396 if (name != nil)
4397 {
4398 [hdr setParameter: name forKey: @"name"];
4399 }
4400
4401 if ([type isEqualToString: @"multipart"] == NO
4402 && [type isEqualToString: @"application"] == NO
4403 && [content isKindOfClass: [NSArray class]] == YES)
4404 {
4405 [NSException raise: NSInvalidArgumentException
4406 format: @"[%@ -%@] content doesn't match content-type",
4407 NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
4408 }
4409
4410 [self setContent: newContent];
4411 [self setHeader: hdr];
4412 RELEASE(arp);
4413 }
4414
4415 /**
4416 * This method may be called to set a header in the document.
4417 * Any other headers with the same name will be removed from
4418 * the document.
4419 */
4420 - (void) setHeader: (GSMimeHeader*)info
4421 {
4422 NSString *name = [info name];
4423
4424 if (name != nil)
4425 {
4426 unsigned count = [headers count];
4427
4428 /*
4429 * Remove any existing headers with this name.
4430 */
4431 while (count-- > 0)
4432 {
4433 GSMimeHeader *tmp = [headers objectAtIndex: count];
4434
4435 if ([name isEqualToString: [tmp name]] == YES)
4436 {
4437 [headers removeObjectAtIndex: count];
4438 }
4439 }
4440 }
4441 [self addHeader: info];
4442 }
4443
4444 @end
4445
4446 @implementation GSMimeDocument (Private)
4447 /**
4448 * Returns the index of the first header matching the specified name
4449 * or NSNotFound if no match is found.<br />
4450 * NB. The supplied name <em>must</em> be lowercase.<br />
4451 * This method is for internal use
4452 */
4453 - (unsigned) _indexOfHeaderNamed: (NSString*)name
4454 {
4455 unsigned count = [headers count];
4456 unsigned index;
4457
4458 for (index = 0; index < count; index++)
4459 {
4460 GSMimeHeader *hdr = [headers objectAtIndex: index];
4461
4462 if ([name isEqualToString: [hdr name]] == YES)
4463 {
4464 return index;
4465 }
4466 }
4467 return NSNotFound;
4468 }
4469
4470 @end
4471

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