/* SOGoCalendarComponent.m - this file is part of SOGo
 *
 * Copyright (C) 2006-2022 Inverse inc.
 *
 * 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/NSAutoreleasePool.h>
#import <Foundation/NSValue.h>

#import <NGObjWeb/NSException+HTTP.h>
#import <NGObjWeb/SoSecurityManager.h>
#import <NGObjWeb/WOApplication.h>
#import <NGObjWeb/WOContext+SoObjects.h>
#import <NGObjWeb/WORequest+So.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NGHashMap.h>
#import <NGExtensions/NGQuotedPrintableCoding.h>
#import <NGCards/iCalCalendar.h>
#import <NGCards/iCalDateTime.h>
#import <NGCards/iCalEvent.h>
#import <NGCards/iCalToDo.h>
#import <NGMime/NGMimeBodyPart.h>
#import <NGMime/NGMimeMultipartBody.h>
#import <NGMail/NGMimeMessage.h>
#import <GDLContentStore/GCSFolder.h>

#import <SOGo/NSCalendarDate+SOGo.h>
#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSObject+DAV.h>
#import <SOGo/NSObject+Utilities.h>
#import <SOGo/NSString+Crypto.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoBuild.h>
#import <SOGo/SOGoMailer.h>
#import <SOGo/SOGoSource.h>
#import <SOGo/SOGoPermissions.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/SOGoUserSettings.h>
#import <SOGo/SOGoSystemDefaults.h>
#import <SOGo/SOGoUserManager.h>
#import <SOGo/SOGoWebDAVAclManager.h>
#import <SOGo/WORequest+SOGo.h>
#import <Appointments/SOGoAppointmentFolder.h>
#import <Mailer/NSString+Mail.h>

#import "SOGoAptMailICalReply.h"
#import "SOGoAptMailReceipt.h"
#import "SOGoEMailAlarmsManager.h"
#import "iCalEntityObject+SOGo.h"
#import "iCalPerson+SOGo.h"
#import "iCalRepeatableEntityObject+SOGo.h"
#import "SOGoCalendarComponent.h"


static NSArray *allowed_tags = nil;

@implementation SOGoCalendarComponent

+ (SOGoWebDAVAclManager *) webdavAclManager
{
  static SOGoWebDAVAclManager *aclManager = nil;
  NSString *nsD, *nsI;

  if (!aclManager)
    {
      nsD = @"DAV:";
      nsI = @"urn:inverse:params:xml:ns:inverse-dav";

      aclManager = [SOGoWebDAVAclManager new];

      [aclManager registerDAVPermission: davElement (@"read", nsD)
		  abstract: NO
                  withEquivalent: @"SOGoDAVReadPermission" /* hackish */
		  asChildOf: davElement (@"all", nsD)];
      [aclManager registerDAVPermission: davElement (@"view-whole-component", nsI)
		  abstract: NO
		  withEquivalent: SOGoCalendarPerm_ViewAllComponent
                  asChildOf: davElement (@"all", nsD)];
      [aclManager registerDAVPermission: davElement (@"view-date-and-time", nsI)
		  abstract: NO
		  withEquivalent: SOGoCalendarPerm_ViewDAndT
		  asChildOf: davElement (@"all", nsD)];
      [aclManager registerDAVPermission: davElement (@"read-current-user-privilege-set", nsD)
		  abstract: NO
		  withEquivalent: SoPerm_WebDAVAccess
		  asChildOf: davElement (@"all", nsD)];
      [aclManager registerDAVPermission: davElement (@"write", nsD)
		  abstract: NO
		  withEquivalent: SOGoCalendarPerm_ModifyComponent
		  asChildOf: davElement (@"all", nsD)];
      [aclManager
	registerDAVPermission: davElement (@"write-properties", nsD)
	abstract: YES
	withEquivalent: SoPerm_ChangePermissions /* hackish */
	asChildOf: davElement (@"write", nsD)];
      [aclManager
	registerDAVPermission: davElement (@"write-content", nsD)
	abstract: YES
	withEquivalent: nil
	asChildOf: davElement (@"write", nsD)];
      [aclManager
        registerDAVPermission: davElement (@"respond-to-component", nsI)
                     abstract: NO
               withEquivalent: SOGoCalendarPerm_RespondToComponent
                    asChildOf: davElement (@"write-content", nsD)];
      [aclManager registerDAVPermission: davElement (@"admin", nsI)
		  abstract: YES
		  withEquivalent: nil
		  asChildOf: davElement (@"all", nsD)];
      [aclManager
	registerDAVPermission: davElement (@"read-acl", nsD)
	abstract: YES
	withEquivalent: SOGoPerm_ReadAcls
	asChildOf: davElement (@"admin", nsI)];
      [aclManager
	registerDAVPermission: davElement (@"write-acl", nsD)
	abstract: YES
	withEquivalent: nil
	asChildOf: davElement (@"admin", nsI)];
    }

  return aclManager;
}

- (void) setIsNew: (BOOL) newIsNew
{
  [super setIsNew: newIsNew];
}

- (BOOL) isNew
{
  return [super isNew];
}

- (NSException *) changeParticipationStatus: (NSString *) newPartStat
                               withDelegate: (iCalPerson *) delegate
                                      alarm: (iCalAlarm *) alarm
{
  // Required for protocol <SOGoComponentOccurence>
  return nil;
}

- (id) init
{
  if ((self = [super init]))
    {
      fullCalendar = nil;
      safeCalendar = nil;
      originalCalendar = nil;
      componentTag = nil;
    }

  return self;
}

- (void) dealloc
{
  [fullCalendar release];
  [safeCalendar release];
  [originalCalendar release];
  [componentTag release];
  [super dealloc];
}

- (void) flush
{
  DESTROY(fullCalendar);
  DESTROY(safeCalendar);
  DESTROY(originalCalendar);
}

- (Class *) parsingClass
{
  return (Class *)[iCalCalendar class];
}

- (NSString *) davContentType
{
  return @"text/calendar";
}

- (NSString *) componentTag
{
  if (!componentTag)
    [self subclassResponsibility: _cmd];
  
  return componentTag;
}

- (void) setComponentTag: (NSString *) theTag
{
  ASSIGN(componentTag, theTag);
}

- (void) _filterComponent: (iCalEntityObject *) component
{
  NSString *type, *summary, *tag, *uid;
  SOGoUserSettings *settings;
  SOGoUser *calendarOwner;
  NSEnumerator *children;
  CardElement *element;

  int classification;

  type = @"vtodo";
  classification = 0;

  calendarOwner = [SOGoUser userWithLogin: [self ownerInContext: context]];
  settings = [calendarOwner userSettings];

  if ([component isKindOfClass: [iCalEvent class]])
    type = @"vevent";
  
  if ([component symbolicAccessClass] == iCalAccessPrivate)
    classification = 1;
  else if ([component symbolicAccessClass] == iCalAccessConfidential)
    classification = 2;

  summary = [self labelForKey: [NSString stringWithFormat: @"%@_class%d",
                                         type, classification]
                    inContext: context];
  if(!allowed_tags)
      allowed_tags = [[NSArray arrayWithObjects: @"DTSTAMP", @"DTSTART", @"DTEND", @"DUE", @"EXDATE", @"EXRULE", @"RRULE", @"RECURRENCE-ID", nil] retain];
  uid = [[component uid] asCryptedPassUsingScheme: @"ssha256"
                                         withSalt: [[settings userPublicSalt] dataUsingEncoding: NSASCIIStringEncoding]
                                      andEncoding: encHex
                                          keyPath: nil];

  children = [[component children] objectEnumerator];
  NSMutableArray *to_remove = [NSMutableArray arrayWithCapacity: 8];
  
  while ((element = [children nextObject]))
    {
      tag = [element tag];
      if (![allowed_tags containsObject: [tag uppercaseString]])
	  [to_remove addObject: element];
    }

  [component removeChildren: to_remove];

  [component setSummary: summary];
  [component setUid: uid];
}

- (NSString *) secureContentAsString
{
  iCalRepeatableEntityObject *tmpComponent;
  iCalCalendar *tmpCalendar;
  NSArray *allComponents;
  SoSecurityManager *sm;
  NSString *iCalString;

  int i;

  sm = [SoSecurityManager sharedSecurityManager];
  if (activeUserIsOwner
      || [[self ownerInContext: context] isEqualToString: [[context activeUser] login]]
      || ![sm validatePermission: SOGoCalendarPerm_ViewAllComponent
	      onObject: self inContext: context])
    iCalString = content;
  else if (![sm validatePermission: SOGoCalendarPerm_ViewDAndT
		onObject: self inContext: context])
    {
      tmpCalendar = [[self calendar: NO secure: NO] mutableCopy];

      // We filter all components, in case we have RECURRENCE-ID
      allComponents = [tmpCalendar childrenWithTag: [self componentTag]];

      for (i = 0; i < [allComponents count]; i++)
        {
          tmpComponent = (iCalRepeatableEntityObject *)[allComponents objectAtIndex:i];
          [self _filterComponent: tmpComponent];
      
          // We add an additional header here to inform clients (if necessary) that
          // we churned the content of the calendar.
          [tmpComponent addChild: [CardElement simpleElementWithTag: @"X-SOGo-Secure"
                                                              value: @"YES"]];
        }

      iCalString = [tmpCalendar versitString];
      [tmpCalendar release];
    }
  else
    iCalString = nil;

  return iCalString;
}

- (iCalRepeatableEntityObject *) lookupOccurrence: (NSString *) recID
{
  [self subclassResponsibility: _cmd];

  return nil;
}

- (SOGoComponentOccurence *) occurence: (iCalRepeatableEntityObject *) component
{
  [self subclassResponsibility: _cmd];

  return nil;
}

- (iCalRepeatableEntityObject *) newOccurenceWithID: (NSString *) recID
{
  iCalRepeatableEntityObject *masterOccurence, *newOccurence;
  iCalCalendar *calendar;
  NSCalendarDate *recDate;
  NSTimeZone *timeZone;
  iCalPerson *organizer;

  recDate = [NSCalendarDate dateWithTimeIntervalSince1970: [recID intValue]];
  masterOccurence = [self component: NO secure: NO];
  timeZone = [[[context activeUser] userDefaults] timeZone];
  [recDate setTimeZone: timeZone];

  if ([masterOccurence doesOccurOnDate: recDate])
    {
      newOccurence = [masterOccurence mutableCopy];
      organizer = [masterOccurence organizer];
      [newOccurence autorelease];
      [newOccurence removeAllRecurrenceRules];
      [newOccurence removeAllExceptionRules];
      [newOccurence removeAllRecurrenceDates];
      [newOccurence removeAllExceptionDates];

      // It is important to set the organizer as some DAV clients (iCal
      // and Thunderbird 10/Lightning 1.2) will prompt the event "edition"
      // dialog instead of the event "invitation" (for accept/decline/tentative)
      // if the organizer isn't found in a specific recurrence
      [newOccurence setOrganizer: organizer];
      [newOccurence setRecurrenceId: recDate];

      calendar = [masterOccurence parent];
      [calendar addChild: newOccurence];
    }
  else
    newOccurence = nil;

  return newOccurence;
}

- (id) toManyRelationshipKeys
{
  return nil;
}

- (id) toOneRelationshipKeys
{
  NSMutableArray *keys;
  NSArray *occurences;
  NSCalendarDate *recID;
  unsigned int count, max, seconds;

  keys = [NSMutableArray array];
  [keys addObject: @"master"];
  occurences = [[self calendar: NO secure: NO] allObjects];
  max = [occurences count];
  for (count = 1; count < max; count++)
    {
      recID = [[occurences objectAtIndex: count] recurrenceId];
      if (recID)
	{
	  seconds = [recID timeIntervalSince1970];
	  [keys addObject: [NSString stringWithFormat: @"occurence%d",
				     seconds]];
	}
    }

  return keys;
}

- (id) lookupName: (NSString *) lookupName
        inContext: (id) localContext
          acquire: (BOOL) acquire
{
  id obj;
  iCalRepeatableEntityObject *occurence;
  NSString *recID;
  BOOL isNewOccurence;

  obj = [super lookupName: lookupName
	       inContext: localContext
	       acquire: acquire];
  if (!obj)
  {
    if ([lookupName isEqualToString: @"master"])
	    obj = [self occurence: [self component: NO secure: NO]];
    else if ([lookupName hasPrefix: @"occurence"])
	  {
	    recID = [lookupName substringFromIndex: 9];
	    occurence = [self lookupOccurrence: recID];
	    if (occurence)
        isNewOccurence = NO;
      else
	    {
	      occurence = [self newOccurenceWithID: recID];
	      isNewOccurence = YES;
	    }
	    if (occurence)
	    {
	      obj = [self occurence: occurence];
	      if (isNewOccurence)
		      [obj setIsNew: isNewOccurence];
	    }
	  }
  }

  return obj;
}

