From f7bb04e5ecf404f13a610e97809e9fc2b1a2f55d Mon Sep 17 00:00:00 2001 From: revsuine Date: Sat, 23 Nov 2024 02:54:50 +0000 Subject: [PATCH] gpgmymail: remove all 'decoding' code --- gpgmymail | 170 ------------------------------------------------------ 1 file changed, 170 deletions(-) diff --git a/gpgmymail b/gpgmymail index 330adc1..b42e891 100755 --- a/gpgmymail +++ b/gpgmymail @@ -40,15 +40,12 @@ import email.mime.application import email.mime.multipart import email.mime.message import typing -# for decode_email: -import quopri # see: https://gnupg.readthedocs.io/en/latest/ import gnupg # constants DEFAULT_ENCODING='utf-8' # default is latin-1 which fails w some unicode chars -PEP_SUBJECT='=?utf-8?Q?p=E2=89=A1p?=' def is_message_encrypted(message: email.message.Message) -> bool: """Determines whether or not an email message is encrypted. @@ -58,125 +55,6 @@ def is_message_encrypted(message: email.message.Message) -> bool: return message.get_content_subtype() == "encrypted" -def decode_email(message: email.message.Message) -> email.message.Message: - """Turn a quoted-printable or base64 encoded email into a 7or8bit encoded - email - - :param message: email.message.Message to be decoded - :return: decoded email.message.Message""" - def decoded_bytes_to_return_value(decoded_bytes: bytes) -> email.message.Message: - """ - if at any point you want to return, return a call of this function and - pass decoded_bytes - - :param decoded_bytes: an email as an ASCII byte array; this should be - stored in decoded_bytes - :return: the expected return value of decode_email - """ - # if i do message_from_bytes it bizarrely changes it back to base64? - # utf-8 has encoding issues so do latin1 - return email.message_from_string(decoded_bytes.decode("latin1")) - - # this is a kinda hacky way to do this by manipulating the message as a - # string but i couldn't get it to work any other way - - # sometimes this raises an exception and idk why - try: - decoded_bytes = message.as_bytes() - except UnicodeEncodeError: - decoded_bytes = message.as_string().encode() - # if email doesn't need decoding - has_quopri = b'Content-Transfer-Encoding: quoted-printable' in decoded_bytes - has_base64 = is_base64_text(message) # we don't want to decode non-text - if not (has_quopri or has_base64): - return message - decoded_bytes = quopri.decodestring(decoded_bytes) - - # replace any instances of the Content-Transfer-Encoding header - # quopri version, we do base64 version down there - decoded_bytes = decoded_bytes.replace( - b'Content-Transfer-Encoding: quoted-printable', - b'Content-Transfer-Encoding: 7bit' - ) - - # now exit if there's no base64 as i think that's the most fucky - if not has_base64: - return decoded_bytes_to_return_value(decoded_bytes) - - # REALLY hacky but i had issues with the more sensible ways to do this. - # iterates through a Message object to find CTEs of base64 - # gets the b64 payload and the decoded payload - # then find and replaces in decoded_bytes the b64 payload - # with the decoded payload - # lol - - def decode_b64_part( - part: email.message.Message, - decoded_bytes: bytes, - most_recent_boundary: str = None - ) -> bytes: - """ - change decoded_bytes such that part is decoded if base64 (unchanged if - not) and text (so will not decode base64 images etc) - - see usage below for examples - - :param part: email.message.Message to be decoded - :param decoded_bytes: the email as a bytes object (ie not a string with - encoding), will have modified version returned - :param most_recent_boundary: str of the most recent boundary so we - don't overwrite this - :return: bytes of decoded_bytes with part decoded if base64 - """ - if part.get("Content-Transfer-Encoding") == "base64" and \ - part.get_content_maintype() == "text": - b64_str = part.get_payload() - # remove the boundary as we don't want to change this - if most_recent_boundary: - b64_str = b64_str.replace(most_recent_boundary, "") - # sometimes we have leftover hyphens from a boundary, so strip: - # hyphens not in base64 so we know not to use them - # strip whitespace first - b64_str = b64_str.strip() - b64_str = b64_str.strip('-') - b64_str = b64_str.encode() # turn into bytes-like object - # this will also decode the boundary so there'll be some nonsese - # chars at end of email but it's nbd - decoded_b64_str = part.get_payload(decode=True) - return decoded_bytes.replace( - b64_str, - decoded_b64_str - ) - - return decoded_bytes - - quopri_decoded_message = email.message_from_bytes(decoded_bytes) - if quopri_decoded_message.is_multipart(): - most_recent_boundary = None - for part in quopri_decoded_message.walk(): - # multipart and has boundary (not None) - if part.is_multipart() and part.get_boundary(): - most_recent_boundary = part.get_boundary() - else: - decoded_bytes = decode_b64_part( - part, - decoded_bytes, - most_recent_boundary - ) - else: - decoded_bytes = decode_b64_part( - quopri_decoded_message, - decoded_bytes, - None - ) - - decoded_bytes = decoded_bytes.replace( - b'Content-Transfer-Encoding: base64', - b'Content-Transfer-Encoding: 7bit' - ) - - return decoded_bytes_to_return_value(decoded_bytes) - def set_email_header( message: email.message.Message, name: str, @@ -198,25 +76,6 @@ def set_email_header( else: message.add_header(name, value) -def is_base64_text(message: email.message.Message) -> bool: - """ - Return whether or not there is base64-encoded text in a multipart - message, i.e. will be False if it's only e.g. images that are encoded as - base64. - - :param message: the email Message to check - :return: True if there is a text part that's base64 encoded, False if not - """ - if message.is_multipart(): - for part in message.walk(): - if part.get('Content-Transfer-Encoding') == "base64" and \ - part.get_content_maintype() == "text": - return True - return False - else: - return message.get('Content-Transfer-Encoding') == "base64" and \ - message.get_content_maintype() == "text" - def encrypt( message: email.message.Message, recipients: typing.List[str], @@ -224,7 +83,6 @@ def encrypt( unconditionally_encrypt: bool = False, encoding: str = DEFAULT_ENCODING, ignore_errors: bool = False, - decode_before_encrypting: bool = False ) -> str: """Encrypt given message @@ -234,17 +92,7 @@ def encrypt( False (default), will NOT encrypt if any of the following conditions are met: - The message is already encrypted - - The message is encoded with base64 :param encoding: string for encoding to use for the gnupg.GPG object - :param ignore_errors: bool, puts some parts of the function in - - try: - do_stuff() - except: - pass - - blocks. find ignore_errors to see instances where this occurs - :return: The encrypted email as a string""" # mark the email as having passed through us @@ -256,22 +104,7 @@ def encrypt( if not unconditionally_encrypt: if is_message_encrypted(message): return message.as_string() - # bc i just have a bunch of issues w b64 - if is_base64_text(message): - return message.as_string() - # make necessary changes to message - # this function is quite clunky and seems to throw exceptions from time to - # time; if we can't make the necessary changes we want to just continue - if decode_before_encrypting: - try: - message = decode_email(message) - except Exception as e: - if ignore_errors: - pass - else: - raise e - gpg = gnupg.GPG() gpg.encoding = encoding encrypted_content = gpg.encrypt(message.as_string(), recipients, armor=True) @@ -333,8 +166,6 @@ def main() -> None: help="Encrypt mail unconditionally. By default, mail is not encrypted if it is already encrypted.") parser.add_argument('--ignore-errors', action="store_true", help="Ignore errors at certain error-prone points of the script.") - parser.add_argument('--decode', action="store_true", - help="Attempt to decode quoted-printable and base64 parts to latin-1 before encrypting a message") parser.add_argument('recipient', nargs='*', help="Key ID or email of keys to encrypt for") args = parser.parse_args() @@ -349,7 +180,6 @@ def main() -> None: unconditionally_encrypt=args.unconditional, encoding=args.encoding, ignore_errors=args.ignore_errors, - decode_before_encrypting=args.decode )) if __name__ == '__main__':