PageRenderTime 44ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/connectors/cs/src/FreeBusyBuilder/FreeBusyBuilder.cs

http://google-calendar-connectors.googlecode.com/
C# | 1004 lines | 572 code | 108 blank | 324 comment | 74 complexity | 41cddf31f6e80808fa7096e833022fbd MD5 | raw file
Possible License(s): LGPL-2.1, Apache-2.0
  1. /* Copyright (c) 2008 Google Inc. All Rights Reserved
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. using System;
  16. using System.Diagnostics;
  17. using System.Collections.Generic;
  18. using System.Text;
  19. using System.IO;
  20. using System.DirectoryServices;
  21. using System.Net;
  22. using System.Web;
  23. using System.Xml;
  24. using System.Xml.Schema;
  25. namespace Google.CalendarConnector.Plugin
  26. {
  27. class GWiseContact
  28. {
  29. /// <summary>
  30. /// The UID is the GUID used to identify the imported account.
  31. /// It is generateed by the GroupWise connector by hashing
  32. /// some properties of the contact.
  33. /// The format of the string is not the traditional GUID format
  34. /// {5F64F157-0CCA-43CC-A0A9-3A7DAB963F06}, but 4 DWORDs printed
  35. /// in hex with no leading zeros and separated with dashes
  36. /// D3143CCF-E9938337-91AD646-AB45321E, so the lentgh is somewhere
  37. /// between 7 and 19 characters.
  38. /// </summary>
  39. private string uid;
  40. /// <summary>
  41. /// The GroupWise email address including the GWISE: prefix
  42. /// extracted from the proxy addresses.
  43. /// </summary>
  44. private string Address;
  45. /// <summary>
  46. /// The common group used for the account. Normally it is "RECIPIENTS",
  47. /// but some companies use different names, for example "WORKERS".
  48. /// </summary>
  49. private string group;
  50. /// <summary>
  51. /// Generic Boolean weather the contact is marked or not.
  52. /// Right now is used only to say whether the free busy message
  53. /// was found in the public folder, but I wanted to use it for other
  54. /// things like whether the account changed between to queries to AD.
  55. /// </summary>
  56. private bool mark;
  57. /// <summary>
  58. /// Constructs a new GroupWise contact.
  59. /// </summary>
  60. /// <param name="gwiseUid">The contact's UID</param>
  61. /// <param name="gwiseAddress">The contact's GWISE address</param>
  62. /// <param name="commonGroup">The contact's common group</param>
  63. public GWiseContact(
  64. string gwiseUid,
  65. string gwiseAddress,
  66. string commonGroup)
  67. {
  68. uid = gwiseUid;
  69. Address = gwiseAddress;
  70. group = commonGroup;
  71. mark = false;
  72. }
  73. /// <summary>
  74. /// The contact UID.
  75. /// </summary>
  76. public string GwiseUid
  77. {
  78. get
  79. {
  80. return uid;
  81. }
  82. }
  83. /// <summary>
  84. /// The contact GWISE proxy address inlcuding the GWISE: prefix.
  85. /// </summary>
  86. public string GwiseAddress
  87. {
  88. get
  89. {
  90. return Address;
  91. }
  92. }
  93. /// <summary>
  94. /// The contact common group from the legacy DN. Usually RECIPIENTS.
  95. /// </summary>
  96. public string CommonGroup
  97. {
  98. get
  99. {
  100. return group;
  101. }
  102. }
  103. /// <summary>
  104. /// Generic Boolean to mark the contact in certain state.
  105. /// </summary>
  106. public bool Marked
  107. {
  108. get
  109. {
  110. return mark;
  111. }
  112. set
  113. {
  114. mark = value;
  115. }
  116. }
  117. };
  118. class FreeBusyBuilder
  119. {
  120. private static readonly int LDAP_PAGE_SIZE = 500;
  121. private static readonly int ONE_MB = 1024 * 1204;
  122. private static string SLASH_LOWER_CASE_CN = "/cn=";
  123. private static string SLASH_UPPER_CASE_CN = "/CN=";
  124. private static string LEGACY_EXCHANGE_DN = "legacyExchangeDN";
  125. private static string PROXY_ADDRESSES = "proxyAddresses";
  126. private static string GWISE_COLON = "GWISE:";
  127. /// <summary>
  128. /// List of properties we ask to get back from the AD query.
  129. /// Right now only these two are necessary, but others can be added.
  130. /// Still one has to be cautious so we don't ask for too much data.
  131. /// </summary>
  132. private static readonly string[] PROPERTIES_TO_LOAD =
  133. { LEGACY_EXCHANGE_DN, PROXY_ADDRESSES };
  134. /// <summary>
  135. /// The free busy messages are keep in a public folder
  136. /// in the non-ipm. Normally this is above the ipm, but in DAV
  137. /// the way to address them makes it look like a peer to the
  138. /// other public folders. The rest of the path, organization and
  139. /// organizational unit (something like
  140. /// o=GooLab/ou=First Administrative Group) is extracted from the
  141. /// data we get from AD for the contacts.
  142. /// </summary>
  143. private static string FREE_BUSY_URL_TEMPLATE =
  144. "http://localhost/public/NON_IPM_SUBTREE/" +
  145. "SCHEDULE%2B%20FREE%20BUSY/EX:{0}/";
  146. /// <summary>
  147. /// The free busy emails have a subject in the format:
  148. /// USER-/CN=<common_group_name>/CN=<user_id>
  149. /// </summary>
  150. private static readonly string USER_DASH = "USER-";
  151. // The slashes need to be encoded with _xF8FF_ for Exchange,
  152. // since they are not meant to be new folder,
  153. // but rather part of the URL component.
  154. private static readonly string FREE_BUSY_EML_TEMPLATE =
  155. "{0}USER-_xF8FF_CN={1}_xF8FF_CN={2}.EML";
  156. /// <summary>
  157. /// This is the LDAP query for the contacts imported by the GroupWise
  158. /// connector. It is trying to get object that are contacts
  159. /// (objectClass=contact)(objectCategory=person) same as the Exchange
  160. /// recepient policy builder does, then those that are imported by any
  161. /// connector, then those that have their legacyExchangeDN set
  162. /// (I have seem ocasions there was delay), then have their
  163. /// targetAddress as SMTP so we don't get Notes connector contacts,
  164. /// and at the end those that have secondary (hence lower case) gwise
  165. /// address with their UID.
  166. /// </summary>
  167. private static readonly string GWISE_CONTACTS_QUERY =
  168. "(&(objectClass=contact)(objectCategory=person)(importedFrom=*)" +
  169. "(legacyExchangeDN=*)(targetAddress=SMTP:*)" +
  170. "(proxyAddresses=gwise:UID=*))";
  171. private static readonly string PROPPATCH = "PROPPATCH";
  172. private static readonly string SEARCH = "SEARCH";
  173. private static readonly string USER_AGENT =
  174. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; " +
  175. ".NET CLR 2.0.50727)";
  176. private static readonly string SLASH_START_SLASH = "*/*";
  177. private static readonly string TEXT_SLASH_XML = "text/xml";
  178. private static readonly string COLON_SUBJECT = ":subject";
  179. /// <summary>
  180. /// This is the search WebDAV request to find all free busy emails
  181. /// for GroupWise contacts.
  182. /// The SQL query does the following:
  183. /// - it asks for the subject, so I can extract the UID, from something
  184. /// easier to parse than the href. Also it must ask for at leats one
  185. /// property, or the query fails.
  186. /// - it looks just in the folder specified by the url, not subfolders.
  187. /// - it looks for messages, not folders.
  188. /// - it looks for non-hidden messages.
  189. /// - it looks for messages having the connector property set
  190. /// (see http://support.microsoft.com/kb/928874)
  191. /// and set to GWISE address, so we don't get Notes contacts.
  192. /// Note that the spaces at the end of the strings are significant,
  193. /// so if you edit the query make sure the spacing is right.
  194. /// Also note that despite the MSDN page about like GWISE:% must be
  195. /// in single, not double quotes. This is (indirectly) explained on
  196. /// another MSDN page about tokens.
  197. /// </summary>
  198. private static readonly string SEARCH_REQUEST =
  199. "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
  200. "<D:searchrequest xmlns:D = \"DAV:\">" +
  201. "<D:sql>" +
  202. "SELECT \"urn:schemas:httpmail:subject\" " +
  203. "FROM Scope('SHALLOW TRAVERSAL OF \"\"') " +
  204. "WHERE " +
  205. "(\"DAV:ishidden\" is Null OR \"DAV:ishidden\" = false) " +
  206. "AND \"DAV:isfolder\" = false " +
  207. "AND NOT " +
  208. "(\"http://schemas.microsoft.com/mapi/proptag/x7AE0001E\" " +
  209. "is Null) " +
  210. "AND \"http://schemas.microsoft.com/mapi/proptag/x7AE0001E\" " +
  211. "LIKE 'GWISE:%' " +
  212. "</D:sql>" +
  213. "</D:searchrequest>";
  214. /// <summary>
  215. /// This is the proppatch request to create or fix up the free busy
  216. /// email for a single contact. Since the email has no body,
  217. /// a proppath request is enough.
  218. /// It sets the following properties:
  219. /// - PR_SUBJECT (0x0037001E) to the expected format.
  220. /// - The connector property (0x7AE0001E) per the KB article above.
  221. /// - PR_MESSAGE_LOCALE_ID and PR_LOCALE_ID to the hard coded 1033,
  222. /// because by default they are 0, which may cause problems.
  223. /// Since there is no body, the locale should not matter if valid.
  224. /// </summary>
  225. private static readonly string PROPPATCH_REQUEST =
  226. "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
  227. "<D:propertyupdate " +
  228. "xmlns:M=\"http://schemas.microsoft.com/mapi/proptag/\" " +
  229. "xmlns:D=\"DAV:\">" +
  230. "<D:set>" +
  231. "<D:prop>" +
  232. "<M:x0037001E>USER-/CN={0}/CN={1}</M:x0037001E>" +
  233. "<M:x7AE0001E>{2}</M:x7AE0001E>" +
  234. "<M:x3FF10003>1033</M:x3FF10003>" +
  235. "<M:x66A10003>1033</M:x66A10003>" +
  236. "</D:prop>" +
  237. "</D:set>" +
  238. "</D:propertyupdate>";
  239. /// <summary>
  240. /// Returns the string encoded for Exchange, replacing slashes with
  241. /// _xF8FF_ in addition to the normal URL encoding.
  242. /// </summary>
  243. /// <param name="element">The string to encode</param>
  244. /// <returns></returns>
  245. private static string ExchangeEncode(string element)
  246. {
  247. return HttpUtility.UrlPathEncode(element.Replace(@"/", "_xF8FF_"));
  248. }
  249. /// <summary>
  250. /// Searches AD for the objects matching the given filter.
  251. /// The root path, user name and password are optional.
  252. /// If they are given they will be used, otherwise RootDSE
  253. /// and the current user will be used instead.
  254. /// </summary>
  255. /// <param name="searchRootPath">The LDAP root to search,
  256. /// null will search the default domain</param>
  257. /// <param name="userName">User to use to make the query,
  258. /// null will use the default credentials</param>
  259. /// <param name="password">Password for the user</param>
  260. /// <param name="filter">The LDAP filter for the query</param>
  261. /// <param name="propertiesToLoad">The names of the properties
  262. /// to load from AD.</param>
  263. /// <returns>The results got from AD</returns>
  264. private static SearchResultCollection FindGWiseContacts(
  265. string searchRootPath,
  266. string userName,
  267. string password,
  268. string filter,
  269. string[] propertiesToLoad)
  270. {
  271. DirectoryEntry searchRoot = new DirectoryEntry();
  272. DirectorySearcher searcher = new DirectorySearcher();
  273. if (searchRootPath == null)
  274. {
  275. searchRoot.Path = searcher.SearchRoot.Path;
  276. }
  277. else
  278. {
  279. searchRoot.Path = searchRootPath;
  280. }
  281. if ((userName != null) && (password != null))
  282. {
  283. searchRoot.Username = userName;
  284. searchRoot.Password = password;
  285. }
  286. searcher.SearchRoot = searchRoot;
  287. searcher.Filter = filter;
  288. searcher.SearchScope = SearchScope.Subtree;
  289. searcher.PropertiesToLoad.AddRange(propertiesToLoad);
  290. searcher.PageSize = LDAP_PAGE_SIZE;
  291. return searcher.FindAll();
  292. }
  293. /// <summary>
  294. /// Parses the GroupWise UID from legacyExchangeDN AD property.
  295. /// If the property is different (not legacyExchangeDN) or mallformed
  296. /// the function will return null.
  297. /// The string will be returned in upper case.
  298. /// The UID is assumed to be the last CN component.
  299. /// For example for
  300. /// /o=G..b/ou=F..p/cn=Recipients/cn=d0692608-dea9c581-466bd07a-f0d12967
  301. /// it will return D0692608-DEA9C581-466BD07A-F0D12967.
  302. /// </summary>
  303. /// <param name="propName">The name of the property</param>
  304. /// <param name="propValue">The value of the property</param>
  305. /// <returns>The UID or null</returns>
  306. private static string GetGWiseUidFromLegacyExchangeDN(
  307. string propName,
  308. string propValue)
  309. {
  310. string gwiseUid = null;
  311. if (string.Compare(propName, LEGACY_EXCHANGE_DN, true) == 0)
  312. {
  313. int lastCN = propValue.LastIndexOf(SLASH_LOWER_CASE_CN);
  314. if (lastCN != -1)
  315. {
  316. gwiseUid =
  317. propValue.Substring(
  318. lastCN + SLASH_LOWER_CASE_CN.Length).ToUpper();
  319. }
  320. }
  321. return gwiseUid;
  322. }
  323. /// <summary>
  324. /// Parses the common group from legacyExchangeDN AD property.
  325. /// If the property is different (not legacyExchangeDN) or mallformed
  326. /// the function will return null.
  327. /// The string will be returned in upper case.
  328. /// The group is assumed to be prelast CN component.
  329. /// For example for
  330. /// /o=G..b/ou=F..p/cn=Recipients/cn=d0692608-dea9c581-466bd07a-f0d12967
  331. /// it will return RECIPIENTS.
  332. /// </summary>
  333. /// <param name="propName">The name of the property</param>
  334. /// <param name="propValue">The value of the property</param>
  335. /// <returns>The common group or null</returns>
  336. private static string GetCommonGroupFromLegacyExchangeDN(
  337. string propName,
  338. string propValue)
  339. {
  340. string commonGroup = null;
  341. if (string.Compare(propName, LEGACY_EXCHANGE_DN, true) == 0)
  342. {
  343. int lastCN = propValue.LastIndexOf(SLASH_LOWER_CASE_CN);
  344. int prelastCN = -1;
  345. if (lastCN != -1)
  346. {
  347. prelastCN =
  348. propValue.LastIndexOf(SLASH_LOWER_CASE_CN, lastCN);
  349. }
  350. if (prelastCN != -1)
  351. {
  352. commonGroup =
  353. propValue.Substring(
  354. prelastCN + SLASH_LOWER_CASE_CN.Length,
  355. lastCN - prelastCN - SLASH_LOWER_CASE_CN.Length
  356. ).ToUpper();
  357. }
  358. }
  359. return commonGroup;
  360. }
  361. /// <summary>
  362. /// Parses the folder name of for the free busy messages from
  363. /// the legacyExchangeDN AD property and constructs an URL for the
  364. /// free busy folder.
  365. /// The returned string will be also Exchange URL encoded.
  366. /// If the property is different (not legacyExchangeDN) or mallformed
  367. /// the function will return null.
  368. /// The group is assumed to be all components prior to the common group.
  369. /// For example for
  370. /// /o=GooLab/ou=First Administrative Group/cn=Recipients/cn=d...7
  371. /// it will return
  372. /// http://localhost/public/NON_IPM_SUBTREE/SCHEDULE%2B%20FREE%20BUSY/
  373. /// EX:_xF8FF_o=GooLab_xF8FF_ou=First%20Administrative%20Group/
  374. /// </summary>
  375. /// <param name="propName">The name of the property</param>
  376. /// <param name="propValue">The value of the property</param>
  377. /// <returns>The free busy folder url or null</returns>
  378. private static string GenerateParentFreeBusyFolderUrl(
  379. string propName,
  380. string propValue)
  381. {
  382. string freeBusyUrl = null;
  383. if (string.Compare(propName, LEGACY_EXCHANGE_DN, true) == 0)
  384. {
  385. int lastCN = propValue.LastIndexOf(SLASH_LOWER_CASE_CN);
  386. int prelastCN = -1;
  387. if (lastCN != -1)
  388. {
  389. prelastCN = propValue.LastIndexOf(
  390. SLASH_LOWER_CASE_CN, lastCN);
  391. }
  392. if (prelastCN != -1)
  393. {
  394. string encoded =
  395. ExchangeEncode(propValue.Substring(0, prelastCN));
  396. freeBusyUrl = string.Format(
  397. FREE_BUSY_URL_TEMPLATE, encoded);
  398. }
  399. }
  400. return freeBusyUrl;
  401. }
  402. /// <summary>
  403. /// Parses the GroupWise email address from proxy address.
  404. /// If the property is different (not legacyExchangeDN) or
  405. /// the proxy address is in other formar, or mallformed
  406. /// the function will return null.
  407. /// The address return will have the GWISE: prefix.
  408. /// For example for
  409. /// GWISE:user1.postoffice1.domain1
  410. /// it will return GWISE:user1.postoffice1.domain1.
  411. /// </summary>
  412. /// <param name="propName">The name of the property</param>
  413. /// <param name="propValue">The value of the property</param>
  414. /// <returns>The GWISE address (including the GWISE:) or null</returns>
  415. private static string GetGWiseAddressFromProxyAddresses(
  416. string propName,
  417. string propValue)
  418. {
  419. string gwiseAddress = null;
  420. if ((string.Compare(propName, PROXY_ADDRESSES, true) == 0) &&
  421. (string.Compare(propValue, 0, GWISE_COLON, 0, 6)) == 0)
  422. {
  423. gwiseAddress = propValue;
  424. }
  425. return gwiseAddress;
  426. }
  427. /// <summary>
  428. /// Parses the properties for a contact that came from AD
  429. /// and return GWiseContact object created from those properties.
  430. /// If some of the properties are misisng or mallformed
  431. /// the function will return null.
  432. /// In addition to parsing the contact the function returns the
  433. /// URL for the folder that contains the free busy message for
  434. /// the account, if it wasn't computed yet.
  435. /// </summary>
  436. /// <param name="contactProps">The properties of the contact</param>
  437. /// <param name="freeBusyUrl">If not already computed,
  438. /// it will be set to the free busy folder URL</param>
  439. /// <returns>A contact object or null</returns>
  440. private static GWiseContact ParseGWiseContactsFromADProperties(
  441. ResultPropertyCollection contactProps,
  442. ref string freeBusyUrl)
  443. {
  444. string gwiseUid = null;
  445. string gwiseAddress = null;
  446. string commonGroup = null;
  447. string freeBusyUrlTemp = null;
  448. GWiseContact gwiseContact = null;
  449. foreach (string propName in contactProps.PropertyNames)
  450. {
  451. foreach (Object propObject in contactProps[propName])
  452. {
  453. string propValue = propObject.ToString();
  454. if ((freeBusyUrl == null) &&
  455. (freeBusyUrlTemp == null))
  456. {
  457. freeBusyUrlTemp =
  458. GenerateParentFreeBusyFolderUrl(
  459. propName, propValue);
  460. }
  461. if (gwiseUid == null)
  462. {
  463. gwiseUid =
  464. GetGWiseUidFromLegacyExchangeDN(
  465. propName, propValue);
  466. }
  467. if (commonGroup == null)
  468. {
  469. commonGroup =
  470. GetCommonGroupFromLegacyExchangeDN(
  471. propName, propValue);
  472. }
  473. if (gwiseAddress == null)
  474. {
  475. gwiseAddress =
  476. GetGWiseAddressFromProxyAddresses(
  477. propName, propValue);
  478. }
  479. }
  480. }
  481. if ((gwiseAddress != null) &&
  482. (gwiseUid != null) &&
  483. (commonGroup != null))
  484. {
  485. gwiseContact =
  486. new GWiseContact(gwiseUid, gwiseAddress, commonGroup);
  487. }
  488. if ((freeBusyUrl == null) &&
  489. (gwiseContact != null) &&
  490. (freeBusyUrlTemp != null))
  491. {
  492. // Return the free busy URL if not set already,
  493. // but do that only for well formed accounts.
  494. freeBusyUrl = freeBusyUrlTemp;
  495. }
  496. return gwiseContact;
  497. }
  498. /// <summary>
  499. /// Queries AD for all Groupwise connector contacts and returns
  500. /// a dictionary of GWiseContacts. The key is the UID of the contact.
  501. /// If no contacts are found an empty dictionary will be returned,
  502. /// no null.
  503. /// The root path, user name and password are optional.
  504. /// If they are given they will be used, otherwise RootDSE
  505. /// and the current user will be used instead.
  506. /// In addition to all contacts function returns the
  507. /// URL for the folder that contains the free busy message for
  508. /// the accounts.
  509. /// </summary>
  510. /// <param name="searchRootPath">The LDAP root to search,
  511. /// null will search the default domain</param>
  512. /// <param name="userName">User to use to make the query,
  513. /// null will use the default credentials</param>
  514. /// <param name="password">Password for the user</param>
  515. /// <param name="filter">The LDAP filter for the query</param>
  516. /// <param name="freeBusyUrl">
  517. /// Will be set to the free busy folder URL</param>
  518. /// <returns>Dictionary of all contacts keyed by the UID</returns>
  519. private static Dictionary<string, GWiseContact> GetGWiseContactsFromAD(
  520. string searchRootPath,
  521. string userName,
  522. string password,
  523. out string freeBusyUrl)
  524. {
  525. freeBusyUrl = null;
  526. Dictionary<string, GWiseContact> gwise_contacts =
  527. new Dictionary<string, GWiseContact>();
  528. SearchResultCollection adContacts = FindGWiseContacts(
  529. searchRootPath,
  530. userName,
  531. password,
  532. GWISE_CONTACTS_QUERY, PROPERTIES_TO_LOAD);
  533. foreach (SearchResult adContact in adContacts)
  534. {
  535. ResultPropertyCollection contactProps = adContact.Properties;
  536. GWiseContact gwiseContact =
  537. ParseGWiseContactsFromADProperties(
  538. contactProps, ref freeBusyUrl);
  539. if (gwiseContact != null)
  540. {
  541. gwise_contacts.Add(
  542. gwiseContact.GwiseUid, gwiseContact);
  543. }
  544. }
  545. return gwise_contacts;
  546. }
  547. /// <summary>
  548. /// A small help function to build credentials object from
  549. /// given user name and password, if they are given,
  550. /// or return null if either of them is null.
  551. /// Note if the password is empty it should be empty string, not null.
  552. /// </summary>
  553. /// <param name="userName">User to use to for the credentials,
  554. /// or null to use the default credentials</param>
  555. /// <param name="password">Password for the user</param>
  556. /// <returns>Credentials object or null</returns>
  557. private static ICredentials BuildCredentialsHelper(
  558. string userName,
  559. string password)
  560. {
  561. ICredentials credentials = null;
  562. if ((userName != null) && (password != null))
  563. {
  564. credentials = new NetworkCredential(userName, password);
  565. }
  566. return credentials;
  567. }
  568. /// <summary>
  569. /// The function creates a free busy email for the given GWise contact
  570. /// under the public folder specified in free_busy_url.
  571. /// The credentials are optional.
  572. /// If they are given they will be used, otherwise
  573. /// the current user will be used to make the WebDAV call.
  574. /// </summary>
  575. /// <param name="userName">User to authenticate as or
  576. /// null will use the default credentials</param>
  577. /// <param name="password">Password for the user</param>
  578. /// <param name="freeBusyUrl">The Url of the free busy folder,
  579. /// where the free busy email should be created</param>
  580. /// <param name="gwiseContact">The contact for
  581. /// which to create the free busy email</param>
  582. private static void CreateGWiseFreeBusyEmail(
  583. ICredentials credentials,
  584. string freeBusyUrl,
  585. GWiseContact gwiseContact)
  586. {
  587. string gwiseUid = gwiseContact.GwiseUid;
  588. string gwiseAddress = gwiseContact.GwiseAddress;
  589. string commonGroup = gwiseContact.CommonGroup;
  590. string freeBusyMessageUrl =
  591. string.Format(FREE_BUSY_EML_TEMPLATE,
  592. freeBusyUrl, commonGroup, gwiseUid);
  593. HttpWebRequest request =
  594. (HttpWebRequest)HttpWebRequest.Create(freeBusyMessageUrl);
  595. if (credentials != null)
  596. {
  597. request.Credentials = credentials;
  598. request.ConnectionGroupName = "CustomCredentials";
  599. }
  600. else
  601. {
  602. request.UseDefaultCredentials = true;
  603. request.ConnectionGroupName = "DefaultNetworkCredentials";
  604. }
  605. request.Method = PROPPATCH;
  606. // This is necesssary to make Windows Auth use keep alive.
  607. // Due to the large number of connections we may make to Exchange,
  608. // if we don't do this, the process may exhaust the supply of
  609. // available ports.
  610. // To keep this "safe", requests are isolated by connection pool.
  611. // See UnsafeAuthenticatedConnectionSharing on MSDN.
  612. request.UnsafeAuthenticatedConnectionSharing = true;
  613. request.PreAuthenticate = true;
  614. request.AllowAutoRedirect = false;
  615. request.KeepAlive = true;
  616. request.UserAgent = USER_AGENT;
  617. request.Accept = SLASH_START_SLASH;
  618. request.ContentType = TEXT_SLASH_XML;
  619. request.Headers.Add("Translate", "F");
  620. request.Headers.Add("Brief", "t");
  621. string propPatchRequest =
  622. string.Format(PROPPATCH_REQUEST,
  623. commonGroup, gwiseUid, gwiseAddress);
  624. byte[] encodedBody = Encoding.UTF8.GetBytes(propPatchRequest);
  625. request.ContentLength = encodedBody.Length;
  626. Stream requestStream = request.GetRequestStream();
  627. requestStream.Write(encodedBody, 0, encodedBody.Length);
  628. requestStream.Close();
  629. WebResponse response = (HttpWebResponse)request.GetResponse();
  630. Stream responseStream = response.GetResponseStream();
  631. responseStream.Close();
  632. response.Close();
  633. }
  634. /// <summary>
  635. /// The function creates a free busy email for the GWise
  636. /// contacts that are not marked as having working one.
  637. /// The emails are created under the public folder
  638. /// specified in free_busy_url.
  639. /// The credentials are optional.
  640. /// If they are given they will be used, otherwise
  641. /// the current user will be used to make the WebDAV call.
  642. /// </summary>
  643. /// <param name="gwiseContacts">The contacts</param>
  644. /// <param name="credentials">Optional credentials to use</param>
  645. /// <param name="freeBusyUrl">The Url of the free busy folder,
  646. /// where the free busy email should be created</param>
  647. /// <param name="contactsFixed">Will be set to the number of contacts
  648. /// with created free busy emails</param>
  649. /// <param name="contactsSkipped">
  650. /// Will be set to the number of contacts, which free busy
  651. /// emails failed do create</param>
  652. private static void CreateGWiseFreeBusyEmails(
  653. Dictionary<string, GWiseContact> gwiseContacts,
  654. ICredentials credentials,
  655. string freeBusyUrl,
  656. out int contactsFixed,
  657. out int contactsSkipped)
  658. {
  659. int contactsFixedTemp = 0;
  660. int contactsSkippedTemp = 0;
  661. contactsFixed = 0;
  662. contactsSkipped = 0;
  663. foreach (KeyValuePair<string, GWiseContact> contactPair in
  664. gwiseContacts)
  665. {
  666. if (!contactPair.Value.Marked)
  667. {
  668. try
  669. {
  670. CreateGWiseFreeBusyEmail(
  671. credentials,
  672. freeBusyUrl,
  673. contactPair.Value);
  674. contactsFixedTemp++;
  675. }
  676. catch (Exception)
  677. {
  678. // If one contact fails, don't bail totally.
  679. // Instead try to create the others.
  680. contactsSkippedTemp++;
  681. }
  682. }
  683. }
  684. contactsFixed = contactsFixedTemp;
  685. contactsSkipped = contactsSkippedTemp;
  686. }
  687. /// <summary>
  688. /// The function queries Exchange for all the free busy emails
  689. /// of the GroupWise connector contacts.
  690. /// The returned stream is the response from the server,
  691. /// a multistatus xml response, containing the URL/href and the subject
  692. /// of the free busy messages.
  693. /// The credentials are optional.
  694. /// If they are given they will be used, otherwise
  695. /// the current user will be used to make the WebDAV call.
  696. /// </summary>
  697. /// <param name="credentials">Optional credentials to use</param>
  698. /// <param name="freeBusyUrl">The Url of the free busy folder,
  699. /// where to look for free busy </param>
  700. /// <returns></returns>
  701. private static Stream FindGWiseFreeBusyEmails(
  702. ICredentials credentials,
  703. string freeBusyUrl)
  704. {
  705. HttpWebRequest request =
  706. (HttpWebRequest)HttpWebRequest.Create(freeBusyUrl);
  707. if (credentials != null)
  708. {
  709. request.Credentials = credentials;
  710. }
  711. else
  712. {
  713. request.UseDefaultCredentials = true;
  714. }
  715. request.Method = SEARCH;
  716. request.AllowAutoRedirect = false;
  717. request.KeepAlive = true;
  718. request.UserAgent = USER_AGENT;
  719. request.Accept = SLASH_START_SLASH;
  720. request.ContentType = TEXT_SLASH_XML;
  721. request.Headers.Add("Translate", "F");
  722. byte[] encodedBody = Encoding.UTF8.GetBytes(SEARCH_REQUEST);
  723. request.ContentLength = encodedBody.Length;
  724. Stream requestStream = request.GetRequestStream();
  725. requestStream.Write(encodedBody, 0, encodedBody.Length);
  726. requestStream.Close();
  727. WebResponse response = (HttpWebResponse)request.GetResponse();
  728. Stream responseStream = response.GetResponseStream();
  729. return responseStream;
  730. }
  731. /// <summary>
  732. /// Parses the GroupWise UID from the subject of free busy email.
  733. /// If the subject is mallformed the function will return null.
  734. /// The UID is supposed to be in upper case and the function will
  735. /// return it as is.
  736. /// The UID is assumed to be the last CN component in the subject.
  737. /// For example for
  738. /// USER-/CN=RECIPIENTS/CN=D3143CCF-E9938337-91AD646-AB45321E
  739. /// it will return D3143CCF-E9938337-91AD646-AB45321E.
  740. /// </summary>
  741. /// <param name="subject">The subject of the free busy message</param>
  742. /// <returns>The UID or null</returns>
  743. private static string GetGWiseUidFromFreeBusySubject(
  744. string subject)
  745. {
  746. string gwiseUid = null;
  747. int compareResult =
  748. string.Compare(USER_DASH, 0, subject, 0, USER_DASH.Length);
  749. if (compareResult == 0)
  750. {
  751. int lastCN = subject.LastIndexOf(SLASH_UPPER_CASE_CN);
  752. if (lastCN != -1)
  753. {
  754. gwiseUid =
  755. subject.Substring(lastCN + SLASH_UPPER_CASE_CN.Length);
  756. }
  757. }
  758. return gwiseUid;
  759. }
  760. /// <summary>
  761. /// The function gets a stream, which is the result of WebDAV query
  762. /// for free busy emails. It parses the contacts UIDs from the XML
  763. /// response and marks the contacts, which have free buys emails.
  764. /// If the subject is mallformed the function will return null.
  765. /// I am not perfectly happy about mixing the response and
  766. /// the dictionary in a single function. It will be cleaner to parse
  767. /// the response and have this function work on a list.
  768. /// the problem with that is the number of additional allocations that
  769. /// are going to be made, just to be thrown away. So the current design
  770. /// is a compromise for better performance.
  771. /// </summary>
  772. /// <param name="gwiseContacts">Dictionary of contacts</param>
  773. /// <param name="stream">Http response from Exchange</param>
  774. private static void MarkWorkingContacts(
  775. Dictionary<string, GWiseContact> gwiseContacts,
  776. Stream stream)
  777. {
  778. XmlReaderSettings settings = new XmlReaderSettings();
  779. settings.XmlResolver = null;
  780. settings.IgnoreComments = true;
  781. settings.IgnoreWhitespace = true;
  782. settings.ValidationType = ValidationType.None;
  783. settings.ValidationFlags = XmlSchemaValidationFlags.None;
  784. XmlReader reader = XmlReader.Create(stream, settings);
  785. reader.MoveToContent();
  786. while (reader.Read())
  787. {
  788. if ((reader.NodeType == XmlNodeType.Element) &&
  789. (reader.Name.IndexOf(COLON_SUBJECT) != -1))
  790. {
  791. string element = reader.ReadElementString();
  792. string uid = GetGWiseUidFromFreeBusySubject(element);
  793. if (uid != null)
  794. {
  795. GWiseContact gwiseContact = null;
  796. if (gwiseContacts.TryGetValue(uid, out gwiseContact))
  797. {
  798. gwiseContact.Marked = true;
  799. }
  800. }
  801. reader.ReadEndElement();
  802. }
  803. }
  804. }
  805. /// <summary>
  806. /// The function builds the free busy message for GWise contatcs w/o one.
  807. /// </summary>
  808. private static void BuildFreeBusy()
  809. {
  810. string freeBusyUrl = null;
  811. string searchRootPath = null;
  812. string userName = null;
  813. string password = null;
  814. ICredentials credentials =
  815. BuildCredentialsHelper(userName, password);
  816. Dictionary<string, GWiseContact> gwiseContacts =
  817. GetGWiseContactsFromAD(
  818. searchRootPath,
  819. userName,
  820. password,
  821. out freeBusyUrl);
  822. int contactsFound = gwiseContacts.Count;
  823. int contactsFixed = 0;
  824. int contactsSkipped = 0;
  825. if (contactsFound > 0)
  826. {
  827. Stream stream =
  828. FindGWiseFreeBusyEmails(
  829. credentials,
  830. freeBusyUrl);
  831. MarkWorkingContacts(gwiseContacts, stream);
  832. stream.Close();
  833. CreateGWiseFreeBusyEmails(
  834. gwiseContacts,
  835. credentials,
  836. freeBusyUrl,
  837. out contactsFixed,
  838. out contactsSkipped);
  839. }
  840. Console.WriteLine(
  841. "Found {0} GroupWise contacts, " +
  842. "recreated the free busy email for {1} of them " +
  843. "and skipped {2} of them due to errors.",
  844. contactsFound, contactsFixed, contactsSkipped);
  845. }
  846. static int Main(string[] args)
  847. {
  848. bool verbose = false;
  849. Process currentProcess = Process.GetCurrentProcess();
  850. currentProcess.PriorityClass = ProcessPriorityClass.BelowNormal;
  851. if ((args.Length > 0) && ((args[0] == "-v") || (args[0] == "/v")))
  852. {
  853. verbose = true;
  854. }
  855. try
  856. {
  857. BuildFreeBusy();
  858. }
  859. catch (Exception ex)
  860. {
  861. Console.WriteLine("Failed with error \"{0}\" = \"{1}\" from \"{2}\".",
  862. ex.GetType().ToString() ?? string.Empty,
  863. ex.Message ?? string.Empty,
  864. ex.Source ?? string.Empty);
  865. if (verbose)
  866. {
  867. Exception ex2 = ex;
  868. while (ex2 != null)
  869. {
  870. Console.WriteLine("Error details:\n{0}", ex2.ToString());
  871. ex2 = ex2.InnerException;
  872. }
  873. }
  874. return -1;
  875. }
  876. if (verbose)
  877. {
  878. Console.WriteLine(
  879. "PeakPagedMemorySize = {0}MB\n" +
  880. "PeakVirtualMemorySize = {1}MB\n" +
  881. "PeakWorkingSet = {2}MB\n" +
  882. "Handles = {3}\n" +
  883. "UserProcessorTime = {4}\n" +
  884. "TotalProcessorTime = {5}",
  885. currentProcess.PeakPagedMemorySize64 / ONE_MB,
  886. currentProcess.PeakVirtualMemorySize64 / ONE_MB,
  887. currentProcess.PeakWorkingSet64 / ONE_MB,
  888. currentProcess.HandleCount,
  889. currentProcess.UserProcessorTime.ToString(),
  890. currentProcess.TotalProcessorTime.ToString());
  891. }
  892. Console.WriteLine("Finished successfully.");
  893. return 0;
  894. }
  895. }
  896. }