Compare commits

..

33 commits

Author SHA1 Message Date
f7bb04e5ec
gpgmymail: remove all 'decoding' code 2024-11-23 02:54:50 +00:00
e0c200c95e
syntax error fix 2024-11-18 15:16:19 +00:00
f5a0d3fdde
make decoding emails optional 2024-11-17 23:39:48 +00:00
224de12f09
no longer change subject line or add X-pEp-Version header 2024-11-16 19:00:00 +00:00
3ca2cca469
update README to better describe my changes 2024-11-16 15:52:32 +00:00
638f622f24
don't decode base64 non-text parts 2024-11-16 15:44:27 +00:00
52405bf067
Merge branch 'pEp-standards' 2024-11-15 17:59:49 +00:00
f5a31997cf
add X-gpgmymail-Status header 2024-11-15 17:55:22 +00:00
182e1ceb43
conform to pEp standards 2024-11-15 17:40:22 +00:00
0802cf5b3d
add --ignore-errors arg & skip encrypting base64 emails 2024-11-15 17:02:42 +00:00
4670bcdafd
wrap some things in try/except blocks 2024-11-15 14:36:54 +00:00
272842162d
don't decode if decoding unneeded 2024-11-14 20:12:40 +00:00
8fea330537
doc 2024-11-14 19:51:56 +00:00
c148b94be1
decode_b64_part always returns something 2024-11-14 19:47:59 +00:00
ec1fda254a
Merge branch '7or8bit-decode' 2024-11-14 19:45:11 +00:00
633a54e2b1
decode b64 bytes for non-multipart message too 2024-11-14 19:39:17 +00:00
218ee51ac2
abstract b64 decoding behaviour to a function 2024-11-14 19:37:15 +00:00
f1a07cb1e0
use message_from_string instead of message_from_bytes cause message_from_bytes bizarrely changes it back to base64 2024-11-14 19:24:40 +00:00
f559fef2ed
strip trailing hyphens from base64 2024-11-14 19:15:37 +00:00
10f25158bf
try to decode w/o boundary 2024-11-14 19:05:21 +00:00
6590baafd1
try do this by replacing entire payload 2024-11-14 18:55:06 +00:00
b213a0ab52
don't encode decoded 2024-11-14 18:52:18 +00:00
9a3a1f2cb7
encode the find and replaces for b64 decode 2024-11-14 18:46:30 +00:00
158f5356a1
syntax error fix 2024-11-14 18:45:30 +00:00
82c5144e58
implement really hacky way of decoding b64 parts 2024-11-14 18:44:28 +00:00
a21ee759c8
temp remove base64 decoding and unconditionally quopri decode 2024-11-14 18:26:22 +00:00
2955e990da
var name typo 2024-11-14 18:15:06 +00:00
e7a8f40b91
first attempt to implement decoding as 7or8bit through quopri, base64, and byte replacement 2024-11-14 18:13:16 +00:00
8755e0e1cc
gitignore some testing files 2024-11-14 17:39:25 +00:00
b78f1f04af
more testing scripts 2024-11-14 15:59:20 +00:00
f895e87ec0
account for non-multipart emails 2024-11-14 15:19:57 +00:00
ed57c8f0d4
first attempt to implement decoding as 7or8bit (non-working) 2024-11-13 17:22:21 +00:00
61120dd4e8
revert gpgmymail to 40b3ba3760 2024-11-12 19:00:56 +00:00
3 changed files with 59 additions and 32 deletions

7
.gitignore vendored
View file

@ -1,4 +1,9 @@
venv/ venv/
__pycache__/
.idea/ .idea/
testing.py # misc testing scripts
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,7 +1,14 @@
# gpgmymail # gpgmymail
Takes an email from stdin and encrypts it using the recipient's PGP key, Takes an email from stdin and encrypts it to stdout using the recipient's PGP
provided as an argument when calling the script. key, 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.:
@ -33,5 +40,6 @@ 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 * revsuine for modifications to gpgmymail, mostly to make it work well with
Thunderbird

View file

@ -24,6 +24,12 @@ 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
@ -40,13 +46,6 @@ 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.
@ -56,30 +55,34 @@ def is_message_encrypted(message: email.message.Message) -> bool:
return message.get_content_subtype() == "encrypted" return message.get_content_subtype() == "encrypted"
def get_encoder_from_msg(msg: email.message.Message) -> typing.Callable: def set_email_header(
message: email.message.Message,
name: str,
value: str
) -> None:
""" """
Return a suitable encoder function from email.encoders based on an input Set the header of an email Message. Will either replace the first instance
message. If the input message has no Content-Transfer-Encoding header, of the header, or if the header is not present, will add the header.
or there is no encoder function corresponding to the CTE header, a default
encoder will be returned.
:param msg: an unencrypted email Message Note: Python passes objects as references, so there is no need for a return
:return: function from email.encoders, see value.
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
""" """
cte = msg.get("Content-Transfer-Encoding") if message.get(name):
if cte: message.replace_header(name, value)
encoder = CTE_TO_ENCODER_DICT.get(cte)
else: else:
return DEFAULT_ENCODER message.add_header(name, value)
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
@ -89,13 +92,18 @@ 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 is_message_encrypted(message) and not unconditionally_encrypt: if not unconditionally_encrypt:
return message.as_string() if is_message_encrypted(message):
return message.as_string()
gpg = gnupg.GPG() gpg = gnupg.GPG()
gpg.encoding = encoding gpg.encoding = encoding
@ -107,13 +115,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=get_encoder_from_msg(message) _encoder=email.encoders.encode_7or8bit
) )
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=get_encoder_from_msg(message) _encoder=email.encoders.encode_7or8bit
) )
control['Content-Disposition'] = 'inline; filename="msg.asc"' control['Content-Disposition'] = 'inline; filename="msg.asc"'
@ -132,6 +140,9 @@ 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:
@ -153,6 +164,8 @@ 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()
@ -165,7 +178,8 @@ 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__':