Example #1
0
    def _create_attachments(self, msg) -> SafeMIMEMultipart:
        """
        Construct the list of languages and alternatives.

        :param msg: the message to enhance.
        :return: the enhanced message
        """
        encoding = self.encoding or settings.DEFAULT_CHARSET
        body_msg = msg
        msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding)

        if self.body or body_msg.is_multipart():
            msg.attach(body_msg)

        # Attach all attachments.
        for attachment in self.attachments:
            if isinstance(attachment, MIMEBase):
                # This may be dysfunctional, but this is unsure.
                msg.attach(attachment)
            else:
                msg.attach(self._create_attachment(*attachment))

        for lang, body in self.languages:
            att = self._create_attachment(filename=None,
                                          content=body,
                                          mimetype="message/rfc822")
            att.add_header("Content-Language", lang)
            att.add_header("Content-Translation-Type", "automated")
            att.add_header("Content-Disposition",
                           "inline",
                           filename="translation-{}.eml".format(lang))

            msg.attach(att)

        return msg
Example #2
0
 def _create_related_attachments(self, msg):
     encoding = self.encoding or settings.DEFAULT_CHARSET
     if self.related_attachments:
         body_msg = msg
         msg = SafeMIMEMultipart(_subtype=self.related_subtype, encoding=encoding)
         if self.body:
             msg.attach(body_msg)
         for related in self.related_attachments:
             msg.attach(self._create_related_attachment(*related))
     return msg
Example #3
0
    def encrypt_message(self, message):
        to_encrypt = self.get_base_message(message)
        backend = self.get_backend()

        control_msg = backend.get_control_message()
        encrypted_msg = backend.get_octet_stream(
            to_encrypt, recipients=self.gpg_recipients, signer=self.gpg_signer)

        if isinstance(message, SafeMIMEMultipart):
            message.set_payload([control_msg, encrypted_msg])
            message.set_param('protocol', self.protocol)
            message.set_type('multipart/encrypted')
            return message

        gpg_msg = SafeMIMEMultipart(_subtype='encrypted',
                                    encoding=message.encoding)
        gpg_msg.attach(control_msg)
        gpg_msg.attach(encrypted_msg)

        # copy headers
        for key, value in message.items():
            if key.lower() in ['Content-Type', 'Content-Transfer-Encoding']:
                continue
            gpg_msg[key] = value

        gpg_msg.set_param('protocol', self.protocol)
        return gpg_msg
Example #4
0
    def message(self):
        from ..utils import replace_cid_and_change_headers

        to = anyjson.loads(self.to)
        cc = anyjson.loads(self.cc)
        bcc = anyjson.loads(self.bcc)

        html, text, inline_headers = replace_cid_and_change_headers(self.body, self.original_message_id)

        email_message = SafeMIMEMultipart('related')
        email_message['Subject'] = self.subject
        email_message['From'] = self.send_from.to_header()

        if to:
            email_message['To'] = ','.join(list(to))
        if cc:
            email_message['cc'] = ','.join(list(cc))
        if bcc:
            email_message['bcc'] = ','.join(list(bcc))

        email_message_alternative = SafeMIMEMultipart('alternative')
        email_message.attach(email_message_alternative)

        email_message_text = SafeMIMEText(text, 'plain', 'utf-8')
        email_message_alternative.attach(email_message_text)

        email_message_html = SafeMIMEText(html, 'html', 'utf-8')
        email_message_alternative.attach(email_message_html)

        try:
            add_attachments_to_email(self, email_message, inline_headers)
        except IOError:
            return False

        return email_message
