300 lines
12 KiB
Python
300 lines
12 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 urllib.request import urlopen
|
|
|
|
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.timezone import utc, make_aware
|
|
from django.utils.encoding import force_text
|
|
from django.template.defaultfilters import slugify
|
|
|
|
from docbow_project.docbow import models, utils
|
|
from docbow_project.docbow.email_utils import u2u_decode
|
|
from django_journal.journal import record
|
|
from django.db.transaction import atomic
|
|
|
|
from docbow_project.docbow import app_settings as docbow_app_settings
|
|
from docbow_project.pfwb import app_settings
|
|
|
|
logger = logging.getLogger('docbow.mail_interface')
|
|
|
|
# modes
|
|
EXPEDITION = 'expedition'
|
|
ATTACHED_FILE = 'attached_file'
|
|
|
|
|
|
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
|
|
|
|
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('7.7.1 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('7.7.1 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('7.7.1 Internal error when handling the mail', exit_code=5)
|
|
|
|
def error(self, msg, exit_code=None, **kwargs):
|
|
sys.stderr.write(msg.format(**kwargs) + '\n')
|
|
if hasattr(self, 'message_id'):
|
|
record(
|
|
'warning-smtp-interface',
|
|
'message {message_id} to {all_recipients} containing {filenames} refused: ' + msg,
|
|
message_id=self.message_id,
|
|
all_recipients=self.all_recipients,
|
|
filenames=self.filenames,
|
|
**kwargs,
|
|
)
|
|
else:
|
|
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 not from_email:
|
|
self.error('7.7.1 No sender', exit_code=8)
|
|
if from_email == app_settings.PFWB_SENDMAIL_TABELLIO_EXPEDITION_EMAIL:
|
|
mode = EXPEDITION
|
|
elif from_email == app_settings.PFWB_SENDMAIL_ATTACHED_FILE_EMAIL:
|
|
mode = ATTACHED_FILE
|
|
else:
|
|
self.error('Email unknown, not authorized to post', exit_code=7)
|
|
return
|
|
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:
|
|
self.error('7.7.1 Mail is missing a Message-ID', exit_code=6)
|
|
|
|
# determine the filetype
|
|
filetype = None
|
|
if mode == ATTACHED_FILE:
|
|
subject = mail.get('Subject', None)
|
|
if not subject:
|
|
self.error('7.7.1 Mail is missing a subject', exit_code=2)
|
|
try:
|
|
subject = u2u_decode(subject)
|
|
except Exception as e:
|
|
self.error('7.7.1 The subject cannot be decoded', exit_code=3)
|
|
try:
|
|
filetype = models.FileType.objects.get(name=subject)
|
|
except models.FileType.DoesNotExist:
|
|
record('warning', 'unknown filetype ' '{filetype}, using default filetype', filetype=subject)
|
|
else:
|
|
tabellio_doc_type = mail.get('x-tabellio-doc-type')
|
|
if tabellio_doc_type:
|
|
try:
|
|
filetype = models.FileType.objects.get(
|
|
tabelliodoctype__tabellio_doc_type=tabellio_doc_type
|
|
)
|
|
except models.FileType.DoesNotExist:
|
|
record(
|
|
'warning',
|
|
'unknown x-tabellio-doc-type ' '{tabellio_doc_type}, using default filetype',
|
|
tabellio_doc_type=tabellio_doc_type,
|
|
)
|
|
except models.FileType.MultipleObjectsReturned:
|
|
record(
|
|
'warning',
|
|
'unknown x-tabellio-doc-type ' '{tabellio_doc_type}, using default filetype',
|
|
tabellio_doc_type=tabellio_doc_type,
|
|
)
|
|
if filetype is None:
|
|
try:
|
|
filetype = models.FileType.objects.get(id=app_settings.PFWB_SENDMAIL_DEFAULT_TYPE_ID)
|
|
except models.FileType.DoesNotExist:
|
|
filetype, created = models.FileType.objects.get_or_create(
|
|
name=app_settings.PFWB_SENDMAIL_DEFAULT_TYPE_NAME
|
|
)
|
|
|
|
if mode == ATTACHED_FILE:
|
|
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')
|
|
description = force_text(part.get_payload(decode=True), charset)
|
|
|
|
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)))
|
|
else:
|
|
url = mail.get('x-tabellio-doc-url')
|
|
try:
|
|
stream = urlopen(url)
|
|
except Exception as e:
|
|
self.error('7.7.1 Unable to retrieve %r: %r' % (url, e), exit_code=3)
|
|
content = stream.read()
|
|
subject = mail.get('Subject', None)
|
|
if not subject:
|
|
self.error('7.7.1 Mail is missing a subject', exit_code=2)
|
|
try:
|
|
subject = u2u_decode(subject)
|
|
except Exception as e:
|
|
self.error('7.7.1 The subject cannot be decoded', exit_code=3)
|
|
try:
|
|
name = subject.split(':', 1)[1].strip()
|
|
# be defensive, truncate
|
|
name = name[: docbow_app_settings.TRUNCATE_FILENAME]
|
|
except IndexError:
|
|
self.error('7.7.1 Filename cannot be extracted from the subject', exit_code=3)
|
|
if not name.endswith('.pdf'):
|
|
name += '.pdf'
|
|
attachments.append((name, content))
|
|
|
|
for email_address in all_recipients:
|
|
username, domain = email_address.split('@', 1)
|
|
# 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
|
|
try:
|
|
user = auth_models.User.objects.get(username=username)
|
|
recipients.append(user)
|
|
except auth_models.User.DoesNotExist:
|
|
try:
|
|
users_qs = auth_models.User.objects.filter(email=email_address)
|
|
if not users_qs.count():
|
|
raise auth_models.User.DoesNotExist()
|
|
recipients.extend(users_qs.all())
|
|
except auth_models.User.DoesNotExist:
|
|
msg = 'Recipient %r is not an user of the platform' % email_address
|
|
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.')
|
|
try:
|
|
if mode == ATTACHED_FILE:
|
|
user_id = app_settings.PFWB_SENDMAIL_ATTACHED_FILE_USER_ID
|
|
else:
|
|
user_id = app_settings.PFWB_SENDMAIL_TABELLIO_EXPEDITION_USER_ID
|
|
sender = auth_models.User.objects.get(id=user_id)
|
|
except auth_models.User.DoesNotExist:
|
|
content_errors.append('No user match the sender user_id %s in mode ' '%s' % (user_id, mode))
|
|
if content_errors:
|
|
msg = ['7.7.1 The email sent contains many errors:']
|
|
for error in content_errors:
|
|
msg.append(' - %s' % error)
|
|
self.error('\n'.join(msg), exit_code=4)
|
|
else:
|
|
if mode == ATTACHED_FILE:
|
|
record(
|
|
'smtp-received-document',
|
|
'mode: {mode} message-id: {message_id} subject: {subject}',
|
|
mode=mode,
|
|
message_id=self.message_id,
|
|
subject=subject,
|
|
)
|
|
else:
|
|
record(
|
|
'smtp-received-document',
|
|
'mode: {mode} message-id: '
|
|
'{message_id} x-tabellio-doc-url: {x_tabellio_doc_url} '
|
|
'x-tabellio-doc-type: {x_tabellio_doc_type}',
|
|
mode=mode,
|
|
message_id=self.message_id,
|
|
subject=repr(subject),
|
|
x_tabellio_doc_url=url,
|
|
x_tabellio_doc_type=tabellio_doc_type,
|
|
)
|
|
document = models.Document(sender=sender, comment=description, filetype=filetype)
|
|
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()
|