Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
488 changes: 447 additions & 41 deletions resources/translations/messages.en.xlf

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions src/Domain/Analytics/Exception/MissingMessageIdException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace PhpList\Core\Domain\Analytics\Exception;

use LogicException;

class MissingMessageIdException extends LogicException
{
public function __construct()
{
parent::__construct('Message must have an ID');
}
}
6 changes: 3 additions & 3 deletions src/Domain/Analytics/Service/LinkTrackService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

namespace PhpList\Core\Domain\Analytics\Service;

use InvalidArgumentException;
use PhpList\Core\Core\ConfigProvider;
use PhpList\Core\Domain\Analytics\Exception\MissingMessageIdException;
use PhpList\Core\Domain\Analytics\Model\LinkTrack;
use PhpList\Core\Domain\Analytics\Repository\LinkTrackRepository;
use PhpList\Core\Domain\Messaging\Model\Message;
Expand Down Expand Up @@ -36,7 +36,7 @@ public function isExtractAndSaveLinksApplicable(): bool
* Extract links from message content and save them to the LinkTrackRepository
*
* @return LinkTrack[] The saved LinkTrack entities
* @throws InvalidArgumentException if the message doesn't have an ID
* @throws MissingMessageIdException
*/
public function extractAndSaveLinks(Message $message, int $userId): array
{
Expand All @@ -48,7 +48,7 @@ public function extractAndSaveLinks(Message $message, int $userId): array
$messageId = $message->getId();

if ($messageId === null) {
throw new InvalidArgumentException('Message must have an ID');
throw new MissingMessageIdException();
}

$links = $this->extractLinksFromHtml($content->getText() ?? '');
Expand Down
23 changes: 23 additions & 0 deletions src/Domain/Common/Exception/MailboxConnectionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace PhpList\Core\Domain\Common\Exception;

use RuntimeException;
use Throwable;

class MailboxConnectionException extends RuntimeException
{
public function __construct(string $mailbox, ?string $message = null, ?Throwable $previous = null)
{
if ($message === null) {
$message = sprintf(
'Cannot open mailbox "%s": %s',
$mailbox,
imap_last_error() ?: 'unknown error'
);
}
parent::__construct($message, 0, $previous);
}
}
29 changes: 0 additions & 29 deletions src/Domain/Common/I18n/Messages.php

This file was deleted.

6 changes: 6 additions & 0 deletions src/Domain/Common/IspRestrictionsProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ private function readConfigFile(): ?string
$contents = file_get_contents($this->confPath);
if ($contents === false) {
$this->logger->warning('Cannot read ISP restrictions file', ['path' => $this->confPath]);

return null;
}

return $contents;
}

Expand Down Expand Up @@ -106,20 +108,24 @@ private function applyKeyValue(
if ($val !== '' && ctype_digit($val)) {
$maxBatch = (int) $val;
}

return [$maxBatch, $minBatchPeriod, $lockFile];
}
if ($key === 'minbatchperiod') {
if ($val !== '' && ctype_digit($val)) {
$minBatchPeriod = (int) $val;
}

return [$maxBatch, $minBatchPeriod, $lockFile];
}
if ($key === 'lockfile') {
if ($val !== '') {
$lockFile = $val;
}

return [$maxBatch, $minBatchPeriod, $lockFile];
}

return [$maxBatch, $minBatchPeriod, $lockFile];
}

Expand Down
4 changes: 2 additions & 2 deletions src/Domain/Common/Mail/NativeImapMailReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use DateTimeImmutable;
use IMAP\Connection;
use RuntimeException;
use PhpList\Core\Domain\Common\Exception\MailboxConnectionException;

