diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5743aa3 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + tests: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-22.04 + + strategy: + matrix: + python-version: + - '3.8' + - '3.9' + - '3.10' + - '3.11' + - '3.12' + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade 'tox>=4.0.0rc3' + + - name: Run tox targets for ${{ matrix.python-version }} + run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 208bf4f..13e1bd5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog ========= +Unreleased +----- + +* Add Django 3.2, 4.0, 4.1, 4.2, 5.0 support +* Remove support for deprecated Django versions + +* Add Python 3.8, 3.9, 3.10, 3.11, 3.12 support +* Remove support for deprecated Python versions + 4.8.1 ----- diff --git a/django_mailbox/apps.py b/django_mailbox/apps.py index 13b7bf3..56c7e11 100644 --- a/django_mailbox/apps.py +++ b/django_mailbox/apps.py @@ -5,3 +5,5 @@ from django.utils.translation import gettext_lazy as _ class MailBoxConfig(AppConfig): name = 'django_mailbox' verbose_name = _("Mail Box") + + default_auto_field = "django.db.models.AutoField" diff --git a/django_mailbox/models.py b/django_mailbox/models.py index 871b0cc..1b0da89 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -15,7 +15,6 @@ import email import logging import mimetypes import os.path -import sys import uuid from tempfile import NamedTemporaryFile @@ -42,7 +41,7 @@ class MailboxQuerySet(models.QuerySet): for mailbox in self.all(): logger.debug('Receiving mail for %s' % mailbox) count += sum(1 for i in mailbox.get_new_mail()) - logger.debug('Received %d %s.', count, 'mails' if count != 1 else 'mail') + logger.debug('Received %d %s.', count, 'mail(s)') class MailboxManager(models.Manager): @@ -123,6 +122,13 @@ class Mailbox(models.Model): objects = MailboxManager() active_mailboxes = ActiveMailboxManager() + class Meta: + verbose_name = _('Mailbox') + verbose_name_plural = _('Mailboxes') + + def __str__(self): + return self.name + @property def _protocol_info(self): return urlparse(self.uri) @@ -448,13 +454,12 @@ class Mailbox(models.Model): def get_new_mail(self, condition=None): """Connect to this transport and fetch new messages.""" - new_mail = [] connection = self.get_connection() if not connection: return for message in connection.get_message(condition): msg = self.process_incoming_message(message) - if not msg is None: + if msg is not None: yield msg self.last_polling = now() if django.VERSION >= (1, 5): # Django 1.5 introduces update_fields @@ -483,14 +488,6 @@ class Mailbox(models.Model): ) - def __str__(self): - return self.name - - class Meta: - verbose_name = _('Mailbox') - verbose_name_plural = _('Mailboxes') - - class IncomingMessageManager(models.Manager): def get_queryset(self): return super().get_queryset().filter( @@ -757,7 +754,7 @@ class Message(models.Model): """Returns an `email.message.EmailMessage` instance representing the contents of this message and all attachments. - See [email.message.EmailMessage]_ for more information as to what methods + See [email.message.EmailMessage]_ for more information like what methods and properties are available on `email.message.EmailMessage` instances. .. note:: diff --git a/django_mailbox/signals.py b/django_mailbox/signals.py index b445183..8637ea8 100644 --- a/django_mailbox/signals.py +++ b/django_mailbox/signals.py @@ -1,3 +1,3 @@ from django.dispatch.dispatcher import Signal -message_received = Signal(providing_args=['message']) +message_received = Signal() # providing_args=['message'] diff --git a/django_mailbox/tests/test_process_email.py b/django_mailbox/tests/test_process_email.py index a6dbd04..c1179bf 100644 --- a/django_mailbox/tests/test_process_email.py +++ b/django_mailbox/tests/test_process_email.py @@ -1,6 +1,5 @@ import gzip import os.path -import sys import copy from unittest import mock @@ -9,7 +8,7 @@ from django_mailbox.models import Mailbox, Message from django_mailbox.utils import convert_header_to_unicode from django_mailbox import utils from django_mailbox.tests.base import EmailMessageTestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.core.mail import EmailMessage __all__ = ['TestProcessEmail'] @@ -367,12 +366,12 @@ class TestProcessEmail(EmailMessageTestCase): email_object = self._get_email_object( 'message_with_long_content.eml', ) - size = len(force_text(email_object.as_string())) + size = len(force_str(email_object.as_string())) msg = self.mailbox.process_incoming_message(email_object) self.assertEqual(size, - len(force_text(msg.get_email_object().as_string()))) + len(force_str(msg.get_email_object().as_string()))) def test_message_saved(self): message = self._get_email_object('generic_message.eml') @@ -386,7 +385,7 @@ class TestProcessEmail(EmailMessageTestCase): msg = self.mailbox.process_incoming_message(message) - self.assertNotEquals(msg.eml, None) + self.assertNotEqual(msg.eml, None) self.assertTrue(msg.eml.name.endswith('.eml')) @@ -408,7 +407,7 @@ class TestProcessEmail(EmailMessageTestCase): msg = self.mailbox.process_incoming_message(message) - self.assertEquals(msg.eml, None) + self.assertEqual(msg.eml, None) def test_message_compressed(self): message = self._get_email_object('generic_message.eml') diff --git a/django_mailbox/tests/test_processincomingmessage.py b/django_mailbox/tests/test_processincomingmessage.py index 8ba2fb2..bfb54fd 100644 --- a/django_mailbox/tests/test_processincomingmessage.py +++ b/django_mailbox/tests/test_processincomingmessage.py @@ -1,34 +1,25 @@ -from distutils.version import LooseVersion from unittest import mock from django.core.management import call_command, CommandError from django.test import TestCase -import django + class CommandsTestCase(TestCase): def test_processincomingmessage_no_args(self): """Check that processincomingmessage works with no args""" - + mailbox_name = None # Mock handle so that the test doesn't hang waiting for input. Note that we are only testing # the argument parsing here -- functionality should be tested elsewhere with mock.patch('django_mailbox.management.commands.processincomingmessage.Command.handle') as handle: # Don't care about the return value handle.return_value = None - + call_command('processincomingmessage') args, kwargs = handle.call_args # Make sure that we called with the right arguments - try: - self.assertEqual(kwargs['mailbox_name'], mailbox_name) - except KeyError: - # Handle Django 1.7 - # It uses optparse instead of argparse, so instead of being - # set to None, mailbox_name is simply left out altogether - # Thus we expect an empty tuple here - self.assertEqual(args, tuple()) - + self.assertEqual(kwargs['mailbox_name'], mailbox_name) def test_processincomingmessage_with_arg(self): """Check that processincomingmessage works with mailbox_name given""" @@ -37,29 +28,14 @@ class CommandsTestCase(TestCase): with mock.patch('django_mailbox.management.commands.processincomingmessage.Command.handle') as handle: handle.return_value = None - + call_command('processincomingmessage', mailbox_name) args, kwargs = handle.call_args - try: - self.assertEqual(kwargs['mailbox_name'], mailbox_name) - except (AssertionError, KeyError): - # Handle Django 1.7 - # It uses optparse instead of argparse, so instead of being - # in kwargs, mailbox_name is in args - self.assertEqual(args[0], mailbox_name) + + self.assertEqual(kwargs['mailbox_name'], mailbox_name) def test_processincomingmessage_too_many_args(self): """Check that processincomingmessage raises an error if too many args""" - # Only perform this test for Django versions greater than 1.7.*. This - # is because, with optparse, too many arguments doesn't result in an - # error, which means this test is worthless anyway - # For the "compatibility" versions, unexpected arguments aren't handled - # very well, and result in a TypeError - if (LooseVersion(django.get_version()) >= LooseVersion('1.8') and - LooseVersion(django.get_version()) < LooseVersion('1.10')): - with self.assertRaises(TypeError): - call_command('processincomingmessage', 'foo_mailbox', 'invalid_arg') - # In 1.10 and later a proper CommandError should be raised - elif LooseVersion(django.get_version()) >= LooseVersion('1.10'): - with self.assertRaises(CommandError): - call_command('processincomingmessage', 'foo_mailbox', 'invalid_arg') + + with self.assertRaises(CommandError): + call_command('processincomingmessage', 'foo_mailbox', 'invalid_arg') diff --git a/django_mailbox/tests/test_transports.py b/django_mailbox/tests/test_transports.py index 403a306..732efbe 100644 --- a/django_mailbox/tests/test_transports.py +++ b/django_mailbox/tests/test_transports.py @@ -8,7 +8,7 @@ from django_mailbox.transports import ImapTransport, Pop3Transport FAKE_UID_SEARCH_ANSWER = ( 'OK', [ - b'18 19 20 21 22 23 24 25 26 27 28 29 ' + + b'18 19 20 21 22 23 24 25 26 27 28 29 ' + b'30 31 32 33 34 35 36 37 38 39 40 41 42 43 44' ] ) diff --git a/django_mailbox/transports/base.py b/django_mailbox/transports/base.py index a3a2bc0..a90af9f 100644 --- a/django_mailbox/transports/base.py +++ b/django_mailbox/transports/base.py @@ -9,4 +9,3 @@ class EmailTransport: message = email.message_from_bytes(contents) return message - diff --git a/django_mailbox/transports/generic.py b/django_mailbox/transports/generic.py index 5832fda..cdb4b38 100644 --- a/django_mailbox/transports/generic.py +++ b/django_mailbox/transports/generic.py @@ -1,4 +1,4 @@ -import sys + from .base import EmailTransport diff --git a/django_mailbox/transports/office365.py b/django_mailbox/transports/office365.py index 4f748b2..26d074a 100644 --- a/django_mailbox/transports/office365.py +++ b/django_mailbox/transports/office365.py @@ -63,4 +63,3 @@ class Office365Transport(EmailTransport): o365message.delete() return - diff --git a/docs/conf.py b/docs/conf.py index 974706f..c015980 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -244,7 +244,7 @@ sys.path.insert(0, os.path.abspath('..')) import inspect import django from django.utils.html import strip_tags -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.conf import settings settings.configure(INSTALLED_APPS=['django_mailbox', ]) django.setup() @@ -265,11 +265,11 @@ def process_docstring(app, what, name, obj, options, lines): continue # Decode and strip any html out of the field's help text - help_text = strip_tags(force_text(field.help_text)) + help_text = strip_tags(force_str(field.help_text)) # Decode and capitalize the verbose name, for use if there isn't # any help text - verbose_name = force_text(field.verbose_name).capitalize() + verbose_name = force_str(field.verbose_name).capitalize() if help_text: # Add the model field to the end of the docstring as a param diff --git a/rtd_requirements.txt b/rtd_requirements.txt index 5e254ee..aea7a54 100644 --- a/rtd_requirements.txt +++ b/rtd_requirements.txt @@ -1,2 +1,2 @@ -django>=3.1.1,<3.2 +django>=3.2,<4.0 six diff --git a/setup.py b/setup.py index a6ade7d..668b936 100755 --- a/setup.py +++ b/setup.py @@ -32,25 +32,25 @@ setup( 'gmail-oauth2': gmail_oauth2_require, 'office365-oauth2': office365_oauth2_require }, - python_requires=">=3", + python_requires=">=3.8", classifiers=[ 'Framework :: Django', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Framework :: Django', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', + 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.0', + 'Framework :: Django :: 4.1', + 'Framework :: Django :: 4.2', + 'Framework :: Django :: 5.0', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Communications :: Email', 'Topic :: Communications :: Email :: Post-Office', 'Topic :: Communications :: Email :: Post-Office :: IMAP', @@ -59,7 +59,4 @@ setup( ], packages=find_packages(), include_package_data=True, - install_requires=[ - 'six>=1.6.1' - ] ) diff --git a/test_requirements.txt b/test_requirements.txt index 3e5e40e..362ca7a 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,3 +1,3 @@ -pytest==2.9.1 -pytest-django==2.9.1 -mock==2.0.0 +pytest +pytest-django +mock diff --git a/tox.ini b/tox.ini index 6998106..a23030c 100644 --- a/tox.ini +++ b/tox.ini @@ -2,20 +2,32 @@ # sort by django version, next by python version envlist= flake8 - py{35,36}-django111 - py{35,36}-django20 - py{35,36,37}-django21 - py{35,36,37}-django22 - py{36,37,38}-django30 + py{310,311,312}-django50 + py{38,39,310,311}-django42 + py{38,39,310,311}-django41 + py{38,39,310}-django40 + py{38,39,310}-django32 + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 [testenv] -passenv=EMAIL_IMAP_SERVER EMAIL_ACCOUNT EMAIL_PASSWORD EMAIL_SMTP_SERVER +passenv= + EMAIL_IMAP_SERVER + EMAIL_ACCOUNT + EMAIL_PASSWORD + EMAIL_SMTP_SERVER deps= - django111: django==1.11.* - django20: django==2.0.* - django21: django==2.0.* - django22: django==2.0.* - django30: django==3.0b1 + django50: Django==5.0,<5.1 + django42: Django>=4.2,<5.0 + django41: Django>=4.1,<4.2 + django40: Django>=4.0,<4.1 + django32: Django>=3.2,<4.0 -r{toxinidir}/test_requirements.txt sitepackages=False commands=