diff --git a/composer.json b/composer.json index eb5fecd..9b7efc4 100644 --- a/composer.json +++ b/composer.json @@ -2,6 +2,8 @@ "require": { "php-mqtt/client": "^2.2", "fabpot/goutte": "^4.0", - "symfony/polyfill-ctype": "^1.31" + "symfony/polyfill-ctype": "^1.31", + "phpmailer/phpmailer": "^6.9", + "twig/twig": "^3.18" } } diff --git a/composer.lock b/composer.lock index 15669d6..12d3339 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a6dcfb9667ce99508becf6cd0f9310b9", + "content-hash": "8ffb6b03780af1286dc4d1cd3d8f0445", "packages": [ { "name": "fabpot/goutte", @@ -250,6 +250,87 @@ }, "time": "2024-11-24T20:54:32+00:00" }, + { + "name": "phpmailer/phpmailer", + "version": "v6.9.3", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e", + "reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.2", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2024-11-24T18:04:13+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -1199,6 +1280,82 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php81", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.5.1", @@ -1281,6 +1438,86 @@ } ], "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "twig/twig", + "version": "v3.18.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50", + "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php81": "^1.29" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.18.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2024-12-29T10:51:50+00:00" } ], "packages-dev": [], diff --git a/src/emailService/cache/3e/3eb72c23c9363b64752d0504bdd61ac5.php b/src/emailService/cache/3e/3eb72c23c9363b64752d0504bdd61ac5.php new file mode 100644 index 0000000..e517c54 --- /dev/null +++ b/src/emailService/cache/3e/3eb72c23c9363b64752d0504bdd61ac5.php @@ -0,0 +1,162 @@ + + */ + private array $macros = []; + + public function __construct(Environment $env) + { + parent::__construct($env); + + $this->source = $this->getSourceContext(); + + $this->parent = false; + + $this->blocks = [ + ]; + } + + protected function doDisplay(array $context, array $blocks = []): iterable + { + $macros = $this->macros; + // line 1 + yield " + + + + + + Filament Price Change Summary + + + +
+

Filament Price Change Summary

+

Here's the latest update on filament prices from your tracker:

+
+ + + + + + + + + + + + + "; + // line 39 + $context['_parent'] = $context; + $context['_seq'] = CoreExtension::ensureTraversable(($context["filaments"] ?? null)); + foreach ($context['_seq'] as $context["_key"] => $context["filament"]) { + // line 40 + yield " + + + + + + + + "; + } + $_parent = $context['_parent']; + unset($context['_seq'], $context['_key'], $context['filament'], $context['_parent']); + $context = array_intersect_key($context, $_parent) + $_parent; + // line 49 + yield " +
Filament NameBrandNew PriceOld PriceChangeAmazon Link
"; + // line 41 + yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "filamentName", [], "any", false, false, false, 41), "html", null, true); + yield ""; + // line 42 + yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "brand", [], "any", false, false, false, 42), "html", null, true); + yield ""; + // line 43 + yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "newPrice", [], "any", false, false, false, 43), "html", null, true); + yield ""; + // line 44 + yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "oldPrice", [], "any", false, false, false, 44), "html", null, true); + yield ""; + // line 45 + yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "priceChange", [], "any", false, false, false, 45), "html", null, true); + yield "env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "amazonUrl", [], "any", false, false, false, 46), "html", null, true); + yield "\" target=\"_blank\">View on Amazon
+
+

+ Visit your env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(($context["dashboardUrl"] ?? null), "html", null, true); + yield "\" target=\"_blank\">dashboard for more details. +

