|
6 | 6 | __author__ = 'Simon Robinson' |
7 | 7 | __copyright__ = 'Copyright (c) 2022 Simon Robinson' |
8 | 8 | __license__ = 'Apache 2.0' |
9 | | -__version__ = '2023-03-22' # ISO 8601 (YYYY-MM-DD) |
| 9 | +__version__ = '2023-03-27' # ISO 8601 (YYYY-MM-DD) |
10 | 10 |
|
11 | 11 | import abc |
12 | 12 | import argparse |
@@ -148,11 +148,11 @@ class NSObject: |
148 | 148 | LOG_FILE_MAX_SIZE = 32 * 1024 * 1024 # when using a log file, its maximum size in bytes before rollover (0 = no limit) |
149 | 149 | LOG_FILE_MAX_BACKUPS = 10 # the number of log files to keep when LOG_FILE_MAX_SIZE is exceeded (0 = disable rollover) |
150 | 150 |
|
151 | | -IMAP_TAG_PATTERN = r"[!#$&',-\[\]-z|}~]+" # https://ietf.org/rfc/rfc9051.html#name-formal-syntax |
152 | | -IMAP_AUTHENTICATION_REQUEST_MATCHER = re.compile( |
153 | | - r'^(?P<tag>%s) (?P<command>(LOGIN|AUTHENTICATE)) (?P<flags>.*)$' % IMAP_TAG_PATTERN, flags=re.IGNORECASE) |
| 151 | +IMAP_TAG_PATTERN = r'[!#$&\',-\[\]-z|}~]+' # https://ietf.org/rfc/rfc9051.html#name-formal-syntax |
| 152 | +IMAP_AUTHENTICATION_REQUEST_MATCHER = re.compile('^(?P<tag>%s) (?P<command>(LOGIN|AUTHENTICATE)) ' |
| 153 | + '(?P<flags>.*)$' % IMAP_TAG_PATTERN, flags=re.IGNORECASE) |
154 | 154 | IMAP_LITERAL_MATCHER = re.compile(r'^{(?P<length>\d+)(?P<continuation>\+?)}$') |
155 | | -IMAP_CAPABILITY_MATCHER = re.compile(r'^(?:\* |\* OK \[)CAPABILITY .*$', flags=re.IGNORECASE) # note: '* ' and '* OK [' |
| 155 | +IMAP_CAPABILITY_MATCHER = re.compile(r'^\* (?:OK \[)?CAPABILITY .*$', flags=re.IGNORECASE) # note: '* ' *and* '* OK [' |
156 | 156 |
|
157 | 157 | REQUEST_QUEUE = queue.Queue() # requests for authentication |
158 | 158 | RESPONSE_QUEUE = queue.Queue() # responses from user |
@@ -945,7 +945,7 @@ def encode_oauth2_string(input_string): |
945 | 945 | def strip_quotes(text): |
946 | 946 | """Remove double quotes (i.e., " characters) around a string - used for IMAP LOGIN command""" |
947 | 947 | if text.startswith('"') and text.endswith('"'): |
948 | | - return text[1:-1].replace('\\"', '"') # also need to fix any escaped quotes within the string |
| 948 | + return text[1:-1].replace(r'\"', '"') # also need to fix any escaped quotes within the string |
949 | 949 | return text |
950 | 950 |
|
951 | 951 | @staticmethod |
@@ -1151,11 +1151,12 @@ def handle_read(self): |
1151 | 1151 | else: |
1152 | 1152 | # IMAP LOGIN command with inline username/password, POP PASS and IMAP/POP/SMTP AUTH(ENTICATE) |
1153 | 1153 | tag_pattern = IMAP_TAG_PATTERN.encode('utf-8') |
1154 | | - log_data = re.sub(b'(%s) (LOGIN) (.*)\r\n' % tag_pattern, b'\\1 \\2 %s\r\n' % CENSOR_MESSAGE, |
1155 | | - line, flags=re.IGNORECASE) |
1156 | | - log_data = re.sub(b'(PASS) (.*)\r\n', b'\\1 %s\r\n' % CENSOR_MESSAGE, log_data, flags=re.IGNORECASE) |
1157 | | - log_data = re.sub(b'(%s)?( ?)(AUTH)(ENTICATE)? (PLAIN|LOGIN) (.*)\r\n' % tag_pattern, |
1158 | | - b'\\1\\2\\3\\4 \\5 %s\r\n' % CENSOR_MESSAGE, log_data, flags=re.IGNORECASE) |
| 1154 | + log_data = re.sub(b'(%s) (LOGIN) (.*)\r\n' % tag_pattern, |
| 1155 | + br'\1 \2 ' + CENSOR_MESSAGE + b'\r\n', line, flags=re.IGNORECASE) |
| 1156 | + log_data = re.sub(b'(PASS) (.*)\r\n', |
| 1157 | + br'\1 ' + CENSOR_MESSAGE + b'\r\n', log_data, flags=re.IGNORECASE) |
| 1158 | + log_data = re.sub(b'(%s)?( )?(AUTH)(ENTICATE)? (PLAIN|LOGIN) (.*)\r\n' % tag_pattern, |
| 1159 | + br'\1\2\3\4 \5 ' + CENSOR_MESSAGE + b'\r\n', log_data, flags=re.IGNORECASE) |
1159 | 1160 |
|
1160 | 1161 | Log.debug(self.info_string(), '-->', log_data) |
1161 | 1162 | try: |
@@ -1641,16 +1642,16 @@ def process_data(self, byte_data): |
1641 | 1642 |
|
1642 | 1643 | # intercept pre-auth CAPABILITY response to advertise only AUTH=PLAIN (+SASL-IR) and re-enable LOGIN if required |
1643 | 1644 | if IMAP_CAPABILITY_MATCHER.match(str_response): |
1644 | | - capability = r"[!#$&'+-\[^-z|}~]+" # https://ietf.org/rfc/rfc9051.html#name-formal-syntax |
1645 | | - updated_response = re.sub(r'( AUTH=' + capability + r')+', ' AUTH=PLAIN', str_response, flags=re.IGNORECASE) |
1646 | | - if not re.search(r' AUTH=PLAIN', updated_response, re.IGNORECASE): |
| 1645 | + capability = r'[!#$&\'+-\[^-z|}~]+' # https://ietf.org/rfc/rfc9051.html#name-formal-syntax |
| 1646 | + updated_response = re.sub('( AUTH=%s)+' % capability, ' AUTH=PLAIN', str_response, flags=re.IGNORECASE) |
| 1647 | + if not re.search(' AUTH=PLAIN', updated_response, re.IGNORECASE): |
1647 | 1648 | # cannot just replace e.g., one 'CAPABILITY ' match because IMAP4 must be first if present (RFC 1730) |
1648 | | - updated_response = re.sub(r'(CAPABILITY)( IMAP' + capability + r')?', r'\g<1>\g<2> AUTH=PLAIN', |
1649 | | - updated_response, count=1, flags=re.IGNORECASE) |
| 1649 | + updated_response = re.sub('(CAPABILITY)( IMAP%s)?' % capability, r'\1\2 AUTH=PLAIN', updated_response, |
| 1650 | + count=1, flags=re.IGNORECASE) |
1650 | 1651 | updated_response = updated_response.replace(' AUTH=PLAIN', '', updated_response.count(' AUTH=PLAIN') - 1) |
1651 | | - if not re.search(r' SASL-IR', updated_response, re.IGNORECASE): |
| 1652 | + if not re.search(' SASL-IR', updated_response, re.IGNORECASE): |
1652 | 1653 | updated_response = updated_response.replace(' AUTH=PLAIN', ' AUTH=PLAIN SASL-IR') |
1653 | | - updated_response = re.sub(r' LOGINDISABLED', '', updated_response, count=1, flags=re.IGNORECASE) |
| 1654 | + updated_response = re.sub(' LOGINDISABLED', '', updated_response, count=1, flags=re.IGNORECASE) |
1654 | 1655 | byte_data = (b'%s\r\n' % updated_response.encode('utf-8')) |
1655 | 1656 |
|
1656 | 1657 | super().process_data(byte_data) |
@@ -1768,7 +1769,7 @@ def process_data(self, byte_data): |
1768 | 1769 | # intercept EHLO response AUTH capabilities and replace with what we can actually do - note that we assume |
1769 | 1770 | # an AUTH line will be included in the response; if there are any servers for which this is not the case, we |
1770 | 1771 | # could cache and re-stream as in POP. Formal syntax: https://tools.ietf.org/html/rfc4954#section-8 |
1771 | | - updated_response = re.sub(r'250([ -])AUTH( [!-*,-<>-~]+)+', '250\\1AUTH PLAIN LOGIN', str_data, |
| 1772 | + updated_response = re.sub('250([ -])AUTH( [!-*,-<>-~]+)+', r'250\1AUTH PLAIN LOGIN', str_data, |
1772 | 1773 | flags=re.IGNORECASE) |
1773 | 1774 | updated_response = b'%s\r\n' % updated_response.encode('utf-8') |
1774 | 1775 | if self.starttls_state is self.STARTTLS.COMPLETE: |
@@ -2619,7 +2620,7 @@ def get_script_start_command(self, quote_args=True): |
2619 | 2620 | if self.args.external_auth: |
2620 | 2621 | script_command.append('--external-auth') |
2621 | 2622 |
|
2622 | | - return ['"%s"' % arg.replace('"', '\\"') if quote_args and ' ' in arg else arg for arg in script_command] |
| 2623 | + return ['"%s"' % arg.replace('"', r'\"') if quote_args and ' ' in arg else arg for arg in script_command] |
2623 | 2624 |
|
2624 | 2625 | def linux_restart(self, icon): |
2625 | 2626 | # Linux restarting is separate because it is used for reloading the configuration file as well as start at login |
@@ -2680,7 +2681,7 @@ def notify(self, title, text): |
2680 | 2681 | notification_centre.setDelegate_(self.macos_user_notification_centre_delegate) |
2681 | 2682 | notification_centre.deliverNotification_(user_notification) |
2682 | 2683 | except Exception: |
2683 | | - for replacement in (('\\', '\\\\'), ('"', '\\"')): # osascript approach requires sanitisation |
| 2684 | + for replacement in (('\\', r'\\'), ('"', r'\"')): # osascript approach requires sanitisation |
2684 | 2685 | text = text.replace(*replacement) |
2685 | 2686 | title = title.replace(*replacement) |
2686 | 2687 | subprocess.call(['osascript', '-e', 'display notification "%s" with title "%s"' % (text, title)]) |
|
0 commit comments