/* MAPIStoreGCSFolder.m - this file is part of SOGo
 *
 * Copyright (C) 2011-2012 Inverse inc
 *
 * Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
 *
 * 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 3, 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/NSCalendarDate.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSException.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSObject+Values.h>
#import <EOControl/EOQualifier.h>
#import <EOControl/EOFetchSpecification.h>
#import <EOControl/EOSortOrdering.h>
#import <GDLContentStore/GCSFolder.h>
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/SOGoGCSFolder.h>
#import <SOGo/SOGoParentFolder.h>
#import <SOGo/SOGoPermissions.h>
#import <SOGo/SOGoUser.h>

#import "MAPIStoreGCSBaseContext.h"
#import "MAPIStoreTypes.h"
#import "MAPIStoreUserContext.h"
#import "NSData+MAPIStore.h"
#import "NSDate+MAPIStore.h"
#import "NSString+MAPIStore.h"
#import "SOGoMAPIDBMessage.h"

#import "MAPIStoreGCSFolder.h"

#undef DEBUG
#include <mapistore/mapistore.h>
#include <mapistore/mapistore_errors.h>

static Class NSNumberK;

@implementation MAPIStoreGCSFolder

+ (void) initialize
{
  NSNumberK = [NSNumber class];
}

- (id) initWithSOGoObject: (id) newSOGoObject
              inContainer: (MAPIStoreObject *) newContainer
{
  if ((self = [super initWithSOGoObject: newSOGoObject inContainer: newContainer]))
    {
      activeUserRoles = nil;
    }

  return self;
}

- (void) setupVersionsMessage
{
  ASSIGN (versionsMessage,
          [SOGoMAPIDBMessage objectWithName: @"versions.plist"
                                inContainer: dbFolder]);
  [versionsMessage setObjectType: MAPIInternalCacheObject];
  [versionsMessage reloadIfNeeded];
}

- (void) dealloc
{
  [versionsMessage release];
  [activeUserRoles release];
  [componentQualifier release];
  [super dealloc];
}

- (enum mapistore_error) deleteFolder
{
  enum mapistore_error rc;
  NSException *error;
  NSString *name;

  name = [self nameInContainer];
  if ([name isEqualToString: @"personal"])
    rc = MAPISTORE_ERR_DENIED;
  else
    {
      [[sogoObject container] removeSubFolder: name];
      error = [(SOGoGCSFolder *) sogoObject delete];
      if (error)
        rc = MAPISTORE_ERROR;
      else
        {
          if (![versionsMessage delete])
            rc = MAPISTORE_SUCCESS;
          else
            rc = MAPISTORE_ERROR;
        }
    }

  return (rc == MAPISTORE_SUCCESS) ? [super deleteFolder] : rc;
}

- (void) setDisplayName: (NSString *) newDisplayName
{
  NSString *suffix, *fullSuffix;
  Class cClass;

  cClass = [(MAPIStoreGCSBaseContext *) [self context] class];

  /* if a suffix exists, we strip it from the final name */
  suffix = [cClass folderNameSuffix];
  if ([suffix length] > 0)
    {
      fullSuffix = [NSString stringWithFormat: @"(%@)", suffix];
      if ([newDisplayName hasSuffix: fullSuffix])
        {
          newDisplayName = [newDisplayName substringToIndex:
                                             [newDisplayName length]
                                           - [fullSuffix length]];
          newDisplayName = [newDisplayName stringByTrimmingSpaces];
        }
    }

  if (![[sogoObject displayName] isEqualToString: newDisplayName])
    [sogoObject renameTo: newDisplayName];
}

- (enum mapistore_error) getPidTagDisplayName: (void **) data
                                     inMemCtx: (TALLOC_CTX *) memCtx
{
  NSString *displayName;
  Class cClass;

  cClass = [(MAPIStoreGCSBaseContext *) [self context] class];
  displayName = [cClass getFolderDisplayName: [sogoObject displayName]];
  *data = [displayName asUnicodeInMemCtx: memCtx];

  return MAPISTORE_SUCCESS;
}

