/*
  Copyright (C) 2004-2005 SKYRIX Software AG

  This file is part of OpenGroupware.org.

  OGo is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  OGo is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with OGo; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

#include "AgenorUserManager.h"
#include "AgenorUserDefaults.h"
#include <NGExtensions/NGExtensions.h>
#include <NGLdap/NGLdap.h>
#include <NGCards/NGCards.h>
#include "SOGoLRUCache.h"

#warning we should rely on the LDAP sources instead...
#define qualifierFormat @"mailNickname = %@"

@interface AgenorUserManager (PrivateAPI)
- (NGLdapConnection *)ldapConnection;

- (void)_cacheCN:(NSString *)_cn forUID:(NSString *)_uid;
- (NSString *)_cachedCNForUID:(NSString *)_uid;
- (void)_cacheServer:(NSString *)_server forUID:(NSString *)_uid;
- (NSString *)_cachedServerForUID:(NSString *)_uid;
- (void)_cacheEmail:(NSString *)_email forUID:(NSString *)_uid;
- (NSString *)_cachedEmailForUID:(NSString *)_uid;
- (void)_cacheUID:(NSString *)_uid forEmail:(NSString *)_email;
- (NSString *)_cachedUIDForEmail:(NSString *)_email;

- (BOOL)primaryIsUserAllowedToChangeSOGoInternetAccess:(NSString *)_uid;

- (BOOL)primaryIsInternetAutoresponderEnabledForUser:(NSString *)_uid;
- (BOOL)primaryIsIntranetAutoresponderEnabledForUser:(NSString *)_uid;
- (NGLdapAttribute *)primaryGetMailAutoresponderAttribute:(NSString *)_uid;
- (BOOL)isAutoresponderEnabledForAttribute:(NGLdapAttribute *)_attr
  matchingPrefix:(NSString *)_prefix;
@end

// TODO: add a timer to flush LRU caches every some hours

@implementation AgenorUserManager

static BOOL     debugOn     = NO;
static BOOL     useLDAP     = NO;
static NSString *ldapHost   = nil;
static NSString *ldapBaseDN = nil;
static NSNull   *sharedNull = nil;
static NSString *fallbackIMAP4Server          = nil;
static NSString *defaultMailDomain            = nil;
static NSString *shareLDAPClass               = @"mineqMelBoite";
static NSString *shareLoginSeparator          = @".-.";
static NSString *mailEmissionAttrName         = @"mineqMelmailEmission";
static NSString *changeInternetAccessAttrName = @"mineqOgoAccesInternet";
static NSString *mailAutoresponderAttrName    = @"mineqMelReponse"; /* sic! */
static NSURL    *AgenorProfileURL             = nil;

static NSArray *fromEMailAttrs = nil;

static unsigned PoolScanInterval = 5 * 60 /* every five minutes */;

+ (void) initialize 
{
  static BOOL didInit = NO;
  NSUserDefaults *ud;
  NSString *tmp;

  if (didInit) return;
  didInit = YES;
  
  ud      = [NSUserDefaults standardUserDefaults];
  debugOn = [ud boolForKey:@"SOGoUserManagerDebugEnabled"];
  
  useLDAP = [ud boolForKey:@"SOGoUserManagerUsesLDAP"];
  if (useLDAP) {
    ldapHost   = [[ud stringForKey:@"SOGoLDAPHost"]   copy];
    ldapBaseDN = [[ud stringForKey:@"SOGoLDAPBaseDN"] copy];
    NSLog(@"Note: using LDAP host to manage accounts: %@", ldapHost);
  }
  else
    NSLog(@"Note: LDAP access is disabled.");
  
  fallbackIMAP4Server = [[ud stringForKey:@"SOGoFallbackIMAP4Server"] copy];
  if ([fallbackIMAP4Server length] > 0)
    NSLog(@"Note: using fallback IMAP4 server: '%@'", fallbackIMAP4Server);
  else
    {
      if (fallbackIMAP4Server)
        [fallbackIMAP4Server release];
      fallbackIMAP4Server = nil;
    }

  defaultMailDomain = [[ud stringForKey:@"SOGoDefaultMailDomain"] copy];
  if ([defaultMailDomain length] == 0)
    {
      if (defaultMailDomain)
        [defaultMailDomain release];
      defaultMailDomain = nil;
      NSLog(@"WARNING: no default mail domain (please specify"
            " 'SOGoDefaultMailDomain' in the user default db)");
    }

  fromEMailAttrs = 
    [[NSArray alloc] initWithObjects:mailEmissionAttrName, nil];

  sharedNull = [[NSNull null] retain];

  /* profile database URL */
  
  if ((tmp = [ud stringForKey:@"AgenorProfileURL"]) == nil)
    NSLog(@"ERROR: no 'AgenorProfileURL' database URL configured!");
  else if ((AgenorProfileURL = [[NSURL alloc] initWithString:tmp]) == nil)
    NSLog(@"ERROR: could not parse AgenorProfileURL: '%@'", tmp);
  else
    NSLog(@"Note: using profile at: %@", [AgenorProfileURL absoluteString]);

  if ((tmp = [ud stringForKey: @"SOGoDefaultMailDomain"]))
    {
      defaultMailDomain = [tmp copy];
    }
  
  PoolScanInterval = [[ud objectForKey:@"AgenorCacheCheckInterval"] intValue];
  if (PoolScanInterval == 0) 
    PoolScanInterval = 60 * 60 /* every hour */;
  NSLog(@"AgenorUserManager: flushing caches every %d minutes "
	@"(AgenorCacheCheckInterval)",
	PoolScanInterval / 60);
}