+
+ + +"; + yield from []; + } + + /** + * @codeCoverageIgnore + */ + public function getTemplateName(): string + { + return "filament_price_summary.html.twig"; + } + + /** + * @codeCoverageIgnore + */ + public function isTraitable(): bool + { + return false; + } + + /** + * @codeCoverageIgnore + */ + public function getDebugInfo(): array + { + return array ( 124 => 53, 118 => 49, 109 => 46, 105 => 45, 101 => 44, 97 => 43, 93 => 42, 89 => 41, 86 => 40, 82 => 39, 42 => 1,); + } + + public function getSourceContext(): Source + { + return new Source("", "filament_price_summary.html.twig", "/mnt/www-live/TechOdyssey_Designs_Dashboard/src/emailService/templates/filament_price_summary.html.twig"); + } +} diff --git a/src/emailService/css/email_styles.css b/src/emailService/css/email_styles.css new file mode 100644 index 0000000..99f6671 --- /dev/null +++ b/src/emailService/css/email_styles.css @@ -0,0 +1,9 @@ +body { + font-family: Arial, sans-serif; + background-color: #f9f9f9; + color: #333; +} + +.alert { + border-radius: 10px; +} diff --git a/src/emailService/emailService.php b/src/emailService/emailService.php new file mode 100644 index 0000000..3ee08ac --- /dev/null +++ b/src/emailService/emailService.php @@ -0,0 +1,54 @@ + $value) { + print_r($key); + $body = str_replace("{{" . $key . "}}", strval($value), $body); + } + + // Add environment-based dynamic replacements + $body = str_replace("{{website_url}}", strval($_ENV['WEBSITE_URL']), $body); + $body = str_replace("{{business_name}}", strval($_ENV['BUSINESS_NAME']), $body); + + // SMTP Configuration + $mail->isSMTP(); + $mail->Host = $_ENV['SMTP_HOST']; + $mail->SMTPAuth = true; + $mail->Username = $_ENV['SMTP_USER']; + $mail->Password = $_ENV['SMTP_PASS']; + $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; + $mail->Port = $_ENV['SMTP_PORT']; + + // Email Settings + $mail->setFrom($_ENV['SMTP_USER'], $_ENV['BUSINESS_NAME']); + $mail->addAddress($to); + $mail->Subject = $subject; + $mail->isHTML(true); + $mail->Body = $body; + + $mail->send(); + echo "Email sent successfully to $to"; + } catch (Exception $e) { + echo "Error: {$mail->ErrorInfo}"; + } +} +?> diff --git a/src/emailService/sendPriceDropEmailService.php b/src/emailService/sendPriceDropEmailService.php new file mode 100644 index 0000000..5ef07ef --- /dev/null +++ b/src/emailService/sendPriceDropEmailService.php @@ -0,0 +1,60 @@ + __DIR__ . '/cache', // Optional: Enable Twig cache + 'auto_reload' => true, + ]); + + // Render the Twig template + $body = $twig->render($template . '.html.twig', $variables); + + // SMTP Configuration + $mail->isSMTP(); + $mail->Host = $_ENV['SMTP_HOST']; + $mail->SMTPAuth = true; + $mail->Username = $_ENV['SMTP_USER']; + $mail->Password = $_ENV['SMTP_PASS']; + $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; + $mail->Port = $_ENV['SMTP_PORT']; + + // Email Charset and Settings + $mail->CharSet = 'UTF-8'; // Ensure correct encoding + $mail->setFrom($_ENV['SMTP_USER'], $_ENV['BUSINESS_NAME']); + // Handle multiple recipients + if (is_array($to)) { + foreach ($to as $recipient) { + $mail->addAddress($recipient); + } + } else { + $mail->addAddress($to); // Single recipient + } + $mail->Subject = $subject; + $mail->isHTML(true); + $mail->Body = $body; + + $mail->send(); + foreach ($to as $recipient) { + echo "Email sent successfully to $recipient\n"; + } + } catch (Exception $e) { + echo "Error: {$mail->ErrorInfo}"; + } +} + + +?> diff --git a/src/emailService/templates/alert_error.html b/src/emailService/templates/alert_error.html new file mode 100644 index 0000000..de5a9e8 --- /dev/null +++ b/src/emailService/templates/alert_error.html @@ -0,0 +1,19 @@ + + + + + + + Error Alert + + +
+ +
+ + diff --git a/src/emailService/templates/alert_info.html b/src/emailService/templates/alert_info.html new file mode 100644 index 0000000..23d85da --- /dev/null +++ b/src/emailService/templates/alert_info.html @@ -0,0 +1,19 @@ + + + + + + + Info Alert + + +
+ +
+ + diff --git a/src/emailService/templates/alert_success.html b/src/emailService/templates/alert_success.html new file mode 100644 index 0000000..d8a348c --- /dev/null +++ b/src/emailService/templates/alert_success.html @@ -0,0 +1,19 @@ + + + + + + + Success Alert + + +
+ +
+ + diff --git a/src/emailService/templates/filament_price_summary.html.twig b/src/emailService/templates/filament_price_summary.html.twig new file mode 100644 index 0000000..92e3d09 --- /dev/null +++ b/src/emailService/templates/filament_price_summary.html.twig @@ -0,0 +1,57 @@ + + + + + + + Filament Price Change Summary + + + +
+

Filament Price Change Summary

+

Here's the latest update on filament prices from your tracker:

