2021-05-11 23:24:52 +02:00
|
|
|
from smtplib import SMTP, SMTPDataError, SMTPException, SMTPRecipientsRefused
|
2021-03-20 14:07:02 +01:00
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
from django.core.mail.backends.locmem import EmailBackend
|
|
|
|
from django.core.mail.backends.smtp import EmailBackend as SMTPBackend
|
2021-04-03 11:58:16 +02:00
|
|
|
from django.core.mail.message import sanitize_address
|
|
|
|
|
2021-04-03 12:14:01 +02:00
|
|
|
from zerver.lib.send_email import (
|
|
|
|
EmailNotDeliveredException,
|
|
|
|
FromAddress,
|
|
|
|
build_email,
|
|
|
|
initialize_connection,
|
|
|
|
logger,
|
|
|
|
send_email,
|
|
|
|
)
|
2021-04-03 11:58:16 +02:00
|
|
|
from zerver.lib.test_classes import ZulipTestCase
|
|
|
|
|
|
|
|
|
|
|
|
class TestBuildEmail(ZulipTestCase):
|
2021-12-14 23:45:18 +01:00
|
|
|
def test_limited_from_length(self) -> None:
|
2021-04-03 11:58:16 +02:00
|
|
|
hamlet = self.example_user("hamlet")
|
2021-12-14 23:45:18 +01:00
|
|
|
# This is exactly the max length
|
2021-04-03 11:58:16 +02:00
|
|
|
limit_length_name = "a" * (320 - len(sanitize_address(FromAddress.NOREPLY, "utf-8")) - 3)
|
|
|
|
mail = build_email(
|
|
|
|
"zerver/emails/password_reset",
|
|
|
|
to_emails=[hamlet],
|
|
|
|
from_name=limit_length_name,
|
|
|
|
from_address=FromAddress.NOREPLY,
|
|
|
|
language="en",
|
|
|
|
)
|
2021-08-02 23:36:06 +02:00
|
|
|
self.assertEqual(mail.extra_headers["From"], f"{limit_length_name} <{FromAddress.NOREPLY}>")
|
2021-04-03 11:58:16 +02:00
|
|
|
|
2021-12-14 23:45:18 +01:00
|
|
|
# One more character makes it flip to just the address, with no name
|
|
|
|
mail = build_email(
|
|
|
|
"zerver/emails/password_reset",
|
|
|
|
to_emails=[hamlet],
|
|
|
|
from_name=limit_length_name + "a",
|
|
|
|
from_address=FromAddress.NOREPLY,
|
|
|
|
language="en",
|
|
|
|
)
|
|
|
|
self.assertEqual(mail.extra_headers["From"], FromAddress.NOREPLY)
|
|
|
|
|
2021-12-15 00:03:53 +01:00
|
|
|
def test_limited_to_length(self) -> None:
|
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
# This is exactly the max length
|
|
|
|
limit_length_name = "澳" * 61
|
|
|
|
hamlet.full_name = limit_length_name
|
|
|
|
hamlet.save()
|
|
|
|
|
|
|
|
mail = build_email(
|
|
|
|
"zerver/emails/password_reset",
|
|
|
|
to_user_ids=[hamlet.id],
|
|
|
|
from_name="Noreply",
|
|
|
|
from_address=FromAddress.NOREPLY,
|
|
|
|
language="en",
|
|
|
|
)
|
|
|
|
self.assertEqual(mail.to[0], f"{hamlet.full_name} <{hamlet.delivery_email}>")
|
|
|
|
|
|
|
|
# One more character makes it flip to just the address, with no name
|
|
|
|
hamlet.full_name += "澳"
|
|
|
|
hamlet.save()
|
|
|
|
mail = build_email(
|
|
|
|
"zerver/emails/password_reset",
|
|
|
|
to_user_ids=[hamlet.id],
|
|
|
|
from_name="Noreply",
|
|
|
|
from_address=FromAddress.NOREPLY,
|
|
|
|
language="en",
|
|
|
|
)
|
|
|
|
self.assertEqual(mail.to[0], hamlet.delivery_email)
|
|
|
|
|
2021-03-20 14:07:02 +01:00
|
|
|
|
|
|
|
class TestSendEmail(ZulipTestCase):
|
|
|
|
def test_initialize_connection(self) -> None:
|
|
|
|
# Test the new connection case
|
|
|
|
with mock.patch.object(EmailBackend, "open", return_value=True):
|
|
|
|
backend = initialize_connection(None)
|
|
|
|
self.assertTrue(isinstance(backend, EmailBackend))
|
|
|
|
|
|
|
|
backend = mock.MagicMock(spec=SMTPBackend)
|
2021-04-03 12:14:01 +02:00
|
|
|
backend.connection = mock.MagicMock(spec=SMTP)
|
2021-03-20 14:07:02 +01:00
|
|
|
|
|
|
|
self.assertTrue(isinstance(backend, SMTPBackend))
|
|
|
|
|
|
|
|
# Test the old connection case when it is still open
|
|
|
|
backend.open.return_value = False
|
|
|
|
backend.connection.noop.return_value = [250]
|
|
|
|
initialize_connection(backend)
|
|
|
|
self.assertEqual(backend.open.call_count, 1)
|
|
|
|
self.assertEqual(backend.connection.noop.call_count, 1)
|
|
|
|
|
|
|
|
# Test the old connection case when it was closed by the server
|
|
|
|
backend.connection.noop.return_value = [404]
|
|
|
|
backend.close.return_value = False
|
|
|
|
initialize_connection(backend)
|
|
|
|
# 2 more calls to open, 1 more call to noop and 1 call to close
|
|
|
|
self.assertEqual(backend.open.call_count, 3)
|
|
|
|
self.assertEqual(backend.connection.noop.call_count, 2)
|
|
|
|
self.assertEqual(backend.close.call_count, 1)
|
|
|
|
|
|
|
|
# Test backoff procedure
|
|
|
|
backend.open.side_effect = OSError
|
|
|
|
with self.assertRaises(OSError):
|
|
|
|
initialize_connection(backend)
|
|
|
|
# 3 more calls to open as we try 3 times before giving up
|
|
|
|
self.assertEqual(backend.open.call_count, 6)
|
2021-04-03 12:14:01 +02:00
|
|
|
|
|
|
|
def test_send_email_exceptions(self) -> None:
|
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
from_name = FromAddress.security_email_from_name(language="en")
|
2021-12-14 23:48:00 +01:00
|
|
|
address = FromAddress.NOREPLY
|
2021-04-03 12:14:01 +02:00
|
|
|
# Used to check the output
|
|
|
|
mail = build_email(
|
|
|
|
"zerver/emails/password_reset",
|
|
|
|
to_emails=[hamlet],
|
|
|
|
from_name=from_name,
|
2021-12-14 23:48:00 +01:00
|
|
|
from_address=address,
|
2021-04-03 12:14:01 +02:00
|
|
|
language="en",
|
|
|
|
)
|
2021-08-02 23:36:06 +02:00
|
|
|
self.assertEqual(mail.extra_headers["From"], f"{from_name} <{FromAddress.NOREPLY}>")
|
2021-04-03 12:14:01 +02:00
|
|
|
|
2021-12-14 23:48:00 +01:00
|
|
|
# We test the cases that should raise an EmailNotDeliveredException
|
2021-05-11 23:24:52 +02:00
|
|
|
errors = {
|
|
|
|
f"Unknown error sending password_reset email to {mail.to}": [0],
|
|
|
|
f"Error sending password_reset email to {mail.to}": [SMTPException()],
|
2021-12-14 23:48:00 +01:00
|
|
|
f"Error sending password_reset email to {mail.to}: {{'{address}': (550, b'User unknown')}}": [
|
|
|
|
SMTPRecipientsRefused(recipients={address: (550, b"User unknown")})
|
|
|
|
],
|
|
|
|
f"Error sending password_reset email to {mail.to} with error code 242: From field too long": [
|
|
|
|
SMTPDataError(242, "From field too long.")
|
|
|
|
],
|
2021-05-11 23:24:52 +02:00
|
|
|
}
|
2021-04-03 12:14:01 +02:00
|
|
|
|
2021-05-11 23:24:52 +02:00
|
|
|
for message, side_effect in errors.items():
|
|
|
|
with mock.patch.object(EmailBackend, "send_messages", side_effect=side_effect):
|
2021-04-03 12:14:01 +02:00
|
|
|
with self.assertLogs(logger=logger) as info_log:
|
|
|
|
with self.assertRaises(EmailNotDeliveredException):
|
|
|
|
send_email(
|
|
|
|
"zerver/emails/password_reset",
|
|
|
|
to_emails=[hamlet],
|
|
|
|
from_name=from_name,
|
|
|
|
from_address=FromAddress.NOREPLY,
|
|
|
|
language="en",
|
|
|
|
)
|
2021-05-17 05:41:32 +02:00
|
|
|
self.assert_length(info_log.records, 2)
|
2021-04-03 12:14:01 +02:00
|
|
|
self.assertEqual(
|
2021-05-11 23:24:52 +02:00
|
|
|
info_log.output[0],
|
|
|
|
f"INFO:{logger.name}:Sending password_reset email to {mail.to}",
|
2021-04-03 12:14:01 +02:00
|
|
|
)
|
2021-05-11 23:24:52 +02:00
|
|
|
self.assertTrue(info_log.output[1].startswith(f"ERROR:zulip.send_email:{message}"))
|
2022-02-06 20:57:58 +01:00
|
|
|
|
|
|
|
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)."
|
|
|
|
],
|
|
|
|
)
|
2022-04-12 23:43:41 +02:00
|
|
|
|
|
|
|
# Empty string is OK
|
|
|
|
with self.settings(EMAIL_HOST_USER="test", EMAIL_HOST_PASSWORD=""):
|
|
|
|
send_email(
|
|
|
|
"zerver/emails/password_reset",
|
|
|
|
to_emails=[hamlet],
|
|
|
|
from_name="From Name",
|
|
|
|
from_address=FromAddress.NOREPLY,
|
|
|
|
language="en",
|
|
|
|
)
|