diff --git a/doc/admin/config.rst b/doc/admin/config.rst index 878a5ceb5e..60df4d8b35 100644 --- a/doc/admin/config.rst +++ b/doc/admin/config.rst @@ -445,8 +445,10 @@ You can configure the maximum file size for uploading various files:: max_size_image = 12 ; Max upload size for favicons in MiB, defaults to 1 MiB max_size_favicon = 2 - ; Max upload size for email attachments in MiB, defaults to 10 MiB + ; Max upload size for email attachments of manually sent emails in MiB, defaults to 10 MiB max_size_email_attachment = 15 + ; Max upload size for email attachments of automatically sent emails in MiB, defaults to 1 MiB + max_size_email_auto_attachment = 2 ; Max upload size for other files in MiB, defaults to 10 MiB ; This includes all file upload type order questions max_size_other = 100 diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 8f936b533e..d56f2e92c5 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -950,7 +950,7 @@ class Order(LockModel, LoggedModel): context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent', user: User=None, headers: dict=None, sender: str=None, invoices: list=None, auth=None, attach_tickets=False, position: 'OrderPosition'=None, auto_email=True, - attach_ical=False): + attach_ical=False, attach_other_files: list=None): """ Sends an email to the user that placed this order. Basically, this method does two things: @@ -994,7 +994,8 @@ class Order(LockModel, LoggedModel): recipient, subject, template, context, self.event, self.locale, self, headers=headers, sender=sender, invoices=invoices, attach_tickets=attach_tickets, - position=position, auto_email=auto_email, attach_ical=attach_ical + position=position, auto_email=auto_email, attach_ical=attach_ical, + attach_other_files=attach_other_files, ) except SendMailException: raise @@ -2316,7 +2317,7 @@ class OrderPosition(AbstractPosition): def send_mail(self, subject: str, template: Union[str, LazyI18nString], context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent', user: User=None, headers: dict=None, sender: str=None, invoices: list=None, - auth=None, attach_tickets=False, attach_ical=False): + auth=None, attach_tickets=False, attach_ical=False, attach_other_files: list=None): """ Sends an email to the attendee. Basically, this method does two things: @@ -2357,6 +2358,7 @@ class OrderPosition(AbstractPosition): invoices=invoices, attach_tickets=attach_tickets, attach_ical=attach_ical, + attach_other_files=attach_other_files, ) except SendMailException: raise diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 0154225a4b..6bc8a77291 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -35,6 +35,7 @@ import hashlib import inspect import logging +import mimetypes import os import re import smtplib @@ -51,6 +52,7 @@ from bs4 import BeautifulSoup from celery import chain from celery.exceptions import MaxRetriesExceededError from django.conf import settings +from django.core.files.storage import default_storage from django.core.mail import ( EmailMultiAlternatives, SafeMIMEMultipart, get_connection, ) @@ -73,6 +75,7 @@ from pretix.base.services.tasks import TransactionAwareTask from pretix.base.services.tickets import get_tickets_for_order from pretix.base.signals import email_filter, global_email_filter from pretix.celery_app import app +from pretix.helpers.hierarkey import clean_filename from pretix.multidomain.urlreverse import build_absolute_uri from pretix.presale.ical import get_ical @@ -94,7 +97,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None, position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None, customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None, - attach_ical=False, attach_cached_files: Sequence = None): + attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None): """ Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation. @@ -142,6 +145,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La :param attach_cached_files: A list of cached file to attach to this email. + :param attach_other_files: A list of file paths on our storage to attach. + :raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean that the email has been sent, just that it has been queued by the email backend. """ @@ -301,6 +306,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La organizer=organizer.pk if organizer else None, customer=customer.pk if customer else None, attach_cached_files=[(cf.id if isinstance(cf, CachedFile) else cf) for cf in attach_cached_files] if attach_cached_files else [], + attach_other_files=attach_other_files, ) if invoices: @@ -338,7 +344,8 @@ class CustomEmail(EmailMultiAlternatives): def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str, event: int = None, position: int = None, headers: dict = None, bcc: List[str] = None, invoices: List[int] = None, order: int = None, attach_tickets=False, user=None, - organizer=None, customer=None, attach_ical=False, attach_cached_files: List[int] = None) -> bool: + organizer=None, customer=None, attach_ical=False, attach_cached_files: List[int] = None, + attach_other_files: List[str] = None) -> bool: email = CustomEmail(subject, body, sender, to=to, bcc=bcc, headers=headers) if html is not None: html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET) @@ -455,6 +462,20 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st logger.exception('Could not attach invoice to email') pass + if attach_other_files: + for fname in attach_other_files: + ftype, _ = mimetypes.guess_type(fname) + data = default_storage.open(fname).read() + try: + email.attach( + clean_filename(os.path.basename(fname)), + data, + ftype + ) + except: + logger.exception('Could not attach file to email') + pass + if attach_cached_files: for cf in CachedFile.objects.filter(id__in=attach_cached_files): if cf.file: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index e1026dbc79..eb7e76a15a 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -941,7 +941,10 @@ def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, log_entry, invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [], attach_tickets=True, - attach_ical=event.settings.mail_attach_ical + attach_ical=event.settings.mail_attach_ical, + attach_other_files=filter(lambda a: a, [ + event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):] + ]), ) except SendMailException: logger.exception('Order received email could not be sent') @@ -958,7 +961,10 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi invoices=[], attach_tickets=True, position=position, - attach_ical=event.settings.mail_attach_ical + attach_ical=event.settings.mail_attach_ical, + attach_other_files=filter(lambda a: a, [ + event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):] + ]), ) except SendMailException: logger.exception('Order received email could not be sent to attendee') diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index a314874eda..f435898937 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -1687,6 +1687,30 @@ You can change your order details and view the status of your order at Best regards, Your {event} team""")) }, + 'mail_attachment_new_order': { + 'default': None, + 'type': File, + 'form_class': ExtFileField, + 'form_kwargs': dict( + label=_('Attachment for new orders'), + ext_whitelist=(".pdf",), + max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT, + help_text=_('This file will be attached to the first email that we send for every new order. Therefore it will be ' + 'combined with the "Placed order", "Free order", or "Received order" texts from above. It will be sent ' + 'to both order contacts and attendees. You can use this e.g. to send your terms of service. Do not use ' + 'it to send non-public information as this file might be sent before payment is confirmed or the order ' + 'is approved. To avoid this vital email going to spam, you can only upload PDF files of up to {size} MB.').format( + size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT // (1024 * 1024), + ) + ), + 'serializer_class': UploadedFileField, + 'serializer_kwargs': dict( + allowed_types=[ + 'application/pdf' + ], + max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT, + ) + }, 'mail_send_order_placed_attendee': { 'type': bool, 'default': 'False' diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 0e3107f878..8e9ea3875f 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -872,6 +872,7 @@ class MailSettingsForm(SMTPSettingsMixin, SettingsForm): 'mail_from_name', 'mail_attach_ical', 'mail_attach_tickets', + 'mail_attachment_new_order', ] mail_sales_channel_placed_paid = forms.MultipleChoiceField( diff --git a/src/pretix/control/templates/pretixcontrol/event/mail.html b/src/pretix/control/templates/pretixcontrol/event/mail.html index 008656f57a..06c41fe554 100644 --- a/src/pretix/control/templates/pretixcontrol/event/mail.html +++ b/src/pretix/control/templates/pretixcontrol/event/mail.html @@ -47,6 +47,7 @@