116 lines
3.9 KiB
Python
Executable file
116 lines
3.9 KiB
Python
Executable file
#!/usr/bin/python3
|
|
|
|
# COPYRIGHT 2024 revsuine <pid1@revsuine.xyz>
|
|
# Copyright 2019 Julian Andres Klode <jak@jak-linux.org>
|
|
#
|
|
# Licensed under the GNU General Public License version 3, which is available
|
|
# at https://www.gnu.org/licenses/gpl-3.0.txt
|
|
|
|
"""
|
|
SOURCE: Based on the following:
|
|
https://github.com/julian-klode/ansible.jak-linux.org/blob/49cd62c6fa2109678c751ae5c2a7e696dd761e8e/roles/mailserver/files/usr/local/lib/dovecot-sieve-filters/gpgmymail
|
|
https://blog.jak-linux.org/2019/06/13/encrypted-email-storage/
|
|
|
|
You may also want to reference the following resources:
|
|
https://www.grepular.com/Automatically_Encrypting_all_Incoming_Email
|
|
https://perot.me/encrypt-specific-incoming-emails-using-dovecot-and-sieve
|
|
|
|
---
|
|
|
|
Encrypt/Decrypt GPG/MIME messages.
|
|
|
|
This tool can encrypt and decrypt emails using PGP/MIME. Decryption only
|
|
works well for emails created with this tool. When encrypting, the tool
|
|
preserves all headers in the original email in the encrypted part, and
|
|
copies relevant headers to the output. When decrypting, any headers are
|
|
ignored, and only the encrypted headers are restored.
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
import email.encoders
|
|
import email.message
|
|
import email.mime.application
|
|
import email.mime.multipart
|
|
import email.mime.message
|
|
import typing
|
|
|
|
# see: https://gnupg.readthedocs.io/en/latest/
|
|
import gnupg
|
|
|
|
def is_message_encrypted(message: email.message.Message) -> bool:
|
|
"""Determines whether or not an email message is encrypted.
|
|
|
|
Currently just does it by checking the content type header:
|
|
https://stackoverflow.com/questions/18819126/checking-encryption-status-of-email"""
|
|
|
|
return message.get_content_type() == "multipart/encrypted"
|
|
|
|
def encrypt(message: email.message.Message, recipients: typing.List[str]) -> str:
|
|
"""Encrypt given message"""
|
|
|
|
# some mail clients like Thunderbird don't like twice-encrypted emails,
|
|
# so we return the message as-is if it's already encrypted
|
|
if is_message_encrypted(message):
|
|
return message.as_string()
|
|
|
|
encrypted_content = gnupg.GPG().encrypt(message.as_string(), recipients, armor=True)
|
|
if not encrypted_content:
|
|
raise ValueError(encrypted_content.status)
|
|
|
|
# Build the parts
|
|
enc = email.mime.application.MIMEApplication(
|
|
_data=str(encrypted_content).encode(),
|
|
_subtype="octet-stream",
|
|
_encoder=email.encoders.encode_7or8bit
|
|
)
|
|
|
|
control = email.mime.application.MIMEApplication(
|
|
_data=b'Version: 1\n',
|
|
_subtype='pgp-encrypted; name="msg.asc"',
|
|
_encoder=email.encoders.encode_7or8bit
|
|
)
|
|
control['Content-Disposition'] = 'inline; filename="msg.asc"'
|
|
|
|
# Put the parts together
|
|
encmsg = email.mime.multipart.MIMEMultipart(
|
|
'encrypted',
|
|
protocol='application/pgp-encrypted'
|
|
)
|
|
encmsg.attach(control)
|
|
encmsg.attach(enc)
|
|
|
|
# Copy headers
|
|
headers_not_to_override = {key.lower() for key in encmsg.keys()}
|
|
|
|
for key, value in message.items():
|
|
if key.lower() not in headers_not_to_override:
|
|
encmsg[key] = value
|
|
|
|
return encmsg.as_string()
|
|
|
|
def decrypt(message: email.message.Message) -> str:
|
|
"""Decrypt the given message
|
|
Likely won't work on this server as I don't store private keys"""
|
|
return str(gnupg.GPG().decrypt(message.as_string()))
|
|
|
|
def main() -> None:
|
|
"""Program entry"""
|
|
parser = argparse.ArgumentParser(
|
|
description="Encrypt/decrypt mail using GPG/MIME"
|
|
)
|
|
parser.add_argument('-d', '--decrypt', action="store_true",
|
|
help="Decrypt rather than encrypt")
|
|
parser.add_argument('recipient', nargs='*',
|
|
help="Key ID or email of keys to encrypt for")
|
|
args = parser.parse_args()
|
|
msg = email.message_from_file(sys.stdin)
|
|
|
|
if args.decrypt:
|
|
sys.stdout.write(decrypt(msg))
|
|
else:
|
|
sys.stdout.write(encrypt(msg, args.recipient))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|