Source code of Windows XP (NT5)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

745 lines
17 KiB

  1. #include <windows.h>
  2. #include "admcmn.h"
  3. // For Net* calls:
  4. #include <lmaccess.h>
  5. #include <lmwksta.h>
  6. #include <lmapibuf.h>
  7. #include <lmerr.h>
  8. // For CAddr:: calls:
  9. #include "cpool.h"
  10. #include "abook.h"
  11. #include "abtype.h"
  12. #include "address.hxx"
  13. #include "webhlpr.h"
  14. static HRESULT GetDCName (
  15. LPCWSTR wszServer,
  16. LPCWSTR wszDomain,
  17. LPWSTR * pwszDC
  18. );
  19. #if 0
  20. // Taken from smtp/shash.hxx
  21. #define TABLE_SIZE 241
  22. static unsigned long ElfHash (const unsigned char * UserName);
  23. static HRESULT RecursiveDeleteDirectory (
  24. LPCWSTR wszPath
  25. );
  26. #endif
  27. //
  28. // Exported functions:
  29. //
  30. HRESULT
  31. EnumerateTrustedDomains (
  32. LPCWSTR wszComputer,
  33. LPWSTR * pmszDomains
  34. )
  35. {
  36. _ASSERT ( wszComputer && *wszComputer );
  37. _ASSERT ( pmszDomains );
  38. HRESULT hr = NOERROR;
  39. DWORD dwErr = NOERROR;
  40. LPWSTR mszDomains = NULL;
  41. DWORD cchDomains = 0;
  42. DWORD cchLocalDomain = 0;
  43. LPWSTR wszPrimaryDomain = NULL;
  44. DWORD cchPrimaryDomain = 0;
  45. LPWSTR wszCurrent;
  46. LPWSTR mszResult = NULL;
  47. *pmszDomains = NULL;
  48. //
  49. // Get a list of the trusted 1st tier domains:
  50. //
  51. dwErr = NetEnumerateTrustedDomains (
  52. const_cast <LPWSTR> (wszComputer),
  53. &mszDomains
  54. );
  55. if ( dwErr != NOERROR ) {
  56. //
  57. // This didn't work, but we should always add the primary
  58. // domain & computer name. Make an empty multisz to signify
  59. // no trusted domains.
  60. //
  61. dwErr = NetApiBufferAllocate (
  62. 2 * sizeof ( WCHAR ),
  63. (LPVOID *) &mszDomains
  64. );
  65. if ( dwErr != NOERROR ) {
  66. BAIL_WITH_FAILURE ( hr, HRESULT_FROM_WIN32 ( dwErr ) );
  67. }
  68. mszDomains[0] = NULL;
  69. mszDomains[1] = NULL;
  70. }
  71. _ASSERT ( mszDomains );
  72. if ( mszDomains == NULL ) {
  73. BAIL_WITH_FAILURE ( hr, E_UNEXPECTED );
  74. }
  75. //
  76. // Check for an empty domain list:
  77. //
  78. if ( mszDomains[0] == NULL && mszDomains[1] == NULL ) {
  79. cchDomains = 2;
  80. }
  81. else {
  82. wszCurrent = mszDomains;
  83. if ( mszDomains[0] == _T('\0') &&
  84. mszDomains[1] == _T('\0') ) {
  85. }
  86. while ( wszCurrent && *wszCurrent ) {
  87. DWORD cchCurrent;
  88. cchCurrent = lstrlen ( wszCurrent ) + 1;
  89. cchDomains += cchCurrent;
  90. wszCurrent += cchCurrent;
  91. }
  92. cchDomains += 1; // Terminating NULL
  93. }
  94. //
  95. // Add the local machine and primary domain:
  96. //
  97. hr = GetPrimaryDomain ( wszComputer, &wszPrimaryDomain );
  98. BAIL_ON_FAILURE ( hr );
  99. //
  100. // PREFIX flagged this as something we need to fix. Easier to
  101. // add check than to argue.
  102. //
  103. if ( !wszPrimaryDomain ) {
  104. hr = E_FAIL;
  105. goto Exit;
  106. }
  107. _ASSERT ( wszPrimaryDomain );
  108. cchPrimaryDomain = lstrlen ( wszPrimaryDomain ) + 1;
  109. if ( wszPrimaryDomain[0] != _T('\\') ||
  110. wszPrimaryDomain[1] != _T('\\') ||
  111. lstrcmpi ( wszPrimaryDomain + 2, wszComputer )
  112. ) {
  113. //
  114. // The primary domain isn't the local machine, so add the local
  115. // machine to the list.
  116. //
  117. cchLocalDomain = lstrlen ( _T("\\\\") ) + lstrlen ( wszComputer ) + 1;
  118. }
  119. else {
  120. //
  121. // The primary domain is the local machine - no need to add it twice.
  122. //
  123. cchLocalDomain = 0;
  124. }
  125. mszResult = new WCHAR [ cchDomains + cchLocalDomain + cchPrimaryDomain];
  126. if ( !mszResult ) {
  127. BAIL_WITH_FAILURE ( hr, E_OUTOFMEMORY );
  128. }
  129. if ( cchLocalDomain ) {
  130. wsprintf ( mszResult, _T("\\\\%s"), wszComputer );
  131. StringToUpper ( mszResult );
  132. }
  133. wsprintf ( mszResult + cchLocalDomain, _T("%s"), wszPrimaryDomain );
  134. StringToUpper ( mszResult + cchLocalDomain );
  135. //
  136. // Copy the rest of the domains in the domain list:
  137. //
  138. CopyMemory (
  139. mszResult + cchLocalDomain + cchPrimaryDomain,
  140. mszDomains,
  141. cchDomains * sizeof (WCHAR)
  142. );
  143. *pmszDomains = mszResult;
  144. Exit:
  145. if ( wszPrimaryDomain ) {
  146. delete [] wszPrimaryDomain;
  147. }
  148. if ( mszDomains ) {
  149. NetApiBufferFree ( mszDomains );
  150. }
  151. return hr;
  152. }
  153. HRESULT
  154. GetPrimaryDomain (
  155. LPCWSTR wszComputer,
  156. LPWSTR * pwszPrimaryDomain
  157. )
  158. {
  159. HRESULT hr = NOERROR;
  160. DWORD dwErr = NOERROR;
  161. WKSTA_INFO_100 * pwkstaInfo = NULL;
  162. LPWSTR wszLangroup = NULL;
  163. *pwszPrimaryDomain = NULL;
  164. //
  165. // Get the workstation info to get the primary domain:
  166. //
  167. dwErr = NetWkstaGetInfo (
  168. const_cast <LPWSTR> (wszComputer),
  169. 100,
  170. (LPBYTE *)&pwkstaInfo
  171. );
  172. if ( dwErr != NOERROR ) {
  173. BAIL_WITH_FAILURE ( hr, HRESULT_FROM_WIN32 ( dwErr ) );
  174. }
  175. _ASSERT ( pwkstaInfo );
  176. wszLangroup = pwkstaInfo->wki100_langroup;
  177. if ( wszLangroup && *wszLangroup ) {
  178. *pwszPrimaryDomain = new WCHAR [ lstrlen ( wszLangroup ) + 1 ];
  179. if ( !*pwszPrimaryDomain ) {
  180. BAIL_WITH_FAILURE(hr, E_OUTOFMEMORY);
  181. }
  182. lstrcpy ( *pwszPrimaryDomain, wszLangroup );
  183. }
  184. else {
  185. //
  186. // If there is no primary domain, use the computer name prefixed
  187. // by backslashes.
  188. //
  189. *pwszPrimaryDomain = new WCHAR [ 2 + lstrlen ( wszComputer ) + 1 ];
  190. if ( !*pwszPrimaryDomain ) {
  191. BAIL_WITH_FAILURE(hr, E_OUTOFMEMORY);
  192. }
  193. wsprintf ( *pwszPrimaryDomain, _T("\\\\%s"), wszComputer );
  194. }
  195. StringToUpper ( *pwszPrimaryDomain );
  196. Exit:
  197. if ( pwkstaInfo ) {
  198. NetApiBufferFree ( pwkstaInfo );
  199. }
  200. return hr;
  201. }
  202. HRESULT CheckNTAccount (
  203. LPCWSTR wszComputer,
  204. LPCWSTR wszUsername,
  205. BOOL * pfExists
  206. )
  207. {
  208. HRESULT hr = NOERROR;
  209. DWORD dwErr = NOERROR;
  210. BOOL fFound;
  211. BYTE sidUser [ MAX_PATH ];
  212. DWORD cbSid = ARRAY_SIZE ( sidUser );
  213. WCHAR wszDomain [ 512 ];
  214. DWORD cchDomain = ARRAY_SIZE ( wszDomain );
  215. SID_NAME_USE nuUser;
  216. *pfExists = FALSE;
  217. fFound = LookupAccountName (
  218. wszComputer,
  219. wszUsername,
  220. &sidUser,
  221. &cbSid,
  222. wszDomain,
  223. &cchDomain,
  224. &nuUser
  225. );
  226. if ( !fFound ) {
  227. dwErr = GetLastError ( );
  228. //
  229. // The not found error is ERROR_NONE_MAPPED:
  230. //
  231. if ( dwErr != ERROR_NONE_MAPPED ) {
  232. BAIL_WITH_FAILURE ( hr, HRESULT_FROM_WIN32 ( dwErr ) );
  233. }
  234. }
  235. *pfExists = fFound;
  236. Exit:
  237. return hr;
  238. }
  239. HRESULT CreateNTAccount (
  240. LPCWSTR wszComputer,
  241. LPCWSTR wszDomain,
  242. LPCWSTR wszUsername,
  243. LPCWSTR wszPassword // = ""
  244. )
  245. {
  246. HRESULT hr = NOERROR;
  247. DWORD dwErr = NOERROR;
  248. DWORD dwParmErr = 0;
  249. LPWSTR wszDC = NULL;
  250. USER_INFO_3 UserInfo;
  251. WCHAR wszNull[] = { 0 };
  252. hr = GetDCName ( wszComputer, wszDomain, &wszDC );
  253. BAIL_ON_FAILURE(hr);
  254. ZeroMemory ( &UserInfo, sizeof ( UserInfo ) );
  255. UserInfo.usri3_name = const_cast <LPWSTR> (wszUsername);
  256. UserInfo.usri3_full_name = const_cast <LPWSTR> (wszUsername);
  257. UserInfo.usri3_password = const_cast <LPWSTR> (wszPassword);
  258. // UserInfo.usri3_password_age
  259. UserInfo.usri3_priv = USER_PRIV_USER;
  260. UserInfo.usri3_home_dir = wszNull;
  261. UserInfo.usri3_comment = wszNull;
  262. UserInfo.usri3_flags = UF_SCRIPT | UF_NORMAL_ACCOUNT;
  263. UserInfo.usri3_script_path = wszNull;
  264. UserInfo.usri3_auth_flags = 0;
  265. UserInfo.usri3_usr_comment = wszNull;
  266. UserInfo.usri3_parms = wszNull;
  267. UserInfo.usri3_workstations = wszNull;
  268. // UserInfo.usri3_last_logon =;
  269. // UserInfo.usri3_last_logoff =;
  270. UserInfo.usri3_acct_expires = TIMEQ_FOREVER;
  271. UserInfo.usri3_max_storage = USER_MAXSTORAGE_UNLIMITED;
  272. // UserInfo.usri3_units_per_week =;
  273. // UserInfo.usri3_logon_hours = NULL;
  274. // UserInfo.usri3_bad_pw_count =;
  275. // UserInfo.usri3_num_logons =;
  276. UserInfo.usri3_logon_server = wszNull;
  277. UserInfo.usri3_country_code = 0;
  278. UserInfo.usri3_code_page = CP_ACP;
  279. // UserInfo.usri3_user_id =;
  280. UserInfo.usri3_primary_group_id = DOMAIN_GROUP_RID_USERS;
  281. UserInfo.usri3_profile = wszNull;
  282. UserInfo.usri3_home_dir_drive = wszNull;
  283. UserInfo.usri3_password_expired = FALSE;
  284. //
  285. // Add the user:
  286. //
  287. dwErr = NetUserAdd (
  288. wszDC,
  289. 3,
  290. (LPBYTE) &UserInfo,
  291. &dwParmErr
  292. );
  293. if ( dwErr != NOERROR ) {
  294. // TRACE ( _T("Failed to add user: %d, parmerr = %d\r\n"), dwErr, dwParmErr );
  295. BAIL_WITH_FAILURE ( hr, HRESULT_FROM_WIN32 ( dwErr ) );
  296. }
  297. #if 0
  298. //
  299. // Attempt to add the user to the "USERS" group:
  300. //
  301. dwErr = NetGroupAddUser (
  302. wszDC,
  303. // (LPWSTR) (LPCWSTR) CString ( strDC + L"\\Users" ),
  304. (LPWSTR) (LPCWSTR) strUsername
  305. );
  306. if ( dwErr != NOERROR ) {
  307. TRACE ( _T("Couldn't add user to USERS group: %d\r\n"), dwErr );
  308. }
  309. #endif
  310. Exit:
  311. delete wszDC;
  312. return hr;
  313. }
  314. LPCWSTR StripBackslashesFromComputerName ( LPCWSTR wsz )
  315. {
  316. if ( wsz[0] == _T('\\') &&
  317. wsz[1] == _T('\\') ) {
  318. return wsz + 2;
  319. }
  320. else {
  321. return wsz;
  322. }
  323. }
  324. static LPWSTR DuplicateString ( LPCWSTR wsz )
  325. {
  326. LPWSTR wszResult;
  327. _ASSERT ( wsz );
  328. wszResult = new WCHAR [ lstrlen ( wsz ) + 1 ];
  329. if ( wszResult ) {
  330. lstrcpy ( wszResult, wsz );
  331. }
  332. return wszResult;
  333. }
  334. void StringToUpper ( LPWSTR wsz )
  335. {
  336. while ( *wsz ) {
  337. *wsz = towupper ( *wsz );
  338. wsz++;
  339. }
  340. }
  341. HRESULT GetDCName (
  342. LPCWSTR wszServer,
  343. LPCWSTR wszDomain,
  344. LPWSTR * pwszDC
  345. )
  346. {
  347. HRESULT hr = NOERROR;
  348. DWORD dwError = NOERROR;
  349. LPWSTR wszServerCopy = NULL;
  350. LPWSTR wszDomainCopy = NULL;
  351. LPCWSTR wszPlainServer = NULL;
  352. LPCWSTR wszPlainDomain = NULL;
  353. LPWSTR wszDC = NULL;
  354. if ( wszServer == NULL ) {
  355. wszServer = _T("");
  356. }
  357. wszServerCopy = DuplicateString ( wszServer );
  358. wszDomainCopy = DuplicateString ( wszDomain );
  359. if ( !wszServerCopy || !wszDomainCopy ) {
  360. BAIL_WITH_FAILURE ( hr, E_OUTOFMEMORY );
  361. }
  362. StringToUpper ( wszServerCopy );
  363. StringToUpper ( wszDomainCopy );
  364. wszPlainServer = StripBackslashesFromComputerName ( wszServerCopy );
  365. wszPlainDomain = StripBackslashesFromComputerName ( wszDomainCopy );
  366. if ( ! lstrcmp ( wszPlainServer, wszPlainDomain ) ) {
  367. *pwszDC = new WCHAR [ lstrlen (_T("\\\\")) + lstrlen (wszPlainServer) + 1 ];
  368. if ( *pwszDC == NULL ) {
  369. BAIL_WITH_FAILURE(hr, E_OUTOFMEMORY);
  370. }
  371. wsprintf ( *pwszDC, _T("\\\\%s"), wszPlainServer );
  372. goto Exit;
  373. }
  374. dwError = NetGetDCName ( wszServer, wszDomain, (LPBYTE *) &wszDC );
  375. if ( dwError != NOERROR ) {
  376. //
  377. // Error, try to get any DC name:
  378. //
  379. _ASSERT ( wszDC == NULL );
  380. hr = HRESULT_FROM_WIN32 ( dwError );
  381. dwError = NetGetAnyDCName ( wszServer, wszDomain, (LPBYTE *) &wszDC );
  382. if ( dwError != NOERROR ) {
  383. goto Exit;
  384. }
  385. }
  386. _ASSERT ( dwError == NOERROR );
  387. _ASSERT ( wszDC[0] == _T('\\') );
  388. _ASSERT ( wszDC[1] == _T('\\') );
  389. *pwszDC = new WCHAR [ lstrlen ( wszDC ) + 1 ];
  390. if ( *pwszDC == NULL ) {
  391. BAIL_WITH_FAILURE(hr, E_OUTOFMEMORY);
  392. }
  393. lstrcpy ( *pwszDC, wszDC );
  394. Exit:
  395. delete wszServerCopy;
  396. delete wszDomainCopy;
  397. if ( wszDC ) {
  398. NetApiBufferFree ( wszDC );
  399. }
  400. return hr;
  401. }
  402. BOOL
  403. IsValidEmailAddress (
  404. LPCWSTR wszEmailAddress
  405. )
  406. {
  407. char szBuf[512];
  408. int cchCopied;
  409. cchCopied = WideCharToMultiByte ( CP_ACP, 0, wszEmailAddress, -1, szBuf, sizeof (szBuf), NULL, NULL );
  410. if ( cchCopied == 0 ) {
  411. return FALSE;
  412. }
  413. return CAddr::ValidateEmailName( szBuf, FALSE );
  414. }
  415. #if 0
  416. HRESULT
  417. DeleteMailbox (
  418. LPCWSTR wszServer,
  419. LPCWSTR wszAlias,
  420. LPCWSTR wszVDirPath,
  421. LPCWSTR wszUsername, // OPTIONAL
  422. LPCWSTR wszPassword // OPTIONAL
  423. )
  424. {
  425. TraceFunctEnter ( "DeleteMailbox" );
  426. HRESULT hr = NOERROR;
  427. UCHAR szAlias[512];
  428. ULONG lHashValue;
  429. DWORD cchMailboxPath;
  430. LPWSTR wszMailboxPath;
  431. LPCWSTR wszFormat;
  432. if ( !wszServer || !wszAlias || !wszVDirPath ) {
  433. BAIL_WITH_FAILURE ( hr, E_INVALIDARG );
  434. }
  435. if ( !*wszServer || !*wszAlias || !*wszVDirPath ) {
  436. BAIL_WITH_FAILURE ( hr, E_INVALIDARG );
  437. }
  438. //
  439. // Compute the mailbox hash value:
  440. //
  441. WideCharToMultiByte ( CP_ACP, 0, wszAlias, -1, (char *) szAlias, ARRAY_SIZE ( szAlias ), NULL, NULL );
  442. DebugTrace ( 0, "Deleting mailbox for %s", szAlias );
  443. lHashValue = ElfHash ( szAlias ) % TABLE_SIZE;
  444. DebugTrace ( 0, "Hash value = %u", lHashValue );
  445. //
  446. // Compute the mailbox directory path,
  447. // this is: <vdir path>\<alias hash>\<alias>
  448. //
  449. cchMailboxPath =
  450. lstrlen ( wszVDirPath ) + // <Virtual directory path>
  451. 1 + // '\'
  452. 20 + // <hash value>
  453. 1 + // '\'
  454. lstrlen ( wszAlias ) + // <alias>
  455. 1; // null terminator
  456. wszMailboxPath = new WCHAR [ cchMailboxPath ];
  457. if ( !wszMailboxPath ) {
  458. BAIL_WITH_FAILURE ( hr, E_OUTOFMEMORY );
  459. }
  460. // Avoid too many '\'s
  461. if ( wszVDirPath [ lstrlen ( wszVDirPath ) - 1 ] == _T('\\') ) {
  462. wszFormat = _T("%s%u\\%s");
  463. }
  464. else {
  465. wszFormat = _T("%s\\%u\\%s");
  466. }
  467. wsprintf (
  468. wszMailboxPath,
  469. wszFormat,
  470. wszVDirPath,
  471. lHashValue,
  472. wszAlias
  473. );
  474. RecursiveDeleteDirectory ( wszMailboxPath );
  475. Exit:
  476. TraceFunctLeave ();
  477. return hr;
  478. }
  479. //---[ ElfHash() ]------------------------------------------------------------
  480. //
  481. // Pulled this function from the stacks project. Calculates the hash value
  482. // given a username. The mailbox of a user is stored in a subdirectory of
  483. // the form <virtual directory root>\<hash value%TABLE_SIZE>\<username>.
  484. // The username is the lowercase of the name part of the proxy address for
  485. // e.g. [email protected].
  486. //
  487. // Params:
  488. // UserName The username to be hashed.
  489. //
  490. // Return: (HRESULT)
  491. // NOERROR if success, ADSI error code if failed.
  492. //
  493. //----------------------------------------------------------------------------
  494. unsigned long ElfHash (const unsigned char * UserName)
  495. {
  496. unsigned long HashValue = 0, g;
  497. while (*UserName)
  498. {
  499. HashValue = (HashValue << 4) + *UserName++;
  500. if( g = HashValue & 0xF0000000)
  501. HashValue ^= g >> 24;
  502. HashValue &= ~g;
  503. }
  504. return HashValue;
  505. }
  506. HRESULT RecursiveDeleteDirectory (
  507. LPCWSTR wszPath
  508. )
  509. {
  510. TraceFunctEnter ( "DeleteDirectory" );
  511. HRESULT hr = NOERROR;
  512. DWORD cchOldCurrentDirectory;
  513. LPWSTR wszOldCurrentDirectory = NULL;
  514. DWORD cchPath = lstrlen ( wszPath );
  515. HANDLE hSearch = INVALID_HANDLE_VALUE;
  516. WIN32_FIND_DATA find;
  517. BOOL fOk = TRUE;
  518. HANDLE hFile;
  519. //
  520. // Save the current directory:
  521. //
  522. cchOldCurrentDirectory = GetCurrentDirectory ( 0, NULL );
  523. _ASSERT ( cchOldCurrentDirectory > 0 );
  524. wszOldCurrentDirectory = new WCHAR [ cchOldCurrentDirectory ];
  525. if ( !wszOldCurrentDirectory ) {
  526. BAIL_WITH_FAILURE(hr, E_OUTOFMEMORY);
  527. }
  528. GetCurrentDirectory ( cchOldCurrentDirectory, wszOldCurrentDirectory );
  529. //
  530. // Set the current directory to the path:
  531. //
  532. SetCurrentDirectory ( wszPath );
  533. //
  534. // Create the search string:
  535. //
  536. hSearch = FindFirstFile ( _T("*.*"), &find );
  537. while ( hSearch != INVALID_HANDLE_VALUE && fOk ) {
  538. //
  539. // Skip the . and .. directories:
  540. //
  541. if (
  542. lstrcmp ( find.cFileName, _T(".") ) &&
  543. lstrcmp ( find.cFileName, _T("..") )
  544. ) {
  545. if ( find.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) {
  546. hr = RecursiveDeleteDirectory ( find.cFileName );
  547. BAIL_ON_FAILURE(hr);
  548. }
  549. else {
  550. //
  551. // Delete this file:
  552. //
  553. hFile = CreateFile (
  554. find.cFileName,
  555. GENERIC_WRITE,
  556. FILE_SHARE_DELETE,
  557. NULL,
  558. OPEN_EXISTING,
  559. FILE_FLAG_DELETE_ON_CLOSE,
  560. NULL
  561. );
  562. if ( hFile != INVALID_HANDLE_VALUE ) {
  563. CloseHandle ( hFile );
  564. }
  565. // Otherwise, press on...
  566. }
  567. }
  568. // Get the next file:
  569. fOk = FindNextFile ( hSearch, &find );
  570. }
  571. //
  572. // Delete the directory:
  573. //
  574. if ( hSearch != INVALID_HANDLE_VALUE ) {
  575. FindClose ( hSearch );
  576. hSearch = INVALID_HANDLE_VALUE;
  577. }
  578. hFile = CreateFile (
  579. wszPath,
  580. GENERIC_WRITE,
  581. FILE_SHARE_DELETE,
  582. NULL,
  583. OPEN_EXISTING,
  584. FILE_FLAG_DELETE_ON_CLOSE,
  585. NULL
  586. );
  587. if ( hFile != INVALID_HANDLE_VALUE ) {
  588. CloseHandle ( hFile );
  589. }
  590. Exit:
  591. if ( wszOldCurrentDirectory ) {
  592. //
  593. // Restore the current directory:
  594. //
  595. SetCurrentDirectory ( wszOldCurrentDirectory );
  596. delete wszOldCurrentDirectory;
  597. }
  598. if ( hSearch != INVALID_HANDLE_VALUE ) {
  599. FindClose ( hSearch );
  600. hSearch = INVALID_HANDLE_VALUE;
  601. }
  602. TraceFunctLeave ( );
  603. return hr;
  604. }
  605. #endif