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.

1165 lines
53KB

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