]> err.no Git - scalable-opengroupware.org/blob - SoObjects/SOGo/AgenorUserManager.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1045 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / SoObjects / SOGo / AgenorUserManager.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 "AgenorUserManager.h"
23 #include "AgenorUserDefaults.h"
24 #include <NGExtensions/NGExtensions.h>
25 #include <NGLdap/NGLdap.h>
26 #include <NGCards/NGCards.h>
27 #include "SOGoLRUCache.h"
28
29 #warning we should rely on the LDAP sources instead...
30 #define qualifierFormat @"mailNickname = %@"
31
32 @interface AgenorUserManager (PrivateAPI)
33 - (NGLdapConnection *)ldapConnection;
34
35 - (void)_cacheCN:(NSString *)_cn forUID:(NSString *)_uid;
36 - (NSString *)_cachedCNForUID:(NSString *)_uid;
37 - (void)_cacheServer:(NSString *)_server forUID:(NSString *)_uid;
38 - (NSString *)_cachedServerForUID:(NSString *)_uid;
39 - (void)_cacheEmail:(NSString *)_email forUID:(NSString *)_uid;
40 - (NSString *)_cachedEmailForUID:(NSString *)_uid;
41 - (void)_cacheUID:(NSString *)_uid forEmail:(NSString *)_email;
42 - (NSString *)_cachedUIDForEmail:(NSString *)_email;
43
44 - (BOOL)primaryIsUserAllowedToChangeSOGoInternetAccess:(NSString *)_uid;
45
46 - (BOOL)primaryIsInternetAutoresponderEnabledForUser:(NSString *)_uid;
47 - (BOOL)primaryIsIntranetAutoresponderEnabledForUser:(NSString *)_uid;
48 - (NGLdapAttribute *)primaryGetMailAutoresponderAttribute:(NSString *)_uid;
49 - (BOOL)isAutoresponderEnabledForAttribute:(NGLdapAttribute *)_attr
50   matchingPrefix:(NSString *)_prefix;
51 @end
52
53 // TODO: add a timer to flush LRU caches every some hours
54
55 @implementation AgenorUserManager
56
57 static BOOL     debugOn     = NO;
58 static BOOL     useLDAP     = NO;
59 static NSString *ldapHost   = nil;
60 static NSString *ldapBaseDN = nil;
61 static NSNull   *sharedNull = nil;
62 static NSString *fallbackIMAP4Server          = nil;
63 static NSString *defaultMailDomain            = nil;
64 static NSString *shareLDAPClass               = @"mineqMelBoite";
65 static NSString *shareLoginSeparator          = @".-.";
66 static NSString *mailEmissionAttrName         = @"mineqMelmailEmission";
67 static NSString *changeInternetAccessAttrName = @"mineqOgoAccesInternet";
68 static NSString *mailAutoresponderAttrName    = @"mineqMelReponse"; /* sic! */
69 static NSURL    *AgenorProfileURL             = nil;
70
71 static NSArray *fromEMailAttrs = nil;
72
73 static unsigned PoolScanInterval = 5 * 60 /* every five minutes */;
74
75 + (void) initialize 
76 {
77   static BOOL didInit = NO;
78   NSUserDefaults *ud;
79   NSString *tmp;
80
81   if (didInit) return;
82   didInit = YES;
83   
84   ud      = [NSUserDefaults standardUserDefaults];
85   debugOn = [ud boolForKey:@"SOGoUserManagerDebugEnabled"];
86   
87   useLDAP = [ud boolForKey:@"SOGoUserManagerUsesLDAP"];
88   if (useLDAP) {
89     ldapHost   = [[ud stringForKey:@"SOGoLDAPHost"]   copy];
90     ldapBaseDN = [[ud stringForKey:@"SOGoLDAPBaseDN"] copy];
91     NSLog(@"Note: using LDAP host to manage accounts: %@", ldapHost);
92   }
93   else
94     NSLog(@"Note: LDAP access is disabled.");
95   
96   fallbackIMAP4Server = [[ud stringForKey:@"SOGoFallbackIMAP4Server"] copy];
97   if ([fallbackIMAP4Server length] > 0)
98     NSLog(@"Note: using fallback IMAP4 server: '%@'", fallbackIMAP4Server);
99   else
100     {
101       if (fallbackIMAP4Server)
102         [fallbackIMAP4Server release];
103       fallbackIMAP4Server = nil;
104     }
105
106   defaultMailDomain = [[ud stringForKey:@"SOGoDefaultMailDomain"] copy];
107   if ([defaultMailDomain length] == 0)
108     {
109       if (defaultMailDomain)
110         [defaultMailDomain release];
111       defaultMailDomain = nil;
112       NSLog(@"WARNING: no default mail domain (please specify"
113             " 'SOGoDefaultMailDomain' in the user default db)");
114     }
115
116   fromEMailAttrs = 
117     [[NSArray alloc] initWithObjects:mailEmissionAttrName, nil];
118
119   sharedNull = [[NSNull null] retain];
120
121   /* profile database URL */
122   
123   if ((tmp = [ud stringForKey:@"AgenorProfileURL"]) == nil)
124     NSLog(@"ERROR: no 'AgenorProfileURL' database URL configured!");
125   else if ((AgenorProfileURL = [[NSURL alloc] initWithString:tmp]) == nil)
126     NSLog(@"ERROR: could not parse AgenorProfileURL: '%@'", tmp);
127   else
128     NSLog(@"Note: using profile at: %@", [AgenorProfileURL absoluteString]);
129
130   if ((tmp = [ud stringForKey: @"SOGoDefaultMailDomain"]))
131     {
132       defaultMailDomain = [tmp copy];
133     }
134   
135   PoolScanInterval = [[ud objectForKey:@"AgenorCacheCheckInterval"] intValue];
136   if (PoolScanInterval == 0) 
137     PoolScanInterval = 60 * 60 /* every hour */;
138   NSLog(@"AgenorUserManager: flushing caches every %d minutes "
139         @"(AgenorCacheCheckInterval)",
140         PoolScanInterval / 60);
141 }
142
143 + (id)sharedUserManager {
144   static AgenorUserManager *mgr = nil;
145   if (mgr == nil)
146     mgr = [[self alloc] init];
147   return mgr;
148 }
149
150 - (id)init {
151   self = [super init];
152   if(self) {
153     self->serverCache     = 
154       [[NSMutableDictionary alloc] initWithCapacity:10000];
155     self->cnCache         = 
156       [[NSMutableDictionary alloc] initWithCapacity:10000];
157     self->uidCache        = 
158       [[NSMutableDictionary alloc] initWithCapacity:10000];
159     self->emailCache      = 
160       [[NSMutableDictionary alloc] initWithCapacity:10000];
161     self->shareStoreCache = 
162       [[NSMutableDictionary alloc] initWithCapacity:10000];
163     self->shareEMailCache = 
164       [[NSMutableDictionary alloc] initWithCapacity:10000];
165     self->changeInternetAccessCache =
166       [[NSMutableDictionary alloc] initWithCapacity:10000];
167     self->internetAutoresponderFlagCache =
168       [[NSMutableDictionary alloc] initWithCapacity:10000];
169     self->intranetAutoresponderFlagCache =
170       [[NSMutableDictionary alloc] initWithCapacity:10000];
171     
172     self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
173                                 PoolScanInterval
174                               target:self selector:@selector(_garbageCollect:)
175                               userInfo:nil repeats:YES] retain];
176   }
177   return self;
178 }
179
180 - (void)dealloc {
181   if (self->gcTimer) [self->gcTimer invalidate];
182   [self->serverCache     release];
183   [self->cnCache         release];
184   [self->uidCache        release];
185   [self->emailCache      release];
186   [self->shareStoreCache release];
187   [self->shareEMailCache release];
188   [self->changeInternetAccessCache      release];
189   [self->internetAutoresponderFlagCache release];
190   [self->intranetAutoresponderFlagCache release];
191   [self->gcTimer release];
192   [super dealloc];
193 }
194
195 /* cache */
196
197 - (void)flush {
198   [self->cnCache         removeAllObjects];
199   [self->serverCache     removeAllObjects];
200   [self->uidCache        removeAllObjects];
201   [self->emailCache      removeAllObjects];
202   [self->shareStoreCache removeAllObjects];
203   [self->shareEMailCache removeAllObjects];
204   
205   [self->changeInternetAccessCache      removeAllObjects];
206   [self->internetAutoresponderFlagCache removeAllObjects];
207   [self->intranetAutoresponderFlagCache removeAllObjects];
208 }
209
210 - (void)_garbageCollect:(NSTimer *)_timer {
211   [self debugWithFormat:@"flushing caches."];
212   [self flush];
213 }
214
215 /* LDAP */
216
217 - (NGLdapConnection *)ldapConnection {
218   static NGLdapConnection *ldapConnection = nil;
219   if(!ldapConnection) {
220     ldapConnection = [[NGLdapConnection alloc] initWithHostName:ldapHost];
221 #if 0
222     [ldapConnection setUseCache:YES];
223 #endif
224   }
225   return ldapConnection;
226 }
227
228
229 /* private cache helpers */
230 // TODO: this is really unnecessary, no?
231
232 - (void)_cacheCN:(NSString *)_cn forUID:(NSString *)_uid {
233   if (_cn == nil) return;
234   [self->cnCache setObject:_cn forKey:_uid];
235 }
236 - (NSString *)_cachedCNForUID:(NSString *)_uid {
237   return [self->cnCache objectForKey:_uid];
238 }
239
240 - (void)_cacheServer:(NSString *)_server forUID:(NSString *)_uid {
241   if (_server == nil) return;
242   [self->serverCache setObject:_server forKey:_uid];
243 }
244 - (NSString *)_cachedServerForUID:(NSString *)_uid {
245   return [self->serverCache objectForKey:_uid];
246 }
247
248 - (void)_cacheEmail:(NSString *)_email forUID:(NSString *)_uid {
249   if (_email == nil) return;
250   [self->emailCache setObject:_email forKey:_uid];
251 }
252 - (NSString *)_cachedEmailForUID:(NSString *)_uid {
253   return [self->emailCache objectForKey:_uid];
254 }
255
256 - (void)_cacheUID:(NSString *)_uid forEmail:(NSString *)_email {
257   if (_uid == nil) return;
258   [self->uidCache setObject:_uid forKey:_email];
259 }
260 - (NSString *)_cachedUIDForEmail:(NSString *)_email {
261   return [self->uidCache objectForKey:_email];
262 }
263
264
265 /* uid <-> email mapping */
266
267 /*
268  UPDATE: the email excerpt below has been marked by Maxime as being
269          wrong. This algorithm can not be expected to work, thus
270          the mapping has been replaced with an LDAP query.
271
272  --- snip ---
273  The uid field is in bijection this the email adress :
274  this field can be construct from the email. Email are uniques.
275  
276  So, we can use email adresse from identifier. 
277  The field is made like this :
278  _ if the email is equipement.gouv.fr then the login
279  is the part before the @
280  for example : fisrtName.lastName
281  _ if the email is not equipement.gouv.fr then the login
282  is the full email adress where @ is change to . (dot)
283  for example : fisrtName.lastName.subDomain.domain.tld
284  --- snap ---
285
286  NOTE: mapping email -> uid is easy, but can also generate uid's not known
287        to the system (i.e. for private addressbook entries, obvious).
288        The reverse mapping can work _only_ if "firstName.lastname." is
289        guaranteed, because the second dot would be mapped to '@'. This
290        is probably error prone.
291        Only LDAP fetches would guarantee correctness in both cases.
292 */
293
294 - (NSString *)primaryGetAgenorUIDForEmail:(NSString *)_email {
295   static NSArray   *uidAttrs = nil;
296   NGLdapConnection *conn;
297   EOQualifier      *q;
298   NSEnumerator     *resultEnum;
299   NGLdapEntry      *entry;
300   NGLdapAttribute  *uidAttr;
301   NSString         *uid;
302
303   if (uidAttrs == nil)
304     uidAttrs = [[NSArray alloc] initWithObjects:@"uid", nil];
305     
306   q = [EOQualifier qualifierWithQualifierFormat:@"mail = %@", _email];
307     
308   conn       = [self ldapConnection];
309   resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
310                      qualifier:q
311                      attributes:uidAttrs];
312   entry = [resultEnum nextObject];
313   if (entry == nil) {
314     if(debugOn) {
315       [self logWithFormat:@"%s Didn't find LDAP entry for email '%@'!",
316             __PRETTY_FUNCTION__,
317             _email];
318     }
319     return nil;
320   }
321   uidAttr = [entry attributeWithName:@"uid"];
322   if (!uidAttr)
323     return nil; /* can happen, not unlikely */
324   uid = [uidAttr stringValueAtIndex:0];
325   return uid;
326 }
327
328 - (NSString *) getUIDForEmail: (NSString *)_email
329 {
330   NSString *uid;
331   NSRange r;
332   NSString *domain;
333
334   uid = nil;
335   if ([_email length] > 0)
336     {
337       uid = [self _cachedUIDForEmail:_email];
338       if (!uid)
339         {
340           if (useLDAP)
341             uid = [self primaryGetAgenorUIDForEmail:_email];
342
343           if (!uid)
344             {    
345               r = [_email rangeOfString:@"@"];
346               if (r.length == 0)
347                 return nil;
348     
349               domain = [_email substringFromIndex:NSMaxRange(r)];
350               if (![domain isEqualToString:defaultMailDomain])
351                 uid = _email;
352               else
353                 uid = [_email substringToIndex:r.location];
354             }
355           if (uid)
356             [self _cacheUID:uid forEmail:_email];
357         }
358     }
359   
360   return uid;
361 }
362
363 #warning big ugly hack. LDAP lookup should be fixed
364 - (NSString *) getUIDForICalPerson: (iCalPerson *) _person
365 {
366   NSString *domainString, *email, *uid;
367
368   domainString = [NSString stringWithFormat: @"@%@", defaultMailDomain];
369   email = [_person rfc822Email];
370   if ([email hasSuffix: domainString])
371     uid = [_person cn];
372   else
373     uid = [self getUIDForEmail: email];
374
375   return uid;
376 }
377
378   /* may insert NSNulls into returned array */
379 - (NSArray  *)getUIDsForICalPersons:(NSArray *)_persons
380   applyStrictMapping:(BOOL)_mapStrictly
381 {
382   NSMutableArray *ma;
383   unsigned       i, count;
384
385   count = [_persons count];
386   ma    = [[[NSMutableArray alloc] initWithCapacity:count] autorelease];
387   for (i = 0; i < count; i++) {
388     iCalPerson *p;
389     id         uid;
390     
391     p   = [_persons objectAtIndex:i];
392     uid = [self getUIDForICalPerson:p];
393     if (uid != nil)
394       [ma addObject:uid];
395     else if (uid == nil && _mapStrictly)
396       [ma addObject:sharedNull];
397   }
398   return ma;
399 }
400
401 - (NSString *)primaryGetEmailForAgenorUID:(NSString *)_uid {
402   NGLdapConnection *conn;
403   EOQualifier      *q;
404   NSEnumerator     *resultEnum;
405   NGLdapEntry      *entry;
406   NGLdapAttribute  *emailAttr;
407   NSString         *email;
408   unsigned         count;
409     
410   q = [EOQualifier qualifierWithQualifierFormat:qualifierFormat, _uid];
411     
412   conn       = [self ldapConnection];
413   resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
414                      qualifier:q
415                      attributes:fromEMailAttrs];
416   entry = [resultEnum nextObject];
417   if (entry == nil) {
418       if(debugOn) {
419         [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
420                               __PRETTY_FUNCTION__,
421                               _uid];
422       }
423       return nil;
424   }
425   emailAttr = [entry attributeWithName:mailEmissionAttrName];
426   if (emailAttr == nil)
427     return nil; /* shit happens */
428   
429   email = nil;
430   count = [emailAttr count];
431 #if 0 // TODO: explain why this is commented out!
432   if (count > 1) {
433         unsigned i;
434
435         /* in case there are multiple email addresses, select the first
436            which doesn't have '@equipement.gouv.fr' in it */
437         for (i = 0; i < count; i++) {
438           NSString *candidate;
439           
440           candidate = [emailAttr stringValueAtIndex:i];
441           if (![candidate hasSuffix:defaultMailDomain]) {
442             // TODO: also check for '@'
443             email = candidate;
444             break;
445           }
446         }
447   }
448 #endif
449   if (email == nil && count > 0)
450     email = [emailAttr stringValueAtIndex:0];
451   
452   return email;
453 }
454
455 - (NSString *)getEmailForUID:(NSString *)_uid
456 {
457   NSString *email;
458   NSRange r;
459   
460   email = nil;
461
462   if ([_uid length] > 0)
463     {
464       email = [self _cachedEmailForUID: _uid];
465       if (!email)
466         {
467           if (useLDAP)
468             email = [self primaryGetEmailForAgenorUID:_uid];
469
470           if (!email)
471             {
472               r = [_uid rangeOfString:@"@"];
473               email = ((r.length > 0)
474                        ? _uid
475                        : [[_uid stringByAppendingString:@"@"]
476                            stringByAppendingString: defaultMailDomain]);
477             }
478
479           if (email)
480             [self _cacheEmail: email forUID: _uid];
481         }
482     }
483
484   return email;
485 }
486
487
488 /* CN */
489
490 - (NSString *)primaryGetCNForAgenorUID:(NSString *)_uid {
491   static NSArray   *cnAttrs = nil;
492   NGLdapConnection *conn;
493   EOQualifier      *q;
494   NSEnumerator     *resultEnum;
495   NGLdapEntry      *entry;
496   NGLdapAttribute  *cnAttr;
497   NSString         *cn;
498
499   if (cnAttrs == nil)
500     cnAttrs = [[NSArray alloc] initWithObjects:@"cn", nil];
501   
502   q = [EOQualifier qualifierWithQualifierFormat:qualifierFormat, _uid];
503   
504   conn = [self ldapConnection];
505   resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
506                      qualifier:q
507                      attributes:cnAttrs];
508   entry = [resultEnum nextObject];
509   if (entry == nil) {
510       if(debugOn) {
511         [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
512                               __PRETTY_FUNCTION__,
513                               _uid];
514       }
515       return nil;
516   }
517   cnAttr = [entry attributeWithName:@"cn"];
518   if(cnAttr == nil && debugOn) {
519       [self logWithFormat:@"%s LDAP entry for uid '%@' has no common name?",
520                             __PRETTY_FUNCTION__,
521                             _uid];
522       return nil; /* nothing we can do about it */
523   }
524   cn = [cnAttr stringValueAtIndex:0];
525   return cn;
526 }
527
528 - (NSString *)getCNForUID:(NSString *)_uid {
529   NSString *cn;
530
531   if ((cn = [self _cachedCNForUID:_uid]) != nil)
532     return cn;
533   
534   if (useLDAP) {
535     cn = [self primaryGetCNForAgenorUID:_uid];
536   }
537   else {
538     NSString *s;
539     NSRange  r;
540     
541     s = _uid;
542     if ([s length] < 10)
543       return s;
544     
545     // TODO: algorithm might be inappropriate, depends on the actual UID
546     r = [s rangeOfString:@"."];
547     if (r.length == 0)
548       cn = s;
549     else
550       cn = [s substringToIndex:r.location];
551   }
552   
553   [self _cacheCN:cn forUID:_uid];
554   return cn;
555 }
556
557
558 /* Servers, IMAP */
559
560 - (NSString *)getIMAPAccountStringForUID:(NSString *)_uid {
561   NSString *server;
562   
563   server = [self getServerForUID:_uid];
564   if (server == nil)
565     return nil;
566   return [NSString stringWithFormat:@"%@@%@", _uid, server];
567 }
568
569 - (NSArray *)mailServerDiscoveryAttributes {
570   static NSArray *attrs = nil;
571
572   if (attrs == nil) {
573     attrs = [[NSArray alloc] initWithObjects:
574                                @"uid", /* required for shares */
575                                @"mineqMelRoutage", 
576                                @"mineqMelServeurPrincipal",
577                                @"mineqMelPartages",
578                                mailEmissionAttrName,
579                              nil];
580   }
581   return attrs;
582 }
583
584 - (NGLdapEntry *)_fetchEntryForAgenorUID:(NSString *)_uid {
585   // TODO: badly named, this fetches the mail server discovery attributes
586   /* called by -primaryGetServerForAgenorUID: */
587   NGLdapConnection *conn;
588   EOQualifier      *q;
589   NSEnumerator     *resultEnum;
590   NGLdapEntry      *entry;
591   
592   q = [EOQualifier qualifierWithQualifierFormat:qualifierFormat, _uid];
593   
594   conn = [self ldapConnection];
595   resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
596                      qualifier:q
597                      attributes:[self mailServerDiscoveryAttributes]];
598   /* we just expect one entry, thus drop the rest */
599   entry = [resultEnum nextObject];
600   if (entry == nil) {
601       if(debugOn) {
602         NSLog(@"%s Didn't find LDAP entry for uid '%@'!",
603               __PRETTY_FUNCTION__,
604               _uid);
605       }
606       return nil;
607   }
608   return entry;
609 }
610
611 - (NSArray *)_serverCandidatesForMineqMelRoutage:(NGLdapAttribute *)attr {
612   /*
613     eg:
614       "Baluh.Hommes.Tests-Montee-En-Charge-Ogo%equipement.gouv.fr@\
615        amelie-01.ac.melanie2.i2"
616   */
617   NSMutableArray *serverCandidates;
618   unsigned i, count;
619
620   count            = [attr count];
621   serverCandidates = [NSMutableArray arrayWithCapacity:count];
622   for (i = 0; i < count; i++) {
623     NSRange  r;
624     NSString *route;
625     unsigned length;
626     unsigned start;
627     NSRange  serverNameRange;
628     NSString *serverName;
629     
630     route = [attr stringValueAtIndex:i];
631
632     /* check for melanie suffix and ignore other entries */
633     
634     r = [route rangeOfString:@".melanie2.i2" options:NSBackwardsSearch];
635     if (r.length == 0) {
636 #if 0
637       [self logWithFormat:@"found no melanie in route: '%@'", route];
638 #endif
639       continue;
640     }
641
642     /* check for @ inside the string, searching backwards (ignoring suffix) */
643     
644     // be clever: TODO: in what way is this clever?
645     length = [route length];
646     r = NSMakeRange(0, length - r.length); /* cut of suffix (.melanie2.i2) */
647     r = [route rangeOfString:@"@" options:NSBackwardsSearch range:r];
648     if (r.length == 0) {
649 #if 0
650       [self logWithFormat:@"found no @ in route: '%@'", route];
651 #endif
652       continue;
653     }
654     
655     /* check for percent sign */
656     
657     start = NSMaxRange(r); /* start behind the @ */
658     
659     /* this range covers everything after @: 'amelie-01.ac.melanie2.i2' */
660     serverNameRange = NSMakeRange(start, length - start);
661     
662     /* and this range covers everything to the @ */
663     r = NSMakeRange(0, start - 1);
664     r = [route rangeOfString:@"%" options:NSBackwardsSearch range:r];
665     if (r.length == 0) {
666 #if 0
667       [self logWithFormat:@"found no %% in route: '%@' / '%@'", 
668             route, [route substringWithRange:NSMakeRange(0, length - start)]];
669 #endif
670       continue;
671     }
672     
673     serverName = [route substringWithRange:serverNameRange];
674     [serverCandidates addObject:serverName];
675   }
676   return serverCandidates;
677 }
678
679 - (NSString *)serverFromEntry:(NGLdapEntry *)_entry {
680   NSString        *server;
681   NGLdapAttribute *attr;
682
683   server = nil;
684   
685   attr = [_entry attributeWithName:@"mineqMelRoutage"];
686   if (attr != nil) {
687     NSArray *serverCandidates;
688     
689     serverCandidates = [self _serverCandidatesForMineqMelRoutage:attr];
690     if ([serverCandidates count] > 0)
691       server = [serverCandidates objectAtIndex:0];
692     
693     if ([serverCandidates count] > 1) {
694       [self logWithFormat:
695               @"WARNING: more than one value for 'mineqMelRoutage': %@",
696               serverCandidates];
697     }
698   }
699   else {
700     [self debugWithFormat:
701             @"%s LDAP entry '%@' has no mineqMelRoutage entry?",
702             __PRETTY_FUNCTION__, [_entry dn]];
703   }
704   
705   /* last resort */
706   if (server == nil) {
707     attr = [_entry attributeWithName:@"mineqMelServeurPrincipal"];
708     if ([attr count] > 0)
709       server = [attr stringValueAtIndex:0];
710   }
711   
712   return server;
713 }
714
715 - (NSString *)primaryGetServerForAgenorUID:(NSString *)_uid {
716   /*
717    First of all : for a particular user IMAP and SMTP are served on the same
718    host.
719    
720    The name of the machine is determined by applying a regex on every values of
721    the mineqMelRoutage LDAP attribute.
722    The regex is :   .*%.*@(.*\.melanie2\.i2$)
723    It extracts the substring that follows '@', ends with 'melanie2', on
724    adresses which have a '%' before the '@'
725    
726    Example: helge.hesse%opengroupware.org@servername1.melanie2.i2
727    -> servername1.melanie2.i2
728    
729    If only one server name is found by applying the regex on every value of the
730    attribute, then this name is the IMAP/SMTP server for that user.
731    Note that this is the case when we got a unique (well formed) value for the
732    attribute.
733    If the regex finds more than one servername when applied to the differents
734    values, then the IMAP/SMTP server name is to be found in the
735    mineqMelServeurPrincipal attribute of the user.
736   */
737   NSString    *server;
738   NGLdapEntry *entry;
739   
740   if ((entry = [self _fetchEntryForAgenorUID:_uid]) == nil)
741     return nil;
742   
743   if ((server = [self serverFromEntry:entry]) != nil)
744     return server;
745   
746   [self debugWithFormat:
747           @"%s no chance of getting at server info for user '%@', "
748           @"tried everything. Sorry.",
749           __PRETTY_FUNCTION__, _uid];
750   return nil;
751 }
752
753 - (NSString *)getServerForUID:(NSString *)_uid {
754   NSString *server;
755
756   if (_uid == nil || [_uid length] == 0)
757     return nil;
758   
759   if ((server = [self _cachedServerForUID:_uid]) != nil)
760     return server;
761   
762   if (useLDAP)
763     server = [self primaryGetServerForAgenorUID:_uid];
764   else if (fallbackIMAP4Server != nil)
765     server = fallbackIMAP4Server;
766   else {
767     [self logWithFormat:@"ERROR: could not get server for uid '%@', "
768           @"neither LDAP (SOGoUserManagerUsesLDAP) nor "
769           @"a fallback (SOGoFallbackIMAP4Server) is configured.",
770           _uid];
771     server = nil;
772   }
773   
774   [self _cacheServer:server forUID:_uid];
775   return server;
776 }
777
778 /* shared mailboxes */
779
780 - (NSArray *)getSharedMailboxAccountStringsForUID:(NSString *)_uid {
781   NSArray *k;
782   
783   k = [[self getSharedMailboxesAndEMailsForUID:_uid] allKeys];
784   
785   /* ensure that ordering is always the same */
786   return [k sortedArrayUsingSelector:@selector(compare:)];
787 }
788
789 - (NSString *)emissionEMailFromEntry:(NGLdapEntry *)_entry {
790   id emissionAttr;
791   
792   emissionAttr = [_entry attributeWithName:mailEmissionAttrName];
793   if ([emissionAttr count] == 0) {
794     [self logWithFormat:@"WARNING: share has no %@ attr: %@",
795             mailEmissionAttrName, [_entry dn]];
796     return nil;
797   }
798     
799   if ([emissionAttr count] > 1) {
800     [self logWithFormat:
801             @"WARNING: share has more than one value in %@ attr: %@",
802             mailEmissionAttrName, [_entry dn]];
803     return nil;
804   }
805   
806   return [emissionAttr stringValueAtIndex:0];
807 }
808
809 - (NSArray *)getSharedMailboxEMailsForUID:(NSString *)_uid {
810   NSMutableArray   *shares = nil;
811   NGLdapConnection *conn;
812   EOQualifier      *q;
813   NSString         *gPattern, *cPattern;
814   NSEnumerator     *resultEnum;
815   NGLdapEntry      *entry;
816   
817   if ([_uid length] == 0)
818     return nil;
819   
820   if (!useLDAP) {
821     [self logWithFormat:
822             @"Note: LDAP access is disabled, returning no shared froms."];
823     return nil;
824   }
825   
826   /* check cache */
827   if ((shares = [self->shareEMailCache objectForKey:_uid]) != nil)
828     return shares;
829   
830   /* G and C mean "emission access" */
831   gPattern = [_uid stringByAppendingString:@":G"];
832   cPattern = [_uid stringByAppendingString:@":C"];
833   
834   q = [EOQualifier qualifierWithQualifierFormat:
835                      @"((mineqMelPartages = %@) OR (mineqMelPartages = %@)) "
836                      @"AND (objectclass = %@)",
837                    gPattern, cPattern, shareLDAPClass];
838   
839   conn = [self ldapConnection];
840   
841   resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
842                      qualifier:q
843                      attributes:fromEMailAttrs];
844   
845   while ((entry = [resultEnum nextObject]) != nil) {
846     NSString *emissionAttr;
847
848     if ((emissionAttr = [self emissionEMailFromEntry:entry]) == nil)
849       continue;
850     
851     if (shares == nil) shares = [NSMutableArray arrayWithCapacity:4];
852     [shares addObject:emissionAttr];
853   }
854   
855   /* ensure that ordering is always the same */
856   [shares sortUsingSelector:@selector(compare:)];
857   
858   /* cache */
859   shares = (shares == nil) ? [NSArray array] : [[shares copy] autorelease];
860   [self->shareEMailCache setObject:shares forKey:_uid];
861   return shares;
862 }
863
864 /* identities */
865
866 - (BOOL)hasUser:(NSString *)_uid partageAccess:(char *)_rightset
867   inEntry:(NGLdapEntry *)_entry
868 {
869   NGLdapAttribute *attr;
870   unsigned i, count;
871   
872   attr = [_entry attributeWithName:@"mineqMelPartages"];
873   if ((count = [attr count]) == 0) {
874     [self logWithFormat:@"WARNING: share has no 'mineqMelPartages' attr: %@",
875             [_entry dn]];
876     return NO;
877   }
878   
879   for (i = 0; i < count; i++) {
880     NSString *p;
881     NSRange  r;
882     unichar  c;
883     register unsigned j;
884     
885     p = [attr stringValueAtIndex:i];
886     r = [p rangeOfString:@":"];
887     if (r.length == 0) {
888       [self errorWithFormat:@"Invalid mineqMelPartages value: '%@'", p];
889       continue;
890     }
891     
892     /* check whether prefix matches, eg: "helian.h:G" */
893     if (r.location != [_uid length]) /* check length */
894       continue;
895     if (![p hasPrefix:_uid])
896       continue;
897     
898     c = [p characterAtIndex:(r.location + r.length)];
899     
900     /* check whether permissions match */
901     for (j = 0; _rightset[j] != '\0'; j++) {
902       if (c == _rightset[j])
903         return YES;
904     }
905   }
906   return NO;
907 }
908
909 - (NSDictionary *)getSharedMailboxesAndEMailsForUID:(NSString *)_uid {
910   /*
911     Sample:
912       "(&(mineqMelPartages=guizmo.g:*)(objectclass=mineqMelBoite))"
913       "guizmo.g" is the uid of the user
914     
915     Login:
916       guizmo.g.-.baluh.hommes.tests-montee-en-charge-ogo
917       (uid + ".-." + share-uid)
918     
919     Note: shared mailboxes can be on different hosts!
920     
921     This returns a dictionary where the keys are the IMAP4 connect strings
922     while the values are the emitter addresses for the box or NSNull if the
923     uid is not allowed to emit for this box.
924   */
925   NSMutableDictionary *shares = nil;
926   NGLdapConnection *conn;
927   EOQualifier      *q;
928   NSString         *sharePattern;
929   NSEnumerator     *resultEnum;
930   NGLdapEntry      *entry;
931   
932   if ([_uid length] == 0)
933     return nil;
934   
935   if (!useLDAP) {
936     [self logWithFormat:
937             @"Note: LDAP access is disabled, returning no shared accounts."];
938     return nil;
939   }
940
941   /* check cache */
942   if ((shares = [self->shareStoreCache objectForKey:_uid]) != nil)
943     return shares;
944   
945   sharePattern = [_uid stringByAppendingString:@":*"];
946   
947   q = [EOQualifier qualifierWithQualifierFormat:
948                      @"(mineqMelPartages = %@) AND (objectclass = %@)",
949                      sharePattern, shareLDAPClass];
950   
951   conn = [self ldapConnection];
952
953   resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
954                      qualifier:q
955                      attributes:[self mailServerDiscoveryAttributes]];
956   
957   while ((entry = [resultEnum nextObject]) != nil) {
958     NSString *server, *shareLogin;
959     id shareUid, emitterAddress;
960
961     /* calculate server connect string */
962     
963     if ([(server = [self serverFromEntry:entry]) length] == 0) {
964       [self errorWithFormat:@"found no mail server host for share: %@",
965               [entry dn]];
966       continue;
967     }
968     
969     shareUid = [entry attributeWithName:@"uid"];
970     if ([shareUid count] < 1) {
971       [self errorWithFormat:@"found no 'uid' for share: %@", [entry dn]];
972       continue;
973     }
974     shareUid = [shareUid stringValueAtIndex:0];
975     
976     shareLogin = [_uid stringByAppendingString:shareLoginSeparator];
977     shareLogin = [shareLogin stringByAppendingString:shareUid];
978     
979     if (shares == nil)
980       shares = [NSMutableDictionary dictionaryWithCapacity:4];
981     
982     shareLogin = [shareLogin stringByAppendingString:@"@"];
983     shareLogin = [shareLogin stringByAppendingString:server];
984     
985     /* calculate emitter address (check for proper access right) */
986     
987     if ([self hasUser:_uid partageAccess:"GC" inEntry:entry])
988       emitterAddress = [self emissionEMailFromEntry:entry];
989     else
990       emitterAddress = [NSNull null];
991     
992     /* set value */
993     
994     [shares setObject: emitterAddress
995             forKey: shareLogin];
996   }
997   
998   /* cache */
999   shares = (shares == nil) 
1000     ? [NSDictionary dictionary] 
1001     : [[shares copy] autorelease];
1002   [self->shareStoreCache setObject:shares forKey:_uid];
1003   return shares;
1004 }
1005
1006 /* free busy */
1007
1008 - (NSURL *)getFreeBusyURLForUID:(NSString *)_uid {
1009   [self logWithFormat:@"TODO(%s): implement", __PRETTY_FUNCTION__];
1010   return nil;
1011 }
1012
1013 /* defaults */
1014
1015 - (NSUserDefaults *) _getUserDefaultsForUID: (NSString *) uid
1016                                   fieldName: (NSString *) fieldName
1017 {
1018   id defaults;
1019   
1020   if (AgenorProfileURL)
1021     {
1022       /* Note: do not cache, otherwise updates can be quite tricky */
1023       defaults = [[AgenorUserDefaults alloc] initWithTableURL: AgenorProfileURL
1024                                              uid: uid fieldName: fieldName];
1025       [defaults autorelease];
1026     }
1027   else
1028     {
1029       [self warnWithFormat:
1030               @"no profile configured, cannot retrieve defaults for user: '%@'",
1031             uid];
1032       return defaults = nil;
1033     }
1034
1035   return defaults;
1036 }
1037
1038 - (NSUserDefaults *) getUserDefaultsForUID: (NSString *) uid
1039 {
1040   return [self _getUserDefaultsForUID: uid fieldName: @"defaults"];
1041 }
1042
1043 - (NSUserDefaults *) getUserSettingsForUID: (NSString *) uid
1044 {
1045   return [self _getUserDefaultsForUID: uid fieldName: @"settings"];
1046 }
1047
1048 /* internet access lock */
1049
1050 - (BOOL)isUserAllowedToChangeSOGoInternetAccess:(NSString *)_uid {
1051   NSNumber *bv;
1052   
1053   bv = [self->changeInternetAccessCache objectForKey:_uid];
1054   if (!bv) {
1055     BOOL value;
1056     
1057     value = [self primaryIsUserAllowedToChangeSOGoInternetAccess:_uid];
1058     bv    = [NSNumber numberWithBool:value];
1059     [self->changeInternetAccessCache setObject:bv forKey:_uid];
1060   }
1061   return [bv boolValue];
1062 }
1063
1064 - (BOOL)primaryIsUserAllowedToChangeSOGoInternetAccess:(NSString *)_uid {
1065   static NSArray   *attrs = nil;
1066   NGLdapConnection *conn;
1067   EOQualifier      *q;
1068   NSEnumerator     *resultEnum;
1069   NGLdapEntry      *entry;
1070   NGLdapAttribute  *attr;
1071   NSString         *value;
1072   
1073   if (attrs == nil)
1074     attrs = [[NSArray alloc] initWithObjects:changeInternetAccessAttrName, nil];
1075   
1076   q = [EOQualifier qualifierWithQualifierFormat:qualifierFormat, _uid];
1077   
1078   conn       = [self ldapConnection];
1079   resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
1080                      qualifier:q
1081                      attributes:attrs];
1082   entry = [resultEnum nextObject];
1083   if (entry == nil) {
1084     if(debugOn) {
1085       [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
1086         __PRETTY_FUNCTION__,
1087         _uid];
1088     }
1089     return NO;
1090   }
1091   attr = [entry attributeWithName:changeInternetAccessAttrName];
1092   if(attr == nil && debugOn) {
1093     [self logWithFormat:@"%s LDAP entry for uid '%@' "
1094                         @"has no mineqOgoAccesInternet attribute?",
1095                         __PRETTY_FUNCTION__,
1096                         _uid];
1097     return NO; /* nothing we can do about it */
1098   }
1099   value = [attr stringValueAtIndex:0];
1100   return [value boolValue];
1101 }
1102
1103 - (BOOL)isInternetAutoresponderEnabledForUser:(NSString *)_uid {
1104   NSNumber *bv;
1105
1106   bv = [self->internetAutoresponderFlagCache objectForKey:_uid];
1107   if (!bv) {
1108     BOOL value;
1109     
1110     value = [self primaryIsInternetAutoresponderEnabledForUser:_uid];
1111     bv    = [NSNumber numberWithBool:value];
1112     [self->internetAutoresponderFlagCache setObject:bv forKey:_uid];
1113   }
1114   return [bv boolValue];
1115 }
1116
1117 - (BOOL)primaryIsInternetAutoresponderEnabledForUser:(NSString *)_uid {
1118   NGLdapAttribute *attr;
1119   
1120   attr = [self primaryGetMailAutoresponderAttribute:_uid];
1121   if (!attr) return NO;
1122   return [self isAutoresponderEnabledForAttribute:attr matchingPrefix:@"60~"];
1123 }
1124
1125 - (BOOL)isIntranetAutoresponderEnabledForUser:(NSString *)_uid {
1126   NSNumber *bv;
1127   
1128   bv = [self->intranetAutoresponderFlagCache objectForKey:_uid];
1129   if (!bv) {
1130     BOOL value;
1131     
1132     value = [self primaryIsIntranetAutoresponderEnabledForUser:_uid];
1133     bv    = [NSNumber numberWithBool:value];
1134     [self->intranetAutoresponderFlagCache setObject:bv forKey:_uid];
1135   }
1136   return [bv boolValue];
1137 }
1138
1139 - (BOOL)primaryIsIntranetAutoresponderEnabledForUser:(NSString *)_uid {
1140   NGLdapAttribute *attr;
1141   
1142   attr = [self primaryGetMailAutoresponderAttribute:_uid];
1143   if (!attr) return NO;
1144   return [self isAutoresponderEnabledForAttribute:attr matchingPrefix:@"50~"];
1145 }
1146
1147 - (BOOL)isAutoresponderEnabledForAttribute:(NGLdapAttribute *)_attr
1148   matchingPrefix:(NSString *)_prefix
1149 {
1150   unsigned i, count;
1151   
1152   count = [_attr count];
1153   for (i = 0; i < count; i++) {
1154     NSString *value;
1155     
1156     value = [_attr stringValueAtIndex:i];
1157     if ([value hasPrefix:_prefix]) {
1158       if ([value rangeOfString:@"DFIN:0"].length > 0)
1159         return NO;
1160       return YES;
1161     }
1162   }
1163   return NO;
1164 }
1165
1166 - (NGLdapAttribute *)primaryGetMailAutoresponderAttribute:(NSString *)_uid {
1167   static NSArray   *attrs = nil;
1168   NGLdapConnection *conn;
1169   EOQualifier      *q;
1170   NSEnumerator     *resultEnum;
1171   NGLdapEntry      *entry;
1172   NGLdapAttribute  *attr;
1173   
1174   if (attrs == nil)
1175     attrs = [[NSArray alloc] initWithObjects:mailAutoresponderAttrName, nil];
1176
1177   q = [EOQualifier qualifierWithQualifierFormat:qualifierFormat, _uid];
1178   
1179   conn       = [self ldapConnection];
1180   resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
1181                      qualifier:q
1182                      attributes:attrs];
1183   entry = [resultEnum nextObject];
1184   if (entry == nil) {
1185     if(debugOn) {
1186       [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
1187         __PRETTY_FUNCTION__,
1188         _uid];
1189     }
1190     return nil;
1191   }
1192   attr = [entry attributeWithName:mailAutoresponderAttrName];
1193   return attr;
1194 }
1195
1196 - (iCalPerson *) iCalPersonWithUid: (NSString *) uid
1197 {
1198   iCalPerson *person;
1199
1200   person = [iCalPerson new];
1201   [person autorelease];
1202   [person setCn: [self getCNForUID: uid]];
1203   [person setEmail: [self getEmailForUID: uid]];
1204
1205   return person;
1206 }
1207
1208 /* debugging */
1209
1210 - (BOOL)isDebuggingEnabled {
1211   return debugOn;
1212 }
1213
1214 @end /* AgenorUserManager */