From 2837126d6032cac1eddf2725f2e065dda0475c93 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 25 Jul 2022 22:55:48 +0200 Subject: [PATCH] Get AddressBook Import/Export working --- dev/Model/Contact.js | 63 +++++ dev/View/Popup/Contacts.js | 2 - plugins/kolab/KolabAddressBook.php | 13 +- .../Providers/AddressBook/PdoAddressBook.php | 10 +- .../RainLoop/Providers/AddressBook/Utils.php | 234 ++++++++---------- .../templates/Views/User/PopupsContacts.html | 24 +- 6 files changed, 200 insertions(+), 146 deletions(-) diff --git a/dev/Model/Contact.js b/dev/Model/Contact.js index 5ca106054..4b1147f9a 100644 --- a/dev/Model/Contact.js +++ b/dev/Model/Contact.js @@ -11,6 +11,69 @@ const nProps = [ 'nameSuffix' ]; +/* +const propertyMap = [ + // vCard 2.1 properties and up + 'N' => 'Text', + 'FN' => 'FlatText', + 'PHOTO' => 'Binary', + 'BDAY' => 'DateAndOrTime', + 'ADR' => 'Text', + 'TEL' => 'FlatText', + 'EMAIL' => 'FlatText', + 'GEO' => 'FlatText', + 'TITLE' => 'FlatText', + 'ROLE' => 'FlatText', + 'LOGO' => 'Binary', + 'ORG' => 'Text', + 'NOTE' => 'FlatText', + 'REV' => 'TimeStamp', + 'SOUND' => 'FlatText', + 'URL' => 'Uri', + 'UID' => 'FlatText', + 'VERSION' => 'FlatText', + 'KEY' => 'FlatText', + 'TZ' => 'Text', + + // vCard 3.0 properties + 'CATEGORIES' => 'Text', + 'SORT-STRING' => 'FlatText', + 'PRODID' => 'FlatText', + 'NICKNAME' => 'Text', + + // rfc2739 properties + 'FBURL' => 'Uri', + 'CAPURI' => 'Uri', + 'CALURI' => 'Uri', + 'CALADRURI' => 'Uri', + + // rfc4770 properties + 'IMPP' => 'Uri', + + // vCard 4.0 properties + 'SOURCE' => 'Uri', + 'XML' => 'FlatText', + 'ANNIVERSARY' => 'DateAndOrTime', + 'CLIENTPIDMAP' => 'Text', + 'LANG' => 'LanguageTag', + 'GENDER' => 'Text', + 'KIND' => 'FlatText', + 'MEMBER' => 'Uri', + 'RELATED' => 'Uri', + + // rfc6474 properties + 'BIRTHPLACE' => 'FlatText', + 'DEATHPLACE' => 'FlatText', + 'DEATHDATE' => 'DateAndOrTime', + + // rfc6715 properties + 'EXPERTISE' => 'FlatText', + 'HOBBY' => 'FlatText', + 'INTEREST' => 'FlatText', + 'ORG-DIRECTORY' => 'FlatText +]; +*/ + export class ContactModel extends AbstractModel { constructor() { super(); diff --git a/dev/View/Popup/Contacts.js b/dev/View/Popup/Contacts.js index 131a92988..9f1d81ded 100644 --- a/dev/View/Popup/Contacts.js +++ b/dev/View/Popup/Contacts.js @@ -1,5 +1,3 @@ -import { koArrayWithDestroy } from 'External/ko'; - import { ComposeType } from 'Common/EnumsUser'; import { registerShortcut } from 'Common/Globals'; import { arrayLength, pInt } from 'Common/Utils'; diff --git a/plugins/kolab/KolabAddressBook.php b/plugins/kolab/KolabAddressBook.php index 5318de38c..e9f721ef7 100644 --- a/plugins/kolab/KolabAddressBook.php +++ b/plugins/kolab/KolabAddressBook.php @@ -116,7 +116,7 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt public function Export(string $sType = 'vcf') : bool { - $bVcf = 'vcf' === $sType; + $rCsv = 'csv' === $sType ? \fopen('php://output', 'w') : null; $bCsvHeader = true; if (!\strlen($this->sFolderName)) { @@ -134,15 +134,12 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt $oParams->iLimit = 999; // Is the max $oMessageList = $this->MailClient()->MessageList($oParams); foreach ($oMessageList as $oMessage) { - if ($bVcf) { - $xCard = $this->fetchXCardFromMessage($oMessage); - if ($xCard) { - echo $xCard->serialize(); - } - } else { + if ($rCsv) { $oContact = $this->MessageAsContact($oMessage); - echo \RainLoop\Providers\AddressBook\Utils::VCardToCsv($oContact, $bCsvHeader); + \RainLoop\Providers\AddressBook\Utils::VCardToCsv($rCsv, $oContact, $bCsvHeader); $bCsvHeader = false; + } else if ($xCard = $this->fetchXCardFromMessage($oMessage)) { + echo $xCard->serialize(); } } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php index 363ce0358..e34d732d8 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php @@ -302,7 +302,7 @@ class PdoAddressBook return false; } - $bVcf = 'vcf' === $sType; + $rCsv = 'csv' === $sType ? \fopen('php://output', 'w') : null; $bCsvHeader = true; $aDatabaseSyncData = $this->prepareDatabaseSyncData(); @@ -311,11 +311,11 @@ class PdoAddressBook if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) { $oContact = $this->GetContactByID($mData['id_contact']); if ($oContact) { - if ($bVcf) { - echo $oContact->vCard->serialize(); - } else { - echo Utils::VCardToCsv($oContact, $bCsvHeader); + if ($rCsv) { + Utils::VCardToCsv($rCsv, $oContact, $bCsvHeader); $bCsvHeader = false; + } else { + echo $oContact->vCard->serialize(); } } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php index 5893f91f4..8dbde7d2a 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php @@ -110,136 +110,129 @@ class Utils } } - private static function csvNameToTypeConvertor(string $sCsvName) : int - { - static $aMap = null; - if (null === $aMap) - { - $aMap = array( - 'Title' => PropertyType::FULLNAME, - 'Name' => PropertyType::FULLNAME, - 'FullName' => PropertyType::FULLNAME, - 'DisplayName' => PropertyType::FULLNAME, - 'GivenName' => PropertyType::FIRST_NAME, - 'First' => PropertyType::FIRST_NAME, - 'FirstName' => PropertyType::FIRST_NAME, - 'Middle' => PropertyType::MIDDLE_NAME, - 'MiddleName' => PropertyType::MIDDLE_NAME, - 'Last' => PropertyType::LAST_NAME, - 'LastName' => PropertyType::LAST_NAME, - 'Suffix' => PropertyType::NAME_SUFFIX, - 'NameSuffix' => PropertyType::NAME_SUFFIX, - 'Prefix' => PropertyType::NAME_PREFIX, - 'NamePrefix' => PropertyType::NAME_PREFIX, - 'ShortName' => PropertyType::NICK_NAME, - 'NickName' => PropertyType::NICK_NAME, - 'BusinessFax' => array(PropertyType::PHONE, 'Work,Fax'), - 'BusinessFax2' => array(PropertyType::PHONE, 'Work,Fax'), - 'BusinessFax3' => array(PropertyType::PHONE, 'Work,Fax'), - 'BusinessPhone' => array(PropertyType::PHONE, 'Work'), - 'BusinessPhone2' => array(PropertyType::PHONE, 'Work'), - 'BusinessPhone3' => array(PropertyType::PHONE, 'Work'), - 'CompanyPhone' => array(PropertyType::PHONE, 'Work'), - 'CompanyMainPhone' => array(PropertyType::PHONE, 'Work'), - 'HomeFax' => array(PropertyType::PHONE, 'Home,Fax'), - 'HomeFax2' => array(PropertyType::PHONE, 'Home,Fax'), - 'HomeFax3' => array(PropertyType::PHONE, 'Home,Fax'), - 'HomePhone' => array(PropertyType::PHONE, 'Home'), - 'HomePhone2' => array(PropertyType::PHONE, 'Home'), - 'HomePhone3' => array(PropertyType::PHONE, 'Home'), - 'Mobile' => array(PropertyType::PHONE, 'Mobile'), - 'MobilePhone' => array(PropertyType::PHONE, 'Mobile'), - 'BusinessMobile' => array(PropertyType::PHONE, 'Work,Mobile'), - 'BusinessMobilePhone' => array(PropertyType::PHONE, 'Work,Mobile'), - 'OtherFax' => array(PropertyType::PHONE, 'Other,Fax'), - 'OtherPhone' => array(PropertyType::PHONE, 'Other'), - 'PrimaryPhone' => array(PropertyType::PHONE, 'Pref,Home'), - 'Email' => array(PropertyType::EMAIl, 'Home'), - 'Email2' => array(PropertyType::EMAIl, 'Home'), - 'Email3' => array(PropertyType::EMAIl, 'Home'), - 'HomeEmail' => array(PropertyType::EMAIl, 'Home'), - 'HomeEmail2' => array(PropertyType::EMAIl, 'Home'), - 'HomeEmail3' => array(PropertyType::EMAIl, 'Home'), - 'PrimaryEmail' => array(PropertyType::EMAIl, 'Home'), - 'PrimaryEmail2' => array(PropertyType::EMAIl, 'Home'), - 'PrimaryEmail3' => array(PropertyType::EMAIl, 'Home'), - 'EmailAddress' => array(PropertyType::EMAIl, 'Home'), - 'Email2Address' => array(PropertyType::EMAIl, 'Home'), - 'Email3Address' => array(PropertyType::EMAIl, 'Home'), - 'OtherEmail' => array(PropertyType::EMAIl, 'Other'), - 'BusinessEmail' => array(PropertyType::EMAIl, 'Work'), - 'BusinessEmail2' => array(PropertyType::EMAIl, 'Work'), - 'BusinessEmail3' => array(PropertyType::EMAIl, 'Work'), - 'PersonalEmail' => array(PropertyType::EMAIl, 'Home'), - 'PersonalEmail2' => array(PropertyType::EMAIl, 'Home'), - 'PersonalEmail3' => array(PropertyType::EMAIl, 'Home'), - 'Notes' => PropertyType::NOTE, - 'Web' => PropertyType::WEB_PAGE, - 'BusinessWeb' => array(PropertyType::WEB_PAGE, 'Work'), - 'WebPage' => PropertyType::WEB_PAGE, - 'BusinessWebPage' => array(PropertyType::WEB_PAGE, 'Work'), - 'WebSite' => PropertyType::WEB_PAGE, - 'BusinessWebSite' => array(PropertyType::WEB_PAGE, 'Work'), - 'PersonalWebSite' => PropertyType::WEB_PAGE - ); + private static $aMap = array( + 'title' => 'FN', + 'name' => 'FN', + 'fullname' => 'FN', + 'displayname' => 'FN', + 'last' => 0, + 'lastname' => 0, + 'givenname' => 1, + 'first' => 1, + 'firstname' => 1, + 'middle' => 2, + 'middlename' => 2, + 'prefix' => 3, + 'nameprefix' => 3, + 'suffix' => 4, + 'namesuffix' => 4, + 'shortname' => 'NICKNAME', + 'nickname' => 'NICKNAME', + 'businessphone' => array('TEL', 'WORK'), + 'businessphone2' => array('TEL', 'WORK'), + 'businessphone3' => array('TEL', 'WORK'), + 'companyphone' => array('TEL', 'WORK'), + 'companymainphone' => array('TEL', 'WORK'), + 'homephone' => array('TEL', 'HOME'), + 'homephone2' => array('TEL', 'HOME'), + 'homephone3' => array('TEL', 'HOME'), + 'mobile' => array('TEL', 'CELL'), + 'mobilephone' => array('TEL', 'CELL'), + 'businessmobile' => array('TEL', 'WORK,CELL'), + 'businessmobilephone' => array('TEL', 'WORK,CELL'), + 'otherphone' => 'TEL', + 'primaryphone' => array('TEL', 'PREF,HOME'), + 'email' => array('EMAIL', 'HOME'), + 'email2' => array('EMAIL', 'HOME'), + 'email3' => array('EMAIL', 'HOME'), + 'homeemail' => array('EMAIL', 'HOME'), + 'homeemail2' => array('EMAIL', 'HOME'), + 'homeemail3' => array('EMAIL', 'HOME'), + 'primaryemail' => array('EMAIL', 'HOME'), + 'primaryemail2' => array('EMAIL', 'HOME'), + 'primaryemail3' => array('EMAIL', 'HOME'), + 'emailaddress' => array('EMAIL', 'HOME'), + 'email2address' => array('EMAIL', 'HOME'), + 'email3address' => array('EMAIL', 'HOME'), + 'otheremail' => 'EMAIL', + 'businessemail' => array('EMAIL', 'WORK'), + 'businessemail2' => array('EMAIL', 'WORK'), + 'businessemail3' => array('EMAIL', 'WORK'), + 'personalemail' => array('EMAIL', 'HOME'), + 'personalemail2' => array('EMAIL', 'HOME'), + 'personalemail3' => array('EMAIL', 'HOME'), + 'notes' => 'NOTE', + 'web' => 'URL', + 'businessweb' => array('URL', 'WORK'), + 'webpage' => 'URL', + 'businesswebpage' => array('URL', 'WORK'), + 'website' => 'URL', + 'businesswebsite' => array('URL', 'WORK'), + 'personalwebsite' => 'URL', + 'birthday' => 'BDAY' + ); - $aMap = \array_change_key_case($aMap, CASE_LOWER); - } - - $sCsvNameLower = \MailSo\Base\Utils::IsAscii($sCsvName) ? \preg_replace('/[\s\-]+/', '', \strtolower($sCsvName)) : ''; - return !empty($sCsvNameLower) && isset($aMap[$sCsvNameLower]) ? $aMap[$sCsvNameLower] : PropertyType::UNKNOWN; - } - - /** - * TODO: broken - */ public static function CsvArrayToContacts(array $aCsvData) : iterable { foreach ($aCsvData as $aItem) { - $oContact = new Classes\Contact(); \MailSo\Base\Utils::ResetTimeLimit(); + $iCount = 0; + $oVCard = new \Sabre\VObject\Component\VCard; + $aName = ['','','','','']; foreach ($aItem as $sItemName => $sItemValue) { - $sItemName = \trim($sItemName); + $sItemName = \strtoupper(\trim(\preg_replace('/[\s\-]+/', '', $sItemName))); $sItemValue = \trim($sItemValue); if (!empty($sItemName) && !empty($sItemValue)) { - $mData = static::csvNameToTypeConvertor($sItemName); - $iType = \is_array($mData) ? $mData[0] : $mData; - if (PropertyType::UNKNOWN !== $iType) { - $oProp = new Classes\Property(); - $oProp->Type = $iType; - $oProp->Value = $sItemValue; - $oProp->TypeStr = \is_array($mData) && !empty($mData[1]) ? $mData[1] : ''; -// $oContact->Properties[] = $oProp; + if (\array_key_exists($sItemName, \Sabre\VObject\Component\VCard::$propertyMap)) { + $mData = $sItemName; + } else { + $sItemName = \strtolower($sItemName); + $mData = !empty($sItemName) && isset($aMap[$sItemName]) ? $aMap[$sItemName] : null; + } + if ($mData) { + $mType = \is_array($mData) ? $mData[0] : $mData; + ++$iCount; + if (\is_int($mType)) { + $aName[$mType] = $sItemValue; + } else if (\is_array($mData)) { + $oVCard->add($mType, $sItemValue, ['type' => $mData[1]]); + } else if ('FN' === $mType || 'NICKNAME' === $mType) { + $oVCard->$mType = $sItemValue; + } else { + $oVCard->add($mType, $sItemValue); + } } } } - if (\count($oContact->Properties)) { + if ($iCount) { + if ('' !== \implode('', $aName)) { + $oVCard->N = $aName; + } + $oContact = new Classes\Contact(); + $oContact->setVCard($oVCard); yield $oContact; } } } - /** - * TODO: broken - */ - public static function VCardToCsv(Classes\Contact $oContact, bool $bWithHeader = false) : string + public static function VCardToCsv($stream, Classes\Contact $oContact, bool $bWithHeader = false)/* : int|false*/ { $aData = array(); if ($bWithHeader) { - $aData[] = array( + \fputcsv($stream, array( 'Title', 'First Name', 'Middle Name', 'Last Name', 'Nick Name', 'Display Name', 'Company', 'Department', 'Job Title', 'Office Location', - 'E-mail Address', 'Notes', 'Web Page', 'Birthday', - 'Other Email', 'Other Phone', 'Other Mobile', 'Mobile Phone', - 'Home Email', 'Home Phone', 'Home Fax', - 'Home Street', 'Home City', 'Home State', 'Home Postal Code', 'Home Country', - 'Business Email', 'Business Phone', 'Business Fax', - 'Business Street', 'Business City', 'Business State', 'Business Postal Code', 'Business Country' - ); + 'E-mail Address', 'Notes', 'Web Page', 'Birthday', 'Mobile Phone', + 'Home Email', 'Home Phone', + 'Home Street', 'Home City', 'Home State', 'Home Postal Code', 'Home Country', + 'Business Email', 'Business Phone', + 'Business Street', 'Business City', 'Business State', 'Business Postal Code', 'Business Country' + )); } $oVCard = $oContact->vCard; + $aName = isset($oVCard->N) ? $oVCard->N->getParts() : ['','','','','']; $adrHome = $oVCard->getByType('ADR', 'HOME'); $adrHome = $adrHome ? $adrHome->getParts() : ['','','','','','','']; @@ -247,11 +240,11 @@ class Utils $adrWork = $oVCard->getByType('ADR', 'WORK'); $adrWork = $adrWork ? $adrWork->getParts() : ['','','','','','','']; - $aValues = array( - '', // Title - '', // First Name - '', // Middle Name - '', // Last Name + return \fputcsv($stream, array( + (string) $oVCard->FN, // Title + $aName[1], // First Name + $aName[2], // Middle Name + $aName[0], // Last Name (string) $oVCard->NICKNAME, // Nick Name (string) $oVCard->FN, // Display Name (string) $oVCard->ORG, // Company @@ -261,15 +254,11 @@ class Utils (string) $oVCard->EMAIL, // E-mail Address (string) $oVCard->NOTE, // Notes (string) $oVCard->URL, // Web Page - '', // Birthday - '', // Other Email - '', // Other Phone - '', // Other Mobile + (string) $oVCard->BDAY, // Birthday (string) $oVCard->getByType('TEL', 'CELL'), // Mobile Phone // Home (string) $oVCard->getByType('EMAIL', 'HOME'), // Email (string) $oVCard->getByType('TEL', 'HOME'), // Phone - '', // Fax, \trim($adrHome[1]."\n".$adrHome[2]), // extended address + street address $adrHome[3], // City $adrHome[4], // State @@ -278,25 +267,12 @@ class Utils // Business (string) $oVCard->getByType('EMAIL', 'WORK'), // Email (string) $oVCard->getByType('TEL', 'WORK'), // Phone - '', // Fax \trim($adrWork[1]."\n".$adrWork[2]), // extended address + street address $adrWork[3], // City $adrWork[4], // State $adrWork[5], // Postal Code $adrWork[6] // Country - ); - - $aData[] = \array_map(function ($sValue) { - $sValue = \trim($sValue); - return \preg_match('/[\r\n,"]/', $sValue) ? '"'.\str_replace('"', '""', $sValue).'"' : $sValue; - }, $aValues); - - $sResult = ''; - foreach ($aData as $aSubData) { - $sResult .= \implode(',', $aSubData)."\r\n"; - } - - return $sResult; + )); } public static function VcfFileToContacts(string $sVcfData) : iterable diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsContacts.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsContacts.html index 4a71f7135..e8f1dc909 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsContacts.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsContacts.html @@ -139,6 +139,26 @@ autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="value: surName" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_LAST_NAME"> +