SwiftMailer not sending mail in MessageHandler - php

I am using SwiftMailer in my Symfony 5 project to send emails.
I was using it in a controller to send a reset password e-mail, and everything was working.
I am now trying to use it in a MessageHandler, here is the code I am now using :
final class SendEmailMessageHandler implements MessageHandlerInterface
{
private $mailer;
public function __construct(\Swift_Mailer $mailer)
{
$this->mailer = $mailer;
}
public function __invoke(SendEmailMessage $message)
{
$mail = (new \Swift_Message())
->setFrom($message->getFrom())
->setTo($message->getTo())
->setBody($message->getBody(), $message->getContentType())
->setSubject($message->getSubject());
$response = $this->mailer->send($mail);
}
}
The response is ok, but the mail never reach my mailbox.
Here is how I am dispatching my SendEmailMessage :
class AskResetPassword extends AbstractController
{
use ResetPasswordControllerTrait;
private $resetPasswordHelper;
private $validator;
private $bus;
public function __construct(ResetPasswordHelperInterface $resetPasswordHelper, ValidatorInterface $validator, MessageBusInterface $bus)
{
$this->resetPasswordHelper = $resetPasswordHelper;
$this->validator = $validator;
$this->bus = $bus;
}
public function __invoke($data)
{
$emailConstraints = new Assert\Email();
$email = $data->getEmail();
if ($email) {
$errors = $this->validator->validate($email, $emailConstraints);
if (count($errors) === 0) {
return $this->processPasswordReset($email);
} else {
return new JsonResponse(['success' => false, 'error' => 'Invalid E-Mail format'], 404);
}
}
}
private function processPasswordReset($email)
{
$user = $this->getDoctrine()->getRepository(User::class)->findOneBy([
'email' => $email,
]);
$this->setCanCheckEmailInSession();
if (!$user) {
// Do not reveal whether a user account was found or not.
return new JsonResponse(['success' => true], 200);
}
try {
$resetToken = $this->resetPasswordHelper->generateResetToken($user);
} catch (ResetPasswordExceptionInterface $e) {
return new JsonResponse(['success' => false, 'error' => 'There was a problem handling your password reset request - ' . $e->getReason()]);
}
$message = new SendEmailMessage($email);
$message->setFrom('from.from#from.from');
$message->setBody(
$this->renderView('reset_password/email.html.twig', [
'resetToken' => $resetToken,
'tokenLifetime' => $this->resetPasswordHelper->getTokenLifetime()
])
);
$message->setSubject('Votre demande de changement de mot de passe');
$this->bus->dispatch($message);
return new JsonResponse(['success' => true], 200);
}
}
Here is my swiftmailer.yaml :
swiftmailer:
url: '%env(MAILER_URL)%'
spool: { type: 'memory' }
Can you help me ?

The answer is "DO NOT spool emails unless you want to process them later".
Check docs Spool Emails
A spooler is a queue mechanism which will process your message queue one by one. This was introduced when swift-mailer was rewritten and added back to symfony. In combination with Messenger Component which provides abstract interface MessageBusInterface, it would delegate to right backend service which can be smtp relay, push notification or any other type of RPC which may trigger actions on separate web services.
As symfony adds new capabilities to the message bus, this feature was added to utilize it for message queues & other services where transactional emails and notifications are processed separately.
To process your spool simply run :
APP_ENV=prod php bin/console swiftmailer:spool:send
In typical installation spooling is disabled, when you enable spooling in memory, it will wait till request is finished and kernel is about to exit. If you are using anything else to debug that terminates kernel halfway or there are other components & parts of application that keeps kernel in memory, events will not be triggered and mail will not be sent.
You can check whole documentation here : Sending Emails

I cant remember the exact reason why and at the time of posting this I'm struggling to find the answer but the swiftmailer type must be file instead of memory. This GitHub issue references this. You can also see how to change the type here.

Related

I have an error whe I try to reset my passord using ResetPasswordBundle from Symfony

