mirror of
https://github.com/coddingtonbear/django-mailbox.git
synced 2026-07-09 22:38:19 +02:00
437 lines
14 KiB
Python
Executable file
437 lines
14 KiB
Python
Executable file
import email
|
|
from email.message import Message as EmailMessage
|
|
from email.utils import formatdate
|
|
import rfc822
|
|
import urllib
|
|
import urlparse
|
|
import os
|
|
|
|
from django.conf import settings
|
|
from django.core.mail.message import make_msgid
|
|
from django.core.files import File
|
|
from django.core.files.temp import NamedTemporaryFile
|
|
from django.db import models
|
|
from django_mailbox.transports import Pop3Transport, ImapTransport,\
|
|
MaildirTransport, MboxTransport, BabylTransport, MHTransport, \
|
|
MMDFTransport
|
|
from django_mailbox.signals import message_received
|
|
|
|
SKIPPED_EXTENSIONS = getattr(
|
|
settings,
|
|
'DJANGO_MAILBOX_SKIPPED_EXTENSIONS',
|
|
['.p7s']
|
|
)
|
|
STRIP_UNALLOWED_MIMETYPES = getattr(
|
|
settings,
|
|
'DJANGO_MAILBOX_STRIP_UNALLOWED_MIMETYPES',
|
|
False
|
|
)
|
|
ALLOWED_MIMETYPES = getattr(
|
|
settings,
|
|
'DJANGO_MAILBOX_ALLOWED_MIMETYPES',
|
|
[
|
|
'text/plain',
|
|
'text/html'
|
|
]
|
|
)
|
|
ALTERED_MESSAGE_HEADER = getattr(
|
|
settings,
|
|
'DJANGO_MAILBOX_ALTERED_MESSAGE_HEADER',
|
|
'X-Django-Mailbox-Altered-Message'
|
|
)
|
|
|
|
|
|
class ActiveMailboxManager(models.Manager):
|
|
def get_query_set(self):
|
|
return super(ActiveMailboxManager, self).get_query_set().filter(
|
|
active=True,
|
|
)
|
|
|
|
|
|
class Mailbox(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
uri = models.CharField(
|
|
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(
|
|
max_length=255,
|
|
help_text=(
|
|
"Example: MailBot <mailbot@yourdomain.com><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(
|
|
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,
|
|
)
|
|
|
|
objects = models.Manager()
|
|
active_mailboxes = ActiveMailboxManager()
|
|
|
|
@property
|
|
def _protocol_info(self):
|
|
return urlparse.urlparse(self.uri)
|
|
|
|
@property
|
|
def _domain(self):
|
|
return self._protocol_info.hostname
|
|
|
|
@property
|
|
def port(self):
|
|
return self._protocol_info.port
|
|
|
|
@property
|
|
def username(self):
|
|
return urllib.unquote(self._protocol_info.username)
|
|
|
|
@property
|
|
def password(self):
|
|
return urllib.unquote(self._protocol_info.password)
|
|
|
|
@property
|
|
def location(self):
|
|
return self._domain if self._domain else '' + self._protocol_info.path
|
|
|
|
@property
|
|
def type(self):
|
|
scheme = self._protocol_info.scheme.lower()
|
|
if '+' in scheme:
|
|
return scheme.split('+')[0]
|
|
return scheme
|
|
|
|
@property
|
|
def use_ssl(self):
|
|
return '+ssl' in self._protocol_info.scheme.lower()
|
|
|
|
def get_connection(self):
|
|
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
|
|
)
|
|
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 == '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):
|
|
msg = self._process_message(message)
|
|
msg.outgoing = False
|
|
msg.save()
|
|
message_received.send(sender=self, message=msg)
|
|
return msg
|
|
|
|
def record_outgoing_message(self, message):
|
|
msg = self._process_message(message)
|
|
msg.outgoing = True
|
|
msg.save()
|
|
return msg
|
|
|
|
def _filter_message_body(self, message):
|
|
if not message.is_multipart() or not STRIP_UNALLOWED_MIMETYPES:
|
|
return message
|
|
stripped_content = {}
|
|
new = EmailMessage()
|
|
for header, value in message.items():
|
|
new[header] = value
|
|
for part in message.walk():
|
|
content_type = part.get_content_type()
|
|
if not content_type in ALLOWED_MIMETYPES:
|
|
if content_type not in stripped_content:
|
|
stripped_content[content_type] = 0
|
|
stripped_content[content_type] = (
|
|
stripped_content[content_type] + 1
|
|
)
|
|
continue
|
|
new.attach(part)
|
|
new[ALTERED_MESSAGE_HEADER] = 'Stripped ' + ', '.join(
|
|
['%s*%s' % (key, value) for key, value in stripped_content.items()]
|
|
)
|
|
return new
|
|
|
|
def _process_message(self, message):
|
|
msg = Message()
|
|
msg.mailbox = self
|
|
msg.subject = message['subject'][0:255]
|
|
msg.message_id = message['message-id'][0:255]
|
|
msg.from_header = message['from']
|
|
msg.to_header = message['to']
|
|
message = self._filter_message_body(message)
|
|
msg.body = message.as_string()
|
|
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()
|
|
if message.is_multipart():
|
|
for part in message.walk():
|
|
if part.get_content_maintype() == 'multipart':
|
|
continue
|
|
if part.get('Content-Disposition') is None:
|
|
continue
|
|
filename = part.get_filename()
|
|
# ignore SMIME extension
|
|
filename_basename, filename_extension = os.path.splitext(
|
|
filename
|
|
)
|
|
buffer_space = 40
|
|
if len(filename) > 100 - buffer_space:
|
|
# Ensure that there're at least a few chars available
|
|
# afterward for duplication things like _1, _2 ... _99 and
|
|
# the FileField's upload_to path.
|
|
filename_basename = filename_basename[
|
|
0:100-len(filename_extension)-buffer_space
|
|
]
|
|
filename = filename_basename + filename_extension
|
|
if filename_extension in SKIPPED_EXTENSIONS:
|
|
continue
|
|
data = part.get_payload(decode=True)
|
|
if not data:
|
|
continue
|
|
temp_file = NamedTemporaryFile(delete=True)
|
|
temp_file.write(data)
|
|
temp_file.flush()
|
|
attachment = MessageAttachment()
|
|
attachment.document.save(filename, File(temp_file))
|
|
attachment.message = msg
|
|
attachment.save()
|
|
return msg
|
|
|
|
def get_new_mail(self):
|
|
new_mail = []
|
|
connection = self.get_connection()
|
|
if not connection:
|
|
return new_mail
|
|
for message in connection.get_message():
|
|
msg = self.process_incoming_message(message)
|
|
new_mail.append(msg)
|
|
return new_mail
|
|
|
|
def __unicode__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name_plural = "Mailboxes"
|
|
|
|
|
|
class IncomingMessageManager(models.Manager):
|
|
def get_query_set(self):
|
|
return super(IncomingMessageManager, self).get_query_set().filter(
|
|
outgoing=False,
|
|
)
|
|
|
|
|
|
class OutgoingMessageManager(models.Manager):
|
|
def get_query_set(self):
|
|
return super(OutgoingMessageManager, self).get_query_set().filter(
|
|
outgoing=True,
|
|
)
|
|
|
|
|
|
class UnreadMessageManager(models.Manager):
|
|
def get_query_set(self):
|
|
return super(UnreadMessageManager, self).get_query_set().filter(
|
|
read=None
|
|
)
|
|
|
|
|
|
class Message(models.Model):
|
|
mailbox = models.ForeignKey(Mailbox, related_name='messages')
|
|
subject = models.CharField(max_length=255)
|
|
message_id = models.CharField(max_length=255)
|
|
in_reply_to = models.ForeignKey(
|
|
'django_mailbox.Message',
|
|
related_name='replies',
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
from_header = models.CharField(
|
|
max_length=255,
|
|
)
|
|
to_header = models.TextField()
|
|
outgoing = models.BooleanField(
|
|
default=False,
|
|
blank=True,
|
|
)
|
|
|
|
body = models.TextField()
|
|
|
|
processed = models.DateTimeField(
|
|
auto_now_add=True
|
|
)
|
|
read = models.DateTimeField(
|
|
default=None,
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
|
|
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.
|
|
|
|
"""
|
|
if self.outgoing:
|
|
return self.to_addresses()
|
|
else:
|
|
return self.from_addresses()
|
|
|
|
@property
|
|
def from_address(self):
|
|
return rfc822.parseaddr(self.from_header)[1]
|
|
|
|
@property
|
|
def to_addresses(self):
|
|
addresses = []
|
|
for address in self.to_header.split(','):
|
|
addresses.append(
|
|
rfc822.parseaddr(
|
|
address
|
|
)[1]
|
|
)
|
|
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 self.mailbox.from_email:
|
|
message.from_email = self.mailbox.from_email
|
|
else:
|
|
message.from_email = 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
|
|
message.send()
|
|
return self.mailbox.record_outgoing_message(
|
|
email.message_from_string(
|
|
message.message().as_string()
|
|
)
|
|
)
|
|
|
|
@property
|
|
def text(self):
|
|
return self.get_text_body()
|
|
|
|
def get_text_body(self):
|
|
def get_body_from_message(message):
|
|
body = ''
|
|
if message.is_multipart():
|
|
for part in message.get_payload():
|
|
mime_type = part.get('content-type', '').split(';')[0]
|
|
if mime_type == 'text/plain':
|
|
body = body + get_body_from_message(part)
|
|
else:
|
|
body = body + message.get_payload()
|
|
return body
|
|
|
|
return get_body_from_message(
|
|
self.get_email_object()
|
|
).replace('=\n', '').rstrip('\n')
|
|
|
|
def get_email_object(self):
|
|
""" Returns an `email.message.Message` instance for this message.
|
|
|
|
Note: Python 2.6's version of email.message_from_string requires
|
|
bytes and will incorrectly create an e-mail object if a unicode
|
|
object is passed-in.
|
|
|
|
Note that in reality 7-bit ascii *should* be an acceptable encoding
|
|
here per RFC-2822 (2.1), but given that `message_from_string` really
|
|
only cares that bytes are incoming rather than a unicode object, and
|
|
django will dutifully, if you were to edit the message in code or
|
|
via the admin, allow you to store unicode characters, returning UTF-8
|
|
encoded bytes is probably the safest answer.
|
|
|
|
"""
|
|
return email.message_from_string(self.body.encode('utf-8'))
|
|
|
|
def delete(self, *args, **kwargs):
|
|
for attachment in self.attachments.all():
|
|
# This attachment is attached only to this message.
|
|
attachment.delete()
|
|
return super(Message, self).delete(*args, **kwargs)
|
|
|
|
def __unicode__(self):
|
|
return self.subject
|
|
|
|
|
|
class MessageAttachment(models.Model):
|
|
message = models.ForeignKey(
|
|
Message,
|
|
related_name='attachments',
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
document = models.FileField(upload_to='mailbox_attachments/%Y/%m/%d/')
|
|
|
|
def delete(self, *args, **kwargs):
|
|
self.document.delete()
|
|
return super(MessageAttachment, self).delete(*args, **kwargs)
|
|
|
|
def __unicode__(self):
|
|
return self.document.url
|