diff --git a/api/common/Controller/Attachments.php b/api/common/Controller/Attachments.php new file mode 100644 index 0000000..e7dd760 --- /dev/null +++ b/api/common/Controller/Attachments.php @@ -0,0 +1,60 @@ +getRealPath(); + if (isset($attachment['decrypted_attachment']) and $attachment['decrypted_attachment'] !== false) { + $attachment['decrypted_attachment'] = $attachment['decrypted_attachment']->getRealPath(); + } + return $attachment; + }, $service->fetchFullAttachments()); + return $this->withJson($response, compact('attachments')); + } + protected function fileToArray(\SplFileInfo $info): array + { + return [ + 'filename' => $info->getFilename(), + 'path' => $info->getPath(), + 'full_name' => $info->getRealPath(), + 'extension' => $info->getExtension() + ]; + } + public function get(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $body = $request->getBody()->getContents(); + $json = json_decode($body); + if (!is_array($json)) { + $json = [$json]; + } + $output = ['input' => $json, 'attachments' => []]; + foreach ($json as $attachment) { + $output['attachments'] []= $this->fileToArray($service->getAttachment($json->attachment)); + } + return $this->withJson($response, $output); + } + public function decrypt(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $body = $request->getBody()->getContents(); + $json = json_decode($body); + if (!is_array($json)) { + $json = [$json]; + } + $output = ['input' => $json, 'attachments' => []]; + foreach ($json as $attachment) { + $output['attachments'] []= $this->fileToArray($service->removeEncryption($attachment)); + } + return $this->withJson($response, $output); + } +} \ No newline at end of file diff --git a/api/common/Controller/Emails.php b/api/common/Controller/Emails.php new file mode 100644 index 0000000..5137841 --- /dev/null +++ b/api/common/Controller/Emails.php @@ -0,0 +1,141 @@ +setLogger($logger); + } + + protected LoggerInterface $logger; + public function getLogger(): LoggerInterface + { + return $this->logger; + } + public function setLogger(LoggerInterface $logger): Emails + { + $this->logger = $logger; + return $this; + } + + public function mailboxes(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $mailboxes = array_map(function(MailboxInterface $mailbox) { + return ['name' => $mailbox->getName(), 'message_count' => $mailbox->count()]; + }, array_values($service->getMailboxes()->getAll())); + return $this->withJson($response, compact('mailboxes')); + } + public function messages(ServerRequestInterface $request, ResponseInterface $response, Service $service, Mailbox $repository): ResponseInterface + { + $body = $request->getBody()->getContents(); + $json = \Safe\json_decode($body); + $mailbox = $repository->fetchById($json->mailbox); + $remote_mailbox = $service->getMailboxes()->get($mailbox->getName()); + $messages = $service->getMessages($remote_mailbox, $json->start ?? 0, $json->amount ?? null); + $mails = []; + foreach ($messages as $message) { + $mails []= [ + 'position' => $message->getPosition(), + 'uid' => $message->getUID(), + 'subject' => $message->getSubject(), + 'date' => $message->getDateTime()->format('Y-m-d H:i:s'), + 'from' => $message->getFrom(), + 'has_attachments' => $message->hasAttachments(), + 'valid_attachments' => $message->hasValidAttachments(), + 'downloaded_attachments' => $message->hasDownloadedAttachments() + ]; + } + return $this->withJson($response, [ + 'input' => $json, + 'total' => $mailbox->count(), + 'messages' => $mails + ]); + } + public function withAttachments(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $body = $request->getBody()->getContents(); + $json = \Safe\json_decode($body); + $mailbox = $service->getMailboxes()->get($json->mailbox); + $messages = $service->getValidMessages($mailbox, $json->start ?? 0, $json->amount ?? null); + $mails = []; + foreach ($messages as $message) { + $mails []= [ + 'position' => $message->getPosition(), + 'uid' => $message->getUID(), + 'subject' => $message->getSubject(), + 'date' => $message->getDateTime()->format('Y-m-d H:i:s'), + 'from' => $message->getFrom(), + 'has_attachments' => $message->hasAttachments(), + 'valid_attachments' => $message->hasValidAttachments(), + 'downloaded_attachments' => $message->hasDownloadedAttachments() + ]; + } + return $this->withJson($response, [ + 'input' => $json, + 'total' => $service->getMailboxes()->countValid($mailbox), + 'messages' => $mails + ]); + } + public function attachments(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $body = $request->getBody()->getContents(); + $json = \Safe\json_decode($body); + $mailbox = $service->getMailboxes()->get($json->mailbox); + $messages = $service->getMessages($mailbox, $json->start ?? 0, $json->amount ?? null); + $cnt = 0; + $attachments = []; + foreach ($messages as $message) { + $attachments = array_merge($attachments, $service->saveAttachments($message, $json->extension ?? null)); + $cnt ++; + } + return $this->withJson($response, [ + 'input' => $json, + 'messages_count' => $cnt, + 'attachments_count' => count($attachments), + 'attachments' => $attachments + ]); + } + public function attachment(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + { + $body = $request->getBody()->getContents(); + $json = \Safe\json_decode($body); + $mailbox = $service->getMailbox($json->mailbox); + $cnt = 0; + $attachments = []; + $errors = []; + foreach ($json->messages as $uid) { + $message = $service->getMessage($mailbox, $uid); + try { + $attachments = array_merge($attachments, $service->saveAttachments($message, $json->extension ?? null)); + } catch (FilesystemException $e) { + $errors []= [ + 'message' => $uid, + 'error' => $e->getMessage() + ]; + } + $cnt ++; + } + $output = [ + 'input' => $json, + 'messages_count' => $cnt, + 'attachments_count' => count($attachments), + 'attachments' => $attachments + ]; + if (count($errors) > 0) { + $output['errors'] = $errors; + } + return $this->withJson($response, $output); + } +} diff --git a/api/common/Controller/Install.php b/api/common/Controller/Install.php new file mode 100644 index 0000000..238838b --- /dev/null +++ b/api/common/Controller/Install.php @@ -0,0 +1,28 @@ +run(); + return $this->withJson($response, [ + 'message' => 'Install finished' + ]); + } catch (Exception $e) { + return $this->withJson($response, [ + 'message' => 'Install with error', + 'error' => $e->getMessage() + ]); + } + } +} \ No newline at end of file diff --git a/api/common/Controller/Mailboxes.php b/api/common/Controller/Mailboxes.php new file mode 100644 index 0000000..d29dc26 --- /dev/null +++ b/api/common/Controller/Mailboxes.php @@ -0,0 +1,83 @@ +getAll(); + $mailboxes = []; + foreach ($source_mailboxes as $mailbox) { + $m = [ + 'name' => $mailbox->getName(), + 'registered' => false + ]; + try { + $s = $repository->fetchByName($mailbox->getName()); + $m['registered'] = true; + $m['id'] = $s->getId(); + } catch (BlankResult $e) { + } + $mailboxes []= $m; + } + return $this->withJson($response, compact('mailboxes')); + } + public function register(ServerRequestInterface $request, ResponseInterface $response, \ProVM\Common\Service\Mailboxes $service, Mailbox $repository): ResponseInterface + { + $body = $request->getBody(); + $json = \Safe\json_decode($body->getContents()); + $output = ['input' => $json, 'mailboxes' => []]; + foreach ($json->mailboxes as $mailbox) { + try { + $model = $repository->create([ + 'name' => $mailbox + ]); + $output['mailboxes'] []= ['name' => $mailbox, 'created' => true, 'id' => $model->getId()]; + } catch (\PDOException $e) { + $output['mailboxes'] []= ['name' => $mailbox, 'created' => false]; + } + } + return $this->withJson($response, $output); + } + public function unregister(ServerRequestInterface $request, ResponseInterface $response, Mailbox $repository): ResponseInterface + { + $body = $request->getBody(); + $json = \Safe\json_decode($body->getContents()); + $output = ['input' => $json, 'mailboxes' => []]; + foreach ($json->mailboxes as $id) { + try { + $mailbox = $repository->fetchById($id); + try { + $repository->delete($mailbox); + $output['mailboxes'] []= ['name' => $mailbox->getName(), 'id' => $id, 'removed' => true]; + } catch (\PDOException $e) { + $output['mailboxes'] []= ['name' => $mailbox->getName(), 'id' => $id, 'removed' => false]; + } + } catch (\PDOException | BlankResult $e) { + $output['mailboxes'] []= ['id' => $id, 'removed' => false]; + } + } + return $this->withJson($response, $output); + } + public function registered(ServerRequestInterface $request, ResponseInterface $response, Mailbox $repository): ResponseInterface + { + $data = $repository->fetchAll(); + $mailboxes = array_map(function(\ProVM\Emails\Model\Mailbox $mailbox) { + return $mailbox->toArray(); + }, $data); + return $this->withJson($response, compact('mailboxes')); + } + public function get(ServerRequestInterface $request, ResponseInterface $response, Mailbox $repository, int $mailbox_id): ResponseInterface + { + $mailbox = $repository->fetchById($mailbox_id); + return $this->withJson($response, ['mailbox' => $mailbox->toArray()]); + } +} \ 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..f4e2507 --- /dev/null +++ b/api/common/Controller/Messages.php @@ -0,0 +1,117 @@ +getBody(); + $json = \Safe\json_decode($body->getContents()); + $messages = []; + foreach ($json->mailboxes as $mailbox_id) { + $messages = array_merge($messages, $repository->fetchByMailbox($mailbox_id) ?? []); + } + $messages = array_map(function(\ProVM\Emails\Model\Message $message) { + return $message->toArray(); + }, $messages); + return $this->withJson($response, ['messages' => $messages, 'total' => count($messages)]); + } + public function valid(ServerRequestInterface $request, ResponseInterface $response, Message $repository): ResponseInterface + { + $body = $request->getBody(); + $json = \Safe\json_decode($body->getContents()); + $messages = []; + foreach ($json->mailboxes as $mailbox_id) { + try { + $messages = array_merge($messages, $repository->fetchByMailbox($mailbox_id) ?? []); + } catch (BlankResult $e) { + } + } + $messages = array_values(array_map(function(\ProVM\Emails\Model\Message $message) { + return $message->toArray(); + }, array_filter($messages, function(\ProVM\Emails\Model\Message $message) { + return $message->hasValidAttachments(); + }))); + return $this->withJson($response, ['messages' => $messages, 'total' => count($messages)]); + } + public function grab(ServerRequestInterface $request, ResponseInterface $response, Model $factory, Service $service): ResponseInterface + { + $body = $request->getBody(); + $json = \Safe\json_decode($body->getContents()); + $message_count = 0; + foreach ($json->mailboxes as $mailbox_name) { + $message_count += $this->grabFromMailbox($factory, $service, $mailbox_name); + } + return $this->withJson($response, compact('message_count')); + } + + protected function grabFromMailbox(Model $factory, Service $service, string $mailbox_name): int + { + $mailbox = $factory->find(\ProVM\Emails\Model\Mailbox::class)->fetchByName($mailbox_name); + $stored = array_reduce($mailbox->getStates(), function($count, Mailbox $state) { + return $count + $state->getCount(); + }) ?? 0; + $remote_mailbox = $service->get($mailbox->getName()); + $total = $remote_mailbox->count(); + if ($stored >= $total) { + return 0; + } + $added = 0; + $uids = []; + $amount = $total - $stored; + $messages = $service->getMessages($remote_mailbox, $stored, $amount); + foreach ($messages as $j => $m) { + if ($this->addMessage($factory->find(\ProVM\Emails\Model\Message::class), $service, $mailbox, $m, $j + 1)) { + $uids []= $m->getId(); + $added ++; + } + } + if ($added > 0 ) { + $data = [ + 'mailbox_id' => $mailbox->getId(), + 'date_time' => (new DateTimeImmutable('now'))->format('Y-m-d H:i:s'), + 'count' => $added, + 'uids' => serialize($uids) + ]; + $state = $factory->find(\ProVM\Emails\Model\State\Mailbox::class)->create($data); + $factory->find(\ProVM\Emails\Model\State\Mailbox::class)->save($state); + } + return $added; + } + protected function addMessage(Message $repository, Service $service, \ProVM\Emails\Model\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()->getFullAddress(), + 'date_time' => $remote_message->getDate()->format('Y-m-d H:i:s') + ]; + $message = $repository->create($data); + if ($message->getId() === 0) { + if ($remote_message->hasAttachments()) { + $message->doesHaveAttachments(); + } + if ($service->validAttachments($remote_message)) { + $message->doesHaveValidAttachments(); + } + $repository->save($message); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/api/common/Define/Model.php b/api/common/Define/Model.php new file mode 100644 index 0000000..28a9b45 --- /dev/null +++ b/api/common/Define/Model.php @@ -0,0 +1,6 @@ +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); + } +} \ No newline at end of file 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..3330796 --- /dev/null +++ b/api/common/Implement/Repository.php @@ -0,0 +1,185 @@ +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): Repository + { + $this->connection = $pdo; + return $this; + } + public function setTable(string $table): Repository + { + $this->table = $table; + return $this; + } + public function setLogger(LoggerInterface $logger): Repository + { + $this->logger = $logger; + return $this; + } + + abstract protected function fieldsForUpdate(): array; + abstract protected function valuesForUpdate(ModelInterface $model): array; + protected function idProperty(): string + { + return 'getId'; + } + protected function idField(): string + { + return 'id'; + } + public function update(ModelInterface $model, ModelInterface $old): void + { + $query = "UPDATE `{$this->getTable()}` SET "; + $model_values = $this->valuesForUpdate($model); + $old_values = $this->valuesForUpdate($old); + $columns = []; + $values = []; + foreach ($this->fieldsForUpdate() as $column => $method) { + if (isset($model_values[$column]) and $old_values[$column] !== $model_values[$column]) { + $columns []= "`{$column}`"; + $values []= $model_values[$column]; + } + } + if (count($columns) === 0) { + return; + } + $query .= implode(', ', $columns) . " WHERE {$this->idField()} = ?"; + $values []= $old->{$this->idProperty()}(); + $st = $this->getConnection()->prepare($query); + $st->execute($values); + } + abstract protected function fieldsForInsert(): array; + abstract protected function valuesForInsert(ModelInterface $model): array; + protected function insert(ModelInterface $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); + } + abstract protected function defaultFind(ModelInterface $model): ModelInterface; + public function save(ModelInterface &$model): void + { + try { + $old = $this->defaultFind($model); + $this->update($model, $old); + } catch (BlankResult $e) { + $this->insert($model); + $model->setId($this->getConnection()->lastInsertId()); + } catch (\Error | \Exception $e) { + $this->getLogger()->error($e); + } + } + abstract public function load(array $row): ModelInterface; + + abstract protected function fieldsForCreate(): array; + abstract protected function valuesForCreate(array $data): array; + abstract protected function defaultSearch(array $data): ModelInterface; + public function create(array $data): ModelInterface + { + try { + return $this->defaultSearch($data); + } catch (PDOException | BlankResult $e) { + $data[$this->idField()] = 0; + return $this->load($data); + } + } + + protected function getId(ModelInterface $model): int + { + return $model->getId(); + } + 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(ModelInterface $model): void + { + $query = "DELETE FROM `{$this->getTable()}` WHERE `{$this->idField()}` = ?"; + $st = $this->getConnection()->prepare($query); + $st->execute([$this->getId($model)]); + $this->resetIndex(); + $this->optimize(); + } + + protected function fetchOne(string $query, ?array $values = null): ModelInterface + { + 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); + } + + public function fetchAll(): array + { + $query = "SELECT * FROM `{$this->getTable()}`"; + return $this->fetchMany($query); + } + public function fetchById(int $id): ModelInterface + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `{$this->idField()}` = ?"; + return $this->fetchOne($query, [$id]); + } +} \ No newline at end of file diff --git a/api/common/Middleware/Auth.php b/api/common/Middleware/Auth.php new file mode 100644 index 0000000..0ff78ed --- /dev/null +++ b/api/common/Middleware/Auth.php @@ -0,0 +1,71 @@ +setResponseFactory($factory); + $this->setLogger($logger); + $this->setAPIKey($api_key); + } + + protected ResponseFactoryInterface $factory; + protected LoggerInterface $logger; + protected string $api_key; + + public function getResponseFactory(): ResponseFactoryInterface + { + return $this->factory; + } + public function getLogger(): LoggerInterface + { + return $this->logger; + } + public function getAPIKey(): string + { + return $this->api_key; + } + + public function setResponseFactory(ResponseFactoryInterface $factory): Auth + { + $this->factory = $factory; + return $this; + } + public function setLogger(LoggerInterface $logger): Auth + { + $this->logger = $logger; + return $this; + } + public function setAPIKey(string $key): Auth + { + $this->api_key = $key; + return $this; + } + + public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if ($request->getMethod() === 'OPTIONS') { + return $handler->handle($request); + } + $auths = $request->getHeader('Authorization'); + foreach ($auths as $auth) { + if (str_contains($auth, 'Bearer')) { + $key = str_replace('Bearer ', '', $auth); + if (sha1($this->getAPIKey()) === $key) { + return $handler->handle($request); + } + } + } + $response = $this->getResponseFactory()->createResponse(401); + $response->getBody()->write(\Safe\json_encode(['error' => 401, 'message' => 'Incorrect token'])); + return $response + ->withHeader('Content-Type', 'application/json'); + } +} \ No newline at end of file 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/Service/Attachments.php b/api/common/Service/Attachments.php new file mode 100644 index 0000000..3843819 --- /dev/null +++ b/api/common/Service/Attachments.php @@ -0,0 +1,159 @@ +setDecrypt($decrypt); + $this->setAttachmentsFolder($attachments_folder); + } + + protected string $attachments_folder; + protected Decrypt $decrypt; + protected \ProVM\Emails\Repository\Attachment $repository; + + public function getAttachmentsFolder(): string + { + return $this->attachments_folder; + } + public function getDecrypt(): Decrypt + { + return $this->decrypt; + } + public function getRepository(): \ProVM\Emails\Repository\Attachment + { + return $this->repository; + } + + public function setAttachmentsFolder(string $folder): Attachments + { + $this->attachments_folder = $folder; + return $this; + } + public function setDecrypt(Decrypt $decrypt): Attachments + { + $this->decrypt = $decrypt; + return $this; + } + public function setRepository(\ProVM\Emails\Repository\Attachment $repository): Attachments + { + $this->repository = $repository; + return $this; + } + + public function validateAttachment(AttachmentInterface $remote_attachment): bool + { + return str_contains($remote_attachment->getFilename(), '.pdf'); + } + + public function buildAttachment(Message $message, AttachmentInterface $remote_attachment): Attachment + { + $data = [ + 'message_id' => $message->getId(), + 'filename' => $this->buildFilename($message, $remote_attachment) + ]; + return $this->getRepository()->create($data); + } + + public function exists(Attachment $attachment): bool + { + $filename = $this->buildFilename($attachment); + return file_exists($filename); + } + + public function buildFilename(Message $message, AttachmentInterface $attachment): string + { + $filename = implode(' - ', [ + $message->getSubject(), + $message->getDateTime()->format('Y-m-d'), + $attachment->getFilename() + ]); + return implode(DIRECTORY_SEPARATOR, [ + $this->getAttachmentsFolder(), + $filename + ]); + } + + public function checkEncrypted(): array + { + $output = []; + foreach ($this->fetchAttachments() as $attachment) { + $output[$attachment->getFilename()] = $this->getDecrypt()->isEncrypted($attachment->getRealPath()); + } + return $output; + } + + public function fetchAttachments(): array + { + $files = new \FilesystemIterator($this->getAttachmentsFolder()); + $attachments = []; + foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + $attachments []= $file; + } + return $attachments; + } + public function fetchDecryptedAttachments(): array + { + $folder = implode(DIRECTORY_SEPARATOR, [$this->getAttachmentsFolder(), 'decrypted']); + $files = new \FilesystemIterator($folder); + $attachments = []; + foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + $attachments []= $file; + } + return $attachments; + } + public function fetchFullAttachments(): array + { + $attachments = $this->fetchAttachments(); + $output = []; + foreach ($attachments as $attachment) { + $att = [ + 'original_attachment' => $attachment, + 'encrypted' => $this->getDecrypt()->isEncrypted($attachment->getRealPath()) + ]; + if ($att['encrypted']) { + $att['decrypted_attachment'] = $this->getDecryptedAttachment($attachment); + } + $output []= $att; + } + return $output; + } + + public function getAttachment(string $filename): \SplFileInfo + { + if (!str_contains($filename, $this->getAttachmentsFolder())) { + $filename = implode(DIRECTORY_SEPARATOR, [$this->getAttachmentsFolder(), $filename]); + } + return new \SplFileInfo($filename); + } + public function getDecryptedAttachment(\SplFileInfo $info): \SplFileInfo|bool + { + $new_file = implode(DIRECTORY_SEPARATOR, [$info->getPath(), 'decrypted', $info->getFilename()]); + if (!file_exists($new_file)) { + return false; + } + return new \SplFileInfo($new_file); + } + + public function removeEncryption(string $filename): \SplFileInfo + { + $attachment = $this->getAttachment($filename); + $new_file = implode(DIRECTORY_SEPARATOR, [$attachment->getPath(), 'decrypted', $attachment->getFilename()]); + if ($this->getDecrypt()->runCommand($attachment->getRealPath(), $new_file)) { + return new \SplFileInfo($new_file); + } + return $attachment; + } +} \ No newline at end of file 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/Emails.php b/api/common/Service/Emails.php new file mode 100644 index 0000000..85a3c40 --- /dev/null +++ b/api/common/Service/Emails.php @@ -0,0 +1,158 @@ +setMailboxes($mailboxService); + $this->setMessageRepository($messageRepository); + $this->setAttachmentsFolder($attachments_folder); + } + + protected Message $messageRepository; + protected Mailboxes $mailboxService; + protected string $attachments_folder; + + public function getMailboxes(): Mailboxes + { + return $this->mailboxService; + } + public function getMessageRepository(): Message + { + return $this->messageRepository; + } + public function getAttachmentsFolder(): string + { + return $this->attachments_folder; + } + + public function setMailboxes(Mailboxes $mailboxService): Emails + { + $this->mailboxService = $mailboxService; + return $this; + } + public function setMessageRepository(Message $messageRepository): Emails + { + $this->messageRepository = $messageRepository; + return $this; + } + public function setAttachmentsFolder(string $folder): Emails + { + $this->attachments_folder = $folder; + return $this; + } + + //---------------------------------------------------------------- + // Messages + + public function getMessages(MailboxInterface $mailbox, int $start = 0, ?int $amount = null): array + { + $output = []; + $cnt = 0; + $messages = $this->getMessageRepository()->fetchByMailboxAndPosition($mailbox->getName(), $start, $amount ?? 1); + if ($messages) { + foreach ($messages as $message) { + $output []= $message; + $cnt ++; + } + } + if ($amount === null or $cnt < $amount) { + $messages = $this->getMailboxes()->getMessages($mailbox, $cnt + $start, $amount ?? $mailbox->count()); + foreach ($messages as $m) { + $message = $this->saveMessage($mailbox, $m, $cnt + $start); + $cnt ++; + if ($message === null) { + continue; + } + $output []= $message; + } + } + return $output; + } + public function saveMessage(MailboxInterface $mailbox, MessageInterface $message, int $position): ?\ProVM\Emails\Model\Message + { + $data = [ + 'mailbox_id' => $mailbox->getId(), + 'position' => $position, + 'uid' => $message->getNumber(), + 'subject' => $message->getSubject(), + 'from' => $message->getFrom()->getFullAddress(), + 'date_time' => $message->getDate()->format('Y-m-d H:i:s'), + ]; + return $this->getMessageRepository()->create($data); + } + public function getValidMessages(MailboxInterface $mailbox, int $start = 0, ?int $amount = null): array + { + $messages = $this->getMessages($mailbox, $start, $amount); + $output = array_filter($messages, function(\ProVM\Emails\Model\Message $message) { + return ($message->hasAttachments() and $message->hasValidAttachments()); + }); + if ($amount === null or count($output) >= $amount) { + return $output; + } + $cnt = $start + $amount; + while (count($output) < $amount) { + \Safe\ini_set('max_execution_time', ((int) \Safe\ini_get('max_execution_time')) + $amount * 5); + $messages = $this->getMailboxes()->getMessages($mailbox, $start + $amount, $amount); + foreach ($messages as $m) { + $message = $this->saveMessage($mailbox, $m, $cnt + $start); + $cnt ++; + if ($message === null) { + continue; + } + if ($message->hasAttachments() and $message->hasValidAttachments() and count($output) < $amount) { + $output []= $message; + } + } + } + return $output; + } + + // Attachments + + public function saveAttachments(MessageInterface $message, ?string $extension = null): array + { + if (!$message->hasAttachments()) { + return []; + } + $attachments = []; + foreach ($message->getAttachments() as $attachment) { + $this->getLogger()->debug($attachment->getFilename()); + if ($extension !== null) { + $extension = trim($extension, '.'); + if (!str_contains($attachment->getFilename(), ".{$extension}")) { + continue; + } + } + $filename = implode(DIRECTORY_SEPARATOR, [ + $this->getAttachmentsFolder(), + "{$message->getSubject()} - {$message->getDate()->format('Y-m-d')} - {$attachment->getFilename()}" + ]); + $this->getLogger()->debug($filename); + \Safe\file_put_contents($filename, $attachment->getDecodedContent()); + $attachments []= $filename; + } + return $attachments; + } + public function getAttachments(?string $mailbox = null, int $start = 0, ?int $amount = null, ?string $extension = null): array + { + if ($mailbox === null) { + $mailbox = '/'; + } + $mb = $this->getMailboxes()->get($mailbox); + $messages = $this->getMessages($mb, $start, $amount); + $attachments = []; + foreach ($messages as $message) { + $attachments = array_merge($attachments, $this->saveAttachments($message, $extension)); + } + return $attachments; + } +} \ 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..8a5bb02 --- /dev/null +++ b/api/common/Service/Install.php @@ -0,0 +1,73 @@ +setLogger($logger); + $this->setConnection($pdo); + } + + protected LoggerInterface $logger; + protected PDO $connection; + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + public function getConnection(): PDO + { + return $this->connection; + } + public function setLogger(LoggerInterface $logger): Install + { + $this->logger = $logger; + return $this; + } + public function setConnection(PDO $pdo): Install + { + $this->connection = $pdo; + return $this; + } + + public function run(): void + { + $tables = [ + 'messages' => [ + '`mailbox_id` int UNSIGNED NOT NULL', + '`position` int UNSIGNED NOT NULL', + '`uid` int UNSIGNED NOT NULL PRIMARY KEY', + '`subject` varchar(255) NOT NULL', + '`from` varchar(100) NOT NULL', + '`date_time` datetime NOT NULL', + '`has_attachments` int(1) DEFAULT 0', + '`valid_attachments` int(1) DEFAULT 0', + '`downloaded_attachments` int(1) DEFAULT 0' + ], + 'attachments' => [ + '`message_uid` int UNSIGNED NOT NULL', + '`filename` varchar(255) NOT NULL PRIMARY KEY', + '`encrypted` int(1) DEFAULT 0', + '`decrypted` int(1) DEFAULT 0', + 'CONSTRAINT `message_uid_fk` FOREIGN KEY (`message_uid`) REFERENCES `messages` (`uid`)' + ], + 'mailboxes' => [ + '`' + ] + ]; + foreach ($tables as $table => $definitions) { + $this->getConnection()->query($this->buildCreateTable($table, $definitions)); + } + } + protected function buildCreateTable(string $table_name, array $columns): string + { + $query = ["CREATE TABLE IF NOT EXISTS `{$table_name}` ("]; + $query []= implode(',' . PHP_EOL, $columns); + $query []= ')'; + return implode(PHP_EOL, $query); + } +} \ No newline at end of file diff --git a/api/common/Service/Mailboxes.php b/api/common/Service/Mailboxes.php new file mode 100644 index 0000000..3e55e04 --- /dev/null +++ b/api/common/Service/Mailboxes.php @@ -0,0 +1,151 @@ +setConnection($connection) + ->setAttachments($attachments) + ->setUsername($username); + } + + protected ConnectionInterface $connection; + protected Attachments $attachments; + protected string $username; + + public function getConnection(): ConnectionInterface + { + return $this->connection; + } + public function getAttachments(): Attachments + { + return $this->attachments; + } + public function getUsername(): string + { + return $this->username; + } + + public function setConnection(ConnectionInterface $connection): Mailboxes + { + $this->connection = $connection; + return $this; + } + public function setAttachments(Attachments $attachments): Mailboxes + { + $this->attachments = $attachments; + return $this; + } + public function setUsername(string $username): Mailboxes + { + $this->username = $username; + return $this; + } + + protected array $mailboxes; + public function getAll(): array + { + if (!isset($this->mailboxes)) { + $this->mailboxes = $this->getConnection()->getMailboxes(); + } + return $this->mailboxes; + } + public function get(string $mailbox): MailboxInterface + { + if (!$this->getConnection()->hasMailbox($mailbox)) { + throw new Invalid(); + } + return $this->getConnection()->getMailbox($mailbox); + } + + // Messages + + protected function advanceIterator(Iterator $iterator, int $up_to): Iterator + { + for ($i = 0; $i < $up_to; $i ++) { + $iterator->next(); + } + return $iterator; + } + + public function getMessages(MailboxInterface $mailbox, int $start = 0, ?int $amount = null): Generator + { + if ($mailbox->count() === 0) { + $this->getLogger()->notice('No mails found.'); + throw new EmptyMailbox(); + } + $it = $mailbox->getIterator(); + if ($amount === null) { + $amount = $mailbox->count() - $start; + } + $it = $this->advanceIterator($it, $start); + for ($i = $start; $i < min($start + $amount, $mailbox->count()); $i ++) { + yield $it->key() => $it->current(); + $it->next(); + } + } + public function countValid(MailboxInterface $mailbox): int + { + $cnt = 0; + foreach ($mailbox->getIterator() as $message) { + if ($this->hasAttachments($message) and $this->validAttachments($message)) { + $cnt ++; + } + } + return $cnt; + } + /** + * @param MailboxInterface $mailbox + * @param int $uid + * @return MessageInterface + */ + public function getMessage(MailboxInterface $mailbox, int $uid): MessageInterface + { + return $mailbox->getMessage($uid); + } + + protected function validateAddress(array $to): bool + { + foreach ($to as $address) { + if (strtolower($address->getAddress()) === strtolower($this->getUsername())) { + return true; + } + } + return false; + } + public function hasAttachments(MessageInterface $message): bool + { + return ($message->hasAttachments() and $this->validateAddress($message->getTo())); + } + protected function validateAttachment(AttachmentInterface $attachment): bool + { + return str_contains($attachment->getFilename(), '.pdf'); + } + public function validAttachments(MessageInterface $message): bool + { + foreach ($message->getAttachments() as $attachment) { + if ($this->validateAttachment($attachment)) { + return true; + } + } + return false; + } + public function downloadAttachments(MessageInterface $message) + { + foreach ($message->getAttachments() as $attachment) { + if ($this->validateAttachment($attachment)) { + $attachment->getContent(); + } + } + } +} \ No newline at end of file diff --git a/api/common/Service/Messages.php b/api/common/Service/Messages.php new file mode 100644 index 0000000..d1b9552 --- /dev/null +++ b/api/common/Service/Messages.php @@ -0,0 +1,37 @@ +setFactory($factory) + ->setLogger($logger); + } + + protected Model $factory; + protected LoggerInterface $logger; + + public function getFactory(): Model + { + return $this->factory; + } + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function setFactory(Model $factory): Messages + { + $this->factory = $factory; + return $this; + } + public function setLogger(LoggerInterface $logger): Messages + { + $this->logger = $logger; + return $this; + } +} \ No newline at end of file diff --git a/api/public/index.php b/api/public/index.php index e8e029a..aed6193 100644 --- a/api/public/index.php +++ b/api/public/index.php @@ -4,5 +4,10 @@ $app = require_once implode(DIRECTORY_SEPARATOR, [ 'setup', 'app.php' ]); -#$app->getContainer()->get(\Psr\Log\LoggerInterface::class)->debug(var_export($app, true)); -$app->run(); +Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface::class)); +try { + $app->run(); +} catch (Error | Exception $e) { + $app->getContainer()->get(\Psr\Log\LoggerInterface::class)->error($e); + throw $e; +} diff --git a/api/resources/routes/01_mailboxes.php b/api/resources/routes/01_mailboxes.php new file mode 100644 index 0000000..b39878e --- /dev/null +++ b/api/resources/routes/01_mailboxes.php @@ -0,0 +1,13 @@ +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->get('[/]', [Mailboxes::class, 'get']); +}); diff --git a/api/resources/routes/02_messages.php b/api/resources/routes/02_messages.php new file mode 100644 index 0000000..1296b5a --- /dev/null +++ b/api/resources/routes/02_messages.php @@ -0,0 +1,8 @@ +group('/messages', function($app) { + $app->post('/valid', [Messages::class, 'valid']); + $app->put('/grab', [Messages::class, 'grab']); + $app->post('[/]', Messages::class); +}); \ No newline at end of file diff --git a/api/setup/setups/02_services.php b/api/setup/setups/02_services.php index 2116a9e..f2ea3ab 100644 --- a/api/setup/setups/02_services.php +++ b/api/setup/setups/02_services.php @@ -5,7 +5,7 @@ return [ \ProVM\Common\Service\Mailboxes::class => function(ContainerInterface $container) { return (new \ProVM\Common\Service\Mailboxes( $container->get(\Ddeboer\Imap\ConnectionInterface::class), - $container->get('emails')->folder, + $container->get(\ProVM\Common\Service\Attachments::class), $container->get('emails')->username ))->setLogger($container->get(\Psr\Log\LoggerInterface::class)); }, @@ -19,6 +19,7 @@ return [ \ProVM\Common\Service\Attachments::class => function(ContainerInterface $container) { return new \ProVM\Common\Service\Attachments( $container->get(\ProVM\Common\Service\Decrypt::class), + $container->get(\ProVM\Emails\Repository\Attachment::class), $container->get('attachments_folder') ); }, diff --git a/api/setup/setups/03_factories.php b/api/setup/setups/03_factories.php new file mode 100644 index 0000000..654ca74 --- /dev/null +++ b/api/setup/setups/03_factories.php @@ -0,0 +1,17 @@ + function(ContainerInterface $container) { + $factory = new \ProVM\Common\Factory\Model($container); + $repositories = [ + 'Mailbox' => \ProVM\Emails\Repository\Mailbox::class, + 'Message' => \ProVM\Emails\Repository\Message::class, + 'Attachment' => \ProVM\Emails\Repository\Attachment::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 + ]; + return $factory->setRepositories($repositories); + } +]; \ No newline at end of file diff --git a/api/setup/setups/98_log.php b/api/setup/setups/98_log.php index eaef107..b81de42 100644 --- a/api/setup/setups/98_log.php +++ b/api/setup/setups/98_log.php @@ -2,14 +2,21 @@ use Psr\Container\ContainerInterface; return [ + \Monolog\Handler\DeduplicationHandler::class => function(ContainerInterface $container) { + return new \Monolog\Handler\DeduplicationHandler($container->get(\Monolog\Handler\RotatingFileHandler::class)); + }, \Monolog\Handler\RotatingFileHandler::class => function(ContainerInterface $container) { $handler = new \Monolog\Handler\RotatingFileHandler($container->get('log_file')); - $handler->setFormatter($container->get(\Monolog\Formatter\LineFormatter::class)); + $handler->setFormatter($container->get(\Monolog\Formatter\SyslogFormatter::class)); return $handler; }, \Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { $logger = new \Monolog\Logger('file_logger'); - $logger->pushHandler($container->get(\Monolog\Handler\RotatingFileHandler::class)); + $logger->pushHandler($container->get(\Monolog\Handler\DeduplicationHandler::class)); + //$logger->pushHandler($container->get(\Monolog\Handler\RotatingFileHandler::class)); + $logger->pushProcessor($container->get(\Monolog\Processor\PsrLogMessageProcessor::class)); + $logger->pushProcessor($container->get(\Monolog\Processor\IntrospectionProcessor::class)); + $logger->pushProcessor($container->get(\Monolog\Processor\MemoryUsageProcessor::class)); return $logger; } ]; \ 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..d6f562d --- /dev/null +++ b/api/src/Model/Attachment.php @@ -0,0 +1,124 @@ +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)) { + $this->setStates($this->getStateRepository()->fetchByAttachment($this->getId())); + } + return $this->states; + } + public function getState(string $name): State\Attachment + { + 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()) + ->setName($name) + ->setAttachment($this) + ); + return $this; + } + + public function isEncrypted(): bool + { + return $this->getState('encrypted')->getValue() ?? false; + } + public function isDecrypted(): bool + { + return $this->getState('encrypted')->getValue() ?? false; + } + 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('encrypted'); + $this->getState('decrypted')->setValue(true); + } + return $this; + } + + public function toArray(): array + { + return [ + 'id' => $this->getId(), + 'message' => $this->getMessage()->toArray(), + 'filename' => $this->getFilename(), + 'encrypted' => $this->isEncrypted(), + 'decrypted' => $this->isDecrypted() + ]; + } +} \ No newline at end of file diff --git a/api/src/Model/Mailbox.php b/api/src/Model/Mailbox.php new file mode 100644 index 0000000..7b86bcf --- /dev/null +++ b/api/src/Model/Mailbox.php @@ -0,0 +1,97 @@ +id; + } + public function getName(): string + { + return $this->name; + } + + public function setId(int $id): Mailbox + { + $this->id = $id; + return $this; + } + public function setName(string $name): Mailbox + { + $this->name = $name; + 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 lastChecked(): ?DateTimeInterface + { + if (count($this->getStates()) == 0) { + return null; + } + return $this->getStates()[array_key_last($this->getStates())]->getDateTime(); + } + public function lastCount(): int + { + if (count($this->getStates()) == 0) { + return 0; + } + return $this->getStates()[array_key_last($this->getStates())]->getCount(); + } + + public function toArray(): array + { + return [ + 'id' => $this->getId(), + 'name' => $this->getName(), + 'last_checked' => [ + 'date' => $this->lastChecked() ?? 'never', + 'count' => $this->lastCount() ?? 0 + ] + ]; + } +} \ No newline at end of file diff --git a/api/src/Model/Message.php b/api/src/Model/Message.php new file mode 100644 index 0000000..2de4d68 --- /dev/null +++ b/api/src/Model/Message.php @@ -0,0 +1,212 @@ +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(\ProVM\Emails\Model\State\Message::class)->fetchByMessage($this->getId())); + } catch (BlankResult $e) { + return []; + } + } + return $this->states; + } + public function getState(string $name): \ProVM\Emails\Model\State\Message + { + return $this->getStates()[$name]; + } + public function addState(\ProVM\Emails\Model\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 \ProVM\Emails\Model\State\Message()) + ->setName($name) + ->setMessage($this) + ); + 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 doesHaveAttachments(): Message + { + if (!isset($this->getStates()['has_attachments'])) { + $this->newState('has_attachments'); + } + $this->getState('has_attachments')->setValue(true); + return $this; + } + public function doesHaveValidAttachments(): Message + { + if (!isset($this->getStates()['valid_attachments'])) { + $this->newState('valid_attachments'); + } + $this->getState('valid_attachments')->setValue(true); + return $this; + } + public function doesHaveDownloadedAttachments(): Message + { + if (!isset($this->getStates()['downloaded_attachments'])) { + $this->newState('downloaded_attachments'); + } + $this->getState('downloaded_attachments')->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() + ] + ]; + } +} \ No newline at end of file diff --git a/api/src/Model/State/Attachment.php b/api/src/Model/State/Attachment.php new file mode 100644 index 0000000..3838098 --- /dev/null +++ b/api/src/Model/State/Attachment.php @@ -0,0 +1,50 @@ +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; + } + + 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; + } +} \ No newline at end of file diff --git a/api/src/Model/State/Mailbox.php b/api/src/Model/State/Mailbox.php new file mode 100644 index 0000000..35d5df6 --- /dev/null +++ b/api/src/Model/State/Mailbox.php @@ -0,0 +1,68 @@ +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; + } +} \ No newline at end of file diff --git a/api/src/Model/State/Message.php b/api/src/Model/State/Message.php new file mode 100644 index 0000000..c1fab5c --- /dev/null +++ b/api/src/Model/State/Message.php @@ -0,0 +1,50 @@ +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; + } +} \ No newline at end of file diff --git a/api/src/Repository/Attachment.php b/api/src/Repository/Attachment.php new file mode 100644 index 0000000..59e0c06 --- /dev/null +++ b/api/src/Repository/Attachment.php @@ -0,0 +1,95 @@ +setStateRepository($stateRepository) + ->setTable('attachments'); + } + protected State\Attachment $stateRepository; + + public function getStateRepository(): State\Attachment + { + return $this->stateRepository; + } + + public function setStateRepository(State\Attachment $repository): Attachment + { + $this->stateRepository = $repository; + return $this; + } + + protected function idField(): string + { + return 'filename'; + } + + protected function fieldsForInsert(): array + { + return array_merge($this->fieldsForUpdate(), ['filename']); + } + protected function fieldsForUpdate(): array + { + return [ + 'message_id' + ]; + } + protected function fieldsForCreate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForUpdate(ModelInterface $model): array + { + return [ + $model->getMessage()->getId(), + $model->getFilename() + ]; + } + protected function valuesForInsert(ModelInterface $model): array + { + return $this->valuesForUpdate($model); + } + protected function defaultFind(ModelInterface $model): ModelInterface + { + return $this->fetchByFilename($model->getFilename()); + } + protected function valuesForCreate(array $data): array + { + return [ + $data['message_id'], + $data['filename'] + ]; + } + protected function defaultSearch(array $data): ModelInterface + { + return $this->fetchByFilename($data['filename']); + } + + public function load(array $row): \ProVM\Emails\Model\Attachment + { + $model = new \ProVM\Emails\Model\Attachment(); + $model + ->setFilename($row['filename']) + ->setStateRepository($this->getStateRepository()); + return $model; + } + + public function fetchByFilename(string $filename): \ProVM\Emails\Model\Attachment + { + $query = "SELECT * FROM {$this->getTable()} WHERE filename = ?"; + return $this->fetchOne($query, [$filename]); + } + public function fetchByMessage(int $message_id): array + { + $query = "SELECT * FROM {$this->getTable()} WHERE message_id = ?"; + return $this->fetchMany($query, [$message_id]); + } +} \ No newline at end of file diff --git a/api/src/Repository/Mailbox.php b/api/src/Repository/Mailbox.php new file mode 100644 index 0000000..3f470e3 --- /dev/null +++ b/api/src/Repository/Mailbox.php @@ -0,0 +1,85 @@ +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; + } + + protected function fieldsForUpdate(): array + { + return $this->fieldsForInsert(); + } + protected function fieldsForInsert(): array + { + return [ + 'name' + ]; + } + protected function fieldsForCreate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForInsert(Model $model): array + { + return [ + $model->getName() + ]; + } + 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'] + ]; + } + 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']) + ->setStateRepository($this->getStates()); + } + + public function fetchByName(string $name): \ProVM\Emails\Model\Mailbox + { + $query = "SELECT * FROM `{$this->getTable()}` WHERE `name` = ?"; + return $this->fetchOne($query, [$name]); + } +} \ No newline at end of file diff --git a/api/src/Repository/Message.php b/api/src/Repository/Message.php new file mode 100644 index 0000000..a0389e3 --- /dev/null +++ b/api/src/Repository/Message.php @@ -0,0 +1,155 @@ +setTable('messages') + ->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; + } + + 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['uid'], + $data['mailbox_id'], + $data['position'], + $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 + { + $model = new \ProVM\Emails\Model\Message(); + $model + ->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']) + ->setFactory($this->getFactory()); + try { + $model->setDateTime(new DateTimeImmutable($row['date_time'])); + } catch (Exception | ErrorfuncException $e) { + $this->getLogger()->error($e); + } + return $model; + } + + public function save(Model &$model): void + { + parent::save($model); + $valid_states = [ + 'has_attachments', + 'valid_attachments', + 'downloaded_attachments' + ]; + foreach ($valid_states as $state_name) { + try { + $model->getState($state_name); + } catch (\Exception $e) { + $data = [ + 'message_id' => $model->getId(), + 'name' => $state_name + ]; + $state = $this->getFactory()->find(\ProVM\Emails\Model\State\Message::class)->create($data); + $model->addState($state); + } + } + foreach ($model->getStates() as $state) { + $state->setMessage($model); + $this->getFactory()->find(\ProVM\Emails\Model\State\Message::class)->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]); + } +} \ No newline at end of file diff --git a/api/src/Repository/State/Attachment.php b/api/src/Repository/State/Attachment.php new file mode 100644 index 0000000..9a40027 --- /dev/null +++ b/api/src/Repository/State/Attachment.php @@ -0,0 +1,80 @@ +setTable('attachments_states'); + } + + 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 array_merge($this->valuesForInsert($model), [$model->getId()]); + } + 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']) + ->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]); + } +} \ No newline at end of file diff --git a/api/src/Repository/State/Mailbox.php b/api/src/Repository/State/Mailbox.php new file mode 100644 index 0000000..7d498d0 --- /dev/null +++ b/api/src/Repository/State/Mailbox.php @@ -0,0 +1,102 @@ +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; + } + + 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]); + } +} \ No newline at end of file diff --git a/api/src/Repository/State/Message.php b/api/src/Repository/State/Message.php new file mode 100644 index 0000000..fda78e5 --- /dev/null +++ b/api/src/Repository/State/Message.php @@ -0,0 +1,80 @@ +setTable('messages_states'); + } + + 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']) + ->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]); + } +} \ No newline at end of file