diff --git a/src/.env.example b/src/.env.example --- a/src/.env.example +++ b/src/.env.example @@ -96,3 +96,6 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" JWT_SECRET= + +KB_ACCOUNT_DELETE= +KB_ACCOUNT_SUSPENDED= diff --git a/src/app/Console/Development/TemplateRender.php b/src/app/Console/Development/TemplateRender.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Development/TemplateRender.php @@ -0,0 +1,37 @@ +argument('template'); + $template = str_replace('/', '\\', $template); + + $class = '\\App\\' . $template; + + echo $class::fakeRender(); + } +} diff --git a/src/app/Mail/NegativeBalance.php b/src/app/Mail/NegativeBalance.php new file mode 100644 --- /dev/null +++ b/src/app/Mail/NegativeBalance.php @@ -0,0 +1,69 @@ +account = $account; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $user = $this->account; + + $subject = \trans('mail.negativebalance-subject', ['site' => \config('app.name')]); + + $this->view('emails.negative_balance') + ->subject($subject) + ->with([ + 'site' => \config('app.name'), + 'subject' => $subject, + 'username' => $user->name(true), + 'supportUrl' => \config('app.support_url'), + 'walletUrl' => Utils::serviceUrl('/wallet'), + ]); + + return $this; + } + + /** + * Render the mail template with fake data + * + * @return string HTML output + */ + public static function fakeRender(): string + { + $user = new User(); + + $mail = new self($user); + + return $mail->build()->render(); + } +} diff --git a/src/app/Mail/PasswordReset.php b/src/app/Mail/PasswordReset.php --- a/src/app/Mail/PasswordReset.php +++ b/src/app/Mail/PasswordReset.php @@ -2,10 +2,12 @@ namespace App\Mail; +use App\User; use App\VerificationCode; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Str; class PasswordReset extends Mailable { @@ -54,4 +56,25 @@ return $this; } + + /** + * Render the mail template with fake data + * + * @return string HTML output + */ + public static function fakeRender(): string + { + $code = new VerificationCode([ + 'code' => Str::random(VerificationCode::CODE_LENGTH), + 'short_code' => VerificationCode::generateShortCode(), + ]); + + $code->user = new User([ + 'email' => 'test@' . \config('app.domain'), + ]); + + $mail = new self($code); + + return $mail->build()->render(); + } } diff --git a/src/app/Mail/SignupVerification.php b/src/app/Mail/SignupVerification.php --- a/src/app/Mail/SignupVerification.php +++ b/src/app/Mail/SignupVerification.php @@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Str; class SignupVerification extends Mailable { @@ -60,4 +61,27 @@ return $this; } + + /** + * Render the mail template with fake data + * + * @return string HTML output + */ + public static function fakeRender(): string + { + $code = new SignupCode([ + 'code' => Str::random(SignupCode::CODE_LENGTH), + 'short_code' => SignupCode::generateShortCode(), + 'data' => [ + 'email' => 'test@' . \config('app.domain'), + 'first_name' => 'Firstname', + 'last_name' => 'Lastname', + ], + ]); + + + $mail = new self($code); + + return $mail->build()->render(); + } } diff --git a/src/app/Mail/SuspendedDebtor.php b/src/app/Mail/SuspendedDebtor.php new file mode 100644 --- /dev/null +++ b/src/app/Mail/SuspendedDebtor.php @@ -0,0 +1,77 @@ +account = $account; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $user = $this->account; + + $subject = \trans('mail.suspendeddebtor-subject', ['site' => \config('app.name')]); + + $moreInfo = null; + if ($moreInfoUrl = \config('app.kb.account_suspended')) { + $moreInfo = \trans('mail.more-info-html', ['href' => $moreInfoUrl]); + } + + $this->view('emails.suspended_debtor') + ->subject($subject) + ->with([ + 'site' => \config('app.name'), + 'subject' => $subject, + 'username' => $user->name(true), + 'cancelUrl' => \config('app.kb.account_delete'), + 'supportUrl' => \config('app.support_url'), + 'walletUrl' => Utils::serviceUrl('/wallet'), + 'moreInfo' => $moreInfo, + 'days' => 14 // TODO: Configurable + ]); + + return $this; + } + + /** + * Render the mail template with fake data + * + * @return string HTML output + */ + public static function fakeRender(): string + { + $user = new User(); + + $mail = new self($user); + + return $mail->build()->render(); + } +} diff --git a/src/config/app.php b/src/config/app.php --- a/src/config/app.php +++ b/src/config/app.php @@ -236,4 +236,12 @@ 'View' => Illuminate\Support\Facades\View::class, ], + + // Locations of knowledge base articles + 'kb' => [ + // An article about suspended accounts + 'account_suspended' => env('KB_ACCOUNT_SUSPENDED'), + // An article about a way to delete an owned account + 'account_delete' => env('KB_ACCOUNT_DELETE'), + ], ]; diff --git a/src/resources/lang/en/mail.php b/src/resources/lang/en/mail.php --- a/src/resources/lang/en/mail.php +++ b/src/resources/lang/en/mail.php @@ -13,6 +13,11 @@ 'header' => "Dear :name,", 'footer' => "Best regards,\nYour :site Team", + 'negativebalance-subject' => ":site Payment Reminder", + 'negativebalance-body' => "It has probably skipped your attention that you are behind on paying for your :site account. " + . "Consider setting up auto-payment to avoid messages like this in the future.\n\n" + . "Settle up to keep your account running.", + 'passwordreset-subject' => ":site Password Reset", 'passwordreset-body' => "Someone recently asked to change your :site password.\n" . "If this was you, use this verification code to complete the process: :code.\n" @@ -22,4 +27,16 @@ 'signupcode-subject' => ":site Registration", 'signupcode-body' => "This is your verification code for the :site registration process: :code.\n" . "You can also click the link below to continue the registration process:", + + 'suspendeddebtor-subject' => ":site Account Suspended", + 'suspendeddebtor-body' => "You have been behind on paying for your :site account " + ."for over :days days. Your account has been suspended.", + 'suspendeddebtor-middle' => "Settle up now to reactivate your account.", + 'suspendeddebtor-cancel' => "Don't want to be our customer anymore? " + . "Here is how you can cancel your account:", + + 'support' => "Special circumstances? Something wrong with a charge?\n" + . " :site Support is here to help:", + + 'more-info-html' => "See here for more information.", ]; diff --git a/src/resources/views/emails/negative_balance.blade.php b/src/resources/views/emails/negative_balance.blade.php new file mode 100644 --- /dev/null +++ b/src/resources/views/emails/negative_balance.blade.php @@ -0,0 +1,19 @@ + + + + + + +

