1 #! /usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 """*********************************************************************
4 * Edward is free software: you can redistribute it and/or modify *
5 * it under the terms of the GNU Affero Public License as published by *
6 * the Free Software Foundation, either version 3 of the License, or *
7 * (at your option) any later version. *
9 * Edward is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU Affero Public License for more details. *
14 * You should have received a copy of the GNU Affero Public License *
15 * along with Edward. If not, see <http://www.gnu.org/licenses/>. *
17 * Copyright (C) 2014-2015 Andrew Engelbrecht (AGPLv3+) *
18 * Copyright (C) 2014 Josh Drake (AGPLv3+) *
19 * Copyright (C) 2014 Lisa Marie Maginnis (AGPLv3+) *
20 * Copyright (C) 2009-2015 Tails developers <tails@boum.org> ( GPLv3+) *
21 * Copyright (C) 2009 W. Trevor King <wking@drexel.edu> ( GPLv2+) *
23 * Special thanks to Josh Drake for writing the original edward bot! :) *
25 ************************************************************************
27 Code sourced from these projects:
29 * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz
30 * https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3
31 * http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime
45 from email
.mime
.multipart
import MIMEMultipart
46 from email
.mime
.application
import MIMEApplication
47 from email
.mime
.nonmultipart
import MIMENonMultipart
55 gpgme_ctx
= get_gpg_context(edward_config
.gnupghome
,
56 edward_config
.sign_with_key
)
58 email_text
= sys
.stdin
.read()
59 email_from
, email_subject
= email_from_subject(email_text
)
61 plaintext
, keys
= email_decode_flatten(email_text
, gpgme_ctx
, False)
62 encrypt_to_key
= choose_reply_encryption_key(keys
)
64 reply_message
= generate_reply(plaintext
, email_from
, \
65 email_subject
, encrypt_to_key
,
71 def get_gpg_context (gnupghome
, sign_with_key_fp
):
73 os
.environ
['GNUPGHOME'] = gnupghome
75 gpgme_ctx
= gpgme
.Context()
76 gpgme_ctx
.armor
= True
79 sign_with_key
= gpgme_ctx
.get_key(sign_with_key_fp
)
81 error("unable to load signing key. is the gnupghome "
82 + "and signing key properly set in the edward_config.py?")
85 gpgme_ctx
.signers
= [sign_with_key
]
90 def email_decode_flatten (email_text
, gpgme_ctx
, from_decryption
):
95 email_struct
= email
.parser
.Parser().parsestr(email_text
)
97 for subpart
in email_struct
.walk():
99 payload
, description
, filename
, content_type \
100 = get_email_subpart_info(subpart
)
105 if content_type
== "multipart":
108 if content_type
== "application/pgp-encrypted":
109 if ((description
== "PGP/MIME version identification")
110 and (payload
.strip() != "Version: 1")):
111 debug("Warning: unknown " + description
112 + ": " + payload
.strip())
113 # ignore the version number
116 if (filename
== "encrypted.asc") or (content_type
== "pgp/mime"):
117 plaintext
, more_keys
= decrypt_text(payload
, gpgme_ctx
)
122 elif content_type
== "application/pgp-keys":
123 keys
+= add_gpg_keys(payload
, gpgme_ctx
)
125 elif content_type
== "text/plain":
126 if from_decryption
== True:
127 body
+= payload
+ "\n"
129 keys
+= add_gpg_keys(payload
, gpgme_ctx
)
132 plaintext
, more_keys
= decrypt_text(payload
, gpgme_ctx
)
137 keys
+= add_gpg_keys(payload
, gpgme_ctx
)
142 def email_from_subject (email_text
):
144 email_struct
= email
.parser
.Parser().parsestr(email_text
)
146 email_from
= email_struct
['From']
147 email_subject
= email_struct
['Subject']
149 return email_from
, email_subject
152 def get_email_subpart_info (part
):
154 charset
= part
.get_content_charset()
155 payload_bytes
= part
.get_payload(decode
=True)
157 filename
= part
.get_filename()
158 content_type
= part
.get_content_type()
159 description_list
= part
.get_params(header
='content-description')
164 if payload_bytes
!= None:
165 payload
= payload_bytes
.decode(charset
)
169 if description_list
!= None:
170 description
= description_list
[0][0]
174 return payload
, description
, filename
, content_type
177 def add_gpg_keys (text
, gpgme_ctx
):
179 key_blocks
= scan_and_grab(text
,
180 '-----BEGIN PGP PUBLIC KEY BLOCK-----',
181 '-----END PGP PUBLIC KEY BLOCK-----')
184 for key_block
in key_blocks
:
185 fp
= io
.BytesIO(key_block
.encode('ascii'))
187 result
= gpgme_ctx
.import_(fp
)
188 imports
= result
.imports
191 fingerprint
= imports
[0][0]
192 keys
+= [gpgme_ctx
.get_key(fingerprint
)]
194 debug("added gpg key: " + fingerprint
)
199 def decrypt_text (gpg_text
, gpgme_ctx
):
204 msg_blocks
= scan_and_grab(gpg_text
,
205 '-----BEGIN PGP MESSAGE-----',
206 '-----END PGP MESSAGE-----')
208 plaintexts_and_sigs
= decrypt_blocks(msg_blocks
, gpgme_ctx
)
210 for pair
in plaintexts_and_sigs
:
215 keys
+= [gpgme_ctx
.get_key(sig
.fpr
)]
217 # recursive for nested layers of mime and/or gpg
218 plaintext
, more_keys
= email_decode_flatten(plaintext
, gpgme_ctx
, True)
226 def scan_and_grab (text
, start_text
, end_text
):
228 matches
= re
.search('(' + start_text
+ '.*' + end_text
+ ')',
229 text
, flags
=re
.DOTALL
)
232 match_tuple
= matches
.groups()
239 def decrypt_blocks (msg_blocks
, gpgme_ctx
):
241 return [decrypt_block(block
, gpgme_ctx
) for block
in msg_blocks
]
244 def decrypt_block (msg_block
, gpgme_ctx
):
246 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
247 plain_b
= io
.BytesIO()
250 sigs
= gpgme_ctx
.decrypt_verify(block_b
, plain_b
)
254 plaintext
= plain_b
.getvalue().decode('utf-8')
255 return (plaintext
, sigs
)
258 def choose_reply_encryption_key (keys
):
262 if (key
.can_encrypt
== True):
269 def generate_reply (plaintext
, email_from
, email_subject
, encrypt_to_key
,
273 reply
= "To: " + email_from
+ "\n"
274 reply
+= "Subject: " + email_subject
+ "\n"
276 if (encrypt_to_key
!= None):
277 plaintext_reply
= "thanks for the message!\n\n\n"
278 plaintext_reply
+= email_quote_text(plaintext
)
280 # quoted printable encoding lets most ascii characters look normal
281 # before the decrypted mime message is decoded.
282 char_set
= email
.charset
.Charset("utf-8")
283 char_set
.body_encoding
= email
.charset
.QP
285 # MIMEText doesn't allow setting the text encoding
286 # so we use MIMENonMultipart.
287 plaintext_mime
= MIMENonMultipart('text', 'plain')
288 plaintext_mime
.set_payload(plaintext_reply
, charset
=char_set
)
290 encrypted_text
= encrypt_sign_message(plaintext_mime
.as_string(),
294 control_mime
= MIMEApplication("Version: 1",
295 _subtype
='pgp-encrypted',
296 _encoder
=email
.encoders
.encode_7or8bit
)
297 control_mime
['Content-Description'] = 'PGP/MIME version identification'
298 control_mime
.set_charset('us-ascii')
300 encoded_mime
= MIMEApplication(encrypted_text
,
301 _subtype
='octet-stream; name="encrypted.asc"',
302 _encoder
=email
.encoders
.encode_7or8bit
)
303 encoded_mime
['Content-Description'] = 'OpenPGP encrypted message'
304 encoded_mime
['Content-Disposition'] = 'inline; filename="encrypted.asc"'
305 encoded_mime
.set_charset('us-ascii')
307 message_mime
= MIMEMultipart(_subtype
="encrypted", protocol
="application/pgp-encrypted")
308 message_mime
.attach(control_mime
)
309 message_mime
.attach(encoded_mime
)
310 message_mime
['Content-Disposition'] = 'inline'
312 reply
+= message_mime
.as_string()
316 reply
+= "Sorry, i couldn't find your key.\n"
317 reply
+= "I'll need that to encrypt a message to you."
322 def email_quote_text (text
):
324 quoted_message
= re
.sub(r
'^', r
'> ', text
, flags
=re
.MULTILINE
)
326 return quoted_message
329 def encrypt_sign_message (plaintext
, encrypt_to_key
, gpgme_ctx
):
331 plaintext_bytes
= io
.BytesIO(plaintext
.encode('ascii'))
332 encrypted_bytes
= io
.BytesIO()
334 gpgme_ctx
.encrypt_sign([encrypt_to_key
], gpgme
.ENCRYPT_ALWAYS_TRUST
,
335 plaintext_bytes
, encrypted_bytes
)
337 encrypted_txt
= encrypted_bytes
.getvalue().decode('ascii')
341 def error (error_msg
):
343 sys
.stderr
.write(progname
+ ": " + error_msg
+ "\n")
346 def debug (debug_msg
):
348 if edward_config
.debug
== True:
353 if __name__
== "__main__":
356 progname
= sys
.argv
[0]
358 if len(sys
.argv
) > 1:
359 print(progname
+ ": error, this program doesn't " \
360 "need any arguments.", file=sys
.stderr
)