mirror of
https://github.com/coddingtonbear/django-mailbox.git
synced 2026-07-09 22:38:19 +02:00
Modernize syntax, drop Python<3, drop Django<1.11
This commit is contained in:
parent
d114008645
commit
84c99afa03
42 changed files with 176 additions and 330 deletions
30
.travis.yml
30
.travis.yml
|
|
@ -1,24 +1,9 @@
|
|||
language: python
|
||||
python: '3.5'
|
||||
python: '3'
|
||||
|
||||
env:
|
||||
matrix:
|
||||
# sort by django version, next to by python version
|
||||
# Django 1.7 support Python 2.7, 3.4
|
||||
- TOX_ENV=django17-py27
|
||||
- TOX_ENV=django17-py34
|
||||
# Django 1.8 support Python 2.7, 3.4, 3.5
|
||||
- TOX_ENV=django18-py27
|
||||
- TOX_ENV=django18-py34
|
||||
- TOX_ENV=django18-py35
|
||||
# Django 1.9 support Python 2.7, 3.4, 3.5
|
||||
- TOX_ENV=django19-py27
|
||||
- TOX_ENV=django19-py34
|
||||
- TOX_ENV=django19-py35
|
||||
# Django 1.10 support Python 2.7, 3.4, 3.5
|
||||
- TOX_ENV=django110-py27
|
||||
- TOX_ENV=django110-py34
|
||||
- TOX_ENV=django110-py35
|
||||
# Django 1.11 support all supported Python interpreter
|
||||
- TOX_ENV=django111-py34
|
||||
- TOX_ENV=django111-py35
|
||||
|
|
@ -27,15 +12,14 @@ env:
|
|||
- TOX_ENV=django20-py34
|
||||
- TOX_ENV=django20-py35
|
||||
- TOX_ENV=django20-py36
|
||||
# Django 2.1 support Python 3.4, 3.5, 3.6
|
||||
# Django 2.1 support Python 3.5, 3.6, 3.7
|
||||
- TOX_ENV=django21-py35
|
||||
- TOX_ENV=django21-py36
|
||||
- TOX_ENV=django21-py37
|
||||
# Django 2.2 support Python 3.5, 3.6
|
||||
- TOX_ENV=django21-py35
|
||||
- TOX_ENV=django21-py36
|
||||
- TOX_ENV=django21-py37
|
||||
|
||||
# Django 2.2 support Python 3.5, 3.6, 3.7
|
||||
- TOX_ENV=django22-py35
|
||||
- TOX_ENV=django22-py36
|
||||
- TOX_ENV=django22-py37
|
||||
|
||||
global:
|
||||
- secure: aswHU7pQroGM+GHoYlhXzzk2FYfqxXJORjqXPsbgoHAIu4Bssaj8+YAzIcdy3j9kSt4I8VBpjnn8H/wzQXki75JBZOosQrIeMK018+uR+ZMONBLqDYW/N7EJHLgZt9QXxQNKeZygrD4GN/Dc4gLHGvPQC/RfPuuHcnF0Liaahoo=
|
||||
|
|
@ -51,4 +35,4 @@ script:
|
|||
- if [[ $TOX_ENV = *"py36"* ]]; then echo "Activated PY3.6"; source ~/virtualenv/python3.6/bin/activate; pip install tox; fi
|
||||
- tox -e $TOX_ENV
|
||||
|
||||
sudo: false
|
||||
sudo: false
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Model configuration in application ``django_mailbox`` for administration
|
||||
|
|
@ -74,7 +73,7 @@ class MessageAdmin(admin.ModelAdmin):
|
|||
def envelope_headers(self, msg):
|
||||
email = msg.get_email_object()
|
||||
return '\n'.join(
|
||||
[('%s: %s' % (h, v)) for h, v in email.items()]
|
||||
[('{}: {}'.format(h, v)) for h, v in email.items()]
|
||||
)
|
||||
|
||||
inlines = [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django_mailbox.utils
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-15 22:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2018-04-21 00:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Models declaration for application ``django_mailbox``.
|
||||
|
|
@ -8,7 +7,9 @@ import gzip
|
|||
from email.encoders import encode_base64
|
||||
from email.message import Message as EmailMessage
|
||||
from email.utils import formatdate, parseaddr
|
||||
from urllib.parse import parse_qs, unquote, urlparse
|
||||
from quopri import encode as encode_quopri
|
||||
from io import BytesIO
|
||||
import base64
|
||||
import email
|
||||
import logging
|
||||
|
|
@ -18,9 +19,6 @@ import sys
|
|||
import uuid
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import six
|
||||
from six.moves.urllib.parse import parse_qs, unquote, urlparse
|
||||
|
||||
import django
|
||||
from django.conf import settings as django_settings
|
||||
from django.core.files.base import ContentFile, File
|
||||
|
|
@ -41,7 +39,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class ActiveMailboxManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super(ActiveMailboxManager, self).get_queryset().filter(
|
||||
return super().get_queryset().filter(
|
||||
active=True,
|
||||
)
|
||||
|
||||
|
|
@ -49,12 +47,12 @@ class ActiveMailboxManager(models.Manager):
|
|||
@python_2_unicode_compatible
|
||||
class Mailbox(models.Model):
|
||||
name = models.CharField(
|
||||
_(u'Name'),
|
||||
_('Name'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
uri = models.CharField(
|
||||
_(u'URI'),
|
||||
_('URI'),
|
||||
max_length=255,
|
||||
help_text=(_(
|
||||
"Example: imap+ssl://myusername:mypassword@someserver <br />"
|
||||
|
|
@ -72,7 +70,7 @@ class Mailbox(models.Model):
|
|||
)
|
||||
|
||||
from_email = models.CharField(
|
||||
_(u'From email'),
|
||||
_('From email'),
|
||||
max_length=255,
|
||||
help_text=(_(
|
||||
"Example: MailBot <mailbot@yourdomain.com><br />"
|
||||
|
|
@ -89,7 +87,7 @@ class Mailbox(models.Model):
|
|||
)
|
||||
|
||||
active = models.BooleanField(
|
||||
_(u'Active'),
|
||||
_('Active'),
|
||||
help_text=(_(
|
||||
"Check this e-mail inbox for new e-mail messages during polling "
|
||||
"cycles. This checkbox does not have an effect upon whether "
|
||||
|
|
@ -102,7 +100,7 @@ class Mailbox(models.Model):
|
|||
)
|
||||
|
||||
last_polling = models.DateTimeField(
|
||||
_(u"Last polling"),
|
||||
_("Last polling"),
|
||||
help_text=(_("The time of last successful polling for messages."
|
||||
"It is blank for new mailboxes and is not set for "
|
||||
"mailboxes that only receive messages via a pipe.")),
|
||||
|
|
@ -294,7 +292,7 @@ class Mailbox(models.Model):
|
|||
attachment.document.save(
|
||||
uuid.uuid4().hex + extension,
|
||||
ContentFile(
|
||||
six.BytesIO(
|
||||
BytesIO(
|
||||
msg.get_payload(decode=True)
|
||||
).getvalue()
|
||||
)
|
||||
|
|
@ -392,7 +390,7 @@ class Mailbox(models.Model):
|
|||
with gzip.GzipFile(fileobj=fp_tmp, mode="w") as fp:
|
||||
fp.write(message.as_string().encode('utf-8'))
|
||||
msg.eml.save(
|
||||
"%s.eml.gz" % (uuid.uuid4(), ),
|
||||
"{}.eml.gz".format(uuid.uuid4()),
|
||||
File(fp_tmp),
|
||||
save=False
|
||||
)
|
||||
|
|
@ -430,21 +428,21 @@ class Mailbox(models.Model):
|
|||
|
||||
class IncomingMessageManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super(IncomingMessageManager, self).get_queryset().filter(
|
||||
return super().get_queryset().filter(
|
||||
outgoing=False,
|
||||
)
|
||||
|
||||
|
||||
class OutgoingMessageManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super(OutgoingMessageManager, self).get_queryset().filter(
|
||||
return super().get_queryset().filter(
|
||||
outgoing=True,
|
||||
)
|
||||
|
||||
|
||||
class UnreadMessageManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super(UnreadMessageManager, self).get_queryset().filter(
|
||||
return super().get_queryset().filter(
|
||||
read=None
|
||||
)
|
||||
|
||||
|
|
@ -454,17 +452,17 @@ class Message(models.Model):
|
|||
mailbox = models.ForeignKey(
|
||||
Mailbox,
|
||||
related_name='messages',
|
||||
verbose_name=_(u'Mailbox'),
|
||||
verbose_name=_('Mailbox'),
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
subject = models.CharField(
|
||||
_(u'Subject'),
|
||||
_('Subject'),
|
||||
max_length=255
|
||||
)
|
||||
|
||||
message_id = models.CharField(
|
||||
_(u'Message ID'),
|
||||
_('Message ID'),
|
||||
max_length=255
|
||||
)
|
||||
|
||||
|
|
@ -473,7 +471,7 @@ class Message(models.Model):
|
|||
related_name='replies',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_(u'In reply to'),
|
||||
verbose_name=_('In reply to'),
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
|
|
@ -483,21 +481,21 @@ class Message(models.Model):
|
|||
)
|
||||
|
||||
to_header = models.TextField(
|
||||
_(u'To header'),
|
||||
_('To header'),
|
||||
)
|
||||
|
||||
outgoing = models.BooleanField(
|
||||
_(u'Outgoing'),
|
||||
_('Outgoing'),
|
||||
default=False,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
body = models.TextField(
|
||||
_(u'Body'),
|
||||
_('Body'),
|
||||
)
|
||||
|
||||
encoded = models.BooleanField(
|
||||
_(u'Encoded'),
|
||||
_('Encoded'),
|
||||
default=False,
|
||||
help_text=_('True if the e-mail body is Base64 encoded'),
|
||||
)
|
||||
|
|
@ -508,17 +506,17 @@ class Message(models.Model):
|
|||
)
|
||||
|
||||
read = models.DateTimeField(
|
||||
_(u'Read'),
|
||||
_('Read'),
|
||||
default=None,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
eml = models.FileField(
|
||||
_(u'Raw message contents'),
|
||||
_('Raw message contents'),
|
||||
null=True,
|
||||
upload_to="messages",
|
||||
help_text=_(u'Original full content of message')
|
||||
help_text=_('Original full content of message')
|
||||
)
|
||||
objects = models.Manager()
|
||||
unread_messages = UnreadMessageManager()
|
||||
|
|
@ -634,9 +632,9 @@ class Message(models.Model):
|
|||
if encoding and encoding.lower() == 'quoted-printable':
|
||||
# Cannot use `email.encoders.encode_quopri due to
|
||||
# bug 14360: http://bugs.python.org/issue14360
|
||||
output = six.BytesIO()
|
||||
output = BytesIO()
|
||||
encode_quopri(
|
||||
six.BytesIO(
|
||||
BytesIO(
|
||||
attachment.document.read()
|
||||
),
|
||||
output,
|
||||
|
|
@ -688,10 +686,8 @@ class Message(models.Model):
|
|||
no fields existed for storing arbitrary bytes.
|
||||
|
||||
"""
|
||||
if six.PY3:
|
||||
body = body.encode('utf-8')
|
||||
self.encoded = True
|
||||
self.body = base64.b64encode(body).decode('ascii')
|
||||
self.body = base64.b64encode(body.encode('utf-8')).decode('ascii')
|
||||
|
||||
def get_email_object(self):
|
||||
"""Returns an `email.message.Message` instance representing the
|
||||
|
|
@ -722,10 +718,7 @@ class Message(models.Model):
|
|||
self.eml.close()
|
||||
else:
|
||||
body = self.get_body()
|
||||
if six.PY3:
|
||||
flat = email.message_from_bytes(body)
|
||||
else:
|
||||
flat = email.message_from_string(body)
|
||||
flat = email.message_from_bytes(body)
|
||||
self._email_object = self._rehydrate(flat)
|
||||
return self._email_object
|
||||
|
||||
|
|
@ -734,7 +727,7 @@ class Message(models.Model):
|
|||
for attachment in self.attachments.all():
|
||||
# This attachment is attached only to this message.
|
||||
attachment.delete()
|
||||
return super(Message, self).delete(*args, **kwargs)
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.subject
|
||||
|
|
@ -756,30 +749,25 @@ class MessageAttachment(models.Model):
|
|||
)
|
||||
|
||||
headers = models.TextField(
|
||||
_(u'Headers'),
|
||||
_('Headers'),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
document = models.FileField(
|
||||
_(u'Document'),
|
||||
_('Document'),
|
||||
upload_to=utils.get_attachment_save_path,
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Deletes the attachment."""
|
||||
self.document.delete()
|
||||
return super(MessageAttachment, self).delete(*args, **kwargs)
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def _get_rehydrated_headers(self):
|
||||
headers = self.headers
|
||||
if headers is None:
|
||||
return EmailMessage()
|
||||
if sys.version_info < (3, 0):
|
||||
try:
|
||||
headers = headers.encode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
headers = headers.decode('utf-8').encode('utf-8')
|
||||
return email.message_from_string(headers)
|
||||
|
||||
def _set_dehydrated_headers(self, email_object):
|
||||
|
|
@ -798,7 +786,7 @@ class MessageAttachment(models.Model):
|
|||
def get_filename(self):
|
||||
"""Returns the original filename of this attachment."""
|
||||
file_name = self._get_rehydrated_headers().get_filename()
|
||||
if isinstance(file_name, six.string_types):
|
||||
if isinstance(file_name, str):
|
||||
result = utils.convert_header_to_unicode(file_name)
|
||||
if result is None:
|
||||
return file_name
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import email
|
||||
from south.db import db
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# encoding: utf-8
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
@ -9,33 +8,33 @@ class Migration(SchemaMigration):
|
|||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'MessageAttachment.message'
|
||||
db.add_column(u'django_mailbox_messageattachment', 'message',
|
||||
db.add_column('django_mailbox_messageattachment', 'message',
|
||||
self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='attachments', null=True, to=orm['django_mailbox.Message']),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'MessageAttachment.message'
|
||||
db.delete_column(u'django_mailbox_messageattachment', 'message_id')
|
||||
db.delete_column('django_mailbox_messageattachment', 'message_id')
|
||||
|
||||
|
||||
models = {
|
||||
u'django_mailbox.mailbox': {
|
||||
'django_mailbox.mailbox': {
|
||||
'Meta': {'object_name': 'Mailbox'},
|
||||
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'from_email': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'uri': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'django_mailbox.message': {
|
||||
'django_mailbox.message': {
|
||||
'Meta': {'object_name': 'Message'},
|
||||
'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'messages_old'", 'blank': 'True', 'to': u"orm['django_mailbox.MessageAttachment']"}),
|
||||
'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'messages_old'", 'blank': 'True', 'to': "orm['django_mailbox.MessageAttachment']"}),
|
||||
'body': ('django.db.models.fields.TextField', [], {}),
|
||||
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': u"orm['django_mailbox.Mailbox']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}),
|
||||
'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'outgoing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'processed': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
|
|
@ -43,11 +42,11 @@ class Migration(SchemaMigration):
|
|||
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'to_header': ('django.db.models.fields.TextField', [], {})
|
||||
},
|
||||
u'django_mailbox.messageattachment': {
|
||||
'django_mailbox.messageattachment': {
|
||||
'Meta': {'object_name': 'MessageAttachment'},
|
||||
'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments_new'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"})
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments_new'", 'null': 'True', 'to': "orm['django_mailbox.Message']"})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
@ -21,22 +20,22 @@ class Migration(SchemaMigration):
|
|||
)
|
||||
|
||||
models = {
|
||||
u'django_mailbox.mailbox': {
|
||||
'django_mailbox.mailbox': {
|
||||
'Meta': {'object_name': 'Mailbox'},
|
||||
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'from_email': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'uri': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'django_mailbox.message': {
|
||||
'django_mailbox.message': {
|
||||
'Meta': {'object_name': 'Message'},
|
||||
'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'messages_old'", 'blank': 'True', 'to': u"orm['django_mailbox.MessageAttachment']"}),
|
||||
'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'messages_old'", 'blank': 'True', 'to': "orm['django_mailbox.MessageAttachment']"}),
|
||||
'body': ('django.db.models.fields.TextField', [], {}),
|
||||
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': u"orm['django_mailbox.Mailbox']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}),
|
||||
'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'outgoing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'processed': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
|
|
@ -44,11 +43,11 @@ class Migration(SchemaMigration):
|
|||
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'to_header': ('django.db.models.fields.TextField', [], {})
|
||||
},
|
||||
u'django_mailbox.messageattachment': {
|
||||
'django_mailbox.messageattachment': {
|
||||
'Meta': {'object_name': 'MessageAttachment'},
|
||||
'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments_new'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"})
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments_new'", 'null': 'True', 'to': "orm['django_mailbox.Message']"})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
@ -12,40 +11,40 @@ class Migration(SchemaMigration):
|
|||
db.delete_table('django_mailbox_message_attachments')
|
||||
|
||||
# Adding field 'MessageAttachment.headers'
|
||||
db.add_column(u'django_mailbox_messageattachment', 'headers',
|
||||
db.add_column('django_mailbox_messageattachment', 'headers',
|
||||
self.gf('django.db.models.fields.TextField')(null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding M2M table for field attachments on 'Message'
|
||||
db.create_table(u'django_mailbox_message_attachments', (
|
||||
db.create_table('django_mailbox_message_attachments', (
|
||||
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('message', models.ForeignKey(orm[u'django_mailbox.message'], null=False)),
|
||||
('messageattachment', models.ForeignKey(orm[u'django_mailbox.messageattachment'], null=False))
|
||||
('message', models.ForeignKey(orm['django_mailbox.message'], null=False)),
|
||||
('messageattachment', models.ForeignKey(orm['django_mailbox.messageattachment'], null=False))
|
||||
))
|
||||
db.create_unique(u'django_mailbox_message_attachments', ['message_id', 'messageattachment_id'])
|
||||
db.create_unique('django_mailbox_message_attachments', ['message_id', 'messageattachment_id'])
|
||||
|
||||
# Deleting field 'MessageAttachment.headers'
|
||||
db.delete_column(u'django_mailbox_messageattachment', 'headers')
|
||||
db.delete_column('django_mailbox_messageattachment', 'headers')
|
||||
|
||||
|
||||
models = {
|
||||
u'django_mailbox.mailbox': {
|
||||
'django_mailbox.mailbox': {
|
||||
'Meta': {'object_name': 'Mailbox'},
|
||||
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'from_email': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'uri': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'django_mailbox.message': {
|
||||
'django_mailbox.message': {
|
||||
'Meta': {'object_name': 'Message'},
|
||||
'body': ('django.db.models.fields.TextField', [], {}),
|
||||
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': u"orm['django_mailbox.Mailbox']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}),
|
||||
'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'outgoing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'processed': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
|
|
@ -53,12 +52,12 @@ class Migration(SchemaMigration):
|
|||
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'to_header': ('django.db.models.fields.TextField', [], {})
|
||||
},
|
||||
u'django_mailbox.messageattachment': {
|
||||
'django_mailbox.messageattachment': {
|
||||
'Meta': {'object_name': 'MessageAttachment'},
|
||||
'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
|
||||
'headers': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"})
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': "orm['django_mailbox.Message']"})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
@ -9,33 +8,33 @@ class Migration(SchemaMigration):
|
|||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'Message.encoded'
|
||||
db.add_column(u'django_mailbox_message', 'encoded',
|
||||
db.add_column('django_mailbox_message', 'encoded',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'Message.encoded'
|
||||
db.delete_column(u'django_mailbox_message', 'encoded')
|
||||
db.delete_column('django_mailbox_message', 'encoded')
|
||||
|
||||
|
||||
models = {
|
||||
u'django_mailbox.mailbox': {
|
||||
'django_mailbox.mailbox': {
|
||||
'Meta': {'object_name': 'Mailbox'},
|
||||
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'from_email': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'uri': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'django_mailbox.message': {
|
||||
'django_mailbox.message': {
|
||||
'Meta': {'object_name': 'Message'},
|
||||
'body': ('django.db.models.fields.TextField', [], {}),
|
||||
'encoded': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': u"orm['django_mailbox.Mailbox']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}),
|
||||
'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'outgoing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'processed': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
|
|
@ -43,12 +42,12 @@ class Migration(SchemaMigration):
|
|||
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'to_header': ('django.db.models.fields.TextField', [], {})
|
||||
},
|
||||
u'django_mailbox.messageattachment': {
|
||||
'django_mailbox.messageattachment': {
|
||||
'Meta': {'object_name': 'MessageAttachment'},
|
||||
'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
|
||||
'headers': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"})
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': "orm['django_mailbox.Message']"})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
|
@ -9,34 +8,34 @@ class Migration(SchemaMigration):
|
|||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'Message.eml'
|
||||
db.add_column(u'django_mailbox_message', 'eml',
|
||||
db.add_column('django_mailbox_message', 'eml',
|
||||
self.gf('django.db.models.fields.files.FileField')(max_length=100, null=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'Message.eml'
|
||||
db.delete_column(u'django_mailbox_message', 'eml')
|
||||
db.delete_column('django_mailbox_message', 'eml')
|
||||
|
||||
|
||||
models = {
|
||||
u'django_mailbox.mailbox': {
|
||||
'django_mailbox.mailbox': {
|
||||
'Meta': {'object_name': 'Mailbox'},
|
||||
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'from_email': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'uri': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'django_mailbox.message': {
|
||||
'django_mailbox.message': {
|
||||
'Meta': {'object_name': 'Message'},
|
||||
'body': ('django.db.models.fields.TextField', [], {}),
|
||||
'eml': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
|
||||
'encoded': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': u"orm['django_mailbox.Mailbox']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': "orm['django_mailbox.Message']"}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': "orm['django_mailbox.Mailbox']"}),
|
||||
'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'outgoing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'processed': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
|
|
@ -44,12 +43,12 @@ class Migration(SchemaMigration):
|
|||
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'to_header': ('django.db.models.fields.TextField', [], {})
|
||||
},
|
||||
u'django_mailbox.messageattachment': {
|
||||
'django_mailbox.messageattachment': {
|
||||
'Meta': {'object_name': 'MessageAttachment'},
|
||||
'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
|
||||
'headers': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"})
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': "orm['django_mailbox.Message']"})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import email
|
|||
import os.path
|
||||
import time
|
||||
|
||||
import six
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
|
||||
|
|
@ -57,7 +55,7 @@ class EmailMessageTestCase(TestCase):
|
|||
settings.EMAIL_HOST_USER = self.test_account
|
||||
settings.EMAIL_HOST_PASSWORD = self.test_password
|
||||
settings.EMAIL_USE_TLS = True
|
||||
super(EmailMessageTestCase, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
def _get_new_messages(self, mailbox, condition=None):
|
||||
start_time = time.time()
|
||||
|
|
@ -70,8 +68,7 @@ class EmailMessageTestCase(TestCase):
|
|||
# check if generator contains at least one element
|
||||
message = next(messages)
|
||||
yield message
|
||||
for message in messages:
|
||||
yield message
|
||||
yield from messages
|
||||
return
|
||||
|
||||
except StopIteration:
|
||||
|
|
@ -92,10 +89,7 @@ class EmailMessageTestCase(TestCase):
|
|||
|
||||
def _get_email_object(self, name):
|
||||
copy = self._get_email_as_text(name)
|
||||
if six.PY3:
|
||||
return email.message_from_bytes(copy)
|
||||
else:
|
||||
return email.message_from_string(copy)
|
||||
return email.message_from_bytes(copy)
|
||||
|
||||
def _headers_identical(self, left, right, header=None):
|
||||
""" Check if headers are (close enough to) identical.
|
||||
|
|
@ -131,7 +125,7 @@ class EmailMessageTestCase(TestCase):
|
|||
raise AssertionError("Extra header '%s'" % key)
|
||||
if not self._headers_identical(right[key], value, header=key):
|
||||
raise AssertionError(
|
||||
"Header '%s' unequal:\n%s\n%s" % (
|
||||
"Header '{}' unequal:\n{}\n{}".format(
|
||||
key,
|
||||
repr(value),
|
||||
repr(right[key]),
|
||||
|
|
@ -144,7 +138,7 @@ class EmailMessageTestCase(TestCase):
|
|||
raise AssertionError("Extra header '%s'" % key)
|
||||
if not self._headers_identical(left[key], value, header=key):
|
||||
raise AssertionError(
|
||||
"Header '%s' unequal:\n%s\n%s" % (
|
||||
"Header '{}' unequal:\n{}\n{}".format(
|
||||
key,
|
||||
repr(value),
|
||||
repr(right[key]),
|
||||
|
|
@ -175,7 +169,7 @@ class EmailMessageTestCase(TestCase):
|
|||
|
||||
def _raise_mismatched(self, left, right):
|
||||
raise AssertionError(
|
||||
"Message payloads do not match:\n%s\n%s" % (
|
||||
"Message payloads do not match:\n{}\n{}".format(
|
||||
left.as_string(),
|
||||
right.as_string()
|
||||
)
|
||||
|
|
@ -183,7 +177,7 @@ class EmailMessageTestCase(TestCase):
|
|||
|
||||
def assertEqual(self, left, right): # noqa: N802
|
||||
if not isinstance(left, email.message.Message):
|
||||
return super(EmailMessageTestCase, self).assertEqual(left, right)
|
||||
return super().assertEqual(left, right)
|
||||
return self.compare_email_objects(left, right)
|
||||
|
||||
def tearDown(self):
|
||||
|
|
@ -194,4 +188,4 @@ class EmailMessageTestCase(TestCase):
|
|||
models.TEXT_STORED_MIMETYPES = self._TEXT_STORED_MIMETYPES
|
||||
|
||||
self.mailbox.delete()
|
||||
super(EmailMessageTestCase, self).tearDown()
|
||||
super().tearDown()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import os
|
||||
import uuid
|
||||
|
||||
from six.moves.urllib import parse
|
||||
from urllib import parse
|
||||
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
|
||||
|
|
@ -14,7 +13,7 @@ __all__ = ['TestImap']
|
|||
|
||||
class TestImap(EmailMessageTestCase):
|
||||
def setUp(self):
|
||||
super(TestImap, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.test_imap_server = (
|
||||
os.environ.get('EMAIL_IMAP_SERVER')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import copy
|
||||
|
||||
import mock
|
||||
from unittest import mock
|
||||
|
||||
from django_mailbox import models, utils
|
||||
from django_mailbox.models import Message
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import os.path
|
|||
import sys
|
||||
|
||||
import copy
|
||||
import mock
|
||||
import six
|
||||
from unittest import mock
|
||||
|
||||
from django_mailbox.models import Mailbox, Message
|
||||
from django_mailbox.utils import convert_header_to_unicode
|
||||
|
|
@ -54,18 +53,18 @@ class TestProcessEmail(EmailMessageTestCase):
|
|||
|
||||
attachments = msg.attachments.order_by('pk').all()
|
||||
self.assertEqual(
|
||||
u'\u041f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043b'
|
||||
u'\u043e\u0436\u0435\u043d\u0438\u0439 HSE Career Fair 8 \u0430'
|
||||
u'\u043f\u0440\u0435\u043b\u044f 2016.pdf',
|
||||
'\u041f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043b'
|
||||
'\u043e\u0436\u0435\u043d\u0438\u0439 HSE Career Fair 8 \u0430'
|
||||
'\u043f\u0440\u0435\u043b\u044f 2016.pdf',
|
||||
attachments[0].get_filename()
|
||||
)
|
||||
self.assertEqual(
|
||||
u'\u0412\u0435\u0434\u043e\u043c\u043e\u0441\u0442\u0438.pdf',
|
||||
'\u0412\u0435\u0434\u043e\u043c\u043e\u0441\u0442\u0438.pdf',
|
||||
attachments[1].get_filename()
|
||||
)
|
||||
self.assertEqual(
|
||||
u'\u041f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043b'
|
||||
u'\u043e\u0436\u0435\u043d\u0438\u0439 2016.pptx',
|
||||
'\u041f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043b'
|
||||
'\u043e\u0436\u0435\u043d\u0438\u0439 2016.pptx',
|
||||
attachments[2].get_filename()
|
||||
)
|
||||
|
||||
|
|
@ -89,33 +88,6 @@ class TestProcessEmail(EmailMessageTestCase):
|
|||
'heart.png',
|
||||
)
|
||||
|
||||
def test_message_with_not_decoded_attachment_header(self):
|
||||
if sys.version_info > (3, 0):
|
||||
self.skipTest(
|
||||
"This test is only relevant on Python 2.x."
|
||||
)
|
||||
|
||||
email_object = self._get_email_object(
|
||||
'message_with_not_decoded_attachment_header.eml',
|
||||
)
|
||||
|
||||
mailbox = Mailbox.objects.create()
|
||||
msg = mailbox.process_incoming_message(email_object)
|
||||
|
||||
expected_count = 1
|
||||
actual_count = msg.attachments.count()
|
||||
|
||||
self.assertEqual(
|
||||
expected_count,
|
||||
actual_count,
|
||||
)
|
||||
|
||||
attachment = msg.attachments.all()[0]
|
||||
self.assertEqual(
|
||||
attachment.get_filename(),
|
||||
u'\xc3\xb0\xcc\x9eo\xce\xb2\xcc\x9ele.png',
|
||||
)
|
||||
|
||||
def test_message_with_utf8_attachment_header(self):
|
||||
""" Ensure that we properly handle UTF-8 encoded attachment
|
||||
|
||||
|
|
@ -138,13 +110,13 @@ class TestProcessEmail(EmailMessageTestCase):
|
|||
attachment = msg.attachments.all()[0]
|
||||
self.assertEqual(
|
||||
attachment.get_filename(),
|
||||
u'pi\u0142kochwyty.jpg'
|
||||
'pi\u0142kochwyty.jpg'
|
||||
)
|
||||
|
||||
attachment = msg.attachments.all()[1]
|
||||
self.assertEqual(
|
||||
attachment.get_filename(),
|
||||
u'odpowied\u017a Burmistrza.jpg'
|
||||
'odpowied\u017a Burmistrza.jpg'
|
||||
)
|
||||
|
||||
def test_message_get_text_body(self):
|
||||
|
|
@ -187,7 +159,7 @@ class TestProcessEmail(EmailMessageTestCase):
|
|||
'messages/generic_message.eml'
|
||||
)
|
||||
) as f:
|
||||
unicode_body = six.u(f.read())
|
||||
unicode_body = f.read()
|
||||
|
||||
message = Message()
|
||||
message.body = unicode_body
|
||||
|
|
@ -281,10 +253,8 @@ class TestProcessEmail(EmailMessageTestCase):
|
|||
|
||||
msg = self.mailbox.process_incoming_message(email_object)
|
||||
|
||||
expected_text = six.u(
|
||||
'This message contains funny UTF16 characters like this one: '
|
||||
'"\xc2\xa0" and this one "\xe2\x9c\xbf".'
|
||||
)
|
||||
expected_text = 'This message contains funny UTF16 characters ' + \
|
||||
'like this one: "\xc2\xa0" and this one "\xe2\x9c\xbf".'
|
||||
actual_text = msg.text
|
||||
|
||||
self.assertEqual(
|
||||
|
|
@ -320,14 +290,12 @@ class TestProcessEmail(EmailMessageTestCase):
|
|||
msg = self.mailbox.process_incoming_message(email_object)
|
||||
|
||||
actual_text = msg.text
|
||||
expected_body = six.u(
|
||||
'\u042d\u0442\u043e '
|
||||
'\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 '
|
||||
'\u0438\u043c\u0435\u0435\u0442 '
|
||||
'\u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d'
|
||||
'\u0443\u044e '
|
||||
expected_body = '\u042d\u0442\u043e ' + \
|
||||
'\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 ' + \
|
||||
'\u0438\u043c\u0435\u0435\u0442 ' + \
|
||||
'\u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d' + \
|
||||
'\u0443\u044e ' + \
|
||||
'\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u043a\u0430.'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
actual_text,
|
||||
|
|
@ -341,23 +309,15 @@ class TestProcessEmail(EmailMessageTestCase):
|
|||
|
||||
msg = self.mailbox.process_incoming_message(email_object)
|
||||
|
||||
expected_subject = six.u(
|
||||
'\u00D3\u00E7\u00ED\u00E0\u00E9 \u00EA\u00E0\u00EA '
|
||||
'\u00E7\u00E0\u00F0\u00E0\u00E1\u00E0\u00F2\u00FB\u00E2'
|
||||
'\u00E0\u00F2\u00FC \u00EE\u00F2 1000$ \u00E2 '
|
||||
expected_subject = '\u00D3\u00E7\u00ED\u00E0\u00E9 \u00EA\u00E0\u00EA ' + \
|
||||
'\u00E7\u00E0\u00F0\u00E0\u00E1\u00E0\u00F2\u00FB\u00E2' + \
|
||||
'\u00E0\u00F2\u00FC \u00EE\u00F2 1000$ \u00E2 ' + \
|
||||
'\u00ED\u00E5\u00E4\u00E5\u00EB\u00FE!'
|
||||
)
|
||||
actual_subject = msg.subject
|
||||
self.assertEqual(actual_subject, expected_subject)
|
||||
|
||||
if sys.version_info >= (3, 3):
|
||||
# There were various bugfixes in Py3k's email module,
|
||||
# this is apparently one of them.
|
||||
expected_from = six.u('test test <mr.test32@mail.ru>')
|
||||
else:
|
||||
expected_from = six.u('test test<mr.test32@mail.ru>')
|
||||
expected_from = 'test test <mr.test32@mail.ru>'
|
||||
actual_from = msg.from_header
|
||||
|
||||
self.assertEqual(expected_from, actual_from)
|
||||
|
||||
def test_message_reply(self):
|
||||
|
|
@ -426,15 +386,15 @@ class TestProcessEmail(EmailMessageTestCase):
|
|||
|
||||
msg = self.mailbox.process_incoming_message(message)
|
||||
|
||||
actual_email_object = msg.get_email_object()
|
||||
|
||||
self.assertNotEquals(msg.eml, None)
|
||||
|
||||
self.assertTrue(msg.eml.name.endswith('.eml'))
|
||||
|
||||
with open(msg.eml.name, 'rb') as f:
|
||||
self.assertEqual(f.read(),
|
||||
self._get_email_as_text('generic_message.eml'))
|
||||
self.assertEqual(
|
||||
f.read(),
|
||||
self._get_email_as_text('generic_message.eml')
|
||||
)
|
||||
|
||||
def test_message_saving_ignored(self):
|
||||
message = self._get_email_object('generic_message.eml')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from distutils.version import LooseVersion
|
||||
import mock
|
||||
from unittest import mock
|
||||
|
||||
from django.core.management import call_command, CommandError
|
||||
from django.test import TestCase
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import mock
|
||||
import six
|
||||
from unittest import mock
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
|
|
@ -9,24 +8,22 @@ from django_mailbox.transports import ImapTransport, Pop3Transport
|
|||
FAKE_UID_SEARCH_ANSWER = (
|
||||
'OK',
|
||||
[
|
||||
six.b(
|
||||
'18 19 20 21 22 23 24 25 26 27 28 29 '
|
||||
'30 31 32 33 34 35 36 37 38 39 40 41 42 43 44'
|
||||
)
|
||||
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'
|
||||
]
|
||||
)
|
||||
FAKE_UID_FETCH_SIZES = (
|
||||
'OK',
|
||||
[
|
||||
six.b('1 (UID 18 RFC822.SIZE 58070000000)'),
|
||||
six.b('2 (UID 19 RFC822.SIZE 2593)')
|
||||
b'1 (UID 18 RFC822.SIZE 58070000000)',
|
||||
b'2 (UID 19 RFC822.SIZE 2593)'
|
||||
]
|
||||
)
|
||||
FAKE_UID_FETCH_MSG = (
|
||||
'OK',
|
||||
[
|
||||
(
|
||||
six.b('1 (UID 18 RFC822 {5807}'),
|
||||
b'1 (UID 18 RFC822 {5807}',
|
||||
get_email_as_text('generic_message.eml')
|
||||
),
|
||||
]
|
||||
|
|
@ -34,13 +31,13 @@ FAKE_UID_FETCH_MSG = (
|
|||
FAKE_UID_COPY_MSG = (
|
||||
'OK',
|
||||
[
|
||||
six.b('[COPYUID 1 2 2] (Success)')
|
||||
b'[COPYUID 1 2 2] (Success)'
|
||||
]
|
||||
)
|
||||
FAKE_LIST_ARCHIVE_FOLDERS_ANSWERS = (
|
||||
'OK',
|
||||
[
|
||||
six.b('(\\HasNoChildren \\All) "/" "[Gmail]/All Mail"')
|
||||
b'(\\HasNoChildren \\All) "/" "[Gmail]/All Mail"'
|
||||
]
|
||||
)
|
||||
|
||||
|
|
@ -66,12 +63,12 @@ class IMAPTestCase(EmailMessageTestCase):
|
|||
self.imap_server = mock.Mock()
|
||||
self.imap_server.uid = imap_server_uid_method
|
||||
self.imap_server.list = imap_server_list_method
|
||||
super(IMAPTestCase, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
|
||||
class TestImapTransport(IMAPTestCase):
|
||||
def setUp(self):
|
||||
super(TestImapTransport, self).setUp()
|
||||
super().setUp()
|
||||
self.arbitrary_hostname = 'one.two.three'
|
||||
self.arbitrary_port = 100
|
||||
self.ssl = False
|
||||
|
|
@ -92,7 +89,7 @@ class TestImapTransport(IMAPTestCase):
|
|||
|
||||
class TestImapArchivedTransport(TestImapTransport):
|
||||
def setUp(self):
|
||||
super(TestImapArchivedTransport, self).setUp()
|
||||
super().setUp()
|
||||
self.archive = 'Archive'
|
||||
self.transport = ImapTransport(
|
||||
self.arbitrary_hostname,
|
||||
|
|
@ -107,7 +104,7 @@ class TestMaxSizeImapTransport(TestImapTransport):
|
|||
|
||||
@override_settings(DJANGO_MAILBOX_MAX_MESSAGE_SIZE=5807)
|
||||
def setUp(self):
|
||||
super(TestMaxSizeImapTransport, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.transport = ImapTransport(
|
||||
self.arbitrary_hostname,
|
||||
|
|
@ -142,7 +139,7 @@ class TestPop3Transport(EmailMessageTestCase):
|
|||
self.ssl
|
||||
)
|
||||
self.transport.server = None
|
||||
super(TestPop3Transport, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
def test_get_email_message(self):
|
||||
with mock.patch.object(self.transport, 'server') as server:
|
||||
|
|
|
|||
|
|
@ -1,19 +1,11 @@
|
|||
import email
|
||||
|
||||
import six
|
||||
|
||||
# Do *not* remove this, we need to use this in subclasses of EmailTransport
|
||||
if six.PY3:
|
||||
from email.errors import MessageParseError # noqa: F401
|
||||
else:
|
||||
from email.Errors import MessageParseError # noqa: F401
|
||||
from email.errors import MessageParseError # noqa: F401
|
||||
|
||||
|
||||
class EmailTransport(object):
|
||||
class EmailTransport:
|
||||
def get_email_from_bytes(self, contents):
|
||||
if six.PY3:
|
||||
message = email.message_from_bytes(contents)
|
||||
else:
|
||||
message = email.message_from_string(contents)
|
||||
message = email.message_from_bytes(contents)
|
||||
|
||||
return message
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import sys
|
||||
import six
|
||||
|
||||
from .base import EmailTransport
|
||||
|
||||
|
|
@ -9,13 +8,8 @@ class GenericFileMailbox(EmailTransport):
|
|||
_path = None
|
||||
|
||||
def __init__(self, path):
|
||||
super(GenericFileMailbox, self).__init__()
|
||||
if six.PY2:
|
||||
self._path = path.encode(
|
||||
sys.getfilesystemencoding()
|
||||
)
|
||||
else:
|
||||
self._path = path
|
||||
super().__init__()
|
||||
self._path = path
|
||||
|
||||
def get_instance(self):
|
||||
return self._variant(self._path)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class GmailImapTransport(ImapTransport):
|
|||
)
|
||||
)
|
||||
|
||||
auth_string = 'user=%s\1auth=Bearer %s\1\1' % (
|
||||
auth_string = 'user={}\1auth=Bearer {}\1\1'.format(
|
||||
google_email_address,
|
||||
access_token
|
||||
)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class ImapTransport(EmailTransport):
|
|||
safe_message_ids.append(uid)
|
||||
except ValueError as e:
|
||||
logger.warning(
|
||||
"ValueError: %s working on %s" % (e, each_msg[0])
|
||||
"ValueError: {} working on {}".format(e, each_msg[0])
|
||||
)
|
||||
pass
|
||||
return safe_message_ids
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import six
|
||||
|
||||
from poplib import POP3, POP3_SSL
|
||||
|
||||
from .base import EmailTransport, MessageParseError
|
||||
|
|
@ -24,9 +22,7 @@ class Pop3Transport(EmailTransport):
|
|||
self.server.pass_(password)
|
||||
|
||||
def get_message_body(self, message_lines):
|
||||
if six.PY3:
|
||||
return six.binary_type('\r\n', 'ascii').join(message_lines)
|
||||
return '\r\n'.join(message_lines)
|
||||
return bytes('\r\n', 'ascii').join(message_lines)
|
||||
|
||||
def get_message(self, condition=None):
|
||||
message_count = len(self.server.list()[1])
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import email.header
|
|||
import logging
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
|
|
@ -75,11 +73,8 @@ def get_settings():
|
|||
def convert_header_to_unicode(header):
|
||||
default_charset = get_settings()['default_charset']
|
||||
|
||||
if six.PY2 and isinstance(header, six.text_type):
|
||||
return header
|
||||
|
||||
def _decode(value, encoding):
|
||||
if isinstance(value, six.text_type):
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if not encoding or encoding == 'unknown-8bit':
|
||||
encoding = default_charset
|
||||
|
|
@ -106,7 +101,7 @@ def get_body_from_message(message, maintype, subtype):
|
|||
"""
|
||||
Fetchs the body message matching main/sub content type.
|
||||
"""
|
||||
body = six.text_type('')
|
||||
body = ''
|
||||
for part in message.walk():
|
||||
if part.get('content-disposition', '').startswith('attachment;'):
|
||||
continue
|
||||
|
|
|
|||
25
docs/conf.py
25
docs/conf.py
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# django-mailbox documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue Jan 22 20:29:12 2013.
|
||||
|
|
@ -40,8 +39,8 @@ source_suffix = '.rst'
|
|||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'django-mailbox'
|
||||
copyright = u'2014, Adam Coddington'
|
||||
project = 'django-mailbox'
|
||||
copyright = '2014, Adam Coddington'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
|
@ -183,8 +182,8 @@ latex_elements = {
|
|||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'django-mailbox.tex', u'django-mailbox Documentation',
|
||||
u'Adam Coddington', 'manual'),
|
||||
('index', 'django-mailbox.tex', 'django-mailbox Documentation',
|
||||
'Adam Coddington', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
|
|
@ -213,8 +212,8 @@ latex_documents = [
|
|||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'django-mailbox', u'django-mailbox Documentation',
|
||||
[u'Adam Coddington'], 1)
|
||||
('index', 'django-mailbox', 'django-mailbox Documentation',
|
||||
['Adam Coddington'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
|
|
@ -227,8 +226,8 @@ man_pages = [
|
|||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'django-mailbox', u'django-mailbox Documentation',
|
||||
u'Adam Coddington', 'django-mailbox', 'One line description of project.',
|
||||
('index', 'django-mailbox', 'django-mailbox Documentation',
|
||||
'Adam Coddington', 'django-mailbox', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
|
@ -275,18 +274,18 @@ def process_docstring(app, what, name, obj, options, lines):
|
|||
if help_text:
|
||||
# Add the model field to the end of the docstring as a param
|
||||
# using the help text as the description
|
||||
lines.append(u':param %s: %s' % (field.attname, help_text))
|
||||
lines.append(':param {}: {}'.format(field.attname, help_text))
|
||||
else:
|
||||
# Add the model field to the end of the docstring as a param
|
||||
# using the verbose name as the description
|
||||
lines.append(u':param %s: %s' % (field.attname, verbose_name))
|
||||
lines.append(':param {}: {}'.format(field.attname, verbose_name))
|
||||
|
||||
# Add the field's type to the docstring
|
||||
if isinstance(field, models.ForeignKey):
|
||||
to = field.rel.to
|
||||
lines.append(u':type %s: %s to :class:`~%s.%s`' % (field.attname, type(field).__name__, to.__module__, to.__name__))
|
||||
lines.append(':type {}: {} to :class:`~{}.{}`'.format(field.attname, type(field).__name__, to.__module__, to.__name__))
|
||||
else:
|
||||
lines.append(u':type %s: %s' % (field.attname, type(field).__name__))
|
||||
lines.append(':type {}: {}'.format(field.attname, type(field).__name__))
|
||||
|
||||
# Return the extended docstring
|
||||
return lines
|
||||
|
|
|
|||
6
setup.py
6
setup.py
|
|
@ -33,17 +33,11 @@ setup(
|
|||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.7',
|
||||
'Framework :: Django :: 1.8',
|
||||
'Framework :: Django :: 1.9',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.11',
|
||||
'Framework :: Django :: 2.0',
|
||||
'Framework :: Django :: 2.1',
|
||||
'Framework :: Django :: 2.2',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
|
|
|
|||
17
tox.ini
17
tox.ini
|
|
@ -2,10 +2,10 @@
|
|||
# sort by django version, next by python version
|
||||
envlist=
|
||||
flake8
|
||||
py{27,34}-django17
|
||||
py{34}-django17
|
||||
py{34,35}-django18
|
||||
py{27,34,35}-django19
|
||||
py{27,34,35}-django110
|
||||
py{34,35}-django19
|
||||
py{34,35}-django110
|
||||
py{27,34,35,36}-django111
|
||||
py{34,35,36}-django20
|
||||
py{35,36,37}-django21
|
||||
|
|
@ -14,23 +14,14 @@ envlist=
|
|||
[testenv]
|
||||
passenv=EMAIL_IMAP_SERVER EMAIL_ACCOUNT EMAIL_PASSWORD EMAIL_SMTP_SERVER
|
||||
deps=
|
||||
django17: django==1.7.*
|
||||
django18: django==1.8.*
|
||||
django19: django==1.9.*
|
||||
django110: django==1.10.*
|
||||
django111: django==1.11.*
|
||||
django20: django==2.0.*
|
||||
django21: django==2.0.*
|
||||
django22: django==2.0.*
|
||||
|
||||
-r{toxinidir}/test_requirements.txt
|
||||
sitepackages=False
|
||||
commands=
|
||||
django110: python {toxinidir}/manage.py makemigrations --check --dry-run
|
||||
django111: python {toxinidir}/manage.py makemigrations --check --dry-run
|
||||
django20: python {toxinidir}/manage.py makemigrations --check --dry-run
|
||||
django21: python {toxinidir}/manage.py makemigrations --check --dry-run
|
||||
django22: python {toxinidir}/manage.py makemigrations --check --dry-run
|
||||
python {toxinidir}/manage.py makemigrations --check --dry-run
|
||||
python manage.py test -v2 {posargs}
|
||||
|
||||
[testenv:flake8]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue