The contact importer and exporter plugin for the Kopano WebApp. See here for more details: https://community.zarafa.com/pg/plugins/project/20393/developer/h44z/webapp-contact-importer
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.

1205 lines
54KB

  1. <?php
  2. /**
  3. * module.contact.php, Kopano Webapp contact to vcf im/exporter
  4. *
  5. * Author: Christoph Haas <christoph.h@sprinternet.at>
  6. * Copyright (C) 2012-2018 Christoph Haas
  7. *
  8. * This library is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU Lesser General Public
  10. * License as published by the Free Software Foundation; either
  11. * version 2.1 of the License, or (at your option) any later version.
  12. *
  13. * This library is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  16. * Lesser General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Lesser General Public
  19. * License along with this library; if not, write to the Free Software
  20. * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  21. *
  22. */
  23. include_once(__DIR__ . "/vendor/autoload.php");
  24. require_once(__DIR__ . "/helper.php");
  25. use JeroenDesloovere\VCard\VCard;
  26. use JeroenDesloovere\VCard\VCardParser;
  27. use contactimporter\Helper;
  28. class ContactModule extends Module
  29. {
  30. private $DEBUG = false; // enable error_log debugging
  31. /**
  32. * @constructor
  33. * @param $id
  34. * @param $data
  35. */
  36. public function __construct($id, $data)
  37. {
  38. parent::__construct($id, $data);
  39. }
  40. /**
  41. * Executes all the actions in the $data variable.
  42. * Exception part is used for authentication errors also
  43. *
  44. * @return boolean true on success or false on failure.
  45. */
  46. public function execute()
  47. {
  48. $result = false;
  49. if (!$this->DEBUG) {
  50. /* disable error printing - otherwise json communication might break... */
  51. ini_set('display_errors', '0');
  52. }
  53. foreach ($this->data as $actionType => $actionData) {
  54. if (isset($actionType)) {
  55. try {
  56. if ($this->DEBUG) {
  57. error_log("exec: " . $actionType);
  58. }
  59. switch ($actionType) {
  60. case "load":
  61. $result = $this->loadContacts($actionType, $actionData);
  62. break;
  63. case "import":
  64. $result = $this->importContacts($actionType, $actionData);
  65. break;
  66. case "export":
  67. $result = $this->exportContacts($actionType, $actionData);
  68. break;
  69. case "importattachment":
  70. $result = $this->getAttachmentPath($actionType, $actionData);
  71. break;
  72. default:
  73. $this->handleUnknownActionType($actionType);
  74. }
  75. } catch (MAPIException $e) {
  76. if ($this->DEBUG) {
  77. error_log("mapi exception: " . $e->getMessage());
  78. }
  79. } catch (Exception $e) {
  80. if ($this->DEBUG) {
  81. error_log("exception: " . $e->getMessage());
  82. }
  83. }
  84. }
  85. }
  86. return $result;
  87. }
  88. /**
  89. * Add an attachment to the give contact
  90. *
  91. * @param $actionType
  92. * @param $actionData
  93. */
  94. private function importContacts($actionType, $actionData)
  95. {
  96. // Get uploaded vcf path
  97. $vcfFile = false;
  98. if (isset($actionData["vcf_filepath"])) {
  99. $vcfFile = $actionData["vcf_filepath"];
  100. }
  101. // Get store id
  102. $storeId = false;
  103. if (isset($actionData["storeid"])) {
  104. $storeId = $actionData["storeid"];
  105. }
  106. // Get folder entryid
  107. $folderId = false;
  108. if (isset($actionData["folderid"])) {
  109. $folderId = $actionData["folderid"];
  110. }
  111. // Get uids
  112. $uids = array();
  113. if (isset($actionData["uids"])) {
  114. $uids = $actionData["uids"];
  115. }
  116. $response = array();
  117. $error = false;
  118. $errorMsg = "";
  119. // parse the vcf file a last time...
  120. $parser = null;
  121. try {
  122. $parser = VCardParser::parseFromFile($vcfFile);
  123. } catch (Exception $e) {
  124. $error = true;
  125. $errorMsg = $e->getMessage();
  126. }
  127. $contacts = array();
  128. if (!$error && iterator_count($parser) > 0) {
  129. $contacts = $this->parseContactsToArray($parser);
  130. $store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeId));
  131. $folder = mapi_msgstore_openentry($store, hex2bin($folderId));
  132. $importAll = false;
  133. if (count($uids) == count($contacts)) {
  134. $importAll = true;
  135. }
  136. $properties = $this->getProperties();
  137. $properties = $this->replaceStringPropertyTags($store, $properties);
  138. $count = 0;
  139. // iterate through all contacts and import them :)
  140. foreach ($contacts as $contact) {
  141. if (isset($contact["display_name"]) && ($importAll || in_array($contact["internal_fields"]["contact_uid"], $uids))) {
  142. $propValuesMAPI = array();
  143. // parse the arraykeys
  144. // TODO: this is very slow...
  145. foreach ($contact as $key => $value) {
  146. if ($key !== "internal_fields") {
  147. $propValuesMAPI[$properties[$key]] = $value;
  148. }
  149. }
  150. $propValuesMAPI[$properties["message_class"]] = "IPM.Contact";
  151. $propValuesMAPI[$properties["icon_index"]] = "512";
  152. $message = mapi_folder_createmessage($folder);
  153. if (isset($contact["internal_fields"]["x_photo_path"])) {
  154. $propValuesMAPI[$properties["picture"]] = 1; // contact has an image
  155. // import the photo
  156. $contactPicture = file_get_contents($contact["internal_fields"]["x_photo_path"]);
  157. $attach = mapi_message_createattach($message);
  158. // Set properties of the attachment
  159. $propValuesIMG = array(
  160. PR_ATTACH_SIZE => strlen($contactPicture),
  161. PR_ATTACH_LONG_FILENAME => 'ContactPicture.jpg',
  162. PR_ATTACHMENT_HIDDEN => false,
  163. PR_DISPLAY_NAME => 'ContactPicture.jpg',
  164. PR_ATTACH_METHOD => ATTACH_BY_VALUE,
  165. PR_ATTACH_MIME_TAG => 'image/jpeg',
  166. PR_ATTACHMENT_CONTACTPHOTO => true,
  167. PR_ATTACH_DATA_BIN => $contactPicture,
  168. PR_ATTACHMENT_FLAGS => 1,
  169. PR_ATTACH_EXTENSION_A => '.jpg',
  170. PR_ATTACH_NUM => 1
  171. );
  172. mapi_setprops($attach, $propValuesIMG);
  173. mapi_savechanges($attach);
  174. if ($this->DEBUG) {
  175. error_log("Contactpicture imported!");
  176. }
  177. if (mapi_last_hresult() > 0) {
  178. error_log("Error saving attach to contact: " . get_mapi_error_name());
  179. }
  180. }
  181. mapi_setprops($message, $propValuesMAPI);
  182. mapi_savechanges($message);
  183. if ($this->DEBUG) {
  184. error_log("New contact added: \"" . $propValuesMAPI[$properties["display_name"]] . "\".\n");
  185. }
  186. $count++;
  187. }
  188. }
  189. $response['status'] = true;
  190. $response['count'] = $count;
  191. $response['message'] = "";
  192. } else {
  193. $response['status'] = false;
  194. $response['count'] = 0;
  195. $response['message'] = $error ? $errorMsg : dgettext("plugin_contactimporter", "VCF file is empty!");
  196. }
  197. $this->addActionData($actionType, $response);
  198. $GLOBALS["bus"]->addData($this->getResponseData());
  199. }
  200. /**
  201. * Get a property from the array.
  202. * @param $props
  203. * @param $propName
  204. * @return string
  205. */
  206. private function getProp($props, $propName)
  207. {
  208. if (isset($props["props"][$propName])) {
  209. return $props["props"][$propName];
  210. }
  211. return "";
  212. }
  213. /**
  214. * Export selected contacts to vCard.
  215. *
  216. * @param $actionType
  217. * @param $actionData
  218. * @return bool
  219. */
  220. private function exportContacts($actionType, $actionData)
  221. {
  222. // Get store id
  223. $storeId = false;
  224. if (isset($actionData["storeid"])) {
  225. $storeId = $actionData["storeid"];
  226. }
  227. // Get records
  228. $records = array();
  229. if (isset($actionData["records"])) {
  230. $records = $actionData["records"];
  231. }
  232. // Get folders
  233. $folder = false;
  234. if (isset($actionData["folder"])) {
  235. $folder = $actionData["folder"];
  236. }
  237. $response = array();
  238. $error = false;
  239. $error_msg = "";
  240. // write csv
  241. $token = Helper::randomstring(16);
  242. $file = PLUGIN_CONTACTIMPORTER_TMP_UPLOAD . "vcf_" . $token . ".vcf";
  243. file_put_contents($file, "");
  244. $store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeId));
  245. if ($store) {
  246. // load folder first
  247. if ($folder !== false) {
  248. $mapiFolder = mapi_msgstore_openentry($store, hex2bin($folder));
  249. $table = mapi_folder_getcontentstable($mapiFolder);
  250. $list = mapi_table_queryallrows($table, array(PR_ENTRYID));
  251. foreach ($list as $item) {
  252. $records[] = bin2hex($item[PR_ENTRYID]);
  253. }
  254. }
  255. for ($index = 0, $count = count($records); $index < $count; $index++) {
  256. // define vcard
  257. $vCard = new VCard();
  258. $message = mapi_msgstore_openentry($store, hex2bin($records[$index]));
  259. // get message properties.
  260. $properties = $GLOBALS['properties']->getContactProperties();
  261. $plaintext = true;
  262. $messageProps = $GLOBALS['operations']->getMessageProps($store, $message, $properties, $plaintext);
  263. // define variables
  264. $firstName = $this->getProp($messageProps, "given_name");
  265. $lastName = $this->getProp($messageProps, "surname");
  266. $additional = $this->getProp($messageProps, "middle_name");
  267. $prefix = $this->getProp($messageProps, "display_name_prefix");
  268. $suffix = '';
  269. // add personal data
  270. $vCard->addName($lastName, $firstName, $additional, $prefix, $suffix);
  271. $company = $this->getProp($messageProps, "company_name");
  272. if (!empty($company)) {
  273. $vCard->addCompany($company);
  274. }
  275. $jobTitle = $this->getProp($messageProps, "title");
  276. if (!empty($jobTitle)) {
  277. $vCard->addJobtitle($jobTitle);
  278. }
  279. // MAIL
  280. $mail = $this->getProp($messageProps, "email_address_1");
  281. if (!empty($mail)) {
  282. $vCard->addEmail($mail);
  283. }
  284. $mail = $this->getProp($messageProps, "email_address_2");
  285. if (!empty($mail)) {
  286. $vCard->addEmail($mail);
  287. }
  288. $mail = $this->getProp($messageProps, "email_address_3");
  289. if (!empty($mail)) {
  290. $vCard->addEmail($mail);
  291. }
  292. // PHONE
  293. $wPhone = $this->getProp($messageProps, "business_telephone_number");
  294. if (!empty($wPhone)) {
  295. $vCard->addPhoneNumber($wPhone, 'WORK');
  296. }
  297. $wPhone = $this->getProp($messageProps, "home_telephone_number");
  298. if (!empty($wPhone)) {
  299. $vCard->addPhoneNumber($wPhone, 'HOME');
  300. }
  301. $wPhone = $this->getProp($messageProps, "cellular_telephone_number");
  302. if (!empty($wPhone)) {
  303. $vCard->addPhoneNumber($wPhone, 'CELL');
  304. }
  305. $wPhone = $this->getProp($messageProps, "business_fax_number");
  306. if (!empty($wPhone)) {
  307. $vCard->addPhoneNumber($wPhone, 'FAX');
  308. }
  309. $wPhone = $this->getProp($messageProps, "pager_telephone_number");
  310. if (!empty($wPhone)) {
  311. $vCard->addPhoneNumber($wPhone, 'PAGER');
  312. }
  313. $wPhone = $this->getProp($messageProps, "car_telephone_number");
  314. if (!empty($wPhone)) {
  315. $vCard->addPhoneNumber($wPhone, 'CAR');
  316. }
  317. // ADDRESS
  318. $address = $this->getProp($messageProps, "business_address");
  319. if (!empty($address)) {
  320. $vCard->addAddress(null, null, $this->getProp($messageProps, "business_address_street"), $this->getProp($messageProps, "business_address_city"), $this->getProp($messageProps, "business_address_state"), $this->getProp($messageProps, "business_address_postal_code"), $this->getProp($messageProps, "business_address_country"), "WORK");
  321. }
  322. $address = $this->getProp($messageProps, "home_address");
  323. if (!empty($address)) {
  324. $vCard->addAddress(null, null, $this->getProp($messageProps, "home_address_street"), $this->getProp($messageProps, "home_address_city"), $this->getProp($messageProps, "home_address_state"), $this->getProp($messageProps, "home_address_postal_code"), $this->getProp($messageProps, "home_address_country"), "HOME");
  325. }
  326. $address = $this->getProp($messageProps, "other_address");
  327. if (!empty($address)) {
  328. $vCard->addAddress(null, null, $this->getProp($messageProps, "other_address_street"), $this->getProp($messageProps, "other_address_city"), $this->getProp($messageProps, "other_address_state"), $this->getProp($messageProps, "other_address_postal_code"), $this->getProp($messageProps, "other_address_country"), "OTHER");
  329. }
  330. // MISC
  331. $url = $this->getProp($messageProps, "webpage");
  332. if (!empty($url)) {
  333. $vCard->addURL($url);
  334. }
  335. $birthday = $this->getProp($messageProps, "birthday");
  336. if (!empty($birthday)) {
  337. $vCard->addBirthday(date("Y-m-d", $birthday));
  338. }
  339. $notes = $this->getProp($messageProps, "body");
  340. if (!empty($notes)) {
  341. $vCard->addNote($notes);
  342. }
  343. $categories = $this->getProp($messageProps, "categories");
  344. if (!empty($categories)) {
  345. $categoryArray = array_map('trim', explode(';', trim($categories, " ;")));
  346. $vCard->addCategories($categoryArray);
  347. }
  348. $hasPicture = $this->getProp($messageProps, "has_picture");
  349. if (!empty($hasPicture) && $hasPicture === true) {
  350. $attachNum = -1;
  351. if (isset($messageProps["attachments"]) && isset($messageProps["attachments"]["item"])) {
  352. foreach ($messageProps["attachments"]["item"] as $attachment) {
  353. if ($attachment["props"]["attachment_contactphoto"] == true) {
  354. $attachNum = $attachment["props"]["attach_num"];
  355. break;
  356. }
  357. }
  358. }
  359. if ($attachNum >= 0) {
  360. $attachment = $this->getAttachmentByAttachNum($message, $attachNum); // get first attachment only
  361. $photoToken = Helper::randomstring(16);
  362. $tmpPhoto = PLUGIN_CONTACTIMPORTER_TMP_UPLOAD . "photo_" . $photoToken . ".jpg";
  363. $this->storeSavedAttachment($tmpPhoto, $attachment);
  364. $vCard->addPhoto($tmpPhoto, true);
  365. unlink($tmpPhoto);
  366. }
  367. }
  368. // write combined vcf
  369. file_put_contents($file, file_get_contents($file) . $vCard->getOutput());
  370. }
  371. } else {
  372. return false;
  373. }
  374. if (count($records) > 0) {
  375. $response['status'] = true;
  376. $response['download_token'] = $token;
  377. // TRANSLATORS: Filename suffix for exported files
  378. $response['filename'] = count($records) . dgettext("plugin_contactimporter", "_contacts.vcf");
  379. } else {
  380. $response['status'] = false;
  381. $response['message'] = dgettext("plugin_contactimporter", "No contacts found. Export skipped!");
  382. }
  383. $this->addActionData($actionType, $response);
  384. $GLOBALS["bus"]->addData($this->getResponseData());
  385. return true;
  386. }
  387. /**
  388. * Returns attachment based on specified attachNum, additionally it will also get embedded message
  389. * if we want to get the inline image attachment.
  390. *
  391. * @param $message
  392. * @param array $attachNum
  393. * @return MAPIAttach embedded message attachment or attachment that is requested
  394. */
  395. private function getAttachmentByAttachNum($message, $attachNum)
  396. {
  397. // open the attachment
  398. $attachment = mapi_message_openattach($message, $attachNum);
  399. return $attachment;
  400. }
  401. /**
  402. * Function will open passed attachment and generate response for that attachment to send it to client.
  403. * This should only be used to download attachment that is already saved in MAPIMessage.
  404. *
  405. * @param MAPIAttach $attachment attachment which will be dumped to client side
  406. * @return Response response to sent to client including attachment data
  407. */
  408. private function storeSavedAttachment($tempPath, $attachment)
  409. {
  410. // Check if the attachment is opened
  411. if ($attachment) {
  412. // Open a stream to get the attachment data
  413. $stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0);
  414. $stat = mapi_stream_stat($stream);
  415. // Read the attachment content from the stream
  416. $body = '';
  417. for ($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) {
  418. $body .= mapi_stream_read($stream, BLOCK_SIZE);
  419. }
  420. file_put_contents($tempPath, $body);
  421. }
  422. }
  423. /**
  424. * Replace String Property Tags
  425. *
  426. * @param $store
  427. * @param $properties
  428. * @return array
  429. */
  430. private function replaceStringPropertyTags($store, $properties)
  431. {
  432. $newProperties = array();
  433. $ids = array("name" => array(), "id" => array(), "guid" => array(), "type" => array()); // this array stores all the information needed to retrieve a named property
  434. $num = 0;
  435. // caching
  436. $guids = array();
  437. foreach ($properties as $name => $val) {
  438. if (is_string($val)) {
  439. $split = explode(":", $val);
  440. if (count($split) != 3) { // invalid string, ignore
  441. trigger_error(sprintf("Invalid property: %s \"%s\"", $name, $val), E_USER_NOTICE);
  442. continue;
  443. }
  444. if (substr($split[2], 0, 2) == "0x") {
  445. $id = hexdec(substr($split[2], 2));
  446. } else {
  447. $id = $split[2];
  448. }
  449. // have we used this guid before?
  450. if (!defined($split[1])) {
  451. if (!array_key_exists($split[1], $guids)) {
  452. $guids[$split[1]] = makeguid($split[1]);
  453. }
  454. $guid = $guids[$split[1]];
  455. } else {
  456. $guid = constant($split[1]);
  457. }
  458. // temp store info about named prop, so we have to call mapi_getidsfromnames just one time
  459. $ids["name"][$num] = $name;
  460. $ids["id"][$num] = $id;
  461. $ids["guid"][$num] = $guid;
  462. $ids["type"][$num] = $split[0];
  463. $num++;
  464. } else {
  465. // not a named property
  466. $newProperties[$name] = $val;
  467. }
  468. }
  469. if (count($ids["id"]) == 0) {
  470. return $newProperties;
  471. }
  472. // get the ids
  473. $named = mapi_getidsfromnames($store, $ids["id"], $ids["guid"]);
  474. foreach ($named as $num => $prop) {
  475. $newProperties[$ids["name"][$num]] = mapi_prop_tag(constant($ids["type"][$num]), mapi_prop_id($prop));
  476. }
  477. return $newProperties;
  478. }
  479. /**
  480. * A simple Property map initialization
  481. *
  482. * @return array the propertyarray
  483. */
  484. private function getProperties()
  485. {
  486. $properties = array();
  487. $properties["subject"] = PR_SUBJECT;
  488. $properties["hide_attachments"] = "PT_BOOLEAN:PSETID_Common:0x851";
  489. $properties["icon_index"] = PR_ICON_INDEX;
  490. $properties["message_class"] = PR_MESSAGE_CLASS;
  491. $properties["display_name"] = PR_DISPLAY_NAME;
  492. $properties["given_name"] = PR_GIVEN_NAME;
  493. $properties["middle_name"] = PR_MIDDLE_NAME;
  494. $properties["surname"] = PR_SURNAME;
  495. $properties["home_telephone_number"] = PR_HOME_TELEPHONE_NUMBER;
  496. $properties["cellular_telephone_number"] = PR_CELLULAR_TELEPHONE_NUMBER;
  497. $properties["office_telephone_number"] = PR_OFFICE_TELEPHONE_NUMBER;
  498. $properties["business_fax_number"] = PR_BUSINESS_FAX_NUMBER;
  499. $properties["company_name"] = PR_COMPANY_NAME;
  500. $properties["title"] = PR_TITLE;
  501. $properties["department_name"] = PR_DEPARTMENT_NAME;
  502. $properties["office_location"] = PR_OFFICE_LOCATION;
  503. $properties["profession"] = PR_PROFESSION;
  504. $properties["manager_name"] = PR_MANAGER_NAME;
  505. $properties["assistant"] = PR_ASSISTANT;
  506. $properties["nickname"] = PR_NICKNAME;
  507. $properties["display_name_prefix"] = PR_DISPLAY_NAME_PREFIX;
  508. $properties["spouse_name"] = PR_SPOUSE_NAME;
  509. $properties["generation"] = PR_GENERATION;
  510. $properties["birthday"] = PR_BIRTHDAY;
  511. $properties["wedding_anniversary"] = PR_WEDDING_ANNIVERSARY;
  512. $properties["sensitivity"] = PR_SENSITIVITY;
  513. $properties["fileas"] = "PT_STRING8:PSETID_Address:0x8005";
  514. $properties["fileas_selection"] = "PT_LONG:PSETID_Address:0x8006";
  515. $properties["email_address_1"] = "PT_STRING8:PSETID_Address:0x8083";
  516. $properties["email_address_display_name_1"] = "PT_STRING8:PSETID_Address:0x8080";
  517. $properties["email_address_display_name_email_1"] = "PT_STRING8:PSETID_Address:0x8084";
  518. $properties["email_address_type_1"] = "PT_STRING8:PSETID_Address:0x8082";
  519. $properties["email_address_2"] = "PT_STRING8:PSETID_Address:0x8093";
  520. $properties["email_address_display_name_2"] = "PT_STRING8:PSETID_Address:0x8090";
  521. $properties["email_address_display_name_email_2"] = "PT_STRING8:PSETID_Address:0x8094";
  522. $properties["email_address_type_2"] = "PT_STRING8:PSETID_Address:0x8092";
  523. $properties["email_address_3"] = "PT_STRING8:PSETID_Address:0x80a3";
  524. $properties["email_address_display_name_3"] = "PT_STRING8:PSETID_Address:0x80a0";
  525. $properties["email_address_display_name_email_3"] = "PT_STRING8:PSETID_Address:0x80a4";
  526. $properties["email_address_type_3"] = "PT_STRING8:PSETID_Address:0x80a2";
  527. $properties["home_address"] = "PT_STRING8:PSETID_Address:0x801a";
  528. $properties["business_address"] = "PT_STRING8:PSETID_Address:0x801b";
  529. $properties["other_address"] = "PT_STRING8:PSETID_Address:0x801c";
  530. $properties["mailing_address"] = "PT_LONG:PSETID_Address:0x8022";
  531. $properties["im"] = "PT_STRING8:PSETID_Address:0x8062";
  532. $properties["webpage"] = "PT_STRING8:PSETID_Address:0x802b";
  533. $properties["business_home_page"] = PR_BUSINESS_HOME_PAGE;
  534. $properties["email_address_entryid_1"] = "PT_BINARY:PSETID_Address:0x8085";
  535. $properties["email_address_entryid_2"] = "PT_BINARY:PSETID_Address:0x8095";
  536. $properties["email_address_entryid_3"] = "PT_BINARY:PSETID_Address:0x80a5";
  537. $properties["address_book_mv"] = "PT_MV_LONG:PSETID_Address:0x8028";
  538. $properties["address_book_long"] = "PT_LONG:PSETID_Address:0x8029";
  539. $properties["oneoff_members"] = "PT_MV_BINARY:PSETID_Address:0x8054";
  540. $properties["members"] = "PT_MV_BINARY:PSETID_Address:0x8055";
  541. $properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506";
  542. $properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
  543. $properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
  544. $properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
  545. $properties["last_modification_time"] = PR_LAST_MODIFICATION_TIME;
  546. // Detailed contacts properties
  547. // Properties for phone numbers
  548. $properties["assistant_telephone_number"] = PR_ASSISTANT_TELEPHONE_NUMBER;
  549. $properties["business2_telephone_number"] = PR_BUSINESS2_TELEPHONE_NUMBER;
  550. $properties["callback_telephone_number"] = PR_CALLBACK_TELEPHONE_NUMBER;
  551. $properties["car_telephone_number"] = PR_CAR_TELEPHONE_NUMBER;
  552. $properties["company_telephone_number"] = PR_COMPANY_MAIN_PHONE_NUMBER;
  553. $properties["home2_telephone_number"] = PR_HOME2_TELEPHONE_NUMBER;
  554. $properties["home_fax_number"] = PR_HOME_FAX_NUMBER;
  555. $properties["isdn_number"] = PR_ISDN_NUMBER;
  556. $properties["other_telephone_number"] = PR_OTHER_TELEPHONE_NUMBER;
  557. $properties["pager_telephone_number"] = PR_PAGER_TELEPHONE_NUMBER;
  558. $properties["primary_fax_number"] = PR_PRIMARY_FAX_NUMBER;
  559. $properties["primary_telephone_number"] = PR_PRIMARY_TELEPHONE_NUMBER;
  560. $properties["radio_telephone_number"] = PR_RADIO_TELEPHONE_NUMBER;
  561. $properties["telex_telephone_number"] = PR_TELEX_NUMBER;
  562. $properties["ttytdd_telephone_number"] = PR_TTYTDD_PHONE_NUMBER;
  563. $properties["business_telephone_number"] = PR_BUSINESS_TELEPHONE_NUMBER;
  564. // Additional fax properties
  565. $properties["fax_1_address_type"] = "PT_STRING8:PSETID_Address:0x80B2";
  566. $properties["fax_1_email_address"] = "PT_STRING8:PSETID_Address:0x80B3";
  567. $properties["fax_1_original_display_name"] = "PT_STRING8:PSETID_Address:0x80B4";
  568. $properties["fax_1_original_entryid"] = "PT_BINARY:PSETID_Address:0x80B5";
  569. $properties["fax_2_address_type"] = "PT_STRING8:PSETID_Address:0x80C2";
  570. $properties["fax_2_email_address"] = "PT_STRING8:PSETID_Address:0x80C3";
  571. $properties["fax_2_original_display_name"] = "PT_STRING8:PSETID_Address:0x80C4";
  572. $properties["fax_2_original_entryid"] = "PT_BINARY:PSETID_Address:0x80C5";
  573. $properties["fax_3_address_type"] = "PT_STRING8:PSETID_Address:0x80D2";
  574. $properties["fax_3_email_address"] = "PT_STRING8:PSETID_Address:0x80D3";
  575. $properties["fax_3_original_display_name"] = "PT_STRING8:PSETID_Address:0x80D4";
  576. $properties["fax_3_original_entryid"] = "PT_BINARY:PSETID_Address:0x80D5";
  577. // Properties for addresses
  578. // Home address
  579. $properties["home_address_street"] = PR_HOME_ADDRESS_STREET;
  580. $properties["home_address_city"] = PR_HOME_ADDRESS_CITY;
  581. $properties["home_address_state"] = PR_HOME_ADDRESS_STATE_OR_PROVINCE;
  582. $properties["home_address_postal_code"] = PR_HOME_ADDRESS_POSTAL_CODE;
  583. $properties["home_address_country"] = PR_HOME_ADDRESS_COUNTRY;
  584. // Other address
  585. $properties["other_address_street"] = PR_OTHER_ADDRESS_STREET;
  586. $properties["other_address_city"] = PR_OTHER_ADDRESS_CITY;
  587. $properties["other_address_state"] = PR_OTHER_ADDRESS_STATE_OR_PROVINCE;
  588. $properties["other_address_postal_code"] = PR_OTHER_ADDRESS_POSTAL_CODE;
  589. $properties["other_address_country"] = PR_OTHER_ADDRESS_COUNTRY;
  590. // Business address
  591. $properties["business_address_street"] = "PT_STRING8:PSETID_Address:0x8045";
  592. $properties["business_address_city"] = "PT_STRING8:PSETID_Address:0x8046";
  593. $properties["business_address_state"] = "PT_STRING8:PSETID_Address:0x8047";
  594. $properties["business_address_postal_code"] = "PT_STRING8:PSETID_Address:0x8048";
  595. $properties["business_address_country"] = "PT_STRING8:PSETID_Address:0x8049";
  596. // Mailing address
  597. $properties["country"] = PR_COUNTRY;
  598. $properties["city"] = PR_LOCALITY;
  599. $properties["postal_address"] = PR_POSTAL_ADDRESS;
  600. $properties["postal_code"] = PR_POSTAL_CODE;
  601. $properties["state"] = PR_STATE_OR_PROVINCE;
  602. $properties["street"] = PR_STREET_ADDRESS;
  603. // Special Date such as birthday n anniversary appoitment's entryid is store
  604. $properties["birthday_eventid"] = "PT_BINARY:PSETID_Address:0x804D";
  605. $properties["anniversary_eventid"] = "PT_BINARY:PSETID_Address:0x804E";
  606. $properties["notes"] = PR_BODY;
  607. // hasimage
  608. $properties["picture"] = "PT_BOOLEAN:{00062004-0000-0000-C000-000000000046}:0x8015";
  609. return $properties;
  610. }
  611. /**
  612. * Function that parses the uploaded vcf file and posts it via json
  613. * @param $actionType
  614. * @param $actionData
  615. */
  616. private function loadContacts($actionType, $actionData)
  617. {
  618. $error = false;
  619. $errorMsg = "";
  620. if (is_readable($actionData["vcf_filepath"])) {
  621. $parser = null;
  622. try {
  623. $parser = VCardParser::parseFromFile($actionData["vcf_filepath"]);
  624. } catch (Exception $e) {
  625. $error = true;
  626. $errorMsg = $e->getMessage();
  627. }
  628. if ($error) {
  629. $response['status'] = false;
  630. $response['message'] = $errorMsg;
  631. } else {
  632. if (iterator_count($parser) == 0) {
  633. $response['status'] = false;
  634. $response['message'] = dgettext("plugin_contactimporter", "No contacts in vcf file");
  635. } else {
  636. $response['status'] = true;
  637. $response['parsed_file'] = $actionData["vcf_filepath"];
  638. $response['parsed'] = array(
  639. 'contacts' => $this->parseContactsToArray($parser)
  640. );
  641. }
  642. }
  643. } else {
  644. $response['status'] = false;
  645. $response['message'] = dgettext("plugin_contactimporter", "File could not be read by server");
  646. }
  647. $this->addActionData($actionType, $response);
  648. $GLOBALS["bus"]->addData($this->getResponseData());
  649. if ($this->DEBUG) {
  650. error_log("parsing done, bus data written!");
  651. }
  652. }
  653. /**
  654. * Create a array with contacts
  655. *
  656. * @param VCard $contacts or csv contacts
  657. * @param bool|optional $csv true if contacts are csv contacts
  658. * @return array parsed contacts
  659. * @private
  660. */
  661. private function parseContactsToArray($contacts, $csv = false)
  662. {
  663. $carr = array();
  664. if (!$csv) {
  665. foreach ($contacts as $Index => $vCard) {
  666. $properties = array();
  667. if (isset($vCard->fullname)) {
  668. $properties["display_name"] = $vCard->fullname;
  669. $properties["fileas"] = $vCard->fullname;
  670. } elseif (!isset($vCard->organization)) {
  671. error_log("Skipping entry! No fullname/organization given.");
  672. continue;
  673. }
  674. $properties["hide_attachments"] = true;
  675. //uid - used for front/backend communication
  676. $properties["internal_fields"] = array();
  677. $properties["internal_fields"]["contact_uid"] = base64_encode($Index . $properties["fileas"]);
  678. $properties["given_name"] = $vCard->firstname;
  679. $properties["middle_name"] = $vCard->additional;
  680. $properties["surname"] = $vCard->lastname;
  681. $properties["display_name_prefix"] = $vCard->prefix;
  682. if (isset($vCard->phone) && count($vCard->phone) > 0) {
  683. foreach ($vCard->phone as $type => $number) {
  684. $number = $number[0]; // we only can store one number
  685. if ($this->startswith(strtolower($type), "home") || strtolower($type) === "default") {
  686. $properties["home_telephone_number"] = $number;
  687. } else {
  688. if ($this->startswith(strtolower($type), "cell")) {
  689. $properties["cellular_telephone_number"] = $number;
  690. } else {
  691. if ($this->startswith(strtolower($type), "work")) {
  692. $properties["business_telephone_number"] = $number;
  693. } else {
  694. if ($this->startswith(strtolower($type), "fax")) {
  695. $properties["business_fax_number"] = $number;
  696. } else {
  697. if ($this->startswith(strtolower($type), "pager")) {
  698. $properties["pager_telephone_number"] = $number;
  699. } else {
  700. if ($this->startswith(strtolower($type), "isdn")) {
  701. $properties["isdn_number"] = $number;
  702. } else {
  703. if ($this->startswith(strtolower($type), "car")) {
  704. $properties["car_telephone_number"] = $number;
  705. } else {
  706. if ($this->startswith(strtolower($type), "modem")) {
  707. $properties["ttytdd_telephone_number"] = $number;
  708. }
  709. }
  710. }
  711. }
  712. }
  713. }
  714. }
  715. }
  716. }
  717. }
  718. if (isset($vCard->email) && count($vCard->email) > 0) {
  719. $emailCount = 0;
  720. $properties["address_book_long"] = 0;
  721. foreach ($vCard->email as $type => $email) {
  722. foreach ($email as $mail) {
  723. $fileAs = $mail;
  724. if (isset($properties["fileas"]) && !empty($properties["fileas"])) {
  725. $fileAs = $properties["fileas"]; // set to real name
  726. }
  727. // we only have storage for 3 mail addresses!
  728. /**
  729. * type of email address address_book_mv address_book_long
  730. * email1 0 1 (0x00000001)
  731. * email2 1 2 (0x00000002)
  732. * email3 2 4 (0x00000004)
  733. * fax2(business fax) 3 8 (0x00000008)
  734. * fax3(home fax) 4 16 (0x00000010)
  735. * fax1(primary fax) 5 32 (0x00000020)
  736. *
  737. * address_book_mv is a multivalued property so all the values are passed in array
  738. * address_book_long stores sum of the flags
  739. * these both properties should be in sync always
  740. */
  741. switch ($emailCount) {
  742. case 0:
  743. $properties["email_address_1"] = $mail;
  744. $properties["email_address_display_name_1"] = $fileAs . " (" . $mail . ")";
  745. $properties["email_address_display_name_email_1"] = $mail;
  746. $properties["address_book_mv"][] = 0; // this is needed for adding the contact to the email address book, 0 = email 1
  747. $properties["address_book_long"] += 1; // this specifies the number of elements in address_book_mv
  748. break;
  749. case 1:
  750. $properties["email_address_2"] = $mail;
  751. $properties["email_address_display_name_2"] = $fileAs . " (" . $mail . ")";
  752. $properties["email_address_display_name_email_2"] = $mail;
  753. $properties["address_book_mv"][] = 1; // this is needed for adding the contact to the email address book, 1 = email 2
  754. $properties["address_book_long"] += 2; // this specifies the number of elements in address_book_mv
  755. break;
  756. case 2:
  757. $properties["email_address_3"] = $mail;
  758. $properties["email_address_display_name_3"] = $fileAs . " (" . $mail . ")";
  759. $properties["email_address_display_name_email_3"] = $mail;
  760. $properties["address_book_mv"][] = 2; // this is needed for adding the contact to the email address book, 2 = email 3
  761. $properties["address_book_long"] += 4; // this specifies the number of elements in address_book_mv
  762. break;
  763. default:
  764. break;
  765. }
  766. $emailCount++;
  767. }
  768. }
  769. }
  770. if (isset($vCard->organization)) {
  771. $properties["company_name"] = $vCard->organization;
  772. if (empty($properties["display_name"])) {
  773. $properties["display_name"] = $vCard->organization; // if we have no displayname - use the company name as displayname
  774. $properties["fileas"] = $vCard->organization;
  775. }
  776. }
  777. if (isset($vCard->department)) {
  778. $properties["department_name"] = $vCard->department;
  779. }
  780. if (isset($vCard->officelocation)) {
  781. $properties["office_location"] = $vCard->officelocation;
  782. }
  783. if (isset($vCard->title)) {
  784. $properties["title"] = $vCard->title;
  785. }
  786. if (isset($vCard->url) && count($vCard->url) > 0) {
  787. foreach ($vCard->url as $type => $url) {
  788. $url = $url[0]; // only 1 webaddress per type
  789. $properties["webpage"] = $url;
  790. break; // we can only store on url
  791. }
  792. }
  793. if (isset($vCard->address) && count($vCard->address) > 0) {
  794. foreach ($vCard->address as $type => $address) {
  795. $address = $address[0]; // we only can store one address per type
  796. if ($this->startswith(strtolower($type), "work")) {
  797. $properties["business_address_street"] = $address->street;
  798. if (!empty($address->extended)) {
  799. $properties["business_address_street"] .= "\n" . $address->extended;
  800. }
  801. $properties["business_address_city"] = $address->city;
  802. $properties["business_address_state"] = $address->region;
  803. $properties["business_address_postal_code"] = $address->zip;
  804. $properties["business_address_country"] = $address->country;
  805. $properties["business_address"] = $this->buildAddressString($properties["business_address_street"], $address->zip, $address->city, $address->region, $address->country);
  806. } else {
  807. if ($this->startswith(strtolower($type), "home")) {
  808. $properties["home_address_street"] = $address->street;
  809. if (!empty($address->extended)) {
  810. $properties["home_address_street"] .= "\n" . $address->extended;
  811. }
  812. $properties["home_address_city"] = $address->city;
  813. $properties["home_address_state"] = $address->region;
  814. $properties["home_address_postal_code"] = $address->zip;
  815. $properties["home_address_country"] = $address->country;
  816. $properties["home_address"] = $this->buildAddressString($properties["home_address_street"], $address->zip, $address->city, $address->region, $address->country);
  817. } else {
  818. $properties["other_address_street"] = $address->street;
  819. if (!empty($address->extended)) {
  820. $properties["other_address_street"] .= "\n" . $address->extended;
  821. }
  822. $properties["other_address_city"] = $address->city;
  823. $properties["other_address_state"] = $address->region;
  824. $properties["other_address_postal_code"] = $address->zip;
  825. $properties["other_address_country"] = $address->country;
  826. $properties["other_address"] = $this->buildAddressString($properties["other_address_street"], $address->zip, $address->city, $address->region, $address->country);
  827. }
  828. }
  829. }
  830. }
  831. if (isset($vCard->birthday)) {
  832. $properties["birthday"] = $vCard->birthday->getTimestamp();
  833. }
  834. if (isset($vCard->note)) {
  835. $properties["notes"] = $vCard->note;
  836. }
  837. if (isset($vCard->x["msassistant"])) {
  838. $properties["assistant"] = $this->unescapeVCFValue($vCard->x["msassistant"]);
  839. }
  840. if (isset($vCard->x["msmanager"])) {
  841. $properties["manager_name"] = $this->unescapeVCFValue($vCard->x["msmanager"]);
  842. }
  843. if (isset($vCard->x["msspouse"])) {
  844. $properties["spouse_name"] = $this->unescapeVCFValue($vCard->x["msspouse"]);
  845. }
  846. if (isset($vCard->categories) && count($vCard->categories) > 0) {
  847. $categories = array();
  848. foreach ($vCard->categories as $category) {
  849. $categories[] = $category;
  850. }
  851. $properties["categories"] = $categories;
  852. }
  853. if (isset($vCard->rawPhoto) || isset($vCard->photo)) {
  854. if (!is_writable(TMP_PATH . "/")) {
  855. error_log("Can not write to export tmp directory!");
  856. } else {
  857. $tmppath = TMP_PATH . "/" . Helper::randomstring(15);
  858. if (isset($vCard->rawPhoto)) {
  859. if (file_put_contents($tmppath, $vCard->rawPhoto)) {
  860. $properties["internal_fields"]["x_photo_path"] = $tmppath;
  861. }
  862. } elseif (isset($vCard->photo)) {
  863. if ($this->startswith(strtolower($vCard->photo), "http://") || $this->startswith(strtolower($vCard->photo), "https://")) { // check if it starts with http
  864. $ctx = stream_context_create(array('http' =>
  865. array(
  866. 'timeout' => 3, //3 Seconds timout
  867. )
  868. ));
  869. if (file_put_contents($tmppath, file_get_contents($vCard->photo, false, $ctx))) {
  870. $properties["internal_fields"]["x_photo_path"] = $tmppath;
  871. }
  872. } else {
  873. error_log("Invalid photo url: " . $vCard->photo);
  874. }
  875. }
  876. }
  877. }
  878. array_push($carr, $properties);
  879. }
  880. } else {
  881. error_log("csv parsing not implemented");
  882. }
  883. return $carr;
  884. }
  885. /**
  886. * Generate the whole address string
  887. *
  888. * @param street
  889. * @param zip
  890. * @param city
  891. * @param state
  892. * @param country
  893. * @return string the concatenated address string
  894. * @private
  895. */
  896. private function buildAddressString($street, $zip, $city, $state, $country)
  897. {
  898. $out = "";
  899. if (isset($country) && $street != "") {
  900. $out = $country;
  901. }
  902. $zcs = "";
  903. if (isset($zip) && $zip != "") {
  904. $zcs = $zip;
  905. }
  906. if (isset($city) && $city != "") {
  907. $zcs .= (($zcs) ? " " : "") . $city;
  908. }
  909. if (isset($state) && $state != "") {
  910. $zcs .= (($zcs) ? " " : "") . $state;
  911. }
  912. if ($zcs) {
  913. $out = $zcs . "\n" . $out;
  914. }
  915. if (isset($street) && $street != "") {
  916. $out = $street . (($out) ? "\n\n" . $out : "");
  917. }
  918. return $out;
  919. }
  920. /**
  921. * Store the file to a temporary directory
  922. *
  923. * @param $actionType
  924. * @param $actionData
  925. * @private
  926. */
  927. private function getAttachmentPath($actionType, $actionData)
  928. {
  929. // Get store id
  930. $storeId = false;
  931. if (isset($actionData["store"])) {
  932. $storeId = $actionData["store"];
  933. }
  934. // Get message entryid
  935. $entryId = false;
  936. if (isset($actionData["entryid"])) {
  937. $entryId = $actionData["entryid"];
  938. }
  939. // Check which type isset
  940. $openType = "attachment";
  941. // Get number of attachment which should be opened.
  942. $attachNum = false;
  943. if (isset($actionData["attachNum"])) {
  944. $attachNum = $actionData["attachNum"];
  945. }
  946. // Check if storeid and entryid isset
  947. if ($storeId && $entryId) {
  948. // Open the store
  949. $store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeId));
  950. if ($store) {
  951. // Open the message
  952. $message = mapi_msgstore_openentry($store, hex2bin($entryId));
  953. if ($message) {
  954. $attachment = false;
  955. // Check if attachNum isset
  956. if ($attachNum) {
  957. // Loop through the attachNums, message in message in message ...
  958. for ($i = 0; $i < (count($attachNum) - 1); $i++) {
  959. // Open the attachment
  960. $tempAttach = mapi_message_openattach($message, (int)$attachNum[$i]);
  961. if ($tempAttach) {
  962. // Open the object in the attachment
  963. $message = mapi_attach_openobj($tempAttach);
  964. }
  965. }
  966. // Open the attachment
  967. $attachment = mapi_message_openattach($message, (int)$attachNum[(count($attachNum) - 1)]);
  968. }
  969. // Check if the attachment is opened
  970. if ($attachment) {
  971. // Get the props of the attachment
  972. $props = mapi_attach_getprops($attachment, array(PR_ATTACH_LONG_FILENAME, PR_ATTACH_MIME_TAG, PR_DISPLAY_NAME, PR_ATTACH_METHOD));
  973. // Content Type
  974. $contentType = "application/octet-stream";
  975. // Filename
  976. $filename = "ERROR";
  977. // Set filename
  978. if (isset($props[PR_ATTACH_LONG_FILENAME])) {
  979. $filename = $props[PR_ATTACH_LONG_FILENAME];
  980. } else {
  981. if (isset($props[PR_ATTACH_FILENAME])) {
  982. $filename = $props[PR_ATTACH_FILENAME];
  983. } else {
  984. if (isset($props[PR_DISPLAY_NAME])) {
  985. $filename = $props[PR_DISPLAY_NAME];
  986. }
  987. }
  988. }
  989. // Set content type
  990. if (isset($props[PR_ATTACH_MIME_TAG])) {
  991. $contentType = $props[PR_ATTACH_MIME_TAG];
  992. } else {
  993. // Parse the extension of the filename to get the content type
  994. if (strrpos($filename, ".") !== false) {
  995. $extension = strtolower(substr($filename, strrpos($filename, ".")));
  996. $contentType = "application/octet-stream";
  997. if (is_readable("mimetypes.dat")) {
  998. $fh = fopen("mimetypes.dat", "r");
  999. $ext_found = false;
  1000. while (!feof($fh) && !$ext_found) {
  1001. $line = fgets($fh);
  1002. preg_match('/(\.[a-z0-9]+)[ \t]+([^ \t\n\r]*)/i', $line, $result);
  1003. if ($extension == $result[1]) {
  1004. $ext_found = true;
  1005. $contentType = $result[2];
  1006. }
  1007. }
  1008. fclose($fh);
  1009. }
  1010. }
  1011. }
  1012. $tmpName = tempnam(TMP_PATH, stripslashes($filename));
  1013. // Open a stream to get the attachment data
  1014. $stream = mapi_openpropertytostream($attachment, PR_ATTACH_DATA_BIN);
  1015. $stat = mapi_stream_stat($stream);
  1016. // File length = $stat["cb"]
  1017. $fHandle = fopen($tmpName, 'w');
  1018. $buffer = null;
  1019. for ($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) {
  1020. // Write stream
  1021. $buffer = mapi_stream_read($stream, BLOCK_SIZE);
  1022. fwrite($fHandle, $buffer, strlen($buffer));
  1023. }
  1024. fclose($fHandle);
  1025. $response = array();
  1026. $response['tmpname'] = $tmpName;
  1027. $response['filename'] = $filename;
  1028. $response['contenttype'] = $contentType;
  1029. $response['status'] = true;
  1030. $this->addActionData($actionType, $response);
  1031. $GLOBALS["bus"]->addData($this->getResponseData());
  1032. }
  1033. }
  1034. } else {
  1035. $response['status'] = false;
  1036. $response['message'] = dgettext("plugin_contactimporter", "Store could not be opened!");
  1037. $this->addActionData($actionType, $response);
  1038. $GLOBALS["bus"]->addData($this->getResponseData());
  1039. }
  1040. } else {
  1041. $response['status'] = false;
  1042. $response['message'] = dgettext("plugin_contactimporter", "Wrong call, store and entryid have to be set!");
  1043. $this->addActionData($actionType, $response);
  1044. $GLOBALS["bus"]->addData($this->getResponseData());
  1045. }
  1046. }
  1047. /**
  1048. * Check if string starts with other string.
  1049. *
  1050. * @param $haystack
  1051. * @param $needle
  1052. * @return bool
  1053. */
  1054. private function startswith($haystack, $needle)
  1055. {
  1056. $haystack = str_replace("type=", "", $haystack); // remove type from string
  1057. return substr($haystack, 0, strlen($needle)) === $needle;
  1058. }
  1059. /**
  1060. * Unescape newline characters according to rfc6350 section 3.4.
  1061. * This function will replace:
  1062. * - escaped line breaks with PHP_EOL
  1063. * - escaped commas by ascii comma
  1064. * - escaped semicolon by ascii semicolon
  1065. *
  1066. * @link https://tools.ietf.org/html/rfc6350#section-3.4
  1067. * @param string $text
  1068. * @return string
  1069. */
  1070. private function unescapeVCFValue($text)
  1071. {
  1072. $unescaped = str_replace("\\n", PHP_EOL, $text);
  1073. $unescaped = str_replace("\\;", ";", $unescaped);
  1074. $unescaped = str_replace("\\,", ",", $unescaped);
  1075. return $unescaped;
  1076. }
  1077. }