typo3-powermail

Expert guidance on TYPO3 Powermail 13+ form extension. Creating forms, custom finishers, validators, spam protection, ViewHelpers, PSR-14 events, TypoScript configuration, email templates, backend modules, and extension development. Use when working with powermail forms, mail handling, form validation, or extending powermail functionality.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "typo3-powermail" with this command: npx skills add dirnbauer/webconsulting-skills/dirnbauer-webconsulting-skills-typo3-powermail

TYPO3 Powermail Development

Compatibility: TYPO3 v13.x and v14.x with Powermail 13.x All code examples target PHP 8.2+ and TYPO3 v13/v14.

TYPO3 API First: Always use TYPO3's built-in APIs, core features, and established conventions before creating custom implementations. Do not reinvent what TYPO3 already provides. Always verify that the APIs and methods you use exist and are not deprecated in your target TYPO3 version (v13 or v14) by checking the official TYPO3 documentation.

Supplements:

  • SKILL-CONDITIONS.md - Conditional field/page visibility (powermail_cond)
  • SKILL-PHP84.md - PHP 8.4 patterns (property hooks, asymmetric visibility, new array functions)
  • SKILL-EXAMPLES.md - Multi-step shop form with Austrian legal types, DDEV SQL + DataHandler CLI

1. Architecture Overview

Domain Model Hierarchy

Form (tx_powermail_domain_model_form)
 └── Page (tx_powermail_domain_model_page)
      └── Field (tx_powermail_domain_model_field)

Mail (tx_powermail_domain_model_mail)
 └── Answer (tx_powermail_domain_model_answer)
      └── references Field

Plugin Registration

  • Pi1 (cached/uncached): form, create, confirmation, optinConfirm, disclaimer
  • Pi5 (uncached): marketing (AJAX tracking)

Composer

composer require in2code/powermail

Requires: PHP ^8.2, TYPO3 ^13.4, ext-json, ext-gd, ext-fileinfo, ext-curl

2. Field Types

TypeKeyValue TypeNotes
TextinputTEXT (0)Standard input
TextareatextareaTEXT (0)Multi-line
SelectselectTEXT/ARRAY (0/1)Multiselect possible
CheckboxcheckARRAY (1)Multiple values
RadioradioTEXT (0)Single selection
SubmitsubmitForm submit button
CaptchacaptchaTEXT (0)Built-in CAPTCHA
ResetresetForm reset button
Static texttextDisplay only
Content elementcontentCE reference
HTMLhtmlTEXT (0)Raw HTML
PasswordpasswordPASSWORD (4)Hashed storage
File uploadfileUPLOAD (3)File attachments
HiddenhiddenTEXT (0)Hidden input
DatedateDATE (2)Datepicker
CountrycountryTEXT (0)Country selector
LocationlocationTEXT (0)Geolocation
TypoScripttyposcriptTEXT (0)TS-generated content

Answer Value Types

Answer::VALUE_TYPE_TEXT     = 0;  // String values
Answer::VALUE_TYPE_ARRAY    = 1;  // JSON-encoded arrays (checkboxes, multiselect)
Answer::VALUE_TYPE_DATE     = 2;  // Timestamps
Answer::VALUE_TYPE_UPLOAD   = 3;  // File references
Answer::VALUE_TYPE_PASSWORD = 4;  // Hashed passwords

3. TypoScript Configuration

Essential Settings

plugin.tx_powermail {
    settings {
        setup {
            # Form settings
            main {
                pid = {$plugin.tx_powermail.settings.main.pid}
                form = {$plugin.tx_powermail.settings.main.form}
                confirmation = 0
                optin = 0
                morestep = 0
            }

            # Receiver mail
            receiver {
                enable = 1
                subject = Mail from {firstname} {lastname}
                body = A new mail from your website
                senderNameField = firstname
                senderEmailField = email
                # Override receiver: receiver.overwrite.email = admin@example.com
                # Attach uploads: receiver.attachment = 1
                # Add CC: receiver.overwrite.cc = copy@example.com
            }

            # Sender confirmation mail
            sender {
                enable = 1
                subject = Thank you for your message
                body = We received your submission
                senderName = Website
                senderEmail = noreply@example.com
            }

            # Double Opt-In
            optin {
                subject = Please confirm your submission
                senderName = Website
                senderEmail = noreply@example.com
            }

            # Thank you page
            thx {
                redirect = # Page UID for redirect after submit
            }

            # Spam protection
            spamshield {
                _enable = 1
                indicator {
                    honeypod = 5
                    link = 3
                    name = 3
                    session = 1
                    unique = 2
                    blacklistString = 7
                    blacklistIp = 7
                    rateLimit = 10
                }
                # Factor threshold (0-100): reject if >=
                factor = 75
            }

            # Validation
            misc {
                htmlForLabels = 1
                showOnlyFilledValues = 1
                ajaxSubmit = 0
                file {
                    folder = uploads/tx_powermail/
                    size = 25000000
                    extension = jpg,jpeg,gif,png,tif,txt,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,csv,svg
                }
            }
        }
    }
}

