123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- """Mailcap file handling. See RFC 1524."""
- import os
- import warnings
- __all__ = ["getcaps","findmatch"]
- 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
- # 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 1:
- line = fp.readline()
- if not line: break
- # 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.
- """
- 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 and os.system(test) != 0:
- continue
- command = subst(e[key], MIMEtype, filename, plist)
- 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':
- res = res + MIMEtype
- elif c == '{':
- start = i
- while i < n and field[i] != '}':
- i = i+1
- name = field[start:i]
- i = i+1
- res = res + findparam(name, plist)
- # 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()
|