]> err.no Git - scalable-opengroupware.org/blob - SOGo/SoObjects/Mailer/SOGoMailManager.m
ea9e1fcf38b0b266a14273f441685c9d8222a916
[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 "SOGoMailboxInfo.h"
25 #include "common.h"
26
27 /*
28   Could check read-write state:
29     dict = [[self->context client] select:[self absoluteName]];
30     self->isReadOnly = 
31       [[dict objectForKey:@"access"] isEqualToString:@"READ-WRITE"]
32       ? NoNumber : YesNumber;
33   
34   TODO: to implement copy, use "uid copy" instead of "copy" as used by
35         NGImap4Client.
36 */
37
38 @implementation SOGoMailManager
39
40 static BOOL           debugOn    = NO;
41 static BOOL           debugCache = NO;
42 static BOOL           debugKeys  = NO;
43 static BOOL           poolingOff = NO;
44 static BOOL           alwaysSelect     = NO;
45 static BOOL           onlyFetchInbox   = NO;
46 static NSTimeInterval PoolScanInterval = 5 * 60 /* every five minutes */;
47 static NSString       *imap4Separator  = nil;
48
49 + (void)initialize {
50   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
51   
52   debugOn      = [ud boolForKey:@"SOGoEnableIMAP4Debug"];
53   debugCache   = [ud boolForKey:@"SOGoEnableIMAP4CacheDebug"];
54   poolingOff   = [ud boolForKey:@"SOGoDisableIMAP4Pooling"];
55   alwaysSelect = [ud boolForKey:@"SOGoAlwaysSelectIMAP4Folder"];
56   
57   if (debugOn)    NSLog(@"Note: SOGoEnableIMAP4Debug is enabled!");
58   if (poolingOff) NSLog(@"WARNING: IMAP4 connection pooling is disabled!");
59
60   if (alwaysSelect)
61     NSLog(@"WARNING: 'SOGoAlwaysSelectIMAP4Folder' enabled (slow down)");
62   
63   imap4Separator = [[ud stringForKey:@"SOGoIMAP4StringSeparator"] copy];
64   if ([imap4Separator length] == 0)
65     imap4Separator = @"/";
66   NSLog(@"Note(SOGoMailManager): using '%@' as the IMAP4 folder separator.", 
67         imap4Separator);
68 }
69
70 + (id)defaultMailManager {
71   static SOGoMailManager *manager = nil; // THREAD
72   if (manager == nil) 
73     manager = [[self alloc] init];
74   return manager;
75 }
76
77 - (id)init {
78   if ((self = [super init])) {
79     if (!poolingOff) {
80       self->urlToEntry = [[NSMutableDictionary alloc] initWithCapacity:256];
81     }
82     
83     self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
84                                 PoolScanInterval
85                               target:self selector:@selector(_garbageCollect:)
86                               userInfo:nil repeats:YES] retain];
87   }
88   return self;
89 }
90
91 - (void)dealloc {
92   if (self->gcTimer) [self->gcTimer invalidate];
93   [self->gcTimer release];
94   
95   [self->urlToEntry release];
96   [super dealloc];
97 }
98
99 /* cache */
100
101 - (id)cacheKeyForURL:(NSURL *)_url {
102   // protocol, user, host, port
103   return [NSString stringWithFormat:@"%@://%@@%@:%@",
104                    [_url scheme], [_url user], [_url host], [_url port]];
105 }
106
107 - (SOGoMailConnectionEntry *)entryForURL:(NSURL *)_url {
108   if (_url == nil)
109     return nil;
110   
111   return [self->urlToEntry objectForKey:[self cacheKeyForURL:_url]];
112 }
113 - (void)cacheEntry:(SOGoMailConnectionEntry *)_entry forURL:(NSURL *)_url {
114   if (_entry == nil) _entry = (id)[NSNull null];
115   [self->urlToEntry setObject:_entry forKey:[self cacheKeyForURL:_url]];
116 }
117
118 - (void)_garbageCollect:(NSTimer *)_timer {
119   // TODO: scan for old IMAP4 channels
120   [self debugWithFormat:@"should collect IMAP4 channels (%d active)",
121           [self->urlToEntry count]];
122 }
123
124 - (id)entryForURL:(NSURL *)_url password:(NSString *)_pwd {
125   /*
126     Three cases:
127     a) not yet connected             => create new entry and connect
128     b) connected, correct password   => return cached entry
129     c) connected, different password => try to recreate entry
130   */
131   SOGoMailConnectionEntry *entry;
132   NGImap4Client *client;
133
134   /* check cache */
135   
136   if ((entry = [self entryForURL:_url]) != nil) {
137     if ([entry isValidPassword:_pwd]) {
138       if (debugCache)
139         [self logWithFormat:@"valid password, reusing cache entry ..."];
140       return entry;
141     }
142     
143     /* different password, password could have changed! */
144     if (debugCache)
145       [self logWithFormat:@"different password than cached entry: %@", _url];
146     entry = nil;
147   }
148   else
149     [self debugWithFormat:@"no connection cached yet for url: %@", _url];
150   
151   /* try to login */
152   
153   client = [entry isValidPassword:_pwd]
154     ? [entry client]
155     : [self imap4ClientForURL:_url password:_pwd];
156   
157   if (client == nil)
158     return nil;
159   
160   /* sideeffect of -imap4ClientForURL:password: is to create a cache entry */
161   return [self entryForURL:_url];
162 }
163
164 /* client object */
165
166 - (NGImap4Client *)imap4ClientForURL:(NSURL *)_url password:(NSString *)_pwd {
167   // TODO: move to some global IMAP4 connection pool manager
168   SOGoMailConnectionEntry *entry;
169   NGImap4Client *client;
170   NSDictionary  *result;
171   
172   if (_url == nil)
173     return nil;
174
175   /* check connection pool */
176   
177   if ((entry = [self entryForURL:_url]) != nil) {
178     if ([entry isValidPassword:_pwd]) {
179       [self debugWithFormat:@"reused IMAP4 connection for URL: %@", _url];
180       return [entry client];
181     }
182     
183     /* different password, password could have changed! */
184     entry = nil;
185   }
186   
187   /* setup connection and attempt login */
188   
189   if ((client = [NGImap4Client clientWithURL:_url]) == nil)
190     return nil;
191   
192   result = [client login:[_url user] password:_pwd];
193   if (![[result valueForKey:@"result"] boolValue]) {
194     [self errorWithFormat:
195             @"IMAP4 login failed:\n"
196             @"  host=%@, user=%@, pwd=%s\n"
197             @"  url=%@\n  base=%@\n  base-class=%@)\n"
198             @"  = %@", 
199             [_url host], [_url user], [_pwd length] > 0 ? "yes" : "no", 
200             [_url absoluteString],
201             [_url baseURL],
202             NSStringFromClass([[_url baseURL] class]),
203             client];
204     return nil;
205   }
206   
207   [self debugWithFormat:@"created new IMAP4 connection for URL: %@", _url];
208   
209   /* cache connection in pool */
210   
211   entry = [[SOGoMailConnectionEntry alloc] initWithClient:client 
212                                            password:_pwd];
213   [self cacheEntry:entry forURL:_url];
214   [entry release]; entry = nil;
215   
216   return client;
217 }
218
219 - (void)flushCachesForURL:(NSURL *)_url {
220   SOGoMailConnectionEntry *entry;
221   
222   if ((entry = [self entryForURL:_url]) == nil) /* nothing cached */
223     return;
224   
225   [entry flushFolderHierarchyCache];
226   [entry flushMailCaches];
227 }
228
229 - (BOOL)selectFolder:(id)_url inClient:(NGImap4Client *)_client {
230   NSDictionary *result;
231   NSString     *newFolder;
232   
233   newFolder = [_url isKindOfClass:[NSURL class]]
234     ? [self imap4FolderNameForURL:_url]
235     : _url;
236   
237   if (!alwaysSelect) {
238     if ([[_client selectedFolderName] isEqualToString:newFolder])
239       return YES;
240   }
241   
242   result = [_client select:newFolder];
243   if (![[result valueForKey:@"result"] boolValue]) {
244     [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
245     return NO;
246   }
247
248   return YES;
249 }
250
251 /* folder hierarchy */
252
253 - (NSArray *)_getDirectChildren:(NSArray *)_array folderName:(NSString *)_fn {
254   /*
255     Scans string '_array' for strings which start with the string in '_fn'.
256     Then split on '/'.
257   */
258   NSMutableArray *ma;
259   unsigned i, count, prefixlen;
260   
261   if ((count = [_array count]) < 2)
262     /* one entry is the folder itself, so we need at least two */
263     return [NSArray array];
264   
265 #if __APPLE__ 
266   // TODO: somehow results are different on OSX
267   prefixlen = [_fn isEqualToString:@""] ? 0 : [_fn length] + 1;
268 #else
269   prefixlen = [_fn isEqualToString:@"/"] ? 1 : [_fn length] + 1;
270 #endif
271   ma = [NSMutableArray arrayWithCapacity:count];
272   for (i = 0; i < count; i++) {
273     NSString *p;
274     
275     p = [_array objectAtIndex:i];
276     if ([p length] <= prefixlen)
277       continue;
278     if (prefixlen != 0 && ![p hasPrefix:_fn])
279       continue;
280     
281     /* cut of common part */
282     p = [p substringFromIndex:prefixlen];
283     
284     /* check whether the path is a sub-subfolder path */
285     if ([p rangeOfString:@"/"].length > 0)
286       continue;
287     
288     [ma addObject:p];
289   }
290   
291   [ma sortUsingSelector:@selector(compare:)];
292   return ma;
293 }
294
295 - (NSString *)imap4Separator {
296   return imap4Separator;
297 }
298
299 - (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn {
300   /* a bit hackish, but should be OK */
301   NSString *folderName;
302   NSArray  *names;
303
304   if (_url == nil)
305     return nil;
306   
307   folderName = [_url path];
308   if ([folderName length] == 0)
309     return nil;
310   if ([folderName characterAtIndex:0] == '/')
311     folderName = [folderName substringFromIndex:1];
312   
313   if (_delfn) folderName = [folderName stringByDeletingLastPathComponent];
314   
315   if ([[self imap4Separator] isEqualToString:@"/"])
316     return folderName;
317   
318   names = [folderName pathComponents];
319   return [names componentsJoinedByString:[self imap4Separator]];
320 }
321 - (NSString *)imap4FolderNameForURL:(NSURL *)_url {
322   return [self imap4FolderNameForURL:_url removeFileName:NO];
323 }
324
325 - (NSArray *)extractSubfoldersForURL:(NSURL *)_url
326   fromResultSet:(NSDictionary *)_result
327 {
328   NSString     *folderName;
329   NSDictionary *result;
330   NSArray      *names;
331   NSArray      *flags;
332   
333   /* Note: the result is normalized, that is, it contains / as the separator */
334   folderName = [_url path];
335 #if __APPLE__ 
336   /* normalized results already have the / in front on libFoundation?! */
337   if ([folderName hasPrefix:@"/"]) 
338     folderName = [folderName substringFromIndex:1];
339 #endif
340   
341   result = [_result valueForKey:@"list"];
342   
343   /* Cyrus already tells us whether we need to check for children */
344   flags = [result objectForKey:folderName];
345   if ([flags containsObject:@"hasnochildren"]) {
346     if (debugKeys)
347       [self logWithFormat:@"folder %@ has no children.", folderName];
348     return nil;
349   }
350
351   if (debugKeys) {
352     [self logWithFormat:@"all keys %@: %@", folderName, 
353           [[result allKeys] componentsJoinedByString:@", "]];
354   }
355   
356   names = [self _getDirectChildren:[result allKeys] folderName:folderName];
357   if (debugKeys) {
358     [self logWithFormat:@"subfolders of '%@': %@", folderName, 
359             [names componentsJoinedByString:@","]];
360   }
361   return names;
362 }
363
364 - (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result {
365   /* Note: the result is normalized, that is, it contains / as the separator */
366   return [[_result valueForKey:@"list"] allKeys];
367 }
368
369 - (NSArray *)subfoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
370   SOGoMailConnectionEntry *entry;
371   NSDictionary  *result;
372
373   if (debugKeys)
374     [self debugWithFormat:@"subfolders for URL: %@ ...",[_url absoluteString]];
375   
376   /* check connection cache */
377   
378   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
379     return nil;
380   
381   /* check hierarchy cache */
382   
383   if ((result = [entry cachedHierarchyResults]) != nil)
384     return [self extractSubfoldersForURL:_url fromResultSet:result];
385   
386   [self debugWithFormat:@"  no folders cached yet .."];
387   
388   /* fetch _all_ folders */
389   
390   result = [[entry client] list:(onlyFetchInbox ? @"INBOX" : @"*")
391                            pattern:@"*"];
392   if (![[result valueForKey:@"result"] boolValue]) {
393     [self errorWithFormat:@"listing of folder failed!"];
394     return nil;
395   }
396   
397   /* cache results */
398   
399   if ([result isNotNull]) {
400     if (entry == nil) /* required in case the entry was not setup */
401       entry = [self entryForURL:_url];
402     
403     [entry cacheHierarchyResults:result];
404     if (debugCache) {
405       [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)", 
406               entry, result, [result count]];
407     }
408   }
409   
410   /* extract list */
411   
412   return [self extractSubfoldersForURL:_url fromResultSet:result];
413 }
414
415 - (NSArray *)allFoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
416   SOGoMailConnectionEntry *entry;
417   NSDictionary  *result;
418
419   if (debugKeys)
420     [self debugWithFormat:@"folders for URL: %@ ...",[_url absoluteString]];
421   
422   /* check connection cache */
423   
424   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
425     return nil;
426   
427   /* check hierarchy cache */
428   
429   if ((result = [entry cachedHierarchyResults]) != nil)
430     return [self extractFoldersFromResultSet:result];
431   
432   [self debugWithFormat:@"  no folders cached yet .."];
433   
434   /* fetch _all_ folders */
435   
436   result = [[entry client] list:@"INBOX" pattern:@"*"];
437   if (![[result valueForKey:@"result"] boolValue]) {
438     [self logWithFormat:@"ERROR: listing of folder failed!"];
439     return nil;
440   }
441   
442   /* cache results */
443   
444   if ([result isNotNull]) {
445     if (entry == nil) /* required in case the entry was not setup */
446       entry = [self entryForURL:_url];
447     
448     [entry cacheHierarchyResults:result];
449     if (debugCache) {
450       [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)", 
451               entry, result, [result count]];
452     }
453   }
454   
455   /* extract list */
456   return [self extractFoldersFromResultSet:result];
457 }
458
459 /* messages */
460
461 - (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
462   sortOrdering:(id)_so password:(NSString *)_pwd
463 {
464   /* 
465      sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
466   */
467   SOGoMailConnectionEntry *entry;
468   NSDictionary  *result;
469   NSArray       *uids;
470   
471   /* check connection cache */
472   
473   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
474     return nil;
475   
476   /* check cache */
477   
478   uids = [entry cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so];
479   if (uids != nil) {
480     if (debugCache) [self logWithFormat:@"reusing uid cache!"];
481     return [uids isNotNull] ? uids : nil;
482   }
483   
484   /* select folder and fetch */
485   
486   if (![self selectFolder:_url inClient:[entry client]])
487     return nil;
488   
489   result = [[entry client] sort:_so qualifier:_qualifier encoding:@"UTF-8"];
490   if (![[result valueForKey:@"result"] boolValue]) {
491     [self errorWithFormat:@"could not sort contents of URL: %@", _url];
492     return nil;
493   }
494   
495   uids = [result valueForKey:@"sort"];
496   if (![uids isNotNull]) {
497     [self errorWithFormat:@"got no UIDs for URL: %@: %@", _url, result];
498     return nil;
499   }
500   
501   /* cache */
502   
503   [entry cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so];
504   return uids;
505 }
506
507 - (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
508   parts:(NSArray *)_parts password:(NSString *)_pwd
509 {
510   // currently returns a dict?!
511   /*
512     Allowed fetch keys:
513       UID
514       BODY.PEEK[<section>]<<partial>>
515       BODY            [this is the bodystructure, supported]
516       BODYSTRUCTURE   [not supported yet!]
517       ENVELOPE        [this is a parsed header, but does not include type]
518       FLAGS
519       INTERNALDATE
520       RFC822
521       RFC822.HEADER
522       RFC822.SIZE
523       RFC822.TEXT
524   */
525   NGImap4Client *client;
526   NSDictionary  *result;
527   
528   if (_uids == nil)
529     return nil;
530   if ([_uids count] == 0)
531     return nil; // TODO: might break empty folders?! return a dict!
532   
533   if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
534     return nil;
535
536   /* select folder */
537
538   if (![self selectFolder:_url inClient:client])
539     return nil;
540   
541   /* fetch parts */
542   
543   // TODO: split uids into batches, otherwise Cyrus will complain
544   //       => not really important because we batch before (in the sort)
545   //       if the list is too long, we get a:
546   //       "* BYE Fatal error: word too long"
547   
548   result = [client fetchUids:_uids parts:_parts];
549   if (![[result valueForKey:@"result"] boolValue]) {
550     [self errorWithFormat:@"could not fetch %d uids for url: %@",
551             [_uids count],_url];
552     return nil;
553   }
554   
555   //[self logWithFormat:@"RESULT: %@", result];
556   return (id)result;
557 }
558
559 - (NSException *)expungeAtURL:(NSURL *)_url password:(NSString *)_pwd {
560   NGImap4Client *client;
561   NSString *p;
562   id result;
563   
564   if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
565     return nil; // TODO: return error?
566   
567   /* select folder */
568   
569   p = [self imap4FolderNameForURL:_url removeFileName:NO];
570   if (![self selectFolder:p inClient:client])
571     return nil;
572   
573   /* expunge */
574   
575   result = [client expunge];
576
577   if (![[result valueForKey:@"result"] boolValue]) {
578     [self errorWithFormat:@"could not expunge url: %@", _url];
579     return nil;
580   }
581   //[self logWithFormat:@"RESULT: %@", result];
582   return nil;
583 }
584
585 - (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts password:(NSString *)_pwd{
586   // currently returns a dict
587   NGImap4Client *client;
588   NSDictionary  *result;
589   NSString *uid;
590   
591   if (![_url isNotNull]) return nil;
592   
593   if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
594     return nil;
595   
596   /* select folder */
597   
598   if (![self selectFolder:[self imap4FolderNameForURL:_url removeFileName:YES]
599              inClient:client])
600     return nil;
601   
602   /* fetch parts */
603   
604   uid = [[_url path] lastPathComponent];
605   
606   result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts];
607   if (![[result valueForKey:@"result"] boolValue]) {
608     [self errorWithFormat:@"could not fetch url: %@", _url];
609     return nil;
610   }
611   //[self logWithFormat:@"RESULT: %@", result];
612   return (id)result;
613 }
614
615 - (NSData *)fetchContentOfBodyPart:(NSString *)_partId
616   atURL:(NSURL *)_url password:(NSString *)_pwd
617 {
618   NSString *key;
619   NSArray  *parts;
620   id result, fetch, body;
621   
622   if (_partId == nil) return nil;
623   
624   key   = [@"body[" stringByAppendingString:_partId];
625   key   = [key stringByAppendingString:@"]"];
626   parts = [NSArray arrayWithObjects:&key count:1];
627   
628   /* fetch */
629   
630   result = [self fetchURL:_url parts:parts password:_pwd];
631   
632   /* process results */
633   
634   result = [result objectForKey:@"fetch"];
635   if ([result count] == 0) { /* did not find part */
636     [self errorWithFormat:@"did not find part: %@", _partId];
637     return nil;
638   }
639   
640   fetch = [result objectAtIndex:0];
641   if ((body = [fetch objectForKey:@"body"]) == nil) {
642     [self errorWithFormat:@"did not find body in response: %@", result];
643     return nil;
644   }
645   
646   if ((result = [body objectForKey:@"data"]) == nil) {
647     [self errorWithFormat:@"did not find data in body: %@", fetch];
648     return nil;
649   }
650   return result;
651 }
652
653 - (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f
654   toURL:(NSURL *)_url password:(NSString *)_p
655 {
656   NGImap4Client *client;
657   id result;
658   
659   if (![_url isNotNull]) return nil;
660   if (![_f   isNotNull]) return nil;
661   
662   if (![_f isKindOfClass:[NSArray class]])
663     _f = [NSArray arrayWithObjects:&_f count:1];
664   
665   /* get client */
666
667   if ((client = [self imap4ClientForURL:_url password:_p]) == nil)
668     // TODO: return 401?
669     return nil;
670   
671   /* select folder */
672   
673   if (![self selectFolder:[self imap4FolderNameForURL:_url removeFileName:YES]
674              inClient:client]) {
675     return [NSException exceptionWithHTTPStatus:404 /* server error */
676                         reason:@"could not select IMAP4 folder"];
677     return nil;
678   }
679   
680   /* store flags */
681   
682   result = [client storeUid:[[[_url path] lastPathComponent] intValue]
683                    add:[NSNumber numberWithBool:_flag]
684                    flags:_f];
685   if (![[result valueForKey:@"result"] boolValue]) {
686     unsigned int status;
687     NSString *r;
688     
689     r = [result valueForKey:@"reason"];
690     if ([r isEqualToString:@"Permission denied"]) {
691       /* different for each server?, no error codes in IMAP4 ... */
692       status = 403 /* Forbidden */;
693     }
694     else
695       status = 500 /* internal server error */;
696     
697     [self logWithFormat:@"DEBUG: fail result %@", result];
698
699     r = [@"Failed to add flag to IMAP4 message: " stringByAppendingString:r];
700     
701     return [NSException exceptionWithHTTPStatus:status /* server error */
702                         reason:r];
703   }
704   /* result contains 'fetch' key with the current flags */
705   return nil;
706 }
707 - (NSException *)addFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
708   return [self addOrRemove:YES flags:_f toURL:_u password:_p];
709 }
710 - (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
711   return [self addOrRemove:NO flags:_f toURL:_u password:_p];
712 }
713
714 - (NSException *)markURLDeleted:(NSURL *)_url password:(NSString *)_p {
715   return [self addOrRemove:YES flags:@"Deleted" toURL:_url password:_p];
716 }
717
718 - (NSException *)postData:(NSData *)_data flags:(id)_f
719   toFolderURL:(NSURL *)_url password:(NSString *)_p
720 {
721   NGImap4Client *client;
722   id result;
723   
724   if (![_url isNotNull]) return nil;
725   if (![_f   isNotNull]) _f = [NSArray array];
726   
727   if ((client = [self imap4ClientForURL:_url password:_p]) == nil)
728     return nil;
729   
730   if (![_f isKindOfClass:[NSArray class]])
731     _f = [NSArray arrayWithObjects:&_f count:1];
732   
733   result = [client append:_data 
734                    toFolder:[self imap4FolderNameForURL:_url]
735                    withFlags:_f];
736   if (![[result valueForKey:@"result"] boolValue]) {
737     [self logWithFormat:@"DEBUG: fail result %@", result];
738     return [NSException exceptionWithHTTPStatus:500 /* server error */
739                         reason:@"failed to store message to IMAP4 message"];
740   }
741   /* result contains 'fetch' key with the current flags */
742   
743   // TODO: need to flush any caches?
744   return nil;
745 }
746
747 - (NSException *)copyMailURL:(NSURL *)_srcurl toFolderURL:(NSURL *)_desturl
748   password:(NSString *)_pwd
749 {
750   SOGoMailConnectionEntry *entry;
751   NSString *srcname, *destname;
752   unsigned uid;
753   id result;
754   
755   /* check connection cache */
756   
757   if ((entry = [self entryForURL:_srcurl password:_pwd]) == nil) {
758     // TODO: better to use an auth exception?
759     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
760                         reason:@"did not find IMAP4 folder (no entry)"];
761   }
762   
763   /* check whether URLs are on different servers */
764   
765   if ([self entryForURL:_desturl password:_pwd] != entry) {
766     // TODO: find a better error code
767     return [NSException exceptionWithHTTPStatus:502 /* Bad Gateway */
768                         reason:@"source and destination on different servers"];
769   }  
770   
771   /* names */
772   
773   srcname  = [self imap4FolderNameForURL:_srcurl removeFileName:YES];
774   uid      = [[[_srcurl path] lastPathComponent] unsignedIntValue];
775   destname = [self imap4FolderNameForURL:_desturl];
776
777   /* select source folder */
778   
779   if (![self selectFolder:srcname inClient:[entry client]]) {
780     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
781                         reason:@"did not find source folder"];
782   }
783   
784   /* copy */
785   
786   result = [[entry client] copyUid:uid toFolder:destname];
787   if (![[result valueForKey:@"result"] boolValue]) {
788     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
789                         reason:@"copy operation failed"];
790   }
791   
792   // TODO: need to flush some caches?
793   
794   return nil;
795 }
796
797 /* managing folders */
798
799 - (BOOL)isPermissionDeniedResult:(id)_result {
800   if ([[_result valueForKey:@"result"] intValue] != 0)
801     return NO;
802   
803   return [[_result valueForKey:@"reason"] 
804                    isEqualToString:@"Permission denied"];
805 }
806
807 - (BOOL)doesMailboxExistAtURL:(NSURL *)_url password:(NSString *)_pwd {
808   SOGoMailConnectionEntry *entry;
809   NSString        *folderName;
810   id result;
811   
812   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
813     return NO;
814   
815   /* check in hierarchy cache */
816   
817   if ((result = [entry cachedHierarchyResults]) != nil) {
818     result = [result objectForKey:@"list"];
819     return ([result objectForKey:[_url path]] != nil) ? YES : NO;
820   }
821   
822   /* check using IMAP4 select */
823   // TODO: we should probably just fetch the whole hierarchy?
824   
825   folderName = [self imap4FolderNameForURL:_url];
826   result = [[entry client] select:folderName];
827   if (![[result valueForKey:@"result"] boolValue])
828     return NO;
829   
830   return YES;
831 }
832
833 - (id)infoForMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
834   SOGoMailConnectionEntry *entry;
835   SOGoMailboxInfo *info;
836   NSString        *folderName;
837   id result;
838   
839   if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
840     // TODO: better to use an auth exception?
841     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
842                         reason:@"did not find IMAP4 folder (no entry)"];
843   }
844   
845   folderName = [self imap4FolderNameForURL:_url];
846   result     = [[entry client] select:folderName];
847   if (![[result valueForKey:@"result"] boolValue]) {
848     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
849                         reason:@"did not find IMAP4 folder (select failed)"];
850   }
851
852   info = [[SOGoMailboxInfo alloc] initWithURL:_url folderName:folderName
853                                   selectDictionary:result];
854   return [info autorelease];
855 }
856
857 - (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url
858   password:(NSString *)_pwd
859 {
860   SOGoMailConnectionEntry *entry;
861   NSString *newPath;
862   id       result;
863
864   /* check connection cache */
865   
866   if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
867     // TODO: better to use an auth exception?
868     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
869                         reason:@"did not find IMAP4 folder (no entry)"];
870   }
871
872   /* construct path */
873   
874   newPath = [self imap4FolderNameForURL:_url];
875   newPath = [newPath stringByAppendingString:[self imap4Separator]];
876   newPath = [newPath stringByAppendingString:_mailbox];
877   
878   /* create */
879   
880   result = [[entry client] create:newPath];
881   if ([self isPermissionDeniedResult:result]) {
882     return [NSException exceptionWithHTTPStatus:403 /* forbidden */
883                         reason:@"creation of folders not allowed"];
884   }
885   else if ([[result valueForKey:@"result"] intValue] == 0) {
886     return [NSException exceptionWithHTTPStatus:500 /* server error */
887                         reason:[result valueForKey:@"reason"]];
888   }
889   
890   [entry flushFolderHierarchyCache];
891   // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result];
892   return nil;
893 }
894
895 - (NSException *)deleteMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
896   SOGoMailConnectionEntry *entry;
897   NSString *path;
898   id       result;
899
900   /* check connection cache */
901   
902   if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
903     // TODO: better to use an auth exception?
904     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
905                         reason:@"did not find IMAP4 folder (no entry)"];
906   }
907   
908   /* delete */
909   
910   path   = [self imap4FolderNameForURL:_url];
911   result = [[entry client] delete:path];
912   
913   if ([self isPermissionDeniedResult:result]) {
914     return [NSException exceptionWithHTTPStatus:403 /* forbidden */
915                         reason:@"creation of folders not allowed"];
916   }
917   else if ([[result valueForKey:@"result"] intValue] == 0) {
918     return [NSException exceptionWithHTTPStatus:500 /* server error */
919                         reason:[result valueForKey:@"reason"]];
920   }
921   
922   [entry flushFolderHierarchyCache];
923 #if 0
924   [self debugWithFormat:@"delete mailbox %@: %@", _url, result];
925 #endif
926   return nil;
927 }
928
929 - (NSException *)moveMailboxAtURL:(NSURL *)_srcurl toURL:(NSURL *)_desturl
930   password:(NSString *)_pwd
931 {
932   SOGoMailConnectionEntry *entry;
933   NSString *srcname, *destname;
934   id result;
935   
936   /* check connection cache */
937   
938   if ((entry = [self entryForURL:_srcurl password:_pwd]) == nil) {
939     // TODO: better to use an auth exception?
940     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
941                         reason:@"did not find IMAP4 folder (no entry)"];
942   }
943   
944   /* check whether URLs are on different servers */
945   
946   if ([self entryForURL:_desturl password:_pwd] != entry) {
947     // TODO: find a better error code
948     return [NSException exceptionWithHTTPStatus:502 /* Bad Gateway */
949                         reason:@"source and destination on different servers"];
950   }  
951   
952   /* rename */
953   
954   srcname  = [self imap4FolderNameForURL:_srcurl];
955   destname = [self imap4FolderNameForURL:_desturl];
956   
957   result = [[entry client] rename:srcname to:destname];
958   
959   if ([self isPermissionDeniedResult:result]) {
960     return [NSException exceptionWithHTTPStatus:403 /* forbidden */
961                         reason:@"creation of folders not allowed"];
962   }
963   else if ([[result valueForKey:@"result"] intValue] == 0) {
964     return [NSException exceptionWithHTTPStatus:500 /* server error */
965                         reason:[result valueForKey:@"reason"]];
966   }
967   
968   [entry flushFolderHierarchyCache];
969 #if 0
970   [self debugWithFormat:@"renamed mailbox %@: %@", _srcurl, result];
971 #endif
972   return nil;
973 }
974
975 - (NSDictionary *)aclForMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
976   /*
977     Returns a mapping of uid => permission strings, eg:
978       guizmo.g = lrs;
979       root     = lrswipcda;
980   */
981   SOGoMailConnectionEntry *entry;
982   NSString *folderName;
983   id       result;
984   
985   if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
986     // TODO: better to use an auth exception?
987     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
988                         reason:@"did not find IMAP4 folder (no entry)"];
989   }
990   
991   folderName = [self imap4FolderNameForURL:_url];
992   result     = [[entry client] getACL:folderName];
993   if (![[result valueForKey:@"result"] boolValue]) {
994     [self logWithFormat:@"ERROR: getacl failed: %@", result];
995     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
996                         reason:@"did not find ACL for IMAP4 folder"];
997   }
998   
999   return [result valueForKey:@"acl"];
1000 }
1001
1002 - (NSString *)myRightsForMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
1003   SOGoMailConnectionEntry *entry;
1004   NSString *folderName;
1005   id       result;
1006   
1007   if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
1008     // TODO: better to use an auth exception?
1009     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
1010                         reason:@"did not find IMAP4 folder (no entry)"];
1011   }
1012
1013   /* check cache */
1014   
1015   if ((result = [entry cachedMyRightsForURL:_url]) != nil)
1016     return result;
1017
1018   /* run IMAP4 op */
1019   
1020   folderName = [self imap4FolderNameForURL:_url];
1021   result     = [[entry client] myRights:folderName];
1022   if (![[result valueForKey:@"result"] boolValue]) {
1023     [self logWithFormat:@"ERROR: myrights failed: %@", result];
1024     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
1025                         reason:@"did not find myrights for IMAP4 folder"];
1026   }
1027
1028   /* cache results */
1029   
1030   if ((result = [result valueForKey:@"myrights"]) != nil)
1031     [entry cacheMyRights:result forURL:_url];
1032   return result;
1033 }
1034
1035 /* bulk flag adding (eg used for empty/trash) */
1036
1037 - (NSException *)addFlags:(id)_f toAllMessagesInURL:(NSURL *)_url
1038   password:(NSString *)_p
1039 {
1040   NGImap4Client *client;
1041   id       result;
1042   
1043   if (![_url isNotNull]) return nil;
1044   if (![_f   isNotNull]) return nil;
1045
1046   if (![_f isKindOfClass:[NSArray class]])
1047     _f = [NSArray arrayWithObjects:&_f count:1];
1048   
1049   /* get client */
1050   
1051   if ((client = [self imap4ClientForURL:_url password:_p]) == nil)
1052     // TODO: return 401?
1053     return nil;
1054   
1055   /* select folder */
1056   
1057   if (![self selectFolder:[self imap4FolderNameForURL:_url] inClient:client]) {
1058     return [NSException exceptionWithHTTPStatus:404 /* not found */
1059                         reason:@"could not select IMAP4 folder"];
1060   }
1061
1062   /* fetch all sequence numbers */
1063   
1064   result = [client searchWithQualifier:nil /* means: ALL */];
1065   if (![[result valueForKey:@"result"] boolValue]) {
1066     return [NSException exceptionWithHTTPStatus:500 /* server error */
1067                         reason:@"could not search in IMAP4 folder"];
1068   }
1069   
1070   result = [result valueForKey:@"search"];
1071   if ([result count] == 0) /* no messages in there, nothin' to be done */
1072     return nil;
1073   
1074   /* store flags */
1075   
1076   result = [client storeFlags:_f forMSNs:result addOrRemove:YES];
1077   if (![[result valueForKey:@"result"] boolValue]) {
1078     return [NSException exceptionWithHTTPStatus:500 /* server error */
1079                         reason:@"could not change flags in IMAP4 folder"];
1080   }
1081   
1082   return nil;
1083 }
1084
1085 /* debugging */
1086
1087 - (BOOL)isDebuggingEnabled {
1088   return debugOn;
1089 }
1090
1091 @end /* SOGoMailManager */