+
+ + + + + + + + + + + + + {% for filament in filaments %} + + + + + + + + + {% endfor %} + +
Filament NameBrandNew PriceOld PriceChangeAmazon Link
{{ filament.filamentName }}{{ filament.brand }}{{ filament.newPrice }}{{ filament.oldPrice }}{{ filament.priceChange }}View on Amazon
+
+

+ Visit your dashboard for more details. +

+
+ + diff --git a/src/filamentTracker/priceDropEmailCheck.php b/src/filamentTracker/priceDropEmailCheck.php new file mode 100644 index 0000000..a7f2cb7 --- /dev/null +++ b/src/filamentTracker/priceDropEmailCheck.php @@ -0,0 +1,132 @@ +query("SELECT id, filamentName, brand, amazonUrl FROM filamentTracker"); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + echo "Error fetching filaments: " . $e->getMessage(); + return []; + } +} + +// Fetch the latest prices for each filament +function getCurrentPrices() { + global $pdo; + try { + $stmt = $pdo->query(" + SELECT fp.filamentId, fp.price, fp.recordedAt + FROM filamentPriceHistory fp + INNER JOIN ( + SELECT filamentId, MAX(recordedAt) AS latestRecordedAt + FROM filamentPriceHistory + GROUP BY filamentId + ) latest ON fp.filamentId = latest.filamentId AND fp.recordedAt = latest.latestRecordedAt + "); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + echo "Error fetching current prices: " . $e->getMessage(); + return []; + } +} + +// Fetch prices from the last 24 hours for each filament +function getPriceHistoryForLast24Hours() { + global $pdo; + try { + $stmt = $pdo->query(" + SELECT filamentId, price, recordedAt + FROM filamentPriceHistory + WHERE recordedAt >= NOW() - INTERVAL 1 DAY + ORDER BY filamentId, recordedAt DESC + "); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + echo "Error fetching price history for the last 24 hours: " . $e->getMessage(); + return []; + } +} + +// Compare prices and find drops +function checkPriceDropsAndNotify() { + $filaments = getFilaments(); + $currentPrices = getCurrentPrices(); + $priceHistory = getPriceHistoryForLast24Hours(); + + // Index prices by filamentId for quick lookup + $currentPricesMap = []; + foreach ($currentPrices as $current) { + $currentPricesMap[$current['filamentId']] = $current['price']; + } + + // Group price history by filamentId + $priceHistoryMap = []; + foreach ($priceHistory as $history) { + $priceHistoryMap[$history['filamentId']][] = $history['price']; + } + + $priceDrops = []; + + foreach ($filaments as $filament) { + $filamentId = $filament['id']; + $currentPrice = $currentPricesMap[$filamentId] ?? null; + + if ($currentPrice !== null && isset($priceHistoryMap[$filamentId])) { + foreach ($priceHistoryMap[$filamentId] as $oldPrice) { + if ($currentPrice < (float)$oldPrice) { + $priceDrops[] = [ + 'filamentName' => $filament['filamentName'], + 'brand' => $filament['brand'] ?: 'Unknown', + 'newPrice' => "£" . number_format($currentPrice, 2), + 'oldPrice' => "£" . number_format((float)$oldPrice, 2), + 'priceChange' => "-£" . number_format((float)$oldPrice - $currentPrice, 2), + 'amazonUrl' => $filament['amazonUrl'] + ]; + break; // Stop checking once a price drop is detected + } + } + } + } + + function getRecipients() { + global $pdo; + try { + $stmt = $pdo->query("SELECT `email` FROM `users` WHERE `alertEmails` = 1"); + return $stmt->fetchAll(PDO::FETCH_COLUMN); + } catch (PDOException $e) { + echo "Error fetching recipients: " . $e->getMessage(); + return []; + } + } + + // Send email if there are price drops + if (!empty($priceDrops)) { + $recipient = getRecipients(); + $subject = 'Filament Price Change Summary'; + $data = [ + 'filaments' => $priceDrops, + 'dashboardUrl' => $_ENV['WEBSITE_URL'] . '/dashboard' + ]; + + sendEmail($recipient, $subject, 'filament_price_summary', $data); + + echo "Email sent for the following price drops:\n"; + foreach ($priceDrops as $drop) { + echo "- {$drop['filamentName']}: Price dropped from {$drop['oldPrice']} to {$drop['newPrice']}\n"; + } + } else { + echo "No price drops detected.\n"; + } +} + +checkPriceDropsAndNotify(); + +?>