diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..bedadcd --- /dev/null +++ b/.env.sample @@ -0,0 +1,13 @@ +CLI_PATH=./cli +API_PATH=./api +UI_PATH=./ui + +COMPOSE_PROJECT_NAME=emails +COMPOSE_PATH_SEPARATOR=: +COMPOSE_FILE=./docker-compose.yml:${CLI_PATH}/docker-compose.yml:${API_PATH}/docker-compose.yml:${UI_PATH}/docker-compose.yml +COMPOSE_PROFILES=api,ui,cli + +ATT_PATH=./attachments +LOGS_PATH=./logs +WEB_PORT=8000 +API_PORT=8080 diff --git a/.gitignore b/.gitignore index e1ccfc6..fd55c59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ # Env **/*.env +**/attachments/ +**/logs/ # Composer **/vendor/ +**/*.lock # Views **/cache/ diff --git a/.key.env.sample b/.key.env.sample new file mode 100644 index 0000000..48204cd --- /dev/null +++ b/.key.env.sample @@ -0,0 +1 @@ +API_KEY= diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..b00fae6 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,43 @@ +## UI +* [x] List `mailboxes` all in the `Email Provider`, identifying those *registered* locally. +* [x] Select which to *register* or unregister for watching. +* [x] List *registered* `mailboxes`. +* [x] List `messages` for selected `mailbox`. +* [x] Schedule `attachments` downloads. +* [ ] Download `attachments` (*encrypted* & *decrypted*). + +## CLI +#### Automatic +* [x] `mailboxes:check`: Get *registered* `mailboxes` and schedule `messages:grab` for `mailbox_id`. +* [x] `attachments:check`: Check *saved* `attachments` and schedule `attachments:decrypt` for `attachment_id`. +* [x] `jobs:check`: Get *pending* `jobs` and run them. +#### Scheduled +* [x] `messages:grab`: Grab `messages` for `mailbox`. Arguments: `mailbox_id`. +* [x] `attachments:grab`: Grab `attachments` for `message`. Arguments: `message_id`. +* [x] `attachments:decrypt`: Decrypt `attachment`. Arguments: `attachment_id`. + +## API +* [x] Grab all `mailboxes` from `Email Provider`, identifying those that are registered. +* [x] Register `mailboxes` into **[database]** and grab latest `messages`. +* [x] Grab new `messages` from `Email Provider` for selected `mailboxes` and store them in the `database`. +* [x] Grab `messages` from **[database]** for selected `mailboxes`. +* [x] Grab `attachments` from `Email Provider` for selected `messages`. +* [x] Register `messages` for `attachment` job. +* [x] Decrypt `attachments`. + + +## Workflow +* **[User]** Choose `mailboxes` to register or unregister. + -> **[API]** Register selected `mailboxes`, register new `messages:grab` job. +* **[Cron]** Get `jobs`, run `jobs`. +* **[User]** Check messages found -> **[API]** Schedule `attachments`. + +## Jobs +#### Automatic +* [x] Check *registered* `mailboxes` for new `messages`. Every weekday. +* [x] Check if `attachments` are *encrypted*. Every weekday. +* [x] Check for new *scheduled* `jobs`. Every minute. +#### Scheduled +* [ ] Grab `messages` for `mailbox` id. +* [ ] Grab `attachments` for `message` id. +* [ ] Decrypt `attachment`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..21652a3 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Emails +ProVM + +## Description +Grab attachments from emails by inbox. +* Choose what mailboxes to watch. +* Select messages that you want to grab attachments from. +* Download (or view in browser) (decrypted) attachments from messages. + +## Requirements + +* Docker with Docker Compose [https://www.docker.com/](https://www.docker.com/) + +## Installation + +1. Pull from repository + ``` + git pull git@git.provm.cl:Incoviba/emails.git + ``` + Change to latest release + ``` + git checkout release + ``` +2. Check the docker-compose.yml files + 1. docker-compose.yml - central proxy container + 2. api/docker-compose.yml - API container and database + 3. cli/docker-compose.yml - CLI container that runs cron jobs + 4. ui/docker-compose.yml - UI container +3. Generate the API Key + If you have openssl (comes with most linux distros) you can run this in terminal + ``` + echo API_KEY=`(openssl rand -hex 128)` >> .key.env + ``` +4. Check Environment files + 1. .env - Docker Compose and Environment settings. Check volumes and ports. + 2. .key.env - API_KEY, generated before. + 3. .mail.env - Email Identification. + 4. api/.env - Encrypted PDF files passwords. + 5. api/.db.env - Database configuration. + 6. cli/.env - API_URI, for connecting to the api container from the cli container. Change it if the api is someplace else. + 7. ui/.env - API_URI, same as from the cli. +5. Check if every configuration is correct before starting the application + ``` + docker compose config + ``` + If everything is fine + ``` + docker compose up -d + ``` +6. Connect to the UI + Default [http://localhost:8000](http://localhost:8000) + +## Development +When development there is a container for adminer, to use it you can add it into `COMPOSE_PROFILES` or run +``` +docker compose up -d adminer +``` diff --git a/api/.adminer.env.sample b/api/.adminer.env.sample new file mode 100644 index 0000000..e6286fe --- /dev/null +++ b/api/.adminer.env.sample @@ -0,0 +1,2 @@ +ADMINER_DESIGN=dracula +ADMINER_PLUGINS="tables-filter table-indexes-structure table-structure struct-comments json-column edit-calendar edit-textarea dump-bz2 dump-date dump-json dump-php enum-option" diff --git a/api/.db.env.sample b/api/.db.env.sample new file mode 100644 index 0000000..6e2de9c --- /dev/null +++ b/api/.db.env.sample @@ -0,0 +1,4 @@ +MYSQL_ROOT_PASSWORD= +MYSQL_DATABASE= +MYSQL_USER= +MYSQL_PASSWORD= diff --git a/api/.env.sample b/api/.env.sample new file mode 100644 index 0000000..3990b1e --- /dev/null +++ b/api/.env.sample @@ -0,0 +1,2 @@ +PASSWORDS_SEPARATOR=, +PASSWORDS= diff --git a/api/.mail.env.sample b/api/.mail.env.sample new file mode 100644 index 0000000..8a047c4 --- /dev/null +++ b/api/.mail.env.sample @@ -0,0 +1,5 @@ +EMAIL_HOST=imap.gmail.com +EMAIL_PORT=993 +EMAIL_USERNAME=@gmail.com +EMAIL_PASSWORD= +ATTACHMENTS_FOLDER=/attachments diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..460c034 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,13 @@ +FROM php:8-fpm + +RUN apt-get update \ + && apt-get install -y libc-client-dev libkrb5-dev git libzip-dev unzip qpdf \ + && rm -r /var/lib/apt/lists/* \ + && docker-php-ext-configure imap --with-kerberos --with-imap-ssl \ + && docker-php-ext-install imap zip pdo pdo_mysql + +COPY ./errors.ini /usr/local/etc/php/conf.d/docker-php-errors.ini + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +WORKDIR /app/api diff --git a/api/common/Controller/Attachments.php b/api/common/Controller/Attachments.php new file mode 100644 index 0000000..d95d028 --- /dev/null +++ b/api/common/Controller/Attachments.php @@ -0,0 +1,58 @@ +toArray(); + },$service->getAll()); + $output = [ + 'total' => count($attachments), + 'attachments' => $attachments + ]; + return $this->withJson($response, $output); + } + public function get(ServerRequestInterface $request, ResponseInterface $response, Service\Attachments $service, LoggerInterface $logger, int $attachment_id): ResponseInterface + { + $attachment = $service->getRepository()->fetchById($attachment_id); + + $response->withHeader('Content-Type', 'application/pdf'); + $response->withHeader('Content-Disposition', "'attachment;filename='{$attachment->getFullFilename()}'"); + $response->getBody()->write($service->getFile($attachment_id)); + return $response; + } + public function grab(ServerRequestInterface $request, ResponseInterface $response, Service\Attachments $service, Service\Messages $messagesService): ResponseInterface + { + $body = $request->getBody(); + $json = json_decode($body->getContents()); + if (!isset($json->messages)) { + throw new MissingArgument('messages', 'array', 'Messages UIDs'); + } + $output = [ + 'messages' => $json->messages, + 'attachments' => [], + 'total' => 0 + ]; + foreach ($json->messages as $message_uid) { + $message = $messagesService->getLocalMessage($message_uid); + $attachments = $service->grab($message->getId()); + $output['attachments'] = array_merge($output['attachments'], $attachments); + $output['total'] += count($attachments); + } + + return $this->withJson($response, $output); + } +} diff --git a/api/common/Controller/Base.php b/api/common/Controller/Base.php new file mode 100644 index 0000000..4b8013f --- /dev/null +++ b/api/common/Controller/Base.php @@ -0,0 +1,19 @@ +withJson($response, [ + 'version' => '1.0.0', + 'app' => 'emails' + ]); + } +} diff --git a/api/common/Controller/Jobs.php b/api/common/Controller/Jobs.php new file mode 100644 index 0000000..5979fdc --- /dev/null +++ b/api/common/Controller/Jobs.php @@ -0,0 +1,85 @@ +getRepository()->fetchAll(); + return $this->withJson($response, compact('jobs')); + } + public function schedule(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $body = $request->getBody(); + $json = json_decode($body->getContents()); + if (!isset($json->jobs)) { + throw new MissingArgument('jobs', 'array', 'job commands with arguments'); + } + $output = [ + 'jobs' => $json->jobs, + 'total' => count($json->jobs), + 'scheduled' => 0 + ]; + foreach ($json->jobs as $job) { + if ($service->queue($job->command, $job->arguments)) { + $output['scheduled'] ++; + } + } + return $this->withJson($response, $output); + } + public function pending(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $pending = $service->getPending(); + $output = [ + 'total' => count($pending), + 'jobs' => $pending + ]; + return $this->withJson($response, $output); + } + public function pendingCommands(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $body = $response->getBody(); + $json = json_decode($body->getContents()); + if (!isset($json->commands)) { + throw new MissingArgument('commands', 'array', 'job commands'); + } + $output = [ + 'commands' => $json->commands, + 'total' => count($json->commands), + 'pending' => [] + ]; + foreach ($json->commands as $command) { + $pending = $service->getPendingByCommand($command); + if (count($pending) === 0) { + continue; + } + $output['pending'][$command] = $pending; + } + return $this->withJson($response, $output); + } + public function finish(ServerRequestInterface $request, ResponseInterface $response, Service $service, $job_id): ResponseInterface + { + $output = [ + 'job_id' => $job_id, + 'status' => $service->finish($job_id) + ]; + return $this->withJson($response, $output); + } + public function failed(ServerRequestInterface $request, ResponseInterface $response, Service $service, $job_id): ResponseInterface + { + $output = [ + 'job_id' => $job_id, + 'status' => $service->failed($job_id) + ]; + return $this->withJson($response, $output); + } +} diff --git a/api/common/Controller/Mailboxes.php b/api/common/Controller/Mailboxes.php new file mode 100644 index 0000000..a224799 --- /dev/null +++ b/api/common/Controller/Mailboxes.php @@ -0,0 +1,109 @@ + $mailbox->getName(), 'registered' => false]; + if ($service->isRegistered($mailbox->getName())) { + $mb = $service->getLocalMailbox($mailbox->getName()); + $arr['id'] = $mb->getId(); + $arr['registered'] = true; + } + return $arr; + }, $service->getAll())); + $output = [ + 'total' => count($mailboxes), + 'mailboxes' => $mailboxes + ]; + return $this->withJson($response, $output); + } + public function registered(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $mailboxes = array_map(function(Mailbox $mailbox) { + return $mailbox->toArray(); + }, $service->getRegistered()); + $output = [ + 'total' => count($mailboxes), + 'mailboxes' => $mailboxes + ]; + return $this->withJson($response, $output); + } + public function get(ServerRequestInterface $request, ResponseInterface $response, Service $service, int $mailbox_id): ResponseInterface + { + $mailbox = $service->getRepository()->fetchById($mailbox_id); + return $this->withJson($response, ['mailbox' => $mailbox->toArray()]); + } + public function register(ServerRequestInterface $request, ResponseInterface $response, Service $service, \ProVM\Common\Service\Messages $messagesService): ResponseInterface + { + $body = $request->getBody(); + $json = \Safe\json_decode($body->getContents()); + if (!isset($json->mailboxes)) { + throw new MissingArgument('mailboxes', 'array', 'mailboxes names'); + } + $output = [ + 'mailboxes' => $json->mailboxes, + 'total' => count($json->mailboxes), + 'registered' => [ + 'total' => 0, + 'mailboxes' => [] + ] + ]; + foreach ($json->mailboxes as $mailbox_name) { + $arr = [ + 'id' => '', + 'name' => $mailbox_name, + 'registered' => false + ]; + if ($service->register($mailbox_name)) { + $mailbox = $service->getLocalMailbox($mailbox_name); + $arr['id'] = $mailbox->getId(); + $arr['registered'] = true; + $output['registered']['total'] ++; + $output['registered']['mailboxes'] []= $arr; + $messagesService->grab($mailbox_name); + } + } + return $this->withJson($response, $output, 201); + } + public function unregister(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $body = $request->getBody(); + $json = \Safe\json_decode($body->getContents()); + if (!isset($json->mailboxes)) { + throw new MissingArgument('mailboxes', 'array', 'mailboxes ids'); + } + $output = [ + 'mailboxes' => $json->mailboxes, + 'total' => count($json->mailboxes), + 'unregistered' => [ + 'total' => 0, + 'mailboxes' => [] + ] + ]; + foreach ($json->mailboxes as $mailbox_id) { + $mailbox = $service->getRepository()->fetchById($mailbox_id); + if ($service->unregister($mailbox->getName())) { + $output['unregistered']['total'] ++; + $output['unregistered']['mailboxes'] []= [ + 'id' => $mailbox->getId(), + 'name' => $mailbox->getName(), + 'unregistered' => true + ]; + } + } + return $this->withJson($response, $output); + } +} \ No newline at end of file diff --git a/api/common/Controller/Messages.php b/api/common/Controller/Messages.php new file mode 100644 index 0000000..7b0d387 --- /dev/null +++ b/api/common/Controller/Messages.php @@ -0,0 +1,111 @@ +getMailboxes()->get($mailbox_id); + $messages = array_map(function(Message $message) { + return $message->toArray(); + }, $service->getAll($mailbox->getName())); + usort($messages, function($a, $b) { + $d = $a['date_time'] - $b['date_time']; + if ($d->days === 0) { + $f = strcmp($a['from'], $b['from']); + if ($f === 0) { + return strcmp($a['subject'], $b['subject']); + } + return $f; + } + return $d->format('%r%a'); + }); + $output = [ + 'mailbox' => $mailbox->toArray(), + 'total' => count($messages), + 'messages' => $messages + ]; + return $this->withJson($response, $output); + } + public function valid(ServerRequestInterface $request, ResponseInterface $response, Service\Messages $service, Service\Attachments $attachments, int $mailbox_id): ResponseInterface + { + $mailbox = $service->getMailboxes()->get($mailbox_id); + $messages = array_values(array_filter(array_map(function(Message $message) use ($service, $attachments) { + return $message->toArray(); + }, $service->getValid($mailbox->getName())), function($message) { + return $message !== null; + })); + usort($messages, function($a, $b) { + $d = strcmp($a['date_time'], $b['date_time']); + if ($d === 0) { + $f = strcmp($a['from'], $b['from']); + if ($f === 0) { + return strcmp($a['subject'], $b['subject']); + } + return $f; + } + return $d; + }); + $output = [ + 'mailbox' => $mailbox->toArray(), + 'total' => count($messages), + 'messages' => $messages + ]; + return $this->withJson($response, $output); + } + public function get(ServerRequestInterface $request, ResponseInterface $response, Service\Messages $service, int $message_id): ResponseInterface + { + $message = $service->getRepository()->fetchById($message_id); + return $this->withJson($response, ['message' => $message->toArray()]); + } + public function grab(ServerRequestInterface $request, ResponseInterface $response, Service\Messages $service, \ProVM\Common\Service\Attachments $attachmentsService, $mailbox_id): ResponseInterface + { + $output = [ + 'mailbox_id' => $mailbox_id, + 'messages' => [], + 'count' => 0 + ]; + $mailbox = $service->getMailboxes()->get($mailbox_id); + $messages = $service->grab($mailbox->getName()); + foreach ($messages as $message_id) { + $message = $service->getLocalMessage($message_id); + if ($message->hasValidAttachments()) { + $attachmentsService->create($message->getId()); + } + } + $output['messages'] = $messages; + $output['count'] = count($messages); + return $this->withJson($response, $output); + } + public function schedule(ServerRequestInterface $request, ResponseInterface $response, Service\Messages $service, Service\Jobs $jobsService): ResponseInterface + { + $body = $request->getBody(); + $json = json_decode($body); + if (!isset($json->messages)) { + throw new MissingArgument('messages', 'array', 'messages IDs'); + } + $output = [ + 'messages' => $json->messages, + 'scheduled' => 0 + ]; + foreach ($json->messages as $message_id) { + if ($jobsService->queue('attachments:grab', [$message_id])) { + $message = $service->getRepository()->fetchById($message_id); + $message->doesHaveScheduledDownloads(); + $service->getRepository()->save($message); + $output['scheduled'] ++; + } + } + return $this->withJson($response, $output); + } +} diff --git a/api/common/Define/Model.php b/api/common/Define/Model.php new file mode 100644 index 0000000..9af5971 --- /dev/null +++ b/api/common/Define/Model.php @@ -0,0 +1,8 @@ +getId()}"; + $code = 120; + parent::__construct($message, $code, $previous); + } +} \ No newline at end of file diff --git a/api/common/Exception/Database/BlankResult.php b/api/common/Exception/Database/BlankResult.php new file mode 100644 index 0000000..12734cc --- /dev/null +++ b/api/common/Exception/Database/BlankResult.php @@ -0,0 +1,14 @@ +getName()}"; + $code = 101; + parent::__construct($message, $code, $previous); + } +} diff --git a/api/common/Exception/Mailbox/Invalid.php b/api/common/Exception/Mailbox/Invalid.php new file mode 100644 index 0000000..c722cf9 --- /dev/null +++ b/api/common/Exception/Mailbox/Invalid.php @@ -0,0 +1,15 @@ +getName()} has not loaded any emails."; + $code = 102; + parent::__construct($message, $code, $previous); + } +} diff --git a/api/common/Exception/Message/NoAttachments.php b/api/common/Exception/Message/NoAttachments.php new file mode 100644 index 0000000..e36ef88 --- /dev/null +++ b/api/common/Exception/Message/NoAttachments.php @@ -0,0 +1,15 @@ +getSubject()} has no attachments"; + $code = 110; + parent::__construct($message, $code, $previous); + } +} \ No newline at end of file diff --git a/api/common/Exception/Request/Auth/Forbidden.php b/api/common/Exception/Request/Auth/Forbidden.php new file mode 100644 index 0000000..9c24549 --- /dev/null +++ b/api/common/Exception/Request/Auth/Forbidden.php @@ -0,0 +1,15 @@ +setContainer($container); + } + + protected ContainerInterface $container; + protected array $repositories; + + public function getContainer(): ContainerInterface + { + return $this->container; + } + public function getRepositories(): array + { + return $this->repositories; + } + public function getRepository(string $name): string + { + return $this->getRepositories()[$name]; + } + + public function setContainer(ContainerInterface $container): Model + { + $this->container = $container; + return $this; + } + public function addRepository(string $name, string $repository_class_name): Model + { + $this->repositories[$name] = $repository_class_name; + return $this; + } + public function setRepositories(array $repositories): Model + { + foreach ($repositories as $name => $class) { + $this->addRepository($name, $class); + } + return $this; + } + + public function find(string $model_class_name): Repository + { + $name = str_replace("ProVM\\Emails\\Model\\", '', $model_class_name); + try { + $repository_class = $this->getRepository($name); + } catch (\Exception $e) { + $repository_class = str_replace('Model', 'Repository', $model_class_name); + } + return $this->getContainer()->get($repository_class); + } +} diff --git a/api/common/Implement/Controller/Json.php b/api/common/Implement/Controller/Json.php new file mode 100644 index 0000000..4afd41d --- /dev/null +++ b/api/common/Implement/Controller/Json.php @@ -0,0 +1,16 @@ +getBody()->write(\Safe\json_encode($data)); + return $response + ->withStatus($status) + ->withHeader('Content-Type', 'application/json'); + + } +} \ No newline at end of file diff --git a/api/common/Implement/Repository.php b/api/common/Implement/Repository.php new file mode 100644 index 0000000..7cbe22d --- /dev/null +++ b/api/common/Implement/Repository.php @@ -0,0 +1,195 @@ +setConnection($connection) + ->setLogger($logger); + } + + protected PDO $connection; + protected string $table; + protected LoggerInterface $logger; + + public function getConnection(): PDO + { + return $this->connection; + } + public function getTable(): string + { + return $this->table; + } + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function setConnection(PDO $pdo): Define\Repository + { + $this->connection = $pdo; + return $this; + } + public function setTable(string $table): Define\Repository + { + $this->table = $table; + return $this; + } + public function setLogger(LoggerInterface $logger): Define\Repository + { + $this->logger = $logger; + return $this; + } + + public function isInstalled(): bool + { + $query = "SHOW TABLES LIKE '{$this->getTable()}'"; + $st = $this->getConnection()->query($query); + if ($st === false) { + throw new PDOException("Could not run query {$query}"); + } + return $st->rowCount() > 0; + } + public function update(Define\Model $model, Define\Model $old): void + { + $query = "UPDATE `{$this->getTable()}` SET "; + $model_values = $this->valuesForUpdate($model); + $old_values = $this->valuesForUpdate($old); + $columns = []; + $values = []; + foreach ($this->fieldsForUpdate() as $i => $column) { + if (isset($model_values[$i]) and $old_values[$i] !== $model_values[$i]) { + $columns []= "`{$column}` = ?"; + $values []= $model_values[$i]; + } + } + if (count($columns) === 0) { + return; + } + $query .= implode(', ', $columns) . " WHERE {$this->idField()} = ?"; + $values []= $old->{$this->idProperty()}(); + $st = $this->getConnection()->prepare($query); + $st->execute($values); + } + public function save(Define\Model &$model): void + { + try { + $old = $this->defaultFind($model); + $this->update($model, $old); + } catch (BlankResult $e) { + $this->insert($model); + $model->setId($this->getConnection()->lastInsertId()); + } catch(PDOException $e) { + $this->getLogger()->error($e); + throw $e; + } + } + public function create(array $data): Define\Model + { + try { + return $this->defaultSearch($data); + } catch (PDOException | BlankResult $e) { + $data[$this->idField()] = 0; + return $this->load($data); + } + } + public function resetIndex(): void + { + $query = "ALTER TABLE `{$this->getTable()}` AUTO_INCREMENT = 1"; + $this->getConnection()->query($query); + } + public function optimize(): void + { + $query = "OPTIMIZE TABLE `{$this->getTable()}`"; + $this->getConnection()->query($query); + } + public function delete(Define\Model $model): void + { + $query = "DELETE FROM `{$this->getTable()}` WHERE `{$this->idField()}` = ?"; + $st = $this->getConnection()->prepare($query); + $st->execute([$this->getId($model)]); + $this->resetIndex(); + $this->optimize(); + } + public function fetchAll(): array + { + $query = "SELECT * FROM `{$this->getTable()}`"; + return $this->fetchMany($query); + } + public function fetchById(int $id): Define\Model + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `{$this->idField()}` = ?"; + return $this->fetchOne($query, [$id]); + } + + protected function idProperty(): string + { + return 'getId'; + } + protected function idField(): string + { + return 'id'; + } + protected function insert(Define\Model $model): void + { + $fields = $this->fieldsForInsert(); + $fields_string = implode(', ', array_map(function($field) { + return "`{$field}`"; + }, $fields)); + $fields_questions = implode(', ', array_fill(0, count($fields), '?')); + $query = "INSERT INTO `{$this->getTable()}` ({$fields_string}) VALUES ({$fields_questions})"; + $values = $this->valuesForInsert($model); + $st = $this->getConnection()->prepare($query); + $st->execute($values); + } + protected function getId(Define\Model $model): int + { + return $model->getId(); + } + protected function fetchOne(string $query, ?array $values = null): Define\Model + { + if ($values !== null) { + $st = $this->getConnection()->prepare($query); + $st->execute($values); + } else { + $st = $this->getConnection()->query($query); + } + $row = $st->fetch(PDO::FETCH_ASSOC); + if (!$row) { + throw new BlankResult(); + } + return $this->load($row); + } + protected function fetchMany(string $query, ?array $values = null): array + { + if ($values !== null) { + $st = $this->getConnection()->prepare($query); + $st->execute($values); + } else { + $st = $this->getConnection()->query($query); + } + $rows = $st->fetchAll(PDO::FETCH_ASSOC); + if (!$rows) { + throw new BlankResult(); + } + return array_map([$this, 'load'], $rows); + } + + abstract public function install(): void; + abstract public function load(array $row): Define\Model; + abstract protected function fieldsForUpdate(): array; + abstract protected function valuesForUpdate(Define\Model $model): array; + abstract protected function fieldsForInsert(): array; + abstract protected function valuesForInsert(Define\Model $model): array; + abstract protected function defaultFind(Define\Model $model): Define\Model; + abstract protected function fieldsForCreate(): array; + abstract protected function valuesForCreate(array $data): array; + abstract protected function defaultSearch(array $data): Define\Model; +} diff --git a/api/common/Middleware/Attachments.php b/api/common/Middleware/Attachments.php new file mode 100644 index 0000000..1baabca --- /dev/null +++ b/api/common/Middleware/Attachments.php @@ -0,0 +1,25 @@ +service->checkDownloaded(); + $this->service->checkEncryption(); + } catch (BlankResult $e) { + $this->logger->notice($e); + } + return $handler->handle($request); + } +} diff --git a/api/common/Middleware/Auth.php b/api/common/Middleware/Auth.php new file mode 100644 index 0000000..330516c --- /dev/null +++ b/api/common/Middleware/Auth.php @@ -0,0 +1,61 @@ +setResponseFactory($factory); + $this->setLogger($logger); + } + + protected ResponseFactoryInterface $factory; + protected LoggerInterface $logger; + + public function getResponseFactory(): ResponseFactoryInterface + { + return $this->factory; + } + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function setResponseFactory(ResponseFactoryInterface $factory): Auth + { + $this->factory = $factory; + return $this; + } + public function setLogger(LoggerInterface $logger): Auth + { + $this->logger = $logger; + return $this; + } + + public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if ($request->getMethod() === 'OPTIONS') { + return $handler->handle($request); + } + try { + if ($this->service->validate($request)) { + return $handler->handle($request); + } + } catch (Unauthorized $e) { + $response = $this->getResponseFactory()->createResponse($e->getCode()); + $response->getBody()->write(json_encode(['error' => $e->getCode(), 'message' => $e->getMessage()])); + } + $response = $this->getResponseFactory()->createResponse(413); + $response->getBody()->write(\Safe\json_encode(['error' => 413, 'message' => 'Incorrect token'])); + return $response + ->withHeader('Content-Type', 'application/json'); + } +} diff --git a/api/common/Middleware/CORS.php b/api/common/Middleware/CORS.php new file mode 100644 index 0000000..33361d2 --- /dev/null +++ b/api/common/Middleware/CORS.php @@ -0,0 +1,20 @@ +handle($request); + $request + ->withHeader('Access-Control-Allow-Origin', '*') + ->withHeader('Access-Control-Allow-Credentials', 'true') + ->withHeader('Access-Control-Allow-Headers', 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range') + ->withHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS,PUT,DELETE,PATCH'); + return $response; + } +} \ No newline at end of file diff --git a/api/common/Middleware/CustomExceptions.php b/api/common/Middleware/CustomExceptions.php new file mode 100644 index 0000000..6bece26 --- /dev/null +++ b/api/common/Middleware/CustomExceptions.php @@ -0,0 +1,58 @@ +setResponseFactory($factory) + ->setLogger($logger); + } + + protected ResponseFactoryInterface $factory; + protected LoggerInterface $logger; + public function getResponseFactory(): ResponseFactoryInterface + { + return $this->factory; + } + public function getLogger(): LoggerInterface + { + return $this->logger; + } + public function setResponseFactory(ResponseFactoryInterface $factory): CustomExceptions + { + $this->factory = $factory; + return $this; + } + public function setLogger(LoggerInterface $logger): CustomExceptions + { + $this->logger = $logger; + return $this; + } + + public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + try { + return $handler->handle($request); + } catch (BlankResult $e) { + $this->getLogger()->error($e); + return $this->getResponseFactory()->createResponse(204); + } catch (JsonException $e) { + $this->getLogger()->error($e); + return $this->getResponseFactory()->createResponse(415, 'Only JSON Media Type is supported for this request'); + } catch (MissingArgument $e) { + $this->getLogger()->error($e); + return $this->getResponseFactory()->createResponse(400, $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/api/common/Middleware/Install.php b/api/common/Middleware/Install.php new file mode 100644 index 0000000..b9a8053 --- /dev/null +++ b/api/common/Middleware/Install.php @@ -0,0 +1,20 @@ +service->check()) { + $this->service->install(); + } + return $handler->handle($request); + } +} diff --git a/api/common/Middleware/Logging.php b/api/common/Middleware/Logging.php new file mode 100644 index 0000000..2e91a6d --- /dev/null +++ b/api/common/Middleware/Logging.php @@ -0,0 +1,25 @@ +handle($request); + $output = [ + 'uri' => var_export($request->getUri(), true), + 'body' => $request->getBody()->getContents(), + 'response' => (clone $response)->getBody()->getContents() + ]; + $this->logger->info(json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return $response; + } +} diff --git a/api/common/Middleware/Mailboxes.php b/api/common/Middleware/Mailboxes.php new file mode 100644 index 0000000..0808f7f --- /dev/null +++ b/api/common/Middleware/Mailboxes.php @@ -0,0 +1,24 @@ +service->checkUpdate(); + } catch (BlankResult $e) { + $this->logger->notice($e); + } + return $handler->handle($request); + } +} diff --git a/api/common/Service/Attachments.php b/api/common/Service/Attachments.php new file mode 100644 index 0000000..c5b6db9 --- /dev/null +++ b/api/common/Service/Attachments.php @@ -0,0 +1,328 @@ +setMessages($messages) + ->setRepository($repository) + ->setRemoteService($remoteService) + ->setDecrypt($decrypt) + ->setFolder($attachments_folder) + ->setLogger($logger); + } + + protected Messages $messages; + protected Attachment $repository; + protected Remote\Attachments $remoteService; + protected Decrypt $decrypt; + protected string $folder; + + public function getMessages(): Messages + { + return $this->messages; + } + public function getRepository(): Attachment + { + return $this->repository; + } + public function getRemoteService(): Remote\Attachments + { + return $this->remoteService; + } + public function getDecrypt(): Decrypt + { + return $this->decrypt; + } + public function getFolder(): string + { + return $this->folder; + } + + public function setMessages(Messages $messages): Attachments + { + $this->messages = $messages; + return $this; + } + public function setRepository(Attachment $repository): Attachments + { + $this->repository = $repository; + return $this; + } + public function setRemoteService(Remote\Attachments $service): Attachments + { + $this->remoteService = $service; + return $this; + } + public function setDecrypt(Decrypt $decrypt): Attachments + { + $this->decrypt = $decrypt; + return $this; + } + public function setFolder(string $folder): Attachments + { + $this->folder = $folder; + return $this; + } + + public function getLocalAttachment(Message $message, string $relative_filename): \ProVM\Emails\Model\Attachment + { + return $this->getRepository()->fetchByMessageAndFilename($message->getId(), $relative_filename); + } + public function getRemoteAttachment(Message $message, string $relative_filename): AttachmentInterface + { + $remote_message = $this->getMessages()->getRemoteMessage($message->getUID()); + return $this->getRemoteService()->get($remote_message, $relative_filename); + } + public function getFile(int $attachment_id): string + { + $attachment = $this->getRepository()->fetchById($attachment_id); + $filename = implode(DIRECTORY_SEPARATOR, [ + $this->getFolder(), + $attachment->getFullFilename() + ]); + if ($attachment->isDecrypted()) { + $filename = implode(DIRECTORY_SEPARATOR, [ + $this->getFolder(), + 'decrypted', + $attachment->getFullFilename() + ]); + } + return file_get_contents($filename); + } + + public function getAll(): array + { + return $this->getRepository()->fetchAll(); + } + public function getDownloadedFiles(): array + { + $downloaded = []; + $folder = $this->getFolder(); + $files = new \FilesystemIterator($folder); + foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + $name = $file->getBasename(".{$file->getExtension()}"); + list($date, $subject, $filename) = explode(' - ', $name); + try { + $message = $this->getMessages()->find($subject, $date)[0]; + $filename = "{$filename}.{$file->getExtension()}"; + $downloaded []= compact('message', 'filename'); + } catch (BlankResult $e) { + } + } + return $downloaded; + } + public function create(int $message_id): array + { + $message = $this->getMessages()->getRepository()->fetchById($message_id); + $remote_message = $this->getMessages()->getRemoteMessage($message->getUID()); + if (!$remote_message->hasAttachments()) { + throw new NoAttachments($remote_message); + } + $attachments = []; + foreach ($remote_message->getAttachments() as $attachment) { + if (!$this->getMessages()->validateAttachment($attachment)) { + continue; + } + if ($this->save($message, $attachment, false)) { + $attachments []= $attachment->getFilename(); + } + } + return $attachments; + } + public function grab(int $message_id): array + { + $message = $this->getMessages()->getRepository()->fetchById($message_id); + $remote_message = $this->getMessages()->getRemoteMessage($message->getUID()); + if (!$remote_message->hasAttachments()) { + throw new NoAttachments($remote_message); + } + $attachments = []; + foreach ($remote_message->getAttachments() as $attachment) { + if (!$this->getMessages()->validateAttachment($attachment)) { + continue; + } + if ($this->save($message, $attachment)) { + $attachments []= $attachment->getFilename(); + } + } + return $attachments; + } + public function save(Message $message, AttachmentInterface $remote_attachment, bool $upload = true): bool + { + $data = [ + 'message_id' => $message->getId(), + 'filename' => $remote_attachment->getFilename() + ]; + try { + $attachment = $this->getRepository()->create($data); + $this->getRepository()->save($attachment); + if ($upload and $this->upload($attachment, $remote_attachment)) { + $attachment->itIsDownloaded(); + $message->doesHaveDownloadedAttachments(); + $this->getMessages()->getRepository()->save($message); + + if ($this->isFileEncrypted($attachment->getMessage(), $attachment->getFilename())) { + $attachment->itIsEncrypted(); + + if ($this->isFileDecrypted($attachment->getMessage(), $attachment->getFilename())) { + $attachment->itIsDecrypted(); + } else { + if ($this->decrypt($attachment->getMessage(), $attachment->getFilename())) { + $attachment->itIsDecrypted(); + } + } + } + } + $this->getRepository()->save($attachment); + return true; + } catch (PDOException $e) { + $this->getLogger()->error($e); + return false; + } + } + public function upload(\ProVM\Emails\Model\Attachment $attachment, AttachmentInterface $remote_attachment): bool + { + $destination = implode(DIRECTORY_SEPARATOR, [ + $this->getFolder(), + $attachment->getFullFilename() + ]); + try { + file_put_contents($destination, $remote_attachment->getDecodedContent()); + return true; + } catch (FilesystemException $e) { + $this->getLogger()->error($e); + return false; + } + } + public function isFileEncrypted(Message $message, string $relative_filename): bool + { + $attachment = $this->getLocalAttachment($message, $relative_filename); + $filename = implode(DIRECTORY_SEPARATOR, [ + $this->getFolder(), + $attachment->getFullFilename() + ]); + try { + return $this->getDecrypt()->isEncrypted($filename); + } catch (\InvalidArgumentException $e) { + return false; + } + } + public function isFileDecrypted(Message $message, string $relative_filename): bool + { + $attachment = $this->getLocalAttachment($message, $relative_filename); + $filename = implode(DIRECTORY_SEPARATOR, [ + $this->getFolder(), + 'decrypted', + $attachment->getFullFilename() + ]); + try { + return !$this->getDecrypt()->isEncrypted($filename); + } catch (\InvalidArgumentException $e) { + return false; + } + } + public function decrypt(Message $message, string $relative_filename): bool + { + $attachment = $this->getLocalAttachment($message, $relative_filename); + $source = implode(DIRECTORY_SEPARATOR, [ + $this->getFolder(), + $attachment->getFullFilename() + ]); + $destination = implode(DIRECTORY_SEPARATOR, [ + $this->getFolder(), + 'decrypted', + $attachment->getFullFilename() + ]); + return $this->getDecrypt()->runCommand($source, $destination); + } + public function isDownloaded(Message $message, MessageInterface $remote_message): bool + { + if (!$message->hasValidAttachments()) { + return false; + } + if ($message->hasDownloadedAttachments()) { + return true; + } + foreach ($remote_message->getAttachments() as $attachment) { + if (!str_contains($attachment->getFilename(), '.pdf')) { + continue; + } + $attachment = $this->getLocalAttachment($message, $attachment->getFilename()); + if (!$attachment->isDownloaded()) { + return false; + } + } + return true; + } + public function find(Message $message, string $filename): \ProVM\Emails\Model\Attachment + { + return $this->getRepository()->fetchByMessageAndFilename($message->getId(), $filename); + } + public function exists(Message $message, string $filename): bool + { + try { + $this->find($message, $filename); + return true; + } catch (BlankResult $e) { + return false; + } + } + public function add(Message $message, string $filename): bool + { + $data = [ + 'message_id' => $message->getId(), + 'filename' => $filename + ]; + try { + $attachment = $this->getRepository()->create($data); + $attachment->itIsDownloaded(); + $this->getRepository()->save($attachment); + $message->doesHaveDownloadedAttachments(); + $this->getMessages()->getRepository()->save($message); + return true; + } catch (PDOException $e) { + $this->getLogger()->error($e); + return false; + } + } + public function checkDownloaded(): void + { + $data = $this->getDownloadedFiles(); + foreach ($data as $info) { + if (!$this->exists($info['message'], $info['filename'])) { + $this->logger->info("Updating attachment {$info['filename']} for message {$info['message']->getSubject()}"); + $this->add($info['message'], $info['filename']); + } + } + } + public function getDownloaded(): array + { + return $this->getRepository()->fetchDownloaded(); + } + public function checkEncryption(): void + { + $attachments = $this->getDownloaded(); + foreach ($attachments as $attachment) { + if ($attachment->isEncrypted() and !$attachment->isDecrypted()) { + $this->logger->notice("Schedule decrypt for {$attachment->getFullFilename()}"); + $this->decrypt($attachment->getMessage(), $attachment->getFilename()); + } + } + } +} diff --git a/api/common/Service/Auth.php b/api/common/Service/Auth.php new file mode 100644 index 0000000..7442981 --- /dev/null +++ b/api/common/Service/Auth.php @@ -0,0 +1,33 @@ +getHeaderKey($request); + if (sha1($this->api_key) === $key) { + return true; + } + return false; + } + + protected function getHeaderKey(ServerRequestInterface $request): string + { + if (!$request->hasHeader('Authorization')) { + throw new Unauthorized(); + } + $auths = $request->getHeader('Authorization'); + foreach ($auths as $auth) { + if (str_contains($auth, 'Bearer')) { + return str_replace('Bearer ', '', $auth); + } + } + throw new Unauthorized(); + } +} diff --git a/api/common/Service/Base.php b/api/common/Service/Base.php new file mode 100644 index 0000000..7336ed5 --- /dev/null +++ b/api/common/Service/Base.php @@ -0,0 +1,27 @@ +logger; + } + + /** + * @param LoggerInterface $logger + * @return $this + */ + public function setLogger(LoggerInterface $logger): Base + { + $this->logger = $logger; + return $this; + } +} \ No newline at end of file diff --git a/api/common/Service/Decrypt.php b/api/common/Service/Decrypt.php new file mode 100644 index 0000000..1a06f34 --- /dev/null +++ b/api/common/Service/Decrypt.php @@ -0,0 +1,91 @@ +setLogger($logger); + $this->setBaseCommand($base_command); + $this->setPasswords($passwords); + } + + protected array $passwords; + protected string $base_command; + protected LoggerInterface $logger; + + public function getPasswords(): array + { + return $this->passwords; + } + public function getBaseCommand(): string + { + return $this->base_command; + } + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function addPassword(string $password): Decrypt + { + $this->passwords []= $password; + return $this; + } + public function setPasswords(array $passwords): Decrypt + { + foreach ($passwords as $password) { + $this->addPassword($password); + } + return $this; + } + public function setBaseCommand(string $command): Decrypt + { + $this->base_command = $command; + return $this; + } + public function setLogger(LoggerInterface $logger): Decrypt + { + $this->logger = $logger; + return $this; + } + + public function isEncrypted(string $filename): bool + { + if (!file_exists($filename)) { + throw new \InvalidArgumentException("File not found {$filename}"); + } + $escaped_filename = escapeshellarg($filename); + $cmd = "{$this->getBaseCommand()} --is-encrypted {$escaped_filename}"; + exec($cmd, $output, $retcode); + return $retcode == 0; + } + + public function buildCommand(string $in_file, string $out_file, string $password): string + { + return $this->getBaseCommand() . ' -password=' . escapeshellarg($password) . ' -decrypt ' . escapeshellarg($in_file) . ' ' . escapeshellarg($out_file); + } + public function runCommand(string $in_file, string $out_file): bool + { + if (file_exists($out_file)) { + return true; + } + + foreach ($this->getPasswords() as $password) { + $cmd = $this->buildCommand($in_file, $out_file, $password); + exec($cmd, $output, $retcode); + $success = $retcode == 0; + if ($success) { + return true; + } + if (file_exists($out_file)) { + unlink($out_file); + } + unset($output); + } + return false; + } +} \ No newline at end of file diff --git a/api/common/Service/Install.php b/api/common/Service/Install.php new file mode 100644 index 0000000..2ae9574 --- /dev/null +++ b/api/common/Service/Install.php @@ -0,0 +1,40 @@ +model_list as $model_class) { + $repository = $this->factory->find($model_class); + if (!$repository->isInstalled()) { + return false; + } + } + return true; + } + public function install(): void + { + $check = true; + $repository = null; + foreach ($this->model_list as $model_class) { + $repository = $this->factory->find($model_class); + if ($check) { + $query = "SET FOREIGN_KEY_CHECKS = 0"; + $repository->getConnection()->query($query); + $check = false; + } + if (!$repository->isInstalled()) { + $repository->install(); + } + } + if (!$check) { + $query = "SET FOREIGN_KEY_CHECKS = 1"; + $repository->getConnection()->query($query); + } + } +} diff --git a/api/common/Service/Jobs.php b/api/common/Service/Jobs.php new file mode 100644 index 0000000..f9822c4 --- /dev/null +++ b/api/common/Service/Jobs.php @@ -0,0 +1,94 @@ +setRepository($repository); + } + + protected Repository\Job $repository; + + public function getRepository(): Repository\Job + { + return $this->repository; + } + + public function setRepository(Repository\Job $repository): Jobs + { + $this->repository = $repository; + return $this; + } + + public function queue(string $command, ?array $arguments = null): bool + { + $data = [ + 'command' => $command, + 'arguments' => implode(' ', $arguments) + ]; + try { + $job = $this->getRepository()->create($data); + $this->getRepository()->save($job); + $data = [ + 'job_id' => $job->getId(), + 'date_time' => (new DateTimeImmutable())->format('Y-m-d H:i:s'), + 'status' => Model\State\Job::Pending + ]; + $state = $this->stateRepository->create($data); + $this->stateRepository->save($state); + return true; + } catch (PDOException $e) { + return false; + } + } + public function getPending(): array + { + return $this->getRepository()->fetchAllPending(); + } + public function getPendingByCommand(string $command): array + { + try { + return $this->getRepository()->fetchAllPendingByCommand($command); + } catch (BlankResult $e) { + return []; + } + } + public function finish(int $job_id): bool + { + $data = [ + 'job_id' => $job_id, + 'date_time' => (new DateTimeImmutable())->format('Y-m-d H:i:s'), + 'status' => Model\State\Job::Executed + ]; + try { + $state = $this->stateRepository->create($data); + $this->stateRepository->save($state); + return true; + } catch (PDOException $e) { + return false; + } + } + public function failed(int $job_id): bool + { + $data = [ + 'job_id' => $job_id, + 'date_time' => (new DateTimeImmutable())->format('Y-m-d H:i:s'), + 'status' => Model\State\Job::Failure + ]; + try { + $state = $this->stateRepository->create($data); + $this->stateRepository->save($state); + return true; + } catch (PDOException $e) { + return false; + } + } +} diff --git a/api/common/Service/Mailboxes.php b/api/common/Service/Mailboxes.php new file mode 100644 index 0000000..3eda6e6 --- /dev/null +++ b/api/common/Service/Mailboxes.php @@ -0,0 +1,153 @@ +setRepository($repository) + ->setRemoteService($remoteService) + ->setStatesRepository($states) + ->setLogger($logger); + } + + protected Mailbox $repository; + protected Remote\Mailboxes $remoteService; + protected State\Mailbox $statesRepository; + + public function getRepository(): Mailbox + { + return $this->repository; + } + public function getRemoteService(): Remote\Mailboxes + { + return $this->remoteService; + } + public function getStatesRepository(): State\Mailbox + { + return $this->statesRepository; + } + public function setRepository(Mailbox $repository): Mailboxes + { + $this->repository = $repository; + return $this; + } + public function setRemoteService(Remote\Mailboxes $service): Mailboxes + { + $this->remoteService = $service; + return $this; + } + public function setStatesRepository(State\Mailbox $repository): Mailboxes + { + $this->statesRepository = $repository; + return $this; + } + + public function getLocalMailbox(string $mailbox_name): \ProVM\Emails\Model\Mailbox + { + return $this->getRepository()->fetchByName($mailbox_name); + } + public function getRemoteMailbox(string $mailbox_name): MailboxInterface + { + return $this->getRemoteService()->get($mailbox_name); + } + + public function getAll(): array + { + return $this->getRemoteService()->getAll(); + } + public function getRegistered(): array + { + return $this->getRepository()->fetchAll(); + } + public function get(int $mailbox_id): \ProVM\Emails\Model\Mailbox + { + return $this->getRepository()->fetchById($mailbox_id); + } + + public function isRegistered(string $mailbox_name): bool + { + try { + $mailbox = $this->getRepository()->fetchByName($mailbox_name); + return true; + } catch (BlankResult $e) { + return false; + } + } + + public function register(string $mailbox_name): bool + { + $remote_mailbox = $this->getRemoteMailbox($mailbox_name); + $name = $remote_mailbox->getName(); + $validity = $remote_mailbox->getStatus()->uidvalidity; + try { + $mailbox = $this->getRepository()->create(compact('name', 'validity')); + $this->getRepository()->save($mailbox); + return true; + } catch (PDOException $e) { + return false; + } + } + public function updateState(\ProVM\Emails\Model\Mailbox $mailbox, array $messages, \DateTimeInterface $dateTime): bool + { + $data = [ + 'mailbox_id' => $mailbox->getId(), + 'date_time' => $dateTime->format('Y-m-d H:i:s'), + 'count' => count($messages), + 'uids' => serialize($messages) + ]; + try { + $state = $this->getStatesRepository()->create($data); + $this->getStatesRepository()->save($state); + return true; + } catch (PDOException $e) { + $this->getLogger()->error($e); + return false; + } + } + public function unregister(string $mailbox_name): bool + { + try { + $mailbox = $this->getRepository()->fetchByName($mailbox_name); + } catch (BlankResult $e) { + $this->getLogger()->error($e); + // It's already unregistered + return true; + } + try { + $this->getRepository()->delete($mailbox); + return true; + } catch (PDOException $e) { + return false; + } + } + public function validate(string $mailbox_name): bool + { + $mailbox = $this->getLocalMailbox($mailbox_name); + + if (!$this->getRemoteService()->validate($mailbox_name, $mailbox->getValidity())) { + $remote_mailbox = $this->getRemoteMailbox($mailbox_name); + $mailbox->setValidity($remote_mailbox->getStatus()->uidvalidity); + $this->getRepository()->save($mailbox); + return false; + } + return true; + } + public function isUpdated(\ProVM\Emails\Model\Mailbox $mailbox): bool + { + $states = $mailbox->getStates(); + if (count($states) === 0) { + return false; + } + $last = $states[count($states) - 1]; + return abs((int) $last->getDateTime()->diff(new \DateTimeImmutable())->format('%r%a')) < $this->max_update_days; + } +} diff --git a/api/common/Service/Messages.php b/api/common/Service/Messages.php new file mode 100644 index 0000000..ac52463 --- /dev/null +++ b/api/common/Service/Messages.php @@ -0,0 +1,202 @@ +setMailboxes($mailboxes) + ->setRepository($repository) + ->setRemoteService($remoteService) + ->setJobsService($jobsService) + ->setLogger($logger); + } + + protected Mailboxes $mailboxes; + protected Message $repository; + protected Remote\Messages $remoteService; + protected Jobs $jobsService; + + public function getMailboxes(): Mailboxes + { + return $this->mailboxes; + } + public function getRepository(): Message + { + return $this->repository; + } + public function getRemoteService(): Remote\Messages + { + return $this->remoteService; + } + public function getJobsService(): Jobs + { + return $this->jobsService; + } + + public function setMailboxes(Mailboxes $mailboxes): Messages + { + $this->mailboxes = $mailboxes; + return $this; + } + public function setRepository(Message $repository): Messages + { + $this->repository = $repository; + return $this; + } + public function setRemoteService(Remote\Messages $service): Messages + { + $this->remoteService = $service; + return $this; + } + public function setJobsService(Jobs $service): Messages + { + $this->jobsService = $service; + return $this; + } + + public function getLocalMessage(string $message_uid): \ProVM\Emails\Model\Message + { + return $this->getRepository()->fetchByUID($message_uid); + } + public function getRemoteMessage(string $message_uid): MessageInterface + { + $message = $this->getLocalMessage($message_uid); + $remote_mailbox = $this->getMailboxes()->getRemoteMailbox($message->getMailbox()->getName()); + return $this->getRemoteService()->get($remote_mailbox, $message->getSubject(), $message->getFrom(), $message->getDateTime()); + } + + public function getAll(string $mailbox_name): array + { + $mailbox = $this->getMailboxes()->getLocalMailbox($mailbox_name); + return $this->getRepository()->fetchByMailbox($mailbox->getId()); + } + public function getValid(string $mailbox_name): array + { + $mailbox = $this->getMailboxes()->getLocalMailbox($mailbox_name); + return $this->getRepository()->fetchValidByMailbox($mailbox->getId()); + } + public function grab(string $mailbox_name): array + { + $mailbox = $this->getMailboxes()->getLocalMailbox($mailbox_name); + if (!$this->getMailboxes()->validate($mailbox_name)) { + $this->restoreUIDs($mailbox_name); + } + try { + $start = $mailbox->lastPosition() + 1; + } catch (Stateless $e) { + $start = 0; + } + $remote_mailbox = $this->getMailboxes()->getRemoteMailbox($mailbox_name); + $total = $remote_mailbox->count(); + $total_amount = $total - $start; + $amount = min(100, $total_amount); + $messages = []; + for ($i = 0; $i < $total_amount; $i += $amount) { + if ($amount + $i > $total_amount) { + $amount = $total_amount - $i; + } + $remote_messages = $this->getRemoteService()->getAll($remote_mailbox, $start + $i, $amount); + foreach ($remote_messages as $p => $m) { + if ($this->save($mailbox, $m, $p)) { + $messages []= $m->getId(); + } + } + } + $this->getMailboxes()->updateState($mailbox, $messages, new DateTimeImmutable()); + return $messages; + } + public function restoreUIDs(string $mailbox_name): void + { + $mailbox = $this->getMailboxes()->getLocalMailbox($mailbox_name); + $remote_mailbox = $this->getMailboxes()->getRemoteMailbox($mailbox_name); + $total_amount = $mailbox->lastPosition(); + $amount = min(100, $total_amount); + for ($i = 0; $i < $total_amount; $i += $amount) { + if ($amount + $i > $total_amount) { + $amount = $total_amount - $i; + } + $remote_messages = $this->getRemoteService()->getAll($remote_mailbox, $i, $amount); + foreach ($remote_messages as $m) { + $this->update($mailbox, $m, $i); + } + } + } + public function save(Mailbox $mailbox, MessageInterface $remote_message, int $position): bool + { + $data = [ + 'mailbox_id' => $mailbox->getId(), + 'position' => $position, + 'uid' => $remote_message->getId(), + 'subject' => $remote_message->getSubject() ?? '', + 'from' => $remote_message->getFrom()->getAddress(), + 'date_time' => $remote_message->getDate()->format('Y-m-d H:i:s') + ]; + try { + $message = $this->getRepository()->create($data); + if ($message->getId() === 0) { + if ($remote_message->hasAttachments()) { + $message->doesHaveAttachments(); + } + if ($this->validAttachments($remote_message)) { + $message->doesHaveValidAttachments(); + } + } + $this->getRepository()->save($message); + return true; + } catch (PDOException $e) { + $this->getLogger()->error($e); + return false; + } + } + public function update(Mailbox $mailbox, MessageInterface $remote_message, int $position): bool + { + try { + $message = $this->getRepository()->fetchByMailboxSubjectFromAndDate($mailbox->getId(), $remote_message->getSubject(), $remote_message->getFrom()->getAddress(), $remote_message->getDate()); + $message->setUID($remote_message->getId()); + $message->setPosition($position); + $this->getRepository()->save($message); + return true; + } catch (PDOException $e) { + return false; + } + } + public function validateAttachment(AttachmentInterface $attachment): bool + { + return str_contains($attachment->getFilename(), '.pdf'); + } + public function validAttachments(MessageInterface $remote_message): bool + { + foreach ($remote_message->getAttachments() as $attachment) { + if ($this->validateAttachment($attachment)) { + return true; + } + } + return false; + } + public function find(string $subject, string $date): array + { + return $this->repository->fetchAllBySubjectAndDate($subject, new DateTimeImmutable($date)); + } + public function checkUpdate(): void + { + $registered = $this->getMailboxes()->getRegistered(); + foreach ($registered as $mailbox) { + if (!$this->getMailboxes()->isUpdated($mailbox)) { + $this->getJobsService()->queue('messages:grab', [$mailbox->getId()]); + } + } + } +} diff --git a/api/common/Service/Remote/Attachments.php b/api/common/Service/Remote/Attachments.php new file mode 100644 index 0000000..bfdb3ba --- /dev/null +++ b/api/common/Service/Remote/Attachments.php @@ -0,0 +1,31 @@ +setConnection($connection) + ->setLogger($logger); + } + + public function getAttachments(MessageInterface $message): array + { + return $message->getAttachments(); + } + public function get(MessageInterface $message, string $filename): AttachmentInterface + { + foreach ($message->getAttachments() as $attachment) { + if ($attachment->getFilename() === $filename) { + return $attachment; + } + } + throw new NotFound($message, $filename); + } +} \ No newline at end of file diff --git a/api/common/Service/Remote/Base.php b/api/common/Service/Remote/Base.php new file mode 100644 index 0000000..df2afa4 --- /dev/null +++ b/api/common/Service/Remote/Base.php @@ -0,0 +1,21 @@ +connection; + } + + public function setConnection(ConnectionInterface $connection): Base + { + $this->connection = $connection; + return $this; + } +} \ No newline at end of file diff --git a/api/common/Service/Remote/Mailboxes.php b/api/common/Service/Remote/Mailboxes.php new file mode 100644 index 0000000..0a83a56 --- /dev/null +++ b/api/common/Service/Remote/Mailboxes.php @@ -0,0 +1,45 @@ +setConnection($connection) + ->setLogger($logger); + } + + protected array $mailboxes; + public function getAll(): array + { + if (!isset($this->mailboxes)) { + $this->mailboxes = $this->getConnection()->getMailboxes(); + } + return $this->mailboxes; + } + + /** + * @throws Invalid + */ + public function get(string $mailbox_name): MailboxInterface + { + if (!$this->getConnection()->hasMailbox($mailbox_name)) { + throw new Invalid($mailbox_name); + } + return $this->getConnection()->getMailbox($mailbox_name); + } + + public function validate(string $mailbox_name, int $uidvalidity): bool + { + $mailbox = $this->get($mailbox_name); + return ($mailbox->getStatus()->uidvalidity === $uidvalidity); + } +} \ No newline at end of file diff --git a/api/common/Service/Remote/Messages.php b/api/common/Service/Remote/Messages.php new file mode 100644 index 0000000..2a252d0 --- /dev/null +++ b/api/common/Service/Remote/Messages.php @@ -0,0 +1,67 @@ +setConnection($connection) + ->setLogger($logger); + } + + public function getAll(MailboxInterface $mailbox, int $start = 0, ?int $amount = null): array + { + if ($mailbox->count() === 0) { + throw new EmptyMailbox($mailbox); + } + + if ($amount === null) { + $amount = $mailbox->count() - $start; + } + + $it = $mailbox->getIterator(); + for ($i = 0; $i < $start; $i ++) { + $it->next(); + } + + $messages = []; + for ($i = $start; $i < $start + $amount; $i ++) { + if (!$it->valid()) { + break; + } + $messages[$i] = $it->current(); + $it->next(); + } + return $messages; + } + public function get(MailboxInterface $mailbox, string $subject, string $from, DateTimeInterface $dateTime): MessageInterface + { + if ($mailbox->count() === 0) { + $this->getLogger()->notice("Mailbox {$mailbox->getName()} is empty"); + throw new EmptyMailbox($mailbox); + } + + $query = new SearchExpression(); + $query->addCondition(new Subject($subject)); + $query->addCondition(new From($from)); + $query->addCondition(new On($dateTime)); + + $result = $mailbox->getMessages($query); + if (count($result) === 0) { + throw new MessageDoesNotExistException("{$mailbox->getName()}: {$subject} - {$from} [{$dateTime->format('Y-m-d H:i:s')}]"); + } + return $result->current(); + } +} \ No newline at end of file diff --git a/api/composer.json b/api/composer.json new file mode 100644 index 0000000..d65c305 --- /dev/null +++ b/api/composer.json @@ -0,0 +1,32 @@ +{ + "name": "provm/emails_api", + "type": "project", + "require": { + "ddeboer/imap": "^1.14", + "monolog/monolog": "^3.2", + "nyholm/psr7": "^1.5", + "nyholm/psr7-server": "^1.0", + "php-di/slim-bridge": "^3.2", + "thecodingmachine/safe": "^2.4", + "zeuxisoo/slim-whoops": "^0.7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "kint-php/kint": "^4.2" + }, + "authors": [ + { + "name": "Aldarien", + "email": "aldarien85@gmail.com" + } + ], + "autoload": { + "psr-4": { + "ProVM\\Common\\": "common/", + "ProVM\\Emails\\": "src/" + } + }, + "config": { + "sort-packages": true + } +} diff --git a/api/docker-compose.yml b/api/docker-compose.yml new file mode 100644 index 0000000..0f71511 --- /dev/null +++ b/api/docker-compose.yml @@ -0,0 +1,49 @@ +version: '3' +services: + proxy: + profiles: + - api + volumes: + - ${API_PATH:-.}:/app/api + - ${API_PATH:-.}/nginx.conf:/etc/nginx/conf.d/api.conf + ports: + - "${API_PORT:-8080}:81" + api: + profiles: + - api + container_name: emails-api + build: + context: ${API_PATH:-.} + restart: unless-stopped + env_file: + - ${API_PATH:-.}/.env + - ${API_PATH:-.}/.db.env + - ${API_PATH:-.}/.mail.env + - .key.env + volumes: + - ${API_PATH:-.}/:/app/api + - ${LOGS_PATH}/api:/logs + - ${ATT_PATH}:/attachments + db: + profiles: + - api + container_name: emails-db + image: mariadb + restart: unless-stopped + env_file: + - ${API_PATH:-.}/.db.env + volumes: + - emails_data:/var/lib/mysql + adminer: + profiles: + - testing + container_name: emails-adminer + image: adminer + restart: unless-stopped + env_file: + - ${API_PATH:-.}/.adminer.env + ports: + - "8081:8080" + +volumes: + emails_data: {} diff --git a/api/errors.ini b/api/errors.ini new file mode 100644 index 0000000..0dcfc29 --- /dev/null +++ b/api/errors.ini @@ -0,0 +1,3 @@ +error_reporting=E_ALL +log_errors=true +error_log=/logs/errors.log \ No newline at end of file diff --git a/api/nginx.conf b/api/nginx.conf new file mode 100644 index 0000000..6a50944 --- /dev/null +++ b/api/nginx.conf @@ -0,0 +1,37 @@ +server { + listen 81; + root /app/api/public; + index index.php index.html index.htm; + + access_log /var/logs/nginx/api.access.log; + error_log /var/logs/nginx/api.error.log; + + location / { + try_files $uri /index.php$is_args$args; + } + location ~ \.php { + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; + add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'application/json'; + add_header 'Content-Length' 0; + return 204; + } + + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; + add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH'; + + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + include fastcgi_params; + fastcgi_pass api:9000; + fastcgi_index index.php; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } +} diff --git a/api/public/index.php b/api/public/index.php new file mode 100644 index 0000000..2e6a761 --- /dev/null +++ b/api/public/index.php @@ -0,0 +1,14 @@ +getContainer()->get(Psr\Log\LoggerInterface::class)); +try { + $app->run(); +} catch (Error $e) { + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->error($e); +} catch (Exception $e) { + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->debug($e); +} diff --git a/api/resources/routes/01_mailboxes.php b/api/resources/routes/01_mailboxes.php new file mode 100644 index 0000000..23e57c8 --- /dev/null +++ b/api/resources/routes/01_mailboxes.php @@ -0,0 +1,19 @@ +group('/mailboxes', function($app) { + $app->post('/register[/]', [Mailboxes::class, 'register']); + $app->delete('/unregister[/]', [Mailboxes::class, 'unregister']); + $app->get('/registered[/]', [Mailboxes::class, 'registered']); + $app->get('[/]', Mailboxes::class); +}); + +$app->group('/mailbox/{mailbox_id}', function($app) { + $app->group('/messages', function($app) { + $app->get('/grab[/]', [Messages::class, 'grab']); + $app->get('/valid[/]', [Messages::class, 'valid']); + $app->get('[/]', Messages::class); + }); + $app->get('[/]', [Mailboxes::class, 'get']); +}); diff --git a/api/resources/routes/02_attachments.php b/api/resources/routes/02_attachments.php new file mode 100644 index 0000000..8c53514 --- /dev/null +++ b/api/resources/routes/02_attachments.php @@ -0,0 +1,11 @@ +group('/attachments', function($app) { + $app->put('/grab[/]', [Attachments::class, 'grab']); + $app->get('/pending[/]', [Attachments::class, 'pending']); + $app->get('[/]', Attachments::class); +}); +$app->group('/attachment/{attachment_id}', function($app) { + $app->get('[/]', [Attachments::class, 'get']); +}); diff --git a/api/resources/routes/02_messages.php b/api/resources/routes/02_messages.php new file mode 100644 index 0000000..4d10259 --- /dev/null +++ b/api/resources/routes/02_messages.php @@ -0,0 +1,12 @@ +group('/messages', function($app) { + $app->put('/grab', [Messages::class, 'grab']); + $app->put('/schedule', [Messages::class, 'schedule']); + //$app->get('/pending', [Jobs::class, 'pending']); +}); +$app->group('/message/{message_id}', function($app) { + $app->get('[/]', [Messages::class, 'get']); +}); diff --git a/api/resources/routes/03_jobs.php b/api/resources/routes/03_jobs.php new file mode 100644 index 0000000..1753d96 --- /dev/null +++ b/api/resources/routes/03_jobs.php @@ -0,0 +1,15 @@ +group('/jobs', function($app) { + $app->group('/pending', function($app) { + $app->put('/command[/]', [Jobs::class, 'pendingCommands']); + $app->get('[/]', [Jobs::class, 'pending']); + }); + $app->post('/schedule[/]', [Jobs::class, 'schedule']); + $app->get('[/]', Jobs::class); +}); +$app->group('/job/{job_id}', function($app) { + $app->get('/finish[/]', [Jobs::class, 'finish']); + $app->get('/failed[/]', [Jobs::class, 'failed']); +}); diff --git a/api/resources/routes/99_base.php b/api/resources/routes/99_base.php new file mode 100644 index 0000000..022aa6f --- /dev/null +++ b/api/resources/routes/99_base.php @@ -0,0 +1,4 @@ +get('[/]', Base::class); \ No newline at end of file diff --git a/api/setup/app.php b/api/setup/app.php new file mode 100644 index 0000000..0ae6f29 --- /dev/null +++ b/api/setup/app.php @@ -0,0 +1,46 @@ +isDir()) { + continue; + } + $builder->addDefinitions($file->getRealPath()); + } +} +$app = Bridge::create($builder->build()); + +$folder = implode(DIRECTORY_SEPARATOR, [ + __DIR__, + 'middleware' +]); +if (file_exists($folder)) { + $files = new FilesystemIterator($folder); + foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + require_once $file->getRealPath(); + } +} + +return $app; diff --git a/api/setup/composer.php b/api/setup/composer.php new file mode 100644 index 0000000..b451f96 --- /dev/null +++ b/api/setup/composer.php @@ -0,0 +1,6 @@ +addRoutingMiddleware(); + +$folder = $app->getContainer()->get('routes_folder'); +$files = new FilesystemIterator($folder); +foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + include_once $file->getRealPath(); +} diff --git a/api/setup/middleware/02_cors.php b/api/setup/middleware/02_cors.php new file mode 100644 index 0000000..12e122e --- /dev/null +++ b/api/setup/middleware/02_cors.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(ProVM\Common\Middleware\CORS::class)); diff --git a/api/setup/middleware/03_custom_exceptions.php b/api/setup/middleware/03_custom_exceptions.php new file mode 100644 index 0000000..9e38f2a --- /dev/null +++ b/api/setup/middleware/03_custom_exceptions.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(ProVM\Common\Middleware\CustomExceptions::class)); diff --git a/api/setup/middleware/04_db.php b/api/setup/middleware/04_db.php new file mode 100644 index 0000000..b71467b --- /dev/null +++ b/api/setup/middleware/04_db.php @@ -0,0 +1,4 @@ +add($app->getContainer()->get(ProVM\Common\Middleware\Attachments::class)); +$app->add($app->getContainer()->get(ProVM\Common\Middleware\Mailboxes::class)); +$app->add($app->getContainer()->get(ProVM\Common\Middleware\Install::class)); diff --git a/api/setup/middleware/97_auth.php b/api/setup/middleware/97_auth.php new file mode 100644 index 0000000..9e2f1be --- /dev/null +++ b/api/setup/middleware/97_auth.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(ProVM\Common\Middleware\Auth::class)); diff --git a/api/setup/middleware/98_log.php b/api/setup/middleware/98_log.php new file mode 100644 index 0000000..4df6f6c --- /dev/null +++ b/api/setup/middleware/98_log.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(ProVM\Common\Middleware\Logging::class)); diff --git a/api/setup/middleware/99_errors.php b/api/setup/middleware/99_errors.php new file mode 100644 index 0000000..29c59fa --- /dev/null +++ b/api/setup/middleware/99_errors.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(Zeuxisoo\Whoops\Slim\WhoopsMiddleware::class)); diff --git a/api/setup/settings/01_env.php b/api/setup/settings/01_env.php new file mode 100644 index 0000000..89deb37 --- /dev/null +++ b/api/setup/settings/01_env.php @@ -0,0 +1,32 @@ + function() { + $data = [ + 'host' => $_ENV['EMAIL_HOST'], + 'username' => $_ENV['EMAIL_USERNAME'], + 'password' => $_ENV['EMAIL_PASSWORD'], + //'folder' => $_ENV['EMAIL_FOLDER'], + ]; + if (isset($_ENV['EMAIL_PORT'])) { + $data['port'] = $_ENV['EMAIL_PORT']; + } + return json_decode(json_encode($data)); + }, + 'passwords' => function() { + return explode($_ENV['PASSWORDS_SEPARATOR'] ?? ',', $_ENV['PASSWORDS'] ?? ''); + }, + 'api_key' => $_ENV['API_KEY'], + 'database' => function() { + $arr = [ + 'host' => 'db', + 'name' => $_ENV['MYSQL_DATABASE'], + 'username' => $_ENV['MYSQL_USER'], + 'password' => $_ENV['MYSQL_PASSWORD'] + ]; + if (isset($_ENV['MYSQL_PORT'])) { + $arr['port'] = $_ENV['MYSQL_PORT']; + } + return (object) $arr; + }, + 'max_update_days' => 7 +]; diff --git a/api/setup/settings/02_folders.php b/api/setup/settings/02_folders.php new file mode 100644 index 0000000..1b381f4 --- /dev/null +++ b/api/setup/settings/02_folders.php @@ -0,0 +1,22 @@ + function() { + return dirname(__FILE__, 3); + }, + 'resources_folder' => function(ContainerInterface $container) { + return implode(DIRECTORY_SEPARATOR, [ + $container->get('base_folder'), + 'resources' + ]); + }, + 'routes_folder' => function(ContainerInterface $container) { + return implode(DIRECTORY_SEPARATOR, [ + $container->get('resources_folder'), + 'routes' + ]); + }, + 'attachments_folder' => $_ENV['ATTACHMENTS_FOLDER'], + 'logs_folder' => '/logs', +]; diff --git a/api/setup/settings/03_decrypt.php b/api/setup/settings/03_decrypt.php new file mode 100644 index 0000000..8a50554 --- /dev/null +++ b/api/setup/settings/03_decrypt.php @@ -0,0 +1,4 @@ + 'qpdf' +]; diff --git a/api/setup/settings/04_db.php b/api/setup/settings/04_db.php new file mode 100644 index 0000000..62415ba --- /dev/null +++ b/api/setup/settings/04_db.php @@ -0,0 +1,33 @@ + function() { + function getClassesFromFolder(string $folder): array { + $classes = []; + $files = new FilesystemIterator($folder); + foreach ($files as $file) { + if ($file->isDir()) { + $classes = array_merge($classes, getClassesFromFolder($file->getRealPath())); + continue; + } + $classes []= ltrim(str_replace("\\\\", "\\", implode("\\", [ + 'ProVM', + 'Emails', + 'Model', + str_replace([implode(DIRECTORY_SEPARATOR, [ + dirname(__FILE__, 3), + 'src', + 'Model' + ]), '/'], ['', "\\"], $folder), + $file->getBasename(".{$file->getExtension()}") + ])), "\\"); + } + return $classes; + } + $folder = implode(DIRECTORY_SEPARATOR, [ + dirname(__FILE__, 3), + 'src', + 'Model' + ]); + return getClassesFromFolder($folder); + } +]; diff --git a/api/setup/settings/98_log.php b/api/setup/settings/98_log.php new file mode 100644 index 0000000..3c1e3f0 --- /dev/null +++ b/api/setup/settings/98_log.php @@ -0,0 +1,5 @@ + '/logs/php.log', + 'logstash_socket' => 'localhost:50000' +]; diff --git a/api/setup/setups/01_emails.php b/api/setup/setups/01_emails.php new file mode 100644 index 0000000..1428bae --- /dev/null +++ b/api/setup/setups/01_emails.php @@ -0,0 +1,27 @@ + function(ContainerInterface $container) { + $emails = $container->get('emails'); + if (isset($emails->port)) { + return new Ddeboer\Imap\Server($emails->host, $emails->port); + } + return new Ddeboer\Imap\Server($emails->host); + }, + Ddeboer\Imap\ConnectionInterface::class => function(ContainerInterface $container) { + $emails = $container->get('emails'); + $server = $container->get(Ddeboer\Imap\ServerInterface::class); + return $server->authenticate($emails->username, $emails->password); + }, + PDO::class => function(ContainerInterface $container) { + $database = $container->get('database'); + $dsn = ["mysql:host={$database->host}"]; + if (isset($database->port)) { + $dsn []= "port={$database->port}"; + } + $dsn []= "dbname={$database->name}"; + $dsn = implode(';', $dsn); + return new PDO($dsn, $database->username, $database->password); + }, +]; diff --git a/api/setup/setups/02_services.php b/api/setup/setups/02_services.php new file mode 100644 index 0000000..4348151 --- /dev/null +++ b/api/setup/setups/02_services.php @@ -0,0 +1,40 @@ + function(ContainerInterface $container) { + return new ProVM\Common\Service\Decrypt( + $container->get(Psr\Log\LoggerInterface::class), + $container->get('base_command'), + $container->get('passwords') + ); + }, + ProVM\Common\Service\Attachments::class => function(ContainerInterface $container) { + return new ProVM\Common\Service\Attachments( + $container->get(ProVM\Common\Service\Messages::class), + $container->get(ProVM\Emails\Repository\Attachment::class), + $container->get(ProVM\Common\Service\Remote\Attachments::class), + $container->get(ProVM\Common\Service\Decrypt::class), + $container->get('attachments_folder'), + $container->get(Psr\Log\LoggerInterface::class) + ); + }, + ProVM\Common\Service\Mailboxes::class => function(ContainerInterface $container) { + return new ProVM\Common\Service\Mailboxes( + $container->get(ProVM\Emails\Repository\Mailbox::class), + $container->get(ProVM\Common\Service\Remote\Mailboxes::class), + $container->get(ProVM\Emails\Repository\State\Mailbox::class), + $container->get(Psr\Log\LoggerInterface::class), + $container->get('max_update_days') + ); + }, + ProVM\Common\Service\Install::class => function(ContainerInterface $container) { + return new ProVM\Common\Service\Install( + $container->get(ProVM\Common\Factory\Model::class), + $container->get('model_list') + ); + }, + ProVM\Common\Service\Auth::class => function(ContainerInterface $container) { + return new ProVM\Common\Service\Auth($container->get('api_key')); + } +]; diff --git a/api/setup/setups/03_factories.php b/api/setup/setups/03_factories.php new file mode 100644 index 0000000..49b605e --- /dev/null +++ b/api/setup/setups/03_factories.php @@ -0,0 +1,18 @@ + function(ContainerInterface $container) { + $factory = new ProVM\Common\Factory\Model($container); + return $factory->setRepositories([ + 'Mailbox' => ProVM\Emails\Repository\Mailbox::class, + 'Message' => ProVM\Emails\Repository\Message::class, + 'Attachment' => ProVM\Emails\Repository\Attachment::class, + 'Job' => ProVM\Emails\Repository\Job::class, + "State\\Mailbox" => ProVM\Emails\Repository\State\Mailbox::class, + "State\\Message" => ProVM\Emails\Repository\State\Message::class, + "State\\Attachment" => ProVM\Emails\Repository\State\Attachment::class, + "State\\Job" => ProVM\Emails\Repository\State\Job::class, + ]); + } +]; diff --git a/api/setup/setups/04_middlewares.php b/api/setup/setups/04_middlewares.php new file mode 100644 index 0000000..10775dd --- /dev/null +++ b/api/setup/setups/04_middlewares.php @@ -0,0 +1,14 @@ + function(ContainerInterface $container) { + return new ProVM\Common\Middleware\CustomExceptions( + $container->get(Nyholm\Psr7\Factory\Psr17Factory::class), + $container->get(Psr\Log\LoggerInterface::class) + ); + }, + ProVM\Common\Middleware\Logging::class => function(ContainerInterface $container) { + return new ProVM\Common\Middleware\Logging($container->get('request_logger')); + }, +]; diff --git a/api/setup/setups/97_auth.php b/api/setup/setups/97_auth.php new file mode 100644 index 0000000..9476f57 --- /dev/null +++ b/api/setup/setups/97_auth.php @@ -0,0 +1,12 @@ + function(ContainerInterface $container) { + return new ProVM\Common\Middleware\Auth( + $container->get(Nyholm\Psr7\Factory\Psr17Factory::class), + $container->get(Psr\Log\LoggerInterface::class), + $container->get(ProVM\Common\Service\Auth::class) + ); + } +]; diff --git a/api/setup/setups/98_log.php b/api/setup/setups/98_log.php new file mode 100644 index 0000000..39788a4 --- /dev/null +++ b/api/setup/setups/98_log.php @@ -0,0 +1,62 @@ + function(ContainerInterface $container) { + return [ + $container->get(Monolog\Processor\PsrLogMessageProcessor::class), + $container->get(Monolog\Processor\IntrospectionProcessor::class), + $container->get(Monolog\Processor\WebProcessor::class), + $container->get(Monolog\Processor\MemoryPeakUsageProcessor::class), + ]; + }, + 'request_log_handler' => function(ContainerInterface $container) { + return (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log']))); + }, + 'request_logger' => function(ContainerInterface $container) { + return new Monolog\Logger( + 'request_logger', + [$container->get('request_log_handler')], + $container->get('log_processors') + ); + }, + 'file_log_handler' => function(ContainerInterface $container) { + return new Monolog\Handler\FilterHandler( + (new Monolog\Handler\RotatingFileHandler($container->get('log_file'))) + ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true)), + Monolog\Level::Error + ); + }, + 'debug_log_handler' => function(ContainerInterface $container) { + return new Monolog\Handler\FilterHandler( + (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'debug.log']))) + ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true)), + Monolog\Level::Debug, + Monolog\Level::Warning + ); + }, + Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { + return $container->get('file_logger'); + }, + 'file_logger' => function(ContainerInterface $container) { + return new Monolog\Logger( + 'file', + [ + $container->get('file_log_handler'), + $container->get('debug_log_handler') + ], + $container->get('log_processors') + ); + }, + 'elk_logger' => function(ContainerInterface $container) { + return new Monolog\Logger('elk', [ + (new Monolog\Handler\SocketHandler($container->get('logstash_socket'))) + ->setFormatter(new Monolog\Formatter\LogstashFormatter('emails', 'docker')) + ], [ + new Monolog\Processor\PsrLogMessageProcessor(), + new Monolog\Processor\WebProcessor(), + new Monolog\Processor\IntrospectionProcessor(), + new Monolog\Processor\MemoryPeakUsageProcessor() + ]); + } +]; diff --git a/api/setup/setups/99_errors.php b/api/setup/setups/99_errors.php new file mode 100644 index 0000000..2026f82 --- /dev/null +++ b/api/setup/setups/99_errors.php @@ -0,0 +1,15 @@ + function(ContainerInterface $container) { + return (new \Whoops\Handler\JsonResponseHandler()) + ->setJsonApi(true) + ->addTraceToOutput(true); + }, + \Zeuxisoo\Whoops\Slim\WhoopsMiddleware::class => function(ContainerInterface $container) { + return new \Zeuxisoo\Whoops\Slim\WhoopsMiddleware([], [ + $container->get(\Whoops\Handler\JsonResponseHandler::class) + ]); + } +]; \ No newline at end of file diff --git a/api/src/Model/Attachment.php b/api/src/Model/Attachment.php new file mode 100644 index 0000000..9cc8683 --- /dev/null +++ b/api/src/Model/Attachment.php @@ -0,0 +1,176 @@ +id; + } + public function getMessage(): Message + { + return $this->message; + } + public function getFilename(): string + { + return $this->filename; + } + + public function setId(int $id): Attachment + { + $this->id = $id; + return $this; + } + public function setMessage(Message $message): Attachment + { + $this->message = $message; + return $this; + } + public function setFilename(string $filename): Attachment + { + $this->filename = $filename; + return $this; + } + + protected \ProVM\Emails\Repository\State\Attachment $stateRepository; + public function getStateRepository(): \ProVM\Emails\Repository\State\Attachment + { + return $this->stateRepository; + } + public function setStateRepository(\ProVM\Emails\Repository\State\Attachment $repository): Attachment + { + $this->stateRepository = $repository; + return $this; + } + + protected array $states; + public function getStates(): array + { + if (!isset($this->states)) { + try { + $this->setStates($this->getStateRepository()->fetchByAttachment($this->getId())); + } catch (BlankResult $e) { + return []; + } + } + return $this->states; + } + public function getState(string $name): State\Attachment + { + try { + return $this->getStates()[$name]; + } catch (\Exception $e) { + $this->newState($name); + return $this->getStates()[$name]; + } + } + public function addState(State\Attachment $state): Attachment + { + $this->states[$state->getName()] = $state; + return $this; + } + public function setStates(array $states): Attachment + { + foreach ($states as $state) { + $this->addState($state); + } + return $this; + } + protected function newState(string $name): Attachment + { + $this->addState((new State\Attachment()) + ->setAttachment($this) + ->setName($name) + ); + return $this; + } + + public function getFullFilename(): string + { + return implode(' - ', [ + $this->getMessage()->getDateTime()->format('Y-m-d His'), + $this->getMessage()->getSubject(), + $this->getFilename(), + ]); + } + + public function isDownloaded(): bool + { + return $this->getState('downloaded')?->getValue() ?? false; + } + public function isEncrypted(): bool + { + return $this->getState('encrypted')?->getValue() ?? false; + } + public function isDecrypted(): bool + { + try { + return $this->getState('decrypted')?->getValue() ?? false; + } catch (\Exception $e) { + $this->newState('decrypted'); + return $this->getState('decrypted')?->getValue() ?? false; + } + } + public function itIsDownloaded(): Attachment + { + try { + $this->getState('downloaded')->setValue(true); + } catch (\Exception $e) { + $this->newState('downloaded'); + $this->getState('downloaded')->setValue(true); + } + return $this; + } + public function itIsEncrypted(): Attachment + { + try { + $this->getState('encrypted')->setValue(true); + } catch (\Exception $e) { + $this->newState('encrypted'); + $this->getState('encrypted')->setValue(true); + } + return $this; + } + public function itIsDecrypted(): Attachment + { + try { + $this->getState('decrypted')->setValue(true); + } catch (\Exception $e) { + $this->newState('decrypted'); + $this->getState('decrypted')->setValue(true); + } + return $this; + } + + public function toArray(): array + { + return [ + 'id' => $this->getId(), + 'message' => [ + 'id' => $this->getMessage()->getId(), + 'mailbox' => $this->getMessage()->getMailbox()->toArray(), + 'position' => $this->getMessage()->getPosition(), + 'uid' => $this->getMessage()->getUID(), + 'subject' => $this->getMessage()->getSubject(), + 'from' => $this->getMessage()->getFrom(), + 'date_time' => $this->getMessage()->getDateTime()->format('Y-m-d H:i:s') + ], + 'filename' => $this->getFilename(), + 'fullname' => $this->getFullFilename(), + 'downloaded' => $this->isDownloaded(), + 'encrypted' => $this->isEncrypted(), + 'decrypted' => $this->isDecrypted() + ]; + } + public function jsonSerialize(): mixed + { + return $this->toArray(); + } +} diff --git a/api/src/Model/Job.php b/api/src/Model/Job.php new file mode 100644 index 0000000..7b69d93 --- /dev/null +++ b/api/src/Model/Job.php @@ -0,0 +1,106 @@ +id; + } + public function getCommand(): string + { + return $this->command; + } + public function getArguments(): string + { + return $this->arguments ?? ''; + } + + public function setId(int $id): Job + { + $this->id = $id; + return $this; + } + public function setCommand(string $command): Job + { + $this->command = $command; + return $this; + } + public function setArguments(string $arguments): Job + { + $this->arguments = $arguments; + return $this; + } + + protected Emails\Repository\State\Job $stateRepository; + public function getStateRepository(): Emails\Repository\State\Job + { + return $this->stateRepository; + } + public function setStateRepository(Emails\Repository\State\Job $repository): Job + { + $this->stateRepository = $repository; + return $this; + } + + protected array $states; + public function getStates(): array + { + if (!isset($this->states)) { + try { + $this->setStates($this->getStateRepository()->fetchByJob($this->getId())); + } catch (BlankResult $e) { + return []; + } + } + return $this->states; + } + public function addState(State\Job $state): Job + { + $this->states []= $state; + return $this; + } + public function setStates(array $states): Job + { + foreach ($states as $state) { + $this->addState($state); + } + return $this; + } + + public function isExecuted(): bool + { + return $this->lastState()->getStatus() === State\Job::Executed; + } + public function lastState(): State\Job + { + if (count($this->getStates()) === 0) { + throw new Stateless($this->getId()); + } + return $this->getStates()[array_key_last($this->getStates())]; + } + + public function toArray(): array + { + return [ + 'id' => $this->getId(), + 'command' => $this->getCommand(), + 'arguments' => $this->getArguments(), + 'states' => $this->getStates() + ]; + } + public function jsonSerialize(): mixed + { + return $this->toArray(); + } +} diff --git a/api/src/Model/Mailbox.php b/api/src/Model/Mailbox.php new file mode 100644 index 0000000..770f715 --- /dev/null +++ b/api/src/Model/Mailbox.php @@ -0,0 +1,129 @@ +id; + } + public function getName(): string + { + return $this->name; + } + public function getValidity(): int + { + return $this->validity; + } + + public function setId(int $id): Mailbox + { + $this->id = $id; + return $this; + } + public function setName(string $name): Mailbox + { + $this->name = $name; + return $this; + } + public function setValidity(int $uidvalidity): Mailbox + { + $this->validity = $uidvalidity; + return $this; + } + + protected \ProVM\Emails\Repository\State\Mailbox $stateRepository; + + public function getStateRepository(): \ProVM\Emails\Repository\State\Mailbox + { + return $this->stateRepository; + } + public function setStateRepository(\ProVM\Emails\Repository\State\Mailbox $repository): Mailbox + { + $this->stateRepository = $repository; + return $this; + } + + protected array $states; + public function getStates(): array + { + if (!isset($this->states)) { + try { + $this->setStates($this->getStateRepository()->fetchByMailbox($this->getId())); + } catch (BlankResult $e) { + return []; + } + } + return $this->states; + } + public function addState(\ProVM\Emails\Model\State\Mailbox $state): Mailbox + { + $this->states []= $state; + return $this; + } + public function setStates(array $states): Mailbox + { + foreach ($states as $state) { + $this->addState($state); + } + return $this; + } + + public function lastState(): State\Mailbox + { + if (count($this->getStates()) === 0) { + throw new Stateless($this); + } + return $this->getStates()[array_key_last($this->getStates())]; + } + public function lastChecked(): ?DateTimeInterface + { + if (count($this->getStates()) == 0) { + return null; + } + return $this->lastState()->getDateTime(); + } + public function lastCount(): int + { + if (count($this->getStates()) == 0) { + return 0; + } + return $this->lastState()->getCount(); + } + public function lastPosition(): int + { + $state = $this->lastState()->getUIDs(); + if (count($state) === 0) { + return 0; + } + return array_key_last($state); + } + + public function toArray(): array + { + return [ + 'id' => $this->getId(), + 'name' => $this->getName(), + //'validity' => $this->getValidity(), + 'last_checked' => [ + 'date' => $this->lastChecked() ?? 'never', + 'count' => $this->lastCount() ?? 0 + ] + ]; + } + public function jsonSerialize(): mixed + { + return $this->toArray(); + } +} diff --git a/api/src/Model/Message.php b/api/src/Model/Message.php new file mode 100644 index 0000000..5992956 --- /dev/null +++ b/api/src/Model/Message.php @@ -0,0 +1,226 @@ +id; + } + public function getMailbox(): Mailbox + { + return $this->mailbox; + } + public function getPosition(): int + { + return $this->position; + } + public function getUID(): string + { + return $this->uid; + } + public function getSubject(): string + { + return $this->subject; + } + public function getFrom(): string + { + return $this->from; + } + public function getDateTime(): DateTimeInterface + { + return $this->dateTime; + } + + public function setId(int $id): Message + { + $this->id = $id; + return $this; + } + public function setMailbox(Mailbox $mailbox): Message + { + $this->mailbox = $mailbox; + return $this; + } + public function setPosition(int $position): Message + { + $this->position = $position; + return $this; + } + public function setUID(string $uid): Message + { + $this->uid = $uid; + return $this; + } + public function setSubject(string $subject): Message + { + $this->subject = $subject; + return $this; + } + public function setFrom(string $from): Message + { + $this->from = $from; + return $this; + } + public function setDateTime(DateTimeInterface $dateTime): Message + { + $this->dateTime = $dateTime; + return $this; + } + + protected \ProVM\Common\Factory\Model $factory; + + public function getFactory(): \ProVM\Common\Factory\Model + { + return $this->factory; + } + + public function setFactory(\ProVM\Common\Factory\Model $factory): Message + { + $this->factory = $factory; + return $this; + } + + protected array $states; + + public function getStates(): array + { + if (!isset($this->states)) { + try { + $this->setStates($this->getFactory()->find(State\Message::class)->fetchByMessage($this->getId())); + } catch (BlankResult $e) { + return []; + } + } + return $this->states; + } + public function getState(string $name): State\Message + { + try { + return $this->getStates()[$name]; + } catch (\Exception $e) { + $this->newState($name); + return $this->getStates()[$name]; + } + } + public function addState(State\Message $state): Message + { + $this->states[$state->getName()] = $state; + return $this; + } + public function setStates(array $states): Message + { + foreach ($states as $state) { + $this->addState($state); + } + return $this; + } + protected function newState(string $name): Message + { + $this->addState((new State\Message()) + ->setName($name) + ->setMessage($this) + ->setValue(false) + ); + return $this; + } + + public function hasAttachments(): bool + { + return $this->getState('has_attachments')->getValue() ?? false; + } + public function hasValidAttachments(): bool + { + return $this->getState('valid_attachments')->getValue() ?? false; + } + public function hasDownloadedAttachments(): bool + { + return $this->getState('downloaded_attachments')->getValue() ?? false; + } + public function hasScheduledDownloads(): bool + { + return $this->getState('scheduled_downloads')->getValue() ?? false; + } + + public function doesHaveAttachments(): Message + { + $this->getState('has_attachments')->setValue(true); + return $this; + } + public function doesHaveValidAttachments(): Message + { + $this->getState('valid_attachments')->setValue(true); + return $this; + } + public function doesHaveDownloadedAttachments(): Message + { + $this->getState('downloaded_attachments')->setValue(true); + return $this; + } + public function doesHaveScheduledDownloads(): Message + { + $this->getState('scheduled_downloads')->setValue(true); + return $this; + } + + protected array $attachments; + public function getAttachments(): array + { + if (!isset($this->attachments)) { + try { + $this->setAttachments($this->getFactory()->find(Attachment::class)->fetchByMessage($this->getId())); + } catch (BlankResult $e) { + return []; + } + } + return $this->attachments ?? []; + } + public function addAttachment(Attachment $attachment): Message + { + $this->attachments []= $attachment; + return $this; + } + public function setAttachments(array $attachments): Message + { + foreach ($attachments as $attachment) { + $this->addAttachment($attachment); + } + return $this; + } + + public function toArray(): array + { + return [ + 'id' => $this->getId(), + 'uid' => $this->getUID(), + 'subject' => $this->getSubject(), + 'from' => $this->getFrom(), + 'date_time' => $this->getDateTime()->format('Y-m-d H:i:s'), + 'states' => [ + 'has_attachments' => $this->hasAttachments(), + 'valid_attachments' => $this->hasValidAttachments(), + 'downloaded_attachments' => $this->hasDownloadedAttachments(), + 'scheduled_downloads' => $this->hasScheduledDownloads() + ], + 'attachments' => $this->hasValidAttachments() ? array_map(function(Attachment $attachment) { + return $attachment->toArray(); + }, $this->getAttachments()) : [] + ]; + } + public function jsonSerialize(): mixed + { + return $this->toArray(); + } +} diff --git a/api/src/Model/State/Attachment.php b/api/src/Model/State/Attachment.php new file mode 100644 index 0000000..edb5e86 --- /dev/null +++ b/api/src/Model/State/Attachment.php @@ -0,0 +1,64 @@ +id; + } + public function getAttachment(): \ProVM\Emails\Model\Attachment + { + return $this->attachment; + } + public function getName(): string + { + return $this->name; + } + public function getValue(): bool + { + return $this->value ?? false; + } + + public function setId(int $id): Attachment + { + $this->id = $id; + return $this; + } + public function setAttachment(\ProVM\Emails\Model\Attachment $attachment): Attachment + { + $this->attachment = $attachment; + return $this; + } + public function setName(string $name): Attachment + { + $this->name = $name; + return $this; + } + public function setValue(bool $value): Attachment + { + $this->value = $value; + return $this; + } + + public function toArray(): array + { + return [ + 'id' => $this->getId(), + 'attachment_id' => $this->getAttachment()->getId(), + 'name' => $this->getName(), + 'value' => $this->getValue() + ]; + } + public function jsonSerialize(): mixed + { + return $this->toArray(); + } +} diff --git a/api/src/Model/State/Job.php b/api/src/Model/State/Job.php new file mode 100644 index 0000000..1d1e9af --- /dev/null +++ b/api/src/Model/State/Job.php @@ -0,0 +1,65 @@ +id; + } + public function getJob(): Emails\Model\Job + { + return $this->job; + } + public function getDateTime(): \DateTimeInterface + { + return $this->dateTime; + } + public function getStatus(): int + { + return $this->status; + } + + public function setId(int $id): Job + { + $this->id = $id; + return $this; + } + public function setJob(Emails\Model\Job $job): Job + { + $this->job = $job; + return $this; + } + public function setDateTime(\DateTimeInterface $dateTime): Job + { + $this->dateTime = $dateTime; + return $this; + } + public function setStatus(int $status): Job + { + $this->status = $status; + return $this; + } + + public function jsonSerialize(): mixed + { + return [ + 'id' => $this->getId(), + 'job_id' => $this->getJob()->getId(), + 'date' => $this->getDateTime(), + 'status' => $this->getStatus() + ]; + } +} diff --git a/api/src/Model/State/Mailbox.php b/api/src/Model/State/Mailbox.php new file mode 100644 index 0000000..35e66d3 --- /dev/null +++ b/api/src/Model/State/Mailbox.php @@ -0,0 +1,83 @@ +id; + } + public function getMailbox(): \ProVM\Emails\Model\Mailbox + { + return $this->mailbox; + } + public function getDateTime(): DateTimeInterface + { + return $this->dateTime; + } + public function getCount(): int + { + return $this->count; + } + public function getUIDs(): array + { + return $this->uids ?? []; + } + + public function setId(int $id): Mailbox + { + $this->id = $id; + return $this; + } + public function setMailbox(\ProVM\Emails\Model\Mailbox $mailbox): Mailbox + { + $this->mailbox = $mailbox; + return $this; + } + public function setDateTime(DateTimeInterface $dateTime): Mailbox + { + $this->dateTime = $dateTime; + return $this; + } + public function setCount(int $count): Mailbox + { + $this->count = $count; + return $this; + } + public function addUID(string $uid): Mailbox + { + $this->uids []= $uid; + return $this; + } + public function setUIDs(array $uids): Mailbox + { + foreach ($uids as $uid) { + $this->addUID($uid); + } + return $this; + } + + public function toArray(): array + { + return [ + 'id' => $this->getId(), + 'mailbox_id' => $this->getMailbox()->getId(), + 'date_time' => $this->getDateTime()->format('Y-m-d H:i:s'), + 'count' => $this->getCount(), + 'uids' => $this->getUIDs() + ]; + } + public function jsonSerialize(): mixed + { + return $this->toArray(); + } +} diff --git a/api/src/Model/State/Message.php b/api/src/Model/State/Message.php new file mode 100644 index 0000000..304cdd6 --- /dev/null +++ b/api/src/Model/State/Message.php @@ -0,0 +1,64 @@ +id; + } + public function getMessage(): \ProVM\Emails\Model\Message + { + return $this->message; + } + public function getName(): string + { + return $this->name; + } + public function getValue(): bool + { + return $this->value; + } + + public function setId(int $id): Message + { + $this->id = $id; + return $this; + } + public function setMessage(\ProVM\Emails\Model\Message $message): Message + { + $this->message = $message; + return $this; + } + public function setName(string $name): Message + { + $this->name = $name; + return $this; + } + public function setValue(bool $value): Message + { + $this->value = $value; + return $this; + } + + public function toArray(): array + { + return [ + 'id' => $this->getId(), + 'message_id' => $this->getMessage()->getId(), + 'name' => $this->getName(), + 'value' => $this->getValue() + ]; + } + public function jsonSerialize(): mixed + { + return $this->toArray(); + } +} diff --git a/api/src/Repository/Attachment.php b/api/src/Repository/Attachment.php new file mode 100644 index 0000000..d27f59c --- /dev/null +++ b/api/src/Repository/Attachment.php @@ -0,0 +1,139 @@ +setFactory($factory) + ->setTable('attachments'); + } + + protected Model $factory; + public function getFactory(): Model + { + return $this->factory; + } + public function setFactory(Model $factory): Attachment + { + $this->factory = $factory; + return $this; + } + + public function install(): void + { + $query = " +CREATE TABLE {$this->getTable()} ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `message_id` INT UNSIGNED NOT NULL, + `filename` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY `fk_messages_{$this->getTable()}` (`message_id`) + REFERENCES `messages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +)"; + $this->getConnection()->query($query); + } + + protected function fieldsForInsert(): array + { + return [ + 'message_id', + 'filename' + ]; + } + protected function fieldsForUpdate(): array + { + return $this->fieldsForInsert(); + } + protected function fieldsForCreate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForUpdate(ModelInterface $model): array + { + + return $this->valuesForInsert($model); + } + protected function valuesForInsert(ModelInterface $model): array + { + return [ + $model->getMessage()->getId(), + $model->getFilename() + ]; + } + protected function defaultFind(ModelInterface $model): ModelInterface + { + return $this->fetchByMessageAndFilename($model->getMessage()->getId(), $model->getFilename()); + } + protected function valuesForCreate(array $data): array + { + return [ + $data['message_id'], + $data['filename'] + ]; + } + protected function defaultSearch(array $data): ModelInterface + { + return $this->fetchByMessageAndFilename($data['message_id'], $data['filename']); + } + + public function load(array $row): \ProVM\Emails\Model\Attachment + { + return (new \ProVM\Emails\Model\Attachment()) + ->setId($row['id']) + ->setMessage($this->getFactory()->find(\ProVM\Emails\Model\Message::class)->fetchById($row['message_id'])) + ->setFilename($row['filename']) + ->setStateRepository($this->getFactory()->find(\ProVM\Emails\Model\State\Attachment::class)); + } + + public function save(ModelInterface &$model): void + { + parent::save($model); + $states_names = [ + 'downloaded', + 'encrypted', + 'decrypted' + ]; + foreach ($states_names as $name) { + try { + $state = $model->getState($name); + $this->getFactory()->find(\ProVM\Emails\Model\State\Attachment::class)->save($state); + } catch (BlankResult $e) { + $this->getLogger()->error($e); + $data = [ + 'attachment_id' => $model->getId(), + 'name' => $name, + 'value' => false + ]; + $state = $this->getFactory()->find(\ProVM\Emails\Model\State\Attachment::class)->create($data); + $this->getFactory()->find(\ProVM\Emails\Model\State\Attachment::class)->save($state); + } + } + } + + public function fetchByMessageAndFilename(int $message_id, string $filename): \ProVM\Emails\Model\Attachment + { + $query = "SELECT * FROM {$this->getTable()} WHERE message_id = ? AND filename = ?"; + return $this->fetchOne($query, [$message_id, $filename]); + } + public function fetchByMessage(int $message_id): array + { + $query = "SELECT * FROM {$this->getTable()} WHERE message_id = ?"; + return $this->fetchMany($query, [$message_id]); + } + public function fetchDownloaded(): array + { + $query = "SELECT a.* +FROM `{$this->getTable()}` a JOIN `attachments_states` `as` ON `as`.attachment_id = a.id +WHERE `as`.name = 'downloaded' AND `as`.value = 1"; + return $this->fetchMany($query); + } +} diff --git a/api/src/Repository/Job.php b/api/src/Repository/Job.php new file mode 100644 index 0000000..17c39df --- /dev/null +++ b/api/src/Repository/Job.php @@ -0,0 +1,119 @@ +setFactory($factory) + ->setTable('jobs'); + } + + protected Factory\Model $factory; + public function getFactory(): Factory\Model + { + return $this->factory; + } + public function setFactory(Factory\Model $factory): Job + { + $this->factory = $factory; + return $this; + } + + public function install(): void + { + $query = " +CREATE TABLE {$this->getTable()} ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `command` VARCHAR(100) NOT NULL, + `arguments` TEXT NOT NULL, + PRIMARY KEY (`id`) +)"; + $this->getConnection()->query($query); + } + + protected function fieldsForUpdate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForUpdate(ModelInterface $model): array + { + return $this->valuesForInsert($model); + } + protected function fieldsForInsert(): array + { + return [ + 'command', + 'arguments', + ]; + } + protected function valuesForInsert(ModelInterface $model): array + { + return [ + $model->getCommand(), + $model->getArguments(), + ]; + } + protected function defaultFind(ModelInterface $model): ModelInterface + { + return $this->fetchByCommandAndArguments($model->getCommand(), $model->getArguments()); + } + protected function fieldsForCreate(): array + { + return [ + 'command', + 'arguments', + ]; + } + protected function valuesForCreate(array $data): array + { + return [ + $data['command'], + $data['arguments'], + ]; + } + protected function defaultSearch(array $data): ModelInterface + { + return $this->fetchByCommandAndArguments($data['command'], $data['arguments']); + } + + public function load(array $row): ModelInterface + { + return (new BaseModel()) + ->setId($row['id']) + ->setCommand($row['command']) + ->setArguments($row['arguments']) + ->setStateRepository($this->getFactory()->find(Emails\Model\State\Job::class)); + } + + public function fetchAllPending(): array + { + $query = "SELECT a.* +FROM `{$this->getTable()}` a + JOIN (SELECT s1.* FROM `jobs_states` s1 JOIN (SELECT MAX(id) AS id, job_id FROM `jobs_states` GROUP BY job_id) s2 ON s2.id = s1.id) b ON b.`job_id` = a.`id` +WHERE b.`status` = ?"; + return $this->fetchMany($query, [Emails\Model\State\Job::Pending]); + } + public function fetchByCommandAndArguments(string $command, string $arguments): Emails\Model\Job + { + $query = "SELECT * FROM {$this->getTable()} WHERE `command` = ? AND `arguments` = ?"; + return $this->fetchOne($query, [$command, $arguments]); + } + public function fetchAllPendingByCommand(string $command): array + { + $query = "SELECT a.* +FROM `{$this->getTable()}` a + JOIN (SELECT s1.* FROM `jobs_states` s1 JOIN (SELECT MAX(id) AS id, job_id FROM `jobs_states` GROUP BY job_id) s2 ON s2.id = s1.id) b ON b.`job_id` = a.`id` +WHERE a.`command` = ? AND b.`status` = ?"; + return $this->fetchMany($query, [$command, Emails\Model\State\Job::Pending]); + } +} diff --git a/api/src/Repository/Mailbox.php b/api/src/Repository/Mailbox.php new file mode 100644 index 0000000..212355d --- /dev/null +++ b/api/src/Repository/Mailbox.php @@ -0,0 +1,101 @@ +setStates($states) + ->setTable('mailboxes'); + } + + protected State\Mailbox $stateRepository; + + public function getStates(): State\Mailbox + { + return $this->stateRepository; + } + + public function setStates(State\Mailbox $states): Mailbox + { + $this->stateRepository = $states; + return $this; + } + + public function install(): void + { + $query = " +CREATE TABLE {$this->getTable()} ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + `validity` INT UNSIGNED NOT NULL, + PRIMARY KEY (`id`) +)"; + $this->getConnection()->query($query); + } + + protected function fieldsForUpdate(): array + { + return $this->fieldsForInsert(); + } + protected function fieldsForInsert(): array + { + return [ + 'name', + 'validity' + ]; + } + protected function fieldsForCreate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForInsert(Model $model): array + { + return [ + $model->getName(), + $model->getValidity() + ]; + } + protected function defaultFind(Model $model): Model + { + return $this->fetchByName($model->getName()); + } + protected function valuesForUpdate(Model $model): array + { + return array_merge($this->valuesForInsert($model), [ + $model->getId() + ]); + } + protected function valuesForCreate(array $data): array + { + return [ + $data['name'], + $data['validity'] + ]; + } + protected function defaultSearch(array $data): Model + { + return $this->fetchByName($data['name']); + } + + public function load(array $row): \ProVM\Emails\Model\Mailbox + { + return (new \ProVM\Emails\Model\Mailbox()) + ->setId($row['id']) + ->setName($row['name']) + ->setValidity($row['validity']) + ->setStateRepository($this->getStates()); + } + + public function fetchByName(string $name): \ProVM\Emails\Model\Mailbox + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `name` = ?"; + return $this->fetchOne($query, [$name]); + } +} diff --git a/api/src/Repository/Message.php b/api/src/Repository/Message.php new file mode 100644 index 0000000..11ed659 --- /dev/null +++ b/api/src/Repository/Message.php @@ -0,0 +1,187 @@ +setTable('messages') + ->setFactory($factory); + } + + protected Factory\Model $factory; + public function getFactory(): Factory\Model + { + return $this->factory; + } + public function setFactory(Factory\Model $factory): Message + { + $this->factory = $factory; + return $this; + } + + public function install(): void + { + $query = " +CREATE TABLE {$this->getTable()} ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `uid` VARCHAR(255) NOT NULL, + `mailbox_id` INT UNSIGNED NOT NULL, + `position` INT UNSIGNED NOT NULL, + `subject` VARCHAR(255) NOT NULL, + `from` VARCHAR(255) NOT NULL, + `date_time` DATETIME NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY `fk_mailboxes_{$this->getTable()}` (`mailbox_id`) + REFERENCES `mailboxes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +)"; + $this->getConnection()->query($query); + } + + protected function fieldsForUpdate(): array + { + return $this->fieldsForInsert(); + } + protected function fieldsForInsert(): array + { + return [ + 'uid', + 'mailbox_id', + 'position', + 'subject', + 'from', + 'date_time' + ]; + } + protected function fieldsForCreate(): array + { + return $this->fieldsForUpdate(); + } + protected function valuesForUpdate(Model $model): array + { + return array_merge($this->valuesForInsert($model), [ + $model->getId() + ]); + } + protected function valuesForInsert(Model $model): array + { + return [ + $model->getUID(), + $model->getMailbox()->getId(), + $model->getPosition(), + $model->getSubject(), + $model->getFrom(), + $model->getDateTime()->format('Y-m-d H:i:s') + ]; + } + protected function defaultFind(Model $model): Model + { + return $this->fetchByUID($model->getUID()); + } + + protected function valuesForCreate(array $data): array + { + return [ + $data['mailbox_id'], + $data['position'], + $data['uid'], + $data['subject'], + $data['from'], + $data['date_time'] + ]; + } + protected function defaultSearch(array $data): Model + { + return $this->fetchByUID($data['uid']); + } + + /** + * @param array $row + * @return \ProVM\Emails\Model\Message + */ + public function load(array $row): \ProVM\Emails\Model\Message + { + return (new \ProVM\Emails\Model\Message()) + ->setId($row['id']) + ->setUID($row['uid']) + ->setMailbox($this->getFactory()->find(\ProVM\Emails\Model\Mailbox::class)->fetchById($row['mailbox_id'])) + ->setPosition($row['position']) + ->setSubject($row['subject']) + ->setFrom($row['from']) + ->setDateTime(new DateTimeImmutable($row['date_time'])) + ->setFactory($this->getFactory()); + } + + public function save(Model &$model): void + { + parent::save($model); + $valid_states = [ + 'has_attachments', + 'valid_attachments', + 'downloaded_attachments', + 'scheduled_downloads' + ]; + $stateRepository = $this->getFactory()->find(\ProVM\Emails\Model\State\Message::class); + foreach ($valid_states as $state_name) { + try { + $model->getState($state_name); + } catch (\Exception $e) { + $this->getLogger()->warning($e); + $data = [ + 'message_id' => $model->getId(), + 'name' => $state_name + ]; + $state = $stateRepository->create($data); + $model->addState($state); + } + } + foreach ($model->getStates() as $state) { + //$state->setMessage($model); + $stateRepository->save($state); + } + } + + public function fetchByUID(string $uid): \ProVM\Emails\Model\Message + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `uid` = ?"; + return $this->fetchOne($query, [$uid]); + } + public function fetchByMailbox(int $mailbox_id): array + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `mailbox_id` = ?"; + return $this->fetchMany($query, [$mailbox_id]); + } + public function fetchByMailboxAndPosition(int $mailbox_id, int $start, int $amount): array + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `mailbox_id` = ? AND `position` BETWEEN ? AND ? LIMIT {$amount}"; + return $this->fetchMany($query, [$mailbox_id, $start, $start + $amount]); + } + public function fetchValidByMailbox(int $mailbox_id): array + { + $query = "SELECT a.* + FROM `{$this->getTable()}` a + JOIN `messages_states` b ON b.`message_id` = a.`id` + WHERE a.`mailbox_id` = ? AND b.`name` = 'valid_attachments' AND b.`value` = 1"; + return $this->fetchMany($query, [$mailbox_id]); + } + public function fetchByMailboxSubjectFromAndDate(int $mailbox_id, string $subject, string $from, DateTimeInterface $dateTime): \ProVM\Emails\Model\Message + { + $query = "SELECT * FROM `{$this->getTable()}` + WHERE `mailbox_id` = ? `subject` = ? AND `from` = ? AND `date_time` = ?"; + return $this->fetchOne($query, [$mailbox_id, $subject, $from, $dateTime->format('Y-m-d H:i:s')]); + } + public function fetchAllBySubjectAndDate(string $subject, DateTimeInterface $dateTime): array + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `subject` = ? AND `date_time` BETWEEN ? AND ?"; + return $this->fetchMany($query, [$subject, $dateTime->format('Y-m-d 00:00:00'), $dateTime->format('Y-m-d 23:59:59')]); + } +} diff --git a/api/src/Repository/State/Attachment.php b/api/src/Repository/State/Attachment.php new file mode 100644 index 0000000..2240eda --- /dev/null +++ b/api/src/Repository/State/Attachment.php @@ -0,0 +1,109 @@ +setTable('attachments_states') + ->setFactory($factory); + } + + protected \ProVM\Common\Factory\Model $factory; + public function getFactory(): \ProVM\Common\Factory\Model + { + return $this->factory; + } + public function setFactory(\ProVM\Common\Factory\Model $factory): Attachment + { + $this->factory = $factory; + return $this; + } + + public function install(): void + { + $query = " +CREATE TABLE {$this->getTable()} ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `attachment_id` INT UNSIGNED NOT NULL, + `name` VARCHAR(100) NOT NULL, + `value` INT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + FOREIGN KEY `fk_attachments_{$this->getTable()}` (`attachment_id`) + REFERENCES `attachments` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) +"; + $this->getConnection()->query($query); + } + + protected function fieldsForInsert(): array + { + return [ + 'attachment_id', + 'name', + 'value' + ]; + } + protected function valuesForInsert(Model $model): array + { + return [ + $model->getAttachment()->getId(), + $model->getName(), + $model->getValue() ? 1 : 0 + ]; + } + protected function fieldsForUpdate(): array + { + return $this->fieldsForInsert(); + } + protected function defaultFind(Model $model): Model + { + return $this->fetchByAttachmentAndName($model->getAttachment()->getId(), $model->getName()); + } + protected function valuesForUpdate(Model $model): array + { + return $this->valuesForInsert($model); + } + protected function fieldsForCreate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForCreate(array $data): array + { + return [ + $data['attachment_id'], + $data['name'], + $data['value'] ? 1 : 0 + ]; + } + protected function defaultSearch(array $data): Model + { + return $this->fetchByAttachmentAndName($data['attachment_id'], $data['name']); + } + + public function load(array $row): \ProVM\Emails\Model\State\Attachment + { + return (new \ProVM\Emails\Model\State\Attachment()) + ->setId($row['id']) + ->setAttachment($this->getFactory()->find(\ProVM\Emails\Model\Attachment::class)->fetchById($row['attachment_id'])) + ->setName($row['name']) + ->setValue($row['value'] !== 0); + } + + public function fetchByAttachment(int $attachment_id): array + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `attachment_id` = ?"; + return $this->fetchMany($query, [$attachment_id]); + } + public function fetchByAttachmentAndName(int $attachment_id, string $name): \ProVM\Emails\Model\State\Attachment + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `attachment_id` = ? AND `name` = ?"; + return $this->fetchOne($query, [$attachment_id, $name]); + } +} diff --git a/api/src/Repository/State/Job.php b/api/src/Repository/State/Job.php new file mode 100644 index 0000000..11a6699 --- /dev/null +++ b/api/src/Repository/State/Job.php @@ -0,0 +1,113 @@ +setTable('jobs_states') + ->setFactory($factory); + } + + protected Factory\Model $factory; + public function getFactory(): Factory\Model + { + return $this->factory; + } + public function setFactory(Factory\Model $factory): Job + { + $this->factory = $factory; + return $this; + } + + public function install(): void + { + $query = " +CREATE TABLE {$this->getTable()} ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `job_id` INT UNSIGNED NOT NULL, + `date_time` DATETIME NOT NULL, + `status` INT NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + FOREIGN KEY `fk_jobs_{$this->getTable()}` (`job_id`) + REFERENCES `jobs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +)"; + $this->getConnection()->query($query); + } + + public function load(array $row): Define\Model + { + return (new Emails\Model\State\Job()) + ->setId($row['id']) + ->setJob($this->getFactory()->find(Emails\Model\Job::class)->fetchById($row['job_id'])) + ->setDateTime(new \DateTimeImmutable($row['date_time'])) + ->setStatus($row['status']); + } + + public function fetchByJob(int $job_id): array + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `job_id` = ?"; + return $this->fetchMany($query, [$job_id]); + } + public function fetchByJobAndStatus(int $job_id, int $status): Emails\Model\State\Job + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `job_id` = ? AND `status` = ?"; + return $this->fetchOne($query, [$job_id, $status]); + } + + protected function fieldsForInsert(): array + { + return [ + 'job_id', + 'date_time', + 'status' + ]; + } + protected function valuesForInsert(Define\Model $model): array + { + return [ + $model->getJob()->getId(), + $model->getDateTime()->format('Y-m-d H:i:s'), + $model->getStatus() + ]; + } + + protected function fieldsForUpdate(): array + { + return $this->fieldsForInsert(); + } + protected function fieldsForCreate(): array + { + return $this->fieldsForInsert(); + } + + protected function valuesForUpdate(Define\Model $model): array + { + return $this->valuesForInsert($model); + } + protected function valuesForCreate(array $data): array + { + return [ + $data['job_id'], + $data['date_time'], + $data['status'] + ]; + } + + protected function defaultFind(Define\Model $model): Define\Model + { + return $this->fetchByJobAndStatus($model->getJob()->getId(), $model->getStatus()); + } + protected function defaultSearch(array $data): Define\Model + { + return $this->fetchByJobAndStatus($data['job_id'], $data['status']); + } +} diff --git a/api/src/Repository/State/Mailbox.php b/api/src/Repository/State/Mailbox.php new file mode 100644 index 0000000..e6e03a5 --- /dev/null +++ b/api/src/Repository/State/Mailbox.php @@ -0,0 +1,119 @@ +setTable('mailboxes_states') + ->setFactory($factory); + } + + protected \ProVM\Common\Factory\Model $factory; + + public function getFactory(): \ProVM\Common\Factory\Model + { + return $this->factory; + } + public function setFactory(\ProVM\Common\Factory\Model $factory): Mailbox + { + $this->factory = $factory; + return $this; + } + + public function install(): void + { + $query = " +CREATE TABLE {$this->getTable()} ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `mailbox_id` INT UNSIGNED NOT NULL, + `date_time` DATETIME NOT NULL, + `count` INT UNSIGNED NOT NULL, + `uids` TEXT NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY `fk_mailboxes_{$this->getTable()}` (`mailbox_id`) + REFERENCES `mailboxes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) +"; + $this->getConnection()->query($query); + } + + protected function fieldsForUpdate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForUpdate(Model $model): array + { + return array_merge($this->valuesForInsert($model), [$model->getId()]); + } + protected function fieldsForInsert(): array + { + return [ + 'mailbox_id', + 'date_time', + 'count', + 'uids' + ]; + } + protected function valuesForInsert(Model $model): array + { + return [ + $model->getMailbox()->getId(), + $model->getDateTime()->format('Y-m-d H:i:s'), + $model->getCount(), + serialize($model->getUIDs()) + ]; + } + protected function defaultFind(Model $model): Model + { + return $this->fetchByMailboxAndDate($model->getMailbox()->getId(), $model->getDateTime()->format('Y-m-d H:i:s')); + } + protected function fieldsForCreate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForCreate(array $data): array + { + return [ + $data['mailbox_id'], + $data['date_time'], + $data['count'], + $data['uids'] + ]; + } + protected function defaultSearch(array $data): Model + { + return $this->fetchByMailboxAndDate($data['mailbox_id'], $data['date_time']); + } + + public function load(array $row): \ProVM\Emails\Model\State\Mailbox + { + return (new \ProVM\Emails\Model\State\Mailbox()) + ->setId($row['id']) + ->setMailbox($this->getFactory()->find(\ProVM\Emails\Model\Mailbox::class)->fetchById($row['mailbox_id'])) + ->setDateTime(new DateTimeImmutable($row['date_time'])) + ->setCount($row['count']) + ->setUIDs(unserialize($row['uids'])); + } + + public function fetchByMailbox(int $mailbox_id): array + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `mailbox_id` = ?"; + return $this->fetchMany($query, [$mailbox_id]); + } + public function fetchByMailboxAndDate(int $mailbox_id, \DateTimeInterface | string $date_time): \ProVM\Emails\Model\State\Mailbox + { + if (!is_string($date_time)) { + $date_time = $date_time->format('Y-m-d H:i:s'); + } + $query = "SELECT * FROM `{$this->getTable()}` WHERE `mailbox_id` = ? AND `date_time` = ?"; + return $this->fetchOne($query, [$mailbox_id, $date_time]); + } +} diff --git a/api/src/Repository/State/Message.php b/api/src/Repository/State/Message.php new file mode 100644 index 0000000..8859eb8 --- /dev/null +++ b/api/src/Repository/State/Message.php @@ -0,0 +1,109 @@ +setTable('messages_states') + ->setFactory($factory); + } + + protected \ProVM\Common\Factory\Model $factory; + public function getFactory(): \ProVM\Common\Factory\Model + { + return $this->factory; + } + public function setFactory(\ProVM\Common\Factory\Model $factory): Message + { + $this->factory = $factory; + return $this; + } + + public function install(): void + { + $query = " +CREATE TABLE {$this->getTable()} ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `message_id` INT UNSIGNED NOT NULL, + `name` VARCHAR(100) NOT NULL, + `value` INT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + FOREIGN KEY `fk_messages_{$this->getTable()}` (`message_id`) + REFERENCES `messages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) +"; + $this->getConnection()->query($query); + } + + protected function fieldsForUpdate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForUpdate(Model $model): array + { + return array_merge($this->valuesForInsert($model), [$model->getId()]); + } + public function fieldsForInsert(): array + { + return [ + 'message_id', + 'name', + 'value' + ]; + } + protected function valuesForInsert(Model $model): array + { + return [ + $model->getMessage()->getId(), + $model->getName(), + $model->getValue() ? 1 : 0 + ]; + } + protected function defaultFind(Model $model): Model + { + return $this->fetchByMessageAndName($model->getMessage()->getId(), $model->getName()); + } + protected function fieldsForCreate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForCreate(array $data): array + { + return [ + $data['message_id'], + $data['name'], + $data['value'] ? 1 : 0 + ]; + } + protected function defaultSearch(array $data): Model + { + return $this->fetchByMessageAndName($data['message_id'], $data['name']); + } + + public function load(array $row): \ProVM\Emails\Model\State\Message + { + return (new \ProVM\Emails\Model\State\Message()) + ->setId($row['id']) + ->setName($row['name']) + ->setMessage($this->getFactory()->find(\ProVM\Emails\Model\Message::class)->fetchById($row['message_id'])) + ->setValue(($row['value'] ?? 0) !== 0); + } + + public function fetchByMessage(int $message_id): array + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `message_id` = ?"; + return $this->fetchMany($query, [$message_id]); + } + public function fetchByMessageAndName(int $message_id, string $name): \ProVM\Emails\Model\State\Message + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `message_id` = ? AND `name` = ?"; + return $this->fetchOne($query, [$message_id, $name]); + } +} diff --git a/cli/.env.sample b/cli/.env.sample new file mode 100644 index 0000000..662913a --- /dev/null +++ b/cli/.env.sample @@ -0,0 +1 @@ +API_URI=http://proxy:8080 \ No newline at end of file diff --git a/cli/Dockerfile b/cli/Dockerfile new file mode 100644 index 0000000..d596ae4 --- /dev/null +++ b/cli/Dockerfile @@ -0,0 +1,14 @@ +FROM php:8-cli + +ENV PATH ${PATH}:/app/bin + +RUN apt-get update \ + && apt-get install -y cron git libzip-dev unzip qpdf \ + && rm -r /var/lib/apt/lists/* \ + && docker-php-ext-install zip + +COPY ./crontab /var/spool/cron/crontabs/root + +WORKDIR /app + +CMD [ "cron", "-f", "-L", "15" ] diff --git a/cli/bin/emails b/cli/bin/emails new file mode 100644 index 0000000..b7e44fd --- /dev/null +++ b/cli/bin/emails @@ -0,0 +1,3 @@ +#!/bin/bash + +php /app/public/index.php "$@" diff --git a/cli/common/Command/Jobs/Check.php b/cli/common/Command/Jobs/Check.php new file mode 100644 index 0000000..5a5ecc6 --- /dev/null +++ b/cli/common/Command/Jobs/Check.php @@ -0,0 +1,52 @@ +section(); + $section2 = $output->section(); + $io1 = new SymfonyStyle($input, $section1); + $io2 = new SymfonyStyle($input, $section2); + $io1->title('Checking Pending Jobs'); + $pending_jobs = $this->service->getPending(); + $notice = 'Found ' . count($pending_jobs) . ' jobs'; + $io1->text($notice); + if (count($pending_jobs) > 0) { + $io1->section('Running Jobs'); + $io1->progressStart(count($pending_jobs)); + foreach ($pending_jobs as $job) { + $section2->clear(); + $io2->text("Running {$job->command}"); + if ($this->service->run($job)) { + $io2->success('Success'); + } else { + $io2->error('Failure'); + } + $io1->progressAdvance(); + } + } + $section2->clear(); + $io2->success('Done'); + + return Command::SUCCESS; + } +} diff --git a/cli/common/Command/Jobs/Execute.php b/cli/common/Command/Jobs/Execute.php new file mode 100644 index 0000000..0c41a4e --- /dev/null +++ b/cli/common/Command/Jobs/Execute.php @@ -0,0 +1,41 @@ +addArgument('job_id', InputArgument::REQUIRED, 'Job ID to be executed'); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $job_id = $input->getArgument('job_id'); + $job = $this->service->get($job_id); + if ($this->service->run($job)) { + $io->success('Success'); + } else { + $io->error('Failed'); + } + return Command::SUCCESS; + } +} diff --git a/cli/common/Command/Mailboxes/Check.php b/cli/common/Command/Mailboxes/Check.php new file mode 100644 index 0000000..eb2f94c --- /dev/null +++ b/cli/common/Command/Mailboxes/Check.php @@ -0,0 +1,53 @@ +section(); + $section2 = $output->section(); + $io1 = new SymfonyStyle($input, $section1); + $io2 = new SymfonyStyle($input, $section2); + $io1->title('Checking for New Messages'); + $mailboxes = $this->service->getAll(); + $notice = 'Found ' . count($mailboxes) . ' mailboxes'; + $io1->text($notice); + if (count($mailboxes) > 0) { + $io1->section('Checking for new messages'); + $io1->progressStart(count($mailboxes)); + foreach ($mailboxes as $mailbox) { + $section2->clear(); + $io2->text("Checking {$mailbox->name}"); + if ($this->service->check($mailbox)) { + $io2->success("Found new emails in {$mailbox->name}"); + } else { + $io2->info("No new emails in {$mailbox->name}"); + } + $io1->progressAdvance(); + } + $io1->progressFinish(); + } + $section2->clear(); + $io2->success('Done'); + + return Command::SUCCESS; + } +} diff --git a/cli/common/Command/Messages/Grab.php b/cli/common/Command/Messages/Grab.php new file mode 100644 index 0000000..1be0fd0 --- /dev/null +++ b/cli/common/Command/Messages/Grab.php @@ -0,0 +1,41 @@ +addArgument('mailbox_id', InputArgument::REQUIRED, 'Mailbox ID to grab emails'); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $mailbox_id = $input->getArgument('mailbox_id'); + $io->title("Grabbing Messages for Mailbox ID {$mailbox_id}"); + $io->section('Grabbing Messages'); + $count = $this->service->grabMessages($mailbox_id); + $io->info("Found {$count} messages"); + $io->success('Done.'); + + return Command::SUCCESS; + } +} diff --git a/cli/common/Exception/Response/EmptyResponse.php b/cli/common/Exception/Response/EmptyResponse.php new file mode 100644 index 0000000..5fff71f --- /dev/null +++ b/cli/common/Exception/Response/EmptyResponse.php @@ -0,0 +1,16 @@ +logger->info('Finding all downloaded attachment files'); + $folder = '/attachments'; + $files = new \FilesystemIterator($folder); + foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + yield $file->getRealPath(); + } + } + public function getAll(): array + { + if (!isset($this->attachments)) { + $this->logger->info('Grabbing all attachments'); + $response = $this->communicator->get('/attachments'); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + $this->attachments = []; + return $this->attachments; + } + $this->attachments = json_decode($body)->attachments; + } + return $this->attachments; + } + public function get(int $attachment_id): object + { + $this->logger->info("Getting attachment {$attachment_id}"); + $uri = "/attachment/{$attachment_id}"; + $response = $this->communicator->get($uri); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + throw new EmptyResponse($uri); + } + $json = json_decode($body); + if (!isset($json->attachment)) { + throw new MissingResponse('attachment'); + } + return $json->attachment; + } + public function find(string $filename): int + { + $this->logger->info("Finding attachment {$filename}"); + foreach ($this->getAll() as $attachment) { + if ($attachment->fullfilename === $filename) { + return $attachment->id; + } + } + throw new \Exception("{$filename} is not in the database"); + } + public function isEncrypted(string $filename): bool + { + if (!file_exists($filename)) { + throw new \InvalidArgumentException("File not found {$filename}"); + } + $escaped_filename = escapeshellarg($filename); + $cmd = "{$this->base_command} --is-encrypted {$escaped_filename}"; + exec($cmd, $output, $retcode); + return $retcode == 0; + } + public function scheduleDecrypt(int $attachment_id): bool + { + $this->logger->info("Scheduling decryption of attachment {$attachment_id}"); + $uri = "/attachment/{$attachment_id}/decrypt"; + $response = $this->communicator->get($uri); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + throw new EmptyResponse($uri); + } + $json = json_decode($body); + if (!isset($json->status)) { + throw new MissingResponse('status'); + } + return $json->status; + } + public function decrypt(string $basename): bool + { + $this->logger->info("Decrypting {$basename}"); + $in_filename = implode('/', ['attachments', $basename]); + $out_filename = implode('/', ['attachments', 'decrypted', $basename]); + if (file_exists($out_filename)) { + throw new \Exception("{$basename} already decrypted"); + } + foreach ($this->passwords as $password) { + $cmd = $this->base_command . ' -password=' . escapeshellarg($password) . ' -decrypt ' . escapeshellarg($in_filename) . ' ' . escapeshellarg($out_filename); + exec($cmd, $output, $retcode); + $success = $retcode == 0; + if ($success) { + return true; + } + if (file_exists($out_filename)) { + unlink($out_filename); + } + unset($output); + } + return false; + } + +} diff --git a/cli/common/Service/Communicator.php b/cli/common/Service/Communicator.php new file mode 100644 index 0000000..c655079 --- /dev/null +++ b/cli/common/Service/Communicator.php @@ -0,0 +1,76 @@ +getStatusCode() < 200 or $response->getStatusCode() >= 300) { + throw new HttpResponseException($response->getStatusCode(), $response->getReasonPhrase()); + } + return $response; + } + + /** + * @throws HttpResponseException + * @throws JsonException + */ + protected function request(string $method, string $uri, ?array $body = null): ResponseInterface + { + $options = []; + if ($body !== null) { + $options['headers'] = [ + 'Content-Type' => 'application/json' + ]; + $options['body'] = json_encode($body); + } + return $this->handleResponse($this->client->request($method, $uri, $options)); + } + + /** + * @throws HttpResponseException + * @throws JsonException + */ + public function get(string $uri): ResponseInterface + { + return $this->request('get', $uri); + } + + /** + * @throws HttpResponseException + * @throws JsonException + */ + public function post(string $uri, array $data): ResponseInterface + { + return $this->request('post', $uri, $data); + } + + /** + * @throws HttpResponseException + * @throws JsonException + */ + public function put(string $uri, array $data): ResponseInterface + { + return $this->request('put', $uri, $data); + } + + /** + * @throws HttpResponseException + * @throws JsonException + */ + public function delete(string $uri, array $data): ResponseInterface + { + return $this->request('delete', $uri, $data); + } +} diff --git a/cli/common/Service/Jobs.php b/cli/common/Service/Jobs.php new file mode 100644 index 0000000..d3543ef --- /dev/null +++ b/cli/common/Service/Jobs.php @@ -0,0 +1,71 @@ +logger->info('Getting pending jobs'); + $response = $this->communicator->get('/jobs/pending'); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + return []; + } + $json = json_decode($body); + if (!isset($json->jobs)) { + return []; + } + return $json->jobs; + } + public function get(int $job_id): object + { + $this->logger->info("Getting Job {$job_id}"); + $uri = "/job/{$job_id}"; + return $this->send($uri, 'job'); + } + public function run(object $job): bool + { + $this->logger->debug("Running Job {$job->id}"); + $base_command = '/app/bin/emails'; + $cmd = [$base_command, $job->command]; + if ($job->arguments !== '') { + $cmd []= $job->arguments; + } + $cmd = implode(' ', $cmd); + $response = shell_exec($cmd); + if ($response !== false) { + return $this->finished($job->id); + } + return $this->failure($job->id); + } + + protected function finished(int $job_id): bool + { + $uri = "/job/{$job_id}/finish"; + return $this->send($uri, 'status'); + } + protected function failure(int $job_id): bool + { + $uri = "/job/{$job_id}/failed"; + return $this->send($uri, 'status'); + } + protected function send(string $uri, string $param): mixed + { + $response = $this->communicator->get($uri); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + throw new EmptyResponse($uri); + } + $json = json_decode($body); + if (!isset($json->{$param})) { + throw new MissingResponse($param); + } + return $json->{$param}; + } +} diff --git a/cli/common/Service/Mailboxes.php b/cli/common/Service/Mailboxes.php new file mode 100644 index 0000000..4d8eabf --- /dev/null +++ b/cli/common/Service/Mailboxes.php @@ -0,0 +1,61 @@ +logger->info('Getting all registered mailboxes'); + $response = $this->communicator->get('/mailboxes/registered'); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + return []; + } + $json = json_decode($body); + if (!isset($json->mailboxes)) { + return []; + } + return $json->mailboxes; + } + public function check(object $mailbox): bool + { + $this->logger->info("Checking mailbox {$mailbox->id}"); + if ((new DateTimeImmutable())->diff(new DateTimeImmutable($mailbox->last_checked->date->date))->days < $this->min_check_days) { + return true; + } + $uri = "/mailbox/{$mailbox->id}/check"; + $response = $this->communicator->get($uri); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + throw new EmptyResponse($uri); + } + $json = json_decode($body); + if (!isset($json->status)) { + throw new MissingResponse('status'); + } + return $json->status; + } + public function grabMessages(int $mailbox_id): int + { + $this->logger->info("Grabbing messages for {$mailbox_id}"); + $uri = "/mailbox/{$mailbox_id}/messages/grab"; + $response = $this->communicator->get($uri); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + return 0; + } + $json = json_decode($body); + if (!isset($json->count)) { + return 0; + } + return $json->count; + } +} diff --git a/cli/common/Service/Messages.php b/cli/common/Service/Messages.php new file mode 100644 index 0000000..3854dbd --- /dev/null +++ b/cli/common/Service/Messages.php @@ -0,0 +1,43 @@ +logger->info("Getting message {$message_id}"); + $uri = "/message/{$message_id}"; + $response = $this->communicator->get($uri); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + throw new EmptyResponse($uri); + } + $json = json_decode($body); + if (!isset($json->message)) { + throw new MissingResponse('message'); + } + return $json->message; + } + public function grabAttachments(string $message_uid): int + { + $this->logger->info("Grabbing attachments for message UID {$message_uid}"); + $uri = '/attachments/grab'; + $response = $this->communicator->put($uri, ['messages' => [$message_uid]]); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + return 0; + } + $json = json_decode($body); + if (!isset($json->total)) { + return 0; + } + return $json->total; + } +} diff --git a/cli/common/Wrapper/Application.php b/cli/common/Wrapper/Application.php new file mode 100644 index 0000000..7ffac08 --- /dev/null +++ b/cli/common/Wrapper/Application.php @@ -0,0 +1,24 @@ +setContainer($container); + parent::__construct($name, $version); + } + + protected ContainerInterface $container; + public function getContainer(): ContainerInterface + { + return $this->container; + } + public function setContainer(ContainerInterface $container): Application + { + $this->container = $container; + return $this; + } +} diff --git a/cli/composer.json b/cli/composer.json new file mode 100644 index 0000000..4365fcb --- /dev/null +++ b/cli/composer.json @@ -0,0 +1,30 @@ +{ + "name": "provm/emails", + "type": "project", + "require": { + "guzzlehttp/guzzle": "^7.5", + "monolog/monolog": "^3.2", + "php-di/php-di": "^6.4", + "symfony/console": "^6.1", + "thecodingmachine/safe": "^2.4" + }, + "require-dev": { + "kint-php/kint": "^4.2", + "mockery/mockery": "^1.5", + "phpunit/phpunit": "^9.5" + }, + "autoload": { + "psr-4": { + "ProVM\\": "common/" + } + }, + "authors": [ + { + "name": "Aldarien", + "email": "aldarien85@gmail.com" + } + ], + "config": { + "sort-packages": true + } +} diff --git a/cli/crontab b/cli/crontab new file mode 100644 index 0000000..b567b58 --- /dev/null +++ b/cli/crontab @@ -0,0 +1,10 @@ +# minutes hour day_of_month month day_of_week command +#0 2 * * 2-6 /app/bin/emails messages:grab >> /logs/messages.log +#0 3 * * 2-6 /app/bin/emails attachments:grab >> /logs/attachments.log + +# Pending jobs every minute +* * * * * /app/bin/emails jobs:check >> /logs/jobs.log +# Check mailboxes for new emails every weekday +0 0 * * 2-6 /app/bin/emails mailboxes:check >> /logs/mailboxes.log +# Check attachments every weekday +0 1 * * 2-6 /app/bin/emails attachments:check >> /logs/attachments.log diff --git a/cli/docker-compose.yml b/cli/docker-compose.yml new file mode 100644 index 0000000..e86b638 --- /dev/null +++ b/cli/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3' +services: + cli: + profiles: + - cli + container_name: emails-cli + build: + context: ${CLI_PATH:-.} + restart: unless-stopped + env_file: + - ${CLI_PATH:-.}/.env + - .key.env + volumes: + - ${CLI_PATH:-.}/:/app + - ${CLI_PATH}/crontab:/var/spool/cron/crontabs/root + - ${LOGS_PATH}/cli:/logs + - ${ATT_PATH}:/attachments diff --git a/cli/public/index.php b/cli/public/index.php new file mode 100644 index 0000000..f8b2dac --- /dev/null +++ b/cli/public/index.php @@ -0,0 +1,14 @@ +getContainer()->get(Psr\Log\LoggerInterface::class)); +try { + $app->run(); +} catch (Error $e) { + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->error($e); +} catch (Exception $e) { + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->warning($e); +} diff --git a/cli/resources/commands/01_mailboxes.php b/cli/resources/commands/01_mailboxes.php new file mode 100644 index 0000000..843e8e4 --- /dev/null +++ b/cli/resources/commands/01_mailboxes.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(ProVM\Command\Mailboxes\Check::class)); diff --git a/cli/resources/commands/01_messages.php b/cli/resources/commands/01_messages.php new file mode 100644 index 0000000..4313973 --- /dev/null +++ b/cli/resources/commands/01_messages.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(ProVM\Command\Messages\Grab::class)); diff --git a/cli/resources/commands/02_attachments.php b/cli/resources/commands/02_attachments.php new file mode 100644 index 0000000..d46dbc1 --- /dev/null +++ b/cli/resources/commands/02_attachments.php @@ -0,0 +1,4 @@ +add($app->getContainer()->get(ProVM\Command\Attachments\Check::class)); +$app->add($app->getContainer()->get(ProVM\Command\Attachments\Grab::class)); +$app->add($app->getContainer()->get(ProVM\Command\Attachments\Decrypt::class)); diff --git a/cli/resources/commands/03_jobs.php b/cli/resources/commands/03_jobs.php new file mode 100644 index 0000000..ea36967 --- /dev/null +++ b/cli/resources/commands/03_jobs.php @@ -0,0 +1,3 @@ +add($app->getContainer()->get(ProVM\Command\Jobs\Check::class)); +$app->add($app->getContainer()->get(ProVM\Command\Jobs\Execute::class)); diff --git a/cli/setup/app.php b/cli/setup/app.php new file mode 100644 index 0000000..d00dbc6 --- /dev/null +++ b/cli/setup/app.php @@ -0,0 +1,43 @@ +isDir()) { + continue; + } + $builder->addDefinitions($file->getRealPath()); + } +} + +$app = new ProVM\Wrapper\Application($builder->build()); + +$folder = implode(DIRECTORY_SEPARATOR, [ + __DIR__, + 'middleware' +]); +if (file_exists($folder)) { + $files = new FilesystemIterator($folder); + foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + include_once $file->getRealPath(); + } +} + +return $app; diff --git a/cli/setup/composer.php b/cli/setup/composer.php new file mode 100644 index 0000000..f735a96 --- /dev/null +++ b/cli/setup/composer.php @@ -0,0 +1,6 @@ +getContainer()->get('commands_folder'); +$files = new FilesystemIterator($folder); +foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + include_once $file->getRealPath(); +} diff --git a/cli/setup/settings/01_env.php b/cli/setup/settings/01_env.php new file mode 100644 index 0000000..76b4051 --- /dev/null +++ b/cli/setup/settings/01_env.php @@ -0,0 +1,10 @@ + $_ENV['API_URI'], + 'api_key' => sha1($_ENV['API_KEY']), + 'base_command' => 'qpdf', + 'passwords' => function() { + return explode($_ENV['PASSWORDS_SEPARATOR'] ?? ',', $_ENV['PASSWORDS'] ?? ''); + }, + 'min_check_days' => 1 +]; diff --git a/cli/setup/settings/02_folders.php b/cli/setup/settings/02_folders.php new file mode 100644 index 0000000..fcafa9b --- /dev/null +++ b/cli/setup/settings/02_folders.php @@ -0,0 +1,19 @@ + dirname(__FILE__, 3), + 'resources_folder' => function(ContainerInterface $container) { + return implode(DIRECTORY_SEPARATOR, [ + $container->get('base_folder'), + 'resources' + ]); + }, + 'commands_folder' => function(ContainerInterface $container) { + return implode(DIRECTORY_SEPARATOR, [ + $container->get('resources_folder'), + 'commands' + ]); + }, + 'logs_folder' => '/logs', +]; diff --git a/cli/setup/settings/98_log.php b/cli/setup/settings/98_log.php new file mode 100644 index 0000000..55328ea --- /dev/null +++ b/cli/setup/settings/98_log.php @@ -0,0 +1,4 @@ + '/logs/php.log' +]; \ No newline at end of file diff --git a/cli/setup/setups/02_api.php b/cli/setup/setups/02_api.php new file mode 100644 index 0000000..bf102d3 --- /dev/null +++ b/cli/setup/setups/02_api.php @@ -0,0 +1,13 @@ + function(ContainerInterface $container) { + return new GuzzleHttp\Client([ + 'base_uri' => $container->get('api_uri'), + 'headers' => [ + 'Authorization' => "Bearer {$container->get('api_key')}" + ] + ]); + } +]; diff --git a/cli/setup/setups/03_middleware.php b/cli/setup/setups/03_middleware.php new file mode 100644 index 0000000..7d872ab --- /dev/null +++ b/cli/setup/setups/03_middleware.php @@ -0,0 +1,8 @@ + function(ContainerInterface $container) { + return new ProVM\Middleware\Logging($container->get('request_logger')); + } +]; diff --git a/cli/setup/setups/04_commands.php b/cli/setup/setups/04_commands.php new file mode 100644 index 0000000..23fd362 --- /dev/null +++ b/cli/setup/setups/04_commands.php @@ -0,0 +1,21 @@ + function(ContainerInterface $container) { + return new ProVM\Service\Mailboxes( + $container->get(ProVM\Service\Communicator::class), + $container->get(Psr\Log\LoggerInterface::class), + $container->get('min_check_days') + ); + }, + ProVM\Service\Attachments::class => function(ContainerInterface $container) { + return new ProVM\Service\Attachments( + $container->get(ProVM\Service\Communicator::class), + $container->get(Psr\Log\LoggerInterface::class), + $container->get('passwords'), + $container->get('base_command') + ); + }, +]; diff --git a/cli/setup/setups/98_log.php b/cli/setup/setups/98_log.php new file mode 100644 index 0000000..ef4a697 --- /dev/null +++ b/cli/setup/setups/98_log.php @@ -0,0 +1,50 @@ + function(ContainerInterface $container) { + return [ + $container->get(Monolog\Processor\PsrLogMessageProcessor::class), + $container->get(Monolog\Processor\IntrospectionProcessor::class), + $container->get(Monolog\Processor\MemoryPeakUsageProcessor::class), + ]; + }, + 'request_log_handler' => function(ContainerInterface $container) { + return (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log']))); + }, + 'request_logger' => function(ContainerInterface $container) { + return new Monolog\Logger( + 'request_logger', + [$container->get('request_log_handler')], + $container->get('log_processors') + ); + }, + 'file_log_handler' => function(ContainerInterface $container) { + return new Monolog\Handler\FilterHandler( + (new Monolog\Handler\RotatingFileHandler($container->get('log_file'))) + ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true)), + Monolog\Level::Error + ); + }, + 'debug_log_handler' => function(ContainerInterface $container) { + return new Monolog\Handler\FilterHandler( + (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'debug.log']))) + ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true)), + Monolog\Level::Debug, + Monolog\Level::Warning + ); + }, + Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { + return $container->get('file_logger'); + }, + 'file_logger' => function(ContainerInterface $container) { + return new Monolog\Logger( + 'file', + [ + $container->get('file_log_handler'), + $container->get('debug_log_handler') + ], + $container->get('log_processors') + ); + }, +]; diff --git a/default.conf b/default.conf new file mode 100644 index 0000000..992ca22 --- /dev/null +++ b/default.conf @@ -0,0 +1,45 @@ +# server { +# listen 80; +# listen [::]:80; +# server_name localhost; +# +# #access_log /var/log/nginx/host.access.log main; +# +# location / { +# root /usr/share/nginx/html; +# index index.html index.htm; +# } +# +# #error_page 404 /404.html; +# +# # redirect server error pages to the static page /50x.html +# # +# error_page 500 502 503 504 /50x.html; +# location = /50x.html { +# root /usr/share/nginx/html; +# } +# +# # proxy the PHP scripts to Apache listening on 127.0.0.1:80 +# # +# #location ~ \.php$ { +# # proxy_pass http://127.0.0.1; +# #} +# +# # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 +# # +# #location ~ \.php$ { +# # root html; +# # fastcgi_pass 127.0.0.1:9000; +# # fastcgi_index index.php; +# # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; +# # include fastcgi_params; +# #} +# +# # deny access to .htaccess files, if Apache's document root +# # concurs with nginx's one +# # +# #location ~ /\.ht { +# # deny all; +# #} +# } + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ac15336 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3' + +services: + proxy: + container_name: emails-proxy + image: nginx + restart: unless-stopped + volumes: + - ./default.conf:/etc/nginx/conf.d/default.conf + - ${LOGS_PATH}/proxy:/var/logs/nginx + ports: + - "${WEB_PORT:-80}:80" diff --git a/ui/.env.sample b/ui/.env.sample new file mode 100644 index 0000000..662913a --- /dev/null +++ b/ui/.env.sample @@ -0,0 +1 @@ +API_URI=http://proxy:8080 \ No newline at end of file diff --git a/ui/Dockerfile b/ui/Dockerfile new file mode 100644 index 0000000..ae2d301 --- /dev/null +++ b/ui/Dockerfile @@ -0,0 +1,8 @@ +FROM php:8-fpm + +RUN apt-get update \ + && apt-get install -y libc-client-dev libkrb5-dev git libzip-dev unzip \ + && rm -r /var/lib/apt/lists/* \ + && docker-php-ext-install zip + +WORKDIR /app/ui diff --git a/ui/common/Controller/Api.php b/ui/common/Controller/Api.php new file mode 100644 index 0000000..7efcdc8 --- /dev/null +++ b/ui/common/Controller/Api.php @@ -0,0 +1,16 @@ +getParsedBody(); + return $service->sendRequest($json); + } +} diff --git a/ui/common/Controller/Attachments.php b/ui/common/Controller/Attachments.php new file mode 100644 index 0000000..70c01df --- /dev/null +++ b/ui/common/Controller/Attachments.php @@ -0,0 +1,21 @@ +get("/attachment/{$attachment_id}", [ + 'stream' => true, + 'sink' => \Safe\fopen('php://stdout', 'wb') + ]); + $response->withHeader('Content-Type', 'application/pdf'); + $response->getBody()->write($rs->getBody()->getContents()); + return $response; + } +} \ No newline at end of file diff --git a/ui/common/Controller/Emails.php b/ui/common/Controller/Emails.php new file mode 100644 index 0000000..dcbb11c --- /dev/null +++ b/ui/common/Controller/Emails.php @@ -0,0 +1,22 @@ +render($response, 'emails.mailboxes'); + } + public function messages(ServerRequestInterface $request, ResponseInterface $response, View $view, int $mailbox_id): ResponseInterface + { + return $view->render($response, 'emails.messages', compact('mailbox_id')); + } + public function show(ServerRequestInterface $request, ResponseInterface $response, View $view, int $message_id): ResponseInterface + { + return $view->render($response, 'emails.show', compact('message_id')); + } +} \ No newline at end of file diff --git a/ui/common/Controller/Home.php b/ui/common/Controller/Home.php new file mode 100644 index 0000000..903d545 --- /dev/null +++ b/ui/common/Controller/Home.php @@ -0,0 +1,14 @@ +render($response, 'home'); + } +} \ No newline at end of file diff --git a/ui/common/Controller/Jobs.php b/ui/common/Controller/Jobs.php new file mode 100644 index 0000000..091539f --- /dev/null +++ b/ui/common/Controller/Jobs.php @@ -0,0 +1,14 @@ +render($response, 'jobs.list'); + } +} diff --git a/ui/common/Middleware/Logging.php b/ui/common/Middleware/Logging.php new file mode 100644 index 0000000..a1e97a0 --- /dev/null +++ b/ui/common/Middleware/Logging.php @@ -0,0 +1,38 @@ +setLogger($logger); + } + + protected LoggerInterface $logger; + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function setLogger(LoggerInterface $logger): Logging + { + $this->logger = $logger; + return $this; + } + + public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $response = $handler->handle($request); + $output = [ + 'uri' => print_r($request->getUri(), true), + 'body' => $request->getParsedBody() + ]; + $this->getLogger()->info(\Safe\json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return $response; + } +} diff --git a/ui/common/Service/Api.php b/ui/common/Service/Api.php new file mode 100644 index 0000000..0b56b92 --- /dev/null +++ b/ui/common/Service/Api.php @@ -0,0 +1,47 @@ +setClient($client) + ->setFactory($factory); + } + + protected ClientInterface $client; + protected RequestFactoryInterface $factory; + + public function getClient(): ClientInterface + { + return $this->client; + } + public function getFactory(): RequestFactoryInterface + { + return $this->factory; + } + public function setClient(ClientInterface $client): Api + { + $this->client = $client; + return $this; + } + public function setFactory(RequestFactoryInterface $factory): Api + { + $this->factory = $factory; + return $this; + } + + public function sendRequest(array $request_data): ResponseInterface + { + $request = $this->getFactory()->createRequest(strtoupper($request_data['method']) ?? 'GET', $request_data['uri']); + if (strtolower($request_data['method']) !== 'get') { + $request->getBody()->write(\Safe\json_encode($request_data['data'])); + $request->withHeader('Content-Type', 'application/json'); + } + return $this->getClient()->sendRequest($request); + } +} diff --git a/ui/composer.json b/ui/composer.json new file mode 100644 index 0000000..aefa104 --- /dev/null +++ b/ui/composer.json @@ -0,0 +1,32 @@ +{ + "name": "provm/emails_ui", + "type": "project", + "require": { + "berrnd/slim-blade-view": "^1.0", + "guzzlehttp/guzzle": "^7.5", + "monolog/monolog": "^3.2", + "nyholm/psr7": "^1.5", + "nyholm/psr7-server": "^1.0", + "php-di/slim-bridge": "^3.2", + "thecodingmachine/safe": "^2.4", + "zeuxisoo/slim-whoops": "^0.7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "kint-php/kint": "^4.2" + }, + "authors": [ + { + "name": "Aldarien", + "email": "aldarien85@gmail.com" + } + ], + "autoload": { + "psr-4": { + "ProVM\\Common\\": "common/" + } + }, + "config": { + "sort-packages": true + } +} diff --git a/ui/docker-compose.yml b/ui/docker-compose.yml new file mode 100644 index 0000000..8d1150f --- /dev/null +++ b/ui/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3' +services: + proxy: + profiles: + - ui + volumes: + - ${UI_PATH:-.}:/app/ui + - ${UI_PATH:-.}/nginx.conf:/etc/nginx/conf.d/ui.conf + ui: + profiles: + - ui + container_name: emails-ui + build: + context: ${UI_PATH:-.} + restart: unless-stopped + env_file: + - ${UI_PATH:-.}/.env + - .key.env + volumes: + - ${UI_PATH:-.}/:/app/ui + - ${LOGS_PATH}/ui:/logs diff --git a/ui/nginx.conf b/ui/nginx.conf new file mode 100644 index 0000000..43895cc --- /dev/null +++ b/ui/nginx.conf @@ -0,0 +1,22 @@ +server { + listen 0.0.0.0:80; + root /app/ui/public; + index index.php index.html index.htm; + + access_log /var/logs/nginx/ui.access.log; + error_log /var/logs/nginx/ui.error.log; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass ui:9000; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } +} diff --git a/ui/public/index.php b/ui/public/index.php new file mode 100644 index 0000000..3408a32 --- /dev/null +++ b/ui/public/index.php @@ -0,0 +1,12 @@ +getContainer()->get(Psr\Log\LoggerInterface::class)); +try { + $app->run(); +} catch (Error | Exception $e) { + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->error($e); +} diff --git a/ui/resources/routes/01_emails.php b/ui/resources/routes/01_emails.php new file mode 100644 index 0000000..e8addbd --- /dev/null +++ b/ui/resources/routes/01_emails.php @@ -0,0 +1,13 @@ +group('/emails', function($app) { + $app->group('/mailbox/{mailbox_id}', function($app) { + $app->get('[/]', [Emails::class, 'messages']); + }); + $app->group('/message/{message_id}', function($app) { + $app->get('[/]', [Emails::class, 'show']); + }); + $app->get('/mailboxes', Emails::class); + $app->get('[/]', Emails::class); +}); diff --git a/ui/resources/routes/02_attachments.php b/ui/resources/routes/02_attachments.php new file mode 100644 index 0000000..c95452a --- /dev/null +++ b/ui/resources/routes/02_attachments.php @@ -0,0 +1,6 @@ +group('/attachment/{attachment_id}', function($app) { + $app->get('[/]', [Attachments::class, 'get']); +}); \ No newline at end of file diff --git a/ui/resources/routes/03_jobs.php b/ui/resources/routes/03_jobs.php new file mode 100644 index 0000000..f9d8f0a --- /dev/null +++ b/ui/resources/routes/03_jobs.php @@ -0,0 +1,4 @@ +get('/jobs', Jobs::class); diff --git a/ui/resources/routes/98_api.php b/ui/resources/routes/98_api.php new file mode 100644 index 0000000..26bf2d3 --- /dev/null +++ b/ui/resources/routes/98_api.php @@ -0,0 +1,4 @@ +post('/api[/]', Api::class); diff --git a/ui/resources/routes/99_home.php b/ui/resources/routes/99_home.php new file mode 100644 index 0000000..b378b41 --- /dev/null +++ b/ui/resources/routes/99_home.php @@ -0,0 +1,4 @@ +get('[/]', Home::class); diff --git a/ui/resources/views/emails/base.blade.php b/ui/resources/views/emails/base.blade.php new file mode 100644 index 0000000..f4e4d40 --- /dev/null +++ b/ui/resources/views/emails/base.blade.php @@ -0,0 +1,14 @@ +@extends('layout.base') + +@section('page_title') + Emails + @hasSection('emails_title') + - + @yield('emails_title') + @endif +@endsection + +@section('page_content') +