Prefill Fields via TypoScript

plugin.tx_powermail.settings.setup.prefill {
    # By field marker
    email = TEXT
    email.data = TSFE:fe_user|user|email

    firstname = TEXT
    firstname.data = TSFE:fe_user|user|first_name

    # Prefill from GET/POST
    subject = TEXT
    subject.data = GP:subject
}

Marketing Information

plugin.tx_powermail.settings.setup.marketing {
    enable = 1
    # Tracked: refererDomain, referer, country, mobileDevice, frontendLanguage, browserLanguage, pageFunnel
}

4. Custom Finishers

Finishers run after successful form submission, sorted by TypoScript key.

Registration

plugin.tx_powermail.settings.setup.finishers {
    # Lower number = runs first
    0.class = In2code\Powermail\Finisher\RateLimitFinisher
    10.class = In2code\Powermail\Finisher\SaveToAnyTableFinisher
    20.class = In2code\Powermail\Finisher\SendParametersFinisher
    100.class = In2code\Powermail\Finisher\RedirectFinisher

    # Custom finisher
    50.class = Vendor\MyExt\Finisher\CrmFinisher
    50.config {
        apiUrl = https://crm.example.com/api
        apiKey = secret123
    }
}

Creating a Custom Finisher

<?php

declare(strict_types=1);

namespace Vendor\MyExt\Finisher;

use In2code\Powermail\Finisher\AbstractFinisher;
use In2code\Powermail\Finisher\FinisherInterface;
use In2code\Powermail\Domain\Model\Mail;

final class CrmFinisher extends AbstractFinisher implements FinisherInterface
{
    /**
     * Method name MUST end with "Finisher"
     * Can have initialize*Finisher() called before
     */
    public function myCustomFinisher(): void
    {
        /** @var Mail $mail */
        $mail = $this->getMail();
        $settings = $this->getSettings();
        $configuration = $this->getConfiguration(); // TS config.*

        // Access form answers
        foreach ($mail->getAnswers() as $answer) {
            $fieldMarker = $answer->getField()->getMarker();
            $value = $answer->getValue();
            // Process...
        }

        // Access by marker
        $answers = $mail->getAnswersByFieldMarker();
        $email = $answers['email'] ?? null;

        // Check if form was actually submitted (not just displayed)
        if (!$this->isFormSubmitted()) {
            return;
        }
    }
}

Built-in Finishers

ClassKeyPurpose
RateLimitFinisher0Consumes rate limiter tokens
SaveToAnyTableFinisher10Save answers to custom DB tables
SendParametersFinisher20POST form data to external URL
RedirectFinisher100Redirect after submission

SaveToAnyTable Configuration

plugin.tx_powermail.settings.setup.dbEntry {
    1 {
        _enable = TEXT
        _enable.value = 1
        _table = fe_users
        _ifUnique.email = update  # update|skip|none
        username.value = {email}
        email.value = {email}
        first_name.value = {firstname}
        last_name.value = {lastname}
        pid.value = 123
    }
}

5. Custom Validators

Creating a Custom Validator (PSR-14 Event)

<?php

declare(strict_types=1);

namespace Vendor\MyExt\EventListener;

use In2code\Powermail\Events\CustomValidatorEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener('vendor-myext/custom-validator')]
final class CustomValidatorListener
{
    public function __invoke(CustomValidatorEvent $event): void
    {
        $mail = $event->getMail();
        $field = $event->getField();

        // Validate specific field by marker
        if ($field->getMarker() === 'company_vat') {
            $answer = null;
            foreach ($mail->getAnswers() as $a) {
                if ($a->getField()?->getUid() === $field->getUid()) {
                    $answer = $a;
                    break;
                }
            }

            if ($answer !== null && !$this->isValidVat((string)$answer->getValue())) {
                $event->setIsValid(false);
                $event->setValidationMessage('Invalid VAT number');
            }
        }
    }