I have an exception whe I try to reset password in my app. I use Symfony 4.4 and and installed "symfonycasts/reset-password-bundle": "^1.13", the controller is the next:
class ResetPasswordController extends AbstractController {
use ResetPasswordControllerTrait;
private $resetPasswordHelper;
private $entityManager;
public function __construct(ResetPasswordHelperInterface $resetPasswordHelper, EntityManagerInterface $entityManager)
{
$this->resetPasswordHelper = $resetPasswordHelper;
$this->entityManager = $entityManager;
}
/**
* Display & process form to request a password reset.
*
* #Route("", name="app_forgot_password_request")
*/
public function request(Request $request, MailerInterface $mailer, TranslatorInterface $translator): Response
{
$form = $this->createForm(ResetPasswordRequestFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return $this->processSendingPasswordResetEmail(
$form->get('email')->getData(),
$mailer,
$translator
);
}
return $this->render('reset_password/request.html.twig', [
'requestForm' => $form->createView(),
]);
}
private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer, TranslatorInterface $translator): RedirectResponse
{
$user = $this->entityManager->getRepository(User::class)->findOneBy([
'email' => $emailFormData,
]);
// Do not reveal whether a user account was found or not.
if (!$user) {
return $this->redirectToRoute('app_check_email');
}
try {
$resetToken = $this->resetPasswordHelper->generateResetToken($user);
} catch (ResetPasswordExceptionInterface $e) {
return new Response(json_encode(['message' => $e->getMessage()]), 500, ['Content-Type' => 'application/json']);
// If you want to tell the user why a reset email was not sent, uncomment
// the lines below and change the redirect to 'app_forgot_password_request'.
// Caution: This may reveal if a user is registered or not.
//
// $this->addFlash('reset_password_error', sprintf(
// '%s - %s',
// $translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE, [], 'ResetPasswordBundle'),
// $translator->trans($e->getReason(), [], 'ResetPasswordBundle')
// ));
return $this->redirectToRoute('app_check_email');
}
$email = (new TemplatedEmail())
->from(new Address('rgutierrez#greicodex.com', 'Yoke Integration Suite'))
->to($user->getEmail())
->subject('Your password reset request')
->htmlTemplate('reset_password/email.html.twig')
->context([
'resetToken' => $resetToken,
])
;
$mailer->send($email);
// Store the token object in session for retrieval in check-email route.
$this->setTokenObjectInSession($resetToken);
return $this->redirectToRoute('app_check_email');
}
}
I called the request function from app and the problem occured when the processSendingPasswordResetEmail function is executed in the line "$resetToken = $this->resetPasswordHelper->generateResetToken($user);" , the function is not executed and returns the following exception:
An exception occurred while executing a query: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'expires_at' in 'where clause'
I followed the installation guide to add reset password feature on the next page:
https://symfony.com/doc/4.4/security/reset_password.html
https://github.com/symfonycasts/reset-password-bundle
I did not add any function, in fact everything is created by the guide as indicated and I do not know why the error occurs, could someone help me solve the problem please.
If you execute php bin/console doc:sch:update --dump-sql, have you SQL updates ?
Regards,
Could you put ResetPasswordHelperInterface $resetPasswordHelper, EntityManagerInterface $entityManager directly in your processSendingPasswordResetEmail function via Dependency Injection and remove your constructor ? You can remove the variable declaration too.
This will fix the issue.
Regards

Laravel Schedule not sending email

It's my first time trying to implement Task Scheduling, I'm trying to send automatic E-mails at a certain time:
Before implementing my cron I first tested my email sending code manually in a normal class to see if there is no error, and there was no error, the email was sent successfully.
After that, I started implementing the Task Scheduling
Democron.php
protected $signature = 'demo:cron';
protected $description = 'Command description';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$tasks = Task::all();
$date = Carbon::now()->toDateTimeString();
foreach ($tasks as $task) {
if($task->completed_at != null){
$validad = $task->completed_at;
$receiver_id = User::findOrFail($task->user_id);
if($date > $validad){
$details = [
'task_id' =>$task->id,
'receiver_id' => $receiver_id
];
$subject = 'TeamWork - Você tem tarefas em atraso!';
$view = 'emails.project.delaydtask';
Mail::to($receiver_id->email)->send(new SendMail($details, $subject, $view));
Log::info('Email enviado com sucesso para '.$receiver_id->email);
}
}
}
}
Kernel.php
protected $commands = [
DemoCron::class,
];
protected function schedule(Schedule $schedule)
{
$schedule->command('demo:cron')
->twiceDaily(12, 15)
->timezone('Africa/Maputo');
}
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
I added to CRON JOBS on CPANEL
and set twiceDaily at 12 and 15
/usr/local/bin/php /.......myProjectPath/artisan schedule:run >> /dev/null 2>&1
I printed a LOG in my DemoCron.php to see if it really works
Result 1: when I select schedule once per minute it prints my LOG respecting all the conditions that are in my Democron.php , but it doesn't send the email.
Result 2: When I select a certain time (Twice per day or once a day) my LOG does not print anything and it does not send the email.
What am I doing wrong? Help me please!
UPDATE
my SendMail class that i use to send emails manually works perfectly,
but the scheduled emails are not going
class SendMail extends Mailable
{
use Queueable, SerializesModels;
public $details, $subject, $view;
public function __construct($details, $subject, $view)
{
$this->details = $details;
$this->subject = $subject;
$this->view = $view;
}
public function build()
{
return $this->subject($this->subject)
->view($this->view, ['details' => $this->details]);
}
}
After trying several times I found a workaround.
1- create a new controller
I created a new controller called MailController instead of using the Kernel.php and Democron.php classes that I generated through Laravel Scheduling
class MailController extends Controller
{
public function delayedtask(){
try {
$tasks = Task::all();
$date = Carbon::now()->toDateTimeString();
foreach ($tasks as $task) {
if($task->completed_at != null){
$validad = $task->completed_at;
$receiver_id = User::findOrFail($task->user_id);
if($date > $validad){
$details = [
'task_id' =>$task->id,
'receiver_id' => $receiver_id
];
$subject = 'TeamWork - Você tem tarefas em atraso!';
$view = 'emails.project.delaydtask';
Mail::to($receiver_id->email)->send(new SendMailQueue($details, $subject, $view));
Log::info('Email enviado com sucesso para '.$receiver_id->email);
}
}
}
return "Done!";
} catch (Exception $e) {
return "Something went wrong!";
}
}
}
2-add a new route
added a new route without Auth
Route::get('/delayedtask',[MailController::class, 'delayedtask']);
3-Added a cronjob on Cpanel
curl -s "https://myWebsiteURL/delayedtask">/dev/null 2>&1
First of all lets check all things:
Verify your mail configurations in your .env;
Verify in your email class if have implements ShouldQueue;
If you are implementing ShouldQueue, you must have to verify too your queue´s configuration in .env;
If is not implementing ShouldQueue, don´t miss time verifying queue´s config;
All right all things validated and still not sending email:
Add the Send mail in try catch and log the catch if something went wrong;
If don´t log nothing in try catch, try to create an command that just send a simple email;
If dosen´t work try to send an email by your mail in Cpanel, because this should be the problem;
Finally
In my cases using cPanel, I always create the croon task to all seconds like * * * * * and in the kernel of my laravel project I verify if some command must be executed with the laravel commands like ->twiceDaily(12, 15).
Try all things and if the error still, please update this thread!
I had the same problem,
i tried a new smtp email server
MAIL_HOST=pro.eu.turbo-smtp.com
MAIL_ENCRYPTION=ssl
instead of
MAIL_HOST=smtpauth.online.net
MAIL_ENCRYPTION=tls
I don't know if it's about the encryption or host features,
but it worked for me

