mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-02 05:59:21 +03:00
Redesign AddressBook
This commit is contained in:
parent
5f6ea3cf29
commit
a71874a8c4
18 changed files with 1403 additions and 1669 deletions
227
dev/DAV/JCard.js
Normal file
227
dev/DAV/JCard.js
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
/**
|
||||
* https://datatracker.ietf.org/doc/html/rfc7095
|
||||
*
|
||||
* Inspired by https://github.com/mcpar-land/vcfer
|
||||
*/
|
||||
|
||||
import { VCardProperty } from './VCardProperty'
|
||||
|
||||
export class JCard {
|
||||
|
||||
constructor(input)
|
||||
{
|
||||
this.props = new Map()
|
||||
this.version = '4.0'
|
||||
if (input) {
|
||||
// read from jCard
|
||||
if (typeof input !== 'object') {
|
||||
throw new Error('error reading vcard')
|
||||
}
|
||||
this.parseFromJCard(input)
|
||||
}
|
||||
}
|
||||
|
||||
parseFromJCard(json)
|
||||
{
|
||||
json = JSON.parse(JSON.stringify(json));
|
||||
if (!/vcard/i.test(json[0])) {
|
||||
throw new SyntaxError('Incorrect jCard format');
|
||||
}
|
||||
json[1].forEach(jprop => this.add(new VCardProperty(jprop)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an array of {@link VCardProperty} objects under the specified field.
|
||||
* Returns [] if there are no VCardProperty objects found.
|
||||
* Properites are always stored in an array.
|
||||
* @param field to get.
|
||||
* @param type If provided, only return {@link VCardProperty}s with the specified
|
||||
* type as a param.
|
||||
*/
|
||||
get(field, type)
|
||||
{
|
||||
if (type) {
|
||||
let props = this.props.get(field);
|
||||
return props
|
||||
? props.filter(prop => {
|
||||
let types = prop.type;
|
||||
return (Array.isArray(types) ? types : [types]).includes(type);
|
||||
})
|
||||
: [];
|
||||
}
|
||||
return this.props.get(field) || [];
|
||||
// TODO with type filter-er
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a _single_ VCardProperty of the specified field. Attempts to pick based
|
||||
* on the following priorities, in order:
|
||||
* - `TYPE={type}` of the value specified in the `type` argument. Ignored
|
||||
* if the argument isn't supplied.
|
||||
* - `TYPE=pref` is present.
|
||||
* - is the VCardProperty at index 0 from get(field)
|
||||
* @param field
|
||||
* @param type
|
||||
*/
|
||||
getOne(field, type)
|
||||
{
|
||||
return this.get(field, type || 'pref')[0] || this.get(field)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contents of a field to contain a single {@link VCardProperty}.
|
||||
*
|
||||
* Accepts either 2-4 arguments to construct a VCardProperty,
|
||||
* or 1 argument of a preexisting VCardProperty object.
|
||||
*
|
||||
* This will always overwrite all existing properties of the given
|
||||
* field. For just adding a new VCardProperty, see {@link VCard#add}
|
||||
* @param arg the field, or a VCardProperty object
|
||||
* @param value the value for the VCardProperty object
|
||||
* @param params the parameters for the VCardProperty object
|
||||
* @param group the group for the VCardProperty object
|
||||
*/
|
||||
set(arg, value, params, group)
|
||||
{
|
||||
if (typeof arg === 'string') {
|
||||
arg = new VCardProperty(String(arg), value, params, group);
|
||||
}
|
||||
if (!(arg instanceof VCardProperty)) {
|
||||
throw new Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
|
||||
}
|
||||
let field = arg.getField();
|
||||
this.props.set(field, [arg]);
|
||||
return arg;
|
||||
}
|
||||
|
||||
add(arg, value, params, group)
|
||||
{
|
||||
// string arguments
|
||||
if (typeof arg === 'string') {
|
||||
arg = new VCardProperty(String(arg), value, params, group);
|
||||
}
|
||||
if (!(arg instanceof VCardProperty)) {
|
||||
throw new Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
|
||||
}
|
||||
// VCardProperty arguments
|
||||
let field = arg.getField();
|
||||
if (this.props.get(field)) this.props.get(field)?.push(arg)
|
||||
else this.props.set(field, [arg])
|
||||
return arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link VCardProperty}, or all properties of the supplied field.
|
||||
* @param arg the field, or a {@link VCardProperty} object
|
||||
* @param paramFilter (incomplete)
|
||||
*/
|
||||
remove(arg) {
|
||||
// string arguments
|
||||
if (typeof arg === 'string') {
|
||||
// TODO filter by param
|
||||
this.props.delete(arg);
|
||||
}
|
||||
// VCardProperty argument
|
||||
else if (arg instanceof VCardProperty) {
|
||||
let propArray = this.props.get(arg.getField());
|
||||
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
|
||||
throw new Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
||||
propArray.splice(propArray.indexOf(arg), 1);
|
||||
if (propArray.length === 0)
|
||||
this.props.delete(arg.getField());
|
||||
}
|
||||
// incorrect arguments
|
||||
else
|
||||
throw new Error('invalid argument of VCard.remove(), expects ' +
|
||||
'string and optional param filter or a VCardProperty');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the vCard has at least one @{link VCardProperty}
|
||||
* of the given field.
|
||||
* @param field The field to query
|
||||
*/
|
||||
has(field)
|
||||
{
|
||||
return (!!this.props.get(field) && this.props.get(field).length > 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns stringified JSON
|
||||
*/
|
||||
toString()
|
||||
{
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link JCard} object as a JSON array.
|
||||
*/
|
||||
toJSON()
|
||||
{
|
||||
let data = [['version', {}, 'text', '4.0']];
|
||||
/*
|
||||
this.props.forEach((props, field) =>
|
||||
(field === 'version') || props.forEach(prop => prop.isEmpty() || data.push(prop.toJSON()))
|
||||
);
|
||||
*/
|
||||
for (const [field, props] of this.props.entries()) {
|
||||
if ('version' !== field) {
|
||||
for (const prop of props) {
|
||||
prop.isEmpty() || data.push(prop.toJSON());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['vcard', data];
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically generate the 'fn' VCardProperty from the preferred 'n' VCardProperty.
|
||||
*
|
||||
* #### `set` (`boolean`)
|
||||
*
|
||||
* - `false`: (default) return the generated full name string without
|
||||
* modifying the VCard.
|
||||
*
|
||||
* - `true`: modify the VCard's `fn` VCardProperty directly, as specified
|
||||
* by `append`
|
||||
*
|
||||
* #### `append` (`boolean`)
|
||||
*
|
||||
* (ignored if `set` is `false`)
|
||||
*
|
||||
* - `false`: (default) replace the existing 'fn' VCardProperty/properties with
|
||||
* a new one.
|
||||
*
|
||||
* - `true`: append a new `fn` VCardProperty to the array.
|
||||
*
|
||||
* see: [RFC 6350 section 6.2.1](https://tools.ietf.org/html/rfc6350#section-6.2.1)
|
||||
*/
|
||||
parseFullName(options) {
|
||||
let n = this.getOne('n');
|
||||
if (n === undefined) {
|
||||
throw new Error('\'fn\' VCardProperty not present in card, cannot parse full name');
|
||||
}
|
||||
let fnString = '';
|
||||
// Position in n -> position in fn
|
||||
[3, 1, 2, 0, 4].forEach(pos => {
|
||||
let splitStr = n.value[pos];
|
||||
if (splitStr) {
|
||||
// comma separated values separated by spaces
|
||||
fnString += ' ' + splitStr.replace(',', ' ');
|
||||
}
|
||||
});
|
||||
fnString = fnString.trim();
|
||||
let fn = new VCardProperty('fn', fnString);
|
||||
if (options?.set) {
|
||||
if (options.append) {
|
||||
this.add(fn);
|
||||
} else {
|
||||
this.set(fn);
|
||||
}
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
}
|
||||
152
dev/DAV/VCardProperty.js
Normal file
152
dev/DAV/VCardProperty.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* Inspired by https://github.com/mcpar-land/vcfer
|
||||
*/
|
||||
|
||||
const
|
||||
camelCase = str =>
|
||||
str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) =>
|
||||
index === 0 ? word.toLowerCase() : word.toUpperCase()
|
||||
).replace(/\s+/g, '');
|
||||
|
||||
export class VCardProperty {
|
||||
|
||||
/**
|
||||
* A class describing a single vCard property.
|
||||
* Will almost always be a member of a
|
||||
* {@link VCard}'s [props]{@link VCard.props} map.
|
||||
*
|
||||
* Accepts either 2-4 arguments, or 1 argument in jCard property format.
|
||||
* @param arg the field, or a jCard property
|
||||
* @param value
|
||||
* @param params
|
||||
* @param type
|
||||
*/
|
||||
constructor(arg, value, params, type = 'text')
|
||||
{
|
||||
this.field = '';
|
||||
|
||||
/**
|
||||
* the value of the property.
|
||||
* @example '(123) 456 7890'
|
||||
*/
|
||||
this.value = '';
|
||||
|
||||
/**
|
||||
* the type of the property value.
|
||||
* @example 'text'
|
||||
*/
|
||||
this.type = '';
|
||||
|
||||
/**
|
||||
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5
|
||||
* An jCard parameters object.
|
||||
* @example
|
||||
* {
|
||||
* type: ['work', 'voice', 'pref'],
|
||||
* value: 'uri'
|
||||
* }
|
||||
*/
|
||||
this.params = {};
|
||||
|
||||
// Construct from arguments
|
||||
if (value !== undefined && typeof arg === 'string') {
|
||||
this.field = arg;
|
||||
this.value = value;
|
||||
this.params = params || {};
|
||||
this.type = type;
|
||||
}
|
||||
// construct from jcard
|
||||
else if (value === undefined && params === undefined && typeof arg === 'object') {
|
||||
this.parseFromJCardProperty(arg);
|
||||
}
|
||||
// invalid property
|
||||
else {
|
||||
throw new Error('invalid Property constructor');
|
||||
}
|
||||
}
|
||||
|
||||
parseFromJCardProperty(jCardProp)
|
||||
{
|
||||
jCardProp = JSON.parse(JSON.stringify(jCardProp));
|
||||
this.field = camelCase(jCardProp[0]);
|
||||
this.params = jCardProp[1];
|
||||
this.type = jCardProp[2];
|
||||
this.value = jCardProp[3];
|
||||
}
|
||||
|
||||
addParam(key, value)
|
||||
{
|
||||
if (Array.isArray(this.params[key])) {
|
||||
this.params[key].push(value);
|
||||
}
|
||||
else if (this.params[key] != null) {
|
||||
this.params[key] = [this.params[key], value];
|
||||
}
|
||||
else {
|
||||
this.params[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a copy of the Property's string value. */
|
||||
getValue()
|
||||
{
|
||||
return '' + this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5.3
|
||||
*/
|
||||
pref()
|
||||
{
|
||||
return this.params.pref || 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5.6
|
||||
*/
|
||||
tags()
|
||||
{
|
||||
return this.params.type || [];
|
||||
}
|
||||
|
||||
/** Returns `true` if all the following are true:
|
||||
* - the property's value contains charactes other than `;`
|
||||
* - the property has no parameters
|
||||
*/
|
||||
isEmpty()
|
||||
{
|
||||
return ((null == this.value || !/[^;]+/.test(this.value)) && !Object.keys(this.params).length);
|
||||
}
|
||||
|
||||
notEmpty()
|
||||
{
|
||||
return !this.isEmpty();
|
||||
}
|
||||
|
||||
/** Returns a readonly string copy of the property's field. */
|
||||
getField()
|
||||
{
|
||||
return '' + this.field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stringified JSON
|
||||
*/
|
||||
toString()
|
||||
{
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON array in a [jCard property]{@link JCardProperty} format
|
||||
*/
|
||||
toJSON()
|
||||
{
|
||||
return [
|
||||
this.field,
|
||||
this.params,
|
||||
this.type || 'text',
|
||||
this.value
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace RainLoop\Providers\AddressBook\Classes;
|
||||
|
||||
class Tag implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $IdContactTag;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $Name;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $ReadOnly;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->Clear();
|
||||
}
|
||||
|
||||
public function Clear() : void
|
||||
{
|
||||
$this->IdContactTag = '';
|
||||
$this->Name = '';
|
||||
$this->ReadOnly = false;
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return array(
|
||||
'@Object' => 'Object/Tag',
|
||||
'id' => $this->IdContactTag,
|
||||
'name' => \MailSo\Base\Utils::Utf8Clear($mResponse->Name),
|
||||
'readOnly' => $this->ReadOnly
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,4 +32,6 @@ class PropertyType
|
|||
const NOTE = 110;
|
||||
|
||||
const CUSTOM = 250;
|
||||
|
||||
const JCARD = 251;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,320 @@
|
|||
<?php
|
||||
|
||||
namespace RainLoop\Providers\AddressBook;
|
||||
|
||||
use
|
||||
RainLoop\Providers\AddressBook\Enumerations\PropertyType,
|
||||
RainLoop\Providers\AddressBook\Classes\Property
|
||||
;
|
||||
|
||||
|
||||
class Utils
|
||||
{
|
||||
/**
|
||||
* @param mixed $oProp
|
||||
*/
|
||||
private static function yieldPropertyHelper($oArrayProp, int $iType) : iterable
|
||||
{
|
||||
$aTmp = [];
|
||||
foreach ($oArrayProp as $oProp) {
|
||||
$sValue = \trim($oProp->getValue());
|
||||
if (\strlen($sValue)) {
|
||||
$oTypes = $oProp['TYPE'];
|
||||
$aTypes = $oTypes ? $oTypes->getParts() : array();
|
||||
$pref = 100;
|
||||
if (0 < $oProp['PREF']) {
|
||||
$pref = (int) $oProp['PREF'];
|
||||
}
|
||||
$aTmp[\substr(1000+$pref,-3) . $sValue] = new Property($iType, $sValue, \implode(',', $aTypes));
|
||||
}
|
||||
}
|
||||
\ksort($aTmp);
|
||||
foreach ($aTmp as $oProp) {
|
||||
yield $oProp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $oProp
|
||||
*/
|
||||
private static function getPropertyValueHelper($oProp, bool $bOldVersion) : string
|
||||
{
|
||||
$sValue = \trim($oProp);
|
||||
if ($bOldVersion && !isset($oProp->parameters['CHARSET'])) {
|
||||
if (\strlen($sValue)) {
|
||||
$sEncValue = \utf8_encode($sValue);
|
||||
if (\strlen($sEncValue)) {
|
||||
$sValue = $sEncValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return \MailSo\Base\Utils::Utf8Clear($sValue);
|
||||
}
|
||||
|
||||
public static function VCardToProperties(\Sabre\VObject\Component\VCard $oVCard) : iterable
|
||||
{
|
||||
yield new Property(PropertyType::JCARD, \json_encode($oVCard));
|
||||
|
||||
$bOldVersion = !empty($oVCard->VERSION) && \in_array((string) $oVCard->VERSION, array('2.1', '2.0', '1.0'));
|
||||
|
||||
if (isset($oVCard->FN) && '' !== \trim($oVCard->FN)) {
|
||||
$sValue = static::getPropertyValueHelper($oVCard->FN, $bOldVersion);
|
||||
yield new Property(PropertyType::FULLNAME, $sValue);
|
||||
}
|
||||
|
||||
if (isset($oVCard->N)) {
|
||||
$aNames = $oVCard->N->getParts();
|
||||
foreach ($aNames as $iIndex => $sValue) {
|
||||
$sValue = \trim($sValue);
|
||||
if ($bOldVersion && !isset($oVCard->N->parameters['CHARSET'])) {
|
||||
if (\strlen($sValue)) {
|
||||
$sEncValue = \utf8_encode($sValue);
|
||||
if (\strlen($sEncValue)) {
|
||||
$sValue = $sEncValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
$sValue = \MailSo\Base\Utils::Utf8Clear($sValue);
|
||||
if ($sValue) {
|
||||
switch ($iIndex) {
|
||||
case 0:
|
||||
yield new Property(PropertyType::LAST_NAME, $sValue);
|
||||
break;
|
||||
case 1:
|
||||
yield new Property(PropertyType::FIRST_NAME, $sValue);
|
||||
break;
|
||||
case 2:
|
||||
yield new Property(PropertyType::MIDDLE_NAME, $sValue);
|
||||
break;
|
||||
case 3:
|
||||
yield new Property(PropertyType::NAME_PREFIX, $sValue);
|
||||
break;
|
||||
case 4:
|
||||
yield new Property(PropertyType::NAME_SUFFIX, $sValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($oVCard->EMAIL)) {
|
||||
yield from static::yieldPropertyHelper($oVCard->EMAIL, PropertyType::EMAIl);
|
||||
}
|
||||
|
||||
if (isset($oVCard->URL)) {
|
||||
yield from static::yieldPropertyHelper($oVCard->URL, PropertyType::WEB_PAGE);
|
||||
}
|
||||
|
||||
if (isset($oVCard->TEL)) {
|
||||
yield from static::yieldPropertyHelper($oVCard->TEL, PropertyType::PHONE);
|
||||
}
|
||||
}
|
||||
|
||||
private static function csvNameToTypeConvertor(string $sCsvName) : int
|
||||
{
|
||||
static $aMap = null;
|
||||
if (null === $aMap)
|
||||
{
|
||||
$aMap = array(
|
||||
'Title' => PropertyType::FULLNAME,
|
||||
'Name' => PropertyType::FULLNAME,
|
||||
'FullName' => PropertyType::FULLNAME,
|
||||
'DisplayName' => PropertyType::FULLNAME,
|
||||
'GivenName' => PropertyType::FIRST_NAME,
|
||||
'First' => PropertyType::FIRST_NAME,
|
||||
'FirstName' => PropertyType::FIRST_NAME,
|
||||
'Middle' => PropertyType::MIDDLE_NAME,
|
||||
'MiddleName' => PropertyType::MIDDLE_NAME,
|
||||
'Last' => PropertyType::LAST_NAME,
|
||||
'LastName' => PropertyType::LAST_NAME,
|
||||
'Suffix' => PropertyType::NAME_SUFFIX,
|
||||
'NameSuffix' => PropertyType::NAME_SUFFIX,
|
||||
'Prefix' => PropertyType::NAME_PREFIX,
|
||||
'NamePrefix' => PropertyType::NAME_PREFIX,
|
||||
'ShortName' => PropertyType::NICK_NAME,
|
||||
'NickName' => PropertyType::NICK_NAME,
|
||||
'BusinessFax' => array(PropertyType::PHONE, 'Work,Fax'),
|
||||
'BusinessFax2' => array(PropertyType::PHONE, 'Work,Fax'),
|
||||
'BusinessFax3' => array(PropertyType::PHONE, 'Work,Fax'),
|
||||
'BusinessPhone' => array(PropertyType::PHONE, 'Work'),
|
||||
'BusinessPhone2' => array(PropertyType::PHONE, 'Work'),
|
||||
'BusinessPhone3' => array(PropertyType::PHONE, 'Work'),
|
||||
'CompanyPhone' => array(PropertyType::PHONE, 'Work'),
|
||||
'CompanyMainPhone' => array(PropertyType::PHONE, 'Work'),
|
||||
'HomeFax' => array(PropertyType::PHONE, 'Home,Fax'),
|
||||
'HomeFax2' => array(PropertyType::PHONE, 'Home,Fax'),
|
||||
'HomeFax3' => array(PropertyType::PHONE, 'Home,Fax'),
|
||||
'HomePhone' => array(PropertyType::PHONE, 'Home'),
|
||||
'HomePhone2' => array(PropertyType::PHONE, 'Home'),
|
||||
'HomePhone3' => array(PropertyType::PHONE, 'Home'),
|
||||
'Mobile' => array(PropertyType::PHONE, 'Mobile'),
|
||||
'MobilePhone' => array(PropertyType::PHONE, 'Mobile'),
|
||||
'BusinessMobile' => array(PropertyType::PHONE, 'Work,Mobile'),
|
||||
'BusinessMobilePhone' => array(PropertyType::PHONE, 'Work,Mobile'),
|
||||
'OtherFax' => array(PropertyType::PHONE, 'Other,Fax'),
|
||||
'OtherPhone' => array(PropertyType::PHONE, 'Other'),
|
||||
'PrimaryPhone' => array(PropertyType::PHONE, 'Pref,Home'),
|
||||
'Email' => array(PropertyType::EMAIl, 'Home'),
|
||||
'Email2' => array(PropertyType::EMAIl, 'Home'),
|
||||
'Email3' => array(PropertyType::EMAIl, 'Home'),
|
||||
'HomeEmail' => array(PropertyType::EMAIl, 'Home'),
|
||||
'HomeEmail2' => array(PropertyType::EMAIl, 'Home'),
|
||||
'HomeEmail3' => array(PropertyType::EMAIl, 'Home'),
|
||||
'PrimaryEmail' => array(PropertyType::EMAIl, 'Home'),
|
||||
'PrimaryEmail2' => array(PropertyType::EMAIl, 'Home'),
|
||||
'PrimaryEmail3' => array(PropertyType::EMAIl, 'Home'),
|
||||
'EmailAddress' => array(PropertyType::EMAIl, 'Home'),
|
||||
'Email2Address' => array(PropertyType::EMAIl, 'Home'),
|
||||
'Email3Address' => array(PropertyType::EMAIl, 'Home'),
|
||||
'OtherEmail' => array(PropertyType::EMAIl, 'Other'),
|
||||
'BusinessEmail' => array(PropertyType::EMAIl, 'Work'),
|
||||
'BusinessEmail2' => array(PropertyType::EMAIl, 'Work'),
|
||||
'BusinessEmail3' => array(PropertyType::EMAIl, 'Work'),
|
||||
'PersonalEmail' => array(PropertyType::EMAIl, 'Home'),
|
||||
'PersonalEmail2' => array(PropertyType::EMAIl, 'Home'),
|
||||
'PersonalEmail3' => array(PropertyType::EMAIl, 'Home'),
|
||||
'Notes' => PropertyType::NOTE,
|
||||
'Web' => PropertyType::WEB_PAGE,
|
||||
'BusinessWeb' => array(PropertyType::WEB_PAGE, 'Work'),
|
||||
'WebPage' => PropertyType::WEB_PAGE,
|
||||
'BusinessWebPage' => array(PropertyType::WEB_PAGE, 'Work'),
|
||||
'WebSite' => PropertyType::WEB_PAGE,
|
||||
'BusinessWebSite' => array(PropertyType::WEB_PAGE, 'Work'),
|
||||
'PersonalWebSite' => PropertyType::WEB_PAGE
|
||||
);
|
||||
|
||||
$aMap = \array_change_key_case($aMap, CASE_LOWER);
|
||||
}
|
||||
|
||||
$sCsvNameLower = \MailSo\Base\Utils::IsAscii($sCsvName) ? \preg_replace('/[\s\-]+/', '', \strtolower($sCsvName)) : '';
|
||||
return !empty($sCsvNameLower) && isset($aMap[$sCsvNameLower]) ? $aMap[$sCsvNameLower] : PropertyType::UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: broken
|
||||
*/
|
||||
public static function CsvArrayToVCards(array $aCsvData) : iterable
|
||||
{
|
||||
foreach ($aCsvData as $aItem) {
|
||||
$oContact = new Classes\Contact();
|
||||
\MailSo\Base\Utils::ResetTimeLimit();
|
||||
foreach ($aItem as $sItemName => $sItemValue) {
|
||||
$sItemName = \trim($sItemName);
|
||||
$sItemValue = \trim($sItemValue);
|
||||
if (!empty($sItemName) && !empty($sItemValue)) {
|
||||
$mData = static::csvNameToTypeConvertor($sItemName);
|
||||
$iType = \is_array($mData) ? $mData[0] : $mData;
|
||||
if (PropertyType::UNKNOWN !== $iType) {
|
||||
$oProp = new Classes\Property();
|
||||
$oProp->Type = $iType;
|
||||
$oProp->Value = $sItemValue;
|
||||
$oProp->TypeStr = \is_array($mData) && !empty($mData[1]) ? $mData[1] : '';
|
||||
// $oContact->Properties[] = $oProp;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (\count($oContact->Properties)) {
|
||||
yield $oContact;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: broken
|
||||
*/
|
||||
public static function VCardToCsv(Classes\Contact $oContact, bool $bWithHeader = false) : string
|
||||
{
|
||||
$aData = array();
|
||||
if ($bWithHeader) {
|
||||
$aData[] = array(
|
||||
'Title', 'First Name', 'Middle Name', 'Last Name', 'Nick Name', 'Display Name',
|
||||
'Company', 'Department', 'Job Title', 'Office Location',
|
||||
'E-mail Address', 'Notes', 'Web Page', 'Birthday',
|
||||
'Other Email', 'Other Phone', 'Other Mobile', 'Mobile Phone',
|
||||
'Home Email', 'Home Phone', 'Home Fax',
|
||||
'Home Street', 'Home City', 'Home State', 'Home Postal Code', 'Home Country',
|
||||
'Business Email', 'Business Phone', 'Business Fax',
|
||||
'Business Street', 'Business City', 'Business State', 'Business Postal Code', 'Business Country'
|
||||
);
|
||||
}
|
||||
|
||||
$oVCard = $oContact->vCard;
|
||||
|
||||
|
||||
$adrHome = $oVCard->getByType('ADR', 'HOME');
|
||||
$adrHome = $adrHome ? $adrHome->getParts() : ['','','','','','',''];
|
||||
|
||||
$adrWork = $oVCard->getByType('ADR', 'WORK');
|
||||
$adrWork = $adrWork ? $adrWork->getParts() : ['','','','','','',''];
|
||||
|
||||
$aValues = array(
|
||||
'', // Title
|
||||
'', // First Name
|
||||
'', // Middle Name
|
||||
'', // Last Name
|
||||
(string) $oVCard->NICKNAME, // Nick Name
|
||||
(string) $oVCard->FN, // Display Name
|
||||
(string) $oVCard->ORG, // Company
|
||||
'', // Department
|
||||
'', // Job Title
|
||||
'', // Office Location
|
||||
(string) $oVCard->EMAIL, // E-mail Address
|
||||
(string) $oVCard->NOTE, // Notes
|
||||
(string) $oVCard->URL, // Web Page
|
||||
'', // Birthday
|
||||
'', // Other Email
|
||||
'', // Other Phone
|
||||
'', // Other Mobile
|
||||
(string) $oVCard->getByType('TEL', 'CELL'), // Mobile Phone
|
||||
// Home
|
||||
(string) $oVCard->getByType('EMAIL', 'HOME'), // Email
|
||||
(string) $oVCard->getByType('TEL', 'HOME'), // Phone
|
||||
'', // Fax,
|
||||
\trim($adrHome[1]."\n".$adrHome[2]), // extended address + street address
|
||||
$adrHome[3], // City
|
||||
$adrHome[4], // State
|
||||
$adrHome[5], // Postal Code
|
||||
$adrHome[6], // Country
|
||||
// Business
|
||||
(string) $oVCard->getByType('EMAIL', 'WORK'), // Email
|
||||
(string) $oVCard->getByType('TEL', 'WORK'), // Phone
|
||||
'', // Fax
|
||||
\trim($adrWork[1]."\n".$adrWork[2]), // extended address + street address
|
||||
$adrWork[3], // City
|
||||
$adrWork[4], // State
|
||||
$adrWork[5], // Postal Code
|
||||
$adrWork[6] // Country
|
||||
);
|
||||
|
||||
$aData[] = \array_map(function ($sValue) {
|
||||
$sValue = \trim($sValue);
|
||||
return \preg_match('/[\r\n,"]/', $sValue) ? '"'.\str_replace('"', '""', $sValue).'"' : $sValue;
|
||||
}, $aValues);
|
||||
|
||||
$sResult = '';
|
||||
foreach ($aData as $aSubData) {
|
||||
$sResult .= \implode(',', $aSubData)."\r\n";
|
||||
}
|
||||
|
||||
return $sResult;
|
||||
}
|
||||
|
||||
public static function VcfFileToVCards(string $sVcfData) : iterable
|
||||
{
|
||||
$sVcfData = \trim($sVcfData);
|
||||
if ("\xef\xbb\xbf" === \substr($sVcfData, 0, 3)) {
|
||||
$sVcfData = \substr($sVcfData, 3);
|
||||
}
|
||||
$oVCardSplitter = new \Sabre\VObject\Splitter\VCard($sVcfData);
|
||||
if ($oVCardSplitter) {
|
||||
while ($oVCard = $oVCardSplitter->getNext()) {
|
||||
if ($oVCard instanceof \Sabre\VObject\Component\VCard) {
|
||||
\MailSo\Base\Utils::ResetTimeLimit();
|
||||
$oContact = new Classes\Contact();
|
||||
$oContact->setVCard($oVCard);
|
||||
yield $oContact;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -72,40 +72,41 @@
|
|||
<!-- ko template: { name: 'Paginator', data: contactsPaginator } --><!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="b-view-content-toolbar btn-toolbar" data-bind="css: {'read-only': viewReadOnly}">
|
||||
<div class="b-view-content-toolbar btn-toolbar" data-bind="css: {'read-only': viewReadOnly}, i18nUpdate: contact">
|
||||
<!-- ko with: contact -->
|
||||
<div class="btn-group">
|
||||
<button class="btn button-save-contact" data-bind="visible: !emptySelection(), command: saveCommand, css: {'dirty': watchDirty}">
|
||||
<i data-bind="css: {'icon-ok': !viewSaving(), 'icon-spinner': viewSaving()}"></i>
|
||||
<span data-i18n="CONTACTS/BUTTON_CREATE_CONTACT" data-bind="visible: '' === viewID()"></span>
|
||||
<span data-i18n="CONTACTS/BUTTON_UPDATE_CONTACT" data-bind="visible: '' !== viewID()"></span>
|
||||
<button class="btn button-save-contact" data-bind="command: $root.saveCommand, css: {'dirty': $root.watchDirty}">
|
||||
<i data-bind="css: {'icon-ok': !$root.viewSaving(), 'icon-spinner': $root.viewSaving()}"></i>
|
||||
<span data-i18n="CONTACTS/BUTTON_CREATE_CONTACT" data-bind="visible: !id"></span>
|
||||
<span data-i18n="CONTACTS/BUTTON_UPDATE_CONTACT" data-bind="visible: id"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group dropdown" data-bind="visible: !emptySelection(), registerBootstrapDropdown: true">
|
||||
<div class="btn-group dropdown" data-bind="registerBootstrapDropdown: true">
|
||||
<a id="button-add-prop-dropdown-id" href="#" tabindex="-1" class="btn dropdown-toggle">
|
||||
<span data-i18n="CONTACTS/ADD_MENU_LABEL"></span>
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu right-edge" style="text-align: left" tabindex="-1" role="menu" aria-labelledby="button-add-prop-dropdown-id">
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="click: addNewEmail">
|
||||
<a href="#" tabindex="-1" data-bind="click: addEmail">
|
||||
<i class="icon-none"></i>
|
||||
<span data-i18n="GLOBAL/EMAIL"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="click: addNewPhone">
|
||||
<a href="#" tabindex="-1" data-bind="click: addTel">
|
||||
<i class="icon-none"></i>
|
||||
<span data-i18n="CONTACTS/ADD_MENU_PHONE"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="click: addNewWeb">
|
||||
<a href="#" tabindex="-1" data-bind="click: addUrl">
|
||||
<i class="icon-none"></i>
|
||||
<span data-i18n="CONTACTS/ADD_MENU_URL"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="dividerbar" role="presentation">
|
||||
<a href="#" tabindex="-1" data-bind="click: addNewNickname">
|
||||
<a href="#" tabindex="-1" data-bind="click: addNickname">
|
||||
<i class="icon-none"></i>
|
||||
<span data-i18n="CONTACTS/ADD_MENU_NICKNAME"></span>
|
||||
</a>
|
||||
|
|
@ -118,81 +119,82 @@
|
|||
</li>-->
|
||||
</ul>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<div class="b-view-content" data-bind="css: {'read-only': viewReadOnly}">
|
||||
<div class="b-contact-view-desc" data-bind="visible: emptySelection"
|
||||
<div class="b-contact-view-desc" data-bind="visible: !contact"
|
||||
data-i18n="CONTACTS/CONTACT_VIEW_DESC"></div>
|
||||
<div data-bind="visible: !emptySelection()">
|
||||
<div data-bind="visible: contact, i18nUpdate: contact">
|
||||
<div class="form-horizontal top-part">
|
||||
<div class="control-group" data-bind="visible: !viewReadOnly() || contactHasValidName()">
|
||||
<div class="control-group" data-bind="visible: !viewReadOnly() || hasValidName()">
|
||||
<label class="fontastic iconsize24">👤</label>
|
||||
<div>
|
||||
<div data-bind="foreach: viewPropertiesNames">
|
||||
<div class="property-line">
|
||||
<span data-bind="text: value"></span>
|
||||
<input type="text"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup', attr: {'placeholder': placeholderValue}">
|
||||
</div>
|
||||
<!-- ko with: contact -->
|
||||
<div class="property-line">
|
||||
<span data-bind="text: givenName"></span>
|
||||
<input type="text"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: givenName" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_FIRST_NAME">
|
||||
</div>
|
||||
<div data-bind="visible: viewPropertiesOther().length, foreach: viewPropertiesOther">
|
||||
<div class="property-line">
|
||||
<!-- ko if: !largeValue() -->
|
||||
<span data-bind="text: value"></span>
|
||||
<input type="text"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup', attr: {'placeholder': placeholderValue}">
|
||||
<!-- /ko -->
|
||||
<!-- ko if: largeValue -->
|
||||
<span data-bind="text: value"></span>
|
||||
<textarea
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup', attr: {'placeholder': placeholderValue}"></textarea>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<div class="property-line">
|
||||
<span data-bind="text: surName"></span>
|
||||
<input type="text"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: surName" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_LAST_NAME">
|
||||
</div>
|
||||
<div class="property-line" data-bind="visible: null != nickname()">
|
||||
<span data-bind="text: nickname"></span>
|
||||
<input type="text"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: nickname" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_NICK_NAME">
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="visible: !viewReadOnly() || viewPropertiesEmails().length">
|
||||
<!-- ko with: contact -->
|
||||
<div class="control-group" data-bind="email().length">
|
||||
<label class="fontastic iconsize24" data-i18n="[title]GLOBAL/EMAIL">@</label>
|
||||
<div>
|
||||
<div data-bind="foreach: viewPropertiesEmails">
|
||||
<div data-bind="foreach: email">
|
||||
<div class="property-line">
|
||||
<span data-bind="text: value"></span>
|
||||
<input type="text"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup'">
|
||||
data-bind="value: value">
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="g-ui-link add-link" data-bind="visible: !viewReadOnly(), click: addNewEmail" data-i18n="CONTACTS/LINK_ADD_EMAIL"></a>
|
||||
<a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: addEmail">✚</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="visible: viewPropertiesPhones().length">
|
||||
<div class="control-group" data-bind="visible: tel().length">
|
||||
<label class="fontastic iconsize24" data-i18n="[title]CONTACTS/LABEL_PHONE">📞</label>
|
||||
<div>
|
||||
<div data-bind="foreach: viewPropertiesPhones">
|
||||
<div data-bind="foreach: tel">
|
||||
<div class="property-line">
|
||||
<span data-bind="text: value"></span>
|
||||
<input type="text"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup'">
|
||||
data-bind="value: value">
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: $root.addTel">✚</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="visible: viewPropertiesWeb().length">
|
||||
<div class="control-group" data-bind="visible: url().length">
|
||||
<label class="fontastic iconsize24" data-i18n="[title]CONTACTS/LABEL_WEB">🌍</label>
|
||||
<div>
|
||||
<div data-bind="foreach: viewPropertiesWeb">
|
||||
<div data-bind="foreach: url">
|
||||
<div class="property-line">
|
||||
<span data-bind="text: value"></span>
|
||||
<input type="text" placeholder="https://"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup'">
|
||||
data-bind="value: value">
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: $root.addUrl">✚</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<!--
|
||||
<div class="e-read-only-sign fontastic iconsize24" data-i18n="[title]CONTACTS/LABEL_READ_ONLY">🔒</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue