2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
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
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.
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
22 #include "AgenorUserManager.h"
23 #include <NGExtensions/NGExtensions.h>
24 #include <NGLdap/NGLdap.h>
25 #include "SOGoLRUCache.h"
27 @interface AgenorUserManager (PrivateAPI)
28 - (NGLdapConnection *)ldapConnection;
30 - (void)_cacheCN:(NSString *)_cn forUID:(NSString *)_uid;
31 - (NSString *)_cachedCNForUID:(NSString *)_uid;
32 - (void)_cacheServer:(NSString *)_server forUID:(NSString *)_uid;
33 - (NSString *)_cachedServerForUID:(NSString *)_uid;
34 - (void)_cacheEmail:(NSString *)_email forUID:(NSString *)_uid;
35 - (NSString *)_cachedEmailForUID:(NSString *)_uid;
36 - (void)_cacheUID:(NSString *)_uid forEmail:(NSString *)_email;
37 - (NSString *)_cachedUIDForEmail:(NSString *)_email;
41 // TODO: add a timer to flush LRU caches every some hours
43 @implementation AgenorUserManager
45 static BOOL debugOn = NO;
46 static BOOL useLDAP = NO;
47 static NSString *ldapHost = nil;
48 static NSString *ldapBaseDN = nil;
49 static NSString *fallbackIMAP4Server = nil;
50 static NSString *defaultMailDomain = @"equipement.gouv.fr";
51 static NSString *shareLDAPClass = @"mineqMelBoite";
52 static NSString *shareLoginSeparator = @".-.";
53 static NSString *mailEmissionAttrName = @"mineqMelmailEmission";
55 static NSArray *fromEMailAttrs = nil;
58 static BOOL didInit = NO;
64 ud = [NSUserDefaults standardUserDefaults];
65 debugOn = [ud boolForKey:@"SOGoUserManagerDebugEnabled"];
67 useLDAP = [ud boolForKey:@"SOGoUserManagerUsesLDAP"];
69 ldapHost = [[ud stringForKey:@"SOGoLDAPHost"] copy];
70 ldapBaseDN = [[ud stringForKey:@"SOGoLDAPBaseDN"] copy];
71 NSLog(@"Note: using LDAP host to manage accounts: %@", ldapHost);
74 NSLog(@"Note: LDAP access is disabled.");
76 fallbackIMAP4Server = [[ud stringForKey:@"SOGoFallbackIMAP4Server"] copy];
77 if ([fallbackIMAP4Server length] > 0)
78 NSLog(@"Note: using fallback IMAP4 server: '%@'", fallbackIMAP4Server);
80 fallbackIMAP4Server = nil;
83 [[NSArray alloc] initWithObjects:mailEmissionAttrName, nil];
86 + (id)sharedUserManager {
87 static AgenorUserManager *mgr = nil;
89 mgr = [[self alloc] init];
96 self->serverCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
97 self->cnCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
98 self->uidCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
99 self->emailCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
100 self->shareStoreCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
101 self->shareEMailCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
107 [self->shareStoreCache release];
108 [self->shareEMailCache release];
109 [self->serverCache release];
110 [self->cnCache release];
111 [self->uidCache release];
112 [self->emailCache release];
116 - (NGLdapConnection *)ldapConnection {
117 static NGLdapConnection *ldapConnection = nil;
118 if(!ldapConnection) {
119 ldapConnection = [[NGLdapConnection alloc] initWithHostName:ldapHost];
121 [ldapConnection setUseCache:YES];
124 return ldapConnection;
128 /* private cache helpers */
129 // TODO: this is really unnecessary, no?
131 - (void)_cacheCN:(NSString *)_cn forUID:(NSString *)_uid {
132 if (_cn == nil) return;
133 [self->cnCache addObject:_cn forKey:_uid];
135 - (NSString *)_cachedCNForUID:(NSString *)_uid {
136 return [self->cnCache objectForKey:_uid];
139 - (void)_cacheServer:(NSString *)_server forUID:(NSString *)_uid {
140 if (_server == nil) return;
141 [self->serverCache addObject:_server forKey:_uid];
143 - (NSString *)_cachedServerForUID:(NSString *)_uid {
144 return [self->serverCache objectForKey:_uid];
147 - (void)_cacheEmail:(NSString *)_email forUID:(NSString *)_uid {
148 if (_email == nil) return;
149 [self->emailCache addObject:_email forKey:_uid];
151 - (NSString *)_cachedEmailForUID:(NSString *)_uid {
152 return [self->emailCache objectForKey:_uid];
155 - (void)_cacheUID:(NSString *)_uid forEmail:(NSString *)_email {
156 if (_uid == nil) return;
157 [self->uidCache addObject:_uid forKey:_email];
159 - (NSString *)_cachedUIDForEmail:(NSString *)_email {
160 return [self->uidCache objectForKey:_email];
164 /* uid <-> email mapping */
167 UPDATE: the email excerpt below has been marked by Maxime as being
168 wrong. This algorithm can not be expected to work, thus
169 the mapping has been replaced with an LDAP query.
172 The uid field is in bijection this the email adress :
173 this field can be construct from the email. Email are uniques.
175 So, we can use email adresse from identifier.
176 The field is made like this :
177 _ if the email is equipement.gouv.fr then the login
178 is the part before the @
179 for example : fisrtName.lastName
180 _ if the email is not equipement.gouv.fr then the login
181 is the full email adress where @ is change to . (dot)
182 for example : fisrtName.lastName.subDomain.domain.tld
185 NOTE: mapping email -> uid is easy, but can also generate uid's not known
186 to the system (i.e. for private addressbook entries, obvious).
187 The reverse mapping can work _only_ if "firstName.lastname." is
188 guaranteed, because the second dot would be mapped to '@'. This
189 is probably error prone.
190 Only LDAP fetches would guarantee correctness in both cases.
193 - (NSString *)primaryGetAgenorUIDForEmail:(NSString *)_email {
194 static NSArray *uidAttrs = nil;
195 NGLdapConnection *conn;
197 NSEnumerator *resultEnum;
199 NGLdapAttribute *uidAttr;
203 uidAttrs = [[NSArray alloc] initWithObjects:@"uid", nil];
205 q = [EOQualifier qualifierWithQualifierFormat:@"mail = %@", _email];
207 conn = [self ldapConnection];
208 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
210 attributes:uidAttrs];
211 entry = [resultEnum nextObject];
214 [self logWithFormat:@"%s Didn't find LDAP entry for email '%@'!",
220 uidAttr = [entry attributeWithName:@"uid"];
222 return nil; /* can happen, not unlikely */
223 uid = [uidAttr stringValueAtIndex:0];
227 - (NSString *)getUIDForEmail:(NSString *)_email {
230 if ((uid = [self _cachedUIDForEmail:_email]) != nil)
234 uid = [self primaryGetAgenorUIDForEmail:_email];
240 if(!_email || [_email length] == 0)
243 r = [_email rangeOfString:@"@"];
247 domain = [_email substringFromIndex:NSMaxRange(r)];
248 if (![domain isEqualToString:defaultMailDomain])
251 uid = [_email substringToIndex:r.location];
254 [self _cacheUID:uid forEmail:_email];
258 - (NSString *)primaryGetEmailForAgenorUID:(NSString *)_uid {
259 NGLdapConnection *conn;
261 NSEnumerator *resultEnum;
263 NGLdapAttribute *emailAttr;
267 q = [EOQualifier qualifierWithQualifierFormat:@"uid = %@", _uid];
269 conn = [self ldapConnection];
270 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
272 attributes:fromEMailAttrs];
273 entry = [resultEnum nextObject];
276 [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
282 emailAttr = [entry attributeWithName:mailEmissionAttrName];
283 if (emailAttr == nil)
284 return nil; /* shit happens */
287 count = [emailAttr count];
288 #if 0 // TODO: explain why this is commented out!
292 /* in case there are multiple email addresses, select the first
293 which doesn't have '@equipement.gouv.fr' in it */
294 for (i = 0; i < count; i++) {
297 candidate = [emailAttr stringValueAtIndex:i];
298 if (![candidate hasSuffix:defaultMailDomain]) {
299 // TODO: also check for '@'
306 if (email == nil && count > 0)
307 email = [emailAttr stringValueAtIndex:0];
312 - (NSString *)getEmailForUID:(NSString *)_uid {
315 if (![_uid isNotNull] || [_uid length] == 0)
317 if ((email = [self _cachedEmailForUID:_uid]) != nil)
321 email = [self primaryGetEmailForAgenorUID:_uid];
326 r = [_uid rangeOfString:@"@"];
327 email = (r.length > 0)
329 : [[_uid stringByAppendingString:@"@"]
330 stringByAppendingString:defaultMailDomain];
333 [self _cacheEmail:email forUID:_uid];
340 - (NSString *)primaryGetCNForAgenorUID:(NSString *)_uid {
341 static NSArray *cnAttrs = nil;
342 NGLdapConnection *conn;
344 NSEnumerator *resultEnum;
346 NGLdapAttribute *cnAttr;
350 cnAttrs = [[NSArray alloc] initWithObjects:@"cn", nil];
352 q = [EOQualifier qualifierWithQualifierFormat:@"uid = %@", _uid];
354 conn = [self ldapConnection];
355 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
358 entry = [resultEnum nextObject];
361 [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
367 cnAttr = [entry attributeWithName:@"cn"];
368 if(cnAttr == nil && debugOn) {
369 [self logWithFormat:@"%s LDAP entry for uid '%@' has no common name?",
372 return nil; /* nothing we can do about it */
374 cn = [cnAttr stringValueAtIndex:0];
378 - (NSString *)getCNForUID:(NSString *)_uid {
381 if ((cn = [self _cachedCNForUID:_uid]) != nil)
385 cn = [self primaryGetCNForAgenorUID:_uid];
395 // TODO: algorithm might be inappropriate, depends on the actual UID
396 r = [s rangeOfString:@"."];
400 cn = [s substringToIndex:r.location];
403 [self _cacheCN:cn forUID:_uid];
410 - (NSString *)getIMAPAccountStringForUID:(NSString *)_uid {
413 server = [self getServerForUID:_uid];
416 return [NSString stringWithFormat:@"%@@%@", _uid, server];
419 - (NSArray *)mailServerDiscoveryAttributes {
420 static NSArray *attrs = nil;
423 attrs = [[NSArray alloc] initWithObjects:
424 @"uid", /* required for shares */
426 @"mineqMelServeurPrincipal",
432 - (NGLdapEntry *)_fetchEntryForAgenorUID:(NSString *)_uid {
433 // TODO: badly named, this fetches the mail server discovery attributes
434 /* called by -primaryGetServerForAgenorUID: */
435 NGLdapConnection *conn;
437 NSEnumerator *resultEnum;
440 q = [EOQualifier qualifierWithQualifierFormat:@"uid = %@", _uid];
442 conn = [self ldapConnection];
443 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
445 attributes:[self mailServerDiscoveryAttributes]];
446 /* we just expect one entry, thus drop the rest */
447 entry = [resultEnum nextObject];
450 NSLog(@"%s Didn't find LDAP entry for uid '%@'!",
459 - (NSArray *)_serverCandidatesForMineqMelRoutage:(NGLdapAttribute *)attr {
462 "Baluh.Hommes.Tests-Montee-En-Charge-Ogo%equipement.gouv.fr@\
463 amelie-01.ac.melanie2.i2"
465 NSMutableArray *serverCandidates;
468 count = [attr count];
469 serverCandidates = [NSMutableArray arrayWithCapacity:count];
470 for (i = 0; i < count; i++) {
475 NSRange serverNameRange;
476 NSString *serverName;
478 route = [attr stringValueAtIndex:i];
480 /* check for melanie suffix and ignore other entries */
482 r = [route rangeOfString:@".melanie2.i2" options:NSBackwardsSearch];
485 [self logWithFormat:@"found no melanie in route: '%@'", route];
490 /* check for @ inside the string, searching backwards (ignoring suffix) */
492 // be clever: TODO: in what way is this clever?
493 length = [route length];
494 r = NSMakeRange(0, length - r.length); /* cut of suffix (.melanie2.i2) */
495 r = [route rangeOfString:@"@" options:NSBackwardsSearch range:r];
498 [self logWithFormat:@"found no @ in route: '%@'", route];
503 /* check for percent sign */
505 start = NSMaxRange(r); /* start behind the @ */
507 /* this range covers everything after @: 'amelie-01.ac.melanie2.i2' */
508 serverNameRange = NSMakeRange(start, length - start);
510 /* and this range covers everything to the @ */
511 r = NSMakeRange(0, start - 1);
512 r = [route rangeOfString:@"%" options:NSBackwardsSearch range:r];
515 [self logWithFormat:@"found no %% in route: '%@' / '%@'",
516 route, [route substringWithRange:NSMakeRange(0, length - start)]];
521 serverName = [route substringWithRange:serverNameRange];
522 [serverCandidates addObject:serverName];
524 return serverCandidates;
527 - (NSString *)serverFromEntry:(NGLdapEntry *)_entry {
529 NGLdapAttribute *attr;
533 attr = [_entry attributeWithName:@"mineqMelRoutage"];
535 NSArray *serverCandidates;
537 serverCandidates = [self _serverCandidatesForMineqMelRoutage:attr];
538 if ([serverCandidates count] > 0)
539 server = [serverCandidates objectAtIndex:0];
541 if ([serverCandidates count] > 1) {
543 @"WARNING: more than one value for 'mineqMelRoutage': %@",
548 [self debugWithFormat:
549 @"%s LDAP entry '%@' has no mineqMelRoutage entry?",
550 __PRETTY_FUNCTION__, [_entry dn]];
555 attr = [_entry attributeWithName:@"mineqMelServeurPrincipal"];
556 if ([attr count] > 0)
557 server = [attr stringValueAtIndex:0];
563 - (NSString *)primaryGetServerForAgenorUID:(NSString *)_uid {
565 First of all : for a particular user IMAP and SMTP are served on the same
568 The name of the machine is determined by applying a regex on every values of
569 the mineqMelRoutage LDAP attribute.
570 The regex is : .*%.*@(.*\.melanie2\.i2$)
571 It extracts the substring that follows '@', ends with 'melanie2', on
572 adresses which have a '%' before the '@'
574 Example: helge.hesse%opengroupware.org@servername1.melanie2.i2
575 -> servername1.melanie2.i2
577 If only one server name is found by applying the regex on every value of the
578 attribute, then this name is the IMAP/SMTP server for that user.
579 Note that this is the case when we got a unique (well formed) value for the
581 If the regex finds more than one servername when applied to the differents
582 values, then the IMAP/SMTP server name is to be found in the
583 mineqMelServeurPrincipal attribute of the user.
588 if ((entry = [self _fetchEntryForAgenorUID:_uid]) == nil)
591 if ((server = [self serverFromEntry:entry]) != nil)
594 [self debugWithFormat:
595 @"%s no chance of getting at server info for user '%@', "
596 @"tried everything. Sorry.",
597 __PRETTY_FUNCTION__, _uid];
601 - (NSString *)getServerForUID:(NSString *)_uid {
604 if (_uid == nil || [_uid length] == 0)
607 if ((server = [self _cachedServerForUID:_uid]) != nil)
611 server = [self primaryGetServerForAgenorUID:_uid];
612 else if (fallbackIMAP4Server != nil)
613 server = fallbackIMAP4Server;
615 [self logWithFormat:@"ERROR: could not get server for uid '%@', "
616 @"neither LDAP (SOGoUserManagerUsesLDAP) nor "
617 @"a fallback (SOGoFallbackIMAP4Server) is configured.",
622 [self _cacheServer:server forUID:_uid];
626 /* shared mailboxes */
628 - (NSArray *)getSharedMailboxAccountStringsForUID:(NSString *)_uid {
631 "(&(mineqMelPartages=guizmo.g:*)(objectclass=mineqMelBoite))"
632 "guizmo.g" is the uid of the user
635 guizmo.g.-.baluh.hommes.tests-montee-en-charge-ogo
636 (uid + ".-." + share-uid)
638 Note: shared mailboxes can be on different hosts!
640 NSMutableArray *shares = nil;
641 NGLdapConnection *conn;
643 NSString *sharePattern;
644 NSEnumerator *resultEnum;
647 if ([_uid length] == 0)
652 @"Note: LDAP access is disabled, returning no shared accounts."];
657 if ((shares = [self->shareStoreCache objectForKey:_uid]) != nil)
660 sharePattern = [_uid stringByAppendingString:@":*"];
662 q = [EOQualifier qualifierWithQualifierFormat:
663 @"(mineqMelPartages = %@) AND (objectclass = %@)",
664 sharePattern, shareLDAPClass];
666 conn = [self ldapConnection];
668 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
670 attributes:[self mailServerDiscoveryAttributes]];
672 while ((entry = [resultEnum nextObject]) != nil) {
673 NSString *server, *shareLogin;
676 if ([(server = [self serverFromEntry:entry]) length] == 0) {
677 [self errorWithFormat:@"found no mail server host for share: %@",
682 shareUid = [entry attributeWithName:@"uid"];
683 if ([shareUid count] < 1) {
684 [self errorWithFormat:@"found no 'uid' for share: %@", [entry dn]];
687 shareUid = [shareUid stringValueAtIndex:0];
689 shareLogin = [_uid stringByAppendingString:shareLoginSeparator];
690 shareLogin = [shareLogin stringByAppendingString:shareUid];
693 shares = [NSMutableArray arrayWithCapacity:4];
695 shareLogin = [shareLogin stringByAppendingString:@"@"];
696 shareLogin = [shareLogin stringByAppendingString:server];
697 [shares addObject:shareLogin];
700 /* ensure that ordering is always the same */
701 [shares sortUsingSelector:@selector(compare:)];
704 shares = (shares == nil) ? [NSArray array] : [[shares copy] autorelease];
705 [self->shareStoreCache addObject:shares forKey:_uid];
709 - (NSArray *)getSharedMailboxEMailsForUID:(NSString *)_uid {
710 NSMutableArray *shares = nil;
711 NGLdapConnection *conn;
713 NSString *gPattern, *cPattern;
714 NSEnumerator *resultEnum;
717 if ([_uid length] == 0)
722 @"Note: LDAP access is disabled, returning no shared froms."];
727 if ((shares = [self->shareEMailCache objectForKey:_uid]) != nil)
730 /* G and C mean "emission access" */
731 gPattern = [_uid stringByAppendingString:@":G"];
732 cPattern = [_uid stringByAppendingString:@":C"];
734 q = [EOQualifier qualifierWithQualifierFormat:
735 @"((mineqMelPartages = %@) OR (mineqMelPartages = %@)) "
736 @"AND (objectclass = %@)",
737 gPattern, cPattern, shareLDAPClass];
739 conn = [self ldapConnection];
741 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
743 attributes:fromEMailAttrs];
745 while ((entry = [resultEnum nextObject]) != nil) {
748 emissionAttr = [entry attributeWithName:mailEmissionAttrName];
749 if ([emissionAttr count] == 0) {
750 [self logWithFormat:@"WARNING: share has no %@ attr: %@",
751 mailEmissionAttrName, [entry dn]];
755 if ([emissionAttr count] > 1) {
757 @"WARNING: share has more than one value in %@ attr: %@",
758 mailEmissionAttrName, [entry dn]];
762 emissionAttr = [emissionAttr stringValueAtIndex:0];
763 if (shares == nil) shares = [NSMutableArray arrayWithCapacity:4];
764 [shares addObject:emissionAttr];
767 /* ensure that ordering is always the same */
768 [shares sortUsingSelector:@selector(compare:)];
771 shares = (shares == nil) ? [NSArray array] : [[shares copy] autorelease];
772 [self->shareEMailCache addObject:shares forKey:_uid];
778 - (NSURL *)getFreeBusyURLForUID:(NSString *)_uid {
779 [self logWithFormat:@"TODO(%s): implement", __PRETTY_FUNCTION__];
785 - (NSUserDefaults *)getUserDefaultsForUID:(NSString *)_uid {
786 [self logWithFormat:@"TODO: implement!"];
792 - (BOOL)isDebuggingEnabled {
798 - (NSString *)description {
801 ms = [NSMutableString stringWithCapacity:16];
802 [ms appendFormat:@"<0x%08X[%@]>", self, NSStringFromClass([self class])];
806 @end /* AgenorUserManager */