diff --git a/appinfo/info.xml b/appinfo/info.xml index 6741863a57dd87d912d76327e309f60f357870a9..f12eca267f341240a4ee4f2f31c1107a59ba3c76 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -18,7 +18,7 @@ OCA\EmailRecovery\Settings\RecoveryEmailSettings - OCA\EmailRecovery\BackgroundJob\WeeklyRecoveryNotificationJob + OCA\EmailRecovery\BackgroundJob\RecoveryWarningNotificationJob OCA\EmailRecovery\Command\UpdateBlacklistedDomains diff --git a/l10n/de.json b/l10n/de.json index d7b79ce45d638e72087b17f2ee850e2941ffd4dc..8ccd9998d60621e346f97cab68bafb2928085cbe 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -36,6 +36,8 @@ "The email could not be verified. Please try again later.": "Die E-Mail konnte nicht verifiziert werden. Bitte versuchen Sie es später noch einmal.", "The email address is disposable. Please provide another recovery address." : "Die E-Mail-Adresse ist eine Wegwerfadresse. Bitte geben Sie eine andere Wiederherstellungsadresse an.", "The email address is not deliverable. Please provide another recovery address.": "Die E-Mail Adresse ist nicht zustellbar. Bitte geben Sie eine andere Wiederherstellungsadresse an.", - "Murena Workspace will be back fully soon! Please read this guide to know more.": "Murena Workspace wird bald wieder vollständig verfügbar sein! Bitte lies diesen Leitfaden, um mehr zu erfahren." + "Murena Workspace will be back fully soon! Please read this guide to know more.": "Murena Workspace wird bald wieder vollständig verfügbar sein! Bitte lies diesen Leitfaden, um mehr zu erfahren.", + "20250610_account_recovery_warning_subject": "Erforderliche Maßnahme: Richten Sie Ihre Wiederherstellungs-E-Mail vor der Kontolöschung ein.", + "20250610_account_recovery_warning_body": "Sehr geehrter **{username}**,\n\nAus Sicherheitsgründen müssen Sie eine Wiederherstellungs-E-Mail-Adresse für Ihr Murena Cloud-Konto festlegen. Da Sie dies noch nicht getan haben, ist die Nutzung Ihrer E-Mail-Adresse eingeschränkt. Bitte richten Sie eine Wiederherstellungs-E-Mail-Adresse ein und bestätigen Sie diese, um alle Nutzungsbeschränkungen für Ihre E-Mail-Adresse aufzuheben. Wenn Sie dies nicht tun, wird Ihr Konto am **{disable_date}** deaktiviert und anschließend gelöscht.\n\n[MEINE WIEDERHERSTELLUNGS-E-MAIL-ADRESSE FESTLEGEN](https://murena.io/settings/user/security#recovery-email-form)\n\nMit freundlichen Grüßen,\nDas Murena-Team" },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/l10n/de_DE.json b/l10n/de_DE.json index ee6e86a28d467802a5dda7f5bf972185399abcc9..baef5b62c6b8aefa37813363042687c9bd89ba69 100644 --- a/l10n/de_DE.json +++ b/l10n/de_DE.json @@ -37,6 +37,8 @@ "The email address is disposable. Please provide another recovery address." : "Die E-Mail-Adresse ist eine Wegwerfadresse. Bitte geben Sie eine andere Wiederherstellungsadresse an.", "The email address is not deliverable. Please provide another recovery address.": "Die E-Mail Adresse ist nicht zustellbar. Bitte geben Sie eine andere Wiederherstellungsadresse an.", "Murena Workspace will be back fully soon! Please read this guide to know more.": "Murena Workspace wird bald wieder vollständig verfügbar sein! Bitte lesen Sie diesen Leitfaden, um mehr zu erfahren.", - "This email address in invalid, please use another one.":"Diese E-Mail-Adresse ist ungültig, bitte verwenden Sie eine andere." + "This email address in invalid, please use another one.":"Diese E-Mail-Adresse ist ungültig, bitte verwenden Sie eine andere.", + "20250610_account_recovery_warning_subject": "Erforderliche Maßnahme: Richten Sie Ihre Wiederherstellungs-E-Mail vor der Kontolöschung ein.", + "20250610_account_recovery_warning_body": "Sehr geehrter **{username}**,\n\nAus Sicherheitsgründen müssen Sie eine Wiederherstellungs-E-Mail-Adresse für Ihr Murena Cloud-Konto festlegen. Da Sie dies noch nicht getan haben, ist die Nutzung Ihrer E-Mail-Adresse eingeschränkt. Bitte richten Sie eine Wiederherstellungs-E-Mail-Adresse ein und bestätigen Sie diese, um alle Nutzungsbeschränkungen für Ihre E-Mail-Adresse aufzuheben. Wenn Sie dies nicht tun, wird Ihr Konto am **{disable_date}** deaktiviert und anschließend gelöscht.\n\n[MEINE WIEDERHERSTELLUNGS-E-MAIL-ADRESSE FESTLEGEN](https://murena.io/settings/user/security#recovery-email-form)\n\nMit freundlichen Grüßen,\nDas Murena-Team" },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/l10n/en.json b/l10n/en.json index 5d2ae37d94d3a99dc86afd138674f3ba99dce9d2..23c2d1b7a70aac92bcf388c88900aa6274d43630 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -36,7 +36,9 @@ "The email could not be verified. Please try again later.": "The email could not be verified. Please try again later.", "The email address is disposable. Please provide another recovery address." : "The email address is disposable. Please provide another recovery address.", "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address.", - "Murena Workspace will be back fully soon! Please read this guide to know more.": "Murena Workspace will be back fully soon! Please read this guide to know more." + "Murena Workspace will be back fully soon! Please read this guide to know more.": "Murena Workspace will be back fully soon! Please read this guide to know more.", + "20250610_account_recovery_warning_subject": "Action required: Set up your recovery email before account deletion", + "20250610_account_recovery_warning_body": "Dear **{username}**,\n\nFor security reasons you need to set a recovery email address for your Murena Cloud account. As you haven't done so yet, the usage of your email address is restricted. Please set and validate a recovery email address to remove all usage restriction on your email address. If you fail to do so, your account will be disabled on **{disable_date}** and then deleted.\n\n[SET MY RECOVERY EMAIL ADDRESS](https://murena.io/settings/user/security#recovery-email-form)\n\nBest regards,\nThe Murena Team" }, "pluralForm": "nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/l10n/es.json b/l10n/es.json index 79d25d7c5a5261ecd32cc65c88f6547f4ba7a2b8..59bfc225c12084ffb27e2749e764392d0fdaa3b5 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -37,6 +37,8 @@ "The email address is disposable. Please provide another recovery address." : "La dirección de correo electrónico es desechable. Por favor, proporcione otra dirección de recuperación.", "The email address is not deliverable. Please provide another recovery address.": "La dirección de correo electrónico no se puede entregar. Por favor, proporcione otra dirección de recuperación.", "Murena Workspace will be back fully soon! Please read this guide to know more.": "¡Murena Workspace estará de vuelta al 100% pronto! Por favor lee esta guía para saber más.", - "This email address in invalid, please use another one.":"Esta dirección de correo electrónico no es válida, por favor utiliza otra." + "This email address in invalid, please use another one.":"Esta dirección de correo electrónico no es válida, por favor utiliza otra.", + "20250610_account_recovery_warning_subject": "Acción necesaria: Configure su correo electrónico de recuperación antes de eliminar la cuenta", + "20250610_account_recovery_warning_body": "Estimado **{username}**,\n\nPor motivos de seguridad, debe establecer una dirección de correo electrónico de recuperación para su cuenta de Murena Cloud. Como aún no lo ha hecho, el uso de su dirección de correo electrónico está restringido. Configure y valide una dirección de correo electrónico de recuperación para eliminar todas las restricciones de uso de su dirección de correo electrónico. Si no lo hace, su cuenta se desactivará el **{disable_date}** y se eliminará.\n\n[ESTABLECER MI DIRECCIÓN DE CORREO ELECTRÓNICO DE RECUPERACIÓN](https://murena.io/settings/user/security#recovery-email-form)\n\nSaludos cordiales,\nEl equipo Murena" },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/l10n/fr.json b/l10n/fr.json index ccb69996e42b6bdf942e1e16c3ec0e51a93d7210..31a45d08b0c5e60148e8126fd06ee092102b88eb 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -37,6 +37,8 @@ "The email address is disposable. Please provide another recovery address." : "L'adresse électronique est jetable. Veuillez fournir une autre adresse de récupération.", "The email address is not deliverable. Please provide another recovery address.": "L'adresse électronique ne peut être délivrée. Veuillez fournir une autre adresse de recouvrement.", "Murena Workspace will be back fully soon! Please read this guide to know more.": "Murena Workspace sera bientôt complètement de retour ! Veuillez lire ce guide pour en savoir plus.", - "This email address in invalid, please use another one.":"Cette adresse e-mail est invalide, veuillez en utiliser une autre." + "This email address in invalid, please use another one.":"Cette adresse e-mail est invalide, veuillez en utiliser une autre.", + "20250610_account_recovery_warning_subject": "Action requise : Configurez votre e-mail de récupération avant la suppression de votre compte", + "20250610_account_recovery_warning_body": "Cher **{username}**,\n\nPour des raisons de sécurité, vous devez définir une adresse électronique de récupération pour votre compte Murena Cloud. Comme vous ne l'avez pas encore fait, l'utilisation de votre adresse e-mail est restreinte. Veuillez définir et valider une adresse e-mail de récupération afin de supprimer toute restriction d'utilisation de votre adresse e-mail. Si vous ne le faites pas, votre compte sera désactivé le **{disable_date}** puis supprimé.\n\n[DÉFINIR MON ADRESSE E-MAIL DE RÉCUPÉRATION](https://murena.io/settings/user/security#recovery-email-form)\n\nMeilleures salutations,\nL'équipe Murena" },"pluralForm" :"nplurals=2; plural=n > 1;" } \ No newline at end of file diff --git a/l10n/it.json b/l10n/it.json index 21e38a034ae1afd521fe3b2d6d5e33708e435e1c..135dda8b5b15eb1d03ab620b51e8dbd221f320c0 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -37,6 +37,8 @@ "The email address is disposable. Please provide another recovery address." : "L'indirizzo e-mail è monouso. Si prega di fornire un altro indirizzo di recupero.", "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address.", "Murena Workspace will be back fully soon! Please read this guide to know more.": "Murena Workspace tornerà presto completamente operativo! Si prega di leggere questa guida per saperne di più.", - "This email address in invalid, please use another one.":"Questo indirizzo email non è valido, per favore usane un altro." + "This email address in invalid, please use another one.":"Questo indirizzo email non è valido, per favore usane un altro.", + "20250610_account_recovery_warning_subject": "Azione necessaria: Impostare l'e-mail di recupero prima dell'eliminazione dell'account.", + "20250610_account_recovery_warning_body": "Caro **{username}**,\n\nPer motivi di sicurezza è necessario impostare un indirizzo e-mail di recupero per il proprio account Murena Cloud. Poiché non l'avete ancora fatto, l'uso del vostro indirizzo e-mail è limitato. Si prega di impostare e convalidare un indirizzo e-mail di ripristino per rimuovere tutte le restrizioni all'uso del proprio indirizzo e-mail. In caso contrario, l'account verrà disattivato alla **{disable_date}** e quindi cancellato.\n\n[IMPOSTARE L'INDIRIZZO E-MAIL DI RECUPERO](https://murena.io/settings/user/security#recovery-email-form)\n\nCordiali saluti,\nIl team Murena" },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/lib/BackgroundJob/RecoveryWarningNotificationJob.php b/lib/BackgroundJob/RecoveryWarningNotificationJob.php new file mode 100644 index 0000000000000000000000000000000000000000..a8a8cbfc8aff5ba610baf5bf271cef6c469d7d1d --- /dev/null +++ b/lib/BackgroundJob/RecoveryWarningNotificationJob.php @@ -0,0 +1,750 @@ + + * + * @author Ronak Patel + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\EmailRecovery\BackgroundJob; + +use OCA\EmailRecovery\AppInfo\Application; +use OCA\EmailRecovery\Service\NotificationService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\IConfig; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Mail\IEMailTemplate; +use OCP\Mail\IMailer; +use OCP\Notification\IManager as INotificationManager; +use OCP\Notification\INotification; +use Psr\Log\LoggerInterface; +use OCA\EmailRecovery\Service\RecoveryEmailService; + +/** + * Class RecoveryWarningNotificationJob + * + * This background job handles sending warning notifications and emails to users who have not set a recovery email. + * It also disables or deletes users after certain periods if they remain unverified. + * + * @package OCA\EmailRecovery\BackgroundJob + */ +class RecoveryWarningNotificationJob extends TimedJob { + private IConfig $config; + private IMailer $mailer; + private LoggerInterface $logger; + private IUserManager $userManager; + private NotificationService $notificationService; + private array $uids = []; + private array $disableUids = []; + private array $deleteUids = []; + private ITimeFactory $timeFactory; + private INotificationManager $notificationManager; + private IURLGenerator $urlGenerator; + private RecoveryEmailService $recoveryEmailService; + /** + * @var int Days after which a user without recovery email is disabled + */ + private const DISABLE_USER_AFTER_UNVERIFIED_DAYS = 30; + /** + * @var int Days after which a disabled user is deleted + */ + private const DELETE_USER_AFTER_DISABLE_DAYS = 45; + /** + * @var int[] Days since start to send reminders + */ + private const REMINDER_DAYS_SINCE_START = [0, 15, 22, 26, 29, 30]; + /** + * @var bool If true, actions are logged but not performed + */ + private bool $dryRun = false; + /** + * @var bool If true, process only 10 users for testing + */ + private bool $testMode = false; + + public function __construct( + IConfig $config, + IMailer $mailer, + IUserManager $userManager, + LoggerInterface $logger, + NotificationService $notificationService, + ITimeFactory $timeFactory, + INotificationManager $notificationManager, + IURLGenerator $urlGenerator, + RecoveryEmailService $recoveryEmailService + ) { + parent::__construct($timeFactory); + + $this->setInterval(1 * 24 * 60 * 60); // Run for 1 days + $this->setTimeSensitivity(self::TIME_INSENSITIVE); + + $this->config = $config; + $this->mailer = $mailer; + $this->logger = $logger; + $this->userManager = $userManager; + $this->notificationService = $notificationService; + $this->timeFactory = $timeFactory; + $this->notificationManager = $notificationManager; + $this->urlGenerator = $urlGenerator; + $this->recoveryEmailService = $recoveryEmailService; + } + + /** + * Run the background job to process recovery warnings, notifications, and user status changes. + * + * @param mixed $argument Not used. + * + * @return void + */ + protected function run($argument): void { + try { + $this->logger->info('RecoveryWarningNotificationJob: Job started.'); + + // Check messageId configuration + $messageId = $this->config->getSystemValue('account_recovery_warning_messageid', ''); + if (!$messageId) { + $this->logger->error('RecoveryWarningNotificationJob: messageId not set! Job will exit.'); + return; + } + + // Check dry run mode + $this->dryRun = $this->config->getSystemValueBool('recovery_email_dry_run', false); + + // Check test mode + $this->testMode = $this->config->getSystemValueBool('recovery_email_test_mode', false); + + if ($this->dryRun) { + $this->logger->info('RecoveryWarningNotificationJob: Dry-run enabled!'); + } + + if ($this->testMode) { + $this->logger->info('RecoveryWarningNotificationJob: Test mode enabled - processing only 10 users!'); + } + + // Initialize arrays + $this->uids = []; + $this->disableUids = []; + $this->deleteUids = []; + + // Execute main processing steps + $this->identifyUnverifiedUsers(); + $this->sendCloudNotifications($messageId); + $this->sendEmails($messageId); + $this->disableUnverifiedUsers(); + $this->deleteExpiredDisabledUsers(); + + $this->logger->info('RecoveryWarningNotificationJob: Job finished successfully.'); + } catch (\Throwable $e) { + $this->logger->error('RecoveryWarningNotificationJob: Error during job execution', [ + 'exception' => $e, + 'message' => $e->getMessage() + ]); + return; + } + } + + /** + * Identify users who have not set a recovery email and queue them for notifications, disabling, or deletion. + * + * @return void + */ + private function identifyUnverifiedUsers(): void { + $users = $this->config->getUsersForUserValue(Application::APP_ID, 'recovery-email', ''); + if (!is_array($users)) { + $this->logger->warning("RecoveryWarningNotificationJob: Expected array for recovery-email users, got " . gettype($users)); + return; + } + + // Limit to 10 users for dry-run mode for quick testing + if ($this->dryRun && count($users) > 10) { + $users = array_slice($users, 0, 10); + $this->logger->info('RecoveryWarningNotificationJob: Limited to 10 users for dry-run testing'); + } + + // Limit to 10 users for test mode (regardless of dry-run status) + if ($this->testMode && count($users) > 10) { + $users = array_slice($users, 0, 10); + $this->logger->info('RecoveryWarningNotificationJob: Limited to 10 users for test mode'); + } + + $processedCount = 0; + $skippedCount = 0; + $invalidCount = 0; + $subscriptionActiveCount = 0; + $errorCount = 0; + + foreach ($users as $username) { + try { + // Add retry logic for user retrieval to handle dirty table reads + $user = $this->getUserWithRetry($username); + if (!$user) { + $invalidCount++; + continue; + } + + if (!$this->isUserValid($user)) { + $invalidCount++; + continue; + } + + $uid = $user->getUID(); + $emailAddress = $user->getEMailAddress(); + + // Check for active subscription + if ($this->recoveryEmailService->hasActiveSubscription($emailAddress)) { + $subscriptionActiveCount++; + continue; + } + + // Get or set reminder start date + $startDate = $this->recoveryEmailService->getRecoveryEmailReminderStartDate($uid); + if ($startDate === null) { + $startDate = date('Y-m-d', time()); + $this->recoveryEmailService->setRecoveryEmailReminderStartDate($uid, $startDate); + } + + // Set disable date if not set + if (!$this->recoveryEmailService->getUnverifiedUserDisableAt($username)) { + $disableAt = date('Y-m-d', time() + self::DISABLE_USER_AFTER_UNVERIFIED_DAYS * 86400); + $this->recoveryEmailService->setUnverifiedUserDisableAt($username, $disableAt); + } + + // Calculate age and process + $age = $this->recoveryEmailService->getDaysSinceDate($startDate); + + if (in_array($age, self::REMINDER_DAYS_SINCE_START)) { + $this->uids[] = $uid; + } + + $this->identifyForDisable($user, $age); + $this->identifyForDelete($user, $age); + + $processedCount++; + + } catch (\Throwable $e) { + $errorCount++; + $this->logger->error('RecoveryWarningNotificationJob: Error processing user', [ + 'username' => $username, + 'message' => $e->getMessage() + ]); + + // If we get too many errors, log a warning but continue + if ($errorCount > 5) { + $this->logger->warning('RecoveryWarningNotificationJob: Too many errors, continuing with remaining users'); + } + + continue; + } + } + + $this->logger->info('RecoveryWarningNotificationJob: User identification completed', [ + 'totalUsers' => count($users), + 'processedCount' => $processedCount, + 'usersForNotifications' => count($this->uids), + 'usersForDisable' => count($this->disableUids), + 'usersForDelete' => count($this->deleteUids) + ]); + } + + /** + * Get user with retry logic to handle dirty table reads + * + * @param string $username The username to retrieve + * @param int $maxRetries Maximum number of retries + * @param int $retryDelay Delay between retries in microseconds + * @return IUser|null The user object or null if not found + */ + private function getUserWithRetry(string $username, int $maxRetries = 3, int $retryDelay = 100000): ?IUser { + for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { + try { + $user = $this->userManager->get($username); + if ($user) { + return $user; + } + + // If user doesn't exist, no need to retry + return null; + + } catch (\Throwable $e) { + $errorMessage = $e->getMessage(); + + // Check if it's a dirty table read error + if (strpos($errorMessage, 'dirty table reads') !== false || + strpos($errorMessage, 'deadlock') !== false || + strpos($errorMessage, 'lock wait timeout') !== false) { + + if ($attempt < $maxRetries) { + usleep($retryDelay); + continue; + } + } + + // For other errors, log and return null + $this->logger->error('RecoveryWarningNotificationJob: Error retrieving user', [ + 'username' => $username, + 'message' => $errorMessage + ]); + + return null; + } + } + + $this->logger->warning('RecoveryWarningNotificationJob: Failed to retrieve user after all retries', [ + 'username' => $username + ]); + + return null; + } + + /** + * Identifies if the user qualifies for disabling and queues them. + * + * @param IUser $user The user to check + * @param int $age Days since reminder start + * + * @return void + */ + private function identifyForDisable(IUser $user, int $age): void { + if ($age >= self::DISABLE_USER_AFTER_UNVERIFIED_DAYS && $age < self::DELETE_USER_AFTER_DISABLE_DAYS) { + if ($user->isEnabled()) { + $this->disableUids[] = $user->getUID(); + } + } + } + + /** + * Identifies if the user qualifies for deletion and queues them. + * + * @param IUser $user The user to check + * @param int $age Days since reminder start + * + * @return void + */ + private function identifyForDelete(IUser $user, int $age): void { + if ($age >= self::DELETE_USER_AFTER_DISABLE_DAYS) { + $this->deleteUids[] = $user->getUID(); + } + } + + /** + * Disable users who have not set a recovery email after the configured period. + * + * @return void + */ + private function disableUnverifiedUsers(): void { + $disabledCount = 0; + $failedCount = 0; + + foreach ($this->disableUids as $uid) { + try { + $user = $this->userManager->get($uid); + if (!$user) { + $this->logger->warning("RecoveryWarningNotificationJob: User not found for disabling", ['uid' => $uid]); + $failedCount++; + continue; + } + + if ($this->dryRun) { + $this->logger->info("RecoveryWarningNotificationJob: [Dry Run] Would disable user", ['uid' => $uid]); + $disabledCount++; + continue; + } + + $user->setEnabled(false); + $this->config->setUserValue($uid, Application::APP_ID, 'disabledForRecoveryEmail', '1'); + $this->logDisabledUser($uid); + + $this->logger->info('RecoveryWarningNotificationJob: Successfully disabled user', ['uid' => $uid]); + $disabledCount++; + + } catch (\Throwable $e) { + $this->logger->error("RecoveryWarningNotificationJob: Failed to disable user", [ + 'uid' => $uid, + 'message' => $e->getMessage() + ]); + $failedCount++; + } + } + + if ($disabledCount > 0 || $failedCount > 0) { + $this->logger->info('RecoveryWarningNotificationJob: Disable users completed', [ + 'disabledCount' => $disabledCount, + 'failedCount' => $failedCount + ]); + } + } + + /** + * Delete users who have been disabled for longer than the configured period. + * + * @return void + */ + private function deleteExpiredDisabledUsers(): void { + $deletedCount = 0; + $failedCount = 0; + + foreach ($this->deleteUids as $uid) { + try { + $user = $this->userManager->get($uid); + if (!$user) { + $this->logger->warning("RecoveryWarningNotificationJob: User not found for deletion", ['uid' => $uid]); + $failedCount++; + continue; + } + + if ($this->dryRun) { + $this->logger->info("RecoveryWarningNotificationJob: [Dry Run] Would delete user", ['uid' => $uid]); + $deletedCount++; + continue; + } + + $this->recoveryEmailService->deleteRecoveryEmailReminderStartDate($uid); + $this->recoveryEmailService->deleteUnverifiedUserDisableAt($uid); + $user->delete(); + $this->logDeletedUser($uid); + + $this->logger->info('RecoveryWarningNotificationJob: Successfully deleted user', ['uid' => $uid]); + $deletedCount++; + + } catch (\Throwable $e) { + $this->logger->error("RecoveryWarningNotificationJob: Failed to delete user", [ + 'uid' => $uid, + 'message' => $e->getMessage() + ]); + $failedCount++; + } + } + + if ($deletedCount > 0 || $failedCount > 0) { + $this->logger->info('RecoveryWarningNotificationJob: Delete users completed', [ + 'deletedCount' => $deletedCount, + 'failedCount' => $failedCount + ]); + } + } + + /** + * Send cloud notifications to users who need to be reminded. + * + * @param string $messageId The identifier for the notification message. + * + * @return void + */ + private function sendCloudNotifications(string $messageId): void { + try { + $datetime = $this->timeFactory->getDateTime(); + $notification = $this->notificationManager->createNotification(); + $notificationType = 'important'; + + $notification->setApp(Application::APP_ID) + ->setDateTime($datetime) + ->setObject(Application::APP_ID . '-' . strtolower($notificationType), $messageId) + ->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath(Application::APP_ID, strtolower($notificationType) . '.svg'))); + + $this->sendNotificationToUsers($messageId, $notification, $this->uids); + } catch (\Throwable $e) { + $this->logger->error('RecoveryWarningNotificationJob: Error sending cloud notifications', [ + 'message' => $e->getMessage() + ]); + } + } + + /** + * Sends a notification with the specified messageId and notification object to a list of users. + * + * @param string $messageId The identifier for the notification message. + * @param INotification $notification The notification object to be sent. + * @param string[] $users An array of user IDs to whom the notification will be sent. + * + * @return void + */ + protected function sendNotificationToUsers(string $messageId, INotification $notification, array $users): void { + $sentCount = 0; + $failedCount = 0; + + foreach ($users as $username) { + try { + if ($this->dryRun) { + $this->logger->info("RecoveryWarningNotificationJob: [Dry Run] Would send notification to user", ['username' => $username]); + $sentCount++; + continue; + } + + $user = $this->userManager->get($username); + if (!$user) { + $this->logger->warning("RecoveryWarningNotificationJob: User not found for notification", ['username' => $username]); + $failedCount++; + continue; + } + + $this->sendNotificationToUser($messageId, $user, $notification); + $sentCount++; + + } catch (\Throwable $e) { + $this->logger->error('RecoveryWarningNotificationJob: Error sending notification to user', [ + 'username' => $username, + 'message' => $e->getMessage() + ]); + $failedCount++; + } + } + + if ($sentCount > 0 || $failedCount > 0) { + $this->logger->info('RecoveryWarningNotificationJob: Send notifications completed', [ + 'sentCount' => $sentCount, + 'failedCount' => $failedCount + ]); + } + } + + /** + * Sends a notification with the specified messageId and notification object to a user. + * + * @param string $messageId The identifier for the notification message. + * @param IUser $user The user to whom the notification will be sent. + * @param INotification $notification The notification object to be sent. + * + * @return void + */ + protected function sendNotificationToUser(string $messageId, IUser $user, INotification $notification): void { + $uid = $user->getUID(); + $displayName = $user->getDisplayName(); + + try { + $notification->setSubject('cli', [$messageId, $displayName]) + ->setMessage('cli', [$messageId, $displayName]) + ->setUser($uid); + + $this->notificationManager->notify($notification); + } catch (\Throwable $e) { + $this->logger->error('RecoveryWarningNotificationJob: Failed to send notification to user', [ + 'uid' => $uid, + 'message' => $e->getMessage() + ]); + throw $e; + } + } + + /** + * Sends recovery emails to all users who need to be reminded. + * + * @param string $messageId The identifier for the notification message. + * + * @return void + */ + private function sendEmails(string $messageId): void { + $sentCount = 0; + $failedCount = 0; + $noEmailCount = 0; + + foreach ($this->uids as $uid) { + try { + if ($this->dryRun) { + $this->logger->info("RecoveryWarningNotificationJob: [Dry Run] Would send email to user", ['uid' => $uid]); + $sentCount++; + continue; + } + + $user = $this->userManager->get($uid); + if (!$user) { + $this->logger->warning("RecoveryWarningNotificationJob: User not found for email", ['uid' => $uid]); + $failedCount++; + continue; + } + + $username = $user->getDisplayName(); + $emailAddress = $user->getEMailAddress(); + + if (!$emailAddress) { + $this->logger->warning("RecoveryWarningNotificationJob: No email address found for user", ['uid' => $uid]); + $noEmailCount++; + continue; + } + + $language = $this->config->getUserValue($uid, 'core', 'lang', null); + $translations = $this->notificationService->getTranslatedSubjectAndMessage($messageId, $language); + $subject = $translations['subject']; + $message = $translations['message']; + + $parsedSubject = $this->notificationService->getParsedString($subject, $username); + $subject = $parsedSubject['message']; + + $disableDate = $this->recoveryEmailService->getUnverifiedUserDisableAt($username); + $parsedMessage = $this->notificationService->getParsedString($message, $username, $disableDate); + $message = $parsedMessage['message']; + + $this->sendEmail($subject, $message, $emailAddress); + $sentCount++; + + } catch (\Throwable $e) { + $this->logger->error('RecoveryWarningNotificationJob: Error sending email to user', [ + 'uid' => $uid, + 'message' => $e->getMessage() + ]); + $failedCount++; + } + } + + if ($sentCount > 0 || $failedCount > 0 || $noEmailCount > 0) { + $this->logger->info('RecoveryWarningNotificationJob: Send emails completed', [ + 'sentCount' => $sentCount, + 'failedCount' => $failedCount, + 'noEmailCount' => $noEmailCount + ]); + } + } + + /** + * Send an email to a user. + * + * @param string $subject The subject of the email. + * @param string $message The body/content of the email. + * @param string $emailAddress The recipient's email address. + * + * @return void + */ + private function sendEmail(string $subject, string $message, string $emailAddress): void { + try { + // Convert Markdown-style links to HTML anchor tags + $message = preg_replace('/\[(.*?)\]\((.*?)\)/', "$1", $message); + + $template = $this->mailer->createEMailTemplate(Application::APP_ID . '::sendMail'); + $template->setSubject($subject); + $template->addHeader(); + $template->addHeading($subject); + $this->setMailBody($template, $message); + $template->addFooter(); + + $email = $this->mailer->createMessage(); + $email->useTemplate($template); + $email->setTo([$emailAddress]); + + $this->mailer->send($email); + + $this->logger->info('RecoveryWarningNotificationJob: Email sent successfully', [ + 'emailAddress' => $emailAddress + ]); + } catch (\Throwable $e) { + $this->logger->error('RecoveryWarningNotificationJob: Failed to send email', [ + 'emailAddress' => $emailAddress, + 'message' => $e->getMessage() + ]); + throw $e; + } + } + + /** + * Add the message body to the email template, handling line breaks and formatting. + * + * @param IEMailTemplate $template The email template object. + * @param string $message The message body. + * + * @return void + */ + private function setMailBody(IEMailTemplate $template, string $message): void { + $lines = explode("\n", $message); + $finalHtml = ""; + $finalText = ""; + + foreach ($lines as $line) { + if (trim($line) === '') { + continue; + } + $finalHtml .= "

" . $line . "

"; + $finalText .= $line; + } + + $template->addBodyText($finalHtml, $finalText); + } + + /** + * Validate a user for notification and email purposes. + * + * @param IUser|null $user The user to be validated. + * + * @return bool Returns true if the user is valid, false otherwise. + */ + private function isUserValid(?IUser $user): bool { + if (!($user instanceof IUser)) { + return false; + } + + $emailAddress = $user->getEMailAddress(); + return ($emailAddress && $this->mailer->validateMailAddress($emailAddress)); + } + + /** + * Log a user action to the custom log file. + * + * @param string $uid The user ID. + * @param string $action The action performed (e.g., 'Disabled', 'Deleted'). + * @param string $filename The log file name. + * @return void + */ + private function logUserAction(string $uid, string $action, string $filename): void { + try { + $defaultLogDir = $this->config->getSystemValue('recovery_email_log_path', '/var/log/email-recovery-logs'); + + if (!is_dir($defaultLogDir)) { + if (!mkdir($defaultLogDir, 0770, true)) { + $this->logger->error("RecoveryWarningNotificationJob: Failed to create log directory", ['logDir' => $defaultLogDir]); + return; + } + } + + $logFile = rtrim($defaultLogDir, '/') . '/' . $filename; + $timestamp = date('Y-m-d H:i:s'); + $entry = "[$timestamp] $action user: $uid\n"; + + if (file_put_contents($logFile, $entry, FILE_APPEND) === false) { + $this->logger->error("RecoveryWarningNotificationJob: Failed to write to log file", ['logFile' => $logFile]); + } + } catch (\Throwable $e) { + $this->logger->error("RecoveryWarningNotificationJob: Failed to log user action", [ + 'uid' => $uid, + 'action' => $action, + 'message' => $e->getMessage() + ]); + } + } + + /** + * Log a disabled user to the custom log file. + * + * @param string $uid The user ID that was disabled. + * @return void + */ + private function logDisabledUser(string $uid): void { + $this->logUserAction($uid, 'Disabled', 'disabled_users.log'); + } + + /** + * Log a deleted user to the custom log file. + * + * @param string $uid The user ID that was deleted. + * @return void + */ + private function logDeletedUser(string $uid): void { + $this->logUserAction($uid, 'Deleted', 'deleted_users.log'); + } +} diff --git a/lib/BackgroundJob/WeeklyRecoveryNotificationJob.php b/lib/BackgroundJob/WeeklyRecoveryNotificationJob.php deleted file mode 100644 index 6e128f07ce12edf0d81925f18177b3ca3029b390..0000000000000000000000000000000000000000 --- a/lib/BackgroundJob/WeeklyRecoveryNotificationJob.php +++ /dev/null @@ -1,255 +0,0 @@ - - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\EmailRecovery\BackgroundJob; - -use OCA\EmailRecovery\AppInfo\Application; -use OCA\EmailRecovery\Service\NotificationService; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\TimedJob; -use OCP\IConfig; -use OCP\IURLGenerator; -use OCP\IUser; -use OCP\IUserManager; -use OCP\Mail\IEMailTemplate; -use OCP\Mail\IMailer; -use OCP\Notification\IManager as INotificationManager; -use OCP\Notification\INotification; -use Psr\Log\LoggerInterface; - -class WeeklyRecoveryNotificationJob extends TimedJob { - private IConfig $config; - private IMailer $mailer; - private LoggerInterface $logger; - private IUserManager $userManager; - private NotificationService $notificationService; - private $uids = []; - private ITimeFactory $timeFactory; - private INotificationManager $notificationManager; - private IURLGenerator $urlGenerator; - - public function __construct( - IConfig $config, - IMailer $mailer, - IUserManager $userManager, - LoggerInterface $logger, - NotificationService $notificationService, - ITimeFactory $timeFactory, - INotificationManager $notificationManager, - IURLGenerator $urlGenerator - ) { - parent::__construct($timeFactory); - - // $this->setInterval(2 * 60); // Run every 2 minutes - // $this->setInterval(60 * 60 * 24); // Run once per day - $this->setInterval(7 * 24 * 60 * 60); // Run for 7 days - $this->setTimeSensitivity(self::TIME_INSENSITIVE); - - $this->config = $config; - $this->mailer = $mailer; - $this->logger = $logger; - $this->userManager = $userManager; - $this->notificationService = $notificationService; - $this->timeFactory = $timeFactory; - $this->notificationManager = $notificationManager; - $this->urlGenerator = $urlGenerator; - } - - protected function run($argument): void { - try { - $this->prepareValidUserIds(); - $messageId = $this->config->getSystemValue('weekly_reminder_messageid'); - $this->sendCloudNotifications($messageId); - $this->sendEmails($messageId); - } catch (\Exception $e) { - $this->logger->error('Error sending notification emails to users', ['exception' => $e]); - return; - } - } - - /** - * Prepares valid user IDs and stores them in the 'uids' array. - * @return void - */ - private function prepareValidUserIds(): void { - $users = $this->config->getUsersForUserValue(Application::APP_ID, 'recovery-email', ''); - foreach ($users as $username) { - $user = $this->userManager->get($username); - if ($this->isUserValid($user)) { - array_push($this->uids, $user->getUID()); - } - } - } - /** - * Send cloud notification to users - * - * @return void - */ - private function sendCloudNotifications(string $messageId): void { - try { - $datetime = $this->timeFactory->getDateTime(); - $notification = $this->notificationManager->createNotification(); - $notificationType = 'important'; - $notification->setApp(Application::APP_ID) - ->setDateTime($datetime) - ->setObject(Application::APP_ID . '-' . strtolower($notificationType), $messageId) - ->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath(Application::APP_ID, strtolower($notificationType) . '.svg'))); - - $this->sendNotificationToUsers($messageId, $notification, $this->uids); - } catch (\Exception $e) { - $this->logger->error('Error sending recovery email cloud notifications to users. Error:' . $e->getMessage(), ['exception' => $e]); - } - } - /** - * Sends a notification with the specified messageId and notification object to a list of users. - * - * @param string $messageId The identifier for the notification message. - * @param INotification $notification The notification object to be sent. - * @param array $users An array of usernames to whom the notification will be sent. - * - * @return void - */ - protected function sendNotificationToUsers(string $messageId, INotification $notification, array $users): void { - foreach ($users as $username) { - try { - $user = $this->userManager->get($username); - $this->sendNotificationToUser($messageId, $user, $notification); - } catch (\Exception $e) { - $this->logger->error('Error sending recovery email cloud notifications to users. Error:' . $e->getMessage(), ['exception' => $e]); - } - } - } - /** - * Sends a notification with the specified messageId and notification object to a user. - * - * @param string $messageId The identifier for the notification message. - * @param IUser $user The user to whom the notification will be sent. - * @param INotification $notification The notification object to be sent. - * - * @return void - */ - protected function sendNotificationToUser(string $messageId, IUser $user, INotification $notification): void { - $uid = $user->getUID(); - $displayName = $user->getDisplayName(); - try { - $notification->setSubject('cli', [$messageId, $displayName])->setMessage('cli', [$messageId, $displayName])->setUser($uid); - $this->notificationManager->notify($notification); - $this->logger->debug('Notificaiton sent to ' . $uid . ' successfully.'); - } catch (\Exception $e) { - $this->logger->error('Failed to send notification to ' . $uid . '. Error:' . $e->getMessage(), ['exception' => $e]); - } - } - /** - * Sends recovery emails to all users. - * - * @param string $messageId The identifier for the notification message. - * - * @return void - */ - private function sendEmails(string $messageId): void { - foreach ($this->uids as $uid) { - try { - $user = $this->userManager->get($uid); - $username = $user->getDisplayName(); - $emailAddress = $user->getEMailAddress(); - - $language = $this->config->getUserValue($uid, 'core', 'lang', null); - $translations = $this->notificationService->getTranslatedSubjectAndMessage($messageId, $language); - $subject = $translations['subject']; - $message = $translations['message']; - - $parsedSubject = $this->notificationService->getParsedString($subject, $username); - $subject = $parsedSubject['message']; - $parsedMessage = $this->notificationService->getParsedString($message, $username); - $message = $parsedMessage['message']; - - $this->sendEmail($subject, $message, $emailAddress); - $this->logger->debug('Recovery email sent to ' . $emailAddress . ' successfully.'); - } catch (\Exception $e) { - $this->logger->error('Error sending notification email to user ' . $uid.'. Error:' . $e->getMessage(), ['exception' => $e]); - } - } - } - - /** - * Send an email. - * - * @param string $subject The subject of the email. - * @param string $message The body/content of the email. - * @param string $emailAddress The recipient's email address. - * - * @return void - */ - private function sendEmail(string $subject, string $message, string $emailAddress): void { - // Convert Markdown-style links to HTML anchor tags - $message = preg_replace('/\[(.*?)\]\((.*?)\)/', "$1", $message); - $template = $this->mailer->createEMailTemplate(Application::APP_ID . '::sendMail'); - $template->setSubject($subject); - $template->addHeader(); - $template->addHeading($subject); - $this->setMailBody($template, $message); - $template->addFooter(); - - $email = $this->mailer->createMessage(); - $email->useTemplate($template); - $email->setTo([$emailAddress]); - - $this->mailer->send($email); - } - /** - * Special-treat list items and strip empty lines - * - * @param IEMailTemplate $template - * @param string $message - * - * @return void - */ - private function setMailBody(IEMailTemplate $template, string $message): void { - $lines = explode("\n", $message); - $finalHtml = ""; - $finalText = ""; - foreach ($lines as $line) { - if (trim($line) === '') { - continue; - } - $finalHtml .= "

" . $line . "

"; - $finalText .= $line; - } - $template->addBodyText($finalHtml, $finalText); - } - /** - * Validate a user. - * - * @param IUser|null $user The user to be validated. - * - * @return bool Returns true if the user is valid, false otherwise. - */ - private function isUserValid(?IUser $user): bool { - if (!($user instanceof IUser)) { - return false; - } - $emailAddress = $user->getEMailAddress(); - return ($emailAddress && $user->isEnabled() && $this->mailer->validateMailAddress($emailAddress)); - } -} diff --git a/lib/Controller/EmailRecoveryController.php b/lib/Controller/EmailRecoveryController.php index 8f955712163245e2a79a29315a6cbd6fd17712c6..58604be8a643264d31bfd8be69e9933f3f57fdb8 100644 --- a/lib/Controller/EmailRecoveryController.php +++ b/lib/Controller/EmailRecoveryController.php @@ -104,7 +104,8 @@ class EmailRecoveryController extends Controller { $this->recoveryEmailService->makeRecoveryEmailVerified($userId); $this->recoveryEmailService->deleteVerificationToken($token, $user, $verificationKey); - + $this->recoveryEmailService->deleteRecoveryEmailReminderStartDate($userId); + $this->recoveryEmailService->deleteUnverifiedUserDisableAt($userId); $responseParams = [ 'title' => $this->l->t('You verified recovery email successfully.'), 'message' => $this->l->t('You verified recovery email successfully.'), diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index 15acaec45eaba5440474dbf965b5ef8687d6492f..f218135b525cc459ba5d7f07593c69ac29b98328 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -31,6 +31,7 @@ use OCP\L10N\IFactory; use OCP\Notification\INotification; use OCP\Notification\INotifier; use Psr\Log\LoggerInterface; +use OCA\EmailRecovery\Service\RecoveryEmailService; class Notifier implements INotifier { /** @var IFactory */ @@ -41,14 +42,19 @@ class Notifier implements INotifier { /** @var NotificationService */ private $NotificationService; + /** @var RecoveryEmailService */ + private $recoveryEmailService; + public function __construct( IFactory $l10nFactory, LoggerInterface $logger, - NotificationService $NotificationService + NotificationService $NotificationService, + RecoveryEmailService $recoveryEmailService ) { $this->l10nFactory = $l10nFactory; $this->logger = $logger; $this->NotificationService = $NotificationService; + $this->recoveryEmailService = $recoveryEmailService; } /** @@ -95,8 +101,9 @@ class Notifier implements INotifier { $subjectParameters = $parsedSubject['parameters']; $plainSubject = $parsedSubject['message']; $richSubject = $parsedSubject['richString']; - - $parsedMessage = $this->NotificationService->getParsedString($message, $username); + + $disableDate = $this->recoveryEmailService->getUnverifiedUserDisableAt($username); + $parsedMessage = $this->NotificationService->getParsedString($message, $username, $disableDate); $messageParameters = $parsedMessage['parameters']; $plainMessage = $parsedMessage['message']; $richMessage = $parsedMessage['richString']; diff --git a/lib/Service/NotificationService.php b/lib/Service/NotificationService.php index 376df29f719fe5671f7f04e0179c0472b25a94f2..cc0fa89b9bc93b6ed8696fe067ba77e7e0f120fa 100644 --- a/lib/Service/NotificationService.php +++ b/lib/Service/NotificationService.php @@ -19,7 +19,7 @@ class NotificationService { * @param string $message * @param string $username */ - public function getParsedString(string $message, string $username) { + public function getParsedString(string $message, string $username, string $disableDate = '') { $richString = $message; $data = $this->prepareRichString($message, $richString, $username, 'url'); @@ -33,6 +33,13 @@ class NotificationService { $data = $this->prepareRichString($message, $richString, $username, 'bold'); $message = $data['message']; $richString = $data['richString']; + + if ($disableDate != '') { + $data = $this->prepareRichString($message, $richString, $username, 'disable_date', $disableDate); + $message = $data['message']; + $richString = $data['richString']; + } + return $this->assignVariables($message, $richString, $username); } @@ -42,7 +49,7 @@ class NotificationService { * @param string $username * @param string $type */ - private function prepareRichString($message, $richString, $username, $type) { + private function prepareRichString($message, $richString, $username, $type, $disableDate = '') { switch ($type) { case 'url': $richString = preg_replace('/\[(.*?)\]\((.*?)\)/', '{$1[$2]}', $message); @@ -62,6 +69,11 @@ class NotificationService { $richString = str_replace('{username}', $username, $richString); $message = str_replace('{username}', $username, $message); break; + + case 'disable_date': + $richString = str_replace('{disable_date}', $disableDate, $richString); + $message = str_replace('{disable_date}', $disableDate, $message); + break; } return ['message' => $message, 'richString' => $richString]; } diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 0053096f811985b94df69eaab63be1dd64d48c55..10af329c44bf97d9a9f07f30b187d132978e9f7a 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -11,6 +11,7 @@ use OCA\EmailRecovery\Exception\MurenaDomainDisallowedException; use OCA\EmailRecovery\Exception\RecoveryEmailAlreadyFoundException; use OCA\EmailRecovery\Exception\SameRecoveryEmailAsEmailException; use OCA\EmailRecovery\Exception\TooManyVerificationAttemptsException; +use OCA\EcloudAccounts\Service\ShopAccountService; use OCA\EmailRecovery\Db\ConfigMapper; use OCP\Defaults; use OCP\Http\Client\IClientService; @@ -49,13 +50,17 @@ class RecoveryEmailService { private const VERIFYMAIL_API_URL = 'https://verifymail.io/api/%s?key=%s'; private const RATE_LIMIT_EMAIL = 'verifymail_email_ratelimit'; private const RATE_LIMIT_DOMAIN = 'verifymail_domain_ratelimit'; + private const RECOVERY_EMAIL_REMINDER_START_DATE = 'recovery-email-reminder-start-date'; + private const UNVERIFIED_USER_DISABLE_AT = 'unverified-user-disabled-at'; + private $cache; private DomainService $domainService; private IL10N $l; private ISession $session; + private ShopAccountService $shopAccountService; - public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, DomainService $domainService, IL10N $l, ICacheFactory $cacheFactory, IClientService $httpClientService, ConfigMapper $configMapper) { + public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, DomainService $domainService, IL10N $l, ICacheFactory $cacheFactory, IClientService $httpClientService, ConfigMapper $configMapper, ShopAccountService $shopAccountService) { $this->logger = $logger; $this->config = $config; $this->appName = $appName; @@ -73,6 +78,7 @@ class RecoveryEmailService { $this->cacheFactory = $cacheFactory; // Initialize the cache factory $this->cache = $this->cacheFactory->createDistributed(self::CACHE_KEY); // Initialize the cache $this->configMapper = $configMapper; + $this->shopAccountService = $shopAccountService; $commonServiceURL = $this->config->getSystemValue('common_services_url', ''); if (!empty($commonServiceURL)) { @@ -260,7 +266,7 @@ class RecoveryEmailService { $this->logger->info("User ID $username's requested recovery email address is not deliverable."); throw new BlacklistedEmailException($l->t('The email address is not deliverable. Please provide another recovery address.')); } - } catch (\Exception $e) { + } catch (Exception $e) { // Optionally handle specific exceptions if needed here (e.g., timeouts, network errors) $this->logger->error("Error while validating email for user $username: " . $e->getMessage()); throw $e; // Re-throw if necessary @@ -404,7 +410,7 @@ class RecoveryEmailService { $email->setTo([$recoveryEmailAddress]); $email->setFrom([Util::getDefaultEmailAddress('no-reply') => $this->themingDefaults->getName()]); $this->mailer->send($email); - } catch (\Exception $e) { + } catch (Exception $e) { $this->logger->error('Error sending notification email to user ' . $uid, ['exception' => $e]); } } @@ -613,4 +619,78 @@ class RecoveryEmailService { } return $spamAccounts; } + /** Recovery email reminder start date **/ + public function getRecoveryEmailReminderStartDate(string $uid): ?string { + return $this->config->getUserValue($uid, $this->appName, self::RECOVERY_EMAIL_REMINDER_START_DATE, null); + } + public function setRecoveryEmailReminderStartDate(string $uid, string $startDate): void { + $this->config->setUserValue($uid, $this->appName, self::RECOVERY_EMAIL_REMINDER_START_DATE, $startDate); + } + public function deleteRecoveryEmailReminderStartDate($uid) { + $this->config->deleteUserValue($uid, $this->appName, self::RECOVERY_EMAIL_REMINDER_START_DATE); + } + /** Unverified user disable At **/ + public function getUnverifiedUserDisableAt(string $uid): ?string { + return $this->config->getUserValue($uid, $this->appName, self::UNVERIFIED_USER_DISABLE_AT, null); + } + public function setUnverifiedUserDisableAt(string $uid, string $disableDate): void { + $this->config->setUserValue($uid, $this->appName, self::UNVERIFIED_USER_DISABLE_AT, $disableDate); + } + public function deleteUnverifiedUserDisableAt($uid) { + $this->config->deleteUserValue($uid, $this->appName, self::UNVERIFIED_USER_DISABLE_AT); + } + public function getUserDisableDate(string $username): string { + try { + $date = $this->getRecoveryEmailReminderStartDate($username); + if (!$date) { + throw new Exception("No date found."); + } + + return date('Y-m-d', strtotime($date . ' +30 days')); + } catch (Exception $e) { + throw new Exception("Error getting deletion date"); + } + } + + /** + * Calculates how many full days have passed since the given start date. + */ + public function getDaysSinceDate($startDate): int { + if (!$startDate || strtotime($startDate) === false) { + return -1; + } + + $startDateTimestamp = strtotime(date('Y-m-d', strtotime($startDate))); + $todayTimestamp = strtotime(date('Y-m-d')); + + return $this->calculateDayDifference($startDateTimestamp, $todayTimestamp); + } + + /** + * Calculates the number of days between two timestamps. + */ + private function calculateDayDifference($fromTimestamp, $toTimestamp): int { + return (int)(($toTimestamp - $fromTimestamp) / 86400); + } + + + /** + * Method hasActiveSubscription + * + * @param string $email [explicite description] + * + * @return bool + */ + public function hasActiveSubscription(string $email): bool { + $shopUsers = $this->shopAccountService->getUsers($email); + if (empty($shopUsers)) { + return false; + } + foreach ($shopUsers as $shopUser) { + if ($shopUser['has_active_subscription']) { + return true; + } + } + return false; + } }