Compare commits
No commits in common. "master" and "same-encoding-as-orig" have entirely different histories.
master
...
same-encod
3 changed files with 32 additions and 59 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -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
|
|
||||||
|
|
||||||
|
|
14
README.md
14
README.md
|
@ -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
|
|
||||||
|
|
||||||
|
|
70
gpgmymail
70
gpgmymail
|
@ -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,19 +89,14 @@ 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"""
|
|
||||||
|
|
||||||
# mark the email as having passed through us
|
:return: The encrypted email as a string"""
|
||||||
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()
|
||||||
gpg.encoding = encoding
|
gpg.encoding = encoding
|
||||||
encrypted_content = gpg.encrypt(message.as_string(), recipients, armor=True)
|
encrypted_content = gpg.encrypt(message.as_string(), recipients, armor=True)
|
||||||
|
@ -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__':
|
||||||
|
|
Loading…
Reference in a new issue