1
0
Fork 0

wrong folder stage

This commit is contained in:
Pietro Mingo 2022-07-27 10:31:01 +02:00
parent a52f20dabe
commit 50ee86d1d9
69 changed files with 4191 additions and 85 deletions

3
.idea/.gitignore generated vendored
View file

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View file

@ -1,41 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="28">
<item index="0" class="java.lang.String" itemvalue="pyfcm" />
<item index="1" class="java.lang.String" itemvalue="qrcode" />
<item index="2" class="java.lang.String" itemvalue="python-dateutil" />
<item index="3" class="java.lang.String" itemvalue="django-stdimage" />
<item index="4" class="java.lang.String" itemvalue="psycopg2-binary" />
<item index="5" class="java.lang.String" itemvalue="xhtml2pdf" />
<item index="6" class="java.lang.String" itemvalue="requests" />
<item index="7" class="java.lang.String" itemvalue="django-mailbox" />
<item index="8" class="java.lang.String" itemvalue="redis" />
<item index="9" class="java.lang.String" itemvalue="fbmq" />
<item index="10" class="java.lang.String" itemvalue="jdcal" />
<item index="11" class="java.lang.String" itemvalue="celery" />
<item index="12" class="java.lang.String" itemvalue="bleach" />
<item index="13" class="java.lang.String" itemvalue="ics" />
<item index="14" class="java.lang.String" itemvalue="djangorestframework" />
<item index="15" class="java.lang.String" itemvalue="django-cors-headers" />
<item index="16" class="java.lang.String" itemvalue="djangorestframework-jwt" />
<item index="17" class="java.lang.String" itemvalue="reportlab" />
<item index="18" class="java.lang.String" itemvalue="et-xmlfile" />
<item index="19" class="java.lang.String" itemvalue="gunicorn" />
<item index="20" class="java.lang.String" itemvalue="python-telegram-bot" />
<item index="21" class="java.lang.String" itemvalue="python-memcached" />
<item index="22" class="java.lang.String" itemvalue="openpyxl" />
<item index="23" class="java.lang.String" itemvalue="Pillow" />
<item index="24" class="java.lang.String" itemvalue="google-cloud-logging" />
<item index="25" class="java.lang.String" itemvalue="Django" />
<item index="26" class="java.lang.String" itemvalue="six" />
<item index="27" class="java.lang.String" itemvalue="urllib3" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View file

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated
View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (django-mailbox)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

8
.idea/modules.xml generated
View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/django-mailbox.iml" filepath="$PROJECT_DIR$/.idea/django-mailbox.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -0,0 +1,3 @@
__version__ = '4.8.2'
default_app_config = 'django_mailbox.apps.MailBoxConfig'

View file

@ -0,0 +1,112 @@
#!/usr/bin/env python
"""
Model configuration in application ``django_mailbox`` for administration
console.
"""
import logging
from django.conf import settings
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from django_mailbox.models import MessageAttachment, Message, Mailbox
from django_mailbox.signals import message_received
from django_mailbox.utils import convert_header_to_unicode
logger = logging.getLogger(__name__)
def get_new_mail(mailbox_admin, request, queryset):
queryset.get_new_mail()
get_new_mail.short_description = _('Get new mail')
def resend_message_received_signal(message_admin, request, queryset):
for message in queryset.all():
logger.debug('Resending \'message_received\' signal for %s' % message)
message_received.send(sender=message_admin, message=message)
resend_message_received_signal.short_description = (
_('Re-send message received signal')
)
class MailboxAdmin(admin.ModelAdmin):
list_display = (
'name',
'uri',
'from_email',
'active',
'last_polling',
)
readonly_fields = ['last_polling', ]
actions = [get_new_mail]
class MessageAttachmentAdmin(admin.ModelAdmin):
raw_id_fields = ('message', )
list_display = ('message', 'document',)
class MessageAttachmentInline(admin.TabularInline):
model = MessageAttachment
extra = 0
class MessageAdmin(admin.ModelAdmin):
def attachment_count(self, msg):
return msg.attachments.count()
attachment_count.short_description = _('Attachment count')
def subject(self, msg):
return convert_header_to_unicode(msg.subject)
def envelope_headers(self, msg):
email = msg.get_email_object()
return '\n'.join(
[('{}: {}'.format(h, v)) for h, v in email.items()]
)
inlines = [
MessageAttachmentInline,
]
list_display = (
'subject',
'processed',
'read',
'mailbox',
'outgoing',
'attachment_count',
)
ordering = ['-processed']
list_filter = (
'mailbox',
'outgoing',
'processed',
'read',
)
exclude = (
'body',
)
raw_id_fields = (
'in_reply_to',
)
readonly_fields = (
'envelope_headers',
'text',
'html',
)
actions = [resend_message_received_signal]
if getattr(settings, 'DJANGO_MAILBOX_ADMIN_ENABLED', True):
admin.site.register(Message, MessageAdmin)
admin.site.register(MessageAttachment, MessageAttachmentAdmin)
admin.site.register(Mailbox, MailboxAdmin)

View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class MailBoxConfig(AppConfig):
name = 'django_mailbox'
verbose_name = _("Mail Box")

View file

@ -0,0 +1,111 @@
import logging
from django.conf import settings
import requests
from social.apps.django_app.default.models import UserSocialAuth
logger = logging.getLogger(__name__)
class AccessTokenNotFound(Exception):
pass
class RefreshTokenNotFound(Exception):
pass
def get_google_consumer_key():
return settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY
def get_google_consumer_secret():
return settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET
def get_google_access_token(email):
# TODO: This should be cacheable
try:
me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2")
return me.extra_data['access_token']
except (UserSocialAuth.DoesNotExist, KeyError):
raise AccessTokenNotFound
def update_google_extra_data(email, extra_data):
try:
me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2")
me.extra_data = extra_data
me.save()
except (UserSocialAuth.DoesNotExist, KeyError):
raise AccessTokenNotFound
def get_google_refresh_token(email):
try:
me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2")
return me.extra_data['refresh_token']
except (UserSocialAuth.DoesNotExist, KeyError):
raise RefreshTokenNotFound
def google_api_get(email, url):
headers = dict(
Authorization="Bearer %s" % get_google_access_token(email),
)
r = requests.get(url, headers=headers)
logger.info("I got a %s", r.status_code)
if r.status_code == 401:
# Go use the refresh token
refresh_authorization(email)
r = requests.get(url, headers=headers)
logger.info("I got a %s", r.status_code)
if r.status_code == 200:
try:
return r.json()
except ValueError:
return r.text
def google_api_post(email, url, post_data, authorized=True):
# TODO: Make this a lot less ugly. especially the 401 handling
headers = dict()
if authorized is True:
headers.update(dict(
Authorization="Bearer %s" % get_google_access_token(email),
))
r = requests.post(url, headers=headers, data=post_data)
if r.status_code == 401:
refresh_authorization(email)
r = requests.post(url, headers=headers, data=post_data)
if r.status_code == 200:
try:
return r.json()
except ValueError:
return r.text
def refresh_authorization(email):
refresh_token = get_google_refresh_token(email)
post_data = dict(
refresh_token=refresh_token,
client_id=get_google_consumer_key(),
client_secret=get_google_consumer_secret(),
grant_type='refresh_token',
)
results = google_api_post(
email,
"https://accounts.google.com/o/oauth2/token?access_type=offline",
post_data,
authorized=False)
results.update({'refresh_token': refresh_token})
update_google_extra_data(email, results)
def fetch_user_info(email):
result = google_api_get(
email,
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json"
)
return result

View file

@ -0,0 +1,193 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-07-09 05:36-0500\n"
"PO-Revision-Date: 2017-07-09 13:37+0300\n"
"Last-Translator: Ivlev Denis <di-erz@yandex.ru>\n"
"Language-Team: \n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
"%100>=11 && n%100<=14)? 2 : 3);\n"
"X-Generator: Poedit 1.8.7.1\n"
#: django_mailbox/admin.py:29
msgid "Get new mail"
msgstr "Получить новые сообщения"
#: django_mailbox/admin.py:39
msgid "Re-send message received signal"
msgstr "Повторно отправить сигнал о получении"
#: django_mailbox/admin.py:69
msgid "Attachment count"
msgstr "Кол-во вложений"
#: django_mailbox/apps.py:7
msgid "Mail Box"
msgstr "Почтовый ящик"
#: django_mailbox/models.py:49
msgid "Name"
msgstr "Название"
#: django_mailbox/models.py:54
msgid "URI"
msgstr "URI"
#: django_mailbox/models.py:57
msgid ""
"Example: imap+ssl://myusername:mypassword@someserver <br /><br />Internet "
"transports include 'imap' and 'pop3'; common local file transports include "
"'maildir', 'mbox', and less commonly 'babyl', 'mh', and 'mmdf'. <br /><br /"
">Be sure to urlencode your username and password should they contain illegal "
"characters (like @, :, etc)."
msgstr ""
"Пример: imap+ssl://myusername:mypassword@someserver <br /><br />Интернет-"
"транспорт может быть 'imap' или 'pop3'; поддерживаются локальные файловые "
"транспорты 'maildir', 'mbox', а также 'babyl', 'mh', and 'mmdf'. <br /><br /"
">Используйте urlencode, если имя пользователя или пароль содержат "
"недопустипые символы (@, :, и т.д.)."
#: django_mailbox/models.py:72
msgid "From email"
msgstr "От"
#: django_mailbox/models.py:75
msgid ""
"Example: MailBot &lt;mailbot@yourdomain.com&gt;<br />'From' header to set "
"for outgoing email.<br /><br />If you do not use this e-mail inbox for "
"outgoing mail, this setting is unnecessary.<br />If you send e-mail without "
"setting this, your 'From' header will'be set to match the setting "
"`DEFAULT_FROM_EMAIL`."
msgstr ""
"Пример: MailBot &lt;mailbot@yourdomain.com&gt;<br />Исходящая электронная "
"почта.<br /><br />Если вы не используете этот почтовый ящик для исходящей "
"почты, этот параметр не нужен.<br />Если вы не указали данный параметр, "
"будет использоваться указанный в настройках `DEFAULT_FROM_EMAIL`."
#: django_mailbox/models.py:89
msgid "Active"
msgstr "Активный"
#: django_mailbox/models.py:91
msgid ""
"Check this e-mail inbox for new e-mail messages during polling cycles. This "
"checkbox does not have an effect upon whether mail is collected here when "
"this mailbox receives mail from a pipe, and does not affect whether e-mail "
"messages can be dispatched from this mailbox. "
msgstr ""
"Параметр указывает на необходимость проверки почтового ящика на наличие "
"новых сообщений в цикле опроса. Этот флажок не влияет на сбор почты, когда "
"этот почтовый ящик получает почту из канала и не влияет на отправку "
"сообщений электронной почты из этого почтового ящика."
#: django_mailbox/models.py:102
msgid "Last polling"
msgstr "Последний опрос"
#: django_mailbox/models.py:103
msgid ""
"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."
msgstr ""
"Время последнего успешного опроса сообщений. Для нового почтового ящика не "
"установлено. Также не устанавливается для почтовых ящиков "
"обновляющих сообщения через pipe."
#: django_mailbox/models.py:409 django_mailbox/models.py:438
msgid "Mailbox"
msgstr "Почтовый ящик"
#: django_mailbox/models.py:410
msgid "Mailboxes"
msgstr "Почтовые ящики"
#: django_mailbox/models.py:442
msgid "Subject"
msgstr "Тема"
#: django_mailbox/models.py:447
msgid "Message ID"
msgstr "Идентификатор сообщения"
#: django_mailbox/models.py:456
msgid "In reply to"
msgstr "В ответ на"
#: django_mailbox/models.py:460
msgid "From header"
msgstr "От(From)"
#: django_mailbox/models.py:465
msgid "To header"
msgstr "Кому(To)"
#: django_mailbox/models.py:469
msgid "Outgoing"
msgstr "Исходящее"
#: django_mailbox/models.py:475
msgid "Body"
msgstr "Тело"
#: django_mailbox/models.py:479
msgid "Encoded"
msgstr "Закодировано"
#: django_mailbox/models.py:481
msgid "True if the e-mail body is Base64 encoded"
msgstr "True если тело сообщения закодировано в Base64"
#: django_mailbox/models.py:485
msgid "Processed"
msgstr "Обработано"
#: django_mailbox/models.py:490
msgid "Read"
msgstr "Прочитано"
#: django_mailbox/models.py:497
msgid "Raw message contents"
msgstr "Исходное содержимое сообщения"
#: django_mailbox/models.py:500
msgid "Original full content of message"
msgstr "Полное содержимое сообщения"
#: django_mailbox/models.py:716
msgid "E-mail message"
msgstr "Сообщение"
#: django_mailbox/models.py:717
msgid "E-mail messages"
msgstr "Сообщения"
#: django_mailbox/models.py:726
msgid "Message"
msgstr "Сообщение"
#: django_mailbox/models.py:730
msgid "Headers"
msgstr "Заголовки"
#: django_mailbox/models.py:736
msgid "Document"
msgstr "Документ"
#: django_mailbox/models.py:793
msgid "Message attachment"
msgstr "Вложение"
#: django_mailbox/models.py:794
msgid "Message attachments"
msgstr "Вложения"

