1 #! /usr/bin/env python3
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+) *
21 * Special thanks to Josh Drake for writing the original edward bot! :) *
23 ************************************************************************
27 * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz
43 email_text
= sys
.stdin
.read()
45 email_from
, email_subject
= email_from_subject(email_text
)
47 plaintext
, keys
= email_decode_flatten (email_text
)
48 encrypt_to_key
= choose_reply_encryption_key(keys
)
50 reply_message
= generate_reply(plaintext
, email_from
, \
51 email_subject
, encrypt_to_key
)
56 def email_decode_flatten (email_text
):
61 email_struct
= email
.parser
.Parser().parsestr(email_text
)
63 for subpart
in email_struct
.walk():
65 payload
, description
, filename
, content_type \
66 = get_email_subpart_info(subpart
)
71 if content_type
== "multipart":
74 if content_type
== "application/pgp-encrypted":
75 if description
== "PGP/MIME version identification":
76 if payload
.strip() != "Version: 1":
77 print(progname
+ ": Warning: unknown " \
78 + description
+ ": " \
79 + payload
.strip(), file=sys
.stderr
)
83 if (filename
== "encrypted.asc") or (content_type
== "pgp/mime"):
84 plaintext
, more_keys
= decrypt_text(payload
)
89 elif content_type
== "text/plain":
90 body
+= payload
+ "\n"
93 body
+= payload
+ "\n"
98 def email_from_subject (email_text
):
100 email_struct
= email
.parser
.Parser().parsestr(email_text
)
102 email_from
= email_struct
['From']
103 email_subject
= email_struct
['Subject']
105 return email_from
, email_subject
108 def get_email_subpart_info (part
):
110 charset
= part
.get_content_charset()
111 payload_bytes
= part
.get_payload(decode
=True)
113 filename
= part
.get_filename()
114 content_type
= part
.get_content_type()
115 description_list
= part
.get_params(header
='content-description')
120 if payload_bytes
!= None:
121 payload
= payload_bytes
.decode(charset
)
125 if description_list
!= None:
126 description
= description_list
[0][0]
130 return payload
, description
, filename
, content_type
133 def decrypt_text (gpg_text
):
138 gpg_chunks
= split_message(gpg_text
)
140 plaintext_and_sigs_chunks
= decrypt_chunks(gpg_chunks
)
142 for chunk
in plaintext_and_sigs_chunks
:
147 key
= get_pub_key(sig
)
150 # recursive for nested layers of mime and/or gpg
151 plaintext
, more_keys
= email_decode_flatten(plaintext
)
159 def get_pub_key (sig
):
161 gpgme_ctx
= gpgme
.Context()
163 fingerprint
= sig
.fpr
164 key
= gpgme_ctx
.get_key(fingerprint
)
169 def split_message (text
):
171 gpg_matches
= re
.search( \
172 '(-----BEGIN PGP MESSAGE-----' + \
174 '-----END PGP MESSAGE-----)', \
178 if gpg_matches
!= None:
179 gpg_chunks
= gpg_matches
.groups()
186 def decrypt_chunks (gpg_chunks
):
188 plaintext_and_sigs_chunks
= []
190 for gpg_chunk
in gpg_chunks
:
191 plaintext_and_sigs_chunks
+= [decrypt_chunk(gpg_chunk
)]
193 return plaintext_and_sigs_chunks
196 def decrypt_chunk (gpg_chunk
):
198 gpgme_ctx
= gpgme
.Context()
200 chunk_b
= io
.BytesIO(gpg_chunk
.encode('ASCII'))
201 plain_b
= io
.BytesIO()
203 sigs
= gpgme_ctx
.decrypt_verify(chunk_b
, plain_b
)
205 plaintext
= plain_b
.getvalue().decode('ASCII')
206 return (plaintext
, sigs
)
209 def choose_reply_encryption_key (keys
):
213 if (key
.can_encrypt
== True):
220 def generate_reply (plaintext
, email_from
, email_subject
, encrypt_to_key
):
222 plaintext_reply
= "thanks for the message!\n\n\n"
223 plaintext_reply
+= email_quote_text(plaintext
)
225 encrypted_reply
= encrypt_message(plaintext_reply
, encrypt_to_key
)
227 reply
= "To: " + email_from
+ "\n"
228 reply
+= "Subject: " + email_subject
+ "\n"
230 reply
+= encrypted_reply
235 def email_quote_text (text
):
237 quoted_message
= re
.sub(r
'^', r
'> ', text
, flags
=re
.MULTILINE
)
239 return quoted_message
242 def encrypt_message (plaintext
, encrypt_to_key
):
244 gpgme_ctx
= gpgme
.Context()
245 gpgme_ctx
.armor
= True
247 plaintext_bytes
= io
.BytesIO(plaintext
.encode('UTF-8'))
248 encrypted_bytes
= io
.BytesIO()
250 gpgme_ctx
.encrypt([encrypt_to_key
], gpgme
.ENCRYPT_ALWAYS_TRUST
,
251 plaintext_bytes
, encrypted_bytes
)
253 encrypted_txt
= encrypted_bytes
.getvalue().decode('ASCII')
258 if __name__
== "__main__":
261 progname
= sys
.argv
[0]
263 if len(sys
.argv
) > 1:
264 print(progname
+ ": error, this program doesn't " \
265 "need any arguments.", file=sys
.stderr
)