(root)/
Python-3.12.0/
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 line := fp.readline():
      94          # Ignore comments and blank lines
      95          if line[0] == '#' or line.strip() == '':
      96              continue
      97          nextline = line
      98          # Join continuation lines
      99          while nextline[-2:] == '\\\n':
     100              nextline = fp.readline()
     101              if not nextline: nextline = '\n'
     102              line = line[:-2] + nextline
     103          # Parse the line
     104          key, fields = parseline(line)
     105          if not (key and fields):
     106              continue
     107          if lineno is not None:
     108              fields['lineno'] = lineno
     109              lineno += 1
     110          # Normalize the key
     111          types = key.split('/')
     112          for j in range(len(types)):
     113              types[j] = types[j].strip()
     114          key = '/'.join(types).lower()
     115          # Update the database
     116          if key in caps:
     117              caps[key].append(fields)
     118          else:
     119              caps[key] = [fields]
     120      return caps, lineno
     121  
     122  def parseline(line):
     123      """Parse one entry in a mailcap file and return a dictionary.
     124  
     125      The viewing command is stored as the value with the key "view",
     126      and the rest of the fields produce key-value pairs in the dict.
     127      """
     128      fields = []
     129      i, n = 0, len(line)
     130      while i < n:
     131          field, i = parsefield(line, i, n)
     132          fields.append(field)
     133          i = i+1 # Skip semicolon
     134      if len(fields) < 2:
     135          return None, None
     136      key, view, rest = fields[0], fields[1], fields[2:]
     137      fields = {'view': view}
     138      for field in rest:
     139          i = field.find('=')
     140          if i < 0:
     141              fkey = field
     142              fvalue = ""
     143          else:
     144              fkey = field[:i].strip()
     145              fvalue = field[i+1:].strip()
     146          if fkey in fields:
     147              # Ignore it
     148              pass
     149          else:
     150              fields[fkey] = fvalue
     151      return key, fields
     152  
     153  def parsefield(line, i, n):
     154      """Separate one key-value pair in a mailcap entry."""
     155      start = i
     156      while i < n:
     157          c = line[i]
     158          if c == ';':
     159              break
     160          elif c == '\\':
     161              i = i+2
     162          else:
     163              i = i+1
     164      return line[start:i].strip(), i
     165  
     166  
     167  # Part 3: using the database.
     168  
     169  def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):
     170      """Find a match for a mailcap entry.
     171  
     172      Return a tuple containing the command line, and the mailcap entry
     173      used; (None, None) if no match is found.  This may invoke the
     174      'test' command of several matching entries before deciding which
     175      entry to use.
     176  
     177      """
     178      if _find_unsafe(filename):
     179          msg = "Refusing to use mailcap with filename %r. Use a safe temporary filename." % (filename,)
     180          warnings.warn(msg, UnsafeMailcapInput)
     181          return None, None
     182      entries = lookup(caps, MIMEtype, key)
     183      # XXX This code should somehow check for the needsterminal flag.
     184      for e in entries:
     185          if 'test' in e:
     186              test = subst(e['test'], filename, plist)
     187              if test is None:
     188                  continue
     189              if test and os.system(test) != 0:
     190                  continue
     191          command = subst(e[key], MIMEtype, filename, plist)
     192          if command is not None:
     193              return command, e
     194      return None, None
     195  
     196  def lookup(caps, MIMEtype, key=None):
     197      entries = []
     198      if MIMEtype in caps:
     199          entries = entries + caps[MIMEtype]
     200      MIMEtypes = MIMEtype.split('/')
     201      MIMEtype = MIMEtypes[0] + '/*'
     202      if MIMEtype in caps:
     203          entries = entries + caps[MIMEtype]
     204      if key is not None:
     205          entries = [e for e in entries if key in e]
     206      entries = sorted(entries, key=lineno_sort_key)
     207      return entries
     208  
     209  def subst(field, MIMEtype, filename, plist=[]):
     210      # XXX Actually, this is Unix-specific
     211      res = ''
     212      i, n = 0, len(field)
     213      while i < n:
     214          c = field[i]; i = i+1
     215          if c != '%':
     216              if c == '\\':
     217                  c = field[i:i+1]; i = i+1
     218              res = res + c
     219          else:
     220              c = field[i]; i = i+1
     221              if c == '%':
     222                  res = res + c
     223              elif c == 's':
     224                  res = res + filename
     225              elif c == 't':
     226                  if _find_unsafe(MIMEtype):
     227                      msg = "Refusing to substitute MIME type %r into a shell command." % (MIMEtype,)
     228                      warnings.warn(msg, UnsafeMailcapInput)
     229                      return None
     230                  res = res + MIMEtype
     231              elif c == '{':
     232                  start = i
     233                  while i < n and field[i] != '}':
     234                      i = i+1
     235                  name = field[start:i]
     236                  i = i+1
     237                  param = findparam(name, plist)
     238                  if _find_unsafe(param):
     239                      msg = "Refusing to substitute parameter %r (%s) into a shell command" % (param, name)
     240                      warnings.warn(msg, UnsafeMailcapInput)
     241                      return None
     242                  res = res + param
     243              # XXX To do:
     244              # %n == number of parts if type is multipart/*
     245              # %F == list of alternating type and filename for parts
     246              else:
     247                  res = res + '%' + c
     248      return res
     249  
     250  def findparam(name, plist):
     251      name = name.lower() + '='
     252      n = len(name)
     253      for p in plist:
     254          if p[:n].lower() == name:
     255              return p[n:]
     256      return ''
     257  
     258  
     259  # Part 4: test program.
     260  
     261  def test():
     262      import sys
     263      caps = getcaps()
     264      if not sys.argv[1:]:
     265          show(caps)
     266          return
     267      for i in range(1, len(sys.argv), 2):
     268          args = sys.argv[i:i+2]
     269          if len(args) < 2:
     270              print("usage: mailcap [MIMEtype file] ...")
     271              return
     272          MIMEtype = args[0]
     273          file = args[1]
     274          command, e = findmatch(caps, MIMEtype, 'view', file)
     275          if not command:
     276              print("No viewer found for", type)
     277          else:
     278              print("Executing:", command)
     279              sts = os.system(command)
     280              sts = os.waitstatus_to_exitcode(sts)
     281              if sts:
     282                  print("Exit status:", sts)
     283  
     284  def show(caps):
     285      print("Mailcap files:")
     286      for fn in listmailcapfiles(): print("\t" + fn)
     287      print()
     288      if not caps: caps = getcaps()
     289      print("Mailcap entries:")
     290      print()
     291      ckeys = sorted(caps)
     292      for type in ckeys:
     293          print(type)
     294          entries = caps[type]
     295          for e in entries:
     296              keys = sorted(e)
     297              for k in keys:
     298                  print("  %-15s" % k, e[k])
     299              print()
     300  
     301  if __name__ == '__main__':
     302      test()