Added missing recurrences support, improved MeetingResponse, added WindowSize support.

Also more bug fixes regarding event invitations, and ServerId handling for
calendar objects.
pull/17/merge
Ludovic Marcotte 2014-02-17 08:46:05 -05:00
parent 4d1fdb33f5
commit 9218c7f253
6 changed files with 465 additions and 154 deletions

View File

@ -49,6 +49,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#import <NGCards/iCalCalendar.h>
#import <NGCards/iCalEntityObject.h>
#import <NGCards/iCalEvent.h>
#import <NGCards/iCalPerson.h>
#import <NGCards/iCalToDo.h>
#import <NGCards/NGVCard.h>
@ -89,6 +90,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#import <Mailer/SOGoMailAccount.h>
#import <Mailer/SOGoMailAccounts.h>
#import <Mailer/SOGoMailFolder.h>
#import <Mailer/SOGoMailObject.h>
#import <Foundation/NSObject.h>
@ -187,20 +189,20 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// invitation email and choses "Add to calendar" BEFORE actually syncing the calendar. That would
// create a duplicate on the server.
if ([allValues objectForKey: @"UID"])
serverId = [NSString stringWithFormat: @"%@.ics", [allValues objectForKey: @"UID"]];
serverId = [allValues objectForKey: @"UID"];
else
serverId = [NSString stringWithFormat: @"%@.ics", [theCollection globallyUniqueObjectId]];
serverId = [theCollection globallyUniqueObjectId];
[allValues setObject: [[[context activeUser] userDefaults] timeZone] forKey: @"SOGoUserTimeZone"];
sogoObject = [theCollection lookupName: serverId
sogoObject = [theCollection lookupName: [serverId sanitizedServerIdWithType: theFolderType]
inContext: context
acquire: NO];
// If object isn't found, we 'create' a new one
if ([sogoObject isKindOfClass: [NSException class]])
{
sogoObject = [[SOGoAppointmentObject alloc] initWithName: serverId
sogoObject = [[SOGoAppointmentObject alloc] initWithName: [serverId sanitizedServerIdWithType: theFolderType]
inContainer: theCollection];
o = [sogoObject component: YES secure: NO];
}
@ -224,7 +226,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
default:
{
// FIXME
continue;
//continue;
NSLog(@"BLARG!");
abort();
}
}
@ -302,7 +306,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
allChanges = [[(id)[aChange getElementsByTagName: @"ApplicationData"] lastObject] applicationData];
// Fetch the object and apply the changes
sogoObject = [theCollection lookupName: serverId
sogoObject = [theCollection lookupName: [serverId sanitizedServerIdWithType: theFolderType]
inContext: context
acquire: NO];
@ -387,7 +391,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
serverId = [[(id)[aDelete getElementsByTagName: @"ServerId"] lastObject] textValue];
sogoObject = [theCollection lookupName: serverId
sogoObject = [theCollection lookupName: [serverId sanitizedServerIdWithType: theFolderType]
inContext: context
acquire: NO];
@ -412,7 +416,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
serverId = [[(id)[theDocumentElement getElementsByTagName: @"ServerId"] lastObject] textValue];
o = [theCollection lookupName: serverId
o = [theCollection lookupName: [serverId sanitizedServerIdWithType: theFolderType]
inContext: context
acquire: NO];
@ -432,13 +436,18 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
- (void) processSyncGetChanges: (id <DOMElement>) theDocumentElement
inCollection: (id) theCollection
withWindowSize: (unsigned int) theWindowSize
withSyncKey: (NSString *) theSyncKey
withFolderType: (SOGoMicrosoftActiveSyncFolderType) theFolderType
withFilterType: (NSCalendarDate *) theFilterType
inBuffer: (NSMutableString *) theBuffer
lastServerKey: (NSString **) theLastServerKey
{
NSMutableString *s;
int i;
BOOL more_available;
int i, max;
//
// No changes in the collection - 2.2.2.19.1.1 Empty Sync Request.
@ -448,7 +457,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
return;
s = [NSMutableString string];
more_available = NO;
switch (theFolderType)
{
// Handle all the GCS components
@ -473,7 +483,17 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
allComponents = [theCollection syncTokenFieldsWithProperties: nil matchingSyncToken: theSyncKey fromDate: theFilterType];
for (i = 0; i < [allComponents count]; i++)
// Check for the WindowSize
max = [allComponents count];
// Disabled for now for GCS folders.
// if (max > theWindowSize)
// {
// max = theWindowSize;
// more_available = YES;
// }
for (i = 0; i < max; i++)
{
component = [allComponents objectAtIndex: i];
deleted = [[component objectForKey: @"c_deleted"] intValue];
@ -481,7 +501,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (!deleted && ![[component objectForKey: @"c_component"] isEqualToString: component_name])
continue;
uid = [component objectForKey: @"c_name"];
uid = [[component objectForKey: @"c_name"] sanitizedServerIdWithType: theFolderType];
if (deleted)
{
@ -496,6 +516,41 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if ([[component objectForKey: @"c_creationdate"] intValue] > [theSyncKey intValue])
updated = NO;
sogoObject = [theCollection lookupName: [uid sanitizedServerIdWithType: theFolderType]
inContext: context
acquire: 0];
if (theFolderType == ActiveSyncContactFolder)
componentObject = [sogoObject vCard];
else
componentObject = [sogoObject component: NO secure: NO];
//
// We do NOT synchronize NEW events that are in fact, invitations
// to events. This is due to the fact that Outlook 2013 creates
// "phantom" events in the calendar that are mapped to invitations mails.
// If we synchronize these events too, it'll interfere with the whole thing
// and prevent Outlook from properly calling MeetingResponse.
//
if (!updated && theFolderType == ActiveSyncEventFolder)
{
iCalPersonPartStat partstat;
iCalPerson *attendee;
NSString *email;
email = [[[context activeUser] allEmails] objectAtIndex: 0];
attendee = [componentObject findAttendeeWithEmail: email];
if (attendee)
{
partstat = [attendee participationStatus];
if (partstat == iCalPersonPartStatNeedsAction)
continue;
}
}
if (updated)
[s appendString: @"<Change xmlns=\"AirSync:\">"];
else
@ -504,15 +559,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[s appendFormat: @"<ServerId xmlns=\"AirSync:\">%@</ServerId>", uid];
[s appendString: @"<ApplicationData xmlns=\"AirSync:\">"];
sogoObject = [theCollection lookupName: uid
inContext: context
acquire: 0];
if (theFolderType == ActiveSyncContactFolder)
componentObject = [sogoObject vCard];
else
componentObject = [sogoObject component: NO secure: NO];
[s appendString: [componentObject activeSyncRepresentation]];
[s appendString: @"</ApplicationData>"];
@ -535,7 +581,18 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
allMessages = [theCollection syncTokenFieldsWithProperties: nil matchingSyncToken: theSyncKey fromDate: theFilterType];
for (i = 0; i < [allMessages count]; i++)
// Check for the WindowSize.
// FIXME: we should eventually check for modseq and slice the maximum
// amount of messages returned to ensure we don't have the same
// modseq accross contiguous boundaries
max = [allMessages count];
if (max > theWindowSize)
{
max = theWindowSize;
more_available = YES;
}
for (i = 0; i < max; i++)
{
aMessage = [allMessages objectAtIndex: i];
@ -571,6 +628,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
}
}
//
if (more_available)
{
*theLastServerKey = uid;
}
}
break;
} // switch (folderType) ...
@ -580,6 +643,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[theBuffer appendString: @"<Commands>"];
[theBuffer appendString: s];
[theBuffer appendString: @"</Commands>"];
if (more_available)
[theBuffer appendString: @"<MoreAvailable/>"];
}
}
@ -663,12 +729,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
inBuffer: (NSMutableString *) theBuffer
changeDetected: (BOOL *) changeDetected
{
NSString *collectionId, *realCollectionId, *syncKey, *davCollectionTag, *bodyPreferenceType;
NSString *collectionId, *realCollectionId, *syncKey, *davCollectionTag, *bodyPreferenceType, *lastServerKey;
SOGoMicrosoftActiveSyncFolderType folderType;
id collection, value;
NSMutableString *changeBuffer, *commandsBuffer;
BOOL getChanges, first_sync;
unsigned int windowSize;
changeBuffer = [NSMutableString string];
commandsBuffer = [NSMutableString string];
@ -679,6 +746,14 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
syncKey = davCollectionTag = [[(id)[theDocumentElement getElementsByTagName: @"SyncKey"] lastObject] textValue];
// We check for a window size, default to 100 if not specfied or out of bounds
windowSize = [[[(id)[theDocumentElement getElementsByTagName: @"WindowSize"] lastObject] textValue] intValue];
if (windowSize == 0 || windowSize > 512)
windowSize = 100;
lastServerKey = nil;
// From the documention, if GetChanges is missing, we must assume it's a YES.
// See http://msdn.microsoft.com/en-us/library/gg675447(v=exchg.80).aspx for all details.
value = [theDocumentElement getElementsByTagName: @"GetChanges"];
@ -712,10 +787,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
{
[self processSyncGetChanges: theDocumentElement
inCollection: collection
withWindowSize: windowSize
withSyncKey: syncKey
withFolderType: folderType
withFilterType: [NSCalendarDate dateFromFilterType: [[(id)[theDocumentElement getElementsByTagName: @"FilterType"] lastObject] textValue]]
inBuffer: changeBuffer];
inBuffer: changeBuffer
lastServerKey: &lastServerKey];
}
//
@ -745,7 +822,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// let's regenerate our SyncKey based on the collection tag.
if ([changeBuffer length] || [commandsBuffer length])
{
davCollectionTag = [collection davCollectionTag];
if (lastServerKey)
davCollectionTag = [collection davCollectionTagFromId: lastServerKey];
else
davCollectionTag = [collection davCollectionTag];
*changeDetected = YES;
}

View File

@ -702,7 +702,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[s appendString: @"<Properties>"];
[s appendFormat: @"<ContentType xmlns=\"AirSyncBase:\">%@/%@</ContentType>", [[currentBodyPart partInfo] objectForKey: @"type"], [[currentBodyPart partInfo] objectForKey: @"subtype"]];
[s appendFormat: @"<Data>%@</Data>", [[[currentBodyPart fetchBLOB] stringByEncodingBase64] stringByReplacingString: @"\n" withString: @""]];
[s appendFormat: @"<Data>%@</Data>", [[currentBodyPart fetchBLOB] activeSyncRepresentation]];
[s appendString: @"</Properties>"];
[s appendString: @"</Fetch>"];
@ -735,7 +735,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- (void) processMeetingResponse: (id <DOMElement>) theDocumentElement
inResponse: (WOResponse *) theResponse
{
NSString *realCollectionId, *requestId, *participationStatus;
NSString *realCollectionId, *requestId, *participationStatus, *calendarId;
SOGoAppointmentObject *appointmentObject;
SOGoMailObject *mailObject;
NSMutableString *s;
NSData *d;
@ -750,34 +751,75 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
status = 1;
realCollectionId = [[[(id)[theDocumentElement getElementsByTagName: @"CollectionId"] lastObject] textValue] realCollectionIdWithFolderType: &folderType];
collection = [self collectionFromId: realCollectionId type: ActiveSyncMailFolder];
// 1 -> accepted, 2 -> tentative, 3 -> declined
userResponse = [[[(id)[theDocumentElement getElementsByTagName: @"UserResponse"] lastObject] textValue] intValue];
requestId = [[(id)[theDocumentElement getElementsByTagName: @"RequestId"] lastObject] textValue];
requestId = [[(id)[theDocumentElement getElementsByTagName: @"RequestId"] lastObject] textValue];
appointmentObject = nil;
calendarId = nil;
// Outlook 2013 calls MeetingResponse on the calendar folder! We have
// no way of handling as we can't retrieve the email (using the id found
// in requestId) in any mail folder! If that happens, let's simply
// assume it comes from the INBOX. This should be generally safe as people
// will answer email invitations as they receive them on their INBOX.
// Note that the mail should also still be there as MeetingResponse is
// called *before* MoveItems.
//
// We fetch the calendar information based on the email (requestId) in the user's INBOX (or elsewhere)
//
// FIXME: that won't work too well for external invitations...
mailObject = [collection lookupName: requestId
inContext: context
acquire: 0];
if (![mailObject isKindOfClass: [NSException class]])
// Apple iOS will also call MeetingResponse on the calendar folder when the
// user accepts/declines the meeting from the Calendar application. Before
// falling back on INBOX, we first check if we can find the event in the
// personal calendar.
if (folderType == ActiveSyncEventFolder)
{
SOGoAppointmentObject *appointmentObject;
iCalCalendar *calendar;
iCalEvent *event;
calendar = [mailObject calendarFromIMIPMessage];
event = [[calendar events] lastObject];
// Fetch the SOGoAppointmentObject
collection = [[context activeUser] personalCalendarFolderInContext: context];
appointmentObject = [collection lookupName: [NSString stringWithFormat: @"%@.ics", [event uid]]
appointmentObject = [collection lookupName: [requestId sanitizedServerIdWithType: ActiveSyncEventFolder]
inContext: context
acquire: NO];
calendarId = requestId;
// Object not found, let's fallback on the INBOX folder
if ([appointmentObject isKindOfClass: [NSException class]])
{
folderType = ActiveSyncMailFolder;
realCollectionId = @"INBOX";
appointmentObject = nil;
}
}
// Fetch the appointment object from the mail message
if (!appointmentObject)
{
collection = [self collectionFromId: realCollectionId type: folderType];
//
// We fetch the calendar information based on the email (requestId) in the user's INBOX (or elsewhere)
//
// FIXME: that won't work too well for external invitations...
mailObject = [collection lookupName: requestId
inContext: context
acquire: 0];
if (![mailObject isKindOfClass: [NSException class]])
{
iCalCalendar *calendar;
iCalEvent *event;
calendar = [mailObject calendarFromIMIPMessage];
event = [[calendar events] lastObject];
calendarId = [event uid];
// Fetch the SOGoAppointmentObject
collection = [[context activeUser] personalCalendarFolderInContext: context];
appointmentObject = [collection lookupName: [NSString stringWithFormat: @"%@.ics", [event uid]]
inContext: context
acquire: NO];
}
}
if (appointmentObject &&
calendarId &&
(![appointmentObject isKindOfClass: [NSException class]]))
{
// 1 -> accepted, 2 -> tentative, 3 -> declined
if (userResponse == 1)
participationStatus = @"ACCEPTED";
else if (userResponse == 2)
@ -785,29 +827,22 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
else
participationStatus = @"DECLINED";
if (![appointmentObject isKindOfClass: [NSException class]])
{
[appointmentObject changeParticipationStatus: participationStatus
withDelegate: nil];
[s appendString: @"<?xml version=\"1.0\" encoding=\"utf-8\"?>"];
[s appendString: @"<!DOCTYPE ActiveSync PUBLIC \"-//MICROSOFT//DTD ActiveSync//EN\" \"http://www.microsoft.com/\">"];
[s appendString: @"<MeetingResponse xmlns=\"MeetingResponse:\">"];
[s appendString: @"<Result>"];
[s appendFormat: @"<RequestId>%@</RequestId>", requestId];
[s appendFormat: @"<CalendarId>%@</CalendarId>", [event uid]];
[s appendFormat: @"<Status>%d</Status>", status];
[s appendString: @"</Result>"];
[s appendString: @"</MeetingResponse>"];
d = [[s dataUsingEncoding: NSUTF8StringEncoding] xml2wbxml];
[theResponse setContent: d];
}
else
{
[theResponse setStatus: 500];
}
[appointmentObject changeParticipationStatus: participationStatus
withDelegate: nil];
[s appendString: @"<?xml version=\"1.0\" encoding=\"utf-8\"?>"];
[s appendString: @"<!DOCTYPE ActiveSync PUBLIC \"-//MICROSOFT//DTD ActiveSync//EN\" \"http://www.microsoft.com/\">"];
[s appendString: @"<MeetingResponse xmlns=\"MeetingResponse:\">"];
[s appendString: @"<Result>"];
[s appendFormat: @"<RequestId>%@</RequestId>", requestId];
[s appendFormat: @"<CalendarId>%@</CalendarId>", calendarId];
[s appendFormat: @"<Status>%d</Status>", status];
[s appendString: @"</Result>"];
[s appendString: @"</MeetingResponse>"];
d = [[s dataUsingEncoding: NSUTF8StringEncoding] xml2wbxml];
[theResponse setContent: d];
}
else
{
@ -1042,7 +1077,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[s appendString: @"<Recipient>"];
[s appendFormat: @"<Type>%d</Type>", 1];
[s appendFormat: @"<DisplayName>%@</DisplayName>", [user cn]];
[s appendFormat: @"<EmailAddress>%@</EmailAddress>", [user systemEmail]];
[s appendFormat: @"<EmailAddress>%@</EmailAddress>", [[user allEmails] objectAtIndex: 0]];
// Freebusy structure: http://msdn.microsoft.com/en-us/library/gg663493(v=exchg.80).aspx
[s appendString: @"<Availability>"];

View File

@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#import <Foundation/NSArray.h>
#import <Foundation/NSCalendarDate.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSException.h>
#import <Foundation/NSString.h>
#import <NGCards/iCalCalendar.h>
@ -48,9 +49,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#import <NGObjWeb/WOContext+SoObjects.h>
#include "iCalTimeZone+ActiveSync.h"
#include "NSData+ActiveSync.h"
#include "NSDate+ActiveSync.h"
#include "NSString+ActiveSync.h"
#include "../SoObjects/Appointments/iCalPerson+SOGo.h"
#include "../SoObjects/Mailer/NSString+Mail.h"
#include "../SoObjects/Mailer/SOGoMailBodyPart.h"
@ -133,39 +136,37 @@ struct GlobalObjectId {
//
// For debugging purposes...
//
// - (NSString *) _uidFromGlobalObjectId: (NSData *) objectId
// {
// NSString *uid;
- (NSString *) _uidFromGlobalObjectId: (NSData *) objectId
{
NSString *uid;
// struct GlobalObjectId *newGlobalId;
// NSUInteger length;
// uint8_t *bytes;
struct GlobalObjectId *newGlobalId;
NSUInteger length;
uint8_t *bytes;
// length = [objectId length];
// uid = nil;
length = [objectId length];
uid = nil;
// bytes = malloc(length*sizeof(uint8_t));
// [objectId getBytes: bytes length: length];
bytes = malloc(length*sizeof(uint8_t));
[objectId getBytes: bytes length: length];
// newGlobalId = (struct GlobalObjectId *)bytes;
newGlobalId = (struct GlobalObjectId *)bytes;
// // We must take the offset (dataPrefix) into account
// uid = [[NSString alloc] initWithBytes: newGlobalId->Data+12 length: newGlobalId->Size-12 encoding: NSASCIIStringEncoding];
// free(bytes);
// return AUTORELEASE(uid);
// }
// We must take the offset (dataPrefix) into account
uid = [[NSString alloc] initWithBytes: newGlobalId->Data+12 length: newGlobalId->Size-12 encoding: NSASCIIStringEncoding];
free(bytes);
return AUTORELEASE(uid);
}
//
//
//
- (NSString *) _emailAddressesFrom: (NSArray *) enveloppeAddresses
{
NSMutableArray *addresses;
NSString *rc;
NGImap4EnvelopeAddress *address;
NSString *email;
NSMutableArray *addresses;
NSString *email, *rc;
int i, max;
rc = nil;
@ -177,7 +178,7 @@ struct GlobalObjectId {
for (i = 0; i < max; i++)
{
address = [enveloppeAddresses objectAtIndex: i];
email = [address email];
email = [NSString stringWithFormat: @"\"%@\" <%@>", [address personalName], [address baseEMail]];
if (email)
[addresses addObject: email];
@ -317,10 +318,19 @@ struct GlobalObjectId {
if (bodyPart)
{
iCalCalendar *calendar;
NSData *calendarData;
calendarData = [bodyPart fetchBLOB];
return [iCalCalendar parseSingleFromSource: calendarData];
calendar = nil;
NS_DURING
calendar = [iCalCalendar parseSingleFromSource: calendarData];
NS_HANDLER
calendar = nil;
NS_ENDHANDLER
return calendar;
}
}
}
@ -344,25 +354,17 @@ struct GlobalObjectId {
s = [NSMutableString string];
// From
value = [self _emailAddressesFrom: [[self envelope] from]];
if (value)
[s appendFormat: @"<From xmlns=\"Email:\">%@</From>", [value activeSyncRepresentation]];
// To - "The value of this element contains one or more e-mail addresses.
// If there are multiple e-mail addresses, they are separated by commas."
value = [self _emailAddressesFrom: [[self envelope] to]];
if (value)
[s appendFormat: @"<To xmlns=\"Email:\">%@</To>", [value activeSyncRepresentation]];
// DisplayTo
[s appendFormat: @"<DisplayTo xmlns=\"Email:\">\"%@\"</DisplayTo>", [[context activeUser] login]];
// Cc - same syntax as the To field
value = [self _emailAddressesFrom: [[self envelope] cc]];
// From
value = [self _emailAddressesFrom: [[self envelope] from]];
if (value)
[s appendFormat: @"<Cc xmlns=\"Email:\">%@</Cc>", [value activeSyncRepresentation]];
[s appendFormat: @"<From xmlns=\"Email:\">%@</From>", [value activeSyncRepresentation]];
// Subject
value = [self decodedSubject];
if (value)
@ -370,11 +372,19 @@ struct GlobalObjectId {
[s appendFormat: @"<Subject xmlns=\"Email:\">%@</Subject>", [value activeSyncRepresentation]];
[s appendFormat: @"<ThreadTopic xmlns=\"Email:\">%@</ThreadTopic>", [value activeSyncRepresentation]];
}
// DateReceived
value = [self date];
if (value)
[s appendFormat: @"<DateReceived xmlns=\"Email:\">%@</DateReceived>", [value activeSyncRepresentationWithoutSeparators]];
// DisplayTo
[s appendFormat: @"<DisplayTo xmlns=\"Email:\">%@</DisplayTo>", [[context activeUser] login]];
// Cc - same syntax as the To field
value = [self _emailAddressesFrom: [[self envelope] cc]];
if (value)
[s appendFormat: @"<Cc xmlns=\"Email:\">%@</Cc>", [value activeSyncRepresentation]];
// Importance - FIXME
[s appendFormat: @"<Importance xmlns=\"Email:\">%@</Importance>", @"1"];
@ -382,47 +392,84 @@ struct GlobalObjectId {
// Read
[s appendFormat: @"<Read xmlns=\"Email:\">%d</Read>", ([self read] ? 1 : 0)];
// We handle MeetingRequest
calendar = [self calendarFromIMIPMessage];
if (calendar)
{
NSString *method, *className;
iCalPerson *attendee;
iCalTimeZone *tz;
iCalEvent *event;
iCalPersonPartStat partstat;
int v;
event = [[calendar events] lastObject];
method = [[event parent] method];
attendee = [event findAttendeeWithEmail: [[[context activeUser] allEmails] objectAtIndex: 0]];
partstat = [attendee participationStatus];
// We generate the correct MessageClass
if ([method isEqualToString: @"REQUEST"])
className = @"IPM.Schedule.Meeting.Request";
else if ([method isEqualToString: @"REPLY"])
{
switch (partstat)
{
case iCalPersonPartStatAccepted:
className = @"IPM.Schedule.Meeting.Resp.Pos";
break;
case iCalPersonPartStatDeclined:
className = @"IPM.Schedule.Meeting.Resp.Neg";
break;
case iCalPersonPartStatTentative:
className = @"IPM.Schedule.Meeting.Resp.Tent";
break;
default:
className = @"IPM.Appointment";
NSLog(@"unhandled part stat");
}
}
else if ([method isEqualToString: @"COUNTER"])
className = @"IPM.Schedule.Meeting.Resp.Tent";
else if ([method isEqualToString: @"CANCEL"])
className = @"IPM.Schedule.Meeting.Cancelled";
else
className = @"IPM.Appointment";
[s appendFormat: @"<MessageClass xmlns=\"Email:\">%@</MessageClass>", className];
[s appendString: @"<MeetingRequest xmlns=\"Email:\">"];
[s appendFormat: @"<AllDayEvent xmlns=\"Email:\">%d</AllDayEvent>", ([event isAllDay] ? 1 : 0)];
// StartTime -- http://msdn.microsoft.com/en-us/library/ee157132(v=exchg.80).aspx
if ([event startDate])
[s appendFormat: @"<StartTime xmlns=\"Email:\">%@</StartTime>", [[event startDate] activeSyncRepresentationWithoutSeparators]];
if ([event timeStampAsDate])
[s appendFormat: @"<DTStamp xmlns=\"Email:\">%@</DTStamp>", [[event timeStampAsDate] activeSyncRepresentationWithoutSeparators]];
else if ([event created])
[s appendFormat: @"<DTStamp xmlns=\"Email:\">%@</DTStamp>", [[event created] activeSyncRepresentationWithoutSeparators]];
// StartTime -- http://msdn.microsoft.com/en-us/library/ee157132(v=exchg.80).aspx
if ([event startDate])
[s appendFormat: @"<StartTime xmlns=\"Email:\">%@</StartTime>", [[event startDate] activeSyncRepresentationWithoutSeparators]];
// EndTime -- http://msdn.microsoft.com/en-us/library/ee157945(v=exchg.80).aspx
if ([event endDate])
[s appendFormat: @"<EndTime xmlns=\"Email:\">%@</EndTime>", [[event endDate] activeSyncRepresentationWithoutSeparators]];
// Timezone
tz = [(iCalDateTime *)[event firstChildWithTag: @"dtstart"] timeZone];
if (!tz)
tz = [iCalTimeZone timeZoneForName: @"Europe/London"];
[s appendFormat: @"<TimeZone xmlns=\"Email:\">%@</TimeZone>", [[tz activeSyncRepresentation] stringByReplacingString: @"\n" withString: @""]];;
[s appendFormat: @"<InstanceType xmlns=\"Email:\">%d</InstanceType>", 0];
[s appendFormat: @"<Organizer xmlns=\"Email:\">%@</Organizer>", [[event organizer] rfc822Email]];
[s appendFormat: @"<ResponseRequested xmlns=\"Email:\">%d</ResponseRequested>", 1];
// Location
if ([[event location] length])
[s appendFormat: @"<Location xmlns=\"Email:\">%@</Location>", [[event location] activeSyncRepresentation]];
[s appendFormat: @"<Organizer xmlns=\"Email:\">%@</Organizer>", [[[event organizer] mailAddress] activeSyncRepresentation]];
// This will trigger the SendMail command. We set it to no for email invitations as
// SOGo will send emails when MeetingResponse is called.
[s appendFormat: @"<ResponseRequested xmlns=\"Email:\">%d</ResponseRequested>", 0];
// Sensitivity
if ([[event accessClass] isEqualToString: @"PRIVATE"])
v = 2;
@ -433,23 +480,33 @@ struct GlobalObjectId {
[s appendFormat: @"<Sensitivity xmlns=\"Email:\">%d</Sensitivity>", v];
[s appendFormat: @"<BusyStatus xmlns=\"Email:\">%d</BusyStatus>", 2];
// Timezone
tz = [(iCalDateTime *)[event firstChildWithTag: @"dtstart"] timeZone];
if (!tz)
tz = [iCalTimeZone timeZoneForName: @"Europe/London"];
[s appendFormat: @"<TimeZone xmlns=\"Email:\">%@</TimeZone>", [tz activeSyncRepresentation]];
// We disallow new time proposals
[s appendFormat: @"<DisallowNewTimeProposal xmlns=\"Email:\">%d</DisallowNewTimeProposal>", 1];
// We set the right message type - we must set AS version to 14.1 for this
//[s appendFormat: @"<MeetingMessageType xmlns=\"Email2:\">%d</MeetingMessageType>", 1];
// From http://blogs.msdn.com/b/exchangedev/archive/2011/07/22/working-with-meeting-requests-in-exchange-activesync.aspx:
//
// "Clients that need to determine whether the GlobalObjId element for a meeting request corresponds to an existing Calendar
// object in the Calendar folder have to convert the GlobalObjId element value to a UID element value to make the comparison."
//
globalObjId = [self _computeGlobalObjectIdFromEvent: event];
[s appendFormat: @"<GlobalObjId xmlns=\"Email:\">%@</GlobalObjId>", [[globalObjId stringByEncodingBase64] stringByReplacingString: @"\n" withString: @""]];
[s appendString: @"</MeetingRequest>"];
// MesssageClass and ContentClass
[s appendFormat: @"<MessageClass xmlns=\"Email:\">%@</MessageClass>", @"IPM.Schedule.Meeting.Request"];
[s appendFormat: @"<GlobalObjId xmlns=\"Email:\">%@</GlobalObjId>", [globalObjId activeSyncRepresentation]];
// We set the right message type - we must set AS version to 14.1 for this
[s appendFormat: @"<MeetingMessageType xmlns=\"Email2:\">%d</MeetingMessageType>", 1];
[s appendString: @"</MeetingRequest>"];
// ContentClass
[s appendFormat: @"<ContentClass xmlns=\"Email:\">%@</ContentClass>", @"urn:content-classes:calendarmessage"];
}
else
@ -476,19 +533,29 @@ struct GlobalObjectId {
if (d)
{
NSString *content;
int len;
int len, truncated;
content = [[NSString alloc] initWithData: d encoding: NSUTF8StringEncoding];
// FIXME: This is a hack. We should normally avoid doing this as we might get
// broken encodings. We should rather tell that the data was truncated and expect
// a ItemOperations call to download the whole base64 encoding multipart.
if (!content)
content = [[NSString alloc] initWithData: d encoding: NSISOLatin1StringEncoding];
AUTORELEASE(content);
content = [content activeSyncRepresentation];
truncated = 0;
len = [content length];
[s appendString: @"<Body xmlns=\"AirSyncBase:\">"];
[s appendFormat: @"<Type>%d</Type>", preferredBodyType];
[s appendFormat: @"<EstimatedDataSize>%d</EstimatedDataSize>", len];
[s appendFormat: @"<Truncated>%d</Truncated>", 0];
[s appendFormat: @"<Data>%@</Data>", content];
if (!truncated)
[s appendFormat: @"<Data>%@</Data>", content];
[s appendString: @"</Body>"];
}
@ -555,8 +622,7 @@ struct GlobalObjectId {
if ([o intValue])
[self addFlags: @"\\Flagged"];
else
[self removeFlags: @"\\Flagged"];
[self removeFlags: @"\\Flagged"];
}
}

View File

@ -63,6 +63,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
s = [NSMutableString string];
[s appendFormat: @"<AllDayEvent xmlns=\"Calendar:\">%d</AllDayEvent>", ([self isAllDay] ? 1 : 0)];
// DTStamp -- http://msdn.microsoft.com/en-us/library/ee219470(v=exchg.80).aspx
if ([self timeStampAsDate])
[s appendFormat: @"<DTStamp xmlns=\"Calendar:\">%@</DTStamp>", [[self timeStampAsDate] activeSyncRepresentationWithoutSeparators]];
@ -83,18 +85,31 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (!tz)
tz = [iCalTimeZone timeZoneForName: @"Europe/London"];
[s appendFormat: @"<TimeZone xmlns=\"Calendar:\">%@</TimeZone>", [[tz activeSyncRepresentation] stringByReplacingString: @"\n" withString: @""]];;
[s appendFormat: @"<TimeZone xmlns=\"Calendar:\">%@</TimeZone>", [tz activeSyncRepresentation]];
// Organizer
// Organizer and other invitations related properties
if ((organizer = [self organizer]))
{
o = [organizer rfc822Email];
if ([o length])
[s appendFormat: @"<Organizer_Email xmlns=\"Calendar:\">%@</Organizer_Email>", o];
o = [organizer cn];
if ([o length])
[s appendFormat: @"<Organizer_Name xmlns=\"Calendar:\">%@</Organizer_Name>", o];
{
[s appendFormat: @"<Organizer_Email xmlns=\"Calendar:\">%@</Organizer_Email>", o];
o = [organizer cn];
if ([o length])
[s appendFormat: @"<Organizer_Name xmlns=\"Calendar:\">%@</Organizer_Name>", o];
// This depends on the 'NEEDS-ACTION' parameter.
// This will trigger the SendMail command
[s appendFormat: @"<ResponseRequested xmlns=\"Calendar:\">%d</ResponseRequested>", 1];
[s appendFormat: @"<ResponseType xmlns=\"Calendar:\">%d</ResponseType>", 5];
[s appendFormat: @"<MeetingStatus xmlns=\"Calendar:\">%d</MeetingStatus>", 3];
[s appendFormat: @"<DisallowNewTimeProposal xmlns=\"Calendar:\">%d</DisallowNewTimeProposal>", 1];
// BusyStatus -- http://msdn.microsoft.com/en-us/library/ee202290(v=exchg.80).aspx
[s appendFormat: @"<BusyStatus xmlns=\"Calendar:\">%d</BusyStatus>", 2];
}
}
// Attendees
@ -168,9 +183,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[s appendFormat: @"<Sensitivity xmlns=\"Calendar:\">%d</Sensitivity>", v];
// BusyStatus -- http://msdn.microsoft.com/en-us/library/ee202290(v=exchg.80).aspx
[s appendFormat: @"<BusyStatus xmlns=\"Calendar:\">%d</BusyStatus>", 0];
// Reminder -- http://msdn.microsoft.com/en-us/library/ee219691(v=exchg.80).aspx
// TODO

View File

@ -189,6 +189,55 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
return s;
}
//
//
//
- (void) _setByDayFromValues: (NSDictionary *) theValues
{
NSString *day;
id o;
unsigned int day_of_week;
int i, week_of_month;
o = [theValues objectForKey: @"Recurrence_DayOfWeek"];
// The documentation says WeekOfMonth must be between 1 and 5. The value
// 5 means "last week of month"
week_of_month = [[theValues objectForKey: @"Recurrence_WeekOfMonth"] intValue];
if (week_of_month > 4)
week_of_month = -1;
// We find the correct day of the week
day_of_week = [o intValue];
for (i = 0; i < 7; i++)
{
if ((1<<i) == day_of_week)
{
day_of_week = i;
break;
}
}
day = [self iCalRepresentationForWeekDay: i];
[self setSingleValue: [NSString stringWithFormat: @"%d%@",
week_of_month, day]
forKey: @"byday"];
}
- (void) _setByMonthFromValues: (NSDictionary *) theValues
{
unsigned int month_of_year;
month_of_year = [[theValues objectForKey: @"Recurrence_MonthOfYear"] intValue];
[self setSingleValue: [NSString stringWithFormat: @"%d", month_of_year]
forKey: @"bymonth"];
}
//
//
//
@ -200,29 +249,95 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
recurrenceType = [[theValues objectForKey: @"Recurrence_Type"] intValue];
[self setInterval: @"1"];
switch (recurrenceType)
if ((o = [theValues objectForKey: @"Recurrence_Interval"]))
{
[self setRepeatInterval: [o intValue]];
}
else
[self setRepeatInterval: 1];
switch (recurrenceType)
{
//
// Daily
//
case 0:
[self setFrequency: iCalRecurrenceFrequenceDaily];
if ((o = [theValues objectForKey: @"Recurrence_Interval"]))
// Every weekday
if ((o = [theValues objectForKey: @"Recurrence_DayOfWeek"]))
{
[self setRepeatInterval: [o intValue]];
[self setByDayMask: [iCalByDayMask byDayMaskWithWeekDays]];
}
break;
//
// Weekly
//
case 1:
[self setFrequency: iCalRecurrenceFrequenceWeekly];
// 42 == Every Monday, Wednesday and Friday, for example
if ((o = [theValues objectForKey: @"Recurrence_DayOfWeek"]))
{
iCalWeekOccurrences days;
unsigned int i, v;
memset(days, 0, 7 * sizeof(iCalWeekOccurrence));
v = [o intValue];
for (i = 0; i < 7; i++)
{
if (v & (1<<i))
days[i] = iCalWeekOccurrenceAll;
}
[self setByDayMask: [iCalByDayMask byDayMaskWithDays: days]];
}
break;
//
// Montly
//
case 2:
case 3:
[self setFrequency: iCalRecurrenceFrequenceMonthly];
// The 5th of every X month(s)
if ((o = [theValues objectForKey: @"Recurrence_DayOfMonth"]))
{
[self setValues: [NSArray arrayWithObject: o]
atIndex: 0 forKey: @"bymonthday"];
}
// The 3rd Thursay every X month(s)
else if ((o = [theValues objectForKey: @"Recurrence_DayOfWeek"]))
{
[self _setByDayFromValues: theValues];
}
break;
//
// Yearly
//
case 5:
case 6:
default:
[self setFrequency: iCalRecurrenceFrequenceYearly];
// On April 19th
if ((o = [theValues objectForKey: @"Recurrence_DayOfMonth"]))
{
[self setValues: [NSArray arrayWithObject: o] atIndex: 0
forKey: @"bymonthday"];
[self _setByMonthFromValues: theValues];
}
else
{
// On the Second Wednesday of April
[self _setByDayFromValues: theValues];
[self _setByMonthFromValues: theValues];
}
break;
}

View File

@ -41,6 +41,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#import <NGExtensions/NGBase64Coding.h>
#include "NSData+ActiveSync.h"
struct SYSTEMTIME {
uint16_t wYear;
uint16_t wMonth;
@ -157,7 +159,7 @@ struct SYSTEMTIME {
[bytes appendBytes: &stDaylightDate length: 16];
[bytes appendBytes: &lDaylightBias length: 4];
return [bytes stringByEncodingBase64];
return [bytes activeSyncRepresentation];
}
@end