2014-08-29 15:39:49 -03:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
Models declaration for application ``django_mailbox``.
|
|
|
|
|
"""
|
2017-07-09 20:36:37 +02:00
|
|
|
import gzip
|
2014-08-29 15:39:49 -03:00
|
|
|
from email.encoders import encode_base64
|
2013-03-11 21:00:53 -07:00
|
|
|
from email.message import Message as EmailMessage
|
2013-06-22 15:42:50 -07:00
|
|
|
from email.utils import formatdate, parseaddr
|
2019-10-15 05:31:13 +02:00
|
|
|
from urllib.parse import parse_qs, unquote, urlparse
|
2014-08-29 15:39:49 -03:00
|
|
|
from quopri import encode as encode_quopri
|
2019-10-15 05:31:13 +02:00
|
|
|
from io import BytesIO
|
2014-08-29 15:39:49 -03:00
|
|
|
import base64
|
|
|
|
|
import email
|
2014-08-22 19:46:39 -07:00
|
|
|
import logging
|
2013-06-23 20:27:50 -07:00
|
|
|
import mimetypes
|
|
|
|
|
import os.path
|
|
|
|
|
import uuid
|
2017-07-09 20:36:37 +02:00
|
|
|
from tempfile import NamedTemporaryFile
|
2014-08-29 15:40:22 -03:00
|
|
|
|
2016-08-16 01:10:55 +02:00
|
|
|
import django
|
2016-05-14 22:06:32 -07:00
|
|
|
from django.conf import settings as django_settings
|
2017-07-09 20:36:37 +02:00
|
|
|
from django.core.files.base import ContentFile, File
|
2014-08-29 15:39:49 -03:00
|
|
|
from django.core.mail.message import make_msgid
|
2012-06-27 20:45:01 -07:00
|
|
|
from django.db import models
|
2019-10-15 05:31:13 +02:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2016-08-16 01:00:26 +02:00
|
|
|
from django.utils.timezone import now
|
2012-06-27 20:45:01 -07:00
|
|
|
|
2016-05-14 22:06:32 -07:00
|
|
|
from django_mailbox import utils
|
2014-08-29 15:39:49 -03:00
|
|
|
from django_mailbox.signals import message_received
|
|
|
|
|
from django_mailbox.transports import Pop3Transport, ImapTransport, \
|
|
|
|
|
MaildirTransport, MboxTransport, BabylTransport, MHTransport, \
|
2023-01-16 22:23:38 +01:00
|
|
|
MMDFTransport, GmailImapTransport, Office365Transport
|
2014-04-22 15:49:49 -07:00
|
|
|
|
2014-08-22 19:46:39 -07:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2020-07-26 14:21:21 +08:00
|
|
|
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())
|
2023-12-10 04:01:53 +01:00
|
|
|
logger.debug('Received %d %s.', count, 'mail(s)')
|
2020-07-26 14:21:21 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MailboxManager(models.Manager):
|
|
|
|
|
def get_queryset(self):
|
|
|
|
|
return MailboxQuerySet(self.model, using=self._db)
|
|
|
|
|
|
|
|
|
|
|
2020-07-26 14:22:40 +08:00
|
|
|
class ActiveMailboxManager(MailboxManager):
|
2014-09-06 16:31:12 -07:00
|
|
|
def get_queryset(self):
|
2020-07-26 14:22:40 +08:00
|
|
|
return super().get_queryset().filter(
|
2012-10-09 05:52:04 +00:00
|
|
|
active=True,
|
|
|
|
|
)
|
|
|
|
|
|
2013-06-22 14:35:47 -07:00
|
|
|
|
2012-06-27 20:45:01 -07:00
|
|
|
class Mailbox(models.Model):
|
2014-08-29 15:40:22 -03:00
|
|
|
name = models.CharField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Name'),
|
2014-08-29 15:40:22 -03:00
|
|
|
max_length=255,
|
|
|
|
|
)
|
|
|
|
|
|
2012-06-27 20:45:01 -07:00
|
|
|
uri = models.CharField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('URI'),
|
2013-06-22 14:35:47 -07:00
|
|
|
max_length=255,
|
2014-08-29 15:40:22 -03:00
|
|
|
help_text=(_(
|
2013-06-22 14:35:47 -07:00
|
|
|
"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)."
|
2014-08-29 15:40:22 -03:00
|
|
|
)),
|
2013-06-22 14:35:47 -07:00
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
default=None,
|
|
|
|
|
)
|
2014-08-29 15:40:22 -03:00
|
|
|
|
2012-10-28 12:25:37 -07:00
|
|
|
from_email = models.CharField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('From email'),
|
2013-06-22 14:35:47 -07:00
|
|
|
max_length=255,
|
2014-08-29 15:40:22 -03:00
|
|
|
help_text=(_(
|
2013-06-22 14:35:47 -07:00
|
|
|
"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`."
|
2014-08-29 15:40:22 -03:00
|
|
|
)),
|
2013-06-22 14:35:47 -07:00
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
default=None,
|
|
|
|
|
)
|
2014-08-29 15:40:22 -03:00
|
|
|
|
2012-10-09 05:52:04 +00:00
|
|
|
active = models.BooleanField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Active'),
|
2014-08-29 15:40:22 -03:00
|
|
|
help_text=(_(
|
2013-06-22 14:35:47 -07:00
|
|
|
"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. "
|
2014-08-29 15:40:22 -03:00
|
|
|
)),
|
2013-06-22 14:35:47 -07:00
|
|
|
blank=True,
|
|
|
|
|
default=True,
|
|
|
|
|
)
|
2012-10-09 05:52:04 +00:00
|
|
|
|
2016-08-16 01:00:26 +02:00
|
|
|
last_polling = models.DateTimeField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_("Last polling"),
|
2016-08-16 07:18:51 +02:00
|
|
|
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.")),
|
2016-08-16 01:00:26 +02:00
|
|
|
blank=True,
|
|
|
|
|
null=True
|
|
|
|
|
)
|
|
|
|
|
|
2020-07-26 14:21:21 +08:00
|
|
|
objects = MailboxManager()
|
2012-10-09 05:52:04 +00:00
|
|
|
active_mailboxes = ActiveMailboxManager()
|
2012-06-27 20:45:01 -07:00
|
|
|
|
2023-12-10 04:01:53 +01:00
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _('Mailbox')
|
|
|
|
|
verbose_name_plural = _('Mailboxes')
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.name
|
|
|
|
|
|
2012-06-27 20:45:01 -07:00
|
|
|
@property
|
|
|
|
|
def _protocol_info(self):
|
2014-04-24 15:28:54 -07:00
|
|
|
return urlparse(self.uri)
|
2012-06-27 20:45:01 -07:00
|
|
|
|
2014-05-25 12:35:20 -07:00
|
|
|
@property
|
|
|
|
|
def _query_string(self):
|
|
|
|
|
return parse_qs(self._protocol_info.query)
|
|
|
|
|
|
2012-06-27 20:45:01 -07:00
|
|
|
@property
|
|
|
|
|
def _domain(self):
|
|
|
|
|
return self._protocol_info.hostname
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def port(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns the port to use for fetching messages."""
|
2012-06-27 20:45:01 -07:00
|
|
|
return self._protocol_info.port
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def username(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns the username to use for fetching messages."""
|
2014-04-24 15:28:54 -07:00
|
|
|
return unquote(self._protocol_info.username)
|
2012-06-27 20:45:01 -07:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def password(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns the password to use for fetching messages."""
|
2014-04-24 15:28:54 -07:00
|
|
|
return unquote(self._protocol_info.password)
|
2012-06-27 20:45:01 -07:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def location(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns the location (domain and path) of messages."""
|
2012-06-27 20:45:01 -07:00
|
|
|
return self._domain if self._domain else '' + self._protocol_info.path
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def type(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns the 'transport' name for this mailbox."""
|
2012-06-30 19:08:18 +00:00
|
|
|
scheme = self._protocol_info.scheme.lower()
|
|
|
|
|
if '+' in scheme:
|
|
|
|
|
return scheme.split('+')[0]
|
|
|
|
|
return scheme
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def use_ssl(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns whether or not this mailbox's connection uses SSL."""
|
2012-06-30 19:08:18 +00:00
|
|
|
return '+ssl' in self._protocol_info.scheme.lower()
|
2012-06-27 20:45:01 -07:00
|
|
|
|
2016-05-15 21:05:11 +02:00
|
|
|
@property
|
|
|
|
|
def use_tls(self):
|
|
|
|
|
"""Returns whether or not this mailbox's connection uses STARTTLS."""
|
|
|
|
|
return '+tls' in self._protocol_info.scheme.lower()
|
|
|
|
|
|
2014-05-23 09:17:14 +01:00
|
|
|
@property
|
|
|
|
|
def archive(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns (if specified) the folder to archive messages to."""
|
2014-05-25 12:35:20 -07:00
|
|
|
archive_folder = self._query_string.get('archive', None)
|
|
|
|
|
if not archive_folder:
|
|
|
|
|
return None
|
|
|
|
|
return archive_folder[0]
|
2015-03-22 20:57:07 -07:00
|
|
|
|
2015-03-16 16:52:25 +03:00
|
|
|
@property
|
|
|
|
|
def folder(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns (if specified) the folder to fetch mail from."""
|
2015-03-16 16:52:25 +03:00
|
|
|
folder = self._query_string.get('folder', None)
|
|
|
|
|
if not folder:
|
|
|
|
|
return None
|
|
|
|
|
return folder[0]
|
2014-05-23 09:17:14 +01:00
|
|
|
|
2023-01-16 22:23:38 +01:00
|
|
|
@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('tenant_id', None)
|
|
|
|
|
if not tenant_id:
|
|
|
|
|
return None
|
|
|
|
|
return tenant_id[0]
|
|
|
|
|
|
2012-06-27 20:45:01 -07:00
|
|
|
def get_connection(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns the transport instance for this mailbox.
|
|
|
|
|
|
|
|
|
|
These will always be instances of
|
|
|
|
|
`django_mailbox.transports.base.EmailTransport`.
|
|
|
|
|
|
|
|
|
|
"""
|
2012-10-08 15:30:32 +00:00
|
|
|
if not self.uri:
|
|
|
|
|
return None
|
|
|
|
|
elif self.type == 'imap':
|
2012-06-30 22:30:13 -07:00
|
|
|
conn = ImapTransport(
|
2013-06-22 14:35:47 -07:00
|
|
|
self.location,
|
|
|
|
|
port=self.port if self.port else None,
|
2014-05-23 09:17:14 +01:00
|
|
|
ssl=self.use_ssl,
|
2016-05-15 21:05:11 +02:00
|
|
|
tls=self.use_tls,
|
2015-03-16 16:52:25 +03:00
|
|
|
archive=self.archive,
|
|
|
|
|
folder=self.folder
|
2013-06-22 14:35:47 -07:00
|
|
|
)
|
2012-06-30 22:30:13 -07:00
|
|
|
conn.connect(self.username, self.password)
|
2014-05-28 21:24:46 -06:00
|
|
|
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)
|
2012-06-27 20:45:01 -07:00
|
|
|
elif self.type == 'pop3':
|
2012-06-30 22:30:13 -07:00
|
|
|
conn = Pop3Transport(
|
2013-06-22 14:35:47 -07:00
|
|
|
self.location,
|
|
|
|
|
port=self.port if self.port else None,
|
|
|
|
|
ssl=self.use_ssl
|
|
|
|
|
)
|
2012-06-30 22:30:13 -07:00
|
|
|
conn.connect(self.username, self.password)
|
2023-01-16 22:23:38 +01:00
|
|
|
elif self.type == 'office365':
|
|
|
|
|
conn = Office365Transport(
|
|
|
|
|
self.location,
|
|
|
|
|
self.username,
|
|
|
|
|
folder=self.folder,
|
|
|
|
|
archive=self.archive
|
|
|
|
|
)
|
|
|
|
|
conn.connect(self.client_id, self.client_secret, self.tenant_id)
|
2012-06-30 22:30:13 -07:00
|
|
|
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)
|
2012-06-27 20:45:01 -07:00
|
|
|
return conn
|
|
|
|
|
|
2012-10-08 03:07:30 +00:00
|
|
|
def process_incoming_message(self, message):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Process a message incoming to this mailbox."""
|
2012-10-27 19:03:15 -07:00
|
|
|
msg = self._process_message(message)
|
2017-05-09 23:28:19 +02:00
|
|
|
if msg is None:
|
|
|
|
|
return None
|
2012-10-27 18:51:41 -07:00
|
|
|
msg.outgoing = False
|
|
|
|
|
msg.save()
|
2013-04-11 15:56:22 +04:00
|
|
|
|
2015-08-04 16:14:29 +02:00
|
|
|
message_received.send(sender=self, message=msg)
|
2015-08-14 14:48:27 +02:00
|
|
|
|
2012-10-27 18:51:41 -07:00
|
|
|
return msg
|
|
|
|
|
|
|
|
|
|
def record_outgoing_message(self, message):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Record an outgoing message associated with this mailbox."""
|
2012-10-27 19:03:15 -07:00
|
|
|
msg = self._process_message(message)
|
2017-05-09 23:28:19 +02:00
|
|
|
if msg is None:
|
|
|
|
|
return None
|
2012-10-27 18:51:41 -07:00
|
|
|
msg.outgoing = True
|
|
|
|
|
msg.save()
|
|
|
|
|
return msg
|
|
|
|
|
|
2013-06-23 20:27:50 -07:00
|
|
|
def _get_dehydrated_message(self, msg, record):
|
2016-05-14 22:06:32 -07:00
|
|
|
settings = utils.get_settings()
|
|
|
|
|
|
2013-03-11 21:00:53 -07:00
|
|
|
new = EmailMessage()
|
2020-05-29 15:22:01 +02:00
|
|
|
if (
|
|
|
|
|
msg.is_multipart()
|
|
|
|
|
and not 'attachment' in msg.get('Content-Disposition', '')
|
|
|
|
|
):
|
2013-06-23 20:27:50 -07:00
|
|
|
for header, value in msg.items():
|
|
|
|
|
new[header] = value
|
|
|
|
|
for part in msg.get_payload():
|
|
|
|
|
new.attach(
|
|
|
|
|
self._get_dehydrated_message(part, record)
|
2013-03-11 21:00:53 -07:00
|
|
|
)
|
2013-06-23 20:27:50 -07:00
|
|
|
elif (
|
2016-05-14 22:06:32 -07:00
|
|
|
settings['strip_unallowed_mimetypes']
|
|
|
|
|
and not msg.get_content_type() in settings['allowed_mimetypes']
|
2013-06-23 20:27:50 -07:00
|
|
|
):
|
|
|
|
|
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']
|
2016-05-14 22:06:32 -07:00
|
|
|
new[settings['altered_message_header']] = (
|
2013-06-23 20:27:50 -07:00
|
|
|
'Stripped; Content type %s not allowed' % (
|
|
|
|
|
msg.get_content_type()
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
new.set_payload('')
|
2015-07-07 23:09:41 -07:00
|
|
|
elif (
|
2016-05-14 22:06:32 -07:00
|
|
|
(
|
|
|
|
|
msg.get_content_type() not in settings['text_stored_mimetypes']
|
|
|
|
|
) or
|
2015-07-07 23:09:41 -07:00
|
|
|
('attachment' in msg.get('Content-Disposition', ''))
|
|
|
|
|
):
|
2016-05-18 12:57:56 -07:00
|
|
|
filename = None
|
|
|
|
|
raw_filename = msg.get_filename()
|
|
|
|
|
if raw_filename:
|
|
|
|
|
filename = utils.convert_header_to_unicode(raw_filename)
|
2013-06-23 20:27:50 -07:00
|
|
|
if not filename:
|
|
|
|
|
extension = mimetypes.guess_extension(msg.get_content_type())
|
|
|
|
|
else:
|
|
|
|
|
_, extension = os.path.splitext(filename)
|
2013-08-03 10:53:38 -05:00
|
|
|
if not extension:
|
|
|
|
|
extension = '.bin'
|
2013-06-23 20:27:50 -07:00
|
|
|
|
|
|
|
|
attachment = MessageAttachment()
|
2013-07-26 22:13:29 -07:00
|
|
|
|
2020-05-29 15:22:01 +02:00
|
|
|
if msg.get_content_type() == 'message/rfc822':
|
|
|
|
|
attachment_payloads = msg.get_payload()
|
|
|
|
|
if len(attachment_payloads) != 1:
|
|
|
|
|
raise AssertionError(
|
|
|
|
|
"Attachment of type 'message/rfc822' "
|
|
|
|
|
'must have exactly 1 payload.'
|
|
|
|
|
)
|
|
|
|
|
attachment_payload = attachment_payloads[0].as_bytes()
|
|
|
|
|
else:
|
|
|
|
|
attachment_payload = msg.get_payload(decode=True)
|
2013-06-23 20:27:50 -07:00
|
|
|
attachment.document.save(
|
|
|
|
|
uuid.uuid4().hex + extension,
|
2013-07-26 22:13:29 -07:00
|
|
|
ContentFile(
|
2019-10-15 05:31:13 +02:00
|
|
|
BytesIO(
|
2020-05-29 15:22:01 +02:00
|
|
|
attachment_payload
|
2013-07-26 22:13:29 -07:00
|
|
|
).getvalue()
|
|
|
|
|
)
|
2013-06-23 20:27:50 -07:00
|
|
|
)
|
|
|
|
|
attachment.message = record
|
|
|
|
|
for key, value in msg.items():
|
|
|
|
|
attachment[key] = value
|
|
|
|
|
attachment.save()
|
|
|
|
|
|
|
|
|
|
placeholder = EmailMessage()
|
2016-05-14 22:06:32 -07:00
|
|
|
placeholder[
|
|
|
|
|
settings['attachment_interpolation_header']
|
|
|
|
|
] = str(attachment.pk)
|
2013-06-23 20:27:50 -07:00
|
|
|
new = placeholder
|
|
|
|
|
else:
|
2014-04-24 15:28:54 -07:00
|
|
|
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)
|
2014-11-15 22:15:25 -08:00
|
|
|
except LookupError:
|
2014-11-15 22:18:19 -08:00
|
|
|
logger.warning(
|
|
|
|
|
"Unknown encoding %s; interpreting as ASCII!",
|
2014-11-15 22:15:25 -08:00
|
|
|
content_charset
|
|
|
|
|
)
|
|
|
|
|
msg.set_payload(
|
|
|
|
|
msg.get_payload(decode=True).decode(
|
|
|
|
|
'ascii',
|
|
|
|
|
'ignore'
|
|
|
|
|
)
|
|
|
|
|
)
|
2014-11-15 22:18:19 -08:00
|
|
|
except ValueError:
|
|
|
|
|
logger.warning(
|
2016-04-04 21:28:11 +02:00
|
|
|
"Decoding error encountered; interpreting %s as ASCII!",
|
2014-11-15 22:18:19 -08:00
|
|
|
content_charset
|
|
|
|
|
)
|
2014-04-24 15:28:54 -07:00
|
|
|
msg.set_payload(
|
|
|
|
|
msg.get_payload(decode=True).decode(
|
2016-04-04 21:28:11 +02:00
|
|
|
'ascii',
|
2014-04-24 15:28:54 -07:00
|
|
|
'ignore'
|
|
|
|
|
)
|
|
|
|
|
)
|
2013-07-26 22:13:29 -07:00
|
|
|
new = msg
|
2013-03-11 21:00:53 -07:00
|
|
|
return new
|
|
|
|
|
|
2012-10-27 18:51:41 -07:00
|
|
|
def _process_message(self, message):
|
2012-10-08 03:07:30 +00:00
|
|
|
msg = Message()
|
2018-03-09 19:10:07 +01:00
|
|
|
msg._email_object = message
|
2016-05-14 22:06:32 -07:00
|
|
|
settings = utils.get_settings()
|
|
|
|
|
|
|
|
|
|
if settings['store_original_message']:
|
2017-07-09 20:36:37 +02:00
|
|
|
self._process_save_original_message(message, msg)
|
2012-10-08 03:07:30 +00:00
|
|
|
msg.mailbox = self
|
2013-07-25 18:47:49 +01:00
|
|
|
if 'subject' in message:
|
2016-05-14 22:06:32 -07:00
|
|
|
msg.subject = (
|
|
|
|
|
utils.convert_header_to_unicode(message['subject'])[0:255]
|
|
|
|
|
)
|
2013-07-25 18:47:49 +01:00
|
|
|
if 'message-id' in message:
|
2016-01-30 22:35:20 -08:00
|
|
|
msg.message_id = message['message-id'][0:255].strip()
|
2013-07-25 18:47:49 +01:00
|
|
|
if 'from' in message:
|
2016-05-14 22:06:32 -07:00
|
|
|
msg.from_header = utils.convert_header_to_unicode(message['from'])
|
2013-07-25 18:47:49 +01:00
|
|
|
if 'to' in message:
|
2016-05-14 22:06:32 -07:00
|
|
|
msg.to_header = utils.convert_header_to_unicode(message['to'])
|
2014-11-10 19:20:17 -08:00
|
|
|
elif 'Delivered-To' in message:
|
2016-05-14 23:37:49 -07:00
|
|
|
msg.to_header = utils.convert_header_to_unicode(
|
|
|
|
|
message['Delivered-To']
|
|
|
|
|
)
|
2013-06-23 20:27:50 -07:00
|
|
|
msg.save()
|
|
|
|
|
message = self._get_dehydrated_message(message, msg)
|
2017-05-09 23:28:19 +02:00
|
|
|
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)
|
2012-10-09 05:52:04 +00:00
|
|
|
if message['in-reply-to']:
|
|
|
|
|
try:
|
2013-06-22 14:35:47 -07:00
|
|
|
msg.in_reply_to = Message.objects.filter(
|
2016-01-30 22:35:20 -08:00
|
|
|
message_id=message['in-reply-to'].strip()
|
2013-06-22 14:35:47 -07:00
|
|
|
)[0]
|
2012-10-09 05:52:04 +00:00
|
|
|
except IndexError:
|
|
|
|
|
pass
|
2012-10-08 03:07:30 +00:00
|
|
|
msg.save()
|
|
|
|
|
return msg
|
|
|
|
|
|
2017-07-09 20:36:37 +02:00
|
|
|
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:
|
2017-07-09 20:47:48 +02:00
|
|
|
fp.write(message.as_string().encode('utf-8'))
|
2017-07-09 20:36:37 +02:00
|
|
|
msg.eml.save(
|
2019-10-15 05:31:13 +02:00
|
|
|
"{}.eml.gz".format(uuid.uuid4()),
|
2017-07-09 20:36:37 +02:00
|
|
|
File(fp_tmp),
|
|
|
|
|
save=False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
msg.eml.save(
|
|
|
|
|
'%s.eml' % uuid.uuid4(),
|
|
|
|
|
ContentFile(message.as_string()),
|
|
|
|
|
save=False
|
|
|
|
|
)
|
|
|
|
|
|
2015-07-07 22:22:15 -07:00
|
|
|
def get_new_mail(self, condition=None):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Connect to this transport and fetch new messages."""
|
2012-10-08 15:30:32 +00:00
|
|
|
connection = self.get_connection()
|
|
|
|
|
if not connection:
|
2018-03-09 19:09:45 +01:00
|
|
|
return
|
2015-07-07 22:30:53 -07:00
|
|
|
for message in connection.get_message(condition):
|
2012-10-08 03:07:30 +00:00
|
|
|
msg = self.process_incoming_message(message)
|
2023-12-10 04:01:53 +01:00
|
|
|
if msg is not None:
|
2018-03-09 19:09:45 +01:00
|
|
|
yield msg
|
2016-08-16 01:00:26 +02:00
|
|
|
self.last_polling = now()
|
2016-08-16 01:10:55 +02:00
|
|
|
if django.VERSION >= (1, 5): # Django 1.5 introduces update_fields
|
|
|
|
|
self.save(update_fields=['last_polling'])
|
|
|
|
|
else:
|
|
|
|
|
self.save()
|
2012-06-27 20:45:01 -07:00
|
|
|
|
2023-01-24 06:08:47 -08:00
|
|
|
@staticmethod
|
|
|
|
|
def get_new_mail_all_mailboxes(args=None):
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2012-10-09 05:52:04 +00:00
|
|
|
class IncomingMessageManager(models.Manager):
|
2014-09-06 16:31:12 -07:00
|
|
|
def get_queryset(self):
|
2019-10-15 05:31:13 +02:00
|
|
|
return super().get_queryset().filter(
|
2012-10-09 05:52:04 +00:00
|
|
|
outgoing=False,
|
|
|
|
|
)
|
|
|
|
|
|
2013-06-22 14:35:47 -07:00
|
|
|
|
2012-10-09 05:52:04 +00:00
|
|
|
class OutgoingMessageManager(models.Manager):
|
2014-09-06 16:31:12 -07:00
|
|
|
def get_queryset(self):
|
2019-10-15 05:31:13 +02:00
|
|
|
return super().get_queryset().filter(
|
2012-10-09 05:52:04 +00:00
|
|
|
outgoing=True,
|
|
|
|
|
)
|
|
|
|
|
|
2013-06-22 14:35:47 -07:00
|
|
|
|
2012-11-27 19:01:49 -08:00
|
|
|
class UnreadMessageManager(models.Manager):
|
2014-09-06 16:31:12 -07:00
|
|
|
def get_queryset(self):
|
2019-10-15 05:31:13 +02:00
|
|
|
return super().get_queryset().filter(
|
2012-11-27 19:01:49 -08:00
|
|
|
read=None
|
|
|
|
|
)
|
|
|
|
|
|
2013-06-22 14:35:47 -07:00
|
|
|
|
2012-06-27 20:45:01 -07:00
|
|
|
class Message(models.Model):
|
2014-08-29 15:40:22 -03:00
|
|
|
mailbox = models.ForeignKey(
|
|
|
|
|
Mailbox,
|
|
|
|
|
related_name='messages',
|
2019-10-15 05:31:13 +02:00
|
|
|
verbose_name=_('Mailbox'),
|
2017-12-05 18:53:18 +03:00
|
|
|
on_delete=models.CASCADE
|
2014-08-29 15:40:22 -03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
subject = models.CharField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Subject'),
|
2014-08-29 15:40:22 -03:00
|
|
|
max_length=255
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
message_id = models.CharField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Message ID'),
|
2014-08-29 15:40:22 -03:00
|
|
|
max_length=255
|
|
|
|
|
)
|
|
|
|
|
|
2012-10-09 05:52:04 +00:00
|
|
|
in_reply_to = models.ForeignKey(
|
2013-06-22 14:35:47 -07:00
|
|
|
'django_mailbox.Message',
|
2012-10-09 05:52:04 +00:00
|
|
|
related_name='replies',
|
|
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
2019-10-15 05:31:13 +02:00
|
|
|
verbose_name=_('In reply to'),
|
2017-12-05 18:53:18 +03:00
|
|
|
on_delete=models.CASCADE
|
2012-10-09 05:52:04 +00:00
|
|
|
)
|
2014-08-29 15:40:22 -03:00
|
|
|
|
2012-10-27 17:46:48 -07:00
|
|
|
from_header = models.CharField(
|
2014-08-29 15:40:22 -03:00
|
|
|
_('From header'),
|
2012-10-27 17:46:48 -07:00
|
|
|
max_length=255,
|
|
|
|
|
)
|
2014-08-29 15:40:22 -03:00
|
|
|
|
|
|
|
|
to_header = models.TextField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('To header'),
|
2014-08-29 15:40:22 -03:00
|
|
|
)
|
|
|
|
|
|
2012-10-09 05:52:04 +00:00
|
|
|
outgoing = models.BooleanField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Outgoing'),
|
2012-10-09 05:52:04 +00:00
|
|
|
default=False,
|
|
|
|
|
blank=True,
|
|
|
|
|
)
|
2012-06-27 20:45:01 -07:00
|
|
|
|
2014-08-29 15:40:22 -03:00
|
|
|
body = models.TextField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Body'),
|
2014-08-29 15:40:22 -03:00
|
|
|
)
|
|
|
|
|
|
2013-07-26 22:13:29 -07:00
|
|
|
encoded = models.BooleanField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Encoded'),
|
2013-07-26 22:13:29 -07:00
|
|
|
default=False,
|
2014-08-29 15:40:22 -03:00
|
|
|
help_text=_('True if the e-mail body is Base64 encoded'),
|
2013-07-26 22:13:29 -07:00
|
|
|
)
|
2012-06-27 20:45:01 -07:00
|
|
|
|
2012-10-09 05:52:04 +00:00
|
|
|
processed = models.DateTimeField(
|
2014-08-29 15:40:22 -03:00
|
|
|
_('Processed'),
|
2012-10-09 05:52:04 +00:00
|
|
|
auto_now_add=True
|
|
|
|
|
)
|
2014-08-29 15:40:22 -03:00
|
|
|
|
2012-11-27 19:01:49 -08:00
|
|
|
read = models.DateTimeField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Read'),
|
2012-11-27 19:01:49 -08:00
|
|
|
default=None,
|
|
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
)
|
2012-10-09 05:52:04 +00:00
|
|
|
|
2015-03-15 02:16:27 +01:00
|
|
|
eml = models.FileField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Raw message contents'),
|
2015-03-15 02:16:27 +01:00
|
|
|
null=True,
|
2023-12-17 06:27:45 +01:00
|
|
|
blank=True,
|
2015-03-18 09:56:52 +01:00
|
|
|
upload_to="messages",
|
2019-10-15 05:31:13 +02:00
|
|
|
help_text=_('Original full content of message')
|
2015-03-15 02:16:27 +01:00
|
|
|
)
|
2012-10-09 05:52:04 +00:00
|
|
|
objects = models.Manager()
|
2012-11-27 19:01:49 -08:00
|
|
|
unread_messages = UnreadMessageManager()
|
2012-10-09 05:52:04 +00:00
|
|
|
incoming_messages = IncomingMessageManager()
|
|
|
|
|
outgoing_messages = OutgoingMessageManager()
|
|
|
|
|
|
2012-10-27 17:46:48 -07:00
|
|
|
@property
|
|
|
|
|
def address(self):
|
|
|
|
|
"""Property allowing one to get the relevant address(es).
|
2013-06-22 14:35:47 -07:00
|
|
|
|
2012-10-27 17:46:48 -07:00
|
|
|
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.
|
2013-06-22 14:35:47 -07:00
|
|
|
|
2012-10-27 17:46:48 -07:00
|
|
|
"""
|
2013-07-19 23:39:41 +08:00
|
|
|
addresses = []
|
2013-07-26 22:13:29 -07:00
|
|
|
addresses = self.to_addresses + self.from_address
|
2013-07-19 23:39:41 +08:00
|
|
|
return addresses
|
2012-10-27 17:46:48 -07:00
|
|
|
|
2012-10-09 05:52:04 +00:00
|
|
|
@property
|
|
|
|
|
def from_address(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""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`.
|
|
|
|
|
|
|
|
|
|
"""
|
2013-07-19 23:39:41 +08:00
|
|
|
if self.from_header:
|
|
|
|
|
return [parseaddr(self.from_header)[1].lower()]
|
|
|
|
|
else:
|
|
|
|
|
return []
|
2012-10-09 05:52:04 +00:00
|
|
|
|
|
|
|
|
@property
|
2012-10-27 17:46:48 -07:00
|
|
|
def to_addresses(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns a list of addresses to which this message was sent."""
|
2012-10-27 17:46:48 -07:00
|
|
|
addresses = []
|
|
|
|
|
for address in self.to_header.split(','):
|
2013-07-19 23:39:41 +08:00
|
|
|
if address:
|
|
|
|
|
addresses.append(
|
|
|
|
|
parseaddr(
|
|
|
|
|
address
|
|
|
|
|
)[1].lower()
|
|
|
|
|
)
|
2012-10-27 17:46:48 -07:00
|
|
|
return addresses
|
2012-06-27 20:45:01 -07:00
|
|
|
|
2012-10-27 18:51:41 -07:00
|
|
|
def reply(self, message):
|
2012-10-28 11:29:56 -07:00
|
|
|
"""Sends a message as a reply to this message instance.
|
2013-06-22 14:35:47 -07:00
|
|
|
|
2012-10-28 11:29:56 -07:00
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
"""
|
2015-03-18 10:11:34 +01:00
|
|
|
if not message.from_email:
|
2015-03-14 18:23:19 +01:00
|
|
|
if self.mailbox.from_email:
|
|
|
|
|
message.from_email = self.mailbox.from_email
|
|
|
|
|
else:
|
2016-05-14 22:06:32 -07:00
|
|
|
message.from_email = django_settings.DEFAULT_FROM_EMAIL
|
2012-10-28 11:33:06 -07:00
|
|
|
message.extra_headers['Message-ID'] = make_msgid()
|
2012-10-28 11:31:31 -07:00
|
|
|
message.extra_headers['Date'] = formatdate()
|
2016-01-30 22:35:20 -08:00
|
|
|
message.extra_headers['In-Reply-To'] = self.message_id.strip()
|
2012-10-28 11:34:04 -07:00
|
|
|
message.send()
|
2012-10-27 18:51:41 -07:00
|
|
|
return self.mailbox.record_outgoing_message(
|
2013-06-22 14:35:47 -07:00
|
|
|
email.message_from_string(
|
|
|
|
|
message.message().as_string()
|
2012-10-27 18:51:41 -07:00
|
|
|
)
|
2013-06-22 14:35:47 -07:00
|
|
|
)
|
2012-10-27 18:51:41 -07:00
|
|
|
|
2013-06-11 22:32:13 -07:00
|
|
|
@property
|
|
|
|
|
def text(self):
|
2014-09-01 15:30:30 -03:00
|
|
|
"""
|
|
|
|
|
Returns the message body matching content type 'text/plain'.
|
|
|
|
|
"""
|
2016-05-14 22:06:32 -07:00
|
|
|
return utils.get_body_from_message(
|
2014-09-01 15:30:30 -03:00
|
|
|
self.get_email_object(), 'text', 'plain'
|
|
|
|
|
).replace('=\n', '').strip()
|
2013-06-11 22:32:13 -07:00
|
|
|
|
2014-08-29 17:24:38 -03:00
|
|
|
@property
|
|
|
|
|
def html(self):
|
2014-09-01 15:30:30 -03:00
|
|
|
"""
|
|
|
|
|
Returns the message body matching content type 'text/html'.
|
|
|
|
|
"""
|
2016-05-14 22:06:32 -07:00
|
|
|
return utils.get_body_from_message(
|
2014-09-01 15:30:30 -03:00
|
|
|
self.get_email_object(), 'text', 'html'
|
|
|
|
|
).replace('\n', '').strip()
|
2013-06-23 20:27:50 -07:00
|
|
|
|
|
|
|
|
def _rehydrate(self, msg):
|
|
|
|
|
new = EmailMessage()
|
2016-05-14 22:06:32 -07:00
|
|
|
settings = utils.get_settings()
|
2013-06-23 20:27:50 -07:00
|
|
|
if msg.is_multipart():
|
|
|
|
|
for header, value in msg.items():
|
|
|
|
|
new[header] = value
|
|
|
|
|
for part in msg.get_payload():
|
|
|
|
|
new.attach(
|
|
|
|
|
self._rehydrate(part)
|
|
|
|
|
)
|
2016-05-14 22:06:32 -07:00
|
|
|
elif settings['attachment_interpolation_header'] in msg.keys():
|
2013-06-23 20:27:50 -07:00
|
|
|
try:
|
|
|
|
|
attachment = MessageAttachment.objects.get(
|
2016-05-14 22:06:32 -07:00
|
|
|
pk=msg[settings['attachment_interpolation_header']]
|
2013-06-23 20:27:50 -07:00
|
|
|
)
|
|
|
|
|
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
|
2023-08-11 10:33:45 -03:00
|
|
|
with open(attachment.document.path, 'rb') as f:
|
|
|
|
|
output = BytesIO()
|
|
|
|
|
encode_quopri(
|
|
|
|
|
BytesIO(
|
|
|
|
|
f.read()
|
|
|
|
|
),
|
|
|
|
|
output,
|
|
|
|
|
quotetabs=True,
|
|
|
|
|
header=False,
|
|
|
|
|
)
|
|
|
|
|
new.set_payload(
|
|
|
|
|
output.getvalue().decode().replace(' ', '=20')
|
|
|
|
|
)
|
2013-06-23 20:27:50 -07:00
|
|
|
del new['Content-Transfer-Encoding']
|
|
|
|
|
new['Content-Transfer-Encoding'] = 'quoted-printable'
|
|
|
|
|
else:
|
2023-08-11 10:33:45 -03:00
|
|
|
with open(attachment.document.path, 'rb') as f:
|
|
|
|
|
new.set_payload(f.read())
|
2013-06-23 20:27:50 -07:00
|
|
|
del new['Content-Transfer-Encoding']
|
|
|
|
|
encode_base64(new)
|
|
|
|
|
except MessageAttachment.DoesNotExist:
|
2016-05-14 22:06:32 -07:00
|
|
|
new[settings['altered_message_header']] = (
|
2013-06-23 20:27:50 -07:00
|
|
|
'Missing; Attachment %s not found' % (
|
2016-05-14 22:06:32 -07:00
|
|
|
msg[settings['attachment_interpolation_header']]
|
2013-06-23 20:27:50 -07:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
new.set_payload('')
|
|
|
|
|
else:
|
|
|
|
|
for header, value in msg.items():
|
|
|
|
|
new[header] = value
|
2013-07-26 22:13:29 -07:00
|
|
|
new.set_payload(
|
|
|
|
|
msg.get_payload()
|
|
|
|
|
)
|
2013-06-23 20:27:50 -07:00
|
|
|
return new
|
2013-01-20 14:27:15 -08:00
|
|
|
|
2013-07-26 22:13:29 -07:00
|
|
|
def get_body(self):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns the `body` field of this record.
|
|
|
|
|
|
|
|
|
|
This will automatically base64-decode the message contents
|
|
|
|
|
if they are encoded as such.
|
|
|
|
|
|
|
|
|
|
"""
|
2013-07-26 22:13:29 -07:00
|
|
|
if self.encoded:
|
|
|
|
|
return base64.b64decode(self.body.encode('ascii'))
|
|
|
|
|
return self.body.encode('utf-8')
|
2013-06-22 15:06:53 -07:00
|
|
|
|
2013-07-26 22:13:29 -07:00
|
|
|
def set_body(self, body):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""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.
|
|
|
|
|
|
|
|
|
|
"""
|
2013-07-26 22:13:29 -07:00
|
|
|
self.encoded = True
|
2019-10-15 05:31:13 +02:00
|
|
|
self.body = base64.b64encode(body.encode('utf-8')).decode('ascii')
|
2013-06-22 15:06:53 -07:00
|
|
|
|
2013-07-26 22:13:29 -07:00
|
|
|
def get_email_object(self):
|
2020-12-07 03:10:15 +01:00
|
|
|
"""Returns an `email.message.EmailMessage` instance representing the
|
2015-04-20 20:54:46 -07:00
|
|
|
contents of this message and all attachments.
|
|
|
|
|
|
2023-12-10 04:01:53 +01:00
|
|
|
See [email.message.EmailMessage]_ for more information like what methods
|
2020-12-07 03:10:15 +01:00
|
|
|
and properties are available on `email.message.EmailMessage` instances.
|
2015-04-20 20:54:46 -07:00
|
|
|
|
|
|
|
|
.. 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.
|
|
|
|
|
|
2020-12-07 03:10:15 +01:00
|
|
|
.. [email.message.EmailMessage] Python's `email.message.EmailMessage` docs
|
|
|
|
|
(https://docs.python.org/3/library/email.message.html)
|
2015-04-20 20:54:46 -07:00
|
|
|
|
|
|
|
|
"""
|
2018-03-09 19:10:07 +01:00
|
|
|
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:
|
2023-08-11 10:33:45 -03:00
|
|
|
with self.eml.open():
|
|
|
|
|
body = self.eml.file.read()
|
2017-07-09 20:36:37 +02:00
|
|
|
else:
|
2018-03-09 19:10:07 +01:00
|
|
|
body = self.get_body()
|
2019-10-15 05:31:13 +02:00
|
|
|
flat = email.message_from_bytes(body)
|
2018-03-09 19:10:07 +01:00
|
|
|
self._email_object = self._rehydrate(flat)
|
|
|
|
|
return self._email_object
|
2012-06-27 20:45:01 -07:00
|
|
|
|
2013-02-03 14:52:42 -08:00
|
|
|
def delete(self, *args, **kwargs):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Delete this message and all stored attachments."""
|
2013-02-03 14:52:42 -08:00
|
|
|
for attachment in self.attachments.all():
|
2013-06-06 01:03:16 -07:00
|
|
|
# This attachment is attached only to this message.
|
|
|
|
|
attachment.delete()
|
2019-10-15 05:31:13 +02:00
|
|
|
return super().delete(*args, **kwargs)
|
2013-02-03 14:52:42 -08:00
|
|
|
|
2017-07-09 04:02:38 +02:00
|
|
|
def __str__(self):
|
2013-01-20 13:44:49 -08:00
|
|
|
return self.subject
|
2013-06-06 01:03:16 -07:00
|
|
|
|
2017-07-09 13:34:50 +03:00
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _('E-mail message')
|
|
|
|
|
verbose_name_plural = _('E-mail messages')
|
|
|
|
|
|
2013-06-06 01:03:16 -07:00
|
|
|
|
|
|
|
|
class MessageAttachment(models.Model):
|
|
|
|
|
message = models.ForeignKey(
|
|
|
|
|
Message,
|
|
|
|
|
related_name='attachments',
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
2014-08-29 15:40:22 -03:00
|
|
|
verbose_name=_('Message'),
|
2017-12-05 18:53:18 +03:00
|
|
|
on_delete=models.CASCADE
|
2014-08-29 15:40:22 -03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
headers = models.TextField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Headers'),
|
2014-08-29 15:40:22 -03:00
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
document = models.FileField(
|
2019-10-15 05:31:13 +02:00
|
|
|
_('Document'),
|
2016-05-14 22:06:32 -07:00
|
|
|
upload_to=utils.get_attachment_save_path,
|
2013-06-06 01:03:16 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def delete(self, *args, **kwargs):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Deletes the attachment."""
|
2013-06-06 01:03:16 -07:00
|
|
|
self.document.delete()
|
2019-10-15 05:31:13 +02:00
|
|
|
return super().delete(*args, **kwargs)
|
2013-06-06 01:03:16 -07:00
|
|
|
|
2013-06-23 20:27:50 -07:00
|
|
|
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):
|
2020-05-29 15:22:01 +02:00
|
|
|
if email_object._payload is None:
|
|
|
|
|
# otherwise it breaks in
|
|
|
|
|
# lib/python3.x/email/generator.py:_handle_message,
|
|
|
|
|
# line: "self._fp.write(payload)"
|
|
|
|
|
email_object._payload = ""
|
2013-06-23 20:27:50 -07:00
|
|
|
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):
|
2015-04-20 20:54:46 -07:00
|
|
|
"""Returns the original filename of this attachment."""
|
2013-07-19 23:39:41 +08:00
|
|
|
file_name = self._get_rehydrated_headers().get_filename()
|
2019-10-15 05:31:13 +02:00
|
|
|
if isinstance(file_name, str):
|
2016-05-14 22:29:23 -07:00
|
|
|
result = utils.convert_header_to_unicode(file_name)
|
|
|
|
|
if result is None:
|
|
|
|
|
return file_name
|
|
|
|
|
return result
|
2013-07-19 23:39:41 +08:00
|
|
|
else:
|
2013-07-26 20:15:39 -07:00
|
|
|
return None
|
2013-06-23 20:27:50 -07:00
|
|
|
|
|
|
|
|
def items(self):
|
|
|
|
|
return self._get_rehydrated_headers().items()
|
|
|
|
|
|
2013-07-26 20:15:39 -07:00
|
|
|
def __getitem__(self, name):
|
|
|
|
|
value = self._get_rehydrated_headers()[name]
|
|
|
|
|
if value is None:
|
|
|
|
|
raise KeyError('Header %s does not exist' % name)
|
|
|
|
|
return value
|
2013-06-23 20:27:50 -07:00
|
|
|
|
2017-07-09 04:02:38 +02:00
|
|
|
def __str__(self):
|
2023-12-24 12:53:18 +01:00
|
|
|
return f'{self.message}: {self.document.url if self.document else None}'
|
|
|
|
|
|
2017-07-09 13:34:50 +03:00
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _('Message attachment')
|
|
|
|
|
verbose_name_plural = _('Message attachments')
|