Outlook 2007 Timezone Structures
[This is now documented here: https://msdn.microsoft.com/en-us/library/ff960198.aspx, among other articles ]
Topic
Properties used by Outlook 2007 to maintain timezone information on appointments.
Timezones
Historically Outlook has maintained a property, dispidTimeZoneStruct, on recurring appointments which describes the time zone in which the appointment was created. The problem with this system is that it ignores the possibility that time zone rules can change over time. Since Outlook could potentially use old rules when computing the times of meetings, some of the meetings that users scheduled before the rules change in the registry will now occur at incorrect times. Additionally, non-recurring meetings had no timezone information stamped on them at all. Because of this Microsoft is writing a rebasing tool to help users adjust the times at which these meetings will occur. Documentation for that tool will appear separately.
The information in this document will help developers who are attempting to write their own rebasing tool. Note that while this information documents direct manipulation of timezone structures on appointments by MAPI, it should not be construed as full support for manipulation of time and recurrence information through MAPI. CDO and the Outlook Object Model are still recommended for manipulation of time and recurrence information.
In the context of this documentation, “older” and “legacy” clients are defined as Outlook 2003 and earlier, and any version of CDO 1.21 prior to the upcoming timezone update. New clients are defined as Outlook 2007 and higher and any version of CDO 1.21 after the upcoming timezone update.
Definitions
#define dispidTimeZoneStruct 0x8233 // legacy timezone property
#define dispidApptTZDefStartDisplay 0x825E // timezone that was used when picking start time
#define dispidApptTZDefEndDisplay 0x825F // timezone that was used when picking end time
#define dispidApptTZDefRecur 0x8260 // timezone for recurring meeting expansion
DEFINE_OLEGUID(PSETID_Appointment, MAKELONG(0x2000+(0x02),0x0006),0,0);
// TZREG
// =====================
// This is an individual description that defines when a daylight
// saving shift, and the return to standard time occurs, and how
// far the shift is. This is basically the same as
// TIME_ZONE_INFORMATION documented in MSDN, except that the strings
// describing the names "daylight" and "standard" time are omitted.
//
typedef struct RenTimeZone
{
long lBias; // offset from GMT
long lStandardBias; // offset from bias during standard time
long lDaylightBias; // offset from bias during daylight time
SYSTEMTIME stStandardDate; // time to switch to standard time
SYSTEMTIME stDaylightDate; // time to switch to daylight time
} TZREG;
// TZRULE
// =====================
// This structure represents both a description when a daylight.
// saving shift occurs, and in addition, the year in which that
// timezone rule came into effect.
//
typedef struct
{
WORD wFlags; // indicates which rule matches legacy recur
SYSTEMTIME stStart; // indicates when the rule starts
TZREG TZReg; // the timezone info
} TZRULE;
const WORD TZRULE_FLAG_RECUR_CURRENT_TZREG = 0x0001; // see dispidApptTZDefRecur
const WORD TZRULE_FLAG_EFFECTIVE_TZREG = 0x0002;
// TZDEFINITION
// =====================
// This represents an entire timezone including all historical, current
// and future timezone shift rules for daylight saving time, etc. It's
// identified by a unique GUID.
//
typedef struct
{
WORD wFlags; // indicates which fields are valid
GUID guidTZID; // guid uniquely identifying this timezone
LPWSTR pwszKeyName; // the name of the key for this timezone
WORD cRules; // the number of timezone rules for this definition
TZRULE* rgRules; // an array of rules describing when shifts occur
} TZDEFINITION;
const WORD TZDEFINITION_FLAG_VALID_GUID = 0x0001; // the guid is valid
const WORD TZDEFINITION_FLAG_VALID_KEYNAME = 0x0002; // the keyname is valid
const ULONG TZ_MAX_RULES = 1024;
const BYTE TZ_BIN_VERSION_MAJOR = 0x02;
const BYTE TZ_BIN_VERSION_MINOR = 0x01;
Properties
These are named properties in the PSETID_Appointment property set. They are all of the type PT_BINARY, each containing a stream with the persisted format of their respective types. The persistence details are below.
dispidTimeZoneStruct
Legacy versions of Outlook store the start and end time of single instance meetings in UTC time. The start and end time of recurring meetings are stored as relative time with a timezone description in the TZREG format. A copy of the timezone description is available in dispidTimeZoneStruct. Third party code should consider this property to be strictly read only. Attempts to write this property can and will corrupt appointments.
This property contains a persisted TZREG structure.
dispidApptTZDefStartDisplay
This represents the timezone that the user was in or picked when picking the start time of the meeting. This is used to show the meeting in the original time it was scheduled and also to determine if the meeting should be adjusted if the definition of a timezone changes.
If this property is missing, the current local timezone is assumed.
This property is used for display purposes only and is not used in recurrence expansion.
This property contains a persisted TZDEFINITION structure.
dispidApptTZDefEndDisplay
This represents the timezone that the user was using, or picked when picking the end time of the meeting.
This property is for display purposes only and is not used in recurrence expansion.
If this property is missing or is invalid, then the dispidApptTZDefStartDisplay is considered instead. If that property is also missing or is invalid, then the current local timezone is assumed.
This property is used for display purposes only and is not used in recurrence expansion.
This property contains a persisted TZDEFINITION structure.
dispidApptTZDefRecur
This property is used for multi-rule recurrence expansion and to determine if the recurring meeting should be adjusted if the definition of a timezone changes.
This property needs to be kept in sync with dispidTimeZoneStruct since older clients may still manipulate dispidTimeZoneStruct. To detect if the two properties are in sync, the wFlags parameter for the rule that matches dispidTimeZoneStruct should have the TZRULE_FLAG_RECUR_CURRENT_TZREG flag set. If this flag is not set, or it is set and the rule in dispidTimeZoneStruct does not match the marked rule, then dispidApptTZDefRecur should be discarded and dispidTimeZoneStruct instead should be used.
When writing both dispidApptTZDefRecur and dispidTimeZoneStruct to a new recurring meeting, when an arbitrary choice needs to be made to pick dispidTimeZoneStruct, the current definition for the timezone (according to the windows registry) should be written to dispidTimeZoneStruct.
This property contains a persisted TZDEFINITION structure.
Structures
TZREG
This is the structure used by legacy clients to store time zone information for recurring meetings. The members of this structure are similar to and derived from TIME_ZONE_INFORMATION.
TZRULE
This structure augments TZREG by providing additional information indicating when time zone rules take effect
The stStart value in the TZRULE structure indicates the time in GMT that the rule took effect.
There are two valid wFlags in the TZRULE structure:
TZRULE_FLAG_EFFECTIVE_TZREG – this marks one of the rules as the rule that should currently be used. Only one rule can be marked as the “effective” rule. All other rules are for comparison purposes only.
TZRULE_FLAG_RECUR_CURRENT_TZREG – on recurring meetings, this marks one of the rules as matching the rule in “dispidTimeZoneStruct”. This can be used to detect if dispidTimeZoneStruct has been significantly modified by a legacy client which would be otherwise unaware of the new more complete property.
TZDEFINITION
This structure fully describes a timezone with multiple rules.
The wFlags in the TZDEFINITION structure indicates if the guid and/or the key name fields are valid. For now, TZDEFINITION_FLAG_VALID_GUID should not be set. Instead, TZDEFINITION_FLAG_VALID_KEYNAME should be set and the key name field should be set to the registry key name for the timezone. These registry key names should not be localized and have a maximum size of MAX_PATH.
If two structures both have guids, then guids are the preferred way to determine if two timezones are the same. If one does not have a guid, then the keyname is used to determine if the timezones are the same. There MUST be at least a keyname.
The first rule in the list or rules is special and is considered to be the rule to use until the second rule starts regardless of the stStart on the first rule.
The rules should be sorted from oldest to newest. There is no overlap allowed between rules and so a prior rule is deemed to end when a new rule starts. Also, there MUST be at least one rule.
Persisting TZREG to a stream
Care must be taken when persisting TZREG to a stream for commitment to a binary property. The following describes the little endian format for persisting the structure
long lBias; // offset from GMT.
long lStandardBias; // offset from bias during standard time.
long lDaylightBias; // offset from bias during daylight time.
WORD wReserved1; // reserved
SYSTEMTIME stStandardDate; // time to switch to standard time.
WORD wReserved2; // reserved
SYSTEMTIME stDaylightDate; // time to switch to daylight time.
Note that the reserved WORD members do not map to any component of the TZREG structure. When parsing the persisted form of the structure, they should be ignored. When writing the persisted form of the structure, they should be NULL.
Persisting TZDEFINITION to a stream
Care must be taken when persisting TZDEFINITION to a stream for commitment to a binary property. The following describes the little endian format for persisting the structure
BYTE bMajorVersion; // breaking change
BYTE bMinorVersion; // extensibility
WORD cbHeader; // size of following data up until rule data
WORD wFlags; // TZDEFINITION_FLAG
if (TZDEFINITION_FLAG_VALID_GUID)
GUID guid; // guid
if (TZDEFINITION_FLAG_VALID_KEYNAME)
WORD cchKeyName; // does not include null char
WCHAR rgchKeyName; // not null terminated
WORD cRules; // number of rules
for each rule
BYTE bMajorVersion; // breaking change
BYTE bMinorVersion; // extensibility
WORD cbRule; // size of following data
WORD wFlags; // flags
SYSTEMTIME stStart; // GMT when this rule started
// Following are the fields of the TZREG sub structure
long lBias; // offset from GMT.
long lStandardBias; // offset from bias during standard time.
long lDaylightBias; // offset from bias during daylight time.
SYSTEMTIME stStandardDate; // time to switch to standard time.
SYSTEMTIME stDaylightDate; // time to switch to daylight time.
If the appropriate TZEFINITION_FLAG_VALID_GUID is not set, then the guid is not present in the stream. Likewise the key name is also not present unless the appropriate flag is set, although the key name should be persisted for the foreseeable future. The key name will have a maximum size of MAX_PATH.
If a parser does not understand the major version of the header, it should not read the rest of the structure and behave as if the data is missing.
If a parser does not understand the minor version of the header, it should read the portions of the stream that it understands, and should use the cbHeader to skip past the portions that it does not understand.
If a parser does not recognize the major version of a rule, the client should skip past the future rule using the cbRule, and then try to parse the rule it was looking for at as if it were at the next location.
If a parser does not recognize the minor version of a block, the client should only parse the parts of the rule that it understands, and use cbHeader/cbRule to skip past the data it does not understand.
When persisting blocks back as a modification, a parser should not try to write any information it does not understand – it should wipe out the information it does not understand.
There is a limit to the number of rules, being 1024.
The major version number is used to make a breaking change. Clients that are unfamiliar with the major version number should treat the property as if it is not there. Clients writing the structure should use TZ_BIN_VERSION_MAJOR.
The minor version number is used for extensibility. Clients that are unfamiliar with the minor version number should read the data that they understand, and skip over the data that might be appended to each rule or the overall stream. Clients writing the structure should use TZ_BIN_VERSION_MINOR.
Note that the TZREG structure is persisted here differently than when persisted alone, so the same code cannot be used to parse it.
Example code
The following code samples illustrate one way to read the TZREG and TZDEFINITION structures from their persisted formats: Code for writing to the perisisted formats is not provided but should be easily derived from the provided examples.
// Allocates return value with new.
// clean up with delete.
TZREG* BinToTZREG(ULONG cbReg, LPBYTE lpbReg)
{
if (!lpbReg) return NULL;
// Update this if parsing code is changed!
if (cbReg < 3*sizeof(long) + 2*sizeof(WORD) + 2*sizeof(SYSTEMTIME)) return NULL;
TZREG tzReg = {0};
LPBYTE lpPtr = lpbReg;
tzReg.lBias = *((long*)lpPtr);
lpPtr += sizeof(long);
tzReg.lStandardBias = *((long*)lpPtr);
lpPtr += sizeof(long);
tzReg.lDaylightBias = *((long*)lpPtr);
lpPtr += sizeof(long);
lpPtr += sizeof(WORD);// reserved
tzReg.stStandardDate = *((SYSTEMTIME*)lpPtr);
lpPtr += sizeof(SYSTEMTIME);
lpPtr += sizeof(WORD);// reserved
tzReg.stDaylightDate = *((SYSTEMTIME*)lpPtr);
lpPtr += sizeof(SYSTEMTIME);
TZREG* ptzReg = NULL;
ptzReg = new TZREG;
if (ptzReg)
{
*ptzReg = tzReg;
}
return ptzReg;
}
// Allocates return value with new.
// clean up with delete[].
TZDEFINITION* BinToTZDEFINITION(ULONG cbDef, LPBYTE lpbDef)
{
if (!lpbDef) return NULL;
// Update this if parsing code is changed!
// this checks the size up to the flags member
if (cbDef < 2*sizeof(BYTE) + 2*sizeof(WORD)) return NULL;
TZDEFINITION tzDef = {0};
TZRULE* lpRules = NULL;
LPBYTE lpPtr = lpbDef;
WORD cchKeyName = NULL;
WCHAR* szKeyName = NULL;
WORD i = 0;
BYTE bMajorVersion = *((BYTE*)lpPtr);
lpPtr += sizeof(BYTE);
BYTE bMinorVersion = *((BYTE*)lpPtr);
lpPtr += sizeof(BYTE);
// We only understand TZ_BIN_VERSION_MAJOR
if (TZ_BIN_VERSION_MAJOR != bMajorVersion) return NULL;
// We only understand if >= TZ_BIN_VERSION_MINOR
if (TZ_BIN_VERSION_MINOR > bMinorVersion) return NULL;
WORD cbHeader = *((WORD*)lpPtr);
lpPtr += sizeof(WORD);
tzDef.wFlags = *((WORD*)lpPtr);
lpPtr += sizeof(WORD);
if (TZDEFINITION_FLAG_VALID_GUID & tzDef.wFlags)
{
if (lpbDef + cbDef - lpPtr < sizeof(GUID)) return NULL;
tzDef.guidTZID = *((GUID*)lpPtr);
lpPtr += sizeof(GUID);
}
if (TZDEFINITION_FLAG_VALID_KEYNAME & tzDef.wFlags)
{
if (lpbDef + cbDef - lpPtr < sizeof(WORD)) return NULL;
cchKeyName = *((WORD*)lpPtr);
lpPtr += sizeof(WORD);
if (cchKeyName)
{
if (lpbDef + cbDef - lpPtr < (BYTE)sizeof(WORD)*cchKeyName) return NULL;
szKeyName = (WCHAR*)lpPtr;
lpPtr += cchKeyName*sizeof(WORD);
}
}
if (lpbDef+ cbDef - lpPtr < sizeof(WORD)) return NULL;
tzDef.cRules = *((WORD*)lpPtr);
lpPtr += sizeof(WORD);
if (tzDef.cRules)
{
lpRules = new TZRULE[tzDef.cRules];
if (!lpRules) return NULL;
LPBYTE lpNextRule = lpPtr;
BOOL bRuleOK = false;
for (i = 0;i < tzDef.cRules;i++)
{
bRuleOK = false;
lpPtr = lpNextRule;
if (lpbDef + cbDef - lpPtr <
2*sizeof(BYTE) + 2*sizeof(WORD) + 3*sizeof(long) + 2*sizeof(SYSTEMTIME)) return NULL;
bRuleOK = true;
BYTE bRuleMajorVersion = *((BYTE*)lpPtr);
lpPtr += sizeof(BYTE);
BYTE bRuleMinorVersion = *((BYTE*)lpPtr);
lpPtr += sizeof(BYTE);
// We only understand TZ_BIN_VERSION_MAJOR
if (TZ_BIN_VERSION_MAJOR != bRuleMajorVersion) return NULL;
// We only understand if >= TZ_BIN_VERSION_MINOR
if (TZ_BIN_VERSION_MINOR > bRuleMinorVersion) return NULL;
WORD cbRule = *((WORD*)lpPtr);
lpPtr += sizeof(WORD);
lpNextRule = lpPtr + cbRule;
lpRules[i].wFlags = *((WORD*)lpPtr);
lpPtr += sizeof(WORD);
lpRules[i].stStart = *((SYSTEMTIME*)lpPtr);
lpPtr += sizeof(SYSTEMTIME);
lpRules[i].TZReg.lBias = *((long*)lpPtr);
lpPtr += sizeof(long);
lpRules[i].TZReg.lStandardBias = *((long*)lpPtr);
lpPtr += sizeof(long);
lpRules[i].TZReg.lDaylightBias = *((long*)lpPtr);
lpPtr += sizeof(long);
lpRules[i].TZReg.stStandardDate = *((SYSTEMTIME*)lpPtr);
lpPtr += sizeof(SYSTEMTIME);
lpRules[i].TZReg.stDaylightDate = *((SYSTEMTIME*)lpPtr);
lpPtr += sizeof(SYSTEMTIME);
}
if (!bRuleOK)
{
delete[] lpRules;
return NULL;
}
}
// Now we've read everything - allocate a structure and copy it in
size_t cbTZDef = sizeof(TZDEFINITION) +
sizeof(WCHAR)*(cchKeyName+1) +
sizeof(TZRULE)*tzDef.cRules;
TZDEFINITION* ptzDef = (TZDEFINITION*) new BYTE[cbTZDef];
if (ptzDef)
{
// Copy main struct over
*ptzDef = tzDef;
lpPtr = (LPBYTE) ptzDef;
lpPtr += sizeof(TZDEFINITION);
if (szKeyName)
{
ptzDef->pwszKeyName = (WCHAR*)lpPtr;
memcpy(lpPtr,szKeyName,cchKeyName*sizeof(WCHAR));
ptzDef->pwszKeyName[cchKeyName] = 0;
lpPtr += (cchKeyName+1)*sizeof(WCHAR);
}
if (ptzDef -> cRules)
{
ptzDef -> rgRules = (TZRULE*)lpPtr;
for (i = 0;i < ptzdef -> cRules;i++)
{
ptzDef -> rgRules[i] = lpRules[i];
}
}
}
delete[] lpRules;
return ptzDef;
}
The following code sample illustrates how the above sample code might be used to read timezone properties from an appointment:
void ReadTimeZones(LPMAPIPROP lpAppointment)
{
HRESULT hRes = S_OK;
LPSPropTagArray lpNamedPropTags = NULL;
MAPINAMEID NamedID[2] = {0};
LPMAPINAMEID lpNamedID[2];
lpNamedID[0] = &NamedID[0];
lpNamedID[1] = &NamedID[1];
NamedID[0].lpguid = (LPGUID)&PSETID_Appointment;
NamedID[0].ulKind = MNID_ID;
NamedID[0].Kind.lID = dispidTimeZoneStruct;
NamedID[1].lpguid = (LPGUID)&PSETID_Appointment;
NamedID[1].ulKind = MNID_ID;
NamedID[1].Kind.lID = dispidApptTZDefStartDisplay;
hRes = lpAppointment->GetIDsFromNames(
2,
lpNamedID,
NULL,
&lpNamedPropTags);
if (SUCCEEDED(hRes) && lpNamedPropTags)
{
SizedSPropTagArray(2,sptaTzProps) = {2,
CHANGE_PROP_TYPE(lpNamedPropTags->aulPropTag[0],PT_BINARY),
CHANGE_PROP_TYPE(lpNamedPropTags->aulPropTag[1],PT_BINARY),
};
LPSPropValue lpProps = NULL;
ULONG cProps = 0;
hRes = lpAppointment->GetProps(
(LPSPropTagArray)&sptaTzProps,
NULL,
&cProps,
&lpProps);
if (SUCCEEDED(hRes) && 2 == cProps && lpProps)
{
if (PT_BINARY == PROP_TYPE(lpProps[0].ulPropTag))
{
TZREG* ptzReg = BinToTZREG(lpProps[0].Value.bin.cb,lpProps[0].Value.bin.lpb);
// TODO: Do something with ptzReg
delete ptzReg;
}
if (PT_BINARY == PROP_TYPE(lpProps[1].ulPropTag))
{
TZDEFINITION* ptzDef = BinToTZDEFINITION(lpProps[1].Value.bin.cb,lpProps[1].Value.bin.lpb);
// TODO: Do something with ptzDef
delete[] ptzDef;
}
}
MAPIFreeBuffer(lpProps);
}
MAPIFreeBuffer(lpNamedPropTags);
}// ReadTimeZones
Remarks
Understanding of these properties should not be needed for appointments which have been rebased by our tool or created with Outlook 2007 or higher or CDO 1.21 after the upcoming time zone update. These properties only need to be looked at by developers writing tools to rebase appointments. The suggested approach for rebasing an appointment is:
- Using CDO or the Outlook Object Model, examine the time and recurrence information on an appointment to determine if the appointment is a candidate for rebasing. If necessary, present information to the user to allow them to decide.
- Using CDO or the Outlook Object Model, write the new time and recurrence information.
- Stamp the appropriate time zone information into dispidApptTZDefStartDisplay, dispidApptTZDefEndDisplay, and dispidApptTZDefRecur.
Comments
Anonymous
February 02, 2007
http://blogs.msdn.com/stephen_griffin/archive/2006/12/06/outlook-2007-timezone-structures.aspxAnonymous
February 08, 2007
What Windows Update classification does it petain to? For example, Security Updates, Critical Updates? ThanksAnonymous
February 21, 2007
I have few quires.. how to find whether server required Timezone Update? how to check whether is it going to work? Regards BalaAnonymous
February 22, 2007
I read and understand most of the above. In Testing, single instance items made after a client DST update display correctly AFTER Exchange is patched (all single instance items created before the patch shift.). What does the exchange server have access to (property wise) that allows it to distinguish Post DST updates? I would like to find a way to determine those items that do not need updating. -MichaelAnonymous
February 27, 2007
The rebasing tool adds time zone information to appointments that it touches. This time zone informationAnonymous
March 01, 2007
All day events created in 2006DST rules with the 2007DST patch applied change to the prior dayAnonymous
March 12, 2007
Hopefully the daylight savings time adjustments are completeAnonymous
August 16, 2007
We're getting near DST time again. Last time around, we had both an Outlook based tool and an ExchangeAnonymous
August 02, 2009
Is it possible to lookup a full TZDEFINITION by GUID or name? TZDEFINITION seems to be an Outlook only thing and the necessary information cannot be derived using the regular Windows API? (TIME_ZONE_INFORMATION etc only contains a single rule?)