Skip to content

Commit 9de4a5c

Browse files
committed
feat: Add confirmation email for form respondents
Implements issue #525 - Send confirmation emails to form respondents after submission. Features: - Add database migration for confirmation email settings (enabled, subject, body) - Add confirmation email fields to Form entity - Implement email sending with placeholder replacement: - {formTitle}, {formDescription}, {data} placeholders - Field name placeholders (e.g. {name}, {email}) - Automatic data overview if {data} not in template - Add UI in Settings sidebar to configure confirmation emails - Automatically detect email field from form submissions - Send email from form owner's email address Technical changes: - Add sendConfirmationEmail() method to FormsService - Integrate email sending into notifyNewSubmission() flow - Add unit tests for confirmation email functionality - Update FormsService constructor with IMailer and AnswerMapper dependencies - Update documentation (DataStructure.md, CHANGELOG.en.md) The feature requires at least one email-validated short text question in the form. Email sending failures are logged but don't break the submission process. Signed-off-by: Dmitry Tretyakov <dtretyakov@gmail.com>
1 parent 9b027a2 commit 9de4a5c

File tree

14 files changed

+952
-9
lines changed

14 files changed

+952
-9
lines changed

CHANGELOG.en.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55

66
# Changelog
77

8+
## Unreleased
9+
10+
- **Confirmation emails for respondents**
11+
12+
Form owners can enable an automatic confirmation email that is sent to the respondent after a successful submission.
13+
Requires an email-validated short text question in the form.
14+
15+
Supported placeholders in subject/body:
16+
17+
- `{formTitle}`, `{formDescription}`
18+
- `{<fieldName>}` (question `name` or text, sanitized)
19+
820
## v5.2.0 - 2025-09-25
921

1022
- **Time: restrictions and ranges**

