Changeset 98c489e in github
- Timestamp:
- Mar 18, 2010 4:53:32 AM (3 years ago)
- Branches:
- master, HEAD, courier-fix, dev-browser-capabilities, pdo, release-0.6, release-0.7, release-0.8
- Children:
- b575fa9
- Parents:
- c16986b
- Files:
-
- 10 edited
-
CHANGELOG (modified) (1 diff)
-
plugins/managesieve/Changelog (modified) (1 diff)
-
plugins/managesieve/lib/rcube_sieve.php (modified) (8 diffs)
-
plugins/managesieve/localization/en_US.inc (modified) (1 diff)
-
plugins/managesieve/localization/pl_PL.inc (modified) (1 diff)
-
plugins/managesieve/managesieve.js (modified) (9 diffs)
-
plugins/managesieve/managesieve.php (modified) (7 diffs)
-
plugins/managesieve/skins/default/managesieve.css (modified) (7 diffs)
-
plugins/managesieve/skins/default/templates/filteredit.html (modified) (1 diff)
-
plugins/managesieve/skins/default/templates/managesieve.html (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
CHANGELOG
rf52c936f r98c489e 2 2 =========================== 3 3 4 - Managesieve: import from Horde-INGO 5 - Managesieve: support for more than one match (#1486078) 6 - Managesieve: support for selectively disabling rules within a single sieve script (#1485882) 4 7 - Threaded message listing now available 5 8 - Added sorting by ARRIVAL and CC -
plugins/managesieve/Changelog
re7b9591 r98c489e 1 * version 2.3 [2010-03-18] 2 ----------------------------------------------------------- 3 - Added import from Horde-INGO 4 - Support for more than one match using if+stop instead of if+elsif structures (#1486078) 5 - Support for selectively disabling rules within a single sieve script (#1485882) 6 - Added vertical splitter 7 1 8 * version 2.2 [2010-02-06] 2 9 ----------------------------------------------------------- -
plugins/managesieve/lib/rcube_sieve.php
ra2cb306 r98c489e 59 59 } 60 60 61 public function __destruct() { 62 $this->sieve->disconnect(); 63 } 64 61 65 /** 62 66 * Getter for error code … … 222 226 $this->script = new rcube_sieve_script($script, $this->disabled); 223 227 224 // ... else try Squirrelmail format 225 if (empty($this->script->content) && $name == 'phpscript') { 226 227 $script = $this->sieve->getScript('phpscript'); 228 $script = $this->_convert_from_squirrel_rules($script); 229 228 // ... else try to import from different formats 229 if (empty($this->script->content)) { 230 $script = $this->_import_rules($script); 230 231 $this->script = new rcube_sieve_script($script, $this->disabled); 231 232 } 233 234 // replace all elsif with if+stop, we support only ifs 235 foreach ($this->script->content as $idx => $rule) { 236 if (!isset($this->script->content[$idx+1]) 237 || preg_match('/^else|elsif$/', $this->script->content[$idx+1]['type'])) { 238 // 'stop' not found? 239 if (!preg_match('/^(stop|vacation)$/', $rule['actions'][count($rule['actions'])-1]['type'])) { 240 $this->script->content[$idx]['actions'][] = array( 241 'type' => 'stop' 242 ); 243 } 244 } 245 } 232 246 233 247 $this->current = $name; … … 255 269 256 270 257 private function _ convert_from_squirrel_rules($script)271 private function _import_rules($script) 258 272 { 259 273 $i = 0; 260 274 $name = array(); 261 275 262 // tokenize rules263 if ($tokens = preg_split('/(#START_SIEVE_RULE.*END_SIEVE_RULE)\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) 276 // Squirrelmail (Avelsieve) 277 if ($tokens = preg_split('/(#START_SIEVE_RULE.*END_SIEVE_RULE)\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) { 264 278 foreach($tokens as $token) { 265 279 if (preg_match('/^#START_SIEVE_RULE.*/', $token, $matches)) { 266 280 $name[$i] = "unnamed rule ".($i+1); 267 $content .= "# rule:[".$name[$i]."]\n";281 $content .= "# rule:[".$name[$i]."]\n"; 268 282 } 269 283 elseif (isset($name[$i])) { … … 272 286 } 273 287 } 288 } 289 // Horde (INGO) 290 else if ($tokens = preg_split('/(# .+)\r?\n/i', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) { 291 foreach($tokens as $token) { 292 if (preg_match('/^# (.+)/i', $token, $matches)) { 293 $name[$i] = $matches[1]; 294 $content .= "# rule:[" . $name[$i] . "]\n"; 295 } 296 elseif (isset($name[$i])) { 297 $token = str_replace(":comparator \"i;ascii-casemap\" ", "", $token); 298 $content .= $token . "\n"; 299 $i++; 300 } 301 } 302 } 274 303 275 304 return $content; … … 294 323 class rcube_sieve_script 295 324 { 296 public $content = array(); // script rules array297 298 private $supported = array( // extensions supported by class299 'fileinto',300 'reject',301 'ereject',302 'vacation', // RFC5230325 public $content = array(); // script rules array 326 327 private $supported = array( // extensions supported by class 328 'fileinto', 329 'reject', 330 'ereject', 331 'vacation', // RFC5230 303 332 // TODO: (most wanted first) body, imapflags, notify, regex 304 333 ); 305 334 306 /**335 /** 307 336 * Object constructor 308 337 * … … 310 339 * @param array Disabled extensions 311 340 */ 312 public function __construct($script, $disabled=NULL)313 { 314 if (!empty($disabled))315 foreach ($disabled as $ext)316 if (($idx = array_search($ext, $this->supported)) !== false)317 unset($this->supported[$idx]);318 319 $this->content = $this->_parse_text($script);320 } 321 322 /**341 public function __construct($script, $disabled=NULL) 342 { 343 if (!empty($disabled)) 344 foreach ($disabled as $ext) 345 if (($idx = array_search($ext, $this->supported)) !== false) 346 unset($this->supported[$idx]); 347 348 $this->content = $this->_parse_text($script); 349 } 350 351 /** 323 352 * Adds script contents as text to the script array (at the end) 324 353 * 325 354 * @param string Text script contents 326 355 */ 327 public function add_text($script) 328 { 329 $content = $this->_parse_text($script); 330 $result = false; 331 332 // check existsing script rules names 333 foreach ($this->content as $idx => $elem) 334 $names[$elem['name']] = $idx; 335 336 foreach ($content as $elem) 337 if (!isset($names[$elem['name']])) 338 { 339 array_push($this->content, $elem); 340 $result = true; 341 } 342 343 return $result; 344 } 345 346 /** 356 public function add_text($script) 357 { 358 $content = $this->_parse_text($script); 359 $result = false; 360 361 // check existsing script rules names 362 foreach ($this->content as $idx => $elem) { 363 $names[$elem['name']] = $idx; 364 } 365 366 foreach ($content as $elem) { 367 if (!isset($names[$elem['name']])) { 368 array_push($this->content, $elem); 369 $result = true; 370 } 371 } 372 373 return $result; 374 } 375 376 /** 347 377 * Adds rule to the script (at the end) 348 378 * … … 350 380 * @param array Rule content (as array) 351 381 */ 352 public function add_rule($content) 353 { 354 // TODO: check this->supported 355 array_push($this->content, $content); 356 return sizeof($this->content)-1; 357 } 358 359 public function delete_rule($index) 360 { 361 if(isset($this->content[$index])) 362 { 363 unset($this->content[$index]); 364 return true; 365 } 366 return false; 367 } 368 369 public function size() 370 { 371 return sizeof($this->content); 372 } 373 374 public function update_rule($index, $content) 375 { 376 // TODO: check this->supported 377 if ($this->content[$index]) 378 { 379 $this->content[$index] = $content; 380 return $index; 381 } 382 return false; 383 } 384 385 /** 382 public function add_rule($content) 383 { 384 // TODO: check this->supported 385 array_push($this->content, $content); 386 return sizeof($this->content)-1; 387 } 388 389 public function delete_rule($index) 390 { 391 if(isset($this->content[$index])) { 392 unset($this->content[$index]); 393 return true; 394 } 395 return false; 396 } 397 398 public function size() 399 { 400 return sizeof($this->content); 401 } 402 403 public function update_rule($index, $content) 404 { 405 // TODO: check this->supported 406 if ($this->content[$index]) { 407 $this->content[$index] = $content; 408 return $index; 409 } 410 return false; 411 } 412 413 /** 386 414 * Returns script as text 387 415 */ 388 public function as_text() 389 { 390 $script = ''; 391 $exts = array(); 392 $idx = 0; 416 public function as_text() 417 { 418 $script = ''; 419 $exts = array(); 420 $idx = 0; 421 422 // rules 423 foreach ($this->content as $rule) { 424 $extension = ''; 425 $tests = array(); 426 $i = 0; 427 428 // header 429 $script .= '# rule:[' . $rule['name'] . "]\n"; 430 431 // constraints expressions 432 foreach ($rule['tests'] as $test) { 433 $tests[$i] = ''; 434 switch ($test['test']) { 435 case 'size': 436 $tests[$i] .= ($test['not'] ? 'not ' : ''); 437 $tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg']; 438 break; 439 case 'true': 440 $tests[$i] .= ($test['not'] ? 'not true' : 'true'); 441 break; 442 case 'exists': 443 $tests[$i] .= ($test['not'] ? 'not ' : ''); 444 if (is_array($test['arg'])) 445 $tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['arg'])) . '"]'; 446 else 447 $tests[$i] .= 'exists "' . $this->_escape_string($test['arg']) . '"'; 448 break; 449 case 'header': 450 $tests[$i] .= ($test['not'] ? 'not ' : ''); 451 $tests[$i] .= 'header :' . $test['type']; 452 if (is_array($test['arg1'])) 453 $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg1'])) . '"]'; 454 else 455 $tests[$i] .= ' "' . $this->_escape_string($test['arg1']) . '"'; 456 if (is_array($test['arg2'])) 457 $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg2'])) . '"]'; 458 else 459 $tests[$i] .= ' "' . $this->_escape_string($test['arg2']) . '"'; 460 break; 461 } 462 $i++; 463 } 464 465 // $script .= ($idx>0 ? 'els' : '').($rule['join'] ? 'if allof (' : 'if anyof ('); 466 // disabled rule: if false #.... 467 $script .= 'if' . ($rule['disabled'] ? ' false #' : ''); 468 $script .= $rule['join'] ? ' allof (' : ' anyof ('; 469 if (sizeof($tests) > 1) 470 $script .= implode(",\n\t", $tests); 471 else if (sizeof($tests)) 472 $script .= $tests[0]; 473 else 474 $script .= 'true'; 475 $script .= ")\n{\n"; 476 477 // action(s) 478 foreach ($rule['actions'] as $action) { 479 switch ($action['type']) { 480 case 'fileinto': 481 $extension = 'fileinto'; 482 $script .= "\tfileinto \"" . $this->_escape_string($action['target']) . "\";\n"; 483 break; 484 case 'redirect': 485 $script .= "\tredirect \"" . $this->_escape_string($action['target']) . "\";\n"; 486 break; 487 case 'reject': 488 case 'ereject': 489 $extension = $action['type']; 490 if (strpos($action['target'], "\n")!==false) 491 $script .= "\t".$action['type']." text:\n" . $action['target'] . "\n.\n;\n"; 492 else 493 $script .= "\t".$action['type']." \"" . $this->_escape_string($action['target']) . "\";\n"; 494 break; 495 case 'keep': 496 case 'discard': 497 case 'stop': 498 $script .= "\t" . $action['type'] .";\n"; 499 break; 500 case 'vacation': 501 $extension = 'vacation'; 502 $script .= "\tvacation"; 503 if ($action['days']) 504 $script .= " :days " . $action['days']; 505 if ($action['addresses']) 506 $script .= " :addresses " . $this->_print_list($action['addresses']); 507 if ($action['subject']) 508 $script .= " :subject \"" . $this->_escape_string($action['subject']) . "\""; 509 if ($action['handle']) 510 $script .= " :handle \"" . $this->_escape_string($action['handle']) . "\""; 511 if ($action['from']) 512 $script .= " :from \"" . $this->_escape_string($action['from']) . "\""; 513 if ($action['mime']) 514 $script .= " :mime"; 515 if (strpos($action['reason'], "\n")!==false) 516 $script .= " text:\n" . $action['reason'] . "\n.\n;\n"; 517 else 518 $script .= " \"" . $this->_escape_string($action['reason']) . "\";\n"; 519 break; 520 } 521 522 if ($extension && !isset($exts[$extension])) 523 $exts[$extension] = $extension; 524 } 525 526 $script .= "}\n"; 527 $idx++; 528 } 393 529 394 // rules 395 foreach ($this->content as $rule) 396 { 397 $extension = ''; 398 $tests = array(); 399 $i = 0; 400 401 // header 402 $script .= '# rule:[' . $rule['name'] . "]\n"; 403 404 // constraints expressions 405 foreach ($rule['tests'] as $test) 406 { 407 $tests[$i] = ''; 408 switch ($test['test']) 409 { 410 case 'size': 411 $tests[$i] .= ($test['not'] ? 'not ' : ''); 412 $tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg']; 413 break; 414 case 'true': 415 $tests[$i] .= ($test['not'] ? 'not true' : 'true'); 416 break; 417 case 'exists': 418 $tests[$i] .= ($test['not'] ? 'not ' : ''); 419 if (is_array($test['arg'])) 420 $tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['arg'])) . '"]'; 421 else 422 $tests[$i] .= 'exists "' . $this->_escape_string($test['arg']) . '"'; 423 break; 424 case 'header': 425 $tests[$i] .= ($test['not'] ? 'not ' : ''); 426 $tests[$i] .= 'header :' . $test['type']; 427 if (is_array($test['arg1'])) 428 $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg1'])) . '"]'; 429 else 430 $tests[$i] .= ' "' . $this->_escape_string($test['arg1']) . '"'; 431 if (is_array($test['arg2'])) 432 $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg2'])) . '"]'; 433 else 434 $tests[$i] .= ' "' . $this->_escape_string($test['arg2']) . '"'; 435 break; 436 } 437 $i++; 530 // requires 531 if (sizeof($exts)) 532 $script = 'require ["' . implode('","', $exts) . "\"];\n" . $script; 533 534 return $script; 535 } 536 537 /** 538 * Returns script object 539 * 540 */ 541 public function as_array() 542 { 543 return $this->content; 544 } 545 546 /** 547 * Returns array of supported extensions 548 * 549 */ 550 public function get_extensions() 551 { 552 return array_values($this->supported); 553 } 554 555 /** 556 * Converts text script to rules array 557 * 558 * @param string Text script 559 */ 560 private function _parse_text($script) 561 { 562 $i = 0; 563 $content = array(); 564 565 // remove C comments 566 $script = preg_replace('|/\*.*?\*/|sm', '', $script); 567 568 // tokenize rules 569 if ($tokens = preg_split('/(# rule:\[.*\])\r?\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) { 570 foreach($tokens as $token) { 571 if (preg_match('/^# rule:\[(.*)\]/', $token, $matches)) { 572 $content[$i]['name'] = $matches[1]; 573 } 574 elseif (isset($content[$i]['name']) && sizeof($content[$i]) == 1) { 575 if ($rule = $this->_tokenize_rule($token)) { 576 $content[$i] = array_merge($content[$i], $rule); 577 $i++; 578 } 579 else // unknown rule format 580 unset($content[$i]); 581 } 438 582 } 439 440 $script .= ($idx>0 ? 'els' : '').($rule['join'] ? 'if allof (' : 'if anyof ('); 441 if (sizeof($tests) > 1) 442 $script .= implode(",\n\t", $tests); 443 elseif (sizeof($tests)) 444 $script .= $tests[0]; 445 else 446 $script .= 'true'; 447 $script .= ")\n{\n"; 448 449 // action(s) 450 foreach ($rule['actions'] as $action) 451 { 452 switch ($action['type']) 453 { 454 case 'fileinto': 455 $extension = 'fileinto'; 456 $script .= "\tfileinto \"" . $this->_escape_string($action['target']) . "\";\n"; 457 break; 458 case 'redirect': 459 $script .= "\tredirect \"" . $this->_escape_string($action['target']) . "\";\n"; 460 break; 461 case 'reject': 462 case 'ereject': 463 $extension = $action['type']; 464 if (strpos($action['target'], "\n")!==false) 465 $script .= "\t".$action['type']." text:\n" . $action['target'] . "\n.\n;\n"; 466 else 467 $script .= "\t".$action['type']." \"" . $this->_escape_string($action['target']) . "\";\n"; 468 break; 469 case 'keep': 470 case 'discard': 471 case 'stop': 472 $script .= "\t" . $action['type'] .";\n"; 473 break; 474 case 'vacation': 475 $extension = 'vacation'; 476 $script .= "\tvacation"; 477 if ($action['days']) 478 $script .= " :days " . $action['days']; 479 if ($action['addresses']) 480 $script .= " :addresses " . $this->_print_list($action['addresses']); 481 if ($action['subject']) 482 $script .= " :subject \"" . $this->_escape_string($action['subject']) . "\""; 483 if ($action['handle']) 484 $script .= " :handle \"" . $this->_escape_string($action['handle']) . "\""; 485 if ($action['from']) 486 $script .= " :from \"" . $this->_escape_string($action['from']) . "\""; 487 if ($action['mime']) 488 $script .= " :mime"; 489 if (strpos($action['reason'], "\n")!==false) 490 $script .= " text:\n" . $action['reason'] . "\n.\n;\n"; 491 else 492 $script .= " \"" . $this->_escape_string($action['reason']) . "\";\n"; 493 break; 494 } 495 496 if ($extension && !isset($exts[$extension])) 497 $exts[$extension] = $extension; 498 } 499 500 $script .= "}\n"; 501 $idx++; 502 } 503 504 // requires 505 if (sizeof($exts)) 506 $script = 'require ["' . implode('","', $exts) . "\"];\n" . $script; 507 508 return $script; 509 } 510 511 /** 512 * Returns script object 513 * 514 */ 515 public function as_array() 516 { 517 return $this->content; 518 } 519 520 /** 521 * Returns array of supported extensions 522 * 523 */ 524 public function get_extensions() 525 { 526 return array_values($this->supported); 527 } 528 529 /** 530 * Converts text script to rules array 531 * 532 * @param string Text script 533 */ 534 private function _parse_text($script) 535 { 536 $i = 0; 537 $content = array(); 538 539 // remove C comments 540 $script = preg_replace('|/\*.*?\*/|sm', '', $script); 541 542 // tokenize rules 543 if ($tokens = preg_split('/(# rule:\[.*\])\r?\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) 544 foreach($tokens as $token) 545 { 546 if (preg_match('/^# rule:\[(.*)\]/', $token, $matches)) 547 { 548 $content[$i]['name'] = $matches[1]; 549 } 550 elseif (isset($content[$i]['name']) && sizeof($content[$i]) == 1) 551 { 552 if ($rule = $this->_tokenize_rule($token)) 553 { 554 $content[$i] = array_merge($content[$i], $rule); 555 $i++; 556 } 557 else // unknown rule format 558 unset($content[$i]); 559 } 560 } 561 562 return $content; 563 } 564 565 /** 583 } 584 585 return $content; 586 } 587 588 /** 566 589 * Convert text script fragment to rule object 567 590 * 568 591 * @param string Text rule 569 592 */ 570 private function _tokenize_rule($content)571 { 572 $result = NULL;593 private function _tokenize_rule($content) 594 { 595 $result = NULL; 573 596 574 if (preg_match('/^(if|elsif|else)\s+((true|not\s+true|allof|anyof|exists|header|not|size)(.*))\s+\{(.*)\}$/sm', trim($content), $matches)) 575 { 576 list($tests, $join) = $this->_parse_tests(trim($matches[2])); 577 $actions = $this->_parse_actions(trim($matches[5])); 578 579 if ($tests && $actions) 580 $result = array( 597 if (preg_match('/^(if|elsif|else)\s+((true|false|not\s+true|allof|anyof|exists|header|not|size)(.*))\s+\{(.*)\}$/sm', 598 trim($content), $matches)) { 599 600 $tests = trim($matches[2]); 601 602 // disabled rule (false + comment): if false #..... 603 if ($matches[3] == 'false') { 604 $tests = preg_replace('/^false\s+#\s+/', '', $tests); 605 $disabled = true; 606 } 607 else 608 $disabled = false; 609 610 list($tests, $join) = $this->_parse_tests($tests); 611 $actions = $this->_parse_actions(trim($matches[5])); 612 613 if ($tests && $actions) 614 $result = array( 615 'type' => $matches[1], 581 616 'tests' => $tests, 582 617 'actions' => $actions, 583 618 'join' => $join, 584 ); 585 } 586 587 return $result; 619 'disabled' => $disabled, 620 ); 621 } 622 623 return $result; 588 624 } 589 625 590 /**626 /** 591 627 * Parse body of actions section 592 628 * … … 594 630 * @return array Array of parsed action type/target pairs 595 631 */ 596 private function _parse_actions($content) 597 { 598 $result = NULL; 599 600 // supported actions 601 $patterns[] = '^\s*discard;'; 602 $patterns[] = '^\s*keep;'; 603 $patterns[] = '^\s*stop;'; 604 $patterns[] = '^\s*redirect\s+(.*?[^\\\]);'; 605 if (in_array('fileinto', $this->supported)) 606 $patterns[] = '^\s*fileinto\s+(.*?[^\\\]);'; 607 if (in_array('reject', $this->supported)) { 608 $patterns[] = '^\s*reject\s+text:(.*)\n\.\n;'; 609 $patterns[] = '^\s*reject\s+(.*?[^\\\]);'; 610 $patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;'; 611 $patterns[] = '^\s*ereject\s+(.*?[^\\\]);'; 612 } 613 if (in_array('vacation', $this->supported)) 614 $patterns[] = '^\s*vacation\s+(.*?[^\\\]);'; 615 616 $pattern = '/(' . implode('$)|(', $patterns) . '$)/ms'; 617 618 // parse actions body 619 if (preg_match_all($pattern, $content, $mm, PREG_SET_ORDER)) 620 { 621 foreach ($mm as $m) 622 { 623 $content = trim($m[0]); 632 private function _parse_actions($content) 633 { 634 $result = NULL; 635 636 // supported actions 637 $patterns[] = '^\s*discard;'; 638 $patterns[] = '^\s*keep;'; 639 $patterns[] = '^\s*stop;'; 640 $patterns[] = '^\s*redirect\s+(.*?[^\\\]);'; 641 if (in_array('fileinto', $this->supported)) 642 $patterns[] = '^\s*fileinto\s+(.*?[^\\\]);'; 643 if (in_array('reject', $this->supported)) { 644 $patterns[] = '^\s*reject\s+text:(.*)\n\.\n;'; 645 $patterns[] = '^\s*reject\s+(.*?[^\\\]);'; 646 $patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;'; 647 $patterns[] = '^\s*ereject\s+(.*?[^\\\]);'; 648 } 649 if (in_array('vacation', $this->supported)) 650 $patterns[] = '^\s*vacation\s+(.*?[^\\\]);'; 651 652 $pattern = '/(' . implode('$)|(', $patterns) . '$)/ms'; 653 654 // parse actions body 655 if (preg_match_all($pattern, $content, $mm, PREG_SET_ORDER)) { 656 foreach ($mm as $m) { 657 $content = trim($m[0]); 624 658 625 if(preg_match('/^(discard|keep|stop)/', $content, $matches)) 626 { 627 $result[] = array('type' => $matches[1]); 628 } 629 elseif(preg_match('/^fileinto/', $content)) 630 { 631 $result[] = array('type' => 'fileinto', 'target' => $this->_parse_string($m[sizeof($m)-1])); 632 } 633 elseif(preg_match('/^redirect/', $content)) 634 { 635 $result[] = array('type' => 'redirect', 'target' => $this->_parse_string($m[sizeof($m)-1])); 636 } 637 elseif(preg_match('/^(reject|ereject)\s+(.*);$/sm', $content, $matches)) 638 { 639 $result[] = array('type' => $matches[1], 'target' => $this->_parse_string($matches[2])); 640 } 641 elseif(preg_match('/^vacation\s+(.*);$/sm', $content, $matches)) 642 { 643 $vacation = array('type' => 'vacation'); 644 645 if (preg_match('/:(days)\s+([0-9]+)/', $content, $vm)) { 646 $vacation['days'] = $vm[2]; 647 $content = preg_replace('/:(days)\s+([0-9]+)/', '', $content); 648 } 649 if (preg_match('/:(subject)\s+(".*?[^\\\]")/', $content, $vm)) { 650 $vacation['subject'] = $vm[2]; 651 $content = preg_replace('/:(subject)\s+(".*?[^\\\]")/', '', $content); 652 } 653 if (preg_match('/:(addresses)\s+\[(.*?[^\\\])\]/', $content, $vm)) { 654 $vacation['addresses'] = $this->_parse_list($vm[2]); 655 $content = preg_replace('/:(addresses)\s+\[(.*?[^\\\])\]/', '', $content); 656 } 657 if (preg_match('/:(handle)\s+(".*?[^\\\]")/', $content, $vm)) { 658 $vacation['handle'] = $vm[2]; 659 $content = preg_replace('/:(handle)\s+(".*?[^\\\]")/', '', $content); 660 } 661 if (preg_match('/:(from)\s+(".*?[^\\\]")/', $content, $vm)) { 662 $vacation['from'] = $vm[2]; 663 $content = preg_replace('/:(from)\s+(".*?[^\\\]")/', '', $content); 664 } 665 $content = preg_replace('/^vacation/', '', $content); 666 $content = preg_replace('/;$/', '', $content); 667 $content = trim($content); 668 if (preg_match('/^:(mime)/', $content, $vm)) { 669 $vacation['mime'] = true; 670 $content = preg_replace('/^:mime/', '', $content); 671 } 672 673 $vacation['reason'] = $this->_parse_string($content); 674 675 $result[] = $vacation; 659 if(preg_match('/^(discard|keep|stop)/', $content, $matches)) { 660 $result[] = array('type' => $matches[1]); 661 } 662 elseif(preg_match('/^fileinto/', $content)) { 663 $result[] = array('type' => 'fileinto', 'target' => $this->_parse_string($m[sizeof($m)-1])); 664 } 665 elseif(preg_match('/^redirect/', $content)) { 666 $result[] = array('type' => 'redirect', 'target' => $this->_parse_string($m[sizeof($m)-1])); 667 } 668 elseif(preg_match('/^(reject|ereject)\s+(.*);$/sm', $content, $matches)) { 669 $result[] = array('type' => $matches[1], 'target' => $this->_parse_string($matches[2])); 670 } 671 elseif(preg_match('/^vacation\s+(.*);$/sm', $content, $matches)) { 672 $vacation = array('type' => 'vacation'); 673 674 if (preg_match('/:(days)\s+([0-9]+)/', $content, $vm)) { 675 $vacation['days'] = $vm[2]; 676 $content = preg_replace('/:(days)\s+([0-9]+)/', '', $content); 677 } 678 if (preg_match('/:(subject)\s+(".*?[^\\\]")/', $content, $vm)) { 679 $vacation['subject'] = $vm[2]; 680 $content = preg_replace('/:(subject)\s+(".*?[^\\\]")/', '', $content); 681 } 682 if (preg_match('/:(addresses)\s+\[(.*?[^\\\])\]/', $content, $vm)) { 683 $vacation['addresses'] = $this->_parse_list($vm[2]); 684 $content = preg_replace('/:(addresses)\s+\[(.*?[^\\\])\]/', '', $content); 685 } 686 if (preg_match('/:(handle)\s+(".*?[^\\\]")/', $content, $vm)) { 687 $vacation['handle'] = $vm[2]; 688 $content = preg_replace('/:(handle)\s+(".*?[^\\\]")/', '', $content); 689 } 690 if (preg_match('/:(from)\s+(".*?[^\\\]")/', $content, $vm)) { 691 $vacation['from'] = $vm[2]; 692 $content = preg_replace('/:(from)\s+(".*?[^\\\]")/', '', $content); 693 } 694 695 $content = preg_replace('/^vacation/', '', $content); 696 $content = preg_replace('/;$/', '', $content); 697 $content = trim($content); 698 699 if (preg_match('/^:(mime)/', $content, $vm)) { 700 $vacation['mime'] = true; 701 $content = preg_replace('/^:mime/', '', $content); 702 } 703 704 $vacation['reason'] = $this->_parse_string($content); 705 706 $result[] = $vacation; 707 } 676 708 } 677 709 } 678 } 679 680 return $result; 710 711 return $result; 681 712 } 682 713 683 /**714 /** 684 715 * Parse test/conditions section 685 716 * 686 717 * @param string Text 687 718 */ 688 689 private function _parse_tests($content) 690 { 691 $result = NULL; 692 693 // lists 694 if (preg_match('/^(allof|anyof)\s+\((.*)\)$/sm', $content, $matches)) 695 { 696 $content = $matches[2]; 697 $join = $matches[1]=='allof' ? true : false; 698 } 699 else 700 $join = false; 719 private function _parse_tests($content) 720 { 721 $result = NULL; 722 723 // lists 724 if (preg_match('/^(allof|anyof)\s+\((.*)\)$/sm', $content, $matches)) { 725 $content = $matches[2]; 726 $join = $matches[1]=='allof' ? true : false; 727 } 728 else 729 $join = false; 701 730 702 // supported tests regular expressions703 // TODO: comparators, envelope704 $patterns[] = '(not\s+)?(exists)\s+\[(.*?[^\\\])\]';705 $patterns[] = '(not\s+)?(exists)\s+(".*?[^\\\]")';706 $patterns[] = '(not\s+)?(true)';707 $patterns[] = '(not\s+)?(size)\s+:(under|over)\s+([0-9]+[KGM]{0,1})';708 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]';709 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+(".*?[^\\\]")';710 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")';711 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]';731 // supported tests regular expressions 732 // TODO: comparators, envelope 733 $patterns[] = '(not\s+)?(exists)\s+\[(.*?[^\\\])\]'; 734 $patterns[] = '(not\s+)?(exists)\s+(".*?[^\\\]")'; 735 $patterns[] = '(not\s+)?(true)'; 736 $patterns[] = '(not\s+)?(size)\s+:(under|over)\s+([0-9]+[KGM]{0,1})'; 737 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]'; 738 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+(".*?[^\\\]")'; 739 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")'; 740 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]'; 712 741 713 // join patterns... 714 $pattern = '/(' . implode(')|(', $patterns) . ')/'; 715 716 // ...and parse tests list 717 if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) 718 { 719 foreach ($matches as $match) 720 { 721 $size = sizeof($match); 742 // join patterns... 743 $pattern = '/(' . implode(')|(', $patterns) . ')/'; 744 745 // ...and parse tests list 746 if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) { 747 foreach ($matches as $match) { 748 $size = sizeof($match); 722 749 723 if (preg_match('/^(not\s+)?size/', $match[0])) 724 { 725 $result[] = array( 726 'test' => 'size', 727 'not' => $match[$size-4] ? true : false, 728 'type' => $match[$size-2], // under/over 729 'arg' => $match[$size-1], // value 730 ); 750 if (preg_match('/^(not\s+)?size/', $match[0])) { 751 $result[] = array( 752 'test' => 'size', 753 'not' => $match[$size-4] ? true : false, 754 'type' => $match[$size-2], // under/over 755 'arg' => $match[$size-1], // value 756 ); 731 757 } 732 elseif (preg_match('/^(not\s+)?header/', $match[0])) 733 { 734 $result[] = array( 735 'test' => 'header', 736 'not' => $match[$size-5] ? true : false, 737 'type' => $match[$size-3], // is/contains/matches 738 'arg1' => $this->_parse_list($match[$size-2]), // header(s) 739 'arg2' => $this->_parse_list($match[$size-1]), // string(s) 740 ); 758 elseif (preg_match('/^(not\s+)?header/', $match[0])) { 759 $result[] = array( 760 'test' => 'header', 761 'not' => $match[$size-5] ? true : false, 762 'type' => $match[$size-3], // is/contains/matches 763 'arg1' => $this->_parse_list($match[$size-2]), // header(s) 764 'arg2' => $this->_parse_list($match[$size-1]), // string(s) 765 ); 741 766 } 742 elseif (preg_match('/^(not\s+)?exists/', $match[0])) 743 { 744 $result[] = array( 745 'test' => 'exists', 746 'not' => $match[$size-3] ? true : false, 747 'arg' => $this->_parse_list($match[$size-1]), // header(s) 748 ); 767 elseif (preg_match('/^(not\s+)?exists/', $match[0])) { 768 $result[] = array( 769 'test' => 'exists', 770 'not' => $match[$size-3] ? true : false, 771 'arg' => $this->_parse_list($match[$size-1]), // header(s) 772 ); 749 773 } 750 elseif (preg_match('/^(not\s+)?true/', $match[0])) 751 { 752 $result[] = array( 753 'test' => 'true', 754 'not' => $match[$size-2] ? true : false, 755 ); 774 elseif (preg_match('/^(not\s+)?true/', $match[0])) { 775 $result[] = array( 776 'test' => 'true', 777 'not' => $match[$size-2] ? true : false, 778 ); 756 779 } 757 780 } 758 781 } 759 782 760 return array($result, $join);783 return array($result, $join); 761 784 } 762 785 763 /**786 /** 764 787 * Parse string value 765 788 * 766 789 * @param string Text 767 790 */ 768 private function _parse_string($content)769 { 770 $text = '';771 $content = trim($content);772 773 if (preg_match('/^text:(.*)\.$/sm', $content, $matches))774 $text = trim($matches[1]);775 elseif (preg_match('/^"(.*)"$/', $content, $matches))776 $text = str_replace('\"', '"', $matches[1]);777 778 return $text;791 private function _parse_string($content) 792 { 793 $text = ''; 794 $content = trim($content); 795 796 if (preg_match('/^text:(.*)\.$/sm', $content, $matches)) 797 $text = trim($matches[1]); 798 elseif (preg_match('/^"(.*)"$/', $content, $matches)) 799 $text = str_replace('\"', '"', $matches[1]); 800 801 return $text; 779 802 } 780 803 781 /**804 /** 782 805 * Escape special chars in string value 783 806 * 784 807 * @param string Text 785 808 */ 786 private function _escape_string($content)787 { 788 $replace['/"/'] = '\\"';809 private function _escape_string($content) 810 { 811 $replace['/"/'] = '\\"'; 789 812 790 if (is_array($content)) 791 { 792 for ($x=0, $y=sizeof($content); $x<$y; $x++) 793 $content[$x] = preg_replace(array_keys($replace), array_values($replace), $content[$x]); 813 if (is_array($content)) { 814 for ($x=0, $y=sizeof($content); $x<$y; $x++) 815 $content[$x] = preg_replace(array_keys($replace), array_values($replace), $content[$x]); 794 816 795 return $content;796 } 797 else798 return preg_replace(array_keys($replace), array_values($replace), $content);799 } 800 801 /**817 return $content; 818 } 819 else 820 return preg_replace(array_keys($replace), array_values($replace), $content); 821 } 822 823 /** 802 824 * Parse string or list of strings to string or array of strings 803 825 * 804 826 * @param string Text 805 827 */ 806 private function _parse_list($content)807 { 808 $result = array();828 private function _parse_list($content) 829 { 830 $result = array(); 809 831 810 for ($x=0, $len=strlen($content); $x<$len; $x++) 811 { 812 switch ($content[$x]) 813 { 814 case '\\': 815 $str .= $content[++$x]; 816 break; 817 case '"': 818 if (isset($str)) 819 { 820 $result[] = $str; 821 unset($str); 822 } 823 else 824 $str = ''; 825 break; 826 default: 827 if(isset($str)) 828 $str .= $content[$x]; 829 break; 832 for ($x=0, $len=strlen($content); $x<$len; $x++) { 833 switch ($content[$x]) { 834 case '\\': 835 $str .= $content[++$x]; 836 break; 837 case '"': 838 if (isset($str)) { 839 $result[] = $str; 840 unset($str); 841 } 842 else 843 $str = ''; 844 break; 845 default: 846 if(isset($str)) 847 $str .= $content[$x]; 848 break; 830 849 } 831 850 } 832 851 833 if (sizeof($result)>1)834 return $result;835 elseif (sizeof($result) == 1)836 return $result[0];837 else838 return NULL;852 if (sizeof($result)>1) 853 return $result; 854 elseif (sizeof($result) == 1) 855 return $result[0]; 856 else 857 return NULL; 839 858 } 840 859 841 /**860 /** 842 861 * Convert array of elements to list of strings 843 862 * 844 863 * @param string Text 845 864 */ 846 private function _print_list($list)847 { 848 $list = (array) $list;849 foreach($list as $idx => $val)850 $list[$idx] = $this->_escape_string($val);865 private function _print_list($list) 866 { 867 $list = (array) $list; 868 foreach($list as $idx => $val) 869 $list[$idx] = $this->_escape_string($val); 851 870 852 return '["' . implode('","', $list) . '"]';871 return '["' . implode('","', $list) . '"]'; 853 872 } 854 873 } -
plugins/managesieve/localization/en_US.inc
rac67db1 r98c489e 47 47 $labels['copyfromset'] = 'Copy filters from set'; 48 48 $labels['none'] = '- none -'; 49 $labels['filterdisabled'] = 'Filter disabled'; 49 50 50 51 $messages = array(); -
plugins/managesieve/localization/pl_PL.inc
rac67db1 r98c489e 57 57 $labels['copyfromset'] = 'Skopiuj filtry ze zbioru'; 58 58 $labels['none'] = '- brak -'; 59 $labels['filterdisabled'] = 'Filtr wyÅÄ 60 czony'; 59 61 60 62 $messages = array(); -
plugins/managesieve/managesieve.js
r53d3b0f r98c489e 27 27 if (rcmail.env.action == 'plugin.managesieve') 28 28 { 29 if (rcmail.gui_objects.sieveform) 29 if (rcmail.gui_objects.sieveform) { 30 30 rcmail.enable_command('plugin.managesieve-save', true); 31 else { 31 } 32 else { 32 33 rcmail.enable_command('plugin.managesieve-del', 'plugin.managesieve-up', 33 34 'plugin.managesieve-down', false); … … 48 49 } 49 50 } 51 if (rcmail.gui_objects.sieveform && rcmail.env.rule_disabled) 52 $('#disabled').attr('checked', true); 50 53 }); 51 54 … … 92 95 } 93 96 94 rcube_webmail.prototype.managesieve_updatelist = function(action, name, id )97 rcube_webmail.prototype.managesieve_updatelist = function(action, name, id, disabled) 95 98 { 96 99 this.set_busy(true); … … 115 118 case 'down': 116 119 var rows = this.filters_list.rows; 117 var from ;120 var from, fromstatus, status; 118 121 119 122 // we need only to replace filter names... … … 123 126 continue; 124 127 } else if (rows[i].uid == id) { 125 from = rows[i].obj.cells[0]; 128 from = rows[i].obj; 129 fromstatus = $(from).hasClass('disabled'); 126 130 } else if (rows[i].uid == id+1){ 127 131 name = rows[i].obj.cells[0].innerHTML; 128 rows[i].obj.cells[0].innerHTML = from.innerHTML; 129 from.innerHTML = name; 132 status = $(rows[i].obj).hasClass('disabled'); 133 rows[i].obj.cells[0].innerHTML = from.cells[0].innerHTML; 134 from.cells[0].innerHTML = name; 135 $(from)[status?'addClass':'removeClass']('disabled'); 136 $(rows[i].obj)[fromstatus?'addClass':'removeClass']('disabled'); 130 137 this.filters_list.highlight_row(i); 131 138 break; … … 138 145 case 'up': 139 146 var rows = this.filters_list.rows; 140 var from ;147 var from, status, fromstatus; 141 148 142 149 // we need only to replace filter names... … … 146 153 continue; 147 154 } else if (rows[i].uid == id-1) { 148 from = rows[i].obj.cells[0]; 155 from = rows[i].obj; 156 fromstatus = $(from).hasClass('disabled'); 149 157 this.filters_list.highlight_row(i); 150 158 } else if (rows[i].uid == id) { 151 159 name = rows[i].obj.cells[0].innerHTML; 152 rows[i].obj.cells[0].innerHTML = from.innerHTML; 153 from.innerHTML = name; 160 status = $(rows[i].obj).hasClass('disabled'); 161 rows[i].obj.cells[0].innerHTML = from.cells[0].innerHTML; 162 from.cells[0].innerHTML = name; 163 $(from)[status?'addClass':'removeClass']('disabled'); 164 $(rows[i].obj)[fromstatus?'addClass':'removeClass']('disabled'); 154 165 break; 155 166 } … … 165 176 { 166 177 rows[i].obj.cells[0].innerHTML = name; 178 if (disabled) 179 $(rows[i].obj).addClass('disabled'); 180 else 181 $(rows[i].obj).removeClass('disabled'); 167 182 break; 168 183 } … … 187 202 new_row.appendChild(td); 188 203 list.insert_row(new_row, false); 204 if (disabled) 205 $(new_row).addClass('disabled'); 189 206 190 207 if (row.cells[0].className) -
plugins/managesieve/managesieve.php
r929a508 r98c489e 8 8 * with server using managesieve protocol. Adds Filters tab in Settings. 9 9 * 10 * @version 2. 210 * @version 2.3 11 11 * @author Aleksander 'A.L.E.C' Machniak <alec@alec.pl> 12 12 * … … 47 47 function managesieve_start() 48 48 { 49 $rcmail = rcmail::get_instance(); 50 $this->rc = &$rcmail; 51 49 $this->rc = rcmail::get_instance(); 52 50 $this->load_config(); 53 51 … … 303 301 $items[] = $item; 304 302 303 $this->form['disabled'] = $_POST['_disabled'] ? true : false; 305 304 $this->form['join'] = $join=='allof' ? true : false; 306 305 $this->form['name'] = $name; … … 487 486 { 488 487 $this->rc->output->show_message('managesieve.filtersaved', 'confirmation'); 489 $this->rc->output->add_script(sprintf("rcmail.managesieve_updatelist('%s', '%s', %d);", 490 isset($new) ? 'add' : 'update', Q($this->form['name']), $fid), 'foot'); 488 $this->rc->output->add_script( 489 sprintf("rcmail.managesieve_updatelist('%s', '%s', %d, %d);", 490 isset($new) ? 'add' : 'update', Q($this->form['name']), $fid, $this->form['disabled']), 491 'foot'); 491 492 } 492 493 else … … 505 506 // Handle form action 506 507 if (isset($_GET['_framed']) || isset($_POST['_framed'])) { 507 if (isset($_GET['_newset']) || isset($_POST['_newset'])) 508 if (isset($_GET['_newset']) || isset($_POST['_newset'])) { 508 509 $this->rc->output->send('managesieve.setedit'); 509 else 510 } 511 else { 510 512 $this->rc->output->send('managesieve.filteredit'); 513 } 511 514 } else { 512 515 $this->rc->output->set_pagetitle($this->gettext('filters')); … … 526 529 527 530 foreach($this->script as $idx => $filter) 528 $result[] = array('managesieve.filtername' => $filter['name'], 'id' => $idx); 529 531 $result[] = array( 532 'managesieve.filtername' => $filter['name'], 533 'id' => $idx, 534 'class' => $filter['disabled'] ? 'disabled' : '', 535 ); 536 530 537 // create XHTML table 531 538 $out = rcube_table_output($attrib, $result, $a_show_cols, 'id'); … … 723 730 724 731 $out .= "</fieldset>\n"; 725 732 733 if ($scr['disabled']) { 734 $this->rc->output->set_env('rule_disabled', true); 735 } 726 736 $this->rc->output->add_label('managesieve.ruledeleteconfirm'); 727 737 $this->rc->output->add_label('managesieve.actiondeleteconfirm'); -
plugins/managesieve/skins/default/managesieve.css
r0b53ec7 r98c489e 6 6 position: absolute; 7 7 left: 20px; 8 width: 220px;9 8 top: 120px; 10 9 bottom: 30px; … … 29 28 } 30 29 30 #filters-table tbody tr.disabled td 31 { 32 color: #999999; 33 } 34 31 35 #filtersbuttons 32 36 { … … 39 43 { 40 44 position: absolute; 41 left: 2 50px;45 left: 230px; 42 46 top: 85px; 43 47 } … … 132 136 { 133 137 position: absolute; 134 left: 3 80px;138 left: 360px; 135 139 top: 90px; 136 140 } … … 140 144 position: absolute; 141 145 top: 120px; 142 left: 250px;143 146 right: 20px; 144 147 bottom: 30px; … … 171 174 } 172 175 173 #filter-form input, select174 {175 font-size: 10pt;176 font-family: inherit;177 }178 179 176 fieldset 180 177 { … … 249 246 white-space: nowrap; 250 247 } 248 249 #footer 250 { 251 padding-top: 5px; 252 width: 100%; 253 } 254 255 #footer .footerleft 256 { 257 padding-left: 2px; 258 white-space: nowrap; 259 float: left; 260 } 261 262 #footer .footerright 263 { 264 padding-right: 2px; 265 white-space: nowrap; 266 text-align: right; 267 float: right; 268 } -
plugins/managesieve/skins/default/templates/filteredit.html
rac67db1 r98c489e 100 100 <roundcube:object name="filterform" /> 101 101 102 <p> 102 <div id="footer"> 103 <div class="footerleft"> 103 104 <roundcube:button command="plugin.managesieve-save" type="input" class="button mainaction" label="save" /> 104 </p> 105 </div> 106 <div class="footerright"> 107 <label for="disabled"><roundcube:label name="managesieve.filterdisabled" /></label> 108 <input type="checkbox" id="disabled" name="_disabled" value="1" /> 109 </div> 110 </div> 105 111 106 112 </form> -
plugins/managesieve/skins/default/templates/managesieve.html
rac67db1 r98c489e 6 6 <link rel="stylesheet" type="text/css" href="/this/managesieve.css" /> 7 7 <script type="text/javascript" src="/functions.js"></script> 8 <script type="text/javascript" src="/splitter.js"></script> 9 10 <style type="text/css"> 11 #filterslist { width: <roundcube:exp expression="!empty(cookie:sieveviewsplitter) ? cookie:sieveviewsplitter-5 : 210" />px; } 12 #filter-box { left: <roundcube:exp expression="!empty(cookie:sieveviewsplitter) ? cookie:sieveviewsplitter+5 : 220" />px; 13 <roundcube:exp expression="browser:ie ? ('width:expression((parseInt(this.parentNode.offsetWidth)-'.(!empty(cookie:sieveviewsplitter) ? cookie:sieveviewsplitter+5 : 220).')+\\'px\\');') : ''" /> 14 } 15 </style> 16 8 17 </head> 9 18 <body> … … 33 42 <roundcube:object name="filterslist" id="filters-table" class="records-table" cellspacing="0" summary="Filters list" /> 34 43 </div> 35 44 <script type="text/javascript"> 45 var sieveviewsplit = new rcube_splitter({id:'sieveviewsplitter', p1: 'filterslist', p2: 'filter-box', orientation: 'v', relative: true, start: 215}); 46 rcmail.add_onload('sieveviewsplit.init()'); 47 </script> 36 48 <div id="filter-box"> 37 49 <roundcube:object name="filterframe" id="filter-frame" width="100%" height="100%" frameborder="0" src="/watermark.html" />
Note: See TracChangeset
for help on using the changeset viewer.