- (NSString *) _secureContentWithoutAlarms
{
  iCalCalendar *calendar;
  NSArray *allComponents;
  iCalEntityObject *currentComponent;
  NSUInteger count, max;

  calendar = [self calendar: NO secure: YES];
  allComponents = [calendar childrenWithTag: [self componentTag]];
  max = [allComponents count];
  for (count = 0; count < max; count++)
    {
      currentComponent = [allComponents objectAtIndex: count];
      [currentComponent removeAllAlarms];
    }

  return [calendar versitString];
}

- (NSString *) contentAsString
{
  NSString *secureContent;

  if ([[context request] isSoWebDAVRequest])
    {
      if ([container showCalendarAlarms])
        secureContent = [self secureContentAsString];
      else
        secureContent = [self _secureContentWithoutAlarms];
    }
  else
    secureContent = [super contentAsString];

  return secureContent;
}

- (NSString *) davCalendarData
{
  return [self contentAsString];
}

- (iCalCalendar *) calendar: (BOOL) create secure: (BOOL) secure
{
  iCalRepeatableEntityObject *newComponent;
  iCalCalendar **calendar, *returnedCopy;
  NSString *iCalString, *tag, *prodID;

  if (secure)
    calendar = &safeCalendar;
  else
    calendar = &fullCalendar;

  if (!*calendar)
  {
    if (secure)
	    iCalString = [self secureContentAsString];
    else
	    iCalString = content;

    if ([iCalString length] > 0)
    {
      ASSIGN (*calendar, [iCalCalendar parseSingleFromSource: iCalString]);
      if (!secure)
        originalCalendar = [*calendar copy];
    }
    else
    {
      if (create)
      {
        ASSIGN (*calendar, [iCalCalendar groupWithTag: @"vcalendar"]);
        [*calendar setVersion: @"2.0"];
        prodID = [NSString stringWithFormat:@"-//Inverse inc./SOGo %@//EN", SOGoVersion];
        [*calendar setProdID: prodID];
        tag = [[self componentTag] uppercaseString];
        newComponent = [[*calendar classForTag: tag] groupWithTag: tag];
        [newComponent setUid: [self globallyUniqueObjectId]];
        [*calendar addChild: newComponent];
      }
    }
  }

  returnedCopy = [*calendar mutableCopy];
  [returnedCopy autorelease];

  return returnedCopy;
}

- (id) component: (BOOL) create secure: (BOOL) secure
{
  return [[self calendar: create secure: secure]
	   firstChildWithTag: [self componentTag]];
}

//
// Returs "YES" if a a group was decomposed among attendees.
//
// It can also return yes if an attendee was found in the list
// matching the organizer. In which case, it was removed.
//
- (BOOL) expandGroupsInEvent: (iCalEvent *) theEvent
{
  NSString *organizerEmail, *domain;
  NSMutableArray *allAttendees;
  iCalPerson *currentAttendee;
  NSEnumerator *enumerator;
  NSAutoreleasePool *pool;
  NSDictionary *dict;

  BOOL eventWasModified;
  unsigned int i, j;


  domain = [[context activeUser] domain];
  organizerEmail = [[theEvent organizer] rfc822Email];
  eventWasModified = NO;
  allAttendees = [NSMutableArray arrayWithArray: [theEvent attendees]];
  enumerator = [[theEvent attendees] objectEnumerator];

  j = 0;

  pool = [[NSAutoreleasePool alloc] init];

  while ((currentAttendee = [enumerator nextObject]))
    {
      if (j%5 == 0)
        {
          RELEASE(pool);
          pool = [[NSAutoreleasePool alloc] init];
        }

      dict = [[SOGoUserManager sharedUserManager] contactInfosForUserWithUIDorEmail: [currentAttendee rfc822Email]
                                                                           inDomain: domain];

      if (dict && [[dict objectForKey: @"isGroup"] boolValue])
        {
	  iCalPerson *person;
	  NSArray *members;
	  SOGoUser *user;
          id <SOGoSource> source;
	  
	  // We did decompose a group...
	  [allAttendees removeObject: currentAttendee];

          source = [[SOGoUserManager sharedUserManager] sourceWithID: [dict objectForKey: @"SOGoSource"]];
          if ([source conformsToProtocol:@protocol(SOGoMembershipSource)])
            {
              members = [(id<SOGoMembershipSource>)(source) membersForGroupWithUID: [dict objectForKey: @"c_uid"]];
              for (i = 0; i < [members count]; i++)
                {
                  user = [members objectAtIndex: i];
                  eventWasModified = YES;

                  // If the organizer is part of the group, we skip it from
                  // the addition to the attendees' list
                  if ([user hasEmail: organizerEmail])
                    continue;

                  person = [self iCalPersonWithUID: [user login]];
                  [person setTag: @"ATTENDEE"];
                  [person setParticipationStatus: [currentAttendee participationStatus]];
                  [person setRsvp: [currentAttendee rsvp]];
                  [person setRole: [currentAttendee role]];

                  if (![allAttendees containsObject: person])
                    [allAttendees addObject: person];
                }
            }
          else
            {
              [self errorWithFormat: @"Inconsistency error - got group identifier (%@) from a source (%@) that does not support groups.", [currentAttendee rfc822Email], [dict objectForKey: @"SOGoSource"]];
              RELEASE(pool);
              return NO;
            }
	}
      
      j++;
    } // while (currentAttendee ...

  if (eventWasModified)
    [theEvent setAttendees: allAttendees];
  
  RELEASE(pool);

  return eventWasModified;
}

