2 Copyright (C) 2000-2005 SKYRIX Software AG
4 This file is part of SOPE.
6 SOPE is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with SOPE; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include "NGImap4ResponseNormalizer.h"
23 #include "NGImap4Client.h"
26 @interface NGImap4Client(UsedPrivates)
27 - (NSString *)delimiter;
28 - (NSString *)_imapFolder2Folder:(NSString *)_folder;
31 @implementation NGImap4ResponseNormalizer
33 static __inline__ NSArray *
34 _imapFlags2Flags(NGImap4ResponseNormalizer *, NSArray *);
36 static NSDictionary *VersionPrefixDict = nil;
38 static NSNumber *YesNumber = nil;
39 static NSNumber *NoNumber = nil;
40 static Class DictClass = Nil;
41 static Class StrClass = Nil;
42 static int LogImapEnabled = -1;
45 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
46 static BOOL didInit = NO;
50 YesNumber = [[NSNumber numberWithBool:YES] retain];
51 NoNumber = [[NSNumber numberWithBool:NO] retain];
53 DictClass = [NSDictionary class];
54 StrClass = [NSString class];
55 LogImapEnabled = [ud boolForKey:@"ImapLogEnabled"]?1:0;
58 cyrus - * OK defiant Cyrus IMAP4 v2.0.16 server ready
59 courier - * OK Courier-IMAP ready. Copyright 1998-2002 Double
60 Precision, Inc. See COPYING for distribution information.
62 if (VersionPrefixDict == nil) {
64 [[DictClass alloc] initWithObjectsAndKeys:
65 @"cyrus imap4 v", @"cyrus",
66 @" imap4rev1 ", @"washington",
67 @"courier", @"courier", nil];
71 - (id)initWithClient:(NGImap4Client *)_client {
72 if ((self = [super init])) {
73 self->client = _client; /* non-retained */
78 /* client callbacks */
80 - (void)closeConnection {
81 [(id)self->client closeConnection];
84 - (NSString *)delimiter {
85 return [self->client delimiter];
90 - (NSString *)_imapFolder2Folder:(NSString *)_folder {
91 return [self->client _imapFolder2Folder:_folder];
96 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
98 Filter for all responses
99 result : NSNumber (response result)
100 exists : NSNumber (number of exists mails in selected folder
101 recent : NSNumber (number of recent mails in selected folder
102 expunge : NSArray (message sequence number of expunged mails in selected
105 NSMutableDictionary *result;
107 NSDictionary *respRes;
110 return (id)[NSMutableDictionary dictionary];
112 respRes = [[_map objectEnumeratorForKey:@"ResponseResult"] nextObject];
113 result = [NSMutableDictionary dictionaryWithCapacity:32];
114 [result setObject:_map forKey:@"RawResponse"];
116 if ((obj = [_map objectForKey:@"bye"])) {
117 [result setObject:NoNumber forKey:@"result"];
118 [result setObject:obj forKey:@"reason"];
119 [self closeConnection];
123 if ([[respRes objectForKey:@"result"] isEqual:@"ok"]) {
124 [result setObject:YesNumber forKey:@"result"];
128 [result setObject:NoNumber forKey:@"result"];
129 if ((tmp = [respRes objectForKey:@"description"]) != nil) {
130 [result setObject:tmp forKey:@"reason"];
134 if ((obj = [[_map objectEnumeratorForKey:@"exists"] nextObject]) != nil) { //
135 [result setObject:obj forKey:@"exists"];
137 if ((obj = [[_map objectEnumeratorForKey:@"recent"] nextObject]) != nil) {
138 [result setObject:obj forKey:@"recent"];
140 if ((obj = [_map objectsForKey:@"expunge"]) != nil)
141 [result setObject:obj forKey:@"expunge"];
146 - (NSDictionary *)normalizeSortResponse:(NGHashMap *)_map {
147 /* filter for sort response (search : NSArray (msn)) */
149 NSMutableDictionary *result;
151 result = [self normalizeResponse:_map];
153 if ((obj = [[_map objectEnumeratorForKey:@"sort"] nextObject]) != nil)
154 [result setObject:obj forKey:@"sort"];
159 - (NSDictionary *)normalizeCapabilityRespone:(NGHashMap *)_map {
160 /* filter for capability response: capability : NSArray */
162 NSMutableDictionary *result;
164 result = [self normalizeResponse:_map];
166 if ((obj = [[_map objectEnumeratorForKey:@"capability"] nextObject]))
167 [result setObject:obj forKey:@"capability"];
172 - (NSDictionary *)normalizeThreadResponse:(NGHashMap *)_map {
173 /* filter for thread response: thread : NSArray (msn) */
175 NSMutableDictionary *result;
177 result = [self normalizeResponse:_map];
179 if ((obj = [[_map objectEnumeratorForKey:@"thread"] nextObject]))
180 [result setObject:obj forKey:@"thread"];
185 - (NSDictionary *)normalizeSearchResponse:(NGHashMap *)_map {
186 /* filter for search response: search : NSArray (msn) */
188 NSMutableDictionary *result;
190 result = [self normalizeResponse:_map];
192 if ((obj = [[_map objectEnumeratorForKey:@"search"] nextObject]))
193 [result setObject:obj forKey:@"search"];
198 - (NSDictionary *)normalizeSelectResponse:(NGHashMap *)_map {
200 filter for select response
203 access : NSString ([READ-WRITE], ... )
206 NSEnumerator *enumerator;
207 NSMutableDictionary *result;
210 result = [self normalizeResponse:_map];
212 if ((flags = [[_map objectEnumeratorForKey:@"flags"] nextObject]))
213 [result setObject:_imapFlags2Flags(self, flags) forKey:@"flags"];
215 enumerator = [_map objectEnumeratorForKey:@"ok"];
216 while ((obj = [enumerator nextObject])) {
219 if ((o = [obj objectForKey:@"unseen"]))
220 [result setObject:o forKey:@"unseen"];
223 enumerator = [_map objectEnumeratorForKey:@"no"];
224 while ((obj = [enumerator nextObject])) {
227 if ([obj isKindOfClass:DictClass]) {
228 if ((o = [obj objectForKey:@"ALERT"]))
229 [result setObject:o forKey:@"alert"];
232 [result setObject:obj forKey:@"alert"];
236 obj = [_map objectForKey:@"ResponseResult"];
237 if ((obj = [obj objectForKey:@"flag"]))
238 [result setObject:obj forKey:@"access"];
243 - (NSDictionary *)normalizeStatusResponse:(NGHashMap *)_map {
245 filter for status response
251 NSMutableDictionary *result;
254 result = [self normalizeResponse:_map];
256 obj = [[_map objectEnumeratorForKey:@"status"] nextObject];
257 obj = [obj objectForKey:@"flags"];
259 if ((o = [obj objectForKey:@"messages"]) != nil)
260 [result setObject:o forKey:@"messages"];
262 if ((o = [obj objectForKey:@"recent"]) != nil) {
263 if ([result objectForKey:@"recent"] == nil)
264 [result setObject:o forKey:@"recent"];
266 if ((o = [obj objectForKey:@"unseen"]) != nil)
267 [result setObject:o forKey:@"unseen"];
273 filter for fetch response
274 fetch : NSArray (fetch responses)
275 'header' - RFC822.HEADER
280 'msn' - message sequence number
282 'body' - (dictionary with bodystructure)
284 This walks over all 'fetch' responses in the map and adds a 'normalized'
285 dictionary for each response to the 'fetch' key of the normalized response
286 dictionary (as retrieved by 'normalizeResponse')
288 - (NSDictionary *)normalizeFetchResponsePart:(id)obj {
289 // TODO: shouldn't we use a specific object instead of NSDict for that?
291 NSEnumerator *keyEnum;
296 id (*objForKey)(id, SEL, id);
299 Process one 'fetch' reponse dictionary, walk over each key of the
300 dict and check for a collection of known response keys.
303 keyEnum = [obj keyEnumerator];
304 objForKey = (void *)[obj methodForSelector:@selector(objectForKey:)];
306 // TODO: this should add some error handling wrt the count?
307 // TODO: this could return multiple values for the same key?! => fix that
308 while (((key = [keyEnum nextObject]) != nil) && (count < 9)) {
312 if ((klen = [key length]) < 3)
314 c = [key characterAtIndex:0];
318 /* Note: we check for _prefix_! eg body[1] is valid too */
319 if (klen > 3 && [key hasPrefix:@"body"]) {
320 keys[count] = @"body";
321 values[count] = objForKey(obj, @selector(objectForKey:), key);
326 if (klen == 8 && [key isEqualToString:@"envelope"]) {
327 keys[count] = @"envelope";
328 values[count] = objForKey(obj, @selector(objectForKey:), key);
333 if (klen == 5 && [key isEqualToString:@"flags"]) {
336 rawFlags = objForKey(obj, @selector(objectForKey:), key);
337 keys[count] = @"flags";
338 values[count] = _imapFlags2Flags(self, rawFlags);
343 if (klen == 3 && [key isEqualToString:@"msn"]) {
344 keys[count] = @"msn";
345 values[count] = objForKey(obj, @selector(objectForKey:), key);
350 if (klen == 6 && [key isEqualToString:@"rfc822"]) {
351 keys[count] = @"message";
352 values[count] = objForKey(obj, @selector(objectForKey:), key);
355 else if (klen == 13 && [key isEqualToString:@"rfc822.header"]) {
356 keys[count] = @"header";
357 values[count] = objForKey(obj, @selector(objectForKey:), key);
360 else if (klen == 11 && [key isEqualToString:@"rfc822.text"]) {
361 keys[count] = @"text";
362 values[count] = objForKey(obj, @selector(objectForKey:), key);
365 else if (klen == 11 && [key isEqualToString:@"rfc822.size"]) {
366 keys[count] = @"size";
367 values[count] = objForKey(obj, @selector(objectForKey:), key);
372 if (klen == 3 && [key isEqualToString:@"uid"]) {
373 keys[count] = @"uid";
374 values[count] = objForKey(obj, @selector(objectForKey:), key);
381 /* create dictionary */
384 ? [[DictClass alloc] initWithObjects:values forKeys:keys count:count]
387 return entry; /* returns retained object! */
389 - (NSDictionary *)normalizeFetchResponse:(NGHashMap *)_map {
391 Raw Sample (Courier):
392 C[0x8b4e754]: 27 uid fetch 635 (body)
393 S[0x8c8b4e4]: * 627 FETCH (UID 635 BODY
394 ("text" "plain" ("charset" "iso-8859-1" "format" "flowed")
395 NIL NIL "8bit" 2474 51))
396 S[0x8c8b4e4]: * 627 FETCH (FLAGS (\Seen))
397 S[0x8c8b4e4]: 27 OK FETCH completed.
398 - this results in two result records (one for UID and one for FLAGS)
399 TODO: should we coalesce?
402 C[0x8c8ec64]: 14 uid fetch 20199 (body)
403 S[0x8da46a4]: * 93 FETCH (UID 20199 BODY
404 ((("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL "signed data" "7BIT" 691 17)
405 ("APPLICATION" "PKCS7-SIGNATURE" ("NAME" "smime.p7s") NIL
406 "signature" "BASE64" 2936) "SIGNED")
407 ("TEXT" "PLAIN" ("CHARSET" "us-ascii") NIL NIL "7BIT" 146 4)
409 S[0x8da46a4]: 14 OK Completed
410 - UID key is mapped to 'uid'
411 - BODY key is mapped to a nested body structure
412 - MSN is added for the '93'? (TODO: make sure this is the case)
414 Sample returns (not for the above code!):
416 // other message stuff
419 header = < NSData containing the header >;
423 flags = ( answered, deleted, seen );
425 ... for each fetch message ...
429 NSMutableDictionary *result;
431 NSEnumerator *enumerator;
432 NSMutableArray *fetchResponseRecords;
434 // TODO: describe what the generic normalize does.
435 // Q: do we need to run this before the following section or can we
436 // call this method just before [result setObject:...] ? (I guess
437 // the latter, because 'result' is not accessed, but who knows
438 // about side effects in this JR cruft :-( )
439 result = [self normalizeResponse:_map];
441 fetchResponseRecords = [[NSMutableArray alloc] initWithCapacity:512];
443 /* walk over each response tag which is keyed by 'fetch' in the hashmap */
444 enumerator = [_map objectEnumeratorForKey:@"fetch"];
445 while ((obj = [enumerator nextObject]) != nil) {
448 if ((entry = [self normalizeFetchResponsePart:obj]) == nil)
451 [fetchResponseRecords addObject:entry];
452 [entry release]; entry = nil;
455 /* make response array immutable and add to normalized result */
456 obj = [fetchResponseRecords copy];
457 [fetchResponseRecords release];
458 [result setObject:obj forKey:@"fetch"];
461 return [[result copy] autorelease];
464 - (NSDictionary *)normalizeQuotaResponse:(NGHashMap *)_map {
465 /* filter for quota responses */
466 NSMutableDictionary *result, *quotaRoot, *quota, *tmp;
468 NSEnumerator *enumerator;
470 result = [self normalizeResponse:_map];
471 quotaRoot = [_map objectForKey:@"quotaRoot"];
472 quota = [_map objectForKey:@"quota"];
473 enumerator = [quotaRoot keyEnumerator];
474 tmp = [NSMutableDictionary dictionaryWithCapacity:[quota count]];
476 while ((obj = [enumerator nextObject])) {
480 qRoot = [quotaRoot objectForKey:obj];
482 if (![qRoot length]) {
483 if (LogImapEnabled) {
484 [self logWithFormat:@"%s: missing quotaroot for %@",
485 __PRETTY_FUNCTION__, obj];
489 qDesc = [quota objectForKey:qRoot];
491 if ([qDesc count] == 0) {
492 if (LogImapEnabled) {
493 [self logWithFormat:@"%s: missing quota description for"
494 @" folder %@ root %@",
495 __PRETTY_FUNCTION__, obj, qRoot];
499 [tmp setObject:qDesc forKey:[self _imapFolder2Folder:obj]];
501 [result setObject:tmp forKey:@"quotas"];
502 return [[result copy] autorelease];
507 ** filter for open connection
510 - (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
511 NSMutableDictionary *result;
514 result = [self normalizeResponse:_map];
516 obj = [[_map objectEnumeratorForKey:@"ok"] nextObject];
518 [result setObject:NoNumber forKey:@"result"];
522 if ([obj isKindOfClass:DictClass])
523 obj = [(NSDictionary *)obj objectForKey:@"comment"];
525 if ([obj isKindOfClass:StrClass]) {
526 NSEnumerator *enumerator;
530 [result setObject:obj forKey:@"server"];
532 enumerator = [VersionPrefixDict keyEnumerator];
533 lowServer = [obj lowercaseString];
535 while ((key = [enumerator nextObject])) {
540 pref = [VersionPrefixDict objectForKey:key];
541 r = [lowServer rangeOfString:pref];
543 if (r.length == 0) continue;
545 [result setObject:key forKey:@"serverKind"];
546 if (![key isEqualToString:@"cyrus"])
549 /* cyrus server, collect version */
551 vers = [[lowServer substringFromIndex:(r.location + [pref length])]
552 componentsSeparatedByString:@"."];
554 if ([vers count] > 2) {
557 n = [NSNumber numberWithInt:[[vers objectAtIndex:0] intValue]];
558 [result setObject:n forKey:@"version"];
560 n = [NSNumber numberWithInt:[[vers objectAtIndex:1] intValue]];
561 [result setObject:n forKey:@"subversion"];
563 n = [NSNumber numberWithInt:[[vers objectAtIndex:2] intValue]];
564 [result setObject:n forKey:@"tag"];
569 [result setObject:YesNumber forKey:@"result"];
576 ** list : NSDictionary (folder name as key and flags as value)
579 - (NSDictionary *)normalizeListResponse:(NGHashMap *)_map {
580 NSMutableDictionary *result;
582 NSAutoreleasePool *pool;
585 pool = [[NSAutoreleasePool alloc] init];
586 result = [self normalizeResponse:_map];
588 if ((obj = [_map objectsForKey:@"list"]) != nil) {
589 NSEnumerator *enumerator;
591 NSMutableDictionary *folder;
593 enumerator = [obj objectEnumerator];
594 folder = [[NSMutableDictionary alloc] init];
596 while ((o = [enumerator nextObject])) {
597 [folder setObject:_imapFlags2Flags(self, [o objectForKey:@"flags"])
598 forKey:[self _imapFolder2Folder:[o objectForKey:@"folderName"]]];
605 [result setObject:f forKey:@"list"];
606 [f release]; f = nil;
607 [folder release]; folder = nil;
613 return [rr autorelease];
618 static inline NSArray *
619 _imapFlags2Flags(NGImap4ResponseNormalizer *self, NSArray *_flags)
621 NSEnumerator *enumerator;
626 objs = calloc([_flags count] + 2, sizeof(id));
628 enumerator = [_flags objectEnumerator];
630 while ((obj = [enumerator nextObject])) {
631 if ([obj length] == 0)
634 if (![[obj substringToIndex:1] isEqualToString:@"\\"])
637 objs[cnt] = [obj substringFromIndex:1];
640 result = [NSArray arrayWithObjects:objs count:cnt];
641 if (objs) free(objs);
647 - (NSDictionary *)normalizeGetACLResponse:(NGHashMap *)_map {
651 * ACL INBOX test.et.di.cete-lyon lrswipcda helge lrwip
654 NSMutableDictionary *result;
657 result = [self normalizeResponse:_map];
658 if ((obj = [[_map objectEnumeratorForKey:@"acl"] nextObject]) != nil)
659 [result setObject:obj forKey:@"acl"];
660 if ((obj = [[_map objectEnumeratorForKey:@"mailbox"] nextObject]) != nil)
661 [result setObject:obj forKey:@"mailbox"];
665 - (NSDictionary *)normalizeListRightsResponse:(NGHashMap *)_map {
668 16 listrights INBOX anyone
669 * LISTRIGHTS INBOX anyone "" l r s w i p c d a 0 1 2 3 4 5 6 7 8 9
672 NSMutableDictionary *result;
675 result = [self normalizeResponse:_map];
677 if ((obj = [[_map objectEnumeratorForKey:@"listrights"] nextObject]))
678 [result setObject:obj forKey:@"listrights"];
679 if ((obj = [[_map objectEnumeratorForKey:@"requiredRights"] nextObject]))
680 [result setObject:obj forKey:@"requiredRights"];
682 if ((obj = [[_map objectEnumeratorForKey:@"mailbox"] nextObject]) != nil)
683 [result setObject:obj forKey:@"mailbox"];
684 if ((obj = [[_map objectEnumeratorForKey:@"uid"] nextObject]) != nil)
685 [result setObject:obj forKey:@"uid"];
689 - (NSDictionary *)normalizeMyRightsResponse:(NGHashMap *)_map {
693 * MYRIGHTS INBOX lrswipcda
696 NSMutableDictionary *result;
699 result = [self normalizeResponse:_map];
700 if ((obj = [[_map objectEnumeratorForKey:@"myrights"] nextObject]) != nil)
701 [result setObject:obj forKey:@"myrights"];
702 if ((obj = [[_map objectEnumeratorForKey:@"mailbox"] nextObject]) != nil)
703 [result setObject:obj forKey:@"mailbox"];
707 @end /* NGImap4ResponseNormalizer */