diff --git a/Civi/Token/TokenCompatSubscriber.php b/Civi/Token/TokenCompatSubscriber.php
index 3133df6eb3e4da10b1ada4f1136dd1d33f6f4139..b1d9408c6af4dd6e65157611a34d8be7e8d5960a 100644
--- a/Civi/Token/TokenCompatSubscriber.php
+++ b/Civi/Token/TokenCompatSubscriber.php
@@ -298,17 +298,15 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
    * Load token data.
    *
    * @param \Civi\Token\Event\TokenValueEvent $e
+   *
    * @throws TokenException
+   * @throws \CRM_Core_Exception
    */
   public function onEvaluate(TokenValueEvent $e) {
-    // For reasons unknown, replaceHookTokens used to require a pre-computed list of
-    // hook *categories* (aka entities aka namespaces). We cache
-    // this in the TokenProcessor's context but can likely remove it now.
-
-    $e->getTokenProcessor()->context['hookTokenCategories'] = \CRM_Utils_Token::getTokenCategories();
-
     $messageTokens = $e->getTokenProcessor()->getMessageTokens()['contact'] ?? [];
-
+    if (empty($messageTokens)) {
+      return;
+    }
     foreach ($e->getRows() as $row) {
       if (empty($row->context['contactId'])) {
         continue;
@@ -317,15 +315,24 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
       unset($swapLocale);
       $swapLocale = empty($row->context['locale']) ? NULL : \CRM_Utils_AutoClean::swapLocale($row->context['locale']);
 
-      /** @var int $contactId */
-      $contactId = $row->context['contactId'];
       if (empty($row->context['contact'])) {
-        $contact = $this->getContact($contactId, $messageTokens);
+        $row->context['contact'] = $this->getContact($row->context['contactId'], $messageTokens);
       }
-      else {
-        $contact = $row->context['contact'];
+      foreach ($messageTokens as $token) {
+        if ($token === 'checksum') {
+          $cs = \CRM_Contact_BAO_Contact_Utils::generateChecksum($row->context['contactId'],
+            NULL,
+            NULL,
+            $row->context['hash'] ?? NULL
+          );
+          $row->format('text/html')
+            ->tokens('contact', $token, "cs={$cs}");
+        }
+        else {
+          $row->format('text/html')
+            ->tokens('contact', $token, $row->context['contact'][$token] ?? '');
+        }
       }
-      $row->context('contact', $contact);
     }
   }
 
@@ -333,13 +340,22 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
    * Apply the various CRM_Utils_Token helpers.
    *
    * @param \Civi\Token\Event\TokenRenderEvent $e
+   *
+   * @throws \CRM_Core_Exception
    */
-  public function onRender(TokenRenderEvent $e) {
+  public function onRender(TokenRenderEvent $e): void {
     $isHtml = ($e->message['format'] === 'text/html');
     $useSmarty = !empty($e->context['smarty']);
 
     if (!empty($e->context['contact'])) {
-      \CRM_Utils_Token::replaceGreetingTokens($e->string, $e->context['contact'], $e->context['contact']['contact_id'] ?? $e->context['contactId'], NULL, $useSmarty);
+      // @todo - remove this - it simply removes the last unresolved tokens before
+      // they break smarty.
+      // historically it was only called when context['contact'] so that is
+      // retained but it only works because it's almost always true.
+      $remainingTokens = array_keys(\CRM_Utils_Token::getTokens($e->string));
+      if (!empty($remainingTokens)) {
+        $e->string = \CRM_Utils_Token::replaceHookTokens($e->string, $e->context['contact'], $remainingTokens);
+      }
     }
 
     if ($useSmarty) {
@@ -374,8 +390,12 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
     $mappedFields = [
       'email_greeting' => 'email_greeting_display',
       'postal_greeting' => 'postal_greeting_display',
-      'addressee' => 'address_display',
+      'addressee' => 'addressee_display',
     ];
+    if (!empty($returnProperties['checksum'])) {
+      $returnProperties['hash'] = 1;
+    }
+
     foreach ($mappedFields as $tokenName => $realName) {
       if (in_array($tokenName, $requiredFields, TRUE)) {
         $returnProperties[$realName] = 1;
diff --git a/Civi/Token/TokenProcessor.php b/Civi/Token/TokenProcessor.php
index 5411a74fecc80a27a15da702904691b985c18fb1..3bad2d891878c5bf08c94870906c4d27f8f7d2ba 100644
--- a/Civi/Token/TokenProcessor.php
+++ b/Civi/Token/TokenProcessor.php
@@ -383,7 +383,7 @@ class TokenProcessor {
     // Regex examples: '{foo.bar}', '{foo.bar|whiz}'
     // Regex counter-examples: '{foobar}', '{foo bar}', '{$foo.bar}', '{$foo.bar|whiz}', '{foo.bar|whiz{bang}}'
     // Key observations: Civi tokens MUST have a `.` and MUST NOT have a `$`. Civi filters MUST NOT have `{}`s or `$`s.
-    $tokRegex = '([\w]+)\.([\w:]+)';
+    $tokRegex = '([\w]+)\.([\w:\.]+)';
     $filterRegex = '(\w+)';
     $event->string = preg_replace_callback(";\{$tokRegex(?:\|$filterRegex)?\};", $getToken, $message['string']);
     $this->dispatcher->dispatch('civi.token.render', $event);
diff --git a/tests/phpunit/CRM/Core/FormTest.php b/tests/phpunit/CRM/Core/FormTest.php
index d6f3fb2e85975a6ebd9a7d3595f77f7f48a6451b..0041d19ac39be487b4104f2b757fdba25fe3913f 100644
--- a/tests/phpunit/CRM/Core/FormTest.php
+++ b/tests/phpunit/CRM/Core/FormTest.php
@@ -62,7 +62,7 @@ class CRM_Core_FormTest extends CiviUnitTestCase {
     ];
   }
 
-  public function testNewPriceField() {
+  public function testNewPriceField(): void {
     $this->createLoggedInUser();
 
     $priceSetId = $this->callAPISuccess('PriceSet', 'create', [