Skip to content

Commit 87fde1c

Browse files
authored
Merge pull request #3289 from dtretyakov/feat/confirmation-email
feat: Add confirmation email for form respondents
2 parents d385213 + c4aaec3 commit 87fde1c

27 files changed

Lines changed: 1825 additions & 40 deletions

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ 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,
181+
"confirmationEmailQuestionId": null,
178182
"permissions": [
179183
"edit",
180184
"results",

docs/DataStructure.md

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,35 @@ This document describes the Object-Structure, that is used within the Forms App
1313

1414
### Form
1515

16-
| Property | Type | Restrictions | Description |
17-
| -------------------- | ------------------------------------ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
18-
| id | Integer | unique | An instance-wide unique id of the form |
19-
| hash | 16-char String | unique | An instance-wide unique hash |
20-
| title | String | max. 256 ch. | The form title |
21-
| description | String | max. 8192 ch. | The Form description |
22-
| ownerId | String | | The nextcloud userId of the form owner |
23-
| 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-
| created | unix timestamp | | When the form has been created |
25-
| access | [Access-Object](#access-object) | | Describing access-settings of the form |
26-
| expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ |
27-
| isAnonymous | Boolean | | If Answers will be stored anonymously |
28-
| state | Integer | [Form state](#form-state) | The state of the form |
29-
| lockedBy | String | | The user ID for who has exclusive edit access at the moment |
30-
| lockedUntil | unix timestamp | | When the form lock will expire |
31-
| submitMultiple | Boolean | | If users are allowed to submit multiple times to the form |
32-
| allowEditSubmissions | Boolean | | If users are allowed to edit or delete their response |
33-
| showExpiration | Boolean | | If the expiration date will be shown on the form |
34-
| canSubmit | Boolean | | If the user can Submit to the form, i.e. calculated information out of `submitMultiple` and existing submissions. |
35-
| permissions | Array of [Permissions](#permissions) | Array of permissions regarding the form |
36-
| questions | Array of [Questions](#question) | | Array of questions belonging to the form |
37-
| shares | Array of [Shares](#share) | | Array of shares of the form |
38-
| submissions | Array of [Submissions](#submission) | | Array of submissions belonging to the form |
39-
| submissionCount | Integer | | Number of submissions to the form |
40-
| submissionMessage | String | | Message to show after a submission |
16+
| Property | Type | Restrictions | Description |
17+
| --------------------------- | ------------------------------------ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
18+
| id | Integer | unique | An instance-wide unique id of the form |
19+
| hash | 16-char String | unique | An instance-wide unique hash |
20+
| title | String | max. 256 ch. | The form title |
21+
| description | String | max. 8192 ch. | The Form description |
22+
| ownerId | String | | The nextcloud userId of the form owner |
23+
| 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) |
27+
| confirmationEmailQuestionId | Integer | | The id of the question belonging to the form, that holds the email address of the respondent |
28+
| created | unix timestamp | | When the form has been created |
29+
| access | [Access-Object](#access-object) | | Describing access-settings of the form |
30+
| expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ |
31+
| isAnonymous | Boolean | | If Answers will be stored anonymously |
32+
| state | Integer | [Form state](#form-state) | The state of the form |
33+
| lockedBy | String | | The user ID for who has exclusive edit access at the moment |
34+
| lockedUntil | unix timestamp | | When the form lock will expire |
35+
| submitMultiple | Boolean | | If users are allowed to submit multiple times to the form |
36+
| allowEditSubmissions | Boolean | | If users are allowed to edit or delete their response |
37+
| showExpiration | Boolean | | If the expiration date will be shown on the form |
38+
| canSubmit | Boolean | | If the user can Submit to the form, i.e. calculated information out of `submitMultiple` and existing submissions. |
39+
| permissions | Array of [Permissions](#permissions) | Array of permissions regarding the form |
40+
| questions | Array of [Questions](#question) | | Array of questions belonging to the form |
41+
| shares | Array of [Shares](#share) | | Array of shares of the form |
42+
| submissions | Array of [Submissions](#submission) | | Array of submissions belonging to the form |
43+
| submissionCount | Integer | | Number of submissions to the form |
44+
| submissionMessage | String | | Message to show after a submission |
4145

4246
```
4347
{
@@ -46,6 +50,10 @@ This document describes the Object-Structure, that is used within the Forms App
4650
"title": "Form 1",
4751
"description": "Description Text",
4852
"ownerId": "jonas",
53+
"confirmationEmailEnabled": false,
54+
"confirmationEmailSubject": null,
55+
"confirmationEmailBody": null,
56+
"confirmationEmailQuestionId": null,
4957
"created": 1611240961,
5058
"access": {},
5159
"expires": 0,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Forms\BackgroundJob;
10+
11+
use OCP\AppFramework\Utility\ITimeFactory;
12+
use OCP\BackgroundJob\QueuedJob;
13+
use OCP\Defaults;
14+
use OCP\Mail\IMailer;
15+
use OCP\Util;
16+
use Psr\Log\LoggerInterface;
17+
18+
class SendConfirmationMailJob extends QueuedJob {
19+
public function __construct(
20+
ITimeFactory $time,
21+
private IMailer $mailer,
22+
private LoggerInterface $logger,
23+
private Defaults $defaults,
24+
) {
25+
parent::__construct($time);
26+
}
27+
28+
/**
29+
* @param array{recipient: string, subject: string, body: string, formId: int, submissionId: int} $argument
30+
*/
31+
public function run($argument): void {
32+
$recipient = $argument['recipient'];
33+
$subject = $argument['subject'];
34+
$body = $argument['body'];
35+
$formId = $argument['formId'];
36+
$submissionId = $argument['submissionId'];
37+
38+
try {
39+
$emailTemplate = $this->mailer->createEMailTemplate('forms.Confirmation');
40+
$emailTemplate->setSubject($subject);
41+
$emailTemplate->addHeader();
42+
$emailTemplate->addHeading($subject);
43+
$emailTemplate->addBodyText($body);
44+
$emailTemplate->addFooter();
45+
46+
$message = $this->mailer->createMessage();
47+
$message->setFrom([Util::getDefaultEmailAddress('noreply') => $this->defaults->getName()]);
48+
$message->setTo([$recipient]);
49+
$message->useTemplate($emailTemplate);
50+
$this->mailer->send($message);
51+
$this->logger->debug('Confirmation email sent successfully', [
52+
'formId' => $formId,
53+
'submissionId' => $submissionId,
54+
]);
55+
} catch (\Exception $e) {
56+
$this->logger->error('Error while sending confirmation email', [
57+
'exception' => $e,
58+
'formId' => $formId,
59+
'submissionId' => $submissionId,
60+
]);
61+
}
62+
}
63+
}

lib/Constants.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ class Constants {
1818
public const CONFIG_KEY_ALLOWSHOWTOALL = 'allowShowToAll';
1919
public const CONFIG_KEY_CREATIONALLOWEDGROUPS = 'creationAllowedGroups';
2020
public const CONFIG_KEY_RESTRICTCREATION = 'restrictCreation';
21+
public const CONFIG_KEY_ALLOWCONFIRMATIONEMAIL = 'allowConfirmationEmail';
22+
public const CONFIG_KEY_CONFIRMATIONEMAILRATELIMIT = 'confirmationEmailRateLimit';
2123
public const CONFIG_KEYS = [
2224
self::CONFIG_KEY_ALLOWPERMITALL,
2325
self::CONFIG_KEY_ALLOWPUBLICLINK,
2426
self::CONFIG_KEY_ALLOWSHOWTOALL,
2527
self::CONFIG_KEY_CREATIONALLOWEDGROUPS,
26-
self::CONFIG_KEY_RESTRICTCREATION
28+
self::CONFIG_KEY_RESTRICTCREATION,
29+
self::CONFIG_KEY_ALLOWCONFIRMATIONEMAIL,
30+
self::CONFIG_KEY_CONFIRMATIONEMAILRATELIMIT,
2731
];
2832

2933
/**
@@ -33,6 +37,8 @@ class Constants {
3337
'formTitle' => 256,
3438
'formDescription' => 8192,
3539
'submissionMessage' => 2048,
40+
'confirmationEmailSubject' => 255,
41+
'confirmationEmailBody' => 8192,
3642
'questionText' => 2048,
3743
'questionDescription' => 4096,
3844
'optionText' => 1024,

0 commit comments

Comments
 (0)