Alternative EMAIL_BACKEND

Would it be possible to setup an email backend other than django.core.mail.backends.smtp.EmailBackend specifically, I am trying to use anymail.backends.mailjet.EmailBackend.

I have tried to set the EMAIL_BACKEND in the config.yml and the cms.env.yml and lms.env.yml files but this does not seem to get picked up by a build and/or restart of tutor. I was able to add the OPENEDX_EXTRA_PIP_REQUIREMENTS to install django-anymail but that doesn’t help if I can’t override the backend setting.

After a build I still see the EMAIL_BACKEND are pointed to smtp

app@b1a127346f80:~/edx-platform$ grep -rnw . -e "EMAIL_BACKEND"
./cms/envs/devstack-experimental.yml:283:EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
./cms/envs/common.py:1265:EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
./cms/envs/devstack.py:41:EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
Binary file ./cms/envs/__pycache__/common.cpython-311.pyc matches
./lms/envs/devstack-experimental.yml:307:EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
./lms/envs/common.py:1913:EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
./lms/envs/devstack.py:69:EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'

This is what my configs look like

frank_ventura@edx-tutor:~/.local/share$ grep -rnw . -e "EMAIL_BACKEND"
./tutor/config.yml:4:EMAIL_BACKEND: anymail.backends.mailjet.EmailBackend
./tutor/env/apps/openedx/config/cms.env.yml:37:EMAIL_BACKEND: anymail.backends.mailjet.EmailBackend
./tutor/env/apps/openedx/config/lms.env.yml:44:EMAIL_BACKEND: anymail.backends.mailjet.EmailBackend

Is it possible to use a different email backend?

how are you adding values to cms.env and lms.env yaml files? If you are editing the files directly in env, they will be overwritten on config save. You should be using a plugin to do that Configuration and customisation — Tutor documentation

1 Like

Yes, I have a plugin that looks like:

from tutor import hooks

hooks.Filters.CONFIG_USER.add_item(
    ("EMAIL_BACKEND", "anymail.backends.mailjet.EmailBackend"),
)

hooks.Filters.CONFIG_OVERRIDES.add_item(
    ("EMAIL_BACKEND", "anymail.backends.mailjet.EmailBackend"),
)

hooks.Filters.ENV_PATCHES.add_items(
    [
        ("cms-env", "EMAIL_BACKEND: anymail.backends.mailjet.EmailBackend"),
        ("lms-env", "EMAIL_BACKEND: anymail.backends.mailjet.EmailBackend"),
    ]
)

However, I do find that the CONFIG_OVERRIDES does not seem to override a config in those cms.env and lms.env yaml files like I would think it should. Instead, the ENV_PATCHES adds it and I had manually removed the old values.

I think I made some progress. I needed to add ("RUN_SMTP", False), to the CONFIG_USER

hooks.Filters.CONFIG_USER.add_items(
    [
        ("EMAIL_BACKEND", "anymail.backends.mailjet.EmailBackend"),
        ("RUN_SMTP", False),
    ]
)

And then add the settings

hooks.Filters.ENV_PATCHES.add_items(
    [
        ("cms-env", "EMAIL_BACKEND: anymail.backends.mailjet.EmailBackend"),
        ("lms-env", "EMAIL_BACKEND: anymail.backends.mailjet.EmailBackend"),
        ("cms-env", "MAILJET_API_KEY: MAILJET_API_KEY"),
        ("lms-env", "MAILJET_API_KEY: MAILJET_API_KEY"),
        ("cms-env", "MAILJET_SECRET_KEY: MAILJET_SECRET_KEY"),
        ("lms-env", "MAILJET_SECRET_KEY: MAILJET_SECRET_KEY"),
    ]
)

I have verified that anymail should be installed, it is in the config.yml after I ran tutor config save --append OPENEDX_EXTRA_PIP_REQUIREMENTS=django-anymail[mailjet]==8.6

And a grep of the docker shows it is there, cat ~/.local/share/tutor/env/build/openedx/Dockerfile | grep mail

After a disable, enable of the plugin, re build of the openedx with tutor images build openedx --no-cache I watch and see that django-anymail is installed.

