diff --git a/Doc/library/mailcap.rst b/Doc/library/mailcap.rst deleted file mode 100644 index bfaedb46091991..00000000000000 --- a/Doc/library/mailcap.rst +++ /dev/null @@ -1,94 +0,0 @@ -:mod:`mailcap` --- Mailcap file handling -======================================== - -.. module:: mailcap - :synopsis: Mailcap file handling. - :deprecated: - -**Source code:** :source:`Lib/mailcap.py` - -.. deprecated-removed:: 3.11 3.13 - The :mod:`mailcap` module is deprecated - (see :pep:`PEP 594 <594#mailcap>` for details). - The :mod:`mimetypes` module provides an alternative. - --------------- - -Mailcap files are used to configure how MIME-aware applications such as mail -readers and web browsers react to files with different MIME types. (The name -"mailcap" is derived from the phrase "mail capability".) For example, a mailcap -file might contain a line like ``video/mpeg; xmpeg %s``. Then, if the user -encounters an email message or web document with the MIME type -:mimetype:`video/mpeg`, ``%s`` will be replaced by a filename (usually one -belonging to a temporary file) and the :program:`xmpeg` program can be -automatically started to view the file. - -The mailcap format is documented in :rfc:`1524`, "A User Agent Configuration -Mechanism For Multimedia Mail Format Information", but is not an internet -standard. However, mailcap files are supported on most Unix systems. - - -.. function:: findmatch(caps, MIMEtype, key='view', filename='/dev/null', plist=[]) - - Return a 2-tuple; the first element is a string containing the command line to - be executed (which can be passed to :func:`os.system`), and the second element - is the mailcap entry for a given MIME type. If no matching MIME type can be - found, ``(None, None)`` is returned. - - *key* is the name of the field desired, which represents the type of activity to - be performed; the default value is 'view', since in the most common case you - simply want to view the body of the MIME-typed data. Other possible values - might be 'compose' and 'edit', if you wanted to create a new body of the given - MIME type or alter the existing body data. See :rfc:`1524` for a complete list - of these fields. - - *filename* is the filename to be substituted for ``%s`` in the command line; the - default value is ``'/dev/null'`` which is almost certainly not what you want, so - usually you'll override it by specifying a filename. - - *plist* can be a list containing named parameters; the default value is simply - an empty list. Each entry in the list must be a string containing the parameter - name, an equals sign (``'='``), and the parameter's value. Mailcap entries can - contain named parameters like ``%{foo}``, which will be replaced by the value - of the parameter named 'foo'. For example, if the command line ``showpartial - %{id} %{number} %{total}`` was in a mailcap file, and *plist* was set to - ``['id=1', 'number=2', 'total=3']``, the resulting command line would be - ``'showpartial 1 2 3'``. - - In a mailcap file, the "test" field can optionally be specified to test some - external condition (such as the machine architecture, or the window system in - use) to determine whether or not the mailcap line applies. :func:`findmatch` - will automatically check such conditions and skip the entry if the check fails. - - .. versionchanged:: 3.11 - - To prevent security issues with shell metacharacters (symbols that have - special effects in a shell command line), ``findmatch`` will refuse - to inject ASCII characters other than alphanumerics and ``@+=:,./-_`` - into the returned command line. - - If a disallowed character appears in *filename*, ``findmatch`` will always - return ``(None, None)`` as if no entry was found. - If such a character appears elsewhere (a value in *plist* or in *MIMEtype*), - ``findmatch`` will ignore all mailcap entries which use that value. - A :mod:`warning ` will be raised in either case. - -.. function:: getcaps() - - Returns a dictionary mapping MIME types to a list of mailcap file entries. This - dictionary must be passed to the :func:`findmatch` function. An entry is stored - as a list of dictionaries, but it shouldn't be necessary to know the details of - this representation. - - The information is derived from all of the mailcap files found on the system. - Settings in the user's mailcap file :file:`$HOME/.mailcap` will override - settings in the system mailcap files :file:`/etc/mailcap`, - :file:`/usr/etc/mailcap`, and :file:`/usr/local/etc/mailcap`. - -An example usage:: - - >>> import mailcap - >>> d = mailcap.getcaps() - >>> mailcap.findmatch(d, 'video/mpeg', filename='tmp1223') - ('xmpeg tmp1223', {'view': 'xmpeg %s'}) - diff --git a/Doc/library/superseded.rst b/Doc/library/superseded.rst index 1ab7b08d4d202c..3c67ae491636e4 100644 --- a/Doc/library/superseded.rst +++ b/Doc/library/superseded.rst @@ -15,7 +15,6 @@ backwards compatibility. They have been superseded by other modules. chunk.rst crypt.rst imghdr.rst - mailcap.rst msilib.rst nis.rst nntplib.rst diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 9734d43ef87d25..fd4a75ce47b350 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -1737,7 +1737,7 @@ Modules +---------------------+---------------------+---------------------+---------------------+---------------------+ | :mod:`!cgi` | :mod:`imghdr` | :mod:`nntplib` | :mod:`spwd` | :mod:`xdrlib` | +---------------------+---------------------+---------------------+---------------------+---------------------+ - | :mod:`!cgitb` | :mod:`mailcap` | :mod:`!ossaudiodev` | :mod:`!sunau` | | + | :mod:`!cgitb` | :mod:`!mailcap` | :mod:`!ossaudiodev` | :mod:`!sunau` | | +---------------------+---------------------+---------------------+---------------------+---------------------+ (Contributed by Brett Cannon in :issue:`47061` and Victor Stinner in diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 316296ae038e4e..7bf0df17fb72d0 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -893,7 +893,7 @@ Modules (see :pep:`594`): * :mod:`chunk` * :mod:`crypt` * :mod:`imghdr` -* :mod:`mailcap` +* :mod:`!mailcap` * :mod:`msilib` * :mod:`nis` * :mod:`nntplib` diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 83d539b6fc3d9a..1102225e50b658 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -164,6 +164,10 @@ Removed * :pep:`594`: Remove the :mod:`!sunau` module, deprecated in Python 3.11. (Contributed by Victor Stinner in :gh:`104773`.) +* :pep:`594`: Remove the :mod:`!mailcap` module, deprecated in Python 3.11. + The :mod:`mimetypes` module provides an alternative. + (Contributed by Victor Stinner in :gh:`104773`.) + Porting to Python 3.13 ====================== diff --git a/Lib/mailcap.py b/Lib/mailcap.py deleted file mode 100644 index 2f4656e854b3bb..00000000000000 --- a/Lib/mailcap.py +++ /dev/null @@ -1,302 +0,0 @@ -"""Mailcap file handling. See RFC 1524.""" - -import os -import warnings -import re - -__all__ = ["getcaps","findmatch"] - - -_DEPRECATION_MSG = ('The {name} module is deprecated and will be removed in ' - 'Python {remove}. See the mimetypes module for an ' - 'alternative.') -warnings._deprecated(__name__, _DEPRECATION_MSG, remove=(3, 13)) - - -def lineno_sort_key(entry): - # Sort in ascending order, with unspecified entries at the end - if 'lineno' in entry: - return 0, entry['lineno'] - else: - return 1, 0 - -_find_unsafe = re.compile(r'[^\xa1-\U0010FFFF\w@+=:,./-]').search - -class UnsafeMailcapInput(Warning): - """Warning raised when refusing unsafe input""" - - -# Part 1: top-level interface. - -def getcaps(): - """Return a dictionary containing the mailcap database. - - The dictionary maps a MIME type (in all lowercase, e.g. 'text/plain') - to a list of dictionaries corresponding to mailcap entries. The list - collects all the entries for that MIME type from all available mailcap - files. Each dictionary contains key-value pairs for that MIME type, - where the viewing command is stored with the key "view". - - """ - caps = {} - lineno = 0 - for mailcap in listmailcapfiles(): - try: - fp = open(mailcap, 'r') - except OSError: - continue - with fp: - morecaps, lineno = _readmailcapfile(fp, lineno) - for key, value in morecaps.items(): - if not key in caps: - caps[key] = value - else: - caps[key] = caps[key] + value - return caps - -def listmailcapfiles(): - """Return a list of all mailcap files found on the system.""" - # This is mostly a Unix thing, but we use the OS path separator anyway - if 'MAILCAPS' in os.environ: - pathstr = os.environ['MAILCAPS'] - mailcaps = pathstr.split(os.pathsep) - else: - if 'HOME' in os.environ: - home = os.environ['HOME'] - else: - # Don't bother with getpwuid() - home = '.' # Last resort - mailcaps = [home + '/.mailcap', '/etc/mailcap', - '/usr/etc/mailcap', '/usr/local/etc/mailcap'] - return mailcaps - - -# Part 2: the parser. -def readmailcapfile(fp): - """Read a mailcap file and return a dictionary keyed by MIME type.""" - warnings.warn('readmailcapfile is deprecated, use getcaps instead', - DeprecationWarning, 2) - caps, _ = _readmailcapfile(fp, None) - return caps - - -def _readmailcapfile(fp, lineno): - """Read a mailcap file and return a dictionary keyed by MIME type. - - Each MIME type is mapped to an entry consisting of a list of - dictionaries; the list will contain more than one such dictionary - if a given MIME type appears more than once in the mailcap file. - Each dictionary contains key-value pairs for that MIME type, where - the viewing command is stored with the key "view". - """ - caps = {} - while line := fp.readline(): - # Ignore comments and blank lines - if line[0] == '#' or line.strip() == '': - continue - nextline = line - # Join continuation lines - while nextline[-2:] == '\\\n': - nextline = fp.readline() - if not nextline: nextline = '\n' - line = line[:-2] + nextline - # Parse the line - key, fields = parseline(line) - if not (key and fields): - continue - if lineno is not None: - fields['lineno'] = lineno - lineno += 1 - # Normalize the key - types = key.split('/') - for j in range(len(types)): - types[j] = types[j].strip() - key = '/'.join(types).lower() - # Update the database - if key in caps: - caps[key].append(fields) - else: - caps[key] = [fields] - return caps, lineno - -def parseline(line): - """Parse one entry in a mailcap file and return a dictionary. - - The viewing command is stored as the value with the key "view", - and the rest of the fields produce key-value pairs in the dict. - """ - fields = [] - i, n = 0, len(line) - while i < n: - field, i = parsefield(line, i, n) - fields.append(field) - i = i+1 # Skip semicolon - if len(fields) < 2: - return None, None - key, view, rest = fields[0], fields[1], fields[2:] - fields = {'view': view} - for field in rest: - i = field.find('=') - if i < 0: - fkey = field - fvalue = "" - else: - fkey = field[:i].strip() - fvalue = field[i+1:].strip() - if fkey in fields: - # Ignore it - pass - else: - fields[fkey] = fvalue - return key, fields - -def parsefield(line, i, n): - """Separate one key-value pair in a mailcap entry.""" - start = i - while i < n: - c = line[i] - if c == ';': - break - elif c == '\\': - i = i+2 - else: - i = i+1 - return line[start:i].strip(), i - - -# Part 3: using the database. - -def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]): - """Find a match for a mailcap entry. - - Return a tuple containing the command line, and the mailcap entry - used; (None, None) if no match is found. This may invoke the - 'test' command of several matching entries before deciding which - entry to use. - - """ - if _find_unsafe(filename): - msg = "Refusing to use mailcap with filename %r. Use a safe temporary filename." % (filename,) - warnings.warn(msg, UnsafeMailcapInput) - return None, None - entries = lookup(caps, MIMEtype, key) - # XXX This code should somehow check for the needsterminal flag. - for e in entries: - if 'test' in e: - test = subst(e['test'], filename, plist) - if test is None: - continue - if test and os.system(test) != 0: - continue - command = subst(e[key], MIMEtype, filename, plist) - if command is not None: - return command, e - return None, None - -def lookup(caps, MIMEtype, key=None): - entries = [] - if MIMEtype in caps: - entries = entries + caps[MIMEtype] - MIMEtypes = MIMEtype.split('/') - MIMEtype = MIMEtypes[0] + '/*' - if MIMEtype in caps: - entries = entries + caps[MIMEtype] - if key is not None: - entries = [e for e in entries if key in e] - entries = sorted(entries, key=lineno_sort_key) - return entries - -def subst(field, MIMEtype, filename, plist=[]): - # XXX Actually, this is Unix-specific - res = '' - i, n = 0, len(field) - while i < n: - c = field[i]; i = i+1 - if c != '%': - if c == '\\': - c = field[i:i+1]; i = i+1 - res = res + c - else: - c = field[i]; i = i+1 - if c == '%': - res = res + c - elif c == 's': - res = res + filename - elif c == 't': - if _find_unsafe(MIMEtype): - msg = "Refusing to substitute MIME type %r into a shell command." % (MIMEtype,) - warnings.warn(msg, UnsafeMailcapInput) - return None - res = res + MIMEtype - elif c == '{': - start = i - while i < n and field[i] != '}': - i = i+1 - name = field[start:i] - i = i+1 - param = findparam(name, plist) - if _find_unsafe(param): - msg = "Refusing to substitute parameter %r (%s) into a shell command" % (param, name) - warnings.warn(msg, UnsafeMailcapInput) - return None - res = res + param - # XXX To do: - # %n == number of parts if type is multipart/* - # %F == list of alternating type and filename for parts - else: - res = res + '%' + c - return res - -def findparam(name, plist): - name = name.lower() + '=' - n = len(name) - for p in plist: - if p[:n].lower() == name: - return p[n:] - return '' - - -# Part 4: test program. - -def test(): - import sys - caps = getcaps() - if not sys.argv[1:]: - show(caps) - return - for i in range(1, len(sys.argv), 2): - args = sys.argv[i:i+2] - if len(args) < 2: - print("usage: mailcap [MIMEtype file] ...") - return - MIMEtype = args[0] - file = args[1] - command, e = findmatch(caps, MIMEtype, 'view', file) - if not command: - print("No viewer found for", type) - else: - print("Executing:", command) - sts = os.system(command) - sts = os.waitstatus_to_exitcode(sts) - if sts: - print("Exit status:", sts) - -def show(caps): - print("Mailcap files:") - for fn in listmailcapfiles(): print("\t" + fn) - print() - if not caps: caps = getcaps() - print("Mailcap entries:") - print() - ckeys = sorted(caps) - for type in ckeys: - print(type) - entries = caps[type] - for e in entries: - keys = sorted(e) - for k in keys: - print(" %-15s" % k, e[k]) - print() - -if __name__ == '__main__': - test() diff --git a/Lib/test/mailcap.txt b/Lib/test/mailcap.txt deleted file mode 100644 index 08a76e659410d9..00000000000000 --- a/Lib/test/mailcap.txt +++ /dev/null @@ -1,39 +0,0 @@ -# Mailcap file for test_mailcap; based on RFC 1524 -# Referred to by test_mailcap.py - -# -# This is a comment. -# - -application/frame; showframe %s; print="cat %s | lp" -application/postscript; ps-to-terminal %s;\ - needsterminal -application/postscript; ps-to-terminal %s; \ - compose=idraw %s -application/x-dvi; xdvi %s -application/x-movie; movieplayer %s; compose=moviemaker %s; \ - description="Movie"; \ - x11-bitmap="/usr/lib/Zmail/bitmaps/movie.xbm" -application/*; echo "This is \"%t\" but \ - is 50 \% Greek to me" \; cat %s; copiousoutput - -audio/basic; showaudio %s; compose=audiocompose %s; edit=audiocompose %s;\ -description="An audio fragment" -audio/* ; /usr/local/bin/showaudio %t - -image/rgb; display %s -#image/gif; display %s -image/x-xwindowdump; display %s - -# The continuation char shouldn't \ -# make a difference in a comment. - -message/external-body; showexternal %s %{access-type} %{name} %{site} \ - %{directory} %{mode} %{server}; needsterminal; composetyped = extcompose %s; \ - description="A reference to data stored in an external location" - -text/richtext; shownonascii iso-8859-8 -e richtext -p %s; test=test "`echo \ - %{charset} | tr '[A-Z]' '[a-z]'`" = iso-8859-8; copiousoutput - -video/*; animate %s -video/mpeg; mpeg_play %s \ No newline at end of file diff --git a/Lib/test/test_mailcap.py b/Lib/test/test_mailcap.py deleted file mode 100644 index 8a94b0cb1f27c7..00000000000000 --- a/Lib/test/test_mailcap.py +++ /dev/null @@ -1,274 +0,0 @@ -import copy -import os -import sys -import test.support -import unittest -from test.support import os_helper -from test.support import warnings_helper - - -mailcap = warnings_helper.import_deprecated('mailcap') - - -# Location of mailcap file -MAILCAPFILE = test.support.findfile("mailcap.txt") - -# Dict to act as mock mailcap entry for this test -# The keys and values should match the contents of MAILCAPFILE -MAILCAPDICT = { - 'application/x-movie': - [{'compose': 'moviemaker %s', - 'x11-bitmap': '"/usr/lib/Zmail/bitmaps/movie.xbm"', - 'description': '"Movie"', - 'view': 'movieplayer %s', - 'lineno': 4}], - 'application/*': - [{'copiousoutput': '', - 'view': 'echo "This is \\"%t\\" but is 50 \\% Greek to me" \\; cat %s', - 'lineno': 5}], - 'audio/basic': - [{'edit': 'audiocompose %s', - 'compose': 'audiocompose %s', - 'description': '"An audio fragment"', - 'view': 'showaudio %s', - 'lineno': 6}], - 'video/mpeg': - [{'view': 'mpeg_play %s', 'lineno': 13}], - 'application/postscript': - [{'needsterminal': '', 'view': 'ps-to-terminal %s', 'lineno': 1}, - {'compose': 'idraw %s', 'view': 'ps-to-terminal %s', 'lineno': 2}], - 'application/x-dvi': - [{'view': 'xdvi %s', 'lineno': 3}], - 'message/external-body': - [{'composetyped': 'extcompose %s', - 'description': '"A reference to data stored in an external location"', - 'needsterminal': '', - 'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}', - 'lineno': 10}], - 'text/richtext': - [{'test': 'test "`echo %{charset} | tr \'[A-Z]\' \'[a-z]\'`" = iso-8859-8', - 'copiousoutput': '', - 'view': 'shownonascii iso-8859-8 -e richtext -p %s', - 'lineno': 11}], - 'image/x-xwindowdump': - [{'view': 'display %s', 'lineno': 9}], - 'audio/*': - [{'view': '/usr/local/bin/showaudio %t', 'lineno': 7}], - 'video/*': - [{'view': 'animate %s', 'lineno': 12}], - 'application/frame': - [{'print': '"cat %s | lp"', 'view': 'showframe %s', 'lineno': 0}], - 'image/rgb': - [{'view': 'display %s', 'lineno': 8}] -} - -# For backwards compatibility, readmailcapfile() and lookup() still support -# the old version of mailcapdict without line numbers. -MAILCAPDICT_DEPRECATED = copy.deepcopy(MAILCAPDICT) -for entry_list in MAILCAPDICT_DEPRECATED.values(): - for entry in entry_list: - entry.pop('lineno') - - -class HelperFunctionTest(unittest.TestCase): - - def test_listmailcapfiles(self): - # The return value for listmailcapfiles() will vary by system. - # So verify that listmailcapfiles() returns a list of strings that is of - # non-zero length. - mcfiles = mailcap.listmailcapfiles() - self.assertIsInstance(mcfiles, list) - for m in mcfiles: - self.assertIsInstance(m, str) - with os_helper.EnvironmentVarGuard() as env: - # According to RFC 1524, if MAILCAPS env variable exists, use that - # and only that. - if "MAILCAPS" in env: - env_mailcaps = env["MAILCAPS"].split(os.pathsep) - else: - env_mailcaps = ["/testdir1/.mailcap", "/testdir2/mailcap"] - env["MAILCAPS"] = os.pathsep.join(env_mailcaps) - mcfiles = mailcap.listmailcapfiles() - self.assertEqual(env_mailcaps, mcfiles) - - def test_readmailcapfile(self): - # Test readmailcapfile() using test file. It should match MAILCAPDICT. - with open(MAILCAPFILE, 'r') as mcf: - with self.assertWarns(DeprecationWarning): - d = mailcap.readmailcapfile(mcf) - self.assertDictEqual(d, MAILCAPDICT_DEPRECATED) - - def test_lookup(self): - # Test without key - expected = [{'view': 'animate %s', 'lineno': 12}, - {'view': 'mpeg_play %s', 'lineno': 13}] - actual = mailcap.lookup(MAILCAPDICT, 'video/mpeg') - self.assertListEqual(expected, actual) - - # Test with key - key = 'compose' - expected = [{'edit': 'audiocompose %s', - 'compose': 'audiocompose %s', - 'description': '"An audio fragment"', - 'view': 'showaudio %s', - 'lineno': 6}] - actual = mailcap.lookup(MAILCAPDICT, 'audio/basic', key) - self.assertListEqual(expected, actual) - - # Test on user-defined dicts without line numbers - expected = [{'view': 'mpeg_play %s'}, {'view': 'animate %s'}] - actual = mailcap.lookup(MAILCAPDICT_DEPRECATED, 'video/mpeg') - self.assertListEqual(expected, actual) - - def test_subst(self): - plist = ['id=1', 'number=2', 'total=3'] - # test case: ([field, MIMEtype, filename, plist=[]], ) - test_cases = [ - (["", "audio/*", "foo.txt"], ""), - (["echo foo", "audio/*", "foo.txt"], "echo foo"), - (["echo %s", "audio/*", "foo.txt"], "echo foo.txt"), - (["echo %t", "audio/wav", "foo.txt"], "echo audio/wav"), - (["echo \\%t", "audio/*", "foo.txt"], "echo %t"), - (["echo foo", "audio/*", "foo.txt", plist], "echo foo"), - (["echo %{total}", "audio/*", "foo.txt", plist], "echo 3") - ] - for tc in test_cases: - self.assertEqual(mailcap.subst(*tc[0]), tc[1]) - - -class GetcapsTest(unittest.TestCase): - - def test_mock_getcaps(self): - # Test mailcap.getcaps() using mock mailcap file in this dir. - # Temporarily override any existing system mailcap file by pointing the - # MAILCAPS environment variable to our mock file. - with os_helper.EnvironmentVarGuard() as env: - env["MAILCAPS"] = MAILCAPFILE - caps = mailcap.getcaps() - self.assertDictEqual(caps, MAILCAPDICT) - - def test_system_mailcap(self): - # Test mailcap.getcaps() with mailcap file(s) on system, if any. - caps = mailcap.getcaps() - self.assertIsInstance(caps, dict) - mailcapfiles = mailcap.listmailcapfiles() - existingmcfiles = [mcf for mcf in mailcapfiles if os.path.exists(mcf)] - if existingmcfiles: - # At least 1 mailcap file exists, so test that. - for (k, v) in caps.items(): - self.assertIsInstance(k, str) - self.assertIsInstance(v, list) - for e in v: - self.assertIsInstance(e, dict) - else: - # No mailcap files on system. getcaps() should return empty dict. - self.assertEqual({}, caps) - - -class FindmatchTest(unittest.TestCase): - - def test_findmatch(self): - - # default findmatch arguments - c = MAILCAPDICT - fname = "foo.txt" - plist = ["access-type=default", "name=john", "site=python.org", - "directory=/tmp", "mode=foo", "server=bar"] - audio_basic_entry = { - 'edit': 'audiocompose %s', - 'compose': 'audiocompose %s', - 'description': '"An audio fragment"', - 'view': 'showaudio %s', - 'lineno': 6 - } - audio_entry = {"view": "/usr/local/bin/showaudio %t", 'lineno': 7} - video_entry = {'view': 'animate %s', 'lineno': 12} - message_entry = { - 'composetyped': 'extcompose %s', - 'description': '"A reference to data stored in an external location"', 'needsterminal': '', - 'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}', - 'lineno': 10, - } - - # test case: (findmatch args, findmatch keyword args, expected output) - # positional args: caps, MIMEtype - # keyword args: key="view", filename="/dev/null", plist=[] - # output: (command line, mailcap entry) - cases = [ - ([{}, "video/mpeg"], {}, (None, None)), - ([c, "foo/bar"], {}, (None, None)), - ([c, "video/mpeg"], {}, ('animate /dev/null', video_entry)), - ([c, "audio/basic", "edit"], {}, ("audiocompose /dev/null", audio_basic_entry)), - ([c, "audio/basic", "compose"], {}, ("audiocompose /dev/null", audio_basic_entry)), - ([c, "audio/basic", "description"], {}, ('"An audio fragment"', audio_basic_entry)), - ([c, "audio/basic", "foobar"], {}, (None, None)), - ([c, "video/*"], {"filename": fname}, ("animate %s" % fname, video_entry)), - ([c, "audio/basic", "compose"], - {"filename": fname}, - ("audiocompose %s" % fname, audio_basic_entry)), - ([c, "audio/basic"], - {"key": "description", "filename": fname}, - ('"An audio fragment"', audio_basic_entry)), - ([c, "audio/wav"], - {"filename": fname}, - ("/usr/local/bin/showaudio audio/wav", audio_entry)), - ([c, "message/external-body"], - {"plist": plist}, - ("showexternal /dev/null default john python.org /tmp foo bar", message_entry)) - ] - self._run_cases(cases) - - @unittest.skipUnless(os.name == "posix", "Requires 'test' command on system") - @unittest.skipIf(sys.platform == "vxworks", "'test' command is not supported on VxWorks") - @unittest.skipUnless( - test.support.has_subprocess_support, - "'test' command needs process support." - ) - def test_test(self): - # findmatch() will automatically check any "test" conditions and skip - # the entry if the check fails. - caps = {"test/pass": [{"test": "test 1 -eq 1"}], - "test/fail": [{"test": "test 1 -eq 0"}]} - # test case: (findmatch args, findmatch keyword args, expected output) - # positional args: caps, MIMEtype, key ("test") - # keyword args: N/A - # output: (command line, mailcap entry) - cases = [ - # findmatch will return the mailcap entry for test/pass because it evaluates to true - ([caps, "test/pass", "test"], {}, ("test 1 -eq 1", {"test": "test 1 -eq 1"})), - # findmatch will return None because test/fail evaluates to false - ([caps, "test/fail", "test"], {}, (None, None)) - ] - self._run_cases(cases) - - def test_unsafe_mailcap_input(self): - with self.assertWarnsRegex(mailcap.UnsafeMailcapInput, - 'Refusing to substitute parameter.*' - 'into a shell command'): - unsafe_param = mailcap.subst("echo %{total}", - "audio/wav", - "foo.txt", - ["total=*"]) - self.assertEqual(unsafe_param, None) - - with self.assertWarnsRegex(mailcap.UnsafeMailcapInput, - 'Refusing to substitute MIME type' - '.*into a shell'): - unsafe_mimetype = mailcap.subst("echo %t", "audio/*", "foo.txt") - self.assertEqual(unsafe_mimetype, None) - - with self.assertWarnsRegex(mailcap.UnsafeMailcapInput, - 'Refusing to use mailcap with filename.*' - 'Use a safe temporary filename.'): - unsafe_filename = mailcap.findmatch(MAILCAPDICT, - "audio/wav", - filename="foo*.txt") - self.assertEqual(unsafe_filename, (None, None)) - - def _run_cases(self, cases): - for c in cases: - self.assertEqual(mailcap.findmatch(*c[0], **c[1]), c[2]) - - -if __name__ == '__main__': - unittest.main() diff --git a/Misc/NEWS.d/3.11.0b1.rst b/Misc/NEWS.d/3.11.0b1.rst index d8c2ec0a799711..1338819375bc89 100644 --- a/Misc/NEWS.d/3.11.0b1.rst +++ b/Misc/NEWS.d/3.11.0b1.rst @@ -796,7 +796,7 @@ Patch by Kabir Kwatra. .. nonce: roapI2 .. section: Library -The :mod:`mailcap` module is now deprecated and will be removed in Python +The :mod:`!mailcap` module is now deprecated and will be removed in Python 3.13. See :pep:`594` for the rationale and the :mod:`mimetypes` module for an alternative. Patch by Victor Stinner. diff --git a/Misc/NEWS.d/next/Library/2023-05-24-15-17-05.gh-issue-104773.EmFIQ5.rst b/Misc/NEWS.d/next/Library/2023-05-24-15-17-05.gh-issue-104773.EmFIQ5.rst new file mode 100644 index 00000000000000..95a99a20930c5a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-05-24-15-17-05.gh-issue-104773.EmFIQ5.rst @@ -0,0 +1,2 @@ +:pep:`594`: Remove the :mod:`!mailcap` module, deprecated in Python 3.11. +Patch by Victor Stinner. diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h index 37d5c05579ebe1..7aaa4f4ccdb3c3 100644 --- a/Python/stdlib_module_names.h +++ b/Python/stdlib_module_names.h @@ -175,7 +175,6 @@ static const char* _Py_stdlib_module_names[] = { "logging", "lzma", "mailbox", -"mailcap", "marshal", "math", "mimetypes", diff --git a/Tools/wasm/wasm_assets.py b/Tools/wasm/wasm_assets.py index 47bc238c64819f..34340adb3bc314 100755 --- a/Tools/wasm/wasm_assets.py +++ b/Tools/wasm/wasm_assets.py @@ -69,7 +69,6 @@ "http/", "imaplib.py", "mailbox.py", - "mailcap.py", "nntplib.py", "poplib.py", "smtplib.py",