[This is now documented here: http://msdn.microsoft.com/en-us/library/cc963264.aspx ]
This is the second part of a three part series documenting the MFCMAPI sample add-in CreateOutlookItemsAddin. We're in the process of updating the MAPI documentation and these articles are a preview of some of the new content. This article assumes you have downloaded the add-in from the above link and are running it inside the current version of MFCMAPI. Feedback on the article and on the code is especially welcome.
Part 1: Creating a Recurring Appointment With MAPI
MAPI can be used to create task items
To create a task item
MFCMAPI demonstrates these steps with the AddTask function. AddTask takes numerous parameters from the Add Task dialog box that is displayed when the user selects Add Task on the Addins menu in MFCMAPI. The DisplayAddTaskDialog method in Task.cpp displays the dialog box and passes values from the dialog box to the AddTask method. The DisplayAddTaskDialog method does not relate directly to creating a task item using MAPI, so it is not listed here. Note that MFCMAPI does not ensure a task folder has been selected. Creating task items in non task folders may lead to undefined behavior.
The AddTask method is listed below. Note that the first parameter passed to the AddTask method is a pointer to an IMAPIFolder interface. Given lpFolder that represents an IMAPIFolder interface, the code calls IMAPIFolder::CreateMessage. The CreateMessage method returns a success code and a pointer to a pointer to an IMessage interface. Most of the AddTask function code handles the work of property setting in preparation for IMAPIProp::SetProps. If the SetProps call succeeds, IMAPIProp::SaveChanges commits the changes to the store and creates a new task item.
AddTask sets a number of named properties. See Using MAPI to Create Outlook 2007 Items for a discussion of named properties and how they are created. Since the named properties used for task items occupy multiple property sets, care must be taken when building parameters to pass to GetIDsFromNames.
AddTask uses a helper function to build a structure representing a task recurrence for PidLidTaskRecurrence. This function is named BuildWeeklyTaskRecurrencePattern. The structure it builds is documented in [MS-OXOTASK].pdf, section 2.2.2.2.15, PidListTaskRecurrence, and [MS-OXOCAL].pdf, section 2.2.1.44.1, RecurrencePattern Structure. Note that while a large variety of recurrence patterns are possible, BuildWeeklyTaskRecurrencePattern only builds a weekly recurrence pattern. It also makes a number of assumptions, such as the calendar type (Gregorian), the first day of the week (Sunday), and number of modified or deleted instances (none). A more general purpose recurrence pattern creation function would need to accept these sorts of variables as parameters.
Code Snippet
HRESULT AddTask(LPMAPIFOLDER lpFolder, SYSTEMTIME* lpstStart, SYSTEMTIME* lpstEnd, SYSTEMTIME* lpstFirstDOW, DWORD dwPeriod, DWORD dwOccurrenceCount, DWORD dwPatternTypeSpecific, LPWSTR szSubject, LPWSTR szBody) { if (!lpFolder) return MAPI_E_INVALID_PARAMETER; HRESULT hRes = S_OK; LPMESSAGE lpMessage = 0; // create a message and set its properties hRes = lpFolder->CreateMessage(0, 0, &lpMessage); if (SUCCEEDED(hRes)) { MAPINAMEID rgnmid[ulTaskProps]; LPMAPINAMEID rgpnmid[ulTaskProps]; LPSPropTagArray lpNamedPropTags = NULL; ULONG i = 0; for (i = 0 ; i < ulTaskProps ; i++) { if (i < ulFirstTaskProp) rgnmid[i].lpguid = (LPGUID)&PSETID_Common; else rgnmid[i].lpguid = (LPGUID)&PSETID_Task; rgnmid[i].ulKind = MNID_ID; rgnmid[i].Kind.lID = aulTaskProps[i]; rgpnmid[i] = &rgnmid[i]; } hRes = lpFolder->GetIDsFromNames( ulTaskProps, (LPMAPINAMEID*) &rgpnmid, NULL, &lpNamedPropTags); if (SUCCEEDED(hRes) && lpNamedPropTags) { // Since we know in advance which props // we'll be setting, we can statically // declare most of the structures involved // and save expensive MAPIAllocateBuffer calls // For brevity, code to set most spvProps // has been removed. For the complete listing, see // AddMail in Mails.cpp spvProps[p_PR_MESSAGE_CLASS_W].ulPropTag = PR_MESSAGE_CLASS_W; spvProps[p_PR_ICON_INDEX].ulPropTag = PR_ICON_INDEX; spvProps[p_PR_SUBJECT_W].ulPropTag = PR_SUBJECT_W; spvProps[p_PR_MESSAGE_FLAGS].ulPropTag = PR_MESSAGE_FLAGS; spvProps[p_PR_BODY_W].ulPropTag = PR_BODY_W; spvProps[p_PidLidTaskMode].Value.l = tdmtNothing; SYSTEMTIME stStartUTC = {0}; TzSpecificLocalTimeToSystemTime(NULL,lpstStart,&stStartUTC); SystemTimeToFileTime(&stStartUTC,&spvProps[p_PidLidCommonEnd].Value.ft); spvProps[p_PidLidTaskStatus].Value.l = tsvNotStarted; spvProps[p_PidLidPercentComplete].Value.dbl = 0.0; spvProps[p_PidLidTaskState].Value.l = tdsOWNNEW; spvProps[p_PidLidTaskDeadOccurrence].Value.b = false; spvProps[p_PidLidTaskOwner].Value.lpszW = L"Unknown"; spvProps[p_PidLidTaskFRecurring].Value.b = true; spvProps[p_PidLidTaskOwnership].Value.l = tovNew; spvProps[p_PidLidTaskAcceptanceState].Value.l = tdvNone; spvProps[p_PidLidTaskFFixOffline].Value.b = true; SystemTimeToFileTime(lpstStart,&spvProps[p_PidLidTaskDueDate].Value.ft); spvProps[p_PidLidTaskComplete].Value.b = false; spvProps[p_PR_MESSAGE_CLASS_W].Value.lpszW = L"IPM.Task"; spvProps[p_PR_ICON_INDEX].Value.l = 0x501; // Unassigned Recurring Task spvProps[p_PR_SUBJECT_W].Value.lpszW = szSubject; spvProps[p_PR_MESSAGE_FLAGS].Value.l = MSGFLAG_READ; spvProps[p_PR_BODY_W].Value.lpszW = szBody; hRes = BuildWeeklyTaskRecurrencePattern( lpstStart, lpstEnd, lpstFirstDOW, dwPeriod, dwOccurrenceCount, dwPatternTypeSpecific, &spvProps[p_PidLidTaskRecurrence].Value.bin.cb, &spvProps[p_PidLidTaskRecurrence].Value.bin.lpb); if (SUCCEEDED(hRes)) { hRes = lpMessage->SetProps(NUM_PROPS, spvProps, NULL); if (SUCCEEDED(hRes)) { hRes = lpMessage->SaveChanges(FORCE_SAVE); } } if (spvProps[p_PidLidTaskRecurrence].Value.bin.lpb) delete[] spvProps[p_PidLidTaskRecurrence].Value.bin.lpb; } MAPIFreeBuffer(lpNamedPropTags); } if (lpMessage) lpMessage->Release(); return hRes; }
This is a follow up to Pre Vista Fix MAPI Download Still Available. As promised, we kept the old pre-Vista fix up until August 15th, then we took it down. I didn't note this earlier because:
The error was that in addition to taking down the pre-Vista fix, we also broke the link between the original MAPI download article and the actual download. I just got word and confirmed this has been fixed. So here's what we have (current as of August 28th...):
We kept both pages active to not break any links. Both pages link to the same file. If you're want a "permanent" link, use the first one, the one without the scary note.
BTW - for those interested, build 8039 is a set of timezone/dst fixes for CDO.dll on top of the previous 8022 build. I don't think anything in MAPI itself changed.
[This is now documented here: http://msdn.microsoft.com/en-us/library/cc963259.aspx ]
This is the first of a three part series documenting the MFCMAPI sample add-in CreateOutlookItemsAddin. We're in the process of updating the MAPI documentation and these articles are a preview of some of the new content. This article assumes you have downloaded the add-in from the above link and are running it inside the current version of MFCMAPI. Feedback on the article and on the code is especially welcome.
MAPI can be used to create appointments items
To create an appointment item
MFCMAPI demonstrates these steps with the AddAppointment function. AddAppointment takes numerous parameters from the Add Appointment dialog box that is displayed when the user selects Add Appointment on the Addins menu in MFCMAPI. The DisplayAddAppointmentDialog method in Appointments.cpp displays the dialog box and passes values from the dialog box to the AddAppointment method. The DisplayAddAppointmentDialog method does not relate directly to creating an appointment item using MAPI, so it is not listed here. Note that MFCMAPI does not ensure a calendar folder has been selected. Creating calendar items in non calendar folders may lead to undefined behavior.
The AddAppointment method is listed below. Note that the first parameter passed to the AddAppointment method is a pointer to an IMAPIFolder interface. Given lpFolder that represents an IMAPIFolder interface, the code calls IMAPIFolder::CreateMessage. The CreateMessage method returns a success code and a pointer to a pointer to an IMessage interface. Most of the AddAppointment function code handles the work of property setting in preparation for IMAPIProp::SetProps. If the SetProps call succeeds, IMAPIProp::SaveChanges commits the changes to the store and creates a new calendar item.
AddAppointment sets a number of named properties. See Using MAPI to Create Outlook 2007 Items for a discussion of named properties and how they are created. Since the named properties used for appointment items occupy multiple property sets, care must be taken when building parameters to pass to GetIDsFromNames.
AddAppointment uses helper functions to build a structure for various appointment related properties. BuildTimeZoneStruct and BuildTimeZoneDefinition are used to build the time zone related properties discussed in [MS-OXOCAL].pdf, sections 2.2.1.39 through 2.2.1.43. BuildGlobalObjectID is used to build a structure for the PidLidGlobalObjectID and PidLidCleanGlobalObjectId properties discussed in [MS-OXOCAL].pdf, sections 2.2.1.27 and 2.2.1.28. The PidLidAppointmentRecur property is built using BuildWeeklyAppointmentRecurrencePattern. The structure it builds is documented in [MS-OXOCAL].pdf, section 2.2.1.44, PidLidAppointmentRecur. Note that while a large variety of appointment recurrence patterns are possible, BuildWeeklyAppointmentRecurrencePattern only builds a weekly appointment recurrence pattern. It also makes a number of assumptions, such as the calendar type (Gregorian), the first day of the week (Sunday), and number of modified or deleted instances (none). A more general purpose appointment recurrence pattern creation function would need to accept these sorts of variables as parameters.
HRESULT AddAppointment(LPMAPIFOLDER lpFolder, SYSTEMTIME* lpstStartDateLocal, SYSTEMTIME* lpstEndDateLocal, SYSTEMTIME* lpstStartFirstUST, SYSTEMTIME* lpstEndFirstUST, SYSTEMTIME* lpszClipStartUST, SYSTEMTIME* lpstClipEndUST, SYSTEMTIME* lpstFirstDOW, DWORD dwStartOffsetLocal, DWORD dwEndOffsetLocal, DWORD dwPeriod, DWORD dwOccurrenceCount, DWORD dwPatternTypeSpecific, ULONG ulDuration, LPWSTR szSubject, LPWSTR szLocation, LPWSTR szPattern) { if (!lpFolder) return MAPI_E_INVALID_PARAMETER; HRESULT hRes = S_OK; LPMESSAGE lpMessage = 0; // create a message and set its properties hRes = lpFolder->CreateMessage(0, 0, &lpMessage); if (SUCCEEDED(hRes)) { MAPINAMEID rgnmid[ulAppointmentProps]; LPMAPINAMEID rgpnmid[ulAppointmentProps]; LPSPropTagArray lpNamedPropTags = NULL; ULONG i = 0; for (i = 0 ; i < ulAppointmentProps ; i++) { if (i < ulFirstMeetingProp) rgnmid[i].lpguid = (LPGUID)&PSETID_Appointment; elseif (i < ulFirstCommonProp) rgnmid[i].lpguid = (LPGUID)&PSETID_Meeting; else rgnmid[i].lpguid = (LPGUID)&PSETID_Common; rgnmid[i].ulKind = MNID_ID; rgnmid[i].Kind.lID = aulAppointmentProps[i]; rgpnmid[i] = &rgnmid[i]; } hRes = lpFolder->GetIDsFromNames( ulAppointmentProps, (LPMAPINAMEID*) &rgpnmid, NULL, &lpNamedPropTags); if (SUCCEEDED(hRes) && lpNamedPropTags) { // Since we know in advance which props // we'll be setting, we can statically // declare most of the structures involved // and save expensive MAPIAllocateBuffer calls // For brevity, code to set most spvProps // has been removed. For the complete listing, see // AddAppointments in Appointments.cpp spvProps[p_PR_SUBJECT_W].ulPropTag = PR_SUBJECT_W; spvProps[p_PR_START_DATE].ulPropTag = PR_START_DATE; spvProps[p_PR_END_DATE].ulPropTag = PR_END_DATE; spvProps[p_PR_MESSAGE_CLASS_W].ulPropTag = PR_MESSAGE_CLASS_W; spvProps[p_PR_ICON_INDEX].ulPropTag = PR_ICON_INDEX; spvProps[p_PR_CONVERSATION_INDEX].ulPropTag = PR_CONVERSATION_INDEX; spvProps[p_PR_MESSAGE_FLAGS].ulPropTag = PR_MESSAGE_FLAGS; spvProps[p_PidLidAppointmentSequence].Value.l = 0; spvProps[p_PidLidBusyStatus].Value.l = olBusy; spvProps[p_PidLidLocation].Value.lpszW = szLocation; SystemTimeToFileTime(lpstStartFirstUST,&spvProps[p_PidLidAppointmentStartWhole].Value.ft); SystemTimeToFileTime(lpstEndFirstUST,&spvProps[p_PidLidAppointmentEndWhole].Value.ft); spvProps[p_PidLidAppointmentDuration].Value.l = ulDuration; spvProps[p_PidLidAppointmentColor].Value.l = 0; // No color spvProps[p_PidLidResponseStatus].Value.l = respNone; spvProps[p_PidLidRecurring].Value.b = true; SystemTimeToFileTime(lpszClipStartUST,&spvProps[p_PidLidClipStart].Value.ft); SystemTimeToFileTime(lpstClipEndUST,&spvProps[p_PidLidClipEnd].Value.ft); SYSTEMTIME stStandard = {0}; stStandard.wMonth = 0xB; stStandard.wDay = 0x1; stStandard.wHour = 0x2; SYSTEMTIME stDaylight = {0}; stDaylight.wMonth = 0x3; stDaylight.wDay = 0x2; stDaylight.wHour = 0x2; hRes = BuildTimeZoneStruct( 300, 0, (DWORD)-60, &stStandard, &stDaylight, &spvProps[p_PidLidTimeZoneStruct].Value.bin.cb, &spvProps[p_PidLidTimeZoneStruct].Value.bin.lpb); spvProps[p_PidLidTimeZoneDescription].Value.lpszW = L"(GMT-05:00) Eastern Time (US & Canada)"; SYSTEMTIME stRule1Standard = {0}; stRule1Standard.wMonth = 0xA; stRule1Standard.wDay = 0x5; stRule1Standard.wHour = 0x2; SYSTEMTIME stRule1Daylight = {0}; stRule1Daylight.wMonth = 0x4; stRule1Daylight.wDay = 0x1; stRule1Daylight.wHour = 0x2; if (SUCCEEDED(hRes)) hRes = BuildTimeZoneDefinition( L"Eastern Standard Time", 0, // TZRule Flags 2006, // wYear 300, // lbias 0, // lStandardBias, (DWORD)-60, // lDaylightBias, &stRule1Standard, // stStandardDate &stRule1Daylight, // stDaylightDate TZRULE_FLAG_EFFECTIVE_TZREG, // TZRule Flags 2007, // wYear 300, // lbias 0, // lStandardBias, (DWORD)-60, // lDaylightBias, &stStandard, // stStandardDate &stDaylight, // stDaylightDate &spvProps[p_PidLidAppointmentTimeZoneDefinitionRecur].Value.bin.cb, &spvProps[p_PidLidAppointmentTimeZoneDefinitionRecur].Value.bin.lpb); spvProps[p_PidLidAppointmentTimeZoneDefinitionStartDisplay].Value.bin.cb = spvProps[p_PidLidAppointmentTimeZoneDefinitionRecur].Value.bin.cb; spvProps[p_PidLidAppointmentTimeZoneDefinitionStartDisplay].Value.bin.lpb = spvProps[p_PidLidAppointmentTimeZoneDefinitionRecur].Value.bin.lpb; spvProps[p_PidLidAppointmentTimeZoneDefinitionEndDisplay].Value.bin.cb = spvProps[p_PidLidAppointmentTimeZoneDefinitionRecur].Value.bin.cb; spvProps[p_PidLidAppointmentTimeZoneDefinitionEndDisplay].Value.bin.lpb = spvProps[p_PidLidAppointmentTimeZoneDefinitionRecur].Value.bin.lpb; if (SUCCEEDED(hRes)) hRes = BuildWeeklyAppointmentRecurrencePattern( lpstStartDateLocal, lpstEndDateLocal, lpstFirstDOW, dwStartOffsetLocal, dwEndOffsetLocal, dwPeriod, dwOccurrenceCount, dwPatternTypeSpecific, &spvProps[p_PidLidAppointmentRecur].Value.bin.cb, &spvProps[p_PidLidAppointmentRecur].Value.bin.lpb); spvProps[p_PidLidRecurrenceType].Value.l = rectypeWeekly; spvProps[p_PidLidRecurrencePattern].Value.lpszW = szPattern; spvProps[p_PidLidIsRecurring].Value.b = true; if (SUCCEEDED(hRes)) hRes = BuildGlobalObjectId( &spvProps[p_PidLidGlobalObjectId].Value.bin.cb, &spvProps[p_PidLidGlobalObjectId].Value.bin.lpb); spvProps[p_PidLidCleanGlobalObjectId].Value.bin.cb = spvProps[p_PidLidGlobalObjectId].Value.bin.cb; spvProps[p_PidLidCleanGlobalObjectId].Value.bin.lpb = spvProps[p_PidLidGlobalObjectId].Value.bin.lpb; SystemTimeToFileTime(lpstStartFirstUST,&spvProps[p_PidLidCommonStart].Value.ft); SystemTimeToFileTime(lpstEndFirstUST,&spvProps[p_PidLidCommonEnd].Value.ft); spvProps[p_PidLidSideEffects].Value.l = seOpenToDelete | seOpenToCopy | seOpenToMove | seCoerceToInbox | seOpenForCtxMenu; spvProps[p_PR_SUBJECT_W].Value.lpszW = szSubject; SystemTimeToFileTime(lpstStartFirstUST,&spvProps[p_PR_START_DATE].Value.ft); SystemTimeToFileTime(lpstEndFirstUST,&spvProps[p_PR_END_DATE].Value.ft); spvProps[p_PR_MESSAGE_CLASS_W].Value.lpszW = L"IPM.Appointment"; spvProps[p_PR_ICON_INDEX].Value.l = 0x00000401; // Recurring Appointment if (SUCCEEDED(hRes)) hRes = BuildConversationIndex( &spvProps[p_PR_CONVERSATION_INDEX].Value.bin.cb, &spvProps[p_PR_CONVERSATION_INDEX].Value.bin.lpb); spvProps[p_PR_MESSAGE_FLAGS].Value.l = MSGFLAG_READ; if (SUCCEEDED(hRes)) hRes = lpMessage->SetProps(NUM_PROPS, spvProps, NULL); if (SUCCEEDED(hRes)) { hRes = lpMessage->SaveChanges(FORCE_SAVE); } if (spvProps[p_PidLidTimeZoneStruct].Value.bin.lpb) delete[] spvProps[p_PidLidTimeZoneStruct].Value.bin.lpb; if (spvProps[p_PidLidAppointmentTimeZoneDefinitionRecur].Value.bin.lpb) delete[] spvProps[p_PidLidAppointmentTimeZoneDefinitionRecur].Value.bin.lpb; // Do not delete p_PidLidAppointmentTimeZoneDefinitionStartDisplay, // it was borrowed from p_PidLidAppointmentTimeZoneDefinitionStartDisplay // Do not delete p_PidLidAppointmentTimeZoneDefinitionEndDisplay, // it was borrowed from p_PidLidAppointmentTimeZoneDefinitionStartDisplay if (spvProps[p_PidLidAppointmentRecur].Value.bin.lpb) delete[] spvProps[p_PidLidAppointmentRecur].Value.bin.lpb; if (spvProps[p_PidLidGlobalObjectId].Value.bin.lpb) delete[] spvProps[p_PidLidGlobalObjectId].Value.bin.lpb; // Do not delete p_PidLidCleanGlobalObjectId, // it was borrowed from p_PidLidGlobalObjectId if (spvProps[p_PR_CONVERSATION_INDEX].Value.bin.lpb) delete[] spvProps[p_PR_CONVERSATION_INDEX].Value.bin.lpb; } MAPIFreeBuffer(lpNamedPropTags); } if (lpMessage) lpMessage->Release(); return hRes; }
We recently released a downloadable Exchange System Manager (ESM) for Exchange 2003 which can be installed on Vista machines. Part of what made this possible was the changes we did recently to allow Exchange's implementation of the MAPI client binaries to run on Vista.
Some interesting tidbits from the Release Notes:
Enjoy!
The first answer I give to customers asking for tips on making their store provider work with the latest versions of Outlook is "be well behaved". I've had a couple message store provider cases recently that illustrate this and give me an opportunity to expand on that statement and give some concrete guidance.
The customer's message store wasn't working well in Outlook 2007. Outlook would recreate it's persisted search folders every time it booted. In fact, it would create them, then try to create them again and again, chewing up cycles. They had checked their code to ensure that the folders got created, and that every property Outlook set was available. Except - some properties weren't accessible through the hierarchy table.
When Outlook loaded the hierarchy table for the message store, it looked for any folders with PR_EXTENDED_FOLDER_FLAGS stamped on them. Those that had this property, with the appropriate flags set in it, would be added to the list of known search folders. Later, when Outlook needed to access one of the search folder, it would consult the list it had built. Since this particular store didn't expose a number of properties through the hierarchy table, this list was empty. So Outlook concluded it needed to create the search folder.
The first lesson we learn is this:
Properties set on messages and folders should be accessible through contents tables and hierarchy tables
This lesson was behind the PR_RECORD_KEY issues and it's been the source of countless other store providers issues I've debugged over the years. Outlook (and MFCMAPI and even Outlook Spy) expect most properties to come back the same regardless of whether they're requested through the table or through the object. The solution is not to special case properties you think Outlook might want, but instead to ensure the general case works. That's exactly what this customer did and his provider's working much better now.
Different customer this time. Their issue was a bit more complicated. A user would send a recurring meeting request to another user, both running their message store. The recipient would let Outlook create a tentative appointment on the calendar. If at this point, they open the series, the info bar would prompt "Please respond". If they then opened one instance of the meeting and accepted it, then went back and opened the series, the info bar now indicated "Tentatively accepted". When the same steps were performed with Exchange as the back end, both times the info bar would prompt "Please respond".
Outlook's logic for determining which string to display hinges on two properties - dispidResponseStatus and dispidApptReplyTime. If the status is set to none or not responded, we display "Please respond". If it's set to tentative, we display "Tentatively accepted", but ONLY if the reply time is set. If the reply time isn't set, we display "Please respond" instead. When the recipient accepted a single instance of the meeting, Outlook changed the response status on the master to tentative, but didn't set a reply time. However, when Outlook then looked at the properties on the meeting, the reply time was set, so it displayed "Tentatively accepted".
How did the reply time property get set? This leads to the second lesson:
Message stores which manufacture Outlook's business logic properties do so at their own risk
If Outlook didn't set a property, it's probably not a good idea to manufacture it. And if you do decide to go that route, you better be prepared to crack open the Exchange Protocol documentation and spend a good long time making sure you get everything right. In this case, the customer got out of the property manufacturing business and their meeting started working as expected.
In both cases, as in most message store cases (see this and this), the advice boils down to "if your store acts weird, so will Outlook". Hopefully though, these lessons will help a few of you identify the source of weird behavior in your own providers.
Duncan Smith noted recently that MAPI appears to have a problem with IPv6. He contacted me to see if I knew anything about the issue. I didn't, but one of the Exchange PMs spotted the connection between Duncan's issue and the recently reported DSProxy issue over on the Ehlo blog. It turns out that issue affects regular MAPI connections - not just RPC/HTTP.
The gist is - right now, DSProxy doesn't listen on IPv6. It only listens on IPv4. So if you're running a MAPI client on an IPv6 enabled box (such as, say, installing the MAPI download on an IPv6 enabled Exchange 2007 server), then profile creation is going to fail when the client tries to ask the server for a referral to a GC.
As the Ehlo article notes, a fix is in the works. Until then, you can try one of the following:
We just released an update to our calendar rebasing library, Tzmovelib.dll. This update includes a refresh to the documentation. One thing that didn't make it into this refresh was an updated sample and documentation for a new exported function, primarily because I didn't have it ready in time. So I'm documenting the new function here, and it will make it into the MSDN the next time we refresh the docs.
We've added a new export to tzmovelib.dll in this refresh. It's name is HrCreateApptRebaserEx. It functions exactly the same as HrCreateApptRebaser unless a new flag, REBASE_FLAG_PHYSICAL_MOVE is passed. In that case, we use the new pTZFrom parameter to effect a rebase from an old time zone to a new time zone. Additionally, we renamed the pftInstallDateUTC parameter to ptfRebaseExpireUTC, and the REBASE_FLAG_ONLY_CREATED_PRE_PATCH flag to REBASE_FLAG_ONLY_REBASE_BEFORE_EXPIRE to reflect that this parameter and flag apply to physical moves as well. The behavior of this parameter and flag did not change.
Resources
HrCreateApptRebaserEx
The HrCreateApptRebaserEx function initializes a IOlkApptRebaser object for use in rebasing appointments.
Quick Info
Syntax
HRESULT HrCreateApptRebaserEx( ULONG ulFlags, IMAPISession *pSession, IMsgStore *pCalendarMsgStore, IMAPIFolder *pCalendarFolder, LPCWSTR pwszUpdatePrefix, const FILETIME *pftRebaseExpireUTC, LONG lExpansionDepth, const TZDEFINITION *pTZFrom, const TZDEFINITION *pTZTo, const TZDEFINITION *pTZMissing, MAPIERROR **ppError, IOlkApptRebaser **ppApptRebase);
Parameters ulFlags [in] Bitmask of flags used to control how rebasing is performed. The following flags can be set:
REBASE_FLAG_UPDATE_ORGANIZED_MEETINGS Items in which the user is the meeting organizer will be rebased. Note that by default this will cause meeting updates to be sent out to all attendees of any meeting being rebased. Combine this flag with either REBASE_FLAG_FORCE_NO_EX_UPDATES or REBASE_FLAG_FORCE_NO_UPDATES to change the behavior of how meeting updates are handled. REBASE_FLAG_UPDATE_UNMARKED Update items that aren't marked with a time zone. If this flag is specified, the pTZMissing value passed to HrCreateApptRebaser will be used as created-in time zone for all items that do not have time zone data. REBASE_FLAG_UPDATE_ONLYRECURRING Update only recurring items. REBASE_FLAG_NO_UI Do not show any UI (prevents the display of any logon dialog boxes when opening the message store). REBASE_FLAG_UPDATE_MINIMIZEAPPTS Do not rebase items that will only have changes which occur in the past. REBASE_FLAG_FORCE_REBASE Do not check the organizer for rebasing decisions (enables items in which the user is the attendee to be rebased). REBASE_FLAG_FORCE_NO_EX_UPDATES Only send updates if the user is the organizer and recipient is non-EX. REBASE_FLAG_FORCE_NO_UPDATES Never send updates. REBASE_FLAG_ONLY_REBASE_BEFORE_EXPIRE Only rebase single instance items created before a specified date, which is either a patch install date or a physical move date. REBASE_FLAG_REPORTING_MODE Do not actually rebase, just report items that would be rebased. REBASE_FLAG_SEND_RESOURCE_UPDATES Send meeting updates to resources (attendees explicitly specified on the "Resource" line of a meeting). REBASE_FLAG_PHYSICAL_MOVE Don't skip rebasing of unaffected future recurring appointments for physical move. Only valid for HrCreateApptRebaseEx. If this flag is set, pTZFrom must be a valid time zone definition representing the "from" time zone and pTZTo then represents the "to" time zone. If this new flag is not set, then pTZFrom must be NULL, and HrCreateApptRebaseEx functions exactly the same as HrCreateApptRebase. REBASE_FLAG_PHYSICAL_MOVE is not valid with REBASE_FLAG_UPDATE_ONLYRECURRING. REBASE_FLAG_PHYSICAL_MOVE implies REBASE_FLAG_UPDATE_UNMARKED and REBASE_FLAG_UPDATE_MINIMIZEAPPTS.
pSession [in] Required. Pointer to a MAPI session interface.
pCalendarMsgStore [in] Required. Pointer to a message store containing items to be rebased.
pCalendarFolder [in] Require. Pointer to a Calendar folder containing items to be rebased.
pwszUpdatePrefix [in] Optional. Pointer to a string containing the prefix to be prepended on meeting requests. May be NULL.
pftRebaseExpireUTC [in] Optional. Only rebase items before this date. Only used if the REBASE_FLAG_ONLY_REBASE_BEFORE_EXPIRE flag is set.
lExpansionDepth [in] Expansion depth when expanding distribution lists to exclude EX recipients. Only used if the REBASE_FLAG_FORCE_NO_EX_UPDATES flag is set.
pTZFrom [in] Required. Pointer to a TZDEFINITION structure describing the time zone to be rebased from.
pTZTo [in] Required. Pointer to a TZDEFINITION structure describing the time zone to be rebased to.
pTZMissing [in] Required. Pointer to a TZDEFINITION structure describing the time zone to be assumed if time zone information is not stamped on an item. Must not be NULL, but only used if the REBASE_FLAG_UPDATE_UNMARKED flag is set.
ppError [out] Optional. Pointer to a pointer to a MAPIERROR structure containing version, component, and context information for the error. Can be NULL if no extended error information is desired. Free with MAPIFreeBuffer.
ppApptRebase [out] Pointer to a pointer to the returned IOlkApptRebaser interface.
Remarks
Obtain this function from tzmovelib.dll by calling GetProcAddress with the function name HrCreateApptRebaserEx@48. When REBASE_FLAG_PHYSICAL_MOVE and pTZFrom are not passed, this function is identical in behavior to HrCreateApptRebaser.
Not all of the flags are valid in combination with each other. View the "Glossary of command-line options for the Outlook Time Zone Data Update tool" section of http://support.microsoft.com/kb/931667 for more information on the various options.
The August 2008 Release (build 6.0.0.1008) is live: http://www.codeplex.com/MFCMAPI
I'm continuing to dig through the protocol docs (now on version 1.01). Last release was about getting the plumbing in place for Smart Views. This release is all about parsing structures, most especially, parsing for the recurrence blob on Calendar and Task items. Here's the change list - see the Issue Tracker on Codeplex for more details, or look at the code:
Enjoy.
Ed has asked if a few of us could help out with his survey by posting a link to it here. I'm happy to oblige:
Greetings Blog Readers, My name is Ed Jolly, and I am a director in the Commercial Technical Support (CTS) organization at Microsoft. I am here to request a few minutes of your time. We would like to learn more about blog readership through a brief survey. This is an opportunity for us to better understand what is valuable to you and what you would like to see in the future. Below is a link that will take you to another website to complete the survey. Based on what we learn, we may request more feedback in future surveys like this. When you open the survey, you will see a list of blogs that CTS engineers contribute to across many different products. We have not posted a listing of these blogs in the past, and I hope it helps you find other blogs that are helpful to you. The blog survey is completely anonymous. · Location: http://www.tsisurveys.com/mssurveys/blog/index.asp · Availability: Until August 22. You may receive a request to complete this survey through multiple RSS feeds. You need only to complete it one time. · Length: The survey can be a maximum of 11 questions. · Time: Less than 5 minutes (but providing more information in the open text fields may take a minute or two extra, improving our ability to understand your needs in these blogs). Thank you in advance for your time, participation and assistance. Ed Jolly (edjolly@microsoft.com)
Greetings Blog Readers,
My name is Ed Jolly, and I am a director in the Commercial Technical Support (CTS) organization at Microsoft. I am here to request a few minutes of your time.
We would like to learn more about blog readership through a brief survey. This is an opportunity for us to better understand what is valuable to you and what you would like to see in the future.
Below is a link that will take you to another website to complete the survey. Based on what we learn, we may request more feedback in future surveys like this. When you open the survey, you will see a list of blogs that CTS engineers contribute to across many different products. We have not posted a listing of these blogs in the past, and I hope it helps you find other blogs that are helpful to you.
The blog survey is completely anonymous.
· Location: http://www.tsisurveys.com/mssurveys/blog/index.asp
· Availability: Until August 22. You may receive a request to complete this survey through multiple RSS feeds. You need only to complete it one time.
· Length: The survey can be a maximum of 11 questions.
· Time: Less than 5 minutes (but providing more information in the open text fields may take a minute or two extra, improving our ability to understand your needs in these blogs).
Thank you in advance for your time, participation and assistance.
Ed Jolly (edjolly@microsoft.com)
We had a customer recently who was sending mails with Outlook's MAPI. They wanted to know how to force the message to be plain text, like it is in Outlook when you select Send Plain Text only in the properties for the recipient:
They were using CreateOneOff to create their recipient, and they were passing MAPI_SEND_NO_RICH_INFO. Yet, if the message they were sending contained an attachment, such as a Word document, the content type of the attachment on the received mail was always application/ms-tnef instead of application/msword. In other words, they were still getting a winmail.dat attachment. If they addressed a mail in Outlook with the Send Plain Text only option, sending the same attachment, it came across without the TNEF.
One thing to note right off the bat is that sending rich text (RTF) data and sending TNEF (winmail.dat) are actually two different concepts. RTF data describes the formatting of the text of the message, much in the same way HTML can also describe the formatting of text. TNEF is a mechanism for encapsulating MAPI properties that Exchange and Outlook do not convert natively into MIME format. One such MAPI property commonly encapsulated in TNEF is PR_RTF_COMPRESSED. But TNEF can encapsulate many more properties than just PR_RTF_COMPRESSED, such as properties dealing with certain attachments. So the presence of winmail.dat doesn't necessarily mean rich text is being transmitted. That's what's happening in this case.
If you're trying to avoid TNEF completely, MAPI_SEND_NO_RICH_INFO is a step in the right direction, but doesn't actually tell the content converter not to use TNEF - it just tells it not to use RTF. To avoid TNEF altogether, we need to set some more flags. But which flags? We can look at what CreateOneOff generates to find out. Fortunately, we don't need to reverse engineer the structure - it was recently documented in section 2.2.5.1 of the following Exchange Protocol Documentation: [MS-OXCDATA].pdf
Now - the documentation's in "bytes on the wire - what's a constant?" format, but with a little elbow grease, we can produce the following structure:
{ DWORD dwFlags; BYTE ProviderUID[16]; DWORD dwBitmask; } ONEOFFEIDHEADER, FAR *LPONEOFFEIDHEADER;
which we can map our entry ID to so we can check which flags are set. The following flags are valid (determined by matching the docs up to MAPI's headers):
#define MAPI_SEND_NO_RICH_INFO ((ULONG) 0x00010000) #define ENCODING_PREFERENCE ((ULONG) 0x00020000) #define ENCODING_MIME ((ULONG) 0x00040000) #define BODY_ENCODING_HTML ((ULONG) 0x00080000) #define BODY_ENCODING_TEXT_AND_HTML ((ULONG) 0x00100000) #define MAC_ATTACH_ENCODING_UUENCODE ((ULONG) 0x00200000) #define MAC_ATTACH_ENCODING_APPLESINGLE ((ULONG) 0x00400000) #define MAC_ATTACH_ENCODING_APPLEDOUBLE ((ULONG) 0x00600000) #define OOP_DONT_LOOKUP ((ULONG) 0x10000000)
With this, we can see that setting the Plain Text Only option in Outlook translates to setting the ENCODING_PREFERENCE and ENCODING_MIME flags. Unfortunately, the CreateOneOff function doesn't let you pass those flags. If you try, you'll get MAPI_E_UNKNOWN_FLAGS. We won't let that stop us though - we can use our structure to set the flags as well as read them!
Here's how we can put this all together and get a one-off entry ID which will send without TNEF:
{ DWORD dwFlags; BYTE ProviderUID[16]; DWORD dwBitmask; } ONEOFFEIDHEADER, FAR *LPONEOFFEIDHEADER; hr = lpAddrBook->CreateOneOff( lpszName, lpszAdrType, lpszAddress, MAPI_SEND_NO_RICH_INFO, &cbEntryID, &lpEntryID); if (HR_SUCCEEDED(hr)) { LPONEOFFEIDHEADER lp1Off = (LPONEOFFEIDHEADER) (ENTRYID*) lpEntryID; if ((lp1Off->dwBitmask & MAPI_SEND_NO_RICH_INFO)) { lp1Off->dwBitmask |= (ENCODING_PREFERENCE | ENCODING_MIME); } }
[Update: 8/4/2008 5:45 - rewrote the RTF/TNEF paragraph to make it clearer][Update: 8/4/2008 9:36 - realized I hadn't proofread the code - fixed it]