/* SQLSource.h - this file is part of SOGo
 *
 * Copyright (C) 2009-2022 Inverse inc.
 *
 * This file is part of SOGo.
 *
 * This file is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This file 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#import <Foundation/NSDictionary.h>
#import <Foundation/NSException.h>
#import <Foundation/NSValue.h>

#import <NGExtensions/NSNull+misc.h>
#import <NGExtensions/NSObject+Logs.h>

#import <GDLContentStore/GCSChannelManager.h>
#import <GDLContentStore/NSURL+GCS.h>
#import <GDLContentStore/EOQualifier+GCS.h>
#import <GDLAccess/EOAdaptor.h>
#import <GDLAccess/EOAdaptorChannel.h>
#import <GDLAccess/EOAdaptorContext.h>
#import <GDLAccess/EOAttribute.h>
#import <GDLAccess/EOEntity.h>
#import <GDLAccess/EOSQLQualifier.h>

#import <SOGo/SOGoSystemDefaults.h>

#import "NSArray+Utilities.h"
#import "NSString+Utilities.h"
#import "NSString+Crypto.h"

#import "SQLSource.h"
#import "SOGoPasswordPolicy.h"

/**
 * The view MUST contain the following columns:
 *
 * c_uid      - will be used for authentication - it's a username or username@domain.tld)
 * c_name     - which can be identical to c_uid - will be used to uniquely identify entries)
 * c_password - password of the user, can be encoded in {scheme}pass format, or when stored without
 *              scheme it uses the scheme set in userPasswordAlgorithm.
 *              Possible algorithms are:  plain, md5, crypt-md5, sha, ssha (including 256/512 variants),
 *              cram-md5, smd5, crypt, crypt-md5
 * c_cn       - the user's common name
 * mail       - the user's mail address
 *
 * Other columns can be defined - see LDAPSource.m for the complete list.
 *
 *
 * A SQL source can be defined like this:
 *
 *  {
 *    id = zot;
 *    type = sql;
 *    viewURL = "mysql://sogo:sogo@127.0.0.1:5432/sogo/sogo_view";
 *    canAuthenticate = YES;
 *    isAddressBook = YES;
 *    userPasswordAlgorithm = md5;
 *    prependPasswordScheme = YES;
 *  }
 *
 * If prependPasswordScheme is set to YES, the generated passwords will have the format {scheme}password.
 * If it is NO (the default), the password will be written to database without encryption scheme.
 *
 */

@implementation SQLSource

+ (id) sourceFromUDSource: (NSDictionary *) udSource
                 inDomain: (NSString *) domain
{
  return [[[self alloc] initFromUDSource: udSource
                                inDomain: domain] autorelease];
}

- (id) init
{
  if ((self = [super init]))
    {
      _sourceID = nil;
      _domainField = nil;
      _authenticationFilter = nil;
      _loginFields = nil;
      _mailFields = nil;
      // "mail" expands to all entries of MailFieldNames
      _searchFields = [NSArray arrayWithObjects: @"c_cn", @"mail", nil];
      [_searchFields retain];
      _userPasswordPolicy = nil;
      _userPasswordAlgorithm = nil;
      _keyPath = nil;
      _viewURL = nil;
      _kindField = nil;
      _multipleBookingsField = nil;
      _imapHostField = nil;
      _sieveHostField = nil;
      _listRequiresDot = YES;
      _globalAddressBookFirstEntriesCount = -1;
      _modulesConstraints = nil;
    }

  return self;
}

- (void) dealloc
{
  [_sourceID release];
  [_authenticationFilter release];
  [_loginFields release];
  [_mailFields release];
  [_searchFields release];
  [_userPasswordPolicy release];
  [_userPasswordAlgorithm release];
  [_keyPath release];
  [_viewURL release];
  [_kindField release];
  [_multipleBookingsField release];
  [_domainField release];
  [_imapHostField release];
  [_sieveHostField release];
  [_modulesConstraints release];

  [super dealloc];
}

- (id) initFromUDSource: (NSDictionary *) udSource
               inDomain: (NSString *) sourceDomain
{
  NSNumber *dotValue;

  self = [self init];

  ASSIGN(_sourceID, [udSource objectForKey: @"id"]);
  ASSIGN(_authenticationFilter, [udSource objectForKey: @"authenticationFilter"]);
  ASSIGN(_loginFields, [udSource objectForKey: @"LoginFieldNames"]);
  ASSIGN(_mailFields, [udSource objectForKey: @"MailFieldNames"]);
  ASSIGN(_userPasswordPolicy, [SOGoPasswordPolicy createPasswordPolicyRegex: [udSource objectForKey: @"userPasswordPolicy"]]);
  ASSIGN(_userPasswordAlgorithm, [udSource objectForKey: @"userPasswordAlgorithm"]);
  ASSIGN(_keyPath, [udSource objectForKey: @"keyPath"]);
  ASSIGN(_imapLoginField, [udSource objectForKey: @"IMAPLoginFieldName"]);
  ASSIGN(_imapHostField, [udSource objectForKey:  @"IMAPHostFieldName"]);
  ASSIGN(_sieveHostField, [udSource objectForKey:  @"SieveHostFieldName"]);
  ASSIGN(_kindField, [udSource objectForKey: @"KindFieldName"]);
  ASSIGN(_multipleBookingsField, [udSource objectForKey: @"MultipleBookingsFieldName"]);
  ASSIGN(_domainField, [udSource objectForKey: @"DomainFieldName"]);
  ASSIGN(_modulesConstraints, [udSource objectForKey: @"ModulesConstraints"]);
  if ([udSource objectForKey: @"SearchFieldNames"])
    ASSIGN(_searchFields, [udSource objectForKey: @"SearchFieldNames"]);
  if ([udSource objectForKey: @"prependPasswordScheme"])
    _prependPasswordScheme = [[udSource objectForKey: @"prependPasswordScheme"] boolValue];
  else
    _prependPasswordScheme = NO;

  if (!_userPasswordAlgorithm)
    _userPasswordAlgorithm = @"none";

  if ([udSource objectForKey: @"viewURL"])
    _viewURL = [[NSURL alloc] initWithString: [udSource objectForKey: @"viewURL"]];

  dotValue = [udSource objectForKey: @"listRequiresDot"];
  if (dotValue) {
    [self setListRequiresDot: [dotValue boolValue]];
    if ([udSource objectForKey: @"globalAddressBookFirstEntriesCount"])
      [self setGlobalAddressBookFirstEntriesCount: [[udSource objectForKey: @"globalAddressBookFirstEntriesCount"] intValue]];
  }


#warning this domain code has no effect yet
  if ([sourceDomain length])
    ASSIGN(_domain, sourceDomain);

  if (!_viewURL)
    {
      [self autorelease];
      return nil;
    }

  return self;
}

- (NSString *) domain
{
  return _domain;
}

- (NSArray *) searchFields
{
  return _searchFields;
}

- (NSArray *) userPasswordPolicy
{
  return _userPasswordPolicy;
}

- (BOOL) _isPassword: (NSString *) plainPassword
             equalTo: (NSString *) encryptedPassword
{
  if (!plainPassword || !encryptedPassword)
    return NO;

  return [plainPassword isEqualToCrypted: encryptedPassword
                       withDefaultScheme: _userPasswordAlgorithm
                                 keyPath: _keyPath];
}

/**
 * Encrypts a string using this source password algorithm.
 * @param plainPassword the unencrypted password.
 * @return a new encrypted string.
 * @see _isPassword:equalTo:
 */
- (NSString *) _encryptPassword: (NSString *) plainPassword
{
  NSString *pass, *passwordScheme;
  NSString* result;

  pass = [plainPassword asCryptedPassUsingScheme: _userPasswordAlgorithm
                                         keyPath: _keyPath];

  if (pass == nil)
    {
      [self errorWithFormat: @"Unsupported user-password algorithm: %@", _userPasswordAlgorithm];
      return nil;
    }

  if (_prependPasswordScheme)
    result = [NSString stringWithFormat: @"{%@}%@", _userPasswordAlgorithm, pass];
  else
    result = pass;

  return result;
}

- (BOOL) checkLogin: (NSString *) _login
           password: (NSString *) _pwd
               perr: (SOGoPasswordPolicyError *) _perr
             expire: (int *) _expire
              grace: (int *) _grace
{
  return [self checkLogin: _login
                 password: _pwd
                     perr: _perr
                   expire: _expire
                    grace: _grace
  disablepasswordPolicyCheck: NO];
}

//
// SQL sources don't support right now all the password policy
// stuff supported by OpenLDAP (and others). If we want to support
// this for SQL sources, we'll have to implement the same
// kind of logic in this module.
//
- (BOOL) checkLogin: (NSString *) _login
           password: (NSString *) _pwd
               perr: (SOGoPasswordPolicyError *) _perr
             expire: (int *) _expire
              grace: (int *) _grace
  disablepasswordPolicyCheck: (BOOL) _disablepasswordPolicyCheck
{
  EOAdaptor *adaptor;
  EOAdaptorChannel *channel;
  EOQualifier *qualifier;
  GCSChannelManager *cm;
  NSException *ex;
  NSMutableString *sql;
  BOOL rc;

  rc = NO;

  cm = [GCSChannelManager defaultChannelManager];
  channel = [cm acquireOpenChannelForURL: _viewURL];
  if (channel)
    {
      EOAdaptorContext *adaptorCtx;
      adaptorCtx = [channel adaptorContext];
      adaptor = [adaptorCtx adaptor];
      if (_loginFields)
        {
          NSMutableArray *qualifiers;
          NSString *field;
          EOQualifier *loginQualifier;
          int i;

          qualifiers = [NSMutableArray arrayWithCapacity: [_loginFields count]];
          for (i = 0; i < [_loginFields count]; i++)
            {
              field = [_loginFields objectAtIndex: i];
              loginQualifier = [[EOKeyValueQualifier alloc] initWithKey: field
                                                       operatorSelector: EOQualifierOperatorEqual
                                                                  value: _login];
              [loginQualifier autorelease];
              [qualifiers addObject: loginQualifier];
            }
          qualifier = [[EOOrQualifier alloc] initWithQualifierArray: qualifiers];
        }
      else
        {
          qualifier = [[EOKeyValueQualifier alloc] initWithKey: @"c_uid"
                                              operatorSelector: EOQualifierOperatorEqual
                                                         value: _login];
        }
      [qualifier autorelease];
      sql = [NSMutableString stringWithFormat: @"SELECT c_password"
                             @" FROM %@"
                             @" WHERE ",
                             [_viewURL gcsTableName]];
      if (_authenticationFilter)
        {
          qualifier = [[EOAndQualifier alloc] initWithQualifiers:
                                                qualifier,
                       [EOQualifier qualifierWithQualifierFormat: _authenticationFilter],
                                              nil];
          [qualifier autorelease];
        }
      [qualifier appendSQLToString: sql
                       withAdaptor: adaptor];

      ex = [channel evaluateExpressionX: sql];
      if (!ex)
        {
          NSDictionary *row;
          NSArray *attrs;
          NSString *value;

          attrs = [channel describeResults: NO];
          row = [channel fetchAttributes: attrs  withZone: NULL];
          value = [row objectForKey: @"c_password"];

          if (_pwd != (id)[NSNull null]) {
            rc = [self _isPassword: _pwd  equalTo: value];
          }
          [channel cancelFetch];
        }
      else
        [self errorWithFormat: @"could not run SQL '%@': %@", qualifier, ex];

      [cm releaseChannel: channel];
    }
  else
    [self errorWithFormat:@"failed to acquire channel for URL: %@",
          [_viewURL absoluteString]];
  
  if (YES == rc && !_disablepasswordPolicyCheck) {
    [self checkPasswordPolicyWithPassword:_pwd perr: _perr];
  }

  return rc;
}