- (void) _updateRecurrenceIDsWithEvent: (iCalRepeatableEntityObject*) newEvent
{
  iCalRepeatableEntityObject *oldMaster, *currentComponent;
  iCalDateTime *currentDate;
  int deltaSecs;
  NSArray *components, *dates;
  NSMutableArray *newDates;
  unsigned int count, max;
  NSCalendarDate *recID, *newDate;

  // Compute time interval from previous event definition.
  if (!originalCalendar)
    {
      if (content)
	ASSIGN (originalCalendar, [iCalCalendar parseSingleFromSource: content]);
      else
	{
	  [self warnWithFormat: @"content not available, we don't update the event"];
	  return;
	}
    }

  oldMaster = (iCalRepeatableEntityObject *)
    [originalCalendar firstChildWithTag: [self componentTag]];
  deltaSecs = [[newEvent startDate]
		timeIntervalSinceDate: [oldMaster startDate]];

  components = [[newEvent parent] events];
  max = [components count];

  if (max > 0)
    {
      // Update recurrence-id attribute of occurences.
      for (count = 1; count < max; count++)
	{
	  currentComponent = [components objectAtIndex: count];
	  recID = [[currentComponent recurrenceId] addTimeInterval: deltaSecs];
	  [currentComponent setRecurrenceId: recID];
	}

      // Update exception dates in master vEvent.
      currentComponent = [components objectAtIndex: 0];
      dates = [currentComponent childrenWithTag: @"exdate"];
      max = [dates count];
      if (max > 0)
	{
	  newDates = [NSMutableArray arrayWithCapacity: max];
	  for (count = 0; count < max; count++)
	    {
	      currentDate = [dates objectAtIndex: count];
	      newDate = [[currentDate dateTime] addTimeInterval: deltaSecs];
	      [newDates addObject: newDate];
	    }
	  [currentComponent removeAllExceptionDates];
	  for (count = 0; count < max; count++)
	    [currentComponent addToExceptionDates: [newDates objectAtIndex: count]];
	}
    }
}

- (void) updateComponent: (iCalRepeatableEntityObject *) newObject
{
  NSString *newUid;

  if (!isNew
      && [newObject isRecurrent])
    // We update an repeating event -- update exception dates
    // and recurrence-ids.
    [self _updateRecurrenceIDsWithEvent: newObject];

  // As much as we can, we try to use c_name == c_uid in order
  // to avoid tricky scenarios with some CalDAV clients. For example,
  // if Alice invites Bob (both use SOGo) and Bob accepts the invitation
  // using Lightning before having refreshed their calendar, they'll end up
  // with a duplicate of the event in their database tables.
  if (isNew)
    {
      newUid = nameInContainer;
      
      if ([newUid hasSuffix: @".ics"])
	newUid = [newUid substringToIndex: [newUid length]-4];
      [newObject setUid: newUid];
    }
}

- (NSException *) saveCalendar: (iCalCalendar *) newCalendar
{
  [super saveComponent: newCalendar];

  return nil;
}

- (NSException *) saveComponent: (iCalRepeatableEntityObject *) newObject
{
  [newObject setLastModified: [NSCalendarDate calendarDate]];
  return [self saveCalendar: [newObject parent]];
}

- (NSException *) saveComponent: (iCalRepeatableEntityObject *) newEvent
                          force: (BOOL) forceSave
{
  return [self saveComponent: newEvent];
}

/* raw saving */

/* EMail Notifications */
- (NSString *) homePageURLForPerson: (iCalPerson *) _person
{
  NSString *baseURL;
  NSString *uid;
  NSArray *traversalObjects;

  /* generate URL from traversal stack */
  traversalObjects = [context objectTraversalStack];
  if ([traversalObjects count] > 0)
    baseURL = [[traversalObjects objectAtIndex:0] baseURLInContext: context];
  else
    {
      baseURL = @"http://localhost/";
      [self warnWithFormat:@"Unable to create baseURL from context!"];
    }
  uid = [_person uid];

  return ((uid)
          ? [NSString stringWithFormat:@"%@%@", baseURL, uid]
          : nil);
}

- (NSTimeZone *) timeZoneForUser: (NSString *) email
{
  NSString *uid;
  SOGoUserDefaults *ud;

  uid = [[SOGoUserManager sharedUserManager] getUIDForEmail: email];
  ud = [[SOGoUser userWithLogin: uid] userDefaults];

  return [ud timeZone];
}

- (NGMimeBodyPart *) _bodyPartForICalObject: (iCalRepeatableEntityObject *) object
{
  NGMimeBodyPart *bodyPart;
  NGMutableHashMap *headerMap;
  NSString *iCalString, *header, *charset;
  NSData *objectData;
  iCalCalendar *parent;

  parent = [object parent];
  iCalString = [NSString stringWithFormat: @"%@\r\n", [parent versitString]];
  objectData = [iCalString dataUsingEncoding: NSUTF8StringEncoding];
  charset = @"UTF-8";

  header = [NSString stringWithFormat: @"text/calendar; method=%@;"
                     @" charset=\"%@\"",
                     [(iCalCalendar *) [object parent] method], charset];
  headerMap = [NGMutableHashMap hashMapWithCapacity: 3];
  [headerMap setObject: @"urn:content-classes:calendarmessage"  forKey: @"Content-Class"];
  [headerMap setObject: header forKey: @"content-type"];
  bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap];
  [bodyPart setBody: objectData];

  return bodyPart;
}

