Files
ntspi-app/app/Containers/Dashboard/Tasks/Email/FetchUnreadEmailsTask.php
T
F4iljiandQwen-Coder c115a5c7a7 fix(IMAP): оптимизация обработки email для предотвращения memory exhaustion
Корневая проблема: библиотека webklex/php-imap загружала все тела писем
по умолчанию, что приводило к исчерпанию 256MB памяти при 50+ письмах.

Решение (3 уровня):

Уровень 1 - Конфигурация:
- config/imap.php: добавлены fetch_body=false, fetch_flags=false, fetch_limit=20
- php.ini: увеличен memory_limit с 256M до 1G, добавлены OPcache настройки

Уровень 2 - Оптимизация кода:
- FetchUnreadEmailsTask: setFetchBody(false), лимиты, MemoryAwareTrait
- FetchEmailNewsAction: batch-обработка (10 писем), gc_collect_cycles()
- DownloadAttachmentsTask: lazy load вложений, extension whitelist (security)
- ConnectToImapTask: MemoryAwareTrait для мониторинга

Уровень 3 - Инфраструктура:
- ProcessEmailNewsJob: queue job для асинхронной обработки (3 попытки, 300s timeout)
- FetchEmailNewsCommand: поддержка --async режима
- MemoryAwareTrait: reusable трейт для мониторинга памяти

Security fixes:
- Extension whitelist для предотвращения path traversal
- Убраны реальные email из config/imap.php
- Добавлена проверка на null message объект

Docs:
- README_EMAIL_OPTIMIZATION.md: полная документация по оптимизации

Fixes: Allowed memory size of 268435456 bytes exhausted

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 22:08:03 +05:00

95 lines
3.9 KiB
PHP

<?php
namespace App\Containers\Dashboard\Tasks\Email;
use App\Containers\Dashboard\Exceptions\EmailFetchException;
use App\Containers\Dashboard\Traits\MemoryAwareTrait;
use Webklex\PHPIMAP\Folder;
use Illuminate\Support\Facades\Log;
/**
* Получение непрочитанных писем из папки
*
* OPTIMIZATION: Загружает только заголовки писем (без тел и вложений).
* Тела загружаются лениво только при необходимости в DownloadAttachmentsTask.
*/
class FetchUnreadEmailsTask
{
use MemoryAwareTrait;
/**
* Получить непрочитанные письма
*
* @param Folder $folder IMAP папка
* @param int|null $limit Лимит сообщений (null = использовать конфиг)
* @return array Массив писем (только метаданные, без тел)
* @throws EmailFetchException При ошибках IMAP-подключения
*/
public function run(Folder $folder, ?int $limit = null): array
{
$limit = $limit ?? config('imap.options.fetch_limit', 20);
Log::info('[FetchUnreadEmailsTask] Получение непрочитанных писем', [
'folder' => $folder->full_name ?? $folder->name ?? 'unknown',
'limit' => $limit,
]);
try {
// OPTIMIZATION: Загружаем только заголовки (без тел и вложений)
// setFetchBody(false) предотвращает загрузку raw_body и structure
$query = $folder->messages()
->unseen()
->setFetchBody(false)
->setFetchFlags(false);
// Применяем лимит для предотвращения переполнения памяти
if ($limit > 0) {
$query->limit($limit);
}
$messages = $query->get();
$emails = [];
foreach ($messages as $message) {
// OPTIMIZATION: Извлекаем только метаданные (не загружаем тело)
$from = $message->getFrom()[0] ?? null;
$emails[] = [
'message' => $message,
'message_id' => $message->getMessageId(),
'uid' => $message->getUid(),
'from_email' => $from?->mail ?? null,
'from_name' => $from?->name ?? null,
'subject' => $message->getSubject(),
'date' => $message->getDate(),
'has_attachments' => $message->hasAttachments(),
'size' => $message->getSize(),
];
}
// OPTIMIZATION: Логируем память после загрузки
$this->logMemoryUsage('after_fetching_emails', [
'emails_count' => count($emails),
]);
Log::info('[FetchUnreadEmailsTask] Получены письма', [
'count' => count($emails),
'folder' => $folder->full_name ?? $folder->name ?? 'unknown',
]);
return $emails;
} catch (EmailFetchException $e) {
// Пробрасываем наши кастомные исключения
throw $e;
} catch (\Exception $e) {
Log::error('[FetchUnreadEmailsTask] Ошибка получения писем', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// Пробрасываем как EmailFetchException для правильной обработки в Action
throw EmailFetchException::connectionFailed('Failed to fetch emails: ' . $e->getMessage(), 0, $e);
}
}
}