+ (id)sharedUserManager {
  static AgenorUserManager *mgr = nil;
  if (mgr == nil)
    mgr = [[self alloc] init];
  return mgr;
}

- (id)init {
  self = [super init];
  if(self) {
    self->serverCache     = 
      [[NSMutableDictionary alloc] initWithCapacity:10000];
    self->cnCache         = 
      [[NSMutableDictionary alloc] initWithCapacity:10000];
    self->uidCache        = 
      [[NSMutableDictionary alloc] initWithCapacity:10000];
    self->emailCache      = 
      [[NSMutableDictionary alloc] initWithCapacity:10000];
    self->shareStoreCache = 
      [[NSMutableDictionary alloc] initWithCapacity:10000];
    self->shareEMailCache = 
      [[NSMutableDictionary alloc] initWithCapacity:10000];
    self->changeInternetAccessCache =
      [[NSMutableDictionary alloc] initWithCapacity:10000];
    self->internetAutoresponderFlagCache =
      [[NSMutableDictionary alloc] initWithCapacity:10000];
    self->intranetAutoresponderFlagCache =
      [[NSMutableDictionary alloc] initWithCapacity:10000];
    
    self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
				PoolScanInterval
			      target:self selector:@selector(_garbageCollect:)
			      userInfo:nil repeats:YES] retain];
  }
  return self;
}

- (void)dealloc {
  if (self->gcTimer) [self->gcTimer invalidate];
  [self->serverCache     release];
  [self->cnCache         release];
  [self->uidCache        release];
  [self->emailCache      release];
  [self->shareStoreCache release];
  [self->shareEMailCache release];
  [self->changeInternetAccessCache      release];
  [self->internetAutoresponderFlagCache release];
  [self->intranetAutoresponderFlagCache release];
  [self->gcTimer release];
  [super dealloc];
}

/* cache */

- (void)flush {
  [self->cnCache         removeAllObjects];
  [self->serverCache     removeAllObjects];
  [self->uidCache        removeAllObjects];
  [self->emailCache      removeAllObjects];
  [self->shareStoreCache removeAllObjects];
  [self->shareEMailCache removeAllObjects];
  
  [self->changeInternetAccessCache      removeAllObjects];
  [self->internetAutoresponderFlagCache removeAllObjects];
  [self->intranetAutoresponderFlagCache removeAllObjects];
}

- (void)_garbageCollect:(NSTimer *)_timer {
  [self debugWithFormat:@"flushing caches."];
  [self flush];
}

/* LDAP */

- (NGLdapConnection *)ldapConnection {
  static NGLdapConnection *ldapConnection = nil;
  if(!ldapConnection) {
    ldapConnection = [[NGLdapConnection alloc] initWithHostName:ldapHost];
#if 0
    [ldapConnection setUseCache:YES];
#endif
  }
  return ldapConnection;
}


/* private cache helpers */
// TODO: this is really unnecessary, no?

- (void)_cacheCN:(NSString *)_cn forUID:(NSString *)_uid {
  if (_cn == nil) return;
  [self->cnCache setObject:_cn forKey:_uid];
}
- (NSString *)_cachedCNForUID:(NSString *)_uid {
  return [self->cnCache objectForKey:_uid];
}

- (void)_cacheServer:(NSString *)_server forUID:(NSString *)_uid {
  if (_server == nil) return;
  [self->serverCache setObject:_server forKey:_uid];
}
- (NSString *)_cachedServerForUID:(NSString *)_uid {
  return [self->serverCache objectForKey:_uid];
}

- (void)_cacheEmail:(NSString *)_email forUID:(NSString *)_uid {
  if (_email == nil) return;
  [self->emailCache setObject:_email forKey:_uid];
}
- (NSString *)_cachedEmailForUID:(NSString *)_uid {
  return [self->emailCache objectForKey:_uid];
}

- (void)_cacheUID:(NSString *)_uid forEmail:(NSString *)_email {
  if (_uid == nil) return;
  [self->uidCache setObject:_uid forKey:_email];
}
- (NSString *)_cachedUIDForEmail:(NSString *)_email {
  return [self->uidCache objectForKey:_email];
}


/* uid <-> email mapping */

