Redesign AddressBook

This commit is contained in:
the-djmaze 2022-05-29 00:22:50 +02:00
parent 5f6ea3cf29
commit a71874a8c4
18 changed files with 1403 additions and 1669 deletions

227
dev/DAV/JCard.js Normal file
View file

@ -0,0 +1,227 @@
/**
* https://datatracker.ietf.org/doc/html/rfc7095
*
* Inspired by https://github.com/mcpar-land/vcfer
*/
import { VCardProperty } from './VCardProperty'
export class JCard {
constructor(input)
{
this.props = new Map()
this.version = '4.0'
if (input) {
// read from jCard
if (typeof input !== 'object') {
throw new Error('error reading vcard')
}
this.parseFromJCard(input)
}
}
parseFromJCard(json)
{
json = JSON.parse(JSON.stringify(json));
if (!/vcard/i.test(json[0])) {
throw new SyntaxError('Incorrect jCard format');
}
json[1].forEach(jprop => this.add(new VCardProperty(jprop)));
}
/**
* Retrieve an array of {@link VCardProperty} objects under the specified field.
* Returns [] if there are no VCardProperty objects found.
* Properites are always stored in an array.
* @param field to get.
* @param type If provided, only return {@link VCardProperty}s with the specified
* type as a param.
*/
get(field, type)
{
if (type) {
let props = this.props.get(field);
return props
? props.filter(prop => {
let types = prop.type;
return (Array.isArray(types) ? types : [types]).includes(type);
})
: [];
}
return this.props.get(field) || [];
// TODO with type filter-er
}
/**
* Retrieve a _single_ VCardProperty of the specified field. Attempts to pick based
* on the following priorities, in order:
* - `TYPE={type}` of the value specified in the `type` argument. Ignored
* if the argument isn't supplied.
* - `TYPE=pref` is present.
* - is the VCardProperty at index 0 from get(field)
* @param field
* @param type
*/
getOne(field, type)
{
return this.get(field, type || 'pref')[0] || this.get(field)[0];
}
/**
* Set the contents of a field to contain a single {@link VCardProperty}.
*
* Accepts either 2-4 arguments to construct a VCardProperty,
* or 1 argument of a preexisting VCardProperty object.
*
* This will always overwrite all existing properties of the given
* field. For just adding a new VCardProperty, see {@link VCard#add}
* @param arg the field, or a VCardProperty object
* @param value the value for the VCardProperty object
* @param params the parameters for the VCardProperty object
* @param group the group for the VCardProperty object
*/
set(arg, value, params, group)
{
if (typeof arg === 'string') {
arg = new VCardProperty(String(arg), value, params, group);
}
if (!(arg instanceof VCardProperty)) {
throw new Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
}
let field = arg.getField();
this.props.set(field, [arg]);
return arg;
}
add(arg, value, params, group)
{
// string arguments
if (typeof arg === 'string') {
arg = new VCardProperty(String(arg), value, params, group);
}
if (!(arg instanceof VCardProperty)) {
throw new Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
}
// VCardProperty arguments
let field = arg.getField();
if (this.props.get(field)) this.props.get(field)?.push(arg)
else this.props.set(field, [arg])
return arg;
}
/**
* Removes a {@link VCardProperty}, or all properties of the supplied field.
* @param arg the field, or a {@link VCardProperty} object
* @param paramFilter (incomplete)
*/
remove(arg) {
// string arguments
if (typeof arg === 'string') {
// TODO filter by param
this.props.delete(arg);
}
// VCardProperty argument
else if (arg instanceof VCardProperty) {
let propArray = this.props.get(arg.getField());
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
throw new Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
propArray.splice(propArray.indexOf(arg), 1);
if (propArray.length === 0)
this.props.delete(arg.getField());
}
// incorrect arguments
else
throw new Error('invalid argument of VCard.remove(), expects ' +
'string and optional param filter or a VCardProperty');
}
/**
* Returns true if the vCard has at least one @{link VCardProperty}
* of the given field.
* @param field The field to query
*/
has(field)
{
return (!!this.props.get(field) && this.props.get(field).length > 0);
}
/**
* Returns stringified JSON
*/
toString()
{
return JSON.stringify(this);
}
/**
* Returns a {@link JCard} object as a JSON array.
*/
toJSON()
{
let data = [['version', {}, 'text', '4.0']];
/*
this.props.forEach((props, field) =>
(field === 'version') || props.forEach(prop => prop.isEmpty() || data.push(prop.toJSON()))
);
*/
for (const [field, props] of this.props.entries()) {
if ('version' !== field) {
for (const prop of props) {
prop.isEmpty() || data.push(prop.toJSON());
}
}
}
return ['vcard', data];
}
/**
* Automatically generate the 'fn' VCardProperty from the preferred 'n' VCardProperty.
*
* #### `set` (`boolean`)
*
* - `false`: (default) return the generated full name string without
* modifying the VCard.
*
* - `true`: modify the VCard's `fn` VCardProperty directly, as specified
* by `append`
*
* #### `append` (`boolean`)
*
* (ignored if `set` is `false`)
*
* - `false`: (default) replace the existing 'fn' VCardProperty/properties with
* a new one.
*
* - `true`: append a new `fn` VCardProperty to the array.
*
* see: [RFC 6350 section 6.2.1](https://tools.ietf.org/html/rfc6350#section-6.2.1)
*/
parseFullName(options) {
let n = this.getOne('n');
if (n === undefined) {
throw new Error('\'fn\' VCardProperty not present in card, cannot parse full name');
}
let fnString = '';
// Position in n -> position in fn
[3, 1, 2, 0, 4].forEach(pos => {
let splitStr = n.value[pos];
if (splitStr) {
// comma separated values separated by spaces
fnString += ' ' + splitStr.replace(',', ' ');
}
});
fnString = fnString.trim();
let fn = new VCardProperty('fn', fnString);
if (options?.set) {
if (options.append) {
this.add(fn);
} else {
this.set(fn);
}
}
return fn;
}
}

152
dev/DAV/VCardProperty.js Normal file
View file

@ -0,0 +1,152 @@
/**
* Inspired by https://github.com/mcpar-land/vcfer
*/
const
camelCase = str =>
str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) =>
index === 0 ? word.toLowerCase() : word.toUpperCase()
).replace(/\s+/g, '');
export class VCardProperty {
/**
* A class describing a single vCard property.
* Will almost always be a member of a
* {@link VCard}'s [props]{@link VCard.props} map.
*
* Accepts either 2-4 arguments, or 1 argument in jCard property format.
* @param arg the field, or a jCard property
* @param value
* @param params
* @param type
*/
constructor(arg, value, params, type = 'text')
{
this.field = '';
/**
* the value of the property.
* @example '(123) 456 7890'
*/
this.value = '';
/**
* the type of the property value.
* @example 'text'
*/
this.type = '';
/**
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5
* An jCard parameters object.
* @example
* {
* type: ['work', 'voice', 'pref'],
* value: 'uri'
* }
*/
this.params = {};
// Construct from arguments
if (value !== undefined && typeof arg === 'string') {
this.field = arg;
this.value = value;
this.params = params || {};
this.type = type;
}
// construct from jcard
else if (value === undefined && params === undefined && typeof arg === 'object') {
this.parseFromJCardProperty(arg);
}
// invalid property
else {
throw new Error('invalid Property constructor');
}
}
parseFromJCardProperty(jCardProp)
{
jCardProp = JSON.parse(JSON.stringify(jCardProp));
this.field = camelCase(jCardProp[0]);
this.params = jCardProp[1];
this.type = jCardProp[2];
this.value = jCardProp[3];
}
addParam(key, value)
{
if (Array.isArray(this.params[key])) {
this.params[key].push(value);
}
else if (this.params[key] != null) {
this.params[key] = [this.params[key], value];
}
else {
this.params[key] = value;
}
}
/** Returns a copy of the Property's string value. */
getValue()
{
return '' + this.value;
}
/**
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5.3
*/
pref()
{
return this.params.pref || 100;
}
/**
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5.6
*/
tags()
{
return this.params.type || [];
}
/** Returns `true` if all the following are true:
* - the property's value contains charactes other than `;`
* - the property has no parameters
*/
isEmpty()
{
return ((null == this.value || !/[^;]+/.test(this.value)) && !Object.keys(this.params).length);
}
notEmpty()
{
return !this.isEmpty();
}
/** Returns a readonly string copy of the property's field. */
getField()
{
return '' + this.field;
}
/**
* Returns stringified JSON
*/
toString()
{
return JSON.stringify(this);
}
/**
* Returns a JSON array in a [jCard property]{@link JCardProperty} format
*/
toJSON()
{
return [
this.field,
this.params,
this.type || 'text',
this.value
];
}
}

