diff --git a/ext/flexmailer/flexmailer.php b/ext/flexmailer/flexmailer.php
index c4d8b490b3806ede5c69d326f8f60d2682ba03c5..f5dd151b23f3dcd9b0a9c96725e26e96b978114b 100644
--- a/ext/flexmailer/flexmailer.php
+++ b/ext/flexmailer/flexmailer.php
@@ -48,3 +48,17 @@ function flexmailer_civicrm_container($container) {
   $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
   \Civi\FlexMailer\Services::registerServices($container);
 }
+
+/**
+ * @see \CRM_Utils_Hook::scanClasses()
+ */
+function flexmailer_civicrm_scanClasses(array &$classes): void {
+  $prefix = 'Civi\\FlexMailer\\';
+  $dir = __DIR__ . '/src';
+  $delim = '\\';
+
+  foreach (\CRM_Utils_File::findFiles($dir, '*.php', TRUE) as $relFile) {
+    $relFile = str_replace(DIRECTORY_SEPARATOR, '/', $relFile);
+    $classes[] = $prefix . str_replace('/', $delim, substr($relFile, 0, -4));
+  }
+}
diff --git a/ext/flexmailer/src/ClickTracker/HtmlClickTracker.php b/ext/flexmailer/src/ClickTracker/HtmlClickTracker.php
index 822ccbd19f76ff49f367a1a12e2c8243cb99a139..39fa636add69125fe9588434350b92afd02b4306 100644
--- a/ext/flexmailer/src/ClickTracker/HtmlClickTracker.php
+++ b/ext/flexmailer/src/ClickTracker/HtmlClickTracker.php
@@ -10,7 +10,12 @@
  */
 namespace Civi\FlexMailer\ClickTracker;
 
-class HtmlClickTracker implements ClickTrackerInterface {
+use Civi\Core\Service\AutoService;
+
+/**
+ * @service civi_flexmailer_html_click_tracker
+ */
+class HtmlClickTracker extends AutoService implements ClickTrackerInterface {
 
   public function filterContent($msg, $mailing_id, $queue_id) {
     return self::replaceHrefUrls($msg,
diff --git a/ext/flexmailer/src/ClickTracker/TextClickTracker.php b/ext/flexmailer/src/ClickTracker/TextClickTracker.php
index e3c12f3d591c405909401d719e43e85c4a2fa6cf..e83dceccd65bfcce20a4bc2f4e98477d88d54db1 100644
--- a/ext/flexmailer/src/ClickTracker/TextClickTracker.php
+++ b/ext/flexmailer/src/ClickTracker/TextClickTracker.php
@@ -10,7 +10,12 @@
  */
 namespace Civi\FlexMailer\ClickTracker;
 
-class TextClickTracker implements ClickTrackerInterface {
+use Civi\Core\Service\AutoService;
+
+/**
+ * @service civi_flexmailer_text_click_tracker
+ */
+class TextClickTracker extends AutoService implements ClickTrackerInterface {
 
   public function filterContent($msg, $mailing_id, $queue_id) {
     return self::replaceTextUrls($msg,
diff --git a/ext/flexmailer/src/Listener/Abdicator.php b/ext/flexmailer/src/Listener/Abdicator.php
index e5761cc12a6ade4df3ac5c5c28ac1c682a2da3cd..01467fa9a2a2ca55c6ee672d435cbd668954b956 100644
--- a/ext/flexmailer/src/Listener/Abdicator.php
+++ b/ext/flexmailer/src/Listener/Abdicator.php
@@ -10,6 +10,7 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\RunEvent;
 
 /**
@@ -22,8 +23,12 @@ use Civi\FlexMailer\Event\RunEvent;
  *
  * During incubation, we want to mostly step-aside -- for traditional
  * mailings, simply continue using the old system.
+ *
+ * @service civi_flexmailer_abdicator
  */
-class Abdicator {
+class Abdicator extends AutoService {
+
+  use IsActiveTrait;
 
   /**
    * @param \CRM_Mailing_BAO_Mailing $mailing
diff --git a/ext/flexmailer/src/Listener/Attachments.php b/ext/flexmailer/src/Listener/Attachments.php
index 6b387a7de521f2637860a17209bbfaa9cd6ca1d6..a4f796747328658f670834a5f7d435170d2ed15b 100644
--- a/ext/flexmailer/src/Listener/Attachments.php
+++ b/ext/flexmailer/src/Listener/Attachments.php
@@ -10,9 +10,15 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\ComposeBatchEvent;
 
-class Attachments extends BaseListener {
+/**
+ * @service civi_flexmailer_attachments
+ */
+class Attachments extends AutoService {
+
+  use IsActiveTrait;
 
   /**
    * Add any attachments.
diff --git a/ext/flexmailer/src/Listener/BaseListener.php b/ext/flexmailer/src/Listener/BaseListener.php
index fb4a31b484b0df12e7642a8f0dc0c978d1a10af7..472262c1ec4e039a66d95b52010bf223cbc9a2af 100644
--- a/ext/flexmailer/src/Listener/BaseListener.php
+++ b/ext/flexmailer/src/Listener/BaseListener.php
@@ -10,24 +10,12 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+/**
+ * @deprecated
+ *   As of Feb 2024, still used by Mosaico releases. Need at least a year to phase-out.
+ */
 class BaseListener {
-  /**
-   * @var bool
-   */
-  private $active = TRUE;
-
-  /**
-   * @return bool
-   */
-  public function isActive() {
-    return $this->active;
-  }
 
-  /**
-   * @param bool $active
-   */
-  public function setActive($active) {
-    $this->active = $active;
-  }
+  use IsActiveTrait;
 
 }
diff --git a/ext/flexmailer/src/Listener/BasicHeaders.php b/ext/flexmailer/src/Listener/BasicHeaders.php
index 4a448379962509a75ac32b99880193c1d4aea70e..e6eb5d82925a0a2129057ea2b7aac5236e9f789e 100644
--- a/ext/flexmailer/src/Listener/BasicHeaders.php
+++ b/ext/flexmailer/src/Listener/BasicHeaders.php
@@ -10,9 +10,15 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\ComposeBatchEvent;
 
-class BasicHeaders extends BaseListener {
+/**
+ * @service civi_flexmailer_basic_headers
+ */
+class BasicHeaders extends AutoService {
+
+  use IsActiveTrait;
 
   /**
    * Inject basic headers
diff --git a/ext/flexmailer/src/Listener/BounceTracker.php b/ext/flexmailer/src/Listener/BounceTracker.php
index e3c3dd112517079f8a830a96bb199441fd452256..2f57278c9adf9f1daab1958e911fe21f24893481 100644
--- a/ext/flexmailer/src/Listener/BounceTracker.php
+++ b/ext/flexmailer/src/Listener/BounceTracker.php
@@ -10,9 +10,15 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\ComposeBatchEvent;
 
-class BounceTracker extends BaseListener {
+/**
+ * @service civi_flexmailer_bounce_tracker
+ */
+class BounceTracker extends AutoService {
+
+  use IsActiveTrait;
 
   /**
    * Inject bounce-tracking codes.
diff --git a/ext/flexmailer/src/Listener/DefaultBatcher.php b/ext/flexmailer/src/Listener/DefaultBatcher.php
index 316e37b13a95090156cc339e40f3dc7dc152d292..b18287079da82f1df6bfd1b56212c75314ab1556 100644
--- a/ext/flexmailer/src/Listener/DefaultBatcher.php
+++ b/ext/flexmailer/src/Listener/DefaultBatcher.php
@@ -10,10 +10,16 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\WalkBatchesEvent;
 use Civi\FlexMailer\FlexMailerTask;
 
-class DefaultBatcher extends BaseListener {
+/**
+ * @service civi_flexmailer_default_batcher
+ */
+class DefaultBatcher extends AutoService {
+
+  use IsActiveTrait;
 
   /**
    * Given a MailingJob (`$e->getJob()`), enumerate the recipients as
diff --git a/ext/flexmailer/src/Listener/DefaultComposer.php b/ext/flexmailer/src/Listener/DefaultComposer.php
index 25b2e69b5dce098459cfe0af8b91a6c93daa0d87..0a7f1254af3604ed011ec6fac825eeaa24cb1df7 100644
--- a/ext/flexmailer/src/Listener/DefaultComposer.php
+++ b/ext/flexmailer/src/Listener/DefaultComposer.php
@@ -10,6 +10,7 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\ComposeBatchEvent;
 use Civi\FlexMailer\Event\RunEvent;
 use Civi\FlexMailer\FlexMailerTask;
@@ -22,8 +23,12 @@ use Civi\Token\TokenRow;
  *
  * The DefaultComposer uses a TokenProcessor to generate all messages as
  * a batch.
+ *
+ * @service civi_flexmailer_default_composer
  */
-class DefaultComposer extends BaseListener {
+class DefaultComposer extends AutoService {
+
+  use IsActiveTrait;
 
   public function onRun(RunEvent $e) {
     // FIXME: This probably doesn't belong here...
diff --git a/ext/flexmailer/src/Listener/DefaultSender.php b/ext/flexmailer/src/Listener/DefaultSender.php
index 0d0a9e2f0997c1e292b0d98aa8546a13662fc9bb..c29050cdf8ce0af9bd1a257b31a53c4328d091ed 100644
--- a/ext/flexmailer/src/Listener/DefaultSender.php
+++ b/ext/flexmailer/src/Listener/DefaultSender.php
@@ -10,9 +10,16 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\SendBatchEvent;
 
-class DefaultSender extends BaseListener {
+/**
+ * @service civi_flexmailer_default_sender
+ */
+class DefaultSender extends AutoService {
+
+  use IsActiveTrait;
+
   const BULK_MAIL_INSERT_COUNT = 10;
 
   public function onSend(SendBatchEvent $e) {
diff --git a/ext/flexmailer/src/Listener/HookAdapter.php b/ext/flexmailer/src/Listener/HookAdapter.php
index 53d3aa4f8b96998949e3820a2e3db4d6c4772415..62e4e29cf788c1041d7cfddad2653326584e52b5 100644
--- a/ext/flexmailer/src/Listener/HookAdapter.php
+++ b/ext/flexmailer/src/Listener/HookAdapter.php
@@ -10,9 +10,15 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\ComposeBatchEvent;
 
-class HookAdapter extends BaseListener {
+/**
+ * @service civi_flexmailer_hooks
+ */
+class HookAdapter extends AutoService {
+
+  use IsActiveTrait;
 
   /**
    * Expose to hook_civicrm_alterMailParams.
diff --git a/ext/flexmailer/src/Listener/IsActiveTrait.php b/ext/flexmailer/src/Listener/IsActiveTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..20c38ba4f1214089c2a7d81ada385ab27899fbd8
--- /dev/null
+++ b/ext/flexmailer/src/Listener/IsActiveTrait.php
@@ -0,0 +1,36 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+namespace Civi\FlexMailer\Listener;
+
+trait IsActiveTrait {
+
+  /**
+   * @var bool
+   */
+  private $active = TRUE;
+
+  /**
+   * @return bool
+   */
+  public function isActive() {
+    return $this->active;
+  }
+
+  /**
+   * @param bool $active
+   * @return $this
+   */
+  public function setActive($active) {
+    $this->active = $active;
+    return $this;
+  }
+
+}
diff --git a/ext/flexmailer/src/Listener/OpenTracker.php b/ext/flexmailer/src/Listener/OpenTracker.php
index 75c9c4e0efdb68552733ef35e34288a5f76e6064..f4787f700ecaf56a98b98b1adb4fc3542d73a3f8 100644
--- a/ext/flexmailer/src/Listener/OpenTracker.php
+++ b/ext/flexmailer/src/Listener/OpenTracker.php
@@ -10,9 +10,15 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\ComposeBatchEvent;
 
-class OpenTracker extends BaseListener {
+/**
+ * @service civi_flexmailer_open_tracker
+ */
+class OpenTracker extends AutoService {
+
+  use IsActiveTrait;
 
   /**
    * Inject open-tracking codes.
diff --git a/ext/flexmailer/src/Listener/RequiredFields.php b/ext/flexmailer/src/Listener/RequiredFields.php
index 4eaa02d7502c342c7e3e34c867b62e0397da007f..95766226594b5fb19d298e320a89922f515b6140 100644
--- a/ext/flexmailer/src/Listener/RequiredFields.php
+++ b/ext/flexmailer/src/Listener/RequiredFields.php
@@ -10,6 +10,7 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use CRM_Flexmailer_ExtensionUtil as E;
 use Civi\FlexMailer\Event\CheckSendableEvent;
 
@@ -19,7 +20,22 @@ use Civi\FlexMailer\Event\CheckSendableEvent;
  *
  * The RequiredFields listener checks that all mandatory fields have a value.
  */
-class RequiredFields extends BaseListener {
+class RequiredFields extends AutoService {
+
+  use IsActiveTrait;
+
+  /**
+   * @service civi_flexmailer_required_fields
+   */
+  public static function factory(): RequiredFields {
+    return new static([
+      'subject',
+      'name',
+      'from_name',
+      'from_email',
+      '(body_html|body_text)',
+    ]);
+  }
 
   /**
    * @var array
diff --git a/ext/flexmailer/src/Listener/RequiredTokens.php b/ext/flexmailer/src/Listener/RequiredTokens.php
index 1d6a87af655f4dd762d7678de5c0a1d200a67e64..50637ebb23f0de2722df2ce05fa5ea76d69c080e 100644
--- a/ext/flexmailer/src/Listener/RequiredTokens.php
+++ b/ext/flexmailer/src/Listener/RequiredTokens.php
@@ -10,6 +10,7 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use CRM_Flexmailer_ExtensionUtil as E;
 use Civi\FlexMailer\Event\CheckSendableEvent;
 
@@ -21,7 +22,27 @@ use Civi\FlexMailer\Event\CheckSendableEvent;
  * CiviMail tokens like `{action.unsubscribeUrl}`, which are often required
  * to comply with anti-spam regulations.
  */
-class RequiredTokens extends BaseListener {
+class RequiredTokens extends AutoService {
+
+  use IsActiveTrait;
+
+  /**
+   * @service civi_flexmailer_required_tokens
+   */
+  public static function factory(): RequiredTokens {
+    return new static(
+      ['traditional'],
+      [
+        'domain.address' => ts("Domain address - displays your organization's postal address."),
+        'action.optOutUrl or action.unsubscribeUrl' => [
+          'action.optOut' => ts("'Opt out via email' - displays an email address for recipients to opt out of receiving emails from your organization."),
+          'action.optOutUrl' => ts("'Opt out via web page' - creates a link for recipients to click if they want to opt out of receiving emails from your organization. Alternatively, you can include the 'Opt out via email' token."),
+          'action.unsubscribe' => ts("'Unsubscribe via email' - displays an email address for recipients to unsubscribe from the specific mailing list used to send this message."),
+          'action.unsubscribeUrl' => ts("'Unsubscribe via web page' - creates a link for recipients to unsubscribe from the specific mailing list used to send this message. Alternatively, you can include the 'Unsubscribe via email' token or one of the Opt-out tokens."),
+        ],
+      ]
+    );
+  }
 
   /**
    * @var array
diff --git a/ext/flexmailer/src/Listener/TestPrefix.php b/ext/flexmailer/src/Listener/TestPrefix.php
index b81bb03852230373efe488f51522eb89acf1485f..805cd71662c04e0239c588efa58681e004751177 100644
--- a/ext/flexmailer/src/Listener/TestPrefix.php
+++ b/ext/flexmailer/src/Listener/TestPrefix.php
@@ -10,9 +10,15 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\ComposeBatchEvent;
 
-class TestPrefix extends BaseListener {
+/**
+ * @service civi_flexmailer_test_prefix
+ */
+class TestPrefix extends AutoService {
+
+  use IsActiveTrait;
 
   /**
    * For any test mailings, the "Subject:" should have a prefix.
diff --git a/ext/flexmailer/src/Listener/ToHeader.php b/ext/flexmailer/src/Listener/ToHeader.php
index 855cf85ca057c14921ac5552bc7335302005ef69..97e4d227af90606a6172a77cdc29a1d798d87f9e 100644
--- a/ext/flexmailer/src/Listener/ToHeader.php
+++ b/ext/flexmailer/src/Listener/ToHeader.php
@@ -10,9 +10,15 @@
  */
 namespace Civi\FlexMailer\Listener;
 
+use Civi\Core\Service\AutoService;
 use Civi\FlexMailer\Event\ComposeBatchEvent;
 
-class ToHeader extends BaseListener {
+/**
+ * @service civi_flexmailer_to_header
+ */
+class ToHeader extends AutoService {
+
+  use IsActiveTrait;
 
   /**
    * Inject the "To:" header.
diff --git a/ext/flexmailer/src/Services.php b/ext/flexmailer/src/Services.php
index 899041e4c92c1ef7d0290dafc05da2858a61c647..c1a979d44afe201489e081e2fe251d1fe5bd7655 100644
--- a/ext/flexmailer/src/Services.php
+++ b/ext/flexmailer/src/Services.php
@@ -29,43 +29,6 @@ class Services {
     $apiOverrides = $container->setDefinition('civi_flexmailer_api_overrides', new Definition('Civi\API\Provider\ProviderInterface'))->setPublic(TRUE);
     self::applyStaticFactory($apiOverrides, __CLASS__, 'createApiOverrides');
 
-    $container->setDefinition('civi_flexmailer_required_fields', new Definition('Civi\FlexMailer\Listener\RequiredFields', [
-      [
-        'subject',
-        'name',
-        'from_name',
-        'from_email',
-        '(body_html|body_text)',
-      ],
-    ]))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_required_tokens', new Definition('Civi\FlexMailer\Listener\RequiredTokens', [
-      ['traditional'],
-      [
-        'domain.address' => ts("Domain address - displays your organization's postal address."),
-        'action.optOutUrl or action.unsubscribeUrl' => [
-          'action.optOut' => ts("'Opt out via email' - displays an email address for recipients to opt out of receiving emails from your organization."),
-          'action.optOutUrl' => ts("'Opt out via web page' - creates a link for recipients to click if they want to opt out of receiving emails from your organization. Alternatively, you can include the 'Opt out via email' token."),
-          'action.unsubscribe' => ts("'Unsubscribe via email' - displays an email address for recipients to unsubscribe from the specific mailing list used to send this message."),
-          'action.unsubscribeUrl' => ts("'Unsubscribe via web page' - creates a link for recipients to unsubscribe from the specific mailing list used to send this message. Alternatively, you can include the 'Unsubscribe via email' token or one of the Opt-out tokens."),
-        ],
-      ],
-    ]))->setPublic(TRUE);
-
-    $container->setDefinition('civi_flexmailer_abdicator', new Definition('Civi\FlexMailer\Listener\Abdicator'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_default_batcher', new Definition('Civi\FlexMailer\Listener\DefaultBatcher'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_default_composer', new Definition('Civi\FlexMailer\Listener\DefaultComposer'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_open_tracker', new Definition('Civi\FlexMailer\Listener\OpenTracker'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_basic_headers', new Definition('Civi\FlexMailer\Listener\BasicHeaders'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_to_header', new Definition('Civi\FlexMailer\Listener\ToHeader'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_attachments', new Definition('Civi\FlexMailer\Listener\Attachments'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_bounce_tracker', new Definition('Civi\FlexMailer\Listener\BounceTracker'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_default_sender', new Definition('Civi\FlexMailer\Listener\DefaultSender'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_hooks', new Definition('Civi\FlexMailer\Listener\HookAdapter'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_test_prefix', new Definition('Civi\FlexMailer\Listener\TestPrefix'))->setPublic(TRUE);
-
-    $container->setDefinition('civi_flexmailer_html_click_tracker', new Definition('Civi\FlexMailer\ClickTracker\HtmlClickTracker'))->setPublic(TRUE);
-    $container->setDefinition('civi_flexmailer_text_click_tracker', new Definition('Civi\FlexMailer\ClickTracker\TextClickTracker'))->setPublic(TRUE);
-
     foreach (self::getListenerSpecs() as $listenerSpec) {
       $container->findDefinition('dispatcher')->addMethodCall('addListenerService', $listenerSpec);
     }