View file

@ -0,0 +1,30 @@
import logging
from django.core.management.base import BaseCommand
from django_mailbox.models import Mailbox
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class Command(BaseCommand):
def handle(self, *args, **options):
mailboxes = Mailbox.active_mailboxes.all()
if args:
mailboxes = mailboxes.filter(
name=' '.join(args)
)
for mailbox in mailboxes:
logger.info(
'Gathering messages for %s',
mailbox.name
)
messages = mailbox.get_new_mail()
for message in messages:
logger.info(
'Received %s (from %s)',
message.subject,
message.from_address
)

View file

@ -0,0 +1,52 @@
import email
import logging
import sys
try:
from email import utils
except ImportError:
import rfc822 as utils
from django.core.management.base import BaseCommand
from django_mailbox.models import Mailbox
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class Command(BaseCommand):
args = "<[Mailbox Name (optional)]>"
help = "Receive incoming mail via stdin"
def add_arguments(self, parser):
parser.add_argument(
'mailbox_name',
nargs='?',
help="The name of the mailbox that will receive the message"
)
def handle(self, mailbox_name=None, *args, **options):
message = email.message_from_string(sys.stdin.read())
if message:
if mailbox_name:
mailbox = self.get_mailbox_by_name(mailbox_name)
else:
mailbox = self.get_mailbox_for_message(message)
mailbox.process_incoming_message(message)
logger.info(
"Message received from %s",
message['from']
)
else:
logger.warning("Message not processable.")
def get_mailbox_by_name(self, name):
mailbox, created = Mailbox.objects.get_or_create(
name=name,
)
return mailbox
def get_mailbox_for_message(self, message):
email_address = utils.parseaddr(message['to'])[1][0:255]
return self.get_mailbox_by_name(email_address)

View file

@ -0,0 +1,71 @@
import email
import hashlib
import logging
from django.core.management.base import BaseCommand
from django_mailbox.models import MessageAttachment, Message
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class Command(BaseCommand):
""" Briefly, a bug existed in a migration that may have caused message
attachments to become disassociated with their messages. This management
command will read through existing message attachments and attempt to
re-associate them with their original message.
This isn't foolproof, I'm afraid. If an attachment exists twice, it will
be associated only with the most recent e-mail message. That said,
I'm quite sure that the bug in the migration is gone (and you'd have to
have been quite unlucky to have ran the bad migration).
"""
def handle(self, *args, **options):
attachment_hash_map = {}
attachments_without_messages = MessageAttachment.objects.filter(
message=None
).order_by(
'id'
)
if attachments_without_messages.count() < 1:
return
for attachment in attachments_without_messages:
md5 = hashlib.md5()
for chunk in attachment.document.file.chunks():
md5.update(chunk)
attachment_hash_map[md5.hexdigest()] = attachment.pk
for message_record in Message.objects.all().order_by('id'):
message = email.message_from_string(message_record.body)
if message.is_multipart():
for part in message.walk():
if part.get_content_maintype() == 'multipart':
continue
if part.get('Content-Disposition') is None:
continue
md5 = hashlib.md5()
md5.update(part.get_payload(decode=True))
digest = md5.hexdigest()
if digest in attachment_hash_map:
attachment = MessageAttachment.objects.get(
pk=attachment_hash_map[digest]
)
attachment.message = message_record
attachment.save()
logger.info(
"Associated message %s with attachment %s (%s)",
message_record.pk,
attachment.pk,
digest
)
else:
logger.info(
"%s(%s) not found in currently-stored attachments",
part.get_filename(),
digest
)

View file

@ -0,0 +1,56 @@
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Mailbox',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=255, verbose_name='Name')),
('uri', models.CharField(default=None, max_length=255, blank=True, help_text="Example: imap+ssl://myusername:mypassword@someserver <br /><br />Internet transports include 'imap' and 'pop3'; common local file transports include 'maildir', 'mbox', and less commonly 'babyl', 'mh', and 'mmdf'. <br /><br />Be sure to urlencode your username and password should they contain illegal characters (like @, :, etc).", null=True, verbose_name='URI')),
('from_email', models.CharField(default=None, max_length=255, blank=True, help_text="Example: MailBot &lt;mailbot@yourdomain.com&gt;<br />'From' header to set for outgoing email.<br /><br />If you do not use this e-mail inbox for outgoing mail, this setting is unnecessary.<br />If you send e-mail without setting this, your 'From' header will'be set to match the setting `DEFAULT_FROM_EMAIL`.", null=True, verbose_name='From email')),
('active', models.BooleanField(default=True, help_text='Check this e-mail inbox for new e-mail messages during polling cycles. This checkbox does not have an effect upon whether mail is collected here when this mailbox receives mail from a pipe, and does not affect whether e-mail messages can be dispatched from this mailbox. ', verbose_name='Active')),
],
options={
'verbose_name_plural': 'Mailboxes',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('subject', models.CharField(max_length=255, verbose_name='Subject')),
('message_id', models.CharField(max_length=255, verbose_name='Message ID')),
('from_header', models.CharField(max_length=255, verbose_name='From header')),
('to_header', models.TextField(verbose_name='To header')),
('outgoing', models.BooleanField(default=False, verbose_name='Outgoing')),
('body', models.TextField(verbose_name='Body')),
('encoded', models.BooleanField(default=False, help_text='True if the e-mail body is Base64 encoded', verbose_name='Encoded')),
('processed', models.DateTimeField(auto_now_add=True, verbose_name='Processed')),
('read', models.DateTimeField(default=None, null=True, verbose_name='Read', blank=True)),
('in_reply_to', models.ForeignKey(related_name='replies', verbose_name='In reply to', blank=True, to='django_mailbox.Message', null=True, on_delete=models.CASCADE)),
('mailbox', models.ForeignKey(related_name='messages', verbose_name='Mailbox', to='django_mailbox.Mailbox', on_delete=models.CASCADE)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='MessageAttachment',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('headers', models.TextField(null=True, verbose_name='Headers', blank=True)),
('document', models.FileField(upload_to=b'mailbox_attachments/%Y/%m/%d/', verbose_name='Document')),
('message', models.ForeignKey(related_name='attachments', verbose_name='Message', blank=True, to='django_mailbox.Message', null=True, on_delete=models.CASCADE)),
],
options={
},
bases=(models.Model,),
),
]

View file

@ -0,0 +1,17 @@
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('django_mailbox', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='message',
name='eml',
field=models.FileField(help_text='Original full content of message', upload_to=b'messages', null=True, verbose_name='Message as a file'),
preserve_default=True,
),
]

View file

@ -0,0 +1,16 @@
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('django_mailbox', '0002_add_eml_to_message'),
]
operations = [
migrations.AlterField(
model_name='message',
name='eml',
field=models.FileField(help_text='Original full content of message', upload_to=b'messages', null=True, verbose_name='Raw message contents'),
),
]

View file

@ -0,0 +1,21 @@
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('django_mailbox', '0003_auto_20150409_0316'),
]
operations = [
migrations.AlterField(
model_name='message',
name='eml',
field=models.FileField(verbose_name='Raw message contents', upload_to='messages', null=True, help_text='Original full content of message'),
),
migrations.AlterField(
model_name='messageattachment',
name='document',
field=models.FileField(verbose_name='Document', upload_to='mailbox_attachments/%Y/%m/%d/'),
),
]

View file