/*
 UPDATE: the email excerpt below has been marked by Maxime as being
         wrong. This algorithm can not be expected to work, thus
         the mapping has been replaced with an LDAP query.

 --- snip ---
 The uid field is in bijection this the email adress :
 this field can be construct from the email. Email are uniques.
 
 So, we can use email adresse from identifier. 
 The field is made like this :
 _ if the email is equipement.gouv.fr then the login
 is the part before the @
 for example : fisrtName.lastName
 _ if the email is not equipement.gouv.fr then the login
 is the full email adress where @ is change to . (dot)
 for example : fisrtName.lastName.subDomain.domain.tld
 --- snap ---

 NOTE: mapping email -> uid is easy, but can also generate uid's not known
       to the system (i.e. for private addressbook entries, obvious).
       The reverse mapping can work _only_ if "firstName.lastname." is
       guaranteed, because the second dot would be mapped to '@'. This
       is probably error prone.
       Only LDAP fetches would guarantee correctness in both cases.
*/

- (NSString *)primaryGetAgenorUIDForEmail:(NSString *)_email {
  static NSArray   *uidAttrs = nil;
  NGLdapConnection *conn;
  EOQualifier      *q;
  NSEnumerator     *resultEnum;
  NGLdapEntry      *entry;
  NGLdapAttribute  *uidAttr;
  NSString         *uid;

  if (uidAttrs == nil)
    uidAttrs = [[NSArray alloc] initWithObjects:@"uid", nil];
    
  q = [EOQualifier qualifierWithQualifierFormat:@"mail = %@", _email];
    
  conn       = [self ldapConnection];
  resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
		     qualifier:q
		     attributes:uidAttrs];
  entry = [resultEnum nextObject];
  if (entry == nil) {
    if(debugOn) {
      [self logWithFormat:@"%s Didn't find LDAP entry for email '%@'!",
	    __PRETTY_FUNCTION__,
	    _email];
    }
    return nil;
  }
  uidAttr = [entry attributeWithName:@"uid"];
  if (!uidAttr)
    return nil; /* can happen, not unlikely */
  uid = [uidAttr stringValueAtIndex:0];
  return uid;
}

- (NSString *) getUIDForEmail: (NSString *)_email
{
  NSString *uid;
  NSRange r;
  NSString *domain;

  uid = nil;
  if ([_email length] > 0)
    {
      uid = [self _cachedUIDForEmail:_email];
      if (!uid)
	{
	  if (useLDAP)
	    uid = [self primaryGetAgenorUIDForEmail:_email];

	  if (!uid)
	    {    
	      r = [_email rangeOfString:@"@"];
	      if (r.length == 0)
		return nil;
    
	      domain = [_email substringFromIndex:NSMaxRange(r)];
	      if (![domain isEqualToString:defaultMailDomain])
		uid = _email;
	      else
		uid = [_email substringToIndex:r.location];
	    }
	  if (uid)
	    [self _cacheUID:uid forEmail:_email];
	}
    }
  
  return uid;
}

#warning big ugly hack. LDAP lookup should be fixed
- (NSString *) getUIDForICalPerson: (iCalPerson *) _person
{
  NSString *domainString, *email, *uid;

  domainString = [NSString stringWithFormat: @"@%@", defaultMailDomain];
  email = [_person rfc822Email];
  if ([email hasSuffix: domainString])
    uid = [_person cn];
  else
    uid = [self getUIDForEmail: email];

  return uid;
}

  /* may insert NSNulls into returned array */
- (NSArray  *)getUIDsForICalPersons:(NSArray *)_persons
  applyStrictMapping:(BOOL)_mapStrictly
{
  NSMutableArray *ma;
  unsigned       i, count;

  count = [_persons count];
  ma    = [[[NSMutableArray alloc] initWithCapacity:count] autorelease];
  for (i = 0; i < count; i++) {
    iCalPerson *p;
    id         uid;
    
    p   = [_persons objectAtIndex:i];
    uid = [self getUIDForICalPerson:p];
    if (uid != nil)
      [ma addObject:uid];
    else if (uid == nil && _mapStrictly)
      [ma addObject:sharedNull];
  }
  return ma;
}

- (NSString *)primaryGetEmailForAgenorUID:(NSString *)_uid {
  NGLdapConnection *conn;
  EOQualifier      *q;
  NSEnumerator     *resultEnum;
  NGLdapEntry      *entry;
  NGLdapAttribute  *emailAttr;
  NSString         *email;
  unsigned         count;
    
  q = [EOQualifier qualifierWithQualifierFormat:qualifierFormat, _uid];
    
  conn       = [self ldapConnection];
  resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
		     qualifier:q
		     attributes:fromEMailAttrs];
  entry = [resultEnum nextObject];
  if (entry == nil) {
      if(debugOn) {
        [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
                              __PRETTY_FUNCTION__,
                              _uid];
      }
      return nil;
  }
  emailAttr = [entry attributeWithName:mailEmissionAttrName];
  if (emailAttr == nil)
    return nil; /* shit happens */
  
  email = nil;
  count = [emailAttr count];
#if 0 // TODO: explain why this is commented out!
  if (count > 1) {
        unsigned i;

        /* in case there are multiple email addresses, select the first
           which doesn't have '@equipement.gouv.fr' in it */
        for (i = 0; i < count; i++) {
          NSString *candidate;
          
          candidate = [emailAttr stringValueAtIndex:i];
          if (![candidate hasSuffix:defaultMailDomain]) {
	    // TODO: also check for '@'
            email = candidate;
            break;
          }
        }
  }
#endif
  if (email == nil && count > 0)
    email = [emailAttr stringValueAtIndex:0];
  
  return email;
}

- (NSString *)getEmailForUID:(NSString *)_uid
{
  NSString *email;
  NSRange r;
  
  email = nil;

  if ([_uid length] > 0)
    {
      email = [self _cachedEmailForUID: _uid];
      if (!email)
	{
	  if (useLDAP)
	    email = [self primaryGetEmailForAgenorUID:_uid];

	  if (!email)
	    {
	      r = [_uid rangeOfString:@"@"];
	      email = ((r.length > 0)
		       ? _uid
		       : [[_uid stringByAppendingString:@"@"]
			   stringByAppendingString: defaultMailDomain]);
	    }

	  if (email)
	    [self _cacheEmail: email forUID: _uid];
	}
    }

  return email;
}


/* CN */

- (NSString *)primaryGetCNForAgenorUID:(NSString *)_uid {
  static NSArray   *cnAttrs = nil;
  NGLdapConnection *conn;
  EOQualifier      *q;
  NSEnumerator     *resultEnum;
  NGLdapEntry      *entry;
  NGLdapAttribute  *cnAttr;
  NSString         *cn;

  if (cnAttrs == nil)
    cnAttrs = [[NSArray alloc] initWithObjects:@"cn", nil];
  
  q = [EOQualifier qualifierWithQualifierFormat:qualifierFormat, _uid];
  
  conn = [self ldapConnection];
  resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
		     qualifier:q
		     attributes:cnAttrs];
  entry = [resultEnum nextObject];
  if (entry == nil) {
      if(debugOn) {
        [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
                              __PRETTY_FUNCTION__,
                              _uid];
      }
      return nil;
  }
  cnAttr = [entry attributeWithName:@"cn"];
  if(cnAttr == nil && debugOn) {
      [self logWithFormat:@"%s LDAP entry for uid '%@' has no common name?",
                            __PRETTY_FUNCTION__,
                            _uid];
      return nil; /* nothing we can do about it */
  }
  cn = [cnAttr stringValueAtIndex:0];
  return cn;
}

- (NSString *)getCNForUID:(NSString *)_uid {
  NSString *cn;

  if ((cn = [self _cachedCNForUID:_uid]) != nil)
    return cn;
  
  if (useLDAP) {
    cn = [self primaryGetCNForAgenorUID:_uid];
  }
  else {
    NSString *s;
    NSRange  r;
    
    s = _uid;
    if ([s length] < 10)
      return s;
    
    // TODO: algorithm might be inappropriate, depends on the actual UID
    r = [s rangeOfString:@"."];
    if (r.length == 0)
      cn = s;
    else
      cn = [s substringToIndex:r.location];
  }
  
  [self _cacheCN:cn forUID:_uid];
  return cn;
}


/* Servers, IMAP */

- (NSString *)getIMAPAccountStringForUID:(NSString *)_uid {
  NSString *server;
  
  server = [self getServerForUID:_uid];
  if (server == nil)
    return nil;
  return [NSString stringWithFormat:@"%@@%@", _uid, server];
}

- (NSArray *)mailServerDiscoveryAttributes {
  static NSArray *attrs = nil;

  if (attrs == nil) {
    attrs = [[NSArray alloc] initWithObjects:
			       @"uid", /* required for shares */
			       @"mineqMelRoutage", 
			       @"mineqMelServeurPrincipal",
			       @"mineqMelPartages",
			       mailEmissionAttrName,
			     nil];
  }
  return attrs;
}

- (NGLdapEntry *)_fetchEntryForAgenorUID:(NSString *)_uid {
  // TODO: badly named, this fetches the mail server discovery attributes
  /* called by -primaryGetServerForAgenorUID: */
  NGLdapConnection *conn;
  EOQualifier      *q;
  NSEnumerator     *resultEnum;
  NGLdapEntry      *entry;
  
  q = [EOQualifier qualifierWithQualifierFormat:qualifierFormat, _uid];
  
  conn = [self ldapConnection];
  resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
		     qualifier:q
		     attributes:[self mailServerDiscoveryAttributes]];
  /* we just expect one entry, thus drop the rest */
  entry = [resultEnum nextObject];
  if (entry == nil) {
      if(debugOn) {
        NSLog(@"%s Didn't find LDAP entry for uid '%@'!",
              __PRETTY_FUNCTION__,
              _uid);
      }
      return nil;
  }
  return entry;
}