Example #5
0
    def message(self):
        # If neither encryption nor signing was request, we just return the normal message
        orig_msg = super(GPGEmailMessage, self).message()
        if not self.encrypted and not self.signed:
            return orig_msg

        encoding = self.encoding or settings.DEFAULT_CHARSET
        signers = self.gpg_signers
        recipients = self.gpg_recipients
        context = self.gpg_context

        if isinstance(orig_msg, MIMEMultipart):
            to_encrypt = MIMEMultipart(_subtype='alternative', _subparts=orig_msg.get_payload())
        else:  # No attachments were added
            to_encrypt = orig_msg.get_payload()

        msg = rfc3156(to_encrypt, recipients=recipients, signers=signers, context=context,
                      always_trust=self.gpg_always_trust)

        # if this is already a Multipart message, we can just set the payload and return it
        if isinstance(orig_msg, MIMEMultipart):
            orig_msg.policy = orig_msg.policy.clone(max_line_length=0)
            orig_msg.set_payload(msg.get_payload())
            orig_msg.set_param('protocol', self.protocol)

            # Set the micalg Content-Type parameter. Only present in messages that are only signed
            # TODO:We don't yet know how to get the correct value, we just return GPGs default
            if self.encrypted is False:
                orig_msg.set_param('micalg', 'pgp-sha256')

            return orig_msg

        # This message was not a multipart message, so we create a new multipart message and attach
        # the payload of the signed and/or encrypted payload.
        body, sig = msg.get_payload()

        gpg_msg = SafeMIMEMultipart(_subtype=self.alternative_subtype, encoding=encoding)
        gpg_msg.attach(body)
        gpg_msg.attach(sig)

        for key, value in orig_msg.items():
            if key.lower() in ['Content-Type', 'Content-Transfer-Encoding']:
                continue
            gpg_msg[key] = value

        # TODO: We don't yet know how to get the correct value
        if self.encrypted is False:
            gpg_msg.set_param('micalg', 'pgp-sha256')
        gpg_msg.set_param('protocol', self.protocol)

        return gpg_msg
Example #6
0
	def message(self):
		msg=super(EmailPGP, self).message()
		encoding = self.encoding or settings.DEFAULT_CHARSET
		del msg['From']
		del msg['Subject']
		del msg['To']
		del msg['Date']
		
		if self.signed:
			tmp = SafeMIMEMultipart(_subtype=self.signed_subtype, encoding=encoding)
			tmp.attach(msg)
			attachment = MIMEBase('application', 'pgp-signature') #We don't want base64 enconding
			attachment.set_payload(detach_sign(msg.as_string(), self.from_email))
			attachment.add_header('Content-Disposition', 'attachment', filename='signature.asc')
			tmp.attach(attachment)
			msg=tmp
		
		if self.encrypted:
			tmp = SafeMIMEMultipart(_subtype=self.encrypted_subtype, encoding=encoding)
			tmp.attach(self._create_attachment('', '', 'application/pgp-encrypted'))
			attachment = MIMEBase('application', 'octet-stream') #We don't want base64 enconding
			attachment.set_payload(encrypt(msg.as_string(), self.to))
			attachment.add_header('Content-Disposition', 'inline', filename='msg.asc')
			tmp.attach(attachment)
			msg=tmp
		
		msg['Subject'] = self.subject
		msg['From'] = self.extra_headers.get('From', self.from_email)
		msg['To'] = self.extra_headers.get('To', ', '.join(self.to))
		if self.cc:
			msg['Cc'] = ', '.join(self.cc)
		# Email header names are case-insensitive (RFC 2045), so we have to
		# accommodate that when doing comparisons.
		header_names = [key.lower() for key in self.extra_headers]
		if 'date' not in header_names:
			msg['Date'] = formatdate()
		if 'message-id' not in header_names:
			msg['Message-ID'] = make_msgid()
		for name, value in self.extra_headers.items():
			if name.lower() in ('from', 'to'):  # From and To are already handled
				continue
			msg[name] = value
		return msg
Example #7
0
    def message(self):
        encoding = self.encoding or settings.DEFAULT_CHARSET
        msg = MIMEUTF8QPText(self.body, encoding)
        msg = self._create_message(msg)

        msg['Subject'] = self.subject
        msg['From'] = self.extra_headers.get('From', self.from_email)
        msg['To'] = self.extra_headers.get('To', ', '.join(self.to))
        if self.cc:
            msg['Cc'] = ', '.join(self.cc)

        header_names = [key.lower() for key in self.extra_headers]
        if 'date' not in header_names:
            msg['Date'] = formatdate()
        if 'message-id' not in header_names:
            msg['Message-ID'] = make_msgid()
        for name, value in self.extra_headers.items():
            if name.lower() in ('from', 'to'):
                # From and To are already handled
                continue
            msg[name] = value

        del msg['MIME-Version']

        wrapper = SafeMIMEMultipart(
            'signed', protocol='application/pgp-signature',
            micalg='pgp-sha512')
        wrapper.preamble = (
            "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)"
        )

        # copy headers from original message to PGP/MIME envelope
        for header in msg.keys():
            if header.lower() not in (
                    'content-disposition', 'content-type', 'mime-version'
            ):
                for value in msg.get_all(header):
                    wrapper.add_header(header, value)
                del msg[header]

        for part in msg.walk():
            del part['MIME-Version']

        signature = self._sign(msg)

        wrapper['Content-Disposition'] = 'inline'
        wrapper.attach(msg)
        wrapper.attach(signature)

        return wrapper
Example #8
0
    def message(self):
        # Construct multipart/signed
        multipart_signed = SafeMIMEMultipart(
            _subtype="signed",
            micalg="pgp-{0}".format(self.DIGEST_ALGO.lower()),
            protocol="application/pgp-signature")

        self._set_headers(multipart_signed)

        # Construct multipart/encrypted
        multipart_encrypted = self._create_multipart_encrypted()

        # Sign the encrypted multipart
        multipart_encrypted_text = multipart_encrypted.as_string().replace(
            '\n', '\r\n')
        signature = self._sign(multipart_encrypted_text, self.DIGEST_ALGO)

        # Construct the signature part from signature
        signature_part = self._create_signature_part(signature)

        # Attach both the multipart/encrypted and the signature
        multipart_signed.attach(multipart_encrypted)
        multipart_signed.attach(signature_part)

        return multipart_signed
Example #9
0
 def message(self):
     plain_msg = super(SignedEmailMessage, self).message()
     headers = dict()
     for k, v in plain_msg.items():
         if k.lower() not in self.MIME_HEADERS:
             headers[k] = v
             del plain_msg[k]
     if not self.attachments:
         # When attachment is added, message is automaticaly set to be SafeMIMEMultipart. We have to force it to do so.
         encoding = self.encoding or settings.DEFAULT_CHARSET
         body_msg = plain_msg
         msg = SafeMIMEMultipart(_subtype=self.mixed_subtype,
                                 encoding=encoding)
         if self.body:
             msg.attach(body_msg)
         plain_msg = msg
     message_body = createsmime(plain_msg.as_string(), self.from_key,
                                self.from_cert)
     msg = message_from_string(message_body)
     for k, v in headers.items():
         msg[k] = v
     return msg
Example #10
0
 def _create_related_attachments(self, msg):
     encoding = self.encoding or settings.DEFAULT_CHARSET
     if self.related_attachments:
         body_msg = msg
         msg = SafeMIMEMultipart(_subtype=self.related_subtype,
                                 encoding=encoding)
         if self.body:
             msg.attach(body_msg)
         for related_attachment in self.related_attachments:
             if isinstance(related_attachment, MIMEBase):
                 msg.attach(related_attachment)
             else:
                 msg.attach(
                     self._create_related_attachment(*related_attachment))
     return msg
Example #11
0
    def sign_message(self, message, **kwargs):
        to_sign = self.get_base_message(message)
        backend = self.get_backend()

        if isinstance(message, SafeMIMEMultipart):
            # We have to adjust the policy because Django SOMEHOW adjusts the line-length of
            # multipart messages. This means a line-break in the Content-Type header of to_sign
            # gets removed, and this breaks the signature.
            to_sign.policy = to_sign.policy.clone(max_line_length=0)

        # get the gpg signature
        signature = backend.sign(to_sign.as_bytes(linesep='\r\n'),
                                 self.gpg_signer)
        signature_msg = backend.get_mime_signature(signature)

        if isinstance(message, SafeMIMEMultipart):
            message.set_payload([to_sign, signature_msg])
            message.set_param('protocol', self.protocol)
            message.set_param('micalg', 'pgp-sha256')
            message.set_type('multipart/signed')
            return message

        gpg_msg = SafeMIMEMultipart(_subtype='signed',
                                    encoding=message.encoding)
        gpg_msg.attach(to_sign)
        gpg_msg.attach(signature_msg)

        # copy headers
        for key, value in message.items():
            if key.lower() in ['Content-Type', 'Content-Transfer-Encoding']:
                continue
            gpg_msg[key] = value

        gpg_msg.set_param('protocol', self.protocol)
        gpg_msg.set_param('micalg', 'pgp-sha256')
        return gpg_msg
