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;
}
}
Related
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'm trying to join a presence channel (Public channels work well), but I can't get this to work:
Vue code:
mounted(){
Echo.join('game.' + "0").here((users) => {
alert("In the channel!");
})
.joining((user) => {
console.log("Someone entered");
})
.leaving((user) => {
console.log(user.name);
})
.listen('GameEvent', (e) => {
console.log("Hey")
});
Echo.channel('NewSentence')
.listen('NewSentence',(sentence)=>{
alert("HOLA");
});
}
I'm trying to join the channel "game.0". As I'm using Laravel Passport I need to authenticate myself with a token, and that is working. Sending the auth request for Laravel Echo returns a key, but the JavaScript events are not triggering .here(), .listening() ....
BroadcastService provider boot function:
public function boot() {
Broadcast::routes(["middleware" => "auth:api"]);
require base_path('routes/channels.php');
}
channels.php
Broadcast::channel('game.0', function ($user,$id) {
return ['id' => $user->id];
});
The auth route:
Route::post('/broadcasting/auth', function(Request $request){
$pusher = new Pusher\Pusher(
env('PUSHER_APP_KEY'),
env('PUSHER_APP_SECRET'),
env('PUSHER_APP_ID'),
array(
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => false,
'host' => '127.0.0.1',
'port' => 6001,
'scheme' => 'http',
)
);
return $pusher->socket_auth($request->request->get('channel_name'),$request->request->get('socket_id'));
});
Do I need to do something extra to make it work? This is the auth request:
EDIT:
GameEvent event:
class GameEvent implements ShouldBroadcastNow {
use Dispatchable, InteractsWithSockets, SerializesModels;
public $gameEvent;
public $gameId;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct($gameEvent, $gameId) {
//
$this->gameEvent = $gameEvent;
$this->gameId = $gameId;
}
/**
* Get the channels the event should broadcast on.
*
* #return \Illuminate\Broadcasting\PresenceChannel|array
*/
public function broadcastOn() {
//return new PrivateChannel('channel-name');
return new PresenceChannel('game.0');
}
public function broadcastWith() {
return $this->gameEvent;
}
}
EDIT:
I've hardcoded the names: 'game.0' is now hardcoded in the routes/channels.php route, in the Echo connection and in the GameEvent. I also removed broadcastAs(). After entering the laravel-websockets debugging dashboard I found that the channel I want to subscribe doesn't even appear. It looks like it won't start a connection, but I can't figure out what it going on.
I hardcoded the
The problem here seems to be that the Echo is not listening on proper channel. First of all the Echo.join is using channel game.0 in which 0 is a user's id, and i don't think that there is actually a user with id 0. Secondly, you are broadcasting as
GameEvent
and Echo is connecting to channel named game.{id} I suggest that you either remove the broadcastAs() function from your event file or listen on GameEvent. Also use the websocket dashboard for testing this. The dashboard will be available at
/laravel-websockets
route automatically, which is available only for local environment so make sure that environment is local in your .env.
Use the debugging dashboard provided by laravel-websockets to send data to channels, first connect to your web socket within the dashboard then just enter the channel name, event name and data in JSON format and hit send on the dashboard.
Try finding out if that helps with resolving your problem.
I also recommend thoroughly reading laravel's official documentation on broadcasting as well as laravel-websockets debugging dashboard guide.
Also update what you got in result to this question.
i am working on facebook messenger bot. I am using Botman (botman.io) without Laravel or botman studio. Version of PHP is 7.4.
Simple hears and reply method works fine, but conversation replying method does not working.
If I try type hi|hello or some greetings, chatbot answer me "Hello! What is your firstname?", then I write my name and chatbot does not returns any text :-/
Can you help me where is a bug?
There is a conversation class:
namespace LiborMatejka\Conversations;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Incoming\Answer;
class OnboardingConversation extends Conversation {
protected $firstname;
protected $email;
function askFirstname() {
$this->ask('Hello! What is your firstname?', function (Answer $answer) {
// Save result
$this->firstname = $answer->getText();
$this->say('Nice to meet you ' . $this->firstname);
$this->askEmail();
});
}
public function askEmail() {
$this->ask('One more thing - what is your email?', function (Answer $answer) {
// Save result
$this->email = $answer->getText();
$this->say('Great - that is all we need, ' . $this->firstname);
});
//$this->bot->typesAndWaits(2);
}
public function run() {
// This will be called immediately
$this->askFirstname();
}
}
and there is config:
require_once "vendor/autoload.php";
require_once "class/onboardingConversation.php";
use BotMan\BotMan\BotMan;
use BotMan\BotMan\BotManFactory;
use BotMan\BotMan\Drivers\DriverManager;
use BotMan\Drivers\Facebook\FacebookDriver;
use LiborMatejka\Conversations\OnboardingConversation;
$config = [
// Your driver-specific configuration
'facebook' => [
'token' => 'my_token',
'app_secret' => 'my_secret_app_code',
'verification' => 'verification_code',
],
'botman' => [
'conversation_cache_time' => 0,
],
];
// Load the driver(s) you want to use
DriverManager::loadDriver(\BotMan\Drivers\Facebook\FacebookDriver::class);
// Create an instance
$botman = BotManFactory::create($config);
$botman->hears('ahoj|hi|hello|cau|cus|zdar|zdarec|cago|hey|ciao', function (BotMan $bot) {
$bot->startConversation(new OnboardingConversation);
});
// Start listening
$botman->listen();
Add symfony/cache to your project using composer
composer require symfony/cache
Put following at top of index.php (or other) file where you're setting up BotMan
use BotMan\BotMan\Cache\SymfonyCache;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
Then create your BotMan using the following:
$adapter = new FilesystemAdapter();
$botman = BotManFactory::create($config, new SymfonyCache($adapter));
Then use your $botman variable accordingly, like example below:
$botman->hears('Hi', function (BotMan $bot) {
$bot->typesAndWaits(2);
$bot->reply('Hello and welcome');
$bot->typesAndWaits(2);
$bot->ask('Anything I can do for you today?',function($answer, $bot){
$bot->say("Oh, really! You said '{$answer->getText()}'... is that right?");
});
});
I would rather to use auto-wiring to inject the SymfonyCache anywhere you create the Botman instance, without creating the adapter and cache again and again.
Step 1: config the cache in cache.yaml
framework:
cache:
# botman: cache adapter
app: cache.adapter.filesystem
Step 2: autowiring in services.yaml
services:
BotMan\BotMan\Cache\SymfonyCache:
arguments:
$adapter: '#cache.app'
Step 3: Inject the SymfonyCache where you need, for example in ChatController::message()
public function message(SymfonyCache $symfonyCache): Response
{
....
$botman = BotManFactory::create([], $symfonyCache);
....
$botman->hears(
'survey',
function (BotMan $bot) {
$bot->startConversation(new OnBoardingConversation());
}
);
}
To create the OnBoardingConversation, just follow the documentation on create a conversation in botman
I'm using Botman 2.0 and Codeigniter 3.1.6
My owner server with HTTPS & FB Webhook setup successfully.
How do I have to setup my project with codeigniter? Because, I set the FB Webhook setup successfully, but, when I type "hi" on messenger this not reply.
Could you give me some advice for that my chat bot works correctly.
Note 1: I had test with PHP (Without framework) and This works correctly.
Note 2: I had test with NodeJS and This works correctly.
use BotMan\BotMan\BotMan;
use BotMan\BotMan\BotManFactory;
use BotMan\BotMan\Drivers\DriverManager;
class Facebook_messenger extends CI_Controller {
function __construct()
{
parent::__construct();
}
public function bot(){
$config = [
// Your driver-specific configuration
'facebook' => [
'token' => 'EAAZA1UK2sl8UBAIueLY2ZCHraasS3E52AS37wUVypMLQatW5taE71LByyl3nWNau8uTKYs1aw11lQXpDOfPrrQE8BLWf6PZBkwuQIdzea7lMeZCc4jCiZCqhKZABKnc2V8FNabVbHF9J6opkb6MCsBAxnqO0kDsgeoYV88gNOIJuTZAr9T7pzoBAZC',
//'app_secret' => 'f1a032fb00484a59c0322617b7623745a',
'verification'=>'xxxxapp',
]
];
// Load the driver(s) you want to use
DriverManager::loadDriver(\BotMan\Drivers\Facebook\FacebookDriver::class);
// Create an instance
$botman = BotManFactory::create($config);
// Give the bot something to listen for.
$botman->hears('hi', function (BotMan $bot) {
$bot->reply('Hello');
});
$botman->hears('delayed', function (BotMan $bot) {
$bot->reply('Field will remain in yellow (delayed)unless a game is scheduled on it.');
});
$botman->hears('canceled', function (BotMan $bot) {
$bot->reply('Games cancelled due to poor field conditions.');
});
// Start listening
$botman->listen();
}
}
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.