- (void) addProperties: (NSDictionary *) newProperties
{
  NSString *newDisplayName;
  NSMutableDictionary *propsCopy;
  NSNumber *key;

  key = MAPIPropertyKey (PR_DISPLAY_NAME_UNICODE);
  newDisplayName = [newProperties objectForKey: key];
  if (newDisplayName)
    {
      [self setDisplayName: newDisplayName];
      propsCopy = [newProperties mutableCopy];
      [propsCopy removeObjectForKey: key];
      [propsCopy autorelease];
      newProperties = propsCopy;
    }

  [super addProperties: newProperties];
}

- (NSArray *) messageKeysMatchingQualifier: (EOQualifier *) qualifier
                          andSortOrderings: (NSArray *) sortOrderings
{
  static NSArray *fields = nil;
  SOGoUser *ownerUser;
  NSArray *records;
  NSMutableArray *qualifierArray;
  EOQualifier *fetchQualifier, *aclQualifier;
  GCSFolder *ocsFolder;
  EOFetchSpecification *fs;
  NSArray *keys;

  if (!fields)
    fields = [[NSArray alloc]
	       initWithObjects: @"c_name", @"c_version", nil];

  qualifierArray = [NSMutableArray new];
  ownerUser = [[self userContext] sogoUser];
  if (![[context activeUser] isEqual: ownerUser])
    {
      aclQualifier = [self aclQualifier];
      if (aclQualifier)
        [qualifierArray addObject: aclQualifier];
    }
  [qualifierArray addObject: [self componentQualifier]];
  if (qualifier)
    [qualifierArray addObject: qualifier];

  fetchQualifier = [[EOAndQualifier alloc]
                     initWithQualifierArray: qualifierArray];

  ocsFolder = [sogoObject ocsFolder];
  fs = [EOFetchSpecification
         fetchSpecificationWithEntityName: [ocsFolder folderName]
                                qualifier: fetchQualifier
                            sortOrderings: sortOrderings];
  [fetchQualifier release];
  [qualifierArray release];
  records = [ocsFolder fetchFields: fields fetchSpecification: fs];
  keys = [records objectsForKey: @"c_name"
                 notFoundMarker: nil];

  return keys;
}

- (NSDate *) lastMessageModificationTime
{
  NSDate *value;
  NSNumber *ti;

  [self synchroniseCache];

  ti = [[versionsMessage properties]
         objectForKey: @"SyncLastModificationDate"];
  if (ti)
    value = [NSDate dateWithTimeIntervalSince1970: [ti doubleValue]];
  else
    value = nil;

  return value;
}

- (SOGoFolder *) aclFolder
{
  return (SOGoFolder *) sogoObject;
}

/* synchronisation */

/* Tree
{
  SyncLastModseq = x;
  SyncLastSynchronisationDate = x; ** not updated until something changed
  Messages = {
    MessageKey = {
      Version = x;
      Modseq = x;
      Deleted = b;
      ChangeKey = d;
      PredecessorChangeList = { guid1 = globcnt1, guid2 ... };
    };
    ...
  };
  VersionMapping = {
    Version = last-modified;
    ...
  }
}
 */
- (void) _setChangeKey: (NSData *) changeKey
       forMessageEntry: (NSMutableDictionary *) messageEntry
{
  struct XID *xid;
  NSString *guid;
  NSData *globCnt;
  NSDictionary *changeKeyDict;
  NSMutableDictionary *changeList;

  xid = [changeKey asXIDInMemCtx: NULL];
  guid = [NSString stringWithGUID: &xid->NameSpaceGuid];
  globCnt = [NSData dataWithBytes: xid->LocalId.data length: xid->LocalId.length];
  talloc_free (xid);

  /* 1. set change key association */
  changeKeyDict = [NSDictionary dictionaryWithObjectsAndKeys: guid, @"GUID",
                                globCnt, @"LocalId",
                                nil];
  [messageEntry setObject: changeKeyDict forKey: @"ChangeKey"];

  /* 2. append/update predecessor change list */
  changeList = [messageEntry objectForKey: @"PredecessorChangeList"];
  if (!changeList)
    {
      changeList = [NSMutableDictionary new];
      [messageEntry setObject: changeList
                       forKey: @"PredecessorChangeList"];
      [changeList release];
    }
  [changeList setObject: globCnt forKey: guid];
}

- (void) _updatePredecessorChangeList: (NSData *) predecessorChangeList
                      forMessageEntry: (NSMutableDictionary *) messageEntry
                     withOldChangeKey: (NSData *) oldChangeKey
{
  NSData *globCnt, *oldGlobCnt;
  NSDictionary *changeKeyDict;
  NSString *guid;
  NSMutableDictionary *changeList;
  struct SizedXid *sizedXIDList;
  struct XID xid, *givenChangeKey;
  TALLOC_CTX *localMemCtx;
  uint32_t i, length;

  localMemCtx = talloc_new (NULL);
  if (!localMemCtx)
    {
      [self errorWithFormat: @"No more memory"];
      return;
    }

  if (predecessorChangeList)
    {
      sizedXIDList = [predecessorChangeList asSizedXidArrayInMemCtx: localMemCtx with: &length];

      changeList = [messageEntry objectForKey: @"PredecessorChangeList"];
      if (!changeList)
        {
          changeList = [NSMutableDictionary new];
          [messageEntry setObject: changeList
                           forKey: @"PredecessorChangeList"];
          [changeList release];
        }

      if (sizedXIDList) {
        for (i = 0; i < length; i++)
          {
            xid = sizedXIDList[i].XID;
            guid = [NSString stringWithGUID: &xid.NameSpaceGuid];
            globCnt = [NSData dataWithBytes: xid.LocalId.data length: xid.LocalId.length];
            oldGlobCnt = [changeList objectForKey: guid];
            if (!oldGlobCnt || ([globCnt compare: oldGlobCnt] == NSOrderedDescending))
              [changeList setObject: globCnt forKey: guid];
          }
      }
    }

  if (oldChangeKey)
    {
      givenChangeKey = [oldChangeKey asXIDInMemCtx: localMemCtx];
      if (givenChangeKey) {
        guid = [NSString stringWithGUID: &givenChangeKey->NameSpaceGuid];
        globCnt = [NSData dataWithBytes: givenChangeKey->LocalId.data length: givenChangeKey->LocalId.length];

        changeKeyDict = [messageEntry objectForKey: @"ChangeKey"];
        if (!changeKeyDict ||
            ([guid isEqualToString: [changeKeyDict objectForKey: @"GUID"]]
             && ([globCnt compare: [changeKeyDict objectForKey: @"LocalId"]] == NSOrderedDescending)))
          {
            /* The given change key is greater than current one stored in
               metadata or it does not exist */
            [messageEntry setObject: [NSDictionary dictionaryWithObjectsAndKeys: guid, @"GUID",
                                                   globCnt, @"LocalId",
                                                   nil]
                             forKey: @"ChangeKey"];
          }
      }
    }

  talloc_free (localMemCtx);
}

- (EOQualifier *) componentQualifier
{
  if (!componentQualifier)
    componentQualifier
      = [[EOKeyValueQualifier alloc] initWithKey: @"c_component"
				operatorSelector: EOQualifierOperatorEqual
					   value: [self component]];

  return componentQualifier;
}

- (EOQualifier *) contentComponentQualifier
{
  EOQualifier *contentComponentQualifier;
  NSString *likeString;
  
  likeString = [NSString stringWithFormat: @"%%BEGIN:%@%%",
                         [[self component] uppercaseString]];
  contentComponentQualifier = [[EOKeyValueQualifier alloc]
                                initWithKey: @"c_content"
                           operatorSelector: EOQualifierOperatorLike
                                      value: likeString];
  [contentComponentQualifier autorelease];

  return contentComponentQualifier;
}