Example #12
0
    def message(self):
        from ..utils import replace_cid_and_change_headers

        to = anyjson.loads(self.to)
        cc = anyjson.loads(self.cc)
        bcc = anyjson.loads(self.bcc)

        html, text, inline_headers = replace_cid_and_change_headers(
            self.body, self.original_message_id)

        email_message = SafeMIMEMultipart('related')
        email_message['Subject'] = self.subject
        email_message['From'] = self.send_from.to_header()

        if to:
            email_message['To'] = ','.join(list(to))
        if cc:
            email_message['cc'] = ','.join(list(cc))
        if bcc:
            email_message['bcc'] = ','.join(list(bcc))

        email_message_alternative = SafeMIMEMultipart('alternative')
        email_message.attach(email_message_alternative)

        email_message_text = SafeMIMEText(text, 'plain', 'utf-8')
        email_message_alternative.attach(email_message_text)

        email_message_html = SafeMIMEText(html, 'html', 'utf-8')
        email_message_alternative.attach(email_message_html)

        try:
            add_attachments_to_email(self, email_message, inline_headers)
        except IOError:
            return False

        return email_message
Example #13
0
 def _create_related_attachments(self, msg):
     encoding = self.encoding or 'utf-8'
     if self.related_attachments:
         body_msg = msg
         msg = SafeMIMEMultipart(_subtype=self.related_subtype,
                                 encoding=encoding)
         if self.body:
             msg.attach(body_msg)
         for related in self.related_attachments:
             msg.attach(self._create_related_attachment(*related))
     return msg
    def _create_alternatives(self, msg):
        """Copy of EmailMultiAlternatives but also takes html_body into
        account.
        """
        encoding = self.encoding or settings.DEFAULT_CHARSET
        if self.alternatives or (self.body and self.html_body):
            body_msg = msg
            msg = SafeMIMEMultipart(_subtype=self.alternative_subtype, encoding=encoding)
            if self.body:
                msg.attach(body_msg)
            if self.html_body:
                msg.attach(self._create_mime_attachment(self.html_body, 'text/html'))
            for alternative in self.alternatives:
                msg.attach(self._create_mime_attachment(*alternative))

        return msg
Example #15
0
    def get_base_message(self, message):
        payload = message.get_payload()

        if isinstance(message, SafeMIMEMultipart):
            # If this is a multipart message, we encrypt all its parts.
            # We create a new SafeMIMEMultipart instance, the original message contains all
            # headers (From, To, ...) which we shouldn't sign/encrypt.
            subtype = message.get_content_subtype()
            base = SafeMIMEMultipart(_subtype=subtype, _subparts=payload)
        else:
            # If it is a non-multipart message (-> plain-text email), we just encrypt the payload
            base = SafeMIMEText(payload)

            # TODO: Is it possible to influence the main content type of the message? If yes, we
            #       need to copy it here.

        del base['MIME-Version']
        return base
Example #16
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))
Example #17
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))
    def message(self):
        """
        Returns the final message to be sent, including all headers etc. Content and attachments are encrypted using
        GPG in PGP/MIME format (RFC 3156).
        """

        def build_plain_message():
            msg = SafeMIMEText(self.body, self.content_subtype, encoding)
            msg = self._create_message(msg)
            return msg

        def build_version_attachment():
            version_attachment = SafeMIMEText('Version: 1\n', self.content_subtype, encoding)
            del version_attachment['Content-Type']
            version_attachment.add_header('Content-Type', 'application/pgp-encrypted')
            version_attachment.add_header('Content-Description', 'PGP/MIME Versions Identification')
            return version_attachment

        def build_gpg_attachment():
            gpg_attachment = SafeMIMEText(encrypted_msg, self.content_subtype, encoding)
            del gpg_attachment['Content-Type']
            gpg_attachment.add_header('Content-Type', 'application/octet-stream', name=self.gpg_attachment_filename)
            gpg_attachment.add_header('Content-Disposition', 'inline', filename=self.gpg_attachment_filename)
            gpg_attachment.add_header('Content-Description', 'OpenPGP encrypted message')
            return gpg_attachment

        encoding = self.encoding or settings.DEFAULT_CHARSET

        # build message including attachments as it would also be built without GPG
        msg = build_plain_message()

        # encrypt whole message including attachments
        encrypted_msg = self._encrypt(str(msg))

        # build new message object wrapping the encrypted message
        msg = SafeMIMEMultipart(_subtype=self.encrypted_subtype,
                                encoding=encoding,
                                protocol='application/pgp-encrypted')

        version_attachment = build_version_attachment()
        gpg_attachment = build_gpg_attachment()
        msg.attach(version_attachment)
        msg.attach(gpg_attachment)

        self.extra_headers['Content-Transfer-Encoding'] = '7bit'

        # add headers
        # everything below this line has not been modified when overriding message()
        ############################################################################

        msg['Subject'] = self.subject
        msg['From'] = self.extra_headers.get('From', self.from_email)
        msg['To'] = self.extra_headers.get('To', ', '.join(map(force_text, self.to)))
        if self.cc:
            msg['Cc'] = ', '.join(map(force_text, self.cc))
        if self.reply_to:
            msg['Reply-To'] = self.extra_headers.get('Reply-To', ', '.join(map(force_text, self.reply_to)))

        # Email header names are case-insensitive (RFC 2045), so we have to
        # accommodate that when doing comparisons.
        header_names = [key.lower() for key in self.extra_headers]
        if 'date' not in header_names:
            msg['Date'] = formatdate()
        if 'message-id' not in header_names:
            # Use cached DNS_NAME for performance
            msg['Message-ID'] = make_msgid(domain=DNS_NAME)
        for name, value in self.extra_headers.items():
            if name.lower() in ('from', 'to'):  # From and To are already handled
                continue
            msg[name] = value

        return msg