@ -0,0 +1,17 @@
from django.db import migrations, models
import django_mailbox.utils
class Migration(migrations.Migration):
dependencies = [
('django_mailbox', '0004_bytestring_to_unicode'),
]
operations = [
migrations.AlterField(
model_name='messageattachment',
name='document',
field=models.FileField(upload_to=django_mailbox.utils.get_attachment_save_path, verbose_name='Document'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 1.9.8 on 2016-08-15 22:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_mailbox', '0005_auto_20160523_2240'),
]
operations = [
migrations.AddField(
model_name='mailbox',
name='last_polling',
field=models.DateTimeField(blank=True, 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.', null=True, verbose_name='Last polling'),
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 1.10.7 on 2018-04-21 00:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('django_mailbox', '0006_mailbox_last_polling'),
]
operations = [
migrations.AlterModelOptions(
name='mailbox',
options={'verbose_name': 'Mailbox', 'verbose_name_plural': 'Mailboxes'},
),
migrations.AlterModelOptions(
name='message',
options={'verbose_name': 'E-mail message', 'verbose_name_plural': 'E-mail messages'},
),
migrations.AlterModelOptions(
name='messageattachment',
options={'verbose_name': 'Message attachment', 'verbose_name_plural': 'Message attachments'},
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 2.1.7 on 2019-02-19 14:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_mailbox', '0007_auto_20180421_0026'),
]
operations = [
migrations.AlterField(
model_name='mailbox',
name='active',
field=models.BooleanField(blank=True, default=True, help_text='Check this e-mail inbox for new e-mail messages during polling cycles. This checkbox does not have an effect upon whether mail is collected here when this mailbox receives mail from a pipe, and does not affect whether e-mail messages can be dispatched from this mailbox. ', verbose_name='Active'),
),
migrations.AlterField(
model_name='message',
name='outgoing',
field=models.BooleanField(blank=True, default=False, verbose_name='Outgoing'),
),
]

View file

@ -0,0 +1,855 @@
#!/usr/bin/env python
"""
Models declaration for application ``django_mailbox``.
"""
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
import mimetypes
import os.path
import sys
import uuid
from tempfile import NamedTemporaryFile
import django
from django.conf import settings as django_settings
from django.core.files.base import ContentFile, File
from django.core.mail.message import make_msgid
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils.timezone import now
from django_mailbox import utils
from django_mailbox.signals import message_received
from django_mailbox.transports import Pop3Transport, ImapTransport, \
MaildirTransport, MboxTransport, BabylTransport, MHTransport, \
MMDFTransport, GmailImapTransport, Office365Transport
logger = logging.getLogger(__name__)
class MailboxQuerySet(models.QuerySet):
def get_new_mail(self):
count = 0
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')
class MailboxManager(models.Manager):
def get_queryset(self):
return MailboxQuerySet(self.model, using=self._db)
class ActiveMailboxManager(MailboxManager):
def get_queryset(self):
return super().get_queryset().filter(
active=True,
)
class Mailbox(models.Model):
name = models.CharField(
_('Name'),
max_length=255,
)
uri = models.CharField(
_('URI'),
max_length=255,
help_text=(_(
"Example: imap+ssl://myusername:mypassword@someserver <br />"
"<br />"
"Internet transports include 'imap' and 'pop3'; "
"common local file transports include 'maildir', 'mbox', "
"and less commonly 'babyl', 'mh', and 'mmdf'. <br />"
"<br />"
"Be sure to urlencode your username and password should they "
"contain illegal characters (like @, :, etc)."
)),
blank=True,
null=True,
default=None,
)
from_email = models.CharField(
_('From email'),
max_length=255,
help_text=(_(
"Example: MailBot &lt;mailbot@yourdomain.com&gt;<br />"
"'From' header to set for outgoing email.<br />"
"<br />"
"If you do not use this e-mail inbox for outgoing mail, this "
"setting is unnecessary.<br />"
"If you send e-mail without setting this, your 'From' header will'"
"be set to match the setting `DEFAULT_FROM_EMAIL`."
)),
blank=True,
null=True,
default=None,
)
active = models.BooleanField(
_('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 "
"mail is collected here when this mailbox receives mail from a "
"pipe, and does not affect whether e-mail messages can be "
"dispatched from this mailbox. "
)),
blank=True,
default=True,
)
last_polling = models.DateTimeField(
_("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.")),
blank=True,
null=True
)
objects = MailboxManager()
active_mailboxes = ActiveMailboxManager()
@property
def _protocol_info(self):
return urlparse(self.uri)
@property
def _query_string(self):
return parse_qs(self._protocol_info.query)
@property
def _domain(self):
return self._protocol_info.hostname
@property
def port(self):
"""Returns the port to use for fetching messages."""
return self._protocol_info.port
@property
def username(self):
"""Returns the username to use for fetching messages."""
return unquote(self._protocol_info.username)
@property
def password(self):
"""Returns the password to use for fetching messages."""
return unquote(self._protocol_info.password)
@property
def location(self):
"""Returns the location (domain and path) of messages."""
return self._domain if self._domain else '' + self._protocol_info.path
@property
def type(self):
"""Returns the 'transport' name for this mailbox."""
scheme = self._protocol_info.scheme.lower()
if '+' in scheme:
return scheme.split('+')[0]
return scheme
@property
def use_ssl(self):
"""Returns whether or not this mailbox's connection uses SSL."""
return '+ssl' in self._protocol_info.scheme.lower()
@property
def use_tls(self):
"""Returns whether or not this mailbox's connection uses STARTTLS."""
return '+tls' in self._protocol_info.scheme.lower()
@property
def archive(self):
"""Returns (if specified) the folder to archive messages to."""
archive_folder = self._query_string.get('archive', None)
if not archive_folder:
return None
return archive_folder[0]
@property
def folder(self):
"""Returns (if specified) the folder to fetch mail from."""
folder = self._query_string.get('folder', None)
if not folder:
return None
return folder[0]
@property
def client_id(self):
"""Returns (if specified) the client id for Office365."""
client_id = self._query_string.get('client_id', None)
if not client_id:
return None
return client_id[0]
@property
def client_secret(self):
"""Returns (if specified) the client secret for Office365."""
client_secret = self._query_string.get('client_secret', None)
if not client_secret:
return None
return client_secret[0]
@property
def tenant_id(self):
"""Returns (if specified) the tenant id for Office365."""
tenant_id = self._query_string.get('tentant_id', None)
if not tenant_id:
return None
return tenant_id[0]
def get_connection(self):
"""Returns the transport instance for this mailbox.
These will always be instances of
`django_mailbox.transports.base.EmailTransport`.
"""
if not self.uri:
return None
elif self.type == 'imap':
conn = ImapTransport(
self.location,
port=self.port if self.port else None,
ssl=self.use_ssl,
tls=self.use_tls,
archive=self.archive,
folder=self.folder
)
conn.connect(self.username, self.password)
elif self.type == 'gmail':
conn = GmailImapTransport(
self.location,
port=self.port if self.port else None,
ssl=True,
archive=self.archive
)
conn.connect(self.username, self.password)
elif self.type == 'pop3':
conn = Pop3Transport(
self.location,
port=self.port if self.port else None,
ssl=self.use_ssl
)
conn.connect(self.username, self.password)
elif self.type == 'office365':
conn = Office365Transport(
port=self.port if self.port else None,
ssl=True,
archive=self.archive,
client_id=self.client_id,
client_secret=self.client_secret,
tenant_id=self.tenant_id
)
conn.connect(self.username, self.password)
elif self.type == 'maildir':
conn = MaildirTransport(self.location)
elif self.type == 'mbox':
conn = MboxTransport(self.location)
elif self.type == 'babyl':
conn = BabylTransport(self.location)
elif self.type == 'mh':
conn = MHTransport(self.location)
elif self.type == 'mmdf':
conn = MMDFTransport(self.location)
return conn
def process_incoming_message(self, message):
"""Process a message incoming to this mailbox."""
msg = self._process_message(message)
if msg is None:
return None
msg.outgoing = False
msg.save()
message_received.send(sender=self, message=msg)
return msg
def record_outgoing_message(self, message):
"""Record an outgoing message associated with this mailbox."""
msg = self._process_message(message)
if msg is None:
return None
msg.outgoing = True
msg.save()
return msg
def _get_dehydrated_message(self, msg, record):
settings = utils.get_settings()
new = EmailMessage()
if msg.is_multipart():
for header, value in msg.items():
new[header] = value
for part in msg.get_payload():
new.attach(
self._get_dehydrated_message(part, record)
)
elif (
settings['strip_unallowed_mimetypes']
and not msg.get_content_type() in settings['allowed_mimetypes']
):
for header, value in msg.items():
new[header] = value
# Delete header, otherwise when attempting to deserialize the
# payload, it will be expecting a body for this.
del new['Content-Transfer-Encoding']
new[settings['altered_message_header']] = (
'Stripped; Content type %s not allowed' % (
msg.get_content_type()
)
)
new.set_payload('')
elif (
(
msg.get_content_type() not in settings['text_stored_mimetypes']
) or
('attachment' in msg.get('Content-Disposition', ''))
):
filename = None
raw_filename = msg.get_filename()
if raw_filename:
filename = utils.convert_header_to_unicode(raw_filename)
if not filename:
extension = mimetypes.guess_extension(msg.get_content_type())
else:
_, extension = os.path.splitext(filename)
if not extension:
extension = '.bin'
attachment = MessageAttachment()
attachment.document.save(
uuid.uuid4().hex + extension,
ContentFile(
BytesIO(
msg.get_payload(decode=True)
).getvalue()
)
)
attachment.message = record
for key, value in msg.items():
attachment[key] = value
attachment.save()
placeholder = EmailMessage()
placeholder[
settings['attachment_interpolation_header']
] = str(attachment.pk)
new = placeholder
else:
content_charset = msg.get_content_charset()
if not content_charset:
content_charset = 'ascii'
try:
# Make sure that the payload can be properly decoded in the
# defined charset, if it can't, let's mash some things
# inside the payload :-\
msg.get_payload(decode=True).decode(content_charset)
except LookupError:
logger.warning(
"Unknown encoding %s; interpreting as ASCII!",
content_charset
)
msg.set_payload(
msg.get_payload(decode=True).decode(
'ascii',
'ignore'
)
)
except ValueError:
logger.warning(
"Decoding error encountered; interpreting %s as ASCII!",
content_charset
)
msg.set_payload(
msg.get_payload(decode=True).decode(
'ascii',
'ignore'
)
)
new = msg
return new
def _process_message(self, message):
msg = Message()
msg._email_object = message
settings = utils.get_settings()
if settings['store_original_message']:
self._process_save_original_message(message, msg)
msg.mailbox = self
if 'subject' in message:
msg.subject = (
utils.convert_header_to_unicode(message['subject'])[0:255]
)
if 'message-id' in message:
msg.message_id = message['message-id'][0:255].strip()
if 'from' in message:
msg.from_header = utils.convert_header_to_unicode(message['from'])
if 'to' in message:
msg.to_header = utils.convert_header_to_unicode(message['to'])
elif 'Delivered-To' in message:
msg.to_header = utils.convert_header_to_unicode(
message['Delivered-To']
)
msg.save()
message = self._get_dehydrated_message(message, msg)
try:
body = message.as_string()
except KeyError as exc:
# email.message.replace_header may raise 'KeyError' if the header
# 'content-transfer-encoding' is missing
logger.warning("Failed to parse message: %s", exc,)
return None
msg.set_body(body)
if message['in-reply-to']:
try:
msg.in_reply_to = Message.objects.filter(
message_id=message['in-reply-to'].strip()
)[0]
except IndexError:
pass
msg.save()
return msg
def _process_save_original_message(self, message, msg):
settings = utils.get_settings()
if settings['compress_original_message']:
with NamedTemporaryFile(suffix=".eml.gz") as fp_tmp:
with gzip.GzipFile(fileobj=fp_tmp, mode="w") as fp:
fp.write(message.as_string().encode('utf-8'))
msg.eml.save(
"{}.eml.gz".format(uuid.uuid4()),
File(fp_tmp),
save=False
)
else:
msg.eml.save(
'%s.eml' % uuid.uuid4(),
ContentFile(message.as_string()),
save=False
)
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:
yield msg
self.last_polling = now()
if django.VERSION >= (1, 5): # Django 1.5 introduces update_fields
self.save(update_fields=['last_polling'])
else:
self.save()
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(
outgoing=False,
)
class OutgoingMessageManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(
outgoing=True,
)
class UnreadMessageManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(
read=None
)
class Message(models.Model):
mailbox = models.ForeignKey(
Mailbox,
related_name='messages',
verbose_name=_('Mailbox'),
on_delete=models.CASCADE
)
subject = models.CharField(
_('Subject'),
max_length=255
)
message_id = models.CharField(
_('Message ID'),
max_length=255
)
in_reply_to = models.ForeignKey(
'django_mailbox.Message',
related_name='replies',
blank=True,
null=True,
verbose_name=_('In reply to'),
on_delete=models.CASCADE
)
from_header = models.CharField(
_('From header'),
max_length=255,
)
to_header = models.TextField(
_('To header'),
)
outgoing = models.BooleanField(
_('Outgoing'),
default=False,
blank=True,
)
body = models.TextField(
_('Body'),
)
encoded = models.BooleanField(
_('Encoded'),
default=False,
help_text=_('True if the e-mail body is Base64 encoded'),
)
processed = models.DateTimeField(
_('Processed'),
auto_now_add=True
)
read = models.DateTimeField(
_('Read'),
default=None,
blank=True,
null=True,
)
eml = models.FileField(
_('Raw message contents'),
null=True,
upload_to="messages",
help_text=_('Original full content of message')
)
objects = models.Manager()
unread_messages = UnreadMessageManager()
incoming_messages = IncomingMessageManager()
outgoing_messages = OutgoingMessageManager()
@property
def address(self):
"""Property allowing one to get the relevant address(es).
In earlier versions of this library, the model had an `address` field
storing the e-mail address from which a message was received. During
later refactorings, it became clear that perhaps storing sent messages
would also be useful, so the address field was replaced with two
separate fields.
"""
addresses = []
addresses = self.to_addresses + self.from_address
return addresses
@property
def from_address(self):
"""Returns the address (as a list) from which this message was received
.. note::
This was once (and probably should be) a string rather than a list,
but in a pull request received long, long ago it was changed;
presumably to make the interface identical to that of
`to_addresses`.
"""
if self.from_header:
return [parseaddr(self.from_header)[1].lower()]
else:
return []
@property
def to_addresses(self):
"""Returns a list of addresses to which this message was sent."""
addresses = []
for address in self.to_header.split(','):
if address:
addresses.append(
parseaddr(
address
)[1].lower()
)
return addresses
def reply(self, message):
"""Sends a message as a reply to this message instance.
Although Django's e-mail processing will set both Message-ID
and Date upon generating the e-mail message, we will not be able
to retrieve that information through normal channels, so we must
pre-set it.
"""
if not message.from_email:
if self.mailbox.from_email:
message.from_email = self.mailbox.from_email
else:
message.from_email = django_settings.DEFAULT_FROM_EMAIL
message.extra_headers['Message-ID'] = make_msgid()
message.extra_headers['Date'] = formatdate()
message.extra_headers['In-Reply-To'] = self.message_id.strip()
message.send()
return self.mailbox.record_outgoing_message(
email.message_from_string(
message.message().as_string()
)
)
@property
def text(self):
"""
Returns the message body matching content type 'text/plain'.
"""
return utils.get_body_from_message(
self.get_email_object(), 'text', 'plain'
).replace('=\n', '').strip()
@property
def html(self):
"""
Returns the message body matching content type 'text/html'.
"""
return utils.get_body_from_message(
self.get_email_object(), 'text', 'html'
).replace('\n', '').strip()
def _rehydrate(self, msg):
new = EmailMessage()
settings = utils.get_settings()
if msg.is_multipart():
for header, value in msg.items():
new[header] = value
for part in msg.get_payload():
new.attach(
self._rehydrate(part)
)
elif settings['attachment_interpolation_header'] in msg.keys():
try:
attachment = MessageAttachment.objects.get(
pk=msg[settings['attachment_interpolation_header']]
)
for header, value in attachment.items():
new[header] = value
encoding = new['Content-Transfer-Encoding']
if encoding and encoding.lower() == 'quoted-printable':
# Cannot use `email.encoders.encode_quopri due to
# bug 14360: http://bugs.python.org/issue14360
output = BytesIO()
encode_quopri(
BytesIO(
attachment.document.read()
),
output,
quotetabs=True,
header=False,
)
new.set_payload(
output.getvalue().decode().replace(' ', '=20')
)
del new['Content-Transfer-Encoding']
new['Content-Transfer-Encoding'] = 'quoted-printable'
else:
new.set_payload(
attachment.document.read()
)
del new['Content-Transfer-Encoding']
encode_base64(new)
except MessageAttachment.DoesNotExist:
new[settings['altered_message_header']] = (
'Missing; Attachment %s not found' % (
msg[settings['attachment_interpolation_header']]
)
)
new.set_payload('')
else:
for header, value in msg.items():
new[header] = value
new.set_payload(
msg.get_payload()
)
return new
def get_body(self):
"""Returns the `body` field of this record.
This will automatically base64-decode the message contents
if they are encoded as such.
"""
if self.encoded:
return base64.b64decode(self.body.encode('ascii'))
return self.body.encode('utf-8')
def set_body(self, body):
"""Set the `body` field of this record.
This will automatically base64-encode the message contents to
circumvent a limitation in earlier versions of Django in which
no fields existed for storing arbitrary bytes.
"""
self.encoded = True
self.body = base64.b64encode(body.encode('utf-8')).decode('ascii')
def get_email_object(self):
"""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
and properties are available on `email.message.EmailMessage` instances.
.. note::
Depending upon the storage methods in use (specifically --
whether ``DJANGO_MAILBOX_STORE_ORIGINAL_MESSAGE`` is set
to ``True``, this may either create a "rehydrated" message
using stored attachments, or read the message contents stored
on-disk.
.. [email.message.EmailMessage] Python's `email.message.EmailMessage` docs
(https://docs.python.org/3/library/email.message.html)
"""
if not hasattr(self, '_email_object'): # Cache fill
if self.eml:
if self.eml.name.endswith('.gz'):
body = gzip.GzipFile(fileobj=self.eml).read()
else:
self.eml.open()
body = self.eml.file.read()
self.eml.close()
else:
body = self.get_body()
flat = email.message_from_bytes(body)
self._email_object = self._rehydrate(flat)
return self._email_object
def delete(self, *args, **kwargs):
"""Delete this message and all stored attachments."""
for attachment in self.attachments.all():
# This attachment is attached only to this message.
attachment.delete()
return super().delete(*args, **kwargs)
def __str__(self):
return self.subject
class Meta:
verbose_name = _('E-mail message')
verbose_name_plural = _('E-mail messages')
class MessageAttachment(models.Model):
message = models.ForeignKey(
Message,
related_name='attachments',
null=True,
blank=True,
verbose_name=_('Message'),
on_delete=models.CASCADE
)
headers = models.TextField(
_('Headers'),
null=True,
blank=True,
)
document = models.FileField(
_('Document'),
upload_to=utils.get_attachment_save_path,
)
def delete(self, *args, **kwargs):
"""Deletes the attachment."""
self.document.delete()
return super().delete(*args, **kwargs)
def _get_rehydrated_headers(self):
headers = self.headers
if headers is None:
return EmailMessage()
return email.message_from_string(headers)
def _set_dehydrated_headers(self, email_object):
self.headers = email_object.as_string()
def __delitem__(self, name):
rehydrated = self._get_rehydrated_headers()
del rehydrated[name]
self._set_dehydrated_headers(rehydrated)
def __setitem__(self, name, value):
rehydrated = self._get_rehydrated_headers()
rehydrated[name] = value
self._set_dehydrated_headers(rehydrated)
def get_filename(self):
"""Returns the original filename of this attachment."""
file_name = self._get_rehydrated_headers().get_filename()
if isinstance(file_name, str):
result = utils.convert_header_to_unicode(file_name)
if result is None:
return file_name
return result
else:
return None
def items(self):
return self._get_rehydrated_headers().items()
def __getitem__(self, name):
value = self._get_rehydrated_headers()[name]
if value is None:
raise KeyError('Header %s does not exist' % name)
return value
def __str__(self):
return self.document.url
class Meta:
verbose_name = _('Message attachment')
verbose_name_plural = _('Message attachments')

View file

@ -0,0 +1,3 @@
from django.dispatch.dispatcher import Signal
message_received = Signal(providing_args=['message'])

View file

@ -0,0 +1,58 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Mailbox'
db.create_table('django_mailbox_mailbox', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
('uri', self.gf('django.db.models.fields.CharField')(max_length=255)),
))
db.send_create_signal('django_mailbox', ['Mailbox'])
# Adding model 'Message'
db.create_table('django_mailbox_message', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('mailbox', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['django_mailbox.Mailbox'])),
('subject', self.gf('django.db.models.fields.CharField')(max_length=255)),
('message_id', self.gf('django.db.models.fields.CharField')(max_length=255)),
('from_address', self.gf('django.db.models.fields.CharField')(max_length=255)),
('body', self.gf('django.db.models.fields.TextField')()),
('received', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
))
db.send_create_signal('django_mailbox', ['Message'])
def backwards(self, orm):
# Deleting model 'Mailbox'
db.delete_table('django_mailbox_mailbox')
# Deleting model 'Message'
db.delete_table('django_mailbox_message')
models = {
'django_mailbox.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'uri': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'body': ('django.db.models.fields.TextField', [], {}),
'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['django_mailbox.Mailbox']"}),
'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,38 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Changing field 'Mailbox.uri'
db.alter_column('django_mailbox_mailbox', 'uri', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
def backwards(self, orm):
# User chose to not deal with backwards NULL issues for 'Mailbox.uri'
raise RuntimeError("Cannot reverse this migration. 'Mailbox.uri' and its values cannot be restored.")
models = {
'django_mailbox.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'body': ('django.db.models.fields.TextField', [], {}),
'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'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'}),
'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,41 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Mailbox.active'
db.add_column('django_mailbox_mailbox', 'active',
self.gf('django.db.models.fields.BooleanField')(default=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Mailbox.active'
db.delete_column('django_mailbox_mailbox', 'active')
models = {
'django_mailbox.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'active': ('django.db.models.fields.BooleanField', [], {'default': '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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'body': ('django.db.models.fields.TextField', [], {}),
'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'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'}),
'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,42 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Message.outgoing'
db.add_column('django_mailbox_message', 'outgoing',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Message.outgoing'
db.delete_column('django_mailbox_message', 'outgoing')
models = {
'django_mailbox.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'active': ('django.db.models.fields.BooleanField', [], {'default': '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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'body': ('django.db.models.fields.TextField', [], {}),
'from_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'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'}),
'received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,38 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
db.rename_column('django_mailbox_message', 'from_address', 'address')
db.rename_column('django_mailbox_message', 'received', 'processed')
def backwards(self, orm):
db.rename_column('django_mailbox_message', 'address', 'from_address')
db.rename_column('django_mailbox_message', 'processed', 'received')
models = {
'django_mailbox.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'active': ('django.db.models.fields.BooleanField', [], {'default': '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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'address': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'body': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'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'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,55 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Message.in_reply_to'
db.add_column('django_mailbox_message', 'in_reply_to',
self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='replies', null=True, to=orm['django_mailbox.Message']),
keep_default=False)
# Adding M2M table for field references on 'Message'
db.create_table('django_mailbox_message_references', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('from_message', models.ForeignKey(orm['django_mailbox.message'], null=False)),
('to_message', models.ForeignKey(orm['django_mailbox.message'], null=False))
))
db.create_unique('django_mailbox_message_references', ['from_message_id', 'to_message_id'])
def backwards(self, orm):
# Deleting field 'Message.in_reply_to'
db.delete_column('django_mailbox_message', 'in_reply_to_id')
# Removing M2M table for field references on 'Message'
db.delete_table('django_mailbox_message_references')
models = {
'django_mailbox.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'active': ('django.db.models.fields.BooleanField', [], {'default': '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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'address': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'body': ('django.db.models.fields.TextField', [], {}),
'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'}),
'references': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'referenced_by'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['django_mailbox.Message']"}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,59 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Deleting field 'Message.address'
db.delete_column('django_mailbox_message', 'address')
# Adding field 'Message.from_header'
db.add_column('django_mailbox_message', 'from_header',
self.gf('django.db.models.fields.CharField')(default='', max_length=255),
keep_default=False)
# Adding field 'Message.to_header'
db.add_column('django_mailbox_message', 'to_header',
self.gf('django.db.models.fields.TextField')(default=''),
keep_default=False)
def backwards(self, orm):
# User chose to not deal with backwards NULL issues for 'Message.address'
raise RuntimeError("Cannot reverse this migration. 'Message.address' and its values cannot be restored.")
# Deleting field 'Message.from_header'
db.delete_column('django_mailbox_message', 'from_header')
# Deleting field 'Message.to_header'
db.delete_column('django_mailbox_message', 'to_header')
models = {
'django_mailbox.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'active': ('django.db.models.fields.BooleanField', [], {'default': '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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'body': ('django.db.models.fields.TextField', [], {}),
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'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'}),
'references': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'referenced_by'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['django_mailbox.Message']"}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,46 @@
import datetime
import email
from south.db import db
from south.v2 import DataMigration
from django.db import models
class Migration(DataMigration):
def forwards(self, orm):
for message in orm['django_mailbox.message'].objects.all():
msg_object = email.message_from_string(
message.body
)
message.from_header = msg_object['from']
message.to_header = msg_object['to']
message.save()
def backwards(self, orm):
raise RuntimeError('Cannot reverse this migration.')
models = {
'django_mailbox.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'active': ('django.db.models.fields.BooleanField', [], {'default': '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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'body': ('django.db.models.fields.TextField', [], {}),
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'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'}),
'references': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'referenced_by'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['django_mailbox.Message']"}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
}
}
complete_apps = ['django_mailbox']
symmetrical = True

View file

@ -0,0 +1,47 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Removing M2M table for field references on 'Message'
db.delete_table('django_mailbox_message_references')
def backwards(self, orm):
# Adding M2M table for field references on 'Message'
db.create_table('django_mailbox_message_references', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('from_message', models.ForeignKey(orm['django_mailbox.message'], null=False)),
('to_message', models.ForeignKey(orm['django_mailbox.message'], null=False))
))
db.create_unique('django_mailbox_message_references', ['from_message_id', 'to_message_id'])
models = {
'django_mailbox.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'active': ('django.db.models.fields.BooleanField', [], {'default': '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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'body': ('django.db.models.fields.TextField', [], {}),
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'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'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,45 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Mailbox.from_email'
db.add_column('django_mailbox_mailbox', 'from_email',
self.gf('django.db.models.fields.CharField')(default=None, max_length=255, null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Mailbox.from_email'
db.delete_column('django_mailbox_mailbox', 'from_email')
models = {
'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'}),
'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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'body': ('django.db.models.fields.TextField', [], {}),
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'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'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,46 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Message.read'
db.add_column('django_mailbox_message', 'read',
self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Message.read'
db.delete_column('django_mailbox_message', 'read')
models = {
'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'}),
'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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'body': ('django.db.models.fields.TextField', [], {}),
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'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'}),
'read': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,66 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'MessageAttachment'
db.create_table('django_mailbox_messageattachment', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('document', self.gf('django.db.models.fields.files.FileField')(max_length=100)),
))
db.send_create_signal('django_mailbox', ['MessageAttachment'])
# Adding M2M table for field attachments on 'Message'
db.create_table('django_mailbox_message_attachments', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('message', models.ForeignKey(orm['django_mailbox.message'], null=False)),
('messageattachment', models.ForeignKey(orm['django_mailbox.messageattachment'], null=False))
))
db.create_unique('django_mailbox_message_attachments', ['message_id', 'messageattachment_id'])
def backwards(self, orm):
# Deleting model 'MessageAttachment'
db.delete_table('django_mailbox_messageattachment')
# Removing M2M table for field attachments on 'Message'
db.delete_table('django_mailbox_message_attachments')
models = {
'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'}),
'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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['django_mailbox.MessageAttachment']", 'symmetrical': 'False'}),
'body': ('django.db.models.fields.TextField', [], {}),
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'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'}),
'read': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
},
'django_mailbox.messageattachment': {
'Meta': {'object_name': 'MessageAttachment'},
'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,53 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field '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('django_mailbox_messageattachment', 'message_id')
models = {
'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'}),
'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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'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'}),
'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'}),
'read': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
},
'django_mailbox.messageattachment': {
'Meta': {'object_name': 'MessageAttachment'},
'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
'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']"})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,54 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
no_dry_run = True
def forwards(self, orm):
for message in orm['django_mailbox.Message'].objects.all():
for attachment in message.attachments.all():
attachment.message = message
attachment.save()
def backwards(self, orm):
for attachment in orm['django_mailbox.MessageAttachment'].objects.all():
if attachment.message:
attachment.message.attachments.add(
attachment
)
models = {
'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'}),
'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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'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'}),
'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'}),
'read': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
},
'django_mailbox.messageattachment': {
'Meta': {'object_name': 'MessageAttachment'},
'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
'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']"})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,64 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Removing M2M table for field attachments on 'Message'
db.delete_table('django_mailbox_message_attachments')
# Adding field '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('django_mailbox_message_attachments', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('message', models.ForeignKey(orm['django_mailbox.message'], null=False)),
('messageattachment', models.ForeignKey(orm['django_mailbox.messageattachment'], null=False))
))
db.create_unique('django_mailbox_message_attachments', ['message_id', 'messageattachment_id'])
# Deleting field 'MessageAttachment.headers'
db.delete_column('django_mailbox_messageattachment', 'headers')
models = {
'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'}),
'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'})
},
'django_mailbox.message': {
'Meta': {'object_name': 'Message'},
'body': ('django.db.models.fields.TextField', [], {}),
'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'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'}),
'read': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
},
'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'}),
'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']"})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,54 @@
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field '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('django_mailbox_message', 'encoded')
models = {
'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'}),
'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'})
},
'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'}),
'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'}),
'read': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
},
'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'}),
'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']"})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,55 @@
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field '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('django_mailbox_message', 'eml')
models = {
'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'}),
'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'})
},
'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'}),
'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'}),
'read': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'to_header': ('django.db.models.fields.TextField', [], {})
},
'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'}),
'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']"})
}
}
complete_apps = ['django_mailbox']

