Commit | Line | Data |
---|---|---|
0bec96d6 | 1 | #! /usr/bin/env python3 |
ff4136c7 | 2 | # -*- coding: utf-8 -*- |
0bec96d6 AE |
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. * | |
8 | * * | |
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. * | |
13 | * * | |
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/>. * | |
16 | * * | |
17 | * Copyright (C) 2014-2015 Andrew Engelbrecht (AGPLv3+) * | |
18 | * Copyright (C) 2014 Josh Drake (AGPLv3+) * | |
19 | * Copyright (C) 2014 Lisa Marie Maginnis (AGPLv3+) * | |
8bdfb6d4 AE |
20 | * Copyright (C) 2009-2015 Tails developers <tails@boum.org> ( GPLv3+) * |
21 | * Copyright (C) 2009 W. Trevor King <wking@drexel.edu> ( GPLv2+) * | |
0bec96d6 AE |
22 | * * |
23 | * Special thanks to Josh Drake for writing the original edward bot! :) * | |
24 | * * | |
25 | ************************************************************************ | |
26 | ||
a5385c04 | 27 | Code sourced from these projects: |
0bec96d6 | 28 | |
dfe75896 | 29 | * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/PREVIOUS-20150530/edward.tar.gz |
8bdfb6d4 AE |
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 | |
0bec96d6 AE |
32 | """ |
33 | ||
0bec96d6 AE |
34 | import re |
35 | import io | |
40c37ab3 | 36 | import os |
2f4ce2b2 | 37 | import sys |
0fd3389f | 38 | import enum |
2f4ce2b2 | 39 | import gpgme |
a9eaa792 | 40 | import smtplib |
adcef2f7 | 41 | import importlib |
0bec96d6 | 42 | |
8bdfb6d4 AE |
43 | import email.parser |
44 | import email.message | |
45 | import email.encoders | |
46 | ||
e5dd6f23 | 47 | from email.mime.text import MIMEText |
8bdfb6d4 AE |
48 | from email.mime.multipart import MIMEMultipart |
49 | from email.mime.application import MIMEApplication | |
50 | from email.mime.nonmultipart import MIMENonMultipart | |
51 | ||
40c37ab3 | 52 | import edward_config |
c96f3837 | 53 | |
c68461f1 | 54 | langs = ["de", "el", "en", "es", "fr", "it", "ja", "pt-br", "ro", "ru", "tr", "zh"] |
adcef2f7 | 55 | |
def9196c AE |
56 | """This list contains the abbreviated names of reply languages available to |
57 | edward.""" | |
58 | ||
0fd3389f AE |
59 | class TxtType (enum.Enum): |
60 | text = 0 | |
61 | message = 1 | |
62 | pubkey = 2 | |
63 | detachedsig = 3 | |
64 | signature = 4 | |
def9196c | 65 | |
0fd3389f | 66 | |
2f4102b9 | 67 | match_pairs = [(TxtType.message, |
56578eaf | 68 | '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'), |
0fd3389f | 69 | (TxtType.pubkey, |
56578eaf | 70 | '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'), |
0fd3389f | 71 | (TxtType.detachedsig, |
38738401 | 72 | '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')] |
56578eaf | 73 | |
def9196c AE |
74 | """This list of tuples matches query names with re.search() queries used |
75 | to find GPG data for edward to process.""" | |
76 | ||
56578eaf AE |
77 | |
78 | class EddyMsg (object): | |
def9196c AE |
79 | """ |
80 | The EddyMsg class represents relevant parts of a mime message. | |
81 | ||
82 | The represented message can be single-part or multi-part. | |
83 | ||
84 | 'multipart' is set to True if there are multiple mime parts. | |
85 | ||
86 | 'subparts' points to a list of mime sub-parts if it is a multi-part | |
87 | message. Otherwise it points to an empty list. | |
88 | ||
6d8faaa7 AE |
89 | 'payload_bytes' is a binary representation of the mime part before header |
90 | removal and message decoding. | |
def9196c AE |
91 | |
92 | 'payload_pieces' is a list of objects containing strings that when strung | |
93 | together form the fully-decoded string representation of the mime part. | |
94 | ||
c00930e9 AE |
95 | The 'filename', 'content_type', 'content_disposition' and |
96 | 'description_list' come from the mime part parameters. | |
97 | ||
def9196c AE |
98 | """ |
99 | ||
7d0b91d2 AE |
100 | multipart = False |
101 | subparts = [] | |
56578eaf | 102 | |
7d0b91d2 AE |
103 | payload_bytes = None |
104 | payload_pieces = [] | |
56578eaf | 105 | |
7d0b91d2 AE |
106 | filename = None |
107 | content_type = None | |
c00930e9 | 108 | content_disposition = None |
7d0b91d2 | 109 | description_list = None |
56578eaf AE |
110 | |
111 | ||
112 | class PayloadPiece (object): | |
def9196c AE |
113 | """ |
114 | PayloadPiece represents a complte or sub-section of a mime part. | |
115 | ||
116 | Instances of this class are often strung together within one or more arrays | |
117 | pointed to by each instance of the EddyMsg class. | |
118 | ||
0fd3389f AE |
119 | 'piece_type' refers to an enum whose value describes the content of |
120 | 'string'. Examples include TxtType.pubkey, for public keys, and | |
121 | TxtType.message, for encrypted data (or armored signatures until they are | |
122 | known to be such.) Some of the names derive from the header and footer of | |
123 | each of these ascii-encoded gpg blocks. | |
def9196c AE |
124 | |
125 | 'string' contains some string of text, such as non-GPG text, an encrypted | |
126 | block of text, a signature, or a public key. | |
127 | ||
128 | 'gpg_data' points to any instances of GPGData that have been created based | |
129 | on the contents of 'string'. | |
130 | """ | |
131 | ||
7d0b91d2 AE |
132 | piece_type = None |
133 | string = None | |
134 | gpg_data = None | |
56578eaf AE |
135 | |
136 | ||
38738401 | 137 | class GPGData (object): |
def9196c AE |
138 | """ |
139 | GPGData holds info from decryption, sig. verification, and/or pub. keys. | |
140 | ||
141 | Instances of this class contain decrypted information, signature | |
142 | fingerprints and/or fingerprints of processed and imported public keys. | |
143 | ||
144 | 'decrypted' is set to True if 'plainobj' was created from encrypted data. | |
145 | ||
146 | 'plainobj' points to any decrypted, or signed part of, a GPG signature. It | |
147 | is intended to be an instance of the EddyMsg class. | |
148 | ||
149 | 'sigs' is a list of fingerprints of keys used to sign the data in plainobj. | |
150 | ||
b4c116d5 AE |
151 | 'sigkey_missing' is set to True if edward doesn't have the key it needs to |
152 | verify the signature on a block of text. | |
9af26cb8 | 153 | |
c035a008 | 154 | 'key_cannot_encrypt' is set to True if pubkeys or sigs' keys in the payload |
e6d33a68 | 155 | piece are not capable of encryption, are revoked or expired, for instance. |
c035a008 | 156 | |
def9196c AE |
157 | 'keys' is a list of fingerprints of keys obtained in public key blocks. |
158 | """ | |
159 | ||
7d0b91d2 AE |
160 | decrypted = False |
161 | ||
162 | plainobj = None | |
163 | sigs = [] | |
b4c116d5 | 164 | sigkey_missing = False |
c035a008 | 165 | key_cannot_encrypt = False |
7d0b91d2 | 166 | keys = [] |
38738401 | 167 | |
38738401 | 168 | |
d873ff48 | 169 | class ReplyInfo (object): |
def9196c AE |
170 | """ |
171 | ReplyInfo contains details that edward uses in generating its reply. | |
172 | ||
173 | Instances of this class contain information about whether a message was | |
174 | successfully encrypted or signed, and whether a public key was attached, or | |
175 | retrievable, from the local GPG store. It stores the fingerprints of | |
176 | potential encryption key candidates and the message (if any at all) to | |
177 | quote in edward's reply. | |
178 | ||
179 | 'replies' points one of the dictionaries of translated replies. | |
180 | ||
181 | 'target_key' refers to the fingerprint of a key used to sign encrypted | |
182 | data. This is the preferred key, if it is set, and if is available. | |
183 | ||
184 | 'fallback_target_key' referst to the fingerprint of a key used to sign | |
185 | unencrypted data; alternatively it may be a public key attached to the | |
186 | message. | |
187 | ||
ca37d0cb AE |
188 | 'encrypt_to_key' the key object to use when encrypting edward's reply |
189 | ||
def9196c AE |
190 | 'msg_to_quote' refers to the part of a message which edward should quote in |
191 | his reply. This should remain as None if there was no encrypted and singed | |
192 | part. This is to avoid making edward a service for decrypting other | |
193 | people's messages to edward. | |
194 | ||
b4c116d5 | 195 | 'decrypt_success' is set to True if edward could decrypt part of the |
def9196c AE |
196 | message. |
197 | ||
def9196c AE |
198 | 'sig_success' is set to True if edward could to some extent verify the |
199 | signature of a signed part of the message to edward. | |
200 | ||
c035a008 AE |
201 | 'key_can_encrypt' is set to True if a key which can be encrypted to has |
202 | been found. | |
203 | ||
32a8996f AE |
204 | 'sig_failure' is set to True if edward could not verify a siganture. |
205 | ||
b4c116d5 AE |
206 | 'pubkey_success' is set to True if edward successfully imported a public |
207 | key. | |
208 | ||
209 | 'sigkey_missing' is set to True if edward doesn't have the public key | |
210 | needed for signature verification. | |
211 | ||
c035a008 AE |
212 | 'key_cannot_encrypt' is set to True if pubkeys or sig's keys in a payload |
213 | piece of the message are not capable of encryption. | |
214 | ||
b4c116d5 AE |
215 | 'have_repy_key' is set to True if edward has a public key to encrypt its |
216 | reply to. | |
def9196c AE |
217 | """ |
218 | ||
7d0b91d2 | 219 | replies = None |
6906d46d | 220 | |
7d0b91d2 AE |
221 | target_key = None |
222 | fallback_target_key = None | |
ca37d0cb | 223 | encrypt_to_key = None |
7d0b91d2 | 224 | msg_to_quote = "" |
d873ff48 | 225 | |
b4c116d5 | 226 | decrypt_success = False |
7d0b91d2 | 227 | sig_success = False |
b4c116d5 | 228 | pubkey_success = False |
c035a008 | 229 | key_can_encrypt = False |
b4c116d5 | 230 | |
4e2c66f7 | 231 | decrypt_failure = False |
32a8996f | 232 | sig_failure = False |
b4c116d5 | 233 | sigkey_missing = False |
c035a008 | 234 | key_cannot_encrypt = False |
371911ad | 235 | |
b4c116d5 | 236 | have_reply_key = False |
d873ff48 | 237 | |
38738401 | 238 | |
0bec96d6 AE |
239 | def main (): |
240 | ||
def9196c AE |
241 | """ |
242 | This is the main function for edward, a GPG reply bot. | |
243 | ||
244 | Edward responds to GPG-encrypted and signed mail, encrypting and signing | |
245 | the response if the user's public key is, or was, included in the message. | |
246 | ||
247 | Args: | |
248 | None | |
249 | ||
250 | Returns: | |
a3d6aff8 | 251 | Nothing |
def9196c AE |
252 | |
253 | Pre: | |
254 | Mime or plaintext email passing in through standard input. Portions of | |
255 | the email may be encrypted. If the To: address contains the text | |
256 | "edward-ja", then the reply will contain a reply written in the | |
257 | Japanese language. There are other languages as well. The default | |
258 | language is English. | |
259 | ||
260 | Post: | |
261 | A reply email will be printed to standard output. The contents of the | |
262 | reply email depends on whether the original email was encrypted or not, | |
263 | has or doesn't have a signature, whether a public key used in the | |
264 | original message is provided or locally stored, and the language | |
265 | implied by the To: address in the original email. | |
266 | """ | |
267 | ||
60ccbaf4 | 268 | print_reply_only = handle_args() |
0bec96d6 | 269 | |
7cc9808b AE |
270 | email_bytes = sys.stdin.buffer.read() |
271 | ||
272 | test_auto_reply(email_bytes) | |
273 | ||
0a064403 AE |
274 | gpgme_ctx = get_gpg_context(edward_config.gnupghome, |
275 | edward_config.sign_with_key) | |
276 | ||
70da0811 | 277 | # do this twice so sigs can be verified with newly imported keys |
b93c3c29 | 278 | parse_pgp_mime(email_bytes, gpgme_ctx) |
85a829fc | 279 | email_struct = parse_pgp_mime(email_bytes, gpgme_ctx) |
adcef2f7 | 280 | |
140c47ca | 281 | email_to, email_reply_to, email_subject = email_to_reply_to_subject(email_bytes) |
60ccbaf4 | 282 | lang, reply_from = import_lang_pick_address(email_to, edward_config.hostname) |
fafa21c3 | 283 | |
d873ff48 AE |
284 | replyinfo_obj = ReplyInfo() |
285 | replyinfo_obj.replies = lang.replies | |
286 | ||
287 | prepare_for_reply(email_struct, replyinfo_obj) | |
ca37d0cb | 288 | get_key_from_fp(replyinfo_obj, gpgme_ctx) |
d873ff48 | 289 | reply_plaintext = write_reply(replyinfo_obj) |
adcef2f7 | 290 | |
2d7bc2a9 | 291 | reply_mime = generate_encrypted_mime(reply_plaintext, email_reply_to, reply_from, \ |
ca37d0cb | 292 | email_subject, replyinfo_obj.encrypt_to_key, |
2007103e | 293 | gpgme_ctx) |
1fccb295 | 294 | |
60ccbaf4 AE |
295 | if print_reply_only == True: |
296 | print(reply_mime) | |
297 | else: | |
140c47ca | 298 | send_reply(reply_mime, email_reply_to, reply_from) |
c96f3837 | 299 | |
0bec96d6 | 300 | |
0a064403 | 301 | def get_gpg_context (gnupghome, sign_with_key_fp): |
a3d6aff8 AE |
302 | """ |
303 | This function returns the GPG context needed for encryption and signing. | |
304 | ||
305 | The context is needed by other functions which use GPG functionality. | |
306 | ||
307 | Args: | |
308 | gnupghome: The path to "~/.gnupg/" or its alternative. | |
309 | sign_with_key: The fingerprint of the key to sign with | |
310 | ||
311 | Returns: | |
312 | A gpgme context to be used for GPG functions. | |
313 | ||
314 | Post: | |
315 | the 'armor' flag is set to True and the list of signing keys contains | |
316 | the single specified key | |
317 | """ | |
0a064403 AE |
318 | |
319 | os.environ['GNUPGHOME'] = gnupghome | |
320 | ||
321 | gpgme_ctx = gpgme.Context() | |
322 | gpgme_ctx.armor = True | |
323 | ||
324 | try: | |
325 | sign_with_key = gpgme_ctx.get_key(sign_with_key_fp) | |
5f6f32b1 | 326 | except gpgme.GpgmeError: |
0a064403 AE |
327 | error("unable to load signing key. is the gnupghome " |
328 | + "and signing key properly set in the edward_config.py?") | |
329 | exit(1) | |
330 | ||
331 | gpgme_ctx.signers = [sign_with_key] | |
332 | ||
333 | return gpgme_ctx | |
334 | ||
335 | ||
85a829fc | 336 | def parse_pgp_mime (email_bytes, gpgme_ctx): |
a3d6aff8 AE |
337 | """Parses the email for mime payloads and decrypts/verfies signatures. |
338 | ||
339 | This function creates a representation of a mime or plaintext email with | |
340 | the EddyMsg class. It then splits each mime payload into one or more pieces | |
341 | which may be plain text or GPG data. It then decrypts encrypted parts and | |
342 | does some very basic signature verification on those parts. | |
343 | ||
344 | Args: | |
85a829fc | 345 | email_bytes: an email message in byte string format |
a3d6aff8 AE |
346 | gpgme_ctx: a gpgme context |
347 | ||
348 | Returns: | |
349 | A message as an instance of EddyMsg | |
350 | ||
351 | Post: | |
352 | the returned EddyMsg instance has split, decrypted, verified and pubkey | |
353 | imported payloads | |
354 | """ | |
394a1476 | 355 | |
85a829fc | 356 | email_struct = email.parser.BytesParser().parsebytes(email_bytes) |
394a1476 | 357 | |
928e3819 AE |
358 | eddymsg_obj = parse_mime(email_struct) |
359 | split_payloads(eddymsg_obj) | |
360 | gpg_on_payloads(eddymsg_obj, gpgme_ctx) | |
8bb4b0d5 | 361 | |
928e3819 | 362 | return eddymsg_obj |
0bec96d6 | 363 | |
0bec96d6 | 364 | |
56578eaf | 365 | def parse_mime(msg_struct): |
a3d6aff8 AE |
366 | """Translates python's email.parser format into an EddyMsg format |
367 | ||
368 | If the message is multi-part, then a recursive object is created, where | |
369 | each sub-part is also a EddyMsg instance. | |
370 | ||
371 | Args: | |
3dad6a92 | 372 | msg_struct: an email parsed with email.parser.BytesParser(), which can be |
a3d6aff8 AE |
373 | multi-part |
374 | ||
375 | Returns: | |
376 | an instance of EddyMsg, potentially a recursive one. | |
377 | """ | |
0bec96d6 | 378 | |
158ddbca | 379 | eddymsg_obj = get_subpart_data(msg_struct) |
8bb4b0d5 | 380 | |
56578eaf AE |
381 | if msg_struct.is_multipart() == True: |
382 | payloads = msg_struct.get_payload() | |
0bec96d6 | 383 | |
928e3819 AE |
384 | eddymsg_obj.multipart = True |
385 | eddymsg_obj.subparts = list(map(parse_mime, payloads)) | |
dd11a483 | 386 | |
928e3819 | 387 | return eddymsg_obj |
c267c233 | 388 | |
80119cab | 389 | |
2f4102b9 | 390 | def scan_and_split (payload_piece, match_name, pattern): |
a3d6aff8 AE |
391 | """This splits the payloads of an EddyMsg object into GPG and text parts. |
392 | ||
393 | An EddyMsg object's payload_pieces starts off as a list containing a single | |
394 | PayloadPiece object. This function returns a list of these objects which | |
395 | have been split into GPG data and regular text, if such splits need to be/ | |
396 | can be made. | |
397 | ||
398 | Args: | |
399 | payload_piece: a single payload or a split part of a payload | |
2f4102b9 | 400 | match_name: the type of data to try to spit out from the payload piece |
a3d6aff8 AE |
401 | pattern: the search pattern to be used for finding that type of data |
402 | ||
403 | Returns: | |
404 | a list of objects of the PayloadPiece class, in the order that the | |
405 | string part of payload_piece originally was, broken up according to | |
406 | matches specified by 'pattern'. | |
407 | """ | |
cf75de65 | 408 | |
a5d37d44 | 409 | # don't try to re-split pieces containing gpg data |
0fd3389f | 410 | if payload_piece.piece_type != TxtType.text: |
a5d37d44 AE |
411 | return [payload_piece] |
412 | ||
56578eaf | 413 | flags = re.DOTALL | re.MULTILINE |
b8a80a18 | 414 | matches = re.search(pattern, payload_piece.string, flags=flags) |
86663388 | 415 | |
56578eaf AE |
416 | if matches == None: |
417 | pieces = [payload_piece] | |
c96f3837 | 418 | |
56578eaf | 419 | else: |
d437f8b2 | 420 | |
56578eaf | 421 | beginning = PayloadPiece() |
b8a80a18 | 422 | beginning.string = payload_piece.string[:matches.start()] |
56578eaf | 423 | beginning.piece_type = payload_piece.piece_type |
d437f8b2 | 424 | |
56578eaf | 425 | match = PayloadPiece() |
b8a80a18 | 426 | match.string = payload_piece.string[matches.start():matches.end()] |
2f4102b9 | 427 | match.piece_type = match_name |
d437f8b2 | 428 | |
56578eaf | 429 | rest = PayloadPiece() |
b8a80a18 | 430 | rest.string = payload_piece.string[matches.end():] |
56578eaf | 431 | rest.piece_type = payload_piece.piece_type |
d437f8b2 | 432 | |
2f4102b9 | 433 | more_pieces = scan_and_split(rest, match_name, pattern) |
4615b156 | 434 | pieces = [beginning, match ] + more_pieces |
d437f8b2 | 435 | |
56578eaf | 436 | return pieces |
d437f8b2 | 437 | |
d437f8b2 | 438 | |
56578eaf | 439 | def get_subpart_data (part): |
a32a1e11 | 440 | """This function grabs information from a mime part. |
a3d6aff8 | 441 | |
3dad6a92 | 442 | It copies needed data from an email.parser.BytesParser() object over to an |
a32a1e11 | 443 | EddyMsg object. |
a3d6aff8 AE |
444 | |
445 | Args: | |
3dad6a92 | 446 | part: an email.parser.BytesParser() object |
a3d6aff8 AE |
447 | |
448 | Returns: | |
a32a1e11 | 449 | an EddyMsg() object |
a3d6aff8 | 450 | """ |
0bec96d6 | 451 | |
6d8faaa7 | 452 | obj = EddyMsg() |
56578eaf | 453 | |
84ff9773 AE |
454 | mime_decoded_bytes = part.get_payload(decode=True) |
455 | charset = part.get_content_charset() | |
56578eaf AE |
456 | |
457 | # your guess is as good as a-myy-ee-ine... | |
6d8faaa7 AE |
458 | if charset == None: |
459 | charset = 'utf-8' | |
56578eaf | 460 | |
84ff9773 AE |
461 | payload_string = part.as_string() |
462 | if payload_string != None: | |
9c348860 AE |
463 | # convert each isolated carriage return or line feed to carriage return + line feed |
464 | payload_string_crlf = re.sub(r'\n', '\r\n', re.sub(r'\r', '\n', re.sub(r'\r\n', '\n', payload_string))) | |
465 | obj.payload_bytes = payload_string_crlf.encode(charset) | |
84ff9773 AE |
466 | |
467 | obj.filename = part.get_filename() | |
468 | obj.content_type = part.get_content_type() | |
c00930e9 | 469 | obj.content_disposition = part['content-disposition'] |
84ff9773 AE |
470 | obj.description_list = part['content-description'] |
471 | ||
6d8faaa7 | 472 | if mime_decoded_bytes != None: |
0eb75d9c AE |
473 | try: |
474 | payload = PayloadPiece() | |
6d8faaa7 | 475 | payload.string = mime_decoded_bytes.decode(charset) |
0fd3389f | 476 | payload.piece_type = TxtType.text |
0eb75d9c AE |
477 | |
478 | obj.payload_pieces = [payload] | |
479 | except UnicodeDecodeError: | |
480 | pass | |
56578eaf AE |
481 | |
482 | return obj | |
483 | ||
484 | ||
928e3819 | 485 | def do_to_eddys_pieces (function_to_do, eddymsg_obj, data): |
a3d6aff8 AE |
486 | """A function which maps another function onto a message's subparts. |
487 | ||
488 | This is a higer-order function which recursively performs a specified | |
489 | function on each subpart of a multi-part message. Each single-part sub-part | |
490 | has the function applied to it. This function also works if the part passed | |
491 | in is single-part. | |
492 | ||
493 | Args: | |
494 | function_to_do: function to perform on sub-parts | |
495 | eddymsg_obj: a single part or multi-part EddyMsg object | |
496 | data: a second argument to pass to 'function_to_do' | |
497 | ||
498 | Returns: | |
499 | Nothing | |
500 | ||
501 | Post: | |
502 | The passed-in EddyMsg object is transformed recursively on its | |
503 | sub-parts according to 'function_to_do'. | |
504 | """ | |
56578eaf | 505 | |
928e3819 AE |
506 | if eddymsg_obj.multipart == True: |
507 | for sub in eddymsg_obj.subparts: | |
d873ff48 | 508 | do_to_eddys_pieces(function_to_do, sub, data) |
394a1476 | 509 | else: |
928e3819 | 510 | function_to_do(eddymsg_obj, data) |
dd11a483 AE |
511 | |
512 | ||
928e3819 | 513 | def split_payloads (eddymsg_obj): |
a3d6aff8 AE |
514 | """Splits all (sub-)payloads of a message into GPG data and regular text. |
515 | ||
516 | Recursively performs payload splitting on all sub-parts of an EddyMsg | |
517 | object into the various GPG data types, such as GPG messages, public key | |
518 | blocks and signed text. | |
519 | ||
520 | Args: | |
521 | eddymsg_obj: an instance of EddyMsg | |
522 | ||
523 | Returns: | |
524 | Nothing | |
525 | ||
526 | Pre: | |
527 | The EddyMsg object has payloads that are unsplit (by may be split).. | |
528 | ||
529 | Post: | |
530 | The EddyMsg object's payloads are all split into GPG and non-GPG parts. | |
531 | """ | |
a5d37d44 | 532 | |
2f4102b9 AE |
533 | for match_pair in match_pairs: |
534 | do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_pair) | |
a5d37d44 | 535 | |
a5d37d44 | 536 | |
2f4102b9 | 537 | def split_payload_pieces (eddymsg_obj, match_pair): |
a3d6aff8 AE |
538 | """A helper function for split_payloads(); works on PayloadPiece objects. |
539 | ||
540 | This function splits up PayloadPiece objects into multipe PayloadPiece | |
541 | objects and replaces the EddyMsg object's previous list of payload pieces | |
542 | with the new split up one. | |
543 | ||
544 | Args: | |
545 | eddymsg_obj: a single-part EddyMsg object. | |
2f4102b9 | 546 | match_pair: a tuple from the match_pairs list, which specifies a match |
a3d6aff8 AE |
547 | name and a match pattern. |
548 | ||
549 | Returns: | |
550 | Nothing | |
551 | ||
552 | Pre: | |
553 | The payload piece(s) of an EddyMsg object may be already split or | |
554 | unsplit. | |
555 | ||
556 | Post: | |
557 | The EddyMsg object's payload piece(s) are split into a list of pieces | |
2f4102b9 | 558 | if matches of the match_pair are found. |
a3d6aff8 | 559 | """ |
a5d37d44 | 560 | |
2f4102b9 | 561 | (match_name, pattern) = match_pair |
a5d37d44 AE |
562 | |
563 | new_pieces_list = [] | |
928e3819 | 564 | for piece in eddymsg_obj.payload_pieces: |
a5d37d44 AE |
565 | new_pieces_list += scan_and_split(piece, match_name, pattern) |
566 | ||
928e3819 | 567 | eddymsg_obj.payload_pieces = new_pieces_list |
a5d37d44 AE |
568 | |
569 | ||
928e3819 | 570 | def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): |
a3d6aff8 AE |
571 | """Performs GPG operations on the GPG parts of the message |
572 | ||
573 | This function decrypts text, verifies signatures, and imports public keys | |
574 | included in an email. | |
575 | ||
576 | Args: | |
577 | eddymsg_obj: an EddyMsg object with its payload_pieces split into GPG | |
578 | and non-GPG sections by split_payloads() | |
579 | gpgme_ctx: a gpgme context | |
580 | ||
581 | prev_parts: a list of mime parts that occur before the eddymsg_obj | |
582 | part, under the same multi-part mime part. This is used for | |
583 | verifying detached signatures. For the root mime part, this should | |
584 | be an empty list, which is the default value if this paramater is | |
585 | omitted. | |
586 | ||
587 | Return: | |
588 | Nothing | |
589 | ||
590 | Pre: | |
591 | eddymsg_obj should have its payloads split into gpg and non-gpg pieces. | |
592 | ||
593 | Post: | |
c035a008 AE |
594 | Decryption, verification and key imports occur. the gpg_data members of |
595 | PayloadPiece objects get filled in with GPGData objects with some of | |
596 | their attributes set. | |
a3d6aff8 | 597 | """ |
38738401 | 598 | |
928e3819 | 599 | if eddymsg_obj.multipart == True: |
101d54a8 | 600 | prev_parts=[] |
928e3819 | 601 | for sub in eddymsg_obj.subparts: |
101d54a8 AE |
602 | gpg_on_payloads (sub, gpgme_ctx, prev_parts) |
603 | prev_parts += [sub] | |
38738401 | 604 | |
d873ff48 | 605 | return |
38738401 | 606 | |
928e3819 | 607 | for piece in eddymsg_obj.payload_pieces: |
38738401 | 608 | |
0fd3389f | 609 | if piece.piece_type == TxtType.text: |
38738401 AE |
610 | # don't transform the plaintext. |
611 | pass | |
612 | ||
0fd3389f | 613 | elif piece.piece_type == TxtType.message: |
b4c116d5 AE |
614 | piece.gpg_data = GPGData() |
615 | ||
c035a008 | 616 | (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = decrypt_block(piece.string, gpgme_ctx) |
b4c116d5 AE |
617 | |
618 | piece.gpg_data.sigkey_missing = sigkey_missing | |
c035a008 | 619 | piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt |
38738401 | 620 | |
85a829fc | 621 | if plaintext_b: |
b23e171d | 622 | piece.gpg_data.decrypted = True |
38738401 AE |
623 | piece.gpg_data.sigs = sigs |
624 | # recurse! | |
85a829fc | 625 | piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx) |
3a753fd0 AE |
626 | continue |
627 | ||
628 | # if not encrypted, check to see if this is an armored signature. | |
c035a008 | 629 | (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = verify_sig_message(piece.string, gpgme_ctx) |
b4c116d5 AE |
630 | |
631 | piece.gpg_data.sigkey_missing = sigkey_missing | |
c035a008 | 632 | piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt |
3a753fd0 | 633 | |
85a829fc | 634 | if plaintext_b: |
0fd3389f | 635 | piece.piece_type = TxtType.signature |
3a753fd0 AE |
636 | piece.gpg_data.sigs = sigs |
637 | # recurse! | |
85a829fc | 638 | piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx) |
129543c3 | 639 | |
0fd3389f | 640 | elif piece.piece_type == TxtType.pubkey: |
c035a008 AE |
641 | piece.gpg_data = GPGData() |
642 | ||
643 | (key_fps, key_cannot_encrypt) = add_gpg_key(piece.string, gpgme_ctx) | |
644 | ||
645 | piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt | |
8f61c66a | 646 | |
f8ee6bd3 | 647 | if key_fps != []: |
f8ee6bd3 | 648 | piece.gpg_data.keys = key_fps |
129543c3 | 649 | |
0fd3389f | 650 | elif piece.piece_type == TxtType.detachedsig: |
b4c116d5 AE |
651 | piece.gpg_data = GPGData() |
652 | ||
101d54a8 | 653 | for prev in prev_parts: |
c035a008 | 654 | (sig_fps, sigkey_missing, key_cannot_encrypt) = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx) |
b4c116d5 AE |
655 | |
656 | piece.gpg_data.sigkey_missing = sigkey_missing | |
c035a008 | 657 | piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt |
101d54a8 | 658 | |
6d240f68 | 659 | if sig_fps != []: |
6d240f68 AE |
660 | piece.gpg_data.sigs = sig_fps |
661 | piece.gpg_data.plainobj = prev | |
662 | break | |
7e18f14d | 663 | |
38738401 AE |
664 | else: |
665 | pass | |
666 | ||
667 | ||
928e3819 | 668 | def prepare_for_reply (eddymsg_obj, replyinfo_obj): |
67691346 AE |
669 | """Updates replyinfo_obj with info on the message's GPG success/failures |
670 | ||
671 | This function marks replyinfo_obj with information about whether encrypted | |
672 | text in eddymsg_obj was successfully decrypted, signatures were verified | |
673 | and whether a public key was found or not. | |
674 | ||
675 | Args: | |
676 | eddymsg_obj: a message in the EddyMsg format | |
677 | replyinfo_obj: an instance of ReplyInfo | |
678 | ||
679 | Returns: | |
680 | Nothing | |
681 | ||
682 | Pre: | |
683 | eddymsg_obj has had its gpg_data created by gpg_on_payloads | |
684 | ||
685 | Post: | |
686 | replyinfo_obj has been updated with info about decryption/sig | |
687 | verififcation status, etc. However the desired key isn't imported until | |
688 | later, so the success or failure of that updates the values set here. | |
689 | """ | |
dd11a483 | 690 | |
928e3819 | 691 | do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj) |
56578eaf | 692 | |
928e3819 | 693 | def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj): |
67691346 AE |
694 | """A helper function for prepare_for_reply |
695 | ||
696 | It updates replyinfo_obj with GPG success/failure information, when | |
697 | supplied a single-part EddyMsg object. | |
698 | ||
699 | Args: | |
700 | eddymsg_obj: a single-part message in the EddyMsg format | |
701 | replyinfo_obj: an object which holds information about the message's | |
702 | GPG status | |
703 | ||
704 | Returns: | |
705 | Nothing | |
706 | ||
707 | Pre: | |
708 | eddymsg_obj is a single-part message. (it may be a part of a multi-part | |
709 | message.) It has had its gpg_data created by gpg_on_payloads if it has | |
710 | gpg data. | |
711 | ||
712 | Post: | |
713 | replyinfo_obj has been updated with gpg success/failure information | |
714 | """ | |
56578eaf | 715 | |
928e3819 | 716 | for piece in eddymsg_obj.payload_pieces: |
0fd3389f | 717 | if piece.piece_type == TxtType.text: |
d873ff48 AE |
718 | # don't quote the plaintext part. |
719 | pass | |
720 | ||
0fd3389f | 721 | elif piece.piece_type == TxtType.message: |
05683768 | 722 | prepare_for_reply_message(piece, replyinfo_obj) |
d873ff48 | 723 | |
0fd3389f | 724 | elif piece.piece_type == TxtType.pubkey: |
05683768 | 725 | prepare_for_reply_pubkey(piece, replyinfo_obj) |
6906d46d | 726 | |
0fd3389f AE |
727 | elif (piece.piece_type == TxtType.detachedsig) \ |
728 | or (piece.piece_type == TxtType.signature): | |
05683768 | 729 | prepare_for_reply_sig(piece, replyinfo_obj) |
d873ff48 | 730 | |
d873ff48 | 731 | |
05683768 | 732 | def prepare_for_reply_message (piece, replyinfo_obj): |
67691346 AE |
733 | """Helper function for prepare_for_reply() |
734 | ||
735 | This function is called when the piece_type of a payload piece is | |
9af26cb8 AE |
736 | TxtType.message, or GPG Message block. This should be encrypted text. If |
737 | the encryted block is correclty signed, a sig will be attached to | |
738 | .target_key unless there is already one there. | |
67691346 AE |
739 | |
740 | Args: | |
741 | piece: a PayloadPiece object. | |
742 | replyinfo_obj: object which gets updated with decryption status, etc. | |
743 | ||
67691346 AE |
744 | Returns: |
745 | Nothing | |
746 | ||
747 | Pre: | |
0fd3389f | 748 | the piece.payload_piece value should be TxtType.message. |
67691346 AE |
749 | |
750 | Post: | |
b4c116d5 | 751 | replyinfo_obj gets updated with decryption status, signing status, a |
c035a008 AE |
752 | potential signing key, posession status of the public key for the |
753 | signature and encryption capability status if that key is missing. | |
67691346 | 754 | """ |
44fe39b7 | 755 | |
c035a008 | 756 | if piece.gpg_data.plainobj == None: |
4e2c66f7 | 757 | replyinfo_obj.decrypt_failure = True |
05683768 AE |
758 | return |
759 | ||
b4c116d5 | 760 | replyinfo_obj.decrypt_success = True |
05683768 AE |
761 | |
762 | # we already have a key (and a message) | |
763 | if replyinfo_obj.target_key != None: | |
764 | return | |
765 | ||
c035a008 | 766 | if piece.gpg_data.sigs == []: |
b4c116d5 AE |
767 | if piece.gpg_data.sigkey_missing == True: |
768 | replyinfo_obj.sigkey_missing = True | |
9af26cb8 | 769 | |
c035a008 AE |
770 | if piece.gpg_data.key_cannot_encrypt == True: |
771 | replyinfo_obj.key_cannot_encrypt = True | |
772 | ||
05683768 AE |
773 | # only include a signed message in the reply. |
774 | get_signed_part = True | |
775 | ||
c035a008 AE |
776 | else: |
777 | replyinfo_obj.target_key = piece.gpg_data.sigs[0] | |
778 | replyinfo_obj.sig_success = True | |
779 | get_signed_part = False | |
780 | ||
8c3e5397 | 781 | flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part) |
05683768 AE |
782 | |
783 | # to catch public keys in encrypted blocks | |
784 | prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj) | |
785 | ||
786 | ||
787 | def prepare_for_reply_pubkey (piece, replyinfo_obj): | |
67691346 AE |
788 | """Helper function for prepare_for_reply(). Marks pubkey import status. |
789 | ||
790 | Marks replyinfo_obj with pub key import status. | |
791 | ||
792 | Args: | |
793 | piece: a PayloadPiece object | |
794 | replyinfo_obj: a ReplyInfo object | |
795 | ||
796 | Pre: | |
0fd3389f | 797 | piece.piece_type should be set to TxtType.pubkey . |
67691346 AE |
798 | |
799 | Post: | |
800 | replyinfo_obj has its fields updated. | |
801 | """ | |
05683768 | 802 | |
c035a008 AE |
803 | if piece.gpg_data.keys == []: |
804 | if piece.gpg_data.key_cannot_encrypt == True: | |
805 | replyinfo_obj.key_cannot_encrypt = True | |
05683768 | 806 | else: |
b4c116d5 | 807 | replyinfo_obj.pubkey_success = True |
05683768 | 808 | |
3ceb92e5 AE |
809 | # prefer public key as a fallback for the encrypted reply |
810 | replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0] | |
05683768 AE |
811 | |
812 | ||
813 | def prepare_for_reply_sig (piece, replyinfo_obj): | |
67691346 AE |
814 | """Helper function for prepare_for_reply(). Marks sig verification status. |
815 | ||
816 | Marks replyinfo_obj with signature verification status. | |
817 | ||
818 | Args: | |
819 | piece: a PayloadPiece object | |
820 | replyinfo_obj: a ReplyInfo object | |
821 | ||
822 | Pre: | |
0fd3389f AE |
823 | piece.piece_type should be set to TxtType.signature, or |
824 | TxtType.detachedsig . | |
67691346 AE |
825 | |
826 | Post: | |
827 | replyinfo_obj has its fields updated. | |
828 | """ | |
05683768 | 829 | |
c035a008 | 830 | if piece.gpg_data.sigs == []: |
32a8996f | 831 | replyinfo_obj.sig_failure = True |
b4c116d5 AE |
832 | |
833 | if piece.gpg_data.sigkey_missing == True: | |
834 | replyinfo_obj.sigkey_missing = True | |
835 | ||
c035a008 AE |
836 | if piece.gpg_data.key_cannot_encrypt == True: |
837 | replyinfo_obj.key_cannot_encrypt = True | |
838 | ||
05683768 AE |
839 | else: |
840 | replyinfo_obj.sig_success = True | |
841 | ||
842 | if replyinfo_obj.fallback_target_key == None: | |
843 | replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0] | |
6906d46d | 844 | |
c230113e AE |
845 | if (piece.piece_type == TxtType.signature): |
846 | # to catch public keys in signature blocks | |
847 | prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj) | |
848 | ||
6906d46d | 849 | |
8c3e5397 AE |
850 | def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part): |
851 | """For creating a string representation of a signed, encrypted part. | |
39e2c525 | 852 | |
8c3e5397 AE |
853 | When given a decrypted payload, it will add either the plaintext or signed |
854 | plaintext to the reply message, depeding on 'get_signed_part'. This is | |
855 | useful for ensuring that the reply message only comes from a signed and | |
856 | ecrypted GPG message. It also sets the target_key for encrypting the reply | |
857 | if it's told to get signed text only. | |
39e2c525 AE |
858 | |
859 | Args: | |
860 | eddymsg_obj: the message in EddyMsg format created by decrypting GPG | |
861 | text | |
8c3e5397 AE |
862 | replyinfo_obj: a ReplyInfo object for holding the message to quote and |
863 | the target_key to encrypt to. | |
39e2c525 AE |
864 | get_signed_part: True if we should only include text that contains a |
865 | further signature. If False, then include plain text. | |
866 | ||
867 | Returns: | |
8c3e5397 | 868 | Nothing |
39e2c525 AE |
869 | |
870 | Pre: | |
871 | The EddyMsg instance passed in should be a piece.gpg_data.plainobj | |
872 | which represents decrypted text. It may or may not be signed on that | |
873 | level. | |
d873ff48 | 874 | |
8c3e5397 AE |
875 | Post: |
876 | the ReplyInfo instance may have a new 'target_key' set and its | |
877 | 'msg_to_quote' will be updated with (possibly signed) plaintext, if any | |
878 | could be found. | |
879 | """ | |
d873ff48 | 880 | |
0aa88c27 | 881 | if eddymsg_obj == None: |
8c3e5397 | 882 | return |
0aa88c27 | 883 | |
0402995a | 884 | # recurse on multi-part mime |
928e3819 AE |
885 | if eddymsg_obj.multipart == True: |
886 | for sub in eddymsg_obj.subparts: | |
8c3e5397 | 887 | flatten_decrypted_payloads(sub, replyinfo_obj, get_signed_part) |
d873ff48 | 888 | |
928e3819 | 889 | for piece in eddymsg_obj.payload_pieces: |
0402995a | 890 | if (get_signed_part): |
0fd3389f AE |
891 | if ((piece.piece_type == TxtType.detachedsig) \ |
892 | or (piece.piece_type == TxtType.signature)) \ | |
b4c116d5 AE |
893 | and (piece.gpg_data != None) \ |
894 | and (piece.gpg_data.plainobj != None): | |
8c3e5397 AE |
895 | flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, False) |
896 | replyinfo_obj.target_key = piece.gpg_data.sigs[0] | |
c3d417bf AE |
897 | break |
898 | else: | |
c00930e9 AE |
899 | if (eddymsg_obj.content_disposition == None \ |
900 | or not eddymsg_obj.content_disposition.startswith("attachment")) \ | |
901 | and piece.piece_type == TxtType.text: | |
8c3e5397 | 902 | replyinfo_obj.msg_to_quote += piece.string |
d873ff48 AE |
903 | |
904 | ||
013f7cb8 | 905 | def get_key_from_fp (replyinfo_obj, gpgme_ctx): |
39e2c525 AE |
906 | """Obtains a public key object from a key fingerprint |
907 | ||
ca37d0cb AE |
908 | If the .target_key is not set, then we use .fallback_target_key, if |
909 | available. | |
39e2c525 AE |
910 | |
911 | Args: | |
912 | replyinfo_obj: ReplyInfo instance | |
913 | gpgme_ctx: the gpgme context | |
914 | ||
915 | Return: | |
c035a008 | 916 | Nothing |
39e2c525 AE |
917 | |
918 | Pre: | |
919 | Loading a key requires that we have the public key imported. This | |
920 | requires that they email contains the pub key block, or that it was | |
921 | previously sent to edward. | |
922 | ||
923 | Post: | |
ca37d0cb AE |
924 | If the key can be loaded, then replyinfo_obj.reply_to_key points to the |
925 | public key object. If the key cannot be loaded, then the replyinfo_obj | |
c035a008 AE |
926 | is marked as having no public key available. If the key is not capable |
927 | of encryption, it will not be used, and replyinfo_obj will be marked | |
928 | accordingly. | |
39e2c525 | 929 | """ |
013f7cb8 | 930 | |
ca37d0cb AE |
931 | for key in (replyinfo_obj.target_key, replyinfo_obj.fallback_target_key): |
932 | if key != None: | |
933 | try: | |
934 | encrypt_to_key = gpgme_ctx.get_key(key) | |
c035a008 AE |
935 | |
936 | except gpgme.GpgmeError: | |
937 | continue | |
938 | ||
c5492aa9 | 939 | if is_key_usable(encrypt_to_key): |
ca37d0cb | 940 | replyinfo_obj.encrypt_to_key = encrypt_to_key |
b4c116d5 | 941 | replyinfo_obj.have_reply_key = True |
c035a008 | 942 | replyinfo_obj.key_can_encrypt = True |
ca37d0cb | 943 | return |
013f7cb8 | 944 | |
c035a008 AE |
945 | else: |
946 | replyinfo_obj.key_cannot_encrypt = True | |
947 | ||
013f7cb8 | 948 | |
013f7cb8 | 949 | |
d873ff48 | 950 | def write_reply (replyinfo_obj): |
39e2c525 AE |
951 | """Write the reply email body about the GPG successes/failures. |
952 | ||
953 | The reply is about whether decryption, sig verification and key | |
954 | import/loading was successful or failed. If text was successfully decrypted | |
955 | and verified, then the first instance of such text will be included in | |
956 | quoted form. | |
957 | ||
958 | Args: | |
959 | replyinfo_obj: contains details of GPG processing status | |
960 | ||
961 | Returns: | |
962 | the plaintext message to be sent to the user | |
963 | ||
964 | Pre: | |
965 | replyinfo_obj should be populated with info about GPG processing status. | |
966 | """ | |
d873ff48 AE |
967 | |
968 | reply_plain = "" | |
969 | ||
cfcc211c AE |
970 | if (replyinfo_obj.pubkey_success == True): |
971 | reply_plain += replyinfo_obj.replies['greeting'] | |
972 | reply_plain += "\n\n" | |
973 | ||
371911ad | 974 | |
b4c116d5 | 975 | if replyinfo_obj.decrypt_success == True: |
234f607b | 976 | debug('decrypt success') |
d873ff48 | 977 | reply_plain += replyinfo_obj.replies['success_decrypt'] |
cfcc211c | 978 | reply_plain += "\n\n" |
d873ff48 | 979 | |
4e2c66f7 | 980 | elif replyinfo_obj.decrypt_failure == True: |
234f607b | 981 | debug('decrypt failure') |
d873ff48 | 982 | reply_plain += replyinfo_obj.replies['failed_decrypt'] |
b4c116d5 | 983 | reply_plain += "\n\n" |
d873ff48 | 984 | |
371911ad | 985 | |
d873ff48 | 986 | if replyinfo_obj.sig_success == True: |
234f607b | 987 | debug('signature success') |
d873ff48 | 988 | reply_plain += replyinfo_obj.replies['sig_success'] |
b4c116d5 | 989 | reply_plain += "\n\n" |
d873ff48 | 990 | |
32a8996f | 991 | elif replyinfo_obj.sig_failure == True: |
234f607b | 992 | debug('signature failure') |
d873ff48 | 993 | reply_plain += replyinfo_obj.replies['sig_failure'] |
b4c116d5 | 994 | reply_plain += "\n\n" |
d873ff48 | 995 | |
371911ad | 996 | |
b4c116d5 | 997 | if (replyinfo_obj.pubkey_success == True): |
234f607b | 998 | debug('public key received') |
b4c116d5 | 999 | reply_plain += replyinfo_obj.replies['public_key_received'] |
d873ff48 | 1000 | reply_plain += "\n\n" |
d873ff48 | 1001 | |
b4c116d5 | 1002 | elif (replyinfo_obj.sigkey_missing == True): |
234f607b | 1003 | debug('no public key') |
b4c116d5 | 1004 | reply_plain += replyinfo_obj.replies['no_public_key'] |
d873ff48 | 1005 | reply_plain += "\n\n" |
d873ff48 | 1006 | |
c035a008 AE |
1007 | elif (replyinfo_obj.key_can_encrypt == False) \ |
1008 | and (replyinfo_obj.key_cannot_encrypt == True): | |
1009 | debug('bad public key') | |
1010 | reply_plain += replyinfo_obj.replies['no_public_key'] | |
1011 | reply_plain += "\n\n" | |
1012 | ||
d873ff48 | 1013 | |
0be09cca AE |
1014 | if (replyinfo_obj.decrypt_success == True) \ |
1015 | and (replyinfo_obj.sig_success == True) \ | |
1016 | and (replyinfo_obj.have_reply_key == True): | |
1017 | debug('message quoted') | |
1018 | reply_plain += replyinfo_obj.replies['quote_follows'] | |
1019 | reply_plain += "\n\n" | |
1020 | quoted_text = email_quote_text(replyinfo_obj.msg_to_quote) | |
1021 | reply_plain += quoted_text | |
1022 | reply_plain += "\n\n" | |
1023 | ||
1024 | ||
4e2c66f7 AE |
1025 | if (reply_plain == ""): |
1026 | debug('plaintext message') | |
1027 | reply_plain += replyinfo_obj.replies['failed_decrypt'] | |
1028 | reply_plain += "\n\n" | |
1029 | ||
1030 | ||
d873ff48 | 1031 | reply_plain += replyinfo_obj.replies['signature'] |
b4c116d5 | 1032 | reply_plain += "\n\n" |
56578eaf | 1033 | |
d873ff48 | 1034 | return reply_plain |
56578eaf AE |
1035 | |
1036 | ||
8f61c66a | 1037 | def add_gpg_key (key_block, gpgme_ctx): |
39e2c525 AE |
1038 | """Adds a GPG pubkey to the local keystore |
1039 | ||
1040 | This adds keys received through email into the key store so they can be | |
1041 | used later. | |
1042 | ||
1043 | Args: | |
1044 | key_block: the string form of the ascii-armored public key block | |
1045 | gpgme_ctx: the gpgme context | |
1046 | ||
1047 | Returns: | |
c035a008 AE |
1048 | the fingerprint(s) of the imported key(s) which can be used for |
1049 | encryption, and a boolean marking whether none of the keys are capable | |
1050 | of encryption. | |
39e2c525 | 1051 | """ |
c267c233 | 1052 | |
8f61c66a | 1053 | fp = io.BytesIO(key_block.encode('ascii')) |
c267c233 | 1054 | |
89c08329 AE |
1055 | try: |
1056 | result = gpgme_ctx.import_(fp) | |
1057 | imports = result.imports | |
1058 | except gpgme.GpgmeError: | |
1059 | imports = [] | |
c267c233 | 1060 | |
f8ee6bd3 | 1061 | key_fingerprints = [] |
c035a008 AE |
1062 | key_cannot_encrypt = False |
1063 | ||
1064 | for import_res in imports: | |
1065 | fingerprint = import_res[0] | |
1066 | ||
1067 | try: | |
1068 | key_obj = gpgme_ctx.get_key(fingerprint) | |
1069 | except: | |
494c6e50 | 1070 | key_obj = None |
c267c233 | 1071 | |
494c6e50 | 1072 | if key_obj != None and is_key_usable(key_obj): |
f8ee6bd3 | 1073 | key_fingerprints += [fingerprint] |
c035a008 | 1074 | key_cannot_encrypt = False |
c267c233 | 1075 | |
e49673aa | 1076 | debug("added gpg key: " + fingerprint) |
ec1e779a | 1077 | |
c035a008 AE |
1078 | elif key_fingerprints == []: |
1079 | key_cannot_encrypt = True | |
1080 | ||
1081 | return (key_fingerprints, key_cannot_encrypt) | |
ec1e779a AE |
1082 | |
1083 | ||
3a753fd0 | 1084 | def verify_sig_message (msg_block, gpgme_ctx): |
39e2c525 AE |
1085 | """Verifies the signature of a signed, ascii-armored block of text. |
1086 | ||
1087 | It encodes the string into ascii, since binary GPG files are currently | |
1088 | unsupported, and alternative, the ascii-armored format is encodable into | |
1089 | ascii. | |
1090 | ||
1091 | Args: | |
1092 | msg_block: a GPG Message block in string form. It may be encrypted or | |
1093 | not. If it is encrypted, it will return empty results. | |
1094 | gpgme_ctx: the gpgme context | |
1095 | ||
1096 | Returns: | |
85a829fc | 1097 | A tuple containing the plaintext bytes of the signed part, the list of |
c035a008 AE |
1098 | fingerprints of encryption-capable keys signing the data, a boolean |
1099 | marking whether edward is missing all public keys for validating any of | |
1100 | the signatures, and a boolean marking whether all sigs' keys are | |
1101 | incapable of encryption. If verification failed, perhaps because the | |
1102 | message was also encrypted, sensible default values are returned. | |
39e2c525 | 1103 | """ |
3a753fd0 AE |
1104 | |
1105 | block_b = io.BytesIO(msg_block.encode('ascii')) | |
1106 | plain_b = io.BytesIO() | |
1107 | ||
1108 | try: | |
1109 | sigs = gpgme_ctx.verify(block_b, None, plain_b) | |
5f6f32b1 | 1110 | except gpgme.GpgmeError: |
c035a008 | 1111 | return ("",[],False,False) |
3a753fd0 | 1112 | |
85a829fc | 1113 | plaintext_b = plain_b.getvalue() |
3a753fd0 | 1114 | |
c035a008 | 1115 | (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx) |
9af26cb8 | 1116 | |
c035a008 | 1117 | return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt) |
3a753fd0 AE |
1118 | |
1119 | ||
101d54a8 | 1120 | def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx): |
39e2c525 AE |
1121 | """Verifies the signature of a detached signature. |
1122 | ||
1123 | This requires the signature part and the signed part as separate arguments. | |
1124 | ||
1125 | Args: | |
1126 | detached_sig: the signature part of the detached signature | |
1127 | plaintext_bytes: the byte form of the message being signed. | |
1128 | gpgme_ctx: the gpgme context | |
1129 | ||
1130 | Returns: | |
c035a008 AE |
1131 | A tuple containging a list of encryption capable signing fingerprints |
1132 | if the signature verification was sucessful, a boolean marking whether | |
1133 | edward is missing all public keys for validating any of the signatures, | |
1134 | and a boolean marking whether all signing keys are incapable of | |
1135 | encryption. Otherwise, a tuple containing an empty list and True are | |
1136 | returned. | |
39e2c525 | 1137 | """ |
101d54a8 AE |
1138 | |
1139 | detached_sig_fp = io.BytesIO(detached_sig.encode('ascii')) | |
1140 | plaintext_fp = io.BytesIO(plaintext_bytes) | |
101d54a8 | 1141 | |
ff7aeb3d | 1142 | try: |
c035a008 | 1143 | sigs = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None) |
ff7aeb3d | 1144 | except gpgme.GpgmeError: |
c035a008 | 1145 | return ([],False,False) |
101d54a8 | 1146 | |
c035a008 | 1147 | (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx) |
101d54a8 | 1148 | |
c035a008 | 1149 | return (fingerprints, sigkey_missing, key_cannot_encrypt) |
101d54a8 AE |
1150 | |
1151 | ||
5b3053c1 | 1152 | def decrypt_block (msg_block, gpgme_ctx): |
9af26cb8 | 1153 | """Decrypts a block of GPG text and verifies any included sigatures. |
39e2c525 AE |
1154 | |
1155 | Some encypted messages have embeded signatures, so those are verified too. | |
1156 | ||
1157 | Args: | |
1158 | msg_block: the encrypted(/signed) text | |
1159 | gpgme_ctx: the gpgme context | |
1160 | ||
1161 | Returns: | |
c035a008 AE |
1162 | A tuple containing plaintext bytes, encryption-capable signatures (if |
1163 | decryption and signature verification were successful, respectively), a | |
1164 | boolean marking whether edward is missing all public keys for | |
1165 | validating any of the signatures, and a boolean marking whether all | |
1166 | signature keys are incapable of encryption. | |
39e2c525 | 1167 | """ |
0bec96d6 | 1168 | |
5b3053c1 | 1169 | block_b = io.BytesIO(msg_block.encode('ascii')) |
0bec96d6 AE |
1170 | plain_b = io.BytesIO() |
1171 | ||
afc1f64c AE |
1172 | try: |
1173 | sigs = gpgme_ctx.decrypt_verify(block_b, plain_b) | |
5f6f32b1 | 1174 | except gpgme.GpgmeError: |
c035a008 | 1175 | return ("",[],False,False) |
0bec96d6 | 1176 | |
85a829fc | 1177 | plaintext_b = plain_b.getvalue() |
cbdf22c1 | 1178 | |
c035a008 AE |
1179 | (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx) |
1180 | ||
1181 | return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt) | |
1182 | ||
1183 | ||
1184 | def get_signature_fp (sigs, gpgme_ctx): | |
1185 | """Selects valid signatures from output of gpgme signature verifying functions | |
1186 | ||
1187 | get_signature_fp returns a list of valid signature fingerprints if those | |
1188 | fingerprints are associated with available keys capable of encryption. | |
1189 | ||
1190 | Args: | |
1191 | sigs: a signature verification result object list | |
1192 | gpgme_ctx: a gpgme context | |
1193 | ||
1194 | Returns: | |
1195 | fingerprints: a list of fingerprints | |
1196 | sigkey_missing: a boolean marking whether public keys are missing for | |
1197 | all available signatures. | |
1198 | key_cannot_encrypt: a boolearn marking whether available public keys are | |
1199 | incapable of encryption. | |
1200 | """ | |
1201 | ||
b4c116d5 | 1202 | sigkey_missing = False |
c035a008 | 1203 | key_cannot_encrypt = False |
cbdf22c1 | 1204 | fingerprints = [] |
c035a008 | 1205 | |
cbdf22c1 | 1206 | for sig in sigs: |
8f011cc9 | 1207 | if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0): |
c035a008 AE |
1208 | try: |
1209 | key_obj = gpgme_ctx.get_key(sig.fpr) | |
1210 | except: | |
1211 | if fingerprints == []: | |
1212 | sigkey_missing = True | |
1213 | continue | |
1214 | ||
c5492aa9 | 1215 | if is_key_usable(key_obj): |
c035a008 AE |
1216 | fingerprints += [sig.fpr] |
1217 | key_cannot_encrypt = False | |
1218 | sigkey_missing = False | |
1219 | ||
1220 | elif fingerprints == []: | |
1221 | key_cannot_encrypt = True | |
1222 | ||
1223 | elif fingerprints == []: | |
b4c116d5 AE |
1224 | if (sig.summary & gpgme.SIGSUM_KEY_MISSING != 0): |
1225 | sigkey_missing = True | |
9af26cb8 | 1226 | |
c035a008 | 1227 | return (fingerprints, sigkey_missing, key_cannot_encrypt) |
0bec96d6 AE |
1228 | |
1229 | ||
c5492aa9 AE |
1230 | def is_key_usable (key_obj): |
1231 | """Returns boolean representing key usability regarding encryption | |
1232 | ||
1233 | Tests various feature of key and returns usability | |
1234 | ||
1235 | Args: | |
1236 | key_obj: a gpgme key object | |
1237 | ||
1238 | Returns: | |
1239 | A boolean representing key usability | |
1240 | """ | |
1241 | if key_obj.can_encrypt and not key_obj.invalid and not key_obj.expired \ | |
1242 | and not key_obj.revoked and not key_obj.disabled: | |
1243 | return True | |
1244 | else: | |
1245 | return False | |
1246 | ||
1247 | ||
7cc9808b AE |
1248 | def test_auto_reply (email_bytes): |
1249 | """Test whether email is auto-generated | |
1250 | ||
1251 | If the email is autogenerated, edward quits without sending a response. | |
1252 | This is not a perfect test. Some auto-responses will go undetected. | |
1253 | ||
1254 | Args: | |
1255 | email_bytes: the byte string from of the email | |
1256 | ||
1257 | Returns: | |
1258 | Nothing, or exits the program | |
1259 | """ | |
1260 | ||
1261 | email_struct = email.parser.BytesHeaderParser().parsebytes(email_bytes) | |
1262 | ||
1263 | auto_submitted = email_struct['Auto-Submitted'] | |
1264 | ||
1265 | if auto_submitted == None or auto_submitted == "no" \ | |
1266 | or auto_submitted == "No": | |
1267 | ||
1268 | return | |
1269 | ||
1270 | debug("autoreply") | |
1271 | exit(0) | |
1272 | ||
1273 | ||
140c47ca AE |
1274 | def email_to_reply_to_subject (email_bytes): |
1275 | """Returns the email's To:, Reply-To: (or From:), and Subject: fields | |
fafa21c3 | 1276 | |
dd2b62d7 | 1277 | Returns this information from an email. |
fafa21c3 | 1278 | |
dd2b62d7 | 1279 | Args: |
85a829fc | 1280 | email_bytes: the byte string form of the email |
fafa21c3 | 1281 | |
dd2b62d7 | 1282 | Returns: |
140c47ca | 1283 | the email To:, Reply-To: (or From:), and Subject: fields as strings |
dd2b62d7 | 1284 | """ |
d65993b8 | 1285 | |
4f468fcd | 1286 | email_struct = email.parser.BytesHeaderParser().parsebytes(email_bytes) |
d65993b8 AE |
1287 | |
1288 | email_to = email_struct['To'] | |
1289 | email_from = email_struct['From'] | |
140c47ca AE |
1290 | email_reply_to = email_struct['Reply-To'] |
1291 | ||
d65993b8 AE |
1292 | email_subject = email_struct['Subject'] |
1293 | ||
140c47ca AE |
1294 | if email_reply_to == None: |
1295 | email_reply_to = email_from | |
1296 | ||
1297 | return email_to, email_reply_to, email_subject | |
d65993b8 AE |
1298 | |
1299 | ||
bb27d257 AE |
1300 | def import_lang_pick_address(email_to, hostname): |
1301 | """Imports language file for i18n support; makes reply from address | |
dd2b62d7 AE |
1302 | |
1303 | The language imported depends on the To: address of the email received by | |
1304 | edward. an -en ending implies the English language, whereas a -ja ending | |
1305 | implies Japanese. The list of supported languages is listed in the 'langs' | |
bb27d257 AE |
1306 | list at the beginning of the program. This function also chooses the |
1307 | language-dependent address which can be used as the From address in the | |
1308 | reply email. | |
dd2b62d7 AE |
1309 | |
1310 | Args: | |
1311 | email_to: the string containing the email address that the mail was | |
bb27d257 AE |
1312 | sent to. |
1313 | hostname: the hostname part of the reply email's from address | |
dd2b62d7 AE |
1314 | |
1315 | Returns: | |
1316 | the reference to the imported language module. The only variable in | |
1317 | this file is the 'replies' dictionary. | |
1318 | """ | |
adcef2f7 | 1319 | |
bb27d257 AE |
1320 | # default |
1321 | use_lang = "en" | |
daed5f10 | 1322 | |
5250b3b8 AE |
1323 | if email_to != None: |
1324 | for lang in langs: | |
1325 | if "edward-" + lang in email_to: | |
bb27d257 AE |
1326 | use_lang = lang |
1327 | break | |
adcef2f7 | 1328 | |
bb27d257 AE |
1329 | lang_mod_name = "lang." + re.sub('-', '_', use_lang) |
1330 | lang_module = importlib.import_module(lang_mod_name) | |
1331 | ||
60ccbaf4 | 1332 | reply_from = "edward-" + use_lang + "@" + hostname |
bb27d257 | 1333 | |
60ccbaf4 | 1334 | return lang_module, reply_from |
adcef2f7 AE |
1335 | |
1336 | ||
2d7bc2a9 AE |
1337 | def generate_encrypted_mime (plaintext, email_to, email_from, email_subject, |
1338 | encrypt_to_key, gpgme_ctx): | |
dd2b62d7 AE |
1339 | """This function creates the mime email reply. It can encrypt the email. |
1340 | ||
1341 | If the encrypt_key is included, then the email is encrypted and signed. | |
1342 | Otherwise it is unencrypted. | |
1343 | ||
1344 | Args: | |
1345 | plaintext: the plaintext body of the message to create. | |
cfb03389 | 1346 | email_to: the email address to reply to |
dd2b62d7 AE |
1347 | email_subject: the subject to use in reply |
1348 | encrypt_to_key: the key object to use for encrypting the email. (or | |
1349 | None) | |
1350 | gpgme_ctx: the gpgme context | |
1351 | ||
1352 | Returns | |
1353 | A string version of the mime message, possibly encrypted and signed. | |
1354 | """ | |
1da9b527 | 1355 | |
59bc3fac AE |
1356 | plaintext_mime = MIMEText(plaintext) |
1357 | plaintext_mime.set_charset('utf-8') | |
8bdfb6d4 | 1358 | |
59bc3fac | 1359 | if (encrypt_to_key != None): |
8bdfb6d4 AE |
1360 | |
1361 | encrypted_text = encrypt_sign_message(plaintext_mime.as_string(), | |
1362 | encrypt_to_key, | |
40c37ab3 | 1363 | gpgme_ctx) |
8bdfb6d4 | 1364 | |
e5dd6f23 AE |
1365 | control_mime = MIMEApplication("Version: 1", |
1366 | _subtype='pgp-encrypted', | |
1367 | _encoder=email.encoders.encode_7or8bit) | |
1368 | control_mime['Content-Description'] = 'PGP/MIME version identification' | |
1369 | control_mime.set_charset('us-ascii') | |
8bdfb6d4 | 1370 | |
e5dd6f23 AE |
1371 | encoded_mime = MIMEApplication(encrypted_text, |
1372 | _subtype='octet-stream; name="encrypted.asc"', | |
1373 | _encoder=email.encoders.encode_7or8bit) | |
1374 | encoded_mime['Content-Description'] = 'OpenPGP encrypted message' | |
1375 | encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"' | |
1376 | encoded_mime.set_charset('us-ascii') | |
8bdfb6d4 | 1377 | |
e5dd6f23 AE |
1378 | message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted") |
1379 | message_mime.attach(control_mime) | |
1380 | message_mime.attach(encoded_mime) | |
1381 | message_mime['Content-Disposition'] = 'inline' | |
216708e9 | 1382 | |
e5dd6f23 | 1383 | else: |
59bc3fac | 1384 | message_mime = plaintext_mime |
2007103e | 1385 | |
387c2588 AE |
1386 | message_mime['Auto-Submitted'] = 'auto-replied' |
1387 | ||
cfb03389 | 1388 | message_mime['To'] = email_to |
2d7bc2a9 | 1389 | message_mime['From'] = email_from |
2007103e AE |
1390 | message_mime['Subject'] = email_subject |
1391 | ||
1392 | reply = message_mime.as_string() | |
1da9b527 AE |
1393 | |
1394 | return reply | |
1395 | ||
1396 | ||
ba34a33c AE |
1397 | def send_reply(email_txt, reply_to, reply_from): |
1398 | """Sends reply email | |
1399 | ||
1400 | Sent to original sender | |
1401 | ||
1402 | Args: | |
1403 | email_txt: message as a string | |
1404 | reply_to: recipient of reply | |
1405 | reply_from: edward's specific email address | |
1406 | ||
1407 | Post: | |
1408 | Email is sent | |
1409 | """ | |
1410 | ||
8ad5c922 AE |
1411 | if reply_to == None: |
1412 | error("*** ERROR: No one to send email to.") | |
1413 | exit(1) | |
60ccbaf4 | 1414 | |
a9eaa792 AE |
1415 | s = smtplib.SMTP('localhost') |
1416 | s.sendmail(reply_from, reply_to, email_txt) | |
1417 | s.quit() | |
60ccbaf4 AE |
1418 | |
1419 | ||
f87041f8 | 1420 | def email_quote_text (text): |
dd2b62d7 AE |
1421 | """Quotes input text by inserting "> "s |
1422 | ||
1423 | This is useful for quoting a text for the reply message. It inserts "> " | |
1424 | strings at the beginning of lines. | |
1425 | ||
1426 | Args: | |
1427 | text: plain text to quote | |
1428 | ||
1429 | Returns: | |
1430 | Quoted text | |
1431 | """ | |
f87041f8 AE |
1432 | |
1433 | quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE) | |
1434 | ||
1435 | return quoted_message | |
1436 | ||
1437 | ||
0a064403 | 1438 | def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx): |
dd2b62d7 AE |
1439 | """Encrypts and signs plaintext |
1440 | ||
1441 | This encrypts and signs a message. | |
1442 | ||
1443 | Args: | |
1444 | plaintext: text to sign and ecrypt | |
1445 | encrypt_to_key: the key object to encrypt to | |
1446 | gpgme_ctx: the gpgme context | |
1447 | ||
1448 | Returns: | |
1449 | An encrypted and signed string of text | |
1450 | """ | |
897cbaf6 | 1451 | |
b33f601b | 1452 | # the plaintext should be mime encoded in an ascii-compatible form |
6aa41372 | 1453 | plaintext_bytes = io.BytesIO(plaintext.encode('ascii')) |
1da9b527 AE |
1454 | encrypted_bytes = io.BytesIO() |
1455 | ||
897cbaf6 | 1456 | gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST, |
1da9b527 AE |
1457 | plaintext_bytes, encrypted_bytes) |
1458 | ||
6aa41372 | 1459 | encrypted_txt = encrypted_bytes.getvalue().decode('ascii') |
1da9b527 AE |
1460 | return encrypted_txt |
1461 | ||
1462 | ||
0a064403 | 1463 | def error (error_msg): |
dd2b62d7 AE |
1464 | """Write an error message to stdout |
1465 | ||
1466 | The error message includes the program name. | |
1467 | ||
1468 | Args: | |
1469 | error_msg: the message to print | |
1470 | ||
1471 | Returns: | |
1472 | Nothing | |
1473 | ||
1474 | Post: | |
1475 | An error message is printed to stdout | |
1476 | """ | |
0a064403 | 1477 | |
e4fb2ab2 | 1478 | sys.stderr.write(progname + ": " + str(error_msg) + "\n") |
0a064403 AE |
1479 | |
1480 | ||
5e8f9094 | 1481 | def debug (debug_msg): |
dd2b62d7 AE |
1482 | """Writes a debug message to stdout if debug == True |
1483 | ||
1484 | If the debug option is set in edward_config.py, then the passed message | |
1485 | gets printed to stdout. | |
1486 | ||
1487 | Args: | |
1488 | debug_msg: the message to print to stdout | |
1489 | ||
1490 | Returns: | |
1491 | Nothing | |
1492 | ||
1493 | Post: | |
1494 | A debug message is printed to stdout | |
1495 | """ | |
5e8f9094 AE |
1496 | |
1497 | if edward_config.debug == True: | |
0a064403 | 1498 | error(debug_msg) |
5e8f9094 AE |
1499 | |
1500 | ||
20f6e7c5 | 1501 | def handle_args (): |
60ccbaf4 | 1502 | """Sets the progname variable and processes optional argument |
dd2b62d7 | 1503 | |
60ccbaf4 AE |
1504 | If there are more than two arguments then edward complains and quits. An |
1505 | single "-p" argument sets the print_reply_only option, which makes edward | |
1506 | print email replies instead of mailing them. | |
dd2b62d7 AE |
1507 | |
1508 | Args: | |
1509 | None | |
1510 | ||
1511 | Returns: | |
60ccbaf4 AE |
1512 | True if edward should print arguments instead of mailing them, |
1513 | otherwise it returns False. | |
dd2b62d7 AE |
1514 | |
1515 | Post: | |
60ccbaf4 AE |
1516 | Exits with error 1 if there are more than two arguments, otherwise |
1517 | returns the print_reply_only option. | |
dd2b62d7 | 1518 | """ |
20f6e7c5 | 1519 | |
a51cdb72 AE |
1520 | global progname |
1521 | progname = sys.argv[0] | |
1522 | ||
60ccbaf4 AE |
1523 | print_reply_only = False |
1524 | ||
1525 | if len(sys.argv) > 2: | |
1526 | print(progname + " usage: " + progname + " [-p]\n\n" \ | |
1527 | + " -p print reply message to stdout, do not mail it\n", \ | |
1528 | file=sys.stderr) | |
a51cdb72 | 1529 | exit(1) |
20f6e7c5 | 1530 | |
60ccbaf4 AE |
1531 | elif (len(sys.argv) == 2) and (sys.argv[1] == "-p"): |
1532 | print_reply_only = True | |
1533 | ||
1534 | return print_reply_only | |
1535 | ||
20f6e7c5 | 1536 | |
a51cdb72 | 1537 | if __name__ == "__main__": |
dd2b62d7 | 1538 | """Executes main if this file is not loaded interactively""" |
20f6e7c5 | 1539 | |
a51cdb72 | 1540 | main() |
0bec96d6 | 1541 |