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
44 from email
.mime
.multipart
import MIMEMultipart
45 from email
.mime
.application
import MIMEApplication
46 from email
.mime
.nonmultipart
import MIMENonMultipart
50 match_types
= [('encrypted',
51 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
53 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
55 '-----END PGP SIGNATURE-----.*?-----BEGIN PGP SIGNATURE-----'),
57 '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
60 class EddyMsg (object):
62 self
.multipart
= False
66 self
.payload_bytes
= None
67 self
.payload_pieces
= []
70 self
.content_type
= None
71 self
.description_list
= None
74 class PayloadPiece (object):
76 self
.piece_type
= None
85 gpgme_ctx
= get_gpg_context(edward_config
.gnupghome
,
86 edward_config
.sign_with_key
)
88 email_text
= sys
.stdin
.read()
89 result
= parse_pgp_mime(email_text
)
91 email_from
, email_subject
= email_from_subject(email_text
)
93 # plaintext, fingerprints = email_decode_flatten(email_text, gpgme_ctx, False)
94 # encrypt_to_key = choose_reply_encryption_key(gpgme_ctx, fingerprints)
96 # reply_message = generate_reply(plaintext, email_from, \
97 # email_subject, encrypt_to_key,
100 print(flatten_eddy(result
))
103 def get_gpg_context (gnupghome
, sign_with_key_fp
):
105 os
.environ
['GNUPGHOME'] = gnupghome
107 gpgme_ctx
= gpgme
.Context()
108 gpgme_ctx
.armor
= True
111 sign_with_key
= gpgme_ctx
.get_key(sign_with_key_fp
)
113 error("unable to load signing key. is the gnupghome "
114 + "and signing key properly set in the edward_config.py?")
117 gpgme_ctx
.signers
= [sign_with_key
]
122 def parse_pgp_mime (email_text
):
124 email_struct
= email
.parser
.Parser().parsestr(email_text
)
126 eddy_obj
= parse_mime(email_struct
)
127 eddy_obj
= split_payloads(eddy_obj
)
132 def parse_mime(msg_struct
):
136 if msg_struct
.is_multipart() == True:
137 payloads
= msg_struct
.get_payload()
139 eddy_obj
.multipart
= True
140 eddy_obj
.subparts
= map(parse_mime
, payloads
)
142 eddy_obj
= get_subpart_data(msg_struct
)
147 def split_payloads (eddy_obj
):
149 if eddy_obj
.multipart
== True:
150 eddy_obj
.subparts
= map(split_payloads
, eddy_obj
.subparts
)
153 for (match_type
, pattern
) in match_types
:
156 for payload_piece
in eddy_obj
.payload_pieces
:
157 new_pieces_list
+= scan_and_split(payload_piece
,
159 eddy_obj
.payload_pieces
= new_pieces_list
164 def scan_and_split (payload_piece
, match_type
, pattern
):
166 flags
= re
.DOTALL | re
.MULTILINE
167 matches
= re
.search("(?P<beginning>.*?)(?P<match>" + pattern
+
168 ")(?P<rest>.*)", payload_piece
.string
, flags
=flags
)
171 pieces
= [payload_piece
]
175 beginning
= PayloadPiece()
176 beginning
.string
= matches
.group('beginning')
177 beginning
.piece_type
= payload_piece
.piece_type
179 match
= PayloadPiece()
180 match
.string
= matches
.group('match')
181 match
.piece_type
= match_type
183 rest
= PayloadPiece()
184 rest
.string
= matches
.group('rest')
185 rest
.piece_type
= payload_piece
.piece_type
187 more_pieces
= scan_and_split(rest
, match_type
, pattern
)
189 if more_pieces
== None:
190 pieces
= [beginning
, match
, rest
]
192 pieces
= [beginning
, match
] + more_pieces
197 def get_subpart_data (part
):
201 obj
.charset
= part
.get_content_charset()
202 obj
.payload_bytes
= part
.get_payload(decode
=True)
204 obj
.filename
= part
.get_filename()
205 obj
.content_type
= part
.get_content_type()
206 obj
.description_list
= part
['content-description']
208 # your guess is as good as a-myy-ee-ine...
209 if obj
.charset
== None:
210 obj
.charset
= 'utf-8'
212 if obj
.payload_bytes
!= None:
213 # this belongs in a specific try statement.
214 payload
= PayloadPiece()
215 payload
.string
= obj
.payload_bytes
.decode(obj
.charset
)
216 payload
.piece_type
= 'text'
218 obj
.payload_pieces
= [payload
]
223 def flatten_eddy (eddy_obj
):
225 if eddy_obj
.multipart
== True:
226 string
= "\n".join(map(flatten_eddy
, eddy_obj
.subparts
))
228 string
= flatten_payload_piece(eddy_obj
.payload_pieces
)
233 def flatten_payload_piece (payload_pieces
):
236 for piece
in payload_pieces
:
237 string
+= piece
.string
242 def email_from_subject (email_text
):
244 email_struct
= email
.parser
.Parser().parsestr(email_text
)
246 email_from
= email_struct
['From']
247 email_subject
= email_struct
['Subject']
249 return email_from
, email_subject
252 def add_gpg_keys (text
, gpgme_ctx
):
254 key_blocks
= scan_and_grab(text
,
255 '-----BEGIN PGP PUBLIC KEY BLOCK-----',
256 '-----END PGP PUBLIC KEY BLOCK-----')
259 for key_block
in key_blocks
:
260 fp
= io
.BytesIO(key_block
.encode('ascii'))
262 result
= gpgme_ctx
.import_(fp
)
263 imports
= result
.imports
266 fingerprint
= imports
[0][0]
267 fingerprints
+= [fingerprint
]
269 debug("added gpg key: " + fingerprint
)
274 def decrypt_text (gpg_text
, gpgme_ctx
):
279 msg_blocks
= scan_and_grab(gpg_text
,
280 '-----BEGIN PGP MESSAGE-----',
281 '-----END PGP MESSAGE-----')
283 plaintexts_and_sigs
= decrypt_blocks(msg_blocks
, gpgme_ctx
)
285 for pair
in plaintexts_and_sigs
:
290 fingerprints
+= [sig
.fpr
]
292 # recursive for nested layers of mime and/or gpg
293 plaintext
, more_fps
= email_decode_flatten(plaintext
, gpgme_ctx
, True)
296 fingerprints
+= more_fps
298 return body
, fingerprints
301 def verify_clear_signature (text
, gpgme_ctx
):
303 sig_blocks
= scan_and_grab(text
,
304 '-----BEGIN PGP SIGNED MESSAGE-----',
305 '-----END PGP SIGNATURE-----')
310 for sig_block
in sig_blocks
:
311 msg_fp
= io
.BytesIO(sig_block
.encode('utf-8'))
312 ptxt_fp
= io
.BytesIO()
314 result
= gpgme_ctx
.verify(msg_fp
, None, ptxt_fp
)
316 plaintext
+= ptxt_fp
.getvalue().decode('utf-8')
317 fingerprint
= result
[0].fpr
319 fingerprints
+= [fingerprint
]
321 return plaintext
, fingerprints
324 def scan_and_grab (text
, start_text
, end_text
):
326 matches
= re
.search('(' + start_text
+ '.*' + end_text
+ ')',
327 text
, flags
=re
.DOTALL
)
330 match_tuple
= matches
.groups()
337 def decrypt_blocks (msg_blocks
, gpgme_ctx
):
339 return [decrypt_block(block
, gpgme_ctx
) for block
in msg_blocks
]
342 def decrypt_block (msg_block
, gpgme_ctx
):
344 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
345 plain_b
= io
.BytesIO()
348 sigs
= gpgme_ctx
.decrypt_verify(block_b
, plain_b
)
352 plaintext
= plain_b
.getvalue().decode('utf-8')
353 return (plaintext
, sigs
)
356 def choose_reply_encryption_key (gpgme_ctx
, fingerprints
):
359 for fp
in fingerprints
:
361 key
= gpgme_ctx
.get_key(fp
)
363 if (key
.can_encrypt
== True):
373 def generate_reply (plaintext
, email_from
, email_subject
, encrypt_to_key
,
377 reply
= "To: " + email_from
+ "\n"
378 reply
+= "Subject: " + email_subject
+ "\n"
380 if (encrypt_to_key
!= None):
381 plaintext_reply
= "thanks for the message!\n\n\n"
382 plaintext_reply
+= email_quote_text(plaintext
)
384 # quoted printable encoding lets most ascii characters look normal
385 # before the decrypted mime message is decoded.
386 char_set
= email
.charset
.Charset("utf-8")
387 char_set
.body_encoding
= email
.charset
.QP
389 # MIMEText doesn't allow setting the text encoding
390 # so we use MIMENonMultipart.
391 plaintext_mime
= MIMENonMultipart('text', 'plain')
392 plaintext_mime
.set_payload(plaintext_reply
, charset
=char_set
)
394 encrypted_text
= encrypt_sign_message(plaintext_mime
.as_string(),
398 control_mime
= MIMEApplication("Version: 1",
399 _subtype
='pgp-encrypted',
400 _encoder
=email
.encoders
.encode_7or8bit
)
401 control_mime
['Content-Description'] = 'PGP/MIME version identification'
402 control_mime
.set_charset('us-ascii')
404 encoded_mime
= MIMEApplication(encrypted_text
,
405 _subtype
='octet-stream; name="encrypted.asc"',
406 _encoder
=email
.encoders
.encode_7or8bit
)
407 encoded_mime
['Content-Description'] = 'OpenPGP encrypted message'
408 encoded_mime
['Content-Disposition'] = 'inline; filename="encrypted.asc"'
409 encoded_mime
.set_charset('us-ascii')
411 message_mime
= MIMEMultipart(_subtype
="encrypted", protocol
="application/pgp-encrypted")
412 message_mime
.attach(control_mime
)
413 message_mime
.attach(encoded_mime
)
414 message_mime
['Content-Disposition'] = 'inline'
416 reply
+= message_mime
.as_string()
420 reply
+= "Sorry, i couldn't find your key.\n"
421 reply
+= "I'll need that to encrypt a message to you."
426 def email_quote_text (text
):
428 quoted_message
= re
.sub(r
'^', r
'> ', text
, flags
=re
.MULTILINE
)
430 return quoted_message
433 def encrypt_sign_message (plaintext
, encrypt_to_key
, gpgme_ctx
):
435 plaintext_bytes
= io
.BytesIO(plaintext
.encode('ascii'))
436 encrypted_bytes
= io
.BytesIO()
438 gpgme_ctx
.encrypt_sign([encrypt_to_key
], gpgme
.ENCRYPT_ALWAYS_TRUST
,
439 plaintext_bytes
, encrypted_bytes
)
441 encrypted_txt
= encrypted_bytes
.getvalue().decode('ascii')
445 def error (error_msg
):
447 sys
.stderr
.write(progname
+ ": " + error_msg
+ "\n")
450 def debug (debug_msg
):
452 if edward_config
.debug
== True:
457 if __name__
== "__main__":
460 progname
= sys
.argv
[0]
462 if len(sys
.argv
) > 1:
463 print(progname
+ ": error, this program doesn't " \
464 "need any arguments.", file=sys
.stderr
)