/
kev
/
Nextcloud_File-Changes-Tracker
Обзор
Документация
Войти
/
kev
/
Nextcloud_File-Changes-Tracker
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
lib/Service/EmailDigestService.php
423 строки
20 KB
Evgeny Konovalov
first_commit
16 фев 2026, 17:45
16 фев 2026, 17:45
476f06c
Код
Авторство
О чём код?
<?php declare(strict_types=1); namespace OCA\FileChangesTracker\Service; use OCA\FileChangesTracker\Db\EventLogMapper; use OCA\FileChangesTracker\Db\FolderConfigMapper; use OCA\FileChangesTracker\Db\NotificationSettingsMapper; use OCP\IURLGenerator; use OCP\Mail\IMailer; use OCP\IUserManager; use Psr\Log\LoggerInterface; /** * Сервис для формирования и отправки ежедневной сводки */ class EmailDigestService { private EventLogMapper $eventLogMapper; private FolderConfigMapper $folderConfigMapper; private NotificationSettingsMapper $settingsMapper; private IMailer $mailer; private IUserManager $userManager; private IURLGenerator $urlGenerator; private LoggerInterface $logger; public function __construct( EventLogMapper $eventLogMapper, FolderConfigMapper $folderConfigMapper, NotificationSettingsMapper $settingsMapper, IMailer $mailer, IUserManager $userManager, IURLGenerator $urlGenerator, LoggerInterface $logger ) { $this->eventLogMapper = $eventLogMapper; $this->folderConfigMapper = $folderConfigMapper; $this->settingsMapper = $settingsMapper; $this->mailer = $mailer; $this->userManager = $userManager; $this->urlGenerator = $urlGenerator; $this->logger = $logger; } /** * Отправить ежедневные сводки всем настроенным пользователям */ public function sendAllDailyDigests(): void { $currentHour = (int)date('H'); $currentMinute = (int)date('i'); $currentTime = sprintf('%02d:%02d', $currentHour, $currentMinute); $settings = $this->settingsMapper->findAllWithDailyDigest(); foreach ($settings as $setting) { try { // Проверяем, пора ли отправлять для этой папки $digestTime = $setting->getDigestTime(); // Сравниваем время (с точностью до часа - чтобы не пропустить) list($targetHour, $targetMinute) = explode(':', $digestTime); $targetHour = (int)$targetHour; // Отправляем если текущий час совпадает с целевым if ($currentHour === $targetHour) { $this->sendDigestForFolder($setting); } } catch (\Exception $e) { $this->logger->error('Failed to send daily digest', [ 'folder_config_id' => $setting->getFolderConfigId(), 'error' => $e->getMessage(), ]); } } } /** * Отправить сводку для конкретной папки */ private function sendDigestForFolder($setting): void { $folderConfig = $this->folderConfigMapper->find($setting->getFolderConfigId()); // Получаем события за ВЧЕРАШНИЙ день (полные сутки 00:00 - 23:59) $yesterday = date('Y-m-d', strtotime('-1 day')); $dateFrom = $yesterday . ' 00:00:00'; $dateTo = $yesterday . ' 23:59:59'; $events = $this->eventLogMapper->findByFilters( [$folderConfig->getId()], null, $dateFrom, $dateTo, null, null, 10000, // Увеличиваем лимит для экспорта 0 ); if (empty($events)) { return; // Нет событий - не отправляем письмо } // Определяем получателей $recipients = $this->getRecipients($folderConfig, $setting); // Формируем и отправляем письмо каждому получателю foreach ($recipients as $email => $displayName) { $this->sendEmail($email, $displayName, $folderConfig, $events, $yesterday); } } /** * Отправить тестовую сводку немедленно (для проверки настроек) * * @param int $folderConfigId ID папки * @throws \Exception если настройки не найдены или отправка не удалась */ public function sendTestDigest(int $folderConfigId): void { $this->logger->info('Test digest started', ['folder_config_id' => $folderConfigId]); try { // Находим настройки $setting = $this->settingsMapper->findByFolderConfigId($folderConfigId); if (!$setting) { throw new \Exception('Настройки уведомлений не найдены для папки ID: ' . $folderConfigId); } $this->logger->info('Settings loaded', [ 'daily_digest_enabled' => $setting->getDailyDigestEnabled(), 'digest_email' => $setting->getDigestEmail() ]); if (!$setting->getDailyDigestEnabled()) { throw new \Exception('Ежедневная сводка не включена для этой папки. Включите её в настройках и сохраните перед отправкой теста.'); } $folderConfig = $this->folderConfigMapper->find($folderConfigId); $this->logger->info('Folder config loaded', [ 'folder_path' => $folderConfig->getFolderPath(), 'responsible' => $folderConfig->getResponsibleId() ]); // Получаем события за ВЧЕРАШНИЙ день (полные сутки 00:00 - 23:59) $yesterday = date('Y-m-d', strtotime('-1 day')); $dateFrom = $yesterday . ' 00:00:00'; $dateTo = $yesterday . ' 23:59:59'; $events = $this->eventLogMapper->findByFilters( [$folderConfig->getId()], null, $dateFrom, $dateTo, null, null, 10000, 0 ); $this->logger->info('Events loaded', ['count' => count($events), 'date' => $yesterday]); // Определяем получателей $recipients = $this->getRecipients($folderConfig, $setting); if (empty($recipients)) { throw new \Exception('Не удалось определить получателя письма. Укажите email в настройках или убедитесь что у ответственного пользователя указан email в профиле.'); } $this->logger->info('Recipients determined', ['recipients' => array_keys($recipients)]); // Отправляем письмо каждому получателю foreach ($recipients as $email => $displayName) { $this->logger->info('Sending email', ['to' => $email, 'name' => $displayName]); $this->sendEmail($email, $displayName, $folderConfig, $events, $yesterday, true); } $this->logger->info('Test digest sent successfully', [ 'folder_config_id' => $folderConfigId, 'recipients_count' => count($recipients), 'events_count' => count($events), ]); } catch (\Exception $e) { $this->logger->error('Test digest failed', [ 'folder_config_id' => $folderConfigId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } } /** * Получить список получателей */ private function getRecipients($folderConfig, $setting): array { $recipients = []; // Если указан конкретный email - используем его if ($setting->getDigestEmail()) { $recipients[$setting->getDigestEmail()] = $folderConfig->getResponsibleId(); return $recipients; } // Иначе берем email ответственного пользователя if ($folderConfig->getResponsibleType() === 'user') { $user = $this->userManager->get($folderConfig->getResponsibleId()); if ($user && $user->getEMailAddress()) { $recipients[$user->getEMailAddress()] = $user->getDisplayName(); } } return $recipients; } /** * Отправить email со сводкой в виде CSV файла * * @param string $email Email получателя * @param string $displayName Отображаемое имя получателя * @param $folderConfig Конфигурация папки * @param array $events События для включения в сводку * @param string $date Дата сводки (Y-m-d) * @param bool $isTest Флаг тестовой отправки */ private function sendEmail(string $email, string $displayName, $folderConfig, array $events, string $date, bool $isTest = false): void { $message = $this->mailer->createMessage(); $dateFormatted = date('d.m.Y', strtotime($date)); $subject = sprintf('%sСводка изменений файлов за %s', $isTest ? '[ТЕСТ] ' : '', $dateFormatted ); // Генерируем CSV файл $csvContent = $this->generateCSV($events); $csvFileName = sprintf('file_changes_%s_%s.csv', $date, preg_replace('/[^a-zA-Z0-9]/', '_', $folderConfig->getFolderPath()) ); // Создаем простое текстовое тело письма $body = $this->generateTextBody($displayName, $folderConfig, $events, $date, $isTest); // Формируем тело письма с CSV данными внутри $fullBody = $body; // Если есть события - добавляем CSV в конец письма if (!empty($events)) { $fullBody .= "\n\n═══════════════════════════════════════════════════════════\n"; $fullBody .= "CSV ДАННЫЕ (Скопируйте в Excel или текстовый редактор)\n"; $fullBody .= "═══════════════════════════════════════════════════════════\n\n"; $fullBody .= $csvContent; $fullBody .= "\n═══════════════════════════════════════════════════════════\n"; $fullBody .= "Инструкция:\n"; $fullBody .= "1. Скопируйте CSV данные выше\n"; $fullBody .= "2. Вставьте в новый файл .csv\n"; $fullBody .= "3. Откройте в Excel (разделитель: точка с запятой)\n"; $this->logger->info('CSV data included in email body', [ 'file_name' => $csvFileName, 'size' => strlen($csvContent), 'events_count' => count($events) ]); } $message->setTo([$email => $displayName]) ->setSubject($subject) ->setBody($fullBody, 'text/plain'); try { $this->logger->info('Attempting to send email', [ 'to' => $email, 'subject' => $subject, 'has_csv_data' => !empty($events) ]); $this->mailer->send($message); $this->logger->info('Daily digest sent successfully', [ 'email' => $email, 'folder' => $folderConfig->getFolderPath(), 'events_count' => count($events), 'is_test' => $isTest, ]); } catch (\Exception $e) { $this->logger->error('Failed to send email', [ 'email' => $email, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw new \Exception('Ошибка отправки письма: ' . $e->getMessage() . '. Проверьте настройки SMTP в Nextcloud (Параметры → Основные → Email сервер)'); } } /** * Сформировать текстовое тело письма */ private function generateTextBody(string $displayName, $folderConfig, array $events, string $date, bool $isTest): string { $journalUrl = $this->urlGenerator->linkToRouteAbsolute('file_changes_tracker.page.index'); $dateFormatted = date('d.m.Y', strtotime($date)); // Подсчет статистики $stats = [ 'created' => 0, 'modified' => 0, 'deleted' => 0, 'renamed' => 0, ]; foreach ($events as $event) { $type = $event->getEventType(); if (isset($stats[$type])) { $stats[$type]++; } } $text = ''; if ($isTest) { $text .= "═══════════════════════════════════════════════════════════\n"; $text .= " ЭТО ТЕСТОВАЯ ОТПРАВКА - для проверки настроек email\n"; $text .= "═══════════════════════════════════════════════════════════\n\n"; } $text .= "Здравствуйте, $displayName!\n\n"; $text .= "Сводка изменений файлов за $dateFormatted\n"; $text .= "Отслеживаемая папка: " . $folderConfig->getFolderPath() . "\n"; $text .= "Период: $dateFormatted 00:00 - $dateFormatted 23:59\n\n"; $text .= "═══════════════════════════════════════════════════════════\n"; $text .= "СТАТИСТИКА\n"; $text .= "═══════════════════════════════════════════════════════════\n\n"; $text .= "Всего событий: " . count($events) . "\n\n"; $text .= " • Создано файлов: " . $stats['created'] . "\n"; $text .= " • Изменено файлов: " . $stats['modified'] . "\n"; $text .= " • Удалено файлов: " . $stats['deleted'] . "\n"; $text .= " • Переименовано файлов: " . $stats['renamed'] . "\n\n"; if (!empty($events)) { $text .= "═══════════════════════════════════════════════════════════\n"; $text .= "ДЕТАЛИ\n"; $text .= "═══════════════════════════════════════════════════════════\n\n"; $text .= "Подробный список всех событий прикреплен к письму в виде CSV файла.\n"; $text .= "Вы можете открыть его в Excel, LibreOffice или любом текстовом редакторе.\n\n"; } else { $text .= "За указанный период событий не зарегистрировано.\n\n"; } $text .= "═══════════════════════════════════════════════════════════\n"; $text .= "ПОЛНЫЙ ЖУРНАЛ\n"; $text .= "═══════════════════════════════════════════════════════════\n\n"; $text .= "Открыть полный журнал событий в Nextcloud:\n"; $text .= "$journalUrl\n\n"; $text .= "---\n"; $text .= "Это автоматическое уведомление от системы отслеживания изменений файлов Nextcloud.\n"; $text .= "Вы получили это письмо, так как являетесь ответственным за папку:\n"; $text .= $folderConfig->getFolderPath() . "\n\n"; $text .= "Чтобы изменить настройки уведомлений, перейдите в раздел\n"; $text .= "администрирования Nextcloud → Отслеживание изменений файлов.\n"; return $text; } /** * Сформировать CSV файл с событиями */ private function generateCSV(array $events): string { // UTF-8 BOM для корректного отображения кириллицы в Excel $csv = "\xEF\xBB\xBF"; // Заголовки (используем точку с запятой для русской локали Excel) $headers = [ 'Дата и время', 'Тип события', 'Путь к файлу', 'Имя файла', 'Пользователь', 'ID пользователя' ]; $csv .= implode(';', array_map([$this, 'escapeCSV'], $headers)) . "\r\n"; // Данные событий foreach ($events as $event) { $row = [ date('d.m.Y H:i:s', strtotime($event->getEventTimestamp())), $this->getEventTypeText($event->getEventType()), $event->getFilePath(), basename($event->getFilePath()), $event->getUserDisplayName(), $event->getUserId() ]; $csv .= implode(';', array_map([$this, 'escapeCSV'], $row)) . "\r\n"; } return $csv; } /** * Экранировать значение для CSV */ private function escapeCSV($value): string { // Если значение содержит точку с запятой, кавычки или перенос строки - оборачиваем в кавычки if (strpos($value, ';') !== false || strpos($value, '"') !== false || strpos($value, "\n") !== false) { $value = '"' . str_replace('"', '""', $value) . '"'; } return $value; } /** * Получить текст типа события */ private function getEventTypeText(string $type): string { $texts = [ 'created' => 'Создание', 'modified' => 'Изменение', 'deleted' => 'Удаление', 'renamed' => 'Переименование', ]; return $texts[$type] ?? $type; } }