]> git.donarmstrong.com Git - roundcube.git/blob - plugins/enigma/enigma.php
Merge commit 'upstream/0.7'
[roundcube.git] / plugins / enigma / enigma.php
1 <?php
2 /*
3  +-------------------------------------------------------------------------+
4  | Enigma Plugin for Roundcube                                             |
5  | Version 0.1                                                             |
6  |                                                                         |
7  | This program is free software; you can redistribute it and/or modify    |
8  | it under the terms of the GNU General Public License version 2          |
9  | as published by the Free Software Foundation.                           |
10  |                                                                         |
11  | This program is distributed in the hope that it will be useful,         |
12  | but WITHOUT ANY WARRANTY; without even the implied warranty of          |
13  | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           |
14  | GNU General Public License for more details.                            |
15  |                                                                         |
16  | You should have received a copy of the GNU General Public License along |
17  | with this program; if not, write to the Free Software Foundation, Inc., |
18  | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.             |
19  |                                                                         |
20  +-------------------------------------------------------------------------+
21  | Author: Aleksander Machniak <alec@alec.pl>                              |
22  +-------------------------------------------------------------------------+
23 */
24
25 /*
26     This class contains only hooks and action handlers.
27     Most plugin logic is placed in enigma_engine and enigma_ui classes.
28 */
29
30 class enigma extends rcube_plugin
31 {
32     public $task = 'mail|settings';
33     public $rc;
34     public $engine;
35
36     private $env_loaded;
37     private $message;
38     private $keys_parts = array();
39     private $keys_bodies = array();
40
41
42     /**
43      * Plugin initialization.
44      */
45     function init()
46     {
47         $rcmail = rcmail::get_instance();
48         $this->rc = $rcmail;
49
50         if ($this->rc->task == 'mail') {
51             // message parse/display hooks
52             $this->add_hook('message_part_structure', array($this, 'parse_structure'));
53             $this->add_hook('message_body_prefix', array($this, 'status_message'));
54
55             // message displaying
56             if ($rcmail->action == 'show' || $rcmail->action == 'preview') {
57                 $this->add_hook('message_load', array($this, 'message_load'));
58                 $this->add_hook('template_object_messagebody', array($this, 'message_output'));
59                 $this->register_action('plugin.enigmaimport', array($this, 'import_file'));
60             }
61             // message composing
62             else if ($rcmail->action == 'compose') {
63                 $this->load_ui();
64                 $this->ui->init($section);
65             }
66             // message sending (and draft storing)
67             else if ($rcmail->action == 'sendmail') {
68                 //$this->add_hook('outgoing_message_body', array($this, 'msg_encode'));
69                 //$this->add_hook('outgoing_message_body', array($this, 'msg_sign'));
70             }
71         }
72         else if ($this->rc->task == 'settings') {
73             // add hooks for Enigma settings
74             $this->add_hook('preferences_sections_list', array($this, 'preferences_section'));
75             $this->add_hook('preferences_list', array($this, 'preferences_list'));
76             $this->add_hook('preferences_save', array($this, 'preferences_save'));
77
78             // register handler for keys/certs management
79             $this->register_action('plugin.enigma', array($this, 'preferences_ui'));
80
81             // grab keys/certs management iframe requests
82             $section = get_input_value('_section', RCUBE_INPUT_GET);
83             if ($this->rc->action == 'edit-prefs' && preg_match('/^enigma(certs|keys)/', $section)) {
84                 $this->load_ui();
85                 $this->ui->init($section);
86             }
87         }
88     }
89
90     /**
91      * Plugin environment initialization.
92      */
93     function load_env()
94     {
95         if ($this->env_loaded)
96             return;
97
98         $this->env_loaded = true;
99
100         // Add include path for Enigma classes and drivers
101         $include_path = $this->home . '/lib' . PATH_SEPARATOR;
102         $include_path .= ini_get('include_path');
103         set_include_path($include_path);
104
105         // load the Enigma plugin configuration
106         $this->load_config();
107
108         // include localization (if wasn't included before)
109         $this->add_texts('localization/');
110     }
111
112     /**
113      * Plugin UI initialization.
114      */
115     function load_ui()
116     {
117         if ($this->ui)
118             return;
119
120         // load config/localization
121         $this->load_env();
122
123         // Load UI
124         $this->ui = new enigma_ui($this, $this->home);
125     }
126
127     /**
128      * Plugin engine initialization.
129      */
130     function load_engine()
131     {
132         if ($this->engine)
133             return;
134
135         // load config/localization
136         $this->load_env();
137
138         $this->engine = new enigma_engine($this);
139     }
140
141     /**
142      * Handler for message_part_structure hook.
143      * Called for every part of the message.
144      *
145      * @param array Original parameters
146      *
147      * @return array Modified parameters
148      */
149     function parse_structure($p)
150     {
151         $struct = $p['structure'];
152
153         if ($p['mimetype'] == 'text/plain' || $p['mimetype'] == 'application/pgp') {
154             $this->parse_plain($p);
155         }
156         else if ($p['mimetype'] == 'multipart/signed') {
157             $this->parse_signed($p);
158         }
159         else if ($p['mimetype'] == 'multipart/encrypted') {
160             $this->parse_encrypted($p);
161         }
162         else if ($p['mimetype'] == 'application/pkcs7-mime') {
163             $this->parse_encrypted($p);
164         }
165
166         return $p;
167     }
168
169     /**
170      * Handler for preferences_sections_list hook.
171      * Adds Enigma settings sections into preferences sections list.
172      *
173      * @param array Original parameters
174      *
175      * @return array Modified parameters
176      */
177     function preferences_section($p)
178     {
179         // add labels
180         $this->add_texts('localization/');
181
182         $p['list']['enigmasettings'] = array(
183             'id' => 'enigmasettings', 'section' => $this->gettext('enigmasettings'),
184         );
185         $p['list']['enigmacerts'] = array(
186             'id' => 'enigmacerts', 'section' => $this->gettext('enigmacerts'),
187         );
188         $p['list']['enigmakeys'] = array(
189             'id' => 'enigmakeys', 'section' => $this->gettext('enigmakeys'),
190         );
191
192         return $p;
193     }
194
195     /**
196      * Handler for preferences_list hook.
197      * Adds options blocks into Enigma settings sections in Preferences.
198      *
199      * @param array Original parameters
200      *
201      * @return array Modified parameters
202      */
203     function preferences_list($p)
204     {
205         if ($p['section'] == 'enigmasettings') {
206             // This makes that section is not removed from the list
207             $p['blocks']['dummy']['options']['dummy'] = array();
208         }
209         else if ($p['section'] == 'enigmacerts') {
210             // This makes that section is not removed from the list
211             $p['blocks']['dummy']['options']['dummy'] = array();
212         }
213         else if ($p['section'] == 'enigmakeys') {
214             // This makes that section is not removed from the list
215             $p['blocks']['dummy']['options']['dummy'] = array();
216         }
217
218         return $p;
219     }
220
221     /**
222      * Handler for preferences_save hook.
223      * Executed on Enigma settings form submit.
224      *
225      * @param array Original parameters
226      *
227      * @return array Modified parameters
228      */
229     function preferences_save($p)
230     {
231         if ($p['section'] == 'enigmasettings') {
232             $a['prefs'] = array(
233 //                'dummy' => get_input_value('_dummy', RCUBE_INPUT_POST),
234             );
235         }
236
237         return $p;
238     }
239
240     /**
241      * Handler for keys/certs management UI template.
242      */
243     function preferences_ui()
244     {
245         $this->load_ui();
246         $this->ui->init();
247     }
248
249     /**
250      * Handler for message_body_prefix hook.
251      * Called for every displayed (content) part of the message.
252      * Adds infobox about signature verification and/or decryption
253      * status above the body.
254      *
255      * @param array Original parameters
256      *
257      * @return array Modified parameters
258      */
259     function status_message($p)
260     {
261         $part_id = $p['part']->mime_id;
262
263         // skip: not a message part
264         if ($p['part'] instanceof rcube_message)
265             return $p;
266
267         // skip: message has no signed/encoded content
268         if (!$this->engine)
269             return $p;
270
271         // Decryption status
272         if (isset($this->engine->decryptions[$part_id])) {
273
274             // get decryption status
275             $status = $this->engine->decryptions[$part_id];
276
277             // Load UI and add css script
278             $this->load_ui();
279             $this->ui->add_css();
280
281             // display status info
282             $attrib['id'] = 'enigma-message';
283
284             if ($status instanceof enigma_error) {
285                 $attrib['class'] = 'enigmaerror';
286                 $code = $status->getCode();
287                 if ($code == enigma_error::E_KEYNOTFOUND)
288                     $msg = Q(str_replace('$keyid', enigma_key::format_id($status->getData('id')),
289                         $this->gettext('decryptnokey')));
290                 else if ($code == enigma_error::E_BADPASS)
291                     $msg = Q($this->gettext('decryptbadpass'));
292                 else
293                     $msg = Q($this->gettext('decrypterror'));
294             }
295             else {
296                 $attrib['class'] = 'enigmanotice';
297                 $msg = Q($this->gettext('decryptok'));
298             }
299
300             $p['prefix'] .= html::div($attrib, $msg);
301         }
302
303         // Signature verification status
304         if (isset($this->engine->signed_parts[$part_id])
305             && ($sig = $this->engine->signatures[$this->engine->signed_parts[$part_id]])
306         ) {
307             // add css script
308             $this->load_ui();
309             $this->ui->add_css();
310
311             // display status info
312             $attrib['id'] = 'enigma-message';
313
314             if ($sig instanceof enigma_signature) {
315                 if ($sig->valid) {
316                     $attrib['class'] = 'enigmanotice';
317                     $sender = ($sig->name ? $sig->name . ' ' : '') . '<' . $sig->email . '>';
318                     $msg = Q(str_replace('$sender', $sender, $this->gettext('sigvalid')));
319                 }
320                 else {
321                     $attrib['class'] = 'enigmawarning';
322                     $sender = ($sig->name ? $sig->name . ' ' : '') . '<' . $sig->email . '>';
323                     $msg = Q(str_replace('$sender', $sender, $this->gettext('siginvalid')));
324                 }
325             }
326             else if ($sig->getCode() == enigma_error::E_KEYNOTFOUND) {
327                 $attrib['class'] = 'enigmawarning';
328                 $msg = Q(str_replace('$keyid', enigma_key::format_id($sig->getData('id')),
329                     $this->gettext('signokey')));
330             }
331             else {
332                 $attrib['class'] = 'enigmaerror';
333                 $msg = Q($this->gettext('sigerror'));
334             }
335 /*
336             $msg .= '&nbsp;' . html::a(array('href' => "#sigdetails",
337                 'onclick' => JS_OBJECT_NAME.".command('enigma-sig-details')"),
338                 Q($this->gettext('showdetails')));
339 */
340             // test
341 //            $msg .= '<br /><pre>'.$sig->body.'</pre>';
342
343             $p['prefix'] .= html::div($attrib, $msg);
344
345             // Display each signature message only once
346             unset($this->engine->signatures[$this->engine->signed_parts[$part_id]]);
347         }
348
349         return $p;
350     }
351
352     /**
353      * Handler for plain/text message.
354      *
355      * @param array Reference to hook's parameters (see enigma::parse_structure())
356      */
357     private function parse_plain(&$p)
358     {
359         $this->load_engine();
360         $this->engine->parse_plain($p);
361     }
362     
363     /**
364      * Handler for multipart/signed message.
365      * Verifies signature.
366      *
367      * @param array Reference to hook's parameters (see enigma::parse_structure())
368      */
369     private function parse_signed(&$p)
370     {
371         $this->load_engine();
372         $this->engine->parse_signed($p);
373     }
374
375     /**
376      * Handler for multipart/encrypted and application/pkcs7-mime message.
377      *
378      * @param array Reference to hook's parameters (see enigma::parse_structure())
379      */
380     private function parse_encrypted(&$p)
381     {
382         $this->load_engine();
383         $this->engine->parse_encrypted($p);
384     }
385     
386     /**
387      * Handler for message_load hook.
388      * Check message bodies and attachments for keys/certs.
389      */
390     function message_load($p)
391     {
392         $this->message = $p['object'];
393     
394         // handle attachments vcard attachments
395         foreach ((array)$this->message->attachments as $attachment) {
396             if ($this->is_keys_part($attachment)) {
397                 $this->keys_parts[] = $attachment->mime_id;
398             }
399         }
400         // the same with message bodies
401         foreach ((array)$this->message->parts as $idx => $part) {
402             if ($this->is_keys_part($part)) {
403                 $this->keys_parts[] = $part->mime_id;
404                 $this->keys_bodies[] = $part->mime_id;
405             }
406         }
407         // @TODO: inline PGP keys
408
409         if ($this->keys_parts) {
410             $this->add_texts('localization');
411         }
412     }
413
414     /**
415      * Handler for template_object_messagebody hook.
416      * This callback function adds a box below the message content
417      * if there is a key/cert attachment available
418      */
419     function message_output($p)
420     {
421         $attach_script = false;
422
423         foreach ($this->keys_parts as $part) {
424
425             // remove part's body
426             if (in_array($part, $this->keys_bodies))
427                 $p['content'] = '';
428
429             $style = "margin:0 1em; padding:0.2em 0.5em; border:1px solid #999; width: auto"
430                 ." border-radius:4px; -moz-border-radius:4px; -webkit-border-radius:4px";
431
432             // add box below messsage body
433             $p['content'] .= html::p(array('style' => $style),
434                 html::a(array(
435                     'href' => "#",
436                     'onclick' => "return ".JS_OBJECT_NAME.".enigma_import_attachment('".JQ($part)."')",
437                     'title' => $this->gettext('keyattimport')),
438                     html::img(array('src' => $this->url('skins/default/key_add.png'), 'style' => "vertical-align:middle")))
439                 . ' ' . html::span(null, $this->gettext('keyattfound')));
440
441             $attach_script = true;
442         }
443
444         if ($attach_script) {
445             $this->include_script('enigma.js');
446         }
447
448         return $p;
449     }
450
451     /**
452      * Handler for attached keys/certs import
453      */
454     function import_file()
455     {
456         $this->load_engine();
457         $this->engine->import_file();
458     }
459
460     /**
461      * Checks if specified message part is a PGP-key or S/MIME cert data
462      *
463      * @param rcube_message_part Part object
464      *
465      * @return boolean True if part is a key/cert
466      */
467     private function is_keys_part($part)
468     {
469         // @TODO: S/MIME
470         return (
471             // Content-Type: application/pgp-keys
472             $part->mimetype == 'application/pgp-keys'
473         );
474     }
475 }