|
|
#include "stdafx.h"
#include "exchange.hpp"
#include "common.hpp"
#include "err.hpp"
#include "ErrDct.hpp"
#include "UString.hpp"
#include "sidcache.hpp"
#include "sd.hpp"
#include "SecObj.hpp"
#include "MAPIProf.hpp"
#include "exldap.h"
#include "Mcs.h"
extern TErrorDct err;
#define NOT_PT_ERROR(x) ( PROP_TYPE(x.ulPropTag) != PT_ERROR )
#define LDAP_PortNumber_DN_Part L"/cn=Protocols/cn=LDAP"
#define ATT_OBJ_CLASS L"Obj-Class"
#define ATT_DIST_NAME L"Obj-Dist-Name"
#define ATT_LDAP_PORT L"Port-Number"
#define LDAP_USE_SITE_VALUES L"Use-Site-Values"
// Stuff related to dynamic loading of DAPI.DLL
HINSTANCE hDapi = NULL; LPDAPISTART pDAPIStart = NULL; LPDAPIEND pDAPIEnd = NULL; LPDAPIREAD pDAPIRead = NULL; LPDAPIWRITE pDAPIWrite = NULL; LPDAPIFREEMEMORY pDAPIFreeMemory = NULL; LPBATCHEXPORT pBatchExport = NULL;
BOOL LoadDAPI() { BOOL success = TRUE;
if ( ! hDapi ) { success = FALSE; hDapi = LoadLibrary(_T("DAPI.DLL")); if ( hDapi ) { do { // once
pDAPIStart = (LPDAPISTART)GetProcAddress(hDapi,"DAPIStartW@8"); if ( ! pDAPIStart ) { err.MsgWrite(ErrE,DCT_MSG_DAPI_ENTRY_POINT_NOT_FOUND_S,L"DAPIStart"); break; } pDAPIEnd = (LPDAPIEND)GetProcAddress(hDapi,"DAPIEnd@4"); if ( ! pDAPIEnd ) { err.MsgWrite(ErrE,DCT_MSG_DAPI_ENTRY_POINT_NOT_FOUND_S,L"DAPIEnd"); break; } pDAPIRead = (LPDAPIREAD)GetProcAddress(hDapi,"DAPIReadW@24"); if ( ! pDAPIRead ) { err.MsgWrite(ErrE,DCT_MSG_DAPI_ENTRY_POINT_NOT_FOUND_S,L"DAPIRead"); break; } pDAPIWrite = (LPDAPIWRITE)GetProcAddress(hDapi,"DAPIWriteW@28"); if ( ! pDAPIWrite ) { err.MsgWrite(ErrE,DCT_MSG_DAPI_ENTRY_POINT_NOT_FOUND_S,L"DAPIWrite"); break; }
pDAPIFreeMemory = (LPDAPIFREEMEMORY)GetProcAddress(hDapi,"DAPIFreeMemory@4"); if ( ! pDAPIFreeMemory ) { err.MsgWrite(ErrE,DCT_MSG_DAPI_ENTRY_POINT_NOT_FOUND_S,L"DAPIFreeMemory"); break; }
pBatchExport = (LPBATCHEXPORT)GetProcAddress(hDapi,"BatchExportA@4"); if ( ! pBatchExport ) { err.MsgWrite(ErrE,DCT_MSG_DAPI_ENTRY_POINT_NOT_FOUND_S,L"BatchExport"); break; } success = TRUE; } while (false); } else { err.MsgWrite(ErrE,DCT_MSG_DAPI_LOAD_LIBRARY_FAILED); } } if ( ! success ) { ReleaseDAPI(); } return success; }
void ReleaseDAPI() { if ( hDapi ) { FreeLibrary(hDapi); hDapi = NULL; pDAPIStart = NULL; pDAPIEnd = NULL; pDAPIRead = NULL; pDAPIWrite = NULL; pDAPIFreeMemory = NULL; pBatchExport = NULL; } }
TGlobalDirectory::TGlobalDirectory() { m_stat = NULL; m_bUseDefaultMapiProfile = FALSE; // Whether to use MAPI profile listed in Registry.
m_bPromptForMapiProfile = TRUE; // Whether to prompt for MAPI profile.
m_pszMapiProfile = NULL; // MAPI profile to use.
// MAPI
m_bMapiInitialized = FALSE; // TRUE if initialization was successful.
m_pMapiSession = NULL; // MAPI session handle.
m_pAdrBook = NULL; // The master AB.
m_pGlobalList = NULL; m_pGlobalTable = NULL; m_pGlobalPropertyTags = NULL; m_pGlobalRows = NULL; m_pRootRows = NULL; m_pContainer = NULL; m_pContainerTable = NULL; }
TGlobalDirectory::~TGlobalDirectory() { delete [] m_pszMapiProfile;
CloseGlobalList(); EndMapiSession(); }
LPTSTR TGlobalDirectory::GetMapiProfile() const { return m_pszMapiProfile; }
BOOL TGlobalDirectory::DoUseDefaultMapiProfile() const { return m_bUseDefaultMapiProfile; }
BOOL TGlobalDirectory::DoPromptForMapiProfile() const { return m_bPromptForMapiProfile; }
///////////////////////////////////////////////////////////////////////////////
// Log a MAPI warning or error.
void TGlobalDirectory::LogMapiError( int iSeverity, // in - Severity (i.e. ErrW, ErrE, etc)
LPCTSTR pszEntryPoint, // in - API that generated the error
HRESULT hr // in - error code
) { if (hr != 0) err.MsgWrite(iSeverity,DCT_MSG_GENERIC_HRESULT_SD, pszEntryPoint, hr); else err.MsgWrite(iSeverity,DCT_MSG_GENERIC_S, pszEntryPoint); }
void TGlobalDirectory::LogDapiError( int iSeverity, // in - severity of error (i.e. ErrW, ErrE, etc.)
LPCTSTR pszUserMessage, // in - message describing the action that failed
DAPI_EVENT * pResult // in - DAPIEvent error structure returned from Exchange
) { WCHAR strMsg[1000]; DWORD dimMsg = DIM(strMsg); DWORD lenMsg; DAPI_EVENT * pEvt; WCHAR msg[2000]; if ( !pResult ) return; safecopy(msg,pszUserMessage);
for ( pEvt = pResult ; pEvt ; pEvt = pEvt->pNextEvent ) { strMsg[0] = 0; lenMsg = FormatMessageW( FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_ARGUMENT_ARRAY, pEvt->hinstDAPI, pEvt->dwDAPIError, 0, strMsg, dimMsg, (va_list*)pEvt->rgpszSubst ); UStrCpy(msg+UStrLen(msg),strMsg,DIM(msg)-UStrLen(msg)); } err.MsgWrite(iSeverity,DCT_MSG_GENERIC_S,&*msg); }
///////////////////////////////////////////////////////////////////////////////
// Start a MAPI session.
// Tries to use the profile specified on the command line.
BOOL TGlobalDirectory::StartMapiSession() { //ASSERT(m_bMapiInitialized == FALSE);
MCSASSERT(m_pMapiSession == NULL); HRESULT hr; if ( !m_bMapiInitialized ) { hr = (*pMAPIInitialize)(NULL);
if (FAILED(hr)) { LogMapiError(ErrE, TEXT("MAPIInitialize"), hr); return FALSE; }
m_bMapiInitialized = TRUE; } FLAGS fLogonOptions = MAPI_NEW_SESSION | MAPI_NO_MAIL; LPTSTR pszMapiProfile;
if (DoUseDefaultMapiProfile()) { fLogonOptions |= MAPI_USE_DEFAULT; pszMapiProfile = NULL; } else if (DoPromptForMapiProfile()) { fLogonOptions |= MAPI_LOGON_UI | MAPI_EXPLICIT_PROFILE; pszMapiProfile = NULL; } else { fLogonOptions |= MAPI_EXPLICIT_PROFILE ; pszMapiProfile = GetMapiProfile(); }
hr = (*pMAPILogonEx)(0, pszMapiProfile, NULL, fLogonOptions | MAPI_UNICODE, &m_pMapiSession); if (FAILED(hr)) { switch (hr) { case MAPI_E_USER_CANCEL: break; default: err.SysMsgWrite(ErrE,hr,DCT_MSG_NO_MAPI_SESSION_D,hr); break; } return FALSE; }
m_bLoggedFailedClose = FALSE; return TRUE; } /* TGlobalDirectory::StartMapiSession() */
///////////////////////////////////////////////////////////////////////////////
// Terminate a MAPI session.
// No-op if there's not one open.
void TGlobalDirectory::EndMapiSession() { if (m_pMapiSession) { HRESULT hr = m_pMapiSession->Logoff(0, 0, 0);
if (SUCCEEDED(hr)) m_pMapiSession = NULL; else if (!m_bLoggedFailedClose) { err.SysMsgWrite(ErrW,hr,DCT_MSG_MAPI_LOGOFF_FAILED_D,hr); }
// NOTE: If this fails once, it may fail twice.
// The second failure will come on the dtor.
// This is on purpose. It's a retry. But we report it only once.
}
if (m_bMapiInitialized) { (*pMAPIUninitialize)(); m_bMapiInitialized = FALSE; } } /* TGlobalDirectory::EndMapiSession() */
///////////////////////////////////////////////////////////////////////////////
// Open the Address Book and get an interface to the Global List.
// The Global List contains the Distribution Lists.
BOOL TGlobalDirectory::OpenGlobalList() { // Open the Address Book.
if ( ! m_pMapiSession ) return FALSE;
HRESULT hr = m_pMapiSession->OpenAddressBook(0, NULL, AB_NO_DIALOG , &m_pAdrBook);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_NO_ADDRBOOK_D,hr); return FALSE; }
// Get the GAL entry ID.
ULONG cbEntryId; LPENTRYID pEntryId;
hr = HrFindExchangeGlobalAddressList(m_pAdrBook, &cbEntryId, &pEntryId);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_FAILED_TO_OPEN_GAL_D,hr); return FALSE; }
// Load the GAL from the AB.
ULONG ulObjType; hr = m_pAdrBook->OpenEntry(cbEntryId, pEntryId, NULL, 0, &ulObjType, (LPUNKNOWN*)&m_pGlobalList);
(*pMAPIFreeBuffer)(pEntryId);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_RETRIEVE_GAL_FAILED_D,hr); return FALSE; }
// Get a list of the contents of the Global List.
hr = m_pGlobalList->GetContentsTable(0, &m_pGlobalTable); if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_RETRIEVE_GAL_FAILED_D,hr); return FALSE; }
return TRUE; } /* TEaSyncCommand::OpenGlobalList() */
///////////////////////////////////////////////////////////////////////////////
// Release the interfaces to the Global List and the Address Book.
void TGlobalDirectory::CloseGlobalList() { if (m_pGlobalTable != NULL) { m_pGlobalTable->Release(); m_pGlobalTable = NULL; }
if (m_pGlobalList != NULL) { m_pGlobalList->Release(); m_pGlobalList = NULL; }
if (m_pAdrBook != NULL) { m_pAdrBook->Release(); m_pAdrBook = NULL; } } /* TGlobalDirectory::CloseGlobalList() */
BOOL TGlobalDirectory::UpdateEntry( LPMAPIPROP pUserEntry, // in - interface to mail recipient object
ULONG ulType, // in - type of object
SecurityTranslatorArgs * args // in - translation settings
) { // Prepare to get the columns that interest us.
// For DCT, we probably only care about ASSOC_NT_ACCOUNT, and possibly PR_EMS_AB_NT_SECURITY_DESCRIPTOR
HRESULT hr; BOOL anychange = FALSE; BOOL verbose = args->LogVerbose();
MCSASSERT(pUserEntry); if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); return FALSE; } SizedSPropTagArray(5, oPropertiesToGet) = { 5, { PR_DISPLAY_NAME, PR_EMS_AB_ASSOC_NT_ACCOUNT, // SID
PR_ENTRYID, PR_EMS_AB_NT_SECURITY_DESCRIPTOR, // SD
PR_DISPLAY_TYPE } }; ULONG ulPropsReturned = 0; LPSPropValue pUserProperties = NULL;
hr = pUserEntry->GetProps((SPropTagArray *)&oPropertiesToGet, 0, &ulPropsReturned, &pUserProperties); if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_GET_SECURITY_FOR_RECIP_FAILED_D,hr); pUserEntry->Release(); pUserEntry = NULL; return FALSE; }
if (ulPropsReturned != oPropertiesToGet.cValues) { err.MsgWrite(ErrE,DCT_MSG_GET_SECURITY_FOR_RECIP_FAILED_D,hr); pUserEntry->Release(); pUserEntry = NULL; return FALSE; }
// 1. Translate the PR_EMS_AB_ASSOC_NT_ACCOUNT property
WCHAR name[MAX_PATH]; safecopy(name,pUserProperties[0].Value.lpszW); if ( m_stat ) { m_stat->DisplayPath(name); m_stat->IncrementExamined(mailbox); } PISID pSid = (PISID)pUserProperties[1].Value.bin.lpb; if (pSid != NULL) { LPSPropValue pNewPropValues = NULL; LPSPropProblemArray pProblems = NULL;
// check if the sid is one we need to change
//TRACE (_T("DisplayName = %s "),pUserProperties[0].Value.lpszW);
PSID newSid = 0; TAcctNode * node;
if ( IsValidSid(pSid) ) { node = args->Cache()->Lookup(pSid); if ( m_stat ) { m_stat->IncrementOwnerExamined(); if ( verbose ) err.MsgWrite(0,DCT_MSG_EXAMINED_S,pUserProperties[0].Value.lpszW); } if ( node == (TAcctNode*)-1 && m_stat ) m_stat->IncrementOwnerNoTarget();
if ( node && (node != (TAcctNode *)-1) && node->IsValidOnTgt() ) newSid = args->Cache()->GetTgtSid(node); else newSid = NULL; } else { newSid = NULL; } if ( newSid ) { //TRACE (_T("needs to be translated\n"));
// update the entry, or maybe put it into a list of entries to be updated
// Allocate a buffer to set the property.
MCSASSERT ( IsValidSid(newSid) ); PSID pMapiSid; DWORD dwSidLength = GetLengthSid(newSid); hr = ResultFromScode((*pMAPIAllocateBuffer)((sizeof SPropValue) * 1, (void **)&pNewPropValues)); if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_FAILED_TO_ALLOCATE_BUFFER_D,hr); goto exit_update_sid; } // Allocate a buffer for the SID
hr = ResultFromScode((*pMAPIAllocateBuffer)(dwSidLength, (void **)&pMapiSid)); if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_FAILED_TO_ALLOCATE_BUFFER_D,hr); goto exit_update_sid; } // Copy the SID.
CopySid(dwSidLength,pMapiSid,newSid); // Write the SID
pNewPropValues[0].ulPropTag = PR_EMS_AB_ASSOC_NT_ACCOUNT; pNewPropValues[0].Value.bin.lpb = (UCHAR *)pMapiSid; pNewPropValues[0].Value.bin.cb = dwSidLength; MCSASSERT (IsValidSid (pMapiSid) );
if ( m_stat ) { m_stat->IncrementOwnerChange(node,mailbox,NULL); } anychange = TRUE; if ( ! args->NoChange() ) { hr = pUserEntry->SetProps(1, pNewPropValues, &pProblems);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_UPDATE_ACCOUNT_FAILED_D, hr); pProblems = NULL; // Don't try to free this if SetProps fails.
goto exit_update_sid; }
// Save changes.
hr = pUserEntry->SaveChanges(KEEP_OPEN_READWRITE);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_SAVE_CHANGES_FAILED_D, hr); goto exit_update_sid; } } exit_update_sid: if (pMapiSid != NULL) (*pMAPIFreeBuffer)(pMapiSid);
if (pProblems != NULL) (*pMAPIFreeBuffer)(pProblems);
if (pNewPropValues != NULL) (*pMAPIFreeBuffer)(pNewPropValues);
} } // 2. Translate the PR_EMS_AB_NT_SECURITY_DESCRIPTOR property
PSECURITY_DESCRIPTOR pSD = (PSECURITY_DESCRIPTOR)pUserProperties[3].Value.bin.lpb; if ( pSD && PR_EMS_AB_NT_SECURITY_DESCRIPTOR == pUserProperties[3].ulPropTag ) { TMapiSD tMailbox((SECURITY_DESCRIPTOR *)pSD); tMailbox.SetName(pUserProperties[0].Value.lpszW); if ( tMailbox.HasSecurity() ) { TSD * pSD = tMailbox.GetSecurity();
bool changes = tMailbox.ResolveSDInternal(args->Cache(),m_stat,verbose,args->TranslationMode(),mailbox, FALSE); if ( changes ) { anychange = TRUE; // need to write the changes
LPSPropValue pNewPropValues = NULL; LPSPropProblemArray pProblems = NULL;
// update the entry
// Allocate a buffer to set the property.
SECURITY_DESCRIPTOR * pMapiSD; SECURITY_DESCRIPTOR * pRelSD = (SECURITY_DESCRIPTOR *)pSD->MakeRelSD(); DWORD dwSDLength = GetSecurityDescriptorLength(pRelSD); if ( ! pRelSD ) { goto exit_update_sd; } hr = ResultFromScode((*pMAPIAllocateBuffer)((sizeof SPropValue) * 1, (void **)&pNewPropValues)); if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_FAILED_TO_ALLOCATE_BUFFER_D,hr); goto exit_update_sd; } // Allocate a buffer for the SD
hr = ResultFromScode((*pMAPIAllocateBuffer)(dwSDLength, (void **)&pMapiSD)); if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_FAILED_TO_ALLOCATE_BUFFER_D,hr); goto exit_update_sd; }
// Copy the SD.
memcpy(pMapiSD,pRelSD,dwSDLength ); // Write the SD
free(pRelSD); pNewPropValues[0].ulPropTag = PR_EMS_AB_NT_SECURITY_DESCRIPTOR; pNewPropValues[0].Value.bin.lpb = (UCHAR *)pMapiSD; pNewPropValues[0].Value.bin.cb = dwSDLength; if ( ! args->NoChange() ) { hr = pUserEntry->SetProps(1, pNewPropValues, &pProblems);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_RECIP_SD_WRITE_FAILED_SD,pUserProperties[0].Value.lpszW,hr); pProblems = NULL; // Don't try to free this if SetProps fails.
goto exit_update_sd; }
// Save changes.
hr = pUserEntry->SaveChanges(KEEP_OPEN_READONLY);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_RECIP_SD_SAVE_FAILED_SD,pUserProperties[0].Value.lpszW,hr); goto exit_update_sd; } } exit_update_sd: if (pMapiSD != NULL) (*pMAPIFreeBuffer)(pMapiSD);
if (pProblems != NULL) (*pMAPIFreeBuffer)(pProblems);
if (pNewPropValues != NULL) (*pMAPIFreeBuffer)(pNewPropValues); } } } if ( anychange && m_stat ) { m_stat->IncrementChanged(mailbox); if ( args->LogFileDetails() ) err.MsgWrite(0,DCT_MSG_CHANGED_S,pUserProperties[0].Value.lpszW); } (*pMAPIFreeBuffer)(pUserProperties); pUserProperties = NULL; return TRUE; }
BOOL TGlobalDirectory::Scan( SecurityTranslatorArgs * args, // in - translation settings
WCHAR const * container // in - distinguished name or display name of container to process
) { LPABCONT pRootEntry = NULL; // root of AB
ULONG ulObjectType = 0; HRESULT hr; // TAccountCache * cache = args->Cache();
if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); return FALSE; } if ( ! m_pAdrBook ) { OpenGlobalList(); } if ( ! m_pAdrBook ) { return FALSE; } /* SizedSPropTagArray(3, rgPropTags) =
{ 3, { PR_ENTRYID, PR_DISPLAY_NAME, PR_DEPTH, } }; */ // Open the root entry.
hr = m_pAdrBook->OpenEntry(0, NULL, NULL, 0, &ulObjectType, (LPUNKNOWN*)&pRootEntry); if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_GET_AB_ROOT_FAILED_D,hr); return FALSE; } ScanHierarchy(pRootEntry,args,container); return TRUE; }
BOOL TGlobalDirectory::ScanHierarchy( LPABCONT pContainer, // in - interface pointer to address book
SecurityTranslatorArgs * args, // in - translation settings
WCHAR const * container // in - distinguished name or display name of container
) { HRESULT hr; LPMAPITABLE pContainerTable = NULL; LPSRowSet pContainerRows = NULL; LPABCONT pEntry = NULL; ULONG ulObjectType = 0; BOOL foundContainer = FALSE; MCSASSERT(pContainer); if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); return FALSE; } // Get its hierarchical table.
SizedSPropTagArray(4, rgPropTags) = { 4, { PR_ENTRYID, PR_DISPLAY_NAME_A, // I tried to get the display name in unicode format, but it did not work.
PR_OBJECT_TYPE, PR_EMS_AB_OBJ_DIST_NAME_A } };
hr = pContainer->GetHierarchyTable(CONVENIENT_DEPTH, &pContainerTable);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_GET_HIER_TABLE_FAILED_D,hr); return FALSE; }
// Get a list of all rows.
hr = (*pHrQueryAllRows)(pContainerTable, (LPSPropTagArray)&rgPropTags, NULL, NULL, 0, &pContainerRows);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_GET_TABLE_CONTENTS_FAILED_D,hr); pContainerTable->Release(); return FALSE; }
for (ULONG ulRow = (ULONG)0; ulRow < pContainerRows->cRows; ++ulRow) { if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); return FALSE; } hr = m_pAdrBook->OpenEntry(pContainerRows->aRow[ulRow].lpProps[0].Value.bin.cb, (LPENTRYID)pContainerRows->aRow[ulRow].lpProps[0].Value.bin.lpb, NULL, MAPI_MODIFY, // or 0 if nochange mode
&ulObjectType, (LPUNKNOWN *)&pEntry);
if (!SUCCEEDED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_OPEN_CONTAINER_FAILED_SD,pContainerRows->aRow[ulRow].lpProps[1].Value.lpszW,hr); return FALSE; } else {
// LPMAPITABLE pContainerTable = NULL; // container table
UCHAR containerA[LEN_DistName]; safecopy(containerA,container); // look for the specified container
if ( NOT_PT_ERROR(pContainerRows->aRow[ulRow].lpProps[1] ) && !UStrICmp(containerA,pContainerRows->aRow[ulRow].lpProps[1].Value.lpszA) ) { foundContainer = TRUE; break; } if ( container && container[0]== '/' ) { // Check the distinguished name
LPSPropValue props; ULONG ulCount; HRESULT hr2;
hr2 = pEntry->GetProps((LPSPropTagArray)&rgPropTags,0,&ulCount,&props); if ( SUCCEEDED(hr) ) { if ( ulCount >= 4 && NOT_PT_ERROR(props[3]) ) { if ( !UStrICmp(props[3].Value.lpszA,containerA) ) { foundContainer = TRUE; } } (*pMAPIFreeBuffer)(props); props = NULL; } else { err.SysMsgWrite(ErrE,hr2,DCT_MSG_GET_CONTAINER_INFO_FAILED_D,hr2); } if ( foundContainer ) break; } } } // if we found the container, process it's contents
if ( foundContainer ) {
if ( args->LogFileDetails() ) err.MsgWrite(0,DCT_MSG_EXAMINING_CONTENTS_S,pContainerRows->aRow[ulRow].lpProps[1].Value.lpszW); // Get its contents table.
hr = pEntry->GetContentsTable(0, &pContainerTable);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_GET_CONTAINER_INFO_FAILED_D,hr); pEntry->Release(); pEntry = NULL; return FALSE; } // need to scan contents and hierarchy for this item
m_pContainer = pEntry; m_name = pContainerRows->aRow[ulRow].lpProps[1].Value.lpszW; ScanContents(pContainerTable,args); } else { err.MsgWrite(ErrW,DCT_MSG_CONTAINER_NOT_FOUND_S,container); }
(*pFreeProws)(pContainerRows); pContainerRows = NULL; pContainerTable->Release(); return TRUE; } ///////////////////////////////////////////////////////////////////////////////
// Scan the contents of the Container for sids that need to be converted.
BOOL TGlobalDirectory::ScanContents( LPMAPITABLE pContainerTable, // in - contents table for container
SecurityTranslatorArgs * args // in - translation settings
) { ULONG ulDLEntries; // WCHAR em[500] = L"";
MCSASSERT (pContainerTable); if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); return FALSE; } HRESULT hr = pContainerTable->GetRowCount(0, &ulDLEntries);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_COUNT_CONTAINER_FAILED_SD,m_name,hr); return FALSE; }
// Determine which column of the table has the Entry ID.
LPSPropTagArray pDLPropTags = NULL;
hr = pContainerTable->QueryColumns(0, &pDLPropTags);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_CONTAINER_CORRUPTED_SD,m_name,hr); return FALSE; }
ULONG ulEntryIdColumn; BOOL bFoundEntryId = FALSE; for (UINT ulCol = 0; ulCol < pDLPropTags->cValues; ++ulCol) { if (pDLPropTags->aulPropTag[ulCol] == PR_ENTRYID) { ulEntryIdColumn = ulCol; bFoundEntryId = TRUE; break; } } (*pMAPIFreeBuffer)(pDLPropTags); pDLPropTags = NULL; if (!bFoundEntryId) { err.SysMsgWrite(ErrE,hr,DCT_MSG_CONTAINER_CORRUPTED_SD,m_name,hr); return FALSE; }
// Peruse the rows for the SIDs and names.
LPSRowSet pDLRows = NULL; LPMAPIPROP pUserEntry = NULL; for (ULONG ulRow = 0; ulRow < ulDLEntries; ++ulRow) { if ( args->Cache()->IsCancelled() ) { break; } hr = pContainerTable->SeekRow(BOOKMARK_BEGINNING, ulRow, NULL);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_CANT_FIND_ENTRY_SD,m_name,hr); return FALSE; }
hr = pContainerTable->QueryRows(1, TBL_NOADVANCE, &pDLRows);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_CANT_LOAD_ENTRY_SD,m_name,hr); return FALSE; }
// Get the current entry.
ULONG ulEntryType = 0;
if ( pDLRows && pDLRows->cRows ) { hr = m_pContainer->OpenEntry(pDLRows->aRow[0].lpProps[ulEntryIdColumn].Value.bin.cb, (ENTRYID *)pDLRows->aRow[0].lpProps[ulEntryIdColumn].Value.bin.lpb, NULL, MAPI_MODIFY, &ulEntryType, (LPUNKNOWN*)&pUserEntry); } (*pFreeProws)(pDLRows); pDLRows = NULL;
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_RECIP_LOAD_FAILED_SD,m_name,hr); return FALSE; } if ( pUserEntry ) { UpdateEntry(pUserEntry,ulEntryType, args); pUserEntry->Release(); pUserEntry = NULL; } } if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); return FALSE; } return TRUE; }
BOOL TGlobalDirectory::OpenContainerByRow(long row) { HRESULT hr; ULONG ulObjectType = 0; MCSASSERT( m_pRootRows ); // TODO: check that row is in valid range.
// Load the container that should contain the DL.
hr = m_pAdrBook->OpenEntry( m_pRootRows->aRow[row].lpProps[0].Value.bin.cb, (LPENTRYID)m_pRootRows->aRow[row].lpProps[0].Value.bin.lpb, NULL, MAPI_MODIFY, // or 0 if nochange mode
&ulObjectType, (LPUNKNOWN *)&m_pContainer);
(*pFreeProws)(m_pRootRows); m_pRootRows = NULL;
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_OPEN_CONTAINER_FAILED_SD,L"",hr); return FALSE; }
// Look for the DL in its container.
// LPMAPITABLE pContainerTable = NULL; // container table
// Get its contents table.
hr = m_pContainer->GetContentsTable(0, &m_pContainerTable);
if (FAILED(hr)) { err.SysMsgWrite(ErrE,hr,DCT_MSG_GET_TABLE_CONTENTS_FAILED_D,hr); m_pContainer->Release(); m_pContainer = NULL; return FALSE; }
return TRUE; }
#define NDX_SID 3
#define NDX_SD 4
DWORD TGlobalDirectory::GetLDAPPort( WCHAR * server ) { DWORD port = LDAP_PORT; // DWORD rc = 0;
DAPI_ENTRY attList; DAPI_ENTRY * pValues; ATT_VALUE atts[3]; WCHAR distName[300]; PDAPI_EVENT result = NULL; DAPI_ENTRY * pAttributes = NULL;
if ( ! LoadDAPI() ) { err.MsgWrite(ErrW,DCT_MSG_LDAP_PORT_DETECT_FAILED_S,server); return port; }
if ( DAPIOpen(server) ) { // set up the attribute list
attList.unAttributes = 3; attList.ulEvalTag = VALUE_ARRAY; attList.rgEntryValues = atts;
atts[0].DapiType = DAPI_UNICODE; atts[0].Value.pszW = ATT_OBJ_CLASS; atts[0].size = UStrLen(atts[0].Value.pszW); atts[0].pNextValue = NULL;
atts[1].DapiType = DAPI_UNICODE; atts[1].Value.pszW = ATT_LDAP_PORT; atts[1].size = UStrLen(atts[1].Value.pszW); atts[1].pNextValue = NULL;
atts[2].DapiType = DAPI_UNICODE; atts[2].Value.pszW = LDAP_USE_SITE_VALUES; atts[2].size = UStrLen(atts[2].Value.pszW); atts[2].pNextValue = NULL;
// construct the DN for the attribute
swprintf(distName,L"/cn=Configuration/cn=Servers/cn=%s%s",server,LDAP_PortNumber_DN_Part);
result = (*pDAPIRead)(m_dSession,DAPI_RAW_MODE,distName,&attList, &pValues, &pAttributes); if ( ! result ) { if ( pValues && pValues->rgEntryValues[2].Value.iValue == 0 ) { // not using site defaults - rgEntryValues[1] contains the correct value for this server
if ( pValues && pValues->rgEntryValues[1].DapiType == DAPI_INT ) { port = pValues->rgEntryValues[1].Value.iValue; } } else { // This server is using the default values for the site - need to look at the site level to find the correct value
(*pDAPIFreeMemory)(pValues); swprintf(distName,L"/cn=Configuration%s",LDAP_PortNumber_DN_Part); attList.unAttributes = 2; // don't want to get 'Use-Site-Defaults' this time (we're looking at the Site)
result = (*pDAPIRead)(m_dSession,DAPI_RAW_MODE,distName,&attList, &pValues, &pAttributes); if ( ! result ) { if ( pValues && pValues->rgEntryValues[1].DapiType == DAPI_INT ) { // Here's the default value for the site
port = pValues->rgEntryValues[1].Value.iValue; } } else { (*pDAPIFreeMemory)(result); } } (*pDAPIFreeMemory)(pValues); } else { (*pDAPIFreeMemory)(result); } } else { err.MsgWrite(ErrW,DCT_MSG_LDAP_PORT_DETECT_FAILED_S,server); } DAPIClose();
ReleaseDAPI(); return port; }
void TGlobalDirectory::GetSiteNameForServer( WCHAR const * server, // in - name of exchange server to use
CLdapEnum * e, // in - LDAP connection to use for query
WCHAR * siteName // out- distinguished name of exchange site for server
) { WCHAR * atts[6] = { L"distinguishedName", L"rdn",NULL }; WCHAR query[200]; DWORD rc; WCHAR ** values = NULL; siteName[0] = 0; swprintf(query,L"(&(objectClass=computer)(rdn=%ls))",server); rc = e->Open(query,L"",2,100,3,atts); // there should be only one server with this name
if (! rc ) rc = e->Next(&values); if (! rc ) { if ( !UStrICmp(values[1],server) ) { WCHAR serverPrefix[LEN_Path]; swprintf(serverPrefix,L"cn=%ls,cn=Servers,cn=Configuration,",values[1]); if ( ! UStrICmp(values[0],serverPrefix,UStrLen(serverPrefix)) ) { UStrCpy(siteName,values[0] + UStrLen(serverPrefix)); } else { err.MsgWrite(ErrE,DCT_MSG_GENERIC_S,values[0]); } } else { err.MsgWrite(ErrE,DCT_MSG_LDAP_CALL_FAILED_SD,server,ERROR_NOT_FOUND); } e->FreeData(values); } else { err.SysMsgWrite(ErrE,e->m_connection.LdapMapErrorToWin32(rc),DCT_MSG_LDAP_CALL_FAILED_SD,server,rc); }
}
BOOL TGlobalDirectory::DoLdapTranslation( WCHAR * server, WCHAR * creds, WCHAR * password, SecurityTranslatorArgs * args, WCHAR * basept, WCHAR * query ) { CLdapEnum e; WCHAR * atts[6] = { L"distinguishedName", L"rdn", L"cn", L"Assoc-NT-Account",L"NT-Security-Descriptor",NULL }; WCHAR ** values = NULL; ULONG port = GetLDAPPort(server); ULONG pageSize = 100; WCHAR basepoint[LEN_Path] = L"";
e.m_connection.SetCredentials(creds,password);
DWORD rc = e.InitConnection(server,port); BOOL anychange = FALSE; BOOL verbose = args->LogVerbose(); BOOL bUseMapFile = args->UsingMapFile();
if (! rc ) { if ( ! basept ) { GetSiteNameForServer(server,&e,basepoint); } else { // use the user-specified basepoint
safecopy(basepoint,basept); } if ( query ) { rc = e.Open(query,basepoint,2,pageSize,5,atts); } else { rc = e.Open(L"(objectClass=*)",basepoint,2,pageSize,5,atts); } if ( ! rc ) { do { rc = e.Next(&values); anychange = FALSE; if (! rc ) { if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); return FALSE; } if ( m_stat ) { m_stat->DisplayPath(values[0]); m_stat->IncrementExamined(mailbox); }
// update the Assoc-NT-Account, if any
if ( values[NDX_SID] && *values[NDX_SID] ) { // convert the SID to a binary value and look it up in the cache
BYTE pSid[500];
if ( e.m_connection.StringToBytes(values[NDX_SID],pSid) ) { // check if the sid is one we need to change
//TRACE (_T("DisplayName = %s "),pUserProperties[0].Value.lpszW);
PSID newSid = 0; TAcctNode * node;
if ( IsValidSid(pSid) ) { if (!bUseMapFile) node = args->Cache()->Lookup(pSid); else node = args->Cache()->LookupWODomain(pSid); if ( m_stat ) { m_stat->IncrementOwnerExamined(); if ( verbose ) err.MsgWrite(0,DCT_MSG_EXAMINED_S,values[0]); } if ( node == (TAcctNode*)-1 && m_stat ) m_stat->IncrementOwnerNoTarget();
if ( node && (node != (TAcctNode *)-1) && node->IsValidOnTgt() ) { if (!bUseMapFile) newSid = args->Cache()->GetTgtSid(node); else newSid = args->Cache()->GetTgtSidWODomain(node); } else newSid = NULL; } else { newSid = NULL; } if ( newSid ) { //TRACE (_T("needs to be translated\n"));
MCSASSERT ( IsValidSid(newSid) ); WCHAR newSidStr[1000];
if ( e.m_connection.BytesToString((BYTE*)newSid,newSidStr,GetLengthSid(newSid)) ) { if ( m_stat ) { m_stat->IncrementOwnerChange(node,mailbox,NULL); } anychange = TRUE; if ( ! args->NoChange() ) { rc = e.m_connection.UpdateSimpleStringValue(values[0],atts[NDX_SID],newSidStr);
if ( rc ) { err.SysMsgWrite(ErrE,rc,DCT_MSG_UPDATE_ACCOUNT_FAILED_D, rc); } }
} } } } // Update the NT-Security-Descriptor, if any
if ( values[NDX_SD] && *values[NDX_SD] ) { // convert the SID to a binary value and look it up in the cache
BYTE * pSD = new BYTE[UStrLen(values[NDX_SD])]; if (!pSD) return FALSE;
if ( e.m_connection.StringToBytes(values[NDX_SD],pSD) ) { TMapiSD tMailbox((SECURITY_DESCRIPTOR *)pSD); if ( tMailbox.HasSecurity() ) { TSD * pSD2 = tMailbox.GetSecurity();
bool changes = tMailbox.ResolveSDInternal(args->Cache(),m_stat,verbose,args->TranslationMode(),mailbox, bUseMapFile); if ( changes ) { anychange = TRUE; SECURITY_DESCRIPTOR * pRelSD = (SECURITY_DESCRIPTOR *)pSD2->MakeRelSD(); DWORD dwSDLength = GetSecurityDescriptorLength(pRelSD); if ( ! args->NoChange() ) { WCHAR * pSDString = new WCHAR[1 + dwSDLength * 2]; if (!pSDString) { delete [] pSD; return FALSE; } if ( e.m_connection.BytesToString((BYTE*)pRelSD,pSDString,dwSDLength) ) { rc = e.m_connection.UpdateSimpleStringValue(values[0],atts[NDX_SD],pSDString); if ( rc ) { err.SysMsgWrite(ErrE,rc,DCT_MSG_RECIP_SD_WRITE_FAILED_SD,values[0],rc); if ( rc == ERROR_INVALID_PARAMETER ) { // this error occurs when the security descriptor is too large
// don't abort in this case
rc = 0; } } } delete [] pSDString; } } } } delete [] pSD; } if ( anychange && m_stat ) { m_stat->IncrementChanged(mailbox); if ( args->LogFileDetails() ) err.MsgWrite(0,DCT_MSG_CHANGED_S,values[0]); } e.FreeData(values); }
} while ( ! rc ); } if ( rc && (rc != LDAP_COMPARE_FALSE) && (rc != ERROR_NOT_FOUND) ) { err.SysMsgWrite(ErrE,e.m_connection.LdapMapErrorToWin32(rc),DCT_MSG_LDAP_CALL_FAILED_SD,server,rc); } } else { err.SysMsgWrite(ErrE,rc,DCT_MSG_CANNOT_CONNECT_TO_EXCHANGE_SERVER_SSD,server,creds,rc); } return rc; } BOOL TGlobalDirectory::DoDAPITranslation( WCHAR * serv, // in - Exchange server
SecurityTranslatorArgs * args // in - translation options
) { // enumerate organizations, sites, and containers
if ( DAPIOpen( serv ) ) { HRESULT hr; LPWSTR pOrganizations = NULL; LPWSTR pSites = NULL; LPWSTR pContainers = NULL; WCHAR * server; WCHAR container[1];
if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); return FALSE; } server = new WCHAR[wcslen(serv) + 1]; if (!server) return FALSE; UStrCpy(server,serv); container[0] = 0; // enumerate organizations
m_stat->DisplayPath(GET_STRING(IDS_ScanningExchangeDirectory)); hr = HrEnumOrganizations(container,server,&pOrganizations); if ( SUCCEEDED(hr) ) { for ( ; pOrganizations && *pOrganizations ; pOrganizations+=UStrLen(pOrganizations) + 1 ) { if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); break; } WCHAR wOrganizations[300]; safecopy(wOrganizations,pOrganizations); DAPITranslate(wOrganizations,args); //enumerate sites
hr = HrEnumSites(server,pOrganizations,&pSites); if ( SUCCEEDED(hr) ) { for ( ; pSites && *pSites ; pSites+=UStrLen(pSites) + 1 ) { if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); break; } WCHAR wSites[300]; safecopy(wSites,pSites); DAPITranslate(wSites,args); //enumerate containers
hr = HrEnumContainers(server,pSites,TRUE,&pContainers); if ( SUCCEEDED(hr) ) { for ( ; pContainers && *pContainers ; pContainers+=UStrLen(pContainers) + 1 ) { if (args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); break; } WCHAR wContainers[300]; safecopy(wContainers,pContainers); DAPITranslate(wContainers,args); } } else { err.SysMsgWrite(ErrE,hr,DCT_MSG_ENUM_CONTAINERS_FAILED_SD,hr); } } } else { err.SysMsgWrite(ErrE,hr,DCT_MSG_ENUM_SITES_FAILED_SD,hr); } } } else { err.SysMsgWrite(ErrE,hr,DCT_MSG_ENUM_ORGS_FAILED_SD,hr); } DAPIClose(); } m_stat->DisplayPath(L""); return TRUE; }
BOOL TGlobalDirectory::DAPIOpen( WCHAR * server // in - name of exchange server
) { PDAPI_EVENT result; BOOL bRc;
WCHAR * serv;
if ( ! LoadDAPI() ) { return FALSE; } serv = new WCHAR [wcslen(server) + 1]; if (!serv) return FALSE; UStrCpy(serv,&*server); // initialize parms
m_dParms.dwDAPISignature = DAPI_SIGNATURE; m_dParms.dwFlags = DAPI_RAW_MODE | DAPI_EVENT_SOME; m_dParms.pszDSAName = serv; // computer name of directory service agent
m_dParms.pszBasePoint = NULL; m_dParms.pszContainer = NULL; m_dParms.pszNTDomain = NULL; m_dParms.pszCreateTemplate = NULL; m_dParms.pAttributes = NULL;
result = (*pDAPIStart)(&m_dSession,&m_dParms); if ( result ) { // this is an error - need to show a message or something
LogDapiError(ErrE,L"DAPIStart: ",result); (*pDAPIFreeMemory)(result); bRc = FALSE; } else bRc = TRUE; return bRc; }
BOOL TGlobalDirectory::DAPIClose() { (*pDAPIEnd)(&m_dSession); return TRUE; }
void StripQuotes(TCHAR * distName) { int nChars = UStrLen(distName); int i, // input index
o; // output index
BOOL prevQuote = FALSE;
for ( i = 0,o = 0 ; i < nChars ; i++ ) { if ( distName[i] != _T('"') || prevQuote ) { prevQuote = FALSE; distName[o] = distName[i]; o++; } else { // leave the output pointer where it is, so the character will be overwritten
prevQuote = TRUE; } } distName[o] = 0; } BOOL TGlobalDirectory::DAPITranslate(WCHAR * dn, SecurityTranslatorArgs * args) { PDAPI_EVENT result; TCHAR * distName; // distinguished name of object to read.
DAPI_ENTRY * pValues = NULL; DAPI_ENTRY * pAttributes = NULL; ATT_VALUE attributes[8]; // DapiType, Value, size, pNextValue
WCHAR * att_names[8]; bool verbose = false;
if ( args->Cache()->IsCancelled() ) { err.MsgWrite(0,DCT_MSG_OPERATION_ABORTED); return FALSE; } att_names[0] = L"Obj-Class"; att_names[1] = L"DSA-Signature"; att_names[2] = L"Directory Name"; att_names[3] = L"Obj-View-Containers"; att_names[4] = L"Obj-Dist-Name"; int sdIndex = 5; att_names[sdIndex] = L"NT-Security-Descriptor"; att_names[6] = L"Organizational-Unit-Name"; att_names[7] = L"Organization-Name"; DAPI_ENTRY attr;
attr.unAttributes = 8; attr.ulEvalTag = VALUE_ARRAY; attr.rgEntryValues = attributes; for ( int i = 0 ; i < 8 ; i++ ) { attributes[i].DapiType = DAPI_UNICODE; attributes[i].Value.pszW = att_names[i]; attributes[i].size = UStrLen(att_names[i]); attributes[i].pNextValue = NULL; } // we may need DAPI_RESTRICT_ACCESS and/or DAPI_MODIFY_REPLACE_PROPERTIES
distName = new TCHAR[wcslen(dn) + 1]; if (!distName) return FALSE; UStrCpy(distName,(LPCTSTR)dn); StripQuotes(distName); result = (*pDAPIRead)(m_dSession,DAPI_RAW_MODE,distName,&attr, &pValues, &pAttributes); if ( m_stat ) { WCHAR wpath[MAX_PATH+1]; safecopy(wpath,&*distName); m_stat->DisplayPath(wpath); m_stat->IncrementExamined(container); if ( verbose ) err.MsgWrite(0,DCT_MSG_EXAMINED_S,&*dn); } if ( result ) { // this is an error - need to show a message or something
LogDapiError(ErrE,L"DAPIRead: ",result); (*pDAPIFreeMemory)(result); } if ( pValues && pValues->rgEntryValues[sdIndex].DapiType == DAPI_BINARY ) { SECURITY_DESCRIPTOR * pSD = (SECURITY_DESCRIPTOR *)pValues->rgEntryValues[sdIndex].Value.lpBinary; TMapiSD exContainer(pSD);
if ( exContainer.HasSecurity() ) { exContainer.SetName(distName);
bool changes = exContainer.ResolveSDInternal(args->Cache(),m_stat,false,args->TranslationMode(),container, FALSE); // ULONG usn = 0;
if ( changes ) { if (m_stat) { m_stat->IncrementChanged(container); if ( args->LogFileDetails() ) err.MsgWrite(0,DCT_MSG_CHANGED_S,&*dn); }
// record the changes
if ( ! args->NoChange() ) { TSD * tSD = exContainer.GetSecurity();
pSD = (SECURITY_DESCRIPTOR *)tSD->MakeRelSD(); pValues->rgEntryValues[sdIndex].Value.lpBinary = (UCHAR *)pSD; // make sure the size is still correct. For NT 3.51 and 4 it
// should always be, since all the SIDs are the same length, but
// we don't want this to break if it gets a different-length SID
pValues->rgEntryValues[sdIndex].size = GetSecurityDescriptorLength(pSD); // Change the directory name to match the distinguished name, otherwise you get
// object not found error.
pValues->rgEntryValues[2].Value.pszW = pValues->rgEntryValues[4].Value.pszW; // Same problem for the OU-Name for Sites
if ( pValues->rgEntryValues[6].DapiType != DAPI_NO_VALUE ) { pValues->rgEntryValues[6].Value.pszW = pValues->rgEntryValues[4].Value.pszW; } // Same problem for the O-name for organizations
if ( pValues->rgEntryValues[7].DapiType != DAPI_NO_VALUE ) { pValues->rgEntryValues[7].Value.pszW = pValues->rgEntryValues[4].Value.pszW; } result = (*pDAPIWrite)(m_dSession,DAPI_WRITE_MODIFY,&attr,pValues,NULL,NULL,NULL); if ( result ) { LogDapiError(ErrE,L"DAPIWrite: ",result); (*pDAPIFreeMemory)(result); } free(pSD); }
} } } // cleanup
if ( pValues ) (*pDAPIFreeMemory)(pValues); if ( pAttributes ) (*pDAPIFreeMemory)(pAttributes); delete [] distName; return TRUE; }
|