    private function isValidVat(string $vat): bool
    {
        return (bool)preg_match('/^[A-Z]{2}\d{8,12}$/', $vat);
    }
}

Built-in Validators

ValidatorPurpose
InputValidatorEmail, URL, phone, number, letters, min/max length, regex
UploadValidatorFile size, extension whitelist
PasswordValidatorPassword match and strength
CaptchaValidatorBuilt-in CAPTCHA
SpamShieldValidatorMulti-method spam detection
UniqueValidatorUnique field values
ForeignValidatorValidate against foreign table
CustomValidatorTypoScript-based custom rules

Spam Shield Methods

MethodWeightDescription
HoneyPodMethod5Hidden honeypot field
LinkMethod3Excessive links detection
NameMethod3Suspicious name patterns
SessionMethod1Session token validation
UniqueMethod2Duplicate submission check
ValueBlacklistMethod7Blacklisted content
IpBlacklistMethod7Blacklisted IP addresses
RateLimitMethod10Request rate limiting

6. PSR-14 Events

Form Lifecycle Events

// Before form is rendered
FormControllerFormActionEvent

// Before confirmation page
FormControllerConfirmationActionEvent

// After mail is saved to database
FormControllerCreateActionAfterMailDbSavedEvent

// After submit view is built
FormControllerCreateActionAfterSubmitViewEvent

// Before final view is rendered
FormControllerCreateActionBeforeRenderViewEvent

// Controller initialization
FormControllerInitializeObjectEvent

Mail Events

// Modify receiver email addresses
ReceiverMailReceiverPropertiesServiceSetReceiverEmailsEvent

// Modify receiver name
ReceiverMailReceiverPropertiesServiceGetReceiverNameEvent

// Modify sender email (receiver mail)
ReceiverMailSenderPropertiesGetSenderEmailEvent

// Modify sender name (receiver mail)
ReceiverMailSenderPropertiesGetSenderNameEvent

// Modify sender email (confirmation mail)
SenderMailPropertiesGetSenderEmailEvent

// Modify sender name (confirmation mail)
SenderMailPropertiesGetSenderNameEvent

// Modify email body before sending
SendMailServiceCreateEmailBodyEvent

// Before email is sent (last chance to modify)
SendMailServicePrepareAndSendEvent

Other Events

// Control if mail should be saved to DB
CheckIfMailIsAllowedToSaveEvent

// Custom validation logic
CustomValidatorEvent

// Prefill field values
PrefillFieldViewHelperEvent
PrefillMultiFieldViewHelperEvent

// File upload processing
UploadServicePreflightEvent
UploadServiceGetFilesEvent
GetNewPathAndFilenameEvent

// Before password is hashed
MailFactoryBeforePasswordIsHashedEvent

// Modify mail variables/markers
MailRepositoryGetVariablesWithMarkersFromMailEvent

// Validation data attributes
ValidationDataAttributeViewHelperEvent

// Double opt-in confirmation
FormControllerOptinConfirmActionAfterPersistEvent
FormControllerOptinConfirmActionBeforeRenderViewEvent

// Disclaimer/unsubscribe
FormControllerDisclaimerActionBeforeRenderViewEvent

Example: Modify Receiver Email

<?php

declare(strict_types=1);

namespace Vendor\MyExt\EventListener;

use In2code\Powermail\Events\ReceiverMailReceiverPropertiesServiceSetReceiverEmailsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener('vendor-myext/dynamic-receiver')]
final class DynamicReceiverListener
{
    public function __invoke(
        ReceiverMailReceiverPropertiesServiceSetReceiverEmailsEvent $event
    ): void {
        $mail = $event->getMail();
        $answers = $mail->getAnswersByFieldMarker();

        // Route to department based on form field
        $department = $answers['department'] ?? null;
        if ($department !== null) {
            $value = (string)$department->getValue();
            $emails = match ($value) {
                'sales' => ['sales@example.com'],
                'support' => ['support@example.com'],
                default => $event->getReceiverEmails(),
            };
            $event->setReceiverEmails($emails);
        }
    }
}

Example: Prevent DB Save

<?php

