Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 347dc95

Browse files
committed
#14977: Make mailcap respect the order of the lines in the mailcap file.
This is required by RFC 1542, so despite the subtle behavior change we are treating it as a bug. Patch by Michael Lazar.
1 parent ae9e5f0 commit 347dc95

5 files changed

Lines changed: 76 additions & 27 deletions

File tree

Lib/mailcap.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
"""Mailcap file handling. See RFC 1524."""
22

33
import os
4+
import warnings
45

56
__all__ = ["getcaps","findmatch"]
67

8+
9+
def lineno_sort_key(entry):
10+
# Sort in ascending order, with unspecified entries at the end
11+
if 'lineno' in entry:
12+
return 0, entry['lineno']
13+
else:
14+
return 1, 0
15+
16+
717
# Part 1: top-level interface.
818

919
def getcaps():
@@ -17,13 +27,14 @@ def getcaps():
1727
1828
"""
1929
caps = {}
30+
lineno = 0
2031
for mailcap in listmailcapfiles():
2132
try:
2233
fp = open(mailcap, 'r')
2334
except OSError:
2435
continue
2536
with fp:
26-
morecaps = readmailcapfile(fp)
37+
morecaps, lineno = _readmailcapfile(fp, lineno)
2738
for key, value in morecaps.items():
2839
if not key in caps:
2940
caps[key] = value
@@ -49,8 +60,15 @@ def listmailcapfiles():
4960

5061

5162
# Part 2: the parser.
52-
5363
def readmailcapfile(fp):
64+
"""Read a mailcap file and return a dictionary keyed by MIME type."""
65+
warnings.warn('readmailcapfile is deprecated, use getcaps instead',
66+
DeprecationWarning, 2)
67+
caps, _ = _readmailcapfile(fp, None)
68+
return caps
69+
70+
71+
def _readmailcapfile(fp, lineno):
5472
"""Read a mailcap file and return a dictionary keyed by MIME type.
5573
5674
Each MIME type is mapped to an entry consisting of a list of
@@ -76,6 +94,9 @@ def readmailcapfile(fp):
7694
key, fields = parseline(line)
7795
if not (key and fields):
7896
continue
97+
if lineno is not None:
98+
fields['lineno'] = lineno
99+
lineno += 1
79100
# Normalize the key
80101
types = key.split('/')
81102
for j in range(len(types)):
@@ -86,7 +107,7 @@ def readmailcapfile(fp):
86107
caps[key].append(fields)
87108
else:
88109
caps[key] = [fields]
89-
return caps
110+
return caps, lineno
90111

91112
def parseline(line):
92113
"""Parse one entry in a mailcap file and return a dictionary.
@@ -165,6 +186,7 @@ def lookup(caps, MIMEtype, key=None):
165186
entries = entries + caps[MIMEtype]
166187
if key is not None:
167188
entries = [e for e in entries if key in e]
189+
entries = sorted(entries, key=lineno_sort_key)
168190
return entries
169191

170192
def subst(field, MIMEtype, filename, plist=[]):

Lib/test/mailcap.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,5 @@ message/external-body; showexternal %s %{access-type} %{name} %{site} \
3535
text/richtext; shownonascii iso-8859-8 -e richtext -p %s; test=test "`echo \
3636
%{charset} | tr '[A-Z]' '[a-z]'`" = iso-8859-8; copiousoutput
3737

38-
video/mpeg; mpeg_play %s
3938
video/*; animate %s
39+
video/mpeg; mpeg_play %s

Lib/test/test_mailcap.py

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import mailcap
22
import os
33
import shutil
4+
import copy
45
import test.support
56
import unittest
67

@@ -14,43 +15,55 @@
1415
[{'compose': 'moviemaker %s',
1516
'x11-bitmap': '"/usr/lib/Zmail/bitmaps/movie.xbm"',
1617
'description': '"Movie"',
17-
'view': 'movieplayer %s'}],
18+
'view': 'movieplayer %s',
19+
'lineno': 4}],
1820
'application/*':
1921
[{'copiousoutput': '',
20-
'view': 'echo "This is \\"%t\\" but is 50 \\% Greek to me" \\; cat %s'}],
22+
'view': 'echo "This is \\"%t\\" but is 50 \\% Greek to me" \\; cat %s',
23+
'lineno': 5}],
2124
'audio/basic':
2225
[{'edit': 'audiocompose %s',
2326
'compose': 'audiocompose %s',
2427
'description': '"An audio fragment"',
25-
'view': 'showaudio %s'}],
28+
'view': 'showaudio %s',
29+
'lineno': 6}],
2630
'video/mpeg':
27-
[{'view': 'mpeg_play %s'}],
31+
[{'view': 'mpeg_play %s', 'lineno': 13}],
2832
'application/postscript':
29-
[{'needsterminal': '', 'view': 'ps-to-terminal %s'},
30-
{'compose': 'idraw %s', 'view': 'ps-to-terminal %s'}],
33+
[{'needsterminal': '', 'view': 'ps-to-terminal %s', 'lineno': 1},
34+
{'compose': 'idraw %s', 'view': 'ps-to-terminal %s', 'lineno': 2}],
3135
'application/x-dvi':
32-
[{'view': 'xdvi %s'}],
36+
[{'view': 'xdvi %s', 'lineno': 3}],
3337
'message/external-body':
3438
[{'composetyped': 'extcompose %s',
3539
'description': '"A reference to data stored in an external location"',
3640
'needsterminal': '',
37-
'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}'}],
41+
'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}',
42+
'lineno': 10}],
3843
'text/richtext':
3944
[{'test': 'test "`echo %{charset} | tr \'[A-Z]\' \'[a-z]\'`" = iso-8859-8',
4045
'copiousoutput': '',
41-
'view': 'shownonascii iso-8859-8 -e richtext -p %s'}],
46+
'view': 'shownonascii iso-8859-8 -e richtext -p %s',
47+
'lineno': 11}],
4248
'image/x-xwindowdump':
43-
[{'view': 'display %s'}],
49+
[{'view': 'display %s', 'lineno': 9}],
4450
'audio/*':
45-
[{'view': '/usr/local/bin/showaudio %t'}],
51+
[{'view': '/usr/local/bin/showaudio %t', 'lineno': 7}],
4652
'video/*':
47-
[{'view': 'animate %s'}],
53+
[{'view': 'animate %s', 'lineno': 12}],
4854
'application/frame':
49-
[{'print': '"cat %s | lp"', 'view': 'showframe %s'}],
55+
[{'print': '"cat %s | lp"', 'view': 'showframe %s', 'lineno': 0}],
5056
'image/rgb':
51-
[{'view': 'display %s'}]
57+
[{'view': 'display %s', 'lineno': 8}]
5258
}
5359

60+
# For backwards compatibility, readmailcapfile() and lookup() still support
61+
# the old version of mailcapdict without line numbers.
62+
MAILCAPDICT_DEPRECATED = copy.deepcopy(MAILCAPDICT)
63+
for entry_list in MAILCAPDICT_DEPRECATED.values():
64+
for entry in entry_list:
65+
entry.pop('lineno')
66+
5467

5568
class HelperFunctionTest(unittest.TestCase):
5669

@@ -76,12 +89,14 @@ def test_listmailcapfiles(self):
7689
def test_readmailcapfile(self):
7790
# Test readmailcapfile() using test file. It should match MAILCAPDICT.
7891
with open(MAILCAPFILE, 'r') as mcf:
79-
d = mailcap.readmailcapfile(mcf)
80-
self.assertDictEqual(d, MAILCAPDICT)
92+
with self.assertWarns(DeprecationWarning):
93+
d = mailcap.readmailcapfile(mcf)
94+
self.assertDictEqual(d, MAILCAPDICT_DEPRECATED)
8195

8296
def test_lookup(self):
8397
# Test without key
84-
expected = [{'view': 'mpeg_play %s'}, {'view': 'animate %s'}]
98+
expected = [{'view': 'animate %s', 'lineno': 12},
99+
{'view': 'mpeg_play %s', 'lineno': 13}]
85100
actual = mailcap.lookup(MAILCAPDICT, 'video/mpeg')
86101
self.assertListEqual(expected, actual)
87102

@@ -90,10 +105,16 @@ def test_lookup(self):
90105
expected = [{'edit': 'audiocompose %s',
91106
'compose': 'audiocompose %s',
92107
'description': '"An audio fragment"',
93-
'view': 'showaudio %s'}]
108+
'view': 'showaudio %s',
109+
'lineno': 6}]
94110
actual = mailcap.lookup(MAILCAPDICT, 'audio/basic', key)
95111
self.assertListEqual(expected, actual)
96112

113+
# Test on user-defined dicts without line numbers
114+
expected = [{'view': 'mpeg_play %s'}, {'view': 'animate %s'}]
115+
actual = mailcap.lookup(MAILCAPDICT_DEPRECATED, 'video/mpeg')
116+
self.assertListEqual(expected, actual)
117+
97118
def test_subst(self):
98119
plist = ['id=1', 'number=2', 'total=3']
99120
# test case: ([field, MIMEtype, filename, plist=[]], <expected string>)
@@ -152,14 +173,16 @@ def test_findmatch(self):
152173
'edit': 'audiocompose %s',
153174
'compose': 'audiocompose %s',
154175
'description': '"An audio fragment"',
155-
'view': 'showaudio %s'
176+
'view': 'showaudio %s',
177+
'lineno': 6
156178
}
157-
audio_entry = {"view": "/usr/local/bin/showaudio %t"}
158-
video_entry = {'view': 'animate %s'}
179+
audio_entry = {"view": "/usr/local/bin/showaudio %t", 'lineno': 7}
180+
video_entry = {'view': 'animate %s', 'lineno': 12}
159181
message_entry = {
160182
'composetyped': 'extcompose %s',
161183
'description': '"A reference to data stored in an external location"', 'needsterminal': '',
162-
'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}'
184+
'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}',
185+
'lineno': 10,
163186
}
164187

165188
# test case: (findmatch args, findmatch keyword args, expected output)
@@ -169,7 +192,7 @@ def test_findmatch(self):
169192
cases = [
170193
([{}, "video/mpeg"], {}, (None, None)),
171194
([c, "foo/bar"], {}, (None, None)),
172-
([c, "video/mpeg"], {}, ('mpeg_play /dev/null', {'view': 'mpeg_play %s'})),
195+
([c, "video/mpeg"], {}, ('animate /dev/null', video_entry)),
173196
([c, "audio/basic", "edit"], {}, ("audiocompose /dev/null", audio_basic_entry)),
174197
([c, "audio/basic", "compose"], {}, ("audiocompose /dev/null", audio_basic_entry)),
175198
([c, "audio/basic", "description"], {}, ('"An audio fragment"', audio_basic_entry)),

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,7 @@ Julia Lawall
833833
Chris Lawrence
834834
Mark Lawrence
835835
Chris Laws
836+
Michael Lazar
836837
Brian Leair
837838
Mathieu Leduc-Hamel
838839
Amandine Lee

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ Core and Builtins
6262
Library
6363
-------
6464

65+
- Issue #14977: mailcap now respects the order of the lines in the mailcap
66+
files ("first match"), as required by RFC 1542. Patch by Michael Lazar.
67+
6568
- Issue #24594: Validates persist parameter when opening MSI database
6669

6770
- Issue #28047: Fixed calculation of line length used for the base64 CTE

0 commit comments

Comments
 (0)