/**
 * Validates a given password against the configured user password policies.
 *
 * This method checks if the provided password complies with all defined 
 * password policies for the user. Each policy is expected to include 
 * a regular expression (`regex`) that the password must match.
 * 
 * If the password violates any policy, the method sets an appropriate error
 * in the `perr` parameter and stops further validation.
 *
 * @param password the password to validate.
 * @param perr will be set to indicate a policy violation, if the password
 *             does not meet the required standards. Possible values include:
 *             `PolicyInsufficientPasswordQuality` for insufficient complexity.
 * @return YES if the password satisfies all policies, NO otherwise.
 *
 * @note This method assumes that `_userPasswordPolicy` is an array of dictionaries,
 *       where each dictionary represents a password policy with at least a "regex" key.
 * @warning If a policy does not include a "regex" key, an error will be logged, and
 *          the method will continue to the next policy.
 */
- (BOOL) checkPasswordPolicyWithPassword: (NSString *)password perr: (SOGoPasswordPolicyError *)perr
{
  BOOL isPolicyOk;
  NSDictionary *policy;
  NSEnumerator *policies;
  NSRange match;
  NSString *regex;

  isPolicyOk = YES;

  if ([_userPasswordPolicy count])
  {
    policies = [_userPasswordPolicy objectEnumerator];
    while (isPolicyOk && (policy = [policies nextObject]))
      {
        regex = [policy objectForKey: @"regex"];
        if (regex)
          {
            match = [password rangeOfString: regex options: NSRegularExpressionSearch];
            isPolicyOk = isPolicyOk && match.length > 0;
            if (match.length == 0)
              {
                // [self errorWithFormat: @"Password not conform to policy %@ (%@)", regex, [policy objectForKey: @"label"]];
                *perr = PolicyInsufficientPasswordQuality;
              }
          }
        else
          [self errorWithFormat: @"Invalid password policy (missing regex): %@", policy];
      }
  }

  return isPolicyOk;
}

/**
 * Change a user's password.
 * @param login the user's login name.
 * @param oldPassword the previous password.
 * @param newPassword the new password.
 * @param perr will be set if the new password is not conform to the policy.
 * @param passwordRecovery YES of this is password recovery, NO otherwise. If password recovery is set, old password won't be checked
 * @return YES if the password was successfully changed.
 */
- (BOOL) changePasswordForLogin: (NSString *) login
                    oldPassword: (NSString *) oldPassword
                    newPassword: (NSString *) newPassword
               passwordRecovery: (BOOL) passwordRecovery
                           perr: (SOGoPasswordPolicyError *) perr
{
  BOOL didChange, isOldPwdOk, isPolicyOk;
  EOAdaptorChannel *channel;
  EOAdaptor *adaptor;
  EOQualifier *qualifier_login, *qualifier_pwd;
  GCSChannelManager *cm;
  NSException *ex;
  NSMutableString *sqlstr;

  *perr = -1;
  isOldPwdOk = NO;
  isPolicyOk = YES;
  didChange = NO;

  // Verify current password
  isOldPwdOk = [self checkLogin:login password:oldPassword perr:perr expire:0 grace:0 disablepasswordPolicyCheck: YES];

  if (isOldPwdOk || passwordRecovery)
    {
      isPolicyOk = [self checkPasswordPolicyWithPassword:newPassword perr: perr];
    }

  if ((isOldPwdOk || passwordRecovery) && isPolicyOk)
    {
      // Encrypt new password
      NSString *encryptedPassword = [self _encryptPassword: newPassword];
      if(encryptedPassword == nil)
        return NO;

      // Save new password
      cm = [GCSChannelManager defaultChannelManager];
      channel = [cm acquireOpenChannelForURL: _viewURL];
      if (channel)
        {
          EOAdaptorContext *adaptorCtx;
          adaptorCtx = [channel adaptorContext];
          adaptor = [adaptorCtx adaptor];
          sqlstr = [NSMutableString stringWithFormat: @"UPDATE %@ SET ",
                                              [_viewURL gcsTableName]];
                
          qualifier_pwd = [[EOKeyValueQualifier alloc] initWithKey: @"c_password"
                                              operatorSelector: EOQualifierOperatorEqual
                                                         value: encryptedPassword];
          [qualifier_pwd appendSQLToString: sqlstr
                          withAdaptor: adaptor];
          [sqlstr appendString: @" WHERE "];
          qualifier_login = [[EOKeyValueQualifier alloc] initWithKey: @"c_uid"
                                              operatorSelector: EOQualifierOperatorEqual
                                                         value: login];
          [qualifier_login appendSQLToString: sqlstr
                          withAdaptor: adaptor];
          ex = [channel evaluateExpressionX: sqlstr];
          if (!ex)
            {
              didChange = YES;
            }
          else
            {
              [self errorWithFormat: @"could not run SQL '%@': %@", sqlstr, ex];
            }
          [cm releaseChannel: channel];
        }
    }

  return didChange;
}