declare(strict_types=1);

namespace Vendor\MyExt\EventListener;

use In2code\Powermail\Events\CheckIfMailIsAllowedToSaveEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener('vendor-myext/skip-db-save')]
final class SkipDbSaveListener
{
    public function __invoke(CheckIfMailIsAllowedToSaveEvent $event): void
    {
        // Skip DB save for specific forms
        $form = $event->getMail()->getForm();
        if ($form !== null && $form->getTitle() === 'Contact (no storage)') {
            $event->setSavingOfMailAllowed(false);
        }
    }
}

7. Email Templates

Template Paths (TypoScript)

plugin.tx_powermail {
    view {
        templateRootPaths {
            0 = EXT:powermail/Resources/Private/Templates/
            10 = EXT:my_ext/Resources/Private/Templates/Powermail/
        }
        partialRootPaths {
            0 = EXT:powermail/Resources/Private/Partials/
            10 = EXT:my_ext/Resources/Private/Partials/Powermail/
        }
        layoutRootPaths {
            0 = EXT:powermail/Resources/Private/Layouts/
            10 = EXT:my_ext/Resources/Private/Layouts/Powermail/
        }
    }
}

Key Templates

TemplatePurpose
Form/Form.htmlMain form rendering
Form/Confirmation.htmlConfirmation page
Form/Create.htmlThank you page
Mail/ReceiverMail.htmlAdmin notification email
Mail/SenderMail.htmlUser confirmation email
Mail/OptinMail.htmlDouble opt-in email
Form/PowermailAll.htmlAll-fields summary

Field Partials

Override individual field types by copying partials:

Partials/Form/Field/Input.html
Partials/Form/Field/Textarea.html
Partials/Form/Field/Select.html
Partials/Form/Field/Check.html
Partials/Form/Field/Radio.html
Partials/Form/Field/File.html
Partials/Form/Field/Date.html
Partials/Form/Field/Captcha.html
Partials/Form/Field/Hidden.html
Partials/Form/Field/Password.html
Partials/Form/Field/Country.html
Partials/Form/Field/Location.html
Partials/Form/Field/Html.html
Partials/Form/Field/Content.html
Partials/Form/Field/Typoscript.html
Partials/Form/Field/Submit.html
Partials/Form/Field/Reset.html

Available Variables in Mail Templates

<!-- In ReceiverMail.html / SenderMail.html -->
{mail}                          <!-- Mail domain object -->
{mail.senderName}               <!-- Sender name -->
{mail.senderMail}               <!-- Sender email -->
{mail.form.title}               <!-- Form title -->
{mail.answers}                  <!-- All answers (ObjectStorage) -->

<!-- Iterate answers -->
<f:for each="{mail.answers}" as="answer">
    {answer.field.title}: {answer.value}
</f:for>

<!-- PowermailAll marker (all fields formatted) -->
{powermail_all}

8. Key ViewHelpers

Validation

<!-- Enable JS validation and/or AJAX submit -->
<vh:validation.enableJavascriptValidationAndAjax
    form="{form}"
    additionalAttributes="{...}" />

<!-- Validation data attributes on fields -->
<vh:validation.validationDataAttribute field="{field}" />

<!-- Error CSS class -->
<vh:validation.errorClass field="{field}" class="error" />

<!-- Upload attributes (accept, multiple) -->
<vh:validation.uploadAttributes field="{field}" />

Form Fields

<!-- Country selector -->
<vh:form.countries
    settings="{settings}"
    field="{field}"
    mail="{mail}" />

<!-- Advanced select with optgroups -->
<vh:form.advancedSelect
    field="{field}"
    mail="{mail}" />

<!-- Multi-upload -->
<vh:form.multiUpload field="{field}" />

Prefill

<!-- Prefill single-value field -->
<vh:misc.prefillField field="{field}" mail="{mail}" />

<!-- Prefill multi-value field (select, check, radio) -->
<vh:misc.prefillMultiField field="{field}" mail="{mail}" cycle="{cycle}" />

Conditions

<!-- Check if field is not empty -->
<vh:condition.isNotEmpty val="{value}">
    <f:then>Has value</f:then>
</vh:condition.isNotEmpty>

<!-- Check if array -->
<vh:condition.isArray val="{value}">
    <f:then>Is array</f:then>
</vh:condition.isArray>

<!-- Check file exists -->
<vh:condition.fileExists file="{path}">
    <f:then>File available</f:then>
</vh:condition.fileExists>

Backend

<!-- Edit link in backend module -->
<vh:be.editLink table="tx_powermail_domain_model_mail" uid="{mail.uid}">
    Edit
</vh:be.editLink>

9. AJAX Form Submission

plugin.tx_powermail.settings.setup.misc.ajaxSubmit = 1

When enabled, form submission is handled via AJAX without page reload. The response replaces the form container with the thank-you content.

10. Double Opt-In

plugin.tx_powermail.settings.setup.main.optin = 1

plugin.tx_powermail.settings.setup.optin {
    subject = Please confirm your submission
    senderName = My Website
    senderEmail = noreply@example.com
}

Flow:

  1. User submits form
  2. Mail is saved with hidden=1
  3. Opt-in email sent with confirmation link (HMAC-secured)
  4. User clicks link -> optinConfirmAction unhides the mail
  5. Receiver email sent after confirmation

11. Backend Module

Powermail provides a backend module under Web > Powermail:

  • List: Browse/filter/search submitted mails
  • Export: CSV and Excel (PhpSpreadsheet) export
  • Reporting: Form analytics and marketing charts
  • System Check: Verify configuration (admin only)

Live Search

Search mails and forms directly from TYPO3 search bar:

  • #mail:searchterm - Search in mails
  • #form:searchterm - Search in forms

12. Extension Best Practices

Register Services (Services.yaml)

services:
  Vendor\MyExt\EventListener\CrmSyncListener:
    tags:
      - name: event.listener
        identifier: 'vendor-myext/crm-sync'

Or use the #[AsEventListener] attribute (preferred in TYPO3 v13+).

Access Mail Answers Efficiently

// By field marker (most common)
$answers = $mail->getAnswersByFieldMarker();
$email = $answers['email']?->getValue();

// By field UID
$answers = $mail->getAnswersByFieldUid();

// Filter by value type
$uploads = $mail->getAnswersByValueType(Answer::VALUE_TYPE_UPLOAD);

Custom Data on Mail Object

// Add custom data (available in all finishers/events)
$mail->addAdditionalData('crm_id', $crmResponse['id']);

// Retrieve in another finisher/event
$crmId = $mail->getAdditionalData()['crm_id'] ?? null;

Rate Limiting

Powermail uses Symfony RateLimiter. Configure in ext_conf_template.txt or extension settings.

Garbage Collection

Powermail auto-registers garbage collection for mails and answers (default: 30 days). Configure via Scheduler task TableGarbageCollectionTask.

13. Common Recipes

Route Enhancer for SEO-Friendly URLs

routeEnhancers:
  PowermailOptIn:
    type: Plugin
    routePath: '/optin/{mail}/{hash}'
    namespace: 'tx_powermail_pi1'
    requirements:
      mail: '\d+'
      hash: '[a-zA-Z0-9]+'

Conditional Receiver Based on Form Field

Use ReceiverMailReceiverPropertiesServiceSetReceiverEmailsEvent (see Section 6).

Custom Spam Shield Method

<?php

declare(strict_types=1);

namespace Vendor\MyExt\SpamShield;

use In2code\Powermail\Domain\Validator\SpamShield\AbstractMethod;

final class ApiCheckMethod extends AbstractMethod
{
    public function spamCheck(): bool
    {
        $mail = $this->getMail();
        // Return true if spam detected
        return $this->callExternalApi($mail);
    }
}

Register in TypoScript:

plugin.tx_powermail.settings.setup.spamshield.methods {
    100 {
        class = Vendor\MyExt\SpamShield\ApiCheckMethod
        _enable = 1
        configuration {
            apiUrl = https://spam-api.example.com
        }
    }
}

Extend Form with TypoScript-Generated Fields

plugin.tx_powermail.settings.setup.manipulateVariablesInPowermailAllMarker {
    timestamp = TEXT
    timestamp.data = date:U
    timestamp.strftime = %Y-%m-%d %H:%M:%S
}

14. Database Structure

Conditions tables: See SKILL-CONDITIONS.md Section 12 for tx_powermailcond_* tables.

TYPO3 Standard Columns

All powermail tables include these TYPO3-managed columns (not listed per table below):

