diff --git a/src/app/Backends/Roundcube.php b/src/app/Backends/Roundcube.php index 809e79a5..7d88048d 100644 --- a/src/app/Backends/Roundcube.php +++ b/src/app/Backends/Roundcube.php @@ -1,256 +1,269 @@ table(self::FILESTORE_TABLE) ->where('user_id', self::userId($email)) ->where('context', 'enigma') ->delete(); } /** * List all files from the Enigma filestore. * * @param string $email User email address * * @return array List of Enigma filestore records */ public static function enigmaList(string $email): array { return self::dbh()->table(self::FILESTORE_TABLE) ->where('user_id', self::userId($email)) ->where('context', 'enigma') ->orderBy('filename') ->get() ->all(); } /** * Synchronize Enigma filestore from/to specified directory * * @param string $email User email address * @param string $homedir Directory location */ public static function enigmaSync(string $email, string $homedir): void { $db = self::dbh(); $debug = \config('app.debug'); $user_id = self::userId($email); $root = \config('filesystems.disks.pgp.root'); $fs = Storage::disk('pgp'); $files = []; $result = $db->table(self::FILESTORE_TABLE)->select('file_id', 'filename', 'mtime') ->where('user_id', $user_id) ->where('context', 'enigma') ->get(); foreach ($result as $record) { $file = $homedir . '/' . $record->filename; $mtime = $fs->exists($file) ? $fs->lastModified($file) : 0; $files[] = $record->filename; if ($mtime < $record->mtime) { $record = $db->table(self::FILESTORE_TABLE)->select('file_id', 'data', 'mtime') ->where('file_id', $record->file_id) ->first(); $data = $record ? base64_decode($record->data) : false; if ($data === false) { \Log::error("Failed to sync $file ({$record->file_id}). Decode error."); continue; } if ($fs->put($file, $data, true)) { // Note: Laravel Filesystem API does not provide touch method touch("$root/$file", $record->mtime); if ($debug) { \Log::debug("[SYNC] Fetched file: $file"); } } } } // Remove files not in database foreach (array_diff(self::enigmaFilesList($homedir), $files) as $file) { $file = $homedir . '/' . $file; if ($fs->delete($file)) { if ($debug) { \Log::debug("[SYNC] Removed file: $file"); } } } // No records found, do initial sync if already have the keyring if (empty($file)) { self::enigmaSave(true, $homedir); } } /** * Save the keys database * * @param string $email User email address * @param string $homedir Directory location * @param bool $is_empty Set to Tre if it is a initial save */ public static function enigmaSave(string $email, string $homedir, bool $is_empty = false): void { $db = self::dbh(); $debug = \config('app.debug'); $user_id = self::userId($email); $fs = Storage::disk('pgp'); $records = []; if (!$is_empty) { $records = $db->table(self::FILESTORE_TABLE)->select('file_id', 'filename', 'mtime') ->where('user_id', $user_id) ->where('context', 'enigma') ->get() ->keyBy('filename') ->all(); } foreach (self::enigmaFilesList($homedir) as $filename) { $file = $homedir . '/' . $filename; $mtime = $fs->exists($file) ? $fs->lastModified($file) : 0; $existing = !empty($records[$filename]) ? $records[$filename] : null; unset($records[$filename]); if ($mtime && (empty($existing) || $mtime > $existing->mtime)) { $data = base64_encode($fs->get($file)); /* if (empty($maxsize)) { $maxsize = min($db->get_variable('max_allowed_packet', 1048500), 4*1024*1024) - 2000; } if (strlen($data) > $maxsize) { \Log::error("Failed to save $file. Size exceeds max_allowed_packet."); continue; } */ $result = $db->table(self::FILESTORE_TABLE)->updateOrInsert( ['user_id' => $user_id, 'context' => 'enigma', 'filename' => $filename], ['mtime' => $mtime, 'data' => $data] ); if ($debug) { \Log::debug("[SYNC] Pushed file: $file"); } } } // Delete removed files from database foreach (array_keys($records) as $filename) { $file = $homedir . '/' . $filename; $result = $db->table(self::FILESTORE_TABLE) ->where('user_id', $user_id) ->where('context', 'enigma') ->where('filename', $filename) ->delete(); if ($debug) { \Log::debug("[SYNC] Removed file: $file"); } } } /** * Find the Roundcube user identifier for the specified user. * * @param string $email User email address * @param bool $create Make sure the user record exists * * @returns ?int Roundcube user identifier */ public static function userId(string $email, bool $create = true): ?int { $db = self::dbh(); $user = $db->table(self::USERS_TABLE)->select('user_id') ->where('username', \strtolower($email)) ->first(); // Create a user record, without it we can't use the Roundcube storage if (empty($user)) { if (!$create) { return null; } $uri = \parse_url(\config('imap.uri')); - return (int) $db->table(self::USERS_TABLE)->insertGetId( + $user_id = (int) $db->table(self::USERS_TABLE)->insertGetId( [ 'username' => $email, 'mail_host' => $uri['host'], 'created' => now()->toDateTimeString(), ], 'user_id' ); + + $username = \App\User::where('email', $email)->first()->name(); + + $db->table(self::IDENTITIES_TABLE)->insert([ + 'user_id' => $user_id, + 'email' => $email, + 'name' => $username, + 'changed' => now()->toDateTimeString(), + 'standard' => 1, + ]); + + return $user_id; } return (int) $user->user_id; } /** * Returns list of Enigma user homedir files to backup/sync */ private static function enigmaFilesList(string $homedir) { $files = []; $fs = Storage::disk('pgp'); foreach (self::$enigma_files as $file) { if ($fs->exists($homedir . '/' . $file)) { $files[] = $file; } } foreach ($fs->files($homedir . '/private-keys-v1.d') as $file) { if (preg_match('/\.key$/', $file)) { $files[] = substr($file, strlen($homedir . '/')); } } return $files; } } diff --git a/src/tests/Feature/Backends/RoundcubeTest.php b/src/tests/Feature/Backends/RoundcubeTest.php new file mode 100644 index 00000000..2741176c --- /dev/null +++ b/src/tests/Feature/Backends/RoundcubeTest.php @@ -0,0 +1,61 @@ +deleteTestUser('roundcube@' . \config('app.domain')); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('roundcube@' . \config('app.domain')); + + parent::tearDown(); + } + + /** + * Test creating a Roundcube user record (and related data) + * + * @group roundcube + */ + public function testUserCreation(): void + { + $user = $this->getTestUser('roundcube@' . \config('app.domain')); + $user->setSetting('first_name', 'First'); + $user->setSetting('last_name', 'Last'); + + $db = Roundcube::dbh(); + + // delete the user record if exists + if ($userid = Roundcube::userId($user->email, false)) { + $db->table('users')->delete(); + } + + // Create the user + $userid = Roundcube::userId($user->email); + + $rcuser = $db->table('users')->where('username', $user->email)->first(); + + $this->assertTrue(!empty($rcuser)); + + $rcidentity = $db->table('identities')->where('user_id', $rcuser->user_id)->first(); + + $this->assertSame($user->email, $rcidentity->email); + $this->assertSame('First Last', $rcidentity->name); + $this->assertSame(1, $rcidentity->standard); + } +}