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 "AgenorUserDefaults.h"
24 #include <NGExtensions/NGExtensions.h>
25 #include <NGLdap/NGLdap.h>
26 #include "SOGoLRUCache.h"
28 @interface AgenorUserManager (PrivateAPI)
29 - (NGLdapConnection *)ldapConnection;
31 - (void)_cacheCN:(NSString *)_cn forUID:(NSString *)_uid;
32 - (NSString *)_cachedCNForUID:(NSString *)_uid;
33 - (void)_cacheServer:(NSString *)_server forUID:(NSString *)_uid;
34 - (NSString *)_cachedServerForUID:(NSString *)_uid;
35 - (void)_cacheEmail:(NSString *)_email forUID:(NSString *)_uid;
36 - (NSString *)_cachedEmailForUID:(NSString *)_uid;
37 - (void)_cacheUID:(NSString *)_uid forEmail:(NSString *)_email;
38 - (NSString *)_cachedUIDForEmail:(NSString *)_email;
42 // TODO: add a timer to flush LRU caches every some hours
44 @implementation AgenorUserManager
46 static BOOL debugOn = NO;
47 static BOOL useLDAP = NO;
48 static NSString *ldapHost = nil;
49 static NSString *ldapBaseDN = nil;
50 static NSString *fallbackIMAP4Server = nil;
51 static NSString *defaultMailDomain = @"equipement.gouv.fr";
52 static NSString *shareLDAPClass = @"mineqMelBoite";
53 static NSString *shareLoginSeparator = @".-.";
54 static NSString *mailEmissionAttrName = @"mineqMelmailEmission";
55 static NSURL *AgenorProfileURL = nil;
57 static NSArray *fromEMailAttrs = nil;
60 static BOOL didInit = NO;
67 ud = [NSUserDefaults standardUserDefaults];
68 debugOn = [ud boolForKey:@"SOGoUserManagerDebugEnabled"];
70 useLDAP = [ud boolForKey:@"SOGoUserManagerUsesLDAP"];
72 ldapHost = [[ud stringForKey:@"SOGoLDAPHost"] copy];
73 ldapBaseDN = [[ud stringForKey:@"SOGoLDAPBaseDN"] copy];
74 NSLog(@"Note: using LDAP host to manage accounts: %@", ldapHost);
77 NSLog(@"Note: LDAP access is disabled.");
79 fallbackIMAP4Server = [[ud stringForKey:@"SOGoFallbackIMAP4Server"] copy];
80 if ([fallbackIMAP4Server length] > 0)
81 NSLog(@"Note: using fallback IMAP4 server: '%@'", fallbackIMAP4Server);
83 fallbackIMAP4Server = nil;
86 [[NSArray alloc] initWithObjects:mailEmissionAttrName, nil];
88 /* profile database URL */
90 if ((tmp = [ud stringForKey:@"AgenorProfileURL"]) == nil)
91 NSLog(@"ERROR: no 'AgenorProfileURL' database URL configured!");
92 else if ((AgenorProfileURL = [[NSURL alloc] initWithString:tmp]) == nil)
93 NSLog(@"ERROR: could not parse AgenorProfileURL: '%@'", tmp);
95 NSLog(@"Note: using profile at: %@", [AgenorProfileURL absoluteString]);
98 + (id)sharedUserManager {
99 static AgenorUserManager *mgr = nil;
101 mgr = [[self alloc] init];
108 self->serverCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
109 self->cnCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
110 self->uidCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
111 self->emailCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
112 self->shareStoreCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
113 self->shareEMailCache = [[SOGoLRUCache alloc] initWithCacheSize:10000];
119 [self->shareStoreCache release];
120 [self->shareEMailCache release];
121 [self->serverCache release];
122 [self->cnCache release];
123 [self->uidCache release];
124 [self->emailCache release];
128 - (NGLdapConnection *)ldapConnection {
129 static NGLdapConnection *ldapConnection = nil;
130 if(!ldapConnection) {
131 ldapConnection = [[NGLdapConnection alloc] initWithHostName:ldapHost];
133 [ldapConnection setUseCache:YES];
136 return ldapConnection;
140 /* private cache helpers */
141 // TODO: this is really unnecessary, no?
143 - (void)_cacheCN:(NSString *)_cn forUID:(NSString *)_uid {
144 if (_cn == nil) return;
145 [self->cnCache addObject:_cn forKey:_uid];
147 - (NSString *)_cachedCNForUID:(NSString *)_uid {
148 return [self->cnCache objectForKey:_uid];
151 - (void)_cacheServer:(NSString *)_server forUID:(NSString *)_uid {
152 if (_server == nil) return;
153 [self->serverCache addObject:_server forKey:_uid];
155 - (NSString *)_cachedServerForUID:(NSString *)_uid {
156 return [self->serverCache objectForKey:_uid];
159 - (void)_cacheEmail:(NSString *)_email forUID:(NSString *)_uid {
160 if (_email == nil) return;
161 [self->emailCache addObject:_email forKey:_uid];
163 - (NSString *)_cachedEmailForUID:(NSString *)_uid {
164 return [self->emailCache objectForKey:_uid];
167 - (void)_cacheUID:(NSString *)_uid forEmail:(NSString *)_email {
168 if (_uid == nil) return;
169 [self->uidCache addObject:_uid forKey:_email];
171 - (NSString *)_cachedUIDForEmail:(NSString *)_email {
172 return [self->uidCache objectForKey:_email];
176 /* uid <-> email mapping */
179 UPDATE: the email excerpt below has been marked by Maxime as being
180 wrong. This algorithm can not be expected to work, thus
181 the mapping has been replaced with an LDAP query.
184 The uid field is in bijection this the email adress :
185 this field can be construct from the email. Email are uniques.
187 So, we can use email adresse from identifier.
188 The field is made like this :
189 _ if the email is equipement.gouv.fr then the login
190 is the part before the @
191 for example : fisrtName.lastName
192 _ if the email is not equipement.gouv.fr then the login
193 is the full email adress where @ is change to . (dot)
194 for example : fisrtName.lastName.subDomain.domain.tld
197 NOTE: mapping email -> uid is easy, but can also generate uid's not known
198 to the system (i.e. for private addressbook entries, obvious).
199 The reverse mapping can work _only_ if "firstName.lastname." is
200 guaranteed, because the second dot would be mapped to '@'. This
201 is probably error prone.
202 Only LDAP fetches would guarantee correctness in both cases.
205 - (NSString *)primaryGetAgenorUIDForEmail:(NSString *)_email {
206 static NSArray *uidAttrs = nil;
207 NGLdapConnection *conn;
209 NSEnumerator *resultEnum;
211 NGLdapAttribute *uidAttr;
215 uidAttrs = [[NSArray alloc] initWithObjects:@"uid", nil];
217 q = [EOQualifier qualifierWithQualifierFormat:@"mail = %@", _email];
219 conn = [self ldapConnection];
220 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
222 attributes:uidAttrs];
223 entry = [resultEnum nextObject];
226 [self logWithFormat:@"%s Didn't find LDAP entry for email '%@'!",
232 uidAttr = [entry attributeWithName:@"uid"];
234 return nil; /* can happen, not unlikely */
235 uid = [uidAttr stringValueAtIndex:0];
239 - (NSString *)getUIDForEmail:(NSString *)_email {
242 if ((uid = [self _cachedUIDForEmail:_email]) != nil)
246 uid = [self primaryGetAgenorUIDForEmail:_email];
252 if(!_email || [_email length] == 0)
255 r = [_email rangeOfString:@"@"];
259 domain = [_email substringFromIndex:NSMaxRange(r)];
260 if (![domain isEqualToString:defaultMailDomain])
263 uid = [_email substringToIndex:r.location];
266 [self _cacheUID:uid forEmail:_email];
270 - (NSString *)primaryGetEmailForAgenorUID:(NSString *)_uid {
271 NGLdapConnection *conn;
273 NSEnumerator *resultEnum;
275 NGLdapAttribute *emailAttr;
279 q = [EOQualifier qualifierWithQualifierFormat:@"uid = %@", _uid];
281 conn = [self ldapConnection];
282 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
284 attributes:fromEMailAttrs];
285 entry = [resultEnum nextObject];
288 [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
294 emailAttr = [entry attributeWithName:mailEmissionAttrName];
295 if (emailAttr == nil)
296 return nil; /* shit happens */
299 count = [emailAttr count];
300 #if 0 // TODO: explain why this is commented out!
304 /* in case there are multiple email addresses, select the first
305 which doesn't have '@equipement.gouv.fr' in it */
306 for (i = 0; i < count; i++) {
309 candidate = [emailAttr stringValueAtIndex:i];
310 if (![candidate hasSuffix:defaultMailDomain]) {
311 // TODO: also check for '@'
318 if (email == nil && count > 0)
319 email = [emailAttr stringValueAtIndex:0];
324 - (NSString *)getEmailForUID:(NSString *)_uid {
327 if (![_uid isNotNull] || [_uid length] == 0)
329 if ((email = [self _cachedEmailForUID:_uid]) != nil)
333 email = [self primaryGetEmailForAgenorUID:_uid];
338 r = [_uid rangeOfString:@"@"];
339 email = (r.length > 0)
341 : [[_uid stringByAppendingString:@"@"]
342 stringByAppendingString:defaultMailDomain];
345 [self _cacheEmail:email forUID:_uid];
352 - (NSString *)primaryGetCNForAgenorUID:(NSString *)_uid {
353 static NSArray *cnAttrs = nil;
354 NGLdapConnection *conn;
356 NSEnumerator *resultEnum;
358 NGLdapAttribute *cnAttr;
362 cnAttrs = [[NSArray alloc] initWithObjects:@"cn", nil];
364 q = [EOQualifier qualifierWithQualifierFormat:@"uid = %@", _uid];
366 conn = [self ldapConnection];
367 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
370 entry = [resultEnum nextObject];
373 [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
379 cnAttr = [entry attributeWithName:@"cn"];
380 if(cnAttr == nil && debugOn) {
381 [self logWithFormat:@"%s LDAP entry for uid '%@' has no common name?",
384 return nil; /* nothing we can do about it */
386 cn = [cnAttr stringValueAtIndex:0];
390 - (NSString *)getCNForUID:(NSString *)_uid {
393 if ((cn = [self _cachedCNForUID:_uid]) != nil)
397 cn = [self primaryGetCNForAgenorUID:_uid];
407 // TODO: algorithm might be inappropriate, depends on the actual UID
408 r = [s rangeOfString:@"."];
412 cn = [s substringToIndex:r.location];
415 [self _cacheCN:cn forUID:_uid];
422 - (NSString *)getIMAPAccountStringForUID:(NSString *)_uid {
425 server = [self getServerForUID:_uid];
428 return [NSString stringWithFormat:@"%@@%@", _uid, server];
431 - (NSArray *)mailServerDiscoveryAttributes {
432 static NSArray *attrs = nil;
435 attrs = [[NSArray alloc] initWithObjects:
436 @"uid", /* required for shares */
438 @"mineqMelServeurPrincipal",
444 - (NGLdapEntry *)_fetchEntryForAgenorUID:(NSString *)_uid {
445 // TODO: badly named, this fetches the mail server discovery attributes
446 /* called by -primaryGetServerForAgenorUID: */
447 NGLdapConnection *conn;
449 NSEnumerator *resultEnum;
452 q = [EOQualifier qualifierWithQualifierFormat:@"uid = %@", _uid];
454 conn = [self ldapConnection];
455 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
457 attributes:[self mailServerDiscoveryAttributes]];
458 /* we just expect one entry, thus drop the rest */
459 entry = [resultEnum nextObject];
462 NSLog(@"%s Didn't find LDAP entry for uid '%@'!",
471 - (NSArray *)_serverCandidatesForMineqMelRoutage:(NGLdapAttribute *)attr {
474 "Baluh.Hommes.Tests-Montee-En-Charge-Ogo%equipement.gouv.fr@\
475 amelie-01.ac.melanie2.i2"
477 NSMutableArray *serverCandidates;
480 count = [attr count];
481 serverCandidates = [NSMutableArray arrayWithCapacity:count];
482 for (i = 0; i < count; i++) {
487 NSRange serverNameRange;
488 NSString *serverName;
490 route = [attr stringValueAtIndex:i];
492 /* check for melanie suffix and ignore other entries */
494 r = [route rangeOfString:@".melanie2.i2" options:NSBackwardsSearch];
497 [self logWithFormat:@"found no melanie in route: '%@'", route];
502 /* check for @ inside the string, searching backwards (ignoring suffix) */
504 // be clever: TODO: in what way is this clever?
505 length = [route length];
506 r = NSMakeRange(0, length - r.length); /* cut of suffix (.melanie2.i2) */
507 r = [route rangeOfString:@"@" options:NSBackwardsSearch range:r];
510 [self logWithFormat:@"found no @ in route: '%@'", route];
515 /* check for percent sign */
517 start = NSMaxRange(r); /* start behind the @ */
519 /* this range covers everything after @: 'amelie-01.ac.melanie2.i2' */
520 serverNameRange = NSMakeRange(start, length - start);
522 /* and this range covers everything to the @ */
523 r = NSMakeRange(0, start - 1);
524 r = [route rangeOfString:@"%" options:NSBackwardsSearch range:r];
527 [self logWithFormat:@"found no %% in route: '%@' / '%@'",
528 route, [route substringWithRange:NSMakeRange(0, length - start)]];
533 serverName = [route substringWithRange:serverNameRange];
534 [serverCandidates addObject:serverName];
536 return serverCandidates;
539 - (NSString *)serverFromEntry:(NGLdapEntry *)_entry {
541 NGLdapAttribute *attr;
545 attr = [_entry attributeWithName:@"mineqMelRoutage"];
547 NSArray *serverCandidates;
549 serverCandidates = [self _serverCandidatesForMineqMelRoutage:attr];
550 if ([serverCandidates count] > 0)
551 server = [serverCandidates objectAtIndex:0];
553 if ([serverCandidates count] > 1) {
555 @"WARNING: more than one value for 'mineqMelRoutage': %@",
560 [self debugWithFormat:
561 @"%s LDAP entry '%@' has no mineqMelRoutage entry?",
562 __PRETTY_FUNCTION__, [_entry dn]];
567 attr = [_entry attributeWithName:@"mineqMelServeurPrincipal"];
568 if ([attr count] > 0)
569 server = [attr stringValueAtIndex:0];
575 - (NSString *)primaryGetServerForAgenorUID:(NSString *)_uid {
577 First of all : for a particular user IMAP and SMTP are served on the same
580 The name of the machine is determined by applying a regex on every values of
581 the mineqMelRoutage LDAP attribute.
582 The regex is : .*%.*@(.*\.melanie2\.i2$)
583 It extracts the substring that follows '@', ends with 'melanie2', on
584 adresses which have a '%' before the '@'
586 Example: helge.hesse%opengroupware.org@servername1.melanie2.i2
587 -> servername1.melanie2.i2
589 If only one server name is found by applying the regex on every value of the
590 attribute, then this name is the IMAP/SMTP server for that user.
591 Note that this is the case when we got a unique (well formed) value for the
593 If the regex finds more than one servername when applied to the differents
594 values, then the IMAP/SMTP server name is to be found in the
595 mineqMelServeurPrincipal attribute of the user.
600 if ((entry = [self _fetchEntryForAgenorUID:_uid]) == nil)
603 if ((server = [self serverFromEntry:entry]) != nil)
606 [self debugWithFormat:
607 @"%s no chance of getting at server info for user '%@', "
608 @"tried everything. Sorry.",
609 __PRETTY_FUNCTION__, _uid];
613 - (NSString *)getServerForUID:(NSString *)_uid {
616 if (_uid == nil || [_uid length] == 0)
619 if ((server = [self _cachedServerForUID:_uid]) != nil)
623 server = [self primaryGetServerForAgenorUID:_uid];
624 else if (fallbackIMAP4Server != nil)
625 server = fallbackIMAP4Server;
627 [self logWithFormat:@"ERROR: could not get server for uid '%@', "
628 @"neither LDAP (SOGoUserManagerUsesLDAP) nor "
629 @"a fallback (SOGoFallbackIMAP4Server) is configured.",
634 [self _cacheServer:server forUID:_uid];
638 /* shared mailboxes */
640 - (NSArray *)getSharedMailboxAccountStringsForUID:(NSString *)_uid {
643 "(&(mineqMelPartages=guizmo.g:*)(objectclass=mineqMelBoite))"
644 "guizmo.g" is the uid of the user
647 guizmo.g.-.baluh.hommes.tests-montee-en-charge-ogo
648 (uid + ".-." + share-uid)
650 Note: shared mailboxes can be on different hosts!
652 NSMutableArray *shares = nil;
653 NGLdapConnection *conn;
655 NSString *sharePattern;
656 NSEnumerator *resultEnum;
659 if ([_uid length] == 0)
664 @"Note: LDAP access is disabled, returning no shared accounts."];
669 if ((shares = [self->shareStoreCache objectForKey:_uid]) != nil)
672 sharePattern = [_uid stringByAppendingString:@":*"];
674 q = [EOQualifier qualifierWithQualifierFormat:
675 @"(mineqMelPartages = %@) AND (objectclass = %@)",
676 sharePattern, shareLDAPClass];
678 conn = [self ldapConnection];
680 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
682 attributes:[self mailServerDiscoveryAttributes]];
684 while ((entry = [resultEnum nextObject]) != nil) {
685 NSString *server, *shareLogin;
688 if ([(server = [self serverFromEntry:entry]) length] == 0) {
689 [self errorWithFormat:@"found no mail server host for share: %@",
694 shareUid = [entry attributeWithName:@"uid"];
695 if ([shareUid count] < 1) {
696 [self errorWithFormat:@"found no 'uid' for share: %@", [entry dn]];
699 shareUid = [shareUid stringValueAtIndex:0];
701 shareLogin = [_uid stringByAppendingString:shareLoginSeparator];
702 shareLogin = [shareLogin stringByAppendingString:shareUid];
705 shares = [NSMutableArray arrayWithCapacity:4];
707 shareLogin = [shareLogin stringByAppendingString:@"@"];
708 shareLogin = [shareLogin stringByAppendingString:server];
709 [shares addObject:shareLogin];
712 /* ensure that ordering is always the same */
713 [shares sortUsingSelector:@selector(compare:)];
716 shares = (shares == nil) ? [NSArray array] : [[shares copy] autorelease];
717 [self->shareStoreCache addObject:shares forKey:_uid];
721 - (NSArray *)getSharedMailboxEMailsForUID:(NSString *)_uid {
722 NSMutableArray *shares = nil;
723 NGLdapConnection *conn;
725 NSString *gPattern, *cPattern;
726 NSEnumerator *resultEnum;
729 if ([_uid length] == 0)
734 @"Note: LDAP access is disabled, returning no shared froms."];
739 if ((shares = [self->shareEMailCache objectForKey:_uid]) != nil)
742 /* G and C mean "emission access" */
743 gPattern = [_uid stringByAppendingString:@":G"];
744 cPattern = [_uid stringByAppendingString:@":C"];
746 q = [EOQualifier qualifierWithQualifierFormat:
747 @"((mineqMelPartages = %@) OR (mineqMelPartages = %@)) "
748 @"AND (objectclass = %@)",
749 gPattern, cPattern, shareLDAPClass];
751 conn = [self ldapConnection];
753 resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
755 attributes:fromEMailAttrs];
757 while ((entry = [resultEnum nextObject]) != nil) {
760 emissionAttr = [entry attributeWithName:mailEmissionAttrName];
761 if ([emissionAttr count] == 0) {
762 [self logWithFormat:@"WARNING: share has no %@ attr: %@",
763 mailEmissionAttrName, [entry dn]];
767 if ([emissionAttr count] > 1) {
769 @"WARNING: share has more than one value in %@ attr: %@",
770 mailEmissionAttrName, [entry dn]];
774 emissionAttr = [emissionAttr stringValueAtIndex:0];
775 if (shares == nil) shares = [NSMutableArray arrayWithCapacity:4];
776 [shares addObject:emissionAttr];
779 /* ensure that ordering is always the same */
780 [shares sortUsingSelector:@selector(compare:)];
783 shares = (shares == nil) ? [NSArray array] : [[shares copy] autorelease];
784 [self->shareEMailCache addObject:shares forKey:_uid];
790 - (NSURL *)getFreeBusyURLForUID:(NSString *)_uid {
791 [self logWithFormat:@"TODO(%s): implement", __PRETTY_FUNCTION__];
797 - (NSUserDefaults *)getUserDefaultsForUID:(NSString *)_uid {
800 if (AgenorProfileURL == nil) {
801 [self warnWithFormat:
802 @"no profile configured, cannot retrieve defaults for user: '%@'",
807 /* Note: do not cache, otherwise updates can be quite tricky */
808 defaults = [[[AgenorUserDefaults alloc]
809 initWithTableURL:AgenorProfileURL uid:_uid] autorelease];
815 - (BOOL)isDebuggingEnabled {
821 - (NSString *)description {
824 ms = [NSMutableString stringWithCapacity:16];
825 [ms appendFormat:@"<0x%08X[%@]", self, NSStringFromClass([self class])];
826 [ms appendString:@">"];
830 @end /* AgenorUserManager */