docs/API_v3.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ Returns the full-depth object of the requested form (without submissions).
175175
"state": 0,
176176
"lockedBy": null,
177177
"lockedUntil": null,
178+
"confirmationEmailEnabled": false,
179+
"confirmationEmailSubject": null,
180+
"confirmationEmailBody": null,
178181
"permissions": [
179182
"edit",
180183
"results",
@@ -240,6 +243,63 @@ Returns the full-depth object of the requested form (without submissions).
240243
}
241244
```
242245

246+
**Example with confirmation email enabled:**
247+
248+
```
249+
"data": {
250+
"id": 4,
251+
"hash": "abc123def456ghi",
252+
"title": "Contact Form",
253+
"description": "Please fill out this contact form",
254+
"ownerId": "admin",
255+
"submissionMessage": "Thank you for your submission."
256+
"created": 1611240961,
257+
"access": {
258+
"permitAllUsers": false,
259+
"showToAllUsers": false
260+
},
261+
"expires": 0,
262+
"fileFormat": null,
263+
"fileId": null,
264+
"filePath": null,
265+
"isAnonymous": false,
266+
"submitMultiple": false,
267+
"allowEditSubmissions": false,
268+
"showExpiration": false,
269+
"canSubmit": true,
270+
"state": 0,
271+
"lockedBy": null,
272+
"lockedUntil": null,
273+
"confirmationEmailEnabled": true,
274+
"confirmationEmailSubject": "Thank you for submitting {formTitle}",
275+
"confirmationEmailBody": "Dear respondent,\n\nThank you for your submission to {formTitle}.\n\nYour submitted data:\n{data}\n\nBest regards",
276+
"permissions": [
277+
"edit",
278+
"results",
279+
"submit"
280+
],
281+
"questions": [
282+
{
283+
"id": 1,
284+
"formId": 4,
285+
"order": 1,
286+
"type": "short",
287+
"isRequired": true,
288+
"text": "Email address",
289+
"name": "email",
290+
"options": [],
291+
"accept": [],
292+
"extraSettings": {
293+
"validationType": "email"
294+
}
295+
}
296+
],
297+
"shares": [],
298+
"submissionCount": 0,
299+
"submissionMessage": "Thank you for your submission."
300+
}
301+
```
302+
243303
### Clone a form
244304

245305
Creates a clone of a form (without submissions).

docs/DataStructure.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ This document describes the Object-Structure, that is used within the Forms App
2121
| description | String | max. 8192 ch. | The Form description |
2222
| ownerId | String | | The nextcloud userId of the form owner |
2323
| submissionMessage | String | max. 2048 ch. | Optional custom message, with Markdown support, to be shown to users when the form is submitted (default is used if set to null) |
24+
| confirmationEmailEnabled | Boolean | | If enabled, send a confirmation email to the respondent after submission |
25+
| confirmationEmailSubject | String | max. 255 ch. | Optional confirmation email subject template (supports placeholders) |
26+
| confirmationEmailBody | String | | Optional confirmation email body template (plain text, supports placeholders) |
2427
| created | unix timestamp | | When the form has been created |
2528
| access | [Access-Object](#access-object) | | Describing access-settings of the form |
2629
| expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ |
@@ -46,6 +49,9 @@ This document describes the Object-Structure, that is used within the Forms App
4649
"title": "Form 1",
4750
"description": "Description Text",
4851
"ownerId": "jonas",
52+
"confirmationEmailEnabled": false,
53+
"confirmationEmailSubject": null,
54+
"confirmationEmailBody": null,
4955
"created": 1611240961,
5056
"access": {},
5157
"expires": 0,

lib/Db/Form.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
* @method void setLockedBy(string|null $value)
5252
* @method int getLockedUntil()
5353
* @method void setLockedUntil(int|null $value)
54+
* @method int getConfirmationEmailEnabled()
55+
* @method void setConfirmationEmailEnabled(bool $value)
56+
* @method string|null getConfirmationEmailSubject()
57+
* @method void setConfirmationEmailSubject(string|null $value)
58+
* @method string|null getConfirmationEmailBody()
59+
* @method void setConfirmationEmailBody(string|null $value)
5460
*/
5561
class Form extends Entity {
5662
protected $hash;
@@ -71,6 +77,9 @@ class Form extends Entity {
7177
protected $state;
7278
protected $lockedBy;
7379
protected $lockedUntil;
80+
protected $confirmationEmailEnabled;
81+
protected $confirmationEmailSubject;
82+
protected $confirmationEmailBody;
7483

7584
/**
7685
* Form constructor.
@@ -86,6 +95,7 @@ public function __construct() {
8695
$this->addType('state', 'integer');
8796
$this->addType('lockedBy', 'string');
8897
$this->addType('lockedUntil', 'integer');
98+
$this->addType('confirmationEmailEnabled', 'boolean');
8999
}
90100

91101
// JSON-Decoding of access-column.
@@ -159,6 +169,9 @@ public function setAccess(array $access): void {
159169
* state: 0|1|2,
160170
* lockedBy: ?string,
161171
* lockedUntil: ?int,
172+
* confirmationEmailEnabled: bool,
173+
* confirmationEmailSubject: ?string,
174+
* confirmationEmailBody: ?string,
162175
* }
163176
*/
164177
public function read() {
@@ -182,6 +195,9 @@ public function read() {
182195
'state' => $this->getState(),
183196
'lockedBy' => $this->getLockedBy(),
184197
'lockedUntil' => $this->getLockedUntil(),
198+
'confirmationEmailEnabled' => (bool)$this->getConfirmationEmailEnabled(),
199+
'confirmationEmailSubject' => $this->getConfirmationEmailSubject(),
200+
'confirmationEmailBody' => $this->getConfirmationEmailBody(),
185201
];
186202
}
187203
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Forms\Migration;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\Migration\IOutput;
16+
use OCP\Migration\SimpleMigrationStep;
17+
18+
/**
19+
* Add confirmation email fields to forms
20+
*/
21+
class Version050202Date20251217203121 extends SimpleMigrationStep {
22+
23+
/**
24+
* @param IOutput $output
25+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
26+
* @param array $options
27+
* @return null|ISchemaWrapper
28+
*/
29+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
30+
/** @var ISchemaWrapper $schema */
31+
$schema = $schemaClosure();
32+
$table = $schema->getTable('forms_v2_forms');
33+
34+
if (!$table->hasColumn('confirmation_email_enabled')) {
35+
$table->addColumn('confirmation_email_enabled', Types::BOOLEAN, [
36+
'notnull' => false,
37+
'default' => 0,
38+
]);
39+
}
40+
41+
if (!$table->hasColumn('confirmation_email_subject')) {
42+
$table->addColumn('confirmation_email_subject', Types::STRING, [
43+
'notnull' => false,
44+
'default' => null,
45+
'length' => 255,
46+
]);
47+
}
48+
49+
if (!$table->hasColumn('confirmation_email_body')) {
50+
$table->addColumn('confirmation_email_body', Types::TEXT, [
51+
'notnull' => false,
52+
'default' => null,
53+
]);
54+
}
55+
56+
return $schema;
57+
}
58+
}

lib/ResponseDefinitions.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@
136136
* shares: list<FormsShare>,
137137
* submissionCount?: int,
138138
* submissionMessage: ?string,
139+
* confirmationEmailEnabled: bool,
140+
* confirmationEmailSubject: ?string,
141+
* confirmationEmailBody: ?string,
139142
* }
140143
*
141144
* @psalm-type FormsUploadedFile = array{

0 commit comments

Comments
 (0)