示例#1
0
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))
示例#2
0
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))
示例#3
0
文件: mail.py 项目: thegcat/pretix
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'])