ColumnTypePurpose
uidint AUTO_INCREMENTPrimary key
pidintStorage page UID
tstampintLast modification timestamp
crdateintCreation timestamp
deletedtinyintSoft-delete flag
hiddentinyintVisibility flag
sys_language_uidintLanguage UID (0 = default, -1 = all)
l10n_parentintUID of the default language record
l10n_diffsourcemediumblobDiff source for translation
starttimeintPublish start (Unix timestamp)
endtimeintPublish end (Unix timestamp)

tx_powermail_domain_model_form

ColumnTypeDescription
titlevarchar(255)Form title
notetinyintBackend note renderer (internal)
cssvarchar(255)CSS class for form wrapper
pagesvarchar(255)IRRE children count or element browser list
autocomplete_tokenvarchar(3)Autocomplete on/off/empty
is_dummy_recordtinyintTest record flag

Indexes: language (l10n_parent, sys_language_uid)

tx_powermail_domain_model_page

ColumnTypeDescription
formintParent form UID
titlevarchar(255)Page/step title
cssvarchar(255)CSS class for fieldset
fieldsintIRRE children count
sortingintSort order within form

Indexes: parent_form (form), language (l10n_parent, sys_language_uid)

tx_powermail_domain_model_field

ColumnTypeDescription
pageintParent page UID
titlevarchar(255)Field label
typevarchar(255)Field type key (input, select, check, ...)
settingstextOptions for select/radio/check (one per line)
pathvarchar(255)File path reference
content_elementintCE reference for type=content
texttextStatic text for type=text
prefill_valuetextDefault/prefill value
placeholdertextPlaceholder text
placeholder_repeattextPlaceholder for repeat field (password)
create_from_typoscripttextTypoScript for type=typoscript
validationintValidation type (0=none, 1=email, ...)
validation_configurationvarchar(255)Regex or config for validation
cssvarchar(255)CSS class for field wrapper
descriptionvarchar(255)Help text / description
multiselecttinyintAllow multi-select
datepicker_settingsvarchar(255)Datepicker format
feuser_valuevarchar(255)Prefill from fe_user property
sender_emailtinyintThis field is the sender email
sender_nametinyintThis field is the sender name
mandatorytinyintRequired field
own_marker_selecttinyintCustom marker enabled
markervarchar(255)Field marker (variable name)
mandatory_textvarchar(255)Custom mandatory error text
autocomplete_tokenvarchar(20)Autocomplete attribute
autocomplete_sectionvarchar(100)Autocomplete section
autocomplete_typevarchar(8)Autocomplete type
autocomplete_purposevarchar(8)Autocomplete purpose
sortingintSort order within page

Indexes: parent_page (page), language (l10n_parent, sys_language_uid)

tx_powermail_domain_model_mail

ColumnTypeDescription
sender_namevarchar(255)Submitter name
sender_mailvarchar(255)Submitter email
subjectvarchar(255)Mail subject
receiver_mailvarchar(1024)Receiver email(s)
bodytextMail body (RTE)
feuserintFrontend user UID (if logged in)
sender_iptinytextSubmitter IP address
user_agenttextBrowser user agent
timeintSubmission timestamp
formintSource form UID
answersintIRRE children count
spam_factorvarchar(255)Spam score
marketing_referer_domaintextHTTP referer domain
marketing_referertextFull HTTP referer
marketing_countrytextVisitor country
marketing_mobile_devicetinyintMobile device flag
marketing_frontend_languageintFrontend language UID
marketing_browser_languagetextBrowser Accept-Language
marketing_page_funneltextPages visited before submit

Indexes: form (form), feuser (feuser)

tx_powermail_domain_model_answer

ColumnTypeDescription
mailintParent mail UID
valuetextAnswer value (JSON for arrays)
value_typeint0=text, 1=array, 2=date, 3=upload, 4=password
fieldintSource field UID

Indexes: mail (mail), deleted (deleted), hidden (hidden), language (l10n_parent, sys_language_uid)

ER Diagram (Relations)

tx_powermail_domain_model_form
  │ 1
  ├──── * tx_powermail_domain_model_page (IRRE via form)
  │       │ 1
  │       └──── * tx_powermail_domain_model_field (IRRE via page)
  │                    │
  │                    │ referenced by
  │                    ▼
  │             tx_powermail_domain_model_answer.field
  │
  └──── * tx_powermail_domain_model_mail (via form)
           │ 1
           └──── * tx_powermail_domain_model_answer (IRRE via mail)

