diff --git a/esp/esp/dbmail/models.py b/esp/esp/dbmail/models.py index 6c42b2687b..0157687e1f 100644 --- a/esp/esp/dbmail/models.py +++ b/esp/esp/dbmail/models.py @@ -70,7 +70,7 @@ # https://support.google.com/a/answer/81126?visit_id=638428689824104778-3542874255&rd=1#subscriptions def send_mail(subject, message, from_email, recipient_list, fail_silently=False, bcc=None, return_path=settings.DEFAULT_EMAIL_ADDRESSES['bounces'], extra_headers={}, user=None, - *args, **kwargs): + attachments=[], *args, **kwargs): from_email = from_email.strip() # the from_email must match one of our DMARC domains/subdomains # or the email may be rejected by email clients @@ -109,7 +109,10 @@ def send_mail(subject, message, from_email, recipient_list, fail_silently=False, # Get whatever type of email connection Django provides. # Normally this will be SMTP, but it also has an in-memory backend for testing. - connection = get_connection(fail_silently=fail_silently, return_path=return_path) + connection = get_connection(fail_silently=fail_silently) + if hasattr(connection, 'return_path'): + connection.return_path=return_path + # Detect HTML tags in message and change content-type if they are found if '' in message: @@ -118,10 +121,12 @@ def send_mail(subject, message, from_email, recipient_list, fail_silently=False, text_only = re.sub('[ \t]+', ' ', strip_tags(message)) # Strip single spaces in the beginning of each line message_text = text_only.replace('\n ', '\n').strip() - msg = EmailMultiAlternatives(subject, message_text, from_email, recipients, bcc=bcc, connection=connection, headers=extra_headers) + msg = EmailMultiAlternatives(subject, message_text, from_email, recipients, bcc=bcc, connection=connection, + headers=extra_headers, attachments=attachments) msg.attach_alternative(message, "text/html") else: - msg = EmailMessage(subject, message, from_email, recipients, bcc=bcc, connection=connection, headers=extra_headers) + msg = EmailMessage(subject, message, from_email, recipients, bcc=bcc, connection=connection, + headers=extra_headers, attachments=attachments) msg.send() @@ -360,7 +365,8 @@ def process(self): if self.creator is not None: send_from = self.creator.get_email_sendto_address() else: - send_from = 'ESP Web Site ' + send_from = '"{} {}" <{}>'.format(settings.INSTITUTION_NAME, settings.ORGANIZATION_SHORT_NAME, + settings.DEFAULT_EMAIL_ADDRESSES['default']) users = self.recipients.getList(ESPUser).distinct() diff --git a/esp/mailgates/mailgate.py b/esp/mailgates/mailgate.py index 5841159a18..dfde815811 100755 --- a/esp/mailgates/mailgate.py +++ b/esp/mailgates/mailgate.py @@ -5,7 +5,7 @@ from __future__ import absolute_import from __future__ import print_function -import sys, os, email, re, smtplib, socket, sha, random +import sys, os, base64, email, hashlib, re, smtplib, socket, random from io import open new_path = '/'.join(sys.path[0].split('/')[:-1]) sys.path += [new_path] @@ -33,14 +33,11 @@ import django django.setup() -from esp.dbmail.models import EmailList +from esp.dbmail.models import EmailList, send_mail +from esp.users.models import ESPUser from django.conf import settings -host = socket.gethostname() import_location = 'esp.dbmail.receivers.' -MAIL_PATH = '/usr/sbin/sendmail' -server = smtplib.SMTP('localhost') -ARCHIVE = settings.DEFAULT_EMAIL_ADDRESSES['archive'] SUPPORT = settings.DEFAULT_EMAIL_ADDRESSES['support'] ORGANIZATION_NAME = settings.INSTITUTION_NAME + '_' + settings.ORGANIZATION_SHORT_NAME @@ -49,14 +46,22 @@ user = "UNKNOWN USER" -def send_mail(message): - p = os.popen("%s -i -t" % MAIL_PATH, 'w') - p.write(message) + +def extract_attachments(msg): + attachments = [] + for part in msg.iter_attachments(): + filename = part.get_filename() + content = part.get_payload(decode=True) + mimetype = part.get_content_type() + attachments.append((filename, content, mimetype)) + # TODO: check if content is null -- SendGrid fails ungracefully + return attachments + try: user = os.environ['LOCAL_PART'] - message = email.message_from_file(sys.stdin) + message = email.message_from_file(sys.stdin, policy=email.policy.default) handlers = EmailList.objects.all() @@ -74,63 +79,86 @@ def send_mail(message): instance.process(user, *match.groups(), **match.groupdict()) if not instance.send: + logger.info("Instance did not send") continue - if hasattr(instance, "direct_send") and instance.direct_send: - if message['Bcc']: - bcc_recipients = [x.strip() for x in message['Bcc'].split(',')] - del(message['Bcc']) - message['Bcc'] = ", ".join(bcc_recipients) + # Catch sender's message and grab the data fields (to, from, subject, body, and attachments) + data = dict() + # TODO: (1) sort out why the email_address.split() breaks when it's a list of users; (2) consider prepending the class code to the subject + # TODO: in the long term, it would be better to implement polymorphism so that class lists and individual user aliases both have `recipients` + if hasattr(instance, 'recipients'): + data['to'] = [x for x in instance.recipients if not x.endswith(settings.EMAIL_HOST_SENDER)] # TODO: make sure to expand the `to` field as needed so sendgrid doesn't just forward in a loop + elif hasattr(instance, 'message'): + data['to'] = instance.message['to'] + else: + raise TypeError("Unknown receiver type for `{}`".format(instance)) + data['from'] = message['from'].split(',') or '' + data['subject'] = message['subject'] or '' + data['body'] = '{}'.format(message.get_body(preferencelist=('html', 'plain')).get_content()) + data['attachments'] = extract_attachments(message) - send_mail(str(message)) - continue - del(message['to']) - del(message['cc']) - message['X-ESP-SENDER'] = 'version 2' - message['X-FORWARDED-FOR'] = message['X-CLIENT-IP'] if message['X-Client-IP'] else message['Client-IP'] - - subject = message['subject'] - del(message['subject']) - if hasattr(instance, 'emailcode') and instance.emailcode: - subject = '[%s] %s' % (instance.emailcode, subject) - if handler.subject_prefix: - subject = '[%s] %s' % (handler.subject_prefix, subject) - message['Subject'] = subject - - if handler.from_email: - del(message['from']) - message['From'] = handler.from_email - - del message['Message-ID'] - - # get a new message id - message['Message-ID'] = '<%s@%s>' % (sha.new(str(random.random())).hexdigest(), - host) - - if handler.cc_all: - # send one mass-email - message['To'] = ', '.join(instance.recipients) - send_mail(str(message)) + # If the sender's email is not associated with an account on the site, + # do not forward the email + if not data['from']: + logger.debug(f"User has no account: `from` field is `{data['from']}`") + continue else: - # send an email for each recipient - for recipient in instance.recipients: - del(message['To']) - message['To'] = recipient - send_mail(str(message)) - + if len(data['from']) != 1: + raise AttributeError(f"More than one sender: `{data['from']}`") + email_address = data['from'][0].split('<')[1].split('>')[0] + users = ESPUser.objects.filter(email=email_address).order_by('date_joined') # sort oldest to newest + if len(users) == 0: + logger.warning('Received email from {}, which is not associated with a user'.format(data['from'])) + # TODO: send the user a bounce message but limit to one bounce message per day/week/something using + # something similar to dbmail.MessageRequests to keep track + continue + elif len(users) == 1: + sender = users[0] + # If there is more than one associated account, choose one by prefering admin > teacher > volunteer > + # student then choosing the earliest account created. Then send as before using the unique account. + elif len(users) > 1: + for group_name in ['Administrator', 'Teacher', 'Volunteer', 'Student', 'Educator']: + group_users = [x for x in users if len(x.groups.filter(name=group_name)) > 0] + if len(group_users) > 0: + sender = group_users[0] # choose the first (oldest) account if there is still more than + # one; it won't matter because they all go to the same email by construction + break + else: # if the users aren't in any of the standard groups above, ... + sender = users[0] # ... then just pick the oldest account created by selecting users[0] as above + logger.debug(f"Group selection: {group_name} -> {group_users}") + else: + logger.error('Negative number of possible senders in supposed list `{}`. Skipping....'.format(users)) + continue + # Having identified the sender, if the sender's email is associated with an account on the website, + # use SendGrid to send an email to each recipient of the original message (To, Cc, Bcc) individually from + # the sender's site email + logger.info('Sending email as {}'.format(sender)) + # TODO: to avoid loops, remove any @site.learningu.org addresses? There's probably a better way + if isinstance(data['to'], str): + data['to'] = [data['to']] + for recipient in data['to']: + logger.debug(f"Sending to `{recipient}`") + send_mail(subject=data['subject'], message=data['body'], + from_email='{}@{}'.format(sender, settings.EMAIL_HOST_SENDER), + recipient_list=[recipient], attachments=data['attachments'], fail_silently=False) + del sender, recipient, users sys.exit(0) except Exception as e: - # we dont' want to care if it's an exit + # we don't want to care if it's an exit if isinstance(e, SystemExit): raise if DEBUG: raise else: - logger.warning("Couldn't find user '%s'", user) + logger.warning("Couldn't find user '{}'. Full error is `{}`".format(user, e)) + import traceback + error_info = traceback.format_exc() + logger.debug("Traceback is\n{}".format(error_info)) + print(""" %s MAIL SERVER ===============