]> err.no Git - scalable-opengroupware.org/blob - SOGo/SoObjects/Mailer/SOGoMailManager.m
refactoring in mailer
[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   TODO: to implement copy, use "uid copy" instead of "copy" as used by
34         NGImap4Client.
35 */
36
37 @implementation SOGoMailManager
38
39 static BOOL           debugOn    = NO;
40 static BOOL           debugCache = NO;
41 static BOOL           debugKeys  = NO;
42 static BOOL           poolingOff = NO;
43 static NSTimeInterval PoolScanInterval = 5 * 60 /* every five minutes */;
44
45 + (void)initialize {
46   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
47   
48   debugOn      = [ud boolForKey:@"SOGoEnableIMAP4Debug"];
49   debugCache   = [ud boolForKey:@"SOGoEnableIMAP4CacheDebug"];
50   poolingOff   = [ud boolForKey:@"SOGoDisableIMAP4Pooling"];
51   
52   if (debugOn)    NSLog(@"Note: SOGoEnableIMAP4Debug is enabled!");
53   if (poolingOff) NSLog(@"WARNING: IMAP4 connection pooling is disabled!");
54 }
55
56 + (id)defaultMailManager {
57   static SOGoMailManager *manager = nil; // THREAD
58   if (manager == nil) 
59     manager = [[self alloc] init];
60   return manager;
61 }
62
63 - (id)init {
64   if ((self = [super init])) {
65     if (!poolingOff) {
66       self->urlToEntry = [[NSMutableDictionary alloc] initWithCapacity:256];
67     }
68     
69     self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
70                                 PoolScanInterval
71                               target:self selector:@selector(_garbageCollect:)
72                               userInfo:nil repeats:YES] retain];
73   }
74   return self;
75 }
76
77 - (void)dealloc {
78   if (self->gcTimer) [self->gcTimer invalidate];
79   [self->gcTimer release];
80   
81   [self->urlToEntry release];
82   [super dealloc];
83 }
84
85 /* cache */
86
87 - (id)cacheKeyForURL:(NSURL *)_url {
88   // protocol, user, host, port
89   return [NSString stringWithFormat:@"%@://%@@%@:%@",
90                    [_url scheme], [_url user], [_url host], [_url port]];
91 }
92
93 - (SOGoMailConnectionEntry *)entryForURL:(NSURL *)_url {
94   if (_url == nil)
95     return nil;
96   
97   return [self->urlToEntry objectForKey:[self cacheKeyForURL:_url]];
98 }
99 - (void)cacheEntry:(SOGoMailConnectionEntry *)_entry forURL:(NSURL *)_url {
100   if (_entry == nil) _entry = (id)[NSNull null];
101   [self->urlToEntry setObject:_entry forKey:[self cacheKeyForURL:_url]];
102 }
103
104 - (void)_garbageCollect:(NSTimer *)_timer {
105   // TODO: scan for old IMAP4 channels
106   [self debugWithFormat:@"should collect IMAP4 channels (%d active)",
107           [self->urlToEntry count]];
108 }
109
110 - (id)entryForURL:(NSURL *)_url password:(NSString *)_pwd {
111   /*
112     Three cases:
113     a) not yet connected             => create new entry and connect
114     b) connected, correct password   => return cached entry
115     c) connected, different password => try to recreate entry
116   */
117   SOGoMailConnectionEntry *entry;
118   NGImap4Client *client;
119
120   /* check cache */
121   
122   if ((entry = [self entryForURL:_url]) != nil) {
123     if ([entry isValidPassword:_pwd]) {
124       if (debugCache)
125         [self logWithFormat:@"valid password, reusing cache entry ..."];
126       return entry;
127     }
128     
129     /* different password, password could have changed! */
130     if (debugCache)
131       [self logWithFormat:@"different password than cached entry: %@", _url];
132     entry = nil;
133   }
134   else
135     [self debugWithFormat:@"no connection cached yet for url: %@", _url];
136   
137   /* try to login */
138   
139   client = [entry isValidPassword:_pwd]
140     ? [entry client]
141     : [self imap4ClientForURL:_url password:_pwd];
142   
143   if (client == nil)
144     return nil;
145   
146   /* sideeffect of -imap4ClientForURL:password: is to create a cache entry */
147   return [self entryForURL:_url];
148 }
149
150 - (NSException *)errorForMissingEntryAtURL:(NSURL *)_url {
151   // TODO: improve
152   return [NSException exceptionWithHTTPStatus:404 /* Not Found */
153                       reason:@"Did not find mail URL"];
154 }
155
156 /* client object */
157
158 - (NGImap4Client *)imap4ClientForURL:(NSURL *)_url password:(NSString *)_pwd {
159   // TODO: move to some global IMAP4 connection pool manager
160   SOGoMailConnectionEntry *entry;
161   NGImap4Client *client;
162   NSDictionary  *result;
163   
164   if (_url == nil)
165     return nil;
166
167   /* check connection pool */
168   
169   if ((entry = [self entryForURL:_url]) != nil) {
170     if ([entry isValidPassword:_pwd]) {
171       [self debugWithFormat:@"reused IMAP4 connection for URL: %@", _url];
172       return [entry client];
173     }
174     
175     /* different password, password could have changed! */
176     entry = nil;
177   }
178   
179   /* setup connection and attempt login */
180   
181   if ((client = [NGImap4Client clientWithURL:_url]) == nil)
182     return nil;
183   
184   result = [client login:[_url user] password:_pwd];
185   if (![[result valueForKey:@"result"] boolValue]) {
186     [self errorWithFormat:
187             @"IMAP4 login failed:\n"
188             @"  host=%@, user=%@, pwd=%s\n"
189             @"  url=%@\n  base=%@\n  base-class=%@)\n"
190             @"  = %@", 
191             [_url host], [_url user], [_pwd length] > 0 ? "yes" : "no", 
192             [_url absoluteString],
193             [_url baseURL],
194             NSStringFromClass([[_url baseURL] class]),
195             client];
196     return nil;
197   }
198   
199   [self debugWithFormat:@"created new IMAP4 connection for URL: %@", _url];
200   
201   /* cache connection in pool */
202   
203   entry = [[SOGoMailConnectionEntry alloc] initWithClient:client 
204                                            password:_pwd];
205   [self cacheEntry:entry forURL:_url];
206   [entry release]; entry = nil;
207   
208   return client;
209 }
210
211 - (void)flushCachesForURL:(NSURL *)_url {
212   SOGoMailConnectionEntry *entry;
213   
214   if ((entry = [self entryForURL:_url]) == nil) /* nothing cached */
215     return;
216   
217   [entry flushFolderHierarchyCache];
218   [entry flushMailCaches];
219 }
220
221 /* folder hierarchy */
222
223 - (NSArray *)subfoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
224   SOGoMailConnectionEntry *entry;
225
226   if (debugKeys)
227     [self debugWithFormat:@"subfolders for URL: %@ ...",[_url absoluteString]];
228   
229   /* check connection cache */
230   
231   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
232     return nil;
233   
234   return [entry subfoldersForURL:_url];
235 }
236
237 - (NSArray *)allFoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
238   SOGoMailConnectionEntry *entry;
239   
240   if (debugKeys)
241     [self debugWithFormat:@"folders for URL: %@ ...",[_url absoluteString]];
242   
243   /* check connection cache */
244   
245   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
246     return nil;
247   
248   return [entry allFoldersForURL:_url];
249 }
250
251 /* messages */
252
253 - (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
254   sortOrdering:(id)_so password:(NSString *)_pwd
255 {
256   /* 
257      sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
258   */
259   SOGoMailConnectionEntry *entry;
260   
261   /* check connection cache */
262   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
263     return nil;
264   
265   return [entry fetchUIDsInURL:_url qualifier:_qualifier sortOrdering:_so];
266 }
267
268 - (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
269   parts:(NSArray *)_parts password:(NSString *)_pwd
270 {
271   // currently returns a dict?!
272   /*
273     Allowed fetch keys:
274       UID
275       BODY.PEEK[<section>]<<partial>>
276       BODY            [this is the bodystructure, supported]
277       BODYSTRUCTURE   [not supported yet!]
278       ENVELOPE        [this is a parsed header, but does not include type]
279       FLAGS
280       INTERNALDATE
281       RFC822
282       RFC822.HEADER
283       RFC822.SIZE
284       RFC822.TEXT
285   */
286   SOGoMailConnectionEntry *entry;
287   
288   if (_uids == nil)
289     return nil;
290   if ([_uids count] == 0)
291     return nil; // TODO: might break empty folders?! return a dict!
292   
293   /* check connection cache */
294   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
295     return nil;
296   
297   return [entry fetchUIDs:_uids inURL:_url parts:_parts];
298 }
299
300 - (NSException *)expungeAtURL:(NSURL *)_url password:(NSString *)_pwd {
301   SOGoMailConnectionEntry *entry;
302   
303   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
304     return [self errorForMissingEntryAtURL:_url];
305   
306   return [entry expungeAtURL:_url];
307 }
308
309 - (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts password:(NSString *)_pwd{
310   SOGoMailConnectionEntry *entry;
311   
312   if (![_url isNotNull]) return nil;
313   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
314     return [self errorForMissingEntryAtURL:_url];
315   
316   return [entry fetchURL:_url parts:_parts];
317 }
318
319 - (NSData *)fetchContentOfBodyPart:(NSString *)_partId
320   atURL:(NSURL *)_url password:(NSString *)_pwd
321 {
322   SOGoMailConnectionEntry *entry;
323
324   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
325     return nil; // TODO: improve?
326
327   return [entry fetchContentOfBodyPart:_partId atURL:_url];
328 }
329
330 - (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f
331   toURL:(NSURL *)_url password:(NSString *)_p
332 {
333   SOGoMailConnectionEntry *entry;
334
335   if ((entry = [self entryForURL:_url password:_p]) == nil)
336     return [self errorForMissingEntryAtURL:_url];
337
338   return [entry addOrRemove:_flag flags:_f toURL:_url];
339 }
340 - (NSException *)addFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
341   return [self addOrRemove:YES flags:_f toURL:_u password:_p];
342 }
343 - (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
344   return [self addOrRemove:NO flags:_f toURL:_u password:_p];
345 }
346
347 - (NSException *)markURLDeleted:(NSURL *)_url password:(NSString *)_p {
348   return [self addOrRemove:YES flags:@"Deleted" toURL:_url password:_p];
349 }
350
351 - (NSException *)postData:(NSData *)_data flags:(id)_f
352   toFolderURL:(NSURL *)_url password:(NSString *)_p
353 {
354   SOGoMailConnectionEntry *entry;
355
356   if (![_url isNotNull]) return nil;
357   
358   if ((entry = [self entryForURL:_url password:_p]) == nil)
359     return [self errorForMissingEntryAtURL:_url];
360   
361   return [entry postData:_data flags:_f toFolderURL:_url];
362 }
363
364 - (NSException *)copyMailURL:(NSURL *)_srcurl toFolderURL:(NSURL *)_desturl
365   password:(NSString *)_pwd
366 {
367   SOGoMailConnectionEntry *entry;
368   
369   /* check connection cache */
370   
371   if ((entry = [self entryForURL:_srcurl password:_pwd]) == nil)
372     return [self errorForMissingEntryAtURL:_srcurl];
373   
374   /* check whether URLs are on different servers */
375   
376   if ([self entryForURL:_desturl password:_pwd] != entry) {
377     // TODO: find a better error code
378     return [NSException exceptionWithHTTPStatus:502 /* Bad Gateway */
379                         reason:@"source and destination on different servers"];
380   }  
381   
382   return [entry copyMailURL:_srcurl toFolderURL:_desturl];
383 }
384
385 /* managing folders */
386
387 - (BOOL)isPermissionDeniedResult:(id)_result {
388   if ([[_result valueForKey:@"result"] intValue] != 0)
389     return NO;
390   
391   return [[_result valueForKey:@"reason"] 
392                    isEqualToString:@"Permission denied"];
393 }
394
395 - (BOOL)doesMailboxExistAtURL:(NSURL *)_url password:(NSString *)_pwd {
396   SOGoMailConnectionEntry *entry;
397   
398   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
399     return NO;
400   
401   return [entry doesMailboxExistAtURL:_url];
402 }
403
404 - (id)infoForMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
405   SOGoMailConnectionEntry *entry;
406   
407   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
408     return [self errorForMissingEntryAtURL:_url];
409   
410   return [entry infoForMailboxAtURL:_url];
411 }
412
413 - (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url
414   password:(NSString *)_pwd
415 {
416   SOGoMailConnectionEntry *entry;
417   
418   /* check connection cache */
419   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
420     return [self errorForMissingEntryAtURL:_url];
421
422   return [entry createMailbox:_mailbox atURL:_url];
423 }
424
425 - (NSException *)deleteMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
426   SOGoMailConnectionEntry *entry;
427   
428   /* check connection cache */
429   
430   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
431     return [self errorForMissingEntryAtURL:_url];
432   
433   return [entry deleteMailboxAtURL:_url];
434 }
435
436 - (NSException *)moveMailboxAtURL:(NSURL *)_srcurl toURL:(NSURL *)_desturl
437   password:(NSString *)_pwd
438 {
439   SOGoMailConnectionEntry *entry;
440   
441   /* check connection cache */
442   
443   if ((entry = [self entryForURL:_srcurl password:_pwd]) == nil)
444     return [self errorForMissingEntryAtURL:_srcurl];
445   
446   /* check whether URLs are on different servers */
447   
448   if ([self entryForURL:_desturl password:_pwd] != entry) {
449     // TODO: find a better error code
450     return [NSException exceptionWithHTTPStatus:502 /* Bad Gateway */
451                         reason:@"source and destination on different servers"];
452   }  
453   
454   return [entry moveMailboxAtURL:_srcurl toURL:_desturl];
455 }
456
457 - (NSDictionary *)aclForMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
458   /*
459     Returns a mapping of uid => permission strings, eg:
460       guizmo.g = lrs;
461       root     = lrswipcda;
462   */
463   SOGoMailConnectionEntry *entry;
464   
465   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
466     return (id)[self errorForMissingEntryAtURL:_url];
467   
468   return [entry aclForMailboxAtURL:_url];
469 }
470
471 - (NSString *)myRightsForMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
472   SOGoMailConnectionEntry *entry;
473   
474   if ((entry = [self entryForURL:_url password:_pwd]) == nil)
475     return (id)[self errorForMissingEntryAtURL:_url];
476
477   return [entry myRightsForMailboxAtURL:_url];
478 }
479
480 /* bulk flag adding (eg used for empty/trash) */
481
482 - (NSException *)addFlags:(id)_f toAllMessagesInURL:(NSURL *)_url
483   password:(NSString *)_p
484 {
485   SOGoMailConnectionEntry *entry;
486   
487   if (![_url isNotNull]) return nil;
488   if (![_f   isNotNull]) return nil;
489   
490   if ((entry = [self entryForURL:_url password:_p]) == nil)
491     return [self errorForMissingEntryAtURL:_url];
492   
493   return [entry addFlags:_f toAllMessagesInURL:_url];
494 }
495
496 /* debugging */
497
498 - (BOOL)isDebuggingEnabled {
499   return debugOn;
500 }
501
502 @end /* SOGoMailManager */