Get AddressBook Import/Export working

This commit is contained in:
the-djmaze 2022-07-25 22:55:48 +02:00
parent 859a18470e
commit 2837126d60
6 changed files with 200 additions and 146 deletions

View file

@ -11,6 +11,69 @@ const nProps = [
'nameSuffix' '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 { export class ContactModel extends AbstractModel {
constructor() { constructor() {
super(); super();

View file

@ -1,5 +1,3 @@
import { koArrayWithDestroy } from 'External/ko';
import { ComposeType } from 'Common/EnumsUser'; import { ComposeType } from 'Common/EnumsUser';
import { registerShortcut } from 'Common/Globals'; import { registerShortcut } from 'Common/Globals';
import { arrayLength, pInt } from 'Common/Utils'; import { arrayLength, pInt } from 'Common/Utils';

View file

@ -116,7 +116,7 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
public function Export(string $sType = 'vcf') : bool public function Export(string $sType = 'vcf') : bool
{ {
$bVcf = 'vcf' === $sType; $rCsv = 'csv' === $sType ? \fopen('php://output', 'w') : null;
$bCsvHeader = true; $bCsvHeader = true;
if (!\strlen($this->sFolderName)) { if (!\strlen($this->sFolderName)) {
@ -134,15 +134,12 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
$oParams->iLimit = 999; // Is the max $oParams->iLimit = 999; // Is the max
$oMessageList = $this->MailClient()->MessageList($oParams); $oMessageList = $this->MailClient()->MessageList($oParams);
foreach ($oMessageList as $oMessage) { foreach ($oMessageList as $oMessage) {
if ($bVcf) { if ($rCsv) {
$xCard = $this->fetchXCardFromMessage($oMessage);
if ($xCard) {
echo $xCard->serialize();
}
} else {
$oContact = $this->MessageAsContact($oMessage); $oContact = $this->MessageAsContact($oMessage);
echo \RainLoop\Providers\AddressBook\Utils::VCardToCsv($oContact, $bCsvHeader); \RainLoop\Providers\AddressBook\Utils::VCardToCsv($rCsv, $oContact, $bCsvHeader);
$bCsvHeader = false; $bCsvHeader = false;
} else if ($xCard = $this->fetchXCardFromMessage($oMessage)) {
echo $xCard->serialize();
} }
} }
} }

View file

@ -302,7 +302,7 @@ class PdoAddressBook
return false; return false;
} }
$bVcf = 'vcf' === $sType; $rCsv = 'csv' === $sType ? \fopen('php://output', 'w') : null;
$bCsvHeader = true; $bCsvHeader = true;
$aDatabaseSyncData = $this->prepareDatabaseSyncData(); $aDatabaseSyncData = $this->prepareDatabaseSyncData();
@ -311,11 +311,11 @@ class PdoAddressBook
if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) { if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) {
$oContact = $this->GetContactByID($mData['id_contact']); $oContact = $this->GetContactByID($mData['id_contact']);
if ($oContact) { if ($oContact) {
if ($bVcf) { if ($rCsv) {
echo $oContact->vCard->serialize(); Utils::VCardToCsv($rCsv, $oContact, $bCsvHeader);
} else {
echo Utils::VCardToCsv($oContact, $bCsvHeader);
$bCsvHeader = false; $bCsvHeader = false;
} else {
echo $oContact->vCard->serialize();
} }
} }
} }

View file

@ -110,136 +110,129 @@ class Utils
} }
} }
private static function csvNameToTypeConvertor(string $sCsvName) : int private static $aMap = array(
{ 'title' => 'FN',
static $aMap = null; 'name' => 'FN',
if (null === $aMap) 'fullname' => 'FN',
{ 'displayname' => 'FN',
$aMap = array( 'last' => 0,
'Title' => PropertyType::FULLNAME, 'lastname' => 0,
'Name' => PropertyType::FULLNAME, 'givenname' => 1,
'FullName' => PropertyType::FULLNAME, 'first' => 1,
'DisplayName' => PropertyType::FULLNAME, 'firstname' => 1,
'GivenName' => PropertyType::FIRST_NAME, 'middle' => 2,
'First' => PropertyType::FIRST_NAME, 'middlename' => 2,
'FirstName' => PropertyType::FIRST_NAME, 'prefix' => 3,
'Middle' => PropertyType::MIDDLE_NAME, 'nameprefix' => 3,
'MiddleName' => PropertyType::MIDDLE_NAME, 'suffix' => 4,
'Last' => PropertyType::LAST_NAME, 'namesuffix' => 4,
'LastName' => PropertyType::LAST_NAME, 'shortname' => 'NICKNAME',
'Suffix' => PropertyType::NAME_SUFFIX, 'nickname' => 'NICKNAME',
'NameSuffix' => PropertyType::NAME_SUFFIX, 'businessphone' => array('TEL', 'WORK'),
'Prefix' => PropertyType::NAME_PREFIX, 'businessphone2' => array('TEL', 'WORK'),
'NamePrefix' => PropertyType::NAME_PREFIX, 'businessphone3' => array('TEL', 'WORK'),
'ShortName' => PropertyType::NICK_NAME, 'companyphone' => array('TEL', 'WORK'),
'NickName' => PropertyType::NICK_NAME, 'companymainphone' => array('TEL', 'WORK'),
'BusinessFax' => array(PropertyType::PHONE, 'Work,Fax'), 'homephone' => array('TEL', 'HOME'),
'BusinessFax2' => array(PropertyType::PHONE, 'Work,Fax'), 'homephone2' => array('TEL', 'HOME'),
'BusinessFax3' => array(PropertyType::PHONE, 'Work,Fax'), 'homephone3' => array('TEL', 'HOME'),
'BusinessPhone' => array(PropertyType::PHONE, 'Work'), 'mobile' => array('TEL', 'CELL'),
'BusinessPhone2' => array(PropertyType::PHONE, 'Work'), 'mobilephone' => array('TEL', 'CELL'),
'BusinessPhone3' => array(PropertyType::PHONE, 'Work'), 'businessmobile' => array('TEL', 'WORK,CELL'),
'CompanyPhone' => array(PropertyType::PHONE, 'Work'), 'businessmobilephone' => array('TEL', 'WORK,CELL'),
'CompanyMainPhone' => array(PropertyType::PHONE, 'Work'), 'otherphone' => 'TEL',
'HomeFax' => array(PropertyType::PHONE, 'Home,Fax'), 'primaryphone' => array('TEL', 'PREF,HOME'),
'HomeFax2' => array(PropertyType::PHONE, 'Home,Fax'), 'email' => array('EMAIL', 'HOME'),
'HomeFax3' => array(PropertyType::PHONE, 'Home,Fax'), 'email2' => array('EMAIL', 'HOME'),
'HomePhone' => array(PropertyType::PHONE, 'Home'), 'email3' => array('EMAIL', 'HOME'),
'HomePhone2' => array(PropertyType::PHONE, 'Home'), 'homeemail' => array('EMAIL', 'HOME'),
'HomePhone3' => array(PropertyType::PHONE, 'Home'), 'homeemail2' => array('EMAIL', 'HOME'),
'Mobile' => array(PropertyType::PHONE, 'Mobile'), 'homeemail3' => array('EMAIL', 'HOME'),
'MobilePhone' => array(PropertyType::PHONE, 'Mobile'), 'primaryemail' => array('EMAIL', 'HOME'),
'BusinessMobile' => array(PropertyType::PHONE, 'Work,Mobile'), 'primaryemail2' => array('EMAIL', 'HOME'),
'BusinessMobilePhone' => array(PropertyType::PHONE, 'Work,Mobile'), 'primaryemail3' => array('EMAIL', 'HOME'),
'OtherFax' => array(PropertyType::PHONE, 'Other,Fax'), 'emailaddress' => array('EMAIL', 'HOME'),
'OtherPhone' => array(PropertyType::PHONE, 'Other'), 'email2address' => array('EMAIL', 'HOME'),
'PrimaryPhone' => array(PropertyType::PHONE, 'Pref,Home'), 'email3address' => array('EMAIL', 'HOME'),
'Email' => array(PropertyType::EMAIl, 'Home'), 'otheremail' => 'EMAIL',
'Email2' => array(PropertyType::EMAIl, 'Home'), 'businessemail' => array('EMAIL', 'WORK'),
'Email3' => array(PropertyType::EMAIl, 'Home'), 'businessemail2' => array('EMAIL', 'WORK'),
'HomeEmail' => array(PropertyType::EMAIl, 'Home'), 'businessemail3' => array('EMAIL', 'WORK'),
'HomeEmail2' => array(PropertyType::EMAIl, 'Home'), 'personalemail' => array('EMAIL', 'HOME'),
'HomeEmail3' => array(PropertyType::EMAIl, 'Home'), 'personalemail2' => array('EMAIL', 'HOME'),
'PrimaryEmail' => array(PropertyType::EMAIl, 'Home'), 'personalemail3' => array('EMAIL', 'HOME'),
'PrimaryEmail2' => array(PropertyType::EMAIl, 'Home'), 'notes' => 'NOTE',
'PrimaryEmail3' => array(PropertyType::EMAIl, 'Home'), 'web' => 'URL',
'EmailAddress' => array(PropertyType::EMAIl, 'Home'), 'businessweb' => array('URL', 'WORK'),
'Email2Address' => array(PropertyType::EMAIl, 'Home'), 'webpage' => 'URL',
'Email3Address' => array(PropertyType::EMAIl, 'Home'), 'businesswebpage' => array('URL', 'WORK'),
'OtherEmail' => array(PropertyType::EMAIl, 'Other'), 'website' => 'URL',
'BusinessEmail' => array(PropertyType::EMAIl, 'Work'), 'businesswebsite' => array('URL', 'WORK'),
'BusinessEmail2' => array(PropertyType::EMAIl, 'Work'), 'personalwebsite' => 'URL',
'BusinessEmail3' => array(PropertyType::EMAIl, 'Work'), 'birthday' => 'BDAY'
'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
);
$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 public static function CsvArrayToContacts(array $aCsvData) : iterable
{ {
foreach ($aCsvData as $aItem) { foreach ($aCsvData as $aItem) {
$oContact = new Classes\Contact();
\MailSo\Base\Utils::ResetTimeLimit(); \MailSo\Base\Utils::ResetTimeLimit();
$iCount = 0;
$oVCard = new \Sabre\VObject\Component\VCard;
$aName = ['','','','',''];
foreach ($aItem as $sItemName => $sItemValue) { foreach ($aItem as $sItemName => $sItemValue) {
$sItemName = \trim($sItemName); $sItemName = \strtoupper(\trim(\preg_replace('/[\s\-]+/', '', $sItemName)));
$sItemValue = \trim($sItemValue); $sItemValue = \trim($sItemValue);
if (!empty($sItemName) && !empty($sItemValue)) { if (!empty($sItemName) && !empty($sItemValue)) {
$mData = static::csvNameToTypeConvertor($sItemName); if (\array_key_exists($sItemName, \Sabre\VObject\Component\VCard::$propertyMap)) {
$iType = \is_array($mData) ? $mData[0] : $mData; $mData = $sItemName;
if (PropertyType::UNKNOWN !== $iType) { } else {
$oProp = new Classes\Property(); $sItemName = \strtolower($sItemName);
$oProp->Type = $iType; $mData = !empty($sItemName) && isset($aMap[$sItemName]) ? $aMap[$sItemName] : null;
$oProp->Value = $sItemValue; }
$oProp->TypeStr = \is_array($mData) && !empty($mData[1]) ? $mData[1] : ''; if ($mData) {
// $oContact->Properties[] = $oProp; $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; yield $oContact;
} }
} }
} }
/** public static function VCardToCsv($stream, Classes\Contact $oContact, bool $bWithHeader = false)/* : int|false*/
* TODO: broken
*/
public static function VCardToCsv(Classes\Contact $oContact, bool $bWithHeader = false) : string
{ {
$aData = array(); $aData = array();
if ($bWithHeader) { if ($bWithHeader) {
$aData[] = array( \fputcsv($stream, array(
'Title', 'First Name', 'Middle Name', 'Last Name', 'Nick Name', 'Display Name', 'Title', 'First Name', 'Middle Name', 'Last Name', 'Nick Name', 'Display Name',
'Company', 'Department', 'Job Title', 'Office Location', 'Company', 'Department', 'Job Title', 'Office Location',
'E-mail Address', 'Notes', 'Web Page', 'Birthday', 'E-mail Address', 'Notes', 'Web Page', 'Birthday', 'Mobile Phone',
'Other Email', 'Other Phone', 'Other Mobile', 'Mobile Phone', 'Home Email', 'Home Phone',
'Home Email', 'Home Phone', 'Home Fax', 'Home Street', 'Home City', 'Home State', 'Home Postal Code', 'Home Country',
'Home Street', 'Home City', 'Home State', 'Home Postal Code', 'Home Country', 'Business Email', 'Business Phone',
'Business Email', 'Business Phone', 'Business Fax', 'Business Street', 'Business City', 'Business State', 'Business Postal Code', 'Business Country'
'Business Street', 'Business City', 'Business State', 'Business Postal Code', 'Business Country' ));
);
} }
$oVCard = $oContact->vCard; $oVCard = $oContact->vCard;
$aName = isset($oVCard->N) ? $oVCard->N->getParts() : ['','','','',''];
$adrHome = $oVCard->getByType('ADR', 'HOME'); $adrHome = $oVCard->getByType('ADR', 'HOME');
$adrHome = $adrHome ? $adrHome->getParts() : ['','','','','','','']; $adrHome = $adrHome ? $adrHome->getParts() : ['','','','','','',''];
@ -247,11 +240,11 @@ class Utils
$adrWork = $oVCard->getByType('ADR', 'WORK'); $adrWork = $oVCard->getByType('ADR', 'WORK');
$adrWork = $adrWork ? $adrWork->getParts() : ['','','','','','','']; $adrWork = $adrWork ? $adrWork->getParts() : ['','','','','','',''];
$aValues = array( return \fputcsv($stream, array(
'', // Title (string) $oVCard->FN, // Title
'', // First Name $aName[1], // First Name
'', // Middle Name $aName[2], // Middle Name
'', // Last Name $aName[0], // Last Name
(string) $oVCard->NICKNAME, // Nick Name (string) $oVCard->NICKNAME, // Nick Name
(string) $oVCard->FN, // Display Name (string) $oVCard->FN, // Display Name
(string) $oVCard->ORG, // Company (string) $oVCard->ORG, // Company
@ -261,15 +254,11 @@ class Utils
(string) $oVCard->EMAIL, // E-mail Address (string) $oVCard->EMAIL, // E-mail Address
(string) $oVCard->NOTE, // Notes (string) $oVCard->NOTE, // Notes
(string) $oVCard->URL, // Web Page (string) $oVCard->URL, // Web Page
'', // Birthday (string) $oVCard->BDAY, // Birthday
'', // Other Email
'', // Other Phone
'', // Other Mobile
(string) $oVCard->getByType('TEL', 'CELL'), // Mobile Phone (string) $oVCard->getByType('TEL', 'CELL'), // Mobile Phone
// Home // Home
(string) $oVCard->getByType('EMAIL', 'HOME'), // Email (string) $oVCard->getByType('EMAIL', 'HOME'), // Email
(string) $oVCard->getByType('TEL', 'HOME'), // Phone (string) $oVCard->getByType('TEL', 'HOME'), // Phone
'', // Fax,
\trim($adrHome[1]."\n".$adrHome[2]), // extended address + street address \trim($adrHome[1]."\n".$adrHome[2]), // extended address + street address
$adrHome[3], // City $adrHome[3], // City
$adrHome[4], // State $adrHome[4], // State
@ -278,25 +267,12 @@ class Utils
// Business // Business
(string) $oVCard->getByType('EMAIL', 'WORK'), // Email (string) $oVCard->getByType('EMAIL', 'WORK'), // Email
(string) $oVCard->getByType('TEL', 'WORK'), // Phone (string) $oVCard->getByType('TEL', 'WORK'), // Phone
'', // Fax
\trim($adrWork[1]."\n".$adrWork[2]), // extended address + street address \trim($adrWork[1]."\n".$adrWork[2]), // extended address + street address
$adrWork[3], // City $adrWork[3], // City
$adrWork[4], // State $adrWork[4], // State
$adrWork[5], // Postal Code $adrWork[5], // Postal Code
$adrWork[6] // Country $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 public static function VcfFileToContacts(string $sVcfData) : iterable

View file

@ -139,6 +139,26 @@
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: surName" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_LAST_NAME"> data-bind="value: surName" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_LAST_NAME">
</div> </div>
<!--
<div class="property-line">
<span data-bind="text: middleName"></span>
<input type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: middleName" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_MIDDLE_NAME">
</div>
<div class="property-line">
<span data-bind="text: namePrefix"></span>
<input type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: namePrefix" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_NAME_PREFIX">
</div>
<div class="property-line">
<span data-bind="text: nameSuffix"></span>
<input type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: nameSuffix" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_NAME_SUFFIX">
</div>
-->
<div class="property-line" data-bind="visible: null != nickname()"> <div class="property-line" data-bind="visible: null != nickname()">
<span data-bind="text: nickname"></span> <span data-bind="text: nickname"></span>
<input type="text" <input type="text"
@ -172,7 +192,7 @@
data-bind="value: value"> data-bind="value: value">
</div> </div>
</div> </div>
<a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: $root.addTel"></a> <a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: addTel"></a>
</div> </div>
</div> </div>
<div class="control-group" data-bind="visible: url().length"> <div class="control-group" data-bind="visible: url().length">
@ -186,7 +206,7 @@
data-bind="value: value"> data-bind="value: value">
</div> </div>
</div> </div>
<a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: $root.addUrl"></a> <a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: addUrl"></a>
</div> </div>
</div> </div>
</div> </div>