mirror of
https://github.com/python/cpython.git
synced 2025-08-04 17:08:35 +00:00
gh-104773: PEP 594: Remove the mailcap module (#104867)
Remove Lib/test/mailcap.txt test file.
This commit is contained in:
parent
bbc5e5c7d7
commit
9d457e1154
12 changed files with 9 additions and 715 deletions
|
@ -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 <warnings>` 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'})
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -893,7 +893,7 @@ Modules (see :pep:`594`):
|
|||
* :mod:`chunk`
|
||||
* :mod:`crypt`
|
||||
* :mod:`imghdr`
|
||||
* :mod:`mailcap`
|
||||
* :mod:`!mailcap`
|
||||
* :mod:`msilib`
|
||||
* :mod:`nis`
|
||||
* :mod:`nntplib`
|
||||
|
|
|
@ -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
|
||||
======================
|
||||
|
|
302
Lib/mailcap.py
302
Lib/mailcap.py
|
@ -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()
|
|
@ -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
|
|
@ -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=[]], <expected string>)
|
||||
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()
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
:pep:`594`: Remove the :mod:`!mailcap` module, deprecated in Python 3.11.
|
||||
Patch by Victor Stinner.
|
1
Python/stdlib_module_names.h
generated
1
Python/stdlib_module_names.h
generated
|
@ -175,7 +175,6 @@ static const char* _Py_stdlib_module_names[] = {
|
|||
"logging",
|
||||
"lzma",
|
||||
"mailbox",
|
||||
"mailcap",
|
||||
"marshal",
|
||||
"math",
|
||||
"mimetypes",
|
||||
|
|
|
@ -69,7 +69,6 @@ OMIT_NETWORKING_FILES = (
|
|||
"http/",
|
||||
"imaplib.py",
|
||||
"mailbox.py",
|
||||
"mailcap.py",
|
||||
"nntplib.py",
|
||||
"poplib.py",
|
||||
"smtplib.py",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue