mirror of
https://github.com/coddingtonbear/django-mailbox.git
synced 2026-07-09 22:38:19 +02:00
Support for Django 5.0 and python 3.12 + Fix CI (#277)
* Upgrade to last Django & Python supported versions * flake8 fixes * Add CI * Add final version of Django 5.0
This commit is contained in:
parent
462fdd3e49
commit
7fde5f7165
16 changed files with 119 additions and 89 deletions
40
.github/workflows/main.yml
vendored
Normal file
40
.github/workflows/main.yml
vendored
Normal file
|
|
@ -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 .)
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
Changelog
|
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
|
4.8.1
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,5 @@ from django.utils.translation import gettext_lazy as _
|
||||||
class MailBoxConfig(AppConfig):
|
class MailBoxConfig(AppConfig):
|
||||||
name = 'django_mailbox'
|
name = 'django_mailbox'
|
||||||
verbose_name = _("Mail Box")
|
verbose_name = _("Mail Box")
|
||||||
|
|
||||||
|
default_auto_field = "django.db.models.AutoField"
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import email
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
|
||||||
import uuid
|
import uuid
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
|
|
@ -42,7 +41,7 @@ class MailboxQuerySet(models.QuerySet):
|
||||||
for mailbox in self.all():
|
for mailbox in self.all():
|
||||||
logger.debug('Receiving mail for %s' % mailbox)
|
logger.debug('Receiving mail for %s' % mailbox)
|
||||||
count += sum(1 for i in mailbox.get_new_mail())
|
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):
|
class MailboxManager(models.Manager):
|
||||||
|
|
@ -123,6 +122,13 @@ class Mailbox(models.Model):
|
||||||
objects = MailboxManager()
|
objects = MailboxManager()
|
||||||
active_mailboxes = ActiveMailboxManager()
|
active_mailboxes = ActiveMailboxManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Mailbox')
|
||||||
|
verbose_name_plural = _('Mailboxes')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _protocol_info(self):
|
def _protocol_info(self):
|
||||||
return urlparse(self.uri)
|
return urlparse(self.uri)
|
||||||
|
|
@ -448,13 +454,12 @@ class Mailbox(models.Model):
|
||||||
|
|
||||||
def get_new_mail(self, condition=None):
|
def get_new_mail(self, condition=None):
|
||||||
"""Connect to this transport and fetch new messages."""
|
"""Connect to this transport and fetch new messages."""
|
||||||
new_mail = []
|
|
||||||
connection = self.get_connection()
|
connection = self.get_connection()
|
||||||
if not connection:
|
if not connection:
|
||||||
return
|
return
|
||||||
for message in connection.get_message(condition):
|
for message in connection.get_message(condition):
|
||||||
msg = self.process_incoming_message(message)
|
msg = self.process_incoming_message(message)
|
||||||
if not msg is None:
|
if msg is not None:
|
||||||
yield msg
|
yield msg
|
||||||
self.last_polling = now()
|
self.last_polling = now()
|
||||||
if django.VERSION >= (1, 5): # Django 1.5 introduces update_fields
|
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):
|
class IncomingMessageManager(models.Manager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(
|
return super().get_queryset().filter(
|
||||||
|
|
@ -757,7 +754,7 @@ class Message(models.Model):
|
||||||
"""Returns an `email.message.EmailMessage` instance representing the
|
"""Returns an `email.message.EmailMessage` instance representing the
|
||||||
contents of this message and all attachments.
|
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.
|
and properties are available on `email.message.EmailMessage` instances.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
from django.dispatch.dispatcher import Signal
|
from django.dispatch.dispatcher import Signal
|
||||||
|
|
||||||
message_received = Signal(providing_args=['message'])
|
message_received = Signal() # providing_args=['message']
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import gzip
|
import gzip
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from unittest import mock
|
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.utils import convert_header_to_unicode
|
||||||
from django_mailbox import utils
|
from django_mailbox import utils
|
||||||
from django_mailbox.tests.base import EmailMessageTestCase
|
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
|
from django.core.mail import EmailMessage
|
||||||
|
|
||||||
__all__ = ['TestProcessEmail']
|
__all__ = ['TestProcessEmail']
|
||||||
|
|
@ -367,12 +366,12 @@ class TestProcessEmail(EmailMessageTestCase):
|
||||||
email_object = self._get_email_object(
|
email_object = self._get_email_object(
|
||||||
'message_with_long_content.eml',
|
'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)
|
msg = self.mailbox.process_incoming_message(email_object)
|
||||||
|
|
||||||
self.assertEqual(size,
|
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):
|
def test_message_saved(self):
|
||||||
message = self._get_email_object('generic_message.eml')
|
message = self._get_email_object('generic_message.eml')
|
||||||
|
|
@ -386,7 +385,7 @@ class TestProcessEmail(EmailMessageTestCase):
|
||||||
|
|
||||||
msg = self.mailbox.process_incoming_message(message)
|
msg = self.mailbox.process_incoming_message(message)
|
||||||
|
|
||||||
self.assertNotEquals(msg.eml, None)
|
self.assertNotEqual(msg.eml, None)
|
||||||
|
|
||||||
self.assertTrue(msg.eml.name.endswith('.eml'))
|
self.assertTrue(msg.eml.name.endswith('.eml'))
|
||||||
|
|
||||||
|
|
@ -408,7 +407,7 @@ class TestProcessEmail(EmailMessageTestCase):
|
||||||
|
|
||||||
msg = self.mailbox.process_incoming_message(message)
|
msg = self.mailbox.process_incoming_message(message)
|
||||||
|
|
||||||
self.assertEquals(msg.eml, None)
|
self.assertEqual(msg.eml, None)
|
||||||
|
|
||||||
def test_message_compressed(self):
|
def test_message_compressed(self):
|
||||||
message = self._get_email_object('generic_message.eml')
|
message = self._get_email_object('generic_message.eml')
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,25 @@
|
||||||
from distutils.version import LooseVersion
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.core.management import call_command, CommandError
|
from django.core.management import call_command, CommandError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
import django
|
|
||||||
|
|
||||||
class CommandsTestCase(TestCase):
|
class CommandsTestCase(TestCase):
|
||||||
def test_processincomingmessage_no_args(self):
|
def test_processincomingmessage_no_args(self):
|
||||||
"""Check that processincomingmessage works with no args"""
|
"""Check that processincomingmessage works with no args"""
|
||||||
|
|
||||||
mailbox_name = None
|
mailbox_name = None
|
||||||
# Mock handle so that the test doesn't hang waiting for input. Note that we are only testing
|
# 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
|
# the argument parsing here -- functionality should be tested elsewhere
|
||||||
with mock.patch('django_mailbox.management.commands.processincomingmessage.Command.handle') as handle:
|
with mock.patch('django_mailbox.management.commands.processincomingmessage.Command.handle') as handle:
|
||||||
# Don't care about the return value
|
# Don't care about the return value
|
||||||
handle.return_value = None
|
handle.return_value = None
|
||||||
|
|
||||||
call_command('processincomingmessage')
|
call_command('processincomingmessage')
|
||||||
args, kwargs = handle.call_args
|
args, kwargs = handle.call_args
|
||||||
|
|
||||||
# Make sure that we called with the right arguments
|
# Make sure that we called with the right arguments
|
||||||
try:
|
self.assertEqual(kwargs['mailbox_name'], mailbox_name)
|
||||||
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())
|
|
||||||
|
|
||||||
|
|
||||||
def test_processincomingmessage_with_arg(self):
|
def test_processincomingmessage_with_arg(self):
|
||||||
"""Check that processincomingmessage works with mailbox_name given"""
|
"""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:
|
with mock.patch('django_mailbox.management.commands.processincomingmessage.Command.handle') as handle:
|
||||||
handle.return_value = None
|
handle.return_value = None
|
||||||
|
|
||||||
call_command('processincomingmessage', mailbox_name)
|
call_command('processincomingmessage', mailbox_name)
|
||||||
args, kwargs = handle.call_args
|
args, kwargs = handle.call_args
|
||||||
try:
|
|
||||||
self.assertEqual(kwargs['mailbox_name'], mailbox_name)
|
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)
|
|
||||||
|
|
||||||
def test_processincomingmessage_too_many_args(self):
|
def test_processincomingmessage_too_many_args(self):
|
||||||
"""Check that processincomingmessage raises an error if too many args"""
|
"""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
|
with self.assertRaises(CommandError):
|
||||||
# error, which means this test is worthless anyway
|
call_command('processincomingmessage', 'foo_mailbox', 'invalid_arg')
|
||||||
# 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')
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from django_mailbox.transports import ImapTransport, Pop3Transport
|
||||||
FAKE_UID_SEARCH_ANSWER = (
|
FAKE_UID_SEARCH_ANSWER = (
|
||||||
'OK',
|
'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'
|
b'30 31 32 33 34 35 36 37 38 39 40 41 42 43 44'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,3 @@ class EmailTransport:
|
||||||
message = email.message_from_bytes(contents)
|
message = email.message_from_bytes(contents)
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import sys
|
|
||||||
|
|
||||||
from .base import EmailTransport
|
from .base import EmailTransport
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,4 +63,3 @@ class Office365Transport(EmailTransport):
|
||||||
|
|
||||||
o365message.delete()
|
o365message.delete()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@ sys.path.insert(0, os.path.abspath('..'))
|
||||||
import inspect
|
import inspect
|
||||||
import django
|
import django
|
||||||
from django.utils.html import strip_tags
|
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
|
from django.conf import settings
|
||||||
settings.configure(INSTALLED_APPS=['django_mailbox', ])
|
settings.configure(INSTALLED_APPS=['django_mailbox', ])
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
@ -265,11 +265,11 @@ def process_docstring(app, what, name, obj, options, lines):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Decode and strip any html out of the field's help text
|
# 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
|
# Decode and capitalize the verbose name, for use if there isn't
|
||||||
# any help text
|
# any help text
|
||||||
verbose_name = force_text(field.verbose_name).capitalize()
|
verbose_name = force_str(field.verbose_name).capitalize()
|
||||||
|
|
||||||
if help_text:
|
if help_text:
|
||||||
# Add the model field to the end of the docstring as a param
|
# Add the model field to the end of the docstring as a param
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
django>=3.1.1,<3.2
|
django>=3.2,<4.0
|
||||||
six
|
six
|
||||||
|
|
|
||||||
23
setup.py
23
setup.py
|
|
@ -32,25 +32,25 @@ setup(
|
||||||
'gmail-oauth2': gmail_oauth2_require,
|
'gmail-oauth2': gmail_oauth2_require,
|
||||||
'office365-oauth2': office365_oauth2_require
|
'office365-oauth2': office365_oauth2_require
|
||||||
},
|
},
|
||||||
python_requires=">=3",
|
python_requires=">=3.8",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
'Framework :: Django :: 1.11',
|
'Framework :: Django :: 3.2',
|
||||||
'Framework :: Django :: 2.0',
|
'Framework :: Django :: 4.0',
|
||||||
'Framework :: Django :: 2.1',
|
'Framework :: Django :: 4.1',
|
||||||
'Framework :: Django :: 2.2',
|
'Framework :: Django :: 4.2',
|
||||||
'Framework :: Django :: 3.0',
|
'Framework :: Django :: 5.0',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 3',
|
'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.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',
|
||||||
'Topic :: Communications :: Email :: Post-Office',
|
'Topic :: Communications :: Email :: Post-Office',
|
||||||
'Topic :: Communications :: Email :: Post-Office :: IMAP',
|
'Topic :: Communications :: Email :: Post-Office :: IMAP',
|
||||||
|
|
@ -59,7 +59,4 @@ setup(
|
||||||
],
|
],
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
|
||||||
'six>=1.6.1'
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
pytest==2.9.1
|
pytest
|
||||||
pytest-django==2.9.1
|
pytest-django
|
||||||
mock==2.0.0
|
mock
|
||||||
|
|
|
||||||
34
tox.ini
34
tox.ini
|
|
@ -2,20 +2,32 @@
|
||||||
# sort by django version, next by python version
|
# sort by django version, next by python version
|
||||||
envlist=
|
envlist=
|
||||||
flake8
|
flake8
|
||||||
py{35,36}-django111
|
py{310,311,312}-django50
|
||||||
py{35,36}-django20
|
py{38,39,310,311}-django42
|
||||||
py{35,36,37}-django21
|
py{38,39,310,311}-django41
|
||||||
py{35,36,37}-django22
|
py{38,39,310}-django40
|
||||||
py{36,37,38}-django30
|
py{38,39,310}-django32
|
||||||
|
|
||||||
|
[gh-actions]
|
||||||
|
python =
|
||||||
|
3.8: py38
|
||||||
|
3.9: py39
|
||||||
|
3.10: py310
|
||||||
|
3.11: py311
|
||||||
|
3.12: py312
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
passenv=EMAIL_IMAP_SERVER EMAIL_ACCOUNT EMAIL_PASSWORD EMAIL_SMTP_SERVER
|
passenv=
|
||||||
|
EMAIL_IMAP_SERVER
|
||||||
|
EMAIL_ACCOUNT
|
||||||
|
EMAIL_PASSWORD
|
||||||
|
EMAIL_SMTP_SERVER
|
||||||
deps=
|
deps=
|
||||||
django111: django==1.11.*
|
django50: Django==5.0,<5.1
|
||||||
django20: django==2.0.*
|
django42: Django>=4.2,<5.0
|
||||||
django21: django==2.0.*
|
django41: Django>=4.1,<4.2
|
||||||
django22: django==2.0.*
|
django40: Django>=4.0,<4.1
|
||||||
django30: django==3.0b1
|
django32: Django>=3.2,<4.0
|
||||||
-r{toxinidir}/test_requirements.txt
|
-r{toxinidir}/test_requirements.txt
|
||||||
sitepackages=False
|
sitepackages=False
|
||||||
commands=
|
commands=
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue