Skip to content

Commit 1fcb39e

Browse files
warsawarhadthedev
andauthored
gh-91520: Rewrite imghdr inlining for clarity and completeness (#91521)
* Rewrite imghdr inlining for clarity and completeness * Move MIMEImage class back closer to the top of the file since it's the important thing. * Use a decorate to mark a given rule function and simplify the rule function names for clarity. * Copy over all the imghdr test data files into the email package's test data directory. This way when imghdr is actually removed, it won't affect the MIMEImage guessing tests. * Rewrite and extend the MIMEImage tests to test for all supported auto-detected MIME image subtypes. * Remove the now redundant PyBanner048.gif data file. * See #91461 (comment) Co-authored-by: Oleg Iarygin <[email protected]> Co-authored-by: Oleg Iarygin <[email protected]>
1 parent ee47543 commit 1fcb39e

17 files changed

+114
-79
lines changed

Doc/includes/email-mime.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# Import smtplib for the actual sending function
1+
# Import smtplib for the actual sending function.
22
import smtplib
33

4-
# Here are the email package modules we'll need
4+
# Here are the email package modules we'll need.
55
from email.message import EmailMessage
66

77
# Create the container email message.
@@ -13,13 +13,13 @@
1313
msg['To'] = ', '.join(family)
1414
msg.preamble = 'You will not see this in a MIME-aware mail reader.\n'
1515

16-
# Open the files in binary mode. Use imghdr to figure out the
17-
# MIME subtype for each specific image.
16+
# Open the files in binary mode. You can also omit the subtype
17+
# if you want MIMEImage to guess it.
1818
for file in pngfiles:
1919
with open(file, 'rb') as fp:
2020
img_data = fp.read()
2121
msg.add_attachment(img_data, maintype='image',
22-
subtype='jpeg')
22+
subtype='png')
2323

2424
# Send the email via our own SMTP server.
2525
with smtplib.SMTP('localhost') as s:

Lib/email/mime/image.py

+73-67
Original file line numberDiff line numberDiff line change
@@ -10,137 +10,143 @@
1010
from email.mime.nonmultipart import MIMENonMultipart
1111

1212

13+
class MIMEImage(MIMENonMultipart):
14+
"""Class for generating image/* type MIME documents."""
15+
16+
def __init__(self, _imagedata, _subtype=None,
17+
_encoder=encoders.encode_base64, *, policy=None, **_params):
18+
"""Create an image/* type MIME document.
19+
20+
_imagedata is a string containing the raw image data. If the data
21+
type can be detected (jpeg, png, gif, tiff, rgb, pbm, pgm, ppm,
22+
rast, xbm, bmp, webp, and exr attempted), then the subtype will be
23+
automatically included in the Content-Type header. Otherwise, you can
24+
specify the specific image subtype via the _subtype parameter.
25+
26+
_encoder is a function which will perform the actual encoding for
27+
transport of the image data. It takes one argument, which is this
28+
Image instance. It should use get_payload() and set_payload() to
29+
change the payload to the encoded form. It should also add any
30+
Content-Transfer-Encoding or other headers to the message as
31+
necessary. The default encoding is Base64.
32+
33+
Any additional keyword arguments are passed to the base class
34+
constructor, which turns them into parameters on the Content-Type
35+
header.
36+
"""
37+
_subtype = _what(_imagedata) if _subtype is None else _subtype
38+
if _subtype is None:
39+
raise TypeError('Could not guess image MIME subtype')
40+
MIMENonMultipart.__init__(self, 'image', _subtype, policy=policy,
41+
**_params)
42+
self.set_payload(_imagedata)
43+
_encoder(self)
44+
45+
46+
_rules = []
47+
48+
1349
# Originally from the imghdr module.
14-
def _what(h):
15-
for tf in tests:
16-
if res := tf(h):
50+
def _what(data):
51+
for rule in _rules:
52+
if res := rule(data):
1753
return res
1854
else:
1955
return None
2056

21-
tests = []
2257

23-
def _test_jpeg(h):
58+
def rule(rulefunc):
59+
_rules.append(rulefunc)
60+
return rulefunc
61+
62+
63+
@rule
64+
def _jpeg(h):
2465
"""JPEG data with JFIF or Exif markers; and raw JPEG"""
2566
if h[6:10] in (b'JFIF', b'Exif'):
2667
return 'jpeg'
2768
elif h[:4] == b'\xff\xd8\xff\xdb':
2869
return 'jpeg'
2970

30-
tests.append(_test_jpeg)
3171

32-
def _test_png(h):
72+
@rule
73+
def _png(h):
3374
if h.startswith(b'\211PNG\r\n\032\n'):
3475
return 'png'
3576

36-
tests.append(_test_png)
3777