Example #19
0
    def message(self):
        from ..utils import get_attachment_filename_from_url, replace_cid_and_change_headers

        to = anyjson.loads(self.to)
        cc = anyjson.loads(self.cc)
        bcc = anyjson.loads(self.bcc)

        if self.send_from.from_name:
            # Add account name to From header if one is available
            from_email = '"%s" <%s>' % (Header(u"%s" % self.send_from.from_name, "utf-8"), self.send_from.email_address)
        else:
            # Otherwise only add the email address
            from_email = self.send_from.email_address

        html, text, inline_headers = replace_cid_and_change_headers(self.body, self.original_message_id)

        email_message = SafeMIMEMultipart("related")
        email_message["Subject"] = self.subject
        email_message["From"] = from_email

        if to:
            email_message["To"] = ",".join(list(to))
        if cc:
            email_message["cc"] = ",".join(list(cc))
        if bcc:
            email_message["bcc"] = ",".join(list(bcc))

        email_message_alternative = SafeMIMEMultipart("alternative")
        email_message.attach(email_message_alternative)

        email_message_text = SafeMIMEText(text, "plain", "utf-8")
        email_message_alternative.attach(email_message_text)

        email_message_html = SafeMIMEText(html, "html", "utf-8")
        email_message_alternative.attach(email_message_html)

        for attachment in self.attachments.all():
            if attachment.inline:
                continue

            try:
                storage_file = default_storage._open(attachment.attachment.name)
            except IOError:
                logger.exception("Couldn't get attachment, not sending %s" % self.id)
                return False

            filename = get_attachment_filename_from_url(attachment.attachment.name)

            storage_file.open()
            content = storage_file.read()
            storage_file.close()

            content_type, encoding = mimetypes.guess_type(filename)
            if content_type is None or encoding is not None:
                content_type = "application/octet-stream"
            main_type, sub_type = content_type.split("/", 1)

            if main_type == "text":
                msg = MIMEText(content, _subtype=sub_type)
            elif main_type == "image":
                msg = MIMEImage(content, _subtype=sub_type)
            elif main_type == "audio":
                msg = MIMEAudio(content, _subtype=sub_type)
            else:
                msg = MIMEBase(main_type, sub_type)
                msg.set_payload(content)
                Encoders.encode_base64(msg)

            msg.add_header("Content-Disposition", "attachment", filename=os.path.basename(filename))

            email_message.attach(msg)

        # Add the inline attachments to email message header
        for inline_header in inline_headers:
            main_type, sub_type = inline_header["content-type"].split("/", 1)
            if main_type == "image":
                msg = MIMEImage(
                    inline_header["content"],
                    _subtype=sub_type,
                    name=os.path.basename(inline_header["content-filename"]),
                )
                msg.add_header(
                    "Content-Disposition",
                    inline_header["content-disposition"],
                    filename=os.path.basename(inline_header["content-filename"]),
                )
                msg.add_header("Content-ID", inline_header["content-id"])

                email_message.attach(msg)

        return email_message