class NativeImapMailReader
{
Expand All @@ -24,7 +24,7 @@ public function open(string $mailbox, int $options = 0): Connection
$link = imap_open($mailbox, $this->username, $this->password, $options);

if ($link === false) {
throw new RuntimeException('Cannot open mailbox: '.(imap_last_error() ?: 'unknown error'));
throw new MailboxConnectionException($mailbox);
}

return $link;
Expand Down
8 changes: 4 additions & 4 deletions src/Domain/Common/Repository/CursorPaginationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace PhpList\Core\Domain\Common\Repository;

use BadMethodCallException;
use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface;
use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel;
use RuntimeException;

trait CursorPaginationTrait
{
Expand All @@ -30,14 +30,14 @@ public function getAfterId(int $lastId, int $limit): array
* Get filtered + paginated messages for a given owner and status.
*
* @return DomainModel[]
* @throws RuntimeException
*/
* @throws BadMethodCallException
* */
public function getFilteredAfterId(int $lastId, int $limit, ?FilterRequestInterface $filter = null): array
{
if ($filter === null) {
return $this->getAfterId($lastId, $limit);
}

throw new RuntimeException('Filter method not implemented');
throw new BadMethodCallException('getFilteredAfterId method not implemented');
}
}
7 changes: 3 additions & 4 deletions src/Domain/Common/SystemInfoCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@ class SystemInfoCollector
/**
* @param string[] $configuredKeys keys to include (empty => use defaults)
*/
public function __construct(
RequestStack $requestStack,
array $configuredKeys = []
) {
public function __construct(RequestStack $requestStack, array $configuredKeys = [])
{
$this->requestStack = $requestStack;
$this->configuredKeys = $configuredKeys;
}
Expand Down Expand Up @@ -72,6 +70,7 @@ public function collectAsString(): string
foreach ($pairs as $k => $v) {
$lines[] = sprintf('%s = %s', $k, $v);
}

return "\n" . implode("\n", $lines);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public function get(
?DateTimeInterface $dateTo = null
): array {
$filter = new EventLogFilter($page, $dateFrom, $dateTo);

return $this->repository->getFilteredAfterId($lastId, $limit, $filter);
}

Expand Down
1 change: 1 addition & 0 deletions src/Domain/Identity/Command/CleanUpOldSessionTokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$output->writeln(sprintf('Successfully removed %d expired session token(s).', $deletedCount));
} catch (Throwable $throwable) {
$output->writeln(sprintf('Error removing expired session tokens: %s', $throwable->getMessage()));

return Command::FAILURE;
}

Expand Down
11 changes: 9 additions & 2 deletions src/Domain/Identity/Service/AdminAttributeDefinitionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,32 @@
use PhpList\Core\Domain\Identity\Repository\AdminAttributeDefinitionRepository;
use PhpList\Core\Domain\Identity\Exception\AttributeDefinitionCreationException;
use PhpList\Core\Domain\Subscription\Validator\AttributeTypeValidator;
use Symfony\Contracts\Translation\TranslatorInterface;

class AdminAttributeDefinitionManager
{
private AdminAttributeDefinitionRepository $definitionRepository;
private AttributeTypeValidator $attributeTypeValidator;
private TranslatorInterface $translator;

public function __construct(
AdminAttributeDefinitionRepository $definitionRepository,
AttributeTypeValidator $attributeTypeValidator
AttributeTypeValidator $attributeTypeValidator,
TranslatorInterface $translator
) {
$this->definitionRepository = $definitionRepository;
$this->attributeTypeValidator = $attributeTypeValidator;
$this->translator = $translator;
}

public function create(AdminAttributeDefinitionDto $attributeDefinitionDto): AdminAttributeDefinition
{
$existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name);
if ($existingAttribute) {
throw new AttributeDefinitionCreationException('Attribute definition already exists', 409);
throw new AttributeDefinitionCreationException(
$this->translator->trans('Attribute definition already exists.'),
409
);
}
$this->attributeTypeValidator->validate($attributeDefinitionDto->type);

Expand Down
3 changes: 1 addition & 2 deletions src/Domain/Identity/Service/PasswordManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace PhpList\Core\Domain\Identity\Service;

use DateTime;
use PhpList\Core\Domain\Common\I18n\Messages;
use PhpList\Core\Domain\Identity\Model\AdminPasswordRequest;
use PhpList\Core\Domain\Identity\Model\Administrator;
use PhpList\Core\Domain\Identity\Repository\AdminPasswordRequestRepository;
Expand Down Expand Up @@ -52,7 +51,7 @@ public function generatePasswordResetToken(string $email): string
{
$administrator = $this->administratorRepository->findOneBy(['email' => $email]);
if ($administrator === null) {
$message = $this->translator->trans(Messages::IDENTITY_ADMIN_NOT_FOUND);
$message = $this->translator->trans('Administrator not found');
throw new NotFoundHttpException($message, null, 1500567100);
}

Expand Down
9 changes: 4 additions & 5 deletions src/Domain/Identity/Service/SessionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace PhpList\Core\Domain\Identity\Service;

use PhpList\Core\Domain\Common\I18n\Messages;
use Symfony\Contracts\Translation\TranslatorInterface;
use PhpList\Core\Domain\Configuration\Service\Manager\EventLogManager;
use PhpList\Core\Domain\Identity\Model\AdministratorToken;
Expand Down Expand Up @@ -35,16 +34,16 @@ public function createSession(string $loginName, string $password): Administrato
{
$administrator = $this->administratorRepository->findOneByLoginCredentials($loginName, $password);
if ($administrator === null) {
$entry = $this->translator->trans(Messages::AUTH_LOGIN_FAILED, ['login' => $loginName]);
$entry = $this->translator->trans("Failed admin login attempt for '%login%'", ['login' => $loginName]);
$this->eventLogManager->log('login', $entry);
$message = $this->translator->trans(Messages::AUTH_NOT_AUTHORIZED);
$message = $this->translator->trans('Not authorized');
throw new UnauthorizedHttpException('', $message, null, 1500567098);
}

if ($administrator->isDisabled()) {
$entry = $this->translator->trans(Messages::AUTH_LOGIN_DISABLED, ['login' => $loginName]);
$entry = $this->translator->trans("Login attempt for disabled admin '%login%'", ['login' => $loginName]);
$this->eventLogManager->log('login', $entry);
$message = $this->translator->trans(Messages::AUTH_NOT_AUTHORIZED);
$message = $this->translator->trans('Not authorized');
throw new UnauthorizedHttpException('', $message, null, 1500567099);
}

Expand Down
17 changes: 10 additions & 7 deletions src/Domain/Messaging/Command/ProcessBouncesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,11 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Contracts\Translation\TranslatorInterface;

#[AsCommand(name: 'phplist:bounces:process', description: 'Process bounce mailbox')]
class ProcessBouncesCommand extends Command
{
private const IMAP_NOT_AVAILABLE = 'PHP IMAP extension not available. Falling back to Webklex IMAP.';
private const FORCE_LOCK_FAILED = 'Could not apply force lock. Aborting.';
private const ALREADY_LOCKED = 'Another bounce processing is already running. Aborting.';

protected function configure(): void
{
$this
Expand All @@ -48,6 +45,7 @@ public function __construct(
private readonly AdvancedBounceRulesProcessor $advancedRulesProcessor,
private readonly UnidentifiedBounceReprocessor $unidentifiedReprocessor,
private readonly ConsecutiveBounceHandler $consecutiveBounceHandler,
private readonly TranslatorInterface $translator,
) {
parent::__construct();
}
Expand All @@ -57,14 +55,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$inputOutput = new SymfonyStyle($input, $output);

if (!function_exists('imap_open')) {
$inputOutput->note(self::IMAP_NOT_AVAILABLE);
$inputOutput->note($this->translator->trans(
'PHP IMAP extension not available. Falling back to Webklex IMAP.'
));
}

$force = (bool)$input->getOption('force');
$lock = $this->lockService->acquirePageLock('bounce_processor', $force);

if (($lock ?? 0) === 0) {
$inputOutput->warning($force ? self::FORCE_LOCK_FAILED : self::ALREADY_LOCKED);
$forceLockFailed = $this->translator->trans('Could not apply force lock. Aborting.');
$lockFailed = $this->translator->trans('Another bounce processing is already running. Aborting.');

$inputOutput->warning($force ? $forceLockFailed : $lockFailed);

return $force ? Command::FAILURE : Command::SUCCESS;
}
Expand All @@ -88,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->consecutiveBounceHandler->handle($inputOutput);

$this->logger->info('Bounce processing completed', ['downloadReport' => $downloadReport]);
$inputOutput->success('Bounce processing completed.');
$inputOutput->success($this->translator->trans('Bounce processing completed.'));

return Command::SUCCESS;
} catch (Exception $e) {
Expand Down
Loading
Loading