//
//
//
- (void) sendEMailUsingTemplateNamed: (NSString *) newPageName
			   forObject: (iCalRepeatableEntityObject *) object
		      previousObject: (iCalRepeatableEntityObject *) previousObject
                         toAttendees: (NSArray *) attendees
                            withType: (NSString *) msgType
{
  NSString *pageName;
  NSString *senderEmail, *shortSenderEmail, *email;
  WOApplication *app;
  unsigned i, count;
  iCalPerson *attendee;
  NSString *recipient;
  SOGoAptMailNotification *p;
  NSString *mailDate, *subject, *text;
  NGMutableHashMap *headerMap;
  NGMimeMessage *msg;
  NGMimeBodyPart *bodyPart, *eventBodyPart;
  NGMimeMultipartBody *body;
  SOGoUser *ownerUser;
  SOGoDomainDefaults *dd;

  // If defined, we return immediately. When not defined, we send the notifications correctly
  if ([object firstChildWithTag: @"X-SOGo-Send-Appointment-Notifications"])
    return;

  // We never send IMIP inivitaton/deletion/update when the "initiator" is an EAS client.
  // That is because Outlook, iOS and Android will always issue a SendMail command
  // with the meeting details (ie., IMIP message with METHOD:REQUEST) so there's
  // no need to send it twice. Moreover, Outlook users can also choose to NOT send
  // the IMIP messsage at all, so SOGo won't send one without user's consent
  if ([[[context request] requestHandlerKey] isEqualToString: @"Microsoft-Server-ActiveSync"])
    return;

  ownerUser = [SOGoUser userWithLogin: owner];
  dd = [ownerUser domainDefaults];
  
  if ([dd appointmentSendEMailNotifications] && [object isStillRelevant])
    {
      count = [attendees count];
      if (count)
	{
	  /* sender */
	  shortSenderEmail = [[object organizer] rfc822Email];
	  if ([shortSenderEmail length])
            senderEmail = [[object organizer] mailAddress];
          else
            {
              shortSenderEmail = [[previousObject organizer] rfc822Email];
              senderEmail = [[previousObject organizer] mailAddress];
            }

          // No organizer, grab the event's owner
          if (![senderEmail length])
            senderEmail = shortSenderEmail = [[ownerUser primaryIdentity] objectForKey: @"email"];

          /* calendar part */
          eventBodyPart = [self _bodyPartForICalObject: object];

	  /* get WOApplication instance */
	  app = [WOApplication application];

	  /* generate dynamic message content */
	  for (i = 0; i < count; i++)
	    {
	      attendee = [attendees objectAtIndex: i];
	      // Don't send a notification to the event organizer nor a deletion
	      // notification to an attendee who already declined the invitation.
	      if (![[attendee uid] isEqualToString: owner] &&
		  !([[attendee partStat] compare: @"DECLINED"] == NSOrderedSame &&
		    [newPageName compare: @"Deletion"] == NSOrderedSame))
		{
		  /* construct recipient */
		  recipient = [attendee mailAddress];
		  email = [attendee rfc822Email];

#warning this could be optimized in a class hierarchy common with the	\
  SOGoObject acl notification mechanism
		  /* create page name */
		  pageName = [NSString stringWithFormat: @"SOGoAptMail%@",
                                       newPageName];
		  /* construct message content */
		  p = [app pageWithName: pageName inContext: context];
		  [p setApt: (iCalEvent *) object];
		  [p setPreviousApt: (iCalEvent *) previousObject];
                  [p setCurrentAttendee: attendee];
		  
		  if ([[object organizer] cn] && [[[object organizer] cn] length])
		    {
		      [p setOrganizerName: [[object organizer] cn]];
		    }
		  else
		    {
		      [p setOrganizerName: [ownerUser cn]];
		    }

		  subject = [[p getSubject] asQPSubjectString: @"UTF-8"];
		  text = [p getBody];

		  /* construct message */
		  headerMap = [NGMutableHashMap hashMapWithCapacity: 5];
          
		  /* NOTE: multipart/alternative seems like the correct choice but
		   * unfortunately Thunderbird doesn't offer the rich content alternative
		   * at all. Mail.app shows the rich content alternative _only_
		   * so we'll stick with multipart/mixed for the time being.
		   */
#warning SOPE is just plain stupid here - if you change the case of keys, it will break the encoding of fields
		  [headerMap setObject: @"multipart/mixed" forKey: @"content-type"];
		  [headerMap setObject: @"1.0" forKey: @"MIME-Version"];
		  [headerMap setObject: senderEmail forKey: @"from"];
		  [headerMap setObject: recipient forKey: @"to"];
		  mailDate = [[NSCalendarDate date] rfc822DateString];
		  [headerMap setObject: mailDate forKey: @"date"];
		  [headerMap setObject: subject forKey: @"subject"];
                  [headerMap setObject: [NSString generateMessageID: senderEmail] forKey: @"message-id"];
                  if ([msgType length] > 0)
                    [headerMap setObject: msgType forKey: @"x-sogo-message-type"];
		  msg = [NGMimeMessage messageWithHeader: headerMap];

		  /* multipart body */
		  body = [[NGMimeMultipartBody alloc] initWithPart: msg];

		  /* text part */
		  headerMap = [NGMutableHashMap hashMapWithCapacity: 1];
		  [headerMap setObject: @"text/html; charset=utf-8"
				forKey: @"content-type"];
		  bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap];
		  [bodyPart setBody: [text dataUsingEncoding: NSUTF8StringEncoding]];

		  /* attach text part to multipart body */
		  [body addBodyPart: bodyPart];
    
		  /* attach calendar part to multipart body only if the participation role is not NON-PARTICIPANT */
                  if ([[attendee role] caseInsensitiveCompare: @"NON-PARTICIPANT"] != NSOrderedSame)
                    [body addBodyPart: eventBodyPart];

		  /* attach multipart body to message */
		  [msg setBody: body];
		  [body release];

		  /* send the damn thing */
		  [[SOGoMailer mailerWithDomainDefaults: dd]
		           sendMimePart: msg
                           toRecipients: [NSArray arrayWithObject: email]
                                 sender: shortSenderEmail
                      withAuthenticator: [self authenticatorInContext: context]
                              inContext: context
                          systemMessage: YES];
		}
	    }
	}
    }
}

//
//
//
- (void) _sendIMIPReplyForEvent: (iCalRepeatableEntityObject *) event
                           from: (SOGoUser *) from
                             to: (iCalPerson *) recipient
                       textOnly: (BOOL) textOnly
{
  NSString *mailDate, *email;
  WOApplication *app;
  iCalPerson *attendee;
  SOGoAptMailICalReply *p;
  NGMutableHashMap *headerMap;
  NGMimeMessage *msg;
  NGMimeBodyPart *bodyPart;
  NGMimeMultipartBody *body;
  NSData *bodyData;
  SOGoDomainDefaults *dd;

  dd = [from domainDefaults];

  /* get WOApplication instance */
  app = [WOApplication application];

  /* remove all alarms to avoid bug #3925 */
  [event removeAllAlarms];

  /* construct message content */
  p = [app pageWithName: @"SOGoAptMailICalReply"  inContext: context];
  [p setApt: (iCalEvent *) event];

  attendee = [event userAsAttendee: from];
  [p setAttendee: attendee];

  /* construct message */
  headerMap = [NGMutableHashMap hashMapWithCapacity: 5];

  /* NOTE: multipart/alternative seems like the correct choice but
   * unfortunately Thunderbird doesn't offer the rich content alternative
   * at all. Mail.app shows the rich content alternative _only_
   * so we'll stick with multipart/mixed for the time being.
   */
#warning SOPE is just plain stupid here - if you change the case of keys, it will break the encoding of fields
  [headerMap setObject: [attendee mailAddress] forKey: @"from"];
  [headerMap setObject: [recipient mailAddress] forKey: @"to"];
  mailDate = [[NSCalendarDate date] rfc822DateString];
  [headerMap setObject: mailDate forKey: @"date"];
  [headerMap setObject: [[p getSubject] asQPSubjectString: @"UTF-8"]
                forKey: @"subject"];
  [headerMap setObject: [NSString generateMessageID: [[attendee mailAddress] pureEMailAddress]] forKey: @"message-id"];
  [headerMap setObject: @"1.0" forKey: @"MIME-Version"];

  if (textOnly)
    [headerMap setObject: @"text/html; charset=utf-8"
                  forKey: @"content-type"];
  else
    [headerMap setObject: @"multipart/mixed" forKey: @"content-type"];

  [headerMap setObject: @"calendar:invitation-reply" forKey: @"x-sogo-message-type"];
  msg = [NGMimeMessage messageWithHeader: headerMap];

  /* text part */
  headerMap = [NGMutableHashMap hashMapWithCapacity: 1];
  [headerMap setObject: @"text/html; charset=utf-8"
                forKey: @"content-type"];
  bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap];
  bodyData = [[p getBody] dataUsingEncoding: NSUTF8StringEncoding];

  if (textOnly)
    {
      [msg setBody: bodyData];
    }
  else
    {
      /* multipart body */
      body = [[NGMimeMultipartBody alloc] initWithPart: msg];
      [bodyPart setBody: bodyData];

      /* attach text part to multipart body */
      [body addBodyPart: bodyPart];

      /* attach calendar part to multipart body */
      [body addBodyPart: [self _bodyPartForICalObject: event]];

      /* attach multipart body to message */
      [msg setBody: body];
      [body release];
    }

  /* send the damn thing */
  email = [recipient rfc822Email];
  [[SOGoMailer mailerWithDomainDefaults: dd]
	       sendMimePart: msg
               toRecipients: [NSArray arrayWithObject: email]
                     sender: [attendee rfc822Email]
          withAuthenticator: [self authenticatorInContext: context]
                  inContext: context
              systemMessage: YES];
}


