diff --git a/src/.env.example b/src/.env.example index 2811f18d..6c2b584b 100644 --- a/src/.env.example +++ b/src/.env.example @@ -1,98 +1,101 @@ APP_NAME=Kolab APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://127.0.0.1:8000 APP_PUBLIC_URL= APP_DOMAIN=kolabnow.com LOG_CHANNEL=stack DB_CONNECTION=mysql DB_DATABASE=kolabdev DB_HOST=127.0.0.1 DB_PASSWORD=kolab DB_PORT=3306 DB_USERNAME=kolabdev BROADCAST_DRIVER=log CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=file SESSION_LIFETIME=120 2FA_DSN=mysql://roundcube:Welcome2KolabSystems@127.0.0.1/roundcube 2FA_TOTP_DIGITS=6 2FA_TOTP_INTERVAL=30 2FA_TOTP_DIGEST=sha1 IMAP_URI=ssl://127.0.0.1:993 IMAP_ADMIN_LOGIN=cyrus-admin IMAP_ADMIN_PASSWORD=Welcome2KolabSystems IMAP_VERIFY_HOST=false IMAP_VERIFY_PEER=false LDAP_BASE_DN="dc=mgmt,dc=com" LDAP_DOMAIN_BASE_DN="ou=Domains,dc=mgmt,dc=com" LDAP_HOSTS=127.0.0.1 LDAP_PORT=389 LDAP_SERVICE_BIND_DN="uid=kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_SERVICE_BIND_PW="Welcome2KolabSystems" LDAP_USE_SSL=false LDAP_USE_TLS=false # Administrative LDAP_ADMIN_BIND_DN="cn=Directory Manager" LDAP_ADMIN_BIND_PW="Welcome2KolabSystems" LDAP_ADMIN_ROOT_DN="dc=mgmt,dc=com" # Hosted (public registration) LDAP_HOSTED_BIND_DN="uid=hosted-kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_HOSTED_BIND_PW="Welcome2KolabSystems" LDAP_HOSTED_ROOT_DN="dc=hosted,dc=com" REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 SWOOLE_HTTP_HOST=127.0.0.1 SWOOLE_HTTP_PORT=8000 PAYMENT_PROVIDER= MOLLIE_KEY= STRIPE_KEY= STRIPE_PUBLIC_KEY= STRIPE_WEBHOOK_SECRET= MAIL_DRIVER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="noreply@example.com" MAIL_FROM_NAME="Example.com" MAIL_REPLYTO_ADDRESS=null MAIL_REPLYTO_NAME=null DNS_TTL=3600 DNS_SPF="v=spf1 mx -all" DNS_STATIC="%s. MX 10 ext-mx01.mykolab.com." DNS_COPY_FROM=null AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= PUSHER_APP_ID= PUSHER_APP_KEY= PUSHER_APP_SECRET= PUSHER_APP_CLUSTER=mt1 MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 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 index 00000000..50893431 --- /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 index 00000000..54ff3371 --- /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 index 7f65d741..1cdd2a96 100644 --- a/src/app/Mail/PasswordReset.php +++ b/src/app/Mail/PasswordReset.php @@ -1,57 +1,80 @@ code = $code; } /** * Build the message. * * @return $this */ public function build() { $href = sprintf( '%s/login/reset/%s-%s', \config('app.url'), $this->code->short_code, $this->code->code ); $this->view('emails.password_reset') ->subject(__('mail.passwordreset-subject', ['site' => \config('app.name')])) ->with([ 'site' => \config('app.name'), 'code' => $this->code->code, 'short_code' => $this->code->short_code, 'link' => sprintf('%s', $href, $href), 'username' => $this->code->user->name(true) ]); 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 index 9036420f..99c98672 100644 --- a/src/app/Mail/SignupVerification.php +++ b/src/app/Mail/SignupVerification.php @@ -1,63 +1,87 @@ code = $code; } /** * Build the message. * * @return $this */ public function build() { $href = sprintf( '%s/signup/%s-%s', \config('app.url'), $this->code->short_code, $this->code->code ); $username = $this->code->data['first_name'] ?? ''; if (!empty($this->code->data['last_name'])) { $username .= ' ' . $this->code->data['last_name']; } $username = trim($username); $this->view('emails.signup_code') ->subject(__('mail.signupcode-subject', ['site' => \config('app.name')])) ->with([ 'site' => \config('app.name'), 'username' => $username ?: 'User', 'code' => $this->code->code, 'short_code' => $this->code->short_code, 'link' => sprintf('%s', $href, $href), ]); 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 index 00000000..e3509f91 --- /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 index 9dd7ddac..8dce8ef0 100644 --- a/src/config/app.php +++ b/src/config/app.php @@ -1,239 +1,247 @@ env('APP_NAME', 'Laravel'), /* |-------------------------------------------------------------------------- | Application Environment |-------------------------------------------------------------------------- | | This value determines the "environment" your application is currently | running in. This may determine how you prefer to configure various | services the application utilizes. Set this in your ".env" file. | */ 'env' => env('APP_ENV', 'production'), /* |-------------------------------------------------------------------------- | Application Debug Mode |-------------------------------------------------------------------------- | | When your application is in debug mode, detailed error messages with | stack traces will be shown on every error that occurs within your | application. If disabled, a simple generic error page is shown. | */ 'debug' => env('APP_DEBUG', false), /* |-------------------------------------------------------------------------- | Application URL |-------------------------------------------------------------------------- | | This URL is used by the console to properly generate URLs when using | the Artisan command line tool. You should set this to the root of | your application so that it is used when running Artisan tasks. */ 'url' => env('APP_URL', 'http://localhost'), 'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')), 'asset_url' => env('ASSET_URL', null), /* |-------------------------------------------------------------------------- | Application Domain |-------------------------------------------------------------------------- | | System domain used for user signup (kolab identity) */ 'domain' => env('APP_DOMAIN', 'domain.tld'), /* |-------------------------------------------------------------------------- | Application Timezone |-------------------------------------------------------------------------- | | Here you may specify the default timezone for your application, which | will be used by the PHP date and date-time functions. We have gone | ahead and set this to a sensible default for you out of the box. | */ 'timezone' => 'UTC', /* |-------------------------------------------------------------------------- | Application Locale Configuration |-------------------------------------------------------------------------- | | The application locale determines the default locale that will be used | by the translation service provider. You are free to set this value | to any of the locales which will be supported by the application. | */ 'locale' => 'en', /* |-------------------------------------------------------------------------- | Application Fallback Locale |-------------------------------------------------------------------------- | | The fallback locale determines the locale to use when the current one | is not available. You may change the value to correspond to any of | the language folders that are provided through your application. | */ 'fallback_locale' => 'en', /* |-------------------------------------------------------------------------- | Faker Locale |-------------------------------------------------------------------------- | | This locale will be used by the Faker PHP library when generating fake | data for your database seeds. For example, this will be used to get | localized telephone numbers, street address information and more. | */ 'faker_locale' => 'en_US', /* |-------------------------------------------------------------------------- | Encryption Key |-------------------------------------------------------------------------- | | This key is used by the Illuminate encrypter service and should be set | to a random, 32 character string, otherwise these encrypted strings | will not be safe. Please do this before deploying an application! | */ 'key' => env('APP_KEY'), 'cipher' => 'AES-256-CBC', /* |-------------------------------------------------------------------------- | Autoloaded Service Providers |-------------------------------------------------------------------------- | | The service providers listed here will be automatically loaded on the | request to your application. Feel free to add your own services to | this array to grant expanded functionality to your applications. | */ 'providers' => [ /* * Laravel Framework Service Providers... */ Illuminate\Auth\AuthServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class, Illuminate\Bus\BusServiceProvider::class, Illuminate\Cache\CacheServiceProvider::class, Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, Illuminate\Cookie\CookieServiceProvider::class, Illuminate\Database\DatabaseServiceProvider::class, Illuminate\Encryption\EncryptionServiceProvider::class, Illuminate\Filesystem\FilesystemServiceProvider::class, Illuminate\Foundation\Providers\FoundationServiceProvider::class, Illuminate\Hashing\HashServiceProvider::class, Illuminate\Mail\MailServiceProvider::class, Illuminate\Notifications\NotificationServiceProvider::class, Illuminate\Pagination\PaginationServiceProvider::class, Illuminate\Pipeline\PipelineServiceProvider::class, Illuminate\Queue\QueueServiceProvider::class, Illuminate\Redis\RedisServiceProvider::class, Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, Illuminate\Session\SessionServiceProvider::class, Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, /* * Package Service Providers... */ /* * Application Service Providers... */ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, ], /* |-------------------------------------------------------------------------- | Class Aliases |-------------------------------------------------------------------------- | | This array of class aliases will be registered when this application | is started. However, feel free to register as many as you wish as | the aliases are "lazy" loaded so they don't hinder performance. | */ 'aliases' => [ 'App' => Illuminate\Support\Facades\App::class, 'Arr' => Illuminate\Support\Arr::class, 'Artisan' => Illuminate\Support\Facades\Artisan::class, 'Auth' => Illuminate\Support\Facades\Auth::class, 'Blade' => Illuminate\Support\Facades\Blade::class, 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, 'Bus' => Illuminate\Support\Facades\Bus::class, 'Cache' => Illuminate\Support\Facades\Cache::class, 'Config' => Illuminate\Support\Facades\Config::class, 'Cookie' => Illuminate\Support\Facades\Cookie::class, 'Crypt' => Illuminate\Support\Facades\Crypt::class, 'DB' => Illuminate\Support\Facades\DB::class, 'Eloquent' => Illuminate\Database\Eloquent\Model::class, 'Event' => Illuminate\Support\Facades\Event::class, 'File' => Illuminate\Support\Facades\File::class, 'Gate' => Illuminate\Support\Facades\Gate::class, 'Hash' => Illuminate\Support\Facades\Hash::class, 'Lang' => Illuminate\Support\Facades\Lang::class, 'Log' => Illuminate\Support\Facades\Log::class, 'Mail' => Illuminate\Support\Facades\Mail::class, 'Notification' => Illuminate\Support\Facades\Notification::class, 'Password' => Illuminate\Support\Facades\Password::class, 'Queue' => Illuminate\Support\Facades\Queue::class, 'Redirect' => Illuminate\Support\Facades\Redirect::class, 'Redis' => Illuminate\Support\Facades\Redis::class, 'Request' => Illuminate\Support\Facades\Request::class, 'Response' => Illuminate\Support\Facades\Response::class, 'Route' => Illuminate\Support\Facades\Route::class, 'Schema' => Illuminate\Support\Facades\Schema::class, 'Session' => Illuminate\Support\Facades\Session::class, 'Storage' => Illuminate\Support\Facades\Storage::class, 'Str' => Illuminate\Support\Str::class, 'URL' => Illuminate\Support\Facades\URL::class, 'Validator' => Illuminate\Support\Facades\Validator::class, '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 index 5b5b3f97..732dc4ac 100644 --- a/src/resources/lang/en/mail.php +++ b/src/resources/lang/en/mail.php @@ -1,25 +1,42 @@ "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" . "You can also click the link below.\n" . "If you did not make such a request, you can either ignore this message or get in touch with us about this incident.", '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 index 00000000..1294aa9e --- /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 index 00000000..ead7fc12 --- /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 index 00000000..1e173a29 --- /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 index 00000000..a823bd71 --- /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); + } +}