- (BOOL) synchroniseCache
{
  BOOL rc = YES;
  uint64_t newChangeNum;
  NSData *changeKey;
  NSString *cName, *changeNumber;
  NSNumber *ti, *lastModificationDate, *cVersion, *cLastModified, *cDeleted;
  EOFetchSpecification *fs;
  EOQualifier *searchQualifier, *fetchQualifier;
  NSUInteger count, max;
  NSArray *fetchResults, *changeNumbers;
  NSMutableArray *keys, *modifiedEntries;
  NSDictionary *result;
  NSMutableDictionary *currentProperties, *messages, *mapping, *messageEntry;
  NSCalendarDate *now;
  GCSFolder *ocsFolder;
  static NSArray *fields = nil;
  static EOSortOrdering *sortOrdering = nil;

  /* NOTE: we are using NSString instance for "changeNumber" because
     NSNumber proved to give very bad performances when used as NSDictionary
     keys with GNUstep 1.22.1. The bug seems to be solved with 1.24 but many
     distros still ship an older version. */

  if (!fields)
    fields = [[NSArray alloc]
	       initWithObjects: @"c_name", @"c_version", @"c_lastmodified",
               @"c_deleted", nil];

  if (!sortOrdering)
    {
      sortOrdering = [EOSortOrdering sortOrderingWithKey: @"c_lastmodified"
                                                selector: EOCompareAscending];
      [sortOrdering retain];
    }

  [versionsMessage reloadIfNeeded];
  currentProperties = [versionsMessage properties];

  lastModificationDate = [currentProperties objectForKey: @"SyncLastModificationDate"];
  if (lastModificationDate)
    {
      searchQualifier = [[EOKeyValueQualifier alloc]
                                initWithKey: @"c_lastmodified"
                           operatorSelector: EOQualifierOperatorGreaterThanOrEqualTo
                                      value: lastModificationDate];
      fetchQualifier = [[EOAndQualifier alloc]
                         initWithQualifiers: searchQualifier,
                         [self contentComponentQualifier],
                         nil];
      [fetchQualifier autorelease];
      [searchQualifier release];
    }
  else
    fetchQualifier = [self componentQualifier];

  ocsFolder = [sogoObject ocsFolder];
  fs = [EOFetchSpecification
             fetchSpecificationWithEntityName: [ocsFolder folderName]
                                    qualifier: fetchQualifier
                                sortOrderings: [NSArray arrayWithObject: sortOrdering]];
  fetchResults = [ocsFolder fetchFields: fields
                     fetchSpecification: fs
                          ignoreDeleted: NO];
  max = [fetchResults count];
  if (max > 0)
    {
      messages = [currentProperties objectForKey: @"Messages"];
      if (!messages)
        {
          messages = [NSMutableDictionary new];
          [currentProperties setObject: messages forKey: @"Messages"];
          [messages release];
        }
      mapping = [currentProperties objectForKey: @"VersionMapping"];
      if (!mapping)
        {
          mapping = [NSMutableDictionary new];
          [currentProperties setObject: mapping forKey: @"VersionMapping"];
          [mapping release];
        }

      keys = [NSMutableArray arrayWithCapacity: max];
      modifiedEntries = [NSMutableArray arrayWithCapacity: max];
      for (count = 0; count < max; count++)
        {
          result = [fetchResults objectAtIndex: count];
          cName = [result objectForKey: @"c_name"];
          [keys addObject: cName];
          cDeleted = [result objectForKey: @"c_deleted"];
          if ([cDeleted isKindOfClass: NSNumberK] && [cDeleted intValue])
            cVersion = [NSNumber numberWithInt: -1];
          else
            cVersion = [result objectForKey: @"c_version"];
          cLastModified = [result objectForKey: @"c_lastmodified"];

          messageEntry = [messages objectForKey: cName];
          if (!messageEntry)
            {
              messageEntry = [NSMutableDictionary new];
              [messages setObject: messageEntry forKey: cName];
              [messageEntry release];
            }

          if (![[messageEntry objectForKey: @"c_version"]
                 isEqual: cVersion])
            {
              [sogoObject removeChildRecordWithName: cName];

              [modifiedEntries addObject: messageEntry];

              [messageEntry setObject: cLastModified forKey: @"c_lastmodified"];
              [messageEntry setObject: cVersion forKey: @"c_version"];

              if (!lastModificationDate
                  || ([lastModificationDate compare: cLastModified]
                      == NSOrderedAscending))
                lastModificationDate = cLastModified;
            }
        }

      /* make sure all returned objects have a corresponding mid */
      [self ensureIDsForChildKeys: keys];

      max = [modifiedEntries count];
      if (max > 0)
        {
          changeNumbers = [[self context] getNewChangeNumbers: max];
          for (count = 0; count < max; count++)
            {
              messageEntry = [modifiedEntries objectAtIndex: count];

              changeNumber = [changeNumbers objectAtIndex: count];
              cLastModified = [messageEntry objectForKey: @"c_lastmodified"];
              [mapping setObject: cLastModified forKey: changeNumber];
              [messageEntry setObject: changeNumber forKey: @"version"];

              newChangeNum = [changeNumber unsignedLongLongValue];
	      
	      // A GLOBCNT structure is a 6-byte global namespace counter,
	      // we strip the first 2 bytes. The first two bytes is the ReplicaId
              changeKey = [self getReplicaKeyFromGlobCnt: newChangeNum >> 16];
              [self _setChangeKey: changeKey forMessageEntry: messageEntry];
            }

          now = [NSCalendarDate date];
          ti = [NSNumber numberWithDouble: [now timeIntervalSince1970]];
          [currentProperties setObject: ti
                                forKey: @"SyncLastSynchronisationDate"];
          [currentProperties setObject: lastModificationDate
                                forKey: @"SyncLastModificationDate"];
          [versionsMessage save];
        }
    }

  return rc;
}

- (BOOL) synchroniseCacheFor: (NSString *) nameInContainer
{
  /* Try to synchronise old messages in versions.plist cache using an
     specific c_name. It returns a boolean indicating if the
     synchronisation was carried out succesfully.

     It should be used as last resort, keeping synchroniseCache as the
     main sync entry point. */

  uint64_t changeNumber;
  NSString *changeNumberStr;
  NSData *changeKey;
  NSNumber *cLastModified, *cDeleted, *cVersion;
  EOFetchSpecification *fs;
  EOQualifier *searchQualifier, *fetchQualifier;
  NSArray *fetchResults;
  NSDictionary *result;
  NSMutableDictionary *currentProperties, *messages, *mapping, *messageEntry;
  GCSFolder *ocsFolder;
  static NSArray *fields;

  [versionsMessage reloadIfNeeded];
  currentProperties = [versionsMessage properties];

  messages = [currentProperties objectForKey: @"Messages"];
  if (!messages)
    {
        messages = [NSMutableDictionary new];
        [currentProperties setObject: messages forKey: @"Messages"];
        [messages release];
    }

  messageEntry = [messages objectForKey: nameInContainer];
  if (!messageEntry)
    {
      /* Fetch the message by its name */
      if (!fields)
        fields = [[NSArray alloc]
                  initWithObjects: @"c_name", @"c_version", @"c_lastmodified",
                  @"c_deleted", nil];

      searchQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"c_name"
                                     operatorSelector: EOQualifierOperatorEqual
                                                value: nameInContainer];
      fetchQualifier = [[EOAndQualifier alloc]
                        initWithQualifiers: searchQualifier,
                        [self contentComponentQualifier],
                        nil];
      [fetchQualifier autorelease];
      [searchQualifier release];

      ocsFolder = [sogoObject ocsFolder];
      fs = [EOFetchSpecification
            fetchSpecificationWithEntityName: [ocsFolder folderName]
                                   qualifier: fetchQualifier
                               sortOrderings: nil];
      fetchResults = [ocsFolder fetchFields: fields
                         fetchSpecification: fs
                              ignoreDeleted: NO];

      if ([fetchResults count] == 1)
        {
          result = [fetchResults objectAtIndex: 0];
          cLastModified = [result objectForKey: @"c_lastmodified"];
          cDeleted = [result objectForKey: @"c_deleted"];
          if ([cDeleted isKindOfClass: NSNumberK] && [cDeleted intValue])
            cVersion = [NSNumber numberWithInt: -1];
          else
            cVersion = [result objectForKey: @"c_version"];

          changeNumber = [[self context] getNewChangeNumber];
          changeNumberStr = [NSString stringWithUnsignedLongLong: changeNumber];

          /* Create new message entry in Messages dict */
          messageEntry = [NSMutableDictionary new];
          [messages setObject: messageEntry forKey: nameInContainer];
          [messageEntry release];

          /* Store cLastModified, cVersion and the change number */
          [messageEntry setObject: cLastModified forKey: @"c_lastmodified"];
          [messageEntry setObject: cVersion forKey: @"c_version"];
          [messageEntry setObject: changeNumberStr forKey: @"version"];

          /* Store the change key */
          changeKey = [self getReplicaKeyFromGlobCnt: changeNumber >> 16];
          [self _setChangeKey: changeKey forMessageEntry: messageEntry];

          /* Store the changeNumber -> cLastModified mapping */
          mapping = [currentProperties objectForKey: @"VersionMapping"];
          if (!mapping)
            {
              mapping = [NSMutableDictionary new];
              [currentProperties setObject: mapping forKey: @"VersionMapping"];
              [mapping release];
            }
          [mapping setObject: cLastModified forKey: changeNumberStr];

          /* Save the message */
          [versionsMessage save];
          return YES;
        }
      else
          return NO;
    }

  /* If message entry exists, then synchroniseCache did its job */
  return YES;
}

