From a71874a8c40e01b48c38e0f3de48e7476213f31f Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 29 May 2022 00:22:50 +0200 Subject: [PATCH] Redesign AddressBook --- dev/DAV/JCard.js | 227 +++++ dev/DAV/VCardProperty.js | 152 +++ dev/Model/Contact.js | 200 ++-- dev/Model/FolderCollection.js | 2 +- dev/Styles/User/Contacts.less | 7 - dev/View/Popup/Contacts.js | 145 +-- plugins/kolab/KolabAddressBook.php | 67 +- .../libraries/RainLoop/Actions/Contacts.php | 60 +- .../RainLoop/Providers/AddressBook.php | 184 +--- .../AddressBook/AddressBookInterface.php | 4 + .../Providers/AddressBook/Classes/Contact.php | 597 +---------- .../AddressBook/Classes/Property.php | 27 +- .../Providers/AddressBook/Classes/Tag.php | 44 - .../AddressBook/Enumerations/PropertyType.php | 2 + .../Providers/AddressBook/PdoAddressBook.php | 940 ++++++++---------- .../RainLoop/Providers/AddressBook/Utils.php | 320 ++++++ .../v/0.0.0/app/localization/en/user.json | 2 - .../templates/Views/User/PopupsContacts.html | 92 +- 18 files changed, 1403 insertions(+), 1669 deletions(-) create mode 100644 dev/DAV/JCard.js create mode 100644 dev/DAV/VCardProperty.js delete mode 100644 snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Tag.php create mode 100644 snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php diff --git a/dev/DAV/JCard.js b/dev/DAV/JCard.js new file mode 100644 index 000000000..38676cdac --- /dev/null +++ b/dev/DAV/JCard.js @@ -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; + } +} diff --git a/dev/DAV/VCardProperty.js b/dev/DAV/VCardProperty.js new file mode 100644 index 000000000..81c251422 --- /dev/null +++ b/dev/DAV/VCardProperty.js @@ -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 + ]; + } +} diff --git a/dev/Model/Contact.js b/dev/Model/Contact.js index edff787bd..61cfca038 100644 --- a/dev/Model/Contact.js +++ b/dev/Model/Contact.js @@ -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 */ diff --git a/dev/Model/FolderCollection.js b/dev/Model/FolderCollection.js index b3fec9770..ff47e6741 100644 --- a/dev/Model/FolderCollection.js +++ b/dev/Model/FolderCollection.js @@ -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(), diff --git a/dev/Styles/User/Contacts.less b/dev/Styles/User/Contacts.less index 620cf5b2f..6be67be6c 100644 --- a/dev/Styles/User/Contacts.less +++ b/dev/Styles/User/Contacts.less @@ -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; diff --git a/dev/View/Popup/Contacts.js b/dev/View/Popup/Contacts.js index 41b272222..073b664ed 100644 --- a/dev/View/Popup/Contacts.js +++ b/dev/View/Popup/Contacts.js @@ -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); diff --git a/plugins/kolab/KolabAddressBook.php b/plugins/kolab/KolabAddressBook.php index 39a111c46..5318de38c 100644 --- a/plugins/kolab/KolabAddressBook.php +++ b/plugins/kolab/KolabAddressBook.php @@ -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 diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php index 977a49d83..1d3a62e71 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php @@ -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 )); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php index 58135dce2..299acecbd 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php @@ -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; } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php index d91bbbeaa..d0ce8ef54 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php @@ -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; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php index 8fe930bfb..a93aff9a3 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php @@ -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 ); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Property.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Property.php index 3c41525e4..4c7e13433 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Property.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Property.php @@ -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, diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Tag.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Tag.php deleted file mode 100644 index a4f2f1102..000000000 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Tag.php +++ /dev/null @@ -1,44 +0,0 @@ -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 - ); - } -} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Enumerations/PropertyType.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Enumerations/PropertyType.php index 5b1bdae94..f9f2039bd 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Enumerations/PropertyType.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Enumerations/PropertyType.php @@ -32,4 +32,6 @@ class PropertyType const NOTE = 110; const CUSTOM = 250; + + const JCARD = 251; } 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 edc7fb373..a9c2a1a64 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 @@ -6,7 +6,7 @@ use RainLoop\Providers\AddressBook\Enumerations\PropertyType; class PdoAddressBook extends \RainLoop\Common\PdoAbstract - implements \RainLoop\Providers\AddressBook\AddressBookInterface + implements AddressBookInterface { use CardDAV; @@ -32,6 +32,13 @@ class PdoAddressBook */ private $sPassword; + private static $aSearchInFields = [ + PropertyType::EMAIl, + PropertyType::FIRST_NAME, + PropertyType::LAST_NAME, + PropertyType::NICK_NAME + ]; + public function __construct() { $oConfig = \RainLoop\Api::Config(); @@ -80,42 +87,26 @@ class PdoAddressBook return 0 < $this->iUserID; } - private function flushDeletedContacts(int $iUserID) : bool + private function flushDeletedContacts() : bool { return !!$this->prepareAndExecute('DELETE FROM rainloop_ab_contacts WHERE id_user = :id_user AND deleted = 1', array( - ':id_user' => array($iUserID, \PDO::PARAM_INT) + ':id_user' => array($this->iUserID, \PDO::PARAM_INT) )); } - private function updateContactEtagAndTime(int $iUserID, int $iID, string $sEtag, int $iChanged) : bool - { - return !!$this->prepareAndExecute('UPDATE rainloop_ab_contacts SET changed = :changed, etag = :etag '. - 'WHERE id_user = :id_user AND id_contact = :id_contact', array( - ':id_user' => array($iUserID, \PDO::PARAM_INT), - ':id_contact' => array($iID, \PDO::PARAM_INT), - ':changed' => array($iChanged, \PDO::PARAM_INT), - ':etag' => array($sEtag, \PDO::PARAM_STR) - ) - ); - } - - private function prepareDatabaseSyncData(int $iUserID) : array + private function prepareDatabaseSyncData() : array { $aResult = array(); $oStmt = $this->prepareAndExecute('SELECT id_contact, id_contact_str, changed, deleted, etag FROM rainloop_ab_contacts WHERE id_user = :id_user', array( - ':id_user' => array($iUserID, \PDO::PARAM_INT) + ':id_user' => array($this->iUserID, \PDO::PARAM_INT) )); - if ($oStmt) - { + if ($oStmt) { $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetch) && \count($aFetch)) - { - foreach ($aFetch as $aItem) - { + if (\is_array($aFetch) && \count($aFetch)) { + foreach ($aFetch as $aItem) { if ($aItem && isset($aItem['id_contact'], $aItem['id_contact_str'], $aItem['changed'], $aItem['deleted'], $aItem['etag']) && - !empty($aItem['id_contact_str'])) - { + !empty($aItem['id_contact_str'])) { $sKeyID = $aItem['id_contact_str']; $aResult[$sKeyID] = array( @@ -137,17 +128,17 @@ class PdoAddressBook public function Sync() : bool { +/* TODO: broken + $this->SyncDatabase(); - if (1 > $this->iUserID) - { + if (1 > $this->iUserID) { \SnappyMail\Log::warning('PdoAddressBook', 'Sync() invalid $iUserID'); return false; } $oClient = $this->getDavClient(); - if (!$oClient) - { + if (!$oClient) { \SnappyMail\Log::warning('PdoAddressBook', 'Sync() invalid DavClient'); return false; } @@ -156,25 +147,22 @@ class PdoAddressBook $time = \microtime(true); $aRemoteSyncData = $this->prepareDavSyncData($oClient, $sPath); - if (false === $aRemoteSyncData) - { + if (false === $aRemoteSyncData) { \SnappyMail\Log::info('PdoAddressBook', 'Sync() no data to sync'); return false; } $time = \microtime(true) - $time; \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Fetched remote data in {$time} seconds"]); - $aDatabaseSyncData = $this->prepareDatabaseSyncData($this->iUserID); + $aDatabaseSyncData = $this->prepareDatabaseSyncData(); // $this->oLogger->WriteDump($aRemoteSyncData); // $this->oLogger->WriteDump($aDatabaseSyncData); // Delete remote when Mode = read + write if ($this->isDAVReadWrite()) { - foreach ($aDatabaseSyncData as $sKey => $aData) - { - if ($aData['deleted'] && isset($aRemoteSyncData[$sKey], $aRemoteSyncData[$sKey]['vcf'])) - { + foreach ($aDatabaseSyncData as $sKey => $aData) { + if ($aData['deleted'] && isset($aRemoteSyncData[$sKey], $aRemoteSyncData[$sKey]['vcf'])) { \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Delete remote {$sKey}"]); $this->davClientRequest($oClient, 'DELETE', $sPath.$aRemoteSyncData[$sKey]['vcf']); } @@ -183,24 +171,20 @@ class PdoAddressBook // Delete from db $aIdsForDeletedion = array(); - foreach ($aDatabaseSyncData as $sKey => $aData) - { - if (!$aData['deleted'] && !empty($aData['etag']) && !isset($aRemoteSyncData[$sKey])) - { + foreach ($aDatabaseSyncData as $sKey => $aData) { + if (!$aData['deleted'] && !empty($aData['etag']) && !isset($aRemoteSyncData[$sKey])) { $aIdsForDeletedion[] = $aData['id_contact']; } } - if (\count($aIdsForDeletedion)) - { + if (\count($aIdsForDeletedion)) { \SnappyMail\HTTP\Stream::JSON(['messsage'=>'Delete local ' . \implode(', ', $aIdsForDeletedion)]); $this->DeleteContacts($aIdsForDeletedion, false); } - $this->flushDeletedContacts($this->iUserID); + $this->flushDeletedContacts(); //+++new or newer (from db) - foreach ($aDatabaseSyncData as $sKey => $aData) - { + foreach ($aDatabaseSyncData as $sKey => $aData) { if (!$aData['deleted'] && (empty($aData['etag']) && !isset($aRemoteSyncData[$sKey])) // new || @@ -208,20 +192,16 @@ class PdoAddressBook $aRemoteSyncData[$sKey]['etag'] !== $aData['etag'] && $aRemoteSyncData[$sKey]['changed'] < $aData['changed'] ) - ) - { + ) { \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Update remote {$sKey}"]); $mID = $aData['id_contact']; $oContact = $this->GetContactByID($mID); - if ($oContact) - { + if ($oContact) { $sExsistensBody = ''; $mExsistenRemoteID = isset($aRemoteSyncData[$sKey]['vcf']) && !empty($aData['etag']) ? $aRemoteSyncData[$sKey]['vcf'] : ''; - if (\strlen($mExsistenRemoteID)) - { + if (\strlen($mExsistenRemoteID)) { $oResponse = $this->davClientRequest($oClient, 'GET', $sPath.$mExsistenRemoteID); - if ($oResponse) - { + if ($oResponse) { $sExsistensBody = \trim($oResponse->body); } @@ -229,18 +209,23 @@ class PdoAddressBook } // Add remote when Mode = read + write - if ($this->isDAVReadWrite()) { + if ($sExsistensBody && $this->isDAVReadWrite()) { $oResponse = $this->davClientRequest($oClient, 'PUT', - $sPath.(\strlen($mExsistenRemoteID) ? $mExsistenRemoteID : $oContact->CardDavNameUri()), - $oContact->ToVCard($sExsistensBody, $this->oLogger)."\r\n\r\n"); - if ($oResponse) - { + $sPath.(\strlen($mExsistenRemoteID) ? $mExsistenRemoteID : $oContact->IdContactStr.'.vcf'), + $oContact->vCard->serialize() . "\r\n\r\n"); + if ($oResponse) { $sEtag = \trim(\trim($oResponse->getHeader('etag')), '"\''); $sDate = \trim($oResponse->getHeader('date')); - if (!empty($sEtag)) - { + if (!empty($sEtag)) { $iChanged = empty($sDate) ? \time() : \MailSo\Base\DateTimeHelper::ParseRFC2822DateString($sDate); - $this->updateContactEtagAndTime($this->iUserID, $mID, $sEtag, $iChanged); + $this->prepareAndExecute('UPDATE rainloop_ab_contacts SET changed = :changed, etag = :etag '. + 'WHERE id_user = :id_user AND id_contact = :id_contact', array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':id_contact' => array($mID, \PDO::PARAM_INT), + ':changed' => array($iChanged, \PDO::PARAM_INT), + ':etag' => array($sEtag, \PDO::PARAM_STR) + ) + ); } } } @@ -252,63 +237,54 @@ class PdoAddressBook //---new //+++new or newer (from carddav) - foreach ($aRemoteSyncData as $sKey => $aData) - { + foreach ($aRemoteSyncData as $sKey => $aData) { if (!isset($aDatabaseSyncData[$sKey]) // new || ($aDatabaseSyncData[$sKey]['etag'] !== $aData['etag'] && // newer $aDatabaseSyncData[$sKey]['changed'] < $aData['changed']) - ) - { + ) { \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Update local {$sKey}"]); $mExsistenContactID = isset($aDatabaseSyncData[$sKey]['id_contact']) ? $aDatabaseSyncData[$sKey]['id_contact'] : ''; $oResponse = $this->davClientRequest($oClient, 'GET', $sPath.$aData['vcf']); - if ($oResponse) - { + if ($oResponse) { $sBody = \trim($oResponse->body); // Remove UTF-8 BOM - if ("\xef\xbb\xbf" === \substr($sBody, 0, 3)) - { + if ("\xef\xbb\xbf" === \substr($sBody, 0, 3)) { $sBody = \substr($sBody, 3); } - if (!empty($sBody)) - { + if (!empty($sBody)) { $oVCard = null; - try - { + try { $oVCard = \Sabre\VObject\Reader::read($sBody); } - catch (\Throwable $oExc) - { - if ($this->oLogger) - { + catch (\Throwable $oExc) { + if ($this->oLogger) { $this->oLogger->WriteException($oExc); $this->oLogger->WriteDump($sBody); } } - if ($oVCard instanceof \Sabre\VObject\Component\VCard) - { + if ($oVCard instanceof \Sabre\VObject\Component\VCard) { $oVCard->UID = $aData['uid']; $oContact = null; - if ($mExsistenContactID) - { + if ($mExsistenContactID) { $oContact = $this->GetContactByID($mExsistenContactID); } - if (!$oContact) - { + if (!$oContact) { $oContact = new Classes\Contact(); } - $oContact->PopulateByVCard( - $oVCard, - \trim(\trim($oResponse->getHeader('etag')), '"\'') - ); + $oContact->setVCard($oVCard); + + $sEtag = \trim(\trim($oResponse->getHeader('etag')), '"\''); + if (!empty($sEtag)) { + $oContact->Etag = $sEtag; + } $this->ContactSave($oContact); unset($oContact); @@ -319,7 +295,7 @@ class PdoAddressBook } } } - +*/ return true; } @@ -334,23 +310,16 @@ class PdoAddressBook $bVcf = 'vcf' === $sType; $bCsvHeader = true; - $aDatabaseSyncData = $this->prepareDatabaseSyncData($this->iUserID); - if (\count($aDatabaseSyncData)) - { - foreach ($aDatabaseSyncData as $mData) - { - if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) - { + $aDatabaseSyncData = $this->prepareDatabaseSyncData(); + if (\count($aDatabaseSyncData)) { + foreach ($aDatabaseSyncData as $mData) { + if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) { $oContact = $this->GetContactByID($mData['id_contact']); - if ($oContact) - { - if ($bVcf) - { - echo $oContact->ToVCard(); - } - else - { - echo $oContact->ToCsv($bCsvHeader); + if ($oContact) { + if ($bVcf) { + echo $oContact->vCard->serialize(); + } else { + echo Utils::VCardToCsv($oContact, $bCsvHeader); $bCsvHeader = false; } } @@ -365,35 +334,33 @@ class PdoAddressBook { $this->SyncDatabase(); - $iIdContact = \strlen($oContact->IdContact) && \is_numeric($oContact->IdContact) ? (int) $oContact->IdContact : 0; + $iIdContact = \strlen($oContact->id) && \is_numeric($oContact->id) ? (int) $oContact->id : 0; $bUpdate = 0 < $iIdContact; - $oContact->UpdateDependentValues(); $oContact->Changed = \time(); - try - { + try { + $sFullName = (string) $oContact->vCard->FN; + $aFreq = array(); - if ($bUpdate) - { + if ($bUpdate) { $aFreq = $this->getContactFreq($this->iUserID, $iIdContact); - $sSql = 'UPDATE rainloop_ab_contacts SET id_contact_str = :id_contact_str, display = :display, changed = :changed, etag = :etag '. - 'WHERE id_user = :id_user AND id_contact = :id_contact'; - - $this->prepareAndExecute($sSql, + $this->prepareAndExecute('UPDATE rainloop_ab_contacts + SET id_contact_str = :id_contact_str, display = :display, changed = :changed, etag = :etag + WHERE id_user = :id_user AND id_contact = :id_contact', array( ':id_user' => array($this->iUserID, \PDO::PARAM_INT), ':id_contact' => array($iIdContact, \PDO::PARAM_INT), ':id_contact_str' => array($oContact->IdContactStr, \PDO::PARAM_STR), - ':display' => array($oContact->Display, \PDO::PARAM_STR), + ':display' => array($sFullName, \PDO::PARAM_STR), ':changed' => array($oContact->Changed, \PDO::PARAM_INT), - ':etag' => array($oContact->Etag, \PDO::PARAM_STR) + ':etag' => array(''/*$oContact->Etag*/, \PDO::PARAM_STR) ) ); - // clear previos props + // clear previous props $this->prepareAndExecute( 'DELETE FROM rainloop_ab_properties WHERE id_user = :id_user AND id_contact = :id_contact', array( @@ -401,43 +368,34 @@ class PdoAddressBook ':id_contact' => array($iIdContact, \PDO::PARAM_INT) ) ); - } - else - { - $sSql = 'INSERT INTO rainloop_ab_contacts '. - '( id_user, id_contact_str, display, changed, etag)'. - ' VALUES '. - '(:id_user, :id_contact_str, :display, :changed, :etag)'; - - $this->prepareAndExecute($sSql, + } else { + $this->prepareAndExecute('INSERT INTO rainloop_ab_contacts + ( id_user, id_contact_str, display, changed, etag) + VALUES + (:id_user, :id_contact_str, :display, :changed, :etag)', array( ':id_user' => array($this->iUserID, \PDO::PARAM_INT), ':id_contact_str' => array($oContact->IdContactStr, \PDO::PARAM_STR), - ':display' => array($oContact->Display, \PDO::PARAM_STR), + ':display' => array($sFullName, \PDO::PARAM_STR), ':changed' => array($oContact->Changed, \PDO::PARAM_INT), - ':etag' => array($oContact->Etag, \PDO::PARAM_STR) + ':etag' => array(''/*$oContact->Etag*/, \PDO::PARAM_STR) ) ); $sLast = $this->lastInsertId('rainloop_ab_contacts', 'id_contact'); - if (\is_numeric($sLast) && 0 < (int) $sLast) - { + if (\is_numeric($sLast) && 0 < (int) $sLast) { $iIdContact = (int) $sLast; - $oContact->IdContact = (string) $iIdContact; + $oContact->id = (string) $iIdContact; } } - if (0 < $iIdContact) - { + if (0 < $iIdContact) { $aParams = array(); - foreach ($oContact->Properties as /* @var $oProp Classes\Property */ $oProp) - { + foreach (Utils::VCardToProperties($oContact->vCard) as /* @var $oProp Classes\Property */ $oProp) { $iFreq = $oProp->Frec; - if ($oProp->IsEmail() && isset($aFreq[$oProp->Value])) - { + if ($oProp->IsEmail() && isset($aFreq[$oProp->Value])) { $iFreq = $aFreq[$oProp->Value]; } - $aParams[] = array( ':id_contact' => array($iIdContact, \PDO::PARAM_INT), ':id_user' => array($this->iUserID, \PDO::PARAM_INT), @@ -449,20 +407,18 @@ class PdoAddressBook ':prop_frec' => array($iFreq, \PDO::PARAM_INT), ); } - - if (\count($aParams)) - { - $sSql = 'INSERT INTO rainloop_ab_properties '. + if ($aParams) { + $this->prepareAndExecute('INSERT INTO rainloop_ab_properties '. '( id_contact, id_user, prop_type, prop_type_str, prop_value, prop_value_lower, prop_value_custom, prop_frec)'. ' VALUES '. - '(:id_contact, :id_user, :prop_type, :prop_type_str, :prop_value, :prop_value_lower, :prop_value_custom, :prop_frec)'; - - $this->prepareAndExecute($sSql, $aParams, true); + '(:id_contact, :id_user, :prop_type, :prop_type_str, :prop_value, :prop_value_lower, :prop_value_custom, :prop_frec)', + $aParams, + true + ); } } } - catch (\Throwable $oException) - { + catch (\Throwable $oException) { throw $oException; } @@ -471,15 +427,13 @@ class PdoAddressBook public function DeleteContacts(array $aContactIds, bool $bSyncDb = true) : bool { - if ($bSyncDb) - { + if ($bSyncDb) { $this->SyncDatabase(); } $aContactIds = \array_filter(\array_map('intval', $aContactIds)); - if (0 === \count($aContactIds)) - { + if (!\count($aContactIds)) { return false; } @@ -513,192 +467,219 @@ class PdoAddressBook return true; } + protected function getContactsFromPDO(?\PDOStatement $oStmt) : array + { + if ($oStmt) { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + + $aContacts = array(); + $aIdContacts = array(); + if (\is_array($aFetch) && \count($aFetch)) { + foreach ($aFetch as $aItem) { + $iIdContact = $aItem && isset($aItem['id_contact']) ? (int) $aItem['id_contact'] : 0; + if (0 < $iIdContact) { + $oContact = new Classes\Contact(); + $oContact->id = (string) $iIdContact; + $oContact->IdContactStr = (string) $aItem['id_contact_str']; + $oContact->Changed = (int) $aItem['changed']; + if (!empty($aItem['jcard'])) { + $oContact->FN = (string) $aItem['display']; + $oContact->setVCard( + \Sabre\VObject\Reader::readJson($aItem['jcard']) + ); +// $oContact->vCard->REV = \gmdate('Ymd\\THis\\Z', $oContact->Changed); + } else { + $aIdContacts[] = $iIdContact; + if (!empty($aItem['display'])) { +// $oContact->vCard->FN = (string) $aItem['display']; + } + } + $aContacts[$iIdContact] = $oContact; + } + } + } + + unset($aFetch); + + // Build vCards using old RainLoop data (missing jCard) + if (\count($aIdContacts)) { + $oStmt->closeCursor(); + + $oStmt = $this->prepareAndExecute('SELECT * FROM rainloop_ab_properties + WHERE id_contact IN ('.\implode(',', $aIdContacts).') + ORDER BY id_contact ASC'); + if ($oStmt) { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + if (\is_array($aFetch) && \count($aFetch)) { + $aVCards = array(); + $sUid = $sFirstName = $sLastName = $sMiddleName = $sSuffix = $sPrefix = ''; + $iPrevId = 0; + foreach ($aFetch as $aItem) { + if ($aItem && isset($aItem['id_prop'], $aItem['id_contact'], $aItem['prop_type'], $aItem['prop_value'])) { + $iId = (int) $aItem['id_contact']; + if (0 < $iId) { + if ($iPrevId != $iId) { + if ($iPrevId) { + $aVCards[$iPrevId]->UID = $sUid ?: \SnappyMail\UUID::generate(); + $aVCards[$iPrevId]->N = array($sLastName, $sFirstName, $sMiddleName, $sPrefix, $sSuffix); + } + $sUid = $sFirstName = $sLastName = $sMiddleName = $sSuffix = $sPrefix = ''; + $iPrevId = $iId; + } + if (!isset($aVCards[$iId])) { + $aVCards[$iId] = new \Sabre\VObject\Component\VCard; + } + $oVCard = $aVCards[$iId]; + $oVCard->VERSION = '4.0'; + $oVCard->PRODID = 'SnappyMail-'.APP_VERSION; + $sPropValue = (string) $aItem['prop_value']; + $aTypes = array(); + if (!empty($aItem['prop_type_str'])) { + $aTypes = \explode(',', \preg_replace('/[\s]+/', '', $aItem['prop_type_str'])); + } + switch ((int) $aItem['prop_type']) + { + case PropertyType::JCARD: + break; + + case PropertyType::FULLNAME: + $oVCard->FN = $sPropValue; + break; + case PropertyType::NICK_NAME: + $oVCard->NICKNAME = $sPropValue; + break; + case PropertyType::NOTE: + $oVCard->NOTE = $sPropValue; + break; + case PropertyType::UID: + $sUid = $sPropValue; + break; + case PropertyType::FIRST_NAME: + $sFirstName = $sPropValue; + break; + case PropertyType::LAST_NAME: + $sLastName = $sPropValue; + break; + case PropertyType::MIDDLE_NAME: + $sMiddleName = $sPropValue; + break; + case PropertyType::NAME_SUFFIX: + $sSuffix = $sPropValue; + break; + case PropertyType::NAME_PREFIX: + $sPrefix = $sPropValue; + break; + + case PropertyType::EMAIl: + $oVCard->add('EMAIL', $sPropValue, \is_array($aTypes) && \count($aTypes) ? array('TYPE' => $aTypes) : null); + break; + + case PropertyType::WEB_PAGE: + $oVCard->add('URL', $sPropValue, \is_array($aTypes) && \count($aTypes) ? array('TYPE' => $aTypes) : null); + break; + + case PropertyType::PHONE: + $oVCard->add('TEL', $sPropValue, \is_array($aTypes) && \count($aTypes) ? array('TYPE' => $aTypes) : null); + break; + } + } + } + } + if ($iPrevId) { + $aVCards[$iPrevId]->UID = $sUid ?: \SnappyMail\UUID::generate(); + $aVCards[$iPrevId]->N = array($sLastName, $sFirstName, $sMiddleName, $sPrefix, $sSuffix); + } + + foreach ($aVCards as $iId => $oVCard) { + $oVCard->REV = \gmdate('Ymd\\THis\\Z', $aContacts[$iId]->Changed); + $aContacts[$iId]->setVCard($oVCard); + } + } + + unset($aFetch); + } + } + + return \array_values($aContacts); + } + + return []; + } + public function GetContacts(int $iOffset = 0, int $iLimit = 20, string $sSearch = '', int &$iResultCount = 0) : array { $this->SyncDatabase(); - $iOffset = 0 <= $iOffset ? $iOffset : 0; - $iLimit = 0 < $iLimit ? (int) $iLimit : 20; - $sSearch = \trim($sSearch); - - $iCount = 0; $aSearchIds = array(); - $aPropertyFromSearchIds = array(); - if (\strlen($sSearch)) - { - $sCustomSearch = $this->specialConvertSearchValueCustomPhone($sSearch); + if (\strlen($sSearch)) { $sLowerSearch = $this->specialConvertSearchValueLower($sSearch, '='); - $sSearchTypes = \implode(',', array( - PropertyType::EMAIl, PropertyType::FIRST_NAME, PropertyType::LAST_NAME, PropertyType::NICK_NAME, - PropertyType::PHONE, PropertyType::WEB_PAGE - )); + $sSearchTypes = \implode(',', static::$aSearchInFields); + // PropertyType::PHONE, PropertyType::WEB_PAGE - $sSql = 'SELECT id_user, id_prop, id_contact FROM rainloop_ab_properties '. + $sSql = 'SELECT DISTINCT id_contact FROM rainloop_ab_properties '. 'WHERE (id_user = :id_user) AND prop_type IN ('.$sSearchTypes.') AND ('. 'prop_value LIKE :search ESCAPE \'=\''. (\strlen($sLowerSearch) ? ' OR (prop_value_lower <> \'\' AND prop_value_lower LIKE :search_lower ESCAPE \'=\')' : ''). -(\strlen($sCustomSearch) ? ' OR (prop_type = '.PropertyType::PHONE.' AND prop_value_custom <> \'\' AND prop_value_custom LIKE :search_custom_phone)' : ''). - ') GROUP BY id_contact, id_prop'; + ')'; $aParams = array( ':id_user' => array($this->iUserID, \PDO::PARAM_INT), ':search' => array($this->specialConvertSearchValue($sSearch, '='), \PDO::PARAM_STR) ); - if (\strlen($sLowerSearch)) - { + if (\strlen($sLowerSearch)) { $aParams[':search_lower'] = array($sLowerSearch, \PDO::PARAM_STR); } - if (\strlen($sCustomSearch)) - { - $aParams[':search_custom_phone'] = array($sCustomSearch, \PDO::PARAM_STR); - } - $oStmt = $this->prepareAndExecute($sSql, $aParams, false, true); - if ($oStmt) - { - $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetch) && \count($aFetch)) - { - foreach ($aFetch as $aItem) - { - $iIdContact = $aItem && isset($aItem['id_contact']) ? (int) $aItem['id_contact'] : 0; - if (0 < $iIdContact) - { - $aSearchIds[] = $iIdContact; - $aPropertyFromSearchIds[$iIdContact] = isset($aItem['id_prop']) ? (int) $aItem['id_prop'] : 0; - } + if ($oStmt) { + while ($aItem = $oStmt->fetch(\PDO::FETCH_NUM)) { + if (0 < $aItem[0]) { + $aSearchIds[] = (int) $aItem[0]; } } - - $aSearchIds = \array_unique($aSearchIds); - $iCount = \count($aSearchIds); + $iResultCount = \count($aSearchIds); } - } - else - { - $sSql = 'SELECT COUNT(DISTINCT id_contact) as contact_count FROM rainloop_ab_properties '. - 'WHERE id_user = :id_user'; - - $aParams = array( - ':id_user' => array($this->iUserID, \PDO::PARAM_INT) + } else { + $oStmt = $this->prepareAndExecute( + 'SELECT COUNT(*) FROM rainloop_ab_contacts WHERE id_user = :id_user', + [':id_user' => array($this->iUserID, \PDO::PARAM_INT)] ); - - $oStmt = $this->prepareAndExecute($sSql, $aParams); - if ($oStmt) - { - $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if ($aFetch && isset($aFetch[0]['contact_count']) && is_numeric($aFetch[0]['contact_count']) && 0 < (int) $aFetch[0]['contact_count']) - { - $iCount = (int) $aFetch[0]['contact_count']; - } + if ($oStmt && $aItem = $oStmt->fetch(\PDO::FETCH_NUM)) { + $iResultCount = (int) $aItem[0]; } } - $iResultCount = $iCount; - - $aResult = array(); - if (0 < $iCount) - { - $sSql = 'SELECT * FROM rainloop_ab_contacts WHERE deleted = 0 AND id_user = :id_user'; - - $aParams = array( - ':id_user' => array($this->iUserID, \PDO::PARAM_INT) - ); - - if (\count($aSearchIds)) - { - $sSql .= ' AND id_contact IN ('.implode(',', $aSearchIds).')'; + if (0 < $iResultCount) { + $sSql = 'SELECT + c.id_contact, + c.id_contact_str, + c.display, + c.changed, + p.prop_value as jcard + FROM rainloop_ab_contacts AS c + LEFT JOIN rainloop_ab_properties AS p ON (p.id_contact = c.id_contact AND p.prop_type = :prop_type) + WHERE c.deleted = 0 AND c.id_user = :id_user'; + if (\count($aSearchIds)) { + $sSql .= ' AND c.id_contact IN ('.\implode(',', $aSearchIds).')'; } - $sSql .= ' ORDER BY display ASC LIMIT :limit OFFSET :offset'; - $aParams[':limit'] = array($iLimit, \PDO::PARAM_INT); - $aParams[':offset'] = array($iOffset, \PDO::PARAM_INT); - $oStmt = $this->prepareAndExecute($sSql, $aParams); - if ($oStmt) - { - $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + $aParams = array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':prop_type' => array(PropertyType::JCARD, \PDO::PARAM_INT), + ':limit' => array($iLimit, \PDO::PARAM_INT), + ':offset' => array($iOffset, \PDO::PARAM_INT) + ); - $aContacts = array(); - $aIdContacts = array(); - if (\is_array($aFetch) && \count($aFetch)) - { - foreach ($aFetch as $aItem) - { - $iIdContact = $aItem && isset($aItem['id_contact']) ? (int) $aItem['id_contact'] : 0; - if (0 < $iIdContact) - { - $aIdContacts[] = $iIdContact; - $oContact = new Classes\Contact(); - - $oContact->IdContact = (string) $iIdContact; - $oContact->IdContactStr = isset($aItem['id_contact_str']) ? (string) $aItem['id_contact_str'] : ''; - $oContact->Display = isset($aItem['display']) ? (string) $aItem['display'] : ''; - $oContact->Changed = isset($aItem['changed']) ? (int) $aItem['changed'] : 0; - $oContact->ReadOnly = $this->iUserID !== (isset($aItem['id_user']) ? (int) $aItem['id_user'] : 0); - - $oContact->IdPropertyFromSearch = isset($aPropertyFromSearchIds[$iIdContact]) && - 0 < $aPropertyFromSearchIds[$iIdContact] ? $aPropertyFromSearchIds[$iIdContact] : 0; - - $aContacts[$iIdContact] = $oContact; - } - } - } - - unset($aFetch); - - if (\count($aIdContacts)) - { - $oStmt->closeCursor(); - - $sSql = 'SELECT * FROM rainloop_ab_properties WHERE id_contact IN ('.\implode(',', $aIdContacts).')'; - $oStmt = $this->prepareAndExecute($sSql); - - if ($oStmt) - { - $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetch) && \count($aFetch)) - { - foreach ($aFetch as $aItem) - { - if ($aItem && isset($aItem['id_prop'], $aItem['id_contact'], $aItem['prop_type'], $aItem['prop_value'])) - { - $iId = (int) $aItem['id_contact']; - if (0 < $iId && isset($aContacts[$iId])) - { - $oProperty = new Classes\Property(); - $oProperty->IdProperty = (int) $aItem['id_prop']; - $oProperty->Type = (int) $aItem['prop_type']; - $oProperty->TypeStr = isset($aItem['prop_type_str']) ? (string) $aItem['prop_type_str'] : ''; - $oProperty->Value = (string) $aItem['prop_value']; - $oProperty->ValueLower = isset($aItem['prop_value_lower']) ? (string) $aItem['prop_value_lower'] : ''; - $oProperty->ValueCustom = isset($aItem['prop_value_custom']) ? (string) $aItem['prop_value_custom'] : ''; - $oProperty->Frec = isset($aItem['prop_frec']) ? (int) $aItem['prop_frec'] : 0; - - $aContacts[$iId]->Properties[] = $oProperty; - } - } - } - } - - unset($aFetch); - - foreach ($aContacts as $oItem) - { - $oItem->UpdateDependentValues(); - } - - $aResult = \array_values($aContacts); - } - } - } + return $this->getContactsFromPDO( + $this->prepareAndExecute($sSql, $aParams) + ); } - return $aResult; + return []; } /** @@ -708,20 +689,26 @@ class PdoAddressBook { $mID = \trim($mID); - $sSql = 'SELECT * FROM rainloop_ab_contacts WHERE deleted = 0 AND id_user = :id_user'; + $sSql = 'SELECT + c.id_contact, + c.id_contact_str, + c.display, + c.changed, + p.prop_value as jcard + FROM rainloop_ab_contacts AS c + LEFT JOIN rainloop_ab_properties AS p ON (p.id_contact = c.id_contact AND p.prop_type = :prop_type) + WHERE c.deleted = 0 AND c.id_user = :id_user'; $aParams = array( - ':id_user' => array($this->iUserID, \PDO::PARAM_INT) + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':prop_type' => array(PropertyType::JCARD, \PDO::PARAM_INT) ); - if ($bIsStrID) - { - $sSql .= ' AND id_contact_str = :id_contact_str'; + if ($bIsStrID) { + $sSql .= ' AND c.id_contact_str = :id_contact_str'; $aParams[':id_contact_str'] = array($mID, \PDO::PARAM_STR); - } - else - { - $sSql .= ' AND id_contact = :id_contact'; + } else { + $sSql .= ' AND c.id_contact = :id_contact'; $aParams[':id_contact'] = array($mID, \PDO::PARAM_INT); } @@ -730,73 +717,11 @@ class PdoAddressBook $oContact = null; $iIdContact = 0; - $oStmt = $this->prepareAndExecute($sSql, $aParams); - if ($oStmt) - { - $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + $aContacts = $this->getContactsFromPDO( + $this->prepareAndExecute($sSql, $aParams) + ); - if (\is_array($aFetch) && \count($aFetch)) - { - foreach ($aFetch as $aItem) - { - $iIdContact = $aItem && isset($aItem['id_contact']) ? (int) $aItem['id_contact'] : 0; - if (0 < $iIdContact) - { - $oContact = new Classes\Contact(); - - $oContact->IdContact = (string) $iIdContact; - $oContact->IdContactStr = isset($aItem['id_contact_str']) ? (string) $aItem['id_contact_str'] : ''; - $oContact->Display = isset($aItem['display']) ? (string) $aItem['display'] : ''; - $oContact->Changed = isset($aItem['changed']) ? (int) $aItem['changed'] : 0; - $oContact->ReadOnly = $this->iUserID !== (isset($aItem['id_user']) ? (int) $aItem['id_user'] : 0); - $oContact->Etag = empty($aItem['etag']) ? '' : (string) $aItem['etag']; - } - } - } - - unset($aFetch); - - if (0 < $iIdContact && $oContact) - { - $oStmt->closeCursor(); - - $sSql = 'SELECT * FROM rainloop_ab_properties WHERE id_contact = '.$iIdContact; - $oStmt = $this->prepareAndExecute($sSql); - - if ($oStmt) - { - $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetch) && \count($aFetch)) - { - foreach ($aFetch as $aItem) - { - if ($aItem && isset($aItem['id_prop'], $aItem['id_contact'], $aItem['prop_type'], $aItem['prop_value'])) - { - if ((string) $oContact->IdContact === (string) $aItem['id_contact']) - { - $oProperty = new Classes\Property(); - $oProperty->IdProperty = (int) $aItem['id_prop']; - $oProperty->Type = (int) $aItem['prop_type']; - $oProperty->TypeStr = isset($aItem['prop_type_str']) ? (string) $aItem['prop_type_str'] : ''; - $oProperty->Value = (string) $aItem['prop_value']; - $oProperty->ValueLower = isset($aItem['prop_value_lower']) ? (string) $aItem['prop_value_lower'] : ''; - $oProperty->ValueCustom = isset($aItem['prop_value_custom']) ? (string) $aItem['prop_value_custom'] : ''; - $oProperty->Frec = isset($aItem['prop_frec']) ? (int) $aItem['prop_frec'] : 0; - - $oContact->Properties[] = $oProperty; - } - } - } - } - - unset($aFetch); - - $oContact->UpdateDependentValues(); - } - } - } - - return $oContact; + return $aContacts ? $aContacts[0] : null; } /** @@ -805,16 +730,13 @@ class PdoAddressBook public function GetSuggestions(string $sSearch, int $iLimit = 20) : array { $sSearch = \trim($sSearch); - if (0 === \strlen($sSearch)) - { + if (!\strlen($sSearch)) { throw new \InvalidArgumentException('Empty Search argument'); } $this->SyncDatabase(); - $sTypes = implode(',', array( - PropertyType::EMAIl, PropertyType::FIRST_NAME, PropertyType::LAST_NAME, PropertyType::NICK_NAME - )); + $sTypes = \implode(',', static::$aSearchInFields); $sLowerSearch = $this->specialConvertSearchValueLower($sSearch); @@ -831,8 +753,7 @@ class PdoAddressBook ':search' => array($this->specialConvertSearchValue($sSearch, '='), \PDO::PARAM_STR) ); - if (\strlen($sLowerSearch)) - { + if (\strlen($sLowerSearch)) { $aParams[':search_lower'] = array($sLowerSearch, \PDO::PARAM_STR); } @@ -842,30 +763,24 @@ class PdoAddressBook $aResult = array(); $oStmt = $this->prepareAndExecute($sSql, $aParams); - if ($oStmt) - { + if ($oStmt) { $aIdContacts = array(); $aIdProps = array(); $aContactAllAccess = array(); $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetch) && \count($aFetch)) - { - foreach ($aFetch as $aItem) - { + if (\is_array($aFetch) && \count($aFetch)) { + foreach ($aFetch as $aItem) { $iIdContact = $aItem && isset($aItem['id_contact']) ? (int) $aItem['id_contact'] : 0; $iIdProp = $aItem && isset($aItem['id_prop']) ? (int) $aItem['id_prop'] : 0; $iType = $aItem && isset($aItem['prop_type']) ? (int) $aItem['prop_type'] : 0; - if (0 < $iIdContact && 0 < $iIdProp) - { + if (0 < $iIdContact && 0 < $iIdProp) { $aIdContacts[$iIdContact] = $iIdContact; $aIdProps[$iIdProp] = $iIdProp; - if (\in_array($iType, array(PropertyType::LAST_NAME, PropertyType::FIRST_NAME, PropertyType::NICK_NAME))) - { - if (!isset($aContactAllAccess[$iIdContact])) - { + if (\in_array($iType, array(PropertyType::LAST_NAME, PropertyType::FIRST_NAME, PropertyType::NICK_NAME))) { + if (!isset($aContactAllAccess[$iIdContact])) { $aContactAllAccess[$iIdContact] = array(); } @@ -878,52 +793,40 @@ class PdoAddressBook unset($aFetch); $aIdContacts = \array_values($aIdContacts); - if (\count($aIdContacts)) - { + if (\count($aIdContacts)) { $oStmt->closeCursor(); - $sTypes = \implode(',', array( - PropertyType::EMAIl, PropertyType::FIRST_NAME, PropertyType::LAST_NAME, PropertyType::NICK_NAME - )); + $sTypes = \implode(',', static::$aSearchInFields); $sSql = 'SELECT id_prop, id_contact, prop_type, prop_value FROM rainloop_ab_properties '. 'WHERE prop_type IN ('.$sTypes.') AND id_contact IN ('.\implode(',', $aIdContacts).')'; $oStmt = $this->prepareAndExecute($sSql); - if ($oStmt) - { + if ($oStmt) { $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetch) && \count($aFetch)) - { + if (\is_array($aFetch) && \count($aFetch)) { $aNames = array(); $aEmails = array(); - foreach ($aFetch as $aItem) - { - if ($aItem && isset($aItem['id_prop'], $aItem['id_contact'], $aItem['prop_type'], $aItem['prop_value'])) - { + foreach ($aFetch as $aItem) { + if ($aItem && isset($aItem['id_prop'], $aItem['id_contact'], $aItem['prop_type'], $aItem['prop_value'])) { $iIdContact = (int) $aItem['id_contact']; $iIdProp = (int) $aItem['id_prop']; $iType = (int) $aItem['prop_type']; - if (PropertyType::NICK_NAME === $iType) - { + if (PropertyType::NICK_NAME === $iType) { $aNicks[$iIdContact] = $aItem['prop_value']; } - else if (\in_array($iType, array(PropertyType::LAST_NAME, PropertyType::FIRST_NAME))) - { - if (!isset($aNames[$iIdContact])) - { + else if (\in_array($iType, array(PropertyType::LAST_NAME, PropertyType::FIRST_NAME))) { + if (!isset($aNames[$iIdContact])) { $aNames[$iIdContact] = array('', ''); } $aNames[$iIdContact][PropertyType::FIRST_NAME === $iType ? 0 : 1] = $aItem['prop_value']; } else if ((isset($aIdProps[$iIdProp]) || isset($aContactAllAccess[$iIdContact])) && - PropertyType::EMAIl === $iType) - { - if (!isset($aEmails[$iIdContact])) - { + PropertyType::EMAIl === $iType) { + if (!isset($aEmails[$iIdContact])) { $aEmails[$iIdContact] = array(); } @@ -932,10 +835,8 @@ class PdoAddressBook } } - foreach ($aEmails as $iId => $aItems) - { - if (isset($aContactAllAccess[$iId])) - { + foreach ($aEmails as $iId => $aItems) { + if (isset($aContactAllAccess[$iId])) { $bName = \in_array(PropertyType::FIRST_NAME, $aContactAllAccess[$iId]) || \in_array(PropertyType::LAST_NAME, $aContactAllAccess[$iId]); $bNick = \in_array(PropertyType::NICK_NAME, $aContactAllAccess[$iId]); @@ -944,36 +845,28 @@ class PdoAddressBook $sNickItem = isset($aNicks[$iId]) ? $aNicks[$iId] : ''; - foreach ($aItems as $sEmail) - { - if ($sEmail) - { - if ($bName) - { + foreach ($aItems as $sEmail) { + if ($sEmail) { + if ($bName) { $aResult[] = array($sEmail, $sNameItem); } - else if ($bNick) - { + else if ($bNick) { $aResult[] = array($sEmail, $sNickItem); } - else - { + else { $aResult[] = array($sEmail, ''); } } } } - else - { + else { $aNameItem = isset($aNames[$iId]) && \is_array($aNames[$iId]) ? $aNames[$iId] : array('', ''); $sNameItem = \trim($aNameItem[0].' '.$aNameItem[1]); - if (0 === \strlen($sNameItem)) - { + if (0 === \strlen($sNameItem)) { $sNameItem = isset($aNicks[$iId]) ? $aNicks[$iId] : ''; } - foreach ($aItems as $sEmail) - { + foreach ($aItems as $sEmail) { $aResult[] = array($sEmail, $sNameItem); } } @@ -982,8 +875,7 @@ class PdoAddressBook unset($aFetch); - if ($iLimit < \count($aResult)) - { + if ($iLimit < \count($aResult)) { $aResult = \array_slice($aResult, 0, $iLimit); } @@ -1000,11 +892,12 @@ class PdoAddressBook $self = $this; $aEmailsObjects = \array_map(function ($mItem) { $oResult = null; - try - { + try { $oResult = \MailSo\Mime\Email::Parse(\trim($mItem)); } - catch (\Throwable $oException) { unset($oException); } + catch (\Throwable $oException) { + unset($oException); + } return $oResult; }, $aEmails); @@ -1012,8 +905,7 @@ class PdoAddressBook return !!$oItem; }); - if (0 === \count($aEmailsObjects)) - { + if (!\count($aEmailsObjects)) { throw new \InvalidArgumentException('Empty Emails argument'); } @@ -1023,23 +915,18 @@ class PdoAddressBook $aEmailsToCreate = array(); $aEmailsToUpdate = array(); - if ($bCreateAuto) - { + if ($bCreateAuto) { $sSql = 'SELECT prop_value FROM rainloop_ab_properties WHERE id_user = :id_user AND prop_type = :prop_type'; $oStmt = $this->prepareAndExecute($sSql, array( ':id_user' => array($this->iUserID, \PDO::PARAM_INT), ':prop_type' => array(PropertyType::EMAIl, \PDO::PARAM_INT) )); - if ($oStmt) - { + if ($oStmt) { $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetch) && \count($aFetch)) - { - foreach ($aFetch as $aItem) - { - if ($aItem && !empty($aItem['prop_value'])) - { + if (\is_array($aFetch) && \count($aFetch)) { + foreach ($aFetch as $aItem) { + if ($aItem && !empty($aItem['prop_value'])) { $aExists[] = \mb_strtolower(\trim($aItem['prop_value'])); } } @@ -1047,28 +934,20 @@ class PdoAddressBook } $aEmailsToCreate = \array_filter($aEmailsObjects, function ($oItem) use ($aExists, &$aEmailsToUpdate) { - if ($oItem) - { + if ($oItem) { $sEmail = \trim($oItem->GetEmail(true)); - if (\strlen($sEmail)) - { + if (\strlen($sEmail)) { $aEmailsToUpdate[] = $sEmail; return !\in_array($sEmail, $aExists); } } - return false; }); - } - else - { - foreach ($aEmailsObjects as $oItem) - { - if ($oItem) - { + } else { + foreach ($aEmailsObjects as $oItem) { + if ($oItem) { $sEmailUpdate = \trim($oItem->GetEmail(true)); - if (\strlen($sEmailUpdate)) - { + if (\strlen($sEmailUpdate)) { $aEmailsToUpdate[] = $sEmailUpdate; } } @@ -1077,13 +956,11 @@ class PdoAddressBook unset($aEmails, $aEmailsObjects); - if (\count($aEmailsToCreate)) - { - $oContact = new Classes\Contact(); - foreach ($aEmailsToCreate as $oEmail) - { - if ('' !== \trim($oEmail->GetEmail())) - { + if (\count($aEmailsToCreate)) { + foreach ($aEmailsToCreate as $oEmail) { + $oContact = new Classes\Contact(); + + if ('' !== \trim($oEmail->GetEmail())) { $oPropEmail = new Classes\Property(); $oPropEmail->Type = Enumerations\PropertyType::EMAIl; $oPropEmail->Value = \trim($oEmail->GetEmail(true)); @@ -1091,23 +968,18 @@ class PdoAddressBook $oContact->Properties[] = $oPropEmail; } - if ('' !== \trim($oEmail->GetDisplayName())) - { + if ('' !== \trim($oEmail->GetDisplayName())) { $sFirst = $sLast = ''; $sFullName = $oEmail->GetDisplayName(); - if (false !== \strpos($sFullName, ' ')) - { + if (false !== \strpos($sFullName, ' ')) { $aNames = \explode(' ', $sFullName, 2); $sFirst = isset($aNames[0]) ? $aNames[0] : ''; $sLast = isset($aNames[1]) ? $aNames[1] : ''; - } - else - { + } else { $sFirst = $sFullName; } - if (\strlen($sFirst)) - { + if (\strlen($sFirst)) { $oPropName = new Classes\Property(); $oPropName->Type = Enumerations\PropertyType::FIRST_NAME; $oPropName->Value = \trim($sFirst); @@ -1115,8 +987,7 @@ class PdoAddressBook $oContact->Properties[] = $oPropName; } - if (\strlen($sLast)) - { + if (\strlen($sLast)) { $oPropName = new Classes\Property(); $oPropName->Type = Enumerations\PropertyType::LAST_NAME; $oPropName->Value = \trim($sLast); @@ -1125,12 +996,9 @@ class PdoAddressBook } } - if (\count($oContact->Properties)) - { + if (\count($oContact->Properties)) { $this->ContactSave($oContact); } - - $oContact->Clear(); } } @@ -1140,12 +1008,9 @@ class PdoAddressBook return $self->quoteValue($mItem); }, $aEmailsToUpdate); - if (1 === \count($aEmailsQuoted)) - { + if (1 === \count($aEmailsQuoted)) { $sSql .= ' AND prop_value = '.$aEmailsQuoted[0]; - } - else - { + } else { $sSql .= ' AND prop_value IN ('.\implode(',', $aEmailsQuoted).')'; } @@ -1158,24 +1023,19 @@ class PdoAddressBook public function Test() : string { $sResult = ''; - try - { + try { $this->SyncDatabase(); - if (0 >= $this->getVersion($this->sDsnType.'-ab-version')) - { + if (0 >= $this->getVersion($this->sDsnType.'-ab-version')) { $sResult = 'Unknown database error'; } } - catch (\Throwable $oException) - { + catch (\Throwable $oException) { $sResult = $oException->getMessage(); - if (!empty($sResult) && !\MailSo\Base\Utils::IsAscii($sResult) && !\MailSo\Base\Utils::IsUtf8($sResult)) - { + if (!empty($sResult) && !\MailSo\Base\Utils::IsAscii($sResult) && !\MailSo\Base\Utils::IsUtf8($sResult)) { $sResult = \utf8_encode($sResult); } - if (!\is_string($sResult) || empty($sResult)) - { + if (!\is_string($sResult) || empty($sResult)) { $sResult = 'Unknown database error'; } } @@ -1185,8 +1045,7 @@ class PdoAddressBook private function getInitialTablesArray(string $sDbType) : array { - switch ($sDbType) - { + switch ($sDbType) { case 'mysql': $sInitial = <<sDsnType) - { + switch ($this->sDsnType) { case 'mysql': $mCache = $this->dataBaseUpgrade($this->sDsnType.'-ab-version', array( 1 => $this->getInitialTablesArray($this->sDsnType), @@ -1367,15 +1221,11 @@ SQLITEINITIAL; ); $oStmt = $this->prepareAndExecute($sSql, $aParams); - if ($oStmt) - { + if ($oStmt) { $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetch)) - { - foreach ($aFetch as $aItem) - { - if ($aItem && !empty($aItem['prop_value']) && !empty($aItem['prop_frec'])) - { + if (\is_array($aFetch)) { + foreach ($aFetch as $aItem) { + if ($aItem && !empty($aItem['prop_value']) && !empty($aItem['prop_frec'])) { $aResult[$aItem['prop_value']] = (int) $aItem['prop_frec']; } } @@ -1395,13 +1245,7 @@ SQLITEINITIAL; { return '%'.\str_replace(array($sEscapeSign, '_', '%'), array($sEscapeSign.$sEscapeSign, $sEscapeSign.'_', $sEscapeSign.'%'), - (string) \mb_strtolower($sSearch, 'UTF-8')).'%'; - } - - private function specialConvertSearchValueCustomPhone(string $sSearch) : string - { - $sResult = '%'.\preg_replace('/[^\d]/', '', $sSearch).'%'; - return '%%' === $sResult ? '' : $sResult; + (string) \mb_strtolower($sSearch)).'%'; } protected function getPdoAccessData() : array @@ -1412,17 +1256,16 @@ SQLITEINITIAL; protected function getUserId(string $sEmail, bool $bSkipInsert = false, bool $bCache = true) : int { static $aCache = array(); - if ($bCache && isset($aCache[$sEmail])) - { - return $aCache[$sEmail]; - } $sEmail = \MailSo\Base\Utils::IdnToAscii(\trim($sEmail), true); - if (empty($sEmail)) - { + if (empty($sEmail)) { throw new \InvalidArgumentException('Empty Email argument'); } + if ($bCache && isset($aCache[$sEmail])) { + return $aCache[$sEmail]; + } + $this->SyncDatabase(); $oStmt = $this->prepareAndExecute('SELECT id_user FROM rainloop_users WHERE rl_email = :rl_email', @@ -1432,33 +1275,26 @@ SQLITEINITIAL; ); $mRow = $oStmt->fetch(\PDO::FETCH_ASSOC); - if ($mRow && isset($mRow['id_user']) && \is_numeric($mRow['id_user'])) - { + if ($mRow && isset($mRow['id_user']) && \is_numeric($mRow['id_user'])) { $iResult = (int) $mRow['id_user']; - if (0 >= $iResult) - { + if (0 >= $iResult) { throw new \Exception('id_user <= 0'); } - - if ($bCache) - { + if ($bCache) { $aCache[$sEmail] = $iResult; } - return $iResult; } - if (!$bSkipInsert) - { + if (!$bSkipInsert) { $oStmt->closeCursor(); - $oStmt = $this->prepareAndExecute('INSERT INTO rainloop_users (rl_email) VALUES (:rl_email)', array(':rl_email' => array($sEmail, \PDO::PARAM_STR)) ); - return $this->getUserId($sEmail, true); } throw new \Exception('id_user = 0'); } + } 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 new file mode 100644 index 000000000..2a16bb3ab --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php @@ -0,0 +1,320 @@ +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; + } + } + } + } +} diff --git a/snappymail/v/0.0.0/app/localization/en/user.json b/snappymail/v/0.0.0/app/localization/en/user.json index b0c303c4d..92b8d9029 100644 --- a/snappymail/v/0.0.0/app/localization/en/user.json +++ b/snappymail/v/0.0.0/app/localization/en/user.json @@ -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", 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 2d463b9ff..e9bef3436 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 @@ -72,40 +72,41 @@ -
+
+
-
-
-
-
+
-
+
-
-
- - -
+ +
+ +
-
-
- - - - - - - - -
+
+ +
+
+ + +
+
-
+ +
-
+
+ data-bind="value: value">
- +
-
+
-
+
+ data-bind="value: value">
+
-
+
-
+
+ data-bind="value: value">
+
+