Skip to content

Commit d536926

Browse files
committed
ISSUE-345: async emails
1 parent 6e8e0a8 commit d536926

File tree

10 files changed

+583
-31
lines changed

10 files changed

+583
-31
lines changed

config/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
imports:
22
- { resource: services.yml }
33
- { resource: doctrine.yml }
4+
- { resource: packages/*.yml }
45

56
# Put parameters here that don't need to change on each machine where the app is deployed
67
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration

config/packages/messenger.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# This file is the Symfony Messenger configuration for asynchronous processing
2+
framework:
3+
messenger:
4+
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
5+
# failure_transport: failed
6+
7+
transports:
8+
# https://symfony.com/doc/current/messenger.html#transport-configuration
9+
async_email:
10+
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
11+
options:
12+
auto_setup: true
13+
use_notify: true
14+
check_delayed_interval: 60000
15+
retry_strategy:
16+
max_retries: 3
17+
# milliseconds delay
18+
delay: 1000
19+
multiplier: 2
20+
max_delay: 0
21+
22+
# failed: 'doctrine://default?queue_name=failed'
23+
24+
routing:
25+
# Route your messages to the transports
26+
'PhpList\Core\Domain\Messaging\Message\AsyncEmailMessage': async_email

config/services.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ services:
3737
public: true
3838
tags: [controller.service_arguments]
3939

40+
# Register message handlers for Symfony Messenger
41+
PhpList\Core\Domain\Messaging\MessageHandler\:
42+
resource: '../src/Domain/Messaging/MessageHandler'
43+
tags: ['messenger.message_handler']
44+
4045
doctrine.orm.metadata.annotation_reader:
4146
alias: doctrine.annotation_reader
4247

docs/AsyncEmailSending.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Asynchronous Email Sending in phpList
2+
3+
This document explains how to use the asynchronous email sending functionality in phpList.
4+
5+
## Overview
6+
7+
phpList now supports sending emails asynchronously using Symfony Messenger. This means that when you send an email, it is queued for delivery rather than being sent immediately. This has several benefits:
8+
9+
1. **Improved Performance**: Your application doesn't have to wait for the email to be sent before continuing execution
10+
2. **Better Reliability**: If the email server is temporarily unavailable, the message remains in the queue and will be retried automatically
11+
3. **Scalability**: You can process the email queue separately from your main application, allowing for better resource management
12+
13+
## Configuration
14+
15+
The asynchronous email functionality is configured in `config/packages/messenger.yaml` and uses the `MESSENGER_TRANSPORT_DSN` environment variable defined in `config/parameters.yml`.
16+
17+
By default, the system uses Doctrine (database) as the transport for queued messages:
18+
19+
```yaml
20+
env(MESSENGER_TRANSPORT_DSN): 'doctrine://default?auto_setup=true'
21+
```
22+
23+
You can change this to use other transports supported by Symfony Messenger, such as:
24+
25+
- **AMQP (RabbitMQ)**: `amqp://guest:guest@localhost:5672/%2f/messages`
26+
- **Redis**: `redis://localhost:6379/messages`
27+
- **In-Memory (for testing)**: `in-memory://`
28+
29+
## Using Asynchronous Email Sending
30+
31+
### Basic Usage
32+
33+
The `EmailService` class now sends emails asynchronously by default:
34+
35+
```php
36+
// This will queue the email for sending
37+
$emailService->sendEmail($email);
38+
```
39+
40+
### Synchronous Sending
41+
42+
If you need to send an email immediately (synchronously), you can use the `sendEmailSync` method:
43+
44+
```php
45+
// This will send the email immediately
46+
$emailService->sendEmailSync($email);
47+
```
48+
49+
### Bulk Emails
50+
51+
For sending to multiple recipients:
52+
53+
```php
54+
// Asynchronous (queued)
55+
$emailService->sendBulkEmail($recipients, $subject, $text, $html);
56+
57+
// Synchronous (immediate)
58+
$emailService->sendBulkEmailSync($recipients, $subject, $text, $html);
59+
```
60+
61+
## Testing Email Sending
62+
63+
You can test the email functionality using the built-in command:
64+
65+
```bash
66+
# Queue an email for asynchronous sending
67+
bin/console app:send-test-email [email protected]
68+
69+
# Send an email synchronously (immediately)
70+
bin/console app:send-test-email [email protected] --sync
71+
```
72+
73+
## Processing the Email Queue
74+
75+
To process queued emails, you need to run the Symfony Messenger worker:
76+
77+
```bash
78+
bin/console messenger:consume async_email
79+
```
80+
81+
For production environments, it's recommended to run this command as a background service or using a process manager like Supervisor.
82+
83+
## Monitoring
84+
85+
You can monitor the queue status using the following commands:
86+
87+
```bash
88+
# View the number of messages in the queue
89+
bin/console messenger:stats
90+
91+
# View failed messages
92+
bin/console messenger:failed:show
93+
```
94+
95+
## Troubleshooting
96+
97+
If emails are not being sent:
98+
99+
1. Make sure the messenger worker is running
100+
2. Check for failed messages using `bin/console messenger:failed:show`
101+
3. Verify your mailer configuration in `config/parameters.yml`
102+
4. Try sending an email synchronously to test the mailer configuration

src/Domain/Messaging/Command/SendTestEmailCommand.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
class SendTestEmailCommand extends Command
1717
{
18-
protected static $defaultName = 'app:send-test-email';
18+
protected static $defaultName = 'phplist:test-email';
1919
protected static $defaultDescription = 'Send a test email to verify email configuration';
2020

2121
private EmailService $emailService;
@@ -30,7 +30,8 @@ protected function configure(): void
3030
{
3131
$this
3232
->setDescription(self::$defaultDescription)
33-
->addArgument('recipient', InputArgument::OPTIONAL, 'Recipient email address');
33+
->addArgument('recipient', InputArgument::OPTIONAL, 'Recipient email address')
34+
->addOption('sync', null, InputArgument::OPTIONAL, 'Send email synchronously instead of queuing it', false);
3435
}
3536

3637
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -49,17 +50,28 @@ protected function execute(InputInterface $input, OutputInterface $output): int
4950
}
5051

5152
try {
52-
$output->writeln('Sending test email to ' . $recipient);
53+
$syncMode = $input->getOption('sync');
54+
55+
if ($syncMode) {
56+
$output->writeln('Sending test email synchronously to ' . $recipient);
57+
} else {
58+
$output->writeln('Queuing test email for ' . $recipient);
59+
}
5360

5461
$email = (new Email())
5562
->from(new Address('[email protected]', 'Admin Team'))
5663
->to($recipient)
5764
->subject('Test Email from phpList')
5865
->text('This is a test email sent from phpList Email Service.')
5966
->html('<h1>Test</h1><p>This is a <strong>test email</strong> sent from phpList Email Service</p>');
60-
61-
$this->emailService->sendEmail($email);
62-
$output->writeln('Test email sent successfully!');
67+
68+
if ($syncMode) {
69+
$this->emailService->sendEmailSync($email);
70+
$output->writeln('Test email sent successfully!');
71+
} else {
72+
$this->emailService->sendEmail($email);
73+
$output->writeln('Test email queued successfully! It will be sent asynchronously.');
74+
}
6375

6476
return Command::SUCCESS;
6577
} catch (Exception $e) {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Messaging\Message;
6+
7+
use Symfony\Component\Mime\Email;
8+
9+
/**
10+
* Message class for asynchronous email processing
11+
*/
12+
class AsyncEmailMessage
13+
{
14+
private Email $email;
15+
private array $cc;
16+
private array $bcc;
17+
private array $replyTo;
18+
private array $attachments;
19+
20+
public function __construct(
21+
Email $email,
22+
array $cc = [],
23+
array $bcc = [],
24+
array $replyTo = [],
25+
array $attachments = []
26+
) {
27+
$this->email = $email;
28+
$this->cc = $cc;
29+
$this->bcc = $bcc;
30+
$this->replyTo = $replyTo;
31+
$this->attachments = $attachments;
32+
}
33+
34+
public function getEmail(): Email
35+
{
36+
return $this->email;
37+
}
38+
39+
public function getCc(): array
40+
{
41+
return $this->cc;
42+
}
43+
44+
public function getBcc(): array
45+
{
46+
return $this->bcc;
47+
}
48+
49+
public function getReplyTo(): array
50+
{
51+
return $this->replyTo;
52+
}
53+
54+
public function getAttachments(): array
55+
{
56+
return $this->attachments;
57+
}
58+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Messaging\MessageHandler;
6+
7+
use PhpList\Core\Domain\Messaging\Message\AsyncEmailMessage;
8+
use PhpList\Core\Domain\Messaging\Service\EmailService;
9+
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
10+
11+
/**
12+
* Handler for processing asynchronous email messages
13+
*/
14+
#[AsMessageHandler]
15+
class AsyncEmailMessageHandler
16+
{
17+
private EmailService $emailService;
18+
19+
public function __construct(EmailService $emailService)
20+
{
21+
$this->emailService = $emailService;
22+
}
23+
24+
/**
25+
* Process an asynchronous email message by sending the email
26+
*/
27+
public function __invoke(AsyncEmailMessage $message): void
28+
{
29+
$this->emailService->sendEmailSync(
30+
$message->getEmail(),
31+
$message->getCc(),
32+
$message->getBcc(),
33+
$message->getReplyTo(),
34+
$message->getAttachments()
35+
);
36+
}
37+
}

0 commit comments

Comments
 (0)