Skip to content

Commit 24770e1

Browse files
authored
Merge pull request #11 from gisce/fix_header_parse
DONE: Fix header parse
2 parents 6ba691e + ec8d5fd commit 24770e1

File tree

3 files changed

+99
-13
lines changed

3 files changed

+99
-13
lines changed

qreu/email.py

+85-6
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
from __future__ import absolute_import, unicode_literals
33

44
import email
5-
from email.encoders import encode_base64
65
from email.header import decode_header, Header
76
from email.mime.application import MIMEApplication
87
from email.mime.multipart import MIMEMultipart
98
from email.mime.text import MIMEText
109

1110
from html2text import html2text
11+
from six import PY2
1212

1313
import re
1414

@@ -74,6 +74,50 @@ def parse(raw_message):
7474
mail.email = email.message_from_string(raw_message)
7575
return mail
7676

77+
@staticmethod
78+
def fix_header_name(header_name):
79+
"""
80+
Fix header names according to RFC 4021:
81+
https://tools.ietf.org/html/rfc4021#section-2.1.5
82+
:param header_name: Name of the header to fix
83+
:type header_name: str
84+
:return: Fixed name of the header
85+
:rtype: str
86+
"""
87+
headers = [
88+
'Date', 'From', 'Sender', 'Reply-To', 'To', 'Cc', 'Bcc',
89+
'Message-ID', 'In-Reply-To', 'References', 'Subject', 'Comments',
90+
'Keywords', 'Resent-Date', 'Resent-From', 'Resent-Sender',
91+
'Resent-To', 'Resent-Cc', 'Resent-Bcc', 'Resent-Reply-To',
92+
'Resent-Message-ID', 'Return-Path', 'Received', 'Encrypted',
93+
'Disposition-Notification-To', 'Disposition-Notification-Options',
94+
'Accept-Language', 'Original-Message-ID', 'PICS-Label', 'Encoding',
95+
'List-Archive', 'List-Help', 'List-ID', 'List-Owner', 'List-Post',
96+
'List-Subscribe', 'List-Unsubscribe', 'Message-Context',
97+
'DL-Expansion-History', 'Alternate-Recipient',
98+
'Original-Encoded-Information-Types', 'Content-Return',
99+
'Generate-Delivery-Report', 'Prevent-NonDelivery-Report',
100+
'Obsoletes', 'Supersedes', 'Content-Identifier', 'Delivery-Date',
101+
'Expiry-Date', 'Expires', 'Reply-By', 'Importance',
102+
'Incomplete-Copy', 'Priority', 'Sensitivity', 'Language',
103+
'Conversion', 'Conversion-With-Loss', 'Message-Type',
104+
'Autosubmitted', 'Autoforwarded', 'Discarded-X400-IPMS-Extensions',
105+
'Discarded-X400-MTS-Extensions', 'Disclose-Recipients',
106+
'Deferred-Delivery', 'Latest-Delivery-Time',
107+
'Originator-Return-Address', 'X400-Content-Identifier',
108+
'X400-Content-Return', 'X400-Content-Type', 'X400-MTS-Identifier',
109+
'X400-Originator', 'X400-Received', 'X400-Recipients', 'X400-Trace',
110+
'MIME-Version', 'Content-ID', 'Content-Description',
111+
'Content-Transfer-Encoding', 'Content-Type', 'Content-Base',
112+
'Content-Location', 'Content-features', 'Content-Disposition',
113+
'Content-Language', 'Content-Alternative', 'Content-MD5',
114+
'Content-Duration',
115+
]
116+
for header in headers:
117+
if header_name.lower() == header.lower():
118+
return header
119+
return ''
120+
77121
def header(self, header, default=None):
78122
"""
79123
Get the email Header always in Unicode
@@ -88,10 +132,11 @@ def header(self, header, default=None):
88132
for part in decode_header(header_value):
89133
if part[1]:
90134
result.append(part[0].decode(part[1]))
135+
elif isinstance(part[0], bytes):
136+
result.append(part[0].decode('utf-8'))
91137
else:
92138
result.append(part[0])
93139
header_value = ''.join(result)
94-
95140
return header_value
96141

97142
def add_header(self, header, value):
@@ -108,9 +153,43 @@ def add_header(self, header, value):
108153
if not (header and value):
109154
raise ValueError('Header not provided!')
110155
recipients_headers = ['to', 'cc', 'bcc']
111-
if isinstance(value, list) and header.lower() in recipients_headers:
112-
value = ','.join(value)
113-
header_value = Header(value, charset='utf-8').encode()
156+
if header.lower() in recipients_headers or header.lower() == 'from':
157+
if not isinstance(value, list):
158+
value = [value]
159+
header_value = []
160+
for addr in value:
161+
# For each address in the recipients headers
162+
# Do the Header Object
163+
# PY3 works fine with Header(values, charset='utf-8')
164+
# PY2:
165+
# - Does not escape correctly the unicode values
166+
# - Must encode the display name as a HEADER
167+
# so the item is encoded properly
168+
# - The encoded display name and the address are joined
169+
# into the Header of the email
170+
mail_addr = address.parse(addr)
171+
display_name = Header(
172+
mail_addr.display_name, charset='utf-8').encode()
173+
if display_name:
174+
# decode_header method in PY2 does not look for closed items
175+
# so a ' ' separator is required between items of a Header
176+
if PY2:
177+
base_addr = '{} <{}>'
178+
else:
179+
base_addr = '{}<{}>'
180+
header_value.append(
181+
base_addr.format(
182+
display_name,
183+
mail_addr.address
184+
).strip()
185+
)
186+
else:
187+
header_value.append(mail_addr.address)
188+
header_value = ','.join(header_value)
189+
else:
190+
header_value = Header(value, charset='utf-8').encode()
191+
# Get correct header name or add the one provided if custom header key
192+
header = Email.fix_header_name(header) or header
114193
self.email[header] = header_value
115194
return header_value
116195

@@ -198,7 +277,7 @@ def is_reply(self):
198277
"""
199278
return (not self.is_forwarded and (
200279
bool(self.header('In-Reply-To'))
201-
or bool(re.match(RE_PATTERNS, self.header('Subject', '')))
280+
or bool(re.match(RE_PATTERNS, self.header('Subject', '')))
202281
))
203282

204283
@property

setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
packages=find_packages(),
88
url='https://github.com/gisce/qreu',
99
install_requires=[
10-
'html2text'
10+
'html2text',
11+
'six'
1112
],
1213
license='MIT',
1314
author='GISCE-TI, S.L.',

spec/qreu_spec.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,17 @@
106106
expect(e.cc).to(be_empty)
107107
expect(e.recipients).to(be_empty)
108108

109-
with it('must add header to Email'):
109+
with it('must add any header to Email'):
110110
e = Email()
111111
header_key = 'X-ORIG-HEADER'
112112
header_value = 'Header String Value'
113113
expect(e.header(header_key, False)).to(be_false)
114+
# Custom Header
114115
e.add_header(header_key, header_value)
116+
# Recipient Header using an address
117+
e.add_header('to', '[email protected]')
118+
# Recipient Header using a list
119+
e.add_header('cc', ['[email protected]', '[email protected]'])
115120
expect(e.header(header_key, False)).to(equal(header_value))
116121

117122
with it('must raise exception wrongly adding a header'):
@@ -147,7 +152,7 @@ def call_wrongly():
147152
expect(e.body_parts).to(have_keys('plain', 'html'))
148153
expect(e.body_parts['plain']).to(equal(plain))
149154
expect(e.body_parts['html']).to(equal(html))
150-
155+
151156
with it('must raise ValueError if no body provided on add_body'):
152157
e = Email()
153158
expect(e.add_body_text).to(raise_error(ValueError))
@@ -307,12 +312,13 @@ def call_wrongly():
307312

308313
with it('must add addresses correctly as "name" <address>'):
309314
address = 'spécial <[email protected]>'
315+
parsed = 'spécial<[email protected]>'
310316
e = Email(to=address)
311-
expect(e.to).to(equal([address]))
317+
expect(e.to).to(equal([parsed]))
312318
e = Email(cc=address)
313-
expect(e.cc).to(equal([address]))
314-
e = Email(bcc=address)
315-
expect(e.bcc).to(equal([address]))
319+
expect(e.cc).to(equal([parsed]))
320+
e = Email(bcc=[address])
321+
expect(e.bcc).to(equal([parsed]))
316322

317323
with it('must parse html2text if no text provided'):
318324
vals = self.vals.copy()

0 commit comments

Comments
 (0)