From 1f4df6830464e8d21b355aa3b84d601047e4cb7a Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 23 Apr 2017 22:04:58 +0200 Subject: [PATCH 01/17] add outgoingemail model, add mailhelper function and worker --- src/teams/email.py | 12 ++++---- src/utils/admin.py | 8 ++++++ src/utils/email.py | 49 ++++++++++++++++++++++++++++---- src/utils/models.py | 11 +++++++ src/utils/outgoingemailworker.py | 28 ++++++++++++++++++ 5 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 src/utils/admin.py create mode 100644 src/utils/outgoingemailworker.py diff --git a/src/teams/email.py b/src/teams/email.py index f20a5055..8a90e82c 100644 --- a/src/teams/email.py +++ b/src/teams/email.py @@ -1,4 +1,4 @@ -from utils.email import _send_email +from utils.email import add_outgoing_email import logging logger = logging.getLogger("bornhack.%s" % __name__) @@ -9,7 +9,7 @@ def send_add_membership_email(membership): 'camp': membership.team.camp.title } - return _send_email( + return add_outgoing_email( text_template='emails/add_membership_email.txt', html_template='emails/add_membership_email.html', recipient=membership.user.email, @@ -31,7 +31,7 @@ def send_remove_membership_email(membership): text_template = 'emails/unapproved_membership_email.txt', html_template = 'emails/unapproved_membership_email.html' - return _send_email( + return add_outgoing_email( text_template=text_template, html_template=html_template, recipient=membership.user.email, @@ -46,10 +46,12 @@ def send_new_membership_email(membership): 'camp': membership.team.camp.title } - return _send_email( + return add_outgoing_email( text_template='emails/new_membership_email.txt', html_template='emails/new_membership_email.html', - recipient=[resp.email for resp in membership.team.responsible], + recipients=', '.join( + [resp.email for resp in membership.team.responsible] + ), formatdict=formatdict, subject='New membership request for {} at {}'.format( membership.team.name, diff --git a/src/utils/admin.py b/src/utils/admin.py new file mode 100644 index 00000000..7eb6ca09 --- /dev/null +++ b/src/utils/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from .models import OutgoingEmail + + +@admin.register(OutgoingEmail) +class OutgoingEmailAdmin(admin.ModelAdmin): + pass diff --git a/src/utils/email.py b/src/utils/email.py index b7b3dc5c..6fbd10cb 100644 --- a/src/utils/email.py +++ b/src/utils/email.py @@ -1,6 +1,8 @@ from django.core.mail import EmailMultiAlternatives from django.conf import settings from django.template.loader import render_to_string +from django.core.validators import validate_email +from .models import OutgoingEmail import logging logger = logging.getLogger("bornhack.%s" % __name__) @@ -8,12 +10,11 @@ logger = logging.getLogger("bornhack.%s" % __name__) def _send_email( text_template, recipient, - formatdict, subject, - html_template=None, + html_template='', sender='BornHack ', - attachment=None, - attachment_filename=None + attachment='', + attachment_filename='' ): if not isinstance(recipient, list): recipient = [recipient] @@ -22,7 +23,7 @@ def _send_email( # put the basic email together msg = EmailMultiAlternatives( subject, - render_to_string(text_template, formatdict), + text_template, sender, recipient, [settings.ARCHIVE_EMAIL] @@ -31,7 +32,7 @@ def _send_email( # is there a html version of this email? if html_template: msg.attach_alternative( - render_to_string(html_template, formatdict), + html_template, 'text/html' ) @@ -47,3 +48,39 @@ def _send_email( msg.send() return True + + +def add_outgoing_email( + text_template, + recipients, + formatdict, + subject, + html_template='', + sender='BornHack ', + attachment='', + attachment_filename='' +): + """ adds an email to the outgoing queue + recipients is either just a str email or a str commaseperated emails + """ + text_template = render_to_string(text_template, formatdict) + + if html_template: + html_template = render_to_string(html_template, formatdict) + + if ',' in recipients: + for recipient in recipients.split(','): + validate_email(recipient.strip()) + else: + validate_email(recipients) + + OutgoingEmail.objects.create( + text_template=text_template, + html_template=html_template, + subject=subject, + sender=sender, + recipient=recipients, + attachment=attachment, + attachment_filename=attachment_filename + ) + return True diff --git a/src/utils/models.py b/src/utils/models.py index 4b6fe155..51c10bbb 100644 --- a/src/utils/models.py +++ b/src/utils/models.py @@ -68,3 +68,14 @@ class CampRelatedModel(CreatedUpdatedModel): raise ValidationError('This camp is in read only mode.') super().delete(**kwargs) + + +class OutgoingEmail(CreatedUpdatedModel): + subject = models.CharField(max_length=500) + text_template = models.TextField() + html_template = models.TextField(blank=True) + recipient = models.CharField(max_length=500) + sender = models.CharField(max_length=500) + attachment = models.CharField(max_length=500, blank=True) + attachment_filename = models.CharField(max_length=500, blank=True) + processed = models.BooleanField(default=False) diff --git a/src/utils/outgoingemailworker.py b/src/utils/outgoingemailworker.py new file mode 100644 index 00000000..48f11a83 --- /dev/null +++ b/src/utils/outgoingemailworker.py @@ -0,0 +1,28 @@ +from .models import OutgoingEmail +from .email import _send_email +import logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger('bornhack.%s' % __name__) + + +def do_work(): + """ + The outgoing email worker sends emails added to the OutgoingEmail + queue. + """ + not_processed_email = OutgoingEmail.objects.filter(processed=False) + + for email in not_processed_email: + if ',' in email.recipient: + recipient = email.recipient.split(',') + else: + recipient = [email.recipient] + + _send_email( + text_template=email.text_template, + recipient=recipient, + subject=email.subject, + html_template=email.html_template, + attachment=email.attachment, + attachment_filename=email.attachment_filename + ) From 61b670931caa202b77fcc3a27763c4c74b48312e Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 23 Apr 2017 22:08:59 +0200 Subject: [PATCH 02/17] mark email as processed when send --- src/utils/outgoingemailworker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/outgoingemailworker.py b/src/utils/outgoingemailworker.py index 48f11a83..1146b874 100644 --- a/src/utils/outgoingemailworker.py +++ b/src/utils/outgoingemailworker.py @@ -26,3 +26,5 @@ def do_work(): attachment=email.attachment, attachment_filename=email.attachment_filename ) + email.processed = True + email.save() From 16eb7cf59451602fc427d3f5e2773f280c155d77 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 30 Apr 2017 11:28:10 +0200 Subject: [PATCH 03/17] use os.path.join to create paths for pdf files --- src/shop/pdf.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/shop/pdf.py b/src/shop/pdf.py index ebad4b6d..32cafc92 100644 --- a/src/shop/pdf.py +++ b/src/shop/pdf.py @@ -3,7 +3,9 @@ from wkhtmltopdf.views import PDFTemplateResponse from PyPDF2 import PdfFileWriter, PdfFileReader from django.test.client import RequestFactory from django.conf import settings -import io, logging +import logging +import io +import os logger = logging.getLogger("bornhack.%s" % __name__) @@ -13,10 +15,10 @@ def generate_pdf_letter(filename, template, formatdict): request.user = AnonymousUser() request.session = {} - ### produce text-only PDF from template + # produce text-only PDF from template pdfgenerator = PDFTemplateResponse( request=request, - template=template, + template=template, context=formatdict, cmd_options={ 'margin-top': 50, @@ -26,33 +28,35 @@ def generate_pdf_letter(filename, template, formatdict): textonlypdf = io.BytesIO() textonlypdf.write(pdfgenerator.rendered_content) - ### create a blank pdf to work with + # create a blank pdf to work with finalpdf = PdfFileWriter() - ### open the text-only pdf + # open the text-only pdf pdfreader = PdfFileReader(textonlypdf) - ### get watermark from watermark file - watermark = PdfFileReader(open("%s/pdf/%s" % (settings.STATICFILES_DIRS[0], settings.PDF_LETTERHEAD_FILENAME), 'rb')) + # get watermark from watermark file + watermark = PdfFileReader( + open(os.path.join(settings.STATICFILES_DIRS[0], 'pdf', settings.PDF_LETTERHEAD_FILENAME), 'rb') + ) - ### add the watermark to all pages + # add the watermark to all pages for pagenum in range(pdfreader.getNumPages()): page = watermark.getPage(0) try: page.mergePage(pdfreader.getPage(pagenum)) except ValueError: - ### watermark pdf might be broken? + # watermark pdf might be broken? return False - ### add page to output + # add page to output finalpdf.addPage(page) - ### save the generated pdf to the archive - fullpath = settings.PDF_ARCHIVE_PATH+filename + # save the generated pdf to the archive + fullpath = os.path.join(settings.PDF_ARCHIVE_PATH, filename) with open(fullpath, 'wb') as fh: finalpdf.write(fh) logger.info('Saved pdf to archive: %s' % fullpath) - ### return a file object with the data + # return a file object with the data returnfile = io.BytesIO() finalpdf.write(returnfile) return returnfile From a8eb0ffe97aa1a2ef3182ea7a728c4bf5a6fd157 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 30 Apr 2017 11:32:49 +0200 Subject: [PATCH 04/17] use filefield for email attachments, add logging for emailworker --- src/shop/email.py | 18 +++++++++--------- src/utils/email.py | 26 +++++++++++++++++--------- src/utils/models.py | 6 ++++-- src/utils/outgoingemailworker.py | 21 ++++++++++++++++----- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/shop/email.py b/src/shop/email.py index 5181050c..de18e082 100644 --- a/src/shop/email.py +++ b/src/shop/email.py @@ -1,4 +1,4 @@ -from utils.email import _send_email +from utils.email import add_outgoing_email import logging logger = logging.getLogger("bornhack.%s" % __name__) @@ -11,11 +11,11 @@ def send_creditnote_email(creditnote): subject = 'BornHack creditnote %s' % creditnote.pk - # send mail - return _send_email( + # add email to outgoing email queue + return add_outgoing_email( text_template='emails/creditnote_email.txt', html_template='emails/creditnote_email.html', - recipient=creditnote.user.email, + recipients=creditnote.user.email, formatdict=formatdict, subject=subject, attachment=creditnote.pdf.read(), @@ -33,11 +33,11 @@ def send_invoice_email(invoice): subject = 'BornHack invoice %s' % invoice.pk - # send mail - return _send_email( + # add email to outgoing email queue + return add_outgoing_email( text_template='emails/invoice_email.txt', html_template='emails/invoice_email.html', - recipient=invoice.order.user.email, + recipients=invoice.order.user.email, formatdict=formatdict, subject=subject, attachment=invoice.pdf.read(), @@ -46,8 +46,8 @@ def send_invoice_email(invoice): def send_test_email(recipient): - return _send_email( + return add_outgoing_email( text_template='emails/testmail.txt', - recipient=recipient, + recipients=recipient, subject='testmail from bornhack website' ) diff --git a/src/utils/email.py b/src/utils/email.py index 6fbd10cb..82f702e3 100644 --- a/src/utils/email.py +++ b/src/utils/email.py @@ -1,7 +1,8 @@ from django.core.mail import EmailMultiAlternatives +from django.core.validators import validate_email +from django.core.files.base import ContentFile from django.conf import settings from django.template.loader import render_to_string -from django.core.validators import validate_email from .models import OutgoingEmail import logging logger = logging.getLogger("bornhack.%s" % __name__) @@ -13,7 +14,7 @@ def _send_email( subject, html_template='', sender='BornHack ', - attachment='', + attachment=None, attachment_filename='' ): if not isinstance(recipient, list): @@ -39,13 +40,16 @@ def _send_email( # is there a pdf attachment to this mail? if attachment: msg.attach(attachment_filename, attachment, 'application/pdf') - except Exception as e: logger.exception('exception while rendering email: {}'.format(e)) return False # send the email - msg.send() + try: + msg.send(fail_silently=False) + except Exception as e: + logger.exception('exception while sending email: {}'.format(e)) + return False return True @@ -57,7 +61,7 @@ def add_outgoing_email( subject, html_template='', sender='BornHack ', - attachment='', + attachment=None, attachment_filename='' ): """ adds an email to the outgoing queue @@ -74,13 +78,17 @@ def add_outgoing_email( else: validate_email(recipients) - OutgoingEmail.objects.create( + email = OutgoingEmail.objects.create( text_template=text_template, html_template=html_template, subject=subject, sender=sender, - recipient=recipients, - attachment=attachment, - attachment_filename=attachment_filename + recipient=recipients ) + + if attachment: + django_file = ContentFile(attachment) + django_file.name = attachment_filename + email.attachment.save(attachment_filename, django_file, save=True) + return True diff --git a/src/utils/models.py b/src/utils/models.py index 51c10bbb..5095c1f6 100644 --- a/src/utils/models.py +++ b/src/utils/models.py @@ -76,6 +76,8 @@ class OutgoingEmail(CreatedUpdatedModel): html_template = models.TextField(blank=True) recipient = models.CharField(max_length=500) sender = models.CharField(max_length=500) - attachment = models.CharField(max_length=500, blank=True) - attachment_filename = models.CharField(max_length=500, blank=True) + attachment = models.FileField(blank=True) processed = models.BooleanField(default=False) + + def __str__(self): + return 'Email {} for {}'.format(self.subject, self.recipient) diff --git a/src/utils/outgoingemailworker.py b/src/utils/outgoingemailworker.py index 1146b874..cceaf7d0 100644 --- a/src/utils/outgoingemailworker.py +++ b/src/utils/outgoingemailworker.py @@ -12,19 +12,30 @@ def do_work(): """ not_processed_email = OutgoingEmail.objects.filter(processed=False) + logger.info('about to process {} emails'.format(len(not_processed_email))) for email in not_processed_email: if ',' in email.recipient: recipient = email.recipient.split(',') else: recipient = [email.recipient] - _send_email( + attachment = None + attachment_filename = '' + if email.attachment: + attachment = email.attachment.read() + attachment_filename = email.attachment.name + + mail_send_success = _send_email( text_template=email.text_template, recipient=recipient, subject=email.subject, html_template=email.html_template, - attachment=email.attachment, - attachment_filename=email.attachment_filename + attachment=attachment, + attachment_filename=attachment_filename ) - email.processed = True - email.save() + if mail_send_success: + email.processed = True + email.save() + logger.info('successfully sent {}'.format(email)) + else: + logger.error('unable to sent {}'.format(email)) From 098b6ea83c4ae6cd02bffd935f02af6c53bf7f5e Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 30 Apr 2017 12:36:03 +0200 Subject: [PATCH 05/17] update invoiceworker to reflect the new email system Also removes some comments (for consistency) and some unused imports --- src/shop/invoiceworker.py | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/src/shop/invoiceworker.py b/src/shop/invoiceworker.py index 8b1227a8..9866b527 100644 --- a/src/shop/invoiceworker.py +++ b/src/shop/invoiceworker.py @@ -1,10 +1,8 @@ from django.core.files import File -from django.utils import timezone from shop.pdf import generate_pdf_letter from shop.email import send_invoice_email, send_creditnote_email from shop.models import Order, CustomOrder, Invoice, CreditNote -from decimal import Decimal -import logging, importlib +import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger('bornhack.%s' % __name__) @@ -16,31 +14,26 @@ def do_work(): that have no PDF. It also emails invoices for shop orders. """ - ############################################################### # check if we need to generate any invoices for shop orders for order in Order.objects.filter(paid=True, invoice__isnull=True): # generate invoice for this Order Invoice.objects.create(order=order) logger.info('Generated Invoice object for %s' % order) - - ############################################################### # check if we need to generate any invoices for custom orders for customorder in CustomOrder.objects.filter(invoice__isnull=True): # generate invoice for this CustomOrder Invoice.objects.create(customorder=customorder) logger.info('Generated Invoice object for %s' % customorder) - - ############################################################### # check if we need to generate any pdf invoices for invoice in Invoice.objects.filter(pdf=''): # generate the pdf try: if invoice.customorder: - template='pdf/custominvoice.html' + template = 'pdf/custominvoice.html' else: - template='pdf/invoice.html' + template = 'pdf/invoice.html' pdffile = generate_pdf_letter( filename=invoice.filename, template=template, @@ -57,21 +50,15 @@ def do_work(): invoice.pdf.save(invoice.filename, File(pdffile)) invoice.save() - - ############################################################### # check if we need to send out any invoices (only for shop orders, and only where pdf has been generated) for invoice in Invoice.objects.filter(order__isnull=False, sent_to_customer=False).exclude(pdf=''): logger.info("found unmailed Invoice object: %s" % invoice) - # send the email - if send_invoice_email(invoice=invoice): - invoice.sent_to_customer=True - invoice.save() - logger.info('OK: Invoice email sent to %s' % invoice.order.user.email) - else: - logger.error('Unable to send invoice email for order %s to %s' % (invoice.order.pk, invoice.order.user.email)) + # add email to the outgoing email queue + send_invoice_email(invoice=invoice) + invoice.sent_to_customer = True + invoice.save() + logger.info('OK: Invoice email added to queue.') - - ############################################################### # check if we need to generate any pdf creditnotes? for creditnote in CreditNote.objects.filter(pdf=''): # generate the pdf @@ -92,14 +79,12 @@ def do_work(): creditnote.pdf.save(creditnote.filename, File(pdffile)) creditnote.save() - - ############################################################### # check if we need to send out any creditnotes (only where pdf has been generated) for creditnote in CreditNote.objects.filter(sent_to_customer=False).exclude(pdf=''): # send the email if send_creditnote_email(creditnote=creditnote): logger.info('OK: Creditnote email sent to %s' % creditnote.user.email) - creditnote.sent_to_customer=True + creditnote.sent_to_customer = True creditnote.save() else: logger.error('Unable to send creditnote email for creditnote %s to %s' % (creditnote.pk, creditnote.user.email)) From e185da74362bf4bb526a79d5929731c85409a005 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 30 Apr 2017 14:20:36 +0200 Subject: [PATCH 06/17] add migrations for the OutgoingEmail model --- src/utils/migrations/0001_initial.py | 34 ++++++++++++++++++++++++++++ src/utils/migrations/__init__.py | 0 2 files changed, 34 insertions(+) create mode 100644 src/utils/migrations/0001_initial.py create mode 100644 src/utils/migrations/__init__.py diff --git a/src/utils/migrations/0001_initial.py b/src/utils/migrations/0001_initial.py new file mode 100644 index 00000000..96ada4b1 --- /dev/null +++ b/src/utils/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-04-30 12:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='OutgoingEmail', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('subject', models.CharField(max_length=500)), + ('text_template', models.TextField()), + ('html_template', models.TextField(blank=True)), + ('recipient', models.CharField(max_length=500)), + ('sender', models.CharField(max_length=500)), + ('attachment', models.FileField(blank=True, upload_to='')), + ('processed', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/utils/migrations/__init__.py b/src/utils/migrations/__init__.py new file mode 100644 index 00000000..e69de29b From 63648cd08e9fce6f30b5e27c55b846dcc370a1e9 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 15:04:51 +0200 Subject: [PATCH 07/17] fix add_outgoing_email to return false if emails cant be validated --- src/utils/email.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/utils/email.py b/src/utils/email.py index 82f702e3..c0899bb8 100644 --- a/src/utils/email.py +++ b/src/utils/email.py @@ -1,5 +1,6 @@ from django.core.mail import EmailMultiAlternatives from django.core.validators import validate_email +from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.conf import settings from django.template.loader import render_to_string @@ -65,7 +66,7 @@ def add_outgoing_email( attachment_filename='' ): """ adds an email to the outgoing queue - recipients is either just a str email or a str commaseperated emails + recipients is a string, if theres multiple emails seperate with a comma """ text_template = render_to_string(text_template, formatdict) @@ -74,9 +75,15 @@ def add_outgoing_email( if ',' in recipients: for recipient in recipients.split(','): - validate_email(recipient.strip()) + try: + validate_email(recipient.strip()) + except ValidationError: + return False else: - validate_email(recipients) + try: + validate_email(recipients) + except ValidationError: + return False email = OutgoingEmail.objects.create( text_template=text_template, From 95e363b8315c058a5f7ecdd5da75ab6d903a1bc9 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 16:23:13 +0200 Subject: [PATCH 08/17] change logging from info to debug in emailworker --- src/utils/outgoingemailworker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/outgoingemailworker.py b/src/utils/outgoingemailworker.py index cceaf7d0..ccc8a948 100644 --- a/src/utils/outgoingemailworker.py +++ b/src/utils/outgoingemailworker.py @@ -12,7 +12,7 @@ def do_work(): """ not_processed_email = OutgoingEmail.objects.filter(processed=False) - logger.info('about to process {} emails'.format(len(not_processed_email))) + logger.debug('about to process {} emails'.format(len(not_processed_email))) for email in not_processed_email: if ',' in email.recipient: recipient = email.recipient.split(',') @@ -36,6 +36,6 @@ def do_work(): if mail_send_success: email.processed = True email.save() - logger.info('successfully sent {}'.format(email)) + logger.debug('successfully sent {}'.format(email)) else: logger.error('unable to sent {}'.format(email)) From f986617c6424511583317af8c2e01e50708ccea7 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 16:48:06 +0200 Subject: [PATCH 09/17] only log if theres emails to be processed --- src/utils/outgoingemailworker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/outgoingemailworker.py b/src/utils/outgoingemailworker.py index ccc8a948..717f5b12 100644 --- a/src/utils/outgoingemailworker.py +++ b/src/utils/outgoingemailworker.py @@ -12,7 +12,11 @@ def do_work(): """ not_processed_email = OutgoingEmail.objects.filter(processed=False) - logger.debug('about to process {} emails'.format(len(not_processed_email))) + if len(not_processed_email) > 0: + logger.debug('about to process {} emails'.format( + len(not_processed_email)) + ) + for email in not_processed_email: if ',' in email.recipient: recipient = email.recipient.split(',') From 07029c260d0e222fee1c0a813f72ee66a41673ce Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 17:23:48 +0200 Subject: [PATCH 10/17] use a list for recipients rather than a string --- src/teams/email.py | 4 +--- src/utils/email.py | 15 ++++++--------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/teams/email.py b/src/teams/email.py index 8a90e82c..8917241b 100644 --- a/src/teams/email.py +++ b/src/teams/email.py @@ -49,9 +49,7 @@ def send_new_membership_email(membership): return add_outgoing_email( text_template='emails/new_membership_email.txt', html_template='emails/new_membership_email.html', - recipients=', '.join( - [resp.email for resp in membership.team.responsible] - ), + recipients=membership.team.responsible, formatdict=formatdict, subject='New membership request for {} at {}'.format( membership.team.name, diff --git a/src/utils/email.py b/src/utils/email.py index c0899bb8..fbb90d78 100644 --- a/src/utils/email.py +++ b/src/utils/email.py @@ -66,22 +66,19 @@ def add_outgoing_email( attachment_filename='' ): """ adds an email to the outgoing queue - recipients is a string, if theres multiple emails seperate with a comma + recipients is a list of to recipients """ text_template = render_to_string(text_template, formatdict) if html_template: html_template = render_to_string(html_template, formatdict) - if ',' in recipients: - for recipient in recipients.split(','): - try: - validate_email(recipient.strip()) - except ValidationError: - return False - else: + if not isinstance(recipients, list): + recipients = [recipients] + + for recipient in recipients: try: - validate_email(recipients) + validate_email(recipient) except ValidationError: return False From 6a98ee3564f1952e28a9ad564308329a2cca091b Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 17:23:48 +0200 Subject: [PATCH 11/17] use a list for recipients rather than a string --- src/teams/email.py | 4 +--- src/utils/email.py | 15 ++++++--------- src/utils/models.py | 2 +- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/teams/email.py b/src/teams/email.py index 8a90e82c..8917241b 100644 --- a/src/teams/email.py +++ b/src/teams/email.py @@ -49,9 +49,7 @@ def send_new_membership_email(membership): return add_outgoing_email( text_template='emails/new_membership_email.txt', html_template='emails/new_membership_email.html', - recipients=', '.join( - [resp.email for resp in membership.team.responsible] - ), + recipients=membership.team.responsible, formatdict=formatdict, subject='New membership request for {} at {}'.format( membership.team.name, diff --git a/src/utils/email.py b/src/utils/email.py index c0899bb8..fbb90d78 100644 --- a/src/utils/email.py +++ b/src/utils/email.py @@ -66,22 +66,19 @@ def add_outgoing_email( attachment_filename='' ): """ adds an email to the outgoing queue - recipients is a string, if theres multiple emails seperate with a comma + recipients is a list of to recipients """ text_template = render_to_string(text_template, formatdict) if html_template: html_template = render_to_string(html_template, formatdict) - if ',' in recipients: - for recipient in recipients.split(','): - try: - validate_email(recipient.strip()) - except ValidationError: - return False - else: + if not isinstance(recipients, list): + recipients = [recipients] + + for recipient in recipients: try: - validate_email(recipients) + validate_email(recipient) except ValidationError: return False diff --git a/src/utils/models.py b/src/utils/models.py index 5095c1f6..4710f4fc 100644 --- a/src/utils/models.py +++ b/src/utils/models.py @@ -1,4 +1,5 @@ import uuid +from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.contrib import messages from django.db import models @@ -74,7 +75,6 @@ class OutgoingEmail(CreatedUpdatedModel): subject = models.CharField(max_length=500) text_template = models.TextField() html_template = models.TextField(blank=True) - recipient = models.CharField(max_length=500) sender = models.CharField(max_length=500) attachment = models.FileField(blank=True) processed = models.BooleanField(default=False) From c55c68beff9e2a3ed595a4d9e19e6dd3e37fe10e Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 20:13:49 +0200 Subject: [PATCH 12/17] add bcc and cc to email system --- src/utils/email.py | 27 ++++++++++------ .../0002_remove_outgoingemail_recipient.py | 19 ++++++++++++ .../migrations/0003_auto_20170521_1932.py | 31 +++++++++++++++++++ src/utils/models.py | 24 ++++++++++++-- src/utils/outgoingemailworker.py | 8 ++--- 5 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 src/utils/migrations/0002_remove_outgoingemail_recipient.py create mode 100644 src/utils/migrations/0003_auto_20170521_1932.py diff --git a/src/utils/email.py b/src/utils/email.py index fbb90d78..6a458f39 100644 --- a/src/utils/email.py +++ b/src/utils/email.py @@ -11,15 +11,17 @@ logger = logging.getLogger("bornhack.%s" % __name__) def _send_email( text_template, - recipient, + to_recipients, subject, + cc_recipients=[], + bcc_recipients=[], html_template='', sender='BornHack ', attachment=None, attachment_filename='' ): - if not isinstance(recipient, list): - recipient = [recipient] + if not isinstance(to_recipients, list): + to_recipients = [to_recipients] try: # put the basic email together @@ -27,8 +29,9 @@ def _send_email( subject, text_template, sender, - recipient, - [settings.ARCHIVE_EMAIL] + to_recipients, + bcc_recipients + [settings.ARCHIVE_EMAIL], + cc_recipients ) # is there a html version of this email? @@ -57,9 +60,11 @@ def _send_email( def add_outgoing_email( text_template, - recipients, + to_recipients, formatdict, subject, + cc_recipients=[], + bcc_recipients=[], html_template='', sender='BornHack ', attachment=None, @@ -73,10 +78,10 @@ def add_outgoing_email( if html_template: html_template = render_to_string(html_template, formatdict) - if not isinstance(recipients, list): - recipients = [recipients] + if not isinstance(to_recipients, list): + to_recipients = [to_recipients] - for recipient in recipients: + for recipient in to_recipients: try: validate_email(recipient) except ValidationError: @@ -87,7 +92,9 @@ def add_outgoing_email( html_template=html_template, subject=subject, sender=sender, - recipient=recipients + to_recipients=to_recipients, + cc_recipients=cc_recipients, + bcc_recipients=bcc_recipients ) if attachment: diff --git a/src/utils/migrations/0002_remove_outgoingemail_recipient.py b/src/utils/migrations/0002_remove_outgoingemail_recipient.py new file mode 100644 index 00000000..65e857b9 --- /dev/null +++ b/src/utils/migrations/0002_remove_outgoingemail_recipient.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-05-21 16:08 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('utils', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='outgoingemail', + name='recipient', + ), + ] diff --git a/src/utils/migrations/0003_auto_20170521_1932.py b/src/utils/migrations/0003_auto_20170521_1932.py new file mode 100644 index 00000000..4867c105 --- /dev/null +++ b/src/utils/migrations/0003_auto_20170521_1932.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-05-21 17:32 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('utils', '0002_remove_outgoingemail_recipient'), + ] + + operations = [ + migrations.AddField( + model_name='outgoingemail', + name='bcc_recipients', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=500), blank=True, null=True, size=None), + ), + migrations.AddField( + model_name='outgoingemail', + name='cc_recipients', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=500), blank=True, null=True, size=None), + ), + migrations.AddField( + model_name='outgoingemail', + name='to_recipients', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=500), blank=True, null=True, size=None), + ), + ] diff --git a/src/utils/models.py b/src/utils/models.py index 4710f4fc..0fe48d72 100644 --- a/src/utils/models.py +++ b/src/utils/models.py @@ -76,8 +76,28 @@ class OutgoingEmail(CreatedUpdatedModel): text_template = models.TextField() html_template = models.TextField(blank=True) sender = models.CharField(max_length=500) + to_recipients = ArrayField( + models.CharField(max_length=500, blank=True), + null=True, + blank=True + ) + cc_recipients = ArrayField( + models.CharField(max_length=500, blank=True), + null=True, + blank=True + ) + bcc_recipients = ArrayField( + models.CharField(max_length=500, blank=True), + null=True, + blank=True + ) attachment = models.FileField(blank=True) processed = models.BooleanField(default=False) - def __str__(self): - return 'Email {} for {}'.format(self.subject, self.recipient) + def clean(self): + if not self.to_recipients \ + and not self.bcc_recipients \ + and not self.cc_recipients: + raise ValidationError( + {'recipient': 'either to_recipient, bcc_recipient or cc_recipient required.'} + ) diff --git a/src/utils/outgoingemailworker.py b/src/utils/outgoingemailworker.py index 717f5b12..01355f7f 100644 --- a/src/utils/outgoingemailworker.py +++ b/src/utils/outgoingemailworker.py @@ -18,10 +18,6 @@ def do_work(): ) for email in not_processed_email: - if ',' in email.recipient: - recipient = email.recipient.split(',') - else: - recipient = [email.recipient] attachment = None attachment_filename = '' @@ -31,8 +27,10 @@ def do_work(): mail_send_success = _send_email( text_template=email.text_template, - recipient=recipient, + to_recipients=email.to_recipients, subject=email.subject, + cc_recipients=email.cc_recipients, + bcc_recipients=email.bcc_recipients, html_template=email.html_template, attachment=attachment, attachment_filename=attachment_filename From febecc137df89f180854e68cff373b1b66ae3596 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 20:15:27 +0200 Subject: [PATCH 13/17] rename send email to add email and log on error --- src/shop/email.py | 6 +++--- src/teams/admin.py | 7 +++---- src/teams/email.py | 12 ++++++------ src/teams/models.py | 9 ++++++--- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/shop/email.py b/src/shop/email.py index de18e082..9be19f58 100644 --- a/src/shop/email.py +++ b/src/shop/email.py @@ -15,7 +15,7 @@ def send_creditnote_email(creditnote): return add_outgoing_email( text_template='emails/creditnote_email.txt', html_template='emails/creditnote_email.html', - recipients=creditnote.user.email, + to_recipients=creditnote.user.email, formatdict=formatdict, subject=subject, attachment=creditnote.pdf.read(), @@ -37,7 +37,7 @@ def send_invoice_email(invoice): return add_outgoing_email( text_template='emails/invoice_email.txt', html_template='emails/invoice_email.html', - recipients=invoice.order.user.email, + to_recipients=invoice.order.user.email, formatdict=formatdict, subject=subject, attachment=invoice.pdf.read(), @@ -48,6 +48,6 @@ def send_invoice_email(invoice): def send_test_email(recipient): return add_outgoing_email( text_template='emails/testmail.txt', - recipients=recipient, + to_recipients=recipient, subject='testmail from bornhack website' ) diff --git a/src/teams/admin.py b/src/teams/admin.py index cc440820..2eb6d5dd 100644 --- a/src/teams/admin.py +++ b/src/teams/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from .models import Team, TeamArea, TeamMember -from .email import send_add_membership_email, send_remove_membership_email +from .email import add_added_membership_email, add_removed_membership_email admin.site.register(TeamArea) @@ -41,7 +41,7 @@ class TeamMemberAdmin(admin.ModelAdmin): membership.approved = True membership.save() updated += 1 - send_add_membership_email(membership) + add_added_membership_email(membership) self.message_user( request, @@ -57,7 +57,7 @@ class TeamMemberAdmin(admin.ModelAdmin): updated = 0 for membership in queryset: - send_remove_membership_email(membership) + add_removed_membership_email(membership) membership.delete() updated += 1 @@ -69,4 +69,3 @@ class TeamMemberAdmin(admin.ModelAdmin): ) ) remove_member.description = 'Remove a user from the team.' - diff --git a/src/teams/email.py b/src/teams/email.py index 8917241b..62d997db 100644 --- a/src/teams/email.py +++ b/src/teams/email.py @@ -3,7 +3,7 @@ import logging logger = logging.getLogger("bornhack.%s" % __name__) -def send_add_membership_email(membership): +def add_added_membership_email(membership): formatdict = { 'team': membership.team.name, 'camp': membership.team.camp.title @@ -12,13 +12,13 @@ def send_add_membership_email(membership): return add_outgoing_email( text_template='emails/add_membership_email.txt', html_template='emails/add_membership_email.html', - recipient=membership.user.email, + to_recipients=membership.user.email, formatdict=formatdict, subject='Team update from {}'.format(membership.team.camp.title) ) -def send_remove_membership_email(membership): +def add_removed_membership_email(membership): formatdict = { 'team': membership.team.name, 'camp': membership.team.camp.title @@ -34,13 +34,13 @@ def send_remove_membership_email(membership): return add_outgoing_email( text_template=text_template, html_template=html_template, - recipient=membership.user.email, + to_recipients=membership.user.email, formatdict=formatdict, subject='Team update from {}'.format(membership.team.camp.title) ) -def send_new_membership_email(membership): +def add_new_membership_email(membership): formatdict = { 'team': membership.team.name, 'camp': membership.team.camp.title @@ -49,7 +49,7 @@ def send_new_membership_email(membership): return add_outgoing_email( text_template='emails/new_membership_email.txt', html_template='emails/new_membership_email.html', - recipients=membership.team.responsible, + to_recipients=[resp.email for resp in membership.team.responsible], formatdict=formatdict, subject='New membership request for {} at {}'.format( membership.team.name, diff --git a/src/teams/models.py b/src/teams/models.py index fd8aced4..7c82e5c5 100644 --- a/src/teams/models.py +++ b/src/teams/models.py @@ -3,9 +3,11 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.utils.text import slugify from utils.models import CampRelatedModel -from .email import send_new_membership_email +from .email import add_new_membership_email from django.core.exceptions import ValidationError from django.contrib.auth.models import User +import logging +logger = logging.getLogger("bornhack.%s" % __name__) class TeamArea(CampRelatedModel): @@ -96,6 +98,7 @@ class TeamMember(models.Model): @receiver(post_save, sender=TeamMember) -def send_responsible_email(sender, instance, created, **kwargs): +def add_responsible_email(sender, instance, created, **kwargs): if created: - send_new_membership_email(instance) + if not add_new_membership_email(instance): + logger.error('Error adding email to outgoing queue') From 46cf9c53f7dac9851321d4ca70fce2a693e4bf75 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 20:43:05 +0200 Subject: [PATCH 14/17] verify invoice emails gets added to the queue and fix names --- src/shop/email.py | 6 +++--- src/shop/invoiceworker.py | 24 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/shop/email.py b/src/shop/email.py index 9be19f58..ba1edef5 100644 --- a/src/shop/email.py +++ b/src/shop/email.py @@ -3,7 +3,7 @@ import logging logger = logging.getLogger("bornhack.%s" % __name__) -def send_creditnote_email(creditnote): +def add_creditnote_email(creditnote): # put formatdict together formatdict = { 'creditnote': creditnote, @@ -23,7 +23,7 @@ def send_creditnote_email(creditnote): ) -def send_invoice_email(invoice): +def add_invoice_email(invoice): # put formatdict together formatdict = { 'ordernumber': invoice.order.pk, @@ -45,7 +45,7 @@ def send_invoice_email(invoice): ) -def send_test_email(recipient): +def add_test_email(recipient): return add_outgoing_email( text_template='emails/testmail.txt', to_recipients=recipient, diff --git a/src/shop/invoiceworker.py b/src/shop/invoiceworker.py index 9866b527..52ce8e45 100644 --- a/src/shop/invoiceworker.py +++ b/src/shop/invoiceworker.py @@ -1,6 +1,6 @@ from django.core.files import File from shop.pdf import generate_pdf_letter -from shop.email import send_invoice_email, send_creditnote_email +from shop.email import add_invoice_email, add_creditnote_email from shop.models import Order, CustomOrder, Invoice, CreditNote import logging logging.basicConfig(level=logging.INFO) @@ -54,10 +54,18 @@ def do_work(): for invoice in Invoice.objects.filter(order__isnull=False, sent_to_customer=False).exclude(pdf=''): logger.info("found unmailed Invoice object: %s" % invoice) # add email to the outgoing email queue - send_invoice_email(invoice=invoice) - invoice.sent_to_customer = True - invoice.save() - logger.info('OK: Invoice email added to queue.') + if add_invoice_email(invoice=invoice): + invoice.sent_to_customer = True + invoice.save() + logger.info('OK: Invoice email to {} added to queue.'.format( + invoice.order.user.email) + ) + else: + logger.error('Unable to add email for invoice {} to {}'.format( + invoice.pk, + invoice.order.user.email + ) + ) # check if we need to generate any pdf creditnotes? for creditnote in CreditNote.objects.filter(pdf=''): @@ -82,10 +90,10 @@ def do_work(): # check if we need to send out any creditnotes (only where pdf has been generated) for creditnote in CreditNote.objects.filter(sent_to_customer=False).exclude(pdf=''): # send the email - if send_creditnote_email(creditnote=creditnote): - logger.info('OK: Creditnote email sent to %s' % creditnote.user.email) + if add_creditnote_email(creditnote=creditnote): + logger.info('OK: Creditnote email to %s added' % creditnote.user.email) creditnote.sent_to_customer = True creditnote.save() else: - logger.error('Unable to send creditnote email for creditnote %s to %s' % (creditnote.pk, creditnote.user.email)) + logger.error('Unable to add creditnote email for creditnote %s to %s' % (creditnote.pk, creditnote.user.email)) From da26b3e0d3fabf85f6ee0288238882e810612da6 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 20:57:16 +0200 Subject: [PATCH 15/17] add str method for OutgoingEmail --- src/utils/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/models.py b/src/utils/models.py index 0fe48d72..5699b323 100644 --- a/src/utils/models.py +++ b/src/utils/models.py @@ -94,6 +94,9 @@ class OutgoingEmail(CreatedUpdatedModel): attachment = models.FileField(blank=True) processed = models.BooleanField(default=False) + def __str__(self): + return 'OutgoingEmail Object id: {} '.format(self.id) + def clean(self): if not self.to_recipients \ and not self.bcc_recipients \ From 750ab94f932fb2132bbefb63e9d9decbe3e4b94a Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 21:14:06 +0200 Subject: [PATCH 16/17] add defaults for to_recipients in send_mail and add_outgoing_email --- src/utils/email.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/email.py b/src/utils/email.py index 6a458f39..0afdc07b 100644 --- a/src/utils/email.py +++ b/src/utils/email.py @@ -11,8 +11,8 @@ logger = logging.getLogger("bornhack.%s" % __name__) def _send_email( text_template, - to_recipients, subject, + to_recipients=[], cc_recipients=[], bcc_recipients=[], html_template='', @@ -60,9 +60,9 @@ def _send_email( def add_outgoing_email( text_template, - to_recipients, formatdict, subject, + to_recipients=[], cc_recipients=[], bcc_recipients=[], html_template='', From eab2c41f9bab7b02da7af3d13dc3cb228923b012 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 21 May 2017 22:11:54 +0200 Subject: [PATCH 17/17] fix bcc_recipients --- src/utils/email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/email.py b/src/utils/email.py index 0afdc07b..1d4a906d 100644 --- a/src/utils/email.py +++ b/src/utils/email.py @@ -30,7 +30,7 @@ def _send_email( text_template, sender, to_recipients, - bcc_recipients + [settings.ARCHIVE_EMAIL], + bcc_recipients + [settings.ARCHIVE_EMAIL] if bcc_recipients else [settings.ARCHIVE_EMAIL], cc_recipients )