diff --git a/changelog.d/6107.bugfix b/changelog.d/6107.bugfix
new file mode 100644
index 000000000..d4b9516ac
--- /dev/null
+++ b/changelog.d/6107.bugfix
@@ -0,0 +1,1 @@
+Ensure that servers which are not configured to support email address verification do not offer it in the registration flows.
\ No newline at end of file
diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py
index f920c2f6c..333eb3062 100644
--- a/synapse/handlers/auth.py
+++ b/synapse/handlers/auth.py
@@ -61,7 +61,8 @@ class AuthHandler(BaseHandler):
         self.checkers = {}  # type: dict[str, UserInteractiveAuthChecker]
         for auth_checker_class in INTERACTIVE_AUTH_CHECKERS:
             inst = auth_checker_class(hs)
-            self.checkers[inst.AUTH_TYPE] = inst
+            if inst.is_enabled():
+                self.checkers[inst.AUTH_TYPE] = inst
 
         self.bcrypt_rounds = hs.config.bcrypt_rounds
 
@@ -156,6 +157,14 @@ class AuthHandler(BaseHandler):
 
         return params
 
+    def get_enabled_auth_types(self):
+        """Return the enabled user-interactive authentication types
+
+        Returns the UI-Auth types which are supported by the homeserver's current
+        config.
+        """
+        return self.checkers.keys()
+
     @defer.inlineCallbacks
     def check_auth(self, flows, clientdict, clientip):
         """
diff --git a/synapse/handlers/ui_auth/checkers.py b/synapse/handlers/ui_auth/checkers.py
index fd633b7b0..ee6922324 100644
--- a/synapse/handlers/ui_auth/checkers.py
+++ b/synapse/handlers/ui_auth/checkers.py
@@ -32,6 +32,13 @@ class UserInteractiveAuthChecker:
     def __init__(self, hs):
         pass
 
+    def is_enabled(self):
+        """Check if the configuration of the homeserver allows this checker to work
+
+        Returns:
+            bool: True if this login type is enabled.
+        """
+
     def check_auth(self, authdict, clientip):
         """Given the authentication dict from the client, attempt to check this step
 
@@ -51,6 +58,9 @@ class UserInteractiveAuthChecker:
 class DummyAuthChecker(UserInteractiveAuthChecker):
     AUTH_TYPE = LoginType.DUMMY
 
+    def is_enabled(self):
+        return True
+
     def check_auth(self, authdict, clientip):
         return defer.succeed(True)
 
@@ -58,6 +68,9 @@ class DummyAuthChecker(UserInteractiveAuthChecker):
 class TermsAuthChecker(UserInteractiveAuthChecker):
     AUTH_TYPE = LoginType.TERMS
 
+    def is_enabled(self):
+        return True
+
     def check_auth(self, authdict, clientip):
         return defer.succeed(True)
 
@@ -67,10 +80,14 @@ class RecaptchaAuthChecker(UserInteractiveAuthChecker):
 
     def __init__(self, hs):
         super().__init__(hs)
+        self._enabled = bool(hs.config.recaptcha_private_key)
         self._http_client = hs.get_simple_http_client()
         self._url = hs.config.recaptcha_siteverify_api
         self._secret = hs.config.recaptcha_private_key
 
+    def is_enabled(self):
+        return self._enabled
+
     @defer.inlineCallbacks
     def check_auth(self, authdict, clientip):
         try:
