]> err.no Git - sope/blob - sope-mime/NGImap4/NGImap4ResponseNormalizer.m
fixed some gcc 4.0 warning
[sope] / sope-mime / NGImap4 / NGImap4ResponseNormalizer.m
1 /*
2   Copyright (C) 2000-2005 SKYRIX Software AG
3
4   This file is part of SOPE.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
22 #include "NGImap4ResponseNormalizer.h"
23 #include "NGImap4Client.h"
24 #include "imCommon.h"
25
26 @interface NGImap4Client(UsedPrivates)
27 - (NSString *)delimiter;
28 - (NSString *)_imapFolder2Folder:(NSString *)_folder;
29 @end
30
31 @implementation NGImap4ResponseNormalizer
32
33 static __inline__ NSArray *
34 _imapFlags2Flags(NGImap4ResponseNormalizer *, NSArray *);
35
36 static NSDictionary *VersionPrefixDict = nil;
37
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;
43
44 + (void)initialize {
45   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
46   static BOOL didInit = NO;
47   if (didInit) return;
48   didInit = YES;
49   
50   YesNumber = [[NSNumber numberWithBool:YES] retain];
51   NoNumber  = [[NSNumber numberWithBool:NO]  retain];
52   
53   DictClass      = [NSDictionary class];
54   StrClass       = [NSString     class];
55   LogImapEnabled = [ud boolForKey:@"ImapLogEnabled"]?1:0;
56
57   /*
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.
61   */
62   if (VersionPrefixDict == nil) {
63     VersionPrefixDict =
64       [[DictClass alloc] initWithObjectsAndKeys:
65                               @"cyrus imap4 v", @"cyrus",
66                               @" imap4rev1 ",   @"washington", 
67                               @"courier",       @"courier", nil];
68   }
69 }
70
71 - (id)initWithClient:(NGImap4Client *)_client {
72   if ((self = [super init])) {
73     self->client = _client; /* non-retained */
74   }
75   return self;
76 }
77
78 /* client callbacks */
79
80 - (void)closeConnection {
81   [(id)self->client closeConnection];
82 }
83
84 - (NSString *)delimiter {
85   return [self->client delimiter];
86 }
87
88 /* folder handling */
89
90 - (NSString *)_imapFolder2Folder:(NSString *)_folder {
91   return [self->client _imapFolder2Folder:_folder];
92 }
93
94 /* primary */
95
96 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
97   /*
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 
103                         folder)
104   */
105   NSMutableDictionary *result;
106   id                  obj;
107   NSDictionary        *respRes;
108   
109   if (_map == nil)
110     return (id)[NSMutableDictionary dictionary];
111   
112   respRes = [[_map objectEnumeratorForKey:@"ResponseResult"] nextObject];
113   result  = [NSMutableDictionary dictionaryWithCapacity:32];
114   [result setObject:_map forKey:@"RawResponse"];
115   
116   if ((obj = [_map objectForKey:@"bye"])) {
117     [result setObject:NoNumber forKey:@"result"];
118     [result setObject:obj forKey:@"reason"];
119     [self closeConnection];
120     return result;
121   }
122
123   if ([[respRes objectForKey:@"result"] isEqual:@"ok"]) {
124     [result setObject:YesNumber forKey:@"result"];
125   }
126   else {
127     id tmp = nil;
128     [result setObject:NoNumber forKey:@"result"];
129     if ((tmp = [respRes objectForKey:@"description"]) != nil) {
130       [result setObject:tmp forKey:@"reason"];
131     }
132     return result;
133   }
134   if ((obj = [[_map objectEnumeratorForKey:@"exists"] nextObject]) != nil) { // 
135     [result setObject:obj forKey:@"exists"];
136   }
137   if ((obj = [[_map objectEnumeratorForKey:@"recent"] nextObject]) != nil) {
138     [result setObject:obj forKey:@"recent"];
139   }
140   if ((obj = [_map objectsForKey:@"expunge"]) != nil)
141     [result setObject:obj forKey:@"expunge"];
142   
143   return result;
144 }
145
146 - (NSDictionary *)normalizeSortResponse:(NGHashMap *)_map {
147   /* filter for sort response (search  : NSArray (msn)) */
148   id                  obj;
149   NSMutableDictionary *result;
150
151   result = [self normalizeResponse:_map];
152   
153   if ((obj = [[_map objectEnumeratorForKey:@"sort"] nextObject]) != nil)
154     [result setObject:obj forKey:@"sort"];
155   
156   return result;
157 }
158
159 - (NSDictionary *)normalizeCapabilityRespone:(NGHashMap *)_map {
160   /* filter for capability response: capability  : NSArray */
161   id                  obj;
162   NSMutableDictionary *result;
163
164   result = [self normalizeResponse:_map];
165   
166   if ((obj = [[_map objectEnumeratorForKey:@"capability"] nextObject]))
167     [result setObject:obj forKey:@"capability"];
168   
169   return result;
170 }
171
172 - (NSDictionary *)normalizeThreadResponse:(NGHashMap *)_map {
173   /* filter for thread response: thread  : NSArray (msn) */
174   id                  obj;
175   NSMutableDictionary *result;
176
177   result = [self normalizeResponse:_map];
178   
179   if ((obj = [[_map objectEnumeratorForKey:@"thread"] nextObject]))
180     [result setObject:obj forKey:@"thread"];
181   
182   return result;
183 }
184
185 - (NSDictionary *)normalizeSearchResponse:(NGHashMap *)_map {
186   /* filter for search response: search  : NSArray (msn) */
187   id                  obj;
188   NSMutableDictionary *result;
189
190   result = [self normalizeResponse:_map];
191   
192   if ((obj = [[_map objectEnumeratorForKey:@"search"] nextObject]))
193     [result setObject:obj forKey:@"search"];
194   
195   return result;
196 }
197
198 - (NSDictionary *)normalizeSelectResponse:(NGHashMap *)_map {
199   /*
200     filter for select response
201       flags  : NSArray
202       unseen : NSNumber
203       access  : NSString ([READ-WRITE], ... )
204   */
205   NSDictionary        *obj;
206   NSEnumerator        *enumerator;
207   NSMutableDictionary *result;
208   id flags;
209   
210   result = [self normalizeResponse:_map];
211   
212   if ((flags = [[_map objectEnumeratorForKey:@"flags"] nextObject]))
213     [result setObject:_imapFlags2Flags(self, flags) forKey:@"flags"];
214   
215   enumerator = [_map objectEnumeratorForKey:@"ok"];
216   while ((obj = [enumerator nextObject])) {
217     id o;
218     
219     if ((o = [obj  objectForKey:@"unseen"]))
220       [result setObject:o forKey:@"unseen"];
221   }
222   
223   enumerator = [_map objectEnumeratorForKey:@"no"];
224   while ((obj = [enumerator nextObject])) {
225     id o;
226     
227     if ([obj isKindOfClass:DictClass]) {
228       if ((o = [obj  objectForKey:@"ALERT"]))
229         [result setObject:o forKey:@"alert"];
230     }
231     else {
232       [result setObject:obj forKey:@"alert"];
233     }
234   }
235   
236   obj = [_map objectForKey:@"ResponseResult"];
237   if ((obj = [obj objectForKey:@"flag"]))
238     [result setObject:obj forKey:@"access"];
239   
240   return result;
241 }
242
243 - (NSDictionary *)normalizeStatusResponse:(NGHashMap *)_map {
244   /*
245     filter for status response
246       messages  : NSNumber
247       recent    : NSNumber
248       unseen    : NSNumber
249   */
250   NSDictionary        *obj;
251   NSMutableDictionary *result;
252   id                  o;
253   
254   result = [self normalizeResponse:_map];
255   
256   obj = [[_map objectEnumeratorForKey:@"status"] nextObject];
257   obj = [obj objectForKey:@"flags"];
258   
259   if ((o = [obj  objectForKey:@"messages"]) != nil)
260     [result setObject:o forKey:@"messages"];
261   
262   if ((o = [obj  objectForKey:@"recent"]) != nil) {
263     if ([result objectForKey:@"recent"] == nil)
264       [result setObject:o forKey:@"recent"];
265   }
266   if ((o = [obj  objectForKey:@"unseen"]) != nil)
267     [result setObject:o forKey:@"unseen"];
268   
269   return result;
270 }
271
272 /*
273   filter for fetch response
274     fetch : NSArray (fetch responses)
275       'header'  - RFC822.HEADER
276       'text'    - RFC822.TEXT
277       'size'    - SIZE
278       'flags'   - FLAGS
279       'uid'     - UID
280       'msn'     - message sequence number
281       'message' - RFC822
282       'body'    - (dictionary with bodystructure)
283
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')
287 */
288 - (NSDictionary *)normalizeFetchResponsePart:(id)obj {
289     // TODO: shouldn't we use a specific object instead of NSDict for that?
290     NSDictionary *entry;
291     NSEnumerator *keyEnum;
292     NSString     *key;
293     NSString     *keys[9];
294     id           values[9];
295     unsigned     count;
296     id (*objForKey)(id, SEL, id);
297     
298     /*
299        Process one 'fetch' reponse dictionary, walk over each key of the 
300        dict and check for a collection of known response keys.
301     */
302     count     = 0;
303     keyEnum   = [obj keyEnumerator];    
304     objForKey = (void *)[obj methodForSelector:@selector(objectForKey:)];
305     
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)) {
309       unsigned klen;
310       unichar  c;
311       
312       if ((klen = [key length]) < 3)
313         continue;
314       c = [key characterAtIndex:0];
315       
316       switch (c) {
317       case 'b':
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);
322           count++;
323         }
324         break;
325       case 'e':
326         if (klen == 8 && [key isEqualToString:@"envelope"]) {
327           keys[count]   = @"envelope";
328           values[count] = objForKey(obj, @selector(objectForKey:), key);
329           count++;
330         }
331         break;
332       case 'f':
333         if (klen == 5 && [key isEqualToString:@"flags"]) {
334           id rawFlags;
335           
336           rawFlags = objForKey(obj, @selector(objectForKey:), key);
337           keys[count]   = @"flags";
338           values[count] = _imapFlags2Flags(self, rawFlags);
339           count++;
340         }
341         break;
342       case 'm':
343         if (klen == 3 && [key isEqualToString:@"msn"]) {
344           keys[count]   = @"msn";
345           values[count] = objForKey(obj, @selector(objectForKey:), key);
346           count++;
347         }
348         break;
349       case 'r':
350         if (klen == 6 && [key isEqualToString:@"rfc822"]) {
351           keys[count]   = @"message";
352           values[count] = objForKey(obj, @selector(objectForKey:), key);
353           count++;
354         }
355         else if (klen == 13 && [key isEqualToString:@"rfc822.header"]) {
356           keys[count]   = @"header";
357           values[count] = objForKey(obj, @selector(objectForKey:), key);
358           count++;
359         }
360         else if (klen == 11 && [key isEqualToString:@"rfc822.text"]) {
361           keys[count]   = @"text";
362           values[count] = objForKey(obj, @selector(objectForKey:), key);
363           count++;
364         }
365         else if (klen == 11 && [key isEqualToString:@"rfc822.size"]) {
366           keys[count]   = @"size";
367           values[count] = objForKey(obj, @selector(objectForKey:), key);
368           count++;
369         }
370         break;
371       case 'u':
372         if (klen == 3 && [key isEqualToString:@"uid"]) {
373           keys[count]   = @"uid";
374           values[count] = objForKey(obj, @selector(objectForKey:), key);
375           count++;
376         }
377         break;
378       }
379     }
380     
381     /* create dictionary */
382     
383     entry = count > 0
384       ? [[DictClass alloc] initWithObjects:values forKeys:keys count:count]
385       : nil;
386     
387     return entry; /* returns retained object! */
388 }
389 - (NSDictionary *)normalizeFetchResponse:(NGHashMap *)_map {
390   /*
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?
400
401     Raw Sample (Cyrus):
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) 
408           "MIXED"))
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)
413
414     Sample returns (not for the above code!):
415       {
416         // other message stuff
417         fetch = (
418           {
419             header = < NSData containing the header >;
420             size   = 3314;
421             uid    = 20187;
422             msn    = 72;
423             flags  = ( answered, deleted, seen );
424           },
425           ... for each fetch message ...
426         )
427       }
428   */
429   NSMutableDictionary *result;
430   id                  obj;
431   NSEnumerator        *enumerator;
432   NSMutableArray      *fetchResponseRecords;
433
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];
440   
441   fetchResponseRecords = [[NSMutableArray alloc] initWithCapacity:512];
442   
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) {
446     NSDictionary *entry;
447     
448     if ((entry = [self normalizeFetchResponsePart:obj]) == nil)
449       continue;
450     
451     [fetchResponseRecords addObject:entry];
452     [entry release]; entry = nil;
453   }
454   
455   /* make response array immutable and add to normalized result */
456   obj = [fetchResponseRecords copy];
457   [fetchResponseRecords release];
458   [result setObject:obj forKey:@"fetch"];
459   [obj release];
460   
461   return [[result copy] autorelease];
462 }
463
464 - (NSDictionary *)normalizeQuotaResponse:(NGHashMap *)_map {
465   /* filter for quota responses */
466   NSMutableDictionary *result, *quotaRoot, *quota, *tmp;
467   id                  obj;
468   NSEnumerator        *enumerator;
469
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]];
475   
476   while ((obj = [enumerator nextObject])) {
477     NSString     *qRoot;
478     NSDictionary *qDesc;
479
480     qRoot = [quotaRoot objectForKey:obj];
481
482     if (![qRoot length]) {
483       if (LogImapEnabled) {
484         [self logWithFormat:@"%s: missing quotaroot for %@",
485               __PRETTY_FUNCTION__, obj];
486       }
487       continue;
488     }
489     qDesc = [quota objectForKey:qRoot];
490
491     if ([qDesc count] == 0) {
492       if (LogImapEnabled) {
493         [self logWithFormat:@"%s: missing quota description for"
494               @" folder %@ root %@",
495               __PRETTY_FUNCTION__, obj, qRoot];
496       }
497       continue;
498     }
499     [tmp setObject:qDesc forKey:[self _imapFolder2Folder:obj]];
500   }
501   [result setObject:tmp forKey:@"quotas"];
502   return [[result copy] autorelease];
503 }
504
505
506 /*
507 ** filter for open connection
508 */
509
510 - (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
511   NSMutableDictionary *result;
512   id obj;
513
514   result = [self normalizeResponse:_map];
515   
516   obj = [[_map objectEnumeratorForKey:@"ok"] nextObject];
517   if (obj == nil) {
518     [result setObject:NoNumber forKey:@"result"];
519     return result;
520   }
521   
522   if ([obj isKindOfClass:DictClass])
523     obj = [(NSDictionary *)obj objectForKey:@"comment"];
524
525   if ([obj isKindOfClass:StrClass]) {
526     NSEnumerator *enumerator;
527     id           key;
528     NSString     *lowServer;
529     
530     [result setObject:obj forKey:@"server"];
531     
532     enumerator = [VersionPrefixDict keyEnumerator];
533     lowServer  = [obj lowercaseString];
534
535     while ((key = [enumerator nextObject])) {
536       NSString *pref;
537       NSArray  *vers;
538       NSRange  r;
539         
540       pref  = [VersionPrefixDict objectForKey:key];
541       r     = [lowServer rangeOfString:pref];
542       
543       if (r.length == 0) continue;
544         
545       [result setObject:key forKey:@"serverKind"];
546       if (![key isEqualToString:@"cyrus"])
547         continue;
548         
549       /* cyrus server, collect version */
550         
551       vers = [[lowServer substringFromIndex:(r.location + [pref length])]
552                          componentsSeparatedByString:@"."];
553       
554       if ([vers count] > 2) {
555         NSNumber *n;
556           
557         n = [NSNumber numberWithInt:[[vers objectAtIndex:0] intValue]];
558         [result setObject:n forKey:@"version"];
559
560         n = [NSNumber numberWithInt:[[vers objectAtIndex:1] intValue]];
561         [result setObject:n forKey:@"subversion"];
562           
563         n = [NSNumber numberWithInt:[[vers objectAtIndex:2] intValue]];
564         [result setObject:n forKey:@"tag"];
565       }
566       break;
567     }
568   }
569   [result setObject:YesNumber forKey:@"result"];
570   
571   return result;
572 }
573
574 /*
575 ** filter for list
576 **       list : NSDictionary (folder name as key and flags as value)
577 */
578
579 - (NSDictionary *)normalizeListResponse:(NGHashMap *)_map {
580   NSMutableDictionary *result;
581   id                  obj;
582   NSAutoreleasePool   *pool;
583   NSDictionary        *rr;
584   
585   pool   = [[NSAutoreleasePool alloc] init];
586   result = [self normalizeResponse:_map];
587   
588   if ((obj = [_map objectsForKey:@"list"]) != nil) {
589     NSEnumerator        *enumerator;
590     NSDictionary        *o;
591     NSMutableDictionary *folder;
592     
593     enumerator = [obj objectEnumerator];
594     folder     = [[NSMutableDictionary alloc] init];
595     
596     while ((o = [enumerator nextObject])) {
597       [folder setObject:_imapFlags2Flags(self, [o objectForKey:@"flags"])
598               forKey:[self _imapFolder2Folder:[o objectForKey:@"folderName"]]];
599     }
600     
601     {
602       NSDictionary *f;
603       
604       f = [folder copy];
605       [result setObject:f forKey:@"list"];
606       [f release];      f      = nil;
607       [folder release]; folder = nil;
608     }
609   }
610   rr = [result copy];
611   [pool release];
612   
613   return [rr autorelease];
614 }
615
616 /* flags */
617
618 static inline NSArray *
619 _imapFlags2Flags(NGImap4ResponseNormalizer *self, NSArray *_flags) 
620 {
621   NSEnumerator *enumerator;
622   NSArray      *result;
623   id           obj, *objs;
624   unsigned     cnt;
625   
626   objs = calloc([_flags count] + 2, sizeof(id));
627   
628   enumerator = [_flags objectEnumerator];
629   cnt = 0;
630   while ((obj = [enumerator nextObject])) {
631     if ([obj length] == 0)
632       continue;
633     
634     if (![[obj substringToIndex:1] isEqualToString:@"\\"])
635       continue;
636
637     objs[cnt] = [obj substringFromIndex:1];
638     cnt++;
639   }
640   result = [NSArray arrayWithObjects:objs count:cnt];
641   if (objs) free(objs);
642   return result;
643 }
644
645 /* ACL */
646
647 - (NSDictionary *)normalizeGetACLResponse:(NGHashMap *)_map {
648   /*
649     Raw Sample (Cyrus):
650       21 GETACL INBOX
651       * ACL INBOX test.et.di.cete-lyon lrswipcda helge lrwip
652       21 OK Completed
653   */
654   NSMutableDictionary *result;
655   id obj;
656   
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"];
662   return result;
663 }
664
665 - (NSDictionary *)normalizeListRightsResponse:(NGHashMap *)_map {
666   /*
667     Raw Sample (Cyrus):
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
670       16 OK Completed
671   */
672   NSMutableDictionary *result;
673   id obj;
674
675   result = [self normalizeResponse:_map];
676
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"];
681
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"];
686   return result;
687 }
688
689 - (NSDictionary *)normalizeMyRightsResponse:(NGHashMap *)_map {
690   /*
691     Raw Sample (Cyrus):
692       18 myrights INBOX
693       * MYRIGHTS INBOX lrswipcda
694       18 OK Completed
695   */
696   NSMutableDictionary *result;
697   id obj;
698
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"];
704   return result;
705 }
706
707 @end /* NGImap4ResponseNormalizer */