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: 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) html_with_cid, cid_images = replace_images_with_cid_paths(html) html_message.attach( SafeMIMEText(html_with_cid, 'html', settings.DEFAULT_CHARSET)) attach_cid_images(html_message, cid_images, verify_ssl=True) email.attach_alternative(html_message, "multipart/related") if user: user = User.objects.get(pk=user) if event: with scopes_disabled(): event = Event.objects.get(id=event) backend = event.get_mail_backend() cm = lambda: scope(organizer=event.organizer) # noqa elif organizer: with scopes_disabled(): organizer = Organizer.objects.get(id=organizer) backend = organizer.get_mail_backend() cm = lambda: scope(organizer=organizer) # noqa else: backend = get_connection(fail_silently=False) cm = lambda: scopes_disabled() # noqa with cm(): if customer: customer = Customer.objects.get(pk=customer) log_target = user or customer if event: if order: try: order = event.orders.get(pk=order) log_target = order except Order.DoesNotExist: order = None else: with language(order.locale, event.settings.region): if not event.settings.mail_attach_tickets: attach_tickets = False if position: try: position = order.positions.get(pk=position) except OrderPosition.DoesNotExist: attach_tickets = False if attach_tickets: args = [] attach_size = 0 for name, ct in get_tickets_for_order( order, base_position=position): try: content = ct.file.read() args.append((name, content, ct.type)) attach_size += len(content) except: # This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out # why (probably some race condition with ticket cache invalidation?), so retry later. try: self.retry(max_retries=5, countdown=60) except MaxRetriesExceededError: # Well then, something is really wrong, let's send it without attachment before we # don't sent at all logger.exception( 'Could not attach invoice to email' ) pass if attach_size < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT: # Do not attach more than 4MB, it will bounce way to often. for a in args: try: email.attach(*a) except: pass else: order.log_action( 'pretix.event.order.email.attachments.skipped', data={ 'subject': 'Attachments skipped', 'message': 'Attachment have not been send because {} bytes are likely too large to arrive.' .format(attach_size), 'recipient': '', 'invoices': [], }) if attach_ical: ical_events = set() if event.has_subevents: if position: ical_events.add(position.subevent) else: for p in order.positions.all(): ical_events.add(p.subevent) else: ical_events.add(order.event) for i, e in enumerate(ical_events): cal = get_ical([e]) email.attach('event-{}.ics'.format(i), cal.serialize(), 'text/calendar') email = email_filter.send_chained(event, 'message', message=email, order=order, user=user) if invoices: invoices = Invoice.objects.filter(pk__in=invoices) for inv in invoices: if inv.file: try: with language(inv.order.locale): email.attach( pgettext('invoice', 'Invoice {num}').format( num=inv.number).replace(' ', '_') + '.pdf', inv.file.file.read(), 'application/pdf') except: logger.exception('Could not attach invoice to email') pass if attach_cached_files: for cf in CachedFile.objects.filter(id__in=attach_cached_files): if cf.file: try: email.attach( cf.filename, cf.file.file.read(), cf.type, ) except: logger.exception('Could not attach file to email') pass email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order, organizer=organizer, customer=customer) try: backend.send_messages([email]) except (smtplib.SMTPResponseException, smtplib.SMTPSenderRefused) as e: if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452): # Most likely temporary, retry again (but pretty soon) try: self.retry( max_retries=5, countdown=2** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes except MaxRetriesExceededError: if log_target: log_target.log_action( 'pretix.email.error', data={ 'subject': 'SMTP code {}, max retries exceeded'.format( e.smtp_code), 'message': e.smtp_error.decode() if isinstance( e.smtp_error, bytes) else str( e.smtp_error), 'recipient': '', 'invoices': [], }) raise e logger.exception('Error sending email') if log_target: log_target.log_action( 'pretix.email.error', data={ 'subject': 'SMTP code {}'.format(e.smtp_code), 'message': e.smtp_error.decode() if isinstance( e.smtp_error, bytes) else str(e.smtp_error), 'recipient': '', 'invoices': [], }) raise SendMailException( 'Failed to send an email to {}.'.format(to)) except smtplib.SMTPRecipientsRefused as e: smtp_codes = [a[0] for a in e.recipients.values()] if not any(c >= 500 for c in smtp_codes): # Not a permanent failure (mailbox full, service unavailable), retry later, but with large intervals try: self.retry( max_retries=5, countdown=2**(self.request.retries * 3) * 4 ) # max is 2 ** (4*3) * 4 = 16384 seconds = approx 4.5 hours except MaxRetriesExceededError: # ignore and go on with logging the error pass logger.exception('Error sending email') if log_target: message = [] for e, val in e.recipients.items(): message.append(f'{e}: {val[0]} {val[1].decode()}') log_target.log_action('pretix.email.error', data={ 'subject': 'SMTP error', 'message': '\n'.join(message), 'recipient': '', 'invoices': [], }) raise SendMailException( 'Failed to send an email to {}.'.format(to)) except Exception as e: if isinstance(e, (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, ssl.SSLError, OSError)): try: self.retry( max_retries=5, countdown=2** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes except MaxRetriesExceededError: if log_target: log_target.log_action('pretix.email.error', data={ 'subject': 'Internal error', 'message': 'Max retries exceeded', 'recipient': '', 'invoices': [], }) raise e if logger: log_target.log_action('pretix.email.error', data={ 'subject': 'Internal error', 'message': str(e), 'recipient': '', 'invoices': [], }) logger.exception('Error sending email') raise SendMailException( 'Failed to send an email to {}.'.format(to))
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, attach_ical=False) -> 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) html_with_cid, cid_images = replace_images_with_cid_paths(html) html_message.attach(SafeMIMEText(html_with_cid, 'html', settings.DEFAULT_CHARSET)) attach_cid_images(html_message, cid_images, verify_ssl=True) email.attach_alternative(html_message, "multipart/related") if user: user = User.objects.get(pk=user) if event: with scopes_disabled(): event = Event.objects.get(id=event) backend = event.get_mail_backend() cm = lambda: scope(organizer=event.organizer) # noqa else: backend = get_connection(fail_silently=False) cm = lambda: scopes_disabled() # noqa with cm(): if invoices: invoices = Invoice.objects.filter(pk__in=invoices) for inv in invoices: if inv.file: try: with language(inv.order.locale): email.attach( pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf', inv.file.file.read(), 'application/pdf' ) except: logger.exception('Could not attach invoice to email') pass if event: if order: try: order = event.orders.get(pk=order) except Order.DoesNotExist: order = None else: if position: try: position = order.positions.get(pk=position) except OrderPosition.DoesNotExist: attach_tickets = False if attach_tickets: args = [] attach_size = 0 for name, ct in get_tickets_for_order(order, base_position=position): content = ct.file.read() args.append((name, content, ct.type)) attach_size += len(content) if attach_size < 4 * 1024 * 1024: # Do not attach more than 4MB, it will bounce way to often. for a in args: try: email.attach(*a) except: pass else: order.log_action( 'pretix.event.order.email.attachments.skipped', data={ 'subject': 'Attachments skipped', 'message': 'Attachment have not been send because {} bytes are likely too large to arrive.'.format(attach_size), 'recipient': '', 'invoices': [], } ) if attach_ical: ical_events = set() if event.has_subevents: if position: ical_events.add(position.subevent) else: for p in order.positions.all(): ical_events.add(p.subevent) else: ical_events.add(order.event) for i, e in enumerate(ical_events): cal = get_ical([e]) email.attach('event-{}.ics'.format(i), cal.serialize(), 'text/calendar') email = email_filter.send_chained(event, 'message', message=email, order=order, user=user) email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order) try: backend.send_messages([email]) except smtplib.SMTPResponseException as e: if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452): self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2)) logger.exception('Error sending email') if order: order.log_action( 'pretix.event.order.email.error', data={ 'subject': 'SMTP code {}'.format(e.smtp_code), 'message': e.smtp_error.decode() if isinstance(e.smtp_error, bytes) else str(e.smtp_error), 'recipient': '', 'invoices': [], } ) raise SendMailException('Failed to send an email to {}.'.format(to)) except Exception as e: if order: order.log_action( 'pretix.event.order.email.error', data={ 'subject': 'Internal error', 'message': str(e), 'recipient': '', 'invoices': [], } ) logger.exception('Error sending email') raise SendMailException('Failed to send an email to {}.'.format(to))
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, 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) html_with_cid, cid_images = replace_images_with_cid_paths(html) html_message.attach( SafeMIMEText(html_with_cid, 'html', settings.DEFAULT_CHARSET)) attach_cid_images(html_message, cid_images, verify_ssl=True) email.attach_alternative(html_message, "multipart/related") if user: user = User.objects.get(pk=user) if event: with scopes_disabled(): event = Event.objects.get(id=event) backend = event.get_mail_backend() cm = lambda: scope(organizer=event.organizer) # noqa elif organizer: with scopes_disabled(): organizer = Organizer.objects.get(id=organizer) backend = organizer.get_mail_backend() cm = lambda: scope(organizer=organizer) # noqa else: backend = get_connection(fail_silently=False) cm = lambda: scopes_disabled() # noqa with cm(): if customer: customer = Customer.objects.get(pk=customer) log_target = user or customer if event: if order: try: order = event.orders.get(pk=order) log_target = order except Order.DoesNotExist: order = None else: with language(order.locale, event.settings.region): if not event.settings.mail_attach_tickets: attach_tickets = False if position: try: position = order.positions.get(pk=position) except OrderPosition.DoesNotExist: attach_tickets = False if attach_tickets: args = [] attach_size = 0 for name, ct in get_tickets_for_order( order, base_position=position): try: content = ct.file.read() args.append((name, content, ct.type)) attach_size += len(content) except: # This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out # why (probably some race condition with ticket cache invalidation?), so retry later. try: self.retry(max_retries=5, countdown=60) except MaxRetriesExceededError: # Well then, something is really wrong, let's send it without attachment before we # don't sent at all logger.exception( 'Could not attach invoice to email' ) pass if attach_size < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT: # Do not attach more than 4MB, it will bounce way to often. for a in args: try: email.attach(*a) except: pass else: order.log_action( 'pretix.event.order.email.attachments.skipped', data={ 'subject': 'Attachments skipped', 'message': 'Attachment have not been send because {} bytes are likely too large to arrive.' .format(attach_size), 'recipient': '', 'invoices': [], }) if attach_ical: for i, cal in enumerate( get_private_icals( event, [position] if position else order.positions.all())): email.attach('event-{}.ics'.format(i), cal.serialize(), 'text/calendar') email = email_filter.send_chained(event, 'message', message=email, order=order, user=user) invoices_sent = [] if invoices: invoices = Invoice.objects.filter(pk__in=invoices) for inv in invoices: if inv.file: try: with language(inv.order.locale): email.attach( pgettext('invoice', 'Invoice {num}').format( num=inv.number).replace(' ', '_') + '.pdf', inv.file.file.read(), 'application/pdf') invoices_sent.append(inv) except: 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: try: email.attach( cf.filename, cf.file.file.read(), cf.type, ) except: logger.exception('Could not attach file to email') pass email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order, organizer=organizer, customer=customer) try: backend.send_messages([email]) except (smtplib.SMTPResponseException, smtplib.SMTPSenderRefused) as e: if e.smtp_code in (101, 111, 421, 422, 431, 432, 442, 447, 452): if e.smtp_code == 432 and settings.HAS_REDIS: # This is likely Microsoft Exchange Online which has a pretty bad rate limit of max. 3 concurrent # SMTP connections which is *easily* exceeded with many celery threads. Just retrying with exponential # backoff won't be good enough if we have a lot of emails, instead we'll need to make sure our retry # intervals scatter such that the email won't all be retried at the same time again and cause the # same problem. # See also https://docs.microsoft.com/en-us/exchange/troubleshoot/send-emails/smtp-submission-improvements from django_redis import get_redis_connection redis_key = "pretix_mail_retry_" + hashlib.sha1( f"{getattr(backend, 'username', '_')}@{getattr(backend, 'host', '_')}" .encode()).hexdigest() rc = get_redis_connection("redis") cnt = rc.incr(redis_key) rc.expire(redis_key, 300) max_retries = 10 retry_after = 30 + cnt * 10 else: # Most likely some other kind of temporary failure, retry again (but pretty soon) max_retries = 5 retry_after = 2**( self.request.retries * 3 ) # max is 2 ** (4*3) = 4096 seconds = 68 minutes try: self.retry(max_retries=max_retries, countdown=retry_after) except MaxRetriesExceededError: if log_target: log_target.log_action( 'pretix.email.error', data={ 'subject': 'SMTP code {}, max retries exceeded'.format( e.smtp_code), 'message': e.smtp_error.decode() if isinstance( e.smtp_error, bytes) else str( e.smtp_error), 'recipient': '', 'invoices': [], }) raise e logger.exception('Error sending email') if log_target: log_target.log_action( 'pretix.email.error', data={ 'subject': 'SMTP code {}'.format(e.smtp_code), 'message': e.smtp_error.decode() if isinstance( e.smtp_error, bytes) else str(e.smtp_error), 'recipient': '', 'invoices': [], }) raise SendMailException( 'Failed to send an email to {}.'.format(to)) except smtplib.SMTPRecipientsRefused as e: smtp_codes = [a[0] for a in e.recipients.values()] if not any(c >= 500 for c in smtp_codes): # Not a permanent failure (mailbox full, service unavailable), retry later, but with large intervals try: self.retry( max_retries=5, countdown=2**(self.request.retries * 3) * 4 ) # max is 2 ** (4*3) * 4 = 16384 seconds = approx 4.5 hours except MaxRetriesExceededError: # ignore and go on with logging the error pass logger.exception('Error sending email') if log_target: message = [] for e, val in e.recipients.items(): message.append(f'{e}: {val[0]} {val[1].decode()}') log_target.log_action('pretix.email.error', data={ 'subject': 'SMTP error', 'message': '\n'.join(message), 'recipient': '', 'invoices': [], }) raise SendMailException( 'Failed to send an email to {}.'.format(to)) except Exception as e: if isinstance(e, (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, ssl.SSLError, OSError)): try: self.retry( max_retries=5, countdown=2** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes except MaxRetriesExceededError: if log_target: log_target.log_action('pretix.email.error', data={ 'subject': 'Internal error', 'message': 'Max retries exceeded', 'recipient': '', 'invoices': [], }) raise e if log_target: log_target.log_action('pretix.email.error', data={ 'subject': 'Internal error', 'message': str(e), 'recipient': '', 'invoices': [], }) logger.exception('Error sending email') raise SendMailException( 'Failed to send an email to {}.'.format(to)) else: for i in invoices_sent: i.sent_to_customer = now() i.save(update_fields=['sent_to_customer'])