diff --git a/src/app/Backends/DAV/Vcard.php b/src/app/Backends/DAV/Vcard.php
index d846e1dc..19161703 100644
--- a/src/app/Backends/DAV/Vcard.php
+++ b/src/app/Backends/DAV/Vcard.php
@@ -1,136 +1,135 @@
getElementsByTagName('address-data')->item(0)) {
$object->fromVcard($data->nodeValue);
}
return $object;
}
/**
* Set object properties from a vcard
*
* @param string $vcard vCard string
*/
protected function fromVcard(string $vcard): void
{
$vobject = Reader::read($vcard, Reader::OPTION_FORGIVING | Reader::OPTION_IGNORE_INVALID_LINES);
if ($vobject->name != 'VCARD') {
// FIXME: throw an exception?
return;
}
$this->vobject = $vobject;
- $this->members = [];
$string_properties = [
'CLASS',
'FN',
'KIND',
'NOTE',
'PRODID',
'REV',
'UID',
'VERSION',
];
foreach ($vobject->children() as $prop) {
if (!($prop instanceof Property)) {
continue;
}
switch ($prop->name) {
// TODO: Map all vCard properties to class properties
case 'EMAIL':
$props = [];
foreach ($prop->parameters() as $name => $value) {
$key = Str::camel(strtolower($name));
$props[$key] = (string) $value;
}
$props['email'] = (string) $prop;
$this->email[] = $props;
break;
case 'MEMBER':
foreach ($prop as $member) {
$value = (string) $member;
- if (preg_match('/^mailto:/', $value)) {
- $this->members[] = $value;
+ if (preg_match('/^(urn:uuid|mailto):/i', $value)) {
+ $this->member[] = $value;
}
}
break;
default:
// map string properties
if (in_array($prop->name, $string_properties)) {
$key = Str::camel(strtolower($prop->name));
$this->{$key} = (string) $prop;
}
// custom properties
if (\str_starts_with($prop->name, 'X-')) {
$this->custom[$prop->name] = (string) $prop;
}
}
}
if (!empty($this->custom['X-ADDRESSBOOKSERVER-KIND']) && empty($this->kind)) {
$this->kind = strtolower($this->custom['X-ADDRESSBOOKSERVER-KIND']);
}
}
/**
* Create string representation of the DAV object (vcard)
*
* @return string
*/
public function __toString()
{
if (!$this->vobject) {
// TODO we currently can only serialize a message back that we just read
throw new \Exception("Writing from properties is not implemented");
}
return Writer::write($this->vobject);
}
}
diff --git a/src/app/Http/Controllers/API/V4/SearchController.php b/src/app/Http/Controllers/API/V4/SearchController.php
index 4f8f208b..5b4a5021 100644
--- a/src/app/Http/Controllers/API/V4/SearchController.php
+++ b/src/app/Http/Controllers/API/V4/SearchController.php
@@ -1,162 +1,162 @@
guard()->user();
$search = trim(request()->input('search'));
$with_aliases = !empty(request()->input('alias'));
$limit = intval(request()->input('limit'));
if ($limit <= 0) {
$limit = 15;
} elseif ($limit > 100) {
$limit = 100;
}
// Prepare the query
$query = User::select('email', 'id')->where('id', $user->id);
$aliases = DB::table('user_aliases')->select(DB::raw('alias as email, user_id as id'))
->where('user_id', $user->id);
if (strlen($search)) {
$aliases->whereLike('alias', $search);
$query->whereLike('email', $search);
}
if ($with_aliases) {
$query->union($aliases);
}
// Execute the query
$result = $query->orderBy('email')->limit($limit)->get();
$result = $this->resultFormat($result);
return response()->json([
'list' => $result,
'count' => count($result),
]);
}
/**
* Search request for addresses of all users (in an account)
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function searchUser(Request $request)
{
$user = $this->guard()->user();
$search = trim(request()->input('search'));
$with_aliases = !empty(request()->input('alias'));
$limit = intval(request()->input('limit'));
if ($limit <= 0) {
$limit = 15;
} elseif ($limit > 100) {
$limit = 100;
}
$wallet = $user->wallet();
// Limit users to the user's account
$allUsers = $wallet->entitlements()
->where('entitleable_type', User::class)
->select('entitleable_id')
->distinct();
// Sub-query for user IDs who's names match the search criteria
$foundUserIds = UserSetting::select('user_id')
->whereIn('key', ['first_name', 'last_name'])
->whereLike('value', $search)
->whereIn('user_id', $allUsers);
// Prepare the query
$query = User::select('email', 'id')->whereIn('id', $allUsers);
$aliases = DB::table('user_aliases')->select(DB::raw('alias as email, user_id as id'))
->whereIn('user_id', $allUsers);
if (strlen($search)) {
$query->where(function ($query) use ($foundUserIds, $search) {
$query->whereLike('email', $search)
->orWhereIn('id', $foundUserIds);
});
$aliases->where(function ($query) use ($foundUserIds, $search) {
$query->whereLike('alias', $search)
->orWhereIn('user_id', $foundUserIds);
});
}
if ($with_aliases) {
$query->union($aliases);
}
// Execute the query
$result = $query->orderBy('email')->limit($limit)->get();
$result = $this->resultFormat($result);
return response()->json([
'list' => $result,
'count' => count($result),
]);
}
/**
* Format the search result, inject user names
*/
protected function resultFormat($result)
{
if ($result->count()) {
// Get user names
$settings = UserSetting::whereIn('key', ['first_name', 'last_name'])
->whereIn('user_id', $result->pluck('id'))
->get()
->mapWithKeys(function ($item) {
return [($item->user_id . ':' . $item->key) => $item->value];
})
->all();
// "Format" the result, include user names
$result = $result->map(function ($record) use ($settings) {
return [
'email' => $record->email,
'name' => trim(
($settings["{$record->id}:first_name"] ?? '')
. ' '
. ($settings["{$record->id}:last_name"] ?? '')
),
];
- })
+ })
->sortBy(['name', 'email'])
->values();
}
return $result;
}
}
diff --git a/src/tests/Feature/Console/Scalpel/Domain/CreateCommandTest.php b/src/tests/Feature/Console/Scalpel/Domain/CreateCommandTest.php
index dbe03d99..f949cf74 100644
--- a/src/tests/Feature/Console/Scalpel/Domain/CreateCommandTest.php
+++ b/src/tests/Feature/Console/Scalpel/Domain/CreateCommandTest.php
@@ -1,64 +1,65 @@
deleteTestDomain('domain-delete.com');
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestDomain('domain-delete.com');
parent::tearDown();
}
/**
* Test the command execution
*/
public function testHandle(): void
{
// Test --help argument
$code = \Artisan::call("scalpel:domain:create --help");
$output = trim(\Artisan::output());
$this->assertSame(0, $code);
$this->assertStringContainsString('--namespace[=NAMESPACE]', $output);
$this->assertStringContainsString('--type[=TYPE]', $output);
$this->assertStringContainsString('--status[=STATUS]', $output);
$this->assertStringContainsString('--tenant_id[=TENANT_ID]', $output);
$tenant = \App\Tenant::orderBy('id', 'desc')->first();
// Test successful domain creation
- $code = \Artisan::call("scalpel:domain:create"
+ $code = \Artisan::call(
+ "scalpel:domain:create"
. " --namespace=domain-delete.com"
- . " --type=" . Domain::TYPE_PUBLIC
+ . " --type={Domain::TYPE_PUBLIC}"
. " --tenant_id={$tenant->id}"
);
$output = trim(\Artisan::output());
$domain = $this->getTestDomain('domain-delete.com');
$this->assertSame(0, $code);
$this->assertSame($output, (string) $domain->id);
$this->assertSame('domain-delete.com', $domain->namespace);
$this->assertSame(Domain::TYPE_PUBLIC, $domain->type);
$this->assertSame($domain->tenant_id, $tenant->id);
}
}
diff --git a/src/tests/Unit/DataMigrator/EWS/AppointmentTest.php b/src/tests/Unit/DataMigrator/EWS/AppointmentTest.php
index 4ee95f26..8e11b99b 100644
--- a/src/tests/Unit/DataMigrator/EWS/AppointmentTest.php
+++ b/src/tests/Unit/DataMigrator/EWS/AppointmentTest.php
@@ -1,112 +1,113 @@
'test']);
$appointment = new EWS\Appointment($ews, $folder);
$ical = file_get_contents(__DIR__ . '/../../../data/ews/event/1.ics');
+ $ical = preg_replace('/\r?\n/', "\r\n", $ical);
// FIXME: I haven't found a way to convert xml content into a Type instance
// therefore we create it "manually", but it would be better to have both
// vcard and xml in a single data file that we could just get content from.
$item = Type::buildFromArray([
'MimeContent' => base64_encode($ical),
'ItemId' => new Type\ItemIdType(
'AAMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NGQ0OABGAAAAAAC9tlDYSlG2TaxWBr'
. 'A1OzWtBwAs2ajhknXlRYN/pbC8JqblAAAAAAEOAAAs2ajhknXlRYN/pbC8JqblAAJnrWkBAAA=',
'EQAAABYAAAAs2ajhknXlRYN/pbC8JqblAAJnqlKm',
),
'UID' => '1F3C13D7E99642A75ABE23D50487B454-8FE68B2E68E1B348',
'Subject' => 'test subject',
'HasAttachments' => false,
'IsAssociated' => false,
'Start' => '2023-11-21T11:00:00Z',
'End' => '2023-11-21T11:30:00Z',
'LegacyFreeBusyStatus' => 'Tentative',
'CalendarItemType' => 'Single',
'Organizer' => [
'Mailbox' => [
'Name' => 'Aleksander Machniak',
'EmailAddress' => 'test@kolab.org',
'RoutingType' => 'SMTP',
'MailboxType' => 'Contact',
],
],
'RequiredAttendees' => (object) [
'Attendee' => [
Type\AttendeeType::buildFromArray([
'Mailbox' => [
'Name' => 'Aleksander Machniak',
'EmailAddress' => 'test@kolab.org',
'RoutingType' => 'SMTP',
'MailboxType' => 'Contact',
],
'ResponseType' => 'Unknown',
]),
Type\AttendeeType::buildFromArray([
'Mailbox' => [
'Name' => 'Alec Machniak',
'EmailAddress' => 'test@outlook.com',
'RoutingType' => 'SMTP',
'MailboxType' => 'Mailbox',
],
'ResponseType' => 'Unknown',
]),
],
],
]);
// Convert the Exchange item into iCalendar
$ical = $this->invokeMethod($appointment, 'processItem', [$item]);
// Parse the iCalendar output
$event = new Vevent();
$this->invokeMethod($event, 'fromIcal', [$ical]);
$msId = implode('!', $item->getItemId()->toArray());
$this->assertSame($msId, $event->custom['X-MS-ID']);
$this->assertSame($item->getUID(), $event->uid);
$this->assertSame('test description', $event->description);
$this->assertSame('test subject', $event->summary);
$this->assertSame('CONFIRMED', $event->status);
$this->assertSame('PUBLIC', $event->class);
$this->assertSame('Microsoft Exchange Server 2010', $event->prodid);
$this->assertSame('2023-11-20T14:50:05+00:00', $event->dtstamp->getDateTime()->format('c'));
$this->assertSame('2023-11-21T12:00:00+01:00', $event->dtstart->getDateTime()->format('c'));
$this->assertSame('2023-11-21T12:30:00+01:00', $event->dtend->getDateTime()->format('c'));
// Organizer/attendees
$this->assertSame('test@kolab.org', $event->organizer['email']);
$this->assertSame('Aleksander Machniak', $event->organizer['cn']);
$this->assertSame('ORGANIZER', $event->organizer['role']);
$this->assertSame('ACCEPTED', $event->organizer['partstat']);
$this->assertSame(false, $event->organizer['rsvp']);
$this->assertCount(1, $event->attendees);
$this->assertSame('alec@outlook.com', $event->attendees[0]['email']);
$this->assertSame('Alec Machniak', $event->attendees[0]['cn']);
$this->assertSame('REQ-PARTICIPANT', $event->attendees[0]['role']);
$this->assertSame('NEEDS-ACTION', $event->attendees[0]['partstat']);
$this->assertSame(true, $event->attendees[0]['rsvp']);
}
}
diff --git a/src/tests/Unit/DataMigrator/EWS/ContactTest.php b/src/tests/Unit/DataMigrator/EWS/ContactTest.php
index 2aaa9a01..87a4be69 100644
--- a/src/tests/Unit/DataMigrator/EWS/ContactTest.php
+++ b/src/tests/Unit/DataMigrator/EWS/ContactTest.php
@@ -1,140 +1,141 @@
'test']);
$contact = new EWS\Contact($ews, $folder);
$vcard = file_get_contents(__DIR__ . '/../../../data/ews/contact/1.vcf');
+ $vcard = preg_replace('/\r?\n/', "\r\n", $vcard);
// FIXME: I haven't found a way to convert xml content into a Type instance
// therefore we create it "manually", but it would be better to have both
// vcard and xml in a single data file that we could just get content from.
$item = Type::buildFromArray([
'MimeContent' => base64_encode($vcard),
'ItemId' => new Type\ItemIdType(
'AAMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NGQ0OABGAAAAAAC9tlDYSlG2TaxWBr'
. 'A1OzWtBwAs2ajhknXlRYN/pbC8JqblAAAAAAEOAAAs2ajhknXlRYN/pbC8JqblAAJnrWkBAAA=',
'EQAAABYAAAAs2ajhknXlRYN/pbC8JqblAAJnqlKm',
),
'HasAttachments' => false,
'LastModifiedTime' => '2024-07-15T11:17:39,701Z',
'DisplayName' => 'Nowy Nazwisko',
'GivenName' => 'Nowy',
'Surname' => 'Nazwisko',
'EmailAddresses' => (object) [
'Entry' => [
Type\EmailAddressDictionaryEntryType::buildFromArray([
'Key' => 'EmailAddress1',
'Name' => 'test1@outlook.com',
'RoutingType' => 'SMTP',
'MailboxType' => 'Contact',
'_value' => 'christian1@outlook.com',
]),
Type\EmailAddressDictionaryEntryType::buildFromArray([
'Key' => 'EmailAddress2',
'Name' => 'test2@outlook.com',
'RoutingType' => 'SMTP',
'MailboxType' => 'Contact',
'_value' => 'test2@outlook.com',
]),
],
],
/*
ContactPicture.jpg
image/jpeg
2081
2024-07-15T11:17:38
false
true
category
en-US
Mr
Nowy
Bartosz
Nazwisko
Jr.
Nowy Nazwisko
alec
Company
Testowa
Warsaw
mazowickie
Poland
00-001
home123456
1234556679200
2014-10-11T11:59:00Z
IT
Developer
Office Location
2020-11-12T11:59:00Z
true
*/
]);
// Convert the Exchange item into vCard
$vcard = $this->invokeMethod($contact, 'processItem', [$item]);
// Parse the vCard
$contact = new Vcard();
$this->invokeMethod($contact, 'fromVcard', [$vcard]);
$this->assertMatchesRegularExpression('/^[a-f0-9]{40}$/', $contact->uid);
$this->assertSame('PUBLIC', $contact->class);
$this->assertSame('Nowy Nazwisko', $contact->fn);
$this->assertSame(null, $contact->kind);
$this->assertSame('Microsoft Exchange', $contact->prodid);
$this->assertSame('2024-07-15T11:17:39,701Z', $contact->rev);
$this->assertSame('Notatki do kontaktu', $contact->note);
// EWS Properties with special handling
$msId = implode('!', $item->getItemId()->toArray());
$this->assertSame($msId, $contact->custom['X-MS-ID']);
$this->assertSame('Partner Name', $contact->custom['X-SPOUSE']);
$this->assertSame('2020-11-12', $contact->custom['X-ANNIVERSARY']);
$this->assertCount(2, $contact->email);
$this->assertSame('internet', $contact->email[0]['type']);
$this->assertSame('christian1@outlook.com', $contact->email[0]['email']);
$this->assertSame('internet', $contact->email[1]['type']);
$this->assertSame('test2@outlook.com', $contact->email[1]['email']);
}
}
diff --git a/src/tests/Unit/DataMigrator/EWS/DistListTest.php b/src/tests/Unit/DataMigrator/EWS/DistListTest.php
index 528bfda7..e9e198a9 100644
--- a/src/tests/Unit/DataMigrator/EWS/DistListTest.php
+++ b/src/tests/Unit/DataMigrator/EWS/DistListTest.php
@@ -1,90 +1,90 @@
'test']);
$distlist = new EWS\DistList($ews, $folder);
// FIXME: I haven't found a way to convert xml content into a Type instance
// therefore we create it "manually", but it would be better to have both
// vcard and xml in a single data file that we could just get content from.
$item = Type::buildFromArray([
'ItemId' => new Type\ItemIdType(
'AAMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NGQ0OABGAAAAAAC9tlDYSlG2TaxWBr'
. 'A1OzWtBwAs2ajhknXlRYN/pbC8JqblAAAAAAEOAAAs2ajhknXlRYN/pbC8JqblAAJnrWkBAAA=',
'EQAAABYAAAAs2ajhknXlRYN/pbC8JqblAAJnqlKm',
),
'Subject' => 'subject list',
'LastModifiedTime' => '2024-06-27T13:44:32Z',
'DisplayName' => 'Lista',
'FileAs' => 'lista',
'Members' => (object) [
'Member' => [
Type\MemberType::buildFromArray([
'Key' => 'AAAAAIErH6S+oxAZnW4A3QEPVAIAAAGAYQBsAGUAYwBAAGEAbABlAGMALgBw'
. 'AGwAAABTAE0AVABQAAAAYQBsAGUAYwBAAGEAbABlAGMALgBwAGwAAAA=',
'Mailbox' => Type\Mailbox::buildFromArray([
'Name' => 'Alec',
'EmailAddress' => 'alec@kolab.org',
'RoutingType' => 'SMTP',
'MailboxType' => 'OneOff',
]),
'Status' => 'Normal',
]),
Type\MemberType::buildFromArray([
'Key' => 'AAAAAIErH6S+oxAZnW4A3QEPVAIAAAGAYQBsAGUAYwBAAGEAbABlAGMALgBw'
. 'AGwAAABTAE0AVABQAAAAYQBsAGUAYwBAAGEAbABlAGMALgBwAGwAAAB=',
'Mailbox' => Type\Mailbox::buildFromArray([
'Name' => 'Christian',
'EmailAddress' => 'christian@kolab.org',
'RoutingType' => 'SMTP',
'MailboxType' => 'OneOff',
'ItemId' => new Type\ItemIdType('AAA', 'BBB'),
]),
'Status' => 'Normal',
]),
],
],
]);
// Convert the Exchange item into vCard
$vcard = $this->invokeMethod($distlist, 'processItem', [$item]);
// Parse the vCard
$distlist = new Vcard();
$this->invokeMethod($distlist, 'fromVcard', [$vcard]);
$msId = implode('!', $item->getItemId()->toArray());
$this->assertSame(['X-MS-ID' => $msId], $distlist->custom);
$this->assertMatchesRegularExpression('/^[a-f0-9]{40}$/', $distlist->uid);
$this->assertSame('group', $distlist->kind);
$this->assertSame('Lista', $distlist->fn);
$this->assertSame('Kolab EWS Data Migrator', $distlist->prodid);
$this->assertSame('2024-06-27T13:44:32Z', $distlist->rev);
$members = [
'mailto:%22Alec%22+%3Calec%40kolab.org%3E',
'urn:uuid:' . sha1('AAA'),
];
- $this->assertSame($members, $distlist->members);
+ $this->assertSame($members, $distlist->member);
}
}