15. Workspace Support

Powermail records (forms, pages, fields) fully support TYPO3 workspaces. When EXT:workspaces is installed, editors can draft form changes in a workspace and publish them after review.

Key points:

  • All powermail tables gain t3ver_wsid, t3ver_oid, t3ver_state, t3ver_stage columns
  • Records with t3ver_wsid > 0 are drafts (not visible in live frontend)
  • Use DataHandler for workspace operations — it handles versioning automatically
  • Raw SQL requires manually setting all t3ver_* columns on every INSERT
  • Conditions (powermail_cond) must be in the same workspace as the form

Workspace lifecycle:

  1. Create records in workspace → t3ver_wsid = <ws_id>, t3ver_state = 1
  2. Stage for review → t3ver_stage = 1
  3. Publish via backend module or CLI → records become live (t3ver_wsid = 0)

Detailed SQL and DataHandler examples: See SKILL-EXAMPLES.md for complete workspace-aware queries, publishing workflows, and CLI options.

16. Translations (Localization)

Powermail supports full TYPO3 localization. Form structure (form, pages, fields) can be translated so editors see localized labels, settings, and options. Submitted mails inherit the frontend language.

How Translation Works

LevelWhat gets translatedKey columns
FormTitlesys_language_uid, l10n_parent
PageTitle (step heading)sys_language_uid, l10n_parent
FieldTitle, settings, placeholder, mandatory_text, descriptionsys_language_uid, l10n_parent
MailAutomatically stored with sys_language_uid from frontendsys_language_uid
AnswerStored with language of submissionsys_language_uid

Translation Rules

  • sys_language_uid = 0 is the default language (e.g., English)
  • sys_language_uid = 1 (or higher) is a translation (e.g., German)
  • l10n_parent points to the default language record UID
  • The marker field is not translated -- markers stay identical across languages
  • Field type is not translated -- structure is shared
  • Field settings (select/radio options) is translated -- option labels change per language

Example: Create Form in English, Translate to German

Default Language (English, sys_language_uid=0)

-- Form
INSERT INTO tx_powermail_domain_model_form (pid, title, sys_language_uid, l10n_parent)
VALUES (1, 'Contact Form', 0, 0);
-- Assume UID = 10

-- Page
INSERT INTO tx_powermail_domain_model_page (pid, form, title, sorting, sys_language_uid, l10n_parent)
VALUES (1, 10, 'Your Details', 1, 0, 0);
-- Assume UID = 20

-- Fields
INSERT INTO tx_powermail_domain_model_field
  (pid, page, title, type, marker, mandatory, sender_name, sorting, sys_language_uid, l10n_parent)
VALUES
  (1, 20, 'First Name', 'input', 'firstname', 1, 1, 1, 0, 0),   -- UID 30
  (1, 20, 'Last Name', 'input', 'lastname', 1, 0, 2, 0, 0),     -- UID 31
  (1, 20, 'Email', 'input', 'email', 1, 0, 3, 0, 0),            -- UID 32
  (1, 20, 'Message', 'textarea', 'message', 0, 0, 4, 0, 0),     -- UID 33
  (1, 20, 'Subject', 'select', 'subject', 1, 0, 5, 0, 0),       -- UID 34
  (1, 20, 'Send', 'submit', 'submit', 0, 0, 6, 0, 0);           -- UID 35

-- Select options for subject (English)
UPDATE tx_powermail_domain_model_field
SET settings = 'General Inquiry\nSupport Request\nPartnership\nOther'
WHERE uid = 34;

-- Mark email field as sender_email
UPDATE tx_powermail_domain_model_field SET sender_email = 1 WHERE uid = 32;

German Translation (sys_language_uid=1)

-- Form translation (l10n_parent = 10, the English form)
INSERT INTO tx_powermail_domain_model_form (pid, title, sys_language_uid, l10n_parent)
VALUES (1, 'Kontaktformular', 1, 10);

-- Page translation (l10n_parent = 20)
INSERT INTO tx_powermail_domain_model_page
  (pid, form, title, sorting, sys_language_uid, l10n_parent)
VALUES (1, 10, 'Ihre Daten', 1, 1, 20);

-- Field translations (l10n_parent points to English field UID)
INSERT INTO tx_powermail_domain_model_field
  (pid, page, title, type, marker, mandatory, sender_name, sorting, sys_language_uid, l10n_parent)
