First pass at event invitations support + few bug fixes.
parent
d709a1216b
commit
9e9407cf30
|
@ -31,6 +31,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
#import <Foundation/NSArray.h>
|
||||
#import <Foundation/NSDictionary.h>
|
||||
#import <Foundation/NSString.h>
|
||||
|
||||
static NSArray *asElementArray = nil;
|
||||
|
||||
@implementation NGDOMElement (ActiveSync)
|
||||
|
||||
|
@ -44,12 +47,39 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
// </Flag>
|
||||
// </ApplicationData>
|
||||
//
|
||||
// and stuff like that:
|
||||
//
|
||||
// <Attendees xmlns="Calendar:">
|
||||
// <Attendee>
|
||||
// <Attendee_Email>sogo1@example.com</Attendee_Email>
|
||||
// <Attendee_Name>John Doe</Attendee_Name>
|
||||
// <Attendee_Status>5</Attendee_Status>
|
||||
// <Attendee_Type>1</Attendee_Type>
|
||||
// </Attendee>
|
||||
// <Attendee>
|
||||
// <Attendee_Email>sogo2@example.com</Attendee_Email>
|
||||
// <Attendee_Name>Balthazar César</Attendee_Name>
|
||||
// <Attendee_Status>5</Attendee_Status>
|
||||
// <Attendee_Type>1</Attendee_Type>
|
||||
// </Attendee>
|
||||
// <Attendee>
|
||||
// <Attendee_Email>sogo3@example.com</Attendee_Email>
|
||||
// <Attendee_Name>Wolfgang Fritz</Attendee_Name>
|
||||
// <Attendee_Status>5</Attendee_Status>
|
||||
// <Attendee_Type>1</Attendee_Type>
|
||||
// </Attendee>
|
||||
// </Attendees>
|
||||
//
|
||||
|
||||
- (NSDictionary *) applicationData
|
||||
{
|
||||
NSMutableDictionary *data;
|
||||
id <DOMNodeList> children;
|
||||
id <DOMElement> element;
|
||||
int i;
|
||||
int i, count;
|
||||
|
||||
if (!asElementArray)
|
||||
asElementArray = [[NSArray alloc] initWithObjects: @"Attendee", nil];
|
||||
|
||||
data = [NSMutableDictionary dictionary];
|
||||
|
||||
|
@ -65,10 +95,55 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
id value;
|
||||
|
||||
tag = [element tagName];
|
||||
count = [(NSArray *)[element childNodes] count];
|
||||
|
||||
// Handle inner data
|
||||
if ([(NSArray *)[element childNodes] count] > 2)
|
||||
value = [(NGDOMElement *)element applicationData];
|
||||
// Handle inner data - see above for samples
|
||||
if (count > 2)
|
||||
{
|
||||
NSMutableArray *innerElements;
|
||||
id <DOMElement> innerElement;
|
||||
NSArray *childNodes;
|
||||
NSString *innerTag;
|
||||
BOOL same;
|
||||
int j;
|
||||
|
||||
childNodes = (NSArray *)[element childNodes];
|
||||
innerElements = [NSMutableArray array];
|
||||
innerTag = nil;
|
||||
same = YES;
|
||||
|
||||
for (j = 1; j < count; j++)
|
||||
{
|
||||
innerElement = [childNodes objectAtIndex: j];
|
||||
|
||||
if ([innerElement nodeType] == DOM_ELEMENT_NODE)
|
||||
{
|
||||
if (!innerTag)
|
||||
innerTag = [innerElement tagName];
|
||||
|
||||
if ([innerTag isEqualToString: [innerElement tagName]])
|
||||
{
|
||||
[innerElements addObject: [(NGDOMElement *)innerElement applicationData]];
|
||||
}
|
||||
else
|
||||
{
|
||||
same = NO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (same && [asElementArray containsObject: innerTag])
|
||||
value = innerElements;
|
||||
else
|
||||
{
|
||||
value = [(NGDOMElement *)element applicationData];
|
||||
|
||||
// Don't set empty values like Foo = {}
|
||||
if (![value count])
|
||||
value = nil;
|
||||
}
|
||||
}
|
||||
else
|
||||
value = [[element firstChild] nodeValue];
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
#include "SOGoActiveSyncConstants.h"
|
||||
|
||||
@class NSCalendarDate;
|
||||
@class NSData;
|
||||
|
||||
@interface NSString (ActiveSync)
|
||||
|
||||
|
@ -43,6 +44,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
- (NSCalendarDate *) calendarDate;
|
||||
- (NSString *) deviceId;
|
||||
- (NSString *) command;
|
||||
- (NSData *) convertHexStringToBytes;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
#include <Foundation/NSArray.h>
|
||||
#include <Foundation/NSCalendarDate.h>
|
||||
#include <Foundation/NSData.h>
|
||||
#include <Foundation/NSDate.h>
|
||||
|
||||
#include <NGExtensions/NSString+misc.h>
|
||||
|
@ -53,30 +54,34 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
- (NSString *) realCollectionIdWithFolderType: (SOGoMicrosoftActiveSyncFolderType *) folderType;
|
||||
{
|
||||
NSString *realCollectionId;
|
||||
NSString *realCollectionId, *v;
|
||||
|
||||
*folderType = ActiveSyncGenericFolder;
|
||||
v = [self stringByUnescapingURL];
|
||||
|
||||
if ([self hasPrefix: @"vevent/"])
|
||||
if ([v hasPrefix: @"vevent/"])
|
||||
{
|
||||
realCollectionId = [self substringFromIndex: 7];
|
||||
realCollectionId = [v substringFromIndex: 7];
|
||||
*folderType = ActiveSyncEventFolder;
|
||||
}
|
||||
else if ([self hasPrefix: @"vtodo/"])
|
||||
else if ([v hasPrefix: @"vtodo/"])
|
||||
{
|
||||
realCollectionId = [self substringFromIndex: 6];
|
||||
realCollectionId = [v substringFromIndex: 6];
|
||||
*folderType = ActiveSyncTaskFolder;
|
||||
}
|
||||
else if ([self hasPrefix: @"vcard/"])
|
||||
else if ([v hasPrefix: @"vcard/"])
|
||||
{
|
||||
realCollectionId = [self substringFromIndex: 6];
|
||||
realCollectionId = [v substringFromIndex: 6];
|
||||
*folderType = ActiveSyncContactFolder;
|
||||
}
|
||||
else if ([v hasPrefix: @"mail/"])
|
||||
{
|
||||
realCollectionId = [[v stringByUnescapingURL] substringFromIndex: 5];
|
||||
*folderType = ActiveSyncMailFolder;
|
||||
}
|
||||
else
|
||||
{
|
||||
// mail/
|
||||
realCollectionId = [[self stringByUnescapingURL] substringFromIndex: 5];
|
||||
*folderType = ActiveSyncMailFolder;
|
||||
realCollectionId = nil;
|
||||
}
|
||||
|
||||
return realCollectionId;
|
||||
|
@ -150,4 +155,100 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
return s;
|
||||
}
|
||||
|
||||
//
|
||||
// FIXME: combine with our OpenChange code.
|
||||
//
|
||||
- (char) _decodeHexByte: (char) byteChar
|
||||
{
|
||||
char newByte;
|
||||
|
||||
if (byteChar >= 48 && byteChar <= 57)
|
||||
newByte = (uint8_t) byteChar - 48;
|
||||
else if (byteChar >= 65 && byteChar <= 70)
|
||||
newByte = (uint8_t) byteChar - 55;
|
||||
else if (byteChar >= 97 && byteChar <= 102)
|
||||
newByte = (uint8_t) byteChar - 87;
|
||||
else
|
||||
newByte = -1;
|
||||
|
||||
return newByte;
|
||||
}
|
||||
|
||||
//
|
||||
// FIXME: combine with our OpenChange code.
|
||||
//
|
||||
- (BOOL) _decodeHexByte: (uint8_t *) byte
|
||||
atPos: (NSUInteger) pos
|
||||
{
|
||||
BOOL error = NO;
|
||||
char newByte;
|
||||
unichar byteChar;
|
||||
|
||||
byteChar = [self characterAtIndex: pos];
|
||||
if (byteChar < 256)
|
||||
{
|
||||
newByte = [self _decodeHexByte: (char) byteChar];
|
||||
if (newByte == -1)
|
||||
error = YES;
|
||||
else
|
||||
*byte = newByte;
|
||||
}
|
||||
else
|
||||
error = YES;
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
//
|
||||
// FIXME: combine with our OpenChange code.
|
||||
//
|
||||
- (BOOL) _decodeHexPair: (uint8_t *) byte
|
||||
atPos: (NSUInteger) pos
|
||||
{
|
||||
BOOL error;
|
||||
uint8_t lowValue, highValue;
|
||||
|
||||
error = [self _decodeHexByte: &highValue atPos: pos];
|
||||
if (!error)
|
||||
{
|
||||
error = [self _decodeHexByte: &lowValue atPos: pos + 1];
|
||||
if (!error)
|
||||
*byte = highValue << 4 | lowValue;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
//
|
||||
// FIXME: combine with our OpenChange code.
|
||||
//
|
||||
- (NSData *) convertHexStringToBytes
|
||||
{
|
||||
NSUInteger count, strLen, bytesLen;
|
||||
uint8_t *bytes, *currentByte;
|
||||
NSData *decoded = nil;
|
||||
BOOL error = NO;
|
||||
|
||||
strLen = [self length];
|
||||
if ((strLen % 2) == 0)
|
||||
{
|
||||
bytesLen = strLen / 2;
|
||||
bytes = NSZoneCalloc (NULL, bytesLen, sizeof (uint8_t));
|
||||
currentByte = bytes;
|
||||
for (count = 0; !error && count < strLen; count += 2)
|
||||
{
|
||||
error = [self _decodeHexPair: currentByte atPos: count];
|
||||
currentByte++;
|
||||
}
|
||||
if (error)
|
||||
NSZoneFree (NULL, bytes);
|
||||
else
|
||||
decoded = [NSData dataWithBytesNoCopy: bytes
|
||||
length: bytesLen
|
||||
freeWhenDone: YES];
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -46,13 +46,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
#import <NGObjWeb/WORequest.h>
|
||||
#import <NGObjWeb/WOResponse.h>
|
||||
|
||||
|
||||
#import <NGCards/iCalCalendar.h>
|
||||
#import <NGCards/iCalEntityObject.h>
|
||||
#import <NGCards/iCalEvent.h>
|
||||
#import <NGCards/iCalToDo.h>
|
||||
#import <NGCards/NGVCard.h>
|
||||
|
||||
|
||||
#import <NGExtensions/NSCalendarDate+misc.h>
|
||||
#import <NGExtensions/NSString+misc.h>
|
||||
|
||||
|
@ -107,46 +106,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
@implementation SOGoActiveSyncDispatcher (Sync)
|
||||
|
||||
- (id) collectionFromId: (NSString *) theCollectionId
|
||||
type: (SOGoMicrosoftActiveSyncFolderType) theFolderType
|
||||
{
|
||||
id collection;
|
||||
|
||||
collection = nil;
|
||||
|
||||
switch (theFolderType)
|
||||
{
|
||||
case ActiveSyncContactFolder:
|
||||
{
|
||||
collection = [[context activeUser] personalContactsFolderInContext: context];
|
||||
}
|
||||
break;
|
||||
case ActiveSyncEventFolder:
|
||||
case ActiveSyncTaskFolder:
|
||||
{
|
||||
collection = [[context activeUser] personalCalendarFolderInContext: context];
|
||||
}
|
||||
break;
|
||||
case ActiveSyncMailFolder:
|
||||
default:
|
||||
{
|
||||
SOGoMailAccounts *accountsFolder;
|
||||
SOGoMailFolder *currentFolder;
|
||||
SOGoUserFolder *userFolder;
|
||||
|
||||
userFolder = [[context activeUser] homeFolderInContext: context];
|
||||
accountsFolder = [userFolder lookupName: @"Mail" inContext: context acquire: NO];
|
||||
currentFolder = [accountsFolder lookupName: @"0" inContext: context acquire: NO];
|
||||
|
||||
collection = [currentFolder lookupName: [NSString stringWithFormat: @"folder%@", theCollectionId]
|
||||
inContext: context
|
||||
acquire: NO];
|
||||
}
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
//
|
||||
// <?xml version="1.0"?>
|
||||
// <!DOCTYPE ActiveSync PUBLIC "-//MICROSOFT//DTD ActiveSync//EN" "http://www.microsoft.com/">
|
||||
|
|
|
@ -29,6 +29,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
*/
|
||||
#import <Foundation/NSObject.h>
|
||||
|
||||
#include "SOGoActiveSyncConstants.h"
|
||||
|
||||
@class NSException;
|
||||
|
||||
@interface SOGoActiveSyncDispatcher : NSObject
|
||||
|
@ -36,6 +38,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
id context;
|
||||
}
|
||||
|
||||
- (id) collectionFromId: (NSString *) theCollectionId
|
||||
type: (SOGoMicrosoftActiveSyncFolderType) theFolderType;
|
||||
|
||||
- (NSException *) dispatchRequest: (id) theRequest
|
||||
inResponse: (id) theResponse
|
||||
context: (id) theContext;
|
||||
|
|
|
@ -45,7 +45,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
#import <NGObjWeb/WORequest.h>
|
||||
#import <NGObjWeb/WOResponse.h>
|
||||
|
||||
|
||||
#import <NGCards/iCalCalendar.h>
|
||||
#import <NGCards/iCalEntityObject.h>
|
||||
#import <NGCards/iCalEvent.h>
|
||||
#import <NGCards/iCalToDo.h>
|
||||
|
@ -85,6 +85,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
#import <Appointments/SOGoAppointmentFolder.h>
|
||||
#import <Appointments/SOGoAppointmentFolders.h>
|
||||
#import <Appointments/SOGoAppointmentObject.h>
|
||||
|
||||
#import <Contacts/SOGoContactGCSFolder.h>
|
||||
#import <Contacts/SOGoContactFolders.h>
|
||||
|
@ -127,6 +128,49 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[[[context activeUser] userSettings] synchronize];
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
- (id) collectionFromId: (NSString *) theCollectionId
|
||||
type: (SOGoMicrosoftActiveSyncFolderType) theFolderType
|
||||
{
|
||||
id collection;
|
||||
|
||||
collection = nil;
|
||||
|
||||
switch (theFolderType)
|
||||
{
|
||||
case ActiveSyncContactFolder:
|
||||
{
|
||||
collection = [[context activeUser] personalContactsFolderInContext: context];
|
||||
}
|
||||
break;
|
||||
case ActiveSyncEventFolder:
|
||||
case ActiveSyncTaskFolder:
|
||||
{
|
||||
collection = [[context activeUser] personalCalendarFolderInContext: context];
|
||||
}
|
||||
break;
|
||||
case ActiveSyncMailFolder:
|
||||
default:
|
||||
{
|
||||
SOGoMailAccounts *accountsFolder;
|
||||
SOGoMailFolder *currentFolder;
|
||||
SOGoUserFolder *userFolder;
|
||||
|
||||
userFolder = [[context activeUser] homeFolderInContext: context];
|
||||
accountsFolder = [userFolder lookupName: @"Mail" inContext: context acquire: NO];
|
||||
currentFolder = [accountsFolder lookupName: @"0" inContext: context acquire: NO];
|
||||
|
||||
collection = [currentFolder lookupName: [NSString stringWithFormat: @"folder%@", theCollectionId]
|
||||
inContext: context
|
||||
acquire: NO];
|
||||
}
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
@ -524,11 +568,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
{
|
||||
EOQualifier *notDeletedQualifier, *sinceDateQualifier;
|
||||
NSString *collectionId, *realCollectionId;
|
||||
id currentFolder, currentCollection;
|
||||
SOGoMailAccounts *accountsFolder;
|
||||
SOGoUserFolder *userFolder;
|
||||
EOAndQualifier *qualifier;
|
||||
NSCalendarDate *filter;
|
||||
id currentCollection;
|
||||
NSMutableString *s;
|
||||
NSArray *uids;
|
||||
NSData *d;
|
||||
|
@ -541,15 +583,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
collectionId = [[(id)[theDocumentElement getElementsByTagName: @"CollectionId"] lastObject] textValue];
|
||||
realCollectionId = [collectionId realCollectionIdWithFolderType: &folderType];
|
||||
currentCollection = [self collectionFromId: realCollectionId type: folderType];
|
||||
|
||||
|
||||
userFolder = [[context activeUser] homeFolderInContext: context];
|
||||
accountsFolder = [userFolder lookupName: @"Mail" inContext: context acquire: NO];
|
||||
currentFolder = [accountsFolder lookupName: @"0" inContext: context acquire: NO];
|
||||
|
||||
currentCollection = [currentFolder lookupName: [NSString stringWithFormat: @"folder%@", realCollectionId]
|
||||
inContext: context
|
||||
acquire: NO];
|
||||
//
|
||||
// For IMAP, we simply build a request like this:
|
||||
//
|
||||
|
@ -677,12 +712,91 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
|
||||
//
|
||||
//
|
||||
// <?xml version="1.0"?>
|
||||
// <!DOCTYPE ActiveSync PUBLIC "-//MICROSOFT//DTD ActiveSync//EN" "http://www.microsoft.com/">
|
||||
// <MeetingResponse xmlns="MeetingResponse:">
|
||||
// <Request>
|
||||
// <UserResponse>1</UserResponse>
|
||||
// <CollectionId>mail%2FINBOX</CollectionId>
|
||||
// <RequestId>283</RequestId>
|
||||
// </Request>
|
||||
// </MeetingResponse>
|
||||
//
|
||||
- (void) processMeetingResponse: (id <DOMElement>) theDocumentElement
|
||||
inResponse: (WOResponse *) theResponse
|
||||
{
|
||||
NSString *realCollectionId, *requestId, *participationStatus;
|
||||
NSMutableString *s;
|
||||
NSData *d;
|
||||
|
||||
id collection;
|
||||
|
||||
SOGoMicrosoftActiveSyncFolderType folderType;
|
||||
int userResponse;
|
||||
int status;
|
||||
|
||||
s = [NSMutableString string];
|
||||
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];
|
||||
|
||||
//
|
||||
// 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...
|
||||
SOGoMailObject *mailObject;
|
||||
|
||||
mailObject = [collection lookupName: requestId
|
||||
inContext: context
|
||||
acquire: 0];
|
||||
|
||||
if (![mailObject isKindOfClass: [NSException class]])
|
||||
{
|
||||
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]]
|
||||
inContext: context
|
||||
acquire: NO];
|
||||
if (userResponse == 1)
|
||||
participationStatus = @"ACCEPTED";
|
||||
else if (userResponse == 2)
|
||||
participationStatus = @"TENTATIVE";
|
||||
else
|
||||
participationStatus = @"DECLINED";
|
||||
|
||||
[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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -710,22 +824,21 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
// FIXME
|
||||
if (srcFolderType == ActiveSyncMailFolder && dstFolderType == ActiveSyncMailFolder)
|
||||
{
|
||||
SOGoMailAccounts *accountsFolder;
|
||||
SOGoMailFolder *currentFolder;
|
||||
SOGoUserFolder *userFolder;
|
||||
NGImap4Client *client;
|
||||
id currentCollection;
|
||||
|
||||
NSDictionary *response;
|
||||
NSString *v;
|
||||
|
||||
userFolder = [[context activeUser] homeFolderInContext: context];
|
||||
accountsFolder = [userFolder lookupName: @"Mail" inContext: context acquire: NO];
|
||||
currentFolder = [accountsFolder lookupName: @"0" inContext: context acquire: NO];
|
||||
// userFolder = [[context activeUser] homeFolderInContext: context];
|
||||
// accountsFolder = [userFolder lookupName: @"Mail" inContext: context acquire: NO];
|
||||
// currentFolder = [accountsFolder lookupName: @"0" inContext: context acquire: NO];
|
||||
|
||||
currentCollection = [currentFolder lookupName: [NSString stringWithFormat: @"folder%@", srcFolderId]
|
||||
inContext: context
|
||||
acquire: NO];
|
||||
currentCollection = [self collectionFromId: srcFolderId type: srcFolderType];
|
||||
|
||||
// [currentFolder lookupName: [NSString stringWithFormat: @"folder%@", srcFolderId]
|
||||
// inContext: context
|
||||
// acquire: NO];
|
||||
|
||||
client = [[currentCollection imap4Connection] client];
|
||||
[client select: srcFolderId];
|
||||
|
@ -747,7 +860,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
addOrRemove: YES];
|
||||
|
||||
if ([[response valueForKey: @"result"] boolValue])
|
||||
[currentCollection expunge];
|
||||
[(SOGoMailFolder *)currentCollection expunge];
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -32,10 +32,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
#import <Mailer/SOGoMailObject.h>
|
||||
|
||||
@class iCalCalendar;
|
||||
@class NSDictionary;
|
||||
|
||||
@interface SOGoMailObject (ActiveSync)
|
||||
|
||||
- (iCalCalendar *) calendarFromIMIPMessage;
|
||||
- (NSString *) activeSyncRepresentation;
|
||||
- (void) takeActiveSyncValues: (NSDictionary *) theValues;
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
#import <NGCards/iCalEvent.h>
|
||||
#import <NGCards/iCalTimeZone.h>
|
||||
|
||||
|
||||
#import <NGExtensions/NGBase64Coding.h>
|
||||
#import <NGExtensions/NSString+misc.h>
|
||||
#import <NGExtensions/NSString+Encoding.h>
|
||||
|
@ -46,14 +45,118 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
#import <NGImap4/NGImap4EnvelopeAddress.h>
|
||||
#import <NGObjWeb/WOContext+SoObjects.h>
|
||||
|
||||
#include "iCalTimeZone+ActiveSync.h"
|
||||
#include "NSDate+ActiveSync.h"
|
||||
#include "NSString+ActiveSync.h"
|
||||
|
||||
#include "../SoObjects/Mailer/NSString+Mail.h"
|
||||
#include "../SoObjects/Mailer/SOGoMailBodyPart.h"
|
||||
|
||||
#include <SOGo/SOGoUser.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t dwLowDateTime;
|
||||
uint32_t dwHighDateTime;
|
||||
} FILETIME;
|
||||
|
||||
struct GlobalObjectId {
|
||||
uint8_t ByteArrayID[16];
|
||||
uint8_t YH;
|
||||
uint8_t YL;
|
||||
uint8_t Month;
|
||||
uint8_t D;
|
||||
FILETIME CreationTime;
|
||||
uint8_t X[8];
|
||||
uint32_t Size;
|
||||
uint8_t* Data;
|
||||
};
|
||||
|
||||
@implementation SOGoMailObject (ActiveSync)
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
- (void) _setInstanceDate: (struct GlobalObjectId *) newGlobalId
|
||||
fromDate: (NSCalendarDate *) instanceDate
|
||||
{
|
||||
uint16_t year;
|
||||
|
||||
if (instanceDate)
|
||||
{
|
||||
//[instanceDate setTimeZone: timeZone];
|
||||
year = [instanceDate yearOfCommonEra];
|
||||
newGlobalId->YH = year >> 8;
|
||||
newGlobalId->YL = year & 0xff;
|
||||
newGlobalId->Month = [instanceDate monthOfYear];
|
||||
newGlobalId->D = [instanceDate dayOfMonth];
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The GlobalObjId is documented here: http://msdn.microsoft.com/en-us/library/ee160198(v=EXCHG.80).aspx
|
||||
//
|
||||
- (NSData *) _computeGlobalObjectIdFromEvent: (iCalEvent *) event
|
||||
{
|
||||
NSData *binPrefix, *globalObjectId;
|
||||
NSString *prefix, *uid;
|
||||
|
||||
struct GlobalObjectId newGlobalId;
|
||||
const char *uidAsUTF8;
|
||||
|
||||
prefix = @"040000008200e00074c5b7101a82e008";
|
||||
|
||||
// dataPrefix is "vCal-Uid %x01 %x00 %x00 %x00"
|
||||
uint8_t dataPrefix[] = { 0x76, 0x43, 0x61, 0x6c, 0x2d, 0x55, 0x69, 0x64, 0x01, 0x00, 0x00, 0x00 };
|
||||
uid = [event uid];
|
||||
|
||||
binPrefix = [prefix convertHexStringToBytes];
|
||||
[binPrefix getBytes: &newGlobalId.ByteArrayID];
|
||||
[self _setInstanceDate: &newGlobalId
|
||||
fromDate: [event recurrenceId]];
|
||||
uidAsUTF8 = [uid UTF8String];
|
||||
|
||||
// 0x0c is the size of our dataPrefix
|
||||
newGlobalId.Size = 0x0c + strlen(uidAsUTF8);
|
||||
newGlobalId.Data = malloc(newGlobalId.Size * sizeof(uint8_t));
|
||||
memcpy(newGlobalId.Data, dataPrefix, 0x0c);
|
||||
memcpy(newGlobalId.Data + 0x0c, uidAsUTF8, newGlobalId.Size - 0x0c);
|
||||
|
||||
globalObjectId = [[NSData alloc] initWithBytes: &newGlobalId length: 40 + newGlobalId.Size*sizeof(uint8_t)];
|
||||
free(newGlobalId.Data);
|
||||
|
||||
return [globalObjectId autorelease];
|
||||
}
|
||||
|
||||
//
|
||||
// For debugging purposes...
|
||||
//
|
||||
// - (NSString *) _uidFromGlobalObjectId: (NSData *) objectId
|
||||
// {
|
||||
// NSString *uid;
|
||||
|
||||
// struct GlobalObjectId *newGlobalId;
|
||||
// NSUInteger length;
|
||||
// uint8_t *bytes;
|
||||
|
||||
// length = [objectId length];
|
||||
// uid = nil;
|
||||
|
||||
// bytes = malloc(length*sizeof(uint8_t));
|
||||
// [objectId getBytes: bytes length: length];
|
||||
|
||||
// newGlobalId = bytes;
|
||||
|
||||
// // We must take the offset (dataPrefix) into account
|
||||
// uid = [[NSString alloc] initWithBytes: newGlobalId->Data+12 length: newGlobalId->Size-12 encoding: NSUTF8StringEncoding];
|
||||
// free(bytes);
|
||||
|
||||
// return AUTORELEASE(uid);
|
||||
// }
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
- (NSString *) _emailAddressesFrom: (NSArray *) enveloppeAddresses
|
||||
{
|
||||
NSMutableArray *addresses;
|
||||
|
@ -183,15 +286,57 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
return d;
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
- (iCalCalendar *) calendarFromIMIPMessage
|
||||
{
|
||||
NSDictionary *part;
|
||||
NSArray *parts;
|
||||
int i;
|
||||
|
||||
// We check if we have at least 2 parts and if one of them is a text/calendar
|
||||
parts = [[self bodyStructure] objectForKey: @"parts"];
|
||||
|
||||
if ([parts count] > 1)
|
||||
{
|
||||
for (i = 0; i < [parts count]; i++)
|
||||
{
|
||||
part = [parts objectAtIndex: i];
|
||||
|
||||
if ([[part objectForKey: @"type"] isEqualToString: @"text"] &&
|
||||
[[part objectForKey: @"subtype"] isEqualToString: @"calendar"])
|
||||
{
|
||||
id bodyPart;
|
||||
|
||||
bodyPart = [self lookupImap4BodyPartKey: [NSString stringWithFormat: @"%d", i+1]
|
||||
inContext: self->context];
|
||||
|
||||
if (bodyPart)
|
||||
{
|
||||
NSData *calendarData;
|
||||
|
||||
calendarData = [bodyPart fetchBLOB];
|
||||
return [iCalCalendar parseSingleFromSource: calendarData];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
- (NSString *) activeSyncRepresentation
|
||||
{
|
||||
NSData *d, *globalObjId;
|
||||
NSMutableString *s;
|
||||
NSData *d;
|
||||
id value;
|
||||
|
||||
iCalCalendar *calendar;
|
||||
|
||||
int preferredBodyType, nativeBodyType;
|
||||
|
||||
s = [NSMutableString string];
|
||||
|
@ -234,58 +379,63 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
// Read
|
||||
[s appendFormat: @"<Read xmlns=\"Email:\">%d</Read>", ([self read] ? 1 : 0)];
|
||||
|
||||
// MesssageClass
|
||||
[s appendFormat: @"<MessageClass xmlns=\"Email:\">%@</MessageClass>", @"IPM.Note"];
|
||||
//[s appendFormat: @"<MessageClass xmlns=\"Email:\">%@</MessageClass>", @"IPM.Schedule.Meeting.Request"];
|
||||
|
||||
//
|
||||
// TEST
|
||||
//
|
||||
#if 0
|
||||
id foo = [self lookupImap4BodyPartKey: @"2" inContext: self->context];
|
||||
NSData *calendarData;
|
||||
iCalCalendar *calendar;
|
||||
iCalEvent *event;
|
||||
// We handle MeetingRequest
|
||||
calendar = [self calendarFromIMIPMessage];
|
||||
|
||||
calendarData = [foo fetchBLOB];
|
||||
calendar = [iCalCalendar parseSingleFromSource: calendarData];
|
||||
event = [[calendar events] lastObject];
|
||||
if (calendar)
|
||||
{
|
||||
iCalTimeZone *tz;
|
||||
iCalEvent *event;
|
||||
|
||||
if ([event timeStampAsDate])
|
||||
[s appendFormat: @"<DTStamp xmlns=\"Calendar:\">%@</DTStamp>", [[event timeStampAsDate] activeSyncRepresentation]];
|
||||
else if ([event created])
|
||||
[s appendFormat: @"<DTStamp xmlns=\"Calendar:\">%@</DTStamp>", [[event created] activeSyncRepresentation]];
|
||||
event = [[calendar events] lastObject];
|
||||
|
||||
[s appendString: @"<MeetingRequest xmlns=\"Email:\">"];
|
||||
[s appendString: @"<MeetingRequest xmlns=\"Email:\">"];
|
||||
|
||||
// StartTime -- http://msdn.microsoft.com/en-us/library/ee157132(v=exchg.80).aspx
|
||||
if ([event startDate])
|
||||
[s appendFormat: @"<StartTime xmlns=\"Email:\">%@</StartTime>", [[event startDate] activeSyncRepresentation]];
|
||||
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]];
|
||||
|
||||
// EndTime -- http://msdn.microsoft.com/en-us/library/ee157945(v=exchg.80).aspx
|
||||
if ([event endDate])
|
||||
[s appendFormat: @"<EndTime xmlns=\"Email:\">%@</EndTime>", [[event endDate] activeSyncRepresentation]];
|
||||
// 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]];
|
||||
|
||||
// Timezone
|
||||
iCalTimeZone *tz;
|
||||
// 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]];
|
||||
|
||||
tz = [(iCalDateTime *)[event firstChildWithTag: @"dtstart"] timeZone];
|
||||
// Timezone
|
||||
tz = [(iCalDateTime *)[event firstChildWithTag: @"dtstart"] timeZone];
|
||||
|
||||
if (!tz)
|
||||
tz = [iCalTimeZone timeZoneForName: @"Europe/London"];
|
||||
if (!tz)
|
||||
tz = [iCalTimeZone timeZoneForName: @"Europe/London"];
|
||||
|
||||
[s appendFormat: @"<TimeZone xmlns=\"Email:\">%@</TimeZone>", [[tz activeSyncRepresentation] stringByReplacingString: @"\n" withString: @""]];;
|
||||
[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>", @"sogo3@example.com"];
|
||||
[s appendFormat: @"<ResponseRequested xmlns=\"Email:\">%d</ResponseRequested>", 1];
|
||||
[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];
|
||||
|
||||
[s appendString: @"</MeetingRequest>"];
|
||||
#endif
|
||||
//
|
||||
// TEST
|
||||
//
|
||||
// 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]];
|
||||
[s appendString: @"</MeetingRequest>"];
|
||||
|
||||
// MesssageClass and ContentClass
|
||||
[s appendFormat: @"<MessageClass xmlns=\"Email:\">%@</MessageClass>", @"IPM.Schedule.Meeting.Request"];
|
||||
[s appendFormat: @"<ContentClass xmlns=\"Email:\">%@</ContentClass>", @"urn:content-classes:calendarmessage"];
|
||||
}
|
||||
else
|
||||
{
|
||||
// MesssageClass and ContentClass
|
||||
[s appendFormat: @"<MessageClass xmlns=\"Email:\">%@</MessageClass>", @"IPM.Note"];
|
||||
[s appendFormat: @"<ContentClass xmlns=\"Email:\">%@</ContentClass>", @"urn:content-classes:message"];
|
||||
}
|
||||
|
||||
// Reply-To - FIXME
|
||||
//NSArray *replyTo = [[message objectForKey: @"envelope"] replyTo];
|
||||
|
@ -349,10 +499,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[s appendString: @"</Attachments>"];
|
||||
}
|
||||
|
||||
// ContentClass
|
||||
[s appendFormat: @"<ContentClass xmlns=\"Email:\">%@</ContentClass>", @"urn:content-classes:message"];
|
||||
//[s appendFormat: @"<ContentClass xmlns=\"Email:\">%@</ContentClass>", @"urn:content-classes:calendarmessage"];
|
||||
|
||||
// Flags
|
||||
[s appendString: @"<Flag xmlns=\"Email:\">"];
|
||||
[s appendFormat: @"<FlagStatus>%d</FlagStatus>", 0];
|
||||
|
|
|
@ -204,6 +204,38 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
// http://blogs.msdn.com/b/exchangedev/archive/2011/07/22/working-with-meeting-requests-in-exchange-activesync.aspx
|
||||
// http://blogs.msdn.com/b/exchangedev/archive/2011/07/29/working-with-meeting-responses-in-exchange-activesync.aspx
|
||||
//
|
||||
//
|
||||
// Here is an example of a Sync call when sogo10 accepts an invitation from sogo3:
|
||||
//
|
||||
// <Change>
|
||||
// <ServerId>2978-52EA9D00-1-A253E70.ics</ServerId>
|
||||
// <ApplicationData>
|
||||
// <TimeZone xmlns="Calendar:">LAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==</TimeZone>
|
||||
// <AllDayEvent xmlns="Calendar:">0</AllDayEvent>
|
||||
// <StartTime xmlns="Calendar:">20140207T130000Z</StartTime>
|
||||
// <EndTime xmlns="Calendar:">20140207T140000Z</EndTime>
|
||||
// <DTStamp xmlns="Calendar:">20140130T185245Z</DTStamp>
|
||||
// <Subject xmlns="Calendar:">test 8</Subject>
|
||||
// <Sensitivity xmlns="Calendar:">0</Sensitivity>
|
||||
// <Body xmlns="AirSyncBase:">
|
||||
// <Type>1</Type>
|
||||
// <Data/>
|
||||
// </Body>
|
||||
// <Organizer_Email xmlns="Calendar:">sogo3@example.com</Organizer_Email>
|
||||
// <UID xmlns="Calendar:">2978-52EA9D00-1-A253E70</UID>
|
||||
// <Attendees xmlns="Calendar:">
|
||||
// <Attendee>
|
||||
// <Attendee_Name>sogo10</Attendee_Name>
|
||||
// <Attendee_Email>sogo10@example.com</Attendee_Email>
|
||||
// <Attendee_Type>1</Attendee_Type>
|
||||
// </Attendee>
|
||||
// </Attendees>
|
||||
// <BusyStatus xmlns="Calendar:">2</BusyStatus>
|
||||
// <MeetingStatus xmlns="Calendar:">3</MeetingStatus>
|
||||
// <Organizer_Name xmlns="Calendar:">Wolfgang Fritz</Organizer_Name>
|
||||
// </ApplicationData>
|
||||
// </Change>
|
||||
//
|
||||
- (void) takeActiveSyncValues: (NSDictionary *) theValues
|
||||
{
|
||||
iCalDateTime *start, *end;
|
||||
|
@ -331,6 +363,66 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
[rule takeActiveSyncValues: o];
|
||||
}
|
||||
|
||||
// Organizer
|
||||
if ((o = [theValues objectForKey: @"Organizer_Email"]))
|
||||
{
|
||||
iCalPerson *person;
|
||||
|
||||
person = [iCalPerson elementWithTag: @"organizer"];
|
||||
[person setEmail: o];
|
||||
[person setCn: [theValues objectForKey: @"Organizer_Name"]];
|
||||
[person setPartStat: @"ACCEPTED"];
|
||||
[self setOrganizer: person];
|
||||
}
|
||||
|
||||
// Attendees
|
||||
if ((o = [theValues objectForKey: @"Attendees"]))
|
||||
{
|
||||
NSMutableArray *attendees;
|
||||
NSDictionary *attendee;
|
||||
iCalPerson *person;
|
||||
int status, i;
|
||||
|
||||
attendees = [NSMutableArray array];
|
||||
|
||||
for (i = 0; i < [o count]; i++)
|
||||
{
|
||||
// Each attendee has is a dictionary similar to this:
|
||||
// { "Attendee_Email" = "sogo3@example.com"; "Attendee_Name" = "Wolfgang Fritz"; "Attendee_Status" = 5; "Attendee_Type" = 1; }
|
||||
attendee = [o objectAtIndex: i];
|
||||
|
||||
person = [iCalPerson elementWithTag: @"attendee"];
|
||||
[person setCn: [attendee objectForKey: @"Attendee_Name"]];
|
||||
[person setEmail: [attendee objectForKey: @"Attendee_Email"]];
|
||||
|
||||
status = [[attendee objectForKey: @"Attendee_Status"] intValue];
|
||||
|
||||
switch (status)
|
||||
{
|
||||
case 2:
|
||||
[person setPartStat: @"TENTATIVE"];
|
||||
break;
|
||||
case 3:
|
||||
[person setPartStat: @"ACCEPTED"];
|
||||
break;
|
||||
case 4:
|
||||
[person setPartStat: @"DECLINED"];
|
||||
break;
|
||||
case 0:
|
||||
case 5:
|
||||
default:
|
||||
[person setPartStat: @"NEEDS-ACTION"];
|
||||
break;
|
||||
}
|
||||
|
||||
// FIXME: handle Attendee_Type
|
||||
|
||||
[attendees addObject: person];
|
||||
}
|
||||
|
||||
[self setAttendees: attendees];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -42,14 +42,14 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
#import <NGExtensions/NGBase64Coding.h>
|
||||
|
||||
struct SYSTEMTIME {
|
||||
uint16_t wYear;
|
||||
uint16_t wMonth;
|
||||
uint16_t wDayOfWeek;
|
||||
uint16_t wDay;
|
||||
uint16_t wHour;
|
||||
uint16_t wMinute;
|
||||
uint16_t wSecond;
|
||||
uint16_t wMilliseconds;
|
||||
uint16_t wYear;
|
||||
uint16_t wMonth;
|
||||
uint16_t wDayOfWeek;
|
||||
uint16_t wDay;
|
||||
uint16_t wHour;
|
||||
uint16_t wMinute;
|
||||
uint16_t wSecond;
|
||||
uint16_t wMilliseconds;
|
||||
};
|
||||
|
||||
@interface iCalTimeZonePeriod (ActiveSync)
|
||||
|
|
Loading…
Reference in New Issue