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:
@@ -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);
|
||||
|
||||
65
api/app/Http/Requests/Workspace/EmailSettingsRequest.php
Normal file
65
api/app/Http/Requests/Workspace/EmailSettingsRequest.php
Normal 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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class Google
|
||||
|
||||
public function getClient(): Client
|
||||
{
|
||||
if($this->client->isAccessTokenExpired()) {
|
||||
if ($this->client->isAccessTokenExpired()) {
|
||||
$this->refreshToken();
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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('@');
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user