View file

@ -0,0 +1,191 @@
import email
import os.path
import time
from django.conf import settings
from django.test import TestCase
from django_mailbox import models, utils
from django_mailbox.models import Mailbox, Message
class EmailIntegrationTimeout(Exception):
pass
def get_email_as_text(name):
with open(
os.path.join(
os.path.dirname(__file__),
'messages',
name,
),
'rb'
) as f:
return f.read()
class EmailMessageTestCase(TestCase):
ALLOWED_EXTRA_HEADERS = [
'MIME-Version',
'Content-Transfer-Encoding',
]
def setUp(self):
dm_settings = utils.get_settings()
self._ALLOWED_MIMETYPES = dm_settings['allowed_mimetypes']
self._STRIP_UNALLOWED_MIMETYPES = (
dm_settings['strip_unallowed_mimetypes']
)
self._TEXT_STORED_MIMETYPES = dm_settings['text_stored_mimetypes']
self.mailbox = Mailbox.objects.create(from_email='from@example.com')
self.test_account = os.environ.get('EMAIL_ACCOUNT')
self.test_password = os.environ.get('EMAIL_PASSWORD')
self.test_smtp_server = os.environ.get('EMAIL_SMTP_SERVER')
self.test_from_email = 'nobody@nowhere.com'
self.maximum_wait_seconds = 60 * 5
settings.EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
settings.EMAIL_HOST = self.test_smtp_server
settings.EMAIL_PORT = 587
settings.EMAIL_HOST_USER = self.test_account
settings.EMAIL_HOST_PASSWORD = self.test_password
settings.EMAIL_USE_TLS = True
super().setUp()
def _get_new_messages(self, mailbox, condition=None):
start_time = time.time()
# wait until there is at least one message
while time.time() - start_time < self.maximum_wait_seconds:
messages = self.mailbox.get_new_mail(condition)
try:
# check if generator contains at least one element
message = next(messages)
yield message
yield from messages
return
except StopIteration:
time.sleep(5)
raise EmailIntegrationTimeout()
def _get_email_as_text(self, name):
with open(
os.path.join(
os.path.dirname(__file__),
'messages',
name,
),
'rb'
) as f:
return f.read()
def _get_email_object(self, name):
copy = self._get_email_as_text(name)
return email.message_from_bytes(copy)
def _headers_identical(self, left, right, header=None):
""" Check if headers are (close enough to) identical.
* This is particularly tricky because Python 2.6, Python 2.7 and
Python 3 each handle header strings slightly differently. This
should mash away all of the differences, though.
* This also has a small loophole in that when re-writing e-mail
payload encodings, we re-build the Content-Type header, so if the
header was originally unquoted, it will be quoted when rehydrating
the e-mail message.
"""
if header.lower() == 'content-type':
# Special case; given that we re-write the header, we'll be quoting
# the new content type; we need to make sure that doesn't cause
# this comparison to fail. Also, the case of the encoding could
# be changed, etc. etc. etc.
left = left.replace('"', '').upper()
right = right.replace('"', '').upper()
left = left.replace('\n\t', ' ').replace('\n ', ' ')
right = right.replace('\n\t', ' ').replace('\n ', ' ')
if right != left:
return False
return True
def compare_email_objects(self, left, right):
# Compare headers
for key, value in left.items():
if not right[key] and key in self.ALLOWED_EXTRA_HEADERS:
continue
if not right[key]:
raise AssertionError("Extra header '%s'" % key)
if not self._headers_identical(right[key], value, header=key):
raise AssertionError(
"Header '{}' unequal:\n{}\n{}".format(
key,
repr(value),
repr(right[key]),
)
)
for key, value in right.items():
if not left[key] and key in self.ALLOWED_EXTRA_HEADERS:
continue
if not left[key]:
raise AssertionError("Extra header '%s'" % key)
if not self._headers_identical(left[key], value, header=key):
raise AssertionError(
"Header '{}' unequal:\n{}\n{}".format(
key,
repr(value),
repr(right[key]),
)
)
if left.is_multipart() != right.is_multipart():
self._raise_mismatched(left, right)
if left.is_multipart():
left_payloads = left.get_payload()
right_payloads = right.get_payload()
if len(left_payloads) != len(right_payloads):
self._raise_mismatched(left, right)
for n in range(len(left_payloads)):
self.compare_email_objects(
left_payloads[n],
right_payloads[n]
)
else:
if left.get_payload() is None or right.get_payload() is None:
if left.get_payload() is None:
if right.get_payload is not None:
self._raise_mismatched(left, right)
if right.get_payload() is None:
if left.get_payload is not None:
self._raise_mismatched(left, right)
elif left.get_payload().strip() != right.get_payload().strip():
self._raise_mismatched(left, right)
def _raise_mismatched(self, left, right):
raise AssertionError(
"Message payloads do not match:\n{}\n{}".format(
left.as_string(),
right.as_string()
)
)
def assertEqual(self, left, right): # noqa: N802
if not isinstance(left, email.message.Message):
return super().assertEqual(left, right)
return self.compare_email_objects(left, right)
def tearDown(self):
for message in Message.objects.all():
message.delete()
models.ALLOWED_MIMETYPES = self._ALLOWED_MIMETYPES
models.STRIP_UNALLOWED_MIMETYPES = self._STRIP_UNALLOWED_MIMETYPES
models.TEXT_STORED_MIMETYPES = self._TEXT_STORED_MIMETYPES
self.mailbox.delete()
super().tearDown()