/*
- (NSString *) _whereClauseFromArray: (NSArray *) theArray
                               value: (NSString *) theValue
	       exact: (BOOL) theBOOL
{
  NSMutableString *s;
  int i;

  s = [NSMutableString string];

  for (i = 0; i < [theArray count]; i++)
    {
      if (theBOOL)
	[s appendFormat: @" OR LOWER(%@) = '%@'", [theArray objectAtIndex: i], theValue];
      else
	[s appendFormat: @" OR LOWER(%@) LIKE '%%%@%%'", [theArray objectAtIndex: i], theValue];
    }

  return s;
}
*/

- (void) _fillConstraintsForModule: (NSString *) module
                        intoRecord: (NSMutableDictionary *) record
{
  NSDictionary *constraints;
  NSEnumerator *matches;
  NSString *currentMatch, *currentValue, *recordValue;
  BOOL result;

  result = YES;

  constraints = [_modulesConstraints objectForKey: module];
  if (constraints)
    {
      matches = [[constraints allKeys] objectEnumerator];
      while (result == YES && (currentMatch = [matches nextObject]))
        {
          currentValue = [constraints objectForKey: currentMatch];
          recordValue = [record objectForKey: currentMatch];
          result = NO;

          if ([recordValue isNotNull] && [recordValue caseInsensitiveMatches: currentValue])
            result = YES;
        }
    }

  [record setObject: [NSNumber numberWithBool: result]
             forKey: [NSString stringWithFormat: @"%@Access", module]];
}

- (id) connection
{
  EOAdaptorChannel *channel;
  GCSChannelManager *cm;

  cm = [GCSChannelManager defaultChannelManager];
  channel = [cm acquireOpenChannelForURL: _viewURL];

  return channel;
}

- (BOOL) isConnected
{
  return [self connection];
}

- (void) releaseConnection: (id) connection
{
  GCSChannelManager *cm;

  if (connection)
    {
      cm = [GCSChannelManager defaultChannelManager];
      [cm releaseChannel: connection];
    }
}