Limit email deployment application wide on non-production environments

I am developing my first Laravel app and want to avoid accidentally deploying emails from my dev and local environments to real people, other than myself. My goal is to have a list of comma-separated email addresses in my .env file that the Laravel's Mailable checks against when APP_ENV is not "production".
# .env
APP_ENV=local
EMAIL_WHITELIST=test#example.com
I checked the send method in vendor\laravel\framework\src\Illuminate\Mail\Mailable.php and could modify the code there but have an eerie feeling that I am not supposed to touch that file.
# vendor\laravel\framework\src\Illuminate\Mail\Mailable.php
public function send($mailer)
{
$this->withLocale($this->locale, function () use ($mailer) {
Container::getInstance()->call([$this, 'build']);
$mailer = $mailer instanceof MailFactory
? $mailer->mailer($this->mailer)
: $mailer;
return $mailer->send($this->buildView(), $this->buildViewData(), function ($message) {
$this->buildFrom($message)
->buildRecipients($message)
->buildSubject($message)
->runCallbacks($message)
->buildAttachments($message);
});
});
}
Thanks!
One option is to create an Event Listener and register it with Laravel's MessageSending event. This allows you to perform all manner of actions on each outgoing message before it is sent.
App\Providers\EventServiceProvider
protected $listen = [
'Illuminate\Mail\Events\MessageSending' => [
'App\Listeners\PrepareMessageForSending'
],
];
App\Listeners\PrepareMessageForSending
class PrepareMessageForSending
{
public function handle(MessageSending $event)
{
if (env('APP_ENV') !== 'production') {
// for example, you could redirect everything to a mail trap account
$event->message->setTo('mailtrap#example.com');
}
return $event->message;
}
}

PHP SDK not sending errors to Sentry when invoked from IBM Cloud Functions