Example #20
0
    def message(self):
        from ..utils import get_attachment_filename_from_url, replace_cid_and_change_headers

        to = anyjson.loads(self.to)
        cc = anyjson.loads(self.cc)
        bcc = anyjson.loads(self.bcc)

        if self.send_from.from_name:
            # Add account name to From header if one is available
            from_email = '"%s" <%s>' % (
                Header(u'%s' % self.send_from.from_name, 'utf-8'),
                self.send_from.email_address
            )
        else:
            # Otherwise only add the email address
            from_email = self.send_from.email_address

        html, text, inline_headers = replace_cid_and_change_headers(self.body, self.original_message_id)

        email_message = SafeMIMEMultipart('related')
        email_message['Subject'] = self.subject
        email_message['From'] = from_email

        if to:
            email_message['To'] = ','.join(list(to))
        if cc:
            email_message['cc'] = ','.join(list(cc))
        if bcc:
            email_message['bcc'] = ','.join(list(bcc))

        email_message_alternative = SafeMIMEMultipart('alternative')
        email_message.attach(email_message_alternative)

        email_message_text = SafeMIMEText(text, 'plain', 'utf-8')
        email_message_alternative.attach(email_message_text)

        email_message_html = SafeMIMEText(html, 'html', 'utf-8')
        email_message_alternative.attach(email_message_html)

        for attachment in self.attachments.all():
            if attachment.inline:
                continue

            try:
                storage_file = default_storage._open(attachment.attachment.name)
            except IOError:
                logger.exception('Couldn\'t get attachment, not sending %s' % self.id)
                return False

            filename = get_attachment_filename_from_url(attachment.attachment.name)

            storage_file.open()
            content = storage_file.read()
            storage_file.close()

            content_type, encoding = mimetypes.guess_type(filename)
            if content_type is None or encoding is not None:
                content_type = 'application/octet-stream'
            main_type, sub_type = content_type.split('/', 1)

            if main_type == 'text':
                msg = MIMEText(content, _subtype=sub_type)
            elif main_type == 'image':
                msg = MIMEImage(content, _subtype=sub_type)
            elif main_type == 'audio':
                msg = MIMEAudio(content, _subtype=sub_type)
            else:
                msg = MIMEBase(main_type, sub_type)
                msg.set_payload(content)
                Encoders.encode_base64(msg)

            msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(filename))

            email_message.attach(msg)

        # Add the inline attachments to email message header
        for inline_header in inline_headers:
            main_type, sub_type = inline_header['content-type'].split('/', 1)
            if main_type == 'image':
                msg = MIMEImage(
                    inline_header['content'],
                    _subtype=sub_type,
                    name=os.path.basename(inline_header['content-filename'])
                )
                msg.add_header(
                    'Content-Disposition',
                    inline_header['content-disposition'],
                    filename=os.path.basename(inline_header['content-filename'])
                )
                msg.add_header('Content-ID', inline_header['content-id'])

                email_message.attach(msg)

        return email_message
Example #21
0
    def message(self):
        """
        Returns the final message to be sent, including all headers etc. Content and attachments are encrypted using
        GPG in PGP/MIME format (RFC 3156).
        """
        def build_plain_message():
            msg = SafeMIMEText(self.body, self.content_subtype, encoding)
            msg = self._create_message(msg)
            return msg

        def build_version_attachment():
            version_attachment = SafeMIMEText('Version: 1\n',
                                              self.content_subtype, encoding)
            del version_attachment['Content-Type']
            version_attachment.add_header('Content-Type',
                                          'application/pgp-encrypted')
            version_attachment.add_header('Content-Description',
                                          'PGP/MIME Versions Identification')
            return version_attachment

        def build_gpg_attachment():
            gpg_attachment = SafeMIMEText(encrypted_msg, self.content_subtype,
                                          encoding)
            del gpg_attachment['Content-Type']
            gpg_attachment.add_header('Content-Type',
                                      'application/octet-stream',
                                      name=self.gpg_attachment_filename)
            gpg_attachment.add_header('Content-Disposition',
                                      'inline',
                                      filename=self.gpg_attachment_filename)
            gpg_attachment.add_header('Content-Description',
                                      'OpenPGP encrypted message')
            return gpg_attachment

        encoding = self.encoding or settings.DEFAULT_CHARSET

        # build message including attachments as it would also be built without GPG
        msg = build_plain_message()

        # encrypt whole message including attachments
        encrypted_msg = self._encrypt(str(msg))

        # build new message object wrapping the encrypted message
        msg = SafeMIMEMultipart(_subtype=self.encrypted_subtype,
                                encoding=encoding,
                                protocol='application/pgp-encrypted')

        version_attachment = build_version_attachment()
        gpg_attachment = build_gpg_attachment()
        msg.attach(version_attachment)
        msg.attach(gpg_attachment)

        self.extra_headers['Content-Transfer-Encoding'] = '7bit'

        # add headers
        # everything below this line has not been modified when overriding message()
        ############################################################################

        msg['Subject'] = self.subject
        msg['From'] = self.extra_headers.get('From', self.from_email)
        msg['To'] = self.extra_headers.get('To',
                                           ', '.join(map(force_text, self.to)))
        if self.cc:
            msg['Cc'] = ', '.join(map(force_text, self.cc))
        if self.reply_to:
            msg['Reply-To'] = self.extra_headers.get(
                'Reply-To', ', '.join(map(force_text, self.reply_to)))

        # Email header names are case-insensitive (RFC 2045), so we have to
        # accommodate that when doing comparisons.
        header_names = [key.lower() for key in self.extra_headers]
        if 'date' not in header_names:
            msg['Date'] = formatdate()
        if 'message-id' not in header_names:
            # Use cached DNS_NAME for performance
            msg['Message-ID'] = make_msgid(domain=DNS_NAME)
        for name, value in self.extra_headers.items():
            if name.lower() in ('from',
                                'to'):  # From and To are already handled
                continue
            msg[name] = value

        return msg