View file

@ -1,21 +1,63 @@
import { arrayLength } from 'Common/Utils';
import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty';
import { AbstractModel } from 'Knoin/AbstractModel';
import { JCard } from 'DAV/JCard';
//import { VCardProperty } from 'DAV/VCardProperty';
const nProps = [
'surName',
'givenName',
'middleName',
'namePrefix',
'nameSuffix'
];
export class ContactModel extends AbstractModel {
constructor() {
super();
this.id = 0;
this.display = '';
this.properties = [];
this.readOnly = false;
this.jCard = new JCard();
this.addObservables({
focused: false,
selected: false,
checked: false,
deleted: false
deleted: false,
readOnly: false,
id: 0,
givenName: '', // FirstName
surName: '', // LastName
middleName: '', // MiddleName
namePrefix: '', // NamePrefix
nameSuffix: '', // NameSuffix
nickname: null
});
// this.email = koArrayWithDestroy();
this.email = ko.observableArray();
this.tel = ko.observableArray();
this.url = ko.observableArray();
this.addComputables({
hasValidName: () => !!(this.givenName() || this.surName()),
fullName: () => (this.givenName() + ' ' + this.surName()).trim(),
display: () => {
let a = this.jCard.getOne('fn')?.value,
b = this.fullName(),
c = this.jCard.getOne('email')?.value,
d = this.nickname();
return a || b || c || d;
}
/*
fullName: {
read: () => this.givenName() + " " + this.surName(),
write: value => {
this.jCard.set('fn', value/*, params, group* /)
}
}
*/
});
}
@ -23,23 +65,15 @@ export class ContactModel extends AbstractModel {
* @returns {Array|null}
*/
getNameAndEmailHelper() {
let name = '',
email = '';
if (arrayLength(this.properties)) {
this.properties.forEach(property => {
if (property) {
if (ContactPropertyType.FirstName === property.type()) {
name = (property.value() + ' ' + name).trim();
} else if (ContactPropertyType.LastName === property.type()) {
name = (name + ' ' + property.value()).trim();
} else if (!email && ContactPropertyType.Email === property.type()) {
email = property.value();
}
}
});
}
let name = (this.givenName() + ' ' + this.surName()).trim(),
email = this.email()[0];
/*
// this.jCard.getOne('fn')?.notEmpty() ||
this.jCard.parseFullName({set:true});
// let name = this.jCard.getOne('nickname'),
let name = this.jCard.getOne('fn'),
email = this.jCard.getOne('email');
*/
return email ? [email, name] : null;
}
@ -52,47 +86,32 @@ export class ContactModel extends AbstractModel {
const contact = super.reviveFromJson(json);
if (contact) {
let list = [];
if (arrayLength(json.properties)) {
json.properties.forEach(property => {
property = ContactPropertyModel.reviveFromJson(property);
property && list.push(property);
let jCard = new JCard(json.jCard),
props = jCard.getOne('n')?.value;
props && props.forEach((value, index) =>
value && contact[nProps[index]](value)
);
props = jCard.getOne('nickname');
props && contact.nickname(props.value);
['email', 'tel', 'url'].forEach(field => {
props = jCard.get(field);
props && props.forEach(prop => {
contact[field].push({
value: ko.observable(prop.value)
// type: prop.params.type
});
});
}
});
contact.properties = list;
contact.initDefaultProperties();
contact.jCard = jCard;
}
return contact;
}
initDefaultProperties() {
let list = this.properties;
list.sort((p1,p2) =>{
if (p2.type() == ContactPropertyType.FirstName) {
return 1;
}
if (p1.type() == ContactPropertyType.FirstName || p1.type() == ContactPropertyType.LastName) {
return -1;
}
if (p2.type() == ContactPropertyType.LastName) {
return 1;
}
return 0;
});
let found = list.find(prop => prop.type() == ContactPropertyType.LastName);
if (!found) {
found = new ContactPropertyModel(ContactPropertyType.LastName);
list.unshift(found);
}
found.placeholder('CONTACTS/PLACEHOLDER_ENTER_LAST_NAME');
found = list.find(prop => prop.type() == ContactPropertyType.FirstName);
if (!found) {
found = new ContactPropertyModel(ContactPropertyType.FirstName);
list.unshift(found);
}
found.placeholder('CONTACTS/PLACEHOLDER_ENTER_FIRST_NAME');
this.properties = list;
}
/**
* @returns {string}
*/
@ -100,6 +119,71 @@ export class ContactModel extends AbstractModel {
return ''+this.id;
}
addEmail() {
// home, work
this.email.push({
value: ko.observable('')
// type: prop.params.type
});
}
addTel() {
// home, work, text, voice, fax, cell, video, pager, textphone, iana-token, x-name
this.tel.push({
value: ko.observable('')
// type: prop.params.type
});
}
addUrl() {
// home, work
this.url.push({
value: ko.observable('')
// type: prop.params.type
});
}
addNickname() {
// home, work
this.nickname() || this.nickname('');
}
toJSON()
{
let jCard = this.jCard;
jCard.set('n', [
this.surName(),
this.givenName(),
this.middleName(),
this.namePrefix(),
this.nameSuffix()
]/*, params, group*/);
// jCard.parseFullName({set:true});
this.nickname() ? jCard.set('nickname', this.nickname()/*, params, group*/) : jCard.remove('nickname');
['email', 'tel', 'url'].forEach(field => {
let values = this[field].map(item => item.value());
jCard.get(field).forEach(prop => {
let i = values.indexOf(prop.value);
if (0 > i || !prop.value) {
jCard.remove(prop);
} else {
values.splice(i, 1);
}
});
values.forEach(value => value && jCard.add(field, value));
});
// Done by server
// jCard.set('rev', '2022-05-21T10:59:52Z')
return {
Uid: this.id,
jCard: JSON.stringify(jCard)
};
}
/**
* @return string
*/

View file

@ -345,7 +345,7 @@ export class FolderModel extends AbstractModel {
canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
isSystemFolder: () => FolderType.User !== folder.type() | !!folder.kolabType(),
isSystemFolder: () => FolderType.User !== folder.type() /*| !!folder.kolabType()*/,
canBeSelected: () => folder.selectable() && !folder.isSystemFolder(),

View file

@ -193,13 +193,6 @@
height: 30px;
}
.add-link {
margin-left: 2px;
padding: 5px;
font-size: 12px;
color: #aaa;
}
input, textarea {
box-shadow: none;
border-color: #fff;

View file

@ -17,7 +17,6 @@ import Remote from 'Remote/User/Fetch';
import { EmailModel } from 'Model/Email';
import { ContactModel } from 'Model/Contact';
import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty';
import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
@ -25,10 +24,9 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { AskPopupView } from 'View/Popup/Ask';
const
viewProperties = koArrayWithDestroy(),
CONTACTS_PER_PAGE = 50,
ScopeContacts = 'Contacts',
propertyIsMail = prop => prop.isType(ContactPropertyType.Email),
propertyIsName = prop => prop.isType(ContactPropertyType.FirstName) || prop.isType(ContactPropertyType.LastName);
ScopeContacts = 'Contacts';
export class ContactsPopupView extends AbstractViewPopup {
constructor() {
@ -47,10 +45,8 @@ export class ContactsPopupView extends AbstractViewPopup {
contactsPage: 1,
emptySelection: true,
viewClearSearch: false,
viewID: '',
viewReadOnly: false,
viewSaveTrigger: SaveSettingsStep.Idle,
@ -58,13 +54,13 @@ export class ContactsPopupView extends AbstractViewPopup {
viewSaving: false,
watchDirty: false,
watchHash: false
watchHash: false,
contact: null
});
this.contacts = ContactUserStore;
this.viewProperties = koArrayWithDestroy();
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
this.selector = new Selector(
@ -78,19 +74,13 @@ export class ContactsPopupView extends AbstractViewPopup {
this.selector.on('ItemSelect', contact => {
this.populateViewContact(contact);
if (!contact) {
this.emptySelection(true);
}
});
this.selector.on('ItemGetUid', contact => contact ? contact.generateUid() : '');
this.bDropPageAfterDelete = false;
// this.saveCommandDebounce = this.saveCommand.bind(this).debounce(1000);
const
// propertyFocused = property => !property.isValid() && !property.focused(),
pagecount = () => Math.max(1, Math.ceil(this.contactsCount() / CONTACTS_PER_PAGE));
this.addComputables({
@ -98,18 +88,6 @@ export class ContactsPopupView extends AbstractViewPopup {
contactsPaginator: computedPaginatorHelper(this.contactsPage, pagecount),
viewPropertiesNames: () => this.viewProperties.filter(propertyIsName),
viewPropertiesEmails: () => this.viewProperties.filter(propertyIsMail),
viewPropertiesOther: () => this.viewProperties.filter(property => property.isType(ContactPropertyType.Nick)),
viewPropertiesWeb: () => this.viewProperties.filter(property => property.isType(ContactPropertyType.Web)),
viewPropertiesPhones: () => this.viewProperties.filter(property => property.isType(ContactPropertyType.Phone)),
contactHasValidName: () => !!this.viewProperties.find(prop => propertyIsName(prop) && prop.isValid()),
contactsCheckedOrSelected: () => {
const checked = ContactUserStore.filter(item => item.checked && item.checked()),
selected = this.currentContact();
@ -119,11 +97,11 @@ export class ContactsPopupView extends AbstractViewPopup {
: checked;
},
contactsCheckedOrSelectedUids: () => this.contactsCheckedOrSelected().map(contact => contact.id),
contactsCheckedOrSelectedUids: () => this.contactsCheckedOrSelected().map(contact => contact.id()),
contactsSyncEnabled: () => ContactUserStore.allowSync() && ContactUserStore.syncMode(),
viewHash: () => '' + this.viewProperties.map(property => property.value && property.value()).join('')
viewHash: () => '' + viewProperties.map(property => property.value && property.value()).join('')
});
this.search.subscribe(() => this.reloadContactList());
@ -134,12 +112,14 @@ export class ContactsPopupView extends AbstractViewPopup {
}
});
this.saveCommand = this.saveCommand.bind(this);
decorateKoCommands(this, {
// close: self => !self.watchDirty(),
deleteCommand: self => 0 < self.contactsCheckedOrSelected().length,
newMessageCommand: self => 0 < self.contactsCheckedOrSelected().length,
saveCommand: self => !self.viewSaving() && !self.viewReadOnly()
&& (self.contactHasValidName() || self.viewProperties.find(prop => propertyIsMail(prop) && prop.isValid())),
&& (self.contact()?.hasValidName() || self.contact()?.email().length),
syncCommand: self => !self.contacts.syncing() && !self.contacts.importing()
});
}
@ -151,7 +131,6 @@ export class ContactsPopupView extends AbstractViewPopup {
deleteCommand() {
this.deleteSelectedContacts();
this.emptySelection(true);
}
newMessageCommand() {
@ -213,22 +192,20 @@ export class ContactsPopupView extends AbstractViewPopup {
this.viewSaving(true);
this.viewSaveTrigger(SaveSettingsStep.Animate);
const requestUid = Jua.randomId();
const
contact = this.contact(),
requestUid = Jua.randomId();
Remote.request('ContactSave',
(iError, oData) => {
let res = false;
this.viewSaving(false);
if (
!iError &&
oData.Result.RequestUid === requestUid &&
0 < pInt(oData.Result.ResultID)
if (!iError
&& oData.Result.RequestUid === requestUid
&& oData.Result.ResultID
) {
if (!this.viewID()) {
this.viewID(pInt(oData.Result.ResultID));
}
contact.id(oData.Result.ResultID);
this.reloadContactList(); // TODO: remove when e-contact-foreach is dynamic
res = true;
}
@ -243,8 +220,9 @@ export class ContactsPopupView extends AbstractViewPopup {
}
}, {
RequestUid: requestUid,
Uid: this.viewID(),
Properties: this.viewProperties.map(oItem => oItem.toJSON())
Contact: contact
// Uid: contact.id(),
// jCard: contact.jCard
}
);
}
@ -257,63 +235,6 @@ export class ContactsPopupView extends AbstractViewPopup {
});
}
getPropertyPlaceholder(type) {
let result = '';
switch (type) {
case ContactPropertyType.LastName:
result = 'CONTACTS/PLACEHOLDER_ENTER_LAST_NAME';
break;
case ContactPropertyType.FirstName:
result = 'CONTACTS/PLACEHOLDER_ENTER_FIRST_NAME';
break;
case ContactPropertyType.Nick:
result = 'CONTACTS/PLACEHOLDER_ENTER_NICK_NAME';
break;
// no default
}
return result;
}
addNewProperty(type, typeStr) {
this.viewProperties.push(
new ContactPropertyModel(type, typeStr || '', '', true, this.getPropertyPlaceholder(type))
);
}
addNewOrFocusProperty(type, typeStr) {
const item = this.viewProperties.find(prop => prop.isType(type));
if (item) {
item.focused(true);
} else {
this.addNewProperty(type, typeStr);
}
}
addNewEmail() {
this.addNewProperty(ContactPropertyType.Email, 'Home');
}
addNewPhone() {
this.addNewProperty(ContactPropertyType.Phone, 'Mobile');
}
addNewWeb() {
this.addNewProperty(ContactPropertyType.Web);
}
addNewNickname() {
this.addNewOrFocusProperty(ContactPropertyType.Nick);
}
addNewNotes() {
this.addNewOrFocusProperty(ContactPropertyType.Note);
}
addNewBirthday() {
this.addNewOrFocusProperty(ContactPropertyType.Birthday);
}
exportVcf() {
download(serverRequestRaw('ContactsVcf'), 'contacts.vcf');
}
@ -330,7 +251,7 @@ export class ContactsPopupView extends AbstractViewPopup {
if (contacts.length) {
contacts.forEach(contact => {
if (currentContact && currentContact.id === contact.id) {
if (currentContact && currentContact.id() === contact.id()) {
currentContact = null;
this.currentContact(null);
}
@ -366,33 +287,19 @@ export class ContactsPopupView extends AbstractViewPopup {
}
}
removeProperty(oProp) {
this.viewProperties.remove(oProp);
}
/**
* @param {?ContactModel} contact
*/
populateViewContact(contact) {
let id = '';
this.watchHash(false);
this.emptySelection(false);
this.viewReadOnly(false);
if (contact) {
id = contact.id;
this.viewReadOnly(!!contact.readOnly);
} else {
if (!contact) {
contact = new ContactModel;
contact.initDefaultProperties();
}
this.viewReadOnly(contact.readOnly());
this.contact(contact);
this.viewID(id);
// this.viewProperties([]);
this.viewProperties(contact.properties);
viewProperties(contact.properties);
this.watchDirty(false);
this.watchHash(true);
@ -509,8 +416,8 @@ export class ContactsPopupView extends AbstractViewPopup {
}
onHide() {
this.contact(null);
this.currentContact(null);
this.emptySelection(true);
this.search('');
this.contactsCount(0);

View file

@ -75,35 +75,21 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
return $xCard;
}
protected function MessageAsContact(\MailSo\Mail\Message $oMessage) : ?Contact
protected function MessageAsContact(\MailSo\Mail\Message $oMessage) : Contact
{
$oContact = new Contact;
$oContact->IdContact = $oMessage->Uid();
// $oContact->Display = isset($aItem['display']) ? (string) $aItem['display'] : '';
$oContact->id = $oMessage->Uid();
$oContact->Changed = $oMessage->HeaderTimeStampInUTC();
$oFrom = $oMessage->From();
if ($oFrom) {
$oMail = $oFrom[0];
$oProperty = new Property(PropertyType::EMAIl, $oMail->GetEmail());
$oContact->Properties[] = $oProperty;
$oProperty = new Property(PropertyType::FULLNAME, $oMail->GetDisplayName());
// $oProperty = new Property(PropertyType::FULLNAME, $oMail->ToString());
$oContact->Properties[] = $oProperty;
// $oProperty = new Property(PropertyType::NICK_NAME, $oMail->GetDisplayName());
// $oContact->Properties[] = $oProperty;
}
// Fetch xCard attachment and populate $oContact with it
$xCard = $this->fetchXCardFromMessage($oMessage);
if ($xCard) {
$oContact->PopulateByVCard($xCard);
$oContact->setVCard($xCard);
}
// Reset, else it is 'urn:uuid:01234567-89AB-CDEF-0123-456789ABCDEF'
// $oContact->IdContactStr = $oMessage->Subject();
$oContact->UpdateDependentValues();
$oContact->IdContactStr = $oMessage->Subject();
$oContact->IdContactStr = \str_replace('urn:uuid:', '', $oContact->IdContactStr);
return $oContact;
}
@ -155,10 +141,8 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
}
} else {
$oContact = $this->MessageAsContact($oMessage);
if ($oContact) {
echo $oContact->ToCsv($bCsvHeader);
$bCsvHeader = false;
}
echo \RainLoop\Providers\AddressBook\Utils::VCardToCsv($oContact, $bCsvHeader);
$bCsvHeader = false;
}
}
}
@ -176,33 +160,36 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
return false;
}
$id = $oContact->IdContact;
$id = $oContact->id;
$sUID = '';
$oVCard = $oContact->vCard;
$oPrevMessage = $this->MailClient()->Message($this->sFolderName, $id);
if ($oPrevMessage) {
$oVCard = $this->fetchXCardFromMessage($oPrevMessage);
$sUID = $oPrevMessage->Subject();
if (!$sUID) {
$oVCard = $this->fetchXCardFromMessage($oPrevMessage);
$sUID = \str_replace('urn:uuid:', '', $oVCard->UID);
}
} else {
$oVCard = null;
$id = 0;
}
$oVCard = $oVCard ?: new \Sabre\VObject\Component\VCard();
$sUid = (string) $oVCard->UID;
$oContact->PopulateDisplayAndFullNameValue();
$oContact->UpdateDependentValues();
$oContact->fillVCard($oVCard);
$sUid = \str_replace('urn:uuid:', '', $sUid ?: $oContact->GetUID());
if (!\SnappyMail\UUID::isValid($sUid)) {
$sUid = \SnappyMail\UUID::generate();
if (!$sUID || !\SnappyMail\UUID::isValid($sUID)) {
$sUID = \SnappyMail\UUID::generate();
}
$oContact->IdContactStr = $sUid;
$oContact->SetUID($sUid);
$oVCard->UID = new \Sabre\VObject\Property\Uri($oVCard, 'uid', 'urn:uuid:' . $sUid);
$oVCard->UID = new \Sabre\VObject\Property\Uri($oVCard, 'uid', 'urn:uuid:' . $sUID);
$oContact->IdContactStr = $sUID;
if (!\count($oVCard->select('x-kolab-version'))) {
$oVCard->add(new \Sabre\VObject\Property\Text($oVCard, 'x-kolab-version', '3.1.0'));
}
$oVCard->VERSION = '3.0';
// $oVCard->PRODID = 'SnappyMail-'.APP_VERSION;
$oVCard->KIND = 'individual';
$oMessage = new \MailSo\Mime\Message();
$oMessage->DoesNotAddDefaultXMailer();
$oMessage->messageIdRequired = false;
@ -217,11 +204,11 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
}
}
if ($sEmail) {
$oMessage->SetFrom(new \MailSo\Mime\Email($sEmail, $oContact->Display));
$oMessage->SetFrom(new \MailSo\Mime\Email($sEmail, (string) $oVCard->FN));
}
}
$oMessage->SetSubject($sUid);
$oMessage->SetSubject($sUID);
// $oMessage->SetDate(\time());
$oMessage->SetCustomHeader('X-Kolab-Type', 'application/x-vnd.kolab.contact');
$oMessage->SetCustomHeader('X-Kolab-Mime-Version', '3.0');
@ -233,7 +220,7 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
$oPart->Body = "This is a Kolab Groupware object.\r\n"
. "To view this object you will need an email client that can understand the Kolab Groupware format.\r\n"
. "For a list of such email clients please visit\r\n"
. "http://www.kolab.org/get-kolab\r\n";
. "https://en.wikipedia.org/wiki/Kolab\r\n";
$oMessage->SubParts->append($oPart);
// Now the vCard

View file

@ -99,59 +99,27 @@ trait Contacts
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
$sRequestUid = \trim($this->GetActionParam('RequestUid', ''));
if ($oAddressBookProvider && $oAddressBookProvider->IsActive() && \strlen($sRequestUid))
{
$sUid = \trim($this->GetActionParam('Uid', ''));
$oContact = null;
if (\strlen($sUid))
{
$oContact = $oAddressBookProvider->GetContactByID($sUid);
}
if (!$oContact)
{
$oContact = new \RainLoop\Providers\AddressBook\Classes\Contact();
if (\strlen($sUid))
{
$oContact->IdContact = $sUid;
}
}
$oContact->Properties = array();
$aProperties = $this->GetActionParam('Properties', array());
if (\is_array($aProperties))
{
foreach ($aProperties as $aItem)
{
if ($aItem && isset($aItem['type'], $aItem['value'])
&& \is_numeric($aItem['type']) && (int) $aItem['type']
&& \RainLoop\Providers\AddressBook\Enumerations\PropertyType::FULLNAME != $aItem['type']
&& \strlen(\trim($aItem['value'])))
{
$oProp = new \RainLoop\Providers\AddressBook\Classes\Property();
$oProp->Type = (int) $aItem['type'];
$oProp->Value = \trim($aItem['value']);
$oProp->TypeStr = $aItem['typeStr'] ?? '';
$oContact->Properties[] = $oProp;
if ($oAddressBookProvider && $oAddressBookProvider->IsActive() && \strlen($sRequestUid)) {
$aContact = $this->GetActionParam('Contact');
if (\is_array($aContact) && isset($aContact['Uid'], $aContact['jCard'])) {
$vCard = \Sabre\VObject\Reader::readJson($aContact['jCard']);
if ($vCard && $vCard instanceof \Sabre\VObject\Component\VCard) {
$vCard->REV = \gmdate('Ymd\\THis\\Z');
$vCard->PRODID = 'SnappyMail-'.APP_VERSION;
$sUid = \trim($aContact['Uid']);
$oContact = $sUid ? $oAddressBookProvider->GetContactByID($sUid) : null;
if (!$oContact) {
$oContact = new \RainLoop\Providers\AddressBook\Classes\Contact();
}
$oContact->setVCard($vCard);
$bResult = $oAddressBookProvider->ContactSave($oContact);
}
}
if (!empty($oContact->Etag))
{
$oContact->Etag = \md5($oContact->ToVCard());
}
$oContact->PopulateDisplayAndFullNameValue(true);
$bResult = $oAddressBookProvider->ContactSave($oContact);
}
return $this->DefaultResponse(__FUNCTION__, array(
'RequestUid' => $sRequestUid,
'ResultID' => $bResult ? $oContact->IdContact : '',
'ResultID' => $bResult ? $oContact->id : '',
'Result' => $bResult
));
}

View file

@ -4,14 +4,14 @@ namespace RainLoop\Providers;
use RainLoop\Providers\AddressBook\Enumerations\PropertyType as PropertyType;
class AddressBook extends \RainLoop\Providers\AbstractProvider
class AddressBook extends AbstractProvider
{
/**
* @var \RainLoop\Providers\AddressBook\AddressBookInterface
*/
private $oDriver;
public function __construct(?\RainLoop\Providers\AddressBook\AddressBookInterface $oDriver)
public function __construct(?AddressBook\AddressBookInterface $oDriver)
{
$this->oDriver = $oDriver;
}
@ -37,7 +37,7 @@ class AddressBook extends \RainLoop\Providers\AbstractProvider
return $this->IsActive() ? $this->oDriver->Export($sType) : false;
}
public function ContactSave(\RainLoop\Providers\AddressBook\Classes\Contact $oContact) : bool
public function ContactSave(AddressBook\Classes\Contact $oContact) : bool
{
return $this->IsActive() ? $this->oDriver->ContactSave($oContact) : false;
}
@ -54,10 +54,15 @@ class AddressBook extends \RainLoop\Providers\AbstractProvider
public function GetContacts(int $iOffset = 0, int $iLimit = 20, string $sSearch = '', int &$iResultCount = 0) : array
{
return $this->IsActive() ? $this->oDriver->GetContacts($iOffset, $iLimit, $sSearch, $iResultCount) : array();
return $this->IsActive() ? $this->oDriver->GetContacts(
\max(0, $iOffset),
0 < $iLimit ? $iLimit : 20,
\trim($sSearch),
$iResultCount
) : array();
}
public function GetContactByID($mID, bool $bIsStrID = false) : ?\RainLoop\Providers\AddressBook\Classes\Contact
public function GetContactByID($mID, bool $bIsStrID = false) : ?AddressBook\Classes\Contact
{
return $this->IsActive() ? $this->oDriver->GetContactByID($mID, $bIsStrID) : null;
}
@ -75,185 +80,36 @@ class AddressBook extends \RainLoop\Providers\AbstractProvider
return $this->IsActive() ? $this->oDriver->IncFrec($aEmails, $bCreateAuto) : false;
}
private 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::FULLNAME,
'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
);
$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;
}
public function ImportCsvArray(array $aCsvData) : int
{
$iCount = 0;
if ($this->IsActive() && \count($aCsvData))
{
$oContact = new \RainLoop\Providers\AddressBook\Classes\Contact();
foreach ($aCsvData as $aItem)
{
\MailSo\Base\Utils::ResetTimeLimit();
foreach ($aItem as $sItemName => $sItemValue)
{
$sItemName = \trim($sItemName);
$sItemValue = \trim($sItemValue);
if (!empty($sItemName) && !empty($sItemValue))
{
$mData = $this->csvNameToTypeConvertor($sItemName);
$iType = \is_array($mData) ? $mData[0] : $mData;
if (PropertyType::UNKNOWN !== $iType)
{
$oProp = new \RainLoop\Providers\AddressBook\Classes\Property();
$oProp->Type = $iType;
$oProp->Value = $sItemValue;
$oProp->TypeStr = \is_array($mData) && !empty($mData[1]) ? $mData[1] : '';
$oContact->Properties[] = $oProp;
}
}
if ($this->IsActive()) {
foreach (Utils::CsvArrayToVCards($aCsvData) as $oContact) {
if ($this->ContactSave($oContact)) {
$iCount++;
}
if ($oContact && \count($oContact->Properties))
{
if ($this->ContactSave($oContact))
{
$iCount++;
}
}
$oContact->Clear();
}
unset($oContact);
}
return $iCount;
}
public function ImportVcfFile(string $sVcfData) : int
{
$iCount = 0;
if ($this->IsActive() && \is_string($sVcfData))
{
$sVcfData = \trim($sVcfData);
if ("\xef\xbb\xbf" === \substr($sVcfData, 0, 3))
{
$sVcfData = \substr($sVcfData, 3);
}
$oVCardSplitter = null;
if ($this->IsActive()) {
try
{
$oVCardSplitter = new \Sabre\VObject\Splitter\VCard($sVcfData);
foreach (Utils::VcfFileToVCards($sVcfData) as $oContact) {
if ($this->ContactSave($oContact)) {
++$iCount;
}
}
}
catch (\Throwable $oExc)
{
$this->Logger()->WriteException($oExc);
}
if ($oVCardSplitter)
{
$oContact = new \RainLoop\Providers\AddressBook\Classes\Contact();
$oVCard = null;
while ($oVCard = $oVCardSplitter->getNext())
{
if ($oVCard instanceof \Sabre\VObject\Component\VCard)
{
\MailSo\Base\Utils::ResetTimeLimit();
$oContact->PopulateByVCard($oVCard);
if (\count($oContact->Properties))
{
if ($this->ContactSave($oContact))
{
$iCount++;
}
}
$oContact->Clear();
}
}
}
}
return $iCount;
}
}

View file

@ -24,6 +24,10 @@ interface AddressBookInterface
public function GetSuggestions(string $sSearch, int $iLimit = 20) : array;
/**
* Add/increment email address usage
* Handy for "most used" sorting suggestions in PdoAddressBook
*/
public function IncFrec(array $aEmails, bool $bCreateAuto = true) : bool;
public function Test() : string;

View file

@ -12,616 +12,75 @@ class Contact implements \JsonSerializable
/**
* @var string
*/
public $IdContact;
public $id = '';
/**
* @var string
*/
public $IdContactStr;
/**
* @var string
*/
public $Display;
public $IdContactStr = '';
/**
* @var int
*/
public $Changed;
/**
* @var array
*/
public $Properties;
/**
* @var bool
*/
public $ReadOnly;
public $ReadOnly = false;
/**
* @var int
* @var \Sabre\VObject\Component\VCard
*/
public $IdPropertyFromSearch;
protected $vCard = null;
/**
* @var string
*/
public $Etag;
public function __construct()
function __construct()
{
$this->Clear();
}
public function Clear() : void
{
$this->IdContact = '';
$this->IdContactStr = '';
$this->Display = '';
$this->Changed = \time();
$this->Properties = array();
$this->ReadOnly = false;
$this->IdPropertyFromSearch = 0;
$this->Etag = '';
}
public function PopulateDisplayAndFullNameValue(bool $bForceFullNameReplace = false) : void
function __get($k)
{
$sFullName = '';
$sLastName = '';
$sFirstName = '';
$sEmail = '';
$sOther = '';
$oFullNameProperty = null;
foreach ($this->Properties as /* @var $oProperty \RainLoop\Providers\AddressBook\Classes\Property */ $oProperty)
{
if ($oProperty)
{
$oProperty->UpdateDependentValues();
if (!$oFullNameProperty && PropertyType::FULLNAME === $oProperty->Type)
{
$oFullNameProperty =& $oProperty;
}
if (\strlen($oProperty->Value))
{
if ('' === $sEmail && $oProperty->IsEmail())
{
$sEmail = $oProperty->Value;
}
else if ('' === $sLastName && PropertyType::LAST_NAME === $oProperty->Type)
{
$sLastName = $oProperty->Value;
}
else if ('' === $sFirstName && PropertyType::FIRST_NAME === $oProperty->Type)
{
$sFirstName = $oProperty->Value;
}
else if ('' === $sFullName && PropertyType::FULLNAME === $oProperty->Type)
{
$sFullName = $oProperty->Value;
}
else if ('' === $sOther && \in_array($oProperty->Type, array(
PropertyType::PHONE
)))
{
$sOther = $oProperty->Value;
}
}
}
}
$sDisplay = $bForceFullNameReplace ? '' : \trim($sFullName);
if ('' === $sDisplay && (\strlen($sLastName) || \strlen($sFirstName)))
{
$sDisplay = \trim($sFirstName.' '.$sLastName);
}
if ('' === $sDisplay)
{
$sDisplay = \trim($sFullName);
}
if ('' === $sDisplay)
{
$sDisplay = \trim($sEmail);
}
if ('' === $sDisplay)
{
$sDisplay = \trim($sOther);
}
$this->Display = \trim($sDisplay);
if ($oFullNameProperty)
{
$oFullNameProperty->Value = $this->Display;
$oFullNameProperty->UpdateDependentValues();
}
if (!$oFullNameProperty)
{
$this->Properties[] = new \RainLoop\Providers\AddressBook\Classes\Property(PropertyType::FULLNAME, $this->Display);
if ('vCard' === $k) {
return $this->vCard;
}
}
public function UpdateDependentValues() : void
{
if (empty($this->IdContactStr))
{
$this->IdContactStr = \SnappyMail\UUID::generate();
}
$this->PopulateDisplayAndFullNameValue();
}
/*
public function GetEmails() : array
{
$aResult = array();
foreach ($this->Properties as /* @var $oProperty \RainLoop\Providers\AddressBook\Classes\Property */ $oProperty)
{
if ($oProperty && $oProperty->IsEmail())
{
$aResult[] = $oProperty->Value;
}
foreach ($this->vCard->EMAIL as $oProperty) {
$aResult[] = $oProperty->Value;
}
return \array_unique($aResult);
}
public function CardDavNameUri() : string
*/
public function setVCard(\Sabre\VObject\Component\VCard $oVCard) : void
{
return $this->IdContactStr.'.vcf';
}
public function SetUID(string $value) : void
{
foreach ($this->Properties as $oProperty) {
if ($oProperty && PropertyType::UID == $oProperty->Type) {
$oProperty->Value = $value;
return;
}
$aWarnings = $oVCard->validate(3);
// \error_log(\print_r($aWarnings,1));
$this->vCard = $oVCard;
$sUid = (string) $oVCard->UID;
if (empty($sUid)) {
$sUid = \SnappyMail\UUID::generate();
// $this->vCard->UID = $sUid;
}
$this->Properties[] = new Property(PropertyType::UID, $value);
}
public function GetUID() : string
{
foreach ($this->Properties as $oProperty) {
if ($oProperty && PropertyType::UID == $oProperty->Type) {
return $oProperty->Value;
}
}
return '';
}
public function fillVCard(\Sabre\VObject\Component\VCard $oVCard) : void
{
$oVCard->VERSION = '3.0';
$oVCard->PRODID = 'SnappyMail-'.APP_VERSION;
unset($oVCard->FN, $oVCard->EMAIL, $oVCard->TEL, $oVCard->URL, $oVCard->NICKNAME);
$sUid = $sFirstName = $sLastName = $sMiddleName = $sSuffix = $sPrefix = '';
foreach ($this->Properties as /* @var $oProperty \RainLoop\Providers\AddressBook\Classes\Property */ $oProperty)
{
if ($oProperty)
{
$sAddKey = '';
switch ($oProperty->Type)
{
case PropertyType::FULLNAME:
$oVCard->FN = $oProperty->Value;
break;
case PropertyType::NICK_NAME:
$oVCard->NICKNAME = $oProperty->Value;
break;
case PropertyType::NOTE:
$oVCard->NOTE = $oProperty->Value;
break;
case PropertyType::UID:
$sUid = $oProperty->Value;
break;
case PropertyType::FIRST_NAME:
$sFirstName = $oProperty->Value;
break;
case PropertyType::LAST_NAME:
$sLastName = $oProperty->Value;
break;
case PropertyType::MIDDLE_NAME:
$sMiddleName = $oProperty->Value;
break;
case PropertyType::NAME_SUFFIX:
$sSuffix = $oProperty->Value;
break;
case PropertyType::NAME_PREFIX:
$sPrefix = $oProperty->Value;
break;
case PropertyType::EMAIl:
if (empty($sAddKey))
{
$sAddKey = 'EMAIL';
}
case PropertyType::WEB_PAGE:
if (empty($sAddKey))
{
$sAddKey = 'URL';
}
case PropertyType::PHONE:
if (empty($sAddKey))
{
$sAddKey = 'TEL';
}
$aTypes = $oProperty->TypesAsArray();
$oVCard->add($sAddKey, $oProperty->Value, \is_array($aTypes) && \count($aTypes) ? array('TYPE' => $aTypes) : null);
break;
}
}
}
$oVCard->UID = $sUid ?: \SnappyMail\UUID::generate();
$oVCard->N = array($sLastName, $sFirstName, $sMiddleName, $sPrefix, $sSuffix);
$oVCard->REV = \gmdate('Ymd', $this->Changed).'T'.\gmdate('His', $this->Changed).'Z';
}
public function ToVCard(string $sPreVCard = '', $oLogger = null) : string
{
$this->UpdateDependentValues();
if ("\xef\xbb\xbf" === \substr($sPreVCard, 0, 3)) {
$sPreVCard = \substr($sPreVCard, 3);
}
$oVCard = null;
if (\strlen($sPreVCard)) {
try
{
$oVCard = \Sabre\VObject\Reader::read($sPreVCard);
}
catch (\Throwable $oExc)
{
if ($oLogger) {
$oLogger->WriteException($oExc);
$oLogger->WriteDump($sPreVCard);
}
}
}
// if ($oLogger) {
// $oLogger->WriteDump($sPreVCard);
// }
$oVCard = $oVCard ?: new \Sabre\VObject\Component\VCard();
$this->fillVCard($oVCard);
return (string) $oVCard->serialize();
}
public function ToCsv(bool $bWithHeader = false) : string
{
$aData = array();
if ($bWithHeader)
{
$aData[] = 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'
);
}
$aValues = array(
'', // 0 'Title',
'', // 1 'First Name',
'', // 2 'Middle Name',
'', // 3 'Last Name',
'', // 4 'Nick Name',
'', // 5 'Display Name',
'', // 6 'Company',
'', // 7 'Department',
'', // 8 'Job Title',
'', // 9 'Office Location',
'', // 10 'E-mail Address',
'', // 11 'Notes',
'', // 12 'Web Page',
'', // 13 'Birthday',
'', // 14 'Other Email',
'', // 15 'Other Phone',
'', // 16 'Other Mobile',
'', // 17 'Mobile Phone',
'', // 18 'Home Email',
'', // 19 'Home Phone',
'', // 20 'Home Fax',
'', // 21 'Home Street',
'', // 22 'Home City',
'', // 23 'Home State',
'', // 24 'Home Postal Code',
'', // 25 'Home Country',
'', // 26 'Business Email',
'', // 27 'Business Phone',
'', // 28 'Business Fax',
'', // 29 'Business Street',
'', // 30 'Business City',
'', // 31 'Business State',
'', // 32 'Business Postal Code',
'' // 33 'Business Country'
);
$this->UpdateDependentValues();
foreach ($this->Properties as /* @var $oProperty \RainLoop\Providers\AddressBook\Classes\Property */ $oProperty)
{
$iIndex = -1;
if ($oProperty)
{
$aUpperTypes = $oProperty->TypesUpperAsArray();
switch ($oProperty->Type)
{
case PropertyType::FULLNAME:
$iIndex = 5;
break;
case PropertyType::NICK_NAME:
$iIndex = 4;
break;
case PropertyType::FIRST_NAME:
$iIndex = 1;
break;
case PropertyType::LAST_NAME:
$iIndex = 3;
break;
case PropertyType::MIDDLE_NAME:
$iIndex = 2;
break;
case PropertyType::EMAIl:
switch (true)
{
case \in_array('OTHER', $aUpperTypes):
$iIndex = 14;
break;
case \in_array('WORK', $aUpperTypes):
$iIndex = 26;
break;
default:
$iIndex = 18;
break;
}
break;
case PropertyType::PHONE:
switch (true)
{
case \in_array('OTHER', $aUpperTypes):
$iIndex = 15;
break;
case \in_array('WORK', $aUpperTypes):
$iIndex = 27;
break;
case \in_array('MOBILE', $aUpperTypes):
$iIndex = 17;
break;
default:
$iIndex = 19;
break;
}
break;
case PropertyType::WEB_PAGE:
$iIndex = 12;
break;
case PropertyType::NOTE:
$iIndex = 11;
break;
}
if (-1 < $iIndex)
{
$aValues[$iIndex] = $oProperty->Value;
}
}
}
// subfix
if (empty($aValues[10])) // 'E-mail Address'
{
if (!empty($aValues[18]))
{
$aValues[10] = $aValues[18];
}
else if (!empty($aValues[26]))
{
$aValues[10] = $aValues[26];
}
else if (!empty($aValues[14]))
{
$aValues[10] = $aValues[14];
}
}
$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;
}
/**
* @param mixed $oProp
*/
private function getPropertyValueHelper($oProp, bool $bOldVersion) : string
{
$sValue = \trim($oProp);
if ($bOldVersion && !isset($oProp->parameters['CHARSET']))
{
if (\strlen($sValue))
{
$sEncValue = \utf8_encode($sValue);
if (0 === \strlen($sEncValue))
{
$sEncValue = $sValue;
}
$sValue = $sEncValue;
}
}
return \MailSo\Base\Utils::Utf8Clear($sValue);
}
/**
* @param mixed $oProp
*/
private function addArrayPropertyHelper(array &$aProperties, $oArrayProp, int $iType) : void
{
foreach ($oArrayProp as $oProp)
{
$oTypes = $oProp ? $oProp['TYPE'] : null;
$aTypes = $oTypes ? $oTypes->getParts() : array();
$sValue = $oProp ? \trim($oProp->getValue()) : '';
if (\strlen($sValue))
{
if (!$oTypes || $oTypes->has('PREF'))
{
\array_unshift($aProperties, new Property($iType, $sValue, \implode(',', $aTypes)));
}
else
{
\array_push($aProperties, new Property($iType, $sValue, \implode(',', $aTypes)));
}
}
}
}
public function PopulateByVCard(\Sabre\VObject\Component\VCard $oVCard, string $sEtag = '') : bool
{
$this->Properties = array();
if (!empty($sEtag))
{
$this->Etag = $sEtag;
}
$aProperties = array();
$bOldVersion = empty($oVCard->VERSION) ? false :
\in_array((string) $oVCard->VERSION, array('2.1', '2.0', '1.0'));
if (isset($oVCard->FN) && '' !== \trim($oVCard->FN))
{
$sValue = $this->getPropertyValueHelper($oVCard->FN, $bOldVersion);
$aProperties[] = new Property(PropertyType::FULLNAME, $sValue);
}
if (isset($oVCard->NICKNAME) && '' !== \trim($oVCard->NICKNAME))
{
$sValue = $sValue = $this->getPropertyValueHelper($oVCard->NICKNAME, $bOldVersion);
$aProperties[] = new Property(PropertyType::NICK_NAME, $sValue);
}
if (isset($oVCard->NOTE) && '' !== \trim($oVCard->NOTE))
{
$sValue = $this->getPropertyValueHelper($oVCard->NOTE, $bOldVersion);
$aProperties[] = new Property(PropertyType::NOTE, $sValue);
}
if (isset($oVCard->N))
{
$aNames = $oVCard->N->getParts();
foreach ($aNames as $iIndex => $sValue)
{
$sValue = \trim($sValue);
if ($bOldVersion && !isset($oVCard->N->parameters['CHARSET']))
{
if (\strlen($sValue))
{
$sEncValue = \utf8_encode($sValue);
if (0 === \strlen($sEncValue))
{
$sEncValue = $sValue;
}
$sValue = $sEncValue;
}
}
$sValue = \MailSo\Base\Utils::Utf8Clear($sValue);
switch ($iIndex) {
case 0:
$aProperties[] = new Property(PropertyType::LAST_NAME, $sValue);
break;
case 1:
$aProperties[] = new Property(PropertyType::FIRST_NAME, $sValue);
break;
case 2:
$aProperties[] = new Property(PropertyType::MIDDLE_NAME, $sValue);
break;
case 3:
$aProperties[] = new Property(PropertyType::NAME_PREFIX, $sValue);
break;
case 4:
$aProperties[] = new Property(PropertyType::NAME_SUFFIX, $sValue);
break;
}
}
}
if (isset($oVCard->EMAIL))
{
$this->addArrayPropertyHelper($aProperties, $oVCard->EMAIL, PropertyType::EMAIl);
}
if (isset($oVCard->URL))
{
$this->addArrayPropertyHelper($aProperties, $oVCard->URL, PropertyType::WEB_PAGE);
}
if (isset($oVCard->TEL))
{
$this->addArrayPropertyHelper($aProperties, $oVCard->TEL, PropertyType::PHONE);
}
$this->IdContactStr = $oVCard->UID ? (string) $oVCard->UID : \SnappyMail\UUID::generate();
$this->Properties = $aProperties;
$this->SetUID($this->IdContactStr);
$this->UpdateDependentValues();
return true;
/*
$rev = new \DateTime($oVCard->REV[0]->getJsonValue());
$this->Changed = $rev->getTimestamp();
*/
$this->IdContactStr = $sUid;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
$properties = array();
foreach ($this->Properties as $property) {
$properties[] = $property->jsonSerialize();
}
return array(
'@Object' => 'Object/Contact',
'id' => $this->IdContact,
'display' => \MailSo\Base\Utils::Utf8Clear($this->Display),
'id' => $this->id,
'readOnly' => $this->ReadOnly,
'IdPropertyFromSearch' => $this->IdPropertyFromSearch,
'properties' => $properties
'jCard' => $this->vCard
);
}
}

View file

@ -9,7 +9,7 @@ class Property implements \JsonSerializable
/**
* @var int
*/
public $IdProperty;
public $IdProperty = 0;
/**
* @var int
@ -29,42 +29,25 @@ class Property implements \JsonSerializable
/**
* @var string
*/
public $ValueLower;
public $ValueLower = '';
/**
* @var string
*/
public $ValueCustom;
public $ValueCustom = '';
/**
* @var int
*/
public $Frec;
public $Frec = 0;
public function __construct(
$iType = \RainLoop\Providers\AddressBook\Enumerations\PropertyType::UNKNOWN, $sValue = '', $sTypeStr = '')
public function __construct(int $iType = PropertyType::UNKNOWN, string $sValue = '', string $sTypeStr = '')
{
$this->Clear();
$this->Type = $iType;
$this->Value = $sValue;
$this->TypeStr = $sTypeStr;
}
public function Clear() : void
{
$this->IdProperty = 0;
$this->Type = PropertyType::UNKNOWN;
$this->TypeStr = '';
$this->Value = '';
$this->ValueLower = '';
$this->ValueCustom = '';
$this->Frec = 0;
}
public function IsName() : bool
{
return \in_array($this->Type, array(PropertyType::FULLNAME, PropertyType::FIRST_NAME,

View file

@ -1,44 +0,0 @@
<?php
namespace RainLoop\Providers\AddressBook\Classes;
class Tag implements \JsonSerializable
{
/**
* @var string
*/
public $IdContactTag;
/**
* @var string
*/
public $Name;
/**
* @var bool
*/
public $ReadOnly;
public function __construct()
{
$this->Clear();
}
public function Clear() : void
{
$this->IdContactTag = '';
$this->Name = '';
$this->ReadOnly = false;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return array(
'@Object' => 'Object/Tag',
'id' => $this->IdContactTag,
'name' => \MailSo\Base\Utils::Utf8Clear($mResponse->Name),
'readOnly' => $this->ReadOnly
);
}
}

View file

@ -32,4 +32,6 @@ class PropertyType
const NOTE = 110;
const CUSTOM = 250;
const JCARD = 251;
}

View file

@ -0,0 +1,320 @@
<?php
namespace RainLoop\Providers\AddressBook;
use
RainLoop\Providers\AddressBook\Enumerations\PropertyType,
RainLoop\Providers\AddressBook\Classes\Property
;
class Utils
{
/**
* @param mixed $oProp
*/
private static function yieldPropertyHelper($oArrayProp, int $iType) : iterable
{
$aTmp = [];
foreach ($oArrayProp as $oProp) {
$sValue = \trim($oProp->getValue());
if (\strlen($sValue)) {
$oTypes = $oProp['TYPE'];
$aTypes = $oTypes ? $oTypes->getParts() : array();
$pref = 100;
if (0 < $oProp['PREF']) {
$pref = (int) $oProp['PREF'];
}
$aTmp[\substr(1000+$pref,-3) . $sValue] = new Property($iType, $sValue, \implode(',', $aTypes));
}
}
\ksort($aTmp);
foreach ($aTmp as $oProp) {
yield $oProp;
}
}
/**
* @param mixed $oProp
*/
private static function getPropertyValueHelper($oProp, bool $bOldVersion) : string
{
$sValue = \trim($oProp);
if ($bOldVersion && !isset($oProp->parameters['CHARSET'])) {
if (\strlen($sValue)) {
$sEncValue = \utf8_encode($sValue);
if (\strlen($sEncValue)) {
$sValue = $sEncValue;
}
}
}
return \MailSo\Base\Utils::Utf8Clear($sValue);
}
public static function VCardToProperties(\Sabre\VObject\Component\VCard $oVCard) : iterable
{
yield new Property(PropertyType::JCARD, \json_encode($oVCard));
$bOldVersion = !empty($oVCard->VERSION) && \in_array((string) $oVCard->VERSION, array('2.1', '2.0', '1.0'));
if (isset($oVCard->FN) && '' !== \trim($oVCard->FN)) {
$sValue = static::getPropertyValueHelper($oVCard->FN, $bOldVersion);
yield new Property(PropertyType::FULLNAME, $sValue);
}
if (isset($oVCard->N)) {
$aNames = $oVCard->N->getParts();
foreach ($aNames as $iIndex => $sValue) {
$sValue = \trim($sValue);
if ($bOldVersion && !isset($oVCard->N->parameters['CHARSET'])) {
if (\strlen($sValue)) {
$sEncValue = \utf8_encode($sValue);
if (\strlen($sEncValue)) {
$sValue = $sEncValue;
}
}
}
$sValue = \MailSo\Base\Utils::Utf8Clear($sValue);
if ($sValue) {
switch ($iIndex) {
case 0:
yield new Property(PropertyType::LAST_NAME, $sValue);
break;
case 1:
yield new Property(PropertyType::FIRST_NAME, $sValue);
break;
case 2:
yield new Property(PropertyType::MIDDLE_NAME, $sValue);
break;
case 3:
yield new Property(PropertyType::NAME_PREFIX, $sValue);
break;
case 4:
yield new Property(PropertyType::NAME_SUFFIX, $sValue);
break;
}
}
}
}
if (isset($oVCard->EMAIL)) {
yield from static::yieldPropertyHelper($oVCard->EMAIL, PropertyType::EMAIl);
}
if (isset($oVCard->URL)) {
yield from static::yieldPropertyHelper($oVCard->URL, PropertyType::WEB_PAGE);
}
if (isset($oVCard->TEL)) {
yield from static::yieldPropertyHelper($oVCard->TEL, PropertyType::PHONE);
}
}
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
);
$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 CsvArrayToVCards(array $aCsvData) : iterable
{
foreach ($aCsvData as $aItem) {
$oContact = new Classes\Contact();
\MailSo\Base\Utils::ResetTimeLimit();
foreach ($aItem as $sItemName => $sItemValue) {
$sItemName = \trim($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 (\count($oContact->Properties)) {
yield $oContact;
}
}
}
/**
* TODO: broken
*/
public static function VCardToCsv(Classes\Contact $oContact, bool $bWithHeader = false) : string
{
$aData = array();
if ($bWithHeader) {
$aData[] = 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'
);
}
$oVCard = $oContact->vCard;
$adrHome = $oVCard->getByType('ADR', 'HOME');
$adrHome = $adrHome ? $adrHome->getParts() : ['','','','','','',''];
$adrWork = $oVCard->getByType('ADR', 'WORK');
$adrWork = $adrWork ? $adrWork->getParts() : ['','','','','','',''];
$aValues = array(
'', // Title
'', // First Name
'', // Middle Name
'', // Last Name
(string) $oVCard->NICKNAME, // Nick Name
(string) $oVCard->FN, // Display Name
(string) $oVCard->ORG, // Company
'', // Department
'', // Job Title
'', // Office Location
(string) $oVCard->EMAIL, // E-mail Address
(string) $oVCard->NOTE, // Notes
(string) $oVCard->URL, // Web Page
'', // Birthday
'', // Other Email
'', // Other Phone
'', // Other Mobile
(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
$adrHome[5], // Postal Code
$adrHome[6], // Country
// 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 VcfFileToVCards(string $sVcfData) : iterable
{
$sVcfData = \trim($sVcfData);
if ("\xef\xbb\xbf" === \substr($sVcfData, 0, 3)) {
$sVcfData = \substr($sVcfData, 3);
}
$oVCardSplitter = new \Sabre\VObject\Splitter\VCard($sVcfData);
if ($oVCardSplitter) {
while ($oVCard = $oVCardSplitter->getNext()) {
if ($oVCard instanceof \Sabre\VObject\Component\VCard) {
\MailSo\Base\Utils::ResetTimeLimit();
$oContact = new Classes\Contact();
$oContact->setVCard($oVCard);
yield $oContact;
}
}
}
}
}

View file

@ -175,8 +175,6 @@
"LABEL_PHONE": "Phone",
"LABEL_WEB": "Web",
"LABEL_BIRTHDAY": "Birthday",
"LINK_ADD_EMAIL": "Add an email address",
"LINK_ADD_PHONE": "Add a phone",
"LINK_BIRTHDAY": "Birthday",
"PLACEHOLDER_ENTER_DISPLAY_NAME": "Enter display name",
"PLACEHOLDER_ENTER_LAST_NAME": "Enter last name",

View file

@ -72,40 +72,41 @@
<!-- ko template: { name: 'Paginator', data: contactsPaginator } --><!-- /ko -->
</div>
</div>
<div class="b-view-content-toolbar btn-toolbar" data-bind="css: {'read-only': viewReadOnly}">
<div class="b-view-content-toolbar btn-toolbar" data-bind="css: {'read-only': viewReadOnly}, i18nUpdate: contact">
<!-- ko with: contact -->
<div class="btn-group">
<button class="btn button-save-contact" data-bind="visible: !emptySelection(), command: saveCommand, css: {'dirty': watchDirty}">
<i data-bind="css: {'icon-ok': !viewSaving(), 'icon-spinner': viewSaving()}"></i>
<span data-i18n="CONTACTS/BUTTON_CREATE_CONTACT" data-bind="visible: '' === viewID()"></span>
<span data-i18n="CONTACTS/BUTTON_UPDATE_CONTACT" data-bind="visible: '' !== viewID()"></span>
<button class="btn button-save-contact" data-bind="command: $root.saveCommand, css: {'dirty': $root.watchDirty}">
<i data-bind="css: {'icon-ok': !$root.viewSaving(), 'icon-spinner': $root.viewSaving()}"></i>
<span data-i18n="CONTACTS/BUTTON_CREATE_CONTACT" data-bind="visible: !id"></span>
<span data-i18n="CONTACTS/BUTTON_UPDATE_CONTACT" data-bind="visible: id"></span>
</button>
</div>
<div class="btn-group dropdown" data-bind="visible: !emptySelection(), registerBootstrapDropdown: true">
<div class="btn-group dropdown" data-bind="registerBootstrapDropdown: true">
<a id="button-add-prop-dropdown-id" href="#" tabindex="-1" class="btn dropdown-toggle">
<span data-i18n="CONTACTS/ADD_MENU_LABEL"></span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu right-edge" style="text-align: left" tabindex="-1" role="menu" aria-labelledby="button-add-prop-dropdown-id">
<li role="presentation">
<a href="#" tabindex="-1" data-bind="click: addNewEmail">
<a href="#" tabindex="-1" data-bind="click: addEmail">
<i class="icon-none"></i>
<span data-i18n="GLOBAL/EMAIL"></span>
</a>
</li>
<li role="presentation">
<a href="#" tabindex="-1" data-bind="click: addNewPhone">
<a href="#" tabindex="-1" data-bind="click: addTel">
<i class="icon-none"></i>
<span data-i18n="CONTACTS/ADD_MENU_PHONE"></span>
</a>
</li>
<li role="presentation">
<a href="#" tabindex="-1" data-bind="click: addNewWeb">
<a href="#" tabindex="-1" data-bind="click: addUrl">
<i class="icon-none"></i>
<span data-i18n="CONTACTS/ADD_MENU_URL"></span>
</a>
</li>
<li class="dividerbar" role="presentation">
<a href="#" tabindex="-1" data-bind="click: addNewNickname">
<a href="#" tabindex="-1" data-bind="click: addNickname">
<i class="icon-none"></i>
<span data-i18n="CONTACTS/ADD_MENU_NICKNAME"></span>
</a>
@ -118,81 +119,82 @@
</li>-->
</ul>
</div>
<!-- /ko -->
</div>
<div class="b-view-content" data-bind="css: {'read-only': viewReadOnly}">
<div class="b-contact-view-desc" data-bind="visible: emptySelection"
<div class="b-contact-view-desc" data-bind="visible: !contact"
data-i18n="CONTACTS/CONTACT_VIEW_DESC"></div>
<div data-bind="visible: !emptySelection()">
<div data-bind="visible: contact, i18nUpdate: contact">
<div class="form-horizontal top-part">
<div class="control-group" data-bind="visible: !viewReadOnly() || contactHasValidName()">
<div class="control-group" data-bind="visible: !viewReadOnly() || hasValidName()">
<label class="fontastic iconsize24">👤</label>
<div>
<div data-bind="foreach: viewPropertiesNames">
<div class="property-line">
<span data-bind="text: value"></span>
<input type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup', attr: {'placeholder': placeholderValue}">
</div>
<!-- ko with: contact -->
<div class="property-line">
<span data-bind="text: givenName"></span>
<input type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: givenName" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_FIRST_NAME">
</div>
<div data-bind="visible: viewPropertiesOther().length, foreach: viewPropertiesOther">
<div class="property-line">
<!-- ko if: !largeValue() -->
<span data-bind="text: value"></span>
<input type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup', attr: {'placeholder': placeholderValue}">
<!-- /ko -->
<!-- ko if: largeValue -->
<span data-bind="text: value"></span>
<textarea
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup', attr: {'placeholder': placeholderValue}"></textarea>
<!-- /ko -->
</div>
<div class="property-line">
<span data-bind="text: surName"></span>
<input type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: surName" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_LAST_NAME">
</div>
<div class="property-line" data-bind="visible: null != nickname()">
<span data-bind="text: nickname"></span>
<input type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: nickname" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_NICK_NAME">
</div>
<!-- /ko -->
</div>
</div>
<div class="control-group" data-bind="visible: !viewReadOnly() || viewPropertiesEmails().length">
<!-- ko with: contact -->
<div class="control-group" data-bind="email().length">
<label class="fontastic iconsize24" data-i18n="[title]GLOBAL/EMAIL">@</label>
<div>
<div data-bind="foreach: viewPropertiesEmails">
<div data-bind="foreach: email">
<div class="property-line">
<span data-bind="text: value"></span>
<input type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup'">
data-bind="value: value">
</div>
</div>
<a href="#" class="g-ui-link add-link" data-bind="visible: !viewReadOnly(), click: addNewEmail" data-i18n="CONTACTS/LINK_ADD_EMAIL"></a>
<a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: addEmail"></a>
</div>
</div>
<div class="control-group" data-bind="visible: viewPropertiesPhones().length">
<div class="control-group" data-bind="visible: tel().length">
<label class="fontastic iconsize24" data-i18n="[title]CONTACTS/LABEL_PHONE">📞</label>
<div>
<div data-bind="foreach: viewPropertiesPhones">
<div data-bind="foreach: tel">
<div class="property-line">
<span data-bind="text: value"></span>
<input type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup'">
data-bind="value: value">
</div>
</div>
<a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: $root.addTel"></a>
</div>
</div>
<div class="control-group" data-bind="visible: viewPropertiesWeb().length">
<div class="control-group" data-bind="visible: url().length">
<label class="fontastic iconsize24" data-i18n="[title]CONTACTS/LABEL_WEB">🌍</label>
<div>
<div data-bind="foreach: viewPropertiesWeb">
<div data-bind="foreach: url">
<div class="property-line">
<span data-bind="text: value"></span>
<input type="text" placeholder="https://"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup'">
data-bind="value: value">
</div>
</div>
<a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: $root.addUrl"></a>
</div>
</div>
<!-- /ko -->
</div>
<!--
<div class="e-read-only-sign fontastic iconsize24" data-i18n="[title]CONTACTS/LABEL_READ_ONLY">🔒</div>