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