Example #22
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,
                   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'])
Example #23
0
    def message(self):
        from ..utils import get_attachment_filename_from_url, replace_cid_and_change_headers

        to = anyjson.loads(self.to)
        cc = anyjson.loads(self.cc)
        bcc = anyjson.loads(self.bcc)

        if self.send_from.from_name:
            # Add account name to From header if one is available
            from_email = '"%s" <%s>' % (Header(
                u'%s' % self.send_from.from_name,
                'utf-8'), self.send_from.email_address)
        else:
            # Otherwise only add the email address
            from_email = self.send_from.email_address

        html, text, inline_headers = replace_cid_and_change_headers(
            self.body, self.original_message_id)

        email_message = SafeMIMEMultipart('related')
        email_message['Subject'] = self.subject
        email_message['From'] = from_email

        if to:
            email_message['To'] = ','.join(list(to))
        if cc:
            email_message['cc'] = ','.join(list(cc))
        if bcc:
            email_message['bcc'] = ','.join(list(bcc))

        email_message_alternative = SafeMIMEMultipart('alternative')
        email_message.attach(email_message_alternative)

        email_message_text = SafeMIMEText(text, 'plain', 'utf-8')
        email_message_alternative.attach(email_message_text)

        email_message_html = SafeMIMEText(html, 'html', 'utf-8')
        email_message_alternative.attach(email_message_html)

        for attachment in self.attachments.all():
            if attachment.inline:
                continue

            try:
                storage_file = default_storage._open(
                    attachment.attachment.name)
            except IOError:
                logger.exception('Couldn\'t get attachment, not sending %s' %
                                 self.id)
                return False

            filename = get_attachment_filename_from_url(
                attachment.attachment.name)

            storage_file.open()
            content = storage_file.read()
            storage_file.close()

            content_type, encoding = mimetypes.guess_type(filename)
            if content_type is None or encoding is not None:
                content_type = 'application/octet-stream'
            main_type, sub_type = content_type.split('/', 1)

            if main_type == 'text':
                msg = MIMEText(content, _subtype=sub_type)
            elif main_type == 'image':
                msg = MIMEImage(content, _subtype=sub_type)
            elif main_type == 'audio':
                msg = MIMEAudio(content, _subtype=sub_type)
            else:
                msg = MIMEBase(main_type, sub_type)
                msg.set_payload(content)
                Encoders.encode_base64(msg)

            msg.add_header('Content-Disposition',
                           'attachment',
                           filename=os.path.basename(filename))

            email_message.attach(msg)

        # Add the inline attachments to email message header
        for inline_header in inline_headers:
            main_type, sub_type = inline_header['content-type'].split('/', 1)
            if main_type == 'image':
                msg = MIMEImage(inline_header['content'],
                                _subtype=sub_type,
                                name=os.path.basename(
                                    inline_header['content-filename']))
                msg.add_header('Content-Disposition',
                               inline_header['content-disposition'],
                               filename=os.path.basename(
                                   inline_header['content-filename']))
                msg.add_header('Content-ID', inline_header['content-id'])

                email_message.attach(msg)

        return email_message