- (NSArray *)_serverCandidatesForMineqMelRoutage:(NGLdapAttribute *)attr {
  /*
    eg:
      "Baluh.Hommes.Tests-Montee-En-Charge-Ogo%equipement.gouv.fr@\
       amelie-01.ac.melanie2.i2"
  */
  NSMutableArray *serverCandidates;
  unsigned i, count;

  count            = [attr count];
  serverCandidates = [NSMutableArray arrayWithCapacity:count];
  for (i = 0; i < count; i++) {
    NSRange  r;
    NSString *route;
    unsigned length;
    unsigned start;
    NSRange  serverNameRange;
    NSString *serverName;
    
    route = [attr stringValueAtIndex:i];

    /* check for melanie suffix and ignore other entries */
    
    r = [route rangeOfString:@".melanie2.i2" options:NSBackwardsSearch];
    if (r.length == 0) {
#if 0
      [self logWithFormat:@"found no melanie in route: '%@'", route];
#endif
      continue;
    }

    /* check for @ inside the string, searching backwards (ignoring suffix) */
    
    // be clever: TODO: in what way is this clever?
    length = [route length];
    r = NSMakeRange(0, length - r.length); /* cut of suffix (.melanie2.i2) */
    r = [route rangeOfString:@"@" options:NSBackwardsSearch range:r];
    if (r.length == 0) {
#if 0
      [self logWithFormat:@"found no @ in route: '%@'", route];
#endif
      continue;
    }
    
    /* check for percent sign */
    
    start = NSMaxRange(r); /* start behind the @ */
    
    /* this range covers everything after @: 'amelie-01.ac.melanie2.i2' */
    serverNameRange = NSMakeRange(start, length - start);
    
    /* and this range covers everything to the @ */
    r = NSMakeRange(0, start - 1);
    r = [route rangeOfString:@"%" options:NSBackwardsSearch range:r];
    if (r.length == 0) {
#if 0
      [self logWithFormat:@"found no %% in route: '%@' / '%@'", 
	    route, [route substringWithRange:NSMakeRange(0, length - start)]];
#endif
      continue;
    }
    
    serverName = [route substringWithRange:serverNameRange];
    [serverCandidates addObject:serverName];
  }
  return serverCandidates;
}

- (NSString *)serverFromEntry:(NGLdapEntry *)_entry {
  NSString        *server;
  NGLdapAttribute *attr;

  server = nil;
  
  attr = [_entry attributeWithName:@"mineqMelRoutage"];
  if (attr != nil) {
    NSArray *serverCandidates;
    
    serverCandidates = [self _serverCandidatesForMineqMelRoutage:attr];
    if ([serverCandidates count] > 0)
      server = [serverCandidates objectAtIndex:0];
    
    if ([serverCandidates count] > 1) {
      [self logWithFormat:
	      @"WARNING: more than one value for 'mineqMelRoutage': %@",
	      serverCandidates];
    }
  }
  else {
    [self debugWithFormat:
	    @"%s LDAP entry '%@' has no mineqMelRoutage entry?",
	    __PRETTY_FUNCTION__, [_entry dn]];
  }
  
  /* last resort */
  if (server == nil) {
    attr = [_entry attributeWithName:@"mineqMelServeurPrincipal"];
    if ([attr count] > 0)
      server = [attr stringValueAtIndex:0];
  }
  
  return server;
}

- (NSString *)primaryGetServerForAgenorUID:(NSString *)_uid {
  /*
   First of all : for a particular user IMAP and SMTP are served on the same
   host.
   
   The name of the machine is determined by applying a regex on every values of
   the mineqMelRoutage LDAP attribute.
   The regex is :   .*%.*@(.*\.melanie2\.i2$)
   It extracts the substring that follows '@', ends with 'melanie2', on
   adresses which have a '%' before the '@'
   
   Example: helge.hesse%opengroupware.org@servername1.melanie2.i2
   -> servername1.melanie2.i2
   
   If only one server name is found by applying the regex on every value of the
   attribute, then this name is the IMAP/SMTP server for that user.
   Note that this is the case when we got a unique (well formed) value for the
   attribute.
   If the regex finds more than one servername when applied to the differents
   values, then the IMAP/SMTP server name is to be found in the
   mineqMelServeurPrincipal attribute of the user.
  */
  NSString    *server;
  NGLdapEntry *entry;
  
  if ((entry = [self _fetchEntryForAgenorUID:_uid]) == nil)
    return nil;
  
  if ((server = [self serverFromEntry:entry]) != nil)
    return server;
  
  [self debugWithFormat:
	  @"%s no chance of getting at server info for user '%@', "
	  @"tried everything. Sorry.",
	  __PRETTY_FUNCTION__, _uid];
  return nil;
}

- (NSString *)getServerForUID:(NSString *)_uid {
  NSString *server;

  if (_uid == nil || [_uid length] == 0)
    return nil;
  
  if ((server = [self _cachedServerForUID:_uid]) != nil)
    return server;
  
  if (useLDAP)
    server = [self primaryGetServerForAgenorUID:_uid];
  else if (fallbackIMAP4Server != nil)
    server = fallbackIMAP4Server;
  else {
    [self logWithFormat:@"ERROR: could not get server for uid '%@', "
	  @"neither LDAP (SOGoUserManagerUsesLDAP) nor "
	  @"a fallback (SOGoFallbackIMAP4Server) is configured.",
	  _uid];
    server = nil;
  }
  
  [self _cacheServer:server forUID:_uid];
  return server;
}

/* shared mailboxes */

- (NSArray *)getSharedMailboxAccountStringsForUID:(NSString *)_uid {
  NSArray *k;
  
  k = [[self getSharedMailboxesAndEMailsForUID:_uid] allKeys];
  
  /* ensure that ordering is always the same */
  return [k sortedArrayUsingSelector:@selector(compare:)];
}

- (NSString *)emissionEMailFromEntry:(NGLdapEntry *)_entry {
  id emissionAttr;
  
  emissionAttr = [_entry attributeWithName:mailEmissionAttrName];
  if ([emissionAttr count] == 0) {
    [self logWithFormat:@"WARNING: share has no %@ attr: %@",
	    mailEmissionAttrName, [_entry dn]];
    return nil;
  }
    
  if ([emissionAttr count] > 1) {
    [self logWithFormat:
	    @"WARNING: share has more than one value in %@ attr: %@",
	    mailEmissionAttrName, [_entry dn]];
    return nil;
  }
  
  return [emissionAttr stringValueAtIndex:0];
}

- (NSArray *)getSharedMailboxEMailsForUID:(NSString *)_uid {
  NSMutableArray   *shares = nil;
  NGLdapConnection *conn;
  EOQualifier      *q;
  NSString         *gPattern, *cPattern;
  NSEnumerator     *resultEnum;
  NGLdapEntry      *entry;
  
  if ([_uid length] == 0)
    return nil;
  
  if (!useLDAP) {
    [self logWithFormat:
	    @"Note: LDAP access is disabled, returning no shared froms."];
    return nil;
  }
  
  /* check cache */
  if ((shares = [self->shareEMailCache objectForKey:_uid]) != nil)
    return shares;
  
  /* G and C mean "emission access" */
  gPattern = [_uid stringByAppendingString:@":G"];
  cPattern = [_uid stringByAppendingString:@":C"];
  
  q = [EOQualifier qualifierWithQualifierFormat:
		     @"((mineqMelPartages = %@) OR (mineqMelPartages = %@)) "
		     @"AND (objectclass = %@)",
		   gPattern, cPattern, shareLDAPClass];
  
  conn = [self ldapConnection];
  
  resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
		     qualifier:q
		     attributes:fromEMailAttrs];
  
  while ((entry = [resultEnum nextObject]) != nil) {
    NSString *emissionAttr;

    if ((emissionAttr = [self emissionEMailFromEntry:entry]) == nil)
      continue;
    
    if (shares == nil) shares = [NSMutableArray arrayWithCapacity:4];
    [shares addObject:emissionAttr];
  }
  
  /* ensure that ordering is always the same */
  [shares sortUsingSelector:@selector(compare:)];
  
  /* cache */
  shares = (shares == nil) ? [NSArray array] : [[shares copy] autorelease];
  [self->shareEMailCache setObject:shares forKey:_uid];
  return shares;
}

/* identities */

- (BOOL)hasUser:(NSString *)_uid partageAccess:(char *)_rightset
  inEntry:(NGLdapEntry *)_entry
{
  NGLdapAttribute *attr;
  unsigned i, count;
  
  attr = [_entry attributeWithName:@"mineqMelPartages"];
  if ((count = [attr count]) == 0) {
    [self logWithFormat:@"WARNING: share has no 'mineqMelPartages' attr: %@",
	    [_entry dn]];
    return NO;
  }
  
  for (i = 0; i < count; i++) {
    NSString *p;
    NSRange  r;
    unichar  c;
    register unsigned j;
    
    p = [attr stringValueAtIndex:i];
    r = [p rangeOfString:@":"];
    if (r.length == 0) {
      [self errorWithFormat:@"Invalid mineqMelPartages value: '%@'", p];
      continue;
    }
    
    /* check whether prefix matches, eg: "helian.h:G" */
    if (r.location != [_uid length]) /* check length */
      continue;
    if (![p hasPrefix:_uid])
      continue;
    
    c = [p characterAtIndex:(r.location + r.length)];
    
    /* check whether permissions match */
    for (j = 0; _rightset[j] != '\0'; j++) {
      if (c == _rightset[j])
	return YES;
    }
  }
  return NO;
}

