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?
Related
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
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.
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?
I have the following code in my AppServiceprovider::boot method, whcih ensures I get an email whenever anything is logged with a warning or greater severity level.
$message = \Swift_Message::newInstance( 'An Error has Occurred in XXX' )
->setTo( env('ERROR_EMAIL_TO') )
->setFrom( env('ERROR_EMAIL_FROM') )
->setReplyTo( env('ERROR_EMAIL_REPLY_TO') )
->setContentType( 'text/html' );
$swiftMailer = \Mail::getSwiftMailer();
$handler = new SwiftMailerHandler( $swiftMailer, $message, Logger::WARNING );
$handler->setFormatter( new HtmlFormatter() );
\Log::getMonolog()->pushHandler( $handler );
But while this works, I can't help but feel that it's in the wrong place.
Where would you add this code to a Laravel web app?
How about using middleware? I've written some middleware to log all requests, and responses in the past for APIs that could just as easily dispatch e-mails to inform a user of errors (this was pretty much the use case of why I set it up).
Using the terminate() method in your middleware class, will allow you to perform logic after a response has been sent to the user - so your e-mails shouldn't slow down the experience for the end user.
namespace App\Http\Middleware;
use Closure;
class LogRequestAndResponseMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
public function terminate($request, $response)
{
// Send out an e-mail to you here
}
I think this could be also allow you to a good time to refactor the code, which will help move the logic outside of the middleware and into its own area of responsibility.
In this instance, I'm thinking that I currently want to be informed via e-mail, but I may at some point in the future want to send an event via a Websocket instead.
Therefore, I'd wrap up the logic using a contract and implement it accordingly:
interface ErrorNotificationContract
{
public function inform($user, $message)
}
class EmailErrorNotification implements ErrorNotificationContract
{
protected $mail;
public function __construct(Mail $mail)
{
$this->mail = $mail;
}
public function inform($user, $message)
{
// Your send e-mail logic.
}
}
You can then register this using a service provider. A side effect is that you get the added benefits of:
Dependency injection in the EmailErrorNotification (better testability)
Better decoupled code
Implementations that can be changed very easily - just create a new class that implements the ErrorNotificationContract
In your middleware you could then do:
public function terminate($request, $response)
{
// ...
$errorNotifier->inform('youremail#domain.com', 'something bad happened');
}
My system sends a couple of important emails. What is the best way to unit test that?
I see you can put it in pretend mode and it goes in the log. Is there something to check that?
There are two options.
Option 1 - Mock the mail facade to test the mail is being sent. Something like this would work:
$mock = Mockery::mock('Swift_Mailer');
$this->app['mailer']->setSwiftMailer($mock);
$mock->shouldReceive('send')->once()
->andReturnUsing(function($msg) {
$this->assertEquals('My subject', $msg->getSubject());
$this->assertEquals('foo#bar.com', $msg->getTo());
$this->assertContains('Some string', $msg->getBody());
});
Option 2 is much easier - it is to test the actual SMTP using MailCatcher.me. Basically you can send SMTP emails, and 'test' the email that is actually sent. Laracasts has a great lesson on how to use it as part of your Laravel testing here.
"Option 1" from "#The Shift Exchange" is not working in Laravel 5.1, so here is modified version using Proxied Partial Mock:
$mock = \Mockery::mock($this->app['mailer']->getSwiftMailer());
$this->app['mailer']->setSwiftMailer($mock);
$mock
->shouldReceive('send')
->withArgs([\Mockery::on(function($message)
{
$this->assertEquals('My subject', $message->getSubject());
$this->assertSame(['foo#bar.com' => null], $message->getTo());
$this->assertContains('Some string', $message->getBody());
return true;
}), \Mockery::any()])
->once();
For Laravel 5.4 check Mail::fake():
https://laravel.com/docs/5.4/mocking#mail-fake
If you just don't want the e-mails be really send, you can turn off them using the "Mail::pretend(true)"
class TestCase extends Illuminate\Foundation\Testing\TestCase {
private function prepareForTests() {
// e-mail will look like will be send but it is just pretending
Mail::pretend(true);
// if you want to test the routes
Route::enableFilters();
}
}
class MyTest extends TestCase {
public function testEmail() {
// be happy
}
}
If any one is using docker as there development environment I end up solving this by:
Setup
.env
...
MAIL_FROM = noreply#example.com
MAIL_DRIVER = smtp
MAIL_HOST = mail
EMAIL_PORT = 1025
MAIL_URL_PORT = 1080
MAIL_USERNAME = null
MAIL_PASSWORD = null
MAIL_ENCRYPTION = null
config/mail.php
# update ...
'port' => env('MAIL_PORT', 587),
# to ...
'port' => env('EMAIL_PORT', 587),
(I had a conflict with this environment variable for some reason)
Carrying on...
docker-compose.ymal
mail:
image: schickling/mailcatcher
ports:
- 1080:1080
app/Http/Controllers/SomeController.php
use App\Mail\SomeMail;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
class SomeController extends BaseController
{
...
public function getSomething(Request $request)
{
...
Mail::to('someone#example.com')->send(new SomeMail('Body of the email'));
...
}
app/Mail/SomeMail.php
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class SomeMail extends Mailable
{
use Queueable, SerializesModels;
public $body;
public function __construct($body = 'Default message')
{
$this->body = $body;
}
public function build()
{
return $this
->from(ENV('MAIL_FROM'))
->subject('Some Subject')
->view('mail.someMail');
}
}
resources/views/mail/SomeMail.blade.php
<h1>{{ $body }}</h1>
Testing
tests\Feature\EmailTest.php
use Tests\TestCase;
use Illuminate\Http\Request;
use App\Http\Controllers\SomeController;
class EmailTest extends TestCase
{
privete $someController;
private $requestMock;
public function setUp()
{
$this->someController = new SomeController();
$this->requestMock = \Mockery::mock(Request::class);
}
public function testEmailGetsSentSuccess()
{
$this->deleteAllEmailMessages();
$emails = app()->make('swift.transport')->driver()->messages();
$this->assertEmpty($emails);
$response = $this->someController->getSomething($this->requestMock);
$emails = app()->make('swift.transport')->driver()->messages();
$this->assertNotEmpty($emails);
$this->assertContains('Some Subject', $emails[0]->getSubject());
$this->assertEquals('someone#example.com', array_keys($emails[0]->getTo())[0]);
}
...
private function deleteAllEmailMessages()
{
$mailcatcher = new Client(['base_uri' => config('mailtester.url')]);
$mailcatcher->delete('/messages');
}
}
(This has been copied and edited from my own code so might not work first time)
(source: https://stackoverflow.com/a/52177526/563247)
I think that inspecting the log is not the good way to go.
You may want to take a look at how you can mock the Mail facade and check that it receives a call with some parameters.
if you are using Notifcations in laravel you can do that like below
Notification::fake();
$this->post(...);
$user = User::first();
Notification::assertSentTo([$user], VerifyEmail::class);
https://laravel.com/docs/7.x/mocking#notification-fake
If you want to test everything around the email, use
Mail::fake()
But if you want to test your Illuminate\Mail\Mailable and the blade, then follow this example. Say, you want to test a Reminder email about some payment, where the email text should have product called 'valorant' and some price in 'USD'.
public function test_PaymentReminder(): void
{
/* #var $payment SalePayment */
$payment = factory(SalePayment::class)->create();
auth()->logout();
$paymentReminder = new PaymentReminder($payment);
$html = $paymentReminder->render();
$this->assertTrue(strpos($html, 'valorant') !== false);
$this->assertTrue(strpos($html, 'USD') !== false);
}
The important part here is ->render() - that is how you make Illuminate\Mail\Mailable to run build() function and process the blade.
Another importan thing is auth()->logout(); - because normally emails being processed in a queue that run in a background environment. This environment has no user and has no request with no URL and no IP...
So you must be sure that you are rendering the email in your unit test in a similar environment as in production.