//
//
//
- (void) sendIMIPReplyForEvent: (iCalRepeatableEntityObject *) event
			  from: (SOGoUser *) from
			    to: (iCalPerson *) recipient
{
  SOGoDomainDefaults *dd;
  SOGoUserSettings *settings;
  BOOL canSendNotification, doNotSendNotificationOnCalDav;

  // We never send IMIP reply when the "initiator" is Outlook 2013/2016 over
  // the EAS protocol. That is because Outlook will always issue a SendMail command
  // with the meeting's response (ie., IMIP message with METHOD:REPLY) so there's
  // no need to send it twice. Moreover, Outlook users can also choose to NOT send
  // the IMIP messsage at all, so SOGo won't send one without user's consent
  if ([[context objectForKey: @"DeviceType"] isEqualToString: @"WindowsOutlook15"])
    return;

  canSendNotification = NO;
  settings = [from userSettings];
  doNotSendNotificationOnCalDav = ([settings objectForKey: @"Calendar"] 
                                  && [[settings objectForKey: @"Calendar"] objectForKey: @"InvitationNotificationsOnCalDavDisabled"]
                                  && 1 == [[[settings objectForKey: @"Calendar"] objectForKey: @"InvitationNotificationsOnCalDavDisabled"] intValue]);
  canSendNotification = !doNotSendNotificationOnCalDav || !(doNotSendNotificationOnCalDav && [[context request] isSoWebDAVRequest]);
  
  dd = [from domainDefaults];
  if ([dd appointmentSendEMailNotifications] && [event isStillRelevant] && canSendNotification)
    {
      // We first send to the real recipient (organizer)
      [self _sendIMIPReplyForEvent: event
                              from: from
                                to: recipient
                          textOnly: NO];

      // If we have a sent-by, we send it to it also. This is useful since
      // if Alice is Bob's asssitant and invites Tony, when Tony accepts/declines
      // the event, Alice will also be informed about this. See #3195 for details.
      if ([recipient hasSentBy])
        {
          iCalPerson *sentBy;

          sentBy = [[[iCalPerson alloc] init] autorelease];
          [sentBy setEmail: [recipient sentBy]];
          [self _sendIMIPReplyForEvent: event
                                  from: from
                                    to: sentBy
                              textOnly: YES];
        }
    }
}

//
//
//
- (void) sendResponseToOrganizer: (iCalRepeatableEntityObject *) newComponent
			    from: (SOGoUser *) from
{
  iCalPerson *organizer, *attendee;
  iCalEvent *event;
  SOGoUser *ownerUser;

  event = [newComponent itipEntryWithMethod: @"reply"];
  ownerUser = [SOGoUser userWithLogin: owner];
  if (![event userIsOrganizer: ownerUser])
    {
      organizer = [event organizer];
      attendee = [event userAsAttendee: ownerUser];
      [event setAttendees: [NSArray arrayWithObject: attendee]];
      [self sendIMIPReplyForEvent: event from: from to: organizer];
    }
}

