Custom SMTP Settings (#561)

* Custom SMTP Settings

* Fix lint

* Custom SMTP add in Pricing plan

* Allow reset email settings

* improve custom SMTP using seprate abstract class

* test case for custom SMTP

* fix test case

* UI improvement

* add CASHIER_KEY in phpunit for testcase

* Attempt to fix tests

* Run pint and attempt to fix cache tests

* Fix user management tests

* Fix code linters

* Merged main & fix linting

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala
2024-09-24 15:46:20 +05:30
committed by GitHub
parent 5dcd4ff8cb
commit 504c7a0f2f
31 changed files with 876 additions and 510 deletions

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Http\Requests\Workspace\CustomDomainRequest;
use App\Http\Requests\Workspace\EmailSettingsRequest;
use App\Http\Resources\WorkspaceResource;
use App\Models\Workspace;
use Illuminate\Http\Request;
@@ -24,12 +25,31 @@ class WorkspaceController extends Controller
public function saveCustomDomain(CustomDomainRequest $request)
{
if (!$request->workspace->is_pro) {
return $this->error([
'message' => 'A Pro plan is required to use this feature.',
], 403);
}
$request->workspace->custom_domains = $request->customDomains;
$request->workspace->save();
return new WorkspaceResource($request->workspace);
}
public function saveEmailSettings(EmailSettingsRequest $request)
{
if (!$request->workspace->is_pro) {
return $this->error([
'message' => 'A Pro plan is required to use this feature.',
], 403);
}
$request->workspace->update(['settings' => array_merge($request->workspace->settings, ['email_settings' => $request->validated()])]);
return new WorkspaceResource($request->workspace);
}
public function delete($id)
{
$workspace = Workspace::findOrFail($id);

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Http\Requests\Workspace;
use App\Models\Workspace;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
class EmailSettingsRequest extends FormRequest
{
public Workspace $workspace;
public function __construct(Request $request, Workspace $workspace)
{
$this->workspace = Workspace::findOrFail($request->workspaceId);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
$allFieldsPresent = $this->filled(['host', 'port', 'username', 'password']);
return [
'host' => [
$allFieldsPresent ? 'required' : 'nullable',
'required_with:port,username,password',
'string',
],
'port' => [
$allFieldsPresent ? 'required' : 'nullable',
'required_with:host,username,password',
'integer',
],
'username' => [
$allFieldsPresent ? 'required' : 'nullable',
'required_with:host,port,password',
'string',
],
'password' => [
$allFieldsPresent ? 'required' : 'nullable',
'required_with:host,port,username',
'string',
],
];
}
/**
* Get the validation messages that apply to the request.
*
* @return array
*/
public function messages()
{
return [
'host.required_with' => 'The host field is required.',
'port.required_with' => 'The port field is required.',
'username.required_with' => 'The username field is required.',
'password.required_with' => 'The password field is required.',
];
}
}

View File

@@ -27,7 +27,7 @@ class Google
public function getClient(): Client
{
if($this->client->isAccessTokenExpired()) {
if ($this->client->isAccessTokenExpired()) {
$this->refreshToken();
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Integrations\Handlers;
use App\Events\Forms\FormSubmitted;
use App\Models\Integration\FormIntegration;
use Illuminate\Support\Facades\Log;
abstract class AbstractEmailIntegrationHandler extends AbstractIntegrationHandler
{
protected $mailer;
public function __construct(FormSubmitted $event, FormIntegration $formIntegration, array $integration)
{
parent::__construct($event, $formIntegration, $integration);
$this->initializeMailer();
}
protected function initializeMailer()
{
$this->mailer = config('mail.default');
$this->setWorkspaceSMTPSettings();
if (!$this->mailer) {
Log::error('Mailer not specified', [
'form_id' => $this->form->id
]);
}
}
protected function setWorkspaceSMTPSettings()
{
$workspace = $this->form->workspace;
$emailSettings = $workspace->settings['email_settings'] ?? [];
if (!$workspace->is_pro || !$emailSettings || empty($emailSettings['host']) || empty($emailSettings['port']) || empty($emailSettings['username']) || empty($emailSettings['password'])) {
return;
}
config([
'mail.mailers.custom_smtp.host' => $emailSettings['host'],
'mail.mailers.custom_smtp.port' => $emailSettings['port'],
'mail.mailers.custom_smtp.username' => $emailSettings['username'],
'mail.mailers.custom_smtp.password' => $emailSettings['password']
]);
$this->mailer = 'custom_smtp';
}
}

View File

@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use App\Notifications\Forms\FormSubmissionNotification;
class EmailIntegration extends AbstractIntegrationHandler
class EmailIntegration extends AbstractEmailIntegrationHandler
{
public static function getValidationRules(): array
{
@@ -36,10 +36,11 @@ class EmailIntegration extends AbstractIntegrationHandler
'recipients' => $subscribers->toArray(),
'form_id' => $this->form->id,
'form_slug' => $this->form->slug,
'mailer' => $this->mailer
]);
$subscribers->each(function ($subscriber) {
Notification::route('mail', $subscriber)->notify(
new FormSubmissionNotification($this->event, $this->integrationData)
new FormSubmissionNotification($this->event, $this->integrationData, $this->mailer)
);
});
}

View File

@@ -58,7 +58,7 @@ class GoogleSheetsIntegration extends AbstractIntegrationHandler
protected function getSpreadsheetId(): string
{
if(!isset($this->integrationData->spreadsheet_id)) {
if (!isset($this->integrationData->spreadsheet_id)) {
throw new Exception('The spreadsheed is not instantiated');
}

View File

@@ -10,7 +10,7 @@ use Stevebauman\Purify\Facades\Purify;
/**
* Sends a confirmation to form respondant that form was submitted
*/
class SubmissionConfirmationIntegration extends AbstractIntegrationHandler
class SubmissionConfirmationIntegration extends AbstractEmailIntegrationHandler
{
public const RISKY_USERS_LIMIT = 120;
@@ -54,8 +54,9 @@ class SubmissionConfirmationIntegration extends AbstractIntegrationHandler
'recipient' => $email,
'form_id' => $this->form->id,
'form_slug' => $this->form->slug,
'mailer' => $this->mailer
]);
Mail::to($email)->send(new SubmissionConfirmationMail($this->event, $this->integrationData));
Mail::mailer($this->mailer)->to($email)->send(new SubmissionConfirmationMail($this->event, $this->integrationData));
}
private function getRespondentEmail()

View File

@@ -38,7 +38,7 @@ class WorkspaceUsersUpdated implements ShouldQueue
public function handle(): void
{
// If self-hosted, no need to update billing
if (!pricing_enabled()) {
if (!pricing_enabled() || \App::environment('testing')) {
return;
}

View File

@@ -15,17 +15,17 @@ class FormIntegrationCreatedHandler implements ShouldQueue
{
$integration = FormIntegration::getIntegration($event->formIntegration->integration_id);
if(!$integration) {
if (!$integration) {
return;
}
if(!isset($integration['file_name'])) {
if (!isset($integration['file_name'])) {
return;
}
$className = 'App\Integrations\Handlers\Events\\' . $integration['file_name'] . 'Created';
if(!class_exists($className)) {
if (!class_exists($className)) {
return;
}

View File

@@ -54,7 +54,7 @@ class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue
private function getFromEmail()
{
if(config('app.self_hosted')) {
if (config('app.self_hosted')) {
return config('mail.from.address');
}

View File

@@ -25,6 +25,7 @@ class Workspace extends Model implements CachableAttributes
'icon',
'user_id',
'custom_domain',
'settings'
];
protected $appends = [
@@ -33,10 +34,11 @@ class Workspace extends Model implements CachableAttributes
'is_enterprise',
];
protected function casts()
protected function casts(): array
{
return [
'custom_domains' => 'array',
'settings' => 'array'
];
}
@@ -201,5 +203,4 @@ class Workspace extends Model implements CachableAttributes
{
return $this->hasMany(Form::class);
}
}

View File

@@ -15,15 +15,17 @@ class FormSubmissionNotification extends Notification implements ShouldQueue
use Queueable;
public FormSubmitted $event;
private $mailer;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(FormSubmitted $event, private $integrationData)
public function __construct(FormSubmitted $event, private $integrationData, string $mailer)
{
$this->event = $event;
$this->mailer = $mailer;
}
/**
@@ -52,6 +54,7 @@ class FormSubmissionNotification extends Notification implements ShouldQueue
->useSignedUrlForFiles();
return (new MailMessage())
->mailer($this->mailer)
->replyTo($this->getReplyToEmail($notifiable->routes['mail']))
->from($this->getFromEmail(), config('app.name'))
->subject('New form submission for "' . $this->event->form->title . '"')
@@ -63,7 +66,7 @@ class FormSubmissionNotification extends Notification implements ShouldQueue
private function getFromEmail()
{
if(config('app.self_hosted')) {
if (config('app.self_hosted')) {
return config('mail.from.address');
}
$originalFromAddress = Str::of(config('mail.from.address'))->explode('@');

View File

@@ -156,7 +156,7 @@ class IntegrationLogicRule implements DataAwareRule, ValidationRule
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if(!$this->passes($attribute, $value)) {
if (!$this->passes($attribute, $value)) {
$fail($this->message());
}
}

View File

@@ -41,7 +41,7 @@ class OneEmailPerLine implements ValidationRule
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if(!$this->passes($attribute, $value)) {
if (!$this->passes($attribute, $value)) {
$fail($this->message());
}
}

View File

@@ -69,7 +69,7 @@ class StorageFile implements ValidationRule
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if(!$this->passes($attribute, $value)) {
if (!$this->passes($attribute, $value)) {
$fail($this->message());
}
}

View File

@@ -34,7 +34,7 @@ class ValidHCaptcha implements ImplicitRule
}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if(!$this->passes($attribute, $value)) {
if (!$this->passes($attribute, $value)) {
$fail($this->message());
}
}

View File

@@ -95,11 +95,11 @@ class FormLogicConditionChecker
private function checkMatrixContains($condition, $fieldValue): bool
{
foreach($condition['value'] as $key => $value) {
if(!(array_key_exists($key, $condition['value']) && array_key_exists($key, $fieldValue))) {
foreach ($condition['value'] as $key => $value) {
if (!(array_key_exists($key, $condition['value']) && array_key_exists($key, $fieldValue))) {
return false;
}
if($condition['value'][$key] == $fieldValue[$key]) {
if ($condition['value'][$key] == $fieldValue[$key]) {
return true;
}
}
@@ -108,8 +108,8 @@ class FormLogicConditionChecker
private function checkMatrixEquals($condition, $fieldValue): bool
{
foreach($condition['value'] as $key => $value) {
if($condition['value'][$key] !== $fieldValue[$key]) {
foreach ($condition['value'] as $key => $value) {
if ($condition['value'][$key] !== $fieldValue[$key]) {
return false;
}
}

View File

@@ -15,6 +15,6 @@ if (!function_exists('front_url')) {
if (!function_exists('pricing_enabled')) {
function pricing_enabled(): bool
{
return App::environment() !== 'testing' && !is_null(config('cashier.key'));
return !is_null(config('cashier.key'));
}
}