-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Version 3.2.0: Copy
mailcap.py
from Python 3.12
Because Python 3.13 dropped it.
- Loading branch information
Showing
8 changed files
with
316 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
__version__ = "3.1.0.post6" | ||
__version__ = "3.2.0" | ||
__author__ = "Oleg Broytman <[email protected]>" | ||
__copyright__ = "Copyright (C) 2001-2024 PhiloSoft Design" | ||
__license__ = "GNU GPL" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,302 @@ | ||
"""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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.