mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[4.0.x] Refs #32074 -- Removed usage of deprecated asyncore and smtpd modules.
asyncore and smtpd modules were deprecated in Python 3.10.
Backport of 569a33579c from main
			
			
This commit is contained in:
		| @@ -273,6 +273,7 @@ Running all the tests | |||||||
| If you want to run the full suite of tests, you'll need to install a number of | If you want to run the full suite of tests, you'll need to install a number of | ||||||
| dependencies: | dependencies: | ||||||
|  |  | ||||||
|  | *  aiosmtpd_ | ||||||
| *  argon2-cffi_ 19.1.0+ | *  argon2-cffi_ 19.1.0+ | ||||||
| *  asgiref_ 3.3.2+ (required) | *  asgiref_ 3.3.2+ (required) | ||||||
| *  bcrypt_ | *  bcrypt_ | ||||||
| @@ -322,6 +323,7 @@ associated tests will be skipped. | |||||||
| To run some of the autoreload tests, you'll need to install the Watchman_ | To run some of the autoreload tests, you'll need to install the Watchman_ | ||||||
| service. | service. | ||||||
|  |  | ||||||
|  | .. _aiosmtpd: https://pypi.org/project/aiosmtpd/ | ||||||
| .. _argon2-cffi: https://pypi.org/project/argon2-cffi/ | .. _argon2-cffi: https://pypi.org/project/argon2-cffi/ | ||||||
| .. _asgiref: https://pypi.org/project/asgiref/ | .. _asgiref: https://pypi.org/project/asgiref/ | ||||||
| .. _bcrypt: https://pypi.org/project/bcrypt/ | .. _bcrypt: https://pypi.org/project/bcrypt/ | ||||||
|   | |||||||
| @@ -1,11 +1,9 @@ | |||||||
| import asyncore |  | ||||||
| import mimetypes | import mimetypes | ||||||
| import os | import os | ||||||
| import shutil | import shutil | ||||||
| import smtpd | import socket | ||||||
| import sys | import sys | ||||||
| import tempfile | import tempfile | ||||||
| import threading |  | ||||||
| from email import charset, message_from_binary_file, message_from_bytes | from email import charset, message_from_binary_file, message_from_bytes | ||||||
| from email.header import Header | from email.header import Header | ||||||
| from email.mime.text import MIMEText | from email.mime.text import MIMEText | ||||||
| @@ -14,7 +12,7 @@ from io import StringIO | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from smtplib import SMTP, SMTPException | from smtplib import SMTP, SMTPException | ||||||
| from ssl import SSLError | from ssl import SSLError | ||||||
| from unittest import mock | from unittest import mock, skipUnless | ||||||
|  |  | ||||||
| from django.core import mail | from django.core import mail | ||||||
| from django.core.mail import ( | from django.core.mail import ( | ||||||
| @@ -27,6 +25,12 @@ from django.test import SimpleTestCase, override_settings | |||||||
| from django.test.utils import requires_tz_support | from django.test.utils import requires_tz_support | ||||||
| from django.utils.translation import gettext_lazy | from django.utils.translation import gettext_lazy | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from aiosmtpd.controller import Controller | ||||||
|  |     HAS_AIOSMTPD = True | ||||||
|  | except ImportError: | ||||||
|  |     HAS_AIOSMTPD = False | ||||||
|  |  | ||||||
|  |  | ||||||
| class HeadersCheckMixin: | class HeadersCheckMixin: | ||||||
|  |  | ||||||
| @@ -1336,109 +1340,78 @@ class ConsoleBackendTests(BaseEmailBackendTests, SimpleTestCase): | |||||||
|         self.assertIn(b'\nDate: ', message) |         self.assertIn(b'\nDate: ', message) | ||||||
|  |  | ||||||
|  |  | ||||||
| class FakeSMTPChannel(smtpd.SMTPChannel): | class SMTPHandler: | ||||||
|  |  | ||||||
|     def collect_incoming_data(self, data): |  | ||||||
|         try: |  | ||||||
|             smtpd.SMTPChannel.collect_incoming_data(self, data) |  | ||||||
|         except UnicodeDecodeError: |  | ||||||
|             # Ignore decode error in SSL/TLS connection tests as the test only |  | ||||||
|             # cares whether the connection attempt was made. |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class FakeSMTPServer(smtpd.SMTPServer, threading.Thread): |  | ||||||
|     """ |  | ||||||
|     Asyncore SMTP server wrapped into a thread. Based on DummyFTPServer from: |  | ||||||
|     http://svn.python.org/view/python/branches/py3k/Lib/test/test_ftplib.py?revision=86061&view=markup |  | ||||||
|     """ |  | ||||||
|     channel_class = FakeSMTPChannel |  | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         threading.Thread.__init__(self) |         self.mailbox = [] | ||||||
|         smtpd.SMTPServer.__init__(self, *args, decode_data=True, **kwargs) |  | ||||||
|         self._sink = [] |  | ||||||
|         self.active = False |  | ||||||
|         self.active_lock = threading.Lock() |  | ||||||
|         self.sink_lock = threading.Lock() |  | ||||||
|  |  | ||||||
|     def process_message(self, peer, mailfrom, rcpttos, data): |     async def handle_DATA(self, server, session, envelope): | ||||||
|         data = data.encode() |         data = envelope.content | ||||||
|         m = message_from_bytes(data) |         mail_from = envelope.mail_from | ||||||
|         maddr = parseaddr(m.get('from'))[1] |  | ||||||
|  |  | ||||||
|         if mailfrom != maddr: |         message = message_from_bytes(data.rstrip()) | ||||||
|             # According to the spec, mailfrom does not necessarily match the |         message_addr = parseaddr(message.get('from'))[1] | ||||||
|  |         if mail_from != message_addr: | ||||||
|  |             # According to the spec, mail_from does not necessarily match the | ||||||
|             # From header - this is the case where the local part isn't |             # From header - this is the case where the local part isn't | ||||||
|             # encoded, so try to correct that. |             # encoded, so try to correct that. | ||||||
|             lp, domain = mailfrom.split('@', 1) |             lp, domain = mail_from.split('@', 1) | ||||||
|             lp = Header(lp, 'utf-8').encode() |             lp = Header(lp, 'utf-8').encode() | ||||||
|             mailfrom = '@'.join([lp, domain]) |             mail_from = '@'.join([lp, domain]) | ||||||
|  |  | ||||||
|         if mailfrom != maddr: |         if mail_from != message_addr: | ||||||
|             return "553 '%s' != '%s'" % (mailfrom, maddr) |             return f"553 '{mail_from}' != '{message_addr}'" | ||||||
|         with self.sink_lock: |         self.mailbox.append(message) | ||||||
|             self._sink.append(m) |         return '250 OK' | ||||||
|  |  | ||||||
|     def get_sink(self): |     def flush_mailbox(self): | ||||||
|         with self.sink_lock: |         self.mailbox[:] = [] | ||||||
|             return self._sink[:] |  | ||||||
|  |  | ||||||
|     def flush_sink(self): |  | ||||||
|         with self.sink_lock: |  | ||||||
|             self._sink[:] = [] |  | ||||||
|  |  | ||||||
|     def start(self): |  | ||||||
|         assert not self.active |  | ||||||
|         self.__flag = threading.Event() |  | ||||||
|         threading.Thread.start(self) |  | ||||||
|         self.__flag.wait() |  | ||||||
|  |  | ||||||
|     def run(self): |  | ||||||
|         self.active = True |  | ||||||
|         self.__flag.set() |  | ||||||
|         while self.active and asyncore.socket_map: |  | ||||||
|             with self.active_lock: |  | ||||||
|                 asyncore.loop(timeout=0.1, count=1) |  | ||||||
|         asyncore.close_all() |  | ||||||
|  |  | ||||||
|     def stop(self): |  | ||||||
|         if self.active: |  | ||||||
|             self.active = False |  | ||||||
|             self.join() |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @skipUnless(HAS_AIOSMTPD, 'No aiosmtpd library detected.') | ||||||
| class SMTPBackendTestsBase(SimpleTestCase): | class SMTPBackendTestsBase(SimpleTestCase): | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def setUpClass(cls): |     def setUpClass(cls): | ||||||
|         super().setUpClass() |         super().setUpClass() | ||||||
|         cls.server = FakeSMTPServer(('127.0.0.1', 0), None) |         # Find a free port. | ||||||
|  |         with socket.socket() as s: | ||||||
|  |             s.bind(('127.0.0.1', 0)) | ||||||
|  |             port = s.getsockname()[1] | ||||||
|  |         cls.smtp_handler = SMTPHandler() | ||||||
|  |         cls.smtp_controller = Controller( | ||||||
|  |             cls.smtp_handler, hostname='127.0.0.1', port=port, | ||||||
|  |         ) | ||||||
|         cls._settings_override = override_settings( |         cls._settings_override = override_settings( | ||||||
|             EMAIL_HOST="127.0.0.1", |             EMAIL_HOST=cls.smtp_controller.hostname, | ||||||
|             EMAIL_PORT=cls.server.socket.getsockname()[1]) |             EMAIL_PORT=cls.smtp_controller.port, | ||||||
|  |         ) | ||||||
|         cls._settings_override.enable() |         cls._settings_override.enable() | ||||||
|         cls.addClassCleanup(cls._settings_override.disable) |         cls.addClassCleanup(cls._settings_override.disable) | ||||||
|         cls.server.start() |         cls.smtp_controller.start() | ||||||
|         cls.addClassCleanup(cls.server.stop) |         cls.addClassCleanup(cls.stop_smtp) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def stop_smtp(cls): | ||||||
|  |         cls.smtp_controller.stop() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @skipUnless(HAS_AIOSMTPD, 'No aiosmtpd library detected.') | ||||||
| class SMTPBackendTests(BaseEmailBackendTests, SMTPBackendTestsBase): | class SMTPBackendTests(BaseEmailBackendTests, SMTPBackendTestsBase): | ||||||
|     email_backend = 'django.core.mail.backends.smtp.EmailBackend' |     email_backend = 'django.core.mail.backends.smtp.EmailBackend' | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.server.flush_sink() |         self.smtp_handler.flush_mailbox() | ||||||
|  |  | ||||||
|     def tearDown(self): |     def tearDown(self): | ||||||
|         self.server.flush_sink() |         self.smtp_handler.flush_mailbox() | ||||||
|         super().tearDown() |         super().tearDown() | ||||||
|  |  | ||||||
|     def flush_mailbox(self): |     def flush_mailbox(self): | ||||||
|         self.server.flush_sink() |         self.smtp_handler.flush_mailbox() | ||||||
|  |  | ||||||
|     def get_mailbox_content(self): |     def get_mailbox_content(self): | ||||||
|         return self.server.get_sink() |         return self.smtp_handler.mailbox | ||||||
|  |  | ||||||
|     @override_settings( |     @override_settings( | ||||||
|         EMAIL_HOST_USER="not empty username", |         EMAIL_HOST_USER="not empty username", | ||||||
| @@ -1657,17 +1630,18 @@ class SMTPBackendTests(BaseEmailBackendTests, SMTPBackendTestsBase): | |||||||
|         self.assertEqual(sent, 0) |         self.assertEqual(sent, 0) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @skipUnless(HAS_AIOSMTPD, 'No aiosmtpd library detected.') | ||||||
| class SMTPBackendStoppedServerTests(SMTPBackendTestsBase): | class SMTPBackendStoppedServerTests(SMTPBackendTestsBase): | ||||||
|     """ |  | ||||||
|     These tests require a separate class, because the FakeSMTPServer is shut |  | ||||||
|     down in setUpClass(), and it cannot be restarted ("RuntimeError: threads |  | ||||||
|     can only be started once"). |  | ||||||
|     """ |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def setUpClass(cls): |     def setUpClass(cls): | ||||||
|         super().setUpClass() |         super().setUpClass() | ||||||
|         cls.backend = smtp.EmailBackend(username='', password='') |         cls.backend = smtp.EmailBackend(username='', password='') | ||||||
|         cls.server.stop() |         cls.smtp_controller.stop() | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def stop_smtp(cls): | ||||||
|  |         # SMTP controller is stopped in setUpClass(). | ||||||
|  |         pass | ||||||
|  |  | ||||||
|     def test_server_stopped(self): |     def test_server_stopped(self): | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | aiosmtpd | ||||||
| asgiref >= 3.3.2 | asgiref >= 3.3.2 | ||||||
| argon2-cffi >= 16.1.0 | argon2-cffi >= 16.1.0 | ||||||
| backports.zoneinfo; python_version < '3.9' | backports.zoneinfo; python_version < '3.9' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user