Skip to content

feat: Add confirmation email for form respondents#3289

Open
dtretyakov wants to merge 9 commits intonextcloud:mainfrom
dtretyakov:feat/confirmation-email
Open

feat: Add confirmation email for form respondents#3289
dtretyakov wants to merge 9 commits intonextcloud:mainfrom
dtretyakov:feat/confirmation-email

Conversation

@dtretyakov
Copy link
Copy Markdown

@dtretyakov dtretyakov commented Apr 14, 2026

Closes #525 - Send confirmation emails to form respondents after submission.

Features:

  • Add confirmation email settings (enabled, subject, body) to Form entity.
  • Implement email sending with placeholder replacement:
    • {formTitle}, {formDescription}
    • Field placeholders based on name or label (e.g. {name}, {email}).
  • Allow form creators to explicitly select the recipient email field when multiple email fields are present.
  • Add UI in Settings sidebar to configure confirmation emails and recipient selection.
  • Replace basic email validation with Nextcloud's internal IEmailValidator.
  • Integration with activity notifications and background jobs for file syncing.

Technical changes:

  • Database migration for new Form properties.
  • Enhanced FormsService with email sending logic and validation.
  • Extensive unit and integration tests covering the new functionality.
  • Updated API documentation and OpenAPI spec.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 66.82243% with 71 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
lib/Migration/Version050300Date20260413233000.php 0.00% 25 Missing ⚠️
lib/BackgroundJob/SendConfirmationMailJob.php 0.00% 23 Missing ⚠️
lib/Service/FormsService.php 82.92% 21 Missing ⚠️
lib/FormsMigrator.php 83.33% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@dtretyakov dtretyakov marked this pull request as ready for review April 14, 2026 07:10
@dtretyakov
Copy link
Copy Markdown
Author

@Chartman123 I also tried to streamline UX for the feature.

Now the form looks like that:

Снимок экрана 2026-04-14 в 09 04 36

@Chartman123
Copy link
Copy Markdown
Collaborator

@dtretyakov looks good :)

I'll ping the design specialists to see what they think about it :)

@dtretyakov dtretyakov force-pushed the feat/confirmation-email branch from b111145 to 9452b76 Compare April 14, 2026 07:28
@dtretyakov
Copy link
Copy Markdown
Author

OCP\Mail\IEmailValidator breaks backward compatibility for this PR. The interface is not available in nextcloud/ocp on stable31.

For app code here, IMailer::validateMailAddress() is the safer choice: it is already available across the supported server branches and covers the same validation use case without introducing a branch-specific compatibility break.

@Chartman123
Copy link
Copy Markdown
Collaborator

No problem, just ignore this. For the next release we will no longer support NC31

@dtretyakov dtretyakov force-pushed the feat/confirmation-email branch 2 times, most recently from 363a0cb to 9cb9c09 Compare April 21, 2026 17:47
@dtretyakov
Copy link
Copy Markdown
Author

@Chartman123 the branch is rebased on the fresh main and uses OCP\Mail\IEmailValidator.
Please let me know if you're missing something to push it forward and make NC customers happier.

Copy link
Copy Markdown
Collaborator

@Chartman123 Chartman123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick review and found some parts that should be changed in my opinion.

Please also add the new fields to FormsMigrator.php.

Also not sure about the frontend part, especially in the sidebar as I'm no vue expert. I added @susnux and @pringelmann as reviewers :)

Btw no need to hurry, the next release is not yet directly around the corner. We'll get it merged once it's ready :)

Comment thread lib/Migration/Version050301Date20260413233000.php Outdated
Comment thread lib/Migration/Version050301Date20260413233000.php Outdated
Comment thread lib/Constants.php Outdated
];