Emails

+ @yield('emails_content') +@endsection \ No newline at end of file diff --git a/ui/resources/views/emails/mailboxes.blade.php b/ui/resources/views/emails/mailboxes.blade.php new file mode 100644 index 0000000..3081487 --- /dev/null +++ b/ui/resources/views/emails/mailboxes.blade.php @@ -0,0 +1,166 @@ +@extends('emails.base') + +@section('emails_content') +

Mailboxes

+
+@endsection + +@push('page_scripts') + +@endpush \ No newline at end of file diff --git a/ui/resources/views/emails/messages.blade.php b/ui/resources/views/emails/messages.blade.php new file mode 100644 index 0000000..089eaee --- /dev/null +++ b/ui/resources/views/emails/messages.blade.php @@ -0,0 +1,379 @@ +@extends('emails.base') + +@section('emails_content') +

Messages -

+
+@endsection + +@push('page_styles') + +@endpush + +@push('page_scripts') + + +@endpush diff --git a/ui/resources/views/emails/show.blade.php b/ui/resources/views/emails/show.blade.php new file mode 100644 index 0000000..8a037d6 --- /dev/null +++ b/ui/resources/views/emails/show.blade.php @@ -0,0 +1,103 @@ +@extends('emails.base') + +@section('emails_content') +

