Commit 63caac63 authored by Corentin Forler's avatar Corentin Forler
Browse files

fix: Encode email headers

- Use `SMTP` policy instead of `SMTPUTF8`
- Add `List-Unsubscribe` header
parent 3634fafa
......@@ -11,7 +11,6 @@ import email.utils
from six import iteritems, text_type, string_types
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email import policy
def get_email(recipients, sender='', msg='', subject='[No Subject]',
text_content = None, footer=None, print_html=None, formatted=None, attachments=None,
......@@ -68,8 +67,8 @@ class EMail:
self.subject = subject
self.expose_recipients = expose_recipients
self.msg_root = MIMEMultipart('mixed', policy=policy.SMTPUTF8)
self.msg_alternative = MIMEMultipart('alternative', policy=policy.SMTPUTF8)
self.msg_root = MIMEMultipart('mixed')
self.msg_alternative = MIMEMultipart('alternative')
self.msg_root.attach(self.msg_alternative) = cc or []
self.bcc = bcc or []
......@@ -100,7 +99,7 @@ class EMail:
Attach message in the text portion of multipart/alternative
from email.mime.text import MIMEText
part = MIMEText(message, 'plain', 'utf-8', policy=policy.SMTPUTF8)
part = MIMEText(message, 'plain', 'utf-8')
def set_part_html(self, message, inline_images):
......@@ -113,9 +112,9 @@ class EMail:
message, _inline_images = replace_filename_with_cid(message)
# prepare parts
msg_related = MIMEMultipart('related', policy=policy.SMTPUTF8)
msg_related = MIMEMultipart('related')
html_part = MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8)
html_part = MIMEText(message, 'html', 'utf-8')
for image in _inline_images:
......@@ -124,7 +123,7 @@ class EMail:
self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8))
self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8'))
def set_html_as_text(self, html):
"""Set plain text from HTML"""
......@@ -135,7 +134,7 @@ class EMail:
from email.mime.text import MIMEText
maintype, subtype = mime_type.split('/')
part = MIMEText(message, _subtype = subtype, policy=policy.SMTPUTF8)
part = MIMEText(message, _subtype = subtype)
if as_attachment:
part.add_header('Content-Disposition', 'attachment', filename=filename)
......@@ -144,12 +143,12 @@ class EMail:
def attach_file(self, n):
"""attach a file from the `FileData` table"""
_file = frappe.get_doc("File", {"file_name": n})
content = _file.get_content()
if not content:
from frappe.utils.file_manager import get_file
res = get_file(n)
if not res:
self.add_attachment(_file.file_name, content)
self.add_attachment(res[0], res[1])
def add_attachment(self, fname, fcontent, content_type=None,
parent=None, content_id=None, inline=False):
......@@ -197,19 +196,16 @@ class EMail:
def set_message_id(self, message_id, is_notification=False):
if message_id:
message_id = '<' + message_id + '>'
self.msg_root["Message-Id"] = '<' + message_id + '>'
message_id = get_message_id()
self.set_header('isnotification', '<notification>')
self.msg_root["Message-Id"] = get_message_id()
self.msg_root["isnotification"] = '<notification>'
if is_notification:
self.set_header('isnotification', '<notification>')
self.set_header('Message-Id', message_id)
self.msg_root["isnotification"] = '<notification>'
def set_in_reply_to(self, in_reply_to):
"""Used to send the Message-Id of a received email back as In-Reply-To"""
self.set_header('In-Reply-To', in_reply_to)
self.msg_root["In-Reply-To"] = in_reply_to
def make(self):
"""build into msg_root"""
......@@ -236,16 +232,13 @@ class EMail:
if key in self.msg_root:
del self.msg_root[key]
self.msg_root[key] = value
except ValueError:
self.msg_root[key] = sanitize_email_header(value)
self.msg_root[key] = value
def as_string(self):
"""validate, build message and convert to string"""
return self.msg_root.as_string(policy=policy.SMTPUTF8)
return self.msg_root.as_string()
def get_formatted_html(subject, message, footer=None, print_html=None,
email_account=None, header=None, unsubscribe_link=None, sender=None, with_container=False):
......@@ -475,8 +468,5 @@ def get_header(header=None):
return email_header
def sanitize_email_header(str):
return str.replace('\r', '').replace('\n', '')
def get_brand_logo(email_account):
return email_account.get('brand_logo')
......@@ -3,8 +3,6 @@
from __future__ import unicode_literals
import frappe
import sys
from six.moves import html_parser as HTMLParser
import smtplib, quopri, json
from frappe import msgprint, _, safe_decode, safe_encode, enqueue
from import SMTPServer, get_outgoing_email_account
......@@ -16,6 +14,9 @@ from frappe.utils import get_url, nowdate, now_datetime, add_days, split_emails,
from rq.timeouts import JobTimeoutException
from six import text_type, string_types, PY3
from email.parser import Parser
from email import policy
EMAIL_POLICY = policy.SMTP + policy.strict
class EmailLimitCrossedError(frappe.ValidationError): pass
......@@ -477,6 +478,8 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False):
email_sent_to_any_recipient = any("Sent" == s.status for s in recipients_list)
#if all are sent set status
if email_sent_to_any_recipient:
frappe.db.sql("""update `tabEmail Queue` set status='Sent', modified=%s where name=%s""",
......@@ -554,6 +557,7 @@ def prepare_message(email, recipient, recipients_list):
# No SSL => No Email Read Reciept
message = message.replace("<!--email open check-->", quopri.encodestring("".encode()).decode())
unsubscribe_url = None
if email.add_unsubscribe_link and email.reference_doctype: # is missing the check for unsubscribe message but will not add as there will be no unsubscribe url
unsubscribe_url = get_unsubcribed_url(email.reference_doctype, email.reference_name, recipient,
email.unsubscribe_method, email.unsubscribe_params)
......@@ -580,11 +584,13 @@ def prepare_message(email, recipient, recipients_list):
message = (message and message.encode('utf8')) or ''
message = safe_decode(message)
if PY3:
from email.policy import SMTPUTF8
message = Parser(policy=SMTPUTF8).parsestr(message)
message = Parser(policy=EMAIL_POLICY).parsestr(message)
message = Parser().parsestr(message)
if unsubscribe_url:
message.add_header('List-Unsubscribe', unsubscribe_url)
if email.attachments:
# On-demand attachments
......@@ -612,7 +618,7 @@ def prepare_message(email, recipient, recipients_list):
print_format_file.update({"parent": message})
return safe_encode(message.as_string())
return safe_encode(message.as_string(policy=EMAIL_POLICY))
def clear_outbox(days=None):
"""Remove low priority older than 31 days in Outbox or configured in Log Settings.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment