Laravel 9: Malformed characters for emails in laravel.log - php

If I adjust in my Laravel 9 project (PHP 8), the .env - file to MAIL_MAILER=log.
The mail is saved in the laravel.log file, but the problem is, that some characters are malformed. For example the mail-<head> looks like this:
<head>
<meta charset=3D"utf-8">
<meta name=3D="viewport" content=3D"width=3Ddevice-width, initial-scale=3D1.0">
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3DUTF-8">
<meta name=3D"color-scheme" content=3D"light">
<meta name=3D"supported-color-schemes" content=3D"light">
</head>
The malformed characters also occur for äöü and other UTF-8 characters (e.g. ä resolves to =C3=A4).
If I'm using MAIL_MAILER=smtp the mail isn't malformed at all.
This makes local email debugging hard.
Anyway, this probably causes another problem on production. I'm using the package (https://github.com/shvetsgroup/laravel-email-database-log) to save all sent mails of Laravel in the database. Here the malformed characters are also saved in the database.
I'm sending mails like this:
\Illuminate\Support\Facades\Mail::to('mail#mail.de')->queue(new \App\Mail\ContactConfirmation(
$name,
$message
));
class ContactConfirmation extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public string $name,
public string $text
)
{
//
}
public function build()
{
return $this->markdown('mails.contact_confirmation')
->subject('Your message')
->with([
'name' => $this->name,
'text' => $this->text
]);
}
}
This problem looks similar to https://github.com/laravel/framework/issues/32954, but the mails sent by SMTP have no problem and Laravel 9 uses the Symfony Mailer. There is also a similar question (Why does Laravel replace a tab by a "=09" string when sending mails?) from 2014, but here is SwiftMailer used.
Is there some way to fix the malformed characters on production and local? And if not, are there alternatives to save the mail without the package and malformed characters into the database?

A work around to this it could be to create a new channel for logging. On laravel documentation https://laravel.com/docs/9.x/logging#creating-custom-channels-via-factories it is explained it how to do it.
The idea is that when a log for mailer is used for sending emails, if the message is quoted-printable, then it will be decoded it back to '8bit' string.
To achieve this a new Logger is created that extends from Illuminate\Log\Logger and override the debug method with the code that convert to '8bit'string.
First, create a class that extends from Illuminate\Log\Logger. For example App\Mail\Logging\CustomLogger.php This contains the override to debug method.
namespace App\Mail\Logging;
use Illuminate\Log\Logger;
class CustomLogger extends Logger
{
public function debug($message, array $context = []): void
{
$message = str_contains($message, "=3D") ? quoted_printable_decode($message) : $message;
$this->writeLog(__FUNCTION__, $message, $context);
}
}
Then create the logger class that will resolve the customLogger previously. Example app\Logging\CreateCustomMailLogger.php
namespace App\Logging;
use App\Mail\Logging\CustomLogger;
class CreateCustomMailLogger
{
public function __invoke(array $config)
{
/** #var \Illuminate\Log\Logger $log **/
$log = resolve('log');
return new CustomLogger($log->getLogger(), $log->getEventDispatcher());
}
}
In config\logging.php file add a new channel using the CreateCustomMailLogger created before.
'maillog' => [
'driver' => 'custom',
'via' => \App\Logging\CreateCustomMailLogger::class
],
In the .env file set MAIL_LOG_CHANNEL to maillog, the channel in logging.php created before
MAIL_MAILER=log
MAIL_LOG_CHANNEL=maillog
This way when you set mailer to log, then this channel will be used and it will be logged as '8bit' string and not quoted-printable.
For futher details, check this reply from the same issue.
https://github.com/laravel/framework/issues/32954#issuecomment-1335488478
Hope this help!

Related

Laravel mail queue subject not being applied