Message - -

+

+ +@endsection + +@push('page_scripts') + +@endpush diff --git a/ui/resources/views/home.blade.php b/ui/resources/views/home.blade.php new file mode 100644 index 0000000..55da21e --- /dev/null +++ b/ui/resources/views/home.blade.php @@ -0,0 +1,78 @@ +@extends('layout.base') + +@section('page_content') +

Registered Mailboxes

+
+@endsection + +@push('page_scripts') + +@endpush diff --git a/ui/resources/views/jobs/base.blade.php b/ui/resources/views/jobs/base.blade.php new file mode 100644 index 0000000..5fec346 --- /dev/null +++ b/ui/resources/views/jobs/base.blade.php @@ -0,0 +1,14 @@ +@extends('layout.base') + +@section('page_title') + Jobs + @hasSection('jobs_title') + - + @yield('jobs_title') + @endif +@endsection + +@section('page_content') +

Jobs

+ @yield('jobs_content') +@endsection diff --git a/ui/resources/views/jobs/list.blade.php b/ui/resources/views/jobs/list.blade.php new file mode 100644 index 0000000..0e909d4 --- /dev/null +++ b/ui/resources/views/jobs/list.blade.php @@ -0,0 +1,165 @@ +@extends('jobs.base') + +@section('jobs_content') +
+@endsection + +@push('page_styles') + +@endpush + +@push('page_scripts') + + +@endpush diff --git a/ui/resources/views/layout/base.blade.php b/ui/resources/views/layout/base.blade.php new file mode 100644 index 0000000..fb96b37 --- /dev/null +++ b/ui/resources/views/layout/base.blade.php @@ -0,0 +1,5 @@ + + +@include('layout.head') +@include('layout.body') + \ No newline at end of file diff --git a/ui/resources/views/layout/body.blade.php b/ui/resources/views/layout/body.blade.php new file mode 100644 index 0000000..3bf15cc --- /dev/null +++ b/ui/resources/views/layout/body.blade.php @@ -0,0 +1,7 @@ + +@include('layout.body.header') +
+ @yield('page_content') +
+@include('layout.body.footer') + \ No newline at end of file diff --git a/ui/resources/views/layout/body/footer.blade.php b/ui/resources/views/layout/body/footer.blade.php new file mode 100644 index 0000000..3dc6d22 --- /dev/null +++ b/ui/resources/views/layout/body/footer.blade.php @@ -0,0 +1,4 @@ + + +@include('layout.body.footer.scripts') \ No newline at end of file diff --git a/ui/resources/views/layout/body/footer/scripts.blade.php b/ui/resources/views/layout/body/footer/scripts.blade.php new file mode 100644 index 0000000..108afea --- /dev/null +++ b/ui/resources/views/layout/body/footer/scripts.blade.php @@ -0,0 +1,6 @@ + + + +@include('layout.body.footer.scripts.main') + +@stack('page_scripts') \ No newline at end of file diff --git a/ui/resources/views/layout/body/footer/scripts/main.blade.php b/ui/resources/views/layout/body/footer/scripts/main.blade.php new file mode 100644 index 0000000..e9327cb --- /dev/null +++ b/ui/resources/views/layout/body/footer/scripts/main.blade.php @@ -0,0 +1,33 @@ + diff --git a/ui/resources/views/layout/body/header.blade.php b/ui/resources/views/layout/body/header.blade.php new file mode 100644 index 0000000..42dc14f --- /dev/null +++ b/ui/resources/views/layout/body/header.blade.php @@ -0,0 +1,5 @@ +
+
+ @include('layout.body.header.navbar') +
+
\ No newline at end of file diff --git a/ui/resources/views/layout/body/header/navbar.blade.php b/ui/resources/views/layout/body/header/navbar.blade.php new file mode 100644 index 0000000..a0a622f --- /dev/null +++ b/ui/resources/views/layout/body/header/navbar.blade.php @@ -0,0 +1,6 @@ + +
diff --git a/ui/resources/views/layout/head.blade.php b/ui/resources/views/layout/head.blade.php new file mode 100644 index 0000000..50bfaa0 --- /dev/null +++ b/ui/resources/views/layout/head.blade.php @@ -0,0 +1,11 @@ + + + + Emails + @hasSection('page_title') + - + @yield('page_title') + @endif + + @include('layout.head.styles') + \ No newline at end of file diff --git a/ui/resources/views/layout/head/styles.blade.php b/ui/resources/views/layout/head/styles.blade.php new file mode 100644 index 0000000..8b8ae7f --- /dev/null +++ b/ui/resources/views/layout/head/styles.blade.php @@ -0,0 +1,3 @@ + + +@stack('page_styles') \ No newline at end of file diff --git a/ui/setup/app.php b/ui/setup/app.php new file mode 100644 index 0000000..7367296 --- /dev/null +++ b/ui/setup/app.php @@ -0,0 +1,43 @@ +isDir()) { + continue; + } + $builder->addDefinitions($file->getRealPath()); + } +} +$app = \DI\Bridge\Slim\Bridge::create($builder->build()); + +$folder = implode(DIRECTORY_SEPARATOR, [ + __DIR__, + 'middleware' +]); +if (file_exists($folder)) { + $files = new FilesystemIterator($folder); + foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + require_once $file->getRealPath(); + } +} + +return $app; diff --git a/ui/setup/composer.php b/ui/setup/composer.php new file mode 100644 index 0000000..b451f96 --- /dev/null +++ b/ui/setup/composer.php @@ -0,0 +1,6 @@ +addRoutingMiddleware(); + +$folder = $app->getContainer()->get('folders')->routes; +$files = new FilesystemIterator($folder); +foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + include_once $file->getRealPath(); +} diff --git a/ui/setup/middleware/98_log.php b/ui/setup/middleware/98_log.php new file mode 100644 index 0000000..4df6f6c --- /dev/null +++ b/ui/setup/middleware/98_log.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(ProVM\Common\Middleware\Logging::class)); diff --git a/ui/setup/middleware/99_errors.php b/ui/setup/middleware/99_errors.php new file mode 100644 index 0000000..29c59fa --- /dev/null +++ b/ui/setup/middleware/99_errors.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(Zeuxisoo\Whoops\Slim\WhoopsMiddleware::class)); diff --git a/ui/setup/settings/01_env.php b/ui/setup/settings/01_env.php new file mode 100644 index 0000000..b0640fb --- /dev/null +++ b/ui/setup/settings/01_env.php @@ -0,0 +1,4 @@ + sha1($_ENV['API_KEY']) +]; diff --git a/ui/setup/settings/02_folders.php b/ui/setup/settings/02_folders.php new file mode 100644 index 0000000..180321f --- /dev/null +++ b/ui/setup/settings/02_folders.php @@ -0,0 +1,26 @@ + function() { + $arr = ['base' => dirname(__FILE__, 3)]; + $arr['resources'] = implode(DIRECTORY_SEPARATOR, [ + $arr['base'], + 'resources' + ]); + $arr['routes'] = implode(DIRECTORY_SEPARATOR, [ + $arr['resources'], + 'routes' + ]); + $arr['views'] = implode(DIRECTORY_SEPARATOR, [ + $arr['resources'], + 'views' + ]); + $arr['cache'] = implode(DIRECTORY_SEPARATOR, [ + $arr['base'], + 'cache' + ]); + $arr['logs'] = '/logs'; + return (object) $arr; + } +]; diff --git a/ui/setup/settings/03_urls.php b/ui/setup/settings/03_urls.php new file mode 100644 index 0000000..11356f1 --- /dev/null +++ b/ui/setup/settings/03_urls.php @@ -0,0 +1,13 @@ + function() { + $arr = ['base' => $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST']]; + $arr['api'] = $_ENV['API_URI']; + return (object) $arr; + }, + 'view_urls' => function() { + $arr = ['base' => $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST']]; + $arr['api'] = implode('/', [$arr['base'], 'api']); + return (object) $arr; + }, +]; diff --git a/ui/setup/settings/98_log.php b/ui/setup/settings/98_log.php new file mode 100644 index 0000000..55328ea --- /dev/null +++ b/ui/setup/settings/98_log.php @@ -0,0 +1,4 @@ + '/logs/php.log' +]; \ No newline at end of file diff --git a/ui/setup/setups/02_api.php b/ui/setup/setups/02_api.php new file mode 100644 index 0000000..32def7b --- /dev/null +++ b/ui/setup/setups/02_api.php @@ -0,0 +1,18 @@ + function(ContainerInterface $container) { + return new GuzzleHttp\Client([ + 'base_uri' => $container->get('urls')->api, + 'headers' => [ + 'Authorization' => [ + "Bearer {$container->get('api_key')}" + ] + ] + ]); + }, + Psr\Http\Message\RequestFactoryInterface::class => function(ContainerInterface $container) { + return $container->get(Nyholm\Psr7\Factory\Psr17Factory::class); + }, +]; diff --git a/ui/setup/setups/03_views.php b/ui/setup/setups/03_views.php new file mode 100644 index 0000000..89251b7 --- /dev/null +++ b/ui/setup/setups/03_views.php @@ -0,0 +1,16 @@ + function(ContainerInterface $container) { + return new Slim\Views\Blade( + $container->get('folders')->views, + $container->get('folders')->cache, + null, + [ + 'urls' => $container->get('view_urls'), + 'api_key' => $container->get('api_key') + ] + ); + } +]; diff --git a/ui/setup/setups/04_middleware.php b/ui/setup/setups/04_middleware.php new file mode 100644 index 0000000..1d6cc44 --- /dev/null +++ b/ui/setup/setups/04_middleware.php @@ -0,0 +1,8 @@ + function(ContainerInterface $container) { + return new ProVM\Common\Middleware\Logging($container->get('request_logger')); + } +]; diff --git a/ui/setup/setups/98_log.php b/ui/setup/setups/98_log.php new file mode 100644 index 0000000..f12fc26 --- /dev/null +++ b/ui/setup/setups/98_log.php @@ -0,0 +1,38 @@ + function(ContainerInterface $container) { + return [ + $container->get(Monolog\Processor\PsrLogMessageProcessor::class), + $container->get(Monolog\Processor\WebProcessor::class), + $container->get(Monolog\Processor\IntrospectionProcessor::class), + $container->get(Monolog\Processor\MemoryPeakUsageProcessor::class) + ]; + }, + Monolog\Handler\RotatingFileHandler::class => function(ContainerInterface $container) { + $handler = new Monolog\Handler\RotatingFileHandler($container->get('log_file')); + $handler->setFormatter($container->get(Monolog\Formatter\SyslogFormatter::class)); + return $handler; + }, + 'request_logger' => function(ContainerInterface $container) { + return new Monolog\Logger('request_logger', [ + (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('folders')->logs, 'requests.log']))), + ], $container->get('log_processors')); + }, + Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { + return new Monolog\Logger('file', [ + new Monolog\Handler\FilterHandler( + (new Monolog\Handler\RotatingFileHandler($container->get('log_file'))) + ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true)), + Monolog\Level::Error + ), + new Monolog\Handler\FilterHandler( + (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('folders')->logs, 'debug.log']))) + ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true)), + Monolog\Level::Debug, + Monolog\Level::Warning + ) + ], $container->get('log_processors')); + }, +]; diff --git a/ui/setup/setups/99_errors.php b/ui/setup/setups/99_errors.php new file mode 100644 index 0000000..d79d93d --- /dev/null +++ b/ui/setup/setups/99_errors.php @@ -0,0 +1,8 @@ + function(ContainerInterface $container) { + return new \Zeuxisoo\Whoops\Slim\WhoopsMiddleware(); + } +]; \ No newline at end of file