- (void) updateVersionsForMessageWithKey: (NSString *) messageKey
                           withChangeKey: (NSData *) oldChangeKey
                andPredecessorChangeList: (NSData *) pcl
{
  NSMutableDictionary *messages, *messageEntry;

  [self synchroniseCache];
  if (oldChangeKey || pcl)
    {
      messages = [[versionsMessage properties] objectForKey: @"Messages"];
      messageEntry = [messages objectForKey: messageKey];
      if (!messageEntry)
        [NSException raise: @"MAPIStoreIOException"
                    format: @"no version record found for message '%@'",
                     messageKey];
      [self _updatePredecessorChangeList: pcl forMessageEntry: messageEntry
                        withOldChangeKey: oldChangeKey];
      [versionsMessage save];
    }
}
 
- (NSNumber *) lastModifiedFromMessageChangeNumber: (NSString *) changeNumber
{
  NSDictionary *mapping;
  NSNumber *lastModified;

  mapping = [[versionsMessage properties] objectForKey: @"VersionMapping"];
  lastModified = [mapping objectForKey: changeNumber];

  return lastModified;
}

- (NSString *) changeNumberForMessageWithKey: (NSString *) messageKey
{
  NSDictionary *messages;
  NSString *changeNumber;

  messages = [[versionsMessage properties] objectForKey: @"Messages"];
  changeNumber = [[messages objectForKey: messageKey]
                   objectForKey: @"version"];

  return changeNumber;
}

- (NSData *) changeKeyForMessageWithKey: (NSString *) messageKey
{
  NSDictionary *messages, *changeKeyDict;
  NSString *guid;
  NSData *globCnt, *changeKey = nil;

  messages = [[versionsMessage properties] objectForKey: @"Messages"];
  changeKeyDict = [[messages objectForKey: messageKey]
                    objectForKey: @"ChangeKey"];
  if (changeKeyDict)
    {
      guid = [changeKeyDict objectForKey: @"GUID"];
      globCnt = [changeKeyDict objectForKey: @"LocalId"];
      changeKey = [NSData dataWithChangeKeyGUID: guid andCnt: globCnt];
    }

  return changeKey;
}

- (NSData *) predecessorChangeListForMessageWithKey: (NSString *) messageKey
{
  NSMutableData *list = nil;
  NSDictionary *messages, *changeListDict;
  NSArray *keys;
  NSMutableArray *changeKeys;
  NSUInteger count, max;
  NSData *changeKey;
  NSString *guid;
  NSData *globCnt;

  messages = [[versionsMessage properties] objectForKey: @"Messages"];
  changeListDict = [[messages objectForKey: messageKey]
                     objectForKey: @"PredecessorChangeList"];
  if (changeListDict)
    {
      keys = [changeListDict allKeys];
      max = [keys count];

      changeKeys = [NSMutableArray arrayWithCapacity: max];
      for (count = 0; count < max; count++)
        {
          guid = [keys objectAtIndex: count];
          globCnt = [changeListDict objectForKey: guid];
          changeKey = [NSData dataWithChangeKeyGUID: guid andCnt: globCnt];
          [changeKeys addObject: changeKey];
        }
      [changeKeys sortUsingFunction: MAPIChangeKeyGUIDCompare
                            context: nil];

      list = [NSMutableData data];
      for (count = 0; count < max; count++)
        {
          changeKey = [changeKeys objectAtIndex: count];
          [list appendUInt8: [changeKey length]];
          [list appendData: changeKey];
        }
    }

  return list;
}

