create('alice', [ 'subject_prefix' => 'Urlop', 'reply_every_message' => false, 'repeat_minutes' => 1440, 'body' => 'Body', 'start_value' => '2026-06-10|morning', 'end_value' => '2026-06-24|evening', 'start_ts' => 1, 'end_ts' => 2, 'enabled' => true, 'dynamic_scope' => true, ]); $calls = []; $api = new class($calls) extends DirectAdminVacationApi { public function __construct(private array &$calls) {} public function vacationExists(string $owner, string $email): bool { return false; } public function saveVacation(string $owner, string $email, array $rule, bool $exists): void { $this->calls[] = ['save', $owner, $email, $exists, $rule['subject_prefix']]; } public function deleteVacation(string $owner, string $email): void { $this->calls[] = ['delete', $owner, $email]; } }; $sync = new DirectAdminVacationSyncService( $repo, new DirectAdminSyncRepository(TEST_TMP . '/da-sync-data'), new MailboxDirectory($virtualRoot), $api ); $sync->syncUser('alice'); assert_same([ ['save', 'alice', 'info@example.com', false, 'Urlop'], ['save', 'alice', 'sales@example.com', false, 'Urlop'], ], $calls); $tracked = (new DirectAdminSyncRepository(TEST_TMP . '/da-sync-data'))->entriesForRule('alice', (string)$rule['id']); assert_same(['info@example.com', 'sales@example.com'], array_column($tracked, 'email')); }); test('directadmin vacation sync deletes stale tracked entries after rule removal', function (): void { $virtualRoot = TEST_TMP . '/da-sync-delete-virtual'; test_write($virtualRoot . '/domainowners', "example.com: alice\n"); test_write($virtualRoot . '/example.com/passwd', "info:x:1000:12::/home/alice/imap/example.com/info:/bin/false\n"); $dataDir = TEST_TMP . '/da-sync-delete-data'; $repo = new RuleRepository($dataDir); $syncRepo = new DirectAdminSyncRepository($dataDir); $rule = $repo->create('alice', [ 'subject_prefix' => 'Urlop', 'reply_every_message' => false, 'repeat_minutes' => 1440, 'body' => 'Body', 'start_value' => '2026-06-10|morning', 'end_value' => '2026-06-24|evening', 'start_ts' => 1, 'end_ts' => 2, 'enabled' => true, 'dynamic_scope' => true, ]); $syncRepo->replaceRuleEntries('alice', (string)$rule['id'], [ ['email' => 'info@example.com', 'domain' => 'example.com', 'mailbox' => 'info', 'api_key' => 'info@example.com'], ]); $repo->delete('alice', (string)$rule['id']); $calls = []; $api = new class($calls) extends DirectAdminVacationApi { public function __construct(private array &$calls) {} public function saveVacation(string $owner, string $email, array $rule, bool $exists): void {} public function deleteVacation(string $owner, string $email): void { $this->calls[] = ['delete', $owner, $email]; } }; $sync = new DirectAdminVacationSyncService($repo, $syncRepo, new MailboxDirectory($virtualRoot), $api); $sync->syncUser('alice'); assert_same([['delete', 'alice', 'info@example.com']], $calls); assert_same([], $syncRepo->entriesForRule('alice', (string)$rule['id'])); }); test('directadmin vacation sync refuses to overwrite untracked native vacation', function (): void { $virtualRoot = TEST_TMP . '/da-sync-conflict-virtual'; test_write($virtualRoot . '/domainowners', "example.com: alice\n"); test_write($virtualRoot . '/example.com/passwd', "info:x:1000:12::/home/alice/imap/example.com/info:/bin/false\n"); $dataDir = TEST_TMP . '/da-sync-conflict-data'; $repo = new RuleRepository($dataDir); $repo->create('alice', [ 'subject_prefix' => 'Urlop', 'reply_every_message' => false, 'repeat_minutes' => 1440, 'body' => 'Body', 'start_value' => '2026-06-10|morning', 'end_value' => '2026-06-24|evening', 'start_ts' => 1, 'end_ts' => 2, 'enabled' => true, 'dynamic_scope' => true, ]); $calls = []; $api = new class($calls) extends DirectAdminVacationApi { public function __construct(private array &$calls) {} public function vacationExists(string $owner, string $email): bool { return $email === 'info@example.com'; } public function saveVacation(string $owner, string $email, array $rule, bool $exists): void { $this->calls[] = ['save', $owner, $email]; } }; $sync = new DirectAdminVacationSyncService( $repo, new DirectAdminSyncRepository($dataDir), new MailboxDirectory($virtualRoot), $api ); $thrown = false; try { $sync->syncUser('alice'); } catch (RuntimeException $e) { $thrown = true; assert_contains('Untracked DirectAdmin vacation already exists', $e->getMessage()); } assert_true($thrown); assert_same([], $calls); });