"Fossies" - the Fresh Open Source Software Archive

Member "mailman-3.3.7/src/mailman/rules/approved.py" (10 Nov 2022, 6959 Bytes) of package /linux/misc/mailman-3.3.7.tar.bz2:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "approved.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 3.3.5_vs_3.3.6.

    1 # Copyright (C) 2007-2022 by the Free Software Foundation, Inc.
    2 #
    3 # This file is part of GNU Mailman.
    4 #
    5 # GNU Mailman is free software: you can redistribute it and/or modify it under
    6 # the terms of the GNU General Public License as published by the Free
    7 # Software Foundation, either version 3 of the License, or (at your option)
    8 # any later version.
    9 #
   10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
   11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
   12 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
   13 # more details.
   14 #
   15 # You should have received a copy of the GNU General Public License along with
   16 # GNU Mailman.  If not, see <https://www.gnu.org/licenses/>.
   17 
   18 """Look for moderator pre-approval."""
   19 
   20 import re
   21 
   22 from email.iterators import typed_subpart_iterator
   23 from mailman.config import config
   24 from mailman.core.i18n import _
   25 from mailman.interfaces.rules import IRule
   26 from public import public
   27 from zope.interface import implementer
   28 
   29 
   30 EMPTYSTRING = ''
   31 HEADERS = [
   32     'approve',
   33     'approved',
   34     'x-approve',
   35     'x-approved',
   36     ]
   37 
   38 
   39 @public
   40 @implementer(IRule)
   41 class Approved:
   42     """Look for moderator pre-approval."""
   43 
   44     name = 'approved'
   45     description = _('The message has a matching Approve or Approved header.')
   46     record = True
   47 
   48     def _get_password(self, msg, missing):
   49         for header in HEADERS:
   50             password = msg.get(header, missing)
   51             if password is not missing:
   52                 return password
   53         return missing
   54 
   55     def check(self, mlist, msg, msgdata):
   56         """See `IRule`."""
   57         if mlist.moderator_password is None:
   58             return False
   59         # See if the message has an Approved or Approve header with a valid
   60         # moderator password.  Also look at the first non-whitespace line in
   61         # the file to see if it looks like an Approved header.
   62         missing = object()
   63         password = self._get_password(msg, missing)
   64         if password is missing:
   65             # Find the first text/plain part in the message
   66             part = None
   67             stripped = False
   68             payload = None
   69             for part in typed_subpart_iterator(msg, 'text', 'plain'):
   70                 payload = part.get_payload(decode=True)
   71                 break
   72             if payload is not None:
   73                 charset = part.get_content_charset('us-ascii')
   74                 try:
   75                     # Do the decoding inside the try/except so that if the
   76                     # charset is unknown, we'll just drop back to ascii.
   77                     payload = payload.decode(charset, 'replace')
   78                 except LookupError:
   79                     # Unknown or empty charset.
   80                     payload = payload.decode('us-ascii', 'replace')
   81                 line = ''
   82                 lines = payload.splitlines(True)
   83                 for lineno, line in enumerate(lines):
   84                     if line.strip() != '':
   85                         break
   86                 if ':' in line:
   87                     header, value = line.split(':', 1)
   88                     if header.lower() in HEADERS:
   89                         password = value.strip()
   90                         # Now strip the first line from the payload so the
   91                         # password doesn't leak.
   92                         del lines[lineno]
   93                         reset_payload(part, EMPTYSTRING.join(lines))
   94                         stripped = True
   95             if stripped:
   96                 # Now try all the text parts in case it's
   97                 # multipart/alternative with the approved line in HTML or
   98                 # other text part.  We make a pattern from the Approved line
   99                 # and delete it from all text/* parts in which we find it.  It
  100                 # would be better to just iterate forward, but email
  101                 # compatability for pre Python 2.2 returns a list, not a true
  102                 # iterator.
  103                 #
  104                 # This will process all the multipart/alternative parts in the
  105                 # message as well as all other text parts.  We shouldn't find
  106                 # the pattern outside the multipart/alternative parts, but if
  107                 # we do, it is probably best to delete it anyway as it does
  108                 # contain the password.
  109                 #
  110                 # Make a pattern to delete.  We can't just delete a line
  111                 # because line of HTML or other fancy text may include
  112                 # additional message text.  This pattern works with HTML.  It
  113                 # may not work with rtf or whatever else is possible.
  114                 pattern = (header + r':(\s|&nbsp;)*' + re.escape(password))
  115                 for part in typed_subpart_iterator(msg, 'text'):
  116                     payload = part.get_payload(decode=True)
  117                     if payload is not None:
  118                         charset = part.get_content_charset('us-ascii')
  119                         try:
  120                             # Do the decoding inside the try/except so that if
  121                             # the charset is unknown, we'll just drop back to
  122                             # ascii.
  123                             payload = payload.decode(charset, 'replace')
  124                         except LookupError:
  125                             # Unknown or empty charset.
  126                             payload = payload.decode('us-ascii', 'replace')
  127                         if re.search(pattern, payload):
  128                             reset_payload(part, re.sub(pattern, '', payload))
  129         else:
  130             for header in HEADERS:
  131                 del msg[header]
  132         if password is missing:
  133             return False
  134         # Email confirm command wants to know there was an Approved:.
  135         msgdata['has_approved'] = True
  136         # The following is a workaround for
  137         # https://foss.heptapod.net/python-libs/passlib/-/issues/133
  138         # Ensure the hash for `verify` is not bytes.
  139         if isinstance(mlist.moderator_password, str):
  140             mpw = mlist.moderator_password
  141         else:
  142             mpw = mlist.moderator_password.decode('utf-8')
  143         is_valid, new_hash = config.password_context.verify(password, mpw)
  144         if is_valid and new_hash:
  145             # Hash algorithm migration.
  146             mlist.moderator_password = new_hash
  147         return is_valid
  148 
  149 
  150 def reset_payload(part, payload):
  151     # Set decoded payload maintaining content-type, charset, format and delsp.
  152     charset = part.get_content_charset() or 'us-ascii'
  153     content_type = part.get_content_type()
  154     format = part.get_param('format')
  155     delsp = part.get_param('delsp')
  156     del part['content-transfer-encoding']
  157     del part['content-type']
  158     try:
  159         part.set_payload(payload, charset)
  160     except LookupError:
  161         part.set_payload(payload, 'us-ascii')
  162     part.set_type(content_type)
  163     if format:
  164         part.set_param('Format', format)
  165     if delsp:
  166         part.set_param('DelSp', delsp)