diff --git a/src/app/Backends/DAV.php b/src/app/Backends/DAV.php
--- a/src/app/Backends/DAV.php
+++ b/src/app/Backends/DAV.php
@@ -2,6 +2,7 @@
namespace App\Backends;
+use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
class DAV
@@ -343,6 +344,65 @@
return $response !== false;
}
+ /**
+ * Initialize default DAV folders (collections)
+ *
+ * @param \App\User $user User object
+ *
+ * @throws \Exception
+ */
+ public static function initDefaultFolders(\App\User $user): void
+ {
+ if (!\config('services.dav.uri')) {
+ return;
+ }
+
+ $folders = \config('services.dav.default_folders');
+ if (!count($folders)) {
+ return;
+ }
+
+ // Cyrus DAV does not support proxy authorization via DAV. Even though it has
+ // the Authorize-As header, it is used only for cummunication with Murder backends.
+ // We use a one-time token instead. It's valid for 10 seconds, assume it's enough time.
+ $password = \App\Auth\Utils::tokenCreate((string) $user->id);
+
+ if ($password === null) {
+ throw new \Exception("Failed to create an authentication token for DAV");
+ }
+
+ $dav = new self($user->email, $password);
+
+ foreach ($folders as $props) {
+ $folder = new DAV\Folder();
+ $folder->href = $props['type'] . 's' . '/user/' . $user->email . '/' . $props['path'];
+ $folder->types = ['collection', $props['type']];
+ $folder->name = $props['displayname'] ?? '';
+ $folder->components = $props['components'] ?? [];
+
+ $existing = null;
+ try {
+ $existing = $dav->folderInfo($folder->href);
+ } catch (RequestException $e) {
+ // Cyrus DAV returns 503 Service Unavailable on a non-existing location (?)
+ if ($e->getCode() != 503 && $e->getCode() != 404) {
+ throw $e;
+ }
+ }
+
+ // folder already exists? check the properties and update if needed
+ if ($existing) {
+ if ($existing->name != $folder->name || $existing->components != $folder->components) {
+ if (!$dav->folderUpdate($folder)) {
+ throw new \Exception("Failed to update DAV folder {$folder->href}");
+ }
+ }
+ } elseif (!$dav->folderCreate($folder)) {
+ throw new \Exception("Failed to create DAV folder {$folder->href}");
+ }
+ }
+ }
+
/**
* Check server options (and authentication)
*
diff --git a/src/app/Backends/DAV/Folder.php b/src/app/Backends/DAV/Folder.php
--- a/src/app/Backends/DAV/Folder.php
+++ b/src/app/Backends/DAV/Folder.php
@@ -94,10 +94,15 @@
$type = 'calendar';
}
- $props .= '' . ($type ? "" : '') . '';
+ // Cyrus DAV does not allow resourcetype property change
+ if ($tag != 'propertyupdate') {
+ $props .= '' . ($type ? "" : '') . '';
+ }
if (!empty($this->components)) {
- $props .= '';
+ // Note: Normally Cyrus DAV does not allow supported-calendar-component-set property update,
+ // but I found in Cyrus code that the action can be forced with force=yes attribute.
+ $props .= '';
foreach ($this->components as $component) {
$props .= '';
}
diff --git a/src/app/Backends/Helper.php b/src/app/Backends/Helper.php
new file mode 100644
--- /dev/null
+++ b/src/app/Backends/Helper.php
@@ -0,0 +1,55 @@
+ 'Default',
+ 'displayname' => 'Calendar',
+ 'components' => ['VEVENT'],
+ 'type' => 'calendar',
+ ],
+ [
+ 'path' => 'Tasks',
+ 'displayname' => 'Tasks',
+ 'components' => ['VTODO'],
+ 'type' => 'calendar',
+ ],
+ [
+ // FIXME: Same here, should we use 'Contacts'?
+ 'path' => 'Default',
+ 'displayname' => 'Contacts',
+ 'type' => 'addressbook',
+ ],
+ ];
+ }
+
+ /**
+ * List of default IMAP folders
+ */
+ public static function defaultImapFolders(): array
+ {
+ // TODO: Move the list from config/imap.php
+ return [];
+ }
+}
diff --git a/src/app/Jobs/User/CreateJob.php b/src/app/Jobs/User/CreateJob.php
--- a/src/app/Jobs/User/CreateJob.php
+++ b/src/app/Jobs/User/CreateJob.php
@@ -106,6 +106,9 @@
$user->status |= \App\User::STATUS_IMAP_READY;
}
+ // FIXME: Should we ignore exceptions on this operation or introduce DAV_READY status?
+ \App\Backends\DAV::initDefaultFolders($user);
+
// Make user active in non-mandate mode only
if (
!($wallet = $user->wallet())
diff --git a/src/config/services.php b/src/config/services.php
--- a/src/config/services.php
+++ b/src/config/services.php
@@ -58,6 +58,7 @@
'dav' => [
'uri' => env('DAV_URI', 'https://proxy/'),
+ 'default_folders' => \App\Backends\Helper::defaultDavFolders(),
'verify' => (bool) env('DAV_VERIFY', true),
],
diff --git a/src/phpunit.xml b/src/phpunit.xml
--- a/src/phpunit.xml
+++ b/src/phpunit.xml
@@ -43,5 +43,6 @@
+
diff --git a/src/tests/Feature/Backends/DAVTest.php b/src/tests/Feature/Backends/DAVTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Backends/DAVTest.php
@@ -0,0 +1,100 @@
+markTestSkipped();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ if ($this->user) {
+ $this->deleteTestUser($this->user->email);
+ }
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test initializing default folders for a user.
+ *
+ * @group imap
+ * @group dav
+ */
+ public function testInitDefaultFolders(): void
+ {
+ Queue::fake();
+
+ $props = ['password' => 'test-pass'];
+ $this->user = $user = $this->getTestUser('davtest-' . time() . '@' . \config('app.domain'), $props);
+
+ // Create the IMAP mailbox, it is required otherwise DAV requests will fail
+ \config(['imap.default_folders' => null]);
+ IMAP::createUser($user);
+
+ $dav_folders = [
+ [
+ 'path' => 'Default',
+ 'displayname' => 'Calendar-Test',
+ 'components' => ['VEVENT'],
+ 'type' => 'calendar',
+ ],
+ [
+ 'path' => 'Tasks',
+ 'displayname' => 'Tasks-Test',
+ 'components' => ['VTODO'],
+ 'type' => 'calendar',
+ ],
+ [
+ 'path' => 'Default',
+ 'displayname' => 'Contacts-Test',
+ 'type' => 'addressbook',
+ ],
+ ];
+
+ \config(['services.dav.default_folders' => $dav_folders]);
+ DAV::initDefaultFolders($user);
+
+ $dav = new DAV($user->email, $props['password']);
+
+ $folders = $dav->listFolders(DAV::TYPE_VCARD);
+ $this->assertCount(1, $folders);
+ $this->assertSame('Contacts-Test', $folders[0]->name);
+
+ $folders = $dav->listFolders(DAV::TYPE_VEVENT);
+ $folders = array_filter($folders, function ($f) { return $f->name != 'Inbox' && $f->name != 'Outbox'; });
+ $folders = array_values($folders);
+ $this->assertCount(1, $folders);
+ $this->assertSame(['VEVENT'], $folders[0]->components);
+ $this->assertSame(['collection', 'calendar'], $folders[0]->types);
+ $this->assertSame('Calendar-Test', $folders[0]->name);
+
+ $folders = $dav->listFolders(DAV::TYPE_VTODO);
+ $folders = array_filter($folders, function ($f) { return $f->name != 'Inbox' && $f->name != 'Outbox'; });
+ $folders = array_values($folders);
+ $this->assertCount(1, $folders);
+ $this->assertSame(['VTODO'], $folders[0]->components);
+ $this->assertSame(['collection', 'calendar'], $folders[0]->types);
+ $this->assertSame('Tasks-Test', $folders[0]->name);
+ }
+}