]> err.no Git - scalable-opengroupware.org/blob - SOGo/SoObjects/Mailer/SOGoMailManager.m
partially implemented mail delete #1212
[scalable-opengroupware.org] / SOGo / SoObjects / Mailer / SOGoMailManager.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 "SOGoMailManager.h"
23 #include "SOGoMailConnectionEntry.h"
24 #include "common.h"
25
26 /*
27   Could check read-write state:
28     dict = [[self->context client] select:[self absoluteName]];
29     self->isReadOnly = 
30       [[dict objectForKey:@"access"] isEqualToString:@"READ-WRITE"]
31       ? NoNumber : YesNumber;
32 */
33
34 @implementation SOGoMailManager
35
36 static BOOL           debugOn    = NO;
37 static BOOL           debugCache = NO;
38 static BOOL           debugKeys  = NO;
39 static BOOL           poolingOff = NO;
40 static NSTimeInterval PoolScanInterval = 5 * 60;
41 static NSString       *imap4Separator  = nil;
42
43 + (void)initialize {
44   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
45   
46   debugOn    = [ud boolForKey:@"SOGoEnableIMAP4Debug"];
47   debugCache = [ud boolForKey:@"SOGoEnableIMAP4CacheDebug"];
48   poolingOff = [ud boolForKey:@"SOGoDisableIMAP4Pooling"];
49
50   if (debugOn)    NSLog(@"Note: SOGoEnableIMAP4Debug is enabled!");
51   if (poolingOff) NSLog(@"WARNING: IMAP4 connection pooling is disabled!");
52   
53   imap4Separator = [[ud stringForKey:@"SOGoIMAP4StringSeparator"] copy];
54   if ([imap4Separator length] == 0)
55     imap4Separator = @"/";
56   NSLog(@"Note(SOGoMailManager): using '%@' as the IMAP4 folder separator.", 
57         imap4Separator);
58 }
59
60 + (id)defaultMailManager {
61   static SOGoMailManager *manager = nil; // THREAD
62   if (manager == nil) 
63     manager = [[self alloc] init];
64   return manager;
65 }
66
67 - (id)init {
68   if ((self = [super init])) {
69     if (!poolingOff) {
70       self->urlToEntry = [[NSMutableDictionary alloc] initWithCapacity:256];
71     }
72     
73     self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
74                                 PoolScanInterval
75                               target:self selector:@selector(_garbageCollect:)
76                               userInfo:nil repeats:YES] retain];
77   }
78   return self;
79 }
80
81 - (void)dealloc {
82   if (self->gcTimer) [self->gcTimer invalidate];
83   [self->gcTimer release];
84   
85   [self->urlToEntry release];
86   [super dealloc];
87 }
88
89 /* cache */
90
91 - (id)cacheKeyForURL:(NSURL *)_url {
92   // protocol, user, host, port
93   return [NSString stringWithFormat:@"%@://%@@%@:%@",
94                    [_url scheme], [_url user], [_url host], [_url port]];
95 }
96
97 - (SOGoMailConnectionEntry *)entryForURL:(NSURL *)_url {
98   if (_url == nil)
99     return nil;
100   
101   return [self->urlToEntry objectForKey:[self cacheKeyForURL:_url]];
102 }
103 - (void)cacheEntry:(SOGoMailConnectionEntry *)_entry forURL:(NSURL *)_url {
104   if (_entry == nil) _entry = (id)[NSNull null];
105   [self->urlToEntry setObject:_entry forKey:[self cacheKeyForURL:_url]];
106 }
107
108 - (void)_garbageCollect:(NSTimer *)_timer {
109   // TODO: scan for old IMAP4 channels
110   [self debugWithFormat:@"should collect IMAP4 channels (%d active)",
111           [self->urlToEntry count]];
112 }
113
114 - (id)entryForURL:(NSURL *)_url password:(NSString *)_pwd {
115   /*
116     Three cases:
117     a) not yet connected             => create new entry and connect
118     b) connected, correct password   => return cached entry
119     c) connected, different password => try to recreate entry
120   */
121   SOGoMailConnectionEntry *entry;
122   NGImap4Client *client;
123
124   /* check cache */
125   
126   if ((entry = [self entryForURL:_url]) != nil) {
127     if ([entry isValidPassword:_pwd]) {
128       if (debugCache)
129         [self logWithFormat:@"valid password, reusing cache entry ..."];
130       return entry;
131     }
132     
133     /* different password, password could have changed! */
134     if (debugCache)
135       [self logWithFormat:@"different password than cached entry: %@", _url];
136     entry = nil;
137   }
138   else
139     [self debugWithFormat:@"no connection cached yet for url: %@", _url];
140   
141   /* try to login */
142   
143   client = [entry isValidPassword:_pwd]
144     ? [entry client]
145     : [self imap4ClientForURL:_url password:_pwd];
146   
147   if (client == nil)
148     return nil;
149   
150   /* sideeffect of -imap4ClientForURL:password: is to create a cache entry */
151   return [self entryForURL:_url];
152 }
153
154 /* client object */
155
156 - (NGImap4Client *)imap4ClientForURL:(NSURL *)_url password:(NSString *)_pwd {
157   // TODO: move to some global IMAP4 connection pool manager
158   SOGoMailConnectionEntry *entry;
159   NGImap4Client *client;
160   NSDictionary  *result;
161   
162   if (_url == nil)
163     return nil;
164
165   /* check connection pool */
166   
167   if ((entry = [self entryForURL:_url]) != nil) {
168     if ([entry isValidPassword:_pwd]) {
169       [self debugWithFormat:@"reused IMAP4 connection for URL: %@", _url];
170       return [entry client];
171     }
172     
173     /* different password, password could have changed! */
174     entry = nil;
175   }
176   
177   /* setup connection and attempt login */
178   
179   if ((client = [NGImap4Client clientWithURL:_url]) == nil)
180     return nil;
181   
182   result = [client login:[_url user] password:_pwd];
183   if (![[result valueForKey:@"result"] boolValue]) {
184     [self errorWithFormat:
185             @"IMAP4 login failed (host=%@,user=%@,pwd=%s,url=%@/%@/%@): %@", 
186             [_url host], [_url user], [_pwd length] > 0 ? "yes" : "no", 
187             [_url absoluteString], [_url baseURL],
188             NSStringFromClass([[_url baseURL] class]),
189             client];
190     return nil;
191   }
192   
193   [self debugWithFormat:@"created new IMAP4 connection for URL: %@", _url];
194   
195   /* cache connection in pool */
196   
197   entry = [[SOGoMailConnectionEntry alloc] initWithClient:client 
198                                            password:_pwd];
199   [self cacheEntry:entry forURL:_url];
200   [entry release]; entry = nil;
201   
202   return client;
203 }
204
205 - (void)flushCachesForURL:(NSURL *)_url {
206   SOGoMailConnectionEntry *entry;
207   
208   if ((entry = [self entryForURL:_url]) == nil) /* nothing cached */
209     return;
210   
211   [entry flushFolderHierarchyCache];
212   [entry flushMailCaches];
213 }
214
215 /* folder hierarchy */
216
217 - (NSArray *)_getDirectChildren:(NSArray *)_array folderName:(NSString *)_fn {
218   /*
219     Scans string '_array' for strings which start with the string in '_fn'.
220     Then split on '/'.
221   */
222   NSMutableArray *ma;
223   unsigned i, count, prefixlen;
224   
225   if ((count = [_array count]) < 2)
226     /* one entry is the folder itself, so we need at least two */
227     return [NSArray array];
228   
229   prefixlen = [_fn length] + 1;
230   ma = [NSMutableArray arrayWithCapacity:count];
231   for (i = 0; i < count; i++) {
232     NSString *p;
233     
234     p = [_array objectAtIndex:i];
235     if ([p length] <= prefixlen)
236       continue;
237     if (![p hasPrefix:_fn])
238       continue;
239     
240     /* cut of common part */
241     p = [p substringFromIndex:prefixlen];
242     
243     /* check whether the path is a sub-subfolder path */
244     if ([p rangeOfString:@"/"].length > 0)
245       continue;
246     
247     [ma addObject:p];
248   }
249   
250   [ma sortUsingSelector:@selector(compare:)];
251   return ma;
252 }
253
254 - (NSString *)imap4Separator {
255   return imap4Separator;
256 }
257
258 - (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn {
259   /* a bit hackish, but should be OK */
260   NSString *folderName;
261   NSArray  *names;
262
263   if (_url == nil)
264     return nil;
265   
266   folderName = [_url path];
267   if ([folderName length] == 0)
268     return nil;
269   if ([folderName characterAtIndex:0] == '/')
270     folderName = [folderName substringFromIndex:1];
271   
272   if (_delfn) folderName = [folderName stringByDeletingLastPathComponent];
273   
274   if ([[self imap4Separator] isEqualToString:@"/"])
275     return folderName;
276   
277   names = [folderName pathComponents];
278   return [names componentsJoinedByString:[self imap4Separator]];
279 }
280 - (NSString *)imap4FolderNameForURL:(NSURL *)_url {
281   return [self imap4FolderNameForURL:_url removeFileName:NO];
282 }
283
284 - (NSArray *)extractSubfoldersForURL:(NSURL *)_url
285   fromResultSet:(NSDictionary *)_result
286 {
287   NSString     *folderName;
288   NSDictionary *result;
289   NSArray      *names;
290   NSArray      *flags;
291   
292   /* Note: the result is normalized, that is, it contains / as the separator */
293   folderName = [_url path];
294 #if __APPLE__ 
295   /* normalized results already have the / in front on libFoundation?! */
296   if ([folderName hasPrefix:@"/"]) 
297     folderName = [folderName substringFromIndex:1];
298 #endif
299   
300   result = [_result valueForKey:@"list"];
301   
302   /* Cyrus already tells us whether we need to check for children */
303   flags = [result objectForKey:folderName];
304   if ([flags containsObject:@"hasnochildren"]) {
305     if (debugKeys)
306       [self logWithFormat:@"folder %@ has no children.", folderName];
307     return nil;
308   }
309
310   if (debugKeys)
311     [self logWithFormat:@"all keys %@: %@", folderName, [result allKeys]];
312   
313   names = [self _getDirectChildren:[result allKeys] folderName:folderName];
314   if (debugKeys) {
315     [self debugWithFormat:@"subfolders of %@: %@", folderName, 
316             [names componentsJoinedByString:@","]];
317   }
318   return names;
319 }
320
321 - (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result {
322   /* Note: the result is normalized, that is, it contains / as the separator */
323   return [[_result valueForKey:@"list"] allKeys];
324 }
325
326 - (NSArray *)subfoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
327   SOGoMailConnectionEntry *entry;
328   NSDictionary  *result;
329
330   if (debugKeys)
331     [self debugWithFormat:@"subfolders for URL: %@ ...",[_url absoluteString]];
332   
333   /* check connection cache */
334   
335   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
336     return nil;
337   
338   /* check hierarchy cache */
339   
340   if ((result = [entry cachedHierarchyResults]) != nil)
341     return [self extractSubfoldersForURL:_url fromResultSet:result];
342   
343   [self debugWithFormat:@"  no folders cached yet .."];
344   
345   /* fetch _all_ folders */
346   
347   result = [[entry client] list:@"INBOX" pattern:@"*"];
348   if (![[result valueForKey:@"result"] boolValue]) {
349     [self errorWithFormat:@"listing of folder failed!"];
350     return nil;
351   }
352   
353   /* cache results */
354   
355   if ([result isNotNull]) {
356     if (entry == nil) /* required in case the entry was not setup */
357       entry = [self entryForURL:_url];
358     
359     [entry cacheHierarchyResults:result];
360     if (debugCache) {
361       [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)", 
362               entry, result, [result count]];
363     }
364   }
365   
366   /* extract list */
367   
368   return [self extractSubfoldersForURL:_url fromResultSet:result];
369 }
370
371 - (NSArray *)allFoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
372   SOGoMailConnectionEntry *entry;
373   NSDictionary  *result;
374
375   if (debugKeys)
376     [self debugWithFormat:@"folders for URL: %@ ...",[_url absoluteString]];
377   
378   /* check connection cache */
379   
380   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
381     return nil;
382   
383   /* check hierarchy cache */
384   
385   if ((result = [entry cachedHierarchyResults]) != nil)
386     return [self extractFoldersFromResultSet:result];
387   
388   [self debugWithFormat:@"  no folders cached yet .."];
389   
390   /* fetch _all_ folders */
391   
392   result = [[entry client] list:@"INBOX" pattern:@"*"];
393   if (![[result valueForKey:@"result"] boolValue]) {
394     [self logWithFormat:@"ERROR: listing of folder failed!"];
395     return nil;
396   }
397   
398   /* cache results */
399   
400   if ([result isNotNull]) {
401     if (entry == nil) /* required in case the entry was not setup */
402       entry = [self entryForURL:_url];
403     
404     [entry cacheHierarchyResults:result];
405     if (debugCache) {
406       [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)", 
407               entry, result, [result count]];
408     }
409   }
410   
411   /* extract list */
412   return [self extractFoldersFromResultSet:result];
413 }
414
415 /* messages */
416
417 - (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
418   sortOrdering:(id)_so password:(NSString *)_pwd
419 {
420   /* 
421      sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
422   */
423   SOGoMailConnectionEntry *entry;
424   NSDictionary  *result;
425   NSArray       *uids;
426   
427   /* check connection cache */
428   
429   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
430     return nil;
431   
432   /* check cache */
433   
434   uids = [entry cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so];
435   if (uids != nil) {
436     if (debugCache) [self logWithFormat:@"reusing uid cache!"];
437     return [uids isNotNull] ? uids : nil;
438   }
439   
440   /* select folder and fetch */
441   
442   result = [[entry client] select:[self imap4FolderNameForURL:_url]];
443   if (![[result valueForKey:@"result"] boolValue]) {
444     [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
445     return nil;
446   }
447   
448   result = [[entry client] sort:_so qualifier:_qualifier encoding:@"UTF-8"];
449   if (![[result valueForKey:@"result"] boolValue]) {
450     [self errorWithFormat:@"could not sort contents of URL: %@", _url];
451     return nil;
452   }
453   
454   uids = [result valueForKey:@"sort"];
455   if (![uids isNotNull]) {
456     [self errorWithFormat:@"got no UIDs for URL: %@: %@", _url, result];
457     return nil;
458   }
459   
460   /* cache */
461   
462   [entry cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so];
463   return uids;
464 }
465
466 - (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
467   parts:(NSArray *)_parts password:(NSString *)_pwd
468 {
469   // currently returns a dict?!
470   /*
471     Allowed fetch keys:
472       UID
473       BODY.PEEK[<section>]<<partial>>
474       BODY            [this is the bodystructure, supported]
475       BODYSTRUCTURE   [not supported yet!]
476       ENVELOPE        [this is a parsed header, but does not include type]
477       FLAGS
478       INTERNALDATE
479       RFC822
480       RFC822.HEADER
481       RFC822.SIZE
482       RFC822.TEXT
483   */
484   NGImap4Client *client;
485   NSDictionary  *result;
486   
487   if (_uids == nil)
488     return nil;
489   if ([_uids count] == 0)
490     return nil; // TODO: might break empty folders?! return a dict!
491   
492   if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
493     return nil;
494
495   /* select folder */
496   
497   result = [client select:[self imap4FolderNameForURL:_url]];
498   if (![[result valueForKey:@"result"] boolValue]) {
499     [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
500     return nil;
501   }
502   
503   /* fetch parts */
504   
505   // TODO: split uids into batches, otherwise Cyrus will complain
506   //       => not really important because we batch before (in the sort)
507   //       if the list is too long, we get a:
508   //       "* BYE Fatal error: word too long"
509   
510   result = [client fetchUids:_uids parts:_parts];
511   if (![[result valueForKey:@"result"] boolValue]) {
512     [self errorWithFormat:@"could not fetch %d uids for url: %@",
513             [_uids count],_url];
514     return nil;
515   }
516   
517   //[self logWithFormat:@"RESULT: %@", result];
518   return (id)result;
519 }
520
521 - (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts password:(NSString *)_pwd{
522   // currently returns a dict
523   NGImap4Client *client;
524   NSDictionary  *result;
525   NSString *uid;
526   
527   if (![_url isNotNull]) return nil;
528   
529   if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
530     return nil;
531   
532   /* select folder */
533   
534   result = [client select:[self imap4FolderNameForURL:_url
535                                 removeFileName:YES]];
536   if (![[result valueForKey:@"result"] boolValue]) {
537     [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
538     return nil;
539   }
540   
541   /* fetch parts */
542   
543   uid = [[_url path] lastPathComponent];
544   
545   result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts];
546   if (![[result valueForKey:@"result"] boolValue]) {
547     [self errorWithFormat:@"could not fetch url: %@", _url];
548     return nil;
549   }
550   //[self logWithFormat:@"RESULT: %@", result];
551   return (id)result;
552 }
553
554 - (NSData *)fetchContentOfBodyPart:(NSString *)_partId
555   atURL:(NSURL *)_url password:(NSString *)_pwd
556 {
557   NSString *key;
558   NSArray  *parts;
559   id result, fetch, body;
560   
561   if (_partId == nil) return nil;
562   
563   key   = [@"body[" stringByAppendingString:_partId];
564   key   = [key stringByAppendingString:@"]"];
565   parts = [NSArray arrayWithObjects:&key count:1];
566   
567   /* fetch */
568   
569   result = [self fetchURL:_url parts:parts password:_pwd];
570   
571   /* process results */
572   
573   result = [result objectForKey:@"fetch"];
574   if ([result count] == 0) { /* did not find part */
575     [self errorWithFormat:@"did not find part: %@", _partId];
576     return nil;
577   }
578   
579   fetch = [result objectAtIndex:0];
580   if ((body = [fetch objectForKey:@"body"]) == nil) {
581     [self errorWithFormat:@"did not find body in response: %@", result];
582     return nil;
583   }
584   
585   if ((result = [body objectForKey:@"data"]) == nil) {
586     [self errorWithFormat:@"did not find data in body: %@", fetch];
587     return nil;
588   }
589   return result;
590 }
591
592 - (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f
593   toURL:(NSURL *)_url password:(NSString *)_p
594 {
595   NGImap4Client *client;
596   id result;
597   
598   if (![_url isNotNull]) return nil;
599   if (![_f   isNotNull]) return nil;
600   
601   if ((client = [self imap4ClientForURL:_url password:_p]) == nil)
602     return nil;
603   
604   if (![_f isKindOfClass:[NSArray class]])
605     _f = [NSArray arrayWithObjects:&_f count:1];
606   
607   result = [client storeUid:[[[_url path] lastPathComponent] intValue]
608                    add:[NSNumber numberWithBool:_flag]
609                    flags:_f];
610   if (![[result valueForKey:@"result"] boolValue]) {
611     [self logWithFormat:@"DEBUG: fail result %@", result];
612     return [NSException exceptionWithHTTPStatus:500 /* server error */
613                         reason:@"failed to add flag to IMAP4 message"];
614   }
615   /* result contains 'fetch' key with the current flags */
616   return nil;
617 }
618 - (NSException *)addFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
619   return [self addOrRemove:YES flags:_f toURL:_u password:_p];
620 }
621 - (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
622   return [self addOrRemove:NO flags:_f toURL:_u password:_p];
623 }
624
625 - (NSException *)markURLDeleted:(NSURL *)_url password:(NSString *)_p {
626   return [self addOrRemove:YES flags:@"Deleted" toURL:_url password:_p];
627 }
628
629 - (NSException *)postData:(NSData *)_data flags:(id)_f
630   toFolderURL:(NSURL *)_url password:(NSString *)_p
631 {
632   NGImap4Client *client;
633   id result;
634   
635   if (![_url isNotNull]) return nil;
636   if (![_f   isNotNull]) _f = [NSArray array];
637   
638   if ((client = [self imap4ClientForURL:_url password:_p]) == nil)
639     return nil;
640   
641   if (![_f isKindOfClass:[NSArray class]])
642     _f = [NSArray arrayWithObjects:&_f count:1];
643   
644   result = [client append:_data 
645                    toFolder:[self imap4FolderNameForURL:_url]
646                    withFlags:_f];
647   if (![[result valueForKey:@"result"] boolValue]) {
648     [self logWithFormat:@"DEBUG: fail result %@", result];
649     return [NSException exceptionWithHTTPStatus:500 /* server error */
650                         reason:@"failed to store message to IMAP4 message"];
651   }
652   /* result contains 'fetch' key with the current flags */
653   return nil;
654 }
655
656 /* managing folders */
657
658 - (BOOL)isPermissionDeniedResult:(id)_result {
659   if ([[_result valueForKey:@"result"] intValue] != 0)
660     return NO;
661   
662   return [[_result valueForKey:@"reason"] 
663                    isEqualToString:@"Permission denied"];
664 }
665
666 - (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url
667   password:(NSString *)_pwd
668 {
669   SOGoMailConnectionEntry *entry;
670   NSString *newPath;
671   id       result;
672
673   /* check connection cache */
674   
675   if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
676     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
677                         reason:@"did not find IMAP4 folder"];
678   }
679
680   /* construct path */
681   
682   newPath = [self imap4FolderNameForURL:_url];
683   newPath = [newPath stringByAppendingString:[self imap4Separator]];
684   newPath = [newPath stringByAppendingString:_mailbox];
685   
686   /* create */
687   
688   result = [[entry client] create:newPath];
689   if ([self isPermissionDeniedResult:result]) {
690     return [NSException exceptionWithHTTPStatus:403 /* forbidden */
691                         reason:@"creation of folders not allowed"];
692   }
693   else if ([[result valueForKey:@"result"] intValue] == 0) {
694     return [NSException exceptionWithHTTPStatus:500 /* server error */
695                         reason:[result valueForKey:@"reason"]];
696   }
697   
698   [entry flushFolderHierarchyCache];
699   // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result];
700   return nil;
701 }
702
703 - (NSException *)deleteMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
704   SOGoMailConnectionEntry *entry;
705   NSString *path;
706   id       result;
707
708   /* check connection cache */
709   
710   if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
711     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
712                         reason:@"did not find IMAP4 folder"];
713   }
714   
715   /* delete */
716   
717   path   = [self imap4FolderNameForURL:_url];
718   result = [[entry client] delete:path];
719   
720   if ([self isPermissionDeniedResult:result]) {
721     return [NSException exceptionWithHTTPStatus:403 /* forbidden */
722                         reason:@"creation of folders not allowed"];
723   }
724   else if ([[result valueForKey:@"result"] intValue] == 0) {
725     return [NSException exceptionWithHTTPStatus:500 /* server error */
726                         reason:[result valueForKey:@"reason"]];
727   }
728
729   [entry flushFolderHierarchyCache];
730 #if 0
731   [self debugWithFormat:@"delete mailbox %@: %@", _url, result];
732 #endif
733   return nil;
734 }
735
736 /* debugging */
737
738 - (BOOL)isDebuggingEnabled {
739   return debugOn;
740 }
741
742 @end /* SOGoMailManager */