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
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
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
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
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
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
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
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
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
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
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
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
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
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
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str, event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None, order: int=None, attach_tickets=False, user=None, attach_ical=False) -> bool: email = CustomEmail(subject, body, sender, to=to, bcc=bcc, headers=headers) if html is not None: html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET) html_with_cid, cid_images = replace_images_with_cid_paths(html) html_message.attach(SafeMIMEText(html_with_cid, 'html', settings.DEFAULT_CHARSET)) attach_cid_images(html_message, cid_images, verify_ssl=True) email.attach_alternative(html_message, "multipart/related") if user: user = User.objects.get(pk=user) if event: with scopes_disabled(): event = Event.objects.get(id=event) backend = event.get_mail_backend() cm = lambda: scope(organizer=event.organizer) # noqa else: backend = get_connection(fail_silently=False) cm = lambda: scopes_disabled() # noqa with cm(): if invoices: invoices = Invoice.objects.filter(pk__in=invoices) for inv in invoices: if inv.file: try: with language(inv.order.locale): email.attach( pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf', inv.file.file.read(), 'application/pdf' ) except: logger.exception('Could not attach invoice to email') pass if event: if order: try: order = event.orders.get(pk=order) except Order.DoesNotExist: order = None else: if position: try: position = order.positions.get(pk=position) except OrderPosition.DoesNotExist: attach_tickets = False if attach_tickets: args = [] attach_size = 0 for name, ct in get_tickets_for_order(order, base_position=position): content = ct.file.read() args.append((name, content, ct.type)) attach_size += len(content) if attach_size < 4 * 1024 * 1024: # Do not attach more than 4MB, it will bounce way to often. for a in args: try: email.attach(*a) except: pass else: order.log_action( 'pretix.event.order.email.attachments.skipped', data={ 'subject': 'Attachments skipped', 'message': 'Attachment have not been send because {} bytes are likely too large to arrive.'.format(attach_size), 'recipient': '', 'invoices': [], } ) if attach_ical: ical_events = set() if event.has_subevents: if position: ical_events.add(position.subevent) else: for p in order.positions.all(): ical_events.add(p.subevent) else: ical_events.add(order.event) for i, e in enumerate(ical_events): cal = get_ical([e]) email.attach('event-{}.ics'.format(i), cal.serialize(), 'text/calendar') email = email_filter.send_chained(event, 'message', message=email, order=order, user=user) email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order) try: backend.send_messages([email]) except smtplib.SMTPResponseException as e: if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452): self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2)) logger.exception('Error sending email') if order: order.log_action( 'pretix.event.order.email.error', data={ 'subject': 'SMTP code {}'.format(e.smtp_code), 'message': e.smtp_error.decode() if isinstance(e.smtp_error, bytes) else str(e.smtp_error), 'recipient': '', 'invoices': [], } ) raise SendMailException('Failed to send an email to {}.'.format(to)) except Exception as e: if order: order.log_action( 'pretix.event.order.email.error', data={ 'subject': 'Internal error', 'message': str(e), 'recipient': '', 'invoices': [], } ) logger.exception('Error sending email') raise SendMailException('Failed to send an email to {}.'.format(to))
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str, event: int = None, position: int = None, headers: dict = None, bcc: List[str] = None, invoices: List[int] = None, order: int = None, attach_tickets=False, user=None, organizer=None, customer=None, attach_ical=False, attach_cached_files: List[int] = None) -> 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
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
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
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
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'])
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