123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398 |
- /*
- * Copyright 2005 - 2016 Zarafa and its licensors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- #include <new>
- #include <kopano/platform.h>
- #include <kopano/ECInterfaceDefs.h>
- #include <kopano/memory.hpp>
- #include <kopano/mapi_ptr.h>
- #include "ECExchangeExportChanges.h"
- #include "WSMessageStreamExporter.h"
- #include "WSSerializedMessage.h"
- #include <set>
- #include <kopano/Util.h>
- #include <kopano/ECGuid.h>
- #include <edkguid.h>
- #include <mapiguid.h>
- #include <kopano/mapiext.h>
- #include <mapiutil.h>
- #include "ics.h"
- #include <kopano/ECDebug.h>
- #include "Mem.h"
- #include "ECMessage.h"
- #include <kopano/stringutil.h>
- #include "ECSyncLog.h"
- #include "ECSyncUtil.h"
- #include "ECSyncSettings.h"
- #include "EntryPoint.h"
- #include <kopano/CommonUtil.h>
- // We use ntohl/htonl for network-order conversion
- #include <arpa/inet.h>
- #include <kopano/charset/convert.h>
- using namespace KCHL;
- ECExchangeExportChanges::ECExchangeExportChanges(ECMsgStore *lpStore, const std::string &sk, const wchar_t * szDisplay, unsigned int ulSyncType)
- : m_iidMessage(IID_IMessage)
- {
- ECSyncLog::GetLogger(&m_lpLogger);
- m_lpStore = lpStore;
- m_sourcekey = sk;
- m_strDisplay = szDisplay ? szDisplay : L"<Unknown>";
- m_ulSyncType = ulSyncType;
- // In server-side sync, only use a batch size of 1.
- if (m_sourcekey.empty())
- m_ulBatchSize = 1;
- else
- m_ulBatchSize = 256;
- memset(&m_tmsStart, 0, sizeof(m_tmsStart));
- m_lpStore->AddRef();
- }
- ECExchangeExportChanges::~ECExchangeExportChanges(){
- MAPIFreeBuffer(m_lpChanges);
- if(m_lpStore)
- m_lpStore->Release();
- if(m_lpStream)
- m_lpStream->Release();
- if(m_lpImportContents)
- m_lpImportContents->Release();
- if(m_lpImportStreamedContents)
- m_lpImportStreamedContents->Release();
- if(m_lpImportHierarchy)
- m_lpImportHierarchy->Release();
- MAPIFreeBuffer(m_lpRestrict);
- if(m_lpLogger)
- m_lpLogger->Release();
- }
- HRESULT ECExchangeExportChanges::SetLogger(ECLogger *lpLogger)
- {
- if(m_lpLogger)
- m_lpLogger->Release();
- m_lpLogger = lpLogger;
- if (m_lpLogger != NULL)
- m_lpLogger->AddRef();
- return hrSuccess;
- }
- HRESULT ECExchangeExportChanges::Create(ECMsgStore *lpStore, REFIID iid, const std::string& sourcekey, const wchar_t *szDisplay, unsigned int ulSyncType, LPEXCHANGEEXPORTCHANGES* lppExchangeExportChanges){
- if (lpStore == NULL || (ulSyncType != ICS_SYNC_CONTENTS && ulSyncType != ICS_SYNC_HIERARCHY))
- return MAPI_E_INVALID_PARAMETER;
- return alloc_wrap<ECExchangeExportChanges>(lpStore, sourcekey,
- szDisplay, ulSyncType).as(iid, lppExchangeExportChanges);
- }
- HRESULT ECExchangeExportChanges::QueryInterface(REFIID refiid, void **lppInterface)
- {
- REGISTER_INTERFACE2(ECExchangeExportChanges, this);
- REGISTER_INTERFACE2(ECUnknown, this);
- REGISTER_INTERFACE2(IExchangeExportChanges, &this->m_xECExportChanges);
- REGISTER_INTERFACE2(IUnknown, &this->m_xECExportChanges);
- REGISTER_INTERFACE2(IECExportChanges, &this->m_xECExportChanges);
- return MAPI_E_INTERFACE_NOT_SUPPORTED;
- }
- HRESULT ECExchangeExportChanges::GetLastError(HRESULT hResult, ULONG ulFlags, LPMAPIERROR *lppMAPIError){
- HRESULT hr = hrSuccess;
- ecmem_ptr<MAPIERROR> lpMapiError;
- memory_ptr<TCHAR> lpszErrorMsg;
- //FIXME: give synchronization errors messages
- hr = Util::HrMAPIErrorToText((hResult == hrSuccess)?MAPI_E_NO_ACCESS : hResult, &~lpszErrorMsg);
- if (hr != hrSuccess)
- return hr;
- hr = ECAllocateBuffer(sizeof(MAPIERROR), &~lpMapiError);
- if(hr != hrSuccess)
- return hr;
- if (ulFlags & MAPI_UNICODE) {
- std::wstring wstrErrorMsg = convert_to<std::wstring>(lpszErrorMsg.get());
- std::wstring wstrCompName = convert_to<std::wstring>(g_strProductName.c_str());
- if ((hr = MAPIAllocateMore(sizeof(std::wstring::value_type) * (wstrErrorMsg.size() + 1), lpMapiError, (void**)&lpMapiError->lpszError)) != hrSuccess)
- return hr;
- wcscpy((wchar_t*)lpMapiError->lpszError, wstrErrorMsg.c_str());
- if ((hr = MAPIAllocateMore(sizeof(std::wstring::value_type) * (wstrCompName.size() + 1), lpMapiError, (void**)&lpMapiError->lpszComponent)) != hrSuccess)
- return hr;
- wcscpy((wchar_t*)lpMapiError->lpszComponent, wstrCompName.c_str());
- } else {
- std::string strErrorMsg = convert_to<std::string>(lpszErrorMsg.get());
- std::string strCompName = convert_to<std::string>(g_strProductName.c_str());
- if ((hr = MAPIAllocateMore(strErrorMsg.size() + 1, lpMapiError, (void**)&lpMapiError->lpszError)) != hrSuccess)
- return hr;
- strcpy((char*)lpMapiError->lpszError, strErrorMsg.c_str());
- if ((hr = MAPIAllocateMore(strCompName.size() + 1, lpMapiError, (void**)&lpMapiError->lpszComponent)) != hrSuccess)
- return hr;
- strcpy((char*)lpMapiError->lpszComponent, strCompName.c_str());
- }
- lpMapiError->ulContext = 0;
- lpMapiError->ulLowLevelError= 0;
- lpMapiError->ulVersion = 0;
- *lppMAPIError = lpMapiError.release();
- return hrSuccess;
- }
- HRESULT ECExchangeExportChanges::Config(LPSTREAM lpStream, ULONG ulFlags, LPUNKNOWN lpCollector, LPSRestriction lpRestriction, LPSPropTagArray lpIncludeProps, LPSPropTagArray lpExcludeProps, ULONG ulBufferSize){
- HRESULT hr;
- ULONG ulSyncId = 0;
- ULONG ulChangeId = 0;
- ULONG ulStep = 0;
- BOOL bCanStream = FALSE;
- bool bForceImplicitStateUpdate = false;
- ECSyncSettings *lpSyncSettings = ECSyncSettings::GetInstance();
- typedef std::map<SBinary, ChangeListIter, Util::SBinaryLess> ChangeMap;
- typedef ChangeMap::iterator ChangeMapIter;
- ChangeMap mapChanges;
- ChangeMapIter iterLastChange;
- ChangeList lstChange;
- std::string sourcekey;
- if(m_bConfiged){
- ZLOG_DEBUG(m_lpLogger, "Config() called twice");
- return MAPI_E_UNCONFIGURED;
- }
- if(lpRestriction) {
- hr = Util::HrCopySRestriction(&m_lpRestrict, lpRestriction);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Invalid restriction");
- return hr;
- }
- } else {
- m_lpRestrict = NULL;
- }
- m_ulFlags = ulFlags;
- if(! (ulFlags & SYNC_CATCHUP)) {
- if(lpCollector == NULL) {
- ZLOG_DEBUG(m_lpLogger, "No importer to export to");
- return MAPI_E_INVALID_PARAMETER;
- }
- // We don't need the importer when doing SYNC_CATCHUP
- if(m_ulSyncType == ICS_SYNC_CONTENTS){
- hr = lpCollector->QueryInterface(IID_IExchangeImportContentsChanges, (LPVOID*) &m_lpImportContents);
- if (hr == hrSuccess && lpSyncSettings->SyncStreamEnabled()) {
- m_lpStore->lpTransport->HrCheckCapabilityFlags(KOPANO_CAP_ENHANCED_ICS, &bCanStream);
- if (bCanStream == TRUE) {
- ZLOG_DEBUG(m_lpLogger, "Exporter supports enhanced ICS, checking importer...");
- hr = lpCollector->QueryInterface(IID_IECImportContentsChanges, (LPVOID*) &m_lpImportStreamedContents);
- if (hr == MAPI_E_INTERFACE_NOT_SUPPORTED) {
- assert(m_lpImportStreamedContents == NULL);
- hr = hrSuccess;
- ZLOG_DEBUG(m_lpLogger, "Importer doesn't support enhanced ICS");
- } else
- ZLOG_DEBUG(m_lpLogger, "Importer supports enhanced ICS");
- } else
- ZLOG_DEBUG(m_lpLogger, "Exporter doesn't support enhanced ICS");
- }
- }else if(m_ulSyncType == ICS_SYNC_HIERARCHY){
- hr = lpCollector->QueryInterface(IID_IExchangeImportHierarchyChanges, (LPVOID*) &m_lpImportHierarchy);
- }else{
- hr = MAPI_E_INVALID_PARAMETER;
- }
- if(hr != hrSuccess)
- return hr;
- }
- if (lpStream == NULL){
- LARGE_INTEGER lint = {{ 0, 0 }};
- ULONG tmp[2] = { 0, 0 };
- ULONG ulSize = 0;
- ZLOG_DEBUG(m_lpLogger, "Creating new exporter stream");
- hr = CreateStreamOnHGlobal(GlobalAlloc(GPTR, sizeof(tmp)), true, &m_lpStream);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to create new exporter stream");
- return hr;
- }
- m_lpStream->Seek(lint, STREAM_SEEK_SET, NULL);
- m_lpStream->Write(tmp, sizeof(tmp), &ulSize);
- } else {
- hr = lpStream->QueryInterface(IID_IStream, (LPVOID*)&m_lpStream);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Passed state stream does not support IStream interface");
- return hr;
- }
- }
- hr = HrDecodeSyncStateStream(m_lpStream, &ulSyncId, &ulChangeId, &m_setProcessedChanges);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to decode sync state stream, hr=0x%08x", hr);
- return hr;
- }
- ZLOG_DEBUG(m_lpLogger, "Decoded state stream: syncid=%u, changeid=%u, processed changes=%lu", ulSyncId, ulChangeId, (long unsigned int)m_setProcessedChanges.size());
- if(ulSyncId == 0) {
- ZLOG_DEBUG(m_lpLogger, "Getting new sync id for %s folder '%ls'...", (m_lpStore->IsOfflineStore() ? "offline" : "online"), m_strDisplay.c_str());
- // Ignore any trailing garbage in the stream
- if (m_sourcekey.size() < 24 && (m_sourcekey[m_sourcekey.size()-1] & 0x80)) {
- // If we're getting a truncated sourcekey, untruncate it now so we can pass it to GetChanges()
- sourcekey = m_sourcekey;
- sourcekey[m_sourcekey.size()-1] &= ~0x80;
- sourcekey.append(1, '\x00');
- sourcekey.append(1, '\x00');
- } else {
- sourcekey = m_sourcekey;
- }
- // Register our sync with the server, get a sync ID
- hr = m_lpStore->lpTransport->HrSetSyncStatus(sourcekey, ulSyncId, ulChangeId, m_ulSyncType, 0, &ulSyncId);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to update sync status on server, hr=0x%08x", hr);
- return hr;
- }
- ZLOG_DEBUG(m_lpLogger, "New sync id for %s folder '%ls': %u", (m_lpStore->IsOfflineStore() ? "offline" : "online"), m_strDisplay.c_str(), ulSyncId);
- bForceImplicitStateUpdate = true;
- }
- MAPIFreeBuffer(m_lpChanges);
- m_lpChanges = NULL;
- hr = m_lpStore->lpTransport->HrGetChanges(sourcekey, ulSyncId, ulChangeId, m_ulSyncType, ulFlags, m_lpRestrict, &m_ulMaxChangeId, &m_ulChanges, &m_lpChanges);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to get changes from server, hr=0x%08x", hr);
- return hr;
- }
- m_ulSyncId = ulSyncId;
- m_ulChangeId = ulChangeId;
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "%s folder: %ls changes: %u syncid: %u changeid: %u", (m_lpStore->IsOfflineStore() ? "offline" : "online"), m_strDisplay.c_str(), m_ulChanges, m_ulSyncId, m_ulChangeId);
- /**
- * Filter the changes.
- * How this works:
- * If no previous change has been seen for a particular sourcekey, the change will just be added to
- * the correct list. The iterator into that list is stored in a map, keyed by the sourcekey.
- * If a previous change is found, action is taken based on the type of that change. The action can
- * involve removing the change from a change list and/or adding it to another list.
- *
- * Note that lstChange is a local list, which is copied into m_lstChange after filtering. This is
- * done because m_lstChange is not a list but a vector, which is not well suited for removing
- * items in the middle of the data.
- */
- for (ulStep = 0; ulStep < m_ulChanges; ++ulStep) {
- // First check if this change hasn't been processed yet
- if (m_setProcessedChanges.find(std::pair<unsigned int, std::string>(m_lpChanges[ulStep].ulChangeId, std::string((char *)m_lpChanges[ulStep].sSourceKey.lpb, m_lpChanges[ulStep].sSourceKey.cb))) != m_setProcessedChanges.end())
- continue;
- iterLastChange = mapChanges.find(m_lpChanges[ulStep].sSourceKey);
- if (iterLastChange == mapChanges.end()) {
- switch (ICS_ACTION(m_lpChanges[ulStep].ulChangeType)) {
- case ICS_NEW:
- case ICS_CHANGE:
- mapChanges.insert(ChangeMap::value_type(m_lpChanges[ulStep].sSourceKey, lstChange.insert(lstChange.end(), m_lpChanges[ulStep])));
- break;
- case ICS_FLAG:
- mapChanges.insert(ChangeMap::value_type(m_lpChanges[ulStep].sSourceKey, m_lstFlag.insert(m_lstFlag.end(), m_lpChanges[ulStep])));
- break;
- case ICS_SOFT_DELETE:
- mapChanges.insert(ChangeMap::value_type(m_lpChanges[ulStep].sSourceKey, m_lstSoftDelete.insert(m_lstSoftDelete.end(), m_lpChanges[ulStep])));
- break;
- case ICS_HARD_DELETE:
- mapChanges.insert(ChangeMap::value_type(m_lpChanges[ulStep].sSourceKey, m_lstHardDelete.insert(m_lstHardDelete.end(), m_lpChanges[ulStep])));
- break;
- default:
- break;
- }
- } else {
- switch (ICS_ACTION(m_lpChanges[ulStep].ulChangeType)) {
- case ICS_NEW:
- // This shouldn't happen since apparently we have another change for the same object.
- // However, if an object gets moved to another folder and back, we'll get a delete followed by an add. If that happens
- // we skip the delete and morph the current add to a change.
- if (ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_SOFT_DELETE || ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_HARD_DELETE) {
- ChangeListIter iterNewChange = lstChange.insert(lstChange.end(), *iterLastChange->second);
- iterNewChange->ulChangeType = (iterNewChange->ulChangeType & ~ICS_ACTION_MASK) | ICS_CHANGE;
- if (ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_SOFT_DELETE)
- m_lstSoftDelete.erase(iterLastChange->second);
- else
- m_lstHardDelete.erase(iterLastChange->second);
- iterLastChange->second = iterNewChange;
- ZLOG_DEBUG(m_lpLogger, "Got an ICS_NEW change for a previously deleted object. I converted it to a change. sourcekey=%s", bin2hex(m_lpChanges[ulStep].sSourceKey.cb, m_lpChanges[ulStep].sSourceKey.lpb).c_str());
- } else
- ZLOG_DEBUG(m_lpLogger, "Got an ICS_NEW change for an object we've seen before. prev_change=%04x, sourcekey=%s", iterLastChange->second->ulChangeType, bin2hex(m_lpChanges[ulStep].sSourceKey.cb, m_lpChanges[ulStep].sSourceKey.lpb).c_str());
- break;
- case ICS_CHANGE:
- // Any change is allowed as the previous change except a change.
- if (ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_CHANGE)
- ZLOG_DEBUG(m_lpLogger, "Got an ICS_CHANGE on an object for which we just saw an ICS_CHANGE. sourcekey=%s", bin2hex(m_lpChanges[ulStep].sSourceKey.cb, m_lpChanges[ulStep].sSourceKey.lpb).c_str());
- // A previous delete is allowed for the same reason as in ICS_NEW.
- if (ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_SOFT_DELETE || ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_HARD_DELETE) {
- if (ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_SOFT_DELETE)
- m_lstSoftDelete.erase(iterLastChange->second);
- else
- m_lstHardDelete.erase(iterLastChange->second);
- iterLastChange->second = lstChange.insert(lstChange.end(), m_lpChanges[ulStep]);
- ZLOG_DEBUG(m_lpLogger, "Got an ICS_CHANGE change for a previously deleted object. sourcekey=%s", bin2hex(m_lpChanges[ulStep].sSourceKey.cb, m_lpChanges[ulStep].sSourceKey.lpb).c_str());
- } else if (ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_FLAG) {
- m_lstFlag.erase(iterLastChange->second);
- iterLastChange->second = lstChange.insert(lstChange.end(), m_lpChanges[ulStep]);
- ZLOG_DEBUG(m_lpLogger, "Upgraded a previous ICS_FLAG to ICS_CHANGED. sourcekey=%s", bin2hex(m_lpChanges[ulStep].sSourceKey.cb, m_lpChanges[ulStep].sSourceKey.lpb).c_str());
- } else
- ZLOG_DEBUG(m_lpLogger, "Ignoring ICS_CHANGE due to a previous change. prev_change=%04x, sourcekey=%s", iterLastChange->second->ulChangeType, bin2hex(m_lpChanges[ulStep].sSourceKey.cb, m_lpChanges[ulStep].sSourceKey.lpb).c_str());
- break;
- case ICS_FLAG:
- // This is only allowed after an ICS_NEW and ICS_CHANGE. It will be ignored in any case.
- if (ICS_ACTION(iterLastChange->second->ulChangeType) != ICS_NEW && ICS_ACTION(iterLastChange->second->ulChangeType) != ICS_CHANGE)
- ZLOG_DEBUG(m_lpLogger, "Got an ICS_FLAG with something else than a ICS_NEW or ICS_CHANGE as the previous changes. prev_change=%04x, sourcekey=%s", iterLastChange->second->ulChangeType, bin2hex(m_lpChanges[ulStep].sSourceKey.cb, m_lpChanges[ulStep].sSourceKey.lpb).c_str());
- ZLOG_DEBUG(m_lpLogger, "Ignoring ICS_FLAG due to previous ICS_NEW or ICS_CHANGE. prev_change=%04x, sourcekey=%s", iterLastChange->second->ulChangeType, bin2hex(m_lpChanges[ulStep].sSourceKey.cb, m_lpChanges[ulStep].sSourceKey.lpb).c_str());
- break;
- case ICS_SOFT_DELETE:
- case ICS_HARD_DELETE:
- // We'll ignore the previous change and replace it with this delete. We won't write it now as
- // we could get an add for the same object. But because of the reordering (deletes after all adds/changes) we would delete
- // the new object. Therefore we'll make a change out of a delete - add/change.
- ZLOG_DEBUG(m_lpLogger, "Replacing previous change with current ICS_xxxx_DELETE. prev_change=%04x, sourcekey=%s", iterLastChange->second->ulChangeType, bin2hex(m_lpChanges[ulStep].sSourceKey.cb, m_lpChanges[ulStep].sSourceKey.lpb).c_str());
- if (ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_NEW || ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_CHANGE)
- lstChange.erase(iterLastChange->second);
- else if (ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_FLAG)
- m_lstFlag.erase(iterLastChange->second);
- else if (ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_SOFT_DELETE)
- m_lstSoftDelete.erase(iterLastChange->second);
- else if (ICS_ACTION(iterLastChange->second->ulChangeType) == ICS_HARD_DELETE)
- m_lstHardDelete.erase(iterLastChange->second);
- if (ICS_ACTION(m_lpChanges[ulStep].ulChangeType) == ICS_SOFT_DELETE)
- iterLastChange->second = m_lstSoftDelete.insert(m_lstSoftDelete.end(), m_lpChanges[ulStep]);
- else
- iterLastChange->second = m_lstHardDelete.insert(m_lstHardDelete.end(), m_lpChanges[ulStep]);
- break;
- default:
- ZLOG_DEBUG(m_lpLogger, "Got an unknown change. change=%04x, sourcekey=%s", m_lpChanges[ulStep].ulChangeType, bin2hex(m_lpChanges[ulStep].sSourceKey.cb, m_lpChanges[ulStep].sSourceKey.lpb).c_str());
- break;
- }
- }
- }
- m_lstChange.assign(lstChange.begin(), lstChange.end());
- if(ulBufferSize != 0){
- m_ulBufferSize = ulBufferSize;
- }else{
- m_ulBufferSize = 10;
- }
- m_bConfiged = true;
- if (bForceImplicitStateUpdate) {
- ZLOG_DEBUG(m_lpLogger, "Forcing state update for %s folder '%ls'", (m_lpStore->IsOfflineStore() ? "offline" : "online"), m_strDisplay.c_str());
- if (m_ulChangeId == 0)
- UpdateState(NULL);
- }
- return hrSuccess;
- }
- HRESULT ECExchangeExportChanges::Synchronize(ULONG *lpulSteps, ULONG *lpulProgress)
- {
- HRESULT hr = hrSuccess;
- if(!m_bConfiged){
- ZLOG_DEBUG(m_lpLogger, "Config() not called before Synchronize()");
- return MAPI_E_UNCONFIGURED;
- }
- if(m_ulFlags & SYNC_CATCHUP){
- m_ulChangeId = m_ulMaxChangeId > m_ulChangeId ? m_ulMaxChangeId : m_ulChangeId;
- hr = UpdateStream(m_lpStream);
- if (hr == hrSuccess)
- *lpulProgress = *lpulSteps = 0;
- return hr;
- }
- if (*lpulProgress == 0 && m_lpLogger->Log(EC_LOGLEVEL_DEBUG))
- m_clkStart = times(&m_tmsStart);
- if(m_ulSyncType == ICS_SYNC_CONTENTS){
- hr = ExportMessageChanges();
- if(hr == SYNC_W_PROGRESS)
- goto progress;
- if(hr != hrSuccess)
- return hr;
- hr = ExportMessageDeletes();
- if(hr != hrSuccess)
- return hr;
- hr = ExportMessageFlags();
- if(hr != hrSuccess)
- return hr;
- }else if(m_ulSyncType == ICS_SYNC_HIERARCHY){
- hr = ExportFolderChanges();
- if(hr == SYNC_W_PROGRESS)
- goto progress;
- if(hr != hrSuccess)
- return hr;
- hr = ExportFolderDeletes();
- if(hr != hrSuccess)
- return hr;
- }else{
- return MAPI_E_INVALID_PARAMETER;
- }
- hr = UpdateStream(m_lpStream);
- if(hr != hrSuccess)
- return hr;
- if(! (m_ulFlags & SYNC_CATCHUP)) {
- if (m_ulSyncType == ICS_SYNC_CONTENTS)
- hr = m_lpImportContents->UpdateState(NULL);
- else
- hr = m_lpImportHierarchy->UpdateState(NULL);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Importer state update failed, hr=0x%08x", hr);
- return hr;
- }
- }
- progress:
- if(hr == hrSuccess) {
- // The sync has completed. This means we can move to the next change ID on the server, and clear our
- // processed change list. If we can't save the new state the the server, we just keep the previous
- // change ID and the (large) change list. This allows us always to have a state, even when we can't
- // communicate with the server.
- if(m_lpStore->lpTransport->HrSetSyncStatus(m_sourcekey, m_ulSyncId, m_ulMaxChangeId, m_ulSyncType, 0, &m_ulSyncId) == hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Done: syncid=%u, changeid=%u/%u", m_ulSyncId, m_ulChangeId, m_ulMaxChangeId);
- m_ulChangeId = m_ulMaxChangeId;
- m_setProcessedChanges.clear();
- if(m_ulChanges) {
- if (m_lpLogger->Log(EC_LOGLEVEL_DEBUG)) {
- struct tms tmsEnd = {0};
- clock_t clkEnd = times(&tmsEnd);
- double dblDuration = 0;
- char szDuration[64] = {0};
- // Calculate diff
- dblDuration = (double)(clkEnd - m_clkStart) / TICKS_PER_SEC;
- if (dblDuration >= 60)
- snprintf(szDuration, sizeof(szDuration), "%u:%02u.%03u min.", (unsigned)(dblDuration / 60), (unsigned)dblDuration % 60, (unsigned)(dblDuration * 1000 + .5) % 1000);
- else
- snprintf(szDuration, sizeof(szDuration), "%u.%03u s.", (unsigned)dblDuration % 60, (unsigned)(dblDuration * 1000 + .5) % 1000);
- m_lpLogger->Log(EC_LOGLEVEL_DEBUG, "folder changes synchronized in %s", szDuration);
- } else
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "folder changes synchronized");
- }
- }
- }
- *lpulSteps = m_lstChange.size();
- *lpulProgress = m_ulStep;
- return hr;
- }
- HRESULT ECExchangeExportChanges::UpdateState(LPSTREAM lpStream){
- if(!m_bConfiged){
- ZLOG_DEBUG(m_lpLogger, "Config() not called before UpdateState()");
- return MAPI_E_UNCONFIGURED;
- }
- if (lpStream == NULL)
- lpStream = m_lpStream;
- return UpdateStream(lpStream);
- }
- HRESULT ECExchangeExportChanges::GetChangeCount(ULONG *lpcChanges) {
- ULONG cChanges = 0;
- if(!m_bConfiged){
- ZLOG_DEBUG(m_lpLogger, "Config() not called before GetChangeCount()");
- return MAPI_E_UNCONFIGURED;
- }
- // Changes in flags or deletions are only one step all together
- if(!m_lstHardDelete.empty() || !m_lstSoftDelete.empty() || !m_lstFlag.empty())
- ++cChanges;
- cChanges += m_lstChange.size();
- *lpcChanges = cChanges;
- return hrSuccess;
- }
- /**
- * This allows you to configure the exporter for a selective export of messages. Since the caller
- * specifies which items to export, it is not an incremental export and it has no state. It will call
- * the importer with data for all the specified messages, unless they are unavailable. This means that
- * the importer will only receive ImportMessageChange() or ImportMessageChangeAsStream() calls.
- *
- * @param[in] ulPropTag Property tag of identifiers in lpEntries, either PR_SOURCE_KEY or PR_ENTRYID
- * @param[in] lpEntries List of entries to export
- * @param[in] lpParents List of parents for entries in lpEntries. Must be the same size as lpEntries. Must be NULL if ulPropTag == PR_ENTRYID.
- * @param[in] ulFlags Unused for now
- * @param[in] lpCollector Importer to send the data to. Must implement either IExchangeImportContentsChanges or IECImportContentsChanges
- * @param[in] lpIncludeProps Unused for now
- * @param[in] lpExcludeProps Unused for now
- * @param[in] ulBufferSize Number of messages so synchronize during a single Synchronize() call. A value of 0 means 'default', which is 1
- * @return result
- */
- HRESULT ECExchangeExportChanges::ConfigSelective(ULONG ulPropTag, LPENTRYLIST lpEntries, LPENTRYLIST lpParents, ULONG ulFlags, LPUNKNOWN lpCollector, LPSPropTagArray lpIncludeProps, LPSPropTagArray lpExcludeProps, ULONG ulBufferSize)
- {
- HRESULT hr;
- ECSyncSettings *lpSyncSettings = ECSyncSettings::GetInstance();
- BOOL bCanStream = false;
- BOOL bSupportsPropTag = false;
-
- if (ulPropTag != PR_ENTRYID && ulPropTag != PR_SOURCE_KEY)
- return MAPI_E_INVALID_PARAMETER;
-
- if(ulPropTag == PR_ENTRYID) {
- m_lpStore->lpTransport->HrCheckCapabilityFlags(KOPANO_CAP_EXPORT_PROPTAG, &bSupportsPropTag);
- if (!bSupportsPropTag)
- return MAPI_E_NO_SUPPORT;
- }
-
- if (ulPropTag == PR_ENTRYID && lpParents != NULL)
- return MAPI_E_INVALID_PARAMETER;
- if (ulPropTag == PR_SOURCE_KEY && lpParents == NULL)
- return MAPI_E_INVALID_PARAMETER;
- if (lpParents != NULL && lpParents->cValues != lpEntries->cValues)
- return MAPI_E_INVALID_PARAMETER;
-
- if(m_bConfiged){
- ZLOG_DEBUG(m_lpLogger, "Config() called twice");
- return MAPI_E_UNCONFIGURED;
- }
-
- // Only available for message syncing
- if (m_ulSyncType != ICS_SYNC_CONTENTS)
- return MAPI_E_NO_SUPPORT;
- // Select an importer interface
- hr = lpCollector->QueryInterface(IID_IExchangeImportContentsChanges, (LPVOID*) &m_lpImportContents);
- if (hr == hrSuccess && lpSyncSettings->SyncStreamEnabled()) {
- m_lpStore->lpTransport->HrCheckCapabilityFlags(KOPANO_CAP_ENHANCED_ICS, &bCanStream);
- if (bCanStream == TRUE) {
- ZLOG_DEBUG(m_lpLogger, "Exporter supports enhanced ICS, checking importer...");
- hr = lpCollector->QueryInterface(IID_IECImportContentsChanges, (LPVOID*) &m_lpImportStreamedContents);
- if (hr == MAPI_E_INTERFACE_NOT_SUPPORTED) {
- assert(m_lpImportStreamedContents == NULL);
- hr = hrSuccess;
- ZLOG_DEBUG(m_lpLogger, "Importer doesn't support enhanced ICS");
- } else
- ZLOG_DEBUG(m_lpLogger, "Importer supports enhanced ICS");
- } else
- ZLOG_DEBUG(m_lpLogger, "Exporter doesn't support enhanced ICS");
- }
-
- m_ulEntryPropTag = ulPropTag;
- // Fill m_lpChanges with items from lpEntries
- hr = MAPIAllocateBuffer(sizeof(ICSCHANGE) * lpEntries->cValues, (void **)&m_lpChanges);
- if(hr != hrSuccess)
- return hr;
- for (unsigned int i = 0; i < lpEntries->cValues; ++i) {
- memset(&m_lpChanges[i], 0, sizeof(ICSCHANGE));
- hr = MAPIAllocateMore(lpEntries->lpbin[i].cb, m_lpChanges, (void **)&m_lpChanges[i].sSourceKey.lpb);
- if(hr != hrSuccess)
- return hr;
-
- memcpy(m_lpChanges[i].sSourceKey.lpb, lpEntries->lpbin[i].lpb, lpEntries->lpbin[i].cb);
- m_lpChanges[i].sSourceKey.cb = lpEntries->lpbin[i].cb;
-
- if(lpParents) {
- hr = MAPIAllocateMore(lpParents->lpbin[i].cb, m_lpChanges, (void **)&m_lpChanges[i].sParentSourceKey.lpb);
- if(hr != hrSuccess)
- return hr;
-
- memcpy(m_lpChanges[i].sParentSourceKey.lpb, lpParents->lpbin[i].lpb, lpParents->lpbin[i].cb);
- m_lpChanges[i].sParentSourceKey.cb = lpParents->lpbin[i].cb;
- }
-
- m_lpChanges[i].ulChangeType = ICS_MESSAGE_NEW;
- // Since all changes are 'change' modifications, duplicate all data in m_lpChanges in m_lstChange
- m_lstChange.push_back(m_lpChanges[i]);
- }
-
- m_bConfiged = true;
- return hrSuccess;
- }
- HRESULT ECExchangeExportChanges::SetMessageInterface(REFIID refiid) {
- m_iidMessage = refiid;
- return hrSuccess;
- }
- DEF_ULONGMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, AddRef, (void))
- DEF_ULONGMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, Release, (void))
- DEF_HRMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, QueryInterface, (REFIID, refiid), (void **, lppInterface))
- DEF_HRMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, GetLastError, (HRESULT, hError), (ULONG, ulFlags), (LPMAPIERROR *, lppMapiError))
- DEF_HRMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, Config, (LPSTREAM, lpStream), (ULONG, ulFlags), (LPUNKNOWN, lpCollector), (LPSRestriction, lpRestriction), (LPSPropTagArray, lpIncludeProps), (LPSPropTagArray, lpExcludeProps), (ULONG, ulBufferSize))
- DEF_HRMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, ConfigSelective, (ULONG, ulPropTag), (LPENTRYLIST, lpEntries), (LPENTRYLIST, lpParents), (ULONG, ulFlags), (LPUNKNOWN, lpCollector), (LPSPropTagArray, lpIncludeProps), (LPSPropTagArray, lpExcludeProps), (ULONG, ulBufferSize))
- DEF_HRMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, Synchronize, (ULONG *, pulSteps), (ULONG *, pulProgress))
- DEF_HRMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, UpdateState, (LPSTREAM, lpStream))
- DEF_HRMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, GetChangeCount, (ULONG *, lpcChanges))
- DEF_HRMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, SetMessageInterface, (REFIID, refiid))
- DEF_HRMETHOD1(TRACE_MAPI, ECExchangeExportChanges, ECExportChanges, SetLogger, (ECLogger *, lpLogger))
- HRESULT ECExchangeExportChanges::ExportMessageChanges() {
- assert(m_lpImportContents != NULL);
- if (m_lpImportStreamedContents != NULL)
- return ExportMessageChangesFast();
- else
- return ExportMessageChangesSlow();
- }
- HRESULT ECExchangeExportChanges::ExportMessageChangesSlow() {
- HRESULT hr = hrSuccess;
- memory_ptr<SPropValue> lpPropArray;
- memory_ptr<SPropTagArray> lpPropTagArray;
- ULONG ulObjType;
- ULONG ulCount;
- ULONG ulFlags;
- ULONG ulSteps = 0;
- ULONG cbEntryID = 0;
- memory_ptr<ENTRYID> lpEntryID;
- static constexpr const SizedSPropTagArray(5, sptMessageExcludes) =
- {5, {PR_MESSAGE_SIZE, PR_MESSAGE_RECIPIENTS,
- PR_MESSAGE_ATTACHMENTS, PR_ATTACH_SIZE, PR_PARENT_SOURCE_KEY}};
- static constexpr const SizedSPropTagArray(7, sptImportProps) =
- {7, {PR_SOURCE_KEY, PR_LAST_MODIFICATION_TIME, PR_CHANGE_KEY,
- PR_PARENT_SOURCE_KEY, PR_PREDECESSOR_CHANGE_LIST, PR_ENTRYID,
- PR_ASSOCIATED}};
- static constexpr const SizedSPropTagArray(1, sptAttach) = {1, {PR_ATTACH_NUM}};
- while(m_ulStep < m_lstChange.size() && (m_ulBufferSize == 0 || ulSteps < m_ulBufferSize)){
- ulFlags = 0;
- if ((m_lstChange.at(m_ulStep).ulChangeType & ICS_ACTION_MASK) == ICS_NEW)
- ulFlags |= SYNC_NEW_MESSAGE;
- object_ptr<IMessage> lpSourceMessage, lpDestMessage;
- object_ptr<IMAPITable> lpTable;
- rowset_ptr lpRows;
- if(!m_sourcekey.empty()) {
- // Normal exporter, get the import properties we need by opening the source message
- hr = m_lpStore->EntryIDFromSourceKey(
- m_lstChange.at(m_ulStep).sParentSourceKey.cb,
- m_lstChange.at(m_ulStep).sParentSourceKey.lpb,
- m_lstChange.at(m_ulStep).sSourceKey.cb,
- m_lstChange.at(m_ulStep).sSourceKey.lpb,
- &cbEntryID, &~lpEntryID);
- if(hr == MAPI_E_NOT_FOUND){
- hr = hrSuccess;
- goto next;
- }
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "change sourcekey: %s", bin2hex(m_lstChange.at(m_ulStep).sSourceKey.cb, m_lstChange.at(m_ulStep).sSourceKey.lpb).c_str());
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Error while getting entryid from sourcekey %s", bin2hex(m_lstChange.at(m_ulStep).sSourceKey.cb, m_lstChange.at(m_ulStep).sSourceKey.lpb).c_str());
- goto exit;
- }
- hr = m_lpStore->OpenEntry(cbEntryID, lpEntryID, &m_iidMessage, MAPI_MODIFY, &ulObjType, &~lpSourceMessage);
- if(hr == MAPI_E_NOT_FOUND){
- hr = hrSuccess;
- goto next;
- }
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to open message with entryid %s", bin2hex(cbEntryID, reinterpret_cast<const unsigned char *>(lpEntryID.get())).c_str());
- goto exit;
- }
- hr = lpSourceMessage->GetProps(sptImportProps, 0, &ulCount, &~lpPropArray);
- if(FAILED(hr)) {
- ZLOG_DEBUG(m_lpLogger, "Unable to get properties from source message");
- goto exit;
- }
- hr = m_lpImportContents->ImportMessageChange(ulCount, lpPropArray, ulFlags, &~lpDestMessage);
- } else {
- // Server-wide ICS exporter, only export source key and parent source key
- SPropValue sProps[2];
- sProps[0].ulPropTag = PR_SOURCE_KEY;
- sProps[0].Value.bin = m_lstChange.at(m_ulStep).sSourceKey; // cheap copy is ok since pointer is valid during ImportMessageChange call
- sProps[1].ulPropTag = PR_PARENT_SOURCE_KEY;
- sProps[1].Value.bin = m_lstChange.at(m_ulStep).sParentSourceKey;
- hr = m_lpImportContents->ImportMessageChange(2, sProps, ulFlags, &~lpDestMessage);
- }
- if (hr == SYNC_E_IGNORE) {
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "ignored change");
- // Mark this change as processed
- hr = hrSuccess;
- goto next;
- }else if(hr == SYNC_E_OBJECT_DELETED){
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "ignored change for deleted item");
- // Mark this change as processed
- hr = hrSuccess;
- goto next;
- }else if(hr == SYNC_E_INVALID_PARAMETER){
- //exchange doesn't like our input sometimes
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "ignored change parameter");
- hr = hrSuccess;
- goto next;
- // TODO handle SYNC_E_OBJECT_DELETED, SYNC_E_CONFLICT, SYNC_E_NO_PARENT, SYNC_E_INCEST, SYNC_E_UNSYNCHRONIZED
- }else if(hr != hrSuccess){
- //m_lpLogger->Log(EC_LOGLEVEL_INFO, "change error: %s", stringify(hr, true).c_str());
- ZLOG_DEBUG(m_lpLogger, "Error during message import");
- goto exit;
- }
- if (lpDestMessage == NULL)
- // Import succeeded, but we have no message to export to. Treat this the same as
- // SYNC_E_IGNORE. This is required for the BES ICS exporter
- goto next;
- if (lpSourceMessage == nullptr)
- /*
- * Well this is getting silly. If we do not have a
- * source message, where the heck should we copy from?
- */
- goto next;
- hr = lpSourceMessage->CopyTo(0, NULL, sptMessageExcludes, 0,
- NULL, &IID_IMessage, lpDestMessage, 0, NULL);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to copy to imported message");
- goto exit;
- }
- hr = lpSourceMessage->GetRecipientTable(0, &~lpTable);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to read source message's recipient table");
- goto exit;
- }
- hr = lpTable->QueryColumns(TBL_ALL_COLUMNS, &~lpPropTagArray);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to get column set from source message's recipient table");
- goto exit;
- }
- hr = lpTable->SetColumns(lpPropTagArray, 0);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to set column set for source message's recipient table");
- goto exit;
- }
- hr = lpTable->QueryRows(0xFFFF, 0, &~lpRows);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to read recipients from source message");
- goto exit;
- }
- //FIXME: named property in the recipienttable ?
- hr = lpDestMessage->ModifyRecipients(0, reinterpret_cast<ADRLIST *>(lpRows.get()));
- if(hr != hrSuccess)
- hr = hrSuccess;
- //goto exit;
- //delete every attachment
- hr = lpDestMessage->GetAttachmentTable(0, &~lpTable);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to get destination's attachment table");
- goto exit;
- }
- hr = lpTable->SetColumns(sptAttach, 0);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to set destination's attachment table's column set");
- goto exit;
- }
- hr = lpTable->QueryRows(0xFFFF, 0, &~lpRows);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to read destination's attachment list");
- goto exit;
- }
- for (ulCount = 0; ulCount < lpRows->cRows; ++ulCount) {
- hr = lpDestMessage->DeleteAttach(lpRows->aRow[ulCount].lpProps[0].Value.ul, 0, NULL, 0);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to delete destination's attachment number %d", lpRows->aRow[ulCount].lpProps[0].Value.ul);
- goto exit;
- }
- }
- //add every attachment
- hr = lpSourceMessage->GetAttachmentTable(0, &~lpTable);
- if(hr != hrSuccess)
- goto exit;
- hr = lpTable->SetColumns(sptAttach, 0);
- if(hr != hrSuccess)
- goto exit;
- hr = lpTable->QueryRows(0xFFFF, 0, &~lpRows);
- if(hr != hrSuccess)
- goto exit;
- for (ulCount = 0; ulCount < lpRows->cRows; ++ulCount) {
- object_ptr<IAttach> lpSourceAttach, lpDestAttach;
- hr = lpSourceMessage->OpenAttach(lpRows->aRow[ulCount].lpProps[0].Value.ul, &IID_IAttachment, 0, &~lpSourceAttach);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to open attachment %d in source message", lpRows->aRow[ulCount].lpProps[0].Value.ul);
- goto exit;
- }
- hr = lpDestMessage->CreateAttach(&IID_IAttachment, 0, &ulObjType, &~lpDestAttach);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to create attachment");
- goto exit;
- }
- hr = lpSourceAttach->CopyTo(0, NULL, sptAttach, 0,
- NULL, &IID_IAttachment, lpDestAttach, 0, NULL);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to copy attachment");
- goto exit;
- }
- hr = lpDestAttach->SaveChanges(0);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "SaveChanges() failed for destination attachment");
- goto exit;
- }
- }
- lpTable.release();
- hr = lpSourceMessage->GetPropList(0, &~lpPropTagArray);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to get property list of source message");
- goto exit;
- }
- hr = Util::HrDeleteResidualProps(lpDestMessage, lpSourceMessage, lpPropTagArray);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to remove old properties from destination message");
- goto exit;
- }
- hr = lpDestMessage->SaveChanges(0);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "SaveChanges failed for destination message");
- goto exit;
- }
- next:
- // Mark this change as processed, even if we skipped it due to SYNC_E_IGNORE or because the item was deleted on the source server
- m_setProcessedChanges.insert(std::pair<unsigned int, std::string>(m_lstChange.at(m_ulStep).ulChangeId, std::string((char *)m_lstChange.at(m_ulStep).sSourceKey.lpb, m_lstChange.at(m_ulStep).sSourceKey.cb)));
- ++m_ulStep;
- ++ulSteps;
- }
- if(m_ulStep < m_lstChange.size())
- hr = SYNC_W_PROGRESS;
- exit:
- if(hr != hrSuccess && hr != SYNC_W_PROGRESS)
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "change error: %s", stringify(hr, true).c_str());
- return hr;
- }
- HRESULT ECExchangeExportChanges::ExportMessageChangesFast()
- {
- HRESULT hr = hrSuccess;
- WSSerializedMessagePtr ptrSerializedMessage;
- ULONG cbProps = 0;
- SPropValuePtr ptrProps;
- const SPropValue *lpPropVal = NULL;
- ULONG ulFlags = 0;
- StreamPtr ptrDestStream;
- static constexpr const SizedSPropTagArray(11, sptImportProps) = { 11, {
- PR_SOURCE_KEY,
- PR_LAST_MODIFICATION_TIME,
- PR_CHANGE_KEY,
- PR_PARENT_SOURCE_KEY,
- PR_PREDECESSOR_CHANGE_LIST,
- PR_ENTRYID,
- PR_ASSOCIATED,
- PR_MESSAGE_FLAGS, /* needed for backward compat since PR_ASSOCIATED is not supported on earlier systems */
- PR_STORE_RECORD_KEY,
- PR_EC_HIERARCHYID,
- PR_EC_PARENT_HIERARCHYID
- } };
- static constexpr const SizedSPropTagArray(7, sptImportPropsServerWide) = { 7, {
- PR_SOURCE_KEY,
- PR_PARENT_SOURCE_KEY,
- PR_STORE_RECORD_KEY,
- PR_STORE_ENTRYID,
- PR_EC_HIERARCHYID,
- PR_EC_PARENT_HIERARCHYID,
- PR_ENTRYID
- } };
- const auto lpImportProps = m_sourcekey.empty() ? sptImportPropsServerWide : sptImportProps;
- // No more changes (add/modify).
- ZLOG_DEBUG(m_lpLogger, "ExportFast: At step %u, changeset contains %lu items)",
- m_ulStep, static_cast<unsigned long>(m_lstChange.size()));
- if (m_ulStep >= m_lstChange.size())
- goto exit;
- if (!m_ptrStreamExporter || m_ptrStreamExporter->IsDone()) {
- ZLOG_DEBUG(m_lpLogger, "ExportFast: Requesting new batch, batch size = %u", m_ulBatchSize);
- hr = m_lpStore->ExportMessageChangesAsStream(m_ulFlags & (SYNC_BEST_BODY | SYNC_LIMITED_IMESSAGE), m_ulEntryPropTag, m_lstChange, m_ulStep, m_ulBatchSize, lpImportProps, &~m_ptrStreamExporter);
- if (hr == MAPI_E_UNABLE_TO_COMPLETE) {
- // There was nothing to export (see ExportMessageChangesAsStream documentation)
- assert(m_ulStep >= m_lstChange.size()); // @todo: Is this a correct assumption?
- hr = hrSuccess;
- goto exit;
- } else if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "ExportFast: %s", "Stream export failed");
- goto exit;
- }
- ZLOG_DEBUG(m_lpLogger, "ExportFast: %s", "Got new batch");
- }
- ZLOG_DEBUG(m_lpLogger, "ExportFast: Requesting serialized message, step = %u", m_ulStep);
- hr = m_ptrStreamExporter->GetSerializedMessage(m_ulStep, &~ptrSerializedMessage);
- if (hr == SYNC_E_OBJECT_DELETED) {
- ZLOG_DEBUG(m_lpLogger, "ExportFast: %s", "Source message is deleted");
- hr = hrSuccess;
- goto skip;
- } else if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "ExportFast: Unable to get serialized message, hr = 0x%08x", hr);
- goto exit;
- }
- hr = ptrSerializedMessage->GetProps(&cbProps, &~ptrProps);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "ExportFast: %s", "Unable to get required properties from serialized message");
- goto exit;
- }
- lpPropVal = PCpropFindProp(ptrProps, cbProps, PR_MESSAGE_FLAGS);
- if (lpPropVal != NULL && (lpPropVal->Value.ul & MSGFLAG_ASSOCIATED))
- ulFlags |= SYNC_ASSOCIATED;
- if ((m_lstChange.at(m_ulStep).ulChangeType & ICS_ACTION_MASK) == ICS_NEW)
- ulFlags |= SYNC_NEW_MESSAGE;
- ZLOG_DEBUG(m_lpLogger, "ExportFast: %s", "Importing message change");
- hr = m_lpImportStreamedContents->ImportMessageChangeAsAStream(cbProps, ptrProps, ulFlags, &~ptrDestStream);
- if (hr == hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "ExportFast: %s", "Copying data");
- hr = ptrSerializedMessage->CopyData(ptrDestStream);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "ExportFast: Failed to copy data, hr = 0x%08x", hr);
- LogMessageProps(EC_LOGLEVEL_DEBUG, cbProps, ptrProps);
- goto exit;
- }
- ZLOG_DEBUG(m_lpLogger, "ExportFast: %s", "Copied data");
- } else if (hr == SYNC_E_IGNORE || hr == SYNC_E_OBJECT_DELETED) {
- ZLOG_DEBUG(m_lpLogger, "ExportFast: Change ignored, code = 0x%08x", hr);
- hr = ptrSerializedMessage->DiscardData();
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "ExportFast: Failed to discard data, hr = 0x%08x", hr);
- LogMessageProps(EC_LOGLEVEL_DEBUG, cbProps, ptrProps);
- goto exit;
- }
- } else {
- ZLOG_DEBUG(m_lpLogger, "ExportFast: Import failed, hr = 0x%08x", hr);
- LogMessageProps(EC_LOGLEVEL_DEBUG, cbProps, ptrProps);
- goto exit;
- }
- skip:
- m_setProcessedChanges.insert(std::pair<unsigned int, std::string>(m_lstChange.at(m_ulStep).ulChangeId, std::string((char *)m_lstChange.at(m_ulStep).sSourceKey.lpb, m_lstChange.at(m_ulStep).sSourceKey.cb)));
- if (++m_ulStep < m_lstChange.size())
- hr = SYNC_W_PROGRESS;
- exit:
- if (FAILED(hr))
- m_ptrStreamExporter.reset();
- ZLOG_DEBUG(m_lpLogger, "ExportFast: Done, hr = 0x%08x", hr);
- return hr;
- }
- HRESULT ECExchangeExportChanges::ExportMessageFlags(){
- HRESULT hr = hrSuccess;
- memory_ptr<READSTATE> lpReadState;
- ULONG ulCount;
- if(m_lstFlag.empty())
- goto exit;
- hr = MAPIAllocateBuffer(sizeof(READSTATE) * m_lstFlag.size(), &~lpReadState);
- if (hr != hrSuccess)
- goto exit;
- ulCount = 0;
- for (const auto &change : m_lstFlag) {
- hr = MAPIAllocateMore(change.sSourceKey.cb, lpReadState, reinterpret_cast<LPVOID *>(&lpReadState[ulCount].pbSourceKey));
- if (hr != hrSuccess)
- goto exit;
- lpReadState[ulCount].cbSourceKey = change.sSourceKey.cb;
- memcpy(lpReadState[ulCount].pbSourceKey, change.sSourceKey.lpb, change.sSourceKey.cb);
- lpReadState[ulCount].ulFlags = change.ulFlags;
- ++ulCount;
- }
- if(ulCount > 0){
- hr = m_lpImportContents->ImportPerUserReadStateChange(ulCount, lpReadState);
- if (hr == SYNC_E_IGNORE)
- hr = hrSuccess;
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Read state change failed");
- goto exit;
- }
- // Mark the flag changes as processed
- for (const auto &change : m_lstFlag)
- m_setProcessedChanges.insert(std::pair<unsigned int, std::string>(change.ulChangeId, std::string(reinterpret_cast<const char *>(change.sSourceKey.lpb), change.sSourceKey.cb)));
- }
- exit:
- if (hr != hrSuccess)
- m_lpLogger->Log(EC_LOGLEVEL_FATAL, "Failed to sync message flags, 0x%08X", hr);
- return hr;
- }
- HRESULT ECExchangeExportChanges::ExportMessageDeletes(){
- HRESULT hr = hrSuccess;
- memory_ptr<ENTRYLIST> lpEntryList;
- if(!m_lstSoftDelete.empty()){
- hr = ChangesToEntrylist(&m_lstSoftDelete, &~lpEntryList);
- if(hr != hrSuccess)
- return hr;
- hr = m_lpImportContents->ImportMessageDeletion(SYNC_SOFT_DELETE, lpEntryList);
- if (hr == SYNC_E_IGNORE)
- hr = hrSuccess;
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Message deletion import failed");
- return hr;
- }
- hr = AddProcessedChanges(m_lstSoftDelete);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to add processed soft deletion changes");
- return hr;
- }
- }
- if(!m_lstHardDelete.empty()){
- hr = ChangesToEntrylist(&m_lstHardDelete, &~lpEntryList);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to create entry list");
- return hr;
- }
- hr = m_lpImportContents->ImportMessageDeletion(0, lpEntryList);
- if (hr == SYNC_E_IGNORE)
- hr = hrSuccess;
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Message hard deletion failed");
- return hr;
- }
- hr = AddProcessedChanges(m_lstHardDelete);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to add processed hard deletion changes");
- return hr;
- }
- }
- return hrSuccess;
- }
- HRESULT ECExchangeExportChanges::ExportFolderChanges(){
- HRESULT hr = hrSuccess;
- LPSPropValue lpPropVal = NULL;
- ULONG ulObjType = 0;
- ULONG ulCount = 0;
- ULONG ulSteps = 0;
- ULONG cbEntryID = 0;
- while(m_ulStep < m_lstChange.size() && (m_ulBufferSize == 0 || ulSteps < m_ulBufferSize)){
- object_ptr<IMAPIFolder> lpFolder;
- memory_ptr<SPropValue> lpPropArray;
- memory_ptr<ENTRYID> lpEntryID;
- if(!m_sourcekey.empty()) {
- // Normal export, need all properties
- hr = m_lpStore->EntryIDFromSourceKey(
- m_lstChange.at(m_ulStep).sSourceKey.cb,
- m_lstChange.at(m_ulStep).sSourceKey.lpb,
- 0, NULL,
- &cbEntryID, &~lpEntryID);
- if(hr != hrSuccess){
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "change sourcekey not found");
- hr = hrSuccess;
- goto next;
- }
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "change sourcekey: %s", bin2hex(m_lstChange.at(m_ulStep).sSourceKey.cb, m_lstChange.at(m_ulStep).sSourceKey.lpb).c_str());
- hr = m_lpStore->OpenEntry(cbEntryID, lpEntryID, &IID_IMAPIFolder, MAPI_MODIFY, &ulObjType, &~lpFolder);
- if(hr != hrSuccess){
- hr = hrSuccess;
- goto next;
- }
- hr = HrGetAllProps(lpFolder, (m_ulFlags & SYNC_UNICODE ? MAPI_UNICODE : 0), &ulCount, &~lpPropArray);
- if(FAILED(hr)) {
- ZLOG_DEBUG(m_lpLogger, "Unable to get source folder properties");
- return hr;
- }
- //for folders directly under m_lpFolder PR_PARENT_SOURCE_KEY must be NULL
- //this protects against recursive problems during syncing when the PR_PARENT_SOURCE_KEY
- //equals the sourcekey of the folder which we are already syncing.
- //If the exporter sends an empty PR_PARENT_SOURCE_KEY to the importer the importer can
- //assume the parent is the folder which it is syncing.
- lpPropVal = PpropFindProp(lpPropArray, ulCount, PR_PARENT_SOURCE_KEY);
- if (lpPropVal != nullptr && m_sourcekey.size() == lpPropVal->Value.bin.cb &&
- memcmp(lpPropVal->Value.bin.lpb, m_sourcekey.c_str(), m_sourcekey.size()) == 0)
- lpPropVal->Value.bin.cb = 0;
- hr = m_lpImportHierarchy->ImportFolderChange(ulCount, lpPropArray);
- } else {
- // Server-wide ICS
- SPropValue sProps[1];
- sProps[0].ulPropTag = PR_SOURCE_KEY;
- sProps[0].Value.bin = m_lstChange.at(m_ulStep).sSourceKey;
- hr = m_lpImportHierarchy->ImportFolderChange(1, sProps);
- }
- if (hr == SYNC_E_IGNORE){
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "change ignored");
- hr = hrSuccess;
- goto next;
- }else if (hr == MAPI_E_INVALID_PARAMETER){
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "change invalid parameter");
- hr = hrSuccess;
- goto next;
- }else if(hr == MAPI_E_NOT_FOUND){
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "change not found");
- hr = hrSuccess;
- goto next;
- }else if(FAILED(hr)) {
- m_lpLogger->Log(EC_LOGLEVEL_INFO, "change error: %s", stringify(hr, true).c_str());
- return hr;
- }else if(hr != hrSuccess){
- m_lpLogger->Log(EC_LOGLEVEL_WARNING, "change warning: %s", stringify(hr, true).c_str());
- }
- next:
- // Mark this change as processed
- m_setProcessedChanges.insert(std::pair<unsigned int, std::string>(m_lstChange.at(m_ulStep).ulChangeId, std::string((char *)m_lstChange.at(m_ulStep).sSourceKey.lpb, m_lstChange.at(m_ulStep).sSourceKey.cb)));
- ++ulSteps;
- ++m_ulStep;
- }
- if(m_ulStep < m_lstChange.size())
- hr = SYNC_W_PROGRESS;
- return hr;
- }
- HRESULT ECExchangeExportChanges::ExportFolderDeletes(){
- HRESULT hr = hrSuccess;
- memory_ptr<ENTRYLIST> lpEntryList;
- if(!m_lstSoftDelete.empty()){
- hr = ChangesToEntrylist(&m_lstSoftDelete, &~lpEntryList);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to create folder deletion entry list");
- return hr;
- }
- hr = m_lpImportHierarchy->ImportFolderDeletion(SYNC_SOFT_DELETE, lpEntryList);
- if (hr == SYNC_E_IGNORE)
- hr = hrSuccess;
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to import folder deletions");
- return hr;
- }
- hr = AddProcessedChanges(m_lstSoftDelete);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to add processed folder soft deletions");
- return hr;
- }
- }
- if(!m_lstHardDelete.empty()){
- hr = ChangesToEntrylist(&m_lstHardDelete, &~lpEntryList);
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to create folder hard delete entry list");
- return hr;
- }
- hr = m_lpImportHierarchy->ImportFolderDeletion(0, lpEntryList);
- if (hr == SYNC_E_IGNORE)
- hr = hrSuccess;
- if(hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Hard delete folder import failed");
- return hr;
- }
- hr = AddProcessedChanges(m_lstHardDelete);
- if (hr != hrSuccess) {
- ZLOG_DEBUG(m_lpLogger, "Unable to add processed folder hard deletions");
- return hr;
- }
- }
- return hrSuccess;
- }
- //write in the stream 4 bytes syncid, 4 bytes changeid, 4 bytes changecount, {4 bytes changeid, 4 bytes sourcekeysize, sourcekey}
- HRESULT ECExchangeExportChanges::UpdateStream(LPSTREAM lpStream){
- HRESULT hr = hrSuccess;
- LARGE_INTEGER liPos = {{0, 0}};
- ULARGE_INTEGER liZero = {{0, 0}};
- ULONG ulSize;
- ULONG ulChangeCount = 0;
- ULONG ulChangeId = 0;
- ULONG ulSourceKeySize = 0;
-
- if(lpStream == NULL)
- goto exit;
- hr = lpStream->SetSize(liZero);
- if(hr != hrSuccess)
- goto exit;
- hr = lpStream->Seek(liPos, STREAM_SEEK_SET, NULL);
- if(hr != hrSuccess)
- goto exit;
- hr = lpStream->Write(&m_ulSyncId, 4, &ulSize);
- if(hr != hrSuccess)
- goto exit;
- if (m_ulSyncId == 0)
- m_ulChangeId = 0;
- hr = lpStream->Write(&m_ulChangeId, 4, &ulSize);
- if(hr != hrSuccess)
- goto exit;
- if(!m_setProcessedChanges.empty()) {
- ulChangeCount = m_setProcessedChanges.size();
- hr = lpStream->Write(&ulChangeCount, 4, &ulSize);
- if(hr != hrSuccess)
- goto exit;
- for (const auto &pc : m_setProcessedChanges) {
- ulChangeId = pc.first;
- hr = lpStream->Write(&ulChangeId, 4, &ulSize);
- if(hr != hrSuccess)
- goto exit;
- ulSourceKeySize = pc.second.size();
- hr = lpStream->Write(&ulSourceKeySize, 4, &ulSize);
- if(hr != hrSuccess)
- goto exit;
- hr = lpStream->Write(pc.second.c_str(), pc.second.size(), &ulSize);
- if(hr != hrSuccess)
- goto exit;
- }
- }
- // Seek back to the beginning after we've finished
- lpStream->Seek(liPos, STREAM_SEEK_SET, NULL);
- exit:
- if(hr != hrSuccess)
- ZLOG_DEBUG(m_lpLogger, "Stream operation failed");
- return hr;
- }
- //convert (delete) changes to entrylist for message and folder deletion.
- HRESULT ECExchangeExportChanges::ChangesToEntrylist(std::list<ICSCHANGE> * lpLstChanges, LPENTRYLIST * lppEntryList){
- HRESULT hr = hrSuccess;
- memory_ptr<ENTRYLIST> lpEntryList;
- ULONG ulCount = 0;
- hr = MAPIAllocateBuffer(sizeof(ENTRYLIST), &~lpEntryList);
- if (hr != hrSuccess)
- return hr;
- lpEntryList->cValues = lpLstChanges->size();
- if(lpEntryList->cValues > 0){
- if ((hr = MAPIAllocateMore(sizeof(SBinary) * lpEntryList->cValues, lpEntryList, (LPVOID *)&lpEntryList->lpbin)) != hrSuccess)
- return hr;
- ulCount = 0;
- for (const auto &change : *lpLstChanges) {
- lpEntryList->lpbin[ulCount].cb = change.sSourceKey.cb;
- hr = MAPIAllocateMore(change.sSourceKey.cb, lpEntryList,
- reinterpret_cast<void **>(&lpEntryList->lpbin[ulCount].lpb));
- if (hr != hrSuccess)
- return hr;
- memcpy(lpEntryList->lpbin[ulCount].lpb, change.sSourceKey.lpb, change.sSourceKey.cb);
- ++ulCount;
- }
- }else{
- lpEntryList->lpbin = NULL;
- }
- lpEntryList->cValues = ulCount;
- *lppEntryList = lpEntryList.release();
- return hrSuccess;
- }
- /**
- * Add processed changes to the precessed changes list
- *
- * @param[in] lstChanges List with changes
- *
- */
- HRESULT ECExchangeExportChanges::AddProcessedChanges(ChangeList &lstChanges)
- {
- for (const auto &i : lstChanges)
- m_setProcessedChanges.insert(std::pair<unsigned int, std::string>(i.ulChangeId,
- std::string(reinterpret_cast<const char *>(i.sSourceKey.lpb), i.sSourceKey.cb)));
- return hrSuccess;
- }
- void ECExchangeExportChanges::LogMessageProps(int loglevel, ULONG cValues, LPSPropValue lpPropArray)
- {
- if (!m_lpLogger->Log(loglevel))
- return;
- auto lpPropEntryID = PCpropFindProp(lpPropArray, cValues, PR_ENTRYID);
- auto lpPropSK = PCpropFindProp(lpPropArray, cValues, PR_SOURCE_KEY);
- auto lpPropFlags = PCpropFindProp(lpPropArray, cValues, PR_MESSAGE_FLAGS);
- auto lpPropHierarchyId = PCpropFindProp(lpPropArray, cValues, PR_EC_HIERARCHYID);
- auto lpPropParentId = PCpropFindProp(lpPropArray, cValues, PR_EC_PARENT_HIERARCHYID);
- m_lpLogger->Log(loglevel, "ExportFast: Message info: id=%u, parentid=%u, msgflags=%x, entryid=%s, sourcekey=%s",
- lpPropHierarchyId != NULL ? lpPropHierarchyId->Value.ul : 0,
- lpPropParentId != NULL ? lpPropParentId->Value.ul : 0,
- lpPropFlags != NULL ? lpPropFlags->Value.ul : 0,
- lpPropEntryID != NULL ? bin2hex(lpPropEntryID->Value.bin.cb, lpPropEntryID->Value.bin.lpb).c_str() : "<Unknown>",
- lpPropSK != NULL ? bin2hex(lpPropSK->Value.bin.cb, lpPropSK->Value.bin.lpb).c_str() : "<Unknown>");
- }
|