VALUES
  (1, 20, 'Vorname', 'input', 'firstname', 1, 1, 1, 1, 30),
  (1, 20, 'Nachname', 'input', 'lastname', 1, 0, 2, 1, 31),
  (1, 20, 'E-Mail-Adresse', 'input', 'email', 1, 0, 3, 1, 32),
  (1, 20, 'Nachricht', 'textarea', 'message', 0, 0, 4, 1, 33),
  (1, 20, 'Betreff', 'select', 'subject', 1, 0, 5, 1, 34),
  (1, 20, 'Absenden', 'submit', 'submit', 0, 0, 6, 1, 35);

-- German select options for subject
UPDATE tx_powermail_domain_model_field
SET settings = 'Allgemeine Anfrage\nSupportanfrage\nPartnerschaft\nSonstiges'
WHERE sys_language_uid = 1 AND l10n_parent = 34;

-- Mark email field as sender_email (must be set on translation too)
UPDATE tx_powermail_domain_model_field
SET sender_email = 1
WHERE sys_language_uid = 1 AND l10n_parent = 32;

Translation via DataHandler

<?php

declare(strict_types=1);

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start([], []);

// Localize form (UID 10) to German (sys_language_uid=1)
$cmdMap = [
    'tx_powermail_domain_model_form' => [
        10 => [
            'localize' => 1, // target language UID
        ],
    ],
];
$dataHandler->start([], $cmdMap);
$dataHandler->process_cmdmap();

// DataHandler auto-creates translations of all IRRE children (pages + fields)
// Then update the translated titles:
$translatedFormUid = $dataHandler->copyMappingArray_merged['tx_powermail_domain_model_form'][10] ?? null;
if ($translatedFormUid) {
    $data = [
        'tx_powermail_domain_model_form' => [
            $translatedFormUid => [
                'title' => 'Kontaktformular',
            ],
        ],
    ];
    $dataHandler->start($data, []);
    $dataHandler->process_datamap();
}

Important Translation Notes

  • Markers are language-independent. The marker email stays email in all languages.
  • IRRE localization: When you localize a form via DataHandler (localize command), TYPO3 automatically creates translations for all child pages and fields.
  • Select options: The settings field (select/radio/check options) must be translated separately -- option values should match (for condition evaluation) but labels can differ.
  • Submitted mails: Mails store sys_language_uid from the frontend context. Answers reference the default-language field UID regardless of submission language.
  • Backend module: The mail list shows mails from all languages. Filter by language if needed.

17. Full Example: Multi-Step Shop Form with Conditions

For a comprehensive multi-step mini-shop example with Austrian legal types (Gesellschaftsformen), conditional fields per legal type, GDPR compliance, and two implementation approaches (DDEV SQL + DataHandler CLI command), see SKILL-EXAMPLES.md.

v14-Only Changes

The following Powermail-related changes apply when running on TYPO3 v14 only.

EXT:form Hooks Removed [v14 only]

If your Powermail extensions also interact with EXT:form, note that all EXT:form hooks are removed in v14:

  • beforeRendering, afterSubmit, initializeFormElement
  • beforeFormSave, beforeFormDelete, beforeFormDuplicate, beforeFormCreate
  • afterBuildingFinished, beforeRemoveFromParentRenderable

These are replaced by corresponding PSR-14 events (e.g., BeforeFormIsSavedEvent, BeforeRenderableIsRenderedEvent).

AbstractFinisher Changes [v14 only]

AbstractFinisher->getTypoScriptFrontendController() is removed (#107507). Finishers needing request context must use the PSR-7 request from the form runtime instead of $GLOBALS['TSFE'].

EXT:form Storage Adapters [v14.1+ only]

TYPO3 v14.1 introduces Storage Adapters for EXT:form, allowing pluggable storage backends for form definitions. This may affect how Powermail and EXT:form coexist in projects using both.

Fluid 5.0 Template Compatibility [v14 only]

Powermail Fluid templates must comply with Fluid 5.0 strict typing:

  • ViewHelper arguments are strictly typed (integers vs strings matter).
  • No underscore-prefixed variables in Fluid templates.
  • Verify custom Fluid partials and templates for type mismatches.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

typo3-ddev

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

cli-tools

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

programmatic-seo

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-refactoring-refactor-clean

No summary provided by upstream source.

Repository SourceNeeds Review