View file

@ -0,0 +1,12 @@
DATABASES = {
'default': {
'NAME': 'db.sqlite3',
'ENGINE': 'django.db.backends.sqlite3',
},
}
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django_mailbox',
]
SECRET_KEY = 'beepboop'

View file

@ -0,0 +1,70 @@
import os
import uuid
from urllib import parse
from django.core.mail import EmailMultiAlternatives
from django_mailbox.models import Mailbox
from django_mailbox.tests.base import EmailMessageTestCase
__all__ = ['TestImap']
class TestImap(EmailMessageTestCase):
def setUp(self):
super().setUp()
self.test_imap_server = (
os.environ.get('EMAIL_IMAP_SERVER')
)
required_settings = [
self.test_imap_server,
self.test_account,
self.test_password,
self.test_smtp_server,
self.test_from_email,
]
if not all(required_settings):
self.skipTest(
"Integration tests are not available without having "
"the the following environment variables set: "
"EMAIL_ACCOUNT, EMAIL_PASSWORD, EMAIL_SMTP_SERVER, "
"EMAIL_IMAP_SERVER."
)
self.mailbox = Mailbox.objects.create(
name='Integration Test Imap',
uri=self.get_connection_string()
)
self.arbitrary_identifier = str(uuid.uuid4())
def get_connection_string(self):
return "imap+ssl://{account}:{password}@{server}".format(
account=parse.quote(self.test_account),
password=parse.quote(self.test_password),
server=self.test_imap_server,
)
def test_get_imap_message(self):
text_content = 'This is some content'
msg = EmailMultiAlternatives(
self.arbitrary_identifier,
text_content,
self.test_from_email,
[
self.test_account,
]
)
msg.send()
messages = self._get_new_messages(
self.mailbox,
condition=lambda m: m['subject'] == self.arbitrary_identifier
)
message = next(messages)
self.assertEqual(message.subject, self.arbitrary_identifier)
self.assertEqual(message.text, text_content)
self.assertEqual(0, len(list(messages)))

