(root)/
Python-3.12.0/
Lib/
quopri.py
       1  #! /usr/bin/env python3
       2  
       3  """Conversions to/from quoted-printable transport encoding as per RFC 1521."""
       4  
       5  # (Dec 1991 version).
       6  
       7  __all__ = ["encode", "decode", "encodestring", "decodestring"]
       8  
       9  ESCAPE = b'='
      10  MAXLINESIZE = 76
      11  HEX = b'0123456789ABCDEF'
      12  EMPTYSTRING = b''
      13  
      14  try:
      15      from binascii import a2b_qp, b2a_qp
      16  except ImportError:
      17      a2b_qp = None
      18      b2a_qp = None
      19  
      20  
      21  def needsquoting(c, quotetabs, header):
      22      """Decide whether a particular byte ordinal needs to be quoted.
      23  
      24      The 'quotetabs' flag indicates whether embedded tabs and spaces should be
      25      quoted.  Note that line-ending tabs and spaces are always encoded, as per
      26      RFC 1521.
      27      """
      28      assert isinstance(c, bytes)
      29      if c in b' \t':
      30          return quotetabs
      31      # if header, we have to escape _ because _ is used to escape space
      32      if c == b'_':
      33          return header
      34      return c == ESCAPE or not (b' ' <= c <= b'~')
      35  
      36  def quote(c):
      37      """Quote a single character."""
      38      assert isinstance(c, bytes) and len(c)==1
      39      c = ord(c)
      40      return ESCAPE + bytes((HEX[c//16], HEX[c%16]))
      41  
      42  
      43  
      44  def encode(input, output, quotetabs, header=False):
      45      """Read 'input', apply quoted-printable encoding, and write to 'output'.
      46  
      47      'input' and 'output' are binary file objects. The 'quotetabs' flag
      48      indicates whether embedded tabs and spaces should be quoted. Note that
      49      line-ending tabs and spaces are always encoded, as per RFC 1521.
      50      The 'header' flag indicates whether we are encoding spaces as _ as per RFC
      51      1522."""
      52  
      53      if b2a_qp is not None:
      54          data = input.read()
      55          odata = b2a_qp(data, quotetabs=quotetabs, header=header)
      56          output.write(odata)
      57          return
      58  
      59      def write(s, output=output, lineEnd=b'\n'):
      60          # RFC 1521 requires that the line ending in a space or tab must have
      61          # that trailing character encoded.
      62          if s and s[-1:] in b' \t':
      63              output.write(s[:-1] + quote(s[-1:]) + lineEnd)
      64          elif s == b'.':
      65              output.write(quote(s) + lineEnd)
      66          else:
      67              output.write(s + lineEnd)
      68  
      69      prevline = None
      70      while line := input.readline():
      71          outline = []
      72          # Strip off any readline induced trailing newline
      73          stripped = b''
      74          if line[-1:] == b'\n':
      75              line = line[:-1]
      76              stripped = b'\n'
      77          # Calculate the un-length-limited encoded line
      78          for c in line:
      79              c = bytes((c,))
      80              if needsquoting(c, quotetabs, header):
      81                  c = quote(c)
      82              if header and c == b' ':
      83                  outline.append(b'_')
      84              else:
      85                  outline.append(c)
      86          # First, write out the previous line
      87          if prevline is not None:
      88              write(prevline)
      89          # Now see if we need any soft line breaks because of RFC-imposed
      90          # length limitations.  Then do the thisline->prevline dance.
      91          thisline = EMPTYSTRING.join(outline)
      92          while len(thisline) > MAXLINESIZE:
      93              # Don't forget to include the soft line break `=' sign in the
      94              # length calculation!
      95              write(thisline[:MAXLINESIZE-1], lineEnd=b'=\n')
      96              thisline = thisline[MAXLINESIZE-1:]
      97          # Write out the current line
      98          prevline = thisline
      99      # Write out the last line, without a trailing newline
     100      if prevline is not None:
     101          write(prevline, lineEnd=stripped)
     102  
     103  def encodestring(s, quotetabs=False, header=False):
     104      if b2a_qp is not None:
     105          return b2a_qp(s, quotetabs=quotetabs, header=header)
     106      from io import BytesIO
     107      infp = BytesIO(s)
     108      outfp = BytesIO()
     109      encode(infp, outfp, quotetabs, header)
     110      return outfp.getvalue()
     111  
     112  
     113  
     114  def decode(input, output, header=False):
     115      """Read 'input', apply quoted-printable decoding, and write to 'output'.
     116      'input' and 'output' are binary file objects.
     117      If 'header' is true, decode underscore as space (per RFC 1522)."""
     118  
     119      if a2b_qp is not None:
     120          data = input.read()
     121          odata = a2b_qp(data, header=header)
     122          output.write(odata)
     123          return
     124  
     125      new = b''
     126      while line := input.readline():
     127          i, n = 0, len(line)
     128          if n > 0 and line[n-1:n] == b'\n':
     129              partial = 0; n = n-1
     130              # Strip trailing whitespace
     131              while n > 0 and line[n-1:n] in b" \t\r":
     132                  n = n-1
     133          else:
     134              partial = 1
     135          while i < n:
     136              c = line[i:i+1]
     137              if c == b'_' and header:
     138                  new = new + b' '; i = i+1
     139              elif c != ESCAPE:
     140                  new = new + c; i = i+1
     141              elif i+1 == n and not partial:
     142                  partial = 1; break
     143              elif i+1 < n and line[i+1:i+2] == ESCAPE:
     144                  new = new + ESCAPE; i = i+2
     145              elif i+2 < n and ishex(line[i+1:i+2]) and ishex(line[i+2:i+3]):
     146                  new = new + bytes((unhex(line[i+1:i+3]),)); i = i+3
     147              else: # Bad escape sequence -- leave it in
     148                  new = new + c; i = i+1
     149          if not partial:
     150              output.write(new + b'\n')
     151              new = b''
     152      if new:
     153          output.write(new)
     154  
     155  def decodestring(s, header=False):
     156      if a2b_qp is not None:
     157          return a2b_qp(s, header=header)
     158      from io import BytesIO
     159      infp = BytesIO(s)
     160      outfp = BytesIO()
     161      decode(infp, outfp, header=header)
     162      return outfp.getvalue()
     163  
     164  
     165  
     166  # Other helper functions
     167  def ishex(c):
     168      """Return true if the byte ordinal 'c' is a hexadecimal digit in ASCII."""
     169      assert isinstance(c, bytes)
     170      return b'0' <= c <= b'9' or b'a' <= c <= b'f' or b'A' <= c <= b'F'
     171  
     172  def unhex(s):
     173      """Get the integer value of a hexadecimal number."""
     174      bits = 0
     175      for c in s:
     176          c = bytes((c,))
     177          if b'0' <= c <= b'9':
     178              i = ord('0')
     179          elif b'a' <= c <= b'f':
     180              i = ord('a')-10
     181          elif b'A' <= c <= b'F':
     182              i = ord(b'A')-10
     183          else:
     184              assert False, "non-hex digit "+repr(c)
     185          bits = bits*16 + (ord(c) - i)
     186      return bits
     187  
     188  
     189  
     190  def main():
     191      import sys
     192      import getopt
     193      try:
     194          opts, args = getopt.getopt(sys.argv[1:], 'td')
     195      except getopt.error as msg:
     196          sys.stdout = sys.stderr
     197          print(msg)
     198          print("usage: quopri [-t | -d] [file] ...")
     199          print("-t: quote tabs")
     200          print("-d: decode; default encode")
     201          sys.exit(2)
     202      deco = False
     203      tabs = False
     204      for o, a in opts:
     205          if o == '-t': tabs = True
     206          if o == '-d': deco = True
     207      if tabs and deco:
     208          sys.stdout = sys.stderr
     209          print("-t and -d are mutually exclusive")
     210          sys.exit(2)
     211      if not args: args = ['-']
     212      sts = 0
     213      for file in args:
     214          if file == '-':
     215              fp = sys.stdin.buffer
     216          else:
     217              try:
     218                  fp = open(file, "rb")
     219              except OSError as msg:
     220                  sys.stderr.write("%s: can't open (%s)\n" % (file, msg))
     221                  sts = 1
     222                  continue
     223          try:
     224              if deco:
     225                  decode(fp, sys.stdout.buffer)
     226              else:
     227                  encode(fp, sys.stdout.buffer, tabs)
     228          finally:
     229              if file != '-':
     230                  fp.close()
     231      if sts:
     232          sys.exit(sts)
     233  
     234  
     235  
     236  if __name__ == '__main__':
     237      main()