//
//
//
- (void) sendReceiptEmailForObject: (iCalEntityObject *) object
		    addedAttendees: (NSArray *) theAddedAttendees
		  deletedAttendees: (NSArray *) theDeletedAttendees
		  updatedAttendees: (NSArray *) theUpdatedAttendees
			 operation: (SOGoComponentOperation) theOperation
{
  NSString *calendarName, *mailDate, *mailText, *fullSenderEmail, *senderEmail, *fullRecipientEmail, *recipientEmail;
  NSDictionary *senderIdentity, *recipientIdentity;
  id <SOGoAuthenticator> authenticator;
  SOGoUser *currentUser, *ownerUser;
  NGMutableHashMap *headerMap;
  SOGoAptMailReceipt *page;
  SOGoDomainDefaults *dd;
  NGMimeMessage *msg;

  calendarName = [[self container] displayName];

  // We must handle three cases here:
  // - Receive a mail when I modify my calendar
  // - Receive a mail when someone else modifies my calendar
  // - When I modify my calendar, send a mail to: <foo@bar.com>

  // We first built the template that we'll slightly adjust for our three use-cases
  page = [[WOApplication application] pageWithName: @"SOGoAptMailReceipt"
					 inContext: context];
  [page setApt: (iCalEvent *) object];
  [page setAddedAttendees: theAddedAttendees];
  [page setDeletedAttendees: theDeletedAttendees];
  [page setUpdatedAttendees: theUpdatedAttendees];
  [page setOperation: theOperation];
  [page setCalendarName: calendarName];
  
  currentUser = [context activeUser];
  senderIdentity = [currentUser primaryIdentity];

  dd = [currentUser domainDefaults];

#warning SOPE is just plain stupid here - if you change the case of keys, it will break the encoding of fields
  headerMap = [NGMutableHashMap hashMapWithCapacity: 5];
  
  // Sender can vary, base on whom modifies the actual event (owner vs. someone else)
  senderEmail = [senderIdentity objectForKey: @"email"];
  fullSenderEmail = [senderIdentity keysWithFormat: @"%{fullName} <%{email}>"];
  [headerMap setObject: fullSenderEmail forKey: @"from"];

  // Recipient is fixed, which is the calendar owner
  ownerUser = [SOGoUser userWithLogin: self->owner];
  recipientIdentity = [ownerUser primaryIdentity];
  
  // Safety net for broken configurations
  if (!recipientIdentity)
    return;

  recipientEmail = [recipientIdentity objectForKey: @"email"];
  fullRecipientEmail = [recipientIdentity keysWithFormat: @"%{fullName} <%{email}>"];
  
  [headerMap setObject: fullRecipientEmail forKey: @"to"];

  mailDate = [[NSCalendarDate date] rfc822DateString];
  [headerMap setObject: mailDate forKey: @"date"];
  [headerMap setObject: [page getSubject] forKey: @"subject"];
  [headerMap setObject: [NSString generateMessageID: senderEmail] forKey: @"message-id"];
  [headerMap setObject: @"1.0" forKey: @"MIME-Version"];
  [headerMap setObject: @"text/html; charset=utf-8"
		forKey: @"content-type"];
  msg = [NGMimeMessage messageWithHeader: headerMap];
  
  /* text part */
  mailText = [page getBody];
  [msg setBody: [mailText dataUsingEncoding: NSUTF8StringEncoding]];

  authenticator = [self authenticatorInContext: context];
  
  if ([self->owner isEqualToString: [currentUser login]])
    {
      if ([[self container] notifyOnPersonalModifications])
	{
	  [[SOGoMailer mailerWithDomainDefaults: dd]
		    sendMimePart: msg
		    toRecipients: [NSArray arrayWithObject: recipientEmail]
                          sender: senderEmail
               withAuthenticator: authenticator
                       inContext: context
                   systemMessage: YES];
	}
      
      if ([[self container] notifyUserOnPersonalModifications] &&
          [[self container] notifiedUserOnPersonalModifications])
	{
	  id o;

	  o = [headerMap objectForKey: @"to"];
	  recipientEmail = [[self container] notifiedUserOnPersonalModifications];
	  [headerMap setObject: recipientEmail forKey: @"to"];
	  
	  [[SOGoMailer mailerWithDomainDefaults: dd]
		    sendMimePart: msg
		    toRecipients: [NSArray arrayWithObject: recipientEmail]
                          sender: senderEmail
               withAuthenticator: authenticator
                       inContext: context
                   systemMessage: YES];

	  [headerMap setObject: o forKey: @"to"];
	}
    }

      
  if ([[self container] notifyOnExternalModifications] &&
      ![self->owner isEqualToString: [currentUser login]])
    {
      [[SOGoMailer mailerWithDomainDefaults: dd]
		    sendMimePart: msg
		    toRecipients: [NSArray arrayWithObject: recipientEmail]
                          sender: senderEmail
               withAuthenticator: authenticator
                       inContext: context
                   systemMessage: YES];
    }

}

//
//
//
- (iCalPerson *) findParticipantWithUID: (NSString *) uid
{
  iCalEntityObject *component;
  SOGoUser *user;

  user = [SOGoUser userWithLogin: uid];
  component = [self component: NO secure: NO];

  return [component userAsAttendee: user];
}

//
//
//
- (iCalPerson *) iCalPersonWithUID: (NSString *) uid
{
  iCalPerson *person;
  SOGoUserManager *um;
  NSDictionary *contactInfos;

  um = [SOGoUserManager sharedUserManager];
  contactInfos = [um contactInfosForUserWithUIDorEmail: uid];

  person = [iCalPerson new];
  [person autorelease];
  [person setCn: [contactInfos objectForKey: @"cn"]];
  [person setEmail: [contactInfos objectForKey: @"c_email"]];

  return person;
}

//
//
//
- (NSArray *) getUIDsForICalPersons: (NSArray *) iCalPersons
{
  iCalPerson *currentPerson;
  NSEnumerator *persons;
  NSMutableArray *uids;
  NSString *uid;

  uids = [NSMutableArray array];

  persons = [iCalPersons objectEnumerator];
  while ((currentPerson = [persons nextObject]))
    {
      uid = [currentPerson uid];
      if (uid)
	[uids addObject: uid];
    }

  return uids;
}

- (SOGoCalendarComponent *) _copyComponent: (iCalCalendar *) calendar
                                  toFolder: (SOGoGCSFolder *) newFolder
                                 updateUID: (BOOL) updateUID
{
  NSString *newUID;
  SOGoCalendarComponent *newComponent;

  if (updateUID)
    {
      NSArray *elements;
      unsigned int count, max;

      newUID = [self globallyUniqueObjectId];
      elements = [calendar allObjects];
      max = [elements count];
      for (count = 0; count < max; count++)
        [[elements objectAtIndex: count] setUid: newUID];
    }
  else
    {
      newUID = [[[calendar allObjects] objectAtIndex: 0] uid];
    }

  newComponent = [[self class] objectWithName:
				 [NSString stringWithFormat: @"%@.ics", newUID]
			       inContainer: newFolder];

  return newComponent;
}

- (NSException *) copyToFolder: (SOGoGCSFolder *) newFolder
{
  return [self copyComponent: [self calendar: NO secure: NO]
		    toFolder: newFolder];
}

- (NSException *) copyComponent: (iCalCalendar *) calendar
		       toFolder: (SOGoGCSFolder *) newFolder
{
  iCalEntityObject *component;
  SOGoCalendarComponent *newComponent;
  SOGoComponentOperation operation;
  NSArray *components;
  NSException *ex;

  operation = EventCreated;
  newComponent = [self _copyComponent: calendar
                             toFolder: newFolder
                            updateUID: YES];
  ex = [newComponent saveCalendar: calendar];

  if (!ex)
    {
      // Trigger notification in destination folder
      components = [calendar events];
      if ([components count] == 0)
        {
          components = [calendar todos];
          operation = TaskCreated;
        }
      if ([components count] > 0)
        {
          component = [components objectAtIndex: 0];
          [newComponent sendReceiptEmailForObject: component
                                   addedAttendees: nil
                                 deletedAttendees: nil
                                 updatedAttendees: nil
                                        operation: operation];
        }
    }

  return ex;
}

