Compare commits
33 commits
same-encod
...
master
Author | SHA1 | Date | |
---|---|---|---|
f7bb04e5ec | |||
e0c200c95e | |||
f5a0d3fdde | |||
224de12f09 | |||
3ca2cca469 | |||
638f622f24 | |||
52405bf067 | |||
f5a31997cf | |||
182e1ceb43 | |||
0802cf5b3d | |||
4670bcdafd | |||
272842162d | |||
8fea330537 | |||
c148b94be1 | |||
ec1fda254a | |||
633a54e2b1 | |||
218ee51ac2 | |||
f1a07cb1e0 | |||
f559fef2ed | |||
10f25158bf | |||
6590baafd1 | |||
b213a0ab52 | |||
9a3a1f2cb7 | |||
158f5356a1 | |||
82c5144e58 | |||
a21ee759c8 | |||
2955e990da | |||
e7a8f40b91 | |||
8755e0e1cc | |||
b78f1f04af | |||
f895e87ec0 | |||
ed57c8f0d4 | |||
61120dd4e8 |
3 changed files with 59 additions and 32 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -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
|
||||||
|
|
||||||
|
|
14
README.md
14
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
70
gpgmymail
70
gpgmymail
|
@ -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,14 +92,19 @@ 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
|
||||||
encrypted_content = gpg.encrypt(message.as_string(), recipients, armor=True)
|
encrypted_content = gpg.encrypt(message.as_string(), recipients, armor=True)
|
||||||
|
@ -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__':
|
||||||
|
|
Loading…
Reference in a new issue