I am using Serverless framework to deploy my PHP code as IBM Cloud Function.
Here is the code from the action PHP file:
function main($args): array {
Sentry\init(['dsn' => 'SENTRY_DSN' ]);
try {
throw new \Exception('Some error')
} catch (\Throwable $exception) {
Sentry\captureException($exception);
}
}
And this is the serverless.yml file:
service: cloudfunc
provider:
name: openwhisk
runtime: php
package:
individually: true
exclude:
- "**"
include:
- "vendor/**"
functions:
test-sentry:
handler: actions/test-sentry.main
annotations:
raw-http: true
events:
- http:
path: /test-sentry
method: post
resp: http
package:
include:
- actions/test-sentry.php
plugins:
- serverless-openwhisk
When I test the action handler from my local environment(NGINX/PHP Docker containers) the errors are being sent to Sentry.
But when I try to invoke the action from IBM Cloud nothing appears in the Sentry console.
Edit:
After some time trying to investigate the source of the problem I saw that its related with the async nature of sending the http request to Sentry(I have other libraries that make HTTP/TCP connections to Loggly, RabbitMQ, MySQL and they all work as expected):
vendor/sentry/sentry/src/Transport/HttpTransport.php
in the send method where the actual http request is being sent:
public function send(Event $event): ?string
{
$request = $this->requestFactory->createRequest(
'POST',
sprintf('/api/%d/store/', $this->config->getProjectId()),
['Content-Type' => 'application/json'],
JSON::encode($event)
);
$promise = $this->httpClient->sendAsyncRequest($request);
//The promise state here is "pending"
//This line here is being logged in the stdout of the invoked action
var_dump($promise->getState());
// This function is defined in-line so it doesn't show up for type-hinting
$cleanupPromiseCallback = function ($responseOrException) use ($promise) {
//The promise state here is "fulfilled"
//This line here is never logged in the stdout of the invoked action
//Like the execution never happens here
var_dump($promise->getState());
$index = array_search($promise, $this->pendingRequests, true);
if (false !== $index) {
unset($this->pendingRequests[$index]);
}
return $responseOrException;
};
$promise->then($cleanupPromiseCallback, $cleanupPromiseCallback);
$this->pendingRequests[] = $promise;
return $event->getId();
}
The requests that are registered asynchronously are sent in the destructor of the HttpTransport instance or when PHP shuts down as a shutdown function is registered. In OpenWhisk we never shut down as we run in a never-ending loop until the Docker container is killed.
Update: You can now call $client-flush() and don't need to worry about reflection.
main() now looks like this:
function main($args): array {
Sentry\init(['dsn' => 'SENTRY_DSN' ]);
try {
throw new \Exception('Some error')
} catch (\Throwable $exception) {
Sentry\captureException($exception);
}
$client = Sentry\State\Hub::getCurrent()->getClient();
$client->flush();
return [
'body' => ['result' => 'ok']
];
}
Original explanation:
As a result, to make this work, we need to call the destructor of the $transport property of the Hub's $client. Unfortunately, this private, so the easiest way to do this is to use reflection to make it visible and then call it:
$client = Sentry\State\Hub::getCurrent()->getClient();
$property = (new ReflectionObject($client))->getProperty('transport');
$property->setAccessible(true);
$transport = $property->getValue($client);
$transport->__destruct();
This will make the $transport property visible so that we can retrieve it and call its destructor which will in turn call cleanupPendingRequests() that will then send the requests to sentry.io.
The main() therefore looks like this:
function main($args): array {
Sentry\init(['dsn' => 'SENTRY_DSN' ]);
try {
throw new \Exception('Some error')
} catch (\Throwable $exception) {
Sentry\captureException($exception);
}
$client = Sentry\State\Hub::getCurrent()->getClient();
$property = (new ReflectionObject($client))->getProperty('transport');
$property->setAccessible(true);
$transport = $property->getValue($client);
$transport->__destruct();
return [
'body' => ['result' => 'ok']
];
}
Incidentally, I wonder if this Sentry SDK works with Swoole?
Function runtimes are "paused" between requests by the platform. This means any background processes will be blocked if they aren't finished when the function returns.
It looks like the asynchronous HTTP request doesn't get a chance to complete before the runtime pauses.
You will need to find some way to block returning from the function until that request is completed. If the Sentry SDK has some callback handler or other mechanism to be notified when messages have been sent, you could use that?

Swiftmailer in Messenger __invoke method doesn't send mail

I'm currently having an issue with the use of \Swift_Mailer into a Symfony MessageHandlerInterface. In the __invoke method of this class I would like to send an email with Swift_Mailer.
/**
* #param MailMessage $mailMessage
*/
public function __invoke(MailMessage $mailMessage)
{
try {
$message = (new \Swift_Message('You Got Mail!'))
->setFrom('no-reply#example.be' , 'No reply')
->setTo('development#example.be')
->setBody(
'hello world',
'text/plain'
)
;
$successfulRecipientsCount = $this->mailer->send($message, $failedRecipients);
if ($successfulRecipientsCount < 1 || count($failedRecipients) > 0) {
throw new \Exception('error sending mail');
}
} catch (\Exception $e) {
$this->logger->error( $e->getMessage());
}
}
But when I want to consume this messages, the mailer never sends the mail to the destination.
When I use this snippet into a controller it just works. So there is no problem with the mail parameters in my config.
I know that the controller of Symfony dispatch the kernel.terminate event and the Swift_Mailer makes use of this event to sends the emails.
How can I dispatch the kernel.terminate event in the magic method __invoke from the MessengerHandlerInterface from Symfony?

Categories