{{ __('mail.header', ['name' => $username]) }}

+ +

{{ __('mail.negativebalance-body', ['site' => $site]) }}

+

{{ $walletUrl }}

+ +@if ($supportUrl) +

{{ __('mail.support', ['site' => $site]) }}

+

{{ $supportUrl }}

+@endif + +

{{ __('mail.footer', ['site' => $site, 'appurl' => config('app.url')]) }}

+ + diff --git a/src/resources/views/emails/suspended_debtor.blade.php b/src/resources/views/emails/suspended_debtor.blade.php new file mode 100644 --- /dev/null +++ b/src/resources/views/emails/suspended_debtor.blade.php @@ -0,0 +1,24 @@ + + + + + + +

{{ __('mail.header', ['name' => $username]) }}

+ +

{{ __('mail.suspendeddebtor-body', ['site' => $site, 'days' => $days]) }} {!! $moreInfo !!}

+

{{ __('mail.suspendeddebtor-middle') }}

+

{{ $walletUrl }}

+ +@if ($supportUrl) +

{{ __('mail.support', ['site' => $site]) }}

+

{{ $supportUrl }}

+@endif +@if ($cancelUrl) +

{{ __('mail.suspendeddebtor-cancel') }}

+

{{ $cancelUrl }}

+@endif + +

{{ __('mail.footer', ['site' => $site, 'appurl' => config('app.url')]) }}

+ + diff --git a/src/tests/Unit/Mail/NegativeBalanceTest.php b/src/tests/Unit/Mail/NegativeBalanceTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Unit/Mail/NegativeBalanceTest.php @@ -0,0 +1,43 @@ + 'https://kolab.org/support', + ]); + + $mail = new NegativeBalance($user); + $html = $mail->build()->render(); + + $walletUrl = \App\Utils::serviceUrl('/wallet'); + $walletLink = sprintf('%s', $walletUrl, $walletUrl); + $supportUrl = \config('app.support_url'); + $supportLink = sprintf('%s', $supportUrl, $supportUrl); + + $appName = \config('app.name'); + + $this->assertSame("$appName Payment Reminder", $mail->subject); + $this->assertStringStartsWith('', $html); + $this->assertTrue(strpos($html, $user->name(true)) > 0); + $this->assertTrue(strpos($html, $walletLink) > 0); + $this->assertTrue(strpos($html, $supportLink) > 0); + $this->assertTrue(strpos($html, "behind on paying for your $appName account") > 0); + $this->assertTrue(strpos($html, "$appName Support") > 0); + $this->assertTrue(strpos($html, "$appName Team") > 0); + } +} diff --git a/src/tests/Unit/Mail/SuspendedDebtorTest.php b/src/tests/Unit/Mail/SuspendedDebtorTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Unit/Mail/SuspendedDebtorTest.php @@ -0,0 +1,52 @@ + 'https://kolab.org/support', + 'app.kb.account_suspended' => 'https://kb.kolab.org/account-suspended', + 'app.kb.account_delete' => 'https://kb.kolab.org/account-delete', + ]); + + $mail = new SuspendedDebtor($user); + $html = $mail->build()->render(); + + $walletUrl = \App\Utils::serviceUrl('/wallet'); + $walletLink = sprintf('%s', $walletUrl, $walletUrl); + $supportUrl = \config('app.support_url'); + $supportLink = sprintf('%s', $supportUrl, $supportUrl); + $deleteUrl = \config('app.kb.account_delete'); + $deleteLink = sprintf('%s', $deleteUrl, $deleteUrl); + $moreUrl = \config('app.kb.account_suspended'); + $moreLink = sprintf('here', $moreUrl); + + $appName = \config('app.name'); + + $this->assertSame("$appName Account Suspended", $mail->subject); + $this->assertStringStartsWith('', $html); + $this->assertTrue(strpos($html, $user->name(true)) > 0); + $this->assertTrue(strpos($html, $walletLink) > 0); + $this->assertTrue(strpos($html, $supportLink) > 0); + $this->assertTrue(strpos($html, $deleteLink) > 0); + $this->assertTrue(strpos($html, "You have been behind on paying for your $appName account") > 0); + $this->assertTrue(strpos($html, "over 14 days") > 0); + $this->assertTrue(strpos($html, "See $moreLink for more information") > 0); + $this->assertTrue(strpos($html, "$appName Support") > 0); + $this->assertTrue(strpos($html, "$appName Team") > 0); + } +}