From 0354449c4a1752c585aa268937e1e0bd9b64b1d6 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:09:35 -0300 Subject: [PATCH 01/35] Readme --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae99296 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Emails +ProVM + +## API + +| Concept | Model | Repository | Table | +|------------|--------------------|--------------------|--------------------| +| Message | | | | +| Attachment | | | | +| Mailbox | | | | + +## UI ++ [ ] Mailboxes + + [ ] Register ++ [ ] Messages ++ [ ] Attachments +## CLI ++ [ ] Mailboxes + + Scrub ++ [ ] Messages + + Grab Attachments ++ [ ] Attachments + + Decrypt \ No newline at end of file From ad8afb574d53825465761847931407c086f1fd2c Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:16:22 -0300 Subject: [PATCH 02/35] Docker base --- .env.sample | 13 +++++++++++++ docker-compose.yml | 12 ++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 .env.sample create mode 100644 docker-compose.yml diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..e2d0fd1 --- /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 + +ATT_PATH=./attachments +LOGS_PATH=./logs +WEB_PORT=8000 +API_PORT=8080 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" From 74ed084d3eb64c21774d531c23069b8f391e9227 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:16:43 -0300 Subject: [PATCH 03/35] API Key, email settings --- .key.env.sample | 1 + .mail.env.sample | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 .key.env.sample create mode 100644 .mail.env.sample 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/.mail.env.sample b/.mail.env.sample new file mode 100644 index 0000000..437f229 --- /dev/null +++ b/.mail.env.sample @@ -0,0 +1,6 @@ +EMAIL_HOST=imap.gmail.com +EMAIL_PORT=993 +EMAIL_USERNAME=@gmail.com +EMAIL_PASSWORD= +EMAIL_FOLDER= +ATTACHMENTS_FOLDER=/attachments \ No newline at end of file From 1a877822e77db21eedadeb1042a6f83976c9f30a Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:17:02 -0300 Subject: [PATCH 04/35] API Docker --- api/Dockerfile | 13 +++++++++++ api/docker-compose.yml | 49 ++++++++++++++++++++++++++++++++++++++++++ api/errors.ini | 3 +++ api/nginx.conf | 36 +++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 api/Dockerfile create mode 100644 api/docker-compose.yml create mode 100644 api/errors.ini create mode 100644 api/nginx.conf 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/docker-compose.yml b/api/docker-compose.yml new file mode 100644 index 0000000..0512fbf --- /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}:8080" + api: + profiles: + - api + container_name: emails-api + build: + context: ${API_PATH:-.} + restart: unless-stopped + env_file: + - ${API_PATH:-.}/.env + - ${API_PATH:-.}/.db.env + - .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..6d22e8e --- /dev/null +++ b/api/nginx.conf @@ -0,0 +1,36 @@ +server { + listen 0.0.0.0:8080; + root /app/api/public; + index index.php index.html index.htm; + + access_log /var/logs/nginx/access.log; + error_log /var/logs/nginx/error.log; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + 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)(/.+)$; + fastcgi_pass api: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; + } +} \ No newline at end of file From fce93896bf6bb6591949b862f935636bd8729821 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:19:08 -0300 Subject: [PATCH 05/35] Ignore .lock files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e1ccfc6..60c36e5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Composer **/vendor/ +**/*.lock # Views **/cache/ From 1cf5a1f094834f8d91c4165edca6f544081cca7c Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:19:19 -0300 Subject: [PATCH 06/35] API Dependencies --- api/composer.json | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 api/composer.json 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 + } +} From 0fbf73b3fd4ac32840fd5f555ab19e7cddb56137 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:20:04 -0300 Subject: [PATCH 07/35] API Base App --- api/public/index.php | 8 +++++ api/resources/routes/01_emails.php | 12 +++++++ api/resources/routes/02_attachments.php | 8 +++++ api/resources/routes/98_install.php | 4 +++ api/setup/app.php | 43 +++++++++++++++++++++++++ api/setup/composer.php | 6 ++++ api/setup/middleware/01_routes.php | 11 +++++++ api/setup/middleware/02_cors.php | 2 ++ api/setup/middleware/97_auth.php | 2 ++ api/setup/middleware/99_errors.php | 2 ++ api/setup/settings/01_env.php | 31 ++++++++++++++++++ api/setup/settings/02_folders.php | 21 ++++++++++++ api/setup/settings/03_decrypt.php | 4 +++ api/setup/settings/98_log.php | 4 +++ api/setup/setups/01_emails.php | 27 ++++++++++++++++ api/setup/setups/02_services.php | 40 +++++++++++++++++++++++ api/setup/setups/97_auth.php | 12 +++++++ api/setup/setups/98_log.php | 15 +++++++++ api/setup/setups/99_errors.php | 15 +++++++++ 19 files changed, 267 insertions(+) create mode 100644 api/public/index.php create mode 100644 api/resources/routes/01_emails.php create mode 100644 api/resources/routes/02_attachments.php create mode 100644 api/resources/routes/98_install.php create mode 100644 api/setup/app.php create mode 100644 api/setup/composer.php create mode 100644 api/setup/middleware/01_routes.php create mode 100644 api/setup/middleware/02_cors.php create mode 100644 api/setup/middleware/97_auth.php create mode 100644 api/setup/middleware/99_errors.php create mode 100644 api/setup/settings/01_env.php create mode 100644 api/setup/settings/02_folders.php create mode 100644 api/setup/settings/03_decrypt.php create mode 100644 api/setup/settings/98_log.php create mode 100644 api/setup/setups/01_emails.php create mode 100644 api/setup/setups/02_services.php create mode 100644 api/setup/setups/97_auth.php create mode 100644 api/setup/setups/98_log.php create mode 100644 api/setup/setups/99_errors.php diff --git a/api/public/index.php b/api/public/index.php new file mode 100644 index 0000000..e8e029a --- /dev/null +++ b/api/public/index.php @@ -0,0 +1,8 @@ +getContainer()->get(\Psr\Log\LoggerInterface::class)->debug(var_export($app, true)); +$app->run(); diff --git a/api/resources/routes/01_emails.php b/api/resources/routes/01_emails.php new file mode 100644 index 0000000..f86121a --- /dev/null +++ b/api/resources/routes/01_emails.php @@ -0,0 +1,12 @@ +group('/emails', function($app) { + $app->get('/mailboxes', [Emails::class, 'mailboxes']); + $app->group('/messages', function($app) { + $app->post('/attachments[/]', [Emails::class, 'withAttachments']); + $app->post('[/]', [Emails::class, 'messages']); + }); + $app->post('/attachments', [Emails::class, 'attachments']); + $app->post('/attachment', [Emails::class, 'attachment']); +}); \ No newline at end of file diff --git a/api/resources/routes/02_attachments.php b/api/resources/routes/02_attachments.php new file mode 100644 index 0000000..5d68696 --- /dev/null +++ b/api/resources/routes/02_attachments.php @@ -0,0 +1,8 @@ +group('/attachments', function($app) { + $app->post('/get', [Attachments::class, 'get']); + $app->post('/decrypt', [Attachments::class, 'decrypt']); + $app->get('[/]', Attachments::class); +}); \ No newline at end of file diff --git a/api/resources/routes/98_install.php b/api/resources/routes/98_install.php new file mode 100644 index 0000000..d446671 --- /dev/null +++ b/api/resources/routes/98_install.php @@ -0,0 +1,4 @@ +get('/install', Install::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..7367296 --- /dev/null +++ b/api/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/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..063dba6 --- /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/97_auth.php b/api/setup/middleware/97_auth.php new file mode 100644 index 0000000..40efe2f --- /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/99_errors.php b/api/setup/middleware/99_errors.php new file mode 100644 index 0000000..c3f0b5f --- /dev/null +++ b/api/setup/middleware/99_errors.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(\Zeuxisoo\Whoops\Slim\WhoopsMiddleware::class)); \ No newline at end of file diff --git a/api/setup/settings/01_env.php b/api/setup/settings/01_env.php new file mode 100644 index 0000000..6e61213 --- /dev/null +++ b/api/setup/settings/01_env.php @@ -0,0 +1,31 @@ + 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; + } +]; diff --git a/api/setup/settings/02_folders.php b/api/setup/settings/02_folders.php new file mode 100644 index 0000000..669c193 --- /dev/null +++ b/api/setup/settings/02_folders.php @@ -0,0 +1,21 @@ + 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'], +]; \ No newline at end of file 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/98_log.php b/api/setup/settings/98_log.php new file mode 100644 index 0000000..55328ea --- /dev/null +++ b/api/setup/settings/98_log.php @@ -0,0 +1,4 @@ + '/logs/php.log' +]; \ No newline at end of file diff --git a/api/setup/setups/01_emails.php b/api/setup/setups/01_emails.php new file mode 100644 index 0000000..23c62b3 --- /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..2116a9e --- /dev/null +++ b/api/setup/setups/02_services.php @@ -0,0 +1,40 @@ + function(ContainerInterface $container) { + return (new \ProVM\Common\Service\Mailboxes( + $container->get(\Ddeboer\Imap\ConnectionInterface::class), + $container->get('emails')->folder, + $container->get('emails')->username + ))->setLogger($container->get(\Psr\Log\LoggerInterface::class)); + }, + \ProVM\Common\Service\Emails::class => function(ContainerInterface $container) { + return (new \ProVM\Common\Service\Emails( + $container->get(\ProVM\Common\Service\Mailboxes::class), + $container->get(\ProVM\Emails\Repository\Message::class), + $container->get('attachments_folder') + ))->setLogger($container->get(\Psr\Log\LoggerInterface::class)); + }, + \ProVM\Common\Service\Attachments::class => function(ContainerInterface $container) { + return new \ProVM\Common\Service\Attachments( + $container->get(\ProVM\Common\Service\Decrypt::class), + $container->get('attachments_folder') + ); + }, + \ProVM\Common\Service\Decrypt::class => 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\Install::class => function(ContainerInterface $container) { + $database = $container->get('database'); + $pdo = new PDO("mysql:host={$database->host};dbname={$database->name}", $database->username, $database->password); + return new \ProVM\Common\Service\Install( + $container->get(\Psr\Log\LoggerInterface::class), + $pdo + ); + } +]; diff --git a/api/setup/setups/97_auth.php b/api/setup/setups/97_auth.php new file mode 100644 index 0000000..17e45ac --- /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('api_key') + ); + } +]; diff --git a/api/setup/setups/98_log.php b/api/setup/setups/98_log.php new file mode 100644 index 0000000..eaef107 --- /dev/null +++ b/api/setup/setups/98_log.php @@ -0,0 +1,15 @@ + function(ContainerInterface $container) { + $handler = new \Monolog\Handler\RotatingFileHandler($container->get('log_file')); + $handler->setFormatter($container->get(\Monolog\Formatter\LineFormatter::class)); + return $handler; + }, + \Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { + $logger = new \Monolog\Logger('file_logger'); + $logger->pushHandler($container->get(\Monolog\Handler\RotatingFileHandler::class)); + return $logger; + } +]; \ No newline at end of file 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 From b919033d6245e3cfcc1cd0e1158703749bb4023b Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:20:51 -0300 Subject: [PATCH 08/35] Ignore attachments and logs --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 60c36e5..fd55c59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Env **/*.env +**/attachments/ +**/logs/ # Composer **/vendor/ From b3115be4f282ac131658484103f21ba2212a3fae Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:22:36 -0300 Subject: [PATCH 09/35] UI Docker --- ui/Dockerfile | 8 ++++++++ ui/docker-compose.yml | 21 +++++++++++++++++++++ ui/nginx.conf | 19 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 ui/Dockerfile create mode 100644 ui/docker-compose.yml create mode 100644 ui/nginx.conf 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/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..893e62a --- /dev/null +++ b/ui/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 0.0.0.0:80; + root /app/ui/public; + index index.php index.html index.htm; + + 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; + } +} \ No newline at end of file From 557d218bcc4fef105fd6489cd4bdf575249f701f Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:22:58 -0300 Subject: [PATCH 10/35] UI Base App --- ui/public/index.php | 7 +++++ ui/setup/app.php | 43 +++++++++++++++++++++++++++++++ ui/setup/composer.php | 6 +++++ ui/setup/middleware/01_routes.php | 11 ++++++++ ui/setup/middleware/99_errors.php | 2 ++ ui/setup/settings/01_env.php | 4 +++ ui/setup/settings/02_folders.php | 25 ++++++++++++++++++ ui/setup/settings/03_urls.php | 8 ++++++ ui/setup/settings/98_log.php | 4 +++ ui/setup/setups/03_views.php | 16 ++++++++++++ ui/setup/setups/98_log.php | 15 +++++++++++ ui/setup/setups/99_errors.php | 8 ++++++ 12 files changed, 149 insertions(+) create mode 100644 ui/public/index.php create mode 100644 ui/setup/app.php create mode 100644 ui/setup/composer.php create mode 100644 ui/setup/middleware/01_routes.php create mode 100644 ui/setup/middleware/99_errors.php create mode 100644 ui/setup/settings/01_env.php create mode 100644 ui/setup/settings/02_folders.php create mode 100644 ui/setup/settings/03_urls.php create mode 100644 ui/setup/settings/98_log.php create mode 100644 ui/setup/setups/03_views.php create mode 100644 ui/setup/setups/98_log.php create mode 100644 ui/setup/setups/99_errors.php diff --git a/ui/public/index.php b/ui/public/index.php new file mode 100644 index 0000000..0606ece --- /dev/null +++ b/ui/public/index.php @@ -0,0 +1,7 @@ +run(); 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/99_errors.php b/ui/setup/middleware/99_errors.php new file mode 100644 index 0000000..c168fe5 --- /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..f138041 --- /dev/null +++ b/ui/setup/settings/01_env.php @@ -0,0 +1,4 @@ + $_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..f3a4efd --- /dev/null +++ b/ui/setup/settings/02_folders.php @@ -0,0 +1,25 @@ + 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' + ]); + return (object) $arr; + } +]; \ No newline at end of file diff --git a/ui/setup/settings/03_urls.php b/ui/setup/settings/03_urls.php new file mode 100644 index 0000000..b0ca32d --- /dev/null +++ b/ui/setup/settings/03_urls.php @@ -0,0 +1,8 @@ + function() { + $arr = ['base' => '/']; + $arr['api'] = 'http://localhost:8080'; + 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/03_views.php b/ui/setup/setups/03_views.php new file mode 100644 index 0000000..c8c7a06 --- /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('urls'), + 'api_key' => sha1($container->get('api_key')) + ] + ); + } +]; diff --git a/ui/setup/setups/98_log.php b/ui/setup/setups/98_log.php new file mode 100644 index 0000000..eaef107 --- /dev/null +++ b/ui/setup/setups/98_log.php @@ -0,0 +1,15 @@ + function(ContainerInterface $container) { + $handler = new \Monolog\Handler\RotatingFileHandler($container->get('log_file')); + $handler->setFormatter($container->get(\Monolog\Formatter\LineFormatter::class)); + return $handler; + }, + \Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { + $logger = new \Monolog\Logger('file_logger'); + $logger->pushHandler($container->get(\Monolog\Handler\RotatingFileHandler::class)); + return $logger; + } +]; \ No newline at end of file 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 From 6a023ab3833dae72b7518646c6d8131766141ea7 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:23:16 -0300 Subject: [PATCH 11/35] UI Dependencies --- ui/composer.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 ui/composer.json diff --git a/ui/composer.json b/ui/composer.json new file mode 100644 index 0000000..8d7e8da --- /dev/null +++ b/ui/composer.json @@ -0,0 +1,31 @@ +{ + "name": "provm/emails_ui", + "type": "project", + "require": { + "berrnd/slim-blade-view": "^1.0", + "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 + } +} From c71061d7a75ffd2f254e485cb5bbc6fc321c49ec Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:25:15 -0300 Subject: [PATCH 12/35] CLI Docker --- cli/Dockerfile | 13 +++++++++++++ cli/docker-compose.yml | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 cli/Dockerfile create mode 100644 cli/docker-compose.yml diff --git a/cli/Dockerfile b/cli/Dockerfile new file mode 100644 index 0000000..c006021 --- /dev/null +++ b/cli/Dockerfile @@ -0,0 +1,13 @@ +FROM php:8-cli + +ENV PATH ${PATH}:/app/bin + +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-configure imap --with-kerberos --with-imap-ssl \ + && docker-php-ext-install imap zip + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +WORKDIR /app diff --git a/cli/docker-compose.yml b/cli/docker-compose.yml new file mode 100644 index 0000000..4099113 --- /dev/null +++ b/cli/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3' +services: + cli: + profiles: + - cli + container_name: cli + build: + context: ${CLI_PATH:-.} + restart: unless-stopped + env_file: + - ${CLI_PATH:-.}/.env + - .mail.env + volumes: + - ${CLI_PATH:-.}/:/app + - ./logs:/logs + - ${ATT_PATH}:/attachments From 02a68d9190fc65de883665fecc728037482de4ea Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:25:30 -0300 Subject: [PATCH 13/35] CLI Dependencies --- cli/composer.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 cli/composer.json diff --git a/cli/composer.json b/cli/composer.json new file mode 100644 index 0000000..9e939e3 --- /dev/null +++ b/cli/composer.json @@ -0,0 +1,31 @@ +{ + "name": "provm/emails", + "type": "project", + "require": { + "ddeboer/imap": "^1.14", + "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\\": "common/", + "Provm\\Emails\\": "src/" + } + }, + "authors": [ + { + "name": "Aldarien", + "email": "aldarien85@gmail.com" + } + ], + "config": { + "sort-packages": true + } +} From 11d8479a0c5422bdbd2cd01a3acfb2cccea513da Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 9 Nov 2022 15:25:59 -0300 Subject: [PATCH 14/35] CLI Base App --- cli/bin/emails | 3 ++ cli/public/index.php | 7 +++++ cli/setup/app.php | 43 ++++++++++++++++++++++++++++ cli/setup/composer.php | 6 ++++ cli/setup/middleware/02_commands.php | 9 ++++++ cli/setup/settings/01_env.php | 16 +++++++++++ cli/setup/settings/02_folders.php | 18 ++++++++++++ cli/setup/settings/98_log.php | 4 +++ cli/setup/setups/03_imap.php | 25 ++++++++++++++++ cli/setup/setups/98_log.php | 15 ++++++++++ 10 files changed, 146 insertions(+) create mode 100644 cli/bin/emails create mode 100644 cli/public/index.php create mode 100644 cli/setup/app.php create mode 100644 cli/setup/composer.php create mode 100644 cli/setup/middleware/02_commands.php create mode 100644 cli/setup/settings/01_env.php create mode 100644 cli/setup/settings/02_folders.php create mode 100644 cli/setup/settings/98_log.php create mode 100644 cli/setup/setups/03_imap.php create mode 100644 cli/setup/setups/98_log.php 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/public/index.php b/cli/public/index.php new file mode 100644 index 0000000..9f9ee18 --- /dev/null +++ b/cli/public/index.php @@ -0,0 +1,7 @@ +run(); \ No newline at end of file diff --git a/cli/setup/app.php b/cli/setup/app.php new file mode 100644 index 0000000..16c1e0f --- /dev/null +++ b/cli/setup/app.php @@ -0,0 +1,43 @@ +isDir()) { + continue; + } + $builder->addDefinitions($file->getRealPath()); + } +} + +$app = new \ProVM\Common\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..09b3241 --- /dev/null +++ b/cli/setup/settings/01_env.php @@ -0,0 +1,16 @@ + function() { + $data = [ + 'host' => $_ENV['EMAIL_HOST'], + 'username' => $_ENV['EMAIL_USERNAME'], + 'password' => $_ENV['EMAIL_PASSWORD'], + 'folder' => $_ENV['EMAIL_FOLDER'], + 'attachments' => $_ENV['ATTACHMENTS_FOLDER'], + ]; + if (isset($_ENV['EMAIL_PORT'])) { + $data['port'] = $_ENV['EMAIL_PORT']; + } + return json_decode(json_encode($data)); + } +]; diff --git a/cli/setup/settings/02_folders.php b/cli/setup/settings/02_folders.php new file mode 100644 index 0000000..0dd206a --- /dev/null +++ b/cli/setup/settings/02_folders.php @@ -0,0 +1,18 @@ + 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' + ]); + } +]; 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/03_imap.php b/cli/setup/setups/03_imap.php new file mode 100644 index 0000000..dc555ca --- /dev/null +++ b/cli/setup/setups/03_imap.php @@ -0,0 +1,25 @@ + function(ContainerInterface $container) { + $emails = $container->get('email'); + 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('email'); + $server = $container->get(\Ddeboer\Imap\ServerInterface::class); + return $server->authenticate($emails->username, $emails->password); + }, + \ProVM\Common\Service\Emails::class => function(ContainerInterface $container) { + return new \ProVM\Common\Service\Emails( + $container->get(\Ddeboer\Imap\ConnectionInterface::class), + $container->get(\Psr\Log\LoggerInterface::class), + $container->get('email')->folder, + $container->get('email')->attachments + ); + } +]; diff --git a/cli/setup/setups/98_log.php b/cli/setup/setups/98_log.php new file mode 100644 index 0000000..eaef107 --- /dev/null +++ b/cli/setup/setups/98_log.php @@ -0,0 +1,15 @@ + function(ContainerInterface $container) { + $handler = new \Monolog\Handler\RotatingFileHandler($container->get('log_file')); + $handler->setFormatter($container->get(\Monolog\Formatter\LineFormatter::class)); + return $handler; + }, + \Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { + $logger = new \Monolog\Logger('file_logger'); + $logger->pushHandler($container->get(\Monolog\Handler\RotatingFileHandler::class)); + return $logger; + } +]; \ No newline at end of file From dd0410a0fbe968bd2fa0ad9b35e2c1271c20fa8a Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 25 Nov 2022 20:52:46 -0300 Subject: [PATCH 15/35] CLI --- cli/Dockerfile | 9 +-- cli/common/Command/DecryptPdf.php | 67 +++++++++++++++++++++++ cli/common/Command/GrabAttachments.php | 64 ++++++++++++++++++++++ cli/common/Command/Messages.php | 66 ++++++++++++++++++++++ cli/common/Service/Communicator.php | 64 ++++++++++++++++++++++ cli/common/Wrapper/Application.php | 25 +++++++++ cli/composer.json | 5 +- cli/crontab | 3 + cli/docker-compose.yml | 7 +-- cli/public/index.php | 7 ++- cli/resources/commands/01_messages.php | 2 + cli/resources/commands/02_attachments.php | 3 + cli/setup/settings/01_env.php | 15 +---- cli/setup/setups/02_api.php | 13 +++++ cli/setup/setups/03_imap.php | 25 --------- 15 files changed, 325 insertions(+), 50 deletions(-) create mode 100644 cli/common/Command/DecryptPdf.php create mode 100644 cli/common/Command/GrabAttachments.php create mode 100644 cli/common/Command/Messages.php create mode 100644 cli/common/Service/Communicator.php create mode 100644 cli/common/Wrapper/Application.php create mode 100644 cli/crontab create mode 100644 cli/resources/commands/01_messages.php create mode 100644 cli/resources/commands/02_attachments.php create mode 100644 cli/setup/setups/02_api.php delete mode 100644 cli/setup/setups/03_imap.php diff --git a/cli/Dockerfile b/cli/Dockerfile index c006021..2ed89eb 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -3,11 +3,12 @@ FROM php:8-cli ENV PATH ${PATH}:/app/bin RUN apt-get update \ - && apt-get install -y libc-client-dev libkrb5-dev git libzip-dev unzip \ + && apt-get install -y cron git libzip-dev unzip \ && rm -r /var/lib/apt/lists/* \ - && docker-php-ext-configure imap --with-kerberos --with-imap-ssl \ - && docker-php-ext-install imap zip + && docker-php-ext-install zip -COPY --from=composer /usr/bin/composer /usr/bin/composer +COPY ./crontab /var/spool/cron/crontabs/root WORKDIR /app + +CMD [ "cron", "-f", "-L", "15" ] diff --git a/cli/common/Command/DecryptPdf.php b/cli/common/Command/DecryptPdf.php new file mode 100644 index 0000000..92027b6 --- /dev/null +++ b/cli/common/Command/DecryptPdf.php @@ -0,0 +1,67 @@ +setCommunicator($communicator); + parent::__construct($name); + } + + protected Communicator $communicator; + public function getCommunicator(): Communicator + { + return $this->communicator; + } + public function setCommunicator(Communicator $communicator): DecryptPdf + { + $this->communicator = $communicator; + return $this; + } + + protected function getAttachments(): array + { + $response = $this->getCommunicator()->get('/attachments/pending'); + return \Safe\json_decode($response->getBody()->getContents())->attachments; + } + protected function decrypt(string $attachment): bool + { + $response = $this->getCommunicator()->put('/attachments/decrypt', ['attachments' => [$attachment]]); + return \Safe\json_decode($response->getBody()->getContents())->status; + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $io->title('Decrypt Attachments'); + + $io->section('Grabbing Attachments'); + $attachments = $this->getAttachments(); + $io->text('Found ' . count($attachments) . ' attachments.'); + $io->section('Decrypting Attachments'); + foreach ($attachments as $attachment) { + $status = $this->decrypt($attachment); + if ($status) { + $io->success("{$attachment} decrypted correctly."); + } else { + $io->error("Problem decrypting {$attachment}."); + } + } + $io->success('Done.'); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/cli/common/Command/GrabAttachments.php b/cli/common/Command/GrabAttachments.php new file mode 100644 index 0000000..dbf60dd --- /dev/null +++ b/cli/common/Command/GrabAttachments.php @@ -0,0 +1,64 @@ +setCommunicator($communicator); + parent::__construct($name); + } + + protected Communicator $service; + public function getCommunicator(): Communicator + { + return $this->service; + } + public function setCommunicator(Communicator $service): GrabAttachments + { + $this->service = $service; + return $this; + } + + protected function getMessages(): array + { + $response = $this->getCommunicator()->get('/messages/pending'); + return \Safe\json_decode($response->getBody()->getContents())->messages; + } + protected function grabAttachments(int $message_uid): int + { + $response = $this->getCommunicator()->put('/attachments/grab', ['messages' => [$message_uid]]); + return \Safe\json_decode($response->getBody()->getContents())->attachment_count; + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title('Grab Attachments'); + + $io->section('Grabbing Messages'); + $messages = $this->getMessages(); + $io->text('Found ' . count($messages) . ' messages.'); + $io->section('Grabbing Attachments'); + foreach ($messages as $message) { + $attachments = $this->grabAttachments($message); + $io->text("Found {$attachments} attachments for message UID:{$message}."); + } + $io->success('Done.'); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/cli/common/Command/Messages.php b/cli/common/Command/Messages.php new file mode 100644 index 0000000..a09b493 --- /dev/null +++ b/cli/common/Command/Messages.php @@ -0,0 +1,66 @@ +setCommunicator($communicator); + parent::__construct($name); + } + + protected Communicator $communicator; + public function getCommunicator(): Communicator + { + return $this->communicator; + } + public function setCommunicator(Communicator $communicator): Messages + { + $this->communicator = $communicator; + return $this; + } + + protected function getMailboxes(): array + { + $response = $this->getCommunicator()->get('/mailboxes/registered'); + return \Safe\json_decode($response->getBody()->getContents())->mailboxes; + } + protected function grabMessages(string $mailbox): int + { + $response = $this->getCommunicator()->put('/messages/grab', ['mailboxes' => [$mailbox]]); + $body = \Safe\json_decode($response->getBody()->getContents()); + return $body->message_count; + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $io->title('Messages'); + $io->section('Grabbing Registered Mailboxes'); + + $mailboxes = $this->getMailboxes(); + $io->text('Found ' . count($mailboxes) . ' registered mailboxes.'); + $io->section('Grabbing Messages'); + foreach ($mailboxes as $mailbox) { + $message_count = $this->grabMessages($mailbox->name); + $io->text("Found {$message_count} messages in {$mailbox->name}."); + } + $io->success('Done.'); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/cli/common/Service/Communicator.php b/cli/common/Service/Communicator.php new file mode 100644 index 0000000..4f04122 --- /dev/null +++ b/cli/common/Service/Communicator.php @@ -0,0 +1,64 @@ +setClient($client); + } + + protected ClientInterface $client; + + public function getClient(): ClientInterface + { + return $this->client; + } + + public function setClient(ClientInterface $client): Communicator + { + $this->client = $client; + return $this; + } + + protected function handleResponse(ResponseInterface $response): ResponseInterface + { + if ($response->getStatusCode() < 200 or $response->getStatusCode() >= 300) { + throw new HttpResponseException($response->getStatusCode(), $response->getReasonPhrase()); + } + return $response; + } + 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->getClient()->request($method, $uri, $options)); + } + + public function get(string $uri): ResponseInterface + { + return $this->request('get', $uri); + } + public function post(string $uri, array $data): ResponseInterface + { + return $this->request('post', $uri, $data); + } + public function put(string $uri, array $data): ResponseInterface + { + return $this->request('put', $uri, $data); + } + public function delete(string $uri, array $data): ResponseInterface + { + return $this->request('delete', $uri, $data); + } +} \ No newline at end of file diff --git a/cli/common/Wrapper/Application.php b/cli/common/Wrapper/Application.php new file mode 100644 index 0000000..8d8e4aa --- /dev/null +++ b/cli/common/Wrapper/Application.php @@ -0,0 +1,25 @@ +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; + } +} \ No newline at end of file diff --git a/cli/composer.json b/cli/composer.json index 9e939e3..2c1d0c1 100644 --- a/cli/composer.json +++ b/cli/composer.json @@ -2,7 +2,7 @@ "name": "provm/emails", "type": "project", "require": { - "ddeboer/imap": "^1.14", + "guzzlehttp/guzzle": "^7.5", "monolog/monolog": "^3.2", "php-di/php-di": "^6.4", "symfony/console": "^6.1", @@ -15,8 +15,7 @@ }, "autoload": { "psr-4": { - "ProVM\\Common\\": "common/", - "Provm\\Emails\\": "src/" + "ProVM\\Common\\": "common/" } }, "authors": [ diff --git a/cli/crontab b/cli/crontab new file mode 100644 index 0000000..49d2722 --- /dev/null +++ b/cli/crontab @@ -0,0 +1,3 @@ +# 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 diff --git a/cli/docker-compose.yml b/cli/docker-compose.yml index 4099113..38d19e3 100644 --- a/cli/docker-compose.yml +++ b/cli/docker-compose.yml @@ -3,14 +3,13 @@ services: cli: profiles: - cli - container_name: cli + container_name: emails-cli build: context: ${CLI_PATH:-.} restart: unless-stopped env_file: - ${CLI_PATH:-.}/.env - - .mail.env + - .key.env volumes: - ${CLI_PATH:-.}/:/app - - ./logs:/logs - - ${ATT_PATH}:/attachments + - ./logs/cli:/logs diff --git a/cli/public/index.php b/cli/public/index.php index 9f9ee18..daaaaed 100644 --- a/cli/public/index.php +++ b/cli/public/index.php @@ -4,4 +4,9 @@ $app = require_once implode(DIRECTORY_SEPARATOR, [ 'setup', 'app.php' ]); -$app->run(); \ No newline at end of file +try { + $app->run(); +} catch (Error | Exception $e) { + $app->getContainer()->get(\Psr\Log\LoggerInterface::class)->error($e); + throw $e; +} diff --git a/cli/resources/commands/01_messages.php b/cli/resources/commands/01_messages.php new file mode 100644 index 0000000..c034ee1 --- /dev/null +++ b/cli/resources/commands/01_messages.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(\ProVM\Common\Command\Messages::class)); diff --git a/cli/resources/commands/02_attachments.php b/cli/resources/commands/02_attachments.php new file mode 100644 index 0000000..85793e1 --- /dev/null +++ b/cli/resources/commands/02_attachments.php @@ -0,0 +1,3 @@ +add($app->getContainer()->get(\ProVM\Common\Command\GrabAttachments::class)); +$app->add($app->getContainer()->get(\ProVM\Common\Command\DecryptPdf::class)); diff --git a/cli/setup/settings/01_env.php b/cli/setup/settings/01_env.php index 09b3241..58b2a65 100644 --- a/cli/setup/settings/01_env.php +++ b/cli/setup/settings/01_env.php @@ -1,16 +1,5 @@ function() { - $data = [ - 'host' => $_ENV['EMAIL_HOST'], - 'username' => $_ENV['EMAIL_USERNAME'], - 'password' => $_ENV['EMAIL_PASSWORD'], - 'folder' => $_ENV['EMAIL_FOLDER'], - 'attachments' => $_ENV['ATTACHMENTS_FOLDER'], - ]; - if (isset($_ENV['EMAIL_PORT'])) { - $data['port'] = $_ENV['EMAIL_PORT']; - } - return json_decode(json_encode($data)); - } + 'api_uri' => $_ENV['API_URI'], + 'api_key' => sha1($_ENV['API_KEY']) ]; diff --git a/cli/setup/setups/02_api.php b/cli/setup/setups/02_api.php new file mode 100644 index 0000000..b123345 --- /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')}" + ] + ]); + } +]; \ No newline at end of file diff --git a/cli/setup/setups/03_imap.php b/cli/setup/setups/03_imap.php deleted file mode 100644 index dc555ca..0000000 --- a/cli/setup/setups/03_imap.php +++ /dev/null @@ -1,25 +0,0 @@ - function(ContainerInterface $container) { - $emails = $container->get('email'); - 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('email'); - $server = $container->get(\Ddeboer\Imap\ServerInterface::class); - return $server->authenticate($emails->username, $emails->password); - }, - \ProVM\Common\Service\Emails::class => function(ContainerInterface $container) { - return new \ProVM\Common\Service\Emails( - $container->get(\Ddeboer\Imap\ConnectionInterface::class), - $container->get(\Psr\Log\LoggerInterface::class), - $container->get('email')->folder, - $container->get('email')->attachments - ); - } -]; From efed50cd7f98713e5c93f2111a95af276434b3ab Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 25 Nov 2022 20:52:52 -0300 Subject: [PATCH 16/35] API --- api/common/Controller/Attachments.php | 60 +++++ api/common/Controller/Emails.php | 141 ++++++++++++ api/common/Controller/Install.php | 28 +++ api/common/Controller/Mailboxes.php | 83 +++++++ api/common/Controller/Messages.php | 117 ++++++++++ api/common/Define/Model.php | 6 + api/common/Exception/Database/BlankResult.php | 14 ++ api/common/Exception/Mailbox/EmptyMailbox.php | 14 ++ api/common/Exception/Mailbox/Invalid.php | 14 ++ api/common/Factory/Model.php | 58 +++++ api/common/Implement/Controller/Json.php | 16 ++ api/common/Implement/Repository.php | 185 +++++++++++++++ api/common/Middleware/Auth.php | 71 ++++++ api/common/Middleware/CORS.php | 20 ++ api/common/Service/Attachments.php | 159 +++++++++++++ api/common/Service/Base.php | 27 +++ api/common/Service/Decrypt.php | 91 ++++++++ api/common/Service/Emails.php | 158 +++++++++++++ api/common/Service/Install.php | 73 ++++++ api/common/Service/Mailboxes.php | 151 +++++++++++++ api/common/Service/Messages.php | 37 +++ api/public/index.php | 9 +- api/resources/routes/01_mailboxes.php | 13 ++ api/resources/routes/02_messages.php | 8 + api/setup/setups/02_services.php | 3 +- api/setup/setups/03_factories.php | 17 ++ api/setup/setups/98_log.php | 11 +- api/src/Model/Attachment.php | 124 ++++++++++ api/src/Model/Mailbox.php | 97 ++++++++ api/src/Model/Message.php | 212 ++++++++++++++++++ api/src/Model/State/Attachment.php | 50 +++++ api/src/Model/State/Mailbox.php | 68 ++++++ api/src/Model/State/Message.php | 50 +++++ api/src/Repository/Attachment.php | 95 ++++++++ api/src/Repository/Mailbox.php | 85 +++++++ api/src/Repository/Message.php | 155 +++++++++++++ api/src/Repository/State/Attachment.php | 80 +++++++ api/src/Repository/State/Mailbox.php | 102 +++++++++ api/src/Repository/State/Message.php | 80 +++++++ 39 files changed, 2777 insertions(+), 5 deletions(-) create mode 100644 api/common/Controller/Attachments.php create mode 100644 api/common/Controller/Emails.php create mode 100644 api/common/Controller/Install.php create mode 100644 api/common/Controller/Mailboxes.php create mode 100644 api/common/Controller/Messages.php create mode 100644 api/common/Define/Model.php create mode 100644 api/common/Exception/Database/BlankResult.php create mode 100644 api/common/Exception/Mailbox/EmptyMailbox.php create mode 100644 api/common/Exception/Mailbox/Invalid.php create mode 100644 api/common/Factory/Model.php create mode 100644 api/common/Implement/Controller/Json.php create mode 100644 api/common/Implement/Repository.php create mode 100644 api/common/Middleware/Auth.php create mode 100644 api/common/Middleware/CORS.php create mode 100644 api/common/Service/Attachments.php create mode 100644 api/common/Service/Base.php create mode 100644 api/common/Service/Decrypt.php create mode 100644 api/common/Service/Emails.php create mode 100644 api/common/Service/Install.php create mode 100644 api/common/Service/Mailboxes.php create mode 100644 api/common/Service/Messages.php create mode 100644 api/resources/routes/01_mailboxes.php create mode 100644 api/resources/routes/02_messages.php create mode 100644 api/setup/setups/03_factories.php create mode 100644 api/src/Model/Attachment.php create mode 100644 api/src/Model/Mailbox.php create mode 100644 api/src/Model/Message.php create mode 100644 api/src/Model/State/Attachment.php create mode 100644 api/src/Model/State/Mailbox.php create mode 100644 api/src/Model/State/Message.php create mode 100644 api/src/Repository/Attachment.php create mode 100644 api/src/Repository/Mailbox.php create mode 100644 api/src/Repository/Message.php create mode 100644 api/src/Repository/State/Attachment.php create mode 100644 api/src/Repository/State/Mailbox.php create mode 100644 api/src/Repository/State/Message.php 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 From 9d1a1cc0f263c9c3ca252dc21034018332aad678 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 25 Nov 2022 20:52:59 -0300 Subject: [PATCH 17/35] UI --- ui/common/Controller/Emails.php | 18 + ui/common/Controller/Home.php | 14 + ui/resources/routes/01_emails.php | 10 + ui/resources/routes/99_home.php | 4 + ui/resources/views/emails/base.blade.php | 14 + ui/resources/views/emails/mailboxes.blade.php | 156 +++++++ ui/resources/views/emails/messages.blade.php | 428 ++++++++++++++++++ ui/resources/views/home.blade.php | 57 +++ ui/resources/views/layout/base.blade.php | 5 + ui/resources/views/layout/body.blade.php | 7 + .../views/layout/body/footer.blade.php | 4 + .../layout/body/footer/scripts.blade.php | 6 + .../layout/body/footer/scripts/main.blade.php | 38 ++ .../views/layout/body/header.blade.php | 5 + .../views/layout/body/header/navbar.blade.php | 5 + ui/resources/views/layout/head.blade.php | 11 + .../views/layout/head/styles.blade.php | 3 + ui/setup/settings/03_urls.php | 2 +- 18 files changed, 786 insertions(+), 1 deletion(-) create mode 100644 ui/common/Controller/Emails.php create mode 100644 ui/common/Controller/Home.php create mode 100644 ui/resources/routes/01_emails.php create mode 100644 ui/resources/routes/99_home.php create mode 100644 ui/resources/views/emails/base.blade.php create mode 100644 ui/resources/views/emails/mailboxes.blade.php create mode 100644 ui/resources/views/emails/messages.blade.php create mode 100644 ui/resources/views/home.blade.php create mode 100644 ui/resources/views/layout/base.blade.php create mode 100644 ui/resources/views/layout/body.blade.php create mode 100644 ui/resources/views/layout/body/footer.blade.php create mode 100644 ui/resources/views/layout/body/footer/scripts.blade.php create mode 100644 ui/resources/views/layout/body/footer/scripts/main.blade.php create mode 100644 ui/resources/views/layout/body/header.blade.php create mode 100644 ui/resources/views/layout/body/header/navbar.blade.php create mode 100644 ui/resources/views/layout/head.blade.php create mode 100644 ui/resources/views/layout/head/styles.blade.php diff --git a/ui/common/Controller/Emails.php b/ui/common/Controller/Emails.php new file mode 100644 index 0000000..a7c7efa --- /dev/null +++ b/ui/common/Controller/Emails.php @@ -0,0 +1,18 @@ +render($response, 'emails.mailboxes'); + } + public function messages(ServerRequestInterface $request, ResponseInterface $response, View $view, string $mailbox): ResponseInterface + { + return $view->render($response, 'emails.messages', ['mailbox_id' => $mailbox]); + } +} \ 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/resources/routes/01_emails.php b/ui/resources/routes/01_emails.php new file mode 100644 index 0000000..a5d9f2d --- /dev/null +++ b/ui/resources/routes/01_emails.php @@ -0,0 +1,10 @@ +group('/emails', function($app) { + $app->group('/mailbox/{mailbox}', function ($app) { + $app->get('[/]', [Emails::class, 'messages']); + }); + $app->get('/mailboxes', Emails::class); + $app->get('[/]', Emails::class); +}); diff --git a/ui/resources/routes/99_home.php b/ui/resources/routes/99_home.php new file mode 100644 index 0000000..f989a1e --- /dev/null +++ b/ui/resources/routes/99_home.php @@ -0,0 +1,4 @@ +get('[/]', Home::class); \ No newline at end of file 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..fea91fd --- /dev/null +++ b/ui/resources/views/emails/mailboxes.blade.php @@ -0,0 +1,156 @@ +@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..a16c0f3 --- /dev/null +++ b/ui/resources/views/emails/messages.blade.php @@ -0,0 +1,428 @@ +@extends('emails.base') + +@section('emails_content') +