View file

@ -0,0 +1,46 @@
import os
from django.test import TestCase
from django_mailbox.models import Mailbox
__all__ = ['TestMailbox']
class TestMailbox(TestCase):
def test_protocol_info(self):
mailbox = Mailbox()
mailbox.uri = 'alpha://test.com'
expected_protocol = 'alpha'
actual_protocol = mailbox._protocol_info.scheme
self.assertEqual(
expected_protocol,
actual_protocol,
)
def test_last_polling_field_exists(self):
mailbox = Mailbox()
self.assertTrue(hasattr(mailbox, 'last_polling'))
def test_get_new_mail_update_last_polling(self):
mailbox = Mailbox.objects.create(uri="mbox://" + os.path.join(
os.path.dirname(__file__),
'messages',
'generic_message.eml',
))
self.assertEqual(mailbox.last_polling, None)
list(mailbox.get_new_mail())
self.assertNotEqual(mailbox.last_polling, None)
def test_queryset_get_new_mail(self):
mailbox = Mailbox.objects.create(uri="mbox://" + os.path.join(
os.path.dirname(__file__),
'messages',
'generic_message.eml',
))
Mailbox.objects.filter(pk=mailbox.pk).get_new_mail()
mailbox.refresh_from_db()
self.assertNotEqual(mailbox.last_polling, None)

View file

@ -0,0 +1,116 @@
import copy
from unittest import mock
from django_mailbox import models, utils
from django_mailbox.models import Message
from django_mailbox.tests.base import EmailMessageTestCase
__all__ = ['TestMessageFlattening']
class TestMessageFlattening(EmailMessageTestCase):
def test_quopri_message_is_properly_rehydrated(self):
incoming_email_object = self._get_email_object(
'message_with_many_multiparts.eml',
)
# Note: this is identical to the above, but it appears that
# while reading-in an e-mail message, we do alter it slightly
expected_email_object = self._get_email_object(
'message_with_many_multiparts.eml',
)
models.TEXT_STORED_MIMETYPES = ['text/plain']
msg = self.mailbox.process_incoming_message(incoming_email_object)
actual_email_object = msg.get_email_object()
self.assertEqual(
actual_email_object,
expected_email_object,
)
def test_base64_message_is_properly_rehydrated(self):
incoming_email_object = self._get_email_object(
'message_with_attachment.eml',
)
# Note: this is identical to the above, but it appears that
# while reading-in an e-mail message, we do alter it slightly
expected_email_object = self._get_email_object(
'message_with_attachment.eml',
)
msg = self.mailbox.process_incoming_message(incoming_email_object)
actual_email_object = msg.get_email_object()
self.assertEqual(
actual_email_object,
expected_email_object,
)
def test_message_handles_rehydration_problems(self):
incoming_email_object = self._get_email_object(
'message_with_defective_attachment_association.eml',
)
expected_email_object = self._get_email_object(
'message_with_defective_attachment_association_result.eml',
)
# Note: this is identical to the above, but it appears that
# while reading-in an e-mail message, we do alter it slightly
message = Message()
message.body = incoming_email_object.as_string()
msg = self.mailbox.process_incoming_message(incoming_email_object)
del msg._email_object # Cache flush
actual_email_object = msg.get_email_object()
self.assertEqual(
actual_email_object,
expected_email_object,
)
def test_message_content_type_stripping(self):
incoming_email_object = self._get_email_object(
'message_with_many_multiparts.eml',
)
expected_email_object = self._get_email_object(
'message_with_many_multiparts_stripped_html.eml',
)
default_settings = utils.get_settings()
with mock.patch('django_mailbox.utils.get_settings') as get_settings:
altered = copy.deepcopy(default_settings)
altered['strip_unallowed_mimetypes'] = True
altered['allowed_mimetypes'] = ['text/plain']
get_settings.return_value = altered
msg = self.mailbox.process_incoming_message(incoming_email_object)
del msg._email_object # Cache flush
actual_email_object = msg.get_email_object()
self.assertEqual(
actual_email_object,
expected_email_object,
)
def test_message_processing_unknown_encoding(self):
incoming_email_object = self._get_email_object(
'message_with_invalid_encoding.eml',
)
msg = self.mailbox.process_incoming_message(incoming_email_object)
expected_text = (
"We offer loans to private individuals and corporate "
"organizations at 2% interest rate. Interested serious "
"applicants should apply via email with details of their "
"requirements.\n\nWarm Regards,\nLoan Team"
)
actual_text = msg.text
self.assertEqual(actual_text, expected_text)

View file

@ -0,0 +1,432 @@
import gzip
import os.path
import sys
import copy
from unittest import mock
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.core.mail import EmailMessage
__all__ = ['TestProcessEmail']
class TestProcessEmail(EmailMessageTestCase):
def test_message_without_attachments(self):
message = self._get_email_object('generic_message.eml')
mailbox = Mailbox.objects.create()
msg = mailbox.process_incoming_message(message)
self.assertEqual(
msg.mailbox,
mailbox
)
self.assertEqual(msg.subject, 'Message Without Attachment')
self.assertEqual(
msg.message_id,
(
'<CAMdmm+hGH8Dgn-_0xnXJCd=PhyNAiouOYm5zFP0z'
'-foqTO60zA@mail.gmail.com>'
)
)
self.assertEqual(
msg.from_header,
'Adam Coddington <test@adamcoddington.net>',
)
self.assertEqual(
msg.to_header,
'Adam Coddington <test@adamcoddington.net>',
)
def test_message_with_encoded_attachment_filenames(self):
message = self._get_email_object(
'message_with_koi8r_filename_attachments.eml'
)
mailbox = Mailbox.objects.create()
msg = mailbox.process_incoming_message(message)
attachments = msg.attachments.order_by('pk').all()
self.assertEqual(
'\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(
'\u0412\u0435\u0434\u043e\u043c\u043e\u0441\u0442\u0438.pdf',
attachments[1].get_filename()
)
self.assertEqual(
'\u041f\u0430\u043a\u0435\u0442 \u043f\u0440\u0435\u0434\u043b'
'\u043e\u0436\u0435\u043d\u0438\u0439 2016.pptx',
attachments[2].get_filename()
)
def test_message_with_attachments(self):
message = self._get_email_object('message_with_attachment.eml')
mailbox = Mailbox.objects.create()
msg = mailbox.process_incoming_message(message)
expected_count = 1
actual_count = msg.attachments.count()
self.assertEqual(
expected_count,
actual_count,
)
attachment = msg.attachments.all()[0]
self.assertEqual(
attachment.get_filename(),
'heart.png',
)
def test_message_with_utf8_attachment_header(self):
""" Ensure that we properly handle UTF-8 encoded attachment
Safe for regress of #104 too
"""
email_object = self._get_email_object(
'message_with_utf8_attachment.eml',
)
mailbox = Mailbox.objects.create()
msg = mailbox.process_incoming_message(email_object)
expected_count = 2
actual_count = msg.attachments.count()
self.assertEqual(
expected_count,
actual_count,
)
attachment = msg.attachments.all()[0]
self.assertEqual(
attachment.get_filename(),
'pi\u0142kochwyty.jpg'
)
attachment = msg.attachments.all()[1]
self.assertEqual(
attachment.get_filename(),
'odpowied\u017a Burmistrza.jpg'
)
def test_message_get_text_body(self):
message = self._get_email_object('multipart_text.eml')
mailbox = Mailbox.objects.create()
msg = mailbox.process_incoming_message(message)
expected_results = 'Hello there!'
actual_results = msg.text.strip()
self.assertEqual(
expected_results,
actual_results,
)
def test_get_text_body_properly_recomposes_line_continuations(self):
message = Message()
email_object = self._get_email_object(
'message_with_long_text_lines.eml'
)
message.get_email_object = lambda: email_object
actual_text = message.text
expected_text = (
'The one of us with a bike pump is far ahead, '
'but a man stopped to help us and gave us his pump.'
)
self.assertEqual(
actual_text,
expected_text
)
def test_get_body_properly_handles_unicode_body(self):
with open(
os.path.join(
os.path.dirname(__file__),
'messages/generic_message.eml'
)
) as f:
unicode_body = f.read()
message = Message()
message.body = unicode_body
expected_body = unicode_body
actual_body = message.get_email_object().as_string()
self.assertEqual(
expected_body,
actual_body
)
def test_message_issue_82(self):
""" Ensure that we properly handle incorrectly encoded messages
"""
email_object = self._get_email_object('email_issue_82.eml')
it = 'works'
try:
# it's ok to call as_string() before passing email_object
# to _get_dehydrated_message()
email_object.as_string()
except:
it = 'do not works'
success = True
try:
self.mailbox.process_incoming_message(email_object)
except ValueError:
success = False
self.assertEqual(it, 'works')
self.assertEqual(True, success)
def test_message_issue_82_bis(self):
""" Ensure that the email object is good before and after
calling _get_dehydrated_message()
"""
message = self._get_email_object('email_issue_82.eml')
success = True
# this is the code of _process_message()
msg = Message()
# if STORE_ORIGINAL_MESSAGE:
# msg.eml.save('%s.eml' % uuid.uuid4(), ContentFile(message),
# save=False)
msg.mailbox = self.mailbox
if 'subject' in message:
msg.subject = convert_header_to_unicode(message['subject'])[0:255]
if 'message-id' in message:
msg.message_id = message['message-id'][0:255]
if 'from' in message:
msg.from_header = convert_header_to_unicode(message['from'])
if 'to' in message:
msg.to_header = convert_header_to_unicode(message['to'])
elif 'Delivered-To' in message:
msg.to_header = convert_header_to_unicode(message['Delivered-To'])
msg.save()
# here the message is ok
str_msg = message.as_string()
message = self.mailbox._get_dehydrated_message(message, msg)
try:
# here as_string raises UnicodeEncodeError
str_msg = message.as_string()
except:
success = False
msg.set_body(str_msg)
if message['in-reply-to']:
try:
msg.in_reply_to = Message.objects.filter(
message_id=message['in-reply-to']
)[0]
except IndexError:
pass
msg.save()
self.assertEqual(True, success)
def test_message_with_misplaced_utf8_content(self):
""" Ensure that we properly handle incorrectly encoded messages
``message_with_utf8_char.eml``'s primary text payload is marked
as being iso-8859-1 data, but actually contains UTF-8 bytes.
"""
email_object = self._get_email_object('message_with_utf8_char.eml')
msg = self.mailbox.process_incoming_message(email_object)
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(
expected_text,
actual_text,
)
def test_message_with_invalid_content_for_declared_encoding(self):
""" Ensure that we gracefully handle mis-encoded bodies.
Should a payload body be misencoded, we should:
- Not explode
Note: there is (intentionally) no assertion below; the only guarantee
we make via this library is that processing this e-mail message will
not cause an exception to be raised.
"""
email_object = self._get_email_object(
'message_with_invalid_content_for_declared_encoding.eml',
)
msg = self.mailbox.process_incoming_message(email_object)
msg.text
def test_message_with_valid_content_in_single_byte_encoding(self):
email_object = self._get_email_object(
'message_with_single_byte_encoding.eml',
)
msg = self.mailbox.process_incoming_message(email_object)
actual_text = msg.text
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,
expected_body,
)
def test_message_with_single_byte_subject_encoding(self):
email_object = self._get_email_object(
'message_with_single_byte_extended_subject_encoding.eml',
)
msg = self.mailbox.process_incoming_message(email_object)
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)
expected_from = 'test test <mr.test32@mail.ru>'
actual_from = msg.from_header
self.assertEqual(expected_from, actual_from)
def test_message_reply(self):
email_object = EmailMessage(
'Test subject', # subject
'Test body', # body
'username@example.com', # from
['mr.test32@mail.ru'], # to
)
msg = self.mailbox.record_outgoing_message(email_object.message())
self.assertTrue(msg.outgoing)
actual_from = 'username@example.com'
reply_email_object = EmailMessage(
'Test subject', # subject
'Test body', # body
actual_from, # from
['mr.test32@mail.ru'], # to
)
with mock.patch.object(reply_email_object, 'send'):
reply_msg = msg.reply(reply_email_object)
self.assertEqual(reply_msg.in_reply_to, msg)
self.assertEqual(actual_from, msg.from_header)
reply_email_object.from_email = None
with mock.patch.object(reply_email_object, 'send'):
second_reply_msg = msg.reply(reply_email_object)
self.assertEqual(self.mailbox.from_email, second_reply_msg.from_header)
def test_message_with_text_attachment(self):
email_object = self._get_email_object(
'message_with_text_attachment.eml',
)
msg = self.mailbox.process_incoming_message(email_object)
self.assertEqual(msg.attachments.all().count(), 1)
self.assertEqual('Has an attached text document, too!', msg.text)
def test_message_with_long_content(self):
email_object = self._get_email_object(
'message_with_long_content.eml',
)
size = len(force_text(email_object.as_string()))
msg = self.mailbox.process_incoming_message(email_object)
self.assertEqual(size,
len(force_text(msg.get_email_object().as_string())))
def test_message_saved(self):
message = self._get_email_object('generic_message.eml')
default_settings = utils.get_settings()
with mock.patch('django_mailbox.utils.get_settings') as get_settings:
altered = copy.deepcopy(default_settings)
altered['store_original_message'] = True
get_settings.return_value = altered
msg = self.mailbox.process_incoming_message(message)
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')
)
def test_message_saving_ignored(self):
message = self._get_email_object('generic_message.eml')
default_settings = utils.get_settings()
with mock.patch('django_mailbox.utils.get_settings') as get_settings:
altered = copy.deepcopy(default_settings)
altered['store_original_message'] = False
get_settings.return_value = altered
msg = self.mailbox.process_incoming_message(message)
self.assertEquals(msg.eml, None)
def test_message_compressed(self):
message = self._get_email_object('generic_message.eml')
default_settings = utils.get_settings()
with mock.patch('django_mailbox.utils.get_settings') as get_settings:
altered = copy.deepcopy(default_settings)
altered['compress_original_message'] = True
altered['store_original_message'] = True
get_settings.return_value = altered
msg = self.mailbox.process_incoming_message(message)
actual_email_object = msg.get_email_object()
self.assertTrue(msg.eml.name.endswith('.eml.gz'))
with gzip.open(msg.eml.name, 'rb') as f:
self.assertEqual(f.read(),
self._get_email_as_text('generic_message.eml'))

View file

@ -0,0 +1,65 @@
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())
def test_processincomingmessage_with_arg(self):
"""Check that processincomingmessage works with mailbox_name given"""
mailbox_name = 'foo_mailbox'
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)
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')

View file

@ -0,0 +1,167 @@
from unittest import mock
from django.test.utils import override_settings
from django_mailbox.tests.base import EmailMessageTestCase, get_email_as_text
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'30 31 32 33 34 35 36 37 38 39 40 41 42 43 44'
]
)
FAKE_UID_FETCH_SIZES = (
'OK',
[
b'1 (UID 18 RFC822.SIZE 58070000000)',
b'2 (UID 19 RFC822.SIZE 2593)'
]
)
FAKE_UID_FETCH_MSG = (
'OK',
[
(
b'1 (UID 18 RFC822 {5807}',
get_email_as_text('generic_message.eml')
),
]
)
FAKE_UID_COPY_MSG = (
'OK',
[
b'[COPYUID 1 2 2] (Success)'
]
)
FAKE_LIST_ARCHIVE_FOLDERS_ANSWERS = (
'OK',
[
b'(\\HasNoChildren \\All) "/" "[Gmail]/All Mail"'
]
)
class IMAPTestCase(EmailMessageTestCase):
def setUp(self):
def imap_server_uid_method(*args):
cmd = args[0]
arg2 = args[2]
if cmd == 'search':
return FAKE_UID_SEARCH_ANSWER
if cmd == 'copy':
return FAKE_UID_COPY_MSG
if cmd == 'fetch':
if arg2 == '(RFC822.SIZE)':
return FAKE_UID_FETCH_SIZES
if arg2 == '(RFC822)':
return FAKE_UID_FETCH_MSG
def imap_server_list_method(pattern=None):
return FAKE_LIST_ARCHIVE_FOLDERS_ANSWERS
self.imap_server = mock.Mock()
self.imap_server.uid = imap_server_uid_method
self.imap_server.list = imap_server_list_method
super().setUp()
class TestImapTransport(IMAPTestCase):
def setUp(self):
super().setUp()
self.arbitrary_hostname = 'one.two.three'
self.arbitrary_port = 100
self.ssl = False
self.transport = ImapTransport(
self.arbitrary_hostname,
self.arbitrary_port,
self.ssl
)
self.transport.server = self.imap_server
def test_get_email_message(self):
actual_messages = list(self.transport.get_message())
self.assertEqual(len(actual_messages), 27)
actual_message = actual_messages[0]
expected_message = self._get_email_object('generic_message.eml')
self.assertEqual(expected_message, actual_message)
class TestImapArchivedTransport(TestImapTransport):
def setUp(self):
super().setUp()
self.archive = 'Archive'
self.transport = ImapTransport(
self.arbitrary_hostname,
self.arbitrary_port,
self.ssl,
self.archive
)
self.transport.server = self.imap_server
class TestMaxSizeImapTransport(TestImapTransport):
@override_settings(DJANGO_MAILBOX_MAX_MESSAGE_SIZE=5807)
def setUp(self):
super().setUp()
self.transport = ImapTransport(
self.arbitrary_hostname,
self.arbitrary_port,
self.ssl,
)
self.transport.server = self.imap_server
def test_size_limit(self):
all_message_ids = self.transport._get_all_message_ids()
small_message_ids = self.transport._get_small_message_ids(
all_message_ids,
)
self.assertEqual(len(small_message_ids), 1)
def test_get_email_message(self):
actual_messages = list(self.transport.get_message())
self.assertEqual(len(actual_messages), 1)
actual_message = actual_messages[0]
expected_message = self._get_email_object('generic_message.eml')
self.assertEqual(expected_message, actual_message)
class TestPop3Transport(EmailMessageTestCase):
def setUp(self):
self.arbitrary_hostname = 'one.two.three'
self.arbitrary_port = 100
self.ssl = False
self.transport = Pop3Transport(
self.arbitrary_hostname,
self.arbitrary_port,
self.ssl
)
self.transport.server = None
super().setUp()
def test_get_email_message(self):
with mock.patch.object(self.transport, 'server') as server:
# Consider this value arbitrary, the second parameter
# should have one entry per message in the inbox
server.list.return_value = [None, ['some_msg']]
server.retr.return_value = [
'+OK message follows',
[
line.encode('ascii')
for line in self._get_email_as_text(
'generic_message.eml'
).decode('ascii').split('\n')
],
10018, # Some arbitrary size, ideally matching the above
]
actual_messages = list(self.transport.get_message())
self.assertEqual(len(actual_messages), 1)
actual_message = actual_messages[0]
expected_message = self._get_email_object('generic_message.eml')
self.assertEqual(expected_message, actual_message)

View file

@ -0,0 +1,11 @@
# all imports below are only used by external modules
# flake8: noqa
from django_mailbox.transports.imap import ImapTransport
from django_mailbox.transports.pop3 import Pop3Transport
from django_mailbox.transports.maildir import MaildirTransport
from django_mailbox.transports.mbox import MboxTransport
from django_mailbox.transports.babyl import BabylTransport
from django_mailbox.transports.mh import MHTransport
from django_mailbox.transports.mmdf import MMDFTransport
from django_mailbox.transports.gmail import GmailImapTransport
from django_mailbox.transports.office365 import Office365Transport

View file

@ -0,0 +1,6 @@
from mailbox import Babyl
from django_mailbox.transports.generic import GenericFileMailbox
class BabylTransport(GenericFileMailbox):
_variant = Babyl

View file

@ -0,0 +1,11 @@
import email
# Do *not* remove this, we need to use this in subclasses of EmailTransport
from email.errors import MessageParseError # noqa: F401
class EmailTransport:
def get_email_from_bytes(self, contents):
message = email.message_from_bytes(contents)
return message

View file

@ -0,0 +1,26 @@
import sys
from .base import EmailTransport
class GenericFileMailbox(EmailTransport):
_variant = None
_path = None
def __init__(self, path):
super().__init__()
self._path = path
def get_instance(self):
return self._variant(self._path)
def get_message(self, condition=None):
repository = self.get_instance()
repository.lock()
for key, message in repository.items():
if condition and not condition(message):
continue
repository.remove(key)
yield message
repository.flush()
repository.unlock()

View file

@ -0,0 +1,57 @@
import logging
from django_mailbox.transports.imap import ImapTransport
logger = logging.getLogger(__name__)
class GmailImapTransport(ImapTransport):
def connect(self, username, password):
# Try to use oauth2 first. It's much safer
try:
self._connect_oauth(username)
except (TypeError, ValueError) as e:
logger.warning("Couldn't do oauth2 because %s" % e)
self.server = self.transport(self.hostname, self.port)
typ, msg = self.server.login(username, password)
self.server.select()
def _connect_oauth(self, username):
# username should be an email address that has already been authorized
# for gmail access
try:
from django_mailbox.google_utils import (
get_google_access_token,
fetch_user_info,
AccessTokenNotFound,
)
except ImportError:
raise ValueError(
"Install python-social-auth to use oauth2 auth for gmail"
)
access_token = None
while access_token is None:
try:
access_token = get_google_access_token(username)
google_email_address = fetch_user_info(username)['email']
except TypeError:
# This means that the google process took too long
# Trying again is the right thing to do
pass
except AccessTokenNotFound:
raise ValueError(
"No Token available in python-social-auth for %s" % (
username
)
)
auth_string = 'user={}\1auth=Bearer {}\1\1'.format(
google_email_address,
access_token
)
self.server = self.transport(self.hostname, self.port)
self.server.authenticate('XOAUTH2', lambda x: auth_string)
self.server.select()

View file