- (NSDictionary *) _lookupContactEntry: (NSString *) theID
                         considerEmail: (BOOL) b
                              inDomain: (NSString *) domain
		       usingConnection: (id) connection
{
  NSMutableDictionary *response;
  NSMutableArray *qualifiers;
  NSArray *fieldNames;
  EOAdaptorChannel *channel;
  EOQualifier *loginQualifier, *domainQualifier, *qualifier;
  NSMutableString *sql;
  NSString *value, *field;
  NSException *ex;
  int i;

  response = nil;

  theID = [theID stringByReplacingString: @"'"  withString: @"''"];
  channel = (EOAdaptorChannel *)connection;
  if (channel)
    {
      qualifiers = [NSMutableArray arrayWithCapacity: [_loginFields count] + 1];

      // Always compare against the c_uid field
      loginQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"c_uid"
                                               operatorSelector: EOQualifierOperatorEqual
                                                          value: theID];
      [loginQualifier autorelease];
      [qualifiers addObject: loginQualifier];

      if (_loginFields)
        {
          for (i = 0; i < [_loginFields count]; i++)
            {
              field = [_loginFields objectAtIndex: i];
              if ([field caseInsensitiveCompare: @"c_uid"] != NSOrderedSame)
                {
                  loginQualifier = [[EOKeyValueQualifier alloc] initWithKey: field
                                                           operatorSelector: EOQualifierOperatorEqual
                                                                      value: theID];
                  [loginQualifier autorelease];
                  [qualifiers addObject: loginQualifier];
                }
            }
        }

      domainQualifier = nil;
      if (_domainField && [domain length])
        domainQualifier = [self visibleDomainsQualifierFromDomain: domain];

      if (b)
        {
          // Always compare againts the mail field
          loginQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"mail"
                                                   operatorSelector: EOQualifierOperatorEqual
                                                              value: [theID lowercaseString]];
          [loginQualifier autorelease];
          [qualifiers addObject: loginQualifier];

          if (_mailFields)
            {
              for (i = 0; i < [_mailFields count]; i++)
                {
                  field = [_mailFields objectAtIndex: i];
                  if ([field caseInsensitiveCompare: @"mail"] != NSOrderedSame
                      && ![_loginFields containsObject: field])
                    {
                      loginQualifier = [[EOKeyValueQualifier alloc] initWithKey: field
                                                               operatorSelector: EOQualifierOperatorEqual
                                                                          value: [theID lowercaseString]];
                      [loginQualifier autorelease];
                      [qualifiers addObject: loginQualifier];
                    }
                }
            }
        }

      sql = [NSMutableString stringWithFormat: @"SELECT *"
                             @" FROM %@"
                             @" WHERE ",
                             [_viewURL gcsTableName]];
      qualifier = [[EOOrQualifier alloc] initWithQualifierArray: qualifiers];
      if (domainQualifier)
        qualifier = [[EOAndQualifier alloc] initWithQualifiers: domainQualifier, qualifier, nil];
      [qualifier appendSQLToString: sql];

      ex = [channel evaluateExpressionX: sql];
      if (!ex)
        {
          NSMutableArray *emails;

          response = [[channel fetchAttributes: [channel describeResults: NO]
                                      withZone: NULL] mutableCopy];
          [response autorelease];
          [channel cancelFetch];

          /* Convert all c_ fields to obtain their ldif equivalent */
          fieldNames = [response allKeys];
          for (i = 0; i < [fieldNames count]; i++)
            {
              field = [fieldNames objectAtIndex: i];
              if ([field hasPrefix: @"c_"])
                [response setObject: [response objectForKey: field]
                             forKey: [field substringFromIndex: 2]];
            }

          [self _fillConstraintsForModule: @"Calendar"    intoRecord: response];
          [self _fillConstraintsForModule: @"Mail"        intoRecord: response];
          [self _fillConstraintsForModule: @"ActiveSync"  intoRecord: response];

          // We set the domain, if any
          value = nil;
          if (_domain)
            value = _domain;
          else if (_domainField)
            value = [response objectForKey: _domainField];
          if (![value isNotNull])
            value = @"";
          [response setObject: value forKey: @"c_domain"];

          // We populate all mail fields
          emails = [NSMutableArray array];

          if ([response objectForKey: @"mail"])
            [emails addObject: [response objectForKey: @"mail"]];

          if (_mailFields && [_mailFields count] > 0)
            {
              NSString *s;
              int i;

              for (i = 0; i < [_mailFields count]; i++)
                if ((s = [response objectForKey: [_mailFields objectAtIndex: i]]) &&
                    [s isNotNull] &&
                    [[s stringByTrimmingSpaces] length] > 0)
                  [emails addObjectsFromArray: [s componentsSeparatedByString: @" "]];
            }

          [response setObject: [emails uniqueObjects]  forKey: @"c_emails"];
          if (_imapHostField)
            {
              value = [response objectForKey: _imapHostField];
              if ([value isNotNull])
                [response setObject: value forKey: @"c_imaphostname"];
            }

          if (_sieveHostField)
            {
              value = [response objectForKey: _sieveHostField];
              if ([value isNotNull])
                [response setObject: value forKey: @"c_sievehostname"];
            }

          // We check if the user can authenticate
          if (_authenticationFilter)
            {
              EOQualifier *q_uid, *q_auth;

              sql = [NSMutableString stringWithFormat: @"SELECT c_uid"
                                     @" FROM %@"
                                     @" WHERE ",
                                     [_viewURL gcsTableName]];

              q_auth = [EOQualifier qualifierWithQualifierFormat: _authenticationFilter];

              q_uid = [[EOKeyValueQualifier alloc] initWithKey: @"c_uid"
                                              operatorSelector: EOQualifierOperatorEqual
                                                         value: theID];
              [q_uid autorelease];

              qualifier = [[EOAndQualifier alloc] initWithQualifiers: q_uid, q_auth, nil];
              [qualifier autorelease];
              [qualifier appendSQLToString: sql];

              ex = [channel evaluateExpressionX: sql];
              if (!ex)
                {
                  NSDictionary *authResponse;

                  authResponse = [channel fetchAttributes: [channel describeResults: NO]  withZone: NULL];
                  [response setObject: [NSNumber numberWithBool: [authResponse count] > 0] forKey: @"canAuthenticate"];
                  [channel cancelFetch];
                }
              else
                [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex];
            }
          else
            [response setObject: [NSNumber numberWithBool: YES] forKey: @"canAuthenticate"];

          // We check if we should use a different login for IMAP
          if (_imapLoginField)
            {
              if ([[response objectForKey: _imapLoginField] isNotNull])
                [response setObject: [response objectForKey: _imapLoginField] forKey: @"c_imaplogin"];
            }

          // We check if it's a resource of not
          if (_kindField)
            {
              if ((value = [response objectForKey: _kindField]) && [value isNotNull])
                {
                  if ([value caseInsensitiveCompare: @"location"] == NSOrderedSame ||
                      [value caseInsensitiveCompare: @"thing"] == NSOrderedSame ||
                      [value caseInsensitiveCompare: @"group"] == NSOrderedSame)
                    {
                      [response setObject: [NSNumber numberWithInt: 1]
                                   forKey: @"isResource"];
                    }
                }
            }

          if (_multipleBookingsField)
            {
              if ((value = [response objectForKey: _multipleBookingsField]))
                {
                  [response setObject: [NSNumber numberWithInt: [value intValue]]
                               forKey: @"numberOfSimultaneousBookings"];
                }
            }

          [response setObject: self forKey: @"source"];
        }
      else
        [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex];
    }
  else
    [self errorWithFormat:@"failed to acquire channel for URL: %@",
          [_viewURL absoluteString]];

  return response;
}


- (NSDictionary *) _lookupContactEntry: (NSString *) theID
                         considerEmail: (BOOL) b
                              inDomain: (NSString *) domain
{
  EOAdaptorChannel *channel;
  NSDictionary *response;

  channel = (EOAdaptorChannel *)[self connection];

  response = [self _lookupContactEntry: theID
			 considerEmail: b
			      inDomain: domain
		       usingConnection: channel];

  [self releaseConnection: channel];

  return response;
}

- (NSDictionary *) lookupContactEntry: (NSString *) theID
			     inDomain: (NSString *) domain
		      usingConnection: (id) connection
{
  return [self _lookupContactEntry: theID
		     considerEmail: NO
			  inDomain: domain
		   usingConnection: connection];
}

- (NSDictionary *) lookupContactEntry: (NSString *) theID
			     inDomain: (NSString *) domain
{
  return [self _lookupContactEntry: theID
		     considerEmail: NO
			  inDomain: domain];
}

- (NSDictionary *) lookupContactEntryWithUIDorEmail: (NSString *) entryID
                                           inDomain: (NSString *) domain
{
  return [self _lookupContactEntry: entryID  considerEmail: YES inDomain: domain];
}

- (void) addVCardProperty: (NSString *) property
               toCriteria: (NSMutableArray *) criteria
{
  static NSDictionary *vCardSQLFieldsTable = nil;
  NSString *field;

  if (!vCardSQLFieldsTable)
    vCardSQLFieldsTable = [[NSDictionary alloc] initWithObjectsAndKeys:
                                                  @"c_cn",            @"fn",
                                                @"c_cn",              @"n",
                                                @"mail",              @"email",
                                                @"mail",              @"mail",
                                                @"c_telephonenumber", @"tel",
                                                @"c_o",               @"org",
                                                @"c_l",               @"adr",
                                                  nil];

  field = [vCardSQLFieldsTable objectForKey: property];
  if (field)
    {
      [criteria addObjectUniquely: field];
    }
}

/* Returns an EOQualifier of the following form:
 * (_domainField = domain OR _domainField = visibleDomain1 [...])
 * Should only be called on SQL sources using _domainField name.
 */
- (EOQualifier *) visibleDomainsQualifierFromDomain: (NSString *) domain
{
  int i;
  EOQualifier *qualifier, *domainQualifier;
  NSArray *visibleDomains;
  NSMutableArray *qualifiers;
  NSString *currentDomain;

  SOGoSystemDefaults *sd;

  /* Return early if no domain or if being called on a 'static' sql source */
  if (!domain || !_domainField)
    return nil;

  sd = [SOGoSystemDefaults sharedSystemDefaults];
  visibleDomains = [sd visibleDomainsForDomain: domain];
  qualifier = nil;

  domainQualifier = [[EOKeyValueQualifier alloc] initWithKey: _domainField
                                            operatorSelector: EOQualifierOperatorEqual
                                                       value: domain];
  [domainQualifier autorelease];

  if ([visibleDomains count])
    {
      qualifiers = [NSMutableArray arrayWithCapacity: [visibleDomains count] + 1];
      [qualifiers addObject: domainQualifier];
      for(i = 0; i < [visibleDomains count]; i++)
        {
          currentDomain = [visibleDomains objectAtIndex: i];
          qualifier =
            [[EOKeyValueQualifier alloc] initWithKey: _domainField
                                    operatorSelector: EOQualifierOperatorEqual
                                               value: currentDomain];
          [qualifier autorelease];
          [qualifiers addObject: qualifier];
        }
      qualifier = [[EOOrQualifier alloc] initWithQualifierArray: qualifiers];
      [qualifier autorelease];
    }

  return qualifier ? qualifier : domainQualifier;
}


- (NSArray *) allEntryIDsVisibleFromDomain: (NSString *) domain
{
  EOAdaptorChannel *channel;
  EOQualifier *domainQualifier;
  GCSChannelManager *cm;
  NSException *ex;
  NSMutableArray *results;
  NSMutableString *sql;

  results = [NSMutableArray array];

  cm = [GCSChannelManager defaultChannelManager];
  channel = [cm acquireOpenChannelForURL: _viewURL];
  if (channel)
    {
      sql = [NSMutableString stringWithFormat: @"SELECT c_uid FROM %@",
                             [_viewURL gcsTableName]];

      if (_domainField)
        {
          if ([domain length])
            {
              domainQualifier = [self visibleDomainsQualifierFromDomain: domain];
              if (domainQualifier)
                {
                  [sql appendString: @" WHERE "];
                  [domainQualifier appendSQLToString: sql];
                }
            }
          else
            {
              /* Should not happen but avoid returning the whole table
               * if a domain should have been defined */
              [sql appendFormat: @" WHERE %@ is NULL", _domainField];
            }
        }

      ex = [channel evaluateExpressionX: sql];
      if (!ex)
        {
          NSDictionary *row;
          NSArray *attrs;
          NSString *value;

          attrs = [channel describeResults: NO];

          while ((row = [channel fetchAttributes: attrs withZone: NULL]))
            {
              value = [row objectForKey: @"c_uid"];
              if (value)
                [results addObject: value];
            }
        }
      else
        [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex];
      [cm releaseChannel: channel];
    }
  else
    [self errorWithFormat:@"failed to acquire channel for URL: %@",
          [_viewURL absoluteString]];


  return results;
}

