]> err.no Git - sope/blob - sope-mime/NGImap4/NGImap4Connection.m
fixed some gcc 4.0 warning
[sope] / sope-mime / NGImap4 / NGImap4Connection.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo 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   OGo 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 OGo; 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 "NGImap4Connection.h"
23 #include "NGImap4MailboxInfo.h"
24 #include "NGImap4Client.h"
25 #include "imCommon.h"
26
27 @implementation NGImap4Connection
28
29 static BOOL     debugOn         = NO;
30 static BOOL     debugCache      = NO;
31 static BOOL     debugKeys       = NO;
32 static BOOL     alwaysSelect    = NO;
33 static BOOL     onlyFetchInbox  = NO;
34 static NSString *imap4Separator = nil;
35
36 + (void)initialize {
37   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
38   
39   debugOn      = [ud boolForKey:@"NGImap4ConnectionDebugEnabled"];
40   debugCache   = [ud boolForKey:@"NGImap4ConnectionCacheDebugEnabled"];
41   debugKeys    = [ud boolForKey:@"NGImap4ConnectionFolderDebugEnabled"];
42   alwaysSelect = [ud boolForKey:@"NGImap4ConnectionAlwaysSelect"];
43   
44   if (debugOn)    NSLog(@"Note: NGImap4ConnectionDebugEnabled is enabled!");
45   if (alwaysSelect)
46     NSLog(@"WARNING: 'NGImap4ConnectionAlwaysSelect' enabled (slow down)");
47
48   imap4Separator = 
49     [[ud stringForKey:@"NGImap4ConnectionStringSeparator"] copy];
50   if ([imap4Separator length] == 0)
51     imap4Separator = @"/";
52   NSLog(@"Note(NGImap4Connection): using '%@' as the IMAP4 folder separator.", 
53         imap4Separator);
54 }
55
56 - (id)initWithClient:(NGImap4Client *)_client password:(NSString *)_pwd {
57   if (_client == nil || _pwd == nil) {
58     [self release];
59     return nil;
60   }
61   
62   if ((self = [super init])) {
63     self->client   = [_client retain];
64     self->password = [_pwd    copy];
65     
66     self->creationTime = [[NSDate alloc] init];
67     
68     // TODO: retrieve from IMAP4 instead of using a default
69     self->separator = imap4Separator;
70   }
71   return self;
72 }
73 - (id)init {
74   return [self initWithClient:nil password:nil];
75 }
76
77 - (void)dealloc {
78   [self->separator       release];
79   [self->urlToRights     release];
80   [self->cachedUIDs      release];
81   [self->uidFolderURL    release];
82   [self->uidSortOrdering release];
83   [self->creationTime    release];
84   [self->subfolders      release];
85   [self->password        release];
86   [self->client          release];
87   [super dealloc];
88 }
89
90 /* accessors */
91
92 - (NGImap4Client *)client {
93   return self->client;
94 }
95 - (BOOL)isValidPassword:(NSString *)_pwd {
96   return [self->password isEqualToString:_pwd];
97 }
98
99 - (NSDate *)creationTime {
100   return self->creationTime;
101 }
102
103 - (void)cacheHierarchyResults:(NSDictionary *)_hierarchy {
104   ASSIGNCOPY(self->subfolders, _hierarchy);
105 }
106 - (NSDictionary *)cachedHierarchyResults {
107   return self->subfolders;
108 }
109 - (void)flushFolderHierarchyCache {
110   [self->subfolders  release]; self->subfolders  = nil;
111   [self->urlToRights release]; self->urlToRights = nil;
112 }
113
114 /* rights */
115
116 - (NSString *)cachedMyRightsForURL:(NSURL *)_url {
117   return (_url != nil) ? [self->urlToRights objectForKey:_url] : nil;
118 }
119 - (void)cacheMyRights:(NSString *)_rights forURL:(NSURL *)_url {
120   if (self->urlToRights == nil)
121     self->urlToRights = [[NSMutableDictionary alloc] initWithCapacity:8];
122   [self->urlToRights setObject:_rights forKey:_url];
123 }
124
125 /* UIDs */
126
127 - (id)cachedUIDsForURL:(NSURL *)_url qualifier:(id)_q sortOrdering:(id)_so {
128   if (_q != nil)
129     return nil;
130   if (![_so isEqual:self->uidSortOrdering])
131     return nil;
132   if (![self->uidFolderURL isEqual:_url])
133     return nil;
134   
135   return self->cachedUIDs;
136 }
137
138 - (void)cacheUIDs:(NSArray *)_uids forURL:(NSURL *)_url
139   qualifier:(id)_q sortOrdering:(id)_so
140 {
141   if (_q != nil)
142     return;
143
144   ASSIGNCOPY(self->uidSortOrdering, _so);
145   ASSIGNCOPY(self->uidFolderURL,    _url);
146   ASSIGNCOPY(self->cachedUIDs,      _uids);
147 }
148
149 - (void)flushMailCaches {
150   ASSIGN(self->uidSortOrdering, nil);
151   ASSIGN(self->uidFolderURL,    nil);
152   ASSIGN(self->cachedUIDs,      nil);
153 }
154
155
156 /* errors */
157
158 - (NSException *)errorCouldNotSelectURL:(NSURL *)_url {
159   NSDictionary *ui;
160   NSString *r;
161   
162   r = [_url isNotNull]
163     ? [@"Could not select IMAP4 folder: " stringByAppendingString:
164           [_url absoluteString]]
165     : @"Could not select IMAP4 folder!";
166
167   ui = [NSDictionary dictionaryWithObjectsAndKeys:
168                        [NSNumber numberWithInt:404], @"http-status",
169                        _url, @"url",
170                      nil];
171   
172   return [NSException exceptionWithName:@"NGImap4Exception"
173                       reason:r userInfo:ui];
174 }
175
176 - (NSException *)errorForResult:(NSDictionary *)_result text:(NSString *)_txt {
177   NSDictionary *ui;
178   NSString *r;
179   int      status;
180   
181   if ([[_result valueForKey:@"result"] boolValue])
182     return nil; /* everything went fine! */
183   
184   if ((r = [_result valueForKey:@"reason"]) != nil)
185     r = [[_txt stringByAppendingString:@": "] stringByAppendingString:r];
186   else
187     r = _txt;
188   
189   if ([r isEqualToString:@"Permission denied"]) {
190     /* different for each server?, no error codes in IMAP4 ... */
191     status = 403 /* Forbidden */;
192   }
193   else
194     status = 500 /* internal server error */;
195   
196   ui = [NSDictionary dictionaryWithObjectsAndKeys:
197                        [NSNumber numberWithInt:status], @"http-status",
198                        _result, @"rawResult",
199                      nil];
200   
201   return [NSException exceptionWithName:@"NGImap4Exception"
202                       reason:r userInfo:ui];
203 }
204
205 /* IMAP4 path/url processing methods */
206
207 NSArray *SOGoMailGetDirectChildren(NSArray *_array, NSString *_fn) {
208   /*
209     Scans string '_array' for strings which start with the string in '_fn'.
210     Then split on '/'.
211   */
212   NSMutableArray *ma;
213   unsigned i, count, prefixlen;
214   
215   if ((count = [_array count]) < 2)
216     /* one entry is the folder itself, so we need at least two */
217     return [NSArray array];
218   
219 #if __APPLE__ 
220   // TODO: somehow results are different on OSX
221   prefixlen = [_fn isEqualToString:@""] ? 0 : [_fn length] + 1;
222 #else
223   prefixlen = [_fn isEqualToString:@"/"] ? 1 : [_fn length] + 1;
224 #endif
225   ma = [NSMutableArray arrayWithCapacity:count];
226   for (i = 0; i < count; i++) {
227     NSString *p;
228     
229     p = [_array objectAtIndex:i];
230     if ([p length] <= prefixlen)
231       continue;
232     if (prefixlen != 0 && ![p hasPrefix:_fn])
233       continue;
234     
235     /* cut of common part */
236     p = [p substringFromIndex:prefixlen];
237     
238     /* check whether the path is a sub-subfolder path */
239     if ([p rangeOfString:@"/"].length > 0)
240       continue;
241     
242     [ma addObject:p];
243   }
244   
245   [ma sortUsingSelector:@selector(compare:)];
246   return ma;
247 }
248
249 - (NSArray *)extractSubfoldersForURL:(NSURL *)_url
250   fromResultSet:(NSDictionary *)_result
251 {
252   NSString     *folderName;
253   NSDictionary *result;
254   NSArray      *names;
255   NSArray      *flags;
256   
257   /* Note: the result is normalized, that is, it contains / as the separator */
258   folderName = [_url path];
259 #if __APPLE__ 
260   /* normalized results already have the / in front on libFoundation?! */
261   if ([folderName hasPrefix:@"/"]) 
262     folderName = [folderName substringFromIndex:1];
263 #endif
264   
265   result = [_result valueForKey:@"list"];
266   
267   /* Cyrus already tells us whether we need to check for children */
268   flags = [result objectForKey:folderName];
269   if ([flags containsObject:@"hasnochildren"]) {
270     if (debugKeys)
271       NSLog(@"%s: folder %@ has no children.", __PRETTY_FUNCTION__,folderName);
272     return nil;
273   }
274   
275   if (debugKeys) {
276     NSLog(@"%s: all keys %@: %@", __PRETTY_FUNCTION__, folderName, 
277           [[result allKeys] componentsJoinedByString:@", "]);
278   }
279   
280   names = SOGoMailGetDirectChildren([result allKeys], folderName);
281   if (debugKeys) {
282     NSLog(@"%s: subfolders of '%@': %@", __PRETTY_FUNCTION__, folderName, 
283           [names componentsJoinedByString:@","]);
284   }
285   return names;
286 }
287
288 - (NSString *)imap4Separator {
289   return self->separator;
290 }
291
292 - (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn {
293   /* a bit hackish, but should be OK */
294   NSString *folderName;
295   NSArray  *names;
296
297   if (_url == nil)
298     return nil;
299   
300   folderName = [_url path];
301   if ([folderName length] == 0)
302     return nil;
303   if ([folderName characterAtIndex:0] == '/')
304     folderName = [folderName substringFromIndex:1];
305   
306   if (_delfn) folderName = [folderName stringByDeletingLastPathComponent];
307   
308   if ([[self imap4Separator] isEqualToString:@"/"])
309     return folderName;
310   
311   names = [folderName pathComponents];
312   return [names componentsJoinedByString:[self imap4Separator]];
313 }
314 - (NSString *)imap4FolderNameForURL:(NSURL *)_url {
315   return [self imap4FolderNameForURL:_url removeFileName:NO];
316 }
317
318 - (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result {
319   /* Note: the result is normalized, that is, it contains / as the separator */
320   return [[_result valueForKey:@"list"] allKeys];
321 }
322
323 /* folder selections */
324
325 - (BOOL)selectFolder:(id)_url {
326   NSDictionary *result;
327   NSString     *newFolder;
328   
329   newFolder = [_url isKindOfClass:[NSURL class]]
330     ? [self imap4FolderNameForURL:_url]
331     : _url;
332   
333   if (!alwaysSelect) {
334     if ([[[self client] selectedFolderName] isEqualToString:newFolder])
335       return YES;
336   }
337   
338   result = [[self client] select:newFolder];
339   if (![[result valueForKey:@"result"] boolValue]) {
340     [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
341     return NO;
342   }
343
344   return YES;
345 }
346
347 - (BOOL)isPermissionDeniedResult:(id)_result {
348   if ([[_result valueForKey:@"result"] intValue] != 0)
349     return NO;
350   
351   return [[_result valueForKey:@"reason"] 
352                    isEqualToString:@"Permission denied"];
353 }
354
355 /* folder operations */
356
357 - (NSDictionary *)primaryFetchMailboxHierarchyForURL:(NSURL *)_url {
358   NSDictionary *result;
359   
360   if ((result = [self cachedHierarchyResults]) != nil)
361     return [result isNotNull] ? result : nil;
362   
363   if (debugCache) [self logWithFormat:@"  no folders cached yet .."];
364   
365   result = [[self client] list:(onlyFetchInbox ? @"INBOX" : @"*")
366                           pattern:@"*"];
367   if (![[result valueForKey:@"result"] boolValue]) {
368     [self errorWithFormat:@"Could not list mailbox hierarchy!"];
369     return nil;
370   }
371
372   /* cache results */
373   
374   if ([result isNotNull]) {
375     [self cacheHierarchyResults:result];
376     if (debugCache) {
377       [self logWithFormat:@"cached results: 0x%08X(%d)", 
378               result, [result count]];
379     }
380   }
381   return result;
382 }
383
384 - (NSArray *)subfoldersForURL:(NSURL *)_url {
385   NSDictionary *result;
386
387   if ((result = [self primaryFetchMailboxHierarchyForURL:_url]) == nil)
388     return nil;
389   if ([result isKindOfClass:[NSException class]]) {
390     [self errorWithFormat:@"failed to retrieve hierarchy: %@", result];
391     return nil;
392   }
393   
394   return [self extractSubfoldersForURL:_url fromResultSet:result];
395 }
396
397 - (NSArray *)allFoldersForURL:(NSURL *)_url {
398   NSDictionary *result;
399
400   if ((result = [self primaryFetchMailboxHierarchyForURL:_url]) == nil)
401     return nil;
402   if ([result isKindOfClass:[NSException class]]) {
403     [self errorWithFormat:@"failed to retrieve hierarchy: %@", result];
404     return nil;
405   }
406   
407   return [self extractFoldersFromResultSet:result];
408 }
409
410 /* message operations */
411
412 - (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
413   sortOrdering:(id)_so
414 {
415   /* 
416      sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
417   */
418   NSDictionary *result;
419   NSArray      *uids;
420
421   /* check cache */
422   
423   uids = [self cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so];
424   if (uids != nil) {
425     if (debugCache) [self logWithFormat:@"reusing uid cache!"];
426     return [uids isNotNull] ? uids : nil;
427   }
428   
429   /* select folder and fetch */
430   
431   if (![self selectFolder:_url])
432     return nil;
433   
434   result = [[self client] sort:_so qualifier:_qualifier encoding:@"UTF-8"];
435   if (![[result valueForKey:@"result"] boolValue]) {
436     [self errorWithFormat:@"could not sort contents of URL: %@", _url];
437     return nil;
438   }
439   
440   uids = [result valueForKey:@"sort"];
441   if (![uids isNotNull]) {
442     [self errorWithFormat:@"got no UIDs for URL: %@: %@", _url, result];
443     return nil;
444   }
445   
446   /* cache */
447   
448   [self cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so];
449   return uids;
450 }
451
452 - (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
453   parts:(NSArray *)_parts
454 {
455   // currently returns a dict?!
456   /*
457     Allowed fetch keys:
458       UID
459       BODY.PEEK[<section>]<<partial>>
460       BODY            [this is the bodystructure, supported]
461       BODYSTRUCTURE   [not supported yet!]
462       ENVELOPE        [this is a parsed header, but does not include type]
463       FLAGS
464       INTERNALDATE
465       RFC822
466       RFC822.HEADER
467       RFC822.SIZE
468       RFC822.TEXT
469   */
470   NSDictionary *result;
471   
472   if (_uids == nil)
473     return nil;
474   if ([_uids count] == 0)
475     return nil; // TODO: might break empty folders?! return a dict!
476   
477   /* select folder */
478
479   if (![self selectFolder:_url])
480     return nil;
481   
482   /* fetch parts */
483   
484   // TODO: split uids into batches, otherwise Cyrus will complain
485   //       => not really important because we batch before (in the sort)
486   //       if the list is too long, we get a:
487   //       "* BYE Fatal error: word too long"
488   
489   result = [[self client] fetchUids:_uids parts:_parts];
490   if (![[result valueForKey:@"result"] boolValue]) {
491     [self errorWithFormat:@"could not fetch %d uids for url: %@",
492             [_uids count],_url];
493     return nil;
494   }
495   
496   //[self logWithFormat:@"RESULT: %@", result];
497   return (id)result;
498 }
499
500 - (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts {
501   // currently returns a dict
502   NSDictionary *result;
503   NSString *uid;
504   
505   if (![_url isNotNull]) return nil;
506   
507   /* select folder */
508
509   uid = [self imap4FolderNameForURL:_url removeFileName:YES];
510   if (![self selectFolder:uid])
511     return nil;
512   
513   /* fetch parts */
514   
515   uid = [[_url path] lastPathComponent];
516   
517   result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts];
518   if (![[result valueForKey:@"result"] boolValue]) {
519     [self errorWithFormat:@"could not fetch url: %@", _url];
520     return nil;
521   }
522   //[self logWithFormat:@"RESULT: %@", result];
523   return (id)result;
524 }
525
526 - (NSData *)fetchContentOfBodyPart:(NSString *)_partId atURL:(NSURL *)_url {
527   NSString *key;
528   NSArray  *parts;
529   id result, fetch, body;
530   
531   if (_partId == nil) return nil;
532   
533   key   = [@"body[" stringByAppendingString:_partId];
534   key   = [key stringByAppendingString:@"]"];
535   parts = [NSArray arrayWithObjects:&key count:1];
536   
537   /* fetch */
538   
539   result = [self fetchURL:_url parts:parts];
540   
541   /* process results */
542   
543   result = [(NSDictionary *)result objectForKey:@"fetch"];
544   if ([result count] == 0) { /* did not find part */
545     [self errorWithFormat:@"did not find part: %@", _partId];
546     return nil;
547   }
548   
549   fetch = [result objectAtIndex:0];
550   if ((body = [(NSDictionary *)fetch objectForKey:@"body"]) == nil) {
551     [self errorWithFormat:@"did not find body in response: %@", result];
552     return nil;
553   }
554   
555   if ((result = [(NSDictionary *)body objectForKey:@"data"]) == nil) {
556     [self errorWithFormat:@"did not find data in body: %@", fetch];
557     return nil;
558   }
559   return result;
560 }
561
562 /* message flags */
563
564 - (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f toURL:(NSURL *)_url {
565   id result;
566   
567   if (![_url isNotNull]) return nil;
568   if (![_f   isNotNull]) return nil;
569   
570   if (![_f isKindOfClass:[NSArray class]])
571     _f = [NSArray arrayWithObjects:&_f count:1];
572   
573   /* select folder */
574   
575   result = [self imap4FolderNameForURL:_url removeFileName:YES];
576   if (![self selectFolder:result])
577     return [self errorCouldNotSelectURL:_url];
578   
579   /* store flags */
580   
581   result = [[self client] storeUid:[[[_url path] lastPathComponent] intValue]
582                           add:[NSNumber numberWithBool:_flag]
583                           flags:_f];
584   if (![[result valueForKey:@"result"] boolValue]) {
585     return [self errorForResult:result 
586                  text:@"Failed to change flags of IMAP4 message"];
587   }
588   /* result contains 'fetch' key with the current flags */
589   return nil;
590 }
591 - (NSException *)addFlags:(id)_f toURL:(NSURL *)_u {
592   return [self addOrRemove:YES flags:_f toURL:_u];
593 }
594 - (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u {
595   return [self addOrRemove:NO flags:_f toURL:_u];
596 }
597
598 - (NSException *)markURLDeleted:(NSURL *)_url {
599   return [self addOrRemove:YES flags:@"Deleted" toURL:_url];
600 }
601
602 - (NSException *)addFlags:(id)_f toAllMessagesInURL:(NSURL *)_url {
603   id result;
604   
605   if (![_url isNotNull]) return nil;
606   if (![_f   isNotNull]) return nil;
607
608   if (![_f isKindOfClass:[NSArray class]])
609     _f = [NSArray arrayWithObjects:&_f count:1];
610   
611   /* select folder */
612   
613   if (![self selectFolder:[self imap4FolderNameForURL:_url]])
614     return [self errorCouldNotSelectURL:_url];
615   
616   /* fetch all sequence numbers */
617   
618   result = [client searchWithQualifier:nil /* means: ALL */];
619   if (![[result valueForKey:@"result"] boolValue]) {
620     return [self errorForResult:result 
621                  text:@"Could not search in IMAP4 folder"];
622   }
623   
624   result = [result valueForKey:@"search"];
625   if ([result count] == 0) /* no messages in there, nothin' to be done */
626     return nil;
627   
628   /* store flags */
629   
630   result = [[self client] storeFlags:_f forMSNs:result addOrRemove:YES];
631   if (![[result valueForKey:@"result"] boolValue]) {
632     return [self errorForResult:result 
633                  text:@"Failed to change flags of IMAP4 message"];
634   }
635   
636   return nil;
637 }
638
639 /* posting new data */
640
641 - (NSException *)postData:(NSData *)_data flags:(id)_f
642   toFolderURL:(NSURL *)_url
643 {
644   id result;
645   
646   if (![_url isNotNull]) return nil;
647   if (![_f   isNotNull]) _f = [NSArray array];
648   
649   if (![_f isKindOfClass:[NSArray class]])
650     _f = [NSArray arrayWithObjects:&_f count:1];
651   
652   result = [[self client] append:_data 
653                           toFolder:[self imap4FolderNameForURL:_url]
654                           withFlags:_f];
655   if (![[result valueForKey:@"result"] boolValue])
656     return [self errorForResult:result text:@"Failed to store message"];
657   
658   /* result contains 'fetch' key with the current flags */
659   
660   // TODO: need to flush any caches?
661   return nil;
662 }
663
664 /* operations */
665
666 - (NSException *)expungeAtURL:(NSURL *)_url {
667   NSString *p;
668   id result;
669   
670   /* select folder */
671   
672   p = [self imap4FolderNameForURL:_url removeFileName:NO];
673   if (![self selectFolder:p])
674     return [self errorCouldNotSelectURL:_url];
675   
676   /* expunge */
677   
678   result = [[self client] expunge];
679
680   if (![[result valueForKey:@"result"] boolValue]) {
681     [self errorWithFormat:@"could not expunge url: %@", _url];
682     return nil;
683   }
684   //[self logWithFormat:@"RESULT: %@", result];
685   return nil;
686 }
687
688 /* copying and moving */
689
690 - (NSException *)copyMailURL:(NSURL *)_srcurl toFolderURL:(NSURL *)_desturl {
691   NSString *srcname, *destname;
692   unsigned uid;
693   id result;
694   
695   /* names */
696   
697   srcname  = [self imap4FolderNameForURL:_srcurl removeFileName:YES];
698   uid      = [[[_srcurl path] lastPathComponent] unsignedIntValue];
699   destname = [self imap4FolderNameForURL:_desturl];
700   
701   /* select source folder */
702   
703   if (![self selectFolder:srcname])
704     return [self errorCouldNotSelectURL:_srcurl];
705   
706   /* copy */
707   
708   result = [[self client] copyUid:uid toFolder:destname];
709   if (![[result valueForKey:@"result"] boolValue])
710     return [self errorForResult:result text:@"Copy operation failed"];
711   
712   // TODO: need to flush some caches?
713   
714   return nil;
715 }
716
717 /* managing folders */
718
719 - (BOOL)doesMailboxExistAtURL:(NSURL *)_url {
720   NSString *folderName;
721   id result;
722
723   /* check in hierarchy cache */
724   
725   if ((result = [self cachedHierarchyResults]) != nil) {
726     NSString *p;
727     
728     result = [(NSDictionary *)result objectForKey:@"list"];
729     p      = [_url path];
730 #if __APPLE__ 
731     /* normalized results already have the / in front on libFoundation?! */
732     if ([p hasPrefix:@"/"]) 
733       p = [p substringFromIndex:1];
734 #endif
735     return ([(NSDictionary *)result objectForKey:p] != nil) ? YES : NO;
736   }
737   
738   /* check using IMAP4 select */
739   // TODO: we should probably just fetch the whole hierarchy?
740   
741   folderName = [self imap4FolderNameForURL:_url];
742   result = [[self client] select:folderName];
743   if (![[result valueForKey:@"result"] boolValue])
744     return NO;
745   
746   return YES;
747 }
748
749 - (id)infoForMailboxAtURL:(NSURL *)_url {
750   NGImap4MailboxInfo *info;
751   NSString        *folderName;
752   id result;
753
754   folderName = [self imap4FolderNameForURL:_url];
755   result     = [[self client] select:folderName];
756   if (![[result valueForKey:@"result"] boolValue])
757     return [self errorCouldNotSelectURL:_url];
758   
759   info = [[NGImap4MailboxInfo alloc] initWithURL:_url folderName:folderName
760                                      selectDictionary:result];
761   return [info autorelease];
762 }
763
764 - (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url {
765   NSString *newPath;
766   id       result;
767   
768   /* construct path */
769   
770   newPath = [self imap4FolderNameForURL:_url];
771   newPath = [newPath stringByAppendingString:[self imap4Separator]];
772   newPath = [newPath stringByAppendingString:_mailbox];
773   
774   /* create */
775   
776   result = [[self client] create:newPath];
777   if (![[result valueForKey:@"result"] boolValue])
778     return [self errorForResult:result text:@"Failed to create folder"];
779   
780   [self flushFolderHierarchyCache];
781   // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result];
782   return nil;
783 }
784
785 - (NSException *)deleteMailboxAtURL:(NSURL *)_url {
786   NSString *path;
787   id       result;
788
789   /* delete */
790   
791   path   = [self imap4FolderNameForURL:_url];
792   result = [[self client] delete:path];
793   if (![[result valueForKey:@"result"] boolValue])
794     return [self errorForResult:result text:@"Failed to delete folder"];
795   
796   [self flushFolderHierarchyCache];
797 #if 0
798   [self debugWithFormat:@"delete mailbox %@: %@", _url, result];
799 #endif
800   return nil;
801 }
802
803 - (NSException *)moveMailboxAtURL:(NSURL *)_srcurl toURL:(NSURL *)_desturl {
804   NSString *srcname, *destname;
805   id result;
806   
807   /* rename */
808   
809   srcname  = [self imap4FolderNameForURL:_srcurl];
810   destname = [self imap4FolderNameForURL:_desturl];
811   
812   result = [[self client] rename:srcname to:destname];
813   if (![[result valueForKey:@"result"] boolValue])
814     return [self errorForResult:result text:@"Failed to move folder"];
815   
816   [self flushFolderHierarchyCache];
817 #if 0
818   [self debugWithFormat:@"renamed mailbox %@: %@", _srcurl, result];
819 #endif
820   return nil;
821 }
822
823 /* ACLs */
824
825 - (NSDictionary *)aclForMailboxAtURL:(NSURL *)_url {
826   /*
827     Returns a mapping of uid => permission strings, eg:
828       guizmo.g = lrs;
829       root     = lrswipcda;
830   */
831   NSString *folderName;
832   id       result;
833   
834   folderName = [self imap4FolderNameForURL:_url];
835   result     = [[self client] getACL:folderName];
836   if (![[result valueForKey:@"result"] boolValue]) {
837     return (id)[self errorForResult:result
838                      text:@"Failed to get ACL of folder"];
839   }
840   
841   return [result valueForKey:@"acl"];
842 }
843
844 - (NSString *)myRightsForMailboxAtURL:(NSURL *)_url {
845   NSString *folderName;
846   id       result;
847   
848   /* check cache */
849   
850   if ((result = [self cachedMyRightsForURL:_url]) != nil)
851     return result;
852
853   /* run IMAP4 op */
854   
855   folderName = [self imap4FolderNameForURL:_url];
856   result     = [[self client] myRights:folderName];
857   if (![[result valueForKey:@"result"] boolValue]) {
858     return (id)[self errorForResult:result 
859                      text:@"Failed to get myrights on folder"];
860   }
861   
862   /* cache results */
863   
864   if ((result = [result valueForKey:@"myrights"]) != nil)
865     [self cacheMyRights:result forURL:_url];
866   return result;
867 }
868
869 /* description */
870
871 - (NSString *)description {
872   NSMutableString *ms;
873   
874   ms = [NSMutableString stringWithCapacity:128];
875   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
876   
877   [ms appendFormat:@" client=0x%08X", self->client];
878   if ([self->password length] > 0)
879     [ms appendString:@" pwd"];
880   
881   [ms appendFormat:@" created=%@", self->creationTime];
882   
883   if (self->subfolders != nil)
884     [ms appendFormat:@" #cached-folders=%d", [self->subfolders count]];
885   
886   if (self->cachedUIDs != nil)
887     [ms appendFormat:@" #cached-uids=%d", [self->cachedUIDs count]];
888   
889   [ms appendString:@">"];
890   return ms;
891 }
892
893 @end /* NGImap4Connection */