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.