public const EXTRA_SETTINGS_SHORT = [
'confirmationEmailRecipient' => ['boolean'],
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, if we should add the extraSetting to the whole short question type or if we'd better have it only for *_SHORT_EMAIL. Otherwise it could also be set for e.g. number fields.

@susnux @pringelmann what do you think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also consider an alternative option.

From the domain perspective the form now has the following settings:

  • replyEmail
  • subject
  • body

In the body we could reference form fields.

What if the same logic will be applied to the replyEmail?

In this case:

  • we don't need to use extraSettings at all
  • we'll be storing email field id in the replyEmail form field

Reference: #3289 (comment)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this. It's more uniform than the FK approach and removes the special-case extraSetting entirely. Three template strings, one resolver, done.

One thing to think through: validation gets a bit fuzzier. With an FK we can verify at save time that the referenced question exists and is email-validated. With a placeholder string we either resolve lazily at send time (silent failure if the field was renamed/deleted) or eagerly validate the template against the current question set (which then needs to re-validate on every question edit).

I'd lean toward eager validation in the API layer plus a clear UI affordance in the sidebar - e.g. a dropdown that writes the placeholder for the user rather than letting them type {whatever} freehand. That keeps the "free-form template" flexibility for power users while making the common path foolproof.

Worth doing IMO.

Copy link
Copy Markdown
Author

@dtretyakov dtretyakov Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented that change in the last commit, please check whether it look OK.

Comment thread lib/ResponseDefinitions.php
Comment thread src/components/SidebarTabs/SettingsSidebarTab.vue Outdated
Comment thread src/components/SidebarTabs/SettingsSidebarTab.vue Outdated
Comment thread src/components/SidebarTabs/SettingsSidebarTab.vue Outdated
@Chartman123 Chartman123 added this to the 5.3 milestone Apr 21, 2026
Comment thread lib/Migration/Version050300Date20260413233000.php
Comment thread lib/Service/FormsService.php Outdated
@pringelmann
Copy link
Copy Markdown
Collaborator

One thing I want to flag before this lands: as written, this turns any public form into an unauthenticated outbound mailer. A submitter controls the recipient address (it's just whatever they typed into the email field), so an attacker can script submissions with a victim's address and use the instance to mailbox-bomb them. Worst case it also burns the operator's MX reputation.

A few options, not mutually exclusive:

  • Rate limit per recipient address and/or per form.
  • Restrict the feature to authenticated submitters only (or at least gate it behind a warning when the form is public).
  • Move sending into a queued job so the queue can be paced.

I'd be more comfortable with at least one of these in place before we ship. Could also be worth a line in the PR description spelling out which mitigations apply, since this is a new outbound channel.

$this->eventDispatcher->dispatchTyped(new FormSubmittedEvent($form, $submission));

// Send confirmation email if enabled
$this->sendConfirmationEmail($form, $submission);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs inline here, so any latency lands directly on the user's submission response. A slow MX, a TLS handshake hiccup, or a transient timeout all become user-visible delays. The try/catch swallows errors but not the time spent. Can lead to poor UX.

Perhaps worth moving into a queued job carrying formId + submissionId and resolving the rest there? Side benefit: it makes the throttling issue for mailbox spamming essentially free, since the job queue is already the right place to pace outbound mail.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps worth moving into a queued job carrying formId + submissionId and resolving the rest there? Side benefit: it makes the throttling issue for mailbox spamming essentially free, since the job queue is already the right place to pace outbound mail.

Good idea 👍🏻 a background job sounds better

@Chartman123
Copy link
Copy Markdown
Collaborator

One thing I want to flag before this lands: as written, this turns any public form into an unauthenticated outbound mailer.

@pringelmann Perhaps we should also add an admin setting that can turn the feature on/off and that should be off by default?

Comment thread lib/Migration/Version050300Date20260413233000.php
@pringelmann
Copy link
Copy Markdown
Collaborator

One thing I want to flag before this lands: as written, this turns any public form into an unauthenticated outbound mailer.

@pringelmann Perhaps we should also add an admin setting that can turn the feature on/off and that should be off by default?

Yeah good idea, default-off admin toggle is a sensible safety net and I'd vote for adding it.

That said, I don't think it replaces a per-form/per-recipient mitigation. Once an admin flips it on (which many likely will, because it is a very useful feature for users), the abuse surface is fully open again on every public form on the instance. So it protects the instances that don't need the feature but does nothing for the ones that do.

I'd still suggest at least one of: rate limit per recipient, gate it behind authentication, or pace via a queued job. The admin toggle then becomes a global kill switch on top of that, which is genuinely useful (e.g. an admin can disable the feature instance-wide if they spot abuse in their mail logs)

dtretyakov and others added 4 commits April 23, 2026 22:08
Signed-off-by: Dmitry Tretyakov <dtretyakov@gmail.com>
Co-authored-by: Christian Hartmann <chris-hartmann@gmx.de>
Signed-off-by: Dmitry Tretyakov <dtretyakov@users.noreply.github.com>
Co-authored-by: Christian Hartmann <chris-hartmann@gmx.de>
Signed-off-by: Dmitry Tretyakov <dtretyakov@users.noreply.github.com>
Co-authored-by: Christian Hartmann <chris-hartmann@gmx.de>
Signed-off-by: Dmitry Tretyakov <dtretyakov@users.noreply.github.com>
dtretyakov and others added 3 commits April 23, 2026 22:08
Co-authored-by: Christian Hartmann <chris-hartmann@gmx.de>
Signed-off-by: Dmitry Tretyakov <dtretyakov@users.noreply.github.com>
Signed-off-by: Dmitry Tretyakov <dtretyakov@gmail.com>
Signed-off-by: Dmitry Tretyakov <dtretyakov@gmail.com>
@dtretyakov dtretyakov force-pushed the feat/confirmation-email branch 3 times, most recently from 15e1a4e to c7f7327 Compare April 23, 2026 22:23
Signed-off-by: Dmitry Tretyakov <dtretyakov@gmail.com>
@dtretyakov dtretyakov force-pushed the feat/confirmation-email branch from c7f7327 to 4950eb3 Compare April 23, 2026 22:25
@dtretyakov
Copy link
Copy Markdown
Author

To mitigate the risks associated with automated emails now we have implemented a 4-layer defense strategy:

  1. Administrative Control: A global "kill switch" (allowConfirmationEmail) is available in the admin settings (defaulting to off), allowing instance operators to opt-in or disable the feature entirely.
  2. Burst Protection: We utilize Nextcloud's built-in AnonRateLimit and UserRateLimit middleware on the submission endpoint to prevent rapid-fire scripting attacks from a single IP.
  3. Recipient Safeguard: A strict per-recipient rate limit (max 3 emails per form/recipient pair per 24 hours) is enforced using a distributed cache with a "silent skip" strategy. This prevents a malicious actor from using a form to target a specific email address repeatedly, even when using multiple source IPs.
  4. Asynchronous Processing: Email sending is offloaded to a QueuedJob (SendConfirmationMailJob). This decouples the SMTP transaction from the HTTP response, preventing server hangs due to mail server latency and ensuring natural pacing of outgoing traffic.

If you see this combination is excessive please let me know and we could relax it.

- Relocate confirmationEmailRecipient from Question extraSettings to Form
- Centralize email question validation in Question model and FormsService
- Simplify SettingsSidebarTab component logic
- Ensure correct recipient ID mapping during form cloning
- Add comprehensive unit tests for validation and notification logic

Signed-off-by: Dmitry Tretyakov <dtretyakov@gmail.com>
@dtretyakov dtretyakov force-pushed the feat/confirmation-email branch from 4950eb3 to 33d9af9 Compare April 24, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Confirmation mail for respondents

3 participants