I always have lots of problems with Mail::queue and this time the subject is not being applied properly.
This is my class:
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PlanExpiringOrExpired extends Mailable
{
use Queueable, SerializesModels;
private $payment = null;
public function __construct($payment)
{
$this->payment = $payment;
$this->subject($this->payment->subject);
\Log::debug("Subject: {$this->payment->subject}");
}
public function build()
{
$this->to($this->payment->email, $this->payment->name)
->view('mails/payment')
->with('payment', $this->payment);
return $this;
}
}
And I call it this way:
$payment = \App\Models\Payments::findOrFail($id);
$payment->subject = 'Your account has been canceled';
\Mail::queue(new \App\Mail\PlanExpiringOrExpired($payment));
The log saved correctly the following content:
[2023-02-12 11:00:04] local.DEBUG: Subject: Your account has been canceled
Yet the user received as subject: Plan Expiring or Expired (which is basically the class name).
Since I've done this change recently, do you think this might be a cache-related problem? If so, I'm using Supervisor to run queues, how do I clear the cache (through PHP) without messing up the production server?
I have used in the past something like this.
\Artisan::call('cache:clear');
But I'm not sure if this is correct, or if it has any implications for my production server.
Have you tried it this way to setup the proper subject?
private $payment = null;
public function __construct($payment)
{
$this->payment = $payment;
}
public function build()
{
$this->to($this->payment->email, $this->payment->name)
->subject($this->payment->subject)
->view('mails/payment')
->with('payment', $this->payment);
\Log::debug("Subject: {$this->payment->subject}");
return $this;
}
Move the subject set into build
iam doing like this in queue class, EmailContactForm is a mailable class.
public function handle()
{
$email = new EmailContactForm([
'locale' => $this->data['locale'],
'from_email' => $this->data['from_email'],
'name' => $this->data['name'],
'topic' => $this->data['topic'],
'subject' => $this->data['subject'],
'msg' => $this->data['msg']
]);
Mail::to($this->data['to_email'])
->bcc(config('app.mail_from_address'))
->send($email);
}
Solved.
It was indeed a cache problem, it is also necessary to restart the queue. My solution was to create a private endpoint like /superadmin/clear-cache and use it whenever I need.
Route::get('/superadmin/clear-cache', function()
{
\Artisan::call('cache:clear');
\Artisan::call('queue:restart');
});

Email verification does not seem to be sending anymore

I want to send email verification when a user signs up with a new Email Address. So at the Register Controller I added this:
public function register(Request $request)
{
if(Session::has('email')){
return Redirect::back()->withErrors(['msg' => 'Email was already sent to you, please check the spam folder too.']);
}else{
$validatedEmail = $request->validate([
'user_input' => 'required|unique:users,usr_email|regex:/(.+)#(.+)\.(.+)/i|max:125|min:3',
],[
'user_input.required' => 'You must enter this field',
'user_input.unique' => 'This email is already registered',
'user_input.regex' => 'This email is not correct',
'user_input.max' => 'Maximum length must be 125 characters',
'user_input.min' => 'Minimum length must be 3 characters',
]);
$register = new NewRegisterMemberWithEmail();
return $register->register();
}
}
So if the email was valid, it will call a helper class NewRegisterMemberWithEmail which goes like this:
class NewRegisterMemberWithEmail
{
public function register()
{
try{
$details = [
'title' => 'Verify email'
];
Mail::to(request()->all()['user_input'])->send(new AuthMail($details));
Session::put('email',request()->all()['user_input']);
return redirect()->route('login.form');
}catch(\PDOException $e){
dd($e);
}
}
}
So it used to work fine and correctly sends the email for verification, but I don't know why it does not send email nowadays.
In fact I have tested this with different mail service providers and for both Yahoo & Gmail the email did not received somehow!
But for local mail service provider based in my country the email was sent properly!
I don't know really what's going on here because the logic seems to be fine...
So if you know, please let me know... I would really really appreciate any idea or suggestion from you guys.
Also here is my AuthMail Class if you want to take a look at:
class AuthMail extends Mailable
{
use Queueable, SerializesModels;
public $details;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct($details)
{
$this->details = $details;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
return $this->subject('Sitename')->view('emails.AuthMail');
}
}
Once I was faced same problem when I was used Gmail as smtp.
Reason:
when we used our Gmail password directly in smtp settings then due to some Gmail policies it'll be blocked after sometime (months) and stopped email sending.
Solution:
we need to create an app-password from our Gmail security and use that password in smtp settings. below google article will guide:
How to create app-password on gmail
.env smtp setting for laravel:
MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=<your-email>
MAIL_PASSWORD=<app-password>
MAIL_ENCRYPTION=tls
I hope that'll help you.
If you use google mail to send email then we have the same problem.
On May 30, 2022 Google stop supporting less secure applications or third party application.
This is I think the reason why your send mail does not work (consider this answer if you use google mail as mail sender)
I was having issues when sending email, especially to gmail accounts. So I have changed my approach and overcome that issue.
Please check my answer below
Laravel Email
Example Mail Class
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Symfony\Component\Mime\Email;
class OrderInfoMail extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* #return void
*/
public $data;
public function __construct($data)
{
$this->data = $data;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
$this
->subject('Order Confirmation')
->from('noreply#app.xxx.co.uk', 'XXX Portal')
->view('orders.templates.order-form')
->with([
'name' => $this->data->name,
'sales_representative_name' => $this->data->sales_representative_name,
'sales_representative_phone' => $this->data->sales_representative_phone,
"items" => $this->data->items,
"address" => $this->data->address,
"net" => $this->data->net,
"payment" => $this->data->payment,
"balance" => $this->data->balance,
]);
$this->withSymfonyMessage(function (Email $message) {
$message->getHeaders()->addTextHeader(
'X-Mailer', 'PHP/' . phpversion()
);
});
return $this;
}
}
Usage
$email = 'a#b.com'; // pls change
$name = 'ab';// pls change
$data = new \stdClass();
$data->name = $name;
$data->sales_representative_name = \App\User::find(Auth::user()->id)->name;
$data->sales_representative_phone = \App\User::find(Auth::user()->id)->phones->first()->number;
$data->items = $order_items;
$data->address = $address;
$data->net = $net;
$data->payment = $payment;
$data->balance = $balance;
Mail::to($email)->send(new \App\Mail\OrderInfoMail($data));
I don't think the issue is your code. I think it is related to you sending practices. A solution is to use a service that is designed to send emails like SparkPost (full disclosure I work for SparkPost). There are many others. These services can help you make sure you are following email best practices.
You can make this work without an email service but at the very least you should verify you are following the best practices presented by MAAWG: https://www.m3aawg.org/published-documents