And I can run the command successfully from command line. This command works and I get the email:
tutor local run --no-deps lms ./manage.py lms shell -c "from django.core.mail import send_mail; send_mail('test subject', 'test message', 'test@test.org', ['me@gmail.com'])"

However, when I bash into the LMS image with sudo docker exec -it tutor_local-lms-1 bash and go a pip freeze | grep anymail I get no results like anymail was not installed. Or maybe I misunderstand how the extra pip installation works? And trying to send an email to a user in the frontend results in the error below:

lms-1  | [pid: 1|app: 0|req: 16/33] 172.18.0.7 () {66 vars in 2842 bytes} [Sat Sep 14 19:12:02 2024] POST /courses/course-v1:ThetaTau+TT101+intro/instructor/api/list_instructor_tasks => generated 17 bytes in 67 msecs (HTTP/1.1 200) 10 headers in 607 bytes (1 switches on core 0)
lms-1  | 2024-09-14 19:12:04,835 INFO 15 [tracking] [user 4] [ip 172.110.170.18] logger.py:41 - {"name": "/courses/course-v1:ThetaTau+TT101+intro/instructor/api/students_update_enrollment", "context": {"course_id": "course-v1:ThetaTau+TT101+intro", "course_user_tags": {}, "user_id": 4, "path": "/courses/course-v1:ThetaTau+TT101+intro/instructor/api/students_update_enrollment", "org_id": "ThetaTau", "enterprise_uuid": ""}, "username": "frank.ventura@thetatau.org", "session": "7145a02b62697f10e29542f8242076b3", "ip": "172.110.170.18", "agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", "host": "ed.thetatau.org", "referer": "https://ed.thetatau.org/courses/course-v1:ThetaTau+TT101+intro/instructor", "accept_language": "en-US,en;q=0.9", "event": "{\"GET\": {}, \"POST\": {\"action\": [\"enroll\"], \"identifiers\": [\"frank.ventura+1234@thetatau.org\"], \"auto_enroll\": [\"true\"], \"email_students\": [\"true\"]}}", "time": "2024-09-14T19:12:04.835549+00:00", "event_type": "/courses/course-v1:ThetaTau+TT101+intro/instructor/api/students_update_enrollment", "event_source": "server", "page": null}
lms-1  | 2024-09-14 19:12:04,944 ERROR 15 [lms.djangoapps.instructor.views.api] [user 4] [ip 172.110.170.18] api.py:873 - Error while #{}ing student
lms-1  | Traceback (most recent call last):
lms-1  |   File "/openedx/edx-platform/lms/djangoapps/instructor/views/api.py", line 819, in students_update_enrollment
lms-1  |     before, after, enrollment_obj = enroll_email(
lms-1  |                                     ^^^^^^^^^^^^^
lms-1  |   File "/openedx/edx-platform/lms/djangoapps/instructor/enrollment.py", line 177, in enroll_email
lms-1  |     send_mail_to_student(student_email, email_params, language=language)
lms-1  |   File "/openedx/edx-platform/lms/djangoapps/instructor/enrollment.py", line 561, in send_mail_to_student
lms-1  |     ace.send(message)
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/edx_ace/ace.py", line 53, in send
lms-1  |     delivery.deliver(channel, rendered_message, msg)
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/edx_ace/delivery.py", line 51, in deliver
lms-1  |     channel.deliver(message, rendered_message)
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/edx_ace/channel/django_email.py", line 68, in deliver
lms-1  |     mail.send()
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/django/core/mail/message.py", line 299, in send
lms-1  |     return self.get_connection(fail_silently).send_messages([self])
lms-1  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/django/core/mail/message.py", line 256, in get_connection
lms-1  |     self.connection = get_connection(fail_silently=fail_silently)
lms-1  |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/django/core/mail/__init__.py", line 50, in get_connection
lms-1  |     klass = import_string(backend or settings.EMAIL_BACKEND)
lms-1  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/django/utils/module_loading.py", line 30, in import_string
lms-1  |     return cached_import(module_path, class_name)
lms-1  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/django/utils/module_loading.py", line 15, in cached_import
lms-1  |     module = import_module(module_path)
lms-1  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "/opt/pyenv/versions/3.11.8/lib/python3.11/importlib/__init__.py", line 126, in import_module
lms-1  |     return _bootstrap._gcd_import(name[level:], package, level)
lms-1  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
lms-1  |   File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
lms-1  |   File "<frozen importlib._bootstrap>", line 1126, in _find_and_load_unlocked
lms-1  |   File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
lms-1  |   File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
lms-1  |   File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
lms-1  |   File "<frozen importlib._bootstrap>", line 1126, in _find_and_load_unlocked
lms-1  |   File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
lms-1  |   File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
lms-1  |   File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
lms-1  |   File "<frozen importlib._bootstrap>", line 1140, in _find_and_load_unlocked
lms-1  | ModuleNotFoundError: No module named 'anymail'
lms-1  | 2024-09-14 19:12:04,948 ERROR 15 [lms.djangoapps.instructor.views.api] [user 4] [ip 172.110.170.18] api.py:874 - No module named 'anymail'
lms-1  | Traceback (most recent call last):
lms-1  |   File "/openedx/edx-platform/lms/djangoapps/instructor/views/api.py", line 819, in students_update_enrollment
lms-1  |     before, after, enrollment_obj = enroll_email(
lms-1  |                                     ^^^^^^^^^^^^^
lms-1  |   File "/openedx/edx-platform/lms/djangoapps/instructor/enrollment.py", line 177, in enroll_email
lms-1  |     send_mail_to_student(student_email, email_params, language=language)
lms-1  |   File "/openedx/edx-platform/lms/djangoapps/instructor/enrollment.py", line 561, in send_mail_to_student
lms-1  |     ace.send(message)
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/edx_ace/ace.py", line 53, in send
lms-1  |     delivery.deliver(channel, rendered_message, msg)
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/edx_ace/delivery.py", line 51, in deliver
lms-1  |     channel.deliver(message, rendered_message)
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/edx_ace/channel/django_email.py", line 68, in deliver
lms-1  |     mail.send()
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/django/core/mail/message.py", line 299, in send
lms-1  |     return self.get_connection(fail_silently).send_messages([self])
lms-1  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/django/core/mail/message.py", line 256, in get_connection
lms-1  |     self.connection = get_connection(fail_silently=fail_silently)
lms-1  |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/django/core/mail/__init__.py", line 50, in get_connection
lms-1  |     klass = import_string(backend or settings.EMAIL_BACKEND)
lms-1  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/django/utils/module_loading.py", line 30, in import_string
lms-1  |     return cached_import(module_path, class_name)
lms-1  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "/openedx/venv/lib/python3.11/site-packages/django/utils/module_loading.py", line 15, in cached_import
lms-1  |     module = import_module(module_path)
lms-1  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "/opt/pyenv/versions/3.11.8/lib/python3.11/importlib/__init__.py", line 126, in import_module
lms-1  |     return _bootstrap._gcd_import(name[level:], package, level)
lms-1  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lms-1  |   File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
lms-1  |   File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
lms-1  |   File "<frozen importlib._bootstrap>", line 1126, in _find_and_load_unlocked
lms-1  |   File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
lms-1  |   File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
lms-1  |   File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
lms-1  |   File "<frozen importlib._bootstrap>", line 1126, in _find_and_load_unlocked
lms-1  |   File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
lms-1  |   File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
lms-1  |   File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
lms-1  |   File "<frozen importlib._bootstrap>", line 1140, in _find_and_load_unlocked
lms-1  | ModuleNotFoundError: No module named 'anymail'
lms-1  | [pid: 15|app: 0|req: 16/34] 172.18.0.7 () {68 vars in 2917 bytes} [Sat Sep 14 19:12:04 2024] POST /courses/course-v1:ThetaTau+TT101+intro/instructor/api/students_update_enrollment => generated 154 bytes in 150 msecs (HTTP/1.1 200) 10 headers in 608 bytes (1 switches on core 0)

What am I doing wrong with the OPENEDX_EXTRA_PIP_REQUIREMENTS and then seeing that in the container of the LMS?

If inside the container with
sudo docker exec -it tutor_local-lms-1 bash
and I run
pip install django-anymail[mailjet]==8.6
the LMS can send emails just fine.

It looks like this whole time I have been running
tutor local restart
This does NOT seem to pick up the new build. I needed to run:
tutor local launch
And now the email through Mailjet seems to be working.