- (NSException *) moveToFolder: (SOGoGCSFolder *) newFolder
{
  iCalCalendar *calendar;
  iCalEntityObject *component;
  SOGoCalendarComponent *newComponent;
  SOGoComponentOperation operation;
  NSArray *components;
  NSException *ex;

  calendar = [self calendar: NO secure: NO];
  operation = EventCreated;
  newComponent = [self _copyComponent: calendar
                             toFolder: newFolder
                            updateUID: NO];
  ex = [newComponent saveCalendar: calendar];

  if (!ex)
    {
      // Trigger notification in destination folder
      components = [calendar events];
      if ([components count] == 0)
        {
          components = [calendar todos];
          operation = TaskCreated;
        }
      if ([components count] > 0)
        {
          component = [components objectAtIndex: 0];
          [newComponent sendReceiptEmailForObject: component
                                   addedAttendees: nil
                                 deletedAttendees: nil
                                 updatedAttendees: nil
                                        operation: operation];
          ex = [self delete];
          if (!ex)
            {
              // Trigger notification in source folder
              [self sendReceiptEmailForObject: component
                               addedAttendees: nil
                             deletedAttendees: nil
                             updatedAttendees: nil
                                    operation: operation == EventCreated ? EventDeleted : TaskDeleted];
            }
        }
    }

  return ex;
}

#warning Should we not remove the concept of Organizer and Participant roles?
- (NSString *) _roleOfOwner: (iCalRepeatableEntityObject *) component
{
  NSString *role;
  iCalPerson *organizer;
  SOGoUser *ownerUser;

  if (isNew)
    role = SOGoCalendarRole_Organizer;
  else
    {
      organizer = [component organizer];
      if ([[organizer rfc822Email] length] > 0)
	{
	  ownerUser = [SOGoUser userWithLogin: owner];
	  if ([component userIsOrganizer: ownerUser])
	    role = SOGoCalendarRole_Organizer;
	  else if ([component userIsAttendee: ownerUser])
	    role = SOGoCalendarRole_Participant;
	  else
	    role = SOGoRole_None;
	}
      else
	role = SOGoCalendarRole_Organizer;
    }

  return role;
}

- (NSString *) _compiledRoleForOwner: (NSString *) ownerRole
			     andUser: (NSString *) userRole
{
  NSString *role;

  if ([userRole isEqualToString: SOGoCalendarRole_ComponentModifier]
      || ([userRole isEqualToString: SOGoCalendarRole_ComponentResponder]
	  && [ownerRole isEqualToString: SOGoCalendarRole_Participant]))
    role = ownerRole;
  else
    role = SOGoRole_None;

  return role;
}

- (NSArray *) aclsForUser: (NSString *) uid
{
  NSMutableArray *roles;
  NSArray *superAcls;
  iCalRepeatableEntityObject *component;
  NSString *accessRole, *ownerRole;
  SOGoUser *aclUser;

  roles = [NSMutableArray array];
  superAcls = [super aclsForUser: uid];
  if ([superAcls count] > 0)
    [roles addObjectsFromArray: superAcls];

  component = [self component: NO secure: NO];
  ownerRole = [self _roleOfOwner: component];
  if ([owner isEqualToString: uid])
    [roles addObject: ownerRole];
  else
    {
      if (isNew)
        {
          if ([roles containsObject: SOGoRole_ObjectCreator])
            [roles addObject: SOGoCalendarRole_Organizer];
        }
      else
        {
          if (component)
            {
              aclUser = [SOGoUser userWithLogin: uid];
              if ([component userIsOrganizer: aclUser])
                [roles addObject: SOGoCalendarRole_Organizer];
              else if ([component userIsAttendee: aclUser])
                [roles addObject: SOGoCalendarRole_Participant];
              accessRole
                = [container roleForComponentsWithAccessClass: [component symbolicAccessClass]
                                                      forUser: uid];
              if ([accessRole length] > 0)
                {
                  [roles addObject: accessRole];
                  [roles addObject: [self _compiledRoleForOwner: ownerRole
                                                        andUser: accessRole]];
                }
            }
        }
    }

  return roles;
}

- (void) snoozeAlarm: (unsigned int) minutes
{
  NSDictionary *quickFields;
  GCSFolder *folder;
  unsigned int nextAlarm;

  folder = [[self container] ocsFolder];
  if (!folder)
    {
      [self errorWithFormat:@"(%s): missing folder for update!",
            __PRETTY_FUNCTION__];
      return;
    }

  nextAlarm = [[NSCalendarDate calendarDate] timeIntervalSince1970]  + minutes * 60;
  quickFields = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: nextAlarm]
                                            forKey: @"c_nextalarm"];

  [folder updateQuickFields: quickFields
                whereColumn: @"c_name"
                  isEqualTo: nameInContainer];
}

/* SOGoComponentOccurence protocol */

- (iCalRepeatableEntityObject *) occurence
{
  return [self component: YES secure: NO];
}

#warning alarms: we do not handle occurrences
- (NSException *) prepareDelete
{
  if ([[SOGoSystemDefaults sharedSystemDefaults] enableEMailAlarms])
    {
      SOGoEMailAlarmsManager *eaMgr;
      
      eaMgr = [SOGoEMailAlarmsManager sharedEMailAlarmsManager];
      [eaMgr deleteAlarmsFromComponent: self];
    }

  return nil;
}


// /* Overriding this method dramatically speeds up PROPFIND request, but may
//    otherwise be a bad idea... Wait and see. */
// - (NSDictionary*) valuesForKeys: (NSArray*)keys
// {
//   NSMutableDictionary *values;

//   values = [NSMutableDictionary dictionaryWithCapacity: [keys count]];
//   [values setObject: [self davCreationDate] forKey: @"davCreationDate"];
//   [values setObject: [self davContentLength] forKey: @"davContentLength"];
//   [values setObject: [self davLastModified] forKey: @"davLastModified"];
//   [values setObject: @"text/calendar" forKey: @"davContentType"];
//   [values setObject: [self baseURL] forKey: @"davURL"];

//   return values;
// }

- (void) adjustClassificationInRequestCalendar: (iCalCalendar *) rqCalendar
{
  SOGoUserDefaults *userDefaults;
  NSString *accessClass;
  NSArray *allObjects;
  id entity;

  int i;

  userDefaults = [[context activeUser] userDefaults];
  allObjects = [rqCalendar allObjects];

  for (i = 0; i < [allObjects count]; i++)
    {
      entity = [allObjects objectAtIndex: i];

      if ([entity respondsToSelector: @selector(accessClass)])
        {
          accessClass = [entity accessClass];

          if (!accessClass || [accessClass length] == 0)
            {
              if ([entity isKindOfClass: [iCalEvent class]])
                [entity setAccessClass: [userDefaults calendarEventsDefaultClassification]];
              else if ([entity isKindOfClass: [iCalToDo class]])
                [entity setAccessClass: [userDefaults calendarTasksDefaultClassification]];
            }
        }
    }
}


@end
