Search by

ipf / newsman

ipf

Newsman subscription extension with Mailman integration

Package info

github.com/ipf/typo3-newsman

Type:typo3-cms-extension

pkg:composer/ipf/newsman

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-09-27 12:56 UTC

This package is auto-updated.

Last update: 2026-09-27 12:59:39 UTC


README

A subscribe button (email field) as a TYPO3 content element, subscribing visitors to a remote GNU Mailman 3 mailing list.

  • Extension key: newsman
  • Namespace: Ipf\NewsMan\
  • Package: ipf/newsman
  • Supports TYPO3 13.4 and 14.x
  • Full documentation: Documentation/ (reStructuredText, TYPO3 Sphinx tooling)

The extension is standalone: it ships its own content element, FlexForm, template and CSS, and stores nothing in the TYPO3 database. Mailman remains the single source of truth for the subscribers.

Installation

composer require ipf/newsman

Then activate it:

vendor/bin/typo3 extension:setup

Configuration

The settings are declared in ext_conf_template.txt. The install tool keeps $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['newsman'] in sync with it, and a site overrides individual values in config/system/settings.php. Because the keys come from the template, the install tool does not drop them when it rewrites the configuration.

The connection details are read in the constructor of MailmanService through constructor injection; the extension does not use GeneralUtility::makeInstance().

Key Default Purpose
mode rest rest = Mailman 3 REST API, email = Mailman 2 style command mail
apiUrl '' Base URL of the REST API, e.g. https://mailman.example.com/3.0
apiUser '' REST user (needs the Mailman admin role)
apiPassword '' Password for apiUser
authToken '' Bearer token; takes precedence over user/password when set
verifySsl 1 TLS certificate validation
timeout 10 HTTP timeout in seconds
emailDomain '' Only for mode = email: the list domain, e.g. example.com
emailCommand subscribe Only for mode = email: subscribe or request, see below
emailSender '' Only for mode = email with emailCommand = request: the sender of the command mail

Read the values from the environment so nothing secret ends up in the repository:

// config/system/settings.php
'EXTENSIONS' => [
    'newsman' => [
        'mode' => getenv('MAILMAN_MODE') ?: 'rest',
        'apiUrl' => getenv('MAILMAN_API_URL') ?: '',
        'apiUser' => getenv('MAILMAN_API_USER') ?: '',
        'apiPassword' => getenv('MAILMAN_API_PASSWORD') ?: '',
        'authToken' => getenv('MAILMAN_AUTH_TOKEN') ?: '',
        'verifySsl' => getenv('MAILMAN_VERIFY_SSL') !== 'false',
        'timeout' => (int)(getenv('MAILMAN_TIMEOUT') ?: 10),
        'emailDomain' => getenv('MAILMAN_EMAIL_DOMAIN') ?: '',
        'emailCommand' => getenv('MAILMAN_EMAIL_COMMAND') ?: 'subscribe',
        'emailSender' => getenv('MAILMAN_EMAIL_SENDER') ?: '',
    ],
],

The settings can also be edited in the Extension Manager under Configure Extension.

With apiUrl empty the plugin shows a "not configured" message instead of failing silently, so a forgotten configuration is visible rather than a dead form.

Content element

Create a content element → Newsman Subscribe (CType: newsman_subscribe).

FlexForm settings:

Setting Description
list The mailing list, e.g. newsletter@example.com
successMessage Optional text shown instead of the default success message
emailCommand Overrides emailCommand for this element (email mode)
emailSender Overrides emailSender for this element (email mode)
emailLabel Text of the field label
emailPlaceholder Placeholder of the input
buttonLabel Text of the submit button

The three form texts are plain texts rather than labels, because the site brings its own wording. Both the posting address (newsletter@example.com) and Mailman's list id (newsletter.example.com, optionally prefixed with list:) are accepted.

Mailman 2 (mode email)

Mailman 2 has no REST API, so mode = email subscribes by sending one command mail; the list server then mails the visitor a confirmation the extension never sees. emailCommand picks how that mail is addressed:

emailCommand Mail Note
subscribe (default) to <list>-subscribe@<domain>, address in the From: header Fails SPF/DMARC at most hosted list servers, because the mail claims to come from the visitor while it comes from the web host
request to <list>-request@<domain>, body subscribe <address>, sender emailSender The address travels in the body, so the mail passes those checks. Needs a valid emailSender

request requires emailSender, because that mail is sent on behalf of the site and the list server answers the site operator, not the visitor. Without it the form reports "the sender of the command email is not configured" rather than sending a mail that would only be rejected. Both settings can be overridden per element, so one site can mix them; an empty FlexForm field means the global value applies.

Styling

The template loads the stylesheet itself with <f:asset.css>, so no TypoScript is needed; EXT:newsman/Resources/Public/Css/newsman.css is published with the page. Classes: newsman-subscribe, newsman-subscribe__form, newsman-subscribe__field, newsman-subscribe__label, newsman-subscribe__input, newsman-subscribe__submit, newsman-subscribe__message--{success,error}.

Mailman setup (server side)

The REST user needs the admin role:

mailman shell
>>> from mailman.rest.auth import add_member
>>> add_member('example.com', 'restadmin', 'secret')   # domain, user, password

The extension subscribes with pre_verified, pre_confirmed and pre_approved set, so the visitor becomes a member immediately. For a double opt-in flow, send those as "false" in MailmanService::subscribeViaRest() and let Mailman send the confirmation mail.

Site integration notes

The plugin is registered as EXTBASEPLUGIN from the extension's own Configuration/TypoScript/setup.typoscript — deliberately not via ExtensionUtility::configurePlugin(). That helper emits tt_content.newsman_subscribe =< lib.contentElement with templateName = Generic at defaultContentRendering, i.e. after every site configuration. A site package that redefines lib.contentElement as a FLUIDTEMPLATE (a common pattern that derives the template name from the CType) would then be overridden, the numbered 20 = EXTBASEPLUGIN child would be dropped by FLUIDTEMPLATE, and the element would render empty.

Because site configuration is applied after extension TypoScript, a site package that dispatches content elements through a CASE has to repeat the key itself. Such a site package can render the plugin as a numbered child of its own template, so that the texts and the design of the site can be put around the form:

tt_content = CASE
# ...
tt_content.newsman_subscribe =< lib.contentElement
tt_content.newsman_subscribe {
  templateName = NewsmanSubscribe
  20 = EXTBASEPLUGIN
  20 {
    extensionName = Newsman
    pluginName = Subscribe
  }
}
<!-- Content/NewsmanSubscribe.fluid.html -->
<f:cObject typoscriptObjectPath="tt_content.{data.CType}.20" data="{data}" table="tt_content"/>

FLUIDTEMPLATE does not render numbered children by itself, which is why the template calls the child the way the core does it in EXT:fluid_styled_content/Resources/Private/Templates/Generic.fluid.html.

templateName stays empty on purpose so Extbase resolves the template from controller and action (Resources/Private/Templates/Subscribe/Subscribe.html) instead of a hard-coded name.

The texts of the field and of the button are plain texts of the FlexForm, not labels: the site brings its own wording. The stylesheet of the form is loaded by the template with f:asset.css, because TYPO3 v14 has no configureExtension() for frontend stylesheets any more and the form is only ever rendered inside a page.

Behaviour

Situation Result
New address Member added, success message
Already subscribed "already subscribed" hint, no duplicate
Invalid address Validation error, nothing sent
Mailman unreachable Connection error, no silent failure
Unknown list Points the editor at the list configuration
No REST API at apiUrl Points the operator at apiUrl, not at the list
apiUrl empty "not configured" hint
email mode Command mail sent, visitor confirms with the list server
email mode, request without a sender "sender not configured" hint, no mail sent

Mailman's REST API expects the pre_* flags as strings; real JSON booleans make its validator throw a 500 (lazr.config calls ->lower() on them). The service sends them quoted for that reason.

Local development with DDEV

Optional: a local GNU Mailman 3 core and its admin interface run as DDEV services, so the form can be developed and tested without a hosted instance. The extension ships them as a DDEV add-on in ddev/, which is installed from the project:

ddev add-on get vendor/ipf/newsman/ddev
ddev start

The add-on writes .ddev/.env.web.newsman (the settings the web container reads, apiUrl = http://mailman:8001/3.0) and .ddev/.env.newsman (the credentials of the two services), so nothing has to be configured by hand. ddev add-on remove newsman takes it away again.

ddev describe then shows both services with their URLs and credentials:

Service URL from the host Credentials
mailman https://<project>.ddev.site:8028/3.0 restadmin / restpass
postorius https://<project>.ddev.site:8030 admin / admin

<project> is the name from .ddev/config.yaml; ddev-router routes by hostname, so requests to localhost have to carry the Host header. Ports and credentials are changed in .ddev/.env.newsman, and ddev/README.md has the steps that create the mail domain, the list and the Postorius password.

Admin interface (Postorius)

Lists, members, moderation and settings are managed in Postorius, Mailman 3's web interface, which runs as the second container:

  • <http://.ddev.site:8029> (or https://<project>.ddev.site:8030)
  • user admin, password admin (POSTORIUS_ADMIN_USER / POSTORIUS_ADMIN_PASSWORD)

HyperKitty, the web archive of sent messages, is not part of this image.

Two things about that container are worth knowing, because the image is built to run behind a reverse proxy:

  • It contains neither nginx nor WhiteNoise, so it cannot serve /static on its own. The compose file therefore runs Django's development server with the DEBUG from ddev/newsman/settings_local.py. Use a real reverse proxy (that is what the maxking/mailman-web image adds) for anything but local use.
  • Its settings.py calls gethostbyname("mailman-web") while building ALLOWED_HOSTS, so the container needs that network alias or Django dies on import before reading any environment variable.

Security

  • Form input is only used to call the Mailman API, nothing is persisted.
  • TLS is verified by default; verifySsl should stay enabled in production.
  • Credentials come from the environment, not from versioned files.
  • The form has no captcha, so an open form lets bots subscribe third-party addresses. subugoe/typo3-cap is an optional dependency that puts a proof-of-work challenge in front of the page the form posts to (details).

Deprecation-free notes

  • No ext_emconf.php. For composer packages it is deprecated (TYPO3 deprecation 108345); metadata and constraints live in composer.json, which declares extra.typo3/cms.version and providesPackages so core treats the package as composer-only.
  • ext_conf_template.txt is the supported way to declare settings, so ExtensionConfiguration::get() has values to work with.
  • ExtensionUtility::registerPlugin() is public API. The controller actions are registered through Extbase's documented configuration array rather than ExtensionUtility::registerControllerActions(), which is marked @internal.
  • Configuration/TypoScript/setup.typoscript replaces the TypoScript that ExtensionUtility::configurePlugin() would add.
  • ExtensionConfiguration is constructor injected (it is a registered service), and no $GLOBALS is touched.
  • Templates use the *.fluid.html extension so typo3 fluid:analyze covers them.