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

248 lines
10 KiB
Python

from optparse import make_option
import sys
import email
import email.errors
import email.utils
import email.header
import logging
import re
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.template.defaultfilters import slugify
from django.db.models.query import Q
from docbow_project.docbow import models, timestamp, utils
from docbow_project.docbow.email_utils import u2u_decode
from django_journal import record, error_record
try:
from django.db.transaction import atomic
except ImportError:
from django.db.transaction import commit_on_success as 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
'''
option_list = BaseCommand.option_list + (
make_option("--sender"),
make_option("--file"))
all_recipients = []
filenames = []
message_id = None
subject = ''
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_file(file(options['file']))
else:
mail = email.message_from_file(sys.stdin)
except email.errors.MessageParseError, e:
self.error('5.6.0 Error parsing message', exite_code=1)
try:
self.handle_mail(mail, args, **options)
except Exception, 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):
print >>sys.stderr, 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(unicode(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', None)
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, 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')
description = unicode(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)))
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 = recipients
document.to_list = 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()
blob = document.timestamp_blob()
try:
tst, gentime = timestamp.timestamp_json(blob)
document._timestamp = tst
document.date = make_aware(gentime, utc) or document.date
document.save()
except:
pass
document.post()