Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F256842
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
21 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/app/Mail/Helper.php b/src/app/Mail/Helper.php
index 66ccc1a0..73586a32 100644
--- a/src/app/Mail/Helper.php
+++ b/src/app/Mail/Helper.php
@@ -1,131 +1,133 @@
<?php
namespace App\Mail;
use App\EventLog;
use App\Tenant;
use Illuminate\Support\Facades\Mail;
class Helper
{
/**
* Render the mail template.
*
* @param Mailable $mail The mailable object
* @param string $type Output format ('html' or 'text')
*
* @return string HTML or Plain Text output
*/
public static function render(Mailable $mail, string $type = 'html'): string
{
// Plain text output
if ($type == 'text') {
$mail->build(); // @phpstan-ignore-line
$mailer = \Illuminate\Container\Container::getInstance()->make('mailer');
return $mailer->render(['text' => $mail->textView], $mail->buildViewData());
}
if ($type != 'html') {
throw new \Exception("Unsupported output format");
}
// HTML output
return $mail->build()->render(); // @phpstan-ignore-line
}
/**
* Sends an email
*
* @param Mailable $mail Email content generator
* @param int|null $tenantId Tenant identifier
* @param array $params Email parameters: to, cc
*
* @throws \Exception
*/
public static function sendMail(Mailable $mail, $tenantId = null, array $params = []): void
{
$class = class_basename(get_class($mail));
$recipients = [];
// For now we do not support addresses + names, only addresses
foreach (['to', 'cc'] as $idx) {
if (!empty($params[$idx])) {
if (is_array($params[$idx])) {
$recipients = array_merge($recipients, $params[$idx]);
} else {
$recipients[] = $params[$idx];
}
}
}
try {
if (!empty($params['to'])) {
$mail->to($params['to']);
}
if (!empty($params['cc'])) {
$mail->cc($params['cc']);
}
- $fromAddress = Tenant::getConfig($tenantId, 'mail.from.address');
- $fromName = Tenant::getConfig($tenantId, 'mail.from.name');
- $replytoAddress = Tenant::getConfig($tenantId, 'mail.reply_to.address');
- $replytoName = Tenant::getConfig($tenantId, 'mail.reply_to.name');
+ // Note: We're not using Laravel's global 'from' and 'reply_to' settings
+
+ $fromAddress = Tenant::getConfig($tenantId, 'mail.sender.address');
+ $fromName = Tenant::getConfig($tenantId, 'mail.sender.name');
+ $replytoAddress = Tenant::getConfig($tenantId, 'mail.replyto.address');
+ $replytoName = Tenant::getConfig($tenantId, 'mail.replyto.name');
if ($fromAddress) {
$mail->from($fromAddress, $fromName);
}
if ($replytoAddress) {
$mail->replyTo($replytoAddress, $replytoName);
}
Mail::send($mail);
if ($user = $mail->getUser()) {
$comment = "[{$class}] " . $mail->getSubject();
EventLog::createFor($user, EventLog::TYPE_MAILSENT, $comment, ['recipients' => $recipients]);
}
} catch (\Exception $e) {
$format = "[%s] Failed to send mail to %s%s: %s";
$msg = sprintf($format, $class, implode(', ', $recipients), $params['add'] ?? '', $e->getMessage());
\Log::error($msg);
throw $e;
}
}
/**
* Return user's email addresses, separately for use in To and Cc.
*
* @param \App\User $user The user
* @param bool $external Include users's external email
*
* @return array To address as the first element, Cc address(es) as the second.
*/
public static function userEmails(\App\User $user, bool $external = false): array
{
$active = $user->isLdapReady() && $user->isImapReady();
// Sending an email to non-(ldap|imap)-ready user will fail, skip it
// (or send to the external email only, when appropriate)
$to = $active ? $user->email : null;
$cc = [];
// If user has no mailbox entitlement we should not send
// the email to his main address, but use external address, if defined
if ($active && !$user->hasSku('mailbox')) {
$to = $user->getSetting('external_email');
} elseif ($external) {
$ext_email = $user->getSetting('external_email');
if ($ext_email && $ext_email != $to) {
$cc[] = $ext_email;
}
}
return [$to, $cc];
}
}
diff --git a/src/config/mail.php b/src/config/mail.php
index 837a2b89..e18e82a3 100644
--- a/src/config/mail.php
+++ b/src/config/mail.php
@@ -1,135 +1,145 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send any email
| messages sent by your application. Alternative mailers may be setup
| and used as needed; however, this mailer will be used by default.
|
*/
'default' => env('MAIL_MAILER', 'smtp'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers to be used while
| sending an e-mail. You will specify which one you are using for your
| mailers below. You are free to add additional mailers as required.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "log", "array", "failover"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN'),
],
'ses' => [
'transport' => 'ses',
],
'mailgun' => [
'transport' => 'mailgun',
],
'postmark' => [
'transport' => 'postmark',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all e-mails sent by your application to be sent from
| the same address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
|
*/
- 'from' => [
- 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
- 'name' => env('MAIL_FROM_NAME', 'Example'),
+ // Note: Laravel uses 'from', we added 'sender'. This way we make sure
+ // Laravel does not overwrite our From header that we set in App\Mail\Helper::sendMail().
+
+ 'from' => null, // do not use!
+ 'sender' => [
+ 'address' => env('MAIL_FROM_ADDRESS', ''),
+ 'name' => env('MAIL_FROM_NAME', ''),
],
/*
|--------------------------------------------------------------------------
| Global "Reply-To" Address
|--------------------------------------------------------------------------
|
| You may wish for all e-mails sent by your application to be sent from
| the same address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
|
*/
- 'reply_to' => [
+ // Note: Laravel uses 'reply_to', we added 'replyto'. This way we make sure
+ // Laravel does not add an extra Reply-To header on top of our per-tenant
+ // Reply-To header that we set in App\Mail\Helper::sendMail().
+ // More https://github.com/laravel/framework/issues/43034.
+
+ 'reply_to' => null, // do not use!
+ 'replyto' => [
'address' => env('MAIL_REPLYTO_ADDRESS', ''),
'name' => env('MAIL_REPLYTO_NAME', ''),
],
/*
|--------------------------------------------------------------------------
| Markdown Mail Settings
|--------------------------------------------------------------------------
|
| If you are using Markdown based email rendering, you may configure your
| theme and component paths here, allowing you to customize the design
| of the emails. Or, you may simply stick with the Laravel defaults!
|
*/
'markdown' => [
'theme' => 'default',
'paths' => [
resource_path('views/vendor/mail'),
],
],
];
diff --git a/src/tests/Feature/Jobs/Password/RetentionEmailJobTest.php b/src/tests/Feature/Jobs/Password/RetentionEmailJobTest.php
index 171106b3..4c8c92bf 100644
--- a/src/tests/Feature/Jobs/Password/RetentionEmailJobTest.php
+++ b/src/tests/Feature/Jobs/Password/RetentionEmailJobTest.php
@@ -1,75 +1,75 @@
<?php
namespace Tests\Feature\Jobs\Password;
use App\Jobs\Password\RetentionEmailJob;
use App\Mail\PasswordExpirationReminder;
use App\User;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class RetentionEmailJobTest extends TestCase
{
/**
* {@inheritDoc}
*
* @return void
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestUser('PasswordRetention@UserAccount.com');
}
/**
* {@inheritDoc}
*
* @return void
*/
public function tearDown(): void
{
$this->deleteTestUser('PasswordRetention@UserAccount.com');
parent::tearDown();
}
/**
* Test job handle
*
* @return void
*/
public function testHandle()
{
$status = User::STATUS_ACTIVE | User::STATUS_LDAP_READY | User::STATUS_IMAP_READY;
$user = $this->getTestUser('PasswordRetention@UserAccount.com', ['status' => $status]);
$expiresOn = now()->copy()->addDays(7)->toDateString();
Mail::fake();
// Assert that no jobs were pushed...
Mail::assertNothingSent();
$job = new RetentionEmailJob($user, $expiresOn);
$job->handle();
$this->assertMatchesRegularExpression(
'/^' . now()->format('Y-m-d') . ' [0-9]{2}:[0-9]{2}:[0-9]{2}$/',
$user->getSetting('password_expiration_warning')
);
// Assert the email sending job was pushed once
Mail::assertSent(PasswordExpirationReminder::class, 1);
// Assert the mail was sent to the code's email
Mail::assertSent(PasswordExpirationReminder::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
// Assert sender
Mail::assertSent(PasswordExpirationReminder::class, function ($mail) {
- return $mail->hasFrom(\config('mail.from.address'), \config('mail.from.name'))
- && $mail->hasReplyTo(\config('mail.reply_to.address'), \config('mail.reply_to.name'));
+ return $mail->hasFrom(\config('mail.sender.address'), \config('mail.sender.name'))
+ && $mail->hasReplyTo(\config('mail.replyto.address'), \config('mail.replyto.name'));
});
}
}
diff --git a/src/tests/Feature/Jobs/PasswordResetEmailTest.php b/src/tests/Feature/Jobs/PasswordResetEmailTest.php
index 829d56cc..362b4a7f 100644
--- a/src/tests/Feature/Jobs/PasswordResetEmailTest.php
+++ b/src/tests/Feature/Jobs/PasswordResetEmailTest.php
@@ -1,75 +1,75 @@
<?php
namespace Tests\Feature\Jobs;
use App\Jobs\PasswordResetEmail;
use App\Mail\PasswordReset;
use App\User;
use App\VerificationCode;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class PasswordResetEmailTest extends TestCase
{
/**
* {@inheritDoc}
*
* @return void
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestUser('PasswordReset@UserAccount.com');
}
/**
* {@inheritDoc}
*
* @return void
*/
public function tearDown(): void
{
$this->deleteTestUser('PasswordReset@UserAccount.com');
parent::tearDown();
}
/**
* Test job handle
*
* @return void
*/
public function testPasswordResetEmailHandle()
{
$code = new VerificationCode([
'mode' => 'password-reset',
]);
$user = $this->getTestUser('PasswordReset@UserAccount.com');
$user->verificationcodes()->save($code);
$user->setSettings(['external_email' => 'etx@email.com']);
Mail::fake();
// Assert that no jobs were pushed...
Mail::assertNothingSent();
$job = new PasswordResetEmail($code);
$job->handle();
// Assert the email sending job was pushed once
Mail::assertSent(PasswordReset::class, 1);
// Assert the mail was sent to the code's email
Mail::assertSent(PasswordReset::class, function ($mail) use ($code) {
return $mail->hasTo($code->user->getSetting('external_email'));
});
// Assert sender
Mail::assertSent(PasswordReset::class, function ($mail) {
- return $mail->hasFrom(\config('mail.from.address'), \config('mail.from.name'))
- && $mail->hasReplyTo(\config('mail.reply_to.address'), \config('mail.reply_to.name'));
+ return $mail->hasFrom(\config('mail.sender.address'), \config('mail.sender.name'))
+ && $mail->hasReplyTo(\config('mail.replyto.address'), \config('mail.replyto.name'));
});
}
}
diff --git a/src/tests/Unit/Mail/HelperTest.php b/src/tests/Unit/Mail/HelperTest.php
index 42ec8e16..efd26b06 100644
--- a/src/tests/Unit/Mail/HelperTest.php
+++ b/src/tests/Unit/Mail/HelperTest.php
@@ -1,179 +1,179 @@
<?php
namespace Tests\Unit\Mail;
use App\EventLog;
use App\Mail\Helper;
use App\User;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class HelperTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestUser('mail-helper-test@kolabnow.com');
\App\TenantSetting::truncate();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestUser('mail-helper-test@kolabnow.com');
\App\TenantSetting::truncate();
parent::tearDown();
}
/**
* Test Helper::sendMail()
*/
public function testSendMail(): void
{
EventLog::truncate();
Mail::fake();
$tenant = \App\Tenant::whereNotIn('id', [1])->first();
$invitation = new \App\SignupInvitation();
$invitation->id = 'test';
$mail = new \App\Mail\SignupInvitation($invitation);
Helper::sendMail($mail, null, ['to' => 'to@test.com', 'cc' => 'cc@test.com']);
Mail::assertSent(\App\Mail\SignupInvitation::class, 1);
Mail::assertSent(\App\Mail\SignupInvitation::class, function ($mail) {
return $mail->hasTo('to@test.com')
&& $mail->hasCc('cc@test.com')
- && $mail->hasFrom(\config('mail.from.address'), \config('mail.from.name'))
- && $mail->hasReplyTo(\config('mail.reply_to.address'), \config('mail.reply_to.name'));
+ && $mail->hasFrom(\config('mail.sender.address'), \config('mail.sender.name'))
+ && $mail->hasReplyTo(\config('mail.replyto.address'), \config('mail.replyto.name'));
});
// Test with a tenant (but no per-tenant settings)
Mail::fake();
$invitation->tenant_id = $tenant->id;
$mail = new \App\Mail\SignupInvitation($invitation);
Helper::sendMail($mail, $tenant->id, ['to' => 'to@test.com', 'cc' => 'cc@test.com']);
Mail::assertSent(\App\Mail\SignupInvitation::class, 1);
Mail::assertSent(\App\Mail\SignupInvitation::class, function ($mail) {
return $mail->hasTo('to@test.com')
&& $mail->hasCc('cc@test.com')
- && $mail->hasFrom(\config('mail.from.address'), \config('mail.from.name'))
- && $mail->hasReplyTo(\config('mail.reply_to.address'), \config('mail.reply_to.name'));
+ && $mail->hasFrom(\config('mail.sender.address'), \config('mail.sender.name'))
+ && $mail->hasReplyTo(\config('mail.replyto.address'), \config('mail.replyto.name'));
});
// Test with a tenant (but with per-tenant settings)
Mail::fake();
$tenant->setSettings([
- 'mail.from.address' => 'from@test.com',
- 'mail.from.name' => 'from name',
- 'mail.reply_to.address' => 'replyto@test.com',
- 'mail.reply_to.name' => 'replyto name',
+ 'mail.sender.address' => 'from@test.com',
+ 'mail.sender.name' => 'from name',
+ 'mail.replyto.address' => 'replyto@test.com',
+ 'mail.replyto.name' => 'replyto name',
]);
$mail = new \App\Mail\SignupInvitation($invitation);
Helper::sendMail($mail, $tenant->id, ['to' => 'to@test.com']);
Mail::assertSent(\App\Mail\SignupInvitation::class, 1);
Mail::assertSent(\App\Mail\SignupInvitation::class, function ($mail) {
return $mail->hasTo('to@test.com')
&& $mail->hasFrom('from@test.com', 'from name')
&& $mail->hasReplyTo('replyto@test.com', 'replyto name');
});
// No EventLog entries up to this point
$this->assertSame(0, EventLog::count());
// Assert EventLog entry
$user = $this->getTestUser('mail-helper-test@kolabnow.com');
$mail = new \App\Mail\TrialEnd($user);
Helper::sendMail($mail, $tenant->id, ['to' => 'to@test.com', 'cc' => 'cc@test.com']);
$event = EventLog::where('object_id', $user->id)->where('object_type', User::class)->first();
$this->assertSame(EventLog::TYPE_MAILSENT, $event->type);
$this->assertSame(['recipients' => ['to@test.com', 'cc@test.com']], $event->data);
$this->assertSame("[TrialEnd] Kolab Now: Your trial phase has ended", $event->comment);
// TODO: Test somehow exception case
}
/**
* Test Helper::userEmails()
*/
public function testUserEmails(): void
{
$status = User::STATUS_ACTIVE | User::STATUS_LDAP_READY | User::STATUS_IMAP_READY;
$user = $this->getTestUser('mail-helper-test@kolabnow.com', ['status' => $status]);
// User with no mailbox and no external email
list($to, $cc) = Helper::userEmails($user);
$this->assertSame(null, $to);
$this->assertSame([], $cc);
list($to, $cc) = Helper::userEmails($user, true);
$this->assertSame(null, $to);
$this->assertSame([], $cc);
// User with no mailbox but with external email
$user->setSetting('external_email', 'external@test.com');
list($to, $cc) = Helper::userEmails($user);
$this->assertSame('external@test.com', $to);
$this->assertSame([], $cc);
list($to, $cc) = Helper::userEmails($user, true);
$this->assertSame('external@test.com', $to);
$this->assertSame([], $cc);
// User with mailbox and external email
$sku = \App\Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
$user->assignSku($sku);
list($to, $cc) = Helper::userEmails($user);
$this->assertSame($user->email, $to);
$this->assertSame([], $cc);
list($to, $cc) = Helper::userEmails($user, true);
$this->assertSame($user->email, $to);
$this->assertSame(['external@test.com'], $cc);
// User with mailbox, but no external email
$user->setSetting('external_email', null);
list($to, $cc) = Helper::userEmails($user);
$this->assertSame($user->email, $to);
$this->assertSame([], $cc);
list($to, $cc) = Helper::userEmails($user, true);
$this->assertSame($user->email, $to);
$this->assertSame([], $cc);
// Use with mailbox, but not ready
$user->setSetting('external_email', 'external@test.com');
$user->status = User::STATUS_ACTIVE | User::STATUS_LDAP_READY;
$user->save();
list($to, $cc) = Helper::userEmails($user, true);
$this->assertSame(null, $to);
$this->assertSame(['external@test.com'], $cc);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Jun 10, 12:27 AM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196899
Default Alt Text
(21 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment