diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6f3e756..200bbc16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ before starting to add changes. Use example [placed in the end of the page](#exa
## [Unreleased]
+- [PR-301](https://github.com/OS2Forms/os2forms/pull/301)
+ Add address information to Digital Post shipments to ensure "*fjernprint*"
+ can be sent.
+
## [5.0.0] 2025-11-18
- [PR-192](https://github.com/OS2Forms/os2forms/pull/192)
diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md
index 999d87c7..16e83513 100644
--- a/modules/os2forms_digital_post/README.md
+++ b/modules/os2forms_digital_post/README.md
@@ -80,3 +80,154 @@ of recipients:
``` shell
drush os2forms-digital-post:test:send --help
```
+
+## Fjernprint (physical digital post)
+
+To comply with the address placement in the envelope window (kuvert-rude) an
+[event subscriber](src/EventSubscriber/Os2formsDigitalPostSubscriber.php) is
+used to inject an address information element into generated HTML before it is
+converted to a PDF.
+
+We are only guaranteed to have the necessary information when in a digital
+post context. For that reason, the injection of address information is only
+done when in a digital post context. Note also that the information is only
+injected – it is not styled. This allows flexibility across installations but
+also means that it is up to individual installations to style it correctly.
+This should be done in OS2Forms Attachment-templates, see
+[Overwriting templates](https://github.com/OS2Forms/os2forms/tree/develop/modules/os2forms_attachment#overwriting-templates).
+
+To see the exact requirements for address placement, see
+[digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf](docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf).
+
+### The injected HTML
+
+Variations of the injected HTML include extended addresses and c/o.
+
+Without extended address information or c/o:
+
+```html
+
+
+
+
Jeppe
+
Test vej HouseNr Floor AppartmentNr
+
2100 Copenhagen
+
+
+```
+
+With just c/o:
+
+```html
+
+
+
+
Jeppe
+
c/o Mikkel
+
Test vej HouseNr Floor AppartmentNr
+
2100 Copenhagen
+
+
+```
+
+
+
+### Styling of the HTML
+
+The following SCSS can be used to style the injected HTML accordingly:
+
+```scss
+$margin-top: 25mm;
+// There is no exact measurement for margin right in the specifications
+$margin-right: 10mm;
+$margin-bottom: 20mm;
+$margin-left: 17mm;
+$page-width: 210mm;
+$page-height: 297mm;
+$envelope-window-height: 89mm;
+$envelope-window-width: 115mm;
+$recipient-window-height: 21mm;
+$recipient-window-width: 59mm;
+
+@page {
+ size: A4;
+ margin: 0;
+}
+
+body {
+ margin-top: $margin-top;
+ margin-right: $margin-right;
+ margin-bottom: $margin-bottom;
+ margin-left: $margin-left;
+}
+
+header {
+ position: fixed;
+ top: 0;
+ height: $margin-top;
+ width: calc($page-width - $margin-left - $margin-right);
+ font-size: 12px;
+}
+
+footer {
+ position: fixed;
+ bottom: 0;
+ height: $margin-bottom;
+ width: calc($page-width - $margin-left - $margin-right);
+ font-size: 12px;
+}
+
+// Style the envelope window that may be injected by Digital Post.
+// Note that top/left is made from the assumption that @page has margin 0.
+// @see \Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber:onPrintRender
+#envelope-window-digital-post {
+ position: absolute;
+ top: $margin-top;
+ left: $margin-left;
+ height: $envelope-window-height;
+ width: $envelope-window-width;
+ background: white
+}
+
+// If envelope window is present, move webform content down
+// @see os2forms_digital_post
+#envelope-window-digital-post ~ * .webform-entity-print-body {
+ margin-top: $envelope-window-height;
+}
+
+// Style the h-card div
+#envelope-window-digital-post > div {
+ position: absolute;
+ top: 16mm;
+ left: 4mm;
+ font-size: 10px;
+ height: $recipient-window-height;
+ width: $recipient-window-width;
+}
+
+// More custom styling...
+```
diff --git a/modules/os2forms_digital_post/docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf b/modules/os2forms_digital_post/docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf
new file mode 100644
index 00000000..9f381c7a
Binary files /dev/null and b/modules/os2forms_digital_post/docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf differ
diff --git a/modules/os2forms_digital_post/os2forms_digital_post.services.yml b/modules/os2forms_digital_post/os2forms_digital_post.services.yml
index 745b88d2..32fdb740 100644
--- a/modules/os2forms_digital_post/os2forms_digital_post.services.yml
+++ b/modules/os2forms_digital_post/os2forms_digital_post.services.yml
@@ -52,6 +52,7 @@ services:
- "@logger.channel.os2forms_digital_post"
- "@logger.channel.os2forms_digital_post_submission"
- "@Drupal\\os2forms_digital_post\\Helper\\DigitalPostHelper"
+ - "@Drupal\\os2forms_digital_post\\EventSubscriber\\Os2formsDigitalPostSubscriber"
Drupal\os2forms_digital_post\Helper\SF1461Helper:
@@ -69,3 +70,9 @@ services:
- '@database'
- '@Drupal\os2forms_digital_post\Helper\MeMoHelper'
- '@logger.channel.os2forms_digital_post'
+
+ Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber:
+ arguments:
+ - '@session'
+ tags:
+ - { name: 'event_subscriber' }
diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php
new file mode 100644
index 00000000..4fefcffa
--- /dev/null
+++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php
@@ -0,0 +1,123 @@
+getHtml();
+
+ // Only modify HTML if there is exactly one submission.
+ if (count($event->getEntities()) === 1) {
+ $submission = $event->getEntities()[0];
+ if ($submission instanceof WebformSubmissionInterface) {
+ // Check whether generation is for digital post.
+ if ($lookupResult = $this->getDigitalPostContext($submission)) {
+
+ // Combine address parts.
+ $streetAddress = $lookupResult->getStreet();
+
+ if ($lookupResult->getHouseNr()) {
+ $streetAddress .= ' ' . $lookupResult->getHouseNr();
+ }
+
+ $extendedAddress = '';
+
+ if ($lookupResult->getFloor()) {
+ $extendedAddress = $lookupResult->getFloor();
+ }
+ if ($lookupResult->getApartmentNr()) {
+ $extendedAddress .= ' ' . $lookupResult->getApartmentNr();
+ }
+
+ // Generate address HTML.
+ $addressHtml = '';
+ $addressHtml .= '
' . htmlspecialchars($lookupResult->getName()) . '
';
+ if ($lookupResult->getCoName()) {
+ $addressHtml .= '
c/o ' . htmlspecialchars($lookupResult->getCoName()) . '
';
+ }
+ $addressHtml .= '
';
+ $addressHtml .= '' . htmlspecialchars($streetAddress) . '';
+ if (!empty($extendedAddress)) {
+ $addressHtml .= ' ' . htmlspecialchars($extendedAddress) . '';
+ }
+ $addressHtml .= '
';
+ $addressHtml .= '
';
+ $addressHtml .= '' . htmlspecialchars($lookupResult->getPostalCode()) . '';
+ $addressHtml .= ' ' . htmlspecialchars($lookupResult->getCity()) . '';
+ $addressHtml .= '
';
+ $addressHtml .= '
';
+ $addressHtml .= '
';
+
+ // Insert address HTML immediately after body opening tag.
+ $html = preg_replace('@]*>@', '${0}' . $addressHtml, $html);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents(): array {
+ return [
+ PrintEvents::POST_RENDER => ['onPrintRender'],
+ ];
+ }
+
+ /**
+ * Indicate Digital Post context in the current session.
+ */
+ public function setDigitalPostContext(WebformSubmissionInterface $submission, CompanyLookupResult|CprLookupResult $lookupResult): void {
+ $key = $this->createSessionKeyFromSubmission($submission);
+ $this->session->set($key, $lookupResult);
+ }
+
+ /**
+ * Check for Digital Post context in the current session.
+ */
+ public function getDigitalPostContext(WebformSubmissionInterface $submission): CompanyLookupResult|CprLookupResult|null {
+ $key = $this->createSessionKeyFromSubmission($submission);
+
+ $digitalPostContext = $this->session->get($key);
+
+ // We only need/use it once, so just remove it after fetching it.
+ if ($digitalPostContext) {
+ $this->session->remove($key);
+ }
+
+ return $digitalPostContext;
+ }
+
+ /**
+ * Create a session key from a submission that is unique to the submission.
+ */
+ public function createSessionKeyFromSubmission(WebformSubmissionInterface $submission): string {
+ // Due to cloning of submission during attachment logic, we cannot use
+ // submission id or uuid. Webform serial, however, is copied along, so a
+ // combination of webform id and serial is used for uniqueness.
+ // @see \Drupal\os2forms_attachment\Element\AttachmentElement::overrideWebformSettings
+ return 'digital_post_context_' . $submission->getWebform()->id() . '_' . $submission->serial();
+ }
+
+}
diff --git a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php
index 60fae6a2..e26e0b1b 100644
--- a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php
+++ b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php
@@ -2,12 +2,15 @@
namespace Drupal\os2forms_digital_post\Helper;
+use Drupal\os2web_datalookup\LookupResult\CompanyLookupResult;
+use Drupal\os2web_datalookup\LookupResult\CprLookupResult;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\advancedqueue\Entity\QueueInterface;
use Drupal\advancedqueue\Job;
use Drupal\advancedqueue\JobResult;
+use Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber;
use Drupal\os2forms_digital_post\Exception\InvalidRecipientIdentifierElementException;
use Drupal\os2forms_digital_post\Exception\RuntimeException;
use Drupal\os2forms_digital_post\Exception\SubmissionNotFoundException;
@@ -62,6 +65,7 @@ public function __construct(
#[Autowire(service: 'logger.channel.os2forms_digital_post_submission')]
private readonly LoggerChannelInterface $submissionLogger,
private readonly DigitalPostHelper $digitalPostHelper,
+ private readonly Os2formsDigitalPostSubscriber $digitalPostSubscriber,
) {
$this->webformSubmissionStorage = $entityTypeManager->getStorage('webform_submission');
$this->queueStorage = $entityTypeManager->getStorage('advancedqueue_queue');
@@ -152,6 +156,8 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h
$recipientIdentifierType = 'CPR';
}
+ $this->digitalPostSubscriber->setDigitalPostContext($submission, $lookupResult);
+
$senderSettings = $this->settings->getSender();
$messageOptions = [
self::RECIPIENT_IDENTIFIER_TYPE => $recipientIdentifierType,