Messages -

+
+@endsection + +@push('page_scripts') + +@endpush \ No newline at end of file diff --git a/ui/resources/views/home.blade.php b/ui/resources/views/home.blade.php new file mode 100644 index 0000000..d5f0851 --- /dev/null +++ b/ui/resources/views/home.blade.php @@ -0,0 +1,57 @@ +@extends('layout.base') + +@section('page_content') +

Registered Mailboxes

+
+@endsection + +@push('page_scripts') + +@endpush \ No newline at end of file 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..e6a57e3 --- /dev/null +++ b/ui/resources/views/layout/body/footer/scripts/main.blade.php @@ -0,0 +1,38 @@ + \ No newline at end of file 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..92fa22b --- /dev/null +++ b/ui/resources/views/layout/body/header/navbar.blade.php @@ -0,0 +1,5 @@ + +
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/settings/03_urls.php b/ui/setup/settings/03_urls.php index b0ca32d..71f5383 100644 --- a/ui/setup/settings/03_urls.php +++ b/ui/setup/settings/03_urls.php @@ -1,7 +1,7 @@ function() { - $arr = ['base' => '/']; + $arr = ['base' => $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST']]; $arr['api'] = 'http://localhost:8080'; return (object) $arr; } From ad3cd6df48e8591f8f59388aa3d43f001f2417d1 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 25 Nov 2022 20:53:13 -0300 Subject: [PATCH 18/35] Notes --- NOTES.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 NOTES.md diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..0ac9946 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,28 @@ +## 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`. +* [ ] Schedule `attachments` downloads. +* [ ] Download `attachments` (*encrypted* & *decrypted*). + +## CLI +* [x] Get `mailboxes` from **[API]** then run `grab messages` job in **[API]** for each one. +* [x] Get `pending attachments` jobs from **[API]** and run. + +## API +* [x] Grab all `mailboxes` from `Email Provider`, identifying those that are registered. +* [x] Register `mailboxes` into **[database]**. +* [x] Grab new `messages` from `Email Provider` for selected `mailboxes` and store them in the `database`. +* [x] Grab `messages` from **[database]** for selected `mailboxes`. +* [ ] Grab `attachments` from `Email Provider` for selected `messages`. +* [ ] Decrypt `attachments`. +* [ ] Register `messages` for `attachment` job. + + +## Workflow +* **[User]** Choose `mailboxes` to register or unregister + -> **[API]** Register selected `mailboxes` and get `messages` for recently registered. +* **[Cron]** Get registered `mailboxes` -> **[API]** Get `messages` +* **[User]** Check messages found -> **[API]** Schedule `attachments` +* **[Cron]** Get `attachment download` jobs -> **[API]** grab `attachments` \ No newline at end of file From 30ef4c6a356b1bfceaaf94c670ddad147fb63613 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 25 Nov 2022 20:53:52 -0300 Subject: [PATCH 19/35] Empty Nginx default configuration --- default.conf | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 default.conf 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; +# #} +# } + From c53eb4c7a695dc926969e477176a557eed2f4c41 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Mon, 28 Nov 2022 22:56:21 -0300 Subject: [PATCH 20/35] Full implemantation --- NOTES.md | 10 +- README.md | 25 +- api/common/Controller/Attachments.php | 68 ++--- api/common/Controller/Base.php | 18 ++ api/common/Controller/Emails.php | 141 --------- api/common/Controller/Install.php | 28 -- api/common/Controller/Jobs.php | 45 +++ api/common/Controller/Mailboxes.php | 141 +++++---- api/common/Controller/Messages.php | 156 ++++------ api/common/Exception/Attachment/NotFound.php | 15 + api/common/Exception/Mailbox/EmptyMailbox.php | 7 +- api/common/Exception/Mailbox/Invalid.php | 4 +- api/common/Exception/Mailbox/Stateless.php | 15 + .../Exception/Message/NoAttachments.php | 15 + .../Exception/Request/MissingArgument.php | 14 + api/common/Implement/Repository.php | 12 +- api/common/Middleware/Auth.php | 1 + api/common/Middleware/CustomExceptions.php | 58 ++++ api/common/Service/Attachments.php | 289 +++++++++++------- api/common/Service/Emails.php | 158 ---------- api/common/Service/Install.php | 73 ----- api/common/Service/Jobs.php | 71 +++++ api/common/Service/Mailboxes.php | 194 ++++++------ api/common/Service/Messages.php | 168 +++++++++- api/common/Service/Remote/Attachments.php | 31 ++ api/common/Service/Remote/Base.php | 21 ++ api/common/Service/Remote/Mailboxes.php | 45 +++ api/common/Service/Remote/Messages.php | 67 ++++ api/resources/routes/01_emails.php | 12 - api/resources/routes/01_mailboxes.php | 11 +- api/resources/routes/02_attachments.php | 2 +- api/resources/routes/02_messages.php | 5 +- api/resources/routes/98_install.php | 4 - api/resources/routes/99_base.php | 4 + api/setup/middleware/02_cors.php | 2 +- api/setup/middleware/03_custom_exceptions.php | 2 + api/setup/middleware/97_auth.php | 2 +- api/setup/middleware/99_errors.php | 2 +- api/setup/setups/02_services.php | 41 +-- api/setup/setups/04_middlewares.php | 11 + api/src/Model/Attachment.php | 61 +++- api/src/Model/Job.php | 61 ++++ api/src/Model/Mailbox.php | 29 +- api/src/Model/Message.php | 7 +- api/src/Model/State/Attachment.php | 2 +- api/src/Repository/Attachment.php | 83 +++-- api/src/Repository/Job.php | 108 +++++++ api/src/Repository/Mailbox.php | 10 +- api/src/Repository/Message.php | 32 +- api/src/Repository/State/Attachment.php | 19 +- api/src/Repository/State/Message.php | 17 +- cli/common/Command/GrabAttachments.php | 7 +- ui/resources/views/emails/mailboxes.blade.php | 25 +- ui/resources/views/emails/messages.blade.php | 54 ++-- ui/resources/views/home.blade.php | 13 +- 55 files changed, 1505 insertions(+), 1011 deletions(-) create mode 100644 api/common/Controller/Base.php delete mode 100644 api/common/Controller/Emails.php delete mode 100644 api/common/Controller/Install.php create mode 100644 api/common/Controller/Jobs.php create mode 100644 api/common/Exception/Attachment/NotFound.php create mode 100644 api/common/Exception/Mailbox/Stateless.php create mode 100644 api/common/Exception/Message/NoAttachments.php create mode 100644 api/common/Exception/Request/MissingArgument.php create mode 100644 api/common/Middleware/CustomExceptions.php delete mode 100644 api/common/Service/Emails.php delete mode 100644 api/common/Service/Install.php create mode 100644 api/common/Service/Jobs.php create mode 100644 api/common/Service/Remote/Attachments.php create mode 100644 api/common/Service/Remote/Base.php create mode 100644 api/common/Service/Remote/Mailboxes.php create mode 100644 api/common/Service/Remote/Messages.php delete mode 100644 api/resources/routes/01_emails.php delete mode 100644 api/resources/routes/98_install.php create mode 100644 api/resources/routes/99_base.php create mode 100644 api/setup/middleware/03_custom_exceptions.php create mode 100644 api/setup/setups/04_middlewares.php create mode 100644 api/src/Model/Job.php create mode 100644 api/src/Repository/Job.php diff --git a/NOTES.md b/NOTES.md index 0ac9946..d20957b 100644 --- a/NOTES.md +++ b/NOTES.md @@ -3,7 +3,7 @@ * [x] Select which to *register* or unregister for watching. * [x] List *registered* `mailboxes`. * [x] List `messages` for selected `mailbox`. -* [ ] Schedule `attachments` downloads. +* [x] Schedule `attachments` downloads. * [ ] Download `attachments` (*encrypted* & *decrypted*). ## CLI @@ -12,12 +12,12 @@ ## API * [x] Grab all `mailboxes` from `Email Provider`, identifying those that are registered. -* [x] Register `mailboxes` into **[database]**. +* [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`. -* [ ] Grab `attachments` from `Email Provider` for selected `messages`. -* [ ] Decrypt `attachments`. -* [ ] Register `messages` for `attachment` job. +* [x] Grab `attachments` from `Email Provider` for selected `messages`. +* [x] Register `messages` for `attachment` job. +* [x] Decrypt `attachments`. ## Workflow diff --git a/README.md b/README.md index ae99296..06f690c 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,8 @@ # Emails ProVM -## API - -| Concept | Model | Repository | Table | -|------------|--------------------|--------------------|--------------------| -| Message | | | | -| Attachment | | | | -| Mailbox | | | | - -## UI -+ [ ] Mailboxes - + [ ] Register -+ [ ] Messages -+ [ ] Attachments -## CLI -+ [ ] Mailboxes - + Scrub -+ [ ] Messages - + Grab Attachments -+ [ ] Attachments - + Decrypt \ No newline at end of file +## 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. diff --git a/api/common/Controller/Attachments.php b/api/common/Controller/Attachments.php index e7dd760..5b33445 100644 --- a/api/common/Controller/Attachments.php +++ b/api/common/Controller/Attachments.php @@ -1,11 +1,12 @@ 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() + $attachments = array_map(function(Attachment $attachment) { + return $attachment->toArray(); + },$service->getAll()); + $output = [ + 'total' => count($attachments), + 'attachments' => $attachments ]; - } - 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 + public function grab(ServerRequestInterface $request, ResponseInterface $response, Service $service, \ProVM\Common\Service\Jobs $jobsService): ResponseInterface { - $body = $request->getBody()->getContents(); - $json = json_decode($body); - if (!is_array($json)) { - $json = [$json]; + $body = $request->getBody(); + $json = \Safe\json_decode($body->getContents()); + if (!isset($json->messages)) { + throw new MissingArgument('messages', 'array', 'message UIDs'); } - $output = ['input' => $json, 'attachments' => []]; - foreach ($json as $attachment) { - $output['attachments'] []= $this->fileToArray($service->removeEncryption($attachment)); + $output = [ + 'messages' => $json->messages, + 'total' => count($json->messages), + 'saved' => [ + 'attachments' => [], + 'total' => 0 + ] + ]; + foreach ($json->messages as $message_id) { + if (!$jobsService->isPending($message_id)) { + continue; + } + if ($service->grab($message_id)) { + $job = $jobsService->find($message_id); + $jobsService->execute($job->getId()); + $output['saved']['attachments'] []= $job->toArray(); + $output['saved']['total'] ++; + } } 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..8611788 --- /dev/null +++ b/api/common/Controller/Base.php @@ -0,0 +1,18 @@ +withJson($response, [ + 'version' => '1.0.0' + ]); + } +} \ No newline at end of file diff --git a/api/common/Controller/Emails.php b/api/common/Controller/Emails.php deleted file mode 100644 index 5137841..0000000 --- a/api/common/Controller/Emails.php +++ /dev/null @@ -1,141 +0,0 @@ -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 deleted file mode 100644 index 238838b..0000000 --- a/api/common/Controller/Install.php +++ /dev/null @@ -1,28 +0,0 @@ -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/Jobs.php b/api/common/Controller/Jobs.php new file mode 100644 index 0000000..d0eca33 --- /dev/null +++ b/api/common/Controller/Jobs.php @@ -0,0 +1,45 @@ +getBody(); + $json = \Safe\json_decode($body->getContents()); + if (!isset($json->messages)) { + throw new MissingArgument('messages', 'array', 'messages ids'); + } + $output = [ + 'messages' => $json->messages, + 'total' => count($json->messages), + 'scheduled' => 0 + ]; + foreach ($json->messages as $message_id) { + if ($jobsService->schedule($message_id)) { + $output['scheduled'] ++; + } + } + return $this->withJson($response, $output); + } + public function pending(ServerRequestInterface $request, ResponseInterface $response, Service $jobsService): ResponseInterface + { + $pending = array_map(function(Job $job) { + return $job->toArray(); + }, $jobsService->getPending()); + $output = [ + 'total' => count($pending), + 'pending' => $pending + ]; + return $this->withJson($response, $output); + } +} \ No newline at end of file diff --git a/api/common/Controller/Mailboxes.php b/api/common/Controller/Mailboxes.php index d29dc26..97fb792 100644 --- a/api/common/Controller/Mailboxes.php +++ b/api/common/Controller/Mailboxes.php @@ -1,83 +1,100 @@ 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 = array_values(array_map(function(MailboxInterface $mailbox) use ($service) { + $arr = ['name' => $mailbox->getName(), 'registered' => false]; + if ($service->isRegistered($mailbox->getName())) { + $mb = $service->getLocalMailbox($mailbox->getName()); + $arr['id'] = $mb->getId(); + $arr['registered'] = true; } - $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 $arr; + }, $service->getAll())); + $output = [ + 'total' => count($mailboxes), + 'mailboxes' => $mailboxes + ]; return $this->withJson($response, $output); } - public function unregister(ServerRequestInterface $request, ResponseInterface $response, Mailbox $repository): ResponseInterface + public function registered(ServerRequestInterface $request, ResponseInterface $response, Service $service): 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) { + $mailboxes = array_map(function(Mailbox $mailbox) { return $mailbox->toArray(); - }, $data); - return $this->withJson($response, compact('mailboxes')); + }, $service->getRegistered()); + $output = [ + 'total' => count($mailboxes), + 'mailboxes' => $mailboxes + ]; + return $this->withJson($response, $output); } - public function get(ServerRequestInterface $request, ResponseInterface $response, Mailbox $repository, int $mailbox_id): ResponseInterface + public function get(ServerRequestInterface $request, ResponseInterface $response, Service $service, int $mailbox_id): ResponseInterface { - $mailbox = $repository->fetchById($mailbox_id); + $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 names'); + } + $output = [ + 'mailboxes' => $json->mailboxes, + 'total' => count($json->mailboxes), + 'unregistered' => 0 + ]; + foreach ($json->mailboxes as $mailbox_name) { + if ($service->unregister($mailbox_name)) { + $output['unregistered'] ++; + } + } + 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 index f4e2507..47425ef 100644 --- a/api/common/Controller/Messages.php +++ b/api/common/Controller/Messages.php @@ -1,117 +1,73 @@ 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) { + $mailbox = $service->getMailboxes()->get($mailbox_id); + $messages = array_map(function(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') + }, $service->getAll($mailbox->getName())); + $output = [ + 'mailbox' => $mailbox->toArray(), + 'total' => count($messages), + 'messages' => $messages ]; - $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 $this->withJson($response, $output); + } + public function valid(ServerRequestInterface $request, ResponseInterface $response, Service $service, \ProVM\Common\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; + })); + $output = [ + 'mailbox' => $mailbox->toArray(), + 'total' => count($messages), + 'messages' => $messages + ]; + return $this->withJson($response, $output); + } + public function get(ServerRequestInterface $request, ResponseInterface $response, Service $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 $service, \ProVM\Common\Service\Attachments $attachmentsService): ResponseInterface + { + $body = $request->getBody(); + $json = \Safe\json_decode($body->getContents()); + if (!isset($json->mailboxes)) { + throw new MissingArgument('mailboxes', 'array', 'mailboxes names'); } - return false; + $output = [ + 'mailboxes' => $json->mailboxes, + 'messages' => [], + 'message_count' => 0 + ]; + foreach ($json->mailboxes as $mailbox_name) { + $messages = $service->grab($mailbox_name); + foreach ($messages as $message) { + if ($message->hasValidAttachments()) { + $attachmentsService->create($message); + } + } + $output['messages'] = array_merge($output['messages'], $messages); + $output['message_count'] += count($messages); + } + return $this->withJson($response, $output); } } \ No newline at end of file diff --git a/api/common/Exception/Attachment/NotFound.php b/api/common/Exception/Attachment/NotFound.php new file mode 100644 index 0000000..626a9f8 --- /dev/null +++ b/api/common/Exception/Attachment/NotFound.php @@ -0,0 +1,15 @@ +getId()}"; + $code = 120; + parent::__construct($message, $code, $previous); + } +} \ No newline at end of file diff --git a/api/common/Exception/Mailbox/EmptyMailbox.php b/api/common/Exception/Mailbox/EmptyMailbox.php index c5f1de1..50085fd 100644 --- a/api/common/Exception/Mailbox/EmptyMailbox.php +++ b/api/common/Exception/Mailbox/EmptyMailbox.php @@ -1,14 +1,15 @@ getName()}"; + $code = 101; parent::__construct($message, $code, $previous); } } \ No newline at end of file diff --git a/api/common/Exception/Mailbox/Invalid.php b/api/common/Exception/Mailbox/Invalid.php index 998a105..075ca3c 100644 --- a/api/common/Exception/Mailbox/Invalid.php +++ b/api/common/Exception/Mailbox/Invalid.php @@ -5,9 +5,9 @@ use Exception; class Invalid extends Exception { - public function __construct(?Throwable $previous = null) + public function __construct(string $mailbox_name, ?Throwable $previous = null) { - $message = "Mailbox not found"; + $message = "Mailbox {$mailbox_name} not found"; $code = 100; parent::__construct($message, $code, $previous); } diff --git a/api/common/Exception/Mailbox/Stateless.php b/api/common/Exception/Mailbox/Stateless.php new file mode 100644 index 0000000..ab18626 --- /dev/null +++ b/api/common/Exception/Mailbox/Stateless.php @@ -0,0 +1,15 @@ +getName()} has not loaded any emails."; + $code = 102; + parent::__construct($message, $code, $previous); + } +} \ No newline at end of file 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/MissingArgument.php b/api/common/Exception/Request/MissingArgument.php new file mode 100644 index 0000000..5466e0e --- /dev/null +++ b/api/common/Exception/Request/MissingArgument.php @@ -0,0 +1,14 @@ +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]; + 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) { @@ -101,8 +102,9 @@ abstract class Repository } catch (BlankResult $e) { $this->insert($model); $model->setId($this->getConnection()->lastInsertId()); - } catch (\Error | \Exception $e) { + } catch(PDOException $e) { $this->getLogger()->error($e); + throw $e; } } abstract public function load(array $row): ModelInterface; diff --git a/api/common/Middleware/Auth.php b/api/common/Middleware/Auth.php index 0ff78ed..f62c8e4 100644 --- a/api/common/Middleware/Auth.php +++ b/api/common/Middleware/Auth.php @@ -63,6 +63,7 @@ class Auth } } } + $this->getLogger()->debug(sha1($this->getAPIKey())); $response = $this->getResponseFactory()->createResponse(401); $response->getBody()->write(\Safe\json_encode(['error' => 401, 'message' => 'Incorrect token'])); return $response 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/Service/Attachments.php b/api/common/Service/Attachments.php index 3843819..0c2a6a0 100644 --- a/api/common/Service/Attachments.php +++ b/api/common/Service/Attachments.php @@ -3,37 +3,65 @@ namespace ProVM\Common\Service; use Ddeboer\Imap\Message\AttachmentInterface; use Ddeboer\Imap\MessageInterface; -use ProVM\Emails\Model\Attachment; +use ProVM\Common\Exception\Message\NoAttachments; use ProVM\Emails\Model\Message; +use ProVM\Emails\Repository\Attachment; +use Psr\Log\LoggerInterface; +use Safe\Exceptions\FilesystemException; -class Attachments +class Attachments extends Base { - public function __construct(Decrypt $decrypt, \ProVM\Emails\Repository\Attachment $repository, string $attachments_folder) + public function __construct(Messages $messages, Attachment $repository, Remote\Attachments $remoteService, + Decrypt $decrypt, string $attachments_folder, LoggerInterface $logger) { - $this->setDecrypt($decrypt); - $this->setAttachmentsFolder($attachments_folder); + $this->setMessages($messages) + ->setRepository($repository) + ->setRemoteService($remoteService) + ->setDecrypt($decrypt) + ->setFolder($attachments_folder) + ->setLogger($logger); } - protected string $attachments_folder; + protected Messages $messages; + protected Attachment $repository; + protected Remote\Attachments $remoteService; protected Decrypt $decrypt; - protected \ProVM\Emails\Repository\Attachment $repository; + protected string $folder; - public function getAttachmentsFolder(): string + public function getMessages(): Messages { - return $this->attachments_folder; + 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 getRepository(): \ProVM\Emails\Repository\Attachment + public function getFolder(): string { - return $this->repository; + return $this->folder; } - public function setAttachmentsFolder(string $folder): Attachments + public function setMessages(Messages $messages): Attachments { - $this->attachments_folder = $folder; + $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 @@ -41,119 +69,164 @@ class Attachments $this->decrypt = $decrypt; return $this; } - public function setRepository(\ProVM\Emails\Repository\Attachment $repository): Attachments + public function setFolder(string $folder): Attachments { - $this->repository = $repository; + $this->folder = $folder; return $this; } - public function validateAttachment(AttachmentInterface $remote_attachment): bool + public function getLocalAttachment(Message $message, string $relative_filename): \ProVM\Emails\Model\Attachment { - return str_contains($remote_attachment->getFilename(), '.pdf'); + 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 buildAttachment(Message $message, AttachmentInterface $remote_attachment): Attachment + public function getAll(): array + { + return $this->getRepository()->fetchAll(); + } + 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' => $this->buildFilename($message, $remote_attachment) + 'filename' => $remote_attachment->getFilename() ]; - return $this->getRepository()->create($data); - } + 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); - public function exists(Attachment $attachment): bool - { - $filename = $this->buildFilename($attachment); - return file_exists($filename); - } + if ($this->isFileEncrypted($attachment->getMessage(), $attachment->getFilename())) { + $attachment->itIsEncrypted(); - 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; + if ($this->isFileDecrypted($attachment->getMessage(), $attachment->getFilename())) { + $attachment->itIsDecrypted(); + } else { + if ($this->decrypt($attachment->getMessage(), $attachment->getFilename())) { + $attachment->itIsDecrypted(); + } + } + } } - $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)) { + $this->getRepository()->save($attachment); + return true; + } catch (PDOException $e) { + $this->getLogger()->error($e); return false; } - return new \SplFileInfo($new_file); } - - public function removeEncryption(string $filename): \SplFileInfo + public function upload(\ProVM\Emails\Model\Attachment $attachment, AttachmentInterface $remote_attachment): bool { - $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); + $destination = implode(DIRECTORY_SEPARATOR, [ + $this->getFolder(), + $attachment->getFullFilename() + ]); + try { + \Safe\file_put_contents($destination, $remote_attachment->getDecodedContent()); + return true; + } catch (FilesystemException $e) { + $this->getLogger()->error($e); + return false; } - return $attachment; + } + 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; + } + 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; } } \ No newline at end of file diff --git a/api/common/Service/Emails.php b/api/common/Service/Emails.php deleted file mode 100644 index 85a3c40..0000000 --- a/api/common/Service/Emails.php +++ /dev/null @@ -1,158 +0,0 @@ -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 deleted file mode 100644 index 8a5bb02..0000000 --- a/api/common/Service/Install.php +++ /dev/null @@ -1,73 +0,0 @@ -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/Jobs.php b/api/common/Service/Jobs.php new file mode 100644 index 0000000..13ef3d8 --- /dev/null +++ b/api/common/Service/Jobs.php @@ -0,0 +1,71 @@ +setRepository($repository); + } + + protected Job $repository; + + public function getRepository(): Job + { + return $this->repository; + } + + public function setRepository(Job $repository): Jobs + { + $this->repository = $repository; + return $this; + } + + public function schedule(int $message_id): bool + { + $data = [ + 'message_id' => $message_id, + 'date_time' => (new DateTimeImmutable())->format('Y-m-d H:i:s') + ]; + try { + $job = $this->getRepository()->create($data); + $this->getRepository()->save($job); + return true; + } catch (PDOException $e) { + return false; + } + } + public function getPending(): array + { + return $this->getRepository()->fetchAllPending(); + } + public function isPending(int $message_id): bool + { + try { + $this->getRepository()->fetchPendingByMessage($message_id); + return true; + } catch (BlankResult $e) { + return false; + } + } + public function find(int $message_id): \ProVM\Emails\Model\Job + { + return $this->getRepository()->fetchPendingByMessage($message_id); + } + public function execute(int $job_id): bool + { + try { + $job = $this->getRepository()->fetchById($job_id); + $job->wasExecuted(); + $this->getRepository()->save($job); + return true; + } catch (PDOException $e) { + return false; + } + } +} \ No newline at end of file diff --git a/api/common/Service/Mailboxes.php b/api/common/Service/Mailboxes.php index 3e55e04..58b5bf7 100644 --- a/api/common/Service/Mailboxes.php +++ b/api/common/Service/Mailboxes.php @@ -1,151 +1,143 @@ setConnection($connection) - ->setAttachments($attachments) - ->setUsername($username); + $this->setRepository($repository) + ->setRemoteService($remoteService) + ->setStatesRepository($states) + ->setLogger($logger); } - protected ConnectionInterface $connection; - protected Attachments $attachments; - protected string $username; + protected Mailbox $repository; + protected Remote\Mailboxes $remoteService; + protected State\Mailbox $statesRepository; - public function getConnection(): ConnectionInterface + public function getRepository(): Mailbox { - return $this->connection; + return $this->repository; } - public function getAttachments(): Attachments + public function getRemoteService(): Remote\Mailboxes { - return $this->attachments; + return $this->remoteService; } - public function getUsername(): string + public function getStatesRepository(): State\Mailbox { - return $this->username; + return $this->statesRepository; } - public function setConnection(ConnectionInterface $connection): Mailboxes + public function setRepository(Mailbox $repository): Mailboxes { - $this->connection = $connection; + $this->repository = $repository; return $this; } - public function setAttachments(Attachments $attachments): Mailboxes + public function setRemoteService(Remote\Mailboxes $service): Mailboxes { - $this->attachments = $attachments; + $this->remoteService = $service; return $this; } - public function setUsername(string $username): Mailboxes + public function setStatesRepository(State\Mailbox $repository): Mailboxes { - $this->username = $username; + $this->statesRepository = $repository; return $this; } - protected array $mailboxes; + 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 { - if (!isset($this->mailboxes)) { - $this->mailboxes = $this->getConnection()->getMailboxes(); - } - return $this->mailboxes; + return $this->getRemoteService()->getAll(); } - public function get(string $mailbox): MailboxInterface + public function getRegistered(): array { - if (!$this->getConnection()->hasMailbox($mailbox)) { - throw new Invalid(); - } - return $this->getConnection()->getMailbox($mailbox); + return $this->getRepository()->fetchAll(); + } + public function get(int $mailbox_id): \ProVM\Emails\Model\Mailbox + { + return $this->getRepository()->fetchById($mailbox_id); } - // Messages - - protected function advanceIterator(Iterator $iterator, int $up_to): Iterator + public function isRegistered(string $mailbox_name): bool { - for ($i = 0; $i < $up_to; $i ++) { - $iterator->next(); + try { + $mailbox = $this->getRepository()->fetchByName($mailbox_name); + return true; + } catch (BlankResult $e) { + return false; } - return $iterator; } - public function getMessages(MailboxInterface $mailbox, int $start = 0, ?int $amount = null): Generator + public function register(string $mailbox_name): bool { - 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(); + $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 countValid(MailboxInterface $mailbox): int + public function updateState(\ProVM\Emails\Model\Mailbox $mailbox, array $messages, \DateTimeInterface $dateTime): bool { - $cnt = 0; - foreach ($mailbox->getIterator() as $message) { - if ($this->hasAttachments($message) and $this->validAttachments($message)) { - $cnt ++; - } + $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) { + return false; } - return $cnt; } - /** - * @param MailboxInterface $mailbox - * @param int $uid - * @return MessageInterface - */ - public function getMessage(MailboxInterface $mailbox, int $uid): MessageInterface + public function unregister(string $mailbox_name): bool { - return $mailbox->getMessage($uid); + try { + $mailbox = $this->getRepository()->fetchByName($mailbox_name); + } catch (BlankResult $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); - 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(); - } + 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; } } \ No newline at end of file diff --git a/api/common/Service/Messages.php b/api/common/Service/Messages.php index d1b9552..c5d0f05 100644 --- a/api/common/Service/Messages.php +++ b/api/common/Service/Messages.php @@ -1,37 +1,177 @@ setFactory($factory) + $this->setMailboxes($mailboxes) + ->setRepository($repository) + ->setRemoteService($remoteService) ->setLogger($logger); } - protected Model $factory; - protected LoggerInterface $logger; + protected Mailboxes $mailboxes; + protected Message $repository; + protected Remote\Messages $remoteService; - public function getFactory(): Model + public function getMailboxes(): Mailboxes { - return $this->factory; + return $this->mailboxes; } - public function getLogger(): LoggerInterface + public function getRepository(): Message { - return $this->logger; + return $this->repository; + } + public function getRemoteService(): Remote\Messages + { + return $this->remoteService; } - public function setFactory(Model $factory): Messages + public function setMailboxes(Mailboxes $mailboxes): Messages { - $this->factory = $factory; + $this->mailboxes = $mailboxes; return $this; } - public function setLogger(LoggerInterface $logger): Messages + public function setRepository(Message $repository): Messages { - $this->logger = $logger; + $this->repository = $repository; return $this; } + public function setRemoteService(Remote\Messages $service): Messages + { + $this->remoteService = $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; + } } \ No newline at end of file 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/resources/routes/01_emails.php b/api/resources/routes/01_emails.php deleted file mode 100644 index f86121a..0000000 --- a/api/resources/routes/01_emails.php +++ /dev/null @@ -1,12 +0,0 @@ -group('/emails', function($app) { - $app->get('/mailboxes', [Emails::class, 'mailboxes']); - $app->group('/messages', function($app) { - $app->post('/attachments[/]', [Emails::class, 'withAttachments']); - $app->post('[/]', [Emails::class, 'messages']); - }); - $app->post('/attachments', [Emails::class, 'attachments']); - $app->post('/attachment', [Emails::class, 'attachment']); -}); \ No newline at end of file diff --git a/api/resources/routes/01_mailboxes.php b/api/resources/routes/01_mailboxes.php index b39878e..c1b464d 100644 --- a/api/resources/routes/01_mailboxes.php +++ b/api/resources/routes/01_mailboxes.php @@ -1,13 +1,18 @@ group('/mailboxes', function($app) { - $app->post('/register', [Mailboxes::class, 'register']); - $app->delete('/unregister', [Mailboxes::class, 'unregister']); - $app->get('/registered', [Mailboxes::class, 'registered']); + $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('/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 index 5d68696..56c26c1 100644 --- a/api/resources/routes/02_attachments.php +++ b/api/resources/routes/02_attachments.php @@ -2,7 +2,7 @@ use ProVM\Common\Controller\Attachments; $app->group('/attachments', function($app) { - $app->post('/get', [Attachments::class, 'get']); + $app->put('/grab', [Attachments::class, 'grab']); $app->post('/decrypt', [Attachments::class, 'decrypt']); $app->get('[/]', Attachments::class); }); \ No newline at end of file diff --git a/api/resources/routes/02_messages.php b/api/resources/routes/02_messages.php index 1296b5a..9c3c07f 100644 --- a/api/resources/routes/02_messages.php +++ b/api/resources/routes/02_messages.php @@ -1,8 +1,9 @@ group('/messages', function($app) { - $app->post('/valid', [Messages::class, 'valid']); $app->put('/grab', [Messages::class, 'grab']); - $app->post('[/]', Messages::class); + $app->put('/schedule', [Jobs::class, 'schedule']); + $app->get('/pending', [Jobs::class, 'pending']); }); \ No newline at end of file diff --git a/api/resources/routes/98_install.php b/api/resources/routes/98_install.php deleted file mode 100644 index d446671..0000000 --- a/api/resources/routes/98_install.php +++ /dev/null @@ -1,4 +0,0 @@ -get('/install', Install::class); \ No newline at end of file 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/middleware/02_cors.php b/api/setup/middleware/02_cors.php index 063dba6..12e122e 100644 --- a/api/setup/middleware/02_cors.php +++ b/api/setup/middleware/02_cors.php @@ -1,2 +1,2 @@ add($app->getContainer()->get(\ProVM\Common\Middleware\CORS::class)); +$app->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/97_auth.php b/api/setup/middleware/97_auth.php index 40efe2f..9e2f1be 100644 --- a/api/setup/middleware/97_auth.php +++ b/api/setup/middleware/97_auth.php @@ -1,2 +1,2 @@ add($app->getContainer()->get(\ProVM\Common\Middleware\Auth::class)); +$app->add($app->getContainer()->get(ProVM\Common\Middleware\Auth::class)); diff --git a/api/setup/middleware/99_errors.php b/api/setup/middleware/99_errors.php index c3f0b5f..29c59fa 100644 --- a/api/setup/middleware/99_errors.php +++ b/api/setup/middleware/99_errors.php @@ -1,2 +1,2 @@ add($app->getContainer()->get(\Zeuxisoo\Whoops\Slim\WhoopsMiddleware::class)); \ No newline at end of file +$app->add($app->getContainer()->get(Zeuxisoo\Whoops\Slim\WhoopsMiddleware::class)); diff --git a/api/setup/setups/02_services.php b/api/setup/setups/02_services.php index f2ea3ab..8fd0f0a 100644 --- a/api/setup/setups/02_services.php +++ b/api/setup/setups/02_services.php @@ -2,40 +2,21 @@ use Psr\Container\ContainerInterface; return [ - \ProVM\Common\Service\Mailboxes::class => function(ContainerInterface $container) { - return (new \ProVM\Common\Service\Mailboxes( - $container->get(\Ddeboer\Imap\ConnectionInterface::class), - $container->get(\ProVM\Common\Service\Attachments::class), - $container->get('emails')->username - ))->setLogger($container->get(\Psr\Log\LoggerInterface::class)); - }, - \ProVM\Common\Service\Emails::class => function(ContainerInterface $container) { - return (new \ProVM\Common\Service\Emails( - $container->get(\ProVM\Common\Service\Mailboxes::class), - $container->get(\ProVM\Emails\Repository\Message::class), - $container->get('attachments_folder') - ))->setLogger($container->get(\Psr\Log\LoggerInterface::class)); - }, - \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') - ); - }, - \ProVM\Common\Service\Decrypt::class => function(ContainerInterface $container) { - return new \ProVM\Common\Service\Decrypt( - $container->get(\Psr\Log\LoggerInterface::class), + ProVM\Common\Service\Decrypt::class => 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\Install::class => function(ContainerInterface $container) { - $database = $container->get('database'); - $pdo = new PDO("mysql:host={$database->host};dbname={$database->name}", $database->username, $database->password); - return new \ProVM\Common\Service\Install( - $container->get(\Psr\Log\LoggerInterface::class), - $pdo + 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) ); } ]; diff --git a/api/setup/setups/04_middlewares.php b/api/setup/setups/04_middlewares.php new file mode 100644 index 0000000..b94e9ec --- /dev/null +++ b/api/setup/setups/04_middlewares.php @@ -0,0 +1,11 @@ + function(ContainerInterface $container) { + return new ProVM\Common\Middleware\CustomExceptions( + $container->get(Nyholm\Psr7\Factory\Psr17Factory::class), + $container->get(Psr\Log\LoggerInterface::class) + ); + } +]; \ No newline at end of file diff --git a/api/src/Model/Attachment.php b/api/src/Model/Attachment.php index d6f562d..2fe5570 100644 --- a/api/src/Model/Attachment.php +++ b/api/src/Model/Attachment.php @@ -2,6 +2,7 @@ namespace ProVM\Emails\Model; use ProVM\Common\Define\Model; +use ProVM\Common\Exception\Database\BlankResult; class Attachment implements Model { @@ -53,13 +54,22 @@ class Attachment implements Model public function getStates(): array { if (!isset($this->states)) { - $this->setStates($this->getStateRepository()->fetchByAttachment($this->getId())); + try { + $this->setStates($this->getStateRepository()->fetchByAttachment($this->getId())); + } catch (BlankResult $e) { + return []; + } } return $this->states; } public function getState(string $name): State\Attachment { - return $this->getStates()[$name]; + try { + return $this->getStates()[$name]; + } catch (\Exception $e) { + $this->newState($name); + return $this->getStates()[$name]; + } } public function addState(State\Attachment $state): Attachment { @@ -76,19 +86,47 @@ class Attachment implements Model protected function newState(string $name): Attachment { $this->addState((new State\Attachment()) - ->setName($name) ->setAttachment($this) + ->setName($name) ); return $this; } + public function getFullFilename(): string + { + return implode(' - ', [ + $this->getMessage()->getSubject(), + $this->getMessage()->getDateTime()->format('Y-m-d His'), + $this->getFilename() + ]); + } + + public function isDownloaded(): bool + { + return $this->getState('downloaded')?->getValue() ?? false; + } public function isEncrypted(): bool { - return $this->getState('encrypted')->getValue() ?? false; + return $this->getState('encrypted')?->getValue() ?? false; } public function isDecrypted(): bool { - return $this->getState('encrypted')->getValue() ?? false; + 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 { @@ -105,7 +143,7 @@ class Attachment implements Model try { $this->getState('decrypted')->setValue(true); } catch (\Exception $e) { - $this->newState('encrypted'); + $this->newState('decrypted'); $this->getState('decrypted')->setValue(true); } return $this; @@ -115,8 +153,17 @@ class Attachment implements Model { return [ 'id' => $this->getId(), - 'message' => $this->getMessage()->toArray(), + '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(), + 'downloaded' => $this->isDownloaded(), 'encrypted' => $this->isEncrypted(), 'decrypted' => $this->isDecrypted() ]; diff --git a/api/src/Model/Job.php b/api/src/Model/Job.php new file mode 100644 index 0000000..421f79a --- /dev/null +++ b/api/src/Model/Job.php @@ -0,0 +1,61 @@ +id; + } + public function getMessage(): Message + { + return $this->message; + } + public function getDateTime(): DateTimeInterface + { + return $this->dateTime; + } + public function isExecuted(): bool + { + return $this->executed ?? false; + } + + public function setId(int $id): Job + { + $this->id = $id; + return $this; + } + public function setMessage(Message $message): Job + { + $this->message = $message; + return $this; + } + public function setDateTime(DateTimeInterface $dateTime): Job + { + $this->dateTime = $dateTime; + return $this; + } + public function wasExecuted(): Job + { + $this->executed = true; + return $this; + } + + public function toArray(): array + { + return [ + 'id' => $this->getId(), + 'message' => $this->getMessage()->toArray(), + 'date_time' => $this->getDateTime()->format('Y-m-d H:i:s'), + 'executed' => $this->isExecuted() + ]; + } +} \ No newline at end of file diff --git a/api/src/Model/Mailbox.php b/api/src/Model/Mailbox.php index 7b86bcf..0a4221a 100644 --- a/api/src/Model/Mailbox.php +++ b/api/src/Model/Mailbox.php @@ -4,12 +4,15 @@ namespace ProVM\Emails\Model; use DateTimeInterface; use ProVM\Common\Define\Model; use ProVM\Common\Exception\Database\BlankResult; +use ProVM\Common\Exception\EmptyMailbox; +use ProVM\Common\Exception\Mailbox\Stateless; use Safe\DateTimeImmutable; class Mailbox implements Model { protected int $id; protected string $name; + protected int $validity; public function getId(): int { @@ -19,6 +22,10 @@ class Mailbox implements Model { return $this->name; } + public function getValidity(): int + { + return $this->validity; + } public function setId(int $id): Mailbox { @@ -30,6 +37,11 @@ class Mailbox implements Model $this->name = $name; return $this; } + public function setValidity(int $uidvalidity): Mailbox + { + $this->validity = $uidvalidity; + return $this; + } protected \ProVM\Emails\Repository\State\Mailbox $stateRepository; @@ -68,19 +80,31 @@ class Mailbox implements Model 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->getStates()[array_key_last($this->getStates())]->getDateTime(); + return $this->lastState()->getDateTime(); } public function lastCount(): int { if (count($this->getStates()) == 0) { return 0; } - return $this->getStates()[array_key_last($this->getStates())]->getCount(); + return $this->lastState()->getCount(); + } + public function lastPosition(): int + { + $state = $this->lastState()->getUIDs(); + return array_key_last($state); } public function toArray(): array @@ -88,6 +112,7 @@ class Mailbox implements Model return [ 'id' => $this->getId(), 'name' => $this->getName(), + //'validity' => $this->getValidity(), 'last_checked' => [ 'date' => $this->lastChecked() ?? 'never', 'count' => $this->lastCount() ?? 0 diff --git a/api/src/Model/Message.php b/api/src/Model/Message.php index 2de4d68..a5f9af7 100644 --- a/api/src/Model/Message.php +++ b/api/src/Model/Message.php @@ -146,7 +146,7 @@ class Message implements Model public function doesHaveAttachments(): Message { - if (!isset($this->getStates()['has_attachments'])) { + if (!isset($this->getState()['has_attachments'])) { $this->newState('has_attachments'); } $this->getState('has_attachments')->setValue(true); @@ -206,7 +206,10 @@ class Message implements Model 'has_attachments' => $this->hasAttachments(), 'valid_attachments' => $this->hasValidAttachments(), 'downloaded_attachments' => $this->hasDownloadedAttachments() - ] + ], + 'attachments' => $this->hasValidAttachments() ? array_map(function(Attachment $attachment) { + return $attachment->toArray(); + }, $this->getAttachments()) : [] ]; } } \ No newline at end of file diff --git a/api/src/Model/State/Attachment.php b/api/src/Model/State/Attachment.php index 3838098..a50961b 100644 --- a/api/src/Model/State/Attachment.php +++ b/api/src/Model/State/Attachment.php @@ -24,7 +24,7 @@ class Attachment implements Model } public function getValue(): bool { - return $this->value; + return $this->value ?? false; } public function setId(int $id): Attachment diff --git a/api/src/Repository/Attachment.php b/api/src/Repository/Attachment.php index 59e0c06..568d4b6 100644 --- a/api/src/Repository/Attachment.php +++ b/api/src/Repository/Attachment.php @@ -2,64 +2,62 @@ namespace ProVM\Emails\Repository; use PDO; +use ProVM\Common\Exception\Database\BlankResult; use Psr\Log\LoggerInterface; use ProVM\Common\Define\Model as ModelInterface; +use ProVM\Common\Factory\Model; use ProVM\Common\Implement\Repository; class Attachment extends Repository { - public function __construct(PDO $connection, LoggerInterface $logger, State\Attachment $stateRepository) + public function __construct(PDO $connection, LoggerInterface $logger, Model $factory) { parent::__construct($connection, $logger); - $this->setStateRepository($stateRepository) + $this->setFactory($factory) ->setTable('attachments'); } - protected State\Attachment $stateRepository; - public function getStateRepository(): State\Attachment + protected Model $factory; + public function getFactory(): Model { - return $this->stateRepository; + return $this->factory; } - - public function setStateRepository(State\Attachment $repository): Attachment + public function setFactory(Model $factory): Attachment { - $this->stateRepository = $repository; + $this->factory = $factory; return $this; } - protected function idField(): string - { - return 'filename'; - } - protected function fieldsForInsert(): array { - return array_merge($this->fieldsForUpdate(), ['filename']); + return [ + 'message_id', + 'filename' + ]; } protected function fieldsForUpdate(): array { - return [ - 'message_id' - ]; + 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 valuesForInsert(ModelInterface $model): array - { - return $this->valuesForUpdate($model); - } protected function defaultFind(ModelInterface $model): ModelInterface { - return $this->fetchByFilename($model->getFilename()); + return $this->fetchByMessageAndFilename($model->getMessage()->getId(), $model->getFilename()); } protected function valuesForCreate(array $data): array { @@ -70,22 +68,47 @@ class Attachment extends Repository } protected function defaultSearch(array $data): ModelInterface { - return $this->fetchByFilename($data['filename']); + return $this->fetchByMessageAndFilename($data['message_id'], $data['filename']); } public function load(array $row): \ProVM\Emails\Model\Attachment { - $model = new \ProVM\Emails\Model\Attachment(); - $model + 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->getStateRepository()); - return $model; + ->setStateRepository($this->getFactory()->find(\ProVM\Emails\Model\State\Attachment::class)); } - public function fetchByFilename(string $filename): \ProVM\Emails\Model\Attachment + public function save(ModelInterface &$model): void { - $query = "SELECT * FROM {$this->getTable()} WHERE filename = ?"; - return $this->fetchOne($query, [$filename]); + 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 { diff --git a/api/src/Repository/Job.php b/api/src/Repository/Job.php new file mode 100644 index 0000000..164d900 --- /dev/null +++ b/api/src/Repository/Job.php @@ -0,0 +1,108 @@ +setFactory($factory) + ->setTable('attachments_jobs'); + } + + protected \ProVM\Common\Factory\Model $factory; + public function getFactory(): \ProVM\Common\Factory\Model + { + return $this->factory; + } + public function setFactory(\ProVM\Common\Factory\Model $factory): Job + { + $this->factory = $factory; + return $this; + } + + protected function fieldsForUpdate(): array + { + return $this->fieldsForInsert(); + } + protected function valuesForUpdate(ModelInterface $model): array + { + return $this->valuesForInsert($model); + } + protected function fieldsForInsert(): array + { + return [ + 'message_id', + 'date_time', + 'executed' + ]; + } + protected function valuesForInsert(ModelInterface $model): array + { + return [ + $model->getMessage()->getId(), + $model->getDateTime()->format('Y-m-d H:i:s'), + $model->isExecuted() ? 1 : 0 + ]; + } + protected function defaultFind(ModelInterface $model): ModelInterface + { + return $this->fetchByMessageAndDate($model->getMessage()->getId(), $model->getDateTime()->format('Y-m-d H:i:s')); + } + protected function fieldsForCreate(): array + { + return [ + 'message_id', + 'date_time', + 'executed' + ]; + } + protected function valuesForCreate(array $data): array + { + return [ + $data['message_id'], + $data['date_time'], + $data['executed'] ?? 0 + ]; + } + protected function defaultSearch(array $data): ModelInterface + { + return $this->fetchByMessageAndDate($data['message_id'], $data['date_time']); + } + + public function load(array $row): ModelInterface + { + $model = (new BaseModel()) + ->setId($row['id']) + ->setMessage($this->getFactory()->find(\ProVM\Emails\Model\Message::class)->fetchById($row['message_id'])) + ->setDateTime(new DateTimeImmutable($row['date_time'])); + if ($row['executed'] ?? 0 === 1) { + $model->wasExecuted(); + } + return $model; + } + + public function fetchAllPending(): array + { + $query = "SELECT * FROM {$this->getTable()} WHERE `executed` = 0"; + return $this->fetchMany($query); + } + public function fetchByMessageAndDate(int $message_id, string $date_time): \ProVM\Emails\Model\Job + { + $query = "SELECT * FROM {$this->getTable()} WHERE `message_id` = ? AND `date_time` = ?"; + return $this->fetchOne($query, [$message_id, $date_time]); + } + public function fetchPendingByMessage(int $message_id): \ProVM\Emails\Model\Job + { + $query = "SELECT * FROM {$this->getTable()} WHERE `message_id` = ? AND `executed` = 0"; + return $this->fetchOne($query, [$message_id]); + } +} \ No newline at end of file diff --git a/api/src/Repository/Mailbox.php b/api/src/Repository/Mailbox.php index 3f470e3..50a0b92 100644 --- a/api/src/Repository/Mailbox.php +++ b/api/src/Repository/Mailbox.php @@ -35,7 +35,8 @@ class Mailbox extends Repository protected function fieldsForInsert(): array { return [ - 'name' + 'name', + 'validity' ]; } protected function fieldsForCreate(): array @@ -45,7 +46,8 @@ class Mailbox extends Repository protected function valuesForInsert(Model $model): array { return [ - $model->getName() + $model->getName(), + $model->getValidity() ]; } protected function defaultFind(Model $model): Model @@ -61,7 +63,8 @@ class Mailbox extends Repository protected function valuesForCreate(array $data): array { return [ - $data['name'] + $data['name'], + $data['validity'] ]; } protected function defaultSearch(array $data): Model @@ -74,6 +77,7 @@ class Mailbox extends Repository return (new \ProVM\Emails\Model\Mailbox()) ->setId($row['id']) ->setName($row['name']) + ->setValidity($row['validity']) ->setStateRepository($this->getStates()); } diff --git a/api/src/Repository/Message.php b/api/src/Repository/Message.php index a0389e3..4976dd4 100644 --- a/api/src/Repository/Message.php +++ b/api/src/Repository/Message.php @@ -1,11 +1,11 @@ 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()); - try { - $model->setDateTime(new DateTimeImmutable($row['date_time'])); - } catch (Exception | ErrorfuncException $e) { - $this->getLogger()->error($e); - } - return $model; } public function save(Model &$model): void @@ -144,12 +138,26 @@ class Message extends Repository } public function fetchByMailbox(int $mailbox_id): array { - $query = "SELECT * FROM {$this->getTable()} WHERE mailbox_id = ?"; + $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}"; + $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')]); + } } \ No newline at end of file diff --git a/api/src/Repository/State/Attachment.php b/api/src/Repository/State/Attachment.php index 9a40027..7da6b9b 100644 --- a/api/src/Repository/State/Attachment.php +++ b/api/src/Repository/State/Attachment.php @@ -8,10 +8,22 @@ use Psr\Log\LoggerInterface; class Attachment extends Repository { - public function __construct(PDO $connection, LoggerInterface $logger) + public function __construct(PDO $connection, LoggerInterface $logger, \ProVM\Common\Factory\Model $factory) { parent::__construct($connection, $logger); - $this->setTable('attachments_states'); + $this->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; } protected function fieldsForInsert(): array @@ -40,7 +52,7 @@ class Attachment extends Repository } protected function valuesForUpdate(Model $model): array { - return array_merge($this->valuesForInsert($model), [$model->getId()]); + return $this->valuesForInsert($model); } protected function fieldsForCreate(): array { @@ -63,6 +75,7 @@ class Attachment extends Repository { 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); } diff --git a/api/src/Repository/State/Message.php b/api/src/Repository/State/Message.php index fda78e5..74acb5b 100644 --- a/api/src/Repository/State/Message.php +++ b/api/src/Repository/State/Message.php @@ -8,10 +8,22 @@ use ProVM\Common\Implement\Repository; class Message extends Repository { - public function __construct(PDO $connection, LoggerInterface $logger) + public function __construct(PDO $connection, LoggerInterface $logger, \ProVM\Common\Factory\Model $factory) { parent::__construct($connection, $logger); - $this->setTable('messages_states'); + $this->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; } protected function fieldsForUpdate(): array @@ -64,6 +76,7 @@ class Message extends Repository 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); } diff --git a/cli/common/Command/GrabAttachments.php b/cli/common/Command/GrabAttachments.php index dbf60dd..1055a8c 100644 --- a/cli/common/Command/GrabAttachments.php +++ b/cli/common/Command/GrabAttachments.php @@ -53,9 +53,10 @@ class GrabAttachments extends Command $messages = $this->getMessages(); $io->text('Found ' . count($messages) . ' messages.'); $io->section('Grabbing Attachments'); - foreach ($messages as $message) { - $attachments = $this->grabAttachments($message); - $io->text("Found {$attachments} attachments for message UID:{$message}."); + foreach ($messages as $job) { + $message = $job->message; + $attachments = $this->grabAttachments($message->uid); + $io->text("Found {$attachments} attachments for message UID:{$message->uid}."); } $io->success('Done.'); diff --git a/ui/resources/views/emails/mailboxes.blade.php b/ui/resources/views/emails/mailboxes.blade.php index fea91fd..fda295c 100644 --- a/ui/resources/views/emails/mailboxes.blade.php +++ b/ui/resources/views/emails/mailboxes.blade.php @@ -19,7 +19,7 @@ this.id = props.id } } - draw() { + draw(tr) { const name = $('') const register = $('') if (this.registered) { @@ -42,27 +42,32 @@ register.append( $('').addClass('ui mini circular icon button').append( $('').addClass('save icon') - ).click(() => { - this.register() + ).click((e) => { + this.register($(e.currentTarget)) }) ) } - return $('').append(name).append(register) + return tr.append(name).append(register) } - register() { + register(button) { const uri = '/mailboxes/register' const data = { mailboxes: [this.name] } + button.html('').append( + $('').addClass('redo loading icon') + ) return Send.post({ uri, data }).then(response => { - response.mailboxes.forEach(mb => { - if (mb.name === this.name && mb.created) { + response.registered.mailboxes.forEach(mb => { + if (mb.name === this.name && mb.registered) { this.id = mb.id this.registered = true - mailboxes.draw().table() + const tr = button.parent().parent() + tr.html('') + this.draw(tr) } }) }) @@ -91,7 +96,7 @@ mailboxes: [], get: function() { this.draw().loading() - return Send.get('/mailboxes').then(response => { + return Send.get('/mailboxes').then((response, status, jqXHR) => { response.mailboxes.forEach(mb => { this.mailboxes.push(new Mailbox(mb)) }) @@ -138,7 +143,7 @@ body: () => { const body = $('') this.mailboxes.forEach(mb => { - body.append(mb.draw()) + body.append(mb.draw($(''))) }) return body } diff --git a/ui/resources/views/emails/messages.blade.php b/ui/resources/views/emails/messages.blade.php index a16c0f3..138ac7a 100644 --- a/ui/resources/views/emails/messages.blade.php +++ b/ui/resources/views/emails/messages.blade.php @@ -19,14 +19,16 @@ valid: false, downloaded: false } + attachments - constructor({id, uid, subject, date_time, from, states}) { + constructor({id, uid, subject, date_time, from, states, attachments}) { this.set().id(id) .set().uid(uid) .set().subject(subject) .set().date(date_time) .set().from(from) .set().states(states) + .set().attachments(attachments) } get() { return { @@ -47,6 +49,9 @@ }, states: () => { return this.states + }, + attachments: () => { + return this.attachments } } } @@ -82,6 +87,9 @@ this.states[map_keys[k]] = states[k] }) return this + }, + attachments: attachments => { + this.attachments = attachments } } } @@ -123,25 +131,35 @@ } catch (e) { console.debug(e) } + const valid = $('').html($('').addClass(this.has().valid() ? 'green check circle icon' : 'red times circle icon')) + if (this.has().valid() && this.get().attachments().length > 0) { + const list = $('
').addClass('ui list') + this.get().attachments().forEach(attachment => { + list.append($('
').addClass('item').html(attachment.filename)) + }) + valid.append( + $('
').addClass('ui popup').append(list) + ) + } return $('').append( $('').html(this.get().subject()) ).append( $('').html(format.format(date)) ).append( - $('').html(this.get().from()) + $('').html(this.get().from().full) ).append( $('').html($('').addClass(this.has().attachments() ? 'green check circle icon' : 'red times circle icon')) ).append( - $('').html($('').addClass(this.has().valid() ? 'green check circle icon' : 'red times circle icon')) + valid ).append( $('').html($('').addClass(this.has().downloaded() ? 'green check circle icon' : 'red times circle icon')) ).append( $('').append( (this.has().attachments() && this.has().valid() && !this.has().downloaded()) ? $('').addClass('ui mini green circular icon button').append( - $('').addClass('paperclip icon') + $('').addClass('clock icon') ).click(() => { - this.download().attachments(this.get().uid()) + this.download().attachments(this.get().id()) }) : '' ) ) @@ -149,21 +167,19 @@ download() { return { - attachments: uid => { - const uri = '/emails/attachment' + attachments: id => { + const uri = '/messages/schedule' const data = { - mailbox: '{{$mailbox_id}}', messages: [ - uid - ], - extension: 'pdf' + id + ] } - return Send.post({ + return Send.put({ uri, data }).then(response => { - if (response.attachments_count > 0) { - alert('Grabbed Attachments') + if (response.scheduled > 0) { + alert('Scheduled Attachments Job') } }) } @@ -191,14 +207,8 @@ }) }, messages: () => { - const uri = '/messages/valid' - const data = { - mailboxes: ['{{$mailbox_id}}'] - } - return Send.post({ - uri, - data - }).then(response => { + const uri = '/mailbox/{{$mailbox_id}}/messages/valid' + return Send.get(uri).then(response => { if (this.total === null) { $(this.id.count).html(' (' + response.total + ')') this.total = response.total diff --git a/ui/resources/views/home.blade.php b/ui/resources/views/home.blade.php index d5f0851..6b50b7c 100644 --- a/ui/resources/views/home.blade.php +++ b/ui/resources/views/home.blade.php @@ -13,8 +13,13 @@ get: function() { const uri = '/mailboxes/registered' this.draw().loading() - return Send.get(uri).then(response => { - this.mailboxes = response.mailboxes + return Send.get(uri).then((response, status, jqXHR) => { + if (parseInt(jqXHR.status / 100) !== 2) { + return + } + if (jqXHR.status === 200) { + this.mailboxes = response.mailboxes + } this.draw().list() }) }, @@ -38,6 +43,10 @@ list: () => { const parent = $(this.div_id) parent.html('') + if (this.mailboxes.length === 0) { + parent.html('No mailboxes registered.') + return + } const list = $('
').addClass('ui list') this.mailboxes.forEach(mb => { list.append( From f8500e061c4d67ca96c3c9b1470588a7a64fa731 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Tue, 29 Nov 2022 11:12:06 -0300 Subject: [PATCH 21/35] Download attachments --- api/common/Controller/Attachments.php | 10 ++ api/common/Service/Attachments.php | 18 +++ api/public/index.php | 2 +- api/resources/routes/02_attachments.php | 3 + api/resources/routes/02_messages.php | 3 + api/setup/setups/98_log.php | 24 ++-- api/src/Model/Attachment.php | 1 + ui/common/Controller/Attachments.php | 21 ++++ ui/common/Controller/Emails.php | 8 +- ui/composer.json | 1 + ui/public/index.php | 8 +- ui/resources/routes/01_emails.php | 5 +- ui/resources/routes/02_attachments.php | 6 + ui/resources/views/emails/messages.blade.php | 15 ++- ui/resources/views/emails/show.blade.php | 103 ++++++++++++++++++ .../layout/body/footer/scripts/main.blade.php | 1 + ui/setup/middleware/99_errors.php | 2 +- ui/setup/settings/01_env.php | 2 +- ui/setup/setups/02_client.php | 15 +++ ui/setup/setups/03_views.php | 2 +- ui/setup/setups/98_log.php | 19 +++- 21 files changed, 242 insertions(+), 27 deletions(-) create mode 100644 ui/common/Controller/Attachments.php create mode 100644 ui/resources/routes/02_attachments.php create mode 100644 ui/resources/views/emails/show.blade.php create mode 100644 ui/setup/setups/02_client.php diff --git a/api/common/Controller/Attachments.php b/api/common/Controller/Attachments.php index 5b33445..68d3dd6 100644 --- a/api/common/Controller/Attachments.php +++ b/api/common/Controller/Attachments.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ServerRequestInterface; use ProVM\Common\Implement\Controller\Json; use ProVM\Common\Service\Attachments as Service; use ProVM\Emails\Model\Attachment; +use Psr\Log\LoggerInterface; class Attachments { @@ -51,4 +52,13 @@ class Attachments } return $this->withJson($response, $output); } + public function get(ServerRequestInterface $request, ResponseInterface $response, Service $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; + } } \ No newline at end of file diff --git a/api/common/Service/Attachments.php b/api/common/Service/Attachments.php index 0c2a6a0..6c92915 100644 --- a/api/common/Service/Attachments.php +++ b/api/common/Service/Attachments.php @@ -3,9 +3,11 @@ namespace ProVM\Common\Service; use Ddeboer\Imap\Message\AttachmentInterface; use Ddeboer\Imap\MessageInterface; +use Nyholm\Psr7\Stream; use ProVM\Common\Exception\Message\NoAttachments; use ProVM\Emails\Model\Message; use ProVM\Emails\Repository\Attachment; +use Psr\Http\Message\StreamInterface; use Psr\Log\LoggerInterface; use Safe\Exceptions\FilesystemException; @@ -84,6 +86,22 @@ class Attachments extends Base $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 \Safe\file_get_contents($filename); + } public function getAll(): array { diff --git a/api/public/index.php b/api/public/index.php index aed6193..9659116 100644 --- a/api/public/index.php +++ b/api/public/index.php @@ -8,6 +8,6 @@ Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface try { $app->run(); } catch (Error | Exception $e) { - $app->getContainer()->get(\Psr\Log\LoggerInterface::class)->error($e); + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->error($e); throw $e; } diff --git a/api/resources/routes/02_attachments.php b/api/resources/routes/02_attachments.php index 56c26c1..62969b7 100644 --- a/api/resources/routes/02_attachments.php +++ b/api/resources/routes/02_attachments.php @@ -5,4 +5,7 @@ $app->group('/attachments', function($app) { $app->put('/grab', [Attachments::class, 'grab']); $app->post('/decrypt', [Attachments::class, 'decrypt']); $app->get('[/]', Attachments::class); +}); +$app->group('/attachment/{attachment_id}', function($app) { + $app->get('[/]', [Attachments::class, 'get']); }); \ No newline at end of file diff --git a/api/resources/routes/02_messages.php b/api/resources/routes/02_messages.php index 9c3c07f..ed080d5 100644 --- a/api/resources/routes/02_messages.php +++ b/api/resources/routes/02_messages.php @@ -6,4 +6,7 @@ $app->group('/messages', function($app) { $app->put('/grab', [Messages::class, 'grab']); $app->put('/schedule', [Jobs::class, 'schedule']); $app->get('/pending', [Jobs::class, 'pending']); +}); +$app->group('/message/{message_id}', function($app) { + $app->get('[/]', [Messages::class, 'get']); }); \ No newline at end of file diff --git a/api/setup/setups/98_log.php b/api/setup/setups/98_log.php index b81de42..0aedf37 100644 --- a/api/setup/setups/98_log.php +++ b/api/setup/setups/98_log.php @@ -2,21 +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\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\SyslogFormatter::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; }, - \Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { - $logger = new \Monolog\Logger('file_logger'); - $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)); + Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { + $logger = new Monolog\Logger('file_logger'); + $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 index 2fe5570..49bf820 100644 --- a/api/src/Model/Attachment.php +++ b/api/src/Model/Attachment.php @@ -163,6 +163,7 @@ class Attachment implements Model '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() 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 index a7c7efa..dcbb11c 100644 --- a/ui/common/Controller/Emails.php +++ b/ui/common/Controller/Emails.php @@ -11,8 +11,12 @@ class Emails { return $view->render($response, 'emails.mailboxes'); } - public function messages(ServerRequestInterface $request, ResponseInterface $response, View $view, string $mailbox): ResponseInterface + public function messages(ServerRequestInterface $request, ResponseInterface $response, View $view, int $mailbox_id): ResponseInterface { - return $view->render($response, 'emails.messages', ['mailbox_id' => $mailbox]); + 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/composer.json b/ui/composer.json index 8d7e8da..aefa104 100644 --- a/ui/composer.json +++ b/ui/composer.json @@ -3,6 +3,7 @@ "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", diff --git a/ui/public/index.php b/ui/public/index.php index 0606ece..9659116 100644 --- a/ui/public/index.php +++ b/ui/public/index.php @@ -4,4 +4,10 @@ $app = require_once implode(DIRECTORY_SEPARATOR, [ 'setup', 'app.php' ]); -$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/ui/resources/routes/01_emails.php b/ui/resources/routes/01_emails.php index a5d9f2d..e8addbd 100644 --- a/ui/resources/routes/01_emails.php +++ b/ui/resources/routes/01_emails.php @@ -2,9 +2,12 @@ use ProVM\Common\Controller\Emails; $app->group('/emails', function($app) { - $app->group('/mailbox/{mailbox}', 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/views/emails/messages.blade.php b/ui/resources/views/emails/messages.blade.php index 138ac7a..2542956 100644 --- a/ui/resources/views/emails/messages.blade.php +++ b/ui/resources/views/emails/messages.blade.php @@ -141,7 +141,7 @@ $('
').addClass('ui popup').append(list) ) } - return $('').append( + const tr = $('').append( $('').html(this.get().subject()) ).append( $('').html(format.format(date)) @@ -163,6 +163,19 @@ }) : '' ) ) + if (this.has().downloaded()) { + tr.find('td').each((i, td) => { + const content = $(td).html() + if (content.indexOf('icon') > -1) { + return + } + $(td).html('') + $(td).append( + $('').attr('href', '{{$urls->base}}/emails/message/' + this.id).html(content) + ) + }) + } + return tr } download() { diff --git a/ui/resources/views/emails/show.blade.php b/ui/resources/views/emails/show.blade.php new file mode 100644 index 0000000..32bba7d --- /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 \ 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 index e6a57e3..b36b304 100644 --- a/ui/resources/views/layout/body/footer/scripts/main.blade.php +++ b/ui/resources/views/layout/body/footer/scripts/main.blade.php @@ -35,4 +35,5 @@ return this.base({method: 'delete', uri, data, dataType, contentType}) } } + const _urls = JSON.parse('{!! Safe\json_encode($urls) !!}') \ No newline at end of file diff --git a/ui/setup/middleware/99_errors.php b/ui/setup/middleware/99_errors.php index c168fe5..29c59fa 100644 --- a/ui/setup/middleware/99_errors.php +++ b/ui/setup/middleware/99_errors.php @@ -1,2 +1,2 @@ add($app->getContainer()->get(\Zeuxisoo\Whoops\Slim\WhoopsMiddleware::class)); +$app->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 index f138041..b0640fb 100644 --- a/ui/setup/settings/01_env.php +++ b/ui/setup/settings/01_env.php @@ -1,4 +1,4 @@ $_ENV['API_KEY'] + 'api_key' => sha1($_ENV['API_KEY']) ]; diff --git a/ui/setup/setups/02_client.php b/ui/setup/setups/02_client.php new file mode 100644 index 0000000..64433f9 --- /dev/null +++ b/ui/setup/setups/02_client.php @@ -0,0 +1,15 @@ + function(ContainerInterface $container) { + return new GuzzleHttp\Client([ + 'base_uri' => "http://proxy:8080", + 'headers' => [ + 'Authorization' => [ + "Bearer {$container->get('api_key')}" + ] + ] + ]); + } +]; \ No newline at end of file diff --git a/ui/setup/setups/03_views.php b/ui/setup/setups/03_views.php index c8c7a06..a3a85ab 100644 --- a/ui/setup/setups/03_views.php +++ b/ui/setup/setups/03_views.php @@ -9,7 +9,7 @@ return [ null, [ 'urls' => $container->get('urls'), - 'api_key' => sha1($container->get('api_key')) + 'api_key' => $container->get('api_key') ] ); } diff --git a/ui/setup/setups/98_log.php b/ui/setup/setups/98_log.php index eaef107..0aedf37 100644 --- a/ui/setup/setups/98_log.php +++ b/ui/setup/setups/98_log.php @@ -2,14 +2,21 @@ use Psr\Container\ContainerInterface; return [ - \Monolog\Handler\RotatingFileHandler::class => function(ContainerInterface $container) { - $handler = new \Monolog\Handler\RotatingFileHandler($container->get('log_file')); - $handler->setFormatter($container->get(\Monolog\Formatter\LineFormatter::class)); + 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\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)); + Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { + $logger = new Monolog\Logger('file_logger'); + $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 From a5d97729dc46536a54ee87d5eb72206ca23d55a8 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 30 Nov 2022 10:40:36 -0300 Subject: [PATCH 22/35] Changed way to connect to api --- api/common/Controller/Mailboxes.php | 19 ++++++-- api/common/Service/Mailboxes.php | 1 + api/src/Model/Message.php | 2 +- ui/common/Controller/Api.php | 17 +++++++ ui/common/Service/Api.php | 47 +++++++++++++++++++ ui/resources/routes/98_api.php | 4 ++ ui/resources/routes/99_home.php | 2 +- ui/resources/views/emails/mailboxes.blade.php | 19 +++++--- .../layout/body/footer/scripts/main.blade.php | 41 +++++++--------- ui/setup/settings/03_urls.php | 9 +++- ui/setup/setups/{02_client.php => 02_api.php} | 5 +- ui/setup/setups/03_views.php | 6 +-- 12 files changed, 129 insertions(+), 43 deletions(-) create mode 100644 ui/common/Controller/Api.php create mode 100644 ui/common/Service/Api.php create mode 100644 ui/resources/routes/98_api.php rename ui/setup/setups/{02_client.php => 02_api.php} (68%) diff --git a/api/common/Controller/Mailboxes.php b/api/common/Controller/Mailboxes.php index 97fb792..a224799 100644 --- a/api/common/Controller/Mailboxes.php +++ b/api/common/Controller/Mailboxes.php @@ -83,16 +83,25 @@ class Mailboxes $body = $request->getBody(); $json = \Safe\json_decode($body->getContents()); if (!isset($json->mailboxes)) { - throw new MissingArgument('mailboxes', 'array', 'mailboxes names'); + throw new MissingArgument('mailboxes', 'array', 'mailboxes ids'); } $output = [ 'mailboxes' => $json->mailboxes, 'total' => count($json->mailboxes), - 'unregistered' => 0 + 'unregistered' => [ + 'total' => 0, + 'mailboxes' => [] + ] ]; - foreach ($json->mailboxes as $mailbox_name) { - if ($service->unregister($mailbox_name)) { - $output['unregistered'] ++; + 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); diff --git a/api/common/Service/Mailboxes.php b/api/common/Service/Mailboxes.php index 58b5bf7..908cf00 100644 --- a/api/common/Service/Mailboxes.php +++ b/api/common/Service/Mailboxes.php @@ -118,6 +118,7 @@ class Mailboxes extends Base try { $mailbox = $this->getRepository()->fetchByName($mailbox_name); } catch (BlankResult $e) { + $this->getLogger()->error($e); // It's already unregistered return true; } diff --git a/api/src/Model/Message.php b/api/src/Model/Message.php index a5f9af7..e89e27a 100644 --- a/api/src/Model/Message.php +++ b/api/src/Model/Message.php @@ -146,7 +146,7 @@ class Message implements Model public function doesHaveAttachments(): Message { - if (!isset($this->getState()['has_attachments'])) { + if (!isset($this->getStates()['has_attachments'])) { $this->newState('has_attachments'); } $this->getState('has_attachments')->setValue(true); diff --git a/ui/common/Controller/Api.php b/ui/common/Controller/Api.php new file mode 100644 index 0000000..4202787 --- /dev/null +++ b/ui/common/Controller/Api.php @@ -0,0 +1,17 @@ +getBody(); + $json = \Safe\json_decode($body->getContents(), JSON_OBJECT_AS_ARRAY); + + return $service->sendRequest($json); + } +} 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/resources/routes/98_api.php b/ui/resources/routes/98_api.php new file mode 100644 index 0000000..4baf51f --- /dev/null +++ b/ui/resources/routes/98_api.php @@ -0,0 +1,4 @@ +post('/api', Api::class); \ No newline at end of file diff --git a/ui/resources/routes/99_home.php b/ui/resources/routes/99_home.php index f989a1e..b378b41 100644 --- a/ui/resources/routes/99_home.php +++ b/ui/resources/routes/99_home.php @@ -1,4 +1,4 @@ get('[/]', Home::class); \ No newline at end of file +$app->get('[/]', Home::class); diff --git a/ui/resources/views/emails/mailboxes.blade.php b/ui/resources/views/emails/mailboxes.blade.php index fda295c..3081487 100644 --- a/ui/resources/views/emails/mailboxes.blade.php +++ b/ui/resources/views/emails/mailboxes.blade.php @@ -33,8 +33,8 @@ ).append( $('').addClass('ui mini circular red icon button').append( $('').addClass('remove icon') - ).click(() => { - this.unregister() + ).click(e => { + this.unregister($(e.currentTarget)) }) ) } else { @@ -42,7 +42,7 @@ register.append( $('').addClass('ui mini circular icon button').append( $('').addClass('save icon') - ).click((e) => { + ).click(e => { this.register($(e.currentTarget)) }) ) @@ -72,20 +72,25 @@ }) }) } - unregister() { + unregister(button) { const uri = '/mailboxes/unregister' const data = { mailboxes: [this.id] } + button.html('').append( + $('').addClass('redo loading icon') + ) return Send.delete({ uri, data }).then(response => { - response.mailboxes.forEach(mb => { - if (mb.id === this.id && mb.removed) { + response.unregistered.mailboxes.forEach(mb => { + if (mb.id === this.id && mb.unregistered) { this.id = null this.registered = false - mailboxes.draw().table() + const tr = button.parent().parent() + tr.html('') + this.draw(tr) } }) }) diff --git a/ui/resources/views/layout/body/footer/scripts/main.blade.php b/ui/resources/views/layout/body/footer/scripts/main.blade.php index b36b304..721f189 100644 --- a/ui/resources/views/layout/body/footer/scripts/main.blade.php +++ b/ui/resources/views/layout/body/footer/scripts/main.blade.php @@ -1,38 +1,33 @@ -@endpush \ No newline at end of file +@endpush From f6ac8eae7ce2713df945fb0badd76884f23b5378 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 30 Nov 2022 11:42:50 -0300 Subject: [PATCH 24/35] Fix: Empty mailbox --- ui/resources/views/emails/messages.blade.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ui/resources/views/emails/messages.blade.php b/ui/resources/views/emails/messages.blade.php index 209f53e..47101f3 100644 --- a/ui/resources/views/emails/messages.blade.php +++ b/ui/resources/views/emails/messages.blade.php @@ -222,12 +222,12 @@ messages: () => { const uri = '/mailbox/{{$mailbox_id}}/messages/valid' return Send.get(uri).then((response, status, jqXHR) => { - if (parseInt(jqXHR.status/100) !== 2) { + if (parseInt(jqXHR.status/100) !== 2 || jqXHR.status === 204) { + $(this.id.results).html('').append( + this.draw().empty() + ) return } - if (jqXHR.status === 204) { - this.draw().empty() - } if (this.total === null) { $(this.id.count).html(' (' + response.total + ')') this.total = response.total @@ -390,6 +390,7 @@ ) }, empty: () => { + $(this.id.count).html(' (0)') return $('
').addClass('ui message').html('No messages found.') } } From 5e4e52e620f0af1eae7d920cb30bdcc4e3b024d3 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 30 Nov 2022 11:46:57 -0300 Subject: [PATCH 25/35] No registered mailboxes --- ui/resources/views/home.blade.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ui/resources/views/home.blade.php b/ui/resources/views/home.blade.php index 6b50b7c..4909b55 100644 --- a/ui/resources/views/home.blade.php +++ b/ui/resources/views/home.blade.php @@ -14,7 +14,8 @@ const uri = '/mailboxes/registered' this.draw().loading() return Send.get(uri).then((response, status, jqXHR) => { - if (parseInt(jqXHR.status / 100) !== 2) { + if (parseInt(jqXHR.status / 100) !== 2 || jqXHR.status === 204) { + this.draw().empty() return } if (jqXHR.status === 200) { @@ -44,7 +45,7 @@ const parent = $(this.div_id) parent.html('') if (this.mailboxes.length === 0) { - parent.html('No mailboxes registered.') + this.draw().empty() return } const list = $('
').addClass('ui list') @@ -54,6 +55,13 @@ ) }) parent.append(list) + }, + empty: () => { + const parent = $(this.div_id) + parent.html('') + parent.append( + $('
').addClass('ui message').html('No mailboxes registered.') + ) } } } @@ -63,4 +71,4 @@ mailboxes.get() }) -@endpush \ No newline at end of file +@endpush From c0ddd00cc68d4841cf14fa0cc8c335ae6bedb95f Mon Sep 17 00:00:00 2001 From: Aldarien Date: Wed, 30 Nov 2022 20:45:18 -0300 Subject: [PATCH 26/35] Scheduling UI corrected --- api/common/Controller/Jobs.php | 13 +- api/src/Model/Message.php | 31 ++- api/src/Repository/Message.php | 5 +- ui/resources/views/emails/messages.blade.php | 266 ++++++------------- 4 files changed, 115 insertions(+), 200 deletions(-) diff --git a/api/common/Controller/Jobs.php b/api/common/Controller/Jobs.php index d0eca33..9320eae 100644 --- a/api/common/Controller/Jobs.php +++ b/api/common/Controller/Jobs.php @@ -12,7 +12,7 @@ class Jobs { use Json; - public function schedule(ServerRequestInterface $request, ResponseInterface $response, Service $jobsService): ResponseInterface + public function schedule(ServerRequestInterface $request, ResponseInterface $response, Service $service, \ProVM\Common\Service\Messages $messagesService): ResponseInterface { $body = $request->getBody(); $json = \Safe\json_decode($body->getContents()); @@ -25,21 +25,24 @@ class Jobs 'scheduled' => 0 ]; foreach ($json->messages as $message_id) { - if ($jobsService->schedule($message_id)) { + if ($service->schedule($message_id)) { + $message = $messagesService->getRepository()->fetchById($message_id); + $message->doesHaveScheduledDownloads(); + $messagesService->getRepository()->save($message); $output['scheduled'] ++; } } return $this->withJson($response, $output); } - public function pending(ServerRequestInterface $request, ResponseInterface $response, Service $jobsService): ResponseInterface + public function pending(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface { $pending = array_map(function(Job $job) { return $job->toArray(); - }, $jobsService->getPending()); + }, $service->getPending()); $output = [ 'total' => count($pending), 'pending' => $pending ]; return $this->withJson($response, $output); } -} \ No newline at end of file +} diff --git a/api/src/Model/Message.php b/api/src/Model/Message.php index e89e27a..d9a4373 100644 --- a/api/src/Model/Message.php +++ b/api/src/Model/Message.php @@ -108,7 +108,12 @@ class Message implements Model } public function getState(string $name): \ProVM\Emails\Model\State\Message { - return $this->getStates()[$name]; + try { + return $this->getStates()[$name]; + } catch (\Exception $e) { + $this->newState($name); + return $this->getStates()[$name]; + } } public function addState(\ProVM\Emails\Model\State\Message $state): Message { @@ -127,6 +132,7 @@ class Message implements Model $this->addState((new \ProVM\Emails\Model\State\Message()) ->setName($name) ->setMessage($this) + ->setValue(false) ); return $this; } @@ -143,31 +149,31 @@ class Message implements Model { return $this->getState('downloaded_attachments')->getValue() ?? false; } + public function hasScheduledDownloads(): bool + { + return $this->getState('scheduled_downloads')->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; } + public function doesHaveScheduledDownloads(): Message + { + $this->getState('scheduled_downloads')->setValue(true); + return $this; + } protected array $attachments; public function getAttachments(): array @@ -205,11 +211,12 @@ class Message implements Model 'states' => [ 'has_attachments' => $this->hasAttachments(), 'valid_attachments' => $this->hasValidAttachments(), - 'downloaded_attachments' => $this->hasDownloadedAttachments() + 'downloaded_attachments' => $this->hasDownloadedAttachments(), + 'scheduled_downloads' => $this->hasScheduledDownloads() ], 'attachments' => $this->hasValidAttachments() ? array_map(function(Attachment $attachment) { return $attachment->toArray(); }, $this->getAttachments()) : [] ]; } -} \ No newline at end of file +} diff --git a/api/src/Repository/Message.php b/api/src/Repository/Message.php index 4976dd4..45d192f 100644 --- a/api/src/Repository/Message.php +++ b/api/src/Repository/Message.php @@ -111,7 +111,8 @@ class Message extends Repository $valid_states = [ 'has_attachments', 'valid_attachments', - 'downloaded_attachments' + 'downloaded_attachments', + 'scheduled_downloads' ]; foreach ($valid_states as $state_name) { try { @@ -160,4 +161,4 @@ class Message extends Repository 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')]); } -} \ No newline at end of file +} diff --git a/ui/resources/views/emails/messages.blade.php b/ui/resources/views/emails/messages.blade.php index 47101f3..4af8bb1 100644 --- a/ui/resources/views/emails/messages.blade.php +++ b/ui/resources/views/emails/messages.blade.php @@ -17,7 +17,8 @@ states = { attachments: false, valid: false, - downloaded: false + downloaded: false, + scheduled: false } attachments @@ -81,10 +82,15 @@ const map_keys = { has_attachments: 'attachments', valid_attachments: 'valid', - downloaded_attachments: 'downloaded' + downloaded_attachments: 'downloaded', + scheduled_downloads: 'scheduled' } Object.keys(states).forEach(k => { - this.states[map_keys[k]] = states[k] + if (k in map_keys) { + this.states[map_keys[k]] = states[k] + } else { + this.states[map_keys[k]] = false + } }) return this }, @@ -103,6 +109,9 @@ }, downloaded: () => { return this.states.downloaded + }, + scheduled: () => { + return this.states.scheduled } } } @@ -119,80 +128,99 @@ downloaded: () => { this.states.downloaded = true return this + }, + scheduled: () => { + this.states.scheduled = true + return this } } } draw() { - const format = Intl.DateTimeFormat('es-CL', {dateStyle: 'full', timeStyle: 'short'}) - let date = new Date() - try { - date = new Date(this.get().date()) - } catch (e) { - console.debug(e) - } - const valid = $('').html($('').addClass(this.has().valid() ? 'green check circle icon' : 'red times circle icon')) - if (this.has().valid() && this.get().attachments().length > 0) { - const list = $('
').addClass('ui list') - this.get().attachments().forEach(attachment => { - list.append($('
').addClass('item').html(attachment.filename)) - }) - valid.append( - $('
').addClass('ui popup').append(list) - ) - } - const tr = $('').append( - $('').html(this.get().subject()) - ).append( - $('').html(format.format(date)) - ).append( - $('').html(this.get().from().full) - ).append( - $('').html($('').addClass(this.has().attachments() ? 'green check circle icon' : 'red times circle icon')) - ).append( - valid - ).append( - $('').html($('').addClass(this.has().downloaded() ? 'green check circle icon' : 'red times circle icon')) - ).append( - $('').append( - (this.has().attachments() && this.has().valid() && !this.has().downloaded()) ? - $('').addClass('ui mini green circular icon button').append( - $('').addClass('clock icon') - ).click(() => { - this.download().attachments(this.get().id()) - }) : '' - ) - ) - if (this.has().downloaded()) { - tr.find('td').each((i, td) => { - const content = $(td).html() - if (content.indexOf('icon') > -1) { - return + return { + row: () => { + const format = Intl.DateTimeFormat('es-CL', {dateStyle: 'full', timeStyle: 'short'}) + let date = new Date() + try { + date = new Date(this.get().date()) + } catch (e) { + console.debug(e) } - $(td).html('') - $(td).append( - $('').attr('href', '{{$urls->base}}/emails/message/' + this.id).html(content) + const valid = $('').html($('').addClass(this.has().valid() ? 'green check circle icon' : 'red times circle icon')) + if (this.has().valid() && this.get().attachments().length > 0) { + const list = $('
').addClass('ui list') + this.get().attachments().forEach(attachment => { + list.append($('
').addClass('item').html(attachment.filename)) + }) + valid.append( + $('
').addClass('ui popup').append(list) + ) + } + const tr = $('').attr('data-id', this.get().id()).append( + $('').html(this.get().subject()) + ).append( + $('').html(format.format(date)) + ).append( + $('').html(this.get().from().full) + ).append( + $('').html($('').addClass(this.has().attachments() ? 'green check circle icon' : 'red times circle icon')) + ).append( + valid + ).append( + $('').html($('').addClass(this.has().downloaded() ? 'green check circle icon' : 'red times circle icon')) + ).append( + $('').append( + (this.has().attachments() && this.has().valid() && !this.has().downloaded()) ? + ((this.has().scheduled()) ? this.draw().scheduledButton() : this.draw().scheduleButton()) : '' + ) ) - }) + if (this.has().downloaded()) { + tr.find('td').each((i, td) => { + const content = $(td).html() + if (content.indexOf('icon') > -1) { + return + } + $(td).html('') + $(td).append( + $('').attr('href', '{{$urls->base}}/emails/message/' + this.id).html(content) + ) + }) + } + return tr + }, + scheduleButton: () => { + return $('').addClass('ui mini circular icon button').append( + $('').addClass('clock icon') + ).click(e => { + this.download().attachments(e) + }) + }, + scheduledButton: () => { + return $('').addClass('ui green circular inverted check icon') + }, + schedulingButton: () => { + return $('').addClass('ui circular inverted redo loading icon') + } } - return tr } - download() { return { - attachments: id => { + attachments: event => { + const td = $(event.currentTarget).parent() + const id = this.get().id() const uri = '/messages/schedule' const data = { messages: [ id ] } + td.html('').append(this.draw().schedulingButton()) return Send.put({ uri, data }).then(response => { if (response.scheduled > 0) { - alert('Scheduled Attachments Job') + td.html('').append(this.draw().scheduledButton()) } }) } @@ -205,9 +233,6 @@ name: '', count: '' }, - page: 1, - current: 0, - shown: 10, total: null, visible: [], get: function() { @@ -250,7 +275,6 @@ } }, draw: function() { - const columns = 8 return { loader: () => { $(this.id.results).html('').append( @@ -270,8 +294,6 @@ this.draw().head() ).append( this.draw().body() - ).append( - this.draw().footer() ) }, head: () => { @@ -286,43 +308,8 @@ } values.push(v) } - const dropdown = $('
').addClass('ui scrolling dropdown').append( - $('').addClass('dropdown icon') - ).append( - $('
').addClass('text').html(this.shown) - ).dropdown({ - values, - onChange: (value, text, $choice) => { - if (value === this.shown) { - return - } - this.shown = parseInt(value) - // Trigger pending transition - $choice.parent().parent().dropdown('hide') - this.get() - } - }) + return $('').append( - $('').append( - $('').addClass('right aligned').attr('colspan', columns).append( - dropdown - ).append( - $('').addClass('ui basic label').html((this.current + 1) + ' - ' + Math.min(this.shown + this.current, this.total ?? 99999999)) - ).append( - $('').addClass('ui mini circular icon button').append( - $('').addClass('left chevron icon') - ).click(() => { - this.prev() - }) - ).append( - $('').addClass('ui mini circular icon button').append( - $('').addClass('right chevron icon') - ).click(() => { - this.next() - }) - ) - ) - ).append( $('').append( $('').html('#') ).append( @@ -345,7 +332,7 @@ body: () => { const tbody = $('') this.visible.forEach((m, i) => { - const row = m.draw() + const row = m.draw().row() row.prepend( $('').html(i + this.current + 1) ) @@ -353,95 +340,12 @@ }) return tbody }, - footer: () => { - const pages = this.lastPage() - const paging = $('
').addClass('ui right floated pagination menu') - if (this.page !== 1) { - paging.append( - $('').addClass('icon item').append( - $('').addClass('step backward icon') - ).click(() => { - this.first() - }) - ) - } - for (let i = 0; i < pages; i ++) { - const page = $('').addClass('item').html(i + 1).click(() => { - this.goto(i + 1) - }) - if (i + 1 === this.page) { - page.addClass('active') - } - paging.append(page) - } - if (this.page !== this.lastPage()) { - paging.append( - $('').addClass('icon item').append( - $('').addClass('step forward icon') - ).click(() => { - this.last() - }) - ) - } - return $('').append( - $('').append( - $('').attr('colspan', columns).append(paging) - ) - ) - }, empty: () => { $(this.id.count).html(' (0)') return $('
').addClass('ui message').html('No messages found.') } } }, - lastPage: function() { - let pages = Math.floor(this.total / this.shown) - if (this.total / this.shown > pages) { - pages ++ - } - return pages - }, - first: function() { - if (this.current === 0) { - return - } - this.page = 1 - this.current = 0 - this.get() - }, - next: function() { - if (this.current + this.shown >= this.total) { - return - } - this.page ++ - this.current += this.shown - this.get() - }, - goto: function(page) { - if (this.page === page) { - return - } - this.page = page - this.current = (this.page - 1) * this.shown - this.get() - }, - prev: function() { - if (this.current < this.shown) { - return - } - this.page -- - this.current -= this.shown - this.get() - }, - last: function() { - if (this.page === this.lastPage()) { - return - } - this.page = this.lastPage() - this.current = (this.page - 1) * this.shown - this.get() - }, setup: function() { this.get().mailbox().then(() => { this.get().messages() From de81f16557c2af78876a71846a46e1d2fd312195 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Thu, 1 Dec 2022 14:15:54 -0300 Subject: [PATCH 27/35] Logging --- api/common/Middleware/Logging.php | 38 +++++++++++++++++++ api/setup/middleware/98_log.php | 2 + api/setup/settings/02_folders.php | 3 +- api/setup/setups/04_middlewares.php | 5 ++- api/setup/setups/98_log.php | 14 ++++++- cli/common/Middleware/Logging.php | 38 +++++++++++++++++++ cli/public/index.php | 1 + cli/setup/middleware/98_log.php | 2 + cli/setup/settings/02_folders.php | 3 +- cli/setup/setups/03_middleware.php | 8 ++++ cli/setup/setups/98_log.php | 30 +++++++++++---- ui/common/Controller/Api.php | 7 ++-- ui/common/Middleware/Logging.php | 38 +++++++++++++++++++ ui/public/index.php | 1 + ui/resources/routes/98_api.php | 2 +- ui/resources/views/emails/show.blade.php | 6 +-- .../layout/body/footer/scripts/main.blade.php | 9 ++--- ui/setup/middleware/98_log.php | 2 + ui/setup/settings/02_folders.php | 3 +- ui/setup/setups/02_api.php | 4 +- ui/setup/setups/04_middleware.php | 8 ++++ ui/setup/setups/98_log.php | 22 ++++++++--- 22 files changed, 211 insertions(+), 35 deletions(-) create mode 100644 api/common/Middleware/Logging.php create mode 100644 api/setup/middleware/98_log.php create mode 100644 cli/common/Middleware/Logging.php create mode 100644 cli/setup/middleware/98_log.php create mode 100644 cli/setup/setups/03_middleware.php create mode 100644 ui/common/Middleware/Logging.php create mode 100644 ui/setup/middleware/98_log.php create mode 100644 ui/setup/setups/04_middleware.php diff --git a/api/common/Middleware/Logging.php b/api/common/Middleware/Logging.php new file mode 100644 index 0000000..a95c37b --- /dev/null +++ b/api/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' => var_export($request->getUri(), true), + 'body' => $request->getBody()->getContents() + ]; + $this->getLogger()->info(\Safe\json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return $response; + } +} 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/settings/02_folders.php b/api/setup/settings/02_folders.php index 669c193..1b381f4 100644 --- a/api/setup/settings/02_folders.php +++ b/api/setup/settings/02_folders.php @@ -18,4 +18,5 @@ return [ ]); }, 'attachments_folder' => $_ENV['ATTACHMENTS_FOLDER'], -]; \ No newline at end of file + 'logs_folder' => '/logs', +]; diff --git a/api/setup/setups/04_middlewares.php b/api/setup/setups/04_middlewares.php index b94e9ec..1ec3b45 100644 --- a/api/setup/setups/04_middlewares.php +++ b/api/setup/setups/04_middlewares.php @@ -7,5 +7,8 @@ return [ $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')); } -]; \ No newline at end of file +]; diff --git a/api/setup/setups/98_log.php b/api/setup/setups/98_log.php index 0aedf37..ec2c9a7 100644 --- a/api/setup/setups/98_log.php +++ b/api/setup/setups/98_log.php @@ -10,13 +10,23 @@ return [ $handler->setFormatter($container->get(Monolog\Formatter\SyslogFormatter::class)); return $handler; }, + 'request_logger' => function(ContainerInterface $container) { + $logger = new Monolog\Logger('request_logger'); + $handler = new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log'])); + $handler->setFormatter($container->get(Monolog\Formatter\SyslogFormatter::class)); + $dedupHandler = new Monolog\Handler\DeduplicationHandler($handler, null, Monolog\Level::Info); + $logger->pushHandler($dedupHandler); + $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; + }, Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { $logger = new Monolog\Logger('file_logger'); $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/cli/common/Middleware/Logging.php b/cli/common/Middleware/Logging.php new file mode 100644 index 0000000..a95c37b --- /dev/null +++ b/cli/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' => var_export($request->getUri(), true), + 'body' => $request->getBody()->getContents() + ]; + $this->getLogger()->info(\Safe\json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return $response; + } +} diff --git a/cli/public/index.php b/cli/public/index.php index daaaaed..aed6193 100644 --- a/cli/public/index.php +++ b/cli/public/index.php @@ -4,6 +4,7 @@ $app = require_once implode(DIRECTORY_SEPARATOR, [ 'setup', 'app.php' ]); +Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface::class)); try { $app->run(); } catch (Error | Exception $e) { diff --git a/cli/setup/middleware/98_log.php b/cli/setup/middleware/98_log.php new file mode 100644 index 0000000..4df6f6c --- /dev/null +++ b/cli/setup/middleware/98_log.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(ProVM\Common\Middleware\Logging::class)); diff --git a/cli/setup/settings/02_folders.php b/cli/setup/settings/02_folders.php index 0dd206a..fcafa9b 100644 --- a/cli/setup/settings/02_folders.php +++ b/cli/setup/settings/02_folders.php @@ -14,5 +14,6 @@ return [ $container->get('resources_folder'), 'commands' ]); - } + }, + 'logs_folder' => '/logs', ]; diff --git a/cli/setup/setups/03_middleware.php b/cli/setup/setups/03_middleware.php new file mode 100644 index 0000000..1d6cc44 --- /dev/null +++ b/cli/setup/setups/03_middleware.php @@ -0,0 +1,8 @@ + function(ContainerInterface $container) { + return new ProVM\Common\Middleware\Logging($container->get('request_logger')); + } +]; diff --git a/cli/setup/setups/98_log.php b/cli/setup/setups/98_log.php index eaef107..b33a5ce 100644 --- a/cli/setup/setups/98_log.php +++ b/cli/setup/setups/98_log.php @@ -2,14 +2,28 @@ use Psr\Container\ContainerInterface; return [ - \Monolog\Handler\RotatingFileHandler::class => function(ContainerInterface $container) { - $handler = new \Monolog\Handler\RotatingFileHandler($container->get('log_file')); - $handler->setFormatter($container->get(\Monolog\Formatter\LineFormatter::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)); return $handler; }, - \Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { - $logger = new \Monolog\Logger('file_logger'); - $logger->pushHandler($container->get(\Monolog\Handler\RotatingFileHandler::class)); + 'request_logger' => function(ContainerInterface $container) { + $logger = new Monolog\Logger('request_logger'); + $handler = new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log'])); + $handler->setFormatter($container->get(Monolog\Formatter\SyslogFormatter::class)); + $dedupHandler = new Monolog\Handler\DeduplicationHandler($handler, null, Monolog\Level::Info); + $logger->pushHandler($dedupHandler); + $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 + }, + Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { + $logger = new Monolog\Logger('file_logger'); + $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; + }, +]; diff --git a/ui/common/Controller/Api.php b/ui/common/Controller/Api.php index 4202787..7efcdc8 100644 --- a/ui/common/Controller/Api.php +++ b/ui/common/Controller/Api.php @@ -4,14 +4,13 @@ namespace ProVM\Common\Controller; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use ProVM\Common\Service\Api as Service; +use Psr\Log\LoggerInterface; class Api { - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, Service $service, LoggerInterface $logger): ResponseInterface { - $body = $request->getBody(); - $json = \Safe\json_decode($body->getContents(), JSON_OBJECT_AS_ARRAY); - + $json = $request->getParsedBody(); return $service->sendRequest($json); } } diff --git a/ui/common/Middleware/Logging.php b/ui/common/Middleware/Logging.php new file mode 100644 index 0000000..c60465d --- /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' => var_export($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/public/index.php b/ui/public/index.php index 9659116..71055da 100644 --- a/ui/public/index.php +++ b/ui/public/index.php @@ -8,6 +8,7 @@ Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface try { $app->run(); } catch (Error | Exception $e) { + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->debug(json_encode(compact('_REQUEST'))); $app->getContainer()->get(Psr\Log\LoggerInterface::class)->error($e); throw $e; } diff --git a/ui/resources/routes/98_api.php b/ui/resources/routes/98_api.php index 4baf51f..26bf2d3 100644 --- a/ui/resources/routes/98_api.php +++ b/ui/resources/routes/98_api.php @@ -1,4 +1,4 @@ post('/api', Api::class); \ No newline at end of file +$app->post('/api[/]', Api::class); diff --git a/ui/resources/views/emails/show.blade.php b/ui/resources/views/emails/show.blade.php index 32bba7d..8a037d6 100644 --- a/ui/resources/views/emails/show.blade.php +++ b/ui/resources/views/emails/show.blade.php @@ -3,7 +3,7 @@ @section('emails_content')

