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