- (NSArray *) allEntryIDs
{
  return [self allEntryIDsVisibleFromDomain: nil];
}

- (NSArray *) fetchContactsMatching: (NSString *) filter
                       withCriteria: (NSArray *) criteria
                           inDomain: (NSString *) domain
                           
{
  if ([filter length] > 0) {
    return [self fetchContactsMatching: (NSString *) filter
                       withCriteria: (NSArray *) criteria
                           inDomain: (NSString *) domain
                              limit: -1];
  } else {
    return [NSMutableArray array];
  }
}

- (NSArray *)fetchContactsMatching: (NSString *)filter
                      withCriteria: (NSArray *)criteria
                          inDomain: (NSString *)domain
                             limit: (int)limit
{
  EOAdaptor *adaptor;
  EOAdaptorChannel *channel;
  EOQualifier *qualifier;
  NSEnumerator *criteriaList;
  NSMutableArray *fields, *results;
  GCSChannelManager *cm;
  NSException *ex;
  NSMutableString *sql;
  NSString *lowerFilter, *filterFormat, *currentCriteria, *qs;

  results = [NSMutableArray array];

  if ([filter length] > 0 || !_listRequiresDot)
    {
      cm = [GCSChannelManager defaultChannelManager];
      channel = [cm acquireOpenChannelForURL: _viewURL];
      if (channel)
        {
          EOAdaptorContext *adaptorCtx;
          adaptorCtx = [channel adaptorContext];
          adaptor = [adaptorCtx adaptor];
          fields = [NSMutableArray array];
          if ([filter length])
            {
              filter = [[filter asSafeSQLString] stringByReplacingString: @"\%" withString: @"%%"];
              filterFormat = [NSString stringWithFormat: @"(%%@ isCaseInsensitiveLike: '*%@*')", filter];
              if (criteria)
                criteriaList = [criteria objectEnumerator];
              else
                criteriaList = [[self searchFields] objectEnumerator];

              while (( currentCriteria = [criteriaList nextObject] ))
                {
                  if ([currentCriteria isEqualToString: @"mail"])
                    {
                      // Expand to all mail fields
                      [fields addObject: currentCriteria];
                      if (_mailFields)
                        [fields addObjectsFromArray: _mailFields];
                    }
                  else if ([[self searchFields] containsObject: currentCriteria])
                    [fields addObject: currentCriteria];
                }
            }

          sql = [NSMutableString stringWithFormat: @"SELECT * FROM %@ WHERE (", [_viewURL gcsTableName]];

          if ([fields count])
            {
              qs = [[[fields uniqueObjects] stringsWithFormat: filterFormat] componentsJoinedByString: @" OR "];
              qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
              [qualifier appendSQLToString: sql
                             withAdaptor: adaptor];
            }
          else
            [sql appendString: @"1 = 1"];
          [sql appendString: @")"];

          if (_domainField)
            {
              if ([domain length])
                {
                  EOQualifier *domainQualifier;
                  domainQualifier = [self visibleDomainsQualifierFromDomain: domain];
                  if (domainQualifier)
                    {
                      [sql appendFormat: @" AND ("];
                      [domainQualifier appendSQLToString: sql];
                      [sql appendFormat: @")"];
                    }
                }
              else
                [sql appendFormat: @" AND %@ IS NULL", _domainField];
            }

          if (limit > 0)
            [sql appendFormat: @" ORDER BY c_cn ASC LIMIT %i", limit];
          ex = [channel evaluateExpressionX: sql];
          if (!ex)
            {
              NSDictionary *row;
              NSMutableDictionary *mutableRow;
              NSArray *attrs;

              attrs = [channel describeResults: NO];

              while ((row = [channel fetchAttributes: attrs withZone: NULL]))
                {
                  mutableRow = [row mutableCopy];
                  [mutableRow setObject: self forKey: @"source"];
                  [results addObject: mutableRow];
                  [mutableRow release];
                }
            }
          else
            [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex];
          [cm releaseChannel: channel];
        }
      else
        [self errorWithFormat:@"failed to acquire channel for URL: %@",
              [_viewURL absoluteString]];
    }

  return results;
}


/**
 * @see [SOGoContactFolder lookupContactsWithQualifier:andSortOrdering:indDomain:]
 * @see [SOGoContactSourceFolder lookupContactsWithQualifier:andSortOrdering:indDomain:]
 * @see [SOGoFolder(CardDAV) _appendComponentsProperties:matchingQualifier:toResponse:context:]
 */
- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier
                          andSortOrdering: (EOSortOrdering *) ordering
                                 inDomain: (NSString *) domain
{
  EOAdaptor *adaptor;
  NSException *ex;
  NSMutableArray *results;
  NSMutableString *sql;
  EOAdaptorChannel *channel;
  GCSChannelManager *cm;

  results = [NSMutableArray array];

  if (qualifier || !_listRequiresDot)
    {
      cm = [GCSChannelManager defaultChannelManager];
      channel = [cm acquireOpenChannelForURL: _viewURL];
      if (channel)
        {
          EOAdaptorContext *adaptorCtx;
          adaptorCtx = [channel adaptorContext];
          adaptor = [adaptorCtx adaptor];
          sql = [NSMutableString stringWithFormat: @"SELECT c_name FROM %@ WHERE (", [_viewURL gcsTableName]];

          if (qualifier)
            [qualifier appendSQLToString: sql
                             withAdaptor: adaptor];
          else
            [sql appendString: @"1 = 1"];
          [sql appendString: @")"];

          if (_domainField)
            {
              if ([domain length])
                {
                  EOQualifier *domainQualifier;
                  domainQualifier = [self visibleDomainsQualifierFromDomain: domain];
                  if (domainQualifier)
                    {
                      [sql appendFormat: @" AND ("];
                      [domainQualifier appendSQLToString: sql];
                      [sql appendFormat: @")"];
                    }
                }
              else
                [sql appendFormat: @" AND %@ IS NULL", _domainField];
            }

          ex = [channel evaluateExpressionX: sql];
          if (!ex)
            {
              NSDictionary *row;
              NSMutableDictionary *mutableRow;
              NSArray *attrs;

              attrs = [channel describeResults: NO];

              while ((row = [channel fetchAttributes: attrs withZone: NULL]))
                {
                  mutableRow = [row mutableCopy];
                  // [mutableRow setObject: self forKey: @"source"];
                  [results addObject: mutableRow];
                  [mutableRow release];
                }
            }
          else
            [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex];
          [cm releaseChannel: channel];
        }
      [self errorWithFormat:@"failed to acquire channel for URL: %@",
            [_viewURL absoluteString]];
    }

  return results;
}

- (void) setSourceID: (NSString *) newSourceID
{
}

- (NSString *) sourceID
{
  return _sourceID;
}

- (void) setDisplayName: (NSString *) newDisplayName
{
}

- (NSString *) displayName
{
  /* This method is only used when supporting user "source" addressbooks,
     which is only supported by the LDAP backend for now. */
  return _sourceID;
}

- (void) setListRequiresDot: (BOOL) newListRequiresDot
{
  _listRequiresDot = newListRequiresDot;
}

- (BOOL) listRequiresDot
{
  return _listRequiresDot;
}

- (void) setGlobalAddressBookFirstEntriesCount: (int)value
{
  _globalAddressBookFirstEntriesCount = value;
}

- (int) globalAddressBookFirstEntriesCount
{
  return _globalAddressBookFirstEntriesCount;
}

/* card editing */
- (void) setModifiers: (NSArray *) newModifiers
{
}

- (NSArray *) modifiers
{
  /* This method is only used when supporting card editing,
     which is only supported by the LDAP backend for now. */
  return nil;
}

- (NSException *) addContactEntry: (NSDictionary *) roLdifRecord
                           withID: (NSString *) aId
{
  NSString *reason;

  reason = [NSString stringWithFormat: @"method '%@' is not available"
                     @" for class '%@'", NSStringFromSelector (_cmd),
                     NSStringFromClass (object_getClass(self))];

  return [NSException exceptionWithName: @"SQLSourceIOException"
                                 reason: reason
                               userInfo: nil];
}

- (NSException *) updateContactEntry: (NSDictionary *) roLdifRecord
{
  NSString *reason;

  reason = [NSString stringWithFormat: @"method '%@' is not available"
                     @" for class '%@'", NSStringFromSelector (_cmd),
                     NSStringFromClass (object_getClass(self))];

  return [NSException exceptionWithName: @"SQLSourceIOException"
                                 reason: reason
                               userInfo: nil];
}

- (NSException *) removeContactEntryWithID: (NSString *) aId
{
  NSString *reason;

  reason = [NSString stringWithFormat: @"method '%@' is not available"
                     @" for class '%@'", NSStringFromSelector (_cmd),
                     NSStringFromClass (object_getClass(self))];

  return [NSException exceptionWithName: @"SQLSourceIOException"
                                 reason: reason
                               userInfo: nil];
}

/* user addressbooks */
- (BOOL) hasUserAddressBooks
{
  return NO;
}

- (NSArray *) addressBookSourcesForUser: (NSString *) user
{
  return nil;
}

- (NSException *) addAddressBookSource: (NSString *) newId
                       withDisplayName: (NSString *) newDisplayName
                               forUser: (NSString *) user
{
  NSString *reason;

  reason = [NSString stringWithFormat: @"method '%@' is not available"
                     @" for class '%@'", NSStringFromSelector (_cmd),
                     NSStringFromClass (object_getClass(self))];

  return [NSException exceptionWithName: @"SQLSourceIOException"
                                 reason: reason
                               userInfo: nil];
}

- (NSException *) renameAddressBookSource: (NSString *) newId
                          withDisplayName: (NSString *) newDisplayName
                                  forUser: (NSString *) user
{
  NSString *reason;

  reason = [NSString stringWithFormat: @"method '%@' is not available"
                     @" for class '%@'", NSStringFromSelector (_cmd),
                     NSStringFromClass (object_getClass(self))];

  return [NSException exceptionWithName: @"SQLSourceIOException"
                                 reason: reason
                               userInfo: nil];
}

- (NSException *) removeAddressBookSource: (NSString *) newId
                                  forUser: (NSString *) user
{
  NSString *reason;

  reason = [NSString stringWithFormat: @"method '%@' is not available"
                     @" for class '%@'", NSStringFromSelector (_cmd),
                     NSStringFromClass (object_getClass(self))];

  return [NSException exceptionWithName: @"SQLSourceIOException"
                                 reason: reason
                               userInfo: nil];
}

@end
