Goal: to be able to read and write a named property to calendar items. ---- Problem: recurring meetings are very broken. ------- Details: ------- Firstly, I'm reading these from the calendar folder for the primary mailbox for the current session -- in other words, GetDefaultFolder(CdoDefaultFolderCalendar) can be used and will return a folder that contains AppointmentItems. We don't have to wrestle with that particular set of undocumented stuff. Setup: ----- There are a lot of code samples below to allow the reader to reproduce the particular problems and bugs I encountered. These all make the following assumptions: A. That you have a recurring meeting scheduled in your calendar. Title of this meeting is "recurrence test one". To guarantee that the code samples work as described, it should be a daily recurrence happening only on weekdays, starting on Monday 20th December, ending after 5 instances. B. That there is a mail profile set up called "Default Profile" which points at your mailbox (or, at least, the one containing the recurring meeting mentioned above". C. The code samples are a mixture of VB and C++/MAPI, depending on exactly which problem I'm describing -- the context required is: C.1 VB: Start a project. Add a reference to CDO to the project. Put the code in Form_Load() in the main form. C.2 C++/MAPI: Start a new MFC Appwizard dialog-based project. Add a button. Put the code in the OnButton1 function. Add #import and using namespace MAPI to the start of your main dialog source file. (or do whatever you would usually do to get around the #import file locking bug stuff). Add #include and to the start of this file, and mapi32.lib to the list of things to link in. Add mapiguid.c from the MFC samples on MS's ftp site to the project. (or manually do the INITGUID stuff if you're feeling brace). Don't forget to turn precompiled headers off if you copy it over from the samples. Note that the C++/MAPI stuff does not release objects and/or free up memory except when relevant; this is for brevity's sake. Problem 1 --------- If you try and loop over all the meetings in your calendar, CDO won't give you instances of recurring meetings unless you filter by date. To reproduce this, use the following code: Dim s As MAPI.Session Set s = New MAPI.Session s.Logon "Default Profile" Dim c As MAPI.Folder Set c = s.GetDefaultFolder(CdoDefaultFolderCalendar) Dim ms As MAPI.Messages Set ms = c.Messages Dim m As MAPI.AppointmentItem Set m = ms.GetFirst While Not m Is Nothing If m.Subject = "recurrence test one" Then Debug.Print "Found it: " & m.StartTime End If Set m = ms.GetNext Wend Note that you only get one report of finding this meeting, despite the fact that it appears in your calendar five times. This is actually relatively sensible behavior -- after all, if you have any meetings that recur without limits, it would have to give you an infinite number of instances of those meetings. The problem is that if you change an instance of the meeting, CDO won't tell you about this. Ignoring, for now, recurring meetings, we can reproduce this as follows: Open the instance of "recurrence test one" on Tuesday 21st. Change the title to be "changed recurrence test one". Change the code above from If m.Subject = "recurrence test one" Then to If m.Subject = "changed recurrence test one" Then and run it again. Note that this time, even though there is a meeting in your calendar with the appropriate subject, you will never find it. This is, sort of, covered in the documentation -- the docs for AppointmentItem contain: "But you may wish to instantiate an individual recurrence so that you can edit it and make it different from other recurrences in the series. You do this by using a MessageFilter object to restrict CdoPR_START_DATE and CdoPR_END_DATE to the start and end of the desired occurrence, and then calling the Messages collection's GetFirst method. " Ignoring, for now, the fact that this won't actually work if you have more than one appointment on the same date because you might get another meeting at the same time, this does vaguely suggest that CDO won't give you individual instances of recurring meetings without this sort of filtering. We can demonstrate this fix as follows; change the code to read: . . . as before . . . Set ms = c.Messages Dim fil As MAPI.MessageFilter Set fil = ms.Filter Dim filflds As MAPI.Fields Set filflds = fil.Fields Dim fld As MAPI.Field Set fld = filflds.Add(CdoPR_START_DATE, #12/25/1999#) Set fld = filflds.Add(CdoPR_END_DATE, #12/20/1999#) Dim m As MAPI.AppointmentItem . . . as before . . . Other than the usual CDO "feature" that you have to reverse the start and end times to filter by date, this will then kick CDO into life and it will create instances of all recurring meetings during that period -- and now it'll find the changed meeting. Why is this a problem? Because if we want to find a meeting called (say) "changed recurrence test one", but we have no idea when it happened, we still have to impose a filter on start/end dates as above -- but we have to make sure that this range covers every possible meeting. This is, strictly speaking, impossible. However, you can make some assumptions about when meetings are likely to be scheduled, and filter between the years 1900 and 2100. If you're prepared to take more risks, you can narrow this down further -- it's unlikely people will have scheduled meetings before your Exchange Server was installed, or more than a couple of years into the future; but this is still a bit risky. No matter how you look at it, though, it's still painful; even if you're confident that you only need to search a two-year range for meetings, that still results in 104 instances of every weekly meeting being generated and searched in case one of them has changed, not to mention any daily entries that may happen. There's not an obvious way around this, really -- it's just an unfortunate aspect of the way calendars in general work. However, it's one of the first problems with recurring meetings that you'll hit when trying to work with them. Let's assume, then, that we do this sort of filtering to get at individual instances of recurring meetings, and suffer the resulting large numbers of meetings to plough through. Problem 2 --------- What happens if we try and write a named property to an instance of a recurring meeting? Answer: it gets through. What happens if we then try and read it back? Answer: CDO claims it isn't there. To replicate this, use the following code: . . . as before, with filter added so we get instances . . . Dim m As MAPI.AppointmentItem Dim msgflds As MAPI.Fields Dim msgfld As MAPI.Field Set m = ms.GetFirst While Not m Is Nothing If m.Subject = "changed recurrence test one" Then Set msgflds = m.Fields Set msgfld = msgflds.Add("testnamedprop", vbLong, 1234) m.Update End If Set m = ms.GetNext Wend Set m = ms.GetFirst While Not m Is Nothing If m.Subject = "changed recurrence test one" Then Set msgflds = m.Fields Set msgfld = msgflds("testnamedprop") ' HERE Debug.Print msgfld.Value End If Set m = ms.GetNext Wend If you run this, you'll get an error at the line marked "HERE", MAPI_E_NOT_FOUND. This is telling us that CDO doesn't believe this field is present. Peculiar, that, given that we just wrote it to this message and saved it. Let's take a look at what the message _really_ contains, then. Use MDBView, open up the meeting from your calendar folder. You should find that it has one attachment. If you double-click on that attachment, and then select "Open Attachment as Message" from the next dialog, you'll get a low-level view of what is really in that message. Scroll down through the list of properties, and just above PR_ACCESS, you'll see a property somewhere in the 0x8000's, value 1234. This is our named property. (on this system, it has ID 0x8222). Let's check to make sure that it really is the named property we're looking for and not some other weird coincidentally-valued property that might happen to be floating around; select "Property Interface", then "GetNamesFromIDs", and enter the appropriate value into the "PropID(Hex)" box. Hit "Add", then hit "Call", then "OK". The resulting dialog will show us that the name associated with this property is, indeed, "testnamedprop". So the question is one of why is CDO hiding this property from us? What is going on, and why is CDO altering the message? Perhaps the attempt to read from this field is failing, and the field is actually there in the message, but we have to find it by hand. Change the code to read: . . . as before . . . Dim m As MAPI.AppointmentItem Dim msgflds As MAPI.Fields Dim msgfld As MAPI.Field Set m = ms.GetFirst While Not m Is Nothing If m.Subject = "changed recurrence test one" Then Set msgflds = m.Fields Dim i As Integer For i = 1 To msgflds.Count Set msgfld = msgflds(i) If msgfld.Type = vbLong Then Debug.Print Hex(msgfld.ID) & ":" & msgfld.Value End If Next End If Set m = ms.GetNext Wend This will print out a list of all the fields with type Long (to keep the list reasonably sane to read). If you look through this list, then you'll find that there's no field with the ID we're looking for. Initial conclusion: this is going to be a pain. CDO is, for some reason, going to some effort to hide information from me. Why is this? I have no idea, but it's doing it. Problem 3 --------- These broken recurring meeting instances CDO gives us are more broken than they at first appear. Given I'm writing all my code in C++ rather than VB, the obvious next step is to try and open the MAPI version of this message to see if things get any better there. Unfortunately, if we try this, we get exactly the same problem -- the message that comes from CDO is broken at a sufficiently low level that using MAPI won't let us sneak in behind the scenes and find out what's going on. To replicate this, modify the instance of the meeting on wednesday to have subject "changed recurrence test two" (so we don't have to worry about hitting the meeting instance we changed last time). Now use the following code (GetIDForName is a helper function to deal with named properties for us, it just makes this code easier to read): (apologies for any wordwrap that this may hit, by the way). HRESULT GetIDForName(LPMESSAGE pMsg, const char * szName, long * plID) { MAPINAMEID mid; mid.lpguid = (LPGUID)&PS_PUBLIC_STRINGS; mid.ulKind = MNID_STRING; char tb[256*2]; // ick! mid.Kind.lpwstrName = (wchar_t*)tb; MultiByteToWideChar((UINT)CP_ACP, (DWORD)0, szName, -1, (unsigned short *)tb, // ick! 256); LPMAPINAMEID pmid = ∣ LPSPropTagArray ptaga = NULL; HRESULT hr = pMsg->GetIDsFromNames(1, &pmid, MAPI_CREATE, &ptaga); if (FAILED(hr)) { return hr; } *plID = ptaga[0].aulPropTag[0]; MAPIFreeBuffer(ptaga); return S_OK; } void CRecurrencebugs1Dlg::OnButton1() { CoInitialize(NULL); _SessionPtr pSession; pSession.CreateInstance("MAPI.Session"); try { pSession->Logon("Default Profile"); FolderPtr pCal = pSession->GetDefaultFolder((long)CdoDefaultFolderCalendar); MessagesPtr pMsgs = pCal->Messages; MessageFilterPtr pFilt = pMsgs->Filter; FieldsPtr pFiltFields = pFilt->Fields; COleDateTime dtStart(1999, 12, 20, 1, 1, 1); COleDateTime dtEnd(1999, 12, 25, 1, 1, 1); FieldPtr pField; pField = pFiltFields->Add((long)CdoPR_START_DATE, (DATE)dtEnd); pField = pFiltFields->Add((long)CdoPR_END_DATE, (DATE)dtStart); AppointmentItemPtr pMsg = pMsgs->GetFirst(); long lPropID; { LPMESSAGE pMsgMAPI = (LPMESSAGE)(LPUNKNOWN)pMsg->MAPIOBJECT; GetIDForName(pMsgMAPI, "testnamedprop",&lPropID); pMsgMAPI->Release(); lPropID |= PT_LONG; } while (pMsg != NULL) { CString csSubject = pMsg->Subject.bstrVal; if (csSubject == "changed recurrence test two") { LPMESSAGE pMsgMAPI = (LPMESSAGE)(LPUNKNOWN)pMsg->MAPIOBJECT; SPropValue pvWrite; pvWrite.dwAlignPad = 0; pvWrite.ulPropTag = lPropID; pvWrite.Value.l = 2345L; HrSetOneProp(pMsgMAPI, &pvWrite); pMsgMAPI->SaveChanges(0); break; } pMsg = pMsgs->GetNext(); } pMsg = pMsgs->GetFirst(); while (pMsg != NULL) { CString csSubject = pMsg->Subject.bstrVal; if (csSubject == "changed recurrence test two") { LPMESSAGE pMsgMAPI = (LPMESSAGE)(LPUNKNOWN)pMsg->MAPIOBJECT; LPSPropValue pvTemp; HRESULT hr = HrGetOneProp(pMsgMAPI, lPropID, &pvTemp); TRACE("HrGetOneProp returned 0x%08x\n", hr); ULONG cValues; LPSPropValue ptaTemp; pMsgMAPI->GetProps(NULL, 0, &cValues, &ptaTemp); for (unsigned int i=0; i < cValues; i++) { if (PROP_TYPE(ptaTemp[i].ulPropTag) == PT_LONG) { TRACE("0x%08x: %d\n", ptaTemp[i].ulPropTag, ptaTemp[i].Value.l); } } break; } pMsg = pMsgs->GetNext(); } } catch (_com_error e) { TRACE("Error caught!\n%s", e.Description); } } This code does the same thing as the VB code before did, just using Extended MAPI on the raw low-level version of this message. If you run this, you'll see that the call to HrGetOneProp returns 0x8004010f, which is MAPI_E_NOT_FOUND -- and the list of properties doesn't contain our named property. However, if you use MDBView to look at the message, and now the second attachment on it, you'll find that our property is there again, now containg, as we'd expect, 2345. What does this tell us? That the message instance CDO is giving us is not simply the result of calling OpenProperty on the attachment -- it's that message, with some properties deliberately removed before we can get at it. Even MAPI can't find the properties, they're just fundamentally not there. So, how to deal with this? Well, we can go from an instance of a recurring meeting to the base meeting that "contains" that instance, by getting the parent of the RecurrencePattern object on the meeting. We could then loop over all the attachments of that message, doing OpenProperty on them, until we found the attachment corresponding to our instance. We could then look at the particular message-built-from-an-attachment we've got, and read our named property from that. How do we know which attachment corresponds to the instance we've got? Well, if we just use standard MAPI messages, we hit Problem 4 --------- CDO's calendar properties aren't documented -- so we have no convenient way to check if the message we've just instantiated from an attachment matches up with the instance of a recurring meeting we started with. This is not unsurmountable -- reverse-engineering the list of properties leads us, with some work, to the names used for calendar item properties; http://www.cdolive.com/cdo10.htm, for instance, contains a list of these. We could use those, and some more named property stuff, to do this matching up. So why don't we use the MAPIOBJECT stuff and try and create an AppointmentItem from the message we've got in MAPI? That leads us to: Problem 5 --------- You can only get AppointmentItems from the calendar folder. This is the standard CDO restriction; you can't get at messages as AppointmentItems unless you get them from the default calendar folder. This means that you can't use OpenEntry to get at them, or, in this case, turn an attachment-made-into-a-message into an AppointmentItem. Actually, it's worse than that: Problem 6 --------- The MAPIOBJECT property of messages is write-only. Despite the documentation's claims that MAPIOBJECT is read/write, you can only write to this property on Session objects. So, I guess we have to plough through the attachments using named properties searching for the attachment that corresponds to the instance of a recurring meeting we're starting with, However, if we did try and do this, we would fairly soon find ourselves hitting: Problem 7 --------- If we have to use MAPI to search attachments for instances of recurring meetings, we get an n^2-slow search. Why is this? Well, imagine we're looking for the instance of a weekly meeting with a particular value for our named property. Assume, for now, that we know we only have to look over a range of one year (call it 50 weeks, even). How many times do we expect to have to do this open-attachment-as-message thing to find this meeting? Worst-case scenario: we have to search every instance before we find it -- so 50 instances. But for each instance, we have to do (worst-case) 50 calls to OpenProperty before we can find the attachment for the particular instance in question. That's 2500 times we've had to open an attachment as a meeting -- n^2 slowness. Now, on average, we'll hit the instance we're looking for halfway through the year, and on average, we'll only have to search through half the attachments before we find a given instance there -- so it's only (n^2)/4 bad. That's still O(n^2), though, which really isn't too good. What can we do about this? Surely there has to be a better way, you might think.. Perhaps we could somehow index the appointments so that we could go from an instance of a meeting to the relevant attachment immediately -- that would cut out the search-through-attachments stage of this, and thus reduce it to an O(n) search -- which is perfectly reasonable and to be expected. Heck, we don't even need to store the attachment index -- we could just store some sort of table in the base meeting which would allow us to go directly from the instance of a meeting to the value we're looking for and not bother opening the attachment at all! Let's try that, and see just where it gets us, shall we? The first thing we'll realise is that we have to use MAPI to store this index -- it could well be a lot of information; more than we can fit in the 32k limit on properties if we write to them directly, certainly. So we have to get to the base meeting here, and then use OpenProperty(IStream) on our index property -- streams can be of arbitrary (under 4gb, probably) size. Before we go any further, then, let's check that we can do this stream writing stuff. This hits a couple of interesting glitches. [side note: for a while when first ploughing through this lot, I wound up in a situation where I couldn't write to the base meeting using MAPI, only with CDO. That problem seems to have gone away now, possibly because I'm being more careful with smart pointers, possibly something else. Another problem came up while investigating that, though] Problem 8 --------- Base recurrence meetings have invalid ENTRYIDs; there is 40 bytes of junk on the front of them for some reason. To reproduce this, replace the loop in the above code with: while (pMsg != NULL) { CString csSubject = pMsg->Subject.bstrVal; if (csSubject == "recurrence test one") { RecurrencePatternPtr pRecurPat = pMsg->GetRecurrencePattern(); AppointmentItemPtr pBaseMsg = pRecurPat->Parent; LPMESSAGE pMsgMAPI = (LPMESSAGE)(LPUNKNOWN)pBaseMsg->MAPIOBJECT; pBaseMsg = NULL; LPSPropValue pvEID; HrGetOneProp(pMsgMAPI, PR_ENTRYID, &pvEID); IMAPISession* pMAPISession = (IMAPISession*)(LPUNKNOWN)pSession->MAPIOBJECT; LPMESSAGE pMsgMAPI1; unsigned long lObjType; HRESULT hr = pMAPISession->OpenEntry(pvEID->Value.bin.cb, (ENTRYID*)pvEID->Value.bin.lpb, &IID_IMessage, 0, &lObjType, (LPUNKNOWN*)&pMsgMAPI1); TRACE("OpenEntry returned 0x%08x\n", hr); TRACE("Base EID: length %d\n", pvEID->Value.bin.cb); hr = pMAPISession->OpenEntry(pvEID->Value.bin.cb-40, (ENTRYID*)(pvEID->Value.bin.lpb+40), &IID_IMessage, 0, &lObjType, (LPUNKNOWN*)&pMsgMAPI1); TRACE("OpenEntry with 40-byte offset returned 0x%08x\n", hr); } You'll get the following result: OpenEntry returned 0x80040201 Base EID: length 110 OpenEntry with 40-byte offset returned 0x00000000 0x80040201 is E_INVALID_ENTRYID. This is because the base meeting that we get from CDO contains 40 bytes of junk on the front for some unknown reason. If we look at those 40 bytes, we find that they contain: ff000000ed0392efa5b91b10acc100aa004233260200000046000000000000000000000002000000 Splitting that up somewhat, we get: ff000000 ed0392efa5b91b10acc100aa00423326 0200000046000000000000000000000002000000 The first four bytes look pretty much like four bytes of ENTRYID flags, so assume that's what they are Second lot look vaguely familiar; if we treat it as a GUID and munge it according to KB 195656, we get: ef9203ed-b9a5-101b-acc1-00aa00423326 now, the GUID for AppointmentItem is: ef9203e8-b9a5-101b-acc1-00aa00423326 which is suspiciously similar; this is presumably an undocumented GUID from somewhere in the depths of CDO. The last lot are another 16 bytes of GUID-looking stuff; IID_IUnknown is 00000000-0000-0000-C000-000000000046 which isn't the same; there's no C0 byte in the mystery junk in our message, but it's near enough I'm convinced this is the same sort of thing. And then there's four more bytes of zeros on the end for some reason, but what the heck, let's assume there's a reason for those. Anyway, if we trim these bytes off, we get a valid ENTRYID that we can use to open messages. The reason for this is that, in earlier code, all changes to the MAPI version of the message were being discarded silently somewhere along the line -- however, that problem seems to have gone away now I'm reproducing things in a simpler environment, so I suspect it was just a mistake on my part somewhere. I'm still not clear why we get messages with broken ENTRYIDs, though. Anyway, the next thing we'll come across when we try and write to streams is: Problem 9 --------- If you try and write to a stream property on a message, those changes silently vanish unless you also do a regular set-property operation of some sort. To replicate this, use: { FolderPtr pInbox = pSession->Inbox; MessagesPtr pMsgs = pInbox->Messages; MessagePtr pMsg = pMsgs->GetFirst(); LPMESSAGE pMsgMAPI = (LPMESSAGE)(LPUNKNOWN)pMsg->MAPIOBJECT; pMsg = NULL; long lID; GetIDForName(pMsgMAPI, "testnamedprop",&lID); LPSTREAM strData; HRESULT hr = pMsgMAPI->OpenProperty( lID | PT_BINARY, &IID_IStream, // we want a stream back STGM_WRITE | MAPI_MODIFY, // need STGM_WRITE MAPI_CREATE | MAPI_MODIFY, // create this if it's not there, we want to write to it. (LPUNKNOWN *)&strData); char tb[] = "This is some text"; ULONG cbWritten; strData->Write(tb, strlen(tb), &cbWritten); strData->Commit(STGC_DEFAULT); strData->Release(); pMsgMAPI->SaveChanges(0); pMsgMAPI->Release(); } Run this, and look at the first message in your inbox. There's no change; our named property isn't there at all. If you change the OpenProperty to open a stream on the subject of the message (ie PR_SUBJECT instead of lID|PT_BINARY), then the same thing happens (or, rather, doesn't). If you run this under a debugger and watch the returned HRESULTs, they are all 0 -- every operation is claiming success. Somehow, though, the stream write is silently and mysteriously discarded. Now add the following after the call to strData->Commit: LPSPropValue pvTemp; HrGetOneProp(pMsgMAPI, PR_MESSAGE_CLASS, &pvTemp); HrSetOneProp(pMsgMAPI, pvTemp); This does absolutely nothing; we just read the message class and put it back. However, if you now try running the code again, you'll find that this time around our stream gets written properly. Why is this? What is it in the system that's causing the stream write to vanish while claiming success? How is it that we can cause the stream write to happen successfully just by doing another write? Anyway, now we know that we can write to streams on the base meeting of our recurrence, let's get back to our original problem, namely creating an index on our base meeting containing some sort of table to let us go from instances of recurring meetings to the named properties we'd like to store in them. First approach: index the meetings by date. The problem here is that meetings can be rescheduled. This isn't CDO's fault at all, it's just a fundamentally flawed approach. If we assume that we can index instances of meetings by their date, that'll break as soon as someone reschedules a meeting. For instance: We index the meeting on the 21st with our key 1234. Someone moves that meeting to the 22nd. We try and find the meeting with key 1234. We'll loop, and hit the instance on the 22nd. But now our index has no entry for the 22nd.. oops, bang, no good. So what _do_ we have about meetings that is (hopefully) unchangeable that we can use to safely index them by? Why, the ENTRYID. If we get a long-term EID by calling HrGetOneProp(PR_ENTRYID) on the message, then there's no way that that should change, surely? After all, an ENTRYID is the guaranteed way we should always be able to get at meetings. but... Problem 10 --------- ENTRYIDs of meetings change.. If we take an instance of a recurring meeting that's never been touched by anything, then the ENTRYID we get from that meeting is consistent. However, as soon as that meeting's been changed in any way, the ENTRYID we get from it is basically completely variable. To demonstrate this, use the following code: AppointmentItemPtr pMsg; for (int i=0; i < 10; i++) { pMsg = pMsgs->GetFirst(); while (pMsg != NULL) { CString csSubject = pMsg->Subject.bstrVal; if (csSubject == "changed recurrence test one") { TRACE("EID: %s\n", CString(pMsg->ID.bstrVal)); break; } pMsg = pMsgs->GetNext(); } } This just loops opening the changed instance we created some while ago and dumping the ENTRYID. If you run this code, you'll see that the ENTRYID you get is different each time through the loop. This is not good -- it means we have no way of indexing this meeting, because the theoretically-constant ENTRYID changes. (if you get the ENTRYID using HrGetOneProp and MAPI, and dump that one, you get exactly the same results as pMsg->ID, so that doesn't help). Note that this ENTRYID is changing _with nothing else going on_. Once we've created an instance of a recurring meeting, we're pretty much stuck; even though nobody's doing anything to the meeting, the ENTRYID still changes. This basically means we're doomed -- we can't index this meeting, because there's no unique aspect of a given recurrence that we can rely on to use to index it. Theoretically, ENTRYIDs should be valid, but they aren't. The time would work, if people never rescheduled meetings. This leaves us needing to hide some sort of index information in the meeting in another way -- ie, named properties. But that's where we started.. One might now turn to PR_SEARCH_KEY -- after all, according to the documentation for PR_SEARCH_KEY, it is unique within "the entire world". However, testing this hits: Problem 11 ---------- PR_SEARCH_KEY on messages built from attachment is always the same. Changing the code above to dump PR_SEARCH_KEY for the instances of our recurring meeting, as follows: CString csSubject = pMsg->Subject.bstrVal; if (csSubject.Right(3) == "one") { LPMESSAGE pMsgMAPI = (LPMESSAGE)(LPUNKNOWN)pMsg->MAPIOBJECT; LPSPropValue pvTemp; HrGetOneProp(pMsgMAPI, PR_SEARCH_KEY, &pvTemp); TRACE("PR_SEARCH_KEY: "); for (UINT j=0; j < pvTemp->Value.bin.cb; j++) { TRACE("%02X", pvTemp->Value.bin.lpb[j]); } TRACE("\n"); } } shows us that the PR_SEARCH_KEY is the same for all instances of a given recurring meeting, in other words it's no use to us. What about PR_RECORD_KEY? That's meant to be unique to a given attachment, and also consistent across multiple openings of the attachment. Sadly, by the time CDO has finished turning attachments into messages, this information is lost, and we get the same value for all instances of a given recurrence again. (to check this, just change the code above to use PR_RECORD_KEY rather than PR_SEARCH_KEY). The next thing I looked at was other ways to hide this information in the meeting -- if named properties won't work, and there's no reliable way to uniquely identify the meeting, then I need another way of doing things. Sadly, there doesn't seem to be any tidy way to do this at all. One solution would be to add my index value to the subject for the meeting -- but that'll break if anyone renames it, and it's ugly and visible to the users. I thought about trying to hide it in the list of categories for the meeting -- but categories only apply to the whole recurrence, so that's no good. This leaves us with the ghastly n^2 solution. One might, at this point, worry about something like the following set of events: 1. We get an instance of a meeting. 2. We drop up to the base meeting, and start looping over attachments opening them and doing the named property stuff required to get at the start time for this attachment, looking for the one that matches up with the instance we started from. 3. While we're looping, someone reschedules the original meeting. 4. We never find the attachment, because the times don't match up by the time we loop onto it. Fortunately, this doesn't seem to happen; testing this scenario by deliberately pausing the search with ::MessageBox(), changing the message in the calendar, and then continuing the search, it appears that the change doesn't percolate through to the attachments in this way. This is presumably due to CDO's local caching of objects rather than anything else, but it appears we can get away with this particular problem as long as we're careful about not doing any Release() calls as late as possible. So, it looks as if we can make this work -- though only with some nasty slow algorithmic pain involved. Possible optimisations for the search: Keep the base meetings for every recurring meeting we hit in memory (that way they can't get rescheduled out from under us). Keep an index of which attachment is at which time as we process them. Use that index to save ourselves looping over all the attachments the second/third/etc time we look at the instances of a given message. This is the standard speed-vs-memory tradeoff.. It appears that we can get away with this as long as we keep the base meeting for each recurrence in memory; we don't need to keep the instances around, and if we reschedule instances, from our point of view they don't change because the base recurrence is still in memory. This isn't too bad, actually -- sure, we have to keep a message object in memory for every recurrence in the calendar, but that's only O(n)-bad in terms of memory consumption. However, this still seems like an awful lot of nasty hacks, so let's think about things from the start again, in case we've missed something. The original idea was to index by date -- but that doesn't work, because people can reschedule meetings. Is this really a problem? If we look at the documentation for this, it claims that you can only reschedule instances of recurring meetings so that they stay after the preceeding recurrence and before the next one; so you only have a week's worth of room in which you can move a given instance of a weekly meeting. (okay, you could get around this if you moved the previous week's one back as far as possible, then this week's one would have six more days of room, etc, but you get the idea). A bit of testing confirms this. We hit an interesting bug in Outlook where it asks me three times if I'm sure that I want to move just this instance of the meeting rather than the whole series -- but other than that, it seems to do what the documentation claims. So, can we use this to our advantage? This sounds as if we should be able to use this information somehow to index things; even if people do move meetings, then we're still okay because we know which one it is by checking the times of the meetings on either side of it -- for instance: 1. Weekly meeting on 1st, 8th, 15th, 22nd, 29th. 2. We index instance on the 8th with 1234. 3. Someone reschedules the 8th instance to the 9th. 4. We loop, hit the instance on the 9th, and try to find it. Our index lookup misses on the 9th, because there's no meeting there; so we drop back to sequence search. 5. Sequence search tells us that the 9th is after the 1st and before the 15th -- that range can only contain the meeting that used to be on the 8th because we can't reschedule meetings out of order. 6. So we use the value for the 8th, 1234, and update the index so that next time through we don't have to do the search again. Is this okay? Sadly, no. Imagine the the following, admittedly unlikely, but possible set of events: 1. Weekly meeting on 1st, 8th, etc as before. 2. We index the instance on the 8th with 1234 and the 15th with 2345. 3. Someone reschedules things back almost-a-week: 8th -> 2nd. 15th -> 8th 22nd -> 15th. These are all okay, because the recurrences are still in order. 4. We loop over meetings again, and hit the instance that used to be on the 15th. Now it's on the 8th, so we search our index for meetings on the 8th. We still have an entry there -- but it's the wrong one.. This isn't too good. We can get around _this_ problem, but only by n^2 badness again -- each time we hit an instance of a meeting, we have to redo _all_ our index-by-date in case of this sort of thing. Not a great improvement, to be honest. Are there other properties in the recurrences we can use to hide information in? Looking at the list of properties CDO gives us on recurrences (ie not the full list that's really there, but the list that CDO decides we're allowed to see) there isn't anything. There's lots of properties, sure, but I don't think we can change any of them; there are basically three sorts of properties on a message: 1. Cosmetic-only ones. These are things like the title, body text, etc. They have no real effect on the meeting, but the user can see them. We can't hide information in these, because the user can see them.. 2. Internal-only ones. Things like the ENTRYID, etc. We can't use these, because while the user may not see them, we don't know that we won't be horribly breaking everything when we change them -- perhaps we'll end up with broken messages that we can't open. 3. Custom named properties. These get hidden by CDO for whatever reason.. Hm. While poking around checking PR_RECORD_KEY, we notice a couple of properties in the _raw_ attachments for the message. These are 0x7ffb and 0x7ffc -- and they seem suspiciously similar to the start and end times of the meeting in question. Strangely, they're in local time -- whereas all other times are stored in GMT. If these are consistent, and a bit more poking around at other meetings would suggest they are, then I think this actually does the trick. What we do now is: Write named properties as usual. Reading them, we: 1. Get instance from CDO post-filter-by-date. 2. Read start time from this instance. 3. Drop back to the base MAPI meeting. 4. Search the table of attachments looking for property 0x7ffb = start time. 5. Open this attachment as a message. 6. Use MAPI to read the named property. Now, this is still O(n^2), but with a much much smaller constant of proportionality. Instead of having to open each attachment as a message when looping over attachments, we can just search the table of attachments -- and searching tables is something MAPI makes it easy (and, hopefully, fast) to do. Problem: 0x7ffb is not a trustworthy-sounding property ID. It's not in the range for named properties, it's in the range for custom properties on messages of type other than IPM.Note. That's fair enough, because calendar entries are of type IPM.Appointment. Well, sort of fair enough -- it's not clear that this range is usable for propecrties on attachments, but let's cross our fingers for now and hope we can get away with it -- it's not in the range for named properties, so perhaps it'll be consistent across different Exchange servers. If it isn't, we're really stuck, because you can't call GetNamesFromIDs on proptags under 0x8000.. When we start doing this, however, we hit: Problem 12 ---------- Attachment tables don't support restrictions on this 0x7ffb property. If we try and restrict, we get MAPI_E_NO_SUPPORT. No code sample for this, sorry, I nuked it and rewrote the loop manually. Now, this isn't a horrible disaster -- we can just SetColumns so that the amount of data coming from the server is minimised (we only need two properties; PR_ATTACH_NUM so we know which attachment to open, and the 0x7ffb thing to search on), and while we still have to loop over all the attachments, we're just doing a CompareFileTime() on each row, which is (hopefully) just an 8-byte memcmp; nothing too horrible. Once we've found the appropriate attachment, we then use OpenProperty et al to get at the message to read our named property out of it. Note that we don't have to use MAPI to get at messages; code like: Set objAttachment = objMessage.Attachments.Items(1) Set objEmbeddedMsg = objAttachment.Source will let us open attachments as messages using CDO. (thanks to Siegfried Weber for pointing this out). We could, instead, save ourselves some time by writing/reading the property to the attachment, not the message contained in the attachment. This is a problem for the first time through, though, because then there is no attachment because the message hasn't been instantiated yet.. We'd have to loop and search even when we're writing -- if we just set the property on the base CDO message using the obvious code to do this, we save ourselves a lot of hassle at the writing end of things (we don't even have any different code for recurring meetings, come to think of it -- we just write and save in every case). Reading is more painful, but still fairly simple; loop over attachments -- if no matching attachment, our property can't be there. If we get a match, we open that message and try a named property read; if nothing, fine, no property. It's difficult to know if this is or isn't faster in the long run; is the time taken to do an OpenProperty on the message to read it outweighed by the time taken to loop and search when we're writing? Without testing, it's impossible to tell. A bit of thought would suggest that, in the case of the code I'm writing, at least, it would probably be worth optimising for read speed, because I read more often than I write, and a search for a message with a given property does a lot of reads no matter how you look at it. However, for the time being I'm going to go for simplicity over optimality and see how it goes. Anyway, glitches and the like aside, this does work. So, to summarise, if you want to do named property stuff on instances of recurring meetings, you need to: Writing: just write as normal. Reading: a. Open instance of meeting. b. Get the Parent of the meeting's RecurrencePattern; this is the "base" message. c. Loop over the table of attachments in the base meeting searching for an attachment where property 0x7ffb0000 | PT_FILETIME contains the same time as the start of the meeting instance (no timezone issues to worry about here). d. Read the PR_ATTACHMENT_INDEX property from this row, and then do: hr = pMsgMAPI->OpenAttach(index, &IID_IMAPIProp, MAPI_BEST_ACCESS, (IAttach**)&pAttach1); LPMESSAGE pAttachMsg; hr = pAttach1->OpenProperty(PR_ATTACH_DATA_OBJ,&IID_IMessage,0,0, (LPUNKNOWN*)&pAttachMsg); Alternatively, do this with CDO, like: Set objAttachment = objMessage.Attachments.Items(1) Set objEmbeddedMsg = objAttachment.Source e. Use MAPI named property stuff to read your property from pAttachMsg. and there you go. Not nice, but it works, and does the right thing even through moving meetings around, moving them to different days, moving them so that they are where another instance was originally, etc.