Compare commits

..

No commits in common. "master" and "same-encoding-as-orig" have entirely different histories.

3 changed files with 32 additions and 59 deletions

7
.gitignore vendored
View file

@ -1,9 +1,4 @@
venv/ venv/
__pycache__/
.idea/ .idea/
# misc testing scripts testing.py
testing*.py
# i have a symlink here so testing scripts can import gpgmymail
# as python expects `import gpgmymail` to be importing `gpgmymail.py`
gpgmymail.py

View file

@ -1,14 +1,7 @@
# gpgmymail # gpgmymail
Takes an email from stdin and encrypts it to stdout using the recipient's PGP Takes an email from stdin and encrypts it using the recipient's PGP key,
key, provided as an argument when calling the script. provided as an argument when calling the script.
Leaves a `X-gpgmymail-Status` header on the email, which has the following
statuses:
* `entered` - the email has entered the encryption function, but not been
encrypted
* `encrypted` - the encryption function has encrypted the email
Written to be a Sieve filter to be used with `sieve_extprograms`. Can be used Written to be a Sieve filter to be used with `sieve_extprograms`. Can be used
in a Sieve filter e.g.: in a Sieve filter e.g.:
@ -40,6 +33,5 @@ behaviour which is much better achieved with Sieve will be implemented, e.g.
# Credits # Credits
* Julian Klode for the [original code](https://github.com/julian-klode/ansible.jak-linux.org/blob/dovecot/roles/mailserver/files/usr/local/lib/dovecot-sieve-filters/gpgmymail) * Julian Klode for the [original code](https://github.com/julian-klode/ansible.jak-linux.org/blob/dovecot/roles/mailserver/files/usr/local/lib/dovecot-sieve-filters/gpgmymail)
* revsuine for modifications to gpgmymail, mostly to make it work well with * revsuine for modifications to gpgmymail
Thunderbird

View file

@ -24,12 +24,6 @@ works well for emails created with this tool. When encrypting, the tool
preserves all headers in the original email in the encrypted part, and preserves all headers in the original email in the encrypted part, and
copies relevant headers to the output. When decrypting, any headers are copies relevant headers to the output. When decrypting, any headers are
ignored, and only the encrypted headers are restored. ignored, and only the encrypted headers are restored.
Emails exiting this script will have the 'X-gpgmymail-Status' header, which has
the following options:
- entered: the email has entered the encrypt() function
- encrypted: the email has been encrypted
""" """
import argparse import argparse
@ -46,6 +40,13 @@ import gnupg
# constants # constants
DEFAULT_ENCODING='utf-8' # default is latin-1 which fails w some unicode chars DEFAULT_ENCODING='utf-8' # default is latin-1 which fails w some unicode chars
CTE_TO_ENCODER_DICT = {
"7bit": email.encoders.encode_7or8bit,
"8bit": email.encoders.encode_7or8bit,
"base64": email.encoders.encode_base64,
"quoted-printable": email.encoders.encode_quopri
}
DEFAULT_ENCODER = email.encoders.encode_7or8bit
def is_message_encrypted(message: email.message.Message) -> bool: def is_message_encrypted(message: email.message.Message) -> bool:
"""Determines whether or not an email message is encrypted. """Determines whether or not an email message is encrypted.
@ -55,34 +56,30 @@ def is_message_encrypted(message: email.message.Message) -> bool:
return message.get_content_subtype() == "encrypted" return message.get_content_subtype() == "encrypted"
def set_email_header( def get_encoder_from_msg(msg: email.message.Message) -> typing.Callable:
message: email.message.Message,
name: str,
value: str
) -> None:
""" """
Set the header of an email Message. Will either replace the first instance Return a suitable encoder function from email.encoders based on an input
of the header, or if the header is not present, will add the header. message. If the input message has no Content-Transfer-Encoding header,
or there is no encoder function corresponding to the CTE header, a default
encoder will be returned.
Note: Python passes objects as references, so there is no need for a return :param msg: an unencrypted email Message
value. :return: function from email.encoders, see
https://docs.python.org/3/library/email.encoders.html
:param message: the Message object to be modified
:param name: the email header to set
:param value: the value to set the header to
""" """
if message.get(name): cte = msg.get("Content-Transfer-Encoding")
message.replace_header(name, value) if cte:
encoder = CTE_TO_ENCODER_DICT.get(cte)
else: else:
message.add_header(name, value) return DEFAULT_ENCODER
return encoder if encoder else DEFAULT_ENCODER
def encrypt( def encrypt(
message: email.message.Message, message: email.message.Message,
recipients: typing.List[str], recipients: typing.List[str],
*, *,
unconditionally_encrypt: bool = False, unconditionally_encrypt: bool = False,
encoding: str = DEFAULT_ENCODING, encoding: str = DEFAULT_ENCODING
ignore_errors: bool = False,
) -> str: ) -> str:
"""Encrypt given message """Encrypt given message
@ -92,17 +89,12 @@ def encrypt(
False (default), will NOT encrypt if any of the following conditions are False (default), will NOT encrypt if any of the following conditions are
met: met:
- The message is already encrypted - The message is already encrypted
:param encoding: string for encoding to use for the gnupg.GPG object
:return: The encrypted email as a string""" :return: The encrypted email as a string"""
# mark the email as having passed through us
set_email_header(message, 'X-gpgmymail-Status', 'entered')
# exclusion criteria:
# some mail clients like Thunderbird don't like twice-encrypted emails, # some mail clients like Thunderbird don't like twice-encrypted emails,
# so we return the message as-is if it's already encrypted # so we return the message as-is if it's already encrypted
if not unconditionally_encrypt: if is_message_encrypted(message) and not unconditionally_encrypt:
if is_message_encrypted(message):
return message.as_string() return message.as_string()
gpg = gnupg.GPG() gpg = gnupg.GPG()
@ -115,13 +107,13 @@ def encrypt(
enc = email.mime.application.MIMEApplication( enc = email.mime.application.MIMEApplication(
_data=str(encrypted_content).encode(), _data=str(encrypted_content).encode(),
_subtype="octet-stream", _subtype="octet-stream",
_encoder=email.encoders.encode_7or8bit _encoder=get_encoder_from_msg(message)
) )
control = email.mime.application.MIMEApplication( control = email.mime.application.MIMEApplication(
_data=b'Version: 1\n', _data=b'Version: 1\n',
_subtype='pgp-encrypted; name="msg.asc"', _subtype='pgp-encrypted; name="msg.asc"',
_encoder=email.encoders.encode_7or8bit _encoder=get_encoder_from_msg(message)
) )
control['Content-Disposition'] = 'inline; filename="msg.asc"' control['Content-Disposition'] = 'inline; filename="msg.asc"'
@ -140,9 +132,6 @@ def encrypt(
if key.lower() not in headers_not_to_override: if key.lower() not in headers_not_to_override:
encmsg[key] = value encmsg[key] = value
# we have encrypted the email, set our gpgmymail header appropriately
set_email_header(encmsg, 'X-gpgmymail-Status', 'encrypted')
return encmsg.as_string() return encmsg.as_string()
def decrypt(message: email.message.Message, *, encoding: str = DEFAULT_ENCODING) -> str: def decrypt(message: email.message.Message, *, encoding: str = DEFAULT_ENCODING) -> str:
@ -164,8 +153,6 @@ def main() -> None:
help="Encoding to use for the gnupg.GPG object") help="Encoding to use for the gnupg.GPG object")
parser.add_argument('--unconditional', action="store_true", parser.add_argument('--unconditional', action="store_true",
help="Encrypt mail unconditionally. By default, mail is not encrypted if it is already encrypted.") 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('recipient', nargs='*', parser.add_argument('recipient', nargs='*',
help="Key ID or email of keys to encrypt for") help="Key ID or email of keys to encrypt for")
args = parser.parse_args() args = parser.parse_args()
@ -178,8 +165,7 @@ def main() -> None:
msg, msg,
args.recipient, args.recipient,
unconditionally_encrypt=args.unconditional, unconditionally_encrypt=args.unconditional,
encoding=args.encoding, encoding=args.encoding
ignore_errors=args.ignore_errors,
)) ))
if __name__ == '__main__': if __name__ == '__main__':