Laravel Reset Password Notification does not get dispatched in test but does send an email

I am writing tests for my Laravel project. Right now I am testing the authentication code like login, logout, reset password and so on.
Sadly, my test is failing because there is no notification send. I have mocked the notifications but assertSendTo always fails with the reason The expected [Illuminate\Auth\Notifications\ResetPassword] notification was not sent..
However, when actual requesting a reset password email (not in the test, as a normal user on my website) I indeed do get an reset password email. So, it is functional and working but not in my test. How can this be? The .evn is also correct, I have set my mail host to mailtrap.io and I also receive this email... This is the best proof I can give you.
Here is my test:
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Tests\TestCase;
class AuthTest extends TestCase
{
/** #test */
public function a_not_logged_in_user_can_request_a_new_password()
{
Notification::fake();
$email = Str::random() . "#gmail.com";
$current_password = Str::random(16);
$current_password_hash = Hash::make($current_password);
$user = User::factory()->create([
'email' => $email,
'password' => $current_password_hash
]);
$response = $this->json('POST', route('password.email'), ['email' => $email]);
$response->assertStatus(200);
$response->assertLocation(route('home'));
//$this->expectsNotification($user, ResetPassword::class);
Notification::assertSentTo($user, ResetPassword::class);
}
}
Any ideas why the test is not working or whats wrong with it?
Whats also very strange is the fact that the response code 200 is indicating that everything succeeded without any problem.
This is the error I get when executing the test with assertSentTo
1) Tests\Feature\Auth\LoggedIn\ForgotPassword\AuthTest::a_not_logged_in_user_can_request_a_new_password
The expected [Illuminate\Auth\Notifications\ResetPassword] notification was not sent.
Failed asserting that false is true.
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Support/Testing/Fakes/NotificationFake.php:68
/MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:261
MyWebsiteProject/tests/Feature/Auth/LoggedIn/ForgotPassword/AuthTest.php:35
And this is the error I get when executing it with expectsNotification
1) Tests\Feature\Auth\LoggedIn\ForgotPassword\AuthTest::a_logged_in_user_can_request_a_new_password
The following expected notification were not dispatched: [Illuminate\Auth\Notifications\ResetPassword]
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MocksApplicationServices.php:281
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:237
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:153
Kind regards and thank you!
My app was initially created in L5 when the default User.php fully qualified class name was
App\User.php but, according to L8 new pattern, I've duplicated it (to avoid refactoring.... my fault) to App\Models\User.php.
https://twitter.com/taylorotwell/status/1296556354593792000
This partial misalignment, though, left some of my tests behind, expecially the ones involving fake-notifications in which UserFactory kept dispatching (via NotificationFake.php) to App\User.php instead of App\Models\User.php and determining the assertSentTo to fail.
TLDR
make sure that config/auth.php (param providers.users.model) and UserFactory.php rely on the same model.
Expects does just that "expect that something is going to happen", in your case you are expecting it after the fact. The latter is the use case for an assertion. Something like the following is what you need.
class AuthTest extends TestCase
{
/** #test */
public function a_not_logged_in_user_can_request_a_new_password()
{
$email = Str::random() . "#gmail.com";
$current_password = Str::random(16);
$current_password_hash = Hash::make($current_password);
$user = User::factory()->create([
'email' => $email,
'password' => $current_password_hash
]);
// Expect that something is going to happen
$this->expectsNotification($user, ResetPassword::class);
$response = $this->json('POST', route('password.email'), ['email' => $email]);
// Assertion that something has happened
$response->assertStatus(200);
$response->assertLocation(route('home'));
}
}
In case non of the above solutions works for you like in my case, try adding this line at the top of your test function;
$this->withoutExceptionHandling();
This will give you a good error message as to where the problem might be coming from. In my case, I was using the wrong route.