Message - -

-
+ @endsection @push('page_scripts') @@ -82,7 +82,7 @@ attachments: parent => { this.get().attachments().forEach(attachment => { parent.append( - $('').attr('href', _urls.base + '/attachment/' + attachment.id).attr('download', attachment.fullname).html(attachment.fullname) + $('').attr('href', '{{$urls->base}}/attachment/' + attachment.id).attr('download', attachment.fullname).html(attachment.fullname) ) }) } @@ -100,4 +100,4 @@ message.setup('{{$message_id}}') }) -@endpush \ No newline at end of file +@endpush diff --git a/ui/resources/views/layout/body/footer/scripts/main.blade.php b/ui/resources/views/layout/body/footer/scripts/main.blade.php index 721f189..e9327cb 100644 --- a/ui/resources/views/layout/body/footer/scripts/main.blade.php +++ b/ui/resources/views/layout/body/footer/scripts/main.blade.php @@ -3,18 +3,17 @@ base_url: '{{$urls->api}}', base: function({method, uri, data = null}) { const request = { - uri: uri.replace(/^\//g, ''), + uri: uri, method } const options = { url: this.base_url, method: 'post', - contentType: 'application/json' } if (method.toLowerCase() !== 'get' && data !== null) { request['data'] = data } - options['data'] = JSON.stringify(request) + options['data'] = request return $.ajax(options) }, get: function(uri) { @@ -30,5 +29,5 @@ return this.base({method: 'delete', uri, data}) } } - const _urls = JSON.parse('{!! Safe\json_encode($urls) !!}') - \ No newline at end of file + //const _urls = JSON.parse('{!! Safe\json_encode($urls) !!}') + 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/settings/02_folders.php b/ui/setup/settings/02_folders.php index f3a4efd..180321f 100644 --- a/ui/setup/settings/02_folders.php +++ b/ui/setup/settings/02_folders.php @@ -20,6 +20,7 @@ return [ $arr['base'], 'cache' ]); + $arr['logs'] = '/logs'; return (object) $arr; } -]; \ No newline at end of file +]; diff --git a/ui/setup/setups/02_api.php b/ui/setup/setups/02_api.php index 7f7be20..32def7b 100644 --- a/ui/setup/setups/02_api.php +++ b/ui/setup/setups/02_api.php @@ -4,7 +4,7 @@ use Psr\Container\ContainerInterface; return [ Psr\Http\Client\ClientInterface::class => function(ContainerInterface $container) { return new GuzzleHttp\Client([ - 'base_uri' => "http://proxy:8080", + 'base_uri' => $container->get('urls')->api, 'headers' => [ 'Authorization' => [ "Bearer {$container->get('api_key')}" @@ -15,4 +15,4 @@ return [ Psr\Http\Message\RequestFactoryInterface::class => function(ContainerInterface $container) { return $container->get(Nyholm\Psr7\Factory\Psr17Factory::class); }, -]; \ No newline at end of file +]; 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 index 0aedf37..ef8b9d6 100644 --- a/ui/setup/setups/98_log.php +++ b/ui/setup/setups/98_log.php @@ -10,13 +10,23 @@ return [ $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\DeduplicationHandler::class)); - //$logger->pushHandler($container->get(Monolog\Handler\RotatingFileHandler::class)); + 'request_logger' => function(ContainerInterface $container) { + $logger = new Monolog\Logger('request_logger'); + $handler = new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('folders')->logs, 'requests.log'])); + $handler->setFormatter($container->get(Monolog\Formatter\SyslogFormatter::class)); + $dedupHandler = new Monolog\Handler\DeduplicationHandler($handler, null, Monolog\Level::Info); + $logger->pushHandler($dedupHandler); $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 + }, + Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { + $logger = new Monolog\Logger('file_logger'); + $logger->pushHandler($container->get(Monolog\Handler\DeduplicationHandler::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; + }, +]; From 025697d37cb3eaf8a2235326ca9f5372bded7aa1 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Thu, 1 Dec 2022 14:22:13 -0300 Subject: [PATCH 28/35] FIX: Check for _REQUEST --- ui/public/index.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/public/index.php b/ui/public/index.php index 71055da..7ef5588 100644 --- a/ui/public/index.php +++ b/ui/public/index.php @@ -8,7 +8,9 @@ Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface try { $app->run(); } catch (Error | Exception $e) { - $app->getContainer()->get(Psr\Log\LoggerInterface::class)->debug(json_encode(compact('_REQUEST'))); + if (isset($_REQUEST)) { + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->debug(json_encode(compact('_REQUEST'))); + } $app->getContainer()->get(Psr\Log\LoggerInterface::class)->error($e); throw $e; } From 1f8d4f0bcebe5c26a762178a32a78ba0068f74f3 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Thu, 1 Dec 2022 14:32:16 -0300 Subject: [PATCH 29/35] Log _SERVER when errors are found --- api/public/index.php | 7 ++++++- cli/public/index.php | 4 +++- ui/public/index.php | 6 ++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/api/public/index.php b/api/public/index.php index 9659116..9e2bc0e 100644 --- a/api/public/index.php +++ b/api/public/index.php @@ -8,6 +8,11 @@ Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface try { $app->run(); } catch (Error | Exception $e) { - $app->getContainer()->get(Psr\Log\LoggerInterface::class)->error($e); + $logger = $app->getContainer()->get(Psr\Log\LoggerInterface::class); + if (isset($_REQUEST)) { + $logger->debug(Safe\json_encode(compact('_REQUEST'))); + } + $logger->debug(Safe\json_encode(compact('_SERVER'))); + $logger->error($e); throw $e; } diff --git a/cli/public/index.php b/cli/public/index.php index aed6193..63341a5 100644 --- a/cli/public/index.php +++ b/cli/public/index.php @@ -8,6 +8,8 @@ Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface try { $app->run(); } catch (Error | Exception $e) { - $app->getContainer()->get(\Psr\Log\LoggerInterface::class)->error($e); + $logger = $app->getContainer()->get(Psr\Log\LoggerInterface::class); + $logger->debug(Safe\json_encode(compact('_SERVER'))); + $logger->error($e); throw $e; } diff --git a/ui/public/index.php b/ui/public/index.php index 7ef5588..9e2bc0e 100644 --- a/ui/public/index.php +++ b/ui/public/index.php @@ -8,9 +8,11 @@ Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface try { $app->run(); } catch (Error | Exception $e) { + $logger = $app->getContainer()->get(Psr\Log\LoggerInterface::class); if (isset($_REQUEST)) { - $app->getContainer()->get(Psr\Log\LoggerInterface::class)->debug(json_encode(compact('_REQUEST'))); + $logger->debug(Safe\json_encode(compact('_REQUEST'))); } - $app->getContainer()->get(Psr\Log\LoggerInterface::class)->error($e); + $logger->debug(Safe\json_encode(compact('_SERVER'))); + $logger->error($e); throw $e; } From e3719fb7b91683bfdfffdda1b4dba81402316813 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Thu, 1 Dec 2022 14:38:22 -0300 Subject: [PATCH 30/35] *.env.sample files --- .env.sample | 2 +- .mail.env.sample | 3 +-- api/.adminer.env.sample | 2 ++ api/.db.env.sample | 4 ++++ api/.env.sample | 2 ++ cli/.env.sample | 1 + ui/.env.sample | 1 + 7 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 api/.adminer.env.sample create mode 100644 api/.db.env.sample create mode 100644 api/.env.sample create mode 100644 cli/.env.sample create mode 100644 ui/.env.sample diff --git a/.env.sample b/.env.sample index e2d0fd1..bedadcd 100644 --- a/.env.sample +++ b/.env.sample @@ -5,7 +5,7 @@ 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 +COMPOSE_PROFILES=api,ui,cli ATT_PATH=./attachments LOGS_PATH=./logs diff --git a/.mail.env.sample b/.mail.env.sample index 437f229..8a047c4 100644 --- a/.mail.env.sample +++ b/.mail.env.sample @@ -2,5 +2,4 @@ EMAIL_HOST=imap.gmail.com EMAIL_PORT=993 EMAIL_USERNAME=@gmail.com EMAIL_PASSWORD= -EMAIL_FOLDER= -ATTACHMENTS_FOLDER=/attachments \ No newline at end of file +ATTACHMENTS_FOLDER=/attachments 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/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/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 From 5202698d06bd1c695170af93a98df178e70e3bd9 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Thu, 1 Dec 2022 15:33:06 -0300 Subject: [PATCH 31/35] Move .mail.env to api container --- .mail.env.sample => api/.mail.env.sample | 0 api/docker-compose.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename .mail.env.sample => api/.mail.env.sample (100%) diff --git a/.mail.env.sample b/api/.mail.env.sample similarity index 100% rename from .mail.env.sample rename to api/.mail.env.sample diff --git a/api/docker-compose.yml b/api/docker-compose.yml index 0512fbf..173b1be 100644 --- a/api/docker-compose.yml +++ b/api/docker-compose.yml @@ -18,7 +18,7 @@ services: env_file: - ${API_PATH:-.}/.env - ${API_PATH:-.}/.db.env - - .mail.env + - ${API_PATH:-.}/.mail.env - .key.env volumes: - ${API_PATH:-.}/:/app/api From 3ed5acf75e1f944037f781b6fbb716b842b8824b Mon Sep 17 00:00:00 2001 From: Aldarien Date: Thu, 1 Dec 2022 15:33:20 -0300 Subject: [PATCH 32/35] Readme --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index 06f690c..21652a3 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,52 @@ 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 +``` From 9307ba330cbb29f8d2733fbede0102a1d29bc073 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Thu, 8 Jun 2023 20:49:27 -0400 Subject: [PATCH 33/35] Various updates --- NOTES.md | 12 ++- api/common/Controller/Jobs.php | 3 +- api/common/Controller/Messages.php | 28 +++++- api/common/Define/Model.php | 6 +- api/common/Define/Repository.php | 28 ++++++ api/common/Factory/Model.php | 2 +- api/common/Implement/Repository.php | 118 +++++++++++++----------- api/common/Middleware/Attachments.php | 25 +++++ api/common/Middleware/Install.php | 20 ++++ api/common/Middleware/Logging.php | 23 +---- api/common/Middleware/Mailboxes.php | 24 +++++ api/common/Middleware/Messages.php | 24 +++++ api/common/Service/Attachments.php | 92 ++++++++++++++++-- api/common/Service/Install.php | 40 ++++++++ api/common/Service/Mailboxes.php | 15 ++- api/common/Service/Messages.php | 43 ++++++++- api/public/index.php | 12 +-- api/setup/middleware/04_db.php | 5 + api/setup/settings/01_env.php | 5 +- api/setup/settings/04_db.php | 33 +++++++ api/setup/settings/98_log.php | 5 +- api/setup/setups/01_emails.php | 8 +- api/setup/setups/02_services.php | 15 +++ api/setup/setups/03_factories.php | 23 +++-- api/setup/setups/04_middlewares.php | 2 +- api/setup/setups/97_auth.php | 8 +- api/setup/setups/98_log.php | 72 ++++++++++----- api/src/Model/Attachment.php | 6 +- api/src/Model/Job.php | 6 +- api/src/Model/Mailbox.php | 9 +- api/src/Model/Message.php | 12 ++- api/src/Model/State/Attachment.php | 16 +++- api/src/Model/State/Mailbox.php | 19 +++- api/src/Model/State/Message.php | 16 +++- api/src/Repository/Attachment.php | 23 ++++- api/src/Repository/Job.php | 17 +++- api/src/Repository/Mailbox.php | 14 ++- api/src/Repository/Message.php | 47 +++++++--- api/src/Repository/State/Attachment.php | 18 +++- api/src/Repository/State/Mailbox.php | 19 +++- api/src/Repository/State/Message.php | 18 +++- cli/common/Command/DecryptPdf.php | 9 +- cli/common/Command/GrabAttachments.php | 9 +- cli/common/Command/Messages.php | 9 +- emails-2022-11-30-03-59-49.sql | 94 +++++++++++++++++++ 45 files changed, 864 insertions(+), 188 deletions(-) create mode 100644 api/common/Define/Repository.php create mode 100644 api/common/Middleware/Attachments.php create mode 100644 api/common/Middleware/Install.php create mode 100644 api/common/Middleware/Mailboxes.php create mode 100644 api/common/Middleware/Messages.php create mode 100644 api/common/Service/Install.php create mode 100644 api/setup/middleware/04_db.php create mode 100644 api/setup/settings/04_db.php create mode 100644 emails-2022-11-30-03-59-49.sql diff --git a/NOTES.md b/NOTES.md index d20957b..ec9035b 100644 --- a/NOTES.md +++ b/NOTES.md @@ -25,4 +25,14 @@ -> **[API]** Register selected `mailboxes` and get `messages` for recently registered. * **[Cron]** Get registered `mailboxes` -> **[API]** Get `messages` * **[User]** Check messages found -> **[API]** Schedule `attachments` -* **[Cron]** Get `attachment download` jobs -> **[API]** grab `attachments` \ No newline at end of file +* **[Cron]** Get `attachment download` jobs -> **[API]** grab `attachments` + +### Jobs +#### Automatic +* [ ] Check registered `mailboxes` for new `messages`, logging last check. +* [ ] Check if `attachments` are `encrypted`. +* [ ] Check for new scheduled jobs. +#### Scheduled +* [ ] Grab `messages`. +* [ ] Grab `attachments`. +* [ ] Decrypt `attachments`. diff --git a/api/common/Controller/Jobs.php b/api/common/Controller/Jobs.php index 9320eae..437555f 100644 --- a/api/common/Controller/Jobs.php +++ b/api/common/Controller/Jobs.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ServerRequestInterface; use ProVM\Common\Exception\Request\MissingArgument; use ProVM\Common\Implement\Controller\Json; use ProVM\Common\Service\Jobs as Service; +use function Safe\json_decode; class Jobs { @@ -15,7 +16,7 @@ class Jobs public function schedule(ServerRequestInterface $request, ResponseInterface $response, Service $service, \ProVM\Common\Service\Messages $messagesService): ResponseInterface { $body = $request->getBody(); - $json = \Safe\json_decode($body->getContents()); + $json = json_decode($body->getContents()); if (!isset($json->messages)) { throw new MissingArgument('messages', 'array', 'messages ids'); } diff --git a/api/common/Controller/Messages.php b/api/common/Controller/Messages.php index 47425ef..db86073 100644 --- a/api/common/Controller/Messages.php +++ b/api/common/Controller/Messages.php @@ -1,13 +1,13 @@ 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), @@ -34,6 +45,17 @@ class Messages }, $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), @@ -49,7 +71,7 @@ class Messages public function grab(ServerRequestInterface $request, ResponseInterface $response, Service $service, \ProVM\Common\Service\Attachments $attachmentsService): ResponseInterface { $body = $request->getBody(); - $json = \Safe\json_decode($body->getContents()); + $json = json_decode($body->getContents()); if (!isset($json->mailboxes)) { throw new MissingArgument('mailboxes', 'array', 'mailboxes names'); } @@ -70,4 +92,4 @@ class Messages } return $this->withJson($response, $output); } -} \ No newline at end of file +} diff --git a/api/common/Define/Model.php b/api/common/Define/Model.php index 28a9b45..9af5971 100644 --- a/api/common/Define/Model.php +++ b/api/common/Define/Model.php @@ -1,6 +1,8 @@ getContainer()->get($repository_class); } -} \ No newline at end of file +} diff --git a/api/common/Implement/Repository.php b/api/common/Implement/Repository.php index f8a7b7a..7cbe22d 100644 --- a/api/common/Implement/Repository.php +++ b/api/common/Implement/Repository.php @@ -3,11 +3,11 @@ namespace ProVM\Common\Implement; use PDO; use PDOException; -use ProVM\Common\Exception\Database\BlankResult; use Psr\Log\LoggerInterface; -use ProVM\Common\Define\Model as ModelInterface; +use ProVM\Common\Exception\Database\BlankResult; +use ProVM\Common\Define; -abstract class Repository +abstract class Repository implements Define\Repository { public function __construct(PDO $connection, LoggerInterface $logger) { @@ -32,33 +32,32 @@ abstract class Repository return $this->logger; } - public function setConnection(PDO $pdo): Repository + public function setConnection(PDO $pdo): Define\Repository { $this->connection = $pdo; return $this; } - public function setTable(string $table): Repository + public function setTable(string $table): Define\Repository { $this->table = $table; return $this; } - public function setLogger(LoggerInterface $logger): Repository + public function setLogger(LoggerInterface $logger): Define\Repository { $this->logger = $logger; return $this; } - abstract protected function fieldsForUpdate(): array; - abstract protected function valuesForUpdate(ModelInterface $model): array; - protected function idProperty(): string + public function isInstalled(): bool { - return 'getId'; + $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; } - protected function idField(): string - { - return 'id'; - } - public function update(ModelInterface $model, ModelInterface $old): void + public function update(Define\Model $model, Define\Model $old): void { $query = "UPDATE `{$this->getTable()}` SET "; $model_values = $this->valuesForUpdate($model); @@ -79,22 +78,7 @@ abstract class Repository $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 + public function save(Define\Model &$model): void { try { $old = $this->defaultFind($model); @@ -107,12 +91,7 @@ abstract class Repository throw $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 + public function create(array $data): Define\Model { try { return $this->defaultSearch($data); @@ -121,11 +100,6 @@ abstract class Repository 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"; @@ -136,7 +110,7 @@ abstract class Repository $query = "OPTIMIZE TABLE `{$this->getTable()}`"; $this->getConnection()->query($query); } - public function delete(ModelInterface $model): void + public function delete(Define\Model $model): void { $query = "DELETE FROM `{$this->getTable()}` WHERE `{$this->idField()}` = ?"; $st = $this->getConnection()->prepare($query); @@ -144,8 +118,42 @@ abstract class Repository $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 fetchOne(string $query, ?array $values = null): ModelInterface + 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); @@ -174,14 +182,14 @@ abstract class Repository 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 + 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/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 index a95c37b..2e91a6d 100644 --- a/api/common/Middleware/Logging.php +++ b/api/common/Middleware/Logging.php @@ -5,34 +5,21 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; +use function Safe\json_encode; class Logging { - public function __construct(LoggerInterface $logger) { - $this->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 __construct(protected LoggerInterface $logger) {} public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); $output = [ 'uri' => var_export($request->getUri(), true), - 'body' => $request->getBody()->getContents() + 'body' => $request->getBody()->getContents(), + 'response' => (clone $response)->getBody()->getContents() ]; - $this->getLogger()->info(\Safe\json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + $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/Middleware/Messages.php b/api/common/Middleware/Messages.php new file mode 100644 index 0000000..13c13cb --- /dev/null +++ b/api/common/Middleware/Messages.php @@ -0,0 +1,24 @@ +service->checkSchedule(); + } 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 index 6c92915..1487b77 100644 --- a/api/common/Service/Attachments.php +++ b/api/common/Service/Attachments.php @@ -1,15 +1,15 @@ getFullFilename() ]); } - return \Safe\file_get_contents($filename); + 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($subject, $date, $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); @@ -183,7 +203,7 @@ class Attachments extends Base $attachment->getFullFilename() ]); try { - \Safe\file_put_contents($destination, $remote_attachment->getDecodedContent()); + file_put_contents($destination, $remote_attachment->getDecodedContent()); return true; } catch (FilesystemException $e) { $this->getLogger()->error($e); @@ -236,6 +256,9 @@ class Attachments extends Base if (!$message->hasValidAttachments()) { return false; } + if ($message->hasDownloadedAttachments()) { + return true; + } foreach ($remote_message->getAttachments() as $attachment) { if (!str_contains($attachment->getFilename(), '.pdf')) { continue; @@ -247,4 +270,59 @@ class Attachments extends Base } return true; } -} \ No newline at end of file + 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/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/Mailboxes.php b/api/common/Service/Mailboxes.php index 908cf00..3eda6e6 100644 --- a/api/common/Service/Mailboxes.php +++ b/api/common/Service/Mailboxes.php @@ -11,7 +11,7 @@ use ProVM\Emails\Repository\State; class Mailboxes extends Base { - public function __construct(Mailbox $repository, Remote\Mailboxes $remoteService, State\Mailbox $states, LoggerInterface $logger) + public function __construct(Mailbox $repository, Remote\Mailboxes $remoteService, State\Mailbox $states, LoggerInterface $logger, protected int $max_update_days) { $this->setRepository($repository) ->setRemoteService($remoteService) @@ -35,7 +35,6 @@ class Mailboxes extends Base { return $this->statesRepository; } - public function setRepository(Mailbox $repository): Mailboxes { $this->repository = $repository; @@ -110,6 +109,7 @@ class Mailboxes extends Base $this->getStatesRepository()->save($state); return true; } catch (PDOException $e) { + $this->getLogger()->error($e); return false; } } @@ -141,4 +141,13 @@ class Mailboxes extends Base } return true; } -} \ No newline at end of file + 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 index c5d0f05..a0cd5e0 100644 --- a/api/common/Service/Messages.php +++ b/api/common/Service/Messages.php @@ -4,6 +4,7 @@ namespace ProVM\Common\Service; use Ddeboer\Imap\Exception\MessageDoesNotExistException; use Ddeboer\Imap\MailboxInterface; use PDOException; +use ProVM\Common\Exception\Database\BlankResult; use ProVM\Common\Exception\Mailbox\Stateless; use Psr\Log\LoggerInterface; use Ddeboer\Imap\MessageInterface; @@ -14,17 +15,19 @@ use Safe\DateTimeImmutable; class Messages extends Base { - public function __construct(Mailboxes $mailboxes, Message $repository, Remote\Messages $remoteService, LoggerInterface $logger) + public function __construct(Mailboxes $mailboxes, Message $repository, Remote\Messages $remoteService, Jobs $jobsService, LoggerInterface $logger) { $this->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 { @@ -38,6 +41,10 @@ class Messages extends Base { return $this->remoteService; } + public function getJobsService(): Jobs + { + return $this->jobsService; + } public function setMailboxes(Mailboxes $mailboxes): Messages { @@ -54,6 +61,11 @@ class Messages extends Base $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 { @@ -142,6 +154,7 @@ class Messages extends Base $message->doesHaveValidAttachments(); } } + error_log(json_encode(compact('message')).PHP_EOL,3,'/logs/debug'); $this->getRepository()->save($message); return true; } catch (PDOException $e) { @@ -174,4 +187,30 @@ class Messages extends Base } return false; } -} \ No newline at end of file + 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->logger->info("Updating messages from {$mailbox->getName()}"); + $this->grab($mailbox->getName()); + } + } + } + public function checkSchedule(): void + { + $messages = $this->getRepository()->fetchAll(); + foreach ($messages as $message) { + if ($message->hasAttachments() and $message->hasValidAttachments() and !$message->hasDownloadedAttachments() and !$message->hasScheduledDownloads()) { + if ($this->getJobsService()->schedule($message->getId())) { + $message->doesHaveDownloadedAttachments(); + $this->getRepository()->save($message); + } + } + } + } +} diff --git a/api/public/index.php b/api/public/index.php index 9e2bc0e..2e6a761 100644 --- a/api/public/index.php +++ b/api/public/index.php @@ -7,12 +7,8 @@ $app = require_once implode(DIRECTORY_SEPARATOR, [ Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface::class)); try { $app->run(); -} catch (Error | Exception $e) { - $logger = $app->getContainer()->get(Psr\Log\LoggerInterface::class); - if (isset($_REQUEST)) { - $logger->debug(Safe\json_encode(compact('_REQUEST'))); - } - $logger->debug(Safe\json_encode(compact('_SERVER'))); - $logger->error($e); - throw $e; +} 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/setup/middleware/04_db.php b/api/setup/middleware/04_db.php new file mode 100644 index 0000000..9d831e8 --- /dev/null +++ b/api/setup/middleware/04_db.php @@ -0,0 +1,5 @@ +add($app->getContainer()->get(ProVM\Common\Middleware\Attachments::class)); +$app->add($app->getContainer()->get(ProVM\Common\Middleware\Messages::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/settings/01_env.php b/api/setup/settings/01_env.php index 6e61213..89deb37 100644 --- a/api/setup/settings/01_env.php +++ b/api/setup/settings/01_env.php @@ -5,7 +5,7 @@ return [ 'host' => $_ENV['EMAIL_HOST'], 'username' => $_ENV['EMAIL_USERNAME'], 'password' => $_ENV['EMAIL_PASSWORD'], - 'folder' => $_ENV['EMAIL_FOLDER'], + //'folder' => $_ENV['EMAIL_FOLDER'], ]; if (isset($_ENV['EMAIL_PORT'])) { $data['port'] = $_ENV['EMAIL_PORT']; @@ -27,5 +27,6 @@ return [ $arr['port'] = $_ENV['MYSQL_PORT']; } return (object) $arr; - } + }, + 'max_update_days' => 7 ]; 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 index 55328ea..3c1e3f0 100644 --- a/api/setup/settings/98_log.php +++ b/api/setup/settings/98_log.php @@ -1,4 +1,5 @@ '/logs/php.log' -]; \ No newline at end of file + 'log_file' => '/logs/php.log', + 'logstash_socket' => 'localhost:50000' +]; diff --git a/api/setup/setups/01_emails.php b/api/setup/setups/01_emails.php index 23c62b3..1428bae 100644 --- a/api/setup/setups/01_emails.php +++ b/api/setup/setups/01_emails.php @@ -5,13 +5,13 @@ return [ Ddeboer\Imap\ServerInterface::class => 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, $emails->port); } - return new \Ddeboer\Imap\Server($emails->host); + return new Ddeboer\Imap\Server($emails->host); }, - \Ddeboer\Imap\ConnectionInterface::class => function(ContainerInterface $container) { + Ddeboer\Imap\ConnectionInterface::class => function(ContainerInterface $container) { $emails = $container->get('emails'); - $server = $container->get(\Ddeboer\Imap\ServerInterface::class); + $server = $container->get(Ddeboer\Imap\ServerInterface::class); return $server->authenticate($emails->username, $emails->password); }, PDO::class => function(ContainerInterface $container) { diff --git a/api/setup/setups/02_services.php b/api/setup/setups/02_services.php index 8fd0f0a..2a09318 100644 --- a/api/setup/setups/02_services.php +++ b/api/setup/setups/02_services.php @@ -18,5 +18,20 @@ return [ $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') + ); } ]; diff --git a/api/setup/setups/03_factories.php b/api/setup/setups/03_factories.php index 654ca74..281eafa 100644 --- a/api/setup/setups/03_factories.php +++ b/api/setup/setups/03_factories.php @@ -2,16 +2,15 @@ use Psr\Container\ContainerInterface; return [ - \ProVM\Common\Factory\Model::class => 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); + ProVM\Common\Factory\Model::class => 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, + "State\\Mailbox" => ProVM\Emails\Repository\State\Mailbox::class, + "State\\Message" => ProVM\Emails\Repository\State\Message::class, + "State\\Attachment" => ProVM\Emails\Repository\State\Attachment::class + ]); } -]; \ No newline at end of file +]; diff --git a/api/setup/setups/04_middlewares.php b/api/setup/setups/04_middlewares.php index 1ec3b45..10775dd 100644 --- a/api/setup/setups/04_middlewares.php +++ b/api/setup/setups/04_middlewares.php @@ -10,5 +10,5 @@ return [ }, 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 index 17e45ac..03fa0f3 100644 --- a/api/setup/setups/97_auth.php +++ b/api/setup/setups/97_auth.php @@ -2,10 +2,10 @@ use Psr\Container\ContainerInterface; return [ - \ProVM\Common\Middleware\Auth::class => function(ContainerInterface $container) { - return new \ProVM\Common\Middleware\Auth( - $container->get(\Nyholm\Psr7\Factory\Psr17Factory::class), - $container->get(\Psr\Log\LoggerInterface::class), + ProVM\Common\Middleware\Auth::class => function(ContainerInterface $container) { + return new ProVM\Common\Middleware\Auth( + $container->get(Nyholm\Psr7\Factory\Psr17Factory::class), + $container->get(Psr\Log\LoggerInterface::class), $container->get('api_key') ); } diff --git a/api/setup/setups/98_log.php b/api/setup/setups/98_log.php index ec2c9a7..4c6a66e 100644 --- a/api/setup/setups/98_log.php +++ b/api/setup/setups/98_log.php @@ -2,31 +2,61 @@ use Psr\Container\ContainerInterface; return [ - Monolog\Handler\DeduplicationHandler::class => function(ContainerInterface $container) { - return new Monolog\Handler\DeduplicationHandler($container->get(Monolog\Handler\RotatingFileHandler::class)); + 'log_processors' => function(ContainerInterface $container) { + return [ + $container->get(Monolog\Processor\PsrLogMessageProcessor::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_log_handler' => function(ContainerInterface $container) { + return (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log']))) + ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true)); }, 'request_logger' => function(ContainerInterface $container) { - $logger = new Monolog\Logger('request_logger'); - $handler = new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log'])); - $handler->setFormatter($container->get(Monolog\Formatter\SyslogFormatter::class)); - $dedupHandler = new Monolog\Handler\DeduplicationHandler($handler, null, Monolog\Level::Info); - $logger->pushHandler($dedupHandler); - $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; + 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) { - $logger = new Monolog\Logger('file_logger'); - $logger->pushHandler($container->get(Monolog\Handler\DeduplicationHandler::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; + return $container->get('elk_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/src/Model/Attachment.php b/api/src/Model/Attachment.php index 49bf820..912a2e8 100644 --- a/api/src/Model/Attachment.php +++ b/api/src/Model/Attachment.php @@ -169,4 +169,8 @@ class Attachment implements Model 'decrypted' => $this->isDecrypted() ]; } -} \ No newline at end of file + public function jsonSerialize(): mixed + { + return $this->toArray(); + } +} diff --git a/api/src/Model/Job.php b/api/src/Model/Job.php index 421f79a..b4a9cc5 100644 --- a/api/src/Model/Job.php +++ b/api/src/Model/Job.php @@ -58,4 +58,8 @@ class Job implements Model 'executed' => $this->isExecuted() ]; } -} \ No newline at end of file + public function jsonSerialize(): mixed + { + return $this->toArray(); + } +} diff --git a/api/src/Model/Mailbox.php b/api/src/Model/Mailbox.php index 0a4221a..770f715 100644 --- a/api/src/Model/Mailbox.php +++ b/api/src/Model/Mailbox.php @@ -104,6 +104,9 @@ class Mailbox implements Model public function lastPosition(): int { $state = $this->lastState()->getUIDs(); + if (count($state) === 0) { + return 0; + } return array_key_last($state); } @@ -119,4 +122,8 @@ class Mailbox implements Model ] ]; } -} \ No newline at end of file + public function jsonSerialize(): mixed + { + return $this->toArray(); + } +} diff --git a/api/src/Model/Message.php b/api/src/Model/Message.php index d9a4373..5992956 100644 --- a/api/src/Model/Message.php +++ b/api/src/Model/Message.php @@ -99,14 +99,14 @@ class Message implements Model { if (!isset($this->states)) { try { - $this->setStates($this->getFactory()->find(\ProVM\Emails\Model\State\Message::class)->fetchByMessage($this->getId())); + $this->setStates($this->getFactory()->find(State\Message::class)->fetchByMessage($this->getId())); } catch (BlankResult $e) { return []; } } return $this->states; } - public function getState(string $name): \ProVM\Emails\Model\State\Message + public function getState(string $name): State\Message { try { return $this->getStates()[$name]; @@ -115,7 +115,7 @@ class Message implements Model return $this->getStates()[$name]; } } - public function addState(\ProVM\Emails\Model\State\Message $state): Message + public function addState(State\Message $state): Message { $this->states[$state->getName()] = $state; return $this; @@ -129,7 +129,7 @@ class Message implements Model } protected function newState(string $name): Message { - $this->addState((new \ProVM\Emails\Model\State\Message()) + $this->addState((new State\Message()) ->setName($name) ->setMessage($this) ->setValue(false) @@ -219,4 +219,8 @@ class Message implements Model }, $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 index a50961b..edb5e86 100644 --- a/api/src/Model/State/Attachment.php +++ b/api/src/Model/State/Attachment.php @@ -47,4 +47,18 @@ class Attachment implements Model $this->value = $value; return $this; } -} \ No newline at end of file + + 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/Mailbox.php b/api/src/Model/State/Mailbox.php index 35d5df6..35e66d3 100644 --- a/api/src/Model/State/Mailbox.php +++ b/api/src/Model/State/Mailbox.php @@ -30,7 +30,7 @@ class Mailbox implements Model } public function getUIDs(): array { - return $this->uids; + return $this->uids ?? []; } public function setId(int $id): Mailbox @@ -65,4 +65,19 @@ class Mailbox implements Model } return $this; } -} \ No newline at end of file + + 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 index c1fab5c..304cdd6 100644 --- a/api/src/Model/State/Message.php +++ b/api/src/Model/State/Message.php @@ -47,4 +47,18 @@ class Message implements Model $this->value = $value; return $this; } -} \ No newline at end of file + + 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 index 568d4b6..d27f59c 100644 --- a/api/src/Repository/Attachment.php +++ b/api/src/Repository/Attachment.php @@ -28,6 +28,20 @@ class Attachment extends Repository 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 [ @@ -115,4 +129,11 @@ class Attachment extends Repository $query = "SELECT * FROM {$this->getTable()} WHERE message_id = ?"; return $this->fetchMany($query, [$message_id]); } -} \ No newline at end of file + 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 index 164d900..0f47ee3 100644 --- a/api/src/Repository/Job.php +++ b/api/src/Repository/Job.php @@ -29,6 +29,21 @@ class Job extends Repository return $this; } + public function install(): void + { + $query = " +CREATE TABLE {$this->getTable()} ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `message_id` INT UNSIGNED NOT NULL, + `date_time` DATETIME NOT NULL, + `executed` INT(1) UNSIGNED 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(); @@ -105,4 +120,4 @@ class Job extends Repository $query = "SELECT * FROM {$this->getTable()} WHERE `message_id` = ? AND `executed` = 0"; return $this->fetchOne($query, [$message_id]); } -} \ No newline at end of file +} diff --git a/api/src/Repository/Mailbox.php b/api/src/Repository/Mailbox.php index 50a0b92..212355d 100644 --- a/api/src/Repository/Mailbox.php +++ b/api/src/Repository/Mailbox.php @@ -28,6 +28,18 @@ class Mailbox extends Repository 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(); @@ -86,4 +98,4 @@ class Mailbox extends Repository $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 index 45d192f..11ed659 100644 --- a/api/src/Repository/Message.php +++ b/api/src/Repository/Message.php @@ -3,34 +3,50 @@ namespace ProVM\Emails\Repository; use DateTimeInterface; use PDO; -use PDOException; -use Exception; -use ProVM\Common\Define\Model; use Psr\Log\LoggerInterface; -use ProVM\Common\Implement\Repository; use Safe\DateTimeImmutable; -use Safe\Exceptions\ErrorfuncException; +use ProVM\Common\Define\Model; +use ProVM\Common\Implement\Repository; +use ProVM\Common\Factory; class Message extends Repository { - public function __construct(PDO $connection, LoggerInterface $logger, \ProVM\Common\Factory\Model $factory) + public function __construct(PDO $connection, LoggerInterface $logger, Factory\Model $factory) { parent::__construct($connection, $logger); $this->setTable('messages') ->setFactory($factory); } - protected \ProVM\Common\Factory\Model $factory; - public function getFactory(): \ProVM\Common\Factory\Model + protected Factory\Model $factory; + public function getFactory(): Factory\Model { return $this->factory; } - public function setFactory(\ProVM\Common\Factory\Model $factory): Message + 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(); @@ -114,21 +130,23 @@ class Message extends Repository '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 = $this->getFactory()->find(\ProVM\Emails\Model\State\Message::class)->create($data); + $state = $stateRepository->create($data); $model->addState($state); } } foreach ($model->getStates() as $state) { - $state->setMessage($model); - $this->getFactory()->find(\ProVM\Emails\Model\State\Message::class)->save($state); + //$state->setMessage($model); + $stateRepository->save($state); } } @@ -161,4 +179,9 @@ class Message extends Repository 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 index 7da6b9b..2240eda 100644 --- a/api/src/Repository/State/Attachment.php +++ b/api/src/Repository/State/Attachment.php @@ -26,6 +26,22 @@ class Attachment extends Repository 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 [ @@ -90,4 +106,4 @@ class Attachment extends Repository $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 index 7d498d0..e6e03a5 100644 --- a/api/src/Repository/State/Mailbox.php +++ b/api/src/Repository/State/Mailbox.php @@ -28,6 +28,23 @@ class Mailbox extends Repository 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(); @@ -99,4 +116,4 @@ class Mailbox extends Repository $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 index 74acb5b..8859eb8 100644 --- a/api/src/Repository/State/Message.php +++ b/api/src/Repository/State/Message.php @@ -26,6 +26,22 @@ class Message extends Repository 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(); @@ -90,4 +106,4 @@ class Message extends Repository $query = "SELECT * FROM `{$this->getTable()}` WHERE `message_id` = ? AND `name` = ?"; return $this->fetchOne($query, [$message_id, $name]); } -} \ No newline at end of file +} diff --git a/cli/common/Command/DecryptPdf.php b/cli/common/Command/DecryptPdf.php index 92027b6..e868843 100644 --- a/cli/common/Command/DecryptPdf.php +++ b/cli/common/Command/DecryptPdf.php @@ -1,12 +1,13 @@ getCommunicator()->get('/attachments/pending'); - return \Safe\json_decode($response->getBody()->getContents())->attachments; + return json_decode($response->getBody()->getContents())->attachments; } protected function decrypt(string $attachment): bool { $response = $this->getCommunicator()->put('/attachments/decrypt', ['attachments' => [$attachment]]); - return \Safe\json_decode($response->getBody()->getContents())->status; + return json_decode($response->getBody()->getContents())->status; } public function execute(InputInterface $input, OutputInterface $output) @@ -64,4 +65,4 @@ class DecryptPdf extends Command return Command::SUCCESS; } -} \ No newline at end of file +} diff --git a/cli/common/Command/GrabAttachments.php b/cli/common/Command/GrabAttachments.php index 1055a8c..23295e1 100644 --- a/cli/common/Command/GrabAttachments.php +++ b/cli/common/Command/GrabAttachments.php @@ -1,12 +1,13 @@ getCommunicator()->get('/messages/pending'); - return \Safe\json_decode($response->getBody()->getContents())->messages; + return json_decode($response->getBody()->getContents())->messages; } protected function grabAttachments(int $message_uid): int { $response = $this->getCommunicator()->put('/attachments/grab', ['messages' => [$message_uid]]); - return \Safe\json_decode($response->getBody()->getContents())->attachment_count; + return json_decode($response->getBody()->getContents())->attachment_count; } public function execute(InputInterface $input, OutputInterface $output): int @@ -62,4 +63,4 @@ class GrabAttachments extends Command return Command::SUCCESS; } -} \ No newline at end of file +} diff --git a/cli/common/Command/Messages.php b/cli/common/Command/Messages.php index a09b493..848436e 100644 --- a/cli/common/Command/Messages.php +++ b/cli/common/Command/Messages.php @@ -1,12 +1,13 @@ getCommunicator()->get('/mailboxes/registered'); - return \Safe\json_decode($response->getBody()->getContents())->mailboxes; + return json_decode($response->getBody()->getContents())->mailboxes; } protected function grabMessages(string $mailbox): int { $response = $this->getCommunicator()->put('/messages/grab', ['mailboxes' => [$mailbox]]); - $body = \Safe\json_decode($response->getBody()->getContents()); + $body = json_decode($response->getBody()->getContents()); return $body->message_count; } @@ -63,4 +64,4 @@ class Messages extends Command return Command::SUCCESS; } -} \ No newline at end of file +} diff --git a/emails-2022-11-30-03-59-49.sql b/emails-2022-11-30-03-59-49.sql new file mode 100644 index 0000000..3fd22d9 --- /dev/null +++ b/emails-2022-11-30-03-59-49.sql @@ -0,0 +1,94 @@ +-- Adminer 4.8.1 MySQL 5.5.5-10.9.3-MariaDB-1:10.9.3+maria~ubu2204 dump + +SET NAMES utf8; +SET time_zone = '+00:00'; +SET foreign_key_checks = 0; +SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'; + +SET NAMES utf8mb4; + +DROP TABLE IF EXISTS `attachments`; +CREATE TABLE `attachments` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `message_id` int(10) unsigned NOT NULL, + `filename` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `message_id` (`message_id`), + CONSTRAINT `attachments_ibfk_2` FOREIGN KEY (`message_id`) REFERENCES `messages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +DROP TABLE IF EXISTS `attachments_jobs`; +CREATE TABLE `attachments_jobs` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `message_id` int(10) unsigned NOT NULL, + `date_time` datetime NOT NULL, + `executed` int(1) DEFAULT 0, + PRIMARY KEY (`id`), + KEY `message_id` (`message_id`), + CONSTRAINT `attachments_jobs_ibfk_2` FOREIGN KEY (`message_id`) REFERENCES `messages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +DROP TABLE IF EXISTS `attachments_states`; +CREATE TABLE `attachments_states` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `attachment_id` int(10) unsigned NOT NULL, + `name` varchar(100) NOT NULL, + `value` int(1) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `attachment_id` (`attachment_id`), + CONSTRAINT `attachments_states_ibfk_1` FOREIGN KEY (`attachment_id`) REFERENCES `attachments` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +DROP TABLE IF EXISTS `mailboxes`; +CREATE TABLE `mailboxes` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + `validity` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +DROP TABLE IF EXISTS `mailboxes_states`; +CREATE TABLE `mailboxes_states` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `mailbox_id` int(10) unsigned NOT NULL, + `date_time` datetime NOT NULL, + `count` int(10) unsigned NOT NULL, + `uids` text NOT NULL, + PRIMARY KEY (`id`), + KEY `mailbox_id` (`mailbox_id`), + CONSTRAINT `mailboxes_states_ibfk_2` FOREIGN KEY (`mailbox_id`) REFERENCES `mailboxes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +DROP TABLE IF EXISTS `messages`; +CREATE TABLE `messages` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `mailbox_id` int(10) unsigned NOT NULL, + `position` int(10) unsigned NOT NULL, + `uid` varchar(255) NOT NULL, + `subject` varchar(255) NOT NULL, + `from` varchar(100) NOT NULL, + `date_time` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `mailbox_id` (`mailbox_id`), + CONSTRAINT `messages_ibfk_1` FOREIGN KEY (`mailbox_id`) REFERENCES `mailboxes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +DROP TABLE IF EXISTS `messages_states`; +CREATE TABLE `messages_states` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `message_id` int(10) unsigned NOT NULL, + `name` varchar(100) NOT NULL, + `value` int(1) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `message_id` (`message_id`), + CONSTRAINT `messages_states_ibfk_2` FOREIGN KEY (`message_id`) REFERENCES `messages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +-- 2022-11-30 03:59:49 From 03c1dac2f2bfb107b41a92c9f02cc18e41a4c191 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 9 Jun 2023 00:54:34 -0400 Subject: [PATCH 34/35] Cleanup of cli --- NOTES.md | 33 ++--- api/common/Exception/Job/Stateless.php | 15 +++ api/common/Exception/Mailbox/EmptyMailbox.php | 5 +- api/common/Exception/Mailbox/Invalid.php | 3 +- api/common/Exception/Mailbox/Stateless.php | 3 +- api/resources/routes/01_mailboxes.php | 1 + api/resources/routes/02_attachments.php | 3 +- api/setup/setups/98_log.php | 3 +- api/src/Model/Job.php | 65 ++++++++-- api/src/Model/State/Job.php | 65 ++++++++++ api/src/Repository/Job.php | 67 +++++------ api/src/Repository/State/Job.php | 113 ++++++++++++++++++ cli/Dockerfile | 2 +- cli/common/Command/DecryptPdf.php | 68 ----------- cli/common/Command/GrabAttachments.php | 66 ---------- cli/common/Command/Jobs/Check.php | 79 ++++++++++++ cli/common/Command/Mailboxes/Check.php | 76 ++++++++++++ .../{Messages.php => Messages/Grab.php} | 43 ++++--- cli/common/Middleware/Logging.php | 2 +- cli/common/Service/Communicator.php | 33 ++++- cli/common/Wrapper/Application.php | 4 +- cli/composer.json | 2 +- cli/crontab | 11 +- cli/docker-compose.yml | 3 +- cli/public/index.php | 9 +- cli/resources/commands/01_mailboxes.php | 2 + cli/resources/commands/01_messages.php | 2 +- cli/resources/commands/02_attachments.php | 5 +- cli/resources/commands/03_jobs.php | 2 + cli/setup/app.php | 4 +- cli/setup/middleware/98_log.php | 2 +- cli/setup/settings/01_env.php | 5 +- cli/setup/setups/02_api.php | 6 +- cli/setup/setups/03_middleware.php | 4 +- cli/setup/setups/04_commands.php | 20 ++++ cli/setup/setups/98_log.php | 60 +++++++--- 36 files changed, 614 insertions(+), 272 deletions(-) create mode 100644 api/common/Exception/Job/Stateless.php create mode 100644 api/src/Model/State/Job.php create mode 100644 api/src/Repository/State/Job.php delete mode 100644 cli/common/Command/DecryptPdf.php delete mode 100644 cli/common/Command/GrabAttachments.php create mode 100644 cli/common/Command/Jobs/Check.php create mode 100644 cli/common/Command/Mailboxes/Check.php rename cli/common/Command/{Messages.php => Messages/Grab.php} (54%) create mode 100644 cli/resources/commands/01_mailboxes.php create mode 100644 cli/resources/commands/03_jobs.php create mode 100644 cli/setup/setups/04_commands.php diff --git a/NOTES.md b/NOTES.md index ec9035b..b00fae6 100644 --- a/NOTES.md +++ b/NOTES.md @@ -7,8 +7,14 @@ * [ ] Download `attachments` (*encrypted* & *decrypted*). ## CLI -* [x] Get `mailboxes` from **[API]** then run `grab messages` job in **[API]** for each one. -* [x] Get `pending attachments` jobs from **[API]** and run. +#### 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. @@ -21,18 +27,17 @@ ## Workflow -* **[User]** Choose `mailboxes` to register or unregister - -> **[API]** Register selected `mailboxes` and get `messages` for recently registered. -* **[Cron]** Get registered `mailboxes` -> **[API]** Get `messages` -* **[User]** Check messages found -> **[API]** Schedule `attachments` -* **[Cron]** Get `attachment download` jobs -> **[API]** grab `attachments` +* **[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 +## Jobs #### Automatic -* [ ] Check registered `mailboxes` for new `messages`, logging last check. -* [ ] Check if `attachments` are `encrypted`. -* [ ] Check for new scheduled jobs. +* [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`. -* [ ] Grab `attachments`. -* [ ] Decrypt `attachments`. +* [ ] Grab `messages` for `mailbox` id. +* [ ] Grab `attachments` for `message` id. +* [ ] Decrypt `attachment`. diff --git a/api/common/Exception/Job/Stateless.php b/api/common/Exception/Job/Stateless.php new file mode 100644 index 0000000..7f2d7f5 --- /dev/null +++ b/api/common/Exception/Job/Stateless.php @@ -0,0 +1,15 @@ +group('/mailboxes', function($app) { $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); }); diff --git a/api/resources/routes/02_attachments.php b/api/resources/routes/02_attachments.php index 62969b7..9b45b9d 100644 --- a/api/resources/routes/02_attachments.php +++ b/api/resources/routes/02_attachments.php @@ -3,9 +3,10 @@ use ProVM\Common\Controller\Attachments; $app->group('/attachments', function($app) { $app->put('/grab', [Attachments::class, 'grab']); + $app->get('/pending', [Attachments::class, 'pending']); $app->post('/decrypt', [Attachments::class, 'decrypt']); $app->get('[/]', Attachments::class); }); $app->group('/attachment/{attachment_id}', function($app) { $app->get('[/]', [Attachments::class, 'get']); -}); \ No newline at end of file +}); diff --git a/api/setup/setups/98_log.php b/api/setup/setups/98_log.php index 4c6a66e..0ebc4cb 100644 --- a/api/setup/setups/98_log.php +++ b/api/setup/setups/98_log.php @@ -6,6 +6,7 @@ return [ 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), ]; }, @@ -36,7 +37,7 @@ return [ ); }, Psr\Log\LoggerInterface::class => function(ContainerInterface $container) { - return $container->get('elk_logger'); + return $container->get('file_logger'); }, 'file_logger' => function(ContainerInterface $container) { return new Monolog\Logger( diff --git a/api/src/Model/Job.php b/api/src/Model/Job.php index b4a9cc5..5aa26ee 100644 --- a/api/src/Model/Job.php +++ b/api/src/Model/Job.php @@ -3,29 +3,31 @@ namespace ProVM\Emails\Model; use DateTimeInterface; use ProVM\Common\Define\Model; +use ProVM\Common\Exception\Database\BlankResult; +use ProVM\Common\Exception\Job\Stateless; +use ProVM\Emails; class Job implements Model { protected int $id; - protected Message $message; - protected DateTimeInterface $dateTime; - protected bool $executed; + protected string $command; + protected string $arguments; public function getId(): int { return $this->id; } - public function getMessage(): Message + public function getCommand(): string { - return $this->message; + return $this->command; } - public function getDateTime(): DateTimeInterface + public function getArguments(): string { - return $this->dateTime; + return $this->arguments ?? ''; } public function isExecuted(): bool { - return $this->executed ?? false; + return $this->lastState()->getStatus() === State\Job::Executed; } public function setId(int $id): Job @@ -33,22 +35,61 @@ class Job implements Model $this->id = $id; return $this; } - public function setMessage(Message $message): Job + public function setCommand(string $message): Job { $this->message = $message; return $this; } - public function setDateTime(DateTimeInterface $dateTime): Job + public function setArguments(string $dateTime): Job { $this->dateTime = $dateTime; return $this; } - public function wasExecuted(): Job + + protected Emails\Repository\State\Job $stateRepository; + public function getStateRepository(): Emails\Repository\State\Job { - $this->executed = true; + 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 lastState(): State\Job + { + if (count($this->getStates()) === 0) { + throw new Stateless($this); + } + return $this->getStates()[array_key_last($this->getStates())]; + } + public function toArray(): array { return [ diff --git a/api/src/Model/State/Job.php b/api/src/Model/State/Job.php new file mode 100644 index 0000000..2826908 --- /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' => $this->getJob(), + 'date' => $this->getDateTime(), + 'status' => $this->getStatus() + ]; + } +} diff --git a/api/src/Repository/Job.php b/api/src/Repository/Job.php index 0f47ee3..e8f6be9 100644 --- a/api/src/Repository/Job.php +++ b/api/src/Repository/Job.php @@ -2,28 +2,28 @@ namespace ProVM\Emails\Repository; use PDO; -use ProVM\Common\Factory\Model; +use ProVM\Common\Factory; use Psr\Log\LoggerInterface; -use Safe\DateTimeImmutable; use ProVM\Common\Define\Model as ModelInterface; use ProVM\Common\Implement\Repository; use ProVM\Emails\Model\Job as BaseModel; +use ProVM\Emails; class Job extends Repository { - public function __construct(PDO $connection, LoggerInterface $logger, Model $factory) + public function __construct(PDO $connection, LoggerInterface $logger, Factory\Model $factory) { parent::__construct($connection, $logger); $this->setFactory($factory) ->setTable('attachments_jobs'); } - protected \ProVM\Common\Factory\Model $factory; - public function getFactory(): \ProVM\Common\Factory\Model + protected Factory\Model $factory; + public function getFactory(): Factory\Model { return $this->factory; } - public function setFactory(\ProVM\Common\Factory\Model $factory): Job + public function setFactory(Factory\Model $factory): Job { $this->factory = $factory; return $this; @@ -34,12 +34,9 @@ class Job extends Repository $query = " CREATE TABLE {$this->getTable()} ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `message_id` INT UNSIGNED NOT NULL, - `date_time` DATETIME NOT NULL, - `executed` INT(1) UNSIGNED DEFAULT 0, - PRIMARY KEY (`id`), - FOREIGN KEY `fk_messages_{$this->getTable()}` (`message_id`) - REFERENCES `messages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE + `command` VARCHAR(100) NOT NULL, + `arguments` TEXT NOT NULL, + PRIMARY KEY (`id`) )"; $this->getConnection()->query($query); } @@ -55,65 +52,61 @@ CREATE TABLE {$this->getTable()} ( protected function fieldsForInsert(): array { return [ - 'message_id', - 'date_time', - 'executed' + 'command', + 'arguments', ]; } protected function valuesForInsert(ModelInterface $model): array { return [ - $model->getMessage()->getId(), - $model->getDateTime()->format('Y-m-d H:i:s'), - $model->isExecuted() ? 1 : 0 + $model->getCommand(), + $model->getArguments(), ]; } protected function defaultFind(ModelInterface $model): ModelInterface { - return $this->fetchByMessageAndDate($model->getMessage()->getId(), $model->getDateTime()->format('Y-m-d H:i:s')); + return $this->fetchByCommandAndArguments($model->getCommand(), $model->getArguments()); } protected function fieldsForCreate(): array { return [ - 'message_id', - 'date_time', - 'executed' + 'command', + 'arguments', ]; } protected function valuesForCreate(array $data): array { return [ - $data['message_id'], - $data['date_time'], - $data['executed'] ?? 0 + $data['command'], + $data['arguments'], ]; } protected function defaultSearch(array $data): ModelInterface { - return $this->fetchByMessageAndDate($data['message_id'], $data['date_time']); + return $this->fetchByCommandAndArguments($data['command'], $data['arguments']); } public function load(array $row): ModelInterface { - $model = (new BaseModel()) + return (new BaseModel()) ->setId($row['id']) - ->setMessage($this->getFactory()->find(\ProVM\Emails\Model\Message::class)->fetchById($row['message_id'])) - ->setDateTime(new DateTimeImmutable($row['date_time'])); - if ($row['executed'] ?? 0 === 1) { - $model->wasExecuted(); - } - return $model; + ->setCommand($row['command']) + ->setArguments($row['arguments']) + ->setStateRepository($this->getFactory()->find(Emails\Model\State\Job::class)); } public function fetchAllPending(): array { - $query = "SELECT * FROM {$this->getTable()} WHERE `executed` = 0"; + $query = "SELECT a.* +FROM {$this->getTable()} a + JOIN `jobs_states` b ON b.job_id = a.id +WHERE b.`status` = ?"; return $this->fetchMany($query); } - public function fetchByMessageAndDate(int $message_id, string $date_time): \ProVM\Emails\Model\Job + public function fetchByCommandAndArguments(string $command, string $arguments): \ProVM\Emails\Model\Job { - $query = "SELECT * FROM {$this->getTable()} WHERE `message_id` = ? AND `date_time` = ?"; - return $this->fetchOne($query, [$message_id, $date_time]); + $query = "SELECT * FROM {$this->getTable()} WHERE `command` = ? AND `arguments` = ?"; + return $this->fetchOne($query, [$command, $arguments]); } public function fetchPendingByMessage(int $message_id): \ProVM\Emails\Model\Job { diff --git a/api/src/Repository/State/Job.php b/api/src/Repository/State/Job.php new file mode 100644 index 0000000..e7f9d57 --- /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\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/cli/Dockerfile b/cli/Dockerfile index 2ed89eb..d596ae4 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -3,7 +3,7 @@ FROM php:8-cli ENV PATH ${PATH}:/app/bin RUN apt-get update \ - && apt-get install -y cron git libzip-dev unzip \ + && apt-get install -y cron git libzip-dev unzip qpdf \ && rm -r /var/lib/apt/lists/* \ && docker-php-ext-install zip diff --git a/cli/common/Command/DecryptPdf.php b/cli/common/Command/DecryptPdf.php deleted file mode 100644 index e868843..0000000 --- a/cli/common/Command/DecryptPdf.php +++ /dev/null @@ -1,68 +0,0 @@ -setCommunicator($communicator); - parent::__construct($name); - } - - protected Communicator $communicator; - public function getCommunicator(): Communicator - { - return $this->communicator; - } - public function setCommunicator(Communicator $communicator): DecryptPdf - { - $this->communicator = $communicator; - return $this; - } - - protected function getAttachments(): array - { - $response = $this->getCommunicator()->get('/attachments/pending'); - return json_decode($response->getBody()->getContents())->attachments; - } - protected function decrypt(string $attachment): bool - { - $response = $this->getCommunicator()->put('/attachments/decrypt', ['attachments' => [$attachment]]); - return json_decode($response->getBody()->getContents())->status; - } - - public function execute(InputInterface $input, OutputInterface $output) - { - $io = new SymfonyStyle($input, $output); - $io->title('Decrypt Attachments'); - - $io->section('Grabbing Attachments'); - $attachments = $this->getAttachments(); - $io->text('Found ' . count($attachments) . ' attachments.'); - $io->section('Decrypting Attachments'); - foreach ($attachments as $attachment) { - $status = $this->decrypt($attachment); - if ($status) { - $io->success("{$attachment} decrypted correctly."); - } else { - $io->error("Problem decrypting {$attachment}."); - } - } - $io->success('Done.'); - - return Command::SUCCESS; - } -} diff --git a/cli/common/Command/GrabAttachments.php b/cli/common/Command/GrabAttachments.php deleted file mode 100644 index 23295e1..0000000 --- a/cli/common/Command/GrabAttachments.php +++ /dev/null @@ -1,66 +0,0 @@ -setCommunicator($communicator); - parent::__construct($name); - } - - protected Communicator $service; - public function getCommunicator(): Communicator - { - return $this->service; - } - public function setCommunicator(Communicator $service): GrabAttachments - { - $this->service = $service; - return $this; - } - - protected function getMessages(): array - { - $response = $this->getCommunicator()->get('/messages/pending'); - return json_decode($response->getBody()->getContents())->messages; - } - protected function grabAttachments(int $message_uid): int - { - $response = $this->getCommunicator()->put('/attachments/grab', ['messages' => [$message_uid]]); - return json_decode($response->getBody()->getContents())->attachment_count; - } - - public function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - $io->title('Grab Attachments'); - - $io->section('Grabbing Messages'); - $messages = $this->getMessages(); - $io->text('Found ' . count($messages) . ' messages.'); - $io->section('Grabbing Attachments'); - foreach ($messages as $job) { - $message = $job->message; - $attachments = $this->grabAttachments($message->uid); - $io->text("Found {$attachments} attachments for message UID:{$message->uid}."); - } - $io->success('Done.'); - - return Command::SUCCESS; - } -} diff --git a/cli/common/Command/Jobs/Check.php b/cli/common/Command/Jobs/Check.php new file mode 100644 index 0000000..d3c9b23 --- /dev/null +++ b/cli/common/Command/Jobs/Check.php @@ -0,0 +1,79 @@ +logger->notice('Grabbing pending jobs.'); + $response = $this->communicator->get('/jobs/pending'); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + return []; + } + return json_decode($body)->jobs; + } + protected function runJob($job): bool + { + $base_command = '/app/bin/emails'; + $cmd = [$base_command, $job->command]; + if ($job->arguments !== '') { + $cmd []= $job->arguments; + } + $cmd = implode(' ', $cmd); + $this->logger->notice("Running '{$cmd}'"); + $response = shell_exec($cmd); + $this->logger->info("Result: {$response}"); + return $response !== false; + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $section1 = $output->section(); + $section2 = $output->section(); + $io1 = new SymfonyStyle($input, $section1); + $io2 = new SymfonyStyle($input, $section2); + $io1->title('Checking Pending Jobs'); + $pending_jobs = $this->getPendingJobs(); + $notice = 'Found ' . count($pending_jobs) . ' jobs'; + $io1->text($notice); + $this->logger->info($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->runJob($job)) { + $io2->success('Success'); + } else { + $io2->error('Failure'); + } + $io1->progressAdvance(); + } + } + $section2->clear(); + $io2->success('Done'); + + 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..c0fa6eb --- /dev/null +++ b/cli/common/Command/Mailboxes/Check.php @@ -0,0 +1,76 @@ +communicator->get('/mailboxes/registered'); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + return []; + } + return json_decode($body)->mailboxes; + } + protected function checkMailbox($mailbox): bool + { + if ((new \DateTimeImmutable())->diff(new \DateTimeImmutable($mailbox->last_checked->date->date))->days < $this->min_check_days) { + return true; + } + $response = $this->communicator->get("/mailbox/{$mailbox->id}/check"); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + return true; + } + return json_decode($body)->status; + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $section1 = $output->section(); + $section2 = $output->section(); + $io1 = new SymfonyStyle($input, $section1); + $io2 = new SymfonyStyle($input, $section2); + $io1->title('Checking for New Messages'); + $mailboxes = $this->getMailboxes(); + $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->checkMailbox($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.php b/cli/common/Command/Messages/Grab.php similarity index 54% rename from cli/common/Command/Messages.php rename to cli/common/Command/Messages/Grab.php index 848436e..89a85f9 100644 --- a/cli/common/Command/Messages.php +++ b/cli/common/Command/Messages/Grab.php @@ -1,65 +1,62 @@ setCommunicator($communicator); parent::__construct($name); } + protected function configure() + { + $this->addArgument('mailbox_id', InputArgument::REQUIRED, 'Mailbox ID to grab emails'); + } protected Communicator $communicator; public function getCommunicator(): Communicator { return $this->communicator; } - public function setCommunicator(Communicator $communicator): Messages + public function setCommunicator(Communicator $communicator): Grab { $this->communicator = $communicator; return $this; } - protected function getMailboxes(): array + protected function grabMessages(int $mailbox_id): int { - $response = $this->getCommunicator()->get('/mailboxes/registered'); - return json_decode($response->getBody()->getContents())->mailboxes; - } - protected function grabMessages(string $mailbox): int - { - $response = $this->getCommunicator()->put('/messages/grab', ['mailboxes' => [$mailbox]]); - $body = json_decode($response->getBody()->getContents()); - return $body->message_count; + $response = $this->getCommunicator()->get("/mailbox/{$mailbox_id}/grab"); + $body = $response->getBody()->getContents(); + if (trim($body) === '') { + return 0; + } + return json_decode($body)->messages->count; } public function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); - $io->title('Messages'); - $io->section('Grabbing Registered Mailboxes'); - - $mailboxes = $this->getMailboxes(); - $io->text('Found ' . count($mailboxes) . ' registered mailboxes.'); + $mailbox_id = $input->getArgument('mailbox_id'); + $io->title("Grabbing Messages for Mailbox ID {$mailbox_id}"); $io->section('Grabbing Messages'); - foreach ($mailboxes as $mailbox) { - $message_count = $this->grabMessages($mailbox->name); - $io->text("Found {$message_count} messages in {$mailbox->name}."); - } + $count = $this->grabMessages($mailbox_id); + $io->info("Found {$count} messages"); $io->success('Done.'); return Command::SUCCESS; diff --git a/cli/common/Middleware/Logging.php b/cli/common/Middleware/Logging.php index a95c37b..25021c5 100644 --- a/cli/common/Middleware/Logging.php +++ b/cli/common/Middleware/Logging.php @@ -1,5 +1,5 @@ getStatusCode() < 200 or $response->getStatusCode() >= 300) { @@ -33,6 +38,11 @@ class Communicator } return $response; } + + /** + * @throws HttpResponseException + * @throws JsonException + */ protected function request(string $method, string $uri, ?array $body = null): ResponseInterface { $options = []; @@ -45,20 +55,39 @@ class Communicator return $this->handleResponse($this->getClient()->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); } -} \ No newline at end of file +} diff --git a/cli/common/Wrapper/Application.php b/cli/common/Wrapper/Application.php index 8d8e4aa..c27dbf4 100644 --- a/cli/common/Wrapper/Application.php +++ b/cli/common/Wrapper/Application.php @@ -1,5 +1,5 @@ container = $container; return $this; } -} \ No newline at end of file +} diff --git a/cli/composer.json b/cli/composer.json index 2c1d0c1..4365fcb 100644 --- a/cli/composer.json +++ b/cli/composer.json @@ -15,7 +15,7 @@ }, "autoload": { "psr-4": { - "ProVM\\Common\\": "common/" + "ProVM\\": "common/" } }, "authors": [ diff --git a/cli/crontab b/cli/crontab index 49d2722..fc8ab81 100644 --- a/cli/crontab +++ b/cli/crontab @@ -1,3 +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 +#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 +1 * * * * /app/bin/emails jobs:pending >> /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 index 38d19e3..ea1ec32 100644 --- a/cli/docker-compose.yml +++ b/cli/docker-compose.yml @@ -12,4 +12,5 @@ services: - .key.env volumes: - ${CLI_PATH:-.}/:/app - - ./logs/cli:/logs + - ${LOGS_PATH}/cli:/logs + - ${ATT_PATH}:/attachments diff --git a/cli/public/index.php b/cli/public/index.php index 63341a5..f8b2dac 100644 --- a/cli/public/index.php +++ b/cli/public/index.php @@ -7,9 +7,8 @@ $app = require_once implode(DIRECTORY_SEPARATOR, [ Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface::class)); try { $app->run(); -} catch (Error | Exception $e) { - $logger = $app->getContainer()->get(Psr\Log\LoggerInterface::class); - $logger->debug(Safe\json_encode(compact('_SERVER'))); - $logger->error($e); - throw $e; +} 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 index c034ee1..4313973 100644 --- a/cli/resources/commands/01_messages.php +++ b/cli/resources/commands/01_messages.php @@ -1,2 +1,2 @@ add($app->getContainer()->get(\ProVM\Common\Command\Messages::class)); +$app->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 index 85793e1..d46dbc1 100644 --- a/cli/resources/commands/02_attachments.php +++ b/cli/resources/commands/02_attachments.php @@ -1,3 +1,4 @@ add($app->getContainer()->get(\ProVM\Common\Command\GrabAttachments::class)); -$app->add($app->getContainer()->get(\ProVM\Common\Command\DecryptPdf::class)); +$app->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..bd6eed1 --- /dev/null +++ b/cli/resources/commands/03_jobs.php @@ -0,0 +1,2 @@ +add($app->getContainer()->get(ProVM\Command\Jobs\Check::class)); diff --git a/cli/setup/app.php b/cli/setup/app.php index 16c1e0f..d00dbc6 100644 --- a/cli/setup/app.php +++ b/cli/setup/app.php @@ -1,7 +1,7 @@ build()); +$app = new ProVM\Wrapper\Application($builder->build()); $folder = implode(DIRECTORY_SEPARATOR, [ __DIR__, diff --git a/cli/setup/middleware/98_log.php b/cli/setup/middleware/98_log.php index 4df6f6c..5628448 100644 --- a/cli/setup/middleware/98_log.php +++ b/cli/setup/middleware/98_log.php @@ -1,2 +1,2 @@ add($app->getContainer()->get(ProVM\Common\Middleware\Logging::class)); +//$app->add($app->getContainer()->get(ProVM\Common\Middleware\Logging::class)); diff --git a/cli/setup/settings/01_env.php b/cli/setup/settings/01_env.php index 58b2a65..107718a 100644 --- a/cli/setup/settings/01_env.php +++ b/cli/setup/settings/01_env.php @@ -1,5 +1,8 @@ $_ENV['API_URI'], - 'api_key' => sha1($_ENV['API_KEY']) + 'api_key' => sha1($_ENV['API_KEY']), + 'passwords' => function() { + return explode($_ENV['PASSWORDS_SEPARATOR'] ?? ',', $_ENV['PASSWORDS'] ?? ''); + }, ]; diff --git a/cli/setup/setups/02_api.php b/cli/setup/setups/02_api.php index b123345..bf102d3 100644 --- a/cli/setup/setups/02_api.php +++ b/cli/setup/setups/02_api.php @@ -2,12 +2,12 @@ use Psr\Container\ContainerInterface; return [ - \Psr\Http\Client\ClientInterface::class => function(ContainerInterface $container) { - return new \GuzzleHttp\Client([ + Psr\Http\Client\ClientInterface::class => function(ContainerInterface $container) { + return new GuzzleHttp\Client([ 'base_uri' => $container->get('api_uri'), 'headers' => [ 'Authorization' => "Bearer {$container->get('api_key')}" ] ]); } -]; \ No newline at end of file +]; diff --git a/cli/setup/setups/03_middleware.php b/cli/setup/setups/03_middleware.php index 1d6cc44..7d872ab 100644 --- a/cli/setup/setups/03_middleware.php +++ b/cli/setup/setups/03_middleware.php @@ -2,7 +2,7 @@ use Psr\Container\ContainerInterface; return [ - ProVM\Common\Middleware\Logging::class => function(ContainerInterface $container) { - return new ProVM\Common\Middleware\Logging($container->get('request_logger')); + ProVM\Middleware\Logging::class => 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..50ae9d1 --- /dev/null +++ b/cli/setup/setups/04_commands.php @@ -0,0 +1,20 @@ + function(ContainerInterface $container) { + return new ProVM\Command\Attachments\DecryptPdf( + $container->get(ProVM\Service\Communicator::class), + $container->get(Psr\Log\LoggerInterface::class), + 'qpdf', + $container->get('passwords') + ); + }, + ProVM\Command\Mailboxes\Check::class => function(ContainerInterface $container) { + return new ProVM\Command\Mailboxes\Check( + $container->get(ProVM\Service\Communicator::class), + 1 + ); + } +]; diff --git a/cli/setup/setups/98_log.php b/cli/setup/setups/98_log.php index b33a5ce..7f6e30b 100644 --- a/cli/setup/setups/98_log.php +++ b/cli/setup/setups/98_log.php @@ -2,28 +2,50 @@ use Psr\Container\ContainerInterface; return [ - Monolog\Handler\RotatingFileHandler::class => function(ContainerInterface $container) { - $handler = new Monolog\Handler\RotatingFileHandler($container->get('log_file')); - $handler->setFormatter($container->get(Monolog\Formatter\LineFormatter::class)); - return $handler; + 'log_processors' => 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']))) + ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true)); }, 'request_logger' => function(ContainerInterface $container) { - $logger = new Monolog\Logger('request_logger'); - $handler = new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log'])); - $handler->setFormatter($container->get(Monolog\Formatter\SyslogFormatter::class)); - $dedupHandler = new Monolog\Handler\DeduplicationHandler($handler, null, Monolog\Level::Info); - $logger->pushHandler($dedupHandler); - $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; + 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) { - $logger = new Monolog\Logger('file_logger'); - $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; + 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') + ); }, ]; From 88f91c4bd52b8b945b7b6f3266fbf59d1e62549f Mon Sep 17 00:00:00 2001 From: Aldarien Date: Mon, 12 Jun 2023 21:14:07 -0400 Subject: [PATCH 35/35] Jobs setup --- api/common/Controller/Attachments.php | 64 +++---- api/common/Controller/Base.php | 5 +- api/common/Controller/Jobs.php | 66 +++++-- api/common/Controller/Messages.php | 56 +++--- .../Exception/Request/Auth/Forbidden.php | 15 ++ .../Exception/Request/Auth/Unauthorized.php | 15 ++ api/common/Middleware/Auth.php | 35 ++-- api/common/Middleware/Messages.php | 24 --- api/common/Service/Attachments.php | 2 +- api/common/Service/Auth.php | 33 ++++ api/common/Service/Jobs.php | 65 ++++--- api/common/Service/Messages.php | 16 +- api/docker-compose.yml | 2 +- api/nginx.conf | 15 +- api/resources/routes/02_attachments.php | 5 +- api/resources/routes/02_messages.php | 6 +- api/resources/routes/03_jobs.php | 15 ++ api/setup/app.php | 7 +- api/setup/middleware/04_db.php | 1 - api/setup/setups/02_services.php | 3 + api/setup/setups/03_factories.php | 4 +- api/setup/setups/97_auth.php | 2 +- api/setup/setups/98_log.php | 3 +- api/src/Model/Attachment.php | 4 +- api/src/Model/Job.php | 24 +-- api/src/Model/State/Job.php | 2 +- api/src/Repository/Job.php | 19 +- api/src/Repository/State/Job.php | 2 +- cli/common/Command/Jobs/Check.php | 35 +--- cli/common/Command/Jobs/Execute.php | 41 +++++ cli/common/Command/Mailboxes/Check.php | 31 +--- cli/common/Command/Messages/Grab.php | 31 +--- .../Exception/Response/EmptyResponse.php | 16 ++ .../Exception/Response/MissingResponse.php | 15 ++ cli/common/Middleware/Logging.php | 38 ---- cli/common/Service/Attachments.php | 114 ++++++++++++ cli/common/Service/Communicator.php | 21 +-- cli/common/Service/Jobs.php | 71 ++++++++ cli/common/Service/Mailboxes.php | 61 +++++++ cli/common/Service/Messages.php | 43 +++++ cli/common/Wrapper/Application.php | 1 - cli/crontab | 10 +- cli/docker-compose.yml | 1 + cli/resources/commands/03_jobs.php | 1 + cli/setup/middleware/98_log.php | 2 - cli/setup/settings/01_env.php | 2 + cli/setup/setups/04_commands.php | 17 +- cli/setup/setups/98_log.php | 3 +- emails-2022-11-30-03-59-49.sql | 94 ---------- ui/common/Controller/Jobs.php | 14 ++ ui/common/Middleware/Logging.php | 2 +- ui/nginx.conf | 5 +- ui/public/index.php | 8 +- ui/resources/routes/03_jobs.php | 4 + ui/resources/views/emails/messages.blade.php | 43 +++-- ui/resources/views/home.blade.php | 6 +- ui/resources/views/jobs/base.blade.php | 14 ++ ui/resources/views/jobs/list.blade.php | 165 ++++++++++++++++++ .../views/layout/body/header/navbar.blade.php | 1 + ui/setup/setups/98_log.php | 40 +++-- 60 files changed, 965 insertions(+), 495 deletions(-) create mode 100644 api/common/Exception/Request/Auth/Forbidden.php create mode 100644 api/common/Exception/Request/Auth/Unauthorized.php delete mode 100644 api/common/Middleware/Messages.php create mode 100644 api/common/Service/Auth.php create mode 100644 api/resources/routes/03_jobs.php create mode 100644 cli/common/Command/Jobs/Execute.php create mode 100644 cli/common/Exception/Response/EmptyResponse.php create mode 100644 cli/common/Exception/Response/MissingResponse.php delete mode 100644 cli/common/Middleware/Logging.php create mode 100644 cli/common/Service/Attachments.php create mode 100644 cli/common/Service/Jobs.php create mode 100644 cli/common/Service/Mailboxes.php create mode 100644 cli/common/Service/Messages.php delete mode 100644 cli/setup/middleware/98_log.php delete mode 100644 emails-2022-11-30-03-59-49.sql create mode 100644 ui/common/Controller/Jobs.php create mode 100644 ui/resources/routes/03_jobs.php create mode 100644 ui/resources/views/jobs/base.blade.php create mode 100644 ui/resources/views/jobs/list.blade.php diff --git a/api/common/Controller/Attachments.php b/api/common/Controller/Attachments.php index 68d3dd6..d95d028 100644 --- a/api/common/Controller/Attachments.php +++ b/api/common/Controller/Attachments.php @@ -1,19 +1,20 @@ toArray(); @@ -24,35 +25,7 @@ class Attachments ]; return $this->withJson($response, $output); } - public function grab(ServerRequestInterface $request, ResponseInterface $response, Service $service, \ProVM\Common\Service\Jobs $jobsService): ResponseInterface - { - $body = $request->getBody(); - $json = \Safe\json_decode($body->getContents()); - if (!isset($json->messages)) { - throw new MissingArgument('messages', 'array', 'message UIDs'); - } - $output = [ - 'messages' => $json->messages, - 'total' => count($json->messages), - 'saved' => [ - 'attachments' => [], - 'total' => 0 - ] - ]; - foreach ($json->messages as $message_id) { - if (!$jobsService->isPending($message_id)) { - continue; - } - if ($service->grab($message_id)) { - $job = $jobsService->find($message_id); - $jobsService->execute($job->getId()); - $output['saved']['attachments'] []= $job->toArray(); - $output['saved']['total'] ++; - } - } - return $this->withJson($response, $output); - } - public function get(ServerRequestInterface $request, ResponseInterface $response, Service $service, LoggerInterface $logger, int $attachment_id): ResponseInterface + public function get(ServerRequestInterface $request, ResponseInterface $response, Service\Attachments $service, LoggerInterface $logger, int $attachment_id): ResponseInterface { $attachment = $service->getRepository()->fetchById($attachment_id); @@ -61,4 +34,25 @@ class Attachments $response->getBody()->write($service->getFile($attachment_id)); return $response; } -} \ No newline at end of file + 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 index 8611788..4b8013f 100644 --- a/api/common/Controller/Base.php +++ b/api/common/Controller/Base.php @@ -12,7 +12,8 @@ class Base public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { return $this->withJson($response, [ - 'version' => '1.0.0' + 'version' => '1.0.0', + 'app' => 'emails' ]); } -} \ No newline at end of file +} diff --git a/api/common/Controller/Jobs.php b/api/common/Controller/Jobs.php index 437555f..5979fdc 100644 --- a/api/common/Controller/Jobs.php +++ b/api/common/Controller/Jobs.php @@ -1,7 +1,6 @@ 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->messages)) { - throw new MissingArgument('messages', 'array', 'messages ids'); + if (!isset($json->jobs)) { + throw new MissingArgument('jobs', 'array', 'job commands with arguments'); } $output = [ - 'messages' => $json->messages, - 'total' => count($json->messages), + 'jobs' => $json->jobs, + 'total' => count($json->jobs), 'scheduled' => 0 ]; - foreach ($json->messages as $message_id) { - if ($service->schedule($message_id)) { - $message = $messagesService->getRepository()->fetchById($message_id); - $message->doesHaveScheduledDownloads(); - $messagesService->getRepository()->save($message); + foreach ($json->jobs as $job) { + if ($service->queue($job->command, $job->arguments)) { $output['scheduled'] ++; } } @@ -37,12 +38,47 @@ class Jobs } public function pending(ServerRequestInterface $request, ResponseInterface $response, Service $service): ResponseInterface { - $pending = array_map(function(Job $job) { - return $job->toArray(); - }, $service->getPending()); + $pending = $service->getPending(); $output = [ 'total' => count($pending), - 'pending' => $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/Messages.php b/api/common/Controller/Messages.php index db86073..7b0d387 100644 --- a/api/common/Controller/Messages.php +++ b/api/common/Controller/Messages.php @@ -1,11 +1,11 @@ getMailboxes()->get($mailbox_id); $messages = array_map(function(Message $message) { @@ -37,7 +37,7 @@ class Messages ]; return $this->withJson($response, $output); } - public function valid(ServerRequestInterface $request, ResponseInterface $response, Service $service, \ProVM\Common\Service\Attachments $attachments, int $mailbox_id): ResponseInterface + 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) { @@ -63,32 +63,48 @@ class Messages ]; return $this->withJson($response, $output); } - public function get(ServerRequestInterface $request, ResponseInterface $response, Service $service, int $message_id): ResponseInterface + 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 $service, \ProVM\Common\Service\Attachments $attachmentsService): ResponseInterface + 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->getContents()); - if (!isset($json->mailboxes)) { - throw new MissingArgument('mailboxes', 'array', 'mailboxes names'); + $json = json_decode($body); + if (!isset($json->messages)) { + throw new MissingArgument('messages', 'array', 'messages IDs'); } $output = [ - 'mailboxes' => $json->mailboxes, - 'messages' => [], - 'message_count' => 0 + 'messages' => $json->messages, + 'scheduled' => 0 ]; - foreach ($json->mailboxes as $mailbox_name) { - $messages = $service->grab($mailbox_name); - foreach ($messages as $message) { - if ($message->hasValidAttachments()) { - $attachmentsService->create($message); - } + 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'] ++; } - $output['messages'] = array_merge($output['messages'], $messages); - $output['message_count'] += count($messages); } return $this->withJson($response, $output); } 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 @@ +setResponseFactory($factory); $this->setLogger($logger); - $this->setAPIKey($api_key); } protected ResponseFactoryInterface $factory; protected LoggerInterface $logger; - protected string $api_key; public function getResponseFactory(): ResponseFactoryInterface { @@ -28,10 +28,6 @@ class Auth { return $this->logger; } - public function getAPIKey(): string - { - return $this->api_key; - } public function setResponseFactory(ResponseFactoryInterface $factory): Auth { @@ -43,30 +39,23 @@ class 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); - } + 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()])); } - $this->getLogger()->debug(sha1($this->getAPIKey())); - $response = $this->getResponseFactory()->createResponse(401); - $response->getBody()->write(\Safe\json_encode(['error' => 401, 'message' => 'Incorrect token'])); + $response = $this->getResponseFactory()->createResponse(413); + $response->getBody()->write(\Safe\json_encode(['error' => 413, 'message' => 'Incorrect token'])); return $response ->withHeader('Content-Type', 'application/json'); } -} \ No newline at end of file +} diff --git a/api/common/Middleware/Messages.php b/api/common/Middleware/Messages.php deleted file mode 100644 index 13c13cb..0000000 --- a/api/common/Middleware/Messages.php +++ /dev/null @@ -1,24 +0,0 @@ -service->checkSchedule(); - } 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 index 1487b77..c5b6db9 100644 --- a/api/common/Service/Attachments.php +++ b/api/common/Service/Attachments.php @@ -117,7 +117,7 @@ class Attachments extends Base continue; } $name = $file->getBasename(".{$file->getExtension()}"); - list($subject, $date, $filename) = explode(' - ', $name); + list($date, $subject, $filename) = explode(' - ', $name); try { $message = $this->getMessages()->find($subject, $date)[0]; $filename = "{$filename}.{$file->getExtension()}"; 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/Jobs.php b/api/common/Service/Jobs.php index 13ef3d8..f9822c4 100644 --- a/api/common/Service/Jobs.php +++ b/api/common/Service/Jobs.php @@ -1,40 +1,49 @@ setRepository($repository); } - protected Job $repository; + protected Repository\Job $repository; - public function getRepository(): Job + public function getRepository(): Repository\Job { return $this->repository; } - public function setRepository(Job $repository): Jobs + public function setRepository(Repository\Job $repository): Jobs { $this->repository = $repository; return $this; } - public function schedule(int $message_id): bool + public function queue(string $command, ?array $arguments = null): bool { $data = [ - 'message_id' => $message_id, - 'date_time' => (new DateTimeImmutable())->format('Y-m-d H:i:s') + '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; @@ -44,28 +53,42 @@ class Jobs extends Base { return $this->getRepository()->fetchAllPending(); } - public function isPending(int $message_id): bool + public function getPendingByCommand(string $command): array { try { - $this->getRepository()->fetchPendingByMessage($message_id); - return true; + return $this->getRepository()->fetchAllPendingByCommand($command); } catch (BlankResult $e) { - return false; + return []; } } - public function find(int $message_id): \ProVM\Emails\Model\Job - { - return $this->getRepository()->fetchPendingByMessage($message_id); - } - public function execute(int $job_id): bool + 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 { - $job = $this->getRepository()->fetchById($job_id); - $job->wasExecuted(); - $this->getRepository()->save($job); + $state = $this->stateRepository->create($data); + $this->stateRepository->save($state); return true; } catch (PDOException $e) { return false; } } -} \ No newline at end of file + 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/Messages.php b/api/common/Service/Messages.php index a0cd5e0..ac52463 100644 --- a/api/common/Service/Messages.php +++ b/api/common/Service/Messages.php @@ -154,7 +154,6 @@ class Messages extends Base $message->doesHaveValidAttachments(); } } - error_log(json_encode(compact('message')).PHP_EOL,3,'/logs/debug'); $this->getRepository()->save($message); return true; } catch (PDOException $e) { @@ -196,20 +195,7 @@ class Messages extends Base $registered = $this->getMailboxes()->getRegistered(); foreach ($registered as $mailbox) { if (!$this->getMailboxes()->isUpdated($mailbox)) { - $this->logger->info("Updating messages from {$mailbox->getName()}"); - $this->grab($mailbox->getName()); - } - } - } - public function checkSchedule(): void - { - $messages = $this->getRepository()->fetchAll(); - foreach ($messages as $message) { - if ($message->hasAttachments() and $message->hasValidAttachments() and !$message->hasDownloadedAttachments() and !$message->hasScheduledDownloads()) { - if ($this->getJobsService()->schedule($message->getId())) { - $message->doesHaveDownloadedAttachments(); - $this->getRepository()->save($message); - } + $this->getJobsService()->queue('messages:grab', [$mailbox->getId()]); } } } diff --git a/api/docker-compose.yml b/api/docker-compose.yml index 173b1be..0f71511 100644 --- a/api/docker-compose.yml +++ b/api/docker-compose.yml @@ -7,7 +7,7 @@ services: - ${API_PATH:-.}:/app/api - ${API_PATH:-.}/nginx.conf:/etc/nginx/conf.d/api.conf ports: - - "${API_PORT:-8080}:8080" + - "${API_PORT:-8080}:81" api: profiles: - api diff --git a/api/nginx.conf b/api/nginx.conf index 6d22e8e..6a50944 100644 --- a/api/nginx.conf +++ b/api/nginx.conf @@ -1,15 +1,15 @@ server { - listen 0.0.0.0:8080; + listen 81; root /app/api/public; index index.php index.html index.htm; - access_log /var/logs/nginx/access.log; - error_log /var/logs/nginx/error.log; + access_log /var/logs/nginx/api.access.log; + error_log /var/logs/nginx/api.error.log; location / { - try_files $uri $uri/ /index.php?$query_string; + try_files $uri /index.php$is_args$args; } - location ~ \.php$ { + 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'; @@ -26,11 +26,12 @@ server { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; + include fastcgi_params; fastcgi_pass api: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 SCRIPT_NAME $fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } -} \ No newline at end of file +} diff --git a/api/resources/routes/02_attachments.php b/api/resources/routes/02_attachments.php index 9b45b9d..8c53514 100644 --- a/api/resources/routes/02_attachments.php +++ b/api/resources/routes/02_attachments.php @@ -2,9 +2,8 @@ use ProVM\Common\Controller\Attachments; $app->group('/attachments', function($app) { - $app->put('/grab', [Attachments::class, 'grab']); - $app->get('/pending', [Attachments::class, 'pending']); - $app->post('/decrypt', [Attachments::class, 'decrypt']); + $app->put('/grab[/]', [Attachments::class, 'grab']); + $app->get('/pending[/]', [Attachments::class, 'pending']); $app->get('[/]', Attachments::class); }); $app->group('/attachment/{attachment_id}', function($app) { diff --git a/api/resources/routes/02_messages.php b/api/resources/routes/02_messages.php index ed080d5..4d10259 100644 --- a/api/resources/routes/02_messages.php +++ b/api/resources/routes/02_messages.php @@ -4,9 +4,9 @@ use ProVM\Common\Controller\Jobs; $app->group('/messages', function($app) { $app->put('/grab', [Messages::class, 'grab']); - $app->put('/schedule', [Jobs::class, 'schedule']); - $app->get('/pending', [Jobs::class, 'pending']); + $app->put('/schedule', [Messages::class, 'schedule']); + //$app->get('/pending', [Jobs::class, 'pending']); }); $app->group('/message/{message_id}', function($app) { $app->get('[/]', [Messages::class, 'get']); -}); \ No newline at end of file +}); 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/setup/app.php b/api/setup/app.php index 7367296..0ae6f29 100644 --- a/api/setup/app.php +++ b/api/setup/app.php @@ -1,7 +1,10 @@ addDefinitions($file->getRealPath()); } } -$app = \DI\Bridge\Slim\Bridge::create($builder->build()); +$app = Bridge::create($builder->build()); $folder = implode(DIRECTORY_SEPARATOR, [ __DIR__, diff --git a/api/setup/middleware/04_db.php b/api/setup/middleware/04_db.php index 9d831e8..b71467b 100644 --- a/api/setup/middleware/04_db.php +++ b/api/setup/middleware/04_db.php @@ -1,5 +1,4 @@ add($app->getContainer()->get(ProVM\Common\Middleware\Attachments::class)); -$app->add($app->getContainer()->get(ProVM\Common\Middleware\Messages::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/setups/02_services.php b/api/setup/setups/02_services.php index 2a09318..4348151 100644 --- a/api/setup/setups/02_services.php +++ b/api/setup/setups/02_services.php @@ -33,5 +33,8 @@ return [ $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 index 281eafa..49b605e 100644 --- a/api/setup/setups/03_factories.php +++ b/api/setup/setups/03_factories.php @@ -8,9 +8,11 @@ return [ '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\\Attachment" => ProVM\Emails\Repository\State\Attachment::class, + "State\\Job" => ProVM\Emails\Repository\State\Job::class, ]); } ]; diff --git a/api/setup/setups/97_auth.php b/api/setup/setups/97_auth.php index 03fa0f3..9476f57 100644 --- a/api/setup/setups/97_auth.php +++ b/api/setup/setups/97_auth.php @@ -6,7 +6,7 @@ return [ return new ProVM\Common\Middleware\Auth( $container->get(Nyholm\Psr7\Factory\Psr17Factory::class), $container->get(Psr\Log\LoggerInterface::class), - $container->get('api_key') + $container->get(ProVM\Common\Service\Auth::class) ); } ]; diff --git a/api/setup/setups/98_log.php b/api/setup/setups/98_log.php index 0ebc4cb..39788a4 100644 --- a/api/setup/setups/98_log.php +++ b/api/setup/setups/98_log.php @@ -11,8 +11,7 @@ return [ ]; }, 'request_log_handler' => function(ContainerInterface $container) { - return (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log']))) - ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true)); + return (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log']))); }, 'request_logger' => function(ContainerInterface $container) { return new Monolog\Logger( diff --git a/api/src/Model/Attachment.php b/api/src/Model/Attachment.php index 912a2e8..9cc8683 100644 --- a/api/src/Model/Attachment.php +++ b/api/src/Model/Attachment.php @@ -95,9 +95,9 @@ class Attachment implements Model public function getFullFilename(): string { return implode(' - ', [ - $this->getMessage()->getSubject(), $this->getMessage()->getDateTime()->format('Y-m-d His'), - $this->getFilename() + $this->getMessage()->getSubject(), + $this->getFilename(), ]); } diff --git a/api/src/Model/Job.php b/api/src/Model/Job.php index 5aa26ee..7b69d93 100644 --- a/api/src/Model/Job.php +++ b/api/src/Model/Job.php @@ -25,24 +25,20 @@ class Job implements Model { return $this->arguments ?? ''; } - public function isExecuted(): bool - { - return $this->lastState()->getStatus() === State\Job::Executed; - } public function setId(int $id): Job { $this->id = $id; return $this; } - public function setCommand(string $message): Job + public function setCommand(string $command): Job { - $this->message = $message; + $this->command = $command; return $this; } - public function setArguments(string $dateTime): Job + public function setArguments(string $arguments): Job { - $this->dateTime = $dateTime; + $this->arguments = $arguments; return $this; } @@ -82,10 +78,14 @@ class Job implements Model 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); + throw new Stateless($this->getId()); } return $this->getStates()[array_key_last($this->getStates())]; } @@ -94,9 +94,9 @@ class Job implements Model { return [ 'id' => $this->getId(), - 'message' => $this->getMessage()->toArray(), - 'date_time' => $this->getDateTime()->format('Y-m-d H:i:s'), - 'executed' => $this->isExecuted() + 'command' => $this->getCommand(), + 'arguments' => $this->getArguments(), + 'states' => $this->getStates() ]; } public function jsonSerialize(): mixed diff --git a/api/src/Model/State/Job.php b/api/src/Model/State/Job.php index 2826908..1d1e9af 100644 --- a/api/src/Model/State/Job.php +++ b/api/src/Model/State/Job.php @@ -57,7 +57,7 @@ class Job implements Model { return [ 'id' => $this->getId(), - 'job' => $this->getJob(), + 'job_id' => $this->getJob()->getId(), 'date' => $this->getDateTime(), 'status' => $this->getStatus() ]; diff --git a/api/src/Repository/Job.php b/api/src/Repository/Job.php index e8f6be9..17c39df 100644 --- a/api/src/Repository/Job.php +++ b/api/src/Repository/Job.php @@ -15,7 +15,7 @@ class Job extends Repository { parent::__construct($connection, $logger); $this->setFactory($factory) - ->setTable('attachments_jobs'); + ->setTable('jobs'); } protected Factory\Model $factory; @@ -98,19 +98,22 @@ CREATE TABLE {$this->getTable()} ( public function fetchAllPending(): array { $query = "SELECT a.* -FROM {$this->getTable()} a - JOIN `jobs_states` b ON b.job_id = a.id +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); + return $this->fetchMany($query, [Emails\Model\State\Job::Pending]); } - public function fetchByCommandAndArguments(string $command, string $arguments): \ProVM\Emails\Model\Job + 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 fetchPendingByMessage(int $message_id): \ProVM\Emails\Model\Job + public function fetchAllPendingByCommand(string $command): array { - $query = "SELECT * FROM {$this->getTable()} WHERE `message_id` = ? AND `executed` = 0"; - return $this->fetchOne($query, [$message_id]); + $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/State/Job.php b/api/src/Repository/State/Job.php index e7f9d57..11a6699 100644 --- a/api/src/Repository/State/Job.php +++ b/api/src/Repository/State/Job.php @@ -57,7 +57,7 @@ CREATE TABLE {$this->getTable()} ( $query = "SELECT * FROM `{$this->getTable()}` WHERE `job_id` = ?"; return $this->fetchMany($query, [$job_id]); } - public function fetchByJobAndStatus(int $job_id, int $status): Emails\Model\Job + 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]); diff --git a/cli/common/Command/Jobs/Check.php b/cli/common/Command/Jobs/Check.php index d3c9b23..5a5ecc6 100644 --- a/cli/common/Command/Jobs/Check.php +++ b/cli/common/Command/Jobs/Check.php @@ -1,14 +1,12 @@ logger->notice('Grabbing pending jobs.'); - $response = $this->communicator->get('/jobs/pending'); - $body = $response->getBody()->getContents(); - if (trim($body) === '') { - return []; - } - return json_decode($body)->jobs; - } - protected function runJob($job): bool - { - $base_command = '/app/bin/emails'; - $cmd = [$base_command, $job->command]; - if ($job->arguments !== '') { - $cmd []= $job->arguments; - } - $cmd = implode(' ', $cmd); - $this->logger->notice("Running '{$cmd}'"); - $response = shell_exec($cmd); - $this->logger->info("Result: {$response}"); - return $response !== false; - } - public function execute(InputInterface $input, OutputInterface $output) { $section1 = $output->section(); @@ -53,17 +27,16 @@ class Check extends Command $io1 = new SymfonyStyle($input, $section1); $io2 = new SymfonyStyle($input, $section2); $io1->title('Checking Pending Jobs'); - $pending_jobs = $this->getPendingJobs(); + $pending_jobs = $this->service->getPending(); $notice = 'Found ' . count($pending_jobs) . ' jobs'; $io1->text($notice); - $this->logger->info($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->runJob($job)) { + if ($this->service->run($job)) { $io2->success('Success'); } else { $io2->error('Failure'); 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 index c0fa6eb..eb2f94c 100644 --- a/cli/common/Command/Mailboxes/Check.php +++ b/cli/common/Command/Mailboxes/Check.php @@ -5,9 +5,8 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use ProVM\Service\Communicator; use Symfony\Component\Console\Style\SymfonyStyle; -use function Safe\json_decode; +use ProVM\Service\Mailboxes; #[AsCommand( name: 'mailboxes:check', @@ -16,33 +15,11 @@ use function Safe\json_decode; )] class Check extends Command { - public function __construct(protected Communicator $communicator, protected int $min_check_days, string $name = null) + public function __construct(protected Mailboxes $service, string $name = null) { parent::__construct($name); } - protected function getMailboxes(): array - { - $response = $this->communicator->get('/mailboxes/registered'); - $body = $response->getBody()->getContents(); - if (trim($body) === '') { - return []; - } - return json_decode($body)->mailboxes; - } - protected function checkMailbox($mailbox): bool - { - if ((new \DateTimeImmutable())->diff(new \DateTimeImmutable($mailbox->last_checked->date->date))->days < $this->min_check_days) { - return true; - } - $response = $this->communicator->get("/mailbox/{$mailbox->id}/check"); - $body = $response->getBody()->getContents(); - if (trim($body) === '') { - return true; - } - return json_decode($body)->status; - } - public function execute(InputInterface $input, OutputInterface $output): int { $section1 = $output->section(); @@ -50,7 +27,7 @@ class Check extends Command $io1 = new SymfonyStyle($input, $section1); $io2 = new SymfonyStyle($input, $section2); $io1->title('Checking for New Messages'); - $mailboxes = $this->getMailboxes(); + $mailboxes = $this->service->getAll(); $notice = 'Found ' . count($mailboxes) . ' mailboxes'; $io1->text($notice); if (count($mailboxes) > 0) { @@ -59,7 +36,7 @@ class Check extends Command foreach ($mailboxes as $mailbox) { $section2->clear(); $io2->text("Checking {$mailbox->name}"); - if ($this->checkMailbox($mailbox)) { + if ($this->service->check($mailbox)) { $io2->success("Found new emails in {$mailbox->name}"); } else { $io2->info("No new emails in {$mailbox->name}"); diff --git a/cli/common/Command/Messages/Grab.php b/cli/common/Command/Messages/Grab.php index 89a85f9..1be0fd0 100644 --- a/cli/common/Command/Messages/Grab.php +++ b/cli/common/Command/Messages/Grab.php @@ -7,19 +7,17 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use ProVM\Service\Communicator; -use function Safe\json_decode; +use ProVM\Service\Mailboxes; #[AsCommand( name: 'messages:grab', - description: 'Run grab messages job for registered mailboxes', + description: 'Run grab messages job for mailbox', hidden: false )] class Grab extends Command { - public function __construct(Communicator $communicator, string $name = null) + public function __construct(protected Mailboxes $service, string $name = null) { - $this->setCommunicator($communicator); parent::__construct($name); } protected function configure() @@ -27,27 +25,6 @@ class Grab extends Command $this->addArgument('mailbox_id', InputArgument::REQUIRED, 'Mailbox ID to grab emails'); } - protected Communicator $communicator; - public function getCommunicator(): Communicator - { - return $this->communicator; - } - public function setCommunicator(Communicator $communicator): Grab - { - $this->communicator = $communicator; - return $this; - } - - protected function grabMessages(int $mailbox_id): int - { - $response = $this->getCommunicator()->get("/mailbox/{$mailbox_id}/grab"); - $body = $response->getBody()->getContents(); - if (trim($body) === '') { - return 0; - } - return json_decode($body)->messages->count; - } - public function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); @@ -55,7 +32,7 @@ class Grab extends Command $mailbox_id = $input->getArgument('mailbox_id'); $io->title("Grabbing Messages for Mailbox ID {$mailbox_id}"); $io->section('Grabbing Messages'); - $count = $this->grabMessages($mailbox_id); + $count = $this->service->grabMessages($mailbox_id); $io->info("Found {$count} messages"); $io->success('Done.'); 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 @@ +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' => var_export($request->getUri(), true), - 'body' => $request->getBody()->getContents() - ]; - $this->getLogger()->info(\Safe\json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); - return $response; - } -} diff --git a/cli/common/Service/Attachments.php b/cli/common/Service/Attachments.php new file mode 100644 index 0000000..681b291 --- /dev/null +++ b/cli/common/Service/Attachments.php @@ -0,0 +1,114 @@ +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 index b22589c..c655079 100644 --- a/cli/common/Service/Communicator.php +++ b/cli/common/Service/Communicator.php @@ -4,29 +4,12 @@ namespace ProVM\Service; use HttpResponseException; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\ResponseInterface; -use Psr\Log\LoggerInterface; use Safe\Exceptions\JsonException; use function Safe\json_encode; class Communicator { - public function __construct(ClientInterface $client) - { - $this->setClient($client); - } - - protected ClientInterface $client; - - public function getClient(): ClientInterface - { - return $this->client; - } - - public function setClient(ClientInterface $client): Communicator - { - $this->client = $client; - return $this; - } + public function __construct(protected ClientInterface $client) {} /** * @throws HttpResponseException @@ -52,7 +35,7 @@ class Communicator ]; $options['body'] = json_encode($body); } - return $this->handleResponse($this->getClient()->request($method, $uri, $options)); + return $this->handleResponse($this->client->request($method, $uri, $options)); } /** 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 index c27dbf4..7ffac08 100644 --- a/cli/common/Wrapper/Application.php +++ b/cli/common/Wrapper/Application.php @@ -3,7 +3,6 @@ namespace ProVM\Wrapper; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Application as Base; - class Application extends Base { public function __construct(ContainerInterface $container, string $name = 'UNKNOWN', string $version = 'UNKNOWN') diff --git a/cli/crontab b/cli/crontab index fc8ab81..b567b58 100644 --- a/cli/crontab +++ b/cli/crontab @@ -1,10 +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 +#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 -1 * * * * /app/bin/emails jobs:pending >> /logs/jobs.log +* * * * * /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 +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 +0 1 * * 2-6 /app/bin/emails attachments:check >> /logs/attachments.log diff --git a/cli/docker-compose.yml b/cli/docker-compose.yml index ea1ec32..e86b638 100644 --- a/cli/docker-compose.yml +++ b/cli/docker-compose.yml @@ -12,5 +12,6 @@ services: - .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/resources/commands/03_jobs.php b/cli/resources/commands/03_jobs.php index bd6eed1..ea36967 100644 --- a/cli/resources/commands/03_jobs.php +++ b/cli/resources/commands/03_jobs.php @@ -1,2 +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/middleware/98_log.php b/cli/setup/middleware/98_log.php deleted file mode 100644 index 5628448..0000000 --- a/cli/setup/middleware/98_log.php +++ /dev/null @@ -1,2 +0,0 @@ -add($app->getContainer()->get(ProVM\Common\Middleware\Logging::class)); diff --git a/cli/setup/settings/01_env.php b/cli/setup/settings/01_env.php index 107718a..76b4051 100644 --- a/cli/setup/settings/01_env.php +++ b/cli/setup/settings/01_env.php @@ -2,7 +2,9 @@ return [ 'api_uri' => $_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/setups/04_commands.php b/cli/setup/setups/04_commands.php index 50ae9d1..23fd362 100644 --- a/cli/setup/setups/04_commands.php +++ b/cli/setup/setups/04_commands.php @@ -3,18 +3,19 @@ use Psr\Container\ContainerInterface; return [ - ProVM\Command\Attachments\DecryptPdf::class => function(ContainerInterface $container) { - return new ProVM\Command\Attachments\DecryptPdf( + ProVM\Service\Mailboxes::class => function(ContainerInterface $container) { + return new ProVM\Service\Mailboxes( $container->get(ProVM\Service\Communicator::class), $container->get(Psr\Log\LoggerInterface::class), - 'qpdf', - $container->get('passwords') + $container->get('min_check_days') ); }, - ProVM\Command\Mailboxes\Check::class => function(ContainerInterface $container) { - return new ProVM\Command\Mailboxes\Check( + ProVM\Service\Attachments::class => function(ContainerInterface $container) { + return new ProVM\Service\Attachments( $container->get(ProVM\Service\Communicator::class), - 1 + $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 index 7f6e30b..ef4a697 100644 --- a/cli/setup/setups/98_log.php +++ b/cli/setup/setups/98_log.php @@ -10,8 +10,7 @@ return [ ]; }, 'request_log_handler' => function(ContainerInterface $container) { - return (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log']))) - ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true)); + return (new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('logs_folder'), 'requests.log']))); }, 'request_logger' => function(ContainerInterface $container) { return new Monolog\Logger( diff --git a/emails-2022-11-30-03-59-49.sql b/emails-2022-11-30-03-59-49.sql deleted file mode 100644 index 3fd22d9..0000000 --- a/emails-2022-11-30-03-59-49.sql +++ /dev/null @@ -1,94 +0,0 @@ --- Adminer 4.8.1 MySQL 5.5.5-10.9.3-MariaDB-1:10.9.3+maria~ubu2204 dump - -SET NAMES utf8; -SET time_zone = '+00:00'; -SET foreign_key_checks = 0; -SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'; - -SET NAMES utf8mb4; - -DROP TABLE IF EXISTS `attachments`; -CREATE TABLE `attachments` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `message_id` int(10) unsigned NOT NULL, - `filename` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `message_id` (`message_id`), - CONSTRAINT `attachments_ibfk_2` FOREIGN KEY (`message_id`) REFERENCES `messages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -DROP TABLE IF EXISTS `attachments_jobs`; -CREATE TABLE `attachments_jobs` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `message_id` int(10) unsigned NOT NULL, - `date_time` datetime NOT NULL, - `executed` int(1) DEFAULT 0, - PRIMARY KEY (`id`), - KEY `message_id` (`message_id`), - CONSTRAINT `attachments_jobs_ibfk_2` FOREIGN KEY (`message_id`) REFERENCES `messages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -DROP TABLE IF EXISTS `attachments_states`; -CREATE TABLE `attachments_states` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `attachment_id` int(10) unsigned NOT NULL, - `name` varchar(100) NOT NULL, - `value` int(1) unsigned NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - KEY `attachment_id` (`attachment_id`), - CONSTRAINT `attachments_states_ibfk_1` FOREIGN KEY (`attachment_id`) REFERENCES `attachments` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -DROP TABLE IF EXISTS `mailboxes`; -CREATE TABLE `mailboxes` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(100) NOT NULL, - `validity` int(11) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -DROP TABLE IF EXISTS `mailboxes_states`; -CREATE TABLE `mailboxes_states` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `mailbox_id` int(10) unsigned NOT NULL, - `date_time` datetime NOT NULL, - `count` int(10) unsigned NOT NULL, - `uids` text NOT NULL, - PRIMARY KEY (`id`), - KEY `mailbox_id` (`mailbox_id`), - CONSTRAINT `mailboxes_states_ibfk_2` FOREIGN KEY (`mailbox_id`) REFERENCES `mailboxes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -DROP TABLE IF EXISTS `messages`; -CREATE TABLE `messages` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `mailbox_id` int(10) unsigned NOT NULL, - `position` int(10) unsigned NOT NULL, - `uid` varchar(255) NOT NULL, - `subject` varchar(255) NOT NULL, - `from` varchar(100) NOT NULL, - `date_time` datetime NOT NULL, - PRIMARY KEY (`id`), - KEY `mailbox_id` (`mailbox_id`), - CONSTRAINT `messages_ibfk_1` FOREIGN KEY (`mailbox_id`) REFERENCES `mailboxes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -DROP TABLE IF EXISTS `messages_states`; -CREATE TABLE `messages_states` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `message_id` int(10) unsigned NOT NULL, - `name` varchar(100) NOT NULL, - `value` int(1) unsigned NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - KEY `message_id` (`message_id`), - CONSTRAINT `messages_states_ibfk_2` FOREIGN KEY (`message_id`) REFERENCES `messages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - --- 2022-11-30 03:59:49 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 index c60465d..a1e97a0 100644 --- a/ui/common/Middleware/Logging.php +++ b/ui/common/Middleware/Logging.php @@ -29,7 +29,7 @@ class Logging { $response = $handler->handle($request); $output = [ - 'uri' => var_export($request->getUri(), true), + '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)); diff --git a/ui/nginx.conf b/ui/nginx.conf index 893e62a..43895cc 100644 --- a/ui/nginx.conf +++ b/ui/nginx.conf @@ -3,6 +3,9 @@ server { 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; } @@ -16,4 +19,4 @@ server { fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } -} \ No newline at end of file +} diff --git a/ui/public/index.php b/ui/public/index.php index 9e2bc0e..3408a32 100644 --- a/ui/public/index.php +++ b/ui/public/index.php @@ -8,11 +8,5 @@ Monolog\ErrorHandler::register($app->getContainer()->get(Psr\Log\LoggerInterface try { $app->run(); } catch (Error | Exception $e) { - $logger = $app->getContainer()->get(Psr\Log\LoggerInterface::class); - if (isset($_REQUEST)) { - $logger->debug(Safe\json_encode(compact('_REQUEST'))); - } - $logger->debug(Safe\json_encode(compact('_SERVER'))); - $logger->error($e); - throw $e; + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->error($e); } 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/views/emails/messages.blade.php b/ui/resources/views/emails/messages.blade.php index 4af8bb1..089eaee 100644 --- a/ui/resources/views/emails/messages.blade.php +++ b/ui/resources/views/emails/messages.blade.php @@ -5,7 +5,12 @@
@endsection +@push('page_styles') + +@endpush + @push('page_scripts') + + +@endpush diff --git a/ui/resources/views/layout/body/header/navbar.blade.php b/ui/resources/views/layout/body/header/navbar.blade.php index 92fa22b..a0a622f 100644 --- a/ui/resources/views/layout/body/header/navbar.blade.php +++ b/ui/resources/views/layout/body/header/navbar.blade.php @@ -1,5 +1,6 @@
diff --git a/ui/setup/setups/98_log.php b/ui/setup/setups/98_log.php index ef8b9d6..f12fc26 100644 --- a/ui/setup/setups/98_log.php +++ b/ui/setup/setups/98_log.php @@ -2,8 +2,13 @@ use Psr\Container\ContainerInterface; return [ - Monolog\Handler\DeduplicationHandler::class => function(ContainerInterface $container) { - return new Monolog\Handler\DeduplicationHandler($container->get(Monolog\Handler\RotatingFileHandler::class)); + 'log_processors' => 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')); @@ -11,22 +16,23 @@ return [ return $handler; }, 'request_logger' => function(ContainerInterface $container) { - $logger = new Monolog\Logger('request_logger'); - $handler = new Monolog\Handler\RotatingFileHandler(implode(DIRECTORY_SEPARATOR, [$container->get('folders')->logs, 'requests.log'])); - $handler->setFormatter($container->get(Monolog\Formatter\SyslogFormatter::class)); - $dedupHandler = new Monolog\Handler\DeduplicationHandler($handler, null, Monolog\Level::Info); - $logger->pushHandler($dedupHandler); - $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; + 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) { - $logger = new Monolog\Logger('file_logger'); - $logger->pushHandler($container->get(Monolog\Handler\DeduplicationHandler::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; + 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')); }, ];