- (NSDictionary *)getSharedMailboxesAndEMailsForUID:(NSString *)_uid {
  /*
    Sample:
      "(&(mineqMelPartages=guizmo.g:*)(objectclass=mineqMelBoite))"
      "guizmo.g" is the uid of the user
    
    Login:
      guizmo.g.-.baluh.hommes.tests-montee-en-charge-ogo
      (uid + ".-." + share-uid)
    
    Note: shared mailboxes can be on different hosts!
    
    This returns a dictionary where the keys are the IMAP4 connect strings
    while the values are the emitter addresses for the box or NSNull if the
    uid is not allowed to emit for this box.
  */
  NSMutableDictionary *shares = nil;
  NGLdapConnection *conn;
  EOQualifier      *q;
  NSString         *sharePattern;
  NSEnumerator     *resultEnum;
  NGLdapEntry      *entry;
  
  if ([_uid length] == 0)
    return nil;
  
  if (!useLDAP) {
    [self logWithFormat:
	    @"Note: LDAP access is disabled, returning no shared accounts."];
    return nil;
  }

  /* check cache */
  if ((shares = [self->shareStoreCache objectForKey:_uid]) != nil)
    return shares;
  
  sharePattern = [_uid stringByAppendingString:@":*"];
  
  q = [EOQualifier qualifierWithQualifierFormat:
		     @"(mineqMelPartages = %@) AND (objectclass = %@)",
		     sharePattern, shareLDAPClass];
  
  conn = [self ldapConnection];

  resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
		     qualifier:q
		     attributes:[self mailServerDiscoveryAttributes]];
  
  while ((entry = [resultEnum nextObject]) != nil) {
    NSString *server, *shareLogin;
    id shareUid, emitterAddress;

    /* calculate server connect string */
    
    if ([(server = [self serverFromEntry:entry]) length] == 0) {
      [self errorWithFormat:@"found no mail server host for share: %@",
	      [entry dn]];
      continue;
    }
    
    shareUid = [entry attributeWithName:@"uid"];
    if ([shareUid count] < 1) {
      [self errorWithFormat:@"found no 'uid' for share: %@", [entry dn]];
      continue;
    }
    shareUid = [shareUid stringValueAtIndex:0];
    
    shareLogin = [_uid stringByAppendingString:shareLoginSeparator];
    shareLogin = [shareLogin stringByAppendingString:shareUid];
    
    if (shares == nil)
      shares = [NSMutableDictionary dictionaryWithCapacity:4];
    
    shareLogin = [shareLogin stringByAppendingString:@"@"];
    shareLogin = [shareLogin stringByAppendingString:server];
    
    /* calculate emitter address (check for proper access right) */
    
    if ([self hasUser:_uid partageAccess:"GC" inEntry:entry])
      emitterAddress = [self emissionEMailFromEntry:entry];
    else
      emitterAddress = [NSNull null];
    
    /* set value */
    
    [shares setObject: emitterAddress
	    forKey: shareLogin];
  }
  
  /* cache */
  shares = (shares == nil) 
    ? [NSDictionary dictionary] 
    : [[shares copy] autorelease];
  [self->shareStoreCache setObject:shares forKey:_uid];
  return shares;
}

/* free busy */

- (NSURL *)getFreeBusyURLForUID:(NSString *)_uid {
  [self logWithFormat:@"TODO(%s): implement", __PRETTY_FUNCTION__];
  return nil;
}

/* defaults */

- (NSUserDefaults *) _getUserDefaultsForUID: (NSString *) uid
				  fieldName: (NSString *) fieldName
{
  id defaults;
  
  if (AgenorProfileURL)
    {
      /* Note: do not cache, otherwise updates can be quite tricky */
      defaults = [[AgenorUserDefaults alloc] initWithTableURL: AgenorProfileURL
					     uid: uid fieldName: fieldName];
      [defaults autorelease];
    }
  else
    {
      [self warnWithFormat:
	      @"no profile configured, cannot retrieve defaults for user: '%@'",
	    uid];
      return defaults = nil;
    }

  return defaults;
}

- (NSUserDefaults *) getUserDefaultsForUID: (NSString *) uid
{
  return [self _getUserDefaultsForUID: uid fieldName: @"defaults"];
}

- (NSUserDefaults *) getUserSettingsForUID: (NSString *) uid
{
  return [self _getUserDefaultsForUID: uid fieldName: @"settings"];
}

/* internet access lock */

- (BOOL)isUserAllowedToChangeSOGoInternetAccess:(NSString *)_uid {
  NSNumber *bv;
  
  bv = [self->changeInternetAccessCache objectForKey:_uid];
  if (!bv) {
    BOOL value;
    
    value = [self primaryIsUserAllowedToChangeSOGoInternetAccess:_uid];
    bv    = [NSNumber numberWithBool:value];
    [self->changeInternetAccessCache setObject:bv forKey:_uid];
  }
  return [bv boolValue];
}

