https://github.com/python/cpython/commit/0a09dafb03dae17895d4a86af04985e373bbdceb
commit: 0a09dafb03dae17895d4a86af04985e373bbdceb
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-07-18T14:28:43+02:00
summary:

gh-153236: Propagate lazy submodule import errors (#153237)

files:
A Lib/test/test_lazy_import/data/missing_dependency.py
A Lib/test/test_lazy_import/data/self_named_module_not_found.py
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-07-06-16-30-00.gh-issue-153236.lazy-import-errors.rst
M Include/internal/pycore_global_objects_fini_generated.h
M Include/internal/pycore_global_strings.h
M Include/internal/pycore_import.h
M Include/internal/pycore_runtime_init_generated.h
M Include/internal/pycore_unicodeobject_generated.h
M Lib/importlib/_bootstrap.py
M Lib/test/test_lazy_import/__init__.py
M Objects/moduleobject.c
M Python/import.c

diff --git a/Include/internal/pycore_global_objects_fini_generated.h 
b/Include/internal/pycore_global_objects_fini_generated.h
index 7d952a1e52561a6..6df1c01f151f68e 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -1544,6 +1544,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_filters));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_finalizing));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_find_and_load));
+    _PyStaticObject_CheckRefcnt((PyObject 
*)&_Py_ID(_find_and_load_lazy_submodule));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_fix_up_module));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_flags_));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_get_sourcefile));
diff --git a/Include/internal/pycore_global_strings.h 
b/Include/internal/pycore_global_strings.h
index 8a8bbc3b6d05bf7..873344fbdcb67b0 100644
--- a/Include/internal/pycore_global_strings.h
+++ b/Include/internal/pycore_global_strings.h
@@ -267,6 +267,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(_filters)
         STRUCT_FOR_ID(_finalizing)
         STRUCT_FOR_ID(_find_and_load)
+        STRUCT_FOR_ID(_find_and_load_lazy_submodule)
         STRUCT_FOR_ID(_fix_up_module)
         STRUCT_FOR_ID(_flags_)
         STRUCT_FOR_ID(_get_sourcefile)
diff --git a/Include/internal/pycore_import.h b/Include/internal/pycore_import.h
index a1078828afa572e..669e328c266d00f 100644
--- a/Include/internal/pycore_import.h
+++ b/Include/internal/pycore_import.h
@@ -39,8 +39,13 @@ extern PyObject * _PyImport_GetAbsName(
 // Symbol is exported for the JIT on Windows builds.
 PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate(
     PyThreadState *tstate, PyObject *lazy_import);
-extern PyObject * _PyImport_TryLoadLazySubmodule(
-    PyObject *mod_name, PyObject *attr_name);
+typedef enum {
+    _Py_LAZY_SUBMODULE_ERROR = -1,
+    _Py_LAZY_SUBMODULE_NOT_FOUND = 0,
+    _Py_LAZY_SUBMODULE_LOADED = 1,
+} _PyLazySubmoduleImportResult;
+extern _PyLazySubmoduleImportResult _PyImport_TryLoadLazySubmodule(
+    PyObject *mod_name, PyObject *attr_name, PyObject **result);
 extern PyObject * _PyImport_LazyImportModuleLevelObject(
     PyThreadState *tstate, PyObject *name, PyObject *builtins,
     PyObject *globals, PyObject *locals, PyObject *fromlist, int level);
diff --git a/Include/internal/pycore_runtime_init_generated.h 
b/Include/internal/pycore_runtime_init_generated.h
index 366d2d300fb4780..378f27ca17b5079 100644
--- a/Include/internal/pycore_runtime_init_generated.h
+++ b/Include/internal/pycore_runtime_init_generated.h
@@ -1542,6 +1542,7 @@ extern "C" {
     INIT_ID(_filters), \
     INIT_ID(_finalizing), \
     INIT_ID(_find_and_load), \
+    INIT_ID(_find_and_load_lazy_submodule), \
     INIT_ID(_fix_up_module), \
     INIT_ID(_flags_), \
     INIT_ID(_get_sourcefile), \
diff --git a/Include/internal/pycore_unicodeobject_generated.h 
b/Include/internal/pycore_unicodeobject_generated.h
index 00d6297432b5fca..daf6840aa47f328 100644
--- a/Include/internal/pycore_unicodeobject_generated.h
+++ b/Include/internal/pycore_unicodeobject_generated.h
@@ -848,6 +848,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
     assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(_find_and_load_lazy_submodule);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
     string = &_Py_ID(_fix_up_module);
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py
index eb1686a5c8217c6..081bb98ec2a9723 100644
--- a/Lib/importlib/_bootstrap.py
+++ b/Lib/importlib/_bootstrap.py
@@ -1254,7 +1254,7 @@ def _sanity_check(name, package, level):
 
 _ERR_MSG_PREFIX = 'No module named '
 
-def _find_and_load_unlocked(name, import_):
+def _find_and_load_unlocked(name, import_, *, lazy_submodule=False):
     path = None
     sys.audit(
         "import",
@@ -1277,6 +1277,8 @@ def _find_and_load_unlocked(name, import_):
         try:
             path = parent_module.__path__
         except AttributeError:
+            if lazy_submodule:
+                return None
             msg = f'{_ERR_MSG_PREFIX}{name!r}; {parent!r} is not a package'
             raise ModuleNotFoundError(msg, name=name) from None
         parent_spec = parent_module.__spec__
@@ -1289,6 +1291,8 @@ def _find_and_load_unlocked(name, import_):
         child = name.rpartition('.')[2]
     spec = _find_spec(name, path)
     if spec is None:
+        if lazy_submodule:
+            return None
         raise ModuleNotFoundError(f'{_ERR_MSG_PREFIX}{name!r}', name=name)
     else:
         if parent_spec:
@@ -1320,7 +1324,7 @@ def _find_and_load_unlocked(name, import_):
 _NEEDS_LOADING = object()
 
 
-def _find_and_load(name, import_):
+def _find_and_load(name, import_, *, lazy_submodule=False):
     """Find and load the module."""
 
     # Optimization: we avoid unneeded module locking if the module
@@ -1337,7 +1341,8 @@ def _find_and_load(name, import_):
         with lock_manager:
             module = sys.modules.get(name, _NEEDS_LOADING)
             if module is _NEEDS_LOADING:
-                return _find_and_load_unlocked(name, import_)
+                return _find_and_load_unlocked(
+                    name, import_, lazy_submodule=lazy_submodule)
 
         # Optimization: only call _bootstrap._lock_unlock_module() if
         # module.__spec__._initializing is True.
@@ -1351,7 +1356,7 @@ def _find_and_load(name, import_):
         # to preserve normal semantics: the caller gets the exception from
         # the actual import failure rather than a synthetic error.
         if sys.modules.get(name) is not module:
-            return _find_and_load(name, import_)
+            return _find_and_load(name, import_, lazy_submodule=lazy_submodule)
 
     if module is None:
         message = f'import of {name} halted; None in sys.modules'
@@ -1360,6 +1365,10 @@ def _find_and_load(name, import_):
     return module
 
 
+def _find_and_load_lazy_submodule(name, import_):
+    return _find_and_load(name, import_, lazy_submodule=True)
+
+
 def _gcd_import(name, package=None, level=0):
     """Import and return the module based on its name, the package the call is
     being made from, and the level adjustment.
diff --git a/Lib/test/test_lazy_import/__init__.py 
b/Lib/test/test_lazy_import/__init__.py
index cf1d3eb793a8b94..899ecec1ba2afa4 100644
--- a/Lib/test/test_lazy_import/__init__.py
+++ b/Lib/test/test_lazy_import/__init__.py
@@ -676,52 +676,35 @@ def test_lazy_modules_tracks_lazy_imports(self):
 
 @support.requires_subprocess()
 class ErrorHandlingTests(LazyImportTestCase):
-    """Tests for error handling during lazy import reification.
+    """Tests for error handling during lazy import reification."""
 
-    PEP 810: Errors during reification should show exception chaining with
-    both the lazy import definition location and the access location.
-    """
-
-    def test_import_error_shows_chained_traceback(self):
+    def test_missing_lazy_submodule_raises_attribute_error(self):
         """Accessing a nonexistent lazy submodule via parent attr raises 
AttributeError."""
         code = textwrap.dedent("""
-            import sys
             lazy import test.test_lazy_import.data.nonexistent_module
 
             try:
-                x = test.test_lazy_import.data.nonexistent_module
-            except AttributeError as e:
-                print("OK")
+                _ = test.test_lazy_import.data.nonexistent_module
+            except AttributeError:
+                pass
+            else:
+                raise AssertionError("AttributeError was not raised")
         """)
-        result = subprocess.run(
-            [sys.executable, "-c", code],
-            capture_output=True,
-            text=True
-        )
-        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, 
stderr: {result.stderr}")
-        self.assertIn("OK", result.stdout)
+        assert_python_ok("-c", code)
 
-    def test_attribute_error_on_from_import_shows_chained_traceback(self):
+    def test_missing_lazy_from_import_shows_chained_traceback(self):
         """Accessing missing attribute from lazy from-import should chain 
errors."""
-        # Tests 'lazy from module import nonexistent' behavior
         code = textwrap.dedent("""
-            import sys
             lazy from test.test_lazy_import.data.basic2 import nonexistent_name
 
             try:
-                x = nonexistent_name
+                _ = nonexistent_name
             except ImportError as e:
-                # PEP 810: Enhanced error reporting through exception chaining
                 assert e.__cause__ is not None, "Expected chained exception"
-                print("OK")
+            else:
+                raise AssertionError("ImportError was not raised")
         """)
-        result = subprocess.run(
-            [sys.executable, "-c", code],
-            capture_output=True,
-            text=True
-        )
-        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, 
stderr: {result.stderr}")
-        self.assertIn("OK", result.stdout)
+        assert_python_ok("-c", code)
 
     def test_reification_retries_on_failure(self):
         """Failed reification should allow retry on subsequent access.
@@ -731,53 +714,92 @@ def test_reification_retries_on_failure(self):
         """
         code = textwrap.dedent("""
             import sys
-            import types
 
             lazy import test.test_lazy_import.data.broken_module
 
-            # First access - should fail
             try:
-                x = test.test_lazy_import.data.broken_module
-            except AttributeError:
-                pass
+                _ = test.test_lazy_import.data.broken_module
+            except ValueError as exc:
+                assert str(exc) == "This module always fails to import", exc
+            else:
+                raise AssertionError("ValueError was not raised")
+
+            assert "test.test_lazy_import.data.broken_module" not in 
sys.modules
 
-            # The lazy object should still be a lazy proxy (not reified)
-            g = globals()
-            lazy_obj = g['test']
-            # The root 'test' binding should still allow retry
-            # Second access - should also fail (retry the import)
             try:
-                x = test.test_lazy_import.data.broken_module
-            except AttributeError:
-                print("OK - retry worked")
+                _ = test.test_lazy_import.data.broken_module
+            except ValueError as exc:
+                assert str(exc) == "This module always fails to import", exc
+            else:
+                raise AssertionError("ValueError was not raised")
         """)
-        result = subprocess.run(
-            [sys.executable, "-c", code],
-            capture_output=True,
-            text=True
-        )
-        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, 
stderr: {result.stderr}")
-        self.assertIn("OK", result.stdout)
+        assert_python_ok("-c", code)
 
-    def test_error_during_module_execution_propagates(self):
-        """Errors in module code during reification should propagate 
correctly."""
+    def test_lazy_submodule_traceback_hides_importlib_frames(self):
         code = textwrap.dedent("""
-            import sys
+            import traceback
+
             lazy import test.test_lazy_import.data.broken_module
 
             try:
                 _ = test.test_lazy_import.data.broken_module
-                print("FAIL - should have raised")
-            except AttributeError:
-                print("OK")
+            except ValueError as exc:
+                frames = traceback.extract_tb(exc.__traceback__)
+                assert [frame.name for frame in frames] == ["<module>", 
"<module>"], frames
+                assert frames[0].filename == "<string>", frames
+                assert frames[1].filename.endswith("broken_module.py"), frames
+            else:
+                raise AssertionError("ValueError was not raised")
         """)
-        result = subprocess.run(
-            [sys.executable, "-c", code],
-            capture_output=True,
-            text=True
-        )
-        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, 
stderr: {result.stderr}")
-        self.assertIn("OK", result.stdout)
+        assert_python_ok("-c", code)
+
+    def test_module_not_found_during_module_execution_propagates(self):
+        code = textwrap.dedent("""
+            lazy import test.test_lazy_import.data.missing_dependency
+
+            try:
+                _ = test.test_lazy_import.data.missing_dependency
+            except ModuleNotFoundError as exc:
+                assert exc.name == "missing_dependency_for_lazy_import_test", 
exc.name
+                assert str(exc) == "No module named 
'missing_dependency_for_lazy_import_test'", exc
+            else:
+                raise AssertionError("ModuleNotFoundError was not raised")
+        """)
+        assert_python_ok("-c", code)
+
+    def 
test_self_named_module_not_found_during_module_execution_propagates(self):
+        code = textwrap.dedent("""
+            lazy import test.test_lazy_import.data.self_named_module_not_found
+
+            try:
+                _ = test.test_lazy_import.data.self_named_module_not_found
+            except ModuleNotFoundError as exc:
+                assert exc.name == 
"test.test_lazy_import.data.self_named_module_not_found", exc.name
+                assert str(exc) == "boom", exc
+            else:
+                raise AssertionError("ModuleNotFoundError was not raised")
+        """)
+        assert_python_ok("-c", code)
+
+    def test_none_in_sys_modules_during_submodule_resolution_propagates(self):
+        code = textwrap.dedent("""
+            import sys
+
+            sys.modules["test.test_lazy_import.data.blocked_module"] = None
+            lazy import test.test_lazy_import.data.blocked_module
+
+            try:
+                _ = test.test_lazy_import.data.blocked_module
+            except ModuleNotFoundError as exc:
+                assert exc.name == 
"test.test_lazy_import.data.blocked_module", exc.name
+                assert str(exc) == (
+                    "import of test.test_lazy_import.data.blocked_module "
+                    "halted; None in sys.modules"
+                ), exc
+            else:
+                raise AssertionError("ModuleNotFoundError was not raised")
+        """)
+        assert_python_ok("-c", code)
 
     def test_circular_lazy_import_does_not_crash_for_gh_144727(self):
         with tempfile.TemporaryDirectory() as tmpdir:
diff --git a/Lib/test/test_lazy_import/data/missing_dependency.py 
b/Lib/test/test_lazy_import/data/missing_dependency.py
new file mode 100644
index 000000000000000..aa2f2d0f65f73fe
--- /dev/null
+++ b/Lib/test/test_lazy_import/data/missing_dependency.py
@@ -0,0 +1 @@
+import missing_dependency_for_lazy_import_test
diff --git a/Lib/test/test_lazy_import/data/self_named_module_not_found.py 
b/Lib/test/test_lazy_import/data/self_named_module_not_found.py
new file mode 100644
index 000000000000000..941230d5da211d8
--- /dev/null
+++ b/Lib/test/test_lazy_import/data/self_named_module_not_found.py
@@ -0,0 +1 @@
+raise ModuleNotFoundError("boom", name=__name__)
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-06-16-30-00.gh-issue-153236.lazy-import-errors.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-06-16-30-00.gh-issue-153236.lazy-import-errors.rst
new file mode 100644
index 000000000000000..c5043af0a4f80b7
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-06-16-30-00.gh-issue-153236.lazy-import-errors.rst
@@ -0,0 +1,2 @@
+Propagate exceptions raised while importing lazy submodules instead of
+reporting them as missing attributes.
diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c
index f447403ef31b43a..b8cd6025c20ba56 100644
--- a/Objects/moduleobject.c
+++ b/Objects/moduleobject.c
@@ -1299,8 +1299,6 @@ _PyModule_IsPossiblyShadowing(PyObject *origin)
     return result;
 }
 
-// Check if `name` is a lazily pending submodule of module `m`.
-// Returns a new reference on success, or NULL with no error set.
 static PyObject *
 try_load_lazy_submodule(PyModuleObject *m, PyObject *name)
 {
@@ -1313,10 +1311,13 @@ try_load_lazy_submodule(PyModuleObject *m, PyObject 
*name)
         Py_DECREF(mod_name);
         return NULL;
     }
-    PyObject *result = _PyImport_TryLoadLazySubmodule(mod_name, name);
+    PyObject *result = NULL;
+    _PyLazySubmoduleImportResult status =
+        _PyImport_TryLoadLazySubmodule(mod_name, name, &result);
     Py_DECREF(mod_name);
-    if (result == NULL) {
-        PyErr_Clear();
+    if (status != _Py_LAZY_SUBMODULE_LOADED) {
+        assert(status == _Py_LAZY_SUBMODULE_ERROR ||
+               status == _Py_LAZY_SUBMODULE_NOT_FOUND);
         return NULL;
     }
     if (PyDict_SetItem(m->md_dict, name, result) < 0) {
diff --git a/Python/import.c b/Python/import.c
index 63e23e21beb1266..5ca78a971fa54c6 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -4103,7 +4103,9 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, 
PyObject *lazy_import)
 }
 
 static PyObject *
-import_find_and_load(PyThreadState *tstate, PyObject *abs_name)
+import_find_and_load_with_name(PyThreadState *tstate, PyObject *abs_name,
+                               PyObject *find_and_load,
+                               PyObject *not_found)
 {
     PyObject *mod = NULL;
     PyInterpreterState *interp = tstate->interp;
@@ -4130,12 +4132,14 @@ import_find_and_load(PyThreadState *tstate, PyObject 
*abs_name)
     if (PyDTrace_IMPORT_FIND_LOAD_START_ENABLED())
         PyDTrace_IMPORT_FIND_LOAD_START(PyUnicode_AsUTF8(abs_name));
 
-    mod = PyObject_CallMethodObjArgs(IMPORTLIB(interp), 
&_Py_ID(_find_and_load),
+    mod = PyObject_CallMethodObjArgs(IMPORTLIB(interp), find_and_load,
                                      abs_name, IMPORT_FUNC(interp), NULL);
 
-    if (PyDTrace_IMPORT_FIND_LOAD_DONE_ENABLED())
+    if (PyDTrace_IMPORT_FIND_LOAD_DONE_ENABLED()) {
+        int found = mod != NULL && mod != not_found;
         PyDTrace_IMPORT_FIND_LOAD_DONE(PyUnicode_AsUTF8(abs_name),
-                                       mod != NULL);
+                                       found);
+    }
 
     if (import_time) {
         PyTime_t t2;
@@ -4156,6 +4160,13 @@ import_find_and_load(PyThreadState *tstate, PyObject 
*abs_name)
 #undef accumulated
 }
 
+static PyObject *
+import_find_and_load(PyThreadState *tstate, PyObject *abs_name)
+{
+    return import_find_and_load_with_name(
+        tstate, abs_name, &_Py_ID(_find_and_load), NULL);
+}
+
 static PyObject *
 get_abs_name(PyThreadState *tstate, PyObject *name, PyObject *globals,
              int level)
@@ -4439,52 +4450,70 @@ register_from_lazy_on_parent(PyThreadState *tstate, 
PyObject *abs_name,
     return res;
 }
 
-PyObject *
-_PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name)
+_PyLazySubmoduleImportResult
+_PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name,
+                               PyObject **result)
 {
-    PyInterpreterState *interp = _PyInterpreterState_GET();
+    assert(result != NULL);
+    *result = NULL;
+
+    PyThreadState *tstate = _PyThreadState_GET();
+    PyInterpreterState *interp = tstate->interp;
     PyObject *lazy_pending = LAZY_PENDING_SUBMODULES(interp);
     if (lazy_pending == NULL) {
-        return NULL;
+        return _Py_LAZY_SUBMODULE_NOT_FOUND;
     }
 
     PyObject *pending_set;
     int rc = PyDict_GetItemRef(lazy_pending, mod_name, &pending_set);
-    if (rc <= 0) {
-        return NULL;
+    if (rc < 0) {
+        return _Py_LAZY_SUBMODULE_ERROR;
+    }
+    if (rc == 0) {
+        return _Py_LAZY_SUBMODULE_NOT_FOUND;
     }
 
     int contains = PySet_Contains(pending_set, attr_name);
-    if (contains <= 0) {
+    if (contains < 0) {
         Py_DECREF(pending_set);
-        return NULL;
+        return _Py_LAZY_SUBMODULE_ERROR;
+    }
+    if (contains == 0) {
+        Py_DECREF(pending_set);
+        return _Py_LAZY_SUBMODULE_NOT_FOUND;
     }
 
     PyObject *full_name = PyUnicode_FromFormat("%U.%U", mod_name, attr_name);
     if (full_name == NULL) {
         Py_DECREF(pending_set);
-        return NULL;
+        return _Py_LAZY_SUBMODULE_ERROR;
     }
 
-    PyObject *mod = PyImport_ImportModuleLevelObject(
-        full_name, NULL, NULL, NULL, 0);
+    PyObject *mod = import_find_and_load_with_name(
+        tstate, full_name, &_Py_ID(_find_and_load_lazy_submodule), Py_None);
     if (mod == NULL) {
         Py_DECREF(pending_set);
         Py_DECREF(full_name);
-        return NULL;
+        remove_importlib_frames(tstate);
+        return _Py_LAZY_SUBMODULE_ERROR;
+    }
+    if (mod == Py_None) {
+        Py_DECREF(mod);
+        Py_DECREF(pending_set);
+        Py_DECREF(full_name);
+        return _Py_LAZY_SUBMODULE_NOT_FOUND;
     }
-    Py_DECREF(mod);
 
     if (PySet_Discard(pending_set, attr_name) < 0) {
+        Py_DECREF(mod);
         Py_DECREF(pending_set);
         Py_DECREF(full_name);
-        return NULL;
+        return _Py_LAZY_SUBMODULE_ERROR;
     }
     Py_DECREF(pending_set);
-
-    PyObject *submod = PyImport_GetModule(full_name);
     Py_DECREF(full_name);
-    return submod;
+    *result = mod;
+    return _Py_LAZY_SUBMODULE_LOADED;
 }
 
 PyObject *

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to