(root)/
Python-3.11.7/
Lib/
mailcap.py
       1  """Mailcap file handling.  See RFC 1524."""
       2  
       3  import os
       4  import warnings
       5  import re
       6  
       7  __all__ = ["getcaps","findmatch"]
       8  
       9  
      10  _DEPRECATION_MSG = ('The {name} module is deprecated and will be removed in '
      11                      'Python {remove}. See the mimetypes module for an '
      12                      'alternative.')
      13  warnings._deprecated(__name__, _DEPRECATION_MSG, remove=(3, 13))
      14  
      15  
      16  def lineno_sort_key(entry):
      17      # Sort in ascending order, with unspecified entries at the end
      18      if 'lineno' in entry:
      19          return 0, entry['lineno']
      20      else:
      21          return 1, 0
      22  
      23  _find_unsafe = re.compile(r'[^\xa1-\U0010FFFF\w@+=:,./-]').search
      24  
      25  class ESC[4;38;5;81mUnsafeMailcapInput(ESC[4;38;5;149mWarning):
      26      """Warning raised when refusing unsafe input"""
      27  
      28  
      29  # Part 1: top-level interface.
      30  
      31  def getcaps():
      32      """Return a dictionary containing the mailcap database.
      33  
      34      The dictionary maps a MIME type (in all lowercase, e.g. 'text/plain')
      35      to a list of dictionaries corresponding to mailcap entries.  The list
      36      collects all the entries for that MIME type from all available mailcap
      37      files.  Each dictionary contains key-value pairs for that MIME type,
      38      where the viewing command is stored with the key "view".
      39  
      40      """
      41      caps = {}
      42      lineno = 0
      43      for mailcap in listmailcapfiles():
      44          try:
      45              fp = open(mailcap, 'r')
      46          except OSError:
      47              continue
      48          with fp:
      49              morecaps, lineno = _readmailcapfile(fp, lineno)
      50          for key, value in morecaps.items():
      51              if not key in caps:
      52                  caps[key] = value
      53              else:
      54                  caps[key] = caps[key] + value
      55      return caps
      56  
      57  def listmailcapfiles():
      58      """Return a list of all mailcap files found on the system."""
      59      # This is mostly a Unix thing, but we use the OS path separator anyway
      60      if 'MAILCAPS' in os.environ:
      61          pathstr = os.environ['MAILCAPS']
      62          mailcaps = pathstr.split(os.pathsep)
      63      else:
      64          if 'HOME' in os.environ:
      65              home = os.environ['HOME']
      66          else:
      67              # Don't bother with getpwuid()
      68              home = '.' # Last resort
      69          mailcaps = [home + '/.mailcap', '/etc/mailcap',
      70                  '/usr/etc/mailcap', '/usr/local/etc/mailcap']
      71      return mailcaps
      72  
      73  
      74  # Part 2: the parser.
      75  def readmailcapfile(fp):
      76      """Read a mailcap file and return a dictionary keyed by MIME type."""
      77      warnings.warn('readmailcapfile is deprecated, use getcaps instead',
      78                    DeprecationWarning, 2)
      79      caps, _ = _readmailcapfile(fp, None)
      80      return caps
      81  
      82  
      83  def _readmailcapfile(fp, lineno):
      84      """Read a mailcap file and return a dictionary keyed by MIME type.
      85  
      86      Each MIME type is mapped to an entry consisting of a list of
      87      dictionaries; the list will contain more than one such dictionary
      88      if a given MIME type appears more than once in the mailcap file.
      89      Each dictionary contains key-value pairs for that MIME type, where
      90      the viewing command is stored with the key "view".
      91      """
      92      caps = {}
      93      while 1:
      94          line = fp.readline()
      95          if not line: break
      96          # Ignore comments and blank lines
      97          if line[0] == '#' or line.strip() == '':
      98              continue
      99          nextline = line
     100          # Join continuation lines
     101          while nextline[-2:] == '\\\n':
     102              nextline = fp.readline()
     103              if not nextline: nextline = '\n'
     104              line = line[:-2] + nextline
     105          # Parse the line
     106          key, fields = parseline(line)
     107          if not (key and fields):
     108              continue
     109          if lineno is not None:
     110              fields['lineno'] = lineno
     111              lineno += 1
     112          # Normalize the key
     113          types = key.split('/')
     114          for j in range(len(types)):
     115              types[j] = types[j].strip()
     116          key = '/'.join(types).lower()
     117          # Update the database
     118          if key in caps:
     119              caps[key].append(fields)
     120          else:
     121              caps[key] = [fields]
     122      return caps, lineno
     123  
     124  def parseline(line):
     125      """Parse one entry in a mailcap file and return a dictionary.
     126  
     127      The viewing command is stored as the value with the key "view",
     128      and the rest of the fields produce key-value pairs in the dict.
     129      """
     130      fields = []
     131      i, n = 0, len(line)
     132      while i < n:
     133          field, i = parsefield(line, i, n)
     134          fields.append(field)
     135          i = i+1 # Skip semicolon
     136      if len(fields) < 2:
     137          return None, None
     138      key, view, rest = fields[0], fields[1], fields[2:]
     139      fields = {'view': view}
     140      for field in rest:
     141          i = field.find('=')
     142          if i < 0:
     143              fkey = field
     144              fvalue = ""
     145          else:
     146              fkey = field[:i].strip()
     147              fvalue = field[i+1:].strip()
     148          if fkey in fields:
     149              # Ignore it
     150              pass
     151          else:
     152              fields[fkey] = fvalue
     153      return key, fields
     154  
     155  def parsefield(line, i, n):
     156      """Separate one key-value pair in a mailcap entry."""
     157      start = i
     158      while i < n:
     159          c = line[i]
     160          if c == ';':
     161              break
     162          elif c == '\\':
     163              i = i+2
     164          else:
     165              i = i+1
     166      return line[start:i].strip(), i
     167  
     168  
     169  # Part 3: using the database.
     170  
     171  def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):
     172      """Find a match for a mailcap entry.
     173  
     174      Return a tuple containing the command line, and the mailcap entry
     175      used; (None, None) if no match is found.  This may invoke the
     176      'test' command of several matching entries before deciding which
     177      entry to use.
     178  
     179      """
     180      if _find_unsafe(filename):
     181          msg = "Refusing to use mailcap with filename %r. Use a safe temporary filename." % (filename,)
     182          warnings.warn(msg, UnsafeMailcapInput)
     183          return None, None
     184      entries = lookup(caps, MIMEtype, key)
     185      # XXX This code should somehow check for the needsterminal flag.
     186      for e in entries:
     187          if 'test' in e:
     188              test = subst(e['test'], filename, plist)
     189              if test is None:
     190                  continue
     191              if test and os.system(test) != 0:
     192                  continue
     193          command = subst(e[key], MIMEtype, filename, plist)
     194          if command is not None:
     195              return command, e
     196      return None, None
     197  
     198  def lookup(caps, MIMEtype, key=None):
     199      entries = []
     200      if MIMEtype in caps:
     201          entries = entries + caps[MIMEtype]
     202      MIMEtypes = MIMEtype.split('/')
     203      MIMEtype = MIMEtypes[0] + '/*'
     204      if MIMEtype in caps:
     205          entries = entries + caps[MIMEtype]
     206      if key is not None:
     207          entries = [e for e in entries if key in e]
     208      entries = sorted(entries, key=lineno_sort_key)
     209      return entries
     210  
     211  def subst(field, MIMEtype, filename, plist=[]):
     212      # XXX Actually, this is Unix-specific
     213      res = ''
     214      i, n = 0, len(field)
     215      while i < n:
     216          c = field[i]; i = i+1
     217          if c != '%':
     218              if c == '\\':
     219                  c = field[i:i+1]; i = i+1
     220              res = res + c
     221          else:
     222              c = field[i]; i = i+1
     223              if c == '%':
     224                  res = res + c
     225              elif c == 's':
     226                  res = res + filename
     227              elif c == 't':
     228                  if _find_unsafe(MIMEtype):
     229                      msg = "Refusing to substitute MIME type %r into a shell command." % (MIMEtype,)
     230                      warnings.warn(msg, UnsafeMailcapInput)
     231                      return None
     232                  res = res + MIMEtype
     233              elif c == '{':
     234                  start = i
     235                  while i < n and field[i] != '}':
     236                      i = i+1
     237                  name = field[start:i]
     238                  i = i+1
     239                  param = findparam(name, plist)
     240                  if _find_unsafe(param):
     241                      msg = "Refusing to substitute parameter %r (%s) into a shell command" % (param, name)
     242                      warnings.warn(msg, UnsafeMailcapInput)
     243                      return None
     244                  res = res + param
     245              # XXX To do:
     246              # %n == number of parts if type is multipart/*
     247              # %F == list of alternating type and filename for parts
     248              else:
     249                  res = res + '%' + c
     250      return res
     251  
     252  def findparam(name, plist):
     253      name = name.lower() + '='
     254      n = len(name)
     255      for p in plist:
     256          if p[:n].lower() == name:
     257              return p[n:]
     258      return ''
     259  
     260  
     261  # Part 4: test program.
     262  
     263  def test():
     264      import sys
     265      caps = getcaps()
     266      if not sys.argv[1:]:
     267          show(caps)
     268          return
     269      for i in range(1, len(sys.argv), 2):
     270          args = sys.argv[i:i+2]
     271          if len(args) < 2:
     272              print("usage: mailcap [MIMEtype file] ...")
     273              return
     274          MIMEtype = args[0]
     275          file = args[1]
     276          command, e = findmatch(caps, MIMEtype, 'view', file)
     277          if not command:
     278              print("No viewer found for", type)
     279          else:
     280              print("Executing:", command)
     281              sts = os.system(command)
     282              sts = os.waitstatus_to_exitcode(sts)
     283              if sts:
     284                  print("Exit status:", sts)
     285  
     286  def show(caps):
     287      print("Mailcap files:")
     288      for fn in listmailcapfiles(): print("\t" + fn)
     289      print()
     290      if not caps: caps = getcaps()
     291      print("Mailcap entries:")
     292      print()
     293      ckeys = sorted(caps)
     294      for type in ckeys:
     295          print(type)
     296          entries = caps[type]
     297          for e in entries:
     298              keys = sorted(e)
     299              for k in keys:
     300                  print("  %-15s" % k, e[k])
     301              print()
     302  
     303  if __name__ == '__main__':
     304      test()