docbow/docbow_project/pw/management/commands/sendmail.py

265 lines
11 KiB
Python

from datetime import datetime as dt
import sys
import mailbox
import email
import email.errors
import email.utils
import email.header
import logging
import re
import time
from django.conf import settings
from django.core.management.base import BaseCommand
import django.contrib.auth.models as auth_models
from django.core.files.base import ContentFile
from django.core.exceptions import MultipleObjectsReturned
from django.utils.encoding import force_text
from django.utils.timezone import utc, make_aware
from django.template.defaultfilters import slugify
from django.db.models.query import Q
from docbow_project.docbow import models, utils
from docbow_project.docbow.email_utils import u2u_decode
from django_journal.journal import record, error_record
from django.db.transaction import atomic
logger = logging.getLogger('docbow.mail_interface')
# modes
EXPEDITION = 'expedition'
ATTACHED_FILE = 'attached_file'
PRIVATE_PREFIX = 'Private - '
PRIVATE_SUFFIX = '-private'
class Command(BaseCommand):
args = ''
help = '''Convert a mail to a document send.
In case of failure the following return value is returned:
- 1 failure to parse the message on stdin
- 2 the mail is missing a subject
- 3 the subject could not be decoded
- 4 obligatory attributes or contents were missing
- 5 notifications mails could not be sent
- 6 missing Message-ID
- 7 sender not authorized
- 8 missing sender
'''
all_recipients = []
filenames = []
message_id = None
subject = ''
def add_arguments(self, parser):
parser.add_argument('recipient', type=str)
parser.add_argument("--sender")
parser.add_argument("--file")
def handle(self, *args, **options):
if not options.get('sender'):
self.error('5.6.0 No sender', exit_code=8)
self.setup_mailing_list_dict()
try:
if options.get('file'):
mail = email.message_from_binary_file(open(options['file'], 'rb'))
else:
mail = email.message_from_binary_file(sys.stdin.buffer)
except email.errors.MessageParseError as e:
self.error('5.6.0 Error parsing message', exit_code=1)
if settings.SENDMAIL_DEBUG_MBOX:
try:
mbox = mailbox.mbox(settings.SENDMAIL_DEBUG_MBOX)
mbox.add(mail)
except:
logger.exception('mbox exception')
try:
self.handle_mail(mail, (options['recipient'],), **options)
except Exception as e:
logger.exception('Unknown exception')
self.error('5.6.0 Internal error when handling the mail', exit_code=5)
def error(self, msg, exit_code=None, **kwargs):
sys.stderr.write(msg.format(**kwargs))
if hasattr(self, 'message_id'):
error_record(
'warning-smtp-interface',
'message {message_id} to {all_recipients} containing {filenames} with subject {subject} refused: '
+ msg,
message_id=repr(self.message_id),
all_recipients=', '.join(map(repr, self.all_recipients)),
filenames=', '.join(map(repr, self.filenames)),
subject=repr(self.subject),
**kwargs,
)
else:
error_record('warning-smtp-interface', 'message refused: ' + msg, **kwargs)
if exit_code:
sys.exit(exit_code)
def decode_filename(self, filename):
'''See if the filename contains encoded-word work around bugs in FileMakerPro'''
m = re.match(r'=\?(.*)\?(.*)\?(.*)\?=', filename)
if m:
result = []
for content, encoding in email.header.decode_header(filename):
result.append(force_text(content, encoding or 'ascii'))
return ''.join(result)
else:
return filename
def setup_mailing_list_dict(self):
self.mailing_lists = {}
for mailing_list in models.MailingList.objects.all():
self.mailing_lists[slugify(mailing_list.name)] = mailing_list
def resolve_username_for_list(self, username):
if not username.startswith('liste-'):
return None
return self.mailing_lists.get(username[len('liste-') :])
@atomic
def handle_mail(self, mail, mail_recipients, **options):
content_errors = []
attachments = []
recipients = []
mailing_list_recipients = []
description = u''
from_email = email.utils.parseaddr(options['sender'])[1]
if options.get('sender'):
try:
sender = auth_models.User.objects.filter(
Q(docbowprofile__is_guest=False) | Q(docbowprofile__isnull=True)
).get(username=options['sender'])
except auth_models.User.DoesNotExist:
self.error('5.6.0 Unknown sender %r' % options['sender'], exit_code=8)
else:
try:
sender = auth_models.User.objects.filter(
Q(docbowprofile__is_guest=False) | Q(docbowprofile__isnull=True)
).get(email=from_email)
except auth_models.User.DoesNotExist:
content_errors.append('No sender user have mail %r' % from_email)
except MultipleObjectsReturned:
content_errors.append('Too many sender users have mail %r' % from_email)
tos = mail.get_all('to', [])
ccs = mail.get_all('cc', [])
resent_tos = mail.get_all('resent-to', [])
resent_ccs = mail.get_all('resent-cc', [])
self.all_recipients = all_recipients = mail_recipients or [
b for a, b in email.utils.getaddresses(tos + ccs + resent_tos + resent_ccs)
]
self.message_id = mail.get('Message-ID', None)
if not self.message_id:
content_errors.append('Mail is missing a Message-ID')
try:
self.private = bool(int(mail.get('Private', 0)))
except ValueError:
content_errors.append('Private header is invalid: %r' % mail.get('Private'))
# determine the filetype
filetype = None
self.subject = subject = mail.get('Subject', '')
if subject.startswith(PRIVATE_PREFIX):
self.private = True
subject = subject[len(PRIVATE_PREFIX) :]
if not subject:
content_errors.append('Mail is missing a filetype subject')
else:
try:
subject = u2u_decode(subject)
except Exception as e:
content_errors.append('The subject cannot be decoded')
else:
try:
filetype = models.FileType.objects.get(name=subject)
except models.FileType.DoesNotExist:
content_errors.append('Unkown filetype %r in the subject' % subject)
for part in mail.walk():
filename = part.get_filename(None)
if part.get_content_type() == 'text/plain' and (
'Content-Disposition' not in part or 'inline' in part['Content-Disposition']
):
charset = part.get_content_charset('us-ascii')
for cset in (charset, 'iso-8859-15', 'utf-8'):
try:
description = force_text(part.get_payload(decode=True), cset)
except UnicodeDecodeError:
continue
break
else:
content_errors.append('Error decoding description')
continue
if filename:
filename = self.decode_filename(filename)
# be defensive, truncate at 230 characters !
filename = utils.truncate_filename(filename)
attachments.append((filename, part.get_payload(decode=True)))
for email_address in all_recipients:
username, domain = email_address.split('@', 1)
if username.endswith(PRIVATE_SUFFIX):
self.private = True
username = username[: -len(PRIVATE_SUFFIX)]
email_address = '%s@%s' % (username, domain)
# mailing list case
mailing_list = self.resolve_username_for_list(username)
if mailing_list is not None:
mailing_list_recipients.append(mailing_list)
continue
# classic user case
users = auth_models.User.objects.filter(
Q(docbowprofile__is_guest=False) | Q(docbowprofile__isnull=True)
).filter(email=email_address)
if users:
for user in users:
recipients.append(user)
else:
try:
user = auth_models.User.objects.get(username=username)
recipients.append(user)
except auth_models.User.DoesNotExist:
msg = 'Recipient %r is not an user of the platform' % username
content_errors.append(msg)
self.filenames = [a for a, b in attachments]
if not len(attachments):
content_errors.append('You must send at least one attached file')
if not len(all_recipients):
content_errors.append('You must have at least one recipient in your message.')
if content_errors:
msg = ['5.6.0 The email sent contains many errors:']
for error in content_errors:
msg.append(' - %s' % error)
self.error('\n'.join(msg), exit_code=4)
else:
record(
'smtp-received-document',
'message-id: {message_id} subject: {subject} to: {all_recipients} filenames: {filenames} private: {private}',
message_id=self.message_id,
subject=subject,
all_recipients=', '.join(map(repr, self.all_recipients)),
filenames=', '.join(map(repr, self.filenames)),
private=self.private,
)
document = models.Document(
sender=sender, comment=description, filetype=filetype, private=self.private
)
document.save()
document.to_user.set(recipients)
document.to_list.set(mailing_list_recipients)
for filename, payload in attachments:
content = ContentFile(payload)
attached_file = models.AttachedFile(document=document, name=filename)
attached_file.content.save(filename, content, save=False)
attached_file.save()
document._timestamp = time.time()
document.date = dt.fromtimestamp(document._timestamp, utc)
document.save()
document.post()