@ -0,0 +1,137 @@
import imaplib
import logging
from django.conf import settings
from .base import EmailTransport, MessageParseError
# By default, imaplib will raise an exception if it encounters more
# than 10k bytes; sometimes users attempt to consume mailboxes that
# have a more, and modern computers are skookum-enough to handle just
# a *few* more messages without causing any sort of problem.
imaplib._MAXLINE = 1000000
logger = logging.getLogger(__name__)
class ImapTransport(EmailTransport):
def __init__(
self, hostname, port=None, ssl=False, tls=False,
archive='', folder=None,
):
self.max_message_size = getattr(
settings,
'DJANGO_MAILBOX_MAX_MESSAGE_SIZE',
False
)
self.integration_testing_subject = getattr(
settings,
'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT',
None
)
self.hostname = hostname
self.port = port
self.archive = archive
self.folder = folder
self.tls = tls
if ssl:
self.transport = imaplib.IMAP4_SSL
if not self.port:
self.port = 993
else:
self.transport = imaplib.IMAP4
if not self.port:
self.port = 143
def connect(self, username, password):
self.server = self.transport(self.hostname, self.port)
if self.tls:
self.server.starttls()
typ, msg = self.server.login(username, password)
if self.folder:
self.server.select(self.folder)
else:
self.server.select()
def _get_all_message_ids(self):
# Fetch all the message uids
response, message_ids = self.server.uid('search', None, 'ALL')
message_id_string = message_ids[0].strip()
# Usually `message_id_string` will be a list of space-separated
# ids; we must make sure that it isn't an empty string before
# splitting into individual UIDs.
if message_id_string:
return message_id_string.decode().split(' ')
return []
def _get_small_message_ids(self, message_ids):
# Using existing message uids, get the sizes and
# return only those that are under the size
# limit
safe_message_ids = []
status, data = self.server.uid(
'fetch',
','.join(message_ids),
'(RFC822.SIZE)'
)
for each_msg in data:
each_msg = each_msg.decode()
try:
uid = each_msg.split(' ')[2]
size = each_msg.split(' ')[4].rstrip(')')
if int(size) <= int(self.max_message_size):
safe_message_ids.append(uid)
except ValueError as e:
logger.warning(
"ValueError: {} working on {}".format(e, each_msg[0])
)
pass
return safe_message_ids
def get_message(self, condition=None):
message_ids = self._get_all_message_ids()
if not message_ids:
return
# Limit the uids to the small ones if we care about that
if self.max_message_size:
message_ids = self._get_small_message_ids(message_ids)
if self.archive:
typ, folders = self.server.list(pattern=self.archive)
if folders[0] is None:
# If the archive folder does not exist, create it
self.server.create(self.archive)
for uid in message_ids:
try:
typ, msg_contents = self.server.uid('fetch', uid, '(RFC822)')
if not msg_contents:
continue
try:
message = self.get_email_from_bytes(msg_contents[0][1])
except TypeError:
# This happens if another thread/process deletes the
# message between our generating the ID list and our
# processing it here.
continue
if condition and not condition(message):
continue
yield message
except MessageParseError:
continue
if self.archive:
self.server.uid('copy', uid, self.archive)
self.server.uid('store', uid, "+FLAGS", "(\\Deleted)")
self.server.expunge()
return

View file

@ -0,0 +1,9 @@
from mailbox import Maildir
from django_mailbox.transports.generic import GenericFileMailbox
class MaildirTransport(GenericFileMailbox):
_variant = Maildir
def get_instance(self):
return self._variant(self._path, None)

View file

@ -0,0 +1,6 @@
from mailbox import mbox
from django_mailbox.transports.generic import GenericFileMailbox
class MboxTransport(GenericFileMailbox):
_variant = mbox

View file

@ -0,0 +1,6 @@
from mailbox import MH
from django_mailbox.transports.generic import GenericFileMailbox
class MHTransport(GenericFileMailbox):
_variant = MH

View file

@ -0,0 +1,6 @@
from mailbox import MMDF
from django_mailbox.transports.generic import GenericFileMailbox
class MMDFTransport(GenericFileMailbox):
_variant = MMDF

View file

@ -0,0 +1,131 @@
import O365
import logging
from django.conf import settings
from .base import EmailTransport, MessageParseError
logger = logging.getLogger(__name__)
class Office365Transport(EmailTransport):
def __init__(
self, hostname, port=None, ssl=False, tls=False,
archive='', folder=None, client_id=None, client_secret=None, tenant_id=None
):
self.max_message_size = getattr(
settings,
'DJANGO_MAILBOX_MAX_MESSAGE_SIZE',
False
)
self.integration_testing_subject = getattr(
settings,
'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT',
None
)
self.hostname = hostname
self.port = port
self.archive = archive
self.folder = folder
self.tls = tls
if ssl:
#self.transport = imaplib.IMAP4_SSL
if not self.port:
self.port = 993
else:
#self.transport = imaplib.IMAP4
if not self.port:
self.port = 143
self.client_id = client_id
self.client_secret = client_secret
self.tenant_id = tenant_id
def connect(self, username, password):
credentials = (self.client_id, self.client_secret)
# the default protocol will be Microsoft Graph
# the default authentication method will be "on behalf of a user"
self.server = O365.Account(credentials, auth_flow_type='credentials', tenant_id=self.tenant_id)
if self.server.authenticate(scopes=['mailbox']):
print('Authenticated!')
def _get_all_message_ids(self):
# # Fetch all the message uids
# response, message_ids = self.server.uid('search', None, 'ALL')
# message_id_string = message_ids[0].strip()
# # Usually `message_id_string` will be a list of space-separated
# # ids; we must make sure that it isn't an empty string before
# # splitting into individual UIDs.
# if message_id_string:
# return message_id_string.decode().split(' ')
# return []
def _get_small_message_ids(self, message_ids):
# Using existing message uids, get the sizes and
# return only those that are under the size
# limit
safe_message_ids = []
status, data = self.server.uid(
'fetch',
','.join(message_ids),
'(RFC822.SIZE)'
)
for each_msg in data:
each_msg = each_msg.decode()
try:
uid = each_msg.split(' ')[2]
size = each_msg.split(' ')[4].rstrip(')')
if int(size) <= int(self.max_message_size):
safe_message_ids.append(uid)
except ValueError as e:
logger.warning(
"ValueError: {} working on {}".format(e, each_msg[0])
)
pass
return safe_message_ids
def get_message(self, condition=None):
message_ids = self._get_all_message_ids()
if not message_ids:
return
# Limit the uids to the small ones if we care about that
if self.max_message_size:
message_ids = self._get_small_message_ids(message_ids)
if self.archive:
typ, folders = self.server.list(pattern=self.archive)
if folders[0] is None:
# If the archive folder does not exist, create it
self.server.create(self.archive)
for uid in message_ids:
try:
typ, msg_contents = self.server.uid('fetch', uid, '(RFC822)')
if not msg_contents:
continue
try:
message = self.get_email_from_bytes(msg_contents[0][1])
except TypeError:
# This happens if another thread/process deletes the
# message between our generating the ID list and our
# processing it here.
continue
if condition and not condition(message):
continue
yield message
except MessageParseError:
continue
if self.archive:
self.server.uid('copy', uid, self.archive)
self.server.uid('store', uid, "+FLAGS", "(\\Deleted)")
self.server.expunge()
return

View file

@ -0,0 +1,44 @@
from poplib import POP3, POP3_SSL
from .base import EmailTransport, MessageParseError
class Pop3Transport(EmailTransport):
def __init__(self, hostname, port=None, ssl=False):
self.hostname = hostname
self.port = port
if ssl:
self.transport = POP3_SSL
if not self.port:
self.port = 995
else:
self.transport = POP3
if not self.port:
self.port = 110
def connect(self, username, password):
self.server = self.transport(self.hostname, self.port)
self.server.user(username)
self.server.pass_(password)
def get_message_body(self, message_lines):
return bytes('\r\n', 'ascii').join(message_lines)
def get_message(self, condition=None):
message_count = len(self.server.list()[1])
for i in range(message_count):
try:
msg_contents = self.get_message_body(
self.server.retr(i + 1)[1]
)
message = self.get_email_from_bytes(msg_contents)
if condition and not condition(message):
continue
yield message
except MessageParseError:
continue
self.server.dele(i + 1)
self.server.quit()
return

View file

@ -0,0 +1,151 @@
import datetime
import email.header
import logging
import os
from django.conf import settings
logger = logging.getLogger(__name__)
def get_settings():
return {
'strip_unallowed_mimetypes': getattr(
settings,
'DJANGO_MAILBOX_STRIP_UNALLOWED_MIMETYPES',
False
),
'allowed_mimetypes': getattr(
settings,
'DJANGO_MAILBOX_ALLOWED_MIMETYPES',
[
'text/plain',
'text/html'
]
),
'text_stored_mimetypes': getattr(
settings,
'DJANGO_MAILBOX_TEXT_STORED_MIMETYPES',
[
'text/plain',
'text/html'
]
),
'altered_message_header': getattr(
settings,
'DJANGO_MAILBOX_ALTERED_MESSAGE_HEADER',
'X-Django-Mailbox-Altered-Message'
),
'attachment_interpolation_header': getattr(
settings,
'DJANGO_MAILBOX_ATTACHMENT_INTERPOLATION_HEADER',
'X-Django-Mailbox-Interpolate-Attachment'
),
'attachment_upload_to': getattr(
settings,
'DJANGO_MAILBOX_ATTACHMENT_UPLOAD_TO',
'mailbox_attachments/%Y/%m/%d/'
),
'store_original_message': getattr(
settings,
'DJANGO_MAILBOX_STORE_ORIGINAL_MESSAGE',
False
),
'compress_original_message': getattr(
settings,
'DJANGO_MAILBOX_COMPRESS_ORIGINAL_MESSAGE',
False
),
'original_message_compression': getattr(
settings,
'DJANGO_MAILBOX_ORIGINAL_MESSAGE_COMPRESSION',
6
),
'default_charset': getattr(
settings,
'DJANGO_MAILBOX_default_charset',
'iso8859-1',
)
}
def convert_header_to_unicode(header):
default_charset = get_settings()['default_charset']
def _decode(value, encoding):
if isinstance(value, str):
return value
if not encoding or encoding == 'unknown-8bit':
encoding = default_charset
return value.decode(encoding, 'replace')
try:
return ''.join(
[
(
_decode(bytestr, encoding)
) for bytestr, encoding in email.header.decode_header(header)
]
)
except UnicodeDecodeError:
logger.exception(
'Errors encountered decoding header %s into encoding %s.',
header,
default_charset,
)
return header.decode(default_charset, 'replace')
def get_body_from_message(message, maintype, subtype):
"""
Fetchs the body message matching main/sub content type.
"""
body = ''
for part in message.walk():
if part.get('content-disposition', '').startswith('attachment;'):
continue
if part.get_content_maintype() == maintype and \
part.get_content_subtype() == subtype:
charset = part.get_content_charset()
this_part = part.get_payload(decode=True)
if charset:
try:
this_part = this_part.decode(charset, 'replace')
except LookupError:
this_part = this_part.decode('ascii', 'replace')
logger.warning(
'Unknown encoding %s encountered while decoding '
'text payload. Interpreting as ASCII with '
'replacement, but some data may not be '
'represented as the sender intended.',
charset
)
except ValueError:
this_part = this_part.decode('ascii', 'replace')
logger.warning(
'Error encountered while decoding text '
'payload from an incorrectly-constructed '
'e-mail; payload was converted to ASCII with '
'replacement, but some data may not be '
'represented as the sender intended.'
)
else:
this_part = this_part.decode('ascii', 'replace')
body += this_part
return body
def get_attachment_save_path(instance, filename):
settings = get_settings()
path = settings['attachment_upload_to']
if '%' in path:
path = datetime.datetime.utcnow().strftime(path)
return os.path.join(
path,
filename,
)