38-
def _test_gif(h):
78+
@rule
79+
def _gif(h):
3980
"""GIF ('87 and '89 variants)"""
4081
if h[:6] in (b'GIF87a', b'GIF89a'):
4182
return 'gif'
4283

43-
tests.append(_test_gif)
4484

45-
def _test_tiff(h):
85+
@rule
86+
def _tiff(h):
4687
"""TIFF (can be in Motorola or Intel byte order)"""
4788
if h[:2] in (b'MM', b'II'):
4889
return 'tiff'
4990

50-
tests.append(_test_tiff)
5191

52-
def _test_rgb(h):
92+
@rule
93+
def _rgb(h):
5394
"""SGI image library"""
5495
if h.startswith(b'\001\332'):
5596
return 'rgb'
5697

57-
tests.append(_test_rgb)
5898

59-
def _test_pbm(h):
99+
@rule
100+
def _pbm(h):
60101
"""PBM (portable bitmap)"""
61102
if len(h) >= 3 and \
62-
h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r':
103+
h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r':
63104
return 'pbm'
64105

65-
tests.append(_test_pbm)
66106

67-
def _test_pgm(h):
107+
@rule
108+
def _pgm(h):
68109
"""PGM (portable graymap)"""
69110
if len(h) >= 3 and \
70-
h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r':
111+
h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r':
71112
return 'pgm'
72113

73-
tests.append(_test_pgm)
74114

75-
def _test_ppm(h):
115+
@rule
116+
def _ppm(h):
76117
"""PPM (portable pixmap)"""
77118
if len(h) >= 3 and \
78-
h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r':
119+
h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r':
79120
return 'ppm'
80121

81-
tests.append(_test_ppm)
82122

83-
def _test_rast(h):
123+
@rule
124+
def _rast(h):
84125
"""Sun raster file"""
85126
if h.startswith(b'\x59\xA6\x6A\x95'):
86127
return 'rast'
87128

88-
tests.append(_test_rast)
89129

90-
def _test_xbm(h):
130+
@rule
131+
def _xbm(h):
91132
"""X bitmap (X10 or X11)"""
92133
if h.startswith(b'#define '):
93134
return 'xbm'
94135

95-
tests.append(_test_xbm)
96136

97-
def _test_bmp(h):
137+
@rule
138+
def _bmp(h):
98139
if h.startswith(b'BM'):
99140
return 'bmp'
100141

101-
tests.append(_test_bmp)
102142

103-
def _test_webp(h):
143+
@rule
144+
def _webp(h):
104145
if h.startswith(b'RIFF') and h[8:12] == b'WEBP':
105146
return 'webp'
106147

107-
tests.append(_test_webp)
108148

109-
def _test_exr(h):
149+
@rule
150+
def _exr(h):
110151
if h.startswith(b'\x76\x2f\x31\x01'):
111152
return 'exr'
112-
113-
tests.append(_test_exr)
114-
115-
116-
class MIMEImage(MIMENonMultipart):
117-
"""Class for generating image/* type MIME documents."""
118-
119-
def __init__(self, _imagedata, _subtype=None,
120-
_encoder=encoders.encode_base64, *, policy=None, **_params):
121-
"""Create an image/* type MIME document.
122-
123-
_imagedata is a string containing the raw image data. If the data
124-
type can be detected (jpeg, png, gif, tiff, rgb, pbm, pgm, ppm,
125-
rast, xbm, bmp, webp, and exr attempted), then the subtype will be
126-
automatically included in the Content-Type header. Otherwise, you can
127-
specify the specific image subtype via the _subtype parameter.
128-
129-
_encoder is a function which will perform the actual encoding for
130-
transport of the image data. It takes one argument, which is this
131-
Image instance. It should use get_payload() and set_payload() to
132-
change the payload to the encoded form. It should also add any
133-
Content-Transfer-Encoding or other headers to the message as
134-
necessary. The default encoding is Base64.
135-
136-
Any additional keyword arguments are passed to the base class
137-
constructor, which turns them into parameters on the Content-Type
138-
header.
139-
"""
140-
if _subtype is None:
141-
if (_subtype := _what(_imagedata)) is None:
142-
raise TypeError('Could not guess image MIME subtype')
143-
MIMENonMultipart.__init__(self, 'image', _subtype, policy=policy,
144-
**_params)
145-
self.set_payload(_imagedata)
146-
_encoder(self)
-896 Bytes
Binary file not shown.

Lib/test/test_email/data/python.bmp

1.13 KB
Binary file not shown.

Lib/test/test_email/data/python.exr

2.57 KB
Binary file not shown.

Lib/test/test_email/data/python.gif

405 Bytes
Loading

Lib/test/test_email/data/python.jpg

543 Bytes
Loading

Lib/test/test_email/data/python.pbm

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
P4
2+
16 16
3+
�������[�a_�X������?��

Lib/test/test_email/data/python.pgm

269 Bytes
Binary file not shown.

Lib/test/test_email/data/python.png

1020 Bytes
Loading

Lib/test/test_email/data/python.ppm

781 Bytes
Binary file not shown.

Lib/test/test_email/data/python.ras

1.03 KB
Binary file not shown.

Lib/test/test_email/data/python.sgi

1.92 KB
Binary file not shown.

Lib/test/test_email/data/python.tiff

1.29 KB
Binary file not shown.

Lib/test/test_email/data/python.webp

432 Bytes
Binary file not shown.

Lib/test/test_email/data/python.xbm

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#define python_width 16
2+
#define python_height 16
3+
static char python_bits[] = {
4+
0xDF, 0xFE, 0x8F, 0xFD, 0x5F, 0xFB, 0xAB, 0xFE, 0xB5, 0x8D, 0xDA, 0x8F,
5+
0xA5, 0x86, 0xFA, 0x83, 0x1A, 0x80, 0x0D, 0x80, 0x0D, 0x80, 0x0F, 0xE0,
6+
0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xFC, 0xFF, 0xFF, };

Lib/test/test_email/test_email.py

+27-7
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ def test_unicode_body_defaults_to_utf8_encoding(self):
798798
class TestEncoders(unittest.TestCase):
799799

800800
def test_EncodersEncode_base64(self):
801-
with openfile('PyBanner048.gif', 'rb') as fp:
801+
with openfile('python.gif', 'rb') as fp:
802802
bindata = fp.read()
803803
mimed = email.mime.image.MIMEImage(bindata)
804804
base64ed = mimed.get_payload()
@@ -1555,24 +1555,44 @@ def test_add_header(self):
15551555

15561556
# Test the basic MIMEImage class
15571557
class TestMIMEImage(unittest.TestCase):
1558-
def setUp(self):
1559-
with openfile('PyBanner048.gif', 'rb') as fp:
1558+
def _make_image(self, ext):
1559+
with openfile(f'python.{ext}', 'rb') as fp:
15601560
self._imgdata = fp.read()
15611561
self._im = MIMEImage(self._imgdata)
15621562

15631563
def test_guess_minor_type(self):
1564-
self.assertEqual(self._im.get_content_type(), 'image/gif')
1564+
for ext, subtype in {
1565+
'bmp': None,
1566+
'exr': None,
1567+
'gif': None,
1568+
'jpg': 'jpeg',
1569+
'pbm': None,
1570+
'pgm': None,
1571+
'png': None,
1572+
'ppm': None,
1573+
'ras': 'rast',
1574+
'sgi': 'rgb',
1575+
'tiff': None,
1576+
'webp': None,
1577+
'xbm': None,
1578+
}.items():
1579+
self._make_image(ext)
1580+
subtype = ext if subtype is None else subtype
1581+
self.assertEqual(self._im.get_content_type(), f'image/{subtype}')
15651582

15661583
def test_encoding(self):
1584+
self._make_image('gif')
15671585
payload = self._im.get_payload()
15681586
self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1569-
self._imgdata)
1587+
self._imgdata)
15701588

15711589
def test_checkSetMinor(self):
1590+
self._make_image('gif')
15721591
im = MIMEImage(self._imgdata, 'fish')
15731592
self.assertEqual(im.get_content_type(), 'image/fish')
15741593

15751594
def test_add_header(self):
1595+
self._make_image('gif')
15761596
eq = self.assertEqual
15771597
self._im.add_header('Content-Disposition', 'attachment',
15781598
filename='dingusfish.gif')
@@ -1747,7 +1767,7 @@ def test_utf8_input_no_charset(self):
17471767
# Test complicated multipart/* messages
17481768
class TestMultipart(TestEmailBase):
17491769
def setUp(self):
1750-
with openfile('PyBanner048.gif', 'rb') as fp:
1770+
with openfile('python.gif', 'rb') as fp:
17511771
data = fp.read()
17521772
container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
17531773
image = MIMEImage(data, name='dingusfish.gif')
@@ -3444,7 +3464,7 @@ def test_BytesGenerator_linend_with_non_ascii(self):
34443464
def test_mime_classes_policy_argument(self):
34453465
with openfile('audiotest.au', 'rb') as fp:
34463466
audiodata = fp.read()
3447-
with openfile('PyBanner048.gif', 'rb') as fp:
3467+
with openfile('python.gif', 'rb') as fp:
34483468
bindata = fp.read()
34493469
classes = [
34503470
(MIMEApplication, ('',)),

0 commit comments

Comments
 (0)