Add custom property to Mail in Laravel

I'm trying to get Postmark's new Message Streams working with Laravel.
Is there a way of adding the property + value '"MessageStream": "notifications" to the JSON body of an email sent using Laravel mail? I'm guessing I will need to extend the Mailable class in some way to do this.
Ideally, i'd like to be able to do something like the following in my Mailable class:
DiscountMailable.php
public function build()
{
return $this->from('hello#example.com')
->markdown('emails.coupons.created')
->subject('🎟 Your Discount')
->with([
'coupon' => $this->coupon,
])
->messageStream(
'notifications',
);
}
Just to make sure this question has an answer, this is what was necessary to make it work (and it worked for my issue in Laravel 7 today as well, where I wanted to use different Postmark Streams for different mail notification types).
public function build()
{
return $this->from('hello#example.com')
->markdown('emails.coupons.created')
->subject('🎟 Your Discount')
->with([
'coupon' => $this->coupon,
])
->withSwiftMessage(function ($message) {
$message->getHeaders()
->addTextHeader('X-PM-Message-Stream', 'notifications')
});
}

Trouble dispatching Laravel Queue

I am currently trying to send mail using a queue in Laravel 5.4 in order to speed up a few requests. But for some reason I just won't resolve.
My job looks like the following:
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Mail;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class NotificationEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $doer, $does, $user;
/**
* Create a new job instance.
*
* #param Podcast $podcast
* #return void
*/
public function __construct($doer, $does, $user)
{
$this->doer = $doer;
$this->does = $does;
$this->user = $user;
}
/**
* Execute the job.
*
* #param AudioProcessor $processor
* #return void
*/
public function handle()
{
$actions = [
'accepted.invite' => 'accepted your invited.',
'accepted.requesting' => 'accepted your request.',
'denied.invite' => 'denied your invite.',
'denied.requesting' => 'denied your request'
];
Mail::send('emails.notification', [
'doer' => $this->does,
'action' => $actions[$this->action]
], function ($m) {
$m->from('noreply#bigriss.com', 'Bigriss');
$m->to("myemail#gmail.com", 'Shawn')->subject('New Notification');
echo "SENT";
});
}
}
With it being dispatched in another class by:
NotificationEmail::dispatch($doer, $does, $user);
Upon listening to the queue, php artisan queue:listen, as soon as I dispatch the job, the listener just runs on endlessly trying to resolve the handle function. I am getting the message "SENT" but the email is never sent (as I can see on my email provider) and the queue is never actually remove instead, the attempts count just goes up indefinitely. Am I missing something here? Is this not what queues are good for?
You are passing string into your to function, and you're missing a variable in your closure.
When you have an anonymous function, you need to pass in any extra variables using use. I don't see a $user variable anywhere in your handle method. It will need to be passed in as a separate variable because you cannot use $this->user to pass it into the closure.
Right now you have
$m->to("$user->email", 'Shawn')->subject('New Notification');
Which is literally interpreting that as a string that says $user->email because you haven't passed anything in. (Side note: there's really no reason to use that here, save that for inline variables with file paths, etc. You don't need an inline variable with this string).
You would need to change it to
$user = $this->user;
Mail::send('emails.notification', [
'doer' => $this->does,
'action' => $actions[$this->action]
], function ($m) use ($user) {
$m->from('noreply#bigriss.com', 'Bigriss');
$m->to($user->email, 'Shawn')->subject('New Notification');
echo "SENT";
});
You may want to consider using something like Laravel Dusk to debug your queue and logging to better control this than trying to just view "SENT" in your browser.
Also, consider sanitizing your website address since you're posting source code from it.

Categories