diff --git a/src/pretix/base/models/invoices.py b/src/pretix/base/models/invoices.py index 5f4de4b8b7..0dfde5d570 100644 --- a/src/pretix/base/models/invoices.py +++ b/src/pretix/base/models/invoices.py @@ -42,6 +42,7 @@ from django.db.models.functions import Cast from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.functional import cached_property +from django.utils.timezone import now from django.utils.translation import gettext_lazy as _, pgettext from django_scopes import ScopedManager @@ -368,6 +369,22 @@ class Invoice(models.Model): from pretix.base.invoicing.transmission import transmission_types return transmission_types.get(identifier=self.transmission_type)[0] + def set_transmission_failed(self, provider, data): + self.transmission_status = Invoice.TRANSMISSION_STATUS_FAILED + self.transmission_date = now() + if not self.transmission_provider and provider: + self.transmission_provider = provider + self.save(update_fields=["transmission_status", "transmission_date", "transmission_provider"]) + self.order.log_action( + "pretix.event.order.invoice.sending_failed", + data={ + "full_invoice_no": self.full_invoice_no, + "transmission_provider": provider, + "transmission_type": self.transmission_type, + "data": data, + } + ) + class InvoiceLine(models.Model): """ diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index 85a69e9e73..bbc7438dde 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -664,20 +664,7 @@ def transmit_invoice(sender, invoice_id, allow_retransmission=True, **kwargs): break if not provider: - invoice.transmission_status = Invoice.TRANSMISSION_STATUS_FAILED - invoice.transmission_date = now() - invoice.save(update_fields=["transmission_status", "transmission_date"]) - invoice.order.log_action( - "pretix.event.order.invoice.sending_failed", - data={ - "full_invoice_no": invoice.full_invoice_no, - "transmission_provider": None, - "transmission_type": invoice.transmission_type, - "data": { - "reason": "no_provider", - }, - } - ) + invoice.set_transmission_failed(provider=None, data={"reason": "no_provider"}) return if invoice.order.testmode and not provider.testmode_supported: @@ -698,18 +685,7 @@ def transmit_invoice(sender, invoice_id, allow_retransmission=True, **kwargs): provider.transmit(invoice) except Exception as e: logger.exception(f"Transmission of invoice {invoice.pk} failed with exception.") - invoice.transmission_status = Invoice.TRANSMISSION_STATUS_FAILED - invoice.transmission_date = now() - invoice.save(update_fields=["transmission_status", "transmission_date"]) - invoice.order.log_action( - "pretix.event.order.invoice.sending_failed", - data={ - "full_invoice_no": invoice.full_invoice_no, - "transmission_provider": None, - "transmission_type": invoice.transmission_type, - "data": { - "reason": "exception", - "exception": str(e), - }, - } - ) + invoice.set_transmission_failed(provider=provider.identifier, data={ + "reason": "exception", + "exception": str(e), + }) diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 1f4257c1fd..3c614941f6 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -495,7 +495,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st email = email_filter.send_chained(event, 'message', message=email, order=order, user=user) - invoices_sent = [] + invoices_to_mark_transmitted = [] if invoices: invoices = Invoice.objects.filter(pk__in=invoices) for inv in invoices: @@ -516,7 +516,23 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st inv.file.file.read(), 'application/pdf' ) - invoices_sent.append(inv) + + if inv.transmission_type == "email": + # Mark invoice as sent when it was sent to the requested address *either* at the time of + # invoice creation *or* as of right now. + expected_recipients = [ + (inv.invoice_to_transmission_info or {}).get("transmission_email_address") + or inv.order.email, + ] + try: + expected_recipients.append( + (inv.order.invoice_address.transmission_info or {}).get("transmission_email_address") + or inv.order.email + ) + except InvoiceAddress.DoesNotExist: + pass + if any(t in expected_recipients for t in to): + invoices_to_mark_transmitted.append(inv) except: logger.exception('Could not attach invoice to email') pass @@ -589,6 +605,11 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st 'invoices': [], } ) + for i in invoices_to_mark_transmitted: + i.set_transmission_failed(provider="email_pdf", data={ + "reason": "exception", + "exception": "SMTP code {}, max retries exceeded".format(e.smtp_code), + }) raise e logger.exception('Error sending email') @@ -602,6 +623,11 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st 'invoices': [], } ) + for i in invoices_to_mark_transmitted: + i.set_transmission_failed(provider="email_pdf", data={ + "reason": "exception", + "exception": "SMTP code {}".format(e.smtp_code), + }) raise SendMailException('Failed to send an email to {}.'.format(to)) except smtplib.SMTPRecipientsRefused as e: @@ -633,6 +659,11 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st 'invoices': [], } ) + for i in invoices_to_mark_transmitted: + i.set_transmission_failed(provider="email_pdf", data={ + "reason": "exception", + "exception": "SMTP error", + }) raise SendMailException('Failed to send an email to {}.'.format(to)) except Exception as e: @@ -650,6 +681,11 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st 'invoices': [], } ) + for i in invoices_to_mark_transmitted: + i.set_transmission_failed(provider="email_pdf", data={ + "reason": "exception", + "exception": "Internal error", + }) raise e if log_target: log_target.log_action( @@ -661,59 +697,52 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st 'invoices': [], } ) + for i in invoices_to_mark_transmitted: + i.set_transmission_failed(provider="email_pdf", data={ + "reason": "exception", + "exception": "Internal error", + }) logger.exception('Error sending email') raise SendMailException('Failed to send an email to {}.'.format(to)) else: - for i in invoices_sent: - if i.transmission_type == "email": - # Mark invoice as sent when it was sent to the requested address *either* at the time of invoice - # creation *or* as of right now. - expected_recipients = [ - (i.invoice_to_transmission_info or {}).get("transmission_email_address") or i.order.email, - ] - try: - expected_recipients.append((i.order.invoice_address.transmission_info or {}).get("transmission_email_address") or i.order.email) - except InvoiceAddress.DoesNotExist: - pass - if not any(t in expected_recipients for t in to): - continue - if i.transmission_status != Invoice.TRANSMISSION_STATUS_COMPLETED: - i.transmission_date = now() - i.transmission_status = Invoice.TRANSMISSION_STATUS_COMPLETED - i.transmission_provider = "email_pdf" - i.transmission_info = { - "sent": [ - { - "recipients": to, - "datetime": now().isoformat(), - } - ] - } - i.save(update_fields=[ - "transmission_date", "transmission_provider", "transmission_status", - "transmission_info" - ]) - elif i.transmission_provider == "email_pdf": - i.transmission_info["sent"].append( + for i in invoices_to_mark_transmitted: + if i.transmission_status != Invoice.TRANSMISSION_STATUS_COMPLETED: + i.transmission_date = now() + i.transmission_status = Invoice.TRANSMISSION_STATUS_COMPLETED + i.transmission_provider = "email_pdf" + i.transmission_info = { + "sent": [ { "recipients": to, "datetime": now().isoformat(), } - ) - i.save(update_fields=[ - "transmission_info" - ]) - i.order.log_action( - "pretix.event.order.invoice.sent", - data={ - "full_invoice_no": i.full_invoice_no, - "transmission_provider": "email_pdf", - "transmission_type": "email", - "data": { - "recipients": [to], - }, + ] + } + i.save(update_fields=[ + "transmission_date", "transmission_provider", "transmission_status", + "transmission_info" + ]) + elif i.transmission_provider == "email_pdf": + i.transmission_info["sent"].append( + { + "recipients": to, + "datetime": now().isoformat(), } ) + i.save(update_fields=[ + "transmission_info" + ]) + i.order.log_action( + "pretix.event.order.invoice.sent", + data={ + "full_invoice_no": i.full_invoice_no, + "transmission_provider": "email_pdf", + "transmission_type": "email", + "data": { + "recipients": [to], + }, + } + ) def mail_send(*args, **kwargs): diff --git a/src/pretix/testutils/mail.py b/src/pretix/testutils/mail.py new file mode 100644 index 0000000000..134989720b --- /dev/null +++ b/src/pretix/testutils/mail.py @@ -0,0 +1,31 @@ +# +# This file is part of pretix (Community Edition). +# +# Copyright (C) 2014-2020 Raphael Michel and contributors +# Copyright (C) 2020-2021 rami.io GmbH and contributors +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General +# Public License as published by the Free Software Foundation in version 3 of the License. +# +# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are +# applicable granting you additional permissions and placing additional restrictions on your usage of this software. +# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive +# this file, see . +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. If not, see +# . +# +import smtplib + +from django.core.mail.backends.locmem import EmailBackend + + +class FailingEmailBackend(EmailBackend): + def send_messages(self, email_messages): + raise smtplib.SMTPRecipientsRefused({ + 'recipient@example.org': (450, b'Recipient unknown') + }) diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py index d9652c0a8a..0d9ce2b8da 100644 --- a/src/tests/base/test_orders.py +++ b/src/tests/base/test_orders.py @@ -29,7 +29,7 @@ import pytest from django.conf import settings from django.core import mail as djmail from django.db.models import F, Sum -from django.test import TestCase +from django.test import TestCase, override_settings from django.utils.timezone import make_aware, now from django_countries.fields import Country from django_scopes import scope @@ -811,6 +811,57 @@ def test_mark_invoices_as_sent(event): assert i.transmission_provider == "email_pdf" +@pytest.mark.django_db(transaction=True) +@override_settings(EMAIL_BACKEND='pretix.testutils.mail.FailingEmailBackend') +def test_mark_invoices_as_failed(event): + djmail.outbox = [] + event.settings.invoice_address_asked = True + event.settings.invoice_address_required = True + event.settings.invoice_generate = "True" + event.settings.invoice_email_attachment = True + o1 = Order.objects.create( + code='FOO', event=event, email='dummy@dummy.test', + status=Order.STATUS_PENDING, + datetime=now(), expires=now() - timedelta(days=10), + total=10, locale='en', + sales_channel=event.organizer.sales_channels.get(identifier="web"), + ) + ticket = Item.objects.create(event=event, name='Early-bird ticket', + default_price=Decimal('23.00'), admission=True) + OrderPosition.objects.create( + order=o1, item=ticket, variation=None, price=Decimal("23.00"), + attendee_name_parts={'full_name': "Peter"}, + positionid=1 + ) + ia = InvoiceAddress.objects.create( + order=o1, + is_business=True, + country=Country('AT'), + transmission_type="email", + transmission_info={ + "transmission_email_address": "invoice@example.org", + } + ) + o1.create_transactions() + i = generate_invoice(o1) + assert i.transmission_type == "email" + assert i.transmission_status == Invoice.TRANSMISSION_STATUS_PENDING + assert not i.transmission_provider + + # If no other address is there, order address will be accepted + ia.transmission_info = {} + ia.save() + o1.send_mail( + subject=LazyI18nString({"en": "Hey"}), + template=LazyI18nString({"en": "Just wanted to send this invoice"}), + context={}, + invoices=[i] + ) + i.refresh_from_db() + assert i.transmission_status == Invoice.TRANSMISSION_STATUS_FAILED + assert i.transmission_provider == "email_pdf" + + class PaymentReminderTests(TestCase): def setUp(self): super().setUp()