- (NSArray *) getDeletedKeysFromChangeNumber: (uint64_t) changeNum
                                       andCN: (NSNumber **) cnNbr
                                 inTableType: (uint8_t) tableType
{
  NSArray *deletedKeys, *deletedCNames, *records;
  NSNumber *lastModified;
  NSString *cName, *changeNumber;
  NSDictionary *versionProperties, *messageEntry;
  NSMutableDictionary *messages;
  uint64_t maxChangeNum = changeNum, currentChangeNum;
  EOAndQualifier *fetchQualifier;
  EOKeyValueQualifier *cDeletedQualifier, *cLastModifiedQualifier;
  EOFetchSpecification *fs;
  GCSFolder *ocsFolder;
  NSUInteger count, max;

  if (tableType == MAPISTORE_MESSAGE_TABLE)
    {
      deletedKeys = [NSMutableArray array];

      changeNumber = [NSString stringWithUnsignedLongLong: changeNum];
      lastModified = [self lastModifiedFromMessageChangeNumber: changeNumber];
      if (lastModified)
        {
          versionProperties = [versionsMessage properties];
          messages = [versionProperties objectForKey: @"Messages"];

          ocsFolder = [sogoObject ocsFolder];
          cLastModifiedQualifier = [[EOKeyValueQualifier alloc]
                                           initWithKey: @"c_lastmodified"
                                      operatorSelector: EOQualifierOperatorGreaterThanOrEqualTo
                                                 value: lastModified];
          cDeletedQualifier = [[EOKeyValueQualifier alloc]
                                           initWithKey: @"c_deleted"
                                      operatorSelector: EOQualifierOperatorEqual
                                                 value: [NSNumber numberWithInt: 1]];
          fetchQualifier = [[EOAndQualifier alloc] initWithQualifiers:
                                                     cLastModifiedQualifier,
                                                   cDeletedQualifier,
                                                   nil];
          [fetchQualifier autorelease];
          [cLastModifiedQualifier release];
          [cDeletedQualifier release];

          fs = [EOFetchSpecification
                 fetchSpecificationWithEntityName: [ocsFolder folderName]
                                        qualifier: fetchQualifier
                                    sortOrderings: nil];
          records = [ocsFolder
                               fetchFields: [NSArray arrayWithObject: @"c_name"]
                        fetchSpecification: fs
                             ignoreDeleted: NO];
          deletedCNames = [records objectsForKey: @"c_name" notFoundMarker: nil];
          max = [deletedCNames count];
          for (count = 0; count < max; count++)
            {
              cName = [deletedCNames objectAtIndex: count];
              [sogoObject removeChildRecordWithName: cName];
              messageEntry = [messages objectForKey: cName];
              if (messageEntry)
                {
                  currentChangeNum
                    = [[messageEntry objectForKey: @"version"]
                        unsignedLongLongValue];
                  if (MAPICNCompare (changeNum, currentChangeNum, NULL)
                      == NSOrderedAscending)
                    {
                      [(NSMutableArray *) deletedKeys addObject: cName];
                      if (MAPICNCompare (maxChangeNum, currentChangeNum, NULL)
                          == NSOrderedAscending)
                        maxChangeNum = currentChangeNum;
                    }
                }
            }
          if (maxChangeNum != changeNum)
            *cnNbr = [NSNumber numberWithUnsignedLongLong: maxChangeNum];
        }
    }
  else
    deletedKeys = [super getDeletedKeysFromChangeNumber: changeNum
                                                  andCN: cnNbr
                                            inTableType: tableType];

  return deletedKeys;
}

- (NSArray *) activeUserRoles
{
  SOGoUser *activeUser;
  WOContext *woContext;

  if (!activeUserRoles)
    {
      activeUser = [[self context] activeUser];
      woContext = [[self userContext] woContext];
      activeUserRoles = [activeUser rolesForObject: sogoObject
                                         inContext: woContext];
      activeUserRoles = [self expandRoles: activeUserRoles];
      [activeUserRoles retain];
    }

  return activeUserRoles;
}

- (BOOL) subscriberCanCreateMessages
{
  return [[self activeUserRoles] containsObject: SOGoRole_ObjectCreator];
}

- (BOOL) subscriberCanDeleteMessages
{
  return [[self activeUserRoles] containsObject: SOGoRole_ObjectEraser];
}

/* subclasses */

- (EOQualifier *) aclQualifier
{
  return nil;
}

- (NSString *) component
{
  [self subclassResponsibility: _cmd];

  return nil;
}

@end
