Skip to content

Commit b1273dd

Browse files
committed
Polished email integration, added customer sender mail
1 parent d59809b commit b1273dd

File tree

5 files changed

+181
-68
lines changed

5 files changed

+181
-68
lines changed

api/app/Console/Commands/EmailNotificationMigration.php

+18-11
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,26 @@ class EmailNotificationMigration extends Command
2929
*/
3030
public function handle()
3131
{
32-
FormIntegration::where(function ($query) {
33-
$query->where('integration_id', 'email')
34-
->orWhere('integration_id', 'submission_confirmation');
35-
})->chunk(
36-
100,
37-
function ($integrations) {
38-
foreach ($integrations as $integration) {
39-
$this->line('Process For Form: ' . $integration->form_id . ' - ' . $integration->integration_id . ' - ' . $integration->id);
32+
if (app()->environment('production')) {
33+
if (!$this->confirm('Are you sure you want to run this migration in production?')) {
34+
$this->info('Migration aborted.');
35+
return 0;
36+
}
37+
}
38+
$query = FormIntegration::whereIn('integration_id', ['email', 'submission_confirmation']);
39+
$totalCount = $query->count();
40+
$progressBar = $this->output->createProgressBar($totalCount);
41+
$progressBar->start();
4042

41-
$this->updateIntegration($integration);
42-
}
43+
$query->chunk(100, function ($integrations) use ($progressBar) {
44+
foreach ($integrations as $integration) {
45+
$this->updateIntegration($integration);
46+
$progressBar->advance();
4347
}
44-
);
48+
});
49+
50+
$progressBar->finish();
51+
$this->newLine();
4552

4653
$this->line('Migration Done');
4754
}

api/app/Integrations/Handlers/EmailIntegration.php

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public static function getValidationRules(): array
1717
return [
1818
'send_to' => 'required',
1919
'sender_name' => 'required',
20+
'sender_email' => 'email|nullable',
2021
'subject' => 'required',
2122
'email_content' => 'required',
2223
'include_submission_data' => 'boolean',

api/app/Notifications/Forms/FormEmailNotification.php

+94-47
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111
use Illuminate\Notifications\Notification;
1212
use Illuminate\Support\Str;
1313
use Vinkla\Hashids\Facades\Hashids;
14+
use Symfony\Component\Mime\Email;
1415

1516
class FormEmailNotification extends Notification implements ShouldQueue
1617
{
1718
use Queueable;
1819

1920
public FormSubmitted $event;
2021
public string $mailer;
21-
private $formattedData;
22+
private array $formattedData;
2223

2324
/**
2425
* Create a new notification instance.
@@ -29,15 +30,7 @@ public function __construct(FormSubmitted $event, private $integrationData, stri
2930
{
3031
$this->event = $event;
3132
$this->mailer = $mailer;
32-
33-
$formatter = (new FormSubmissionFormatter($event->form, $event->data))
34-
->createLinks()
35-
->outputStringsOnly()
36-
->useSignedUrlForFiles();
37-
if ($this->integrationData->include_hidden_fields_submission_data ?? false) {
38-
$formatter->showHiddenFields();
39-
}
40-
$this->formattedData = $formatter->getFieldsWithValue();
33+
$this->formattedData = $this->formatSubmissionData();
4134
}
4235

4336
/**
@@ -62,36 +55,52 @@ public function toMail($notifiable)
6255
return (new MailMessage())
6356
->mailer($this->mailer)
6457
->replyTo($this->getReplyToEmail($notifiable->routes['mail']))
65-
->from($this->getFromEmail(), $this->integrationData->sender_name ?? config('app.name'))
58+
->from($this->getFromEmail(), $this->getSenderName())
6659
->subject($this->getSubject())
67-
->markdown('mail.form.email-notification', [
68-
'emailContent' => $this->getEmailContent(),
69-
'fields' => $this->formattedData,
70-
'form' => $this->event->form,
71-
'integrationData' => $this->integrationData,
72-
'noBranding' => $this->event->form->no_branding,
73-
'submission_id' => (isset($this->event->data['submission_id']) && $this->event->data['submission_id']) ? Hashids::encode($this->event->data['submission_id']) : null,
74-
]);
60+
->withSymfonyMessage(function (Email $message) {
61+
$this->addCustomHeaders($message);
62+
})
63+
->markdown('mail.form.email-notification', $this->getMailData());
7564
}
7665

77-
private function getFromEmail()
66+
private function formatSubmissionData(): array
7867
{
79-
if (config('app.self_hosted')) {
80-
return config('mail.from.address');
68+
$formatter = (new FormSubmissionFormatter($this->event->form, $this->event->data))
69+
->createLinks()
70+
->outputStringsOnly()
71+
->useSignedUrlForFiles();
72+
73+
if ($this->integrationData->include_hidden_fields_submission_data ?? false) {
74+
$formatter->showHiddenFields();
8175
}
8276

83-
$originalFromAddress = Str::of(config('mail.from.address'))->explode('@');
77+
return $formatter->getFieldsWithValue();
78+
}
79+
80+
private function getFromEmail(): string
81+
{
82+
if (
83+
config('app.self_hosted')
84+
&& isset($this->integrationData->sender_email)
85+
&& $this->validateEmail($this->integrationData->sender_email)
86+
) {
87+
return $this->integrationData->sender_email;
88+
}
8489

85-
return $originalFromAddress->first() . '+' . time() . '@' . $originalFromAddress->last();
90+
return config('mail.from.address');
8691
}
8792

88-
private function getReplyToEmail($default)
93+
private function getSenderName(): string
94+
{
95+
return $this->integrationData->sender_name ?? config('app.name');
96+
}
97+
98+
private function getReplyToEmail($default): string
8999
{
90100
$replyTo = $this->integrationData->reply_to ?? null;
91101

92102
if ($replyTo) {
93-
$parser = new MentionParser($replyTo, $this->formattedData);
94-
$parsedReplyTo = $parser->parse();
103+
$parsedReplyTo = $this->parseReplyTo($replyTo);
95104
if ($parsedReplyTo && $this->validateEmail($parsedReplyTo)) {
96105
return $parsedReplyTo;
97106
}
@@ -100,40 +109,78 @@ private function getReplyToEmail($default)
100109
return $this->getRespondentEmail() ?? $default;
101110
}
102111

103-
private function getSubject()
112+
private function parseReplyTo(string $replyTo): ?string
104113
{
105-
$parser = new MentionParser($this->integrationData->subject ?? 'New form submission', $this->formattedData);
114+
$parser = new MentionParser($replyTo, $this->formattedData);
106115
return $parser->parse();
107116
}
108117

109-
private function getRespondentEmail()
118+
private function getSubject(): string
110119
{
111-
// Make sure we only have one email field in the form
112-
$emailFields = collect($this->event->form->properties)->filter(function ($field) {
113-
$hidden = $field['hidden'] ?? false;
114-
115-
return !$hidden && $field['type'] == 'email';
116-
});
117-
if ($emailFields->count() != 1) {
118-
return null;
119-
}
120+
$defaultSubject = 'New form submission';
121+
$parser = new MentionParser($this->integrationData->subject ?? $defaultSubject, $this->formattedData);
122+
return $parser->parse();
123+
}
120124

121-
if (isset($this->event->data[$emailFields->first()['id']])) {
122-
$email = $this->event->data[$emailFields->first()['id']];
123-
if ($this->validateEmail($email)) {
124-
return $email;
125-
}
126-
}
125+
private function addCustomHeaders(Email $message): void
126+
{
127+
$formId = $this->event->form->id;
128+
$submissionId = $this->event->data['submission_id'] ?? 'unknown';
129+
$domain = Str::after(config('app.url'), '://');
127130

128-
return null;
131+
$uniquePart = substr(md5($formId . $submissionId), 0, 8);
132+
$messageId = "form-{$formId}-{$uniquePart}@{$domain}";
133+
$references = "form-{$formId}@{$domain}";
134+
135+
$message->getHeaders()->remove('Message-ID');
136+
$message->getHeaders()->addIdHeader('Message-ID', $messageId);
137+
$message->getHeaders()->addTextHeader('References', $references);
129138
}
130139

131-
private function getEmailContent()
140+
private function getMailData(): array
141+
{
142+
return [
143+
'emailContent' => $this->getEmailContent(),
144+
'fields' => $this->formattedData,
145+
'form' => $this->event->form,
146+
'integrationData' => $this->integrationData,
147+
'noBranding' => $this->event->form->no_branding,
148+
'submission_id' => $this->getEncodedSubmissionId(),
149+
];
150+
}
151+
152+
private function getEmailContent(): string
132153
{
133154
$parser = new MentionParser($this->integrationData->email_content ?? '', $this->formattedData);
134155
return $parser->parse();
135156
}
136157

158+
private function getEncodedSubmissionId(): ?string
159+
{
160+
$submissionId = $this->event->data['submission_id'] ?? null;
161+
return $submissionId ? Hashids::encode($submissionId) : null;
162+
}
163+
164+
private function getRespondentEmail(): ?string
165+
{
166+
$emailFields = ['email', 'e-mail', 'mail'];
167+
168+
foreach ($this->formattedData as $field => $value) {
169+
if (in_array(strtolower($field), $emailFields) && $this->validateEmail($value)) {
170+
return $value;
171+
}
172+
}
173+
174+
// If no email field found, search for any field containing a valid email
175+
foreach ($this->formattedData as $value) {
176+
if ($this->validateEmail($value)) {
177+
return $value;
178+
}
179+
}
180+
181+
return null;
182+
}
183+
137184
public static function validateEmail($email): bool
138185
{
139186
return (bool)filter_var($email, FILTER_VALIDATE_EMAIL);

api/resources/views/mail/form/email-notification.blade.php

+3-10
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,12 @@
99
@endif
1010

1111
@if($integrationData->include_submission_data)
12-
Here is the answer:
13-
1412
@foreach($fields as $field)
1513
@if(isset($field['value']))
16-
17-
--------------------------------------------------------------------------------
18-
19-
**{{$field['name']}}**
20-
21-
<p style="white-space: pre-wrap">
22-
{!! is_array($field['value'])?implode(',',$field['value']):$field['value']!!}
14+
<p style="white-space: pre-wrap; border-top: 1px solid #9ca3af;">
15+
<b>{{$field['name']}}</b>
16+
{!! is_array($field['value'])?implode(',',$field['value']):$field['value']!!}
2317
</p>
24-
2518
@endif
2619
@endforeach
2720
@endif

api/tests/Feature/Forms/EmailNotificationTest.php

+65
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,68 @@ function (FormEmailNotification $notification, $channels, $notifiable) {
9898
}
9999
);
100100
});
101+
102+
it('uses custom sender email in self-hosted mode', function () {
103+
config(['app.self_hosted' => true]);
104+
105+
$user = $this->actingAsUser();
106+
$workspace = $this->createUserWorkspace($user);
107+
$form = $this->createForm($user, $workspace);
108+
$customSenderEmail = 'custom@example.com';
109+
$integrationData = $this->createFormIntegration('email', $form->id, [
110+
'send_to' => 'test@test.com',
111+
'sender_name' => 'Custom Sender',
112+
'sender_email' => $customSenderEmail,
113+
'subject' => 'Custom Subject',
114+
'email_content' => 'Custom content',
115+
'include_submission_data' => true,
116+
'include_hidden_fields_submission_data' => false,
117+
'reply_to' => 'reply@example.com',
118+
]);
119+
120+
$formData = FormSubmissionDataFactory::generateSubmissionData($form);
121+
122+
$event = new \App\Events\Forms\FormSubmitted($form, $formData);
123+
$mailable = new FormEmailNotification($event, $integrationData, 'mail');
124+
$notifiable = new AnonymousNotifiable();
125+
$notifiable->route('mail', 'test@test.com');
126+
$renderedMail = $mailable->toMail($notifiable);
127+
128+
expect($renderedMail->from[0])->toBe($customSenderEmail);
129+
expect($renderedMail->from[1])->toBe('Custom Sender');
130+
expect($renderedMail->subject)->toBe('Custom Subject');
131+
expect(trim($renderedMail->render()))->toContain('Custom content');
132+
});
133+
134+
it('does not use custom sender email in non-self-hosted mode', function () {
135+
config(['app.self_hosted' => false]);
136+
config(['mail.from.address' => 'default@example.com']);
137+
138+
$user = $this->actingAsUser();
139+
$workspace = $this->createUserWorkspace($user);
140+
$form = $this->createForm($user, $workspace);
141+
$customSenderEmail = 'custom@example.com';
142+
$integrationData = $this->createFormIntegration('email', $form->id, [
143+
'send_to' => 'test@test.com',
144+
'sender_name' => 'Custom Sender',
145+
'sender_email' => $customSenderEmail,
146+
'subject' => 'Custom Subject',
147+
'email_content' => 'Custom content',
148+
'include_submission_data' => true,
149+
'include_hidden_fields_submission_data' => false,
150+
'reply_to' => 'reply@example.com',
151+
]);
152+
153+
$formData = FormSubmissionDataFactory::generateSubmissionData($form);
154+
155+
$event = new \App\Events\Forms\FormSubmitted($form, $formData);
156+
$mailable = new FormEmailNotification($event, $integrationData, 'mail');
157+
$notifiable = new AnonymousNotifiable();
158+
$notifiable->route('mail', 'test@test.com');
159+
$renderedMail = $mailable->toMail($notifiable);
160+
161+
expect($renderedMail->from[0])->toBe('default@example.com');
162+
expect($renderedMail->from[1])->toBe('Custom Sender');
163+
expect($renderedMail->subject)->toBe('Custom Subject');
164+
expect(trim($renderedMail->render()))->toContain('Custom content');
165+
});

0 commit comments

Comments
 (0)