- (BOOL)primaryIsUserAllowedToChangeSOGoInternetAccess:(NSString *)_uid {
  static NSArray   *attrs = nil;
  NGLdapConnection *conn;
  EOQualifier      *q;
  NSEnumerator     *resultEnum;
  NGLdapEntry      *entry;
  NGLdapAttribute  *attr;
  NSString         *value;
  
  if (attrs == nil)
    attrs = [[NSArray alloc] initWithObjects:changeInternetAccessAttrName, nil];
  
  q = [EOQualifier qualifierWithQualifierFormat:qualifierFormat, _uid];
  
  conn       = [self ldapConnection];
  resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
                     qualifier:q
                     attributes:attrs];
  entry = [resultEnum nextObject];
  if (entry == nil) {
    if(debugOn) {
      [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
        __PRETTY_FUNCTION__,
        _uid];
    }
    return NO;
  }
  attr = [entry attributeWithName:changeInternetAccessAttrName];
  if(attr == nil && debugOn) {
    [self logWithFormat:@"%s LDAP entry for uid '%@' "
                        @"has no mineqOgoAccesInternet attribute?",
                        __PRETTY_FUNCTION__,
                        _uid];
    return NO; /* nothing we can do about it */
  }
  value = [attr stringValueAtIndex:0];
  return [value boolValue];
}

- (BOOL)isInternetAutoresponderEnabledForUser:(NSString *)_uid {
  NSNumber *bv;

  bv = [self->internetAutoresponderFlagCache objectForKey:_uid];
  if (!bv) {
    BOOL value;
    
    value = [self primaryIsInternetAutoresponderEnabledForUser:_uid];
    bv    = [NSNumber numberWithBool:value];
    [self->internetAutoresponderFlagCache setObject:bv forKey:_uid];
  }
  return [bv boolValue];
}

- (BOOL)primaryIsInternetAutoresponderEnabledForUser:(NSString *)_uid {
  NGLdapAttribute *attr;
  
  attr = [self primaryGetMailAutoresponderAttribute:_uid];
  if (!attr) return NO;
  return [self isAutoresponderEnabledForAttribute:attr matchingPrefix:@"60~"];
}

- (BOOL)isIntranetAutoresponderEnabledForUser:(NSString *)_uid {
  NSNumber *bv;
  
  bv = [self->intranetAutoresponderFlagCache objectForKey:_uid];
  if (!bv) {
    BOOL value;
    
    value = [self primaryIsIntranetAutoresponderEnabledForUser:_uid];
    bv    = [NSNumber numberWithBool:value];
    [self->intranetAutoresponderFlagCache setObject:bv forKey:_uid];
  }
  return [bv boolValue];
}

- (BOOL)primaryIsIntranetAutoresponderEnabledForUser:(NSString *)_uid {
  NGLdapAttribute *attr;
  
  attr = [self primaryGetMailAutoresponderAttribute:_uid];
  if (!attr) return NO;
  return [self isAutoresponderEnabledForAttribute:attr matchingPrefix:@"50~"];
}

- (BOOL)isAutoresponderEnabledForAttribute:(NGLdapAttribute *)_attr
  matchingPrefix:(NSString *)_prefix
{
  unsigned i, count;
  
  count = [_attr count];
  for (i = 0; i < count; i++) {
    NSString *value;
    
    value = [_attr stringValueAtIndex:i];
    if ([value hasPrefix:_prefix]) {
      if ([value rangeOfString:@"DFIN:0"].length > 0)
        return NO;
      return YES;
    }
  }
  return NO;
}

- (NGLdapAttribute *)primaryGetMailAutoresponderAttribute:(NSString *)_uid {
  static NSArray   *attrs = nil;
  NGLdapConnection *conn;
  EOQualifier      *q;
  NSEnumerator     *resultEnum;
  NGLdapEntry      *entry;
  NGLdapAttribute  *attr;
  
  if (attrs == nil)
    attrs = [[NSArray alloc] initWithObjects:mailAutoresponderAttrName, nil];

  q = [EOQualifier qualifierWithQualifierFormat:qualifierFormat, _uid];
  
  conn       = [self ldapConnection];
  resultEnum = [conn deepSearchAtBaseDN:ldapBaseDN
                     qualifier:q
                     attributes:attrs];
  entry = [resultEnum nextObject];
  if (entry == nil) {
    if(debugOn) {
      [self logWithFormat:@"%s Didn't find LDAP entry for uid '%@'!",
        __PRETTY_FUNCTION__,
        _uid];
    }
    return nil;
  }
  attr = [entry attributeWithName:mailAutoresponderAttrName];
  return attr;
}

- (iCalPerson *) iCalPersonWithUid: (NSString *) uid
{
  iCalPerson *person;

  person = [iCalPerson new];
  [person autorelease];
  [person setCn: [self getCNForUID: uid]];
  [person setEmail: [self getEmailForUID: uid]];

  return person;
}

/* debugging */

- (BOOL)isDebuggingEnabled {
  return debugOn;
}

@end /* AgenorUserManager */
