Repository: zulip/zulip

Issue:
Throw an error when EMAIL_HOST_USER is specified but not email_password
As a convenience (from someone who was misreading zulip/zulip-secrets.conf as zulip/secrets.conf), it would be nice to see an error message when EMAIL_HOST_USER is specified but the password is missing in the secrets file.


Discussion:
Hello @namangirdhar16!

Thanks for your interest in Zulip! You have attempted to claim an issue without the labels "help wanted", "good first issue". Since you're a new contributor, you can only claim and submit pull requests for issues with the [help wanted](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+label%3A%22help+wanted%22) or [good first issue](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+label%3A%22good+first+issue%22) labels.

If this is your first time here, we recommend reading our [guide for new contributors](https://zulip.readthedocs.io/en/latest/overview/contributing.html) before getting started.

Hello @zulip/server-production members, this issue was labeled with the "area: production installer" label, so you may want to check it out!

<!-- areaLabelAddition -->

This seems useful and likely easy.  I think perhaps we'd want to change this function to add an additional check after the loop?  Not sure.  @mateuszmandera FYI.

```
def check_config() -> None:                                                                              
    for (setting_name, default) in settings.REQUIRED_SETTINGS:                                           
        # if required setting is the same as default OR is not found in settings,                        
        # throw error to add/set that setting in config                                                  
        try:                                                                                             
            if settings.__getattr__(setting_name) != default:                                            
                continue                                                                                 
        except AttributeError:                                                                           
            pass                                                                                         
                                                                                                         
        raise CommandError(f"Error: You must set {setting_name} in /etc/zulip/settings.py.")             
```


@timabbott Hmm, though this only gets called in `manage.py checkconfig` (and for some reason in `register_server`) - so I'm not sure how many admins actually utilize that command, especially since we don't particularly advertize it (only in `management-commands.md`). Also called in `initialize-database`, but that runs only once, during installation, so it's not useful for this issue either.

I'm wondering if it wouldn't make sense to add some kind of `check_config()` function that's called in zproject/settings.py after the setting imports, that could be generally used for putting all sorts of assertions in it for validating that settings make sense?:
```
from .configured_settings import *  # noqa: F401,F403 isort: skip
from .computed_settings import *  # noqa: F401,F403 isort: skip

check_config()
# Do not add any code after these wildcard imports!  Add it to
# computed_settings instead.
```

Checks like this are a little complicated because they can fire when the server process starts up.  If we add anything like this, we need to make sure that we start running `check_config` in `restart_server` so that we can abort with a clear admin-visible error message before the restart, so they aren't left wondering and having to log-dive for why uwsgi isn't coming up cleanly.
@alexmv Would `logging.error` in the hypothetical `check_config` function solve this concern? (as opposed to hard `AssertionError`s)
That pushes it back into "is this visible enough" since it's only going to show up if folks check their logs.  If you're running the check at startup, that also means it's going to show up in the logs at random intervals when uwsgi processes get (re)started.

We might be better off doing this sort of check, with a `logging.error`, when we're trying to send an email -- rather than trying to do it at "start time," which is not well-defined and not always an interactive process.
Logging an error on email send makes sense to me.

-Tim Abbott (mobile)

On Tue, Feb 1, 2022, 14:21 Alex Vandiver ***@***.***> wrote:

> That pushes it back into "is this visible enough" since it's only going to
> show up if folks check their logs. If you're running the check at startup,
> that also means it's going to show up in the logs at random intervals when
> uwsgi processes get (re)started.
>
> We might be better off doing this sort of check, with a logging.error,
> when we're trying to send an email -- rather than trying to do it at "start
> time," which is not well-defined and not always an interactive process.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/zulip/zulip/issues/20132#issuecomment-1027345167>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAU6NWQ2KKAALE65FXCSFF3UZBMHFANCNFSM5HFHVLCQ>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>


Resolving patch:
diff --git a/zerver/lib/send_email.py b/zerver/lib/send_email.py
--- a/zerver/lib/send_email.py
+++ b/zerver/lib/send_email.py
@@ -249,6 +249,8 @@ def send_email(
     )
     template = template_prefix.split("/")[-1]
 
+    log_email_config_errors()
+
     if dry_run:
         print(mail.message().get_payload()[0])
         return
@@ -588,3 +590,14 @@ def send_custom_email(
 
         if options["dry_run"]:
             break
+
+
+def log_email_config_errors() -> None:
+    """
+    The purpose of this function is to log (potential) config errors,
+    but without raising an exception.
+    """
+    if settings.EMAIL_HOST_USER and not settings.EMAIL_HOST_PASSWORD:
+        logger.error(
+            "An SMTP username was set (EMAIL_HOST_USER), but password is unset (EMAIL_HOST_PASSWORD)."
+        )


Test patch:
diff --git a/zerver/tests/test_send_email.py b/zerver/tests/test_send_email.py
--- a/zerver/tests/test_send_email.py
+++ b/zerver/tests/test_send_email.py
@@ -147,3 +147,24 @@ def test_send_email_exceptions(self) -> None:
                     f"INFO:{logger.name}:Sending password_reset email to {mail.to}",
                 )
                 self.assertTrue(info_log.output[1].startswith(f"ERROR:zulip.send_email:{message}"))
+
+    def test_send_email_config_error_logging(self) -> None:
+        hamlet = self.example_user("hamlet")
+
+        with self.settings(EMAIL_HOST_USER="test", EMAIL_HOST_PASSWORD=None):
+            with self.assertLogs(logger=logger, level="ERROR") as error_log:
+                send_email(
+                    "zerver/emails/password_reset",
+                    to_emails=[hamlet],
+                    from_name="From Name",
+                    from_address=FromAddress.NOREPLY,
+                    language="en",
+                )
+
+        self.assertEqual(
+            error_log.output,
+            [
+                "ERROR:zulip.send_email:"
+                "An SMTP username was set (EMAIL_HOST_USER), but password is unset (EMAIL_HOST_PASSWORD)."
+            ],
+        )