@@ -191,6 +208,12 @@ class EmailIdentityAuthChecker(UserInteractiveAuthChecker, _BaseThreepidAuthChec
         UserInteractiveAuthChecker.__init__(self, hs)
         _BaseThreepidAuthChecker.__init__(self, hs)
 
+    def is_enabled(self):
+        return self.hs.config.threepid_behaviour_email in (
+            ThreepidBehaviour.REMOTE,
+            ThreepidBehaviour.LOCAL,
+        )
+
     def check_auth(self, authdict, clientip):
         return self._check_threepid("email", authdict)
 
@@ -202,6 +225,9 @@ class MsisdnAuthChecker(UserInteractiveAuthChecker, _BaseThreepidAuthChecker):
         UserInteractiveAuthChecker.__init__(self, hs)
         _BaseThreepidAuthChecker.__init__(self, hs)
 
+    def is_enabled(self):
+        return bool(self.hs.config.account_threepid_delegate_msisdn)
+
     def check_auth(self, authdict, clientip):
         return self._check_threepid("msisdn", authdict)
 
diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py
index e3f3d9126..4f24a124a 100644
--- a/synapse/rest/client/v2_alpha/register.py
+++ b/synapse/rest/client/v2_alpha/register.py
@@ -32,12 +32,14 @@ from synapse.api.errors import (
     ThreepidValidationError,
     UnrecognizedRequestError,
 )
+from synapse.config import ConfigError
 from synapse.config.captcha import CaptchaConfig
 from synapse.config.consent_config import ConsentConfig
 from synapse.config.emailconfig import ThreepidBehaviour
 from synapse.config.ratelimiting import FederationRateLimitConfig
 from synapse.config.registration import RegistrationConfig
 from synapse.config.server import is_threepid_reserved
+from synapse.handlers.auth import AuthHandler
 from synapse.http.server import finish_request
 from synapse.http.servlet import (
     RestServlet,
@@ -375,7 +377,9 @@ class RegisterRestServlet(RestServlet):
         self.ratelimiter = hs.get_registration_ratelimiter()
         self.clock = hs.get_clock()
 
-        self._registration_flows = _calculate_registration_flows(hs.config)
+        self._registration_flows = _calculate_registration_flows(
+            hs.config, self.auth_handler
+        )
 
     @interactive_auth_handler
     @defer.inlineCallbacks
@@ -664,11 +668,13 @@ class RegisterRestServlet(RestServlet):
 def _calculate_registration_flows(
     # technically `config` has to provide *all* of these interfaces, not just one
     config: Union[RegistrationConfig, ConsentConfig, CaptchaConfig],
+    auth_handler: AuthHandler,
 ) -> List[List[str]]:
     """Get a suitable flows list for registration
 
     Args:
         config: server configuration
+        auth_handler: authorization handler
 
     Returns: a list of supported flows
     """
@@ -678,10 +684,29 @@ def _calculate_registration_flows(
     require_msisdn = "msisdn" in config.registrations_require_3pid
 
     show_msisdn = True
+    show_email = True
+
     if config.disable_msisdn_registration:
         show_msisdn = False
         require_msisdn = False
 
+    enabled_auth_types = auth_handler.get_enabled_auth_types()
+    if LoginType.EMAIL_IDENTITY not in enabled_auth_types:
+        show_email = False
+        if require_email:
+            raise ConfigError(
+                "Configuration requires email address at registration, but email "
+                "validation is not configured"
+            )
+
+    if LoginType.MSISDN not in enabled_auth_types:
+        show_msisdn = False
+        if require_msisdn:
+            raise ConfigError(
+                "Configuration requires msisdn at registration, but msisdn "
+                "validation is not configured"
+            )
+
     flows = []
 
     # only support 3PIDless registration if no 3PIDs are required
@@ -693,14 +718,15 @@ def _calculate_registration_flows(
         flows.append([LoginType.DUMMY])
 
     # only support the email-only flow if we don't require MSISDN 3PIDs
-    if not require_msisdn:
+    if show_email and not require_msisdn:
         flows.append([LoginType.EMAIL_IDENTITY])
 
     # only support the MSISDN-only flow if we don't require email 3PIDs
     if show_msisdn and not require_email:
         flows.append([LoginType.MSISDN])
 
-    if show_msisdn:
+    if show_email and show_msisdn:
+        # always let users provide both MSISDN & email
         flows.append([LoginType.MSISDN, LoginType.EMAIL_IDENTITY])
 
     # Prepend m.login.terms to all flows if we're requiring consent

Remove email from registration flows if it’s unsupported
If the HS is not configured to support email (email config block missing and not delegated to an IS), then it should remove email from the registration flows so that clients know they shouldn't show the email field.
diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py
index bc2dc4797..dab87e5ed 100644
--- a/tests/rest/client/v2_alpha/test_register.py
+++ b/tests/rest/client/v2_alpha/test_register.py
@@ -198,16 +198,8 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase):
         self.assertEquals(channel.result["code"], b"401", channel.result)
         flows = channel.json_body["flows"]
 
-        # with the stock config, we expect all four combinations of 3pid
-        self.assertCountEqual(
-            [
-                ["m.login.dummy"],
-                ["m.login.email.identity"],
-                ["m.login.msisdn"],
-                ["m.login.msisdn", "m.login.email.identity"],
-            ],
-            (f["stages"] for f in flows),
-        )
+        # with the stock config, we only expect the dummy flow
+        self.assertCountEqual([["m.login.dummy"]], (f["stages"] for f in flows))
 
     @unittest.override_config(
         {
@@ -217,9 +209,13 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase):
                 "template_dir": "/",
                 "require_at_registration": True,
             },
+            "account_threepid_delegates": {
+                "email": "https://id_server",
+                "msisdn": "https://id_server",
+            },
         }
     )
-    def test_advertised_flows_captcha_and_terms(self):
+    def test_advertised_flows_captcha_and_terms_and_3pids(self):
         request, channel = self.make_request(b"POST", self.url, b"{}")
         self.render(request)
         self.assertEquals(channel.result["code"], b"401", channel.result)
@@ -241,7 +237,16 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase):
         )
 
     @unittest.override_config(
-        {"registrations_require_3pid": ["email"], "disable_msisdn_registration": True}
+        {
+            "public_baseurl": "https://test_server",
+            "registrations_require_3pid": ["email"],
+            "disable_msisdn_registration": True,
+            "email": {
+                "smtp_host": "mail_server",
+                "smtp_port": 2525,
+                "notif_from": "sender@host",
+            },
+        }
     )
     def test_advertised_flows_no_msisdn_email_required(self):
         request, channel = self.make_request(b"POST", self.url, b"{}")

pip install -e .[all]
parse_log_pytest
pytest
pytest coverage coverage-enable-subprocess
apt-get update apt-get install -y build-essential python3-dev libffi-dev libssl-dev
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
acme==0.40.1
aiocontextvars==0.2.2
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
Automat==22.10.0
bcrypt==4.0.1
bleach==4.1.0
boltons==23.1.1
canonicaljson==1.5.0
certifi==2021.5.30
cffi==1.15.1
charset-normalizer==2.0.12
constantly==15.1.0
contextvars==2.4
coverage==6.2
coverage-enable-subprocess==1.0
cryptography==40.0.2
daemonize==2.5.0
defusedxml==0.7.1
elementpath==2.4.0
eliot==1.14.0
frozendict==2.4.6
hyperlink==21.0.0
idna==3.10
immutables==0.19
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
importlib-resources==5.4.0
incremental==22.10.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
jaeger-client==4.6.1
Jinja2==3.0.3
josepy==1.13.0
jsonschema==3.2.0
ldap3==2.9.1
lxml==5.3.1
MarkupSafe==2.0.1
-e git+https://github.com/matrix-org/synapse.git@2cd98812ba338eefe83fee4ae2390d00f5499de9#egg=matrix_synapse
matrix-synapse-ldap3==0.1.5
mock==5.2.0
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
msgpack==1.0.5
netaddr==0.10.1
opentracing==2.4.0
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
parameterized==0.8.1
pem==21.2.0
phonenumbers==9.0.2
Pillow==8.4.0
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
prometheus-client==0.7.1
psutil==7.0.0
psycopg2==2.7.7
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycparser==2.21
PyJWT==2.4.0
pymacaroons==0.13.0
PyNaCl==1.5.0
pyOpenSSL==23.2.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pyRFC3339==2.0.1
pyrsistent==0.18.0
pysaml2==7.3.1
pytest==6.2.4
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.1
requests==2.27.1
requests-toolbelt==1.0.0
sentry-sdk==2.24.1
service-identity==21.1.0
signedjson==1.1.4
simplejson==3.20.1
six==1.17.0
sortedcontainers==2.4.0
threadloop==1.0.2
thrift==0.21.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tornado==6.1
treq==22.2.0
Twisted==22.4.0
txacme==0.9.3
TxSNI==0.2.0
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
unpaddedbase64==2.1.0
urllib3==1.26.20
webencodings==0.5.1
xmlschema==1.9.2
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work
zope.interface==5.5.2

name: synapse
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - acme==0.40.1
      - aiocontextvars==0.2.2
      - automat==22.10.0
      - bcrypt==4.0.1
      - bleach==4.1.0
      - boltons==23.1.1
      - canonicaljson==1.5.0
      - cffi==1.15.1
      - charset-normalizer==2.0.12
      - constantly==15.1.0
      - contextvars==2.4
      - coverage==6.2
      - coverage-enable-subprocess==1.0
      - cryptography==40.0.2
      - daemonize==2.5.0
      - defusedxml==0.7.1
      - elementpath==2.4.0
      - eliot==1.14.0
      - frozendict==2.4.6
      - hyperlink==21.0.0
      - idna==3.10
      - immutables==0.19
      - importlib-resources==5.4.0
      - incremental==22.10.0
      - jaeger-client==4.6.1
      - jinja2==3.0.3
      - josepy==1.13.0
      - jsonschema==3.2.0
      - ldap3==2.9.1
      - lxml==5.3.1
      - markupsafe==2.0.1
      - matrix-synapse-ldap3==0.1.5
      - mock==5.2.0
      - msgpack==1.0.5
      - netaddr==0.10.1
      - opentracing==2.4.0
      - parameterized==0.8.1
      - pem==21.2.0
      - phonenumbers==9.0.2
      - pillow==8.4.0
      - prometheus-client==0.7.1
      - psutil==7.0.0
      - psycopg2==2.7.7
      - pyasn1==0.5.1
      - pyasn1-modules==0.3.0
      - pycparser==2.21
      - pyjwt==2.4.0
      - pymacaroons==0.13.0
      - pynacl==1.5.0
      - pyopenssl==23.2.0
      - pyrfc3339==2.0.1
      - pyrsistent==0.18.0
      - pysaml2==7.3.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.1
      - requests==2.27.1
      - requests-toolbelt==1.0.0
      - sentry-sdk==2.24.1
      - service-identity==21.1.0
      - signedjson==1.1.4
      - simplejson==3.20.1
      - six==1.17.0
      - sortedcontainers==2.4.0
      - threadloop==1.0.2
      - thrift==0.21.0
      - tornado==6.1
      - treq==22.2.0
      - twisted==22.4.0
      - txacme==0.9.3
      - txsni==0.2.0
      - unpaddedbase64==2.1.0
      - urllib3==1.26.20
      - webencodings==0.5.1
      - xmlschema==1.9.2
      - zope-interface==5.5.2
prefix: /opt/conda/envs/synapse

tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_advertised_flows
tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_POST_appservice_registration_invalid tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_POST_appservice_registration_valid tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_POST_bad_password tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_POST_bad_username tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_POST_disabled_guest_registration tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_POST_disabled_registration tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_POST_guest_registration tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_POST_ratelimiting tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_POST_ratelimiting_guest tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_POST_user_valid tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_advertised_flows_captcha_and_terms_and_3pids tests/rest/client/v2_alpha/test_register.py::RegisterRestServletTestCase::test_advertised_flows_no_msisdn_email_required tests/rest/client/v2_alpha/test_register.py::AccountValidityTestCase::test_manual_expire tests/rest/client/v2_alpha/test_register.py::AccountValidityTestCase::test_manual_renewal tests/rest/client/v2_alpha/test_register.py::AccountValidityTestCase::test_validity_period tests/rest/client/v2_alpha/test_register.py::AccountValidityRenewalByEmailTestCase::test_deactivated_user tests/rest/client/v2_alpha/test_register.py::AccountValidityRenewalByEmailTestCase::test_manual_email_send tests/rest/client/v2_alpha/test_register.py::AccountValidityRenewalByEmailTestCase::test_manual_email_send_expired_account tests/rest/client/v2_alpha/test_register.py::AccountValidityRenewalByEmailTestCase::test_renewal_email tests/rest/client/v2_alpha/test_register.py::AccountValidityRenewalByEmailTestCase::test_renewal_invalid_token tests/rest/client/v2_alpha/test_register.py::AccountValidityBackgroundJobTestCase::test_background_job
changelog.d/6107.bugfix synapse/rest/client/v2_alpha/register.py synapse/handlers/auth.py synapse/handlers/ui_auth/checkers.py
changelog.d/6107.bugfix synapse/rest/client/v2_alpha/register.py synapse/handlers/auth.py synapse/handlers/ui_auth/checkers.py