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/
|
||||
__pycache__/
|
||||
.idea/
|
||||
# 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
|
||||
testing.py
|
||||
|
||||
|
|
14
README.md
14
README.md
|
@ -1,14 +1,7 @@
|
|||
# gpgmymail
|
||||
|
||||
Takes an email from stdin and encrypts it to stdout using the recipient's PGP
|
||||
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
|
||||
Takes an email from stdin and encrypts it using the recipient's PGP key,
|
||||
provided as an argument when calling the script.
|
||||
|
||||
Written to be a Sieve filter to be used with `sieve_extprograms`. Can be used
|
||||
in a Sieve filter e.g.:
|
||||
|
@ -40,6 +33,5 @@ behaviour which is much better achieved with Sieve will be implemented, e.g.
|
|||
# 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)
|
||||
* revsuine for modifications to gpgmymail, mostly to make it work well with
|
||||
Thunderbird
|
||||
* revsuine for modifications to gpgmymail
|
||||
|
||||
|
|
68
gpgmymail
68
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
|
||||
copies relevant headers to the output. When decrypting, any headers are
|
||||
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
|
||||
|
@ -46,6 +40,13 @@ import gnupg
|
|||
|
||||
# constants
|
||||
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:
|
||||
"""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"
|
||||
|
||||
def set_email_header(
|
||||
message: email.message.Message,
|
||||
name: str,
|
||||
value: str
|
||||
) -> None:
|
||||
def get_encoder_from_msg(msg: email.message.Message) -> typing.Callable:
|
||||
"""
|
||||
Set the header of an email Message. Will either replace the first instance
|
||||
of the header, or if the header is not present, will add the header.
|
||||
Return a suitable encoder function from email.encoders based on an input
|
||||
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
|
||||
value.
|
||||
|
||||
:param message: the Message object to be modified
|
||||
:param name: the email header to set
|
||||
:param value: the value to set the header to
|
||||
:param msg: an unencrypted email Message
|
||||
:return: function from email.encoders, see
|
||||
https://docs.python.org/3/library/email.encoders.html
|
||||
"""
|
||||
if message.get(name):
|
||||
message.replace_header(name, value)
|
||||
cte = msg.get("Content-Transfer-Encoding")
|
||||
if cte:
|
||||
encoder = CTE_TO_ENCODER_DICT.get(cte)
|
||||
else:
|
||||
message.add_header(name, value)
|
||||
return DEFAULT_ENCODER
|
||||
return encoder if encoder else DEFAULT_ENCODER
|
||||
|
||||
def encrypt(
|
||||
message: email.message.Message,
|
||||
recipients: typing.List[str],
|
||||
*,
|
||||
unconditionally_encrypt: bool = False,
|
||||
encoding: str = DEFAULT_ENCODING,
|
||||
ignore_errors: bool = False,
|
||||
encoding: str = DEFAULT_ENCODING
|
||||
) -> str:
|
||||
"""Encrypt given message
|
||||
|
||||
|
@ -92,18 +89,13 @@ def encrypt(
|
|||
False (default), will NOT encrypt if any of the following conditions are
|
||||
met:
|
||||
- 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
|
||||
set_email_header(message, 'X-gpgmymail-Status', 'entered')
|
||||
|
||||
# exclusion criteria:
|
||||
# some mail clients like Thunderbird don't like twice-encrypted emails,
|
||||
# so we return the message as-is if it's already encrypted
|
||||
if not unconditionally_encrypt:
|
||||
if is_message_encrypted(message):
|
||||
return message.as_string()
|
||||
if is_message_encrypted(message) and not unconditionally_encrypt:
|
||||
return message.as_string()
|
||||
|
||||
gpg = gnupg.GPG()
|
||||
gpg.encoding = encoding
|
||||
|
@ -115,13 +107,13 @@ def encrypt(
|
|||
enc = email.mime.application.MIMEApplication(
|
||||
_data=str(encrypted_content).encode(),
|
||||
_subtype="octet-stream",
|
||||
_encoder=email.encoders.encode_7or8bit
|
||||
_encoder=get_encoder_from_msg(message)
|
||||
)
|
||||
|
||||
control = email.mime.application.MIMEApplication(
|
||||
_data=b'Version: 1\n',
|
||||
_subtype='pgp-encrypted; name="msg.asc"',
|
||||
_encoder=email.encoders.encode_7or8bit
|
||||
_encoder=get_encoder_from_msg(message)
|
||||
)
|
||||
control['Content-Disposition'] = 'inline; filename="msg.asc"'
|
||||
|
||||
|
@ -140,9 +132,6 @@ def encrypt(
|
|||
if key.lower() not in headers_not_to_override:
|
||||
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()
|
||||
|
||||
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")
|
||||
parser.add_argument('--unconditional', action="store_true",
|
||||
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='*',
|
||||
help="Key ID or email of keys to encrypt for")
|
||||
args = parser.parse_args()
|
||||
|
@ -178,8 +165,7 @@ def main() -> None:
|
|||
msg,
|
||||
args.recipient,
|
||||
unconditionally_encrypt=args.unconditional,
|
||||
encoding=args.encoding,
|
||||
ignore_errors=args.ignore_errors,
|
||||
encoding=args.encoding
|
||||
))
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
Loading…
Reference in a new issue