gpgmymail/gpgmymail
2024-11-04 16:24:24 +00:00

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()