File Coverage

lib/Net/Sieve/Script.pm
Criterion Covered Total %
statement 160 191 83.7
branch 51 88 57.9
condition 11 15 73.3
subroutine 17 18 94.4
pod 10 10 100.0
total 249 322 77.3


line stmt bran cond sub pod time code
1             package Net::Sieve::Script;
2 5     5   267527 use strict;
  5         10  
  5         133  
3 5     5   26 use warnings;
  5         9  
  5         162  
4              
5             BEGIN {
6 5     5   23 use Exporter ();
  5         12  
  5         99  
7 5     5   24 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  5         16  
  5         479  
8 5     5   16 $VERSION = '0.09';
9 5         52 @ISA = qw(Exporter);
10             #Give a hoot don't pollute, do not export more than needed by default
11 5         14 @EXPORT = qw(_strip);
12 5         8 @EXPORT_OK = qw(_strip);
13 5         102 %EXPORT_TAGS = ();
14             }
15              
16 5     5   27 use base qw(Class::Accessor::Fast);
  5         8  
  5         1470  
17 5     5   11672 use Net::Sieve::Script::Rule;
  5         13  
  5         22  
18              
19             =head1 NAME
20              
21             Net::Sieve::Script - Parse and write sieve scripts
22              
23             =head1 SYNOPSIS
24              
25             use Net::Sieve::Script;
26              
27             my $test_script = 'require "fileinto";
28             # Place all these in the "Test" folder
29             if header :contains "Subject" "[Test]" {
30             fileinto "Test";
31             }';
32              
33             my $script = Net::Sieve::Script->new ($test_script);
34              
35             print "OK" if ( $script->parsing_ok ) ;
36              
37             print $script->write_script;
38              
39             or
40              
41             my $script = Net::Sieve::Script->new();
42              
43             my $cond = Net::Sieve::Script::Condition->new('header');
44             $cond->match_type(':contains');
45             $cond->header_list('"Subject"');
46             $cond->key_list('"Re: Test2"');
47              
48             my $actions = 'fileinto "INBOX.test"; stop;';
49              
50             my $rule = Net::Sieve::Script::Rule->new();
51             $rule->add_condition($cond);
52             $rule->add_action($actions);
53              
54             $script->add_rule($rule);
55              
56             print $script->write_script;
57              
58              
59              
60             =head1 DESCRIPTION
61              
62             Manage sieve script
63              
64             Read and parse file script, make L, L, L objects
65              
66             Write sieve script
67              
68             Support RFC 5228 - sieve base
69             RFC 5231 - relationnal
70             RFC 5230 - vacation
71             Draft regex
72              
73             missing
74             5229 variables
75             5232 imapflags
76             5233 subaddress
77             5235 spamtest
78             notify draft
79              
80             =cut
81              
82             __PACKAGE__->mk_accessors(qw(raw rules require max_priority));
83              
84             =head1 CONSTRUCTOR
85              
86             =head2 new
87              
88             Argument : optional text script
89             Purpose : if param, put script in raw, parse script
90             Return : main Script object
91              
92             Accessors :
93              
94             ->raw() : read or set original text script
95             ->require() : require part of script
96             ->rules() : array of rules
97             ->max_priority() : last rule id
98              
99             =cut
100              
101             sub new
102             {
103 8     8 1 2330 my ($class, $param) = @_;
104              
105 8   33     45 my $self = bless ({}, ref ($class) || $class);
106 8         22 my @LISTS = qw((\[.*?\]|".*?"));
107              
108 8 100       18 if ($param) {
109 2         9 $self->raw($param);
110 2 100       71 $self->require($1) if ( $param =~ m/require @LISTS;/si );
111 2         12 $self->read_rules();
112             }
113              
114             # break if more than 50 rules
115 8 50 66     27 die "50 rules does not sound reasonable !"
116             if ( $self->max_priority() && $self->max_priority() >= 50 );
117              
118 8         93 return $self;
119             }
120              
121             =head1 METHODS
122              
123             =head2 parsing_ok
124              
125             return 1 on raw parsing success
126              
127             =cut
128              
129             sub parsing_ok
130             {
131 1     1 1 4 my $self = shift;
132              
133 1         1 return ( $self->_strip eq _strip($self->write_script) );
134             }
135              
136             =head2 write_script
137              
138             Purpose : write full script, require and rules parts
139              
140             Return : set current require,
141             return rules ordered by priority in text format
142              
143             =cut
144              
145             sub write_script {
146 7     7 1 475 my $self = shift;
147 7         9 my $text;
148 7         11 my %require = ();
149              
150 7         10 foreach my $rule ( sort { $a->priority() <=> $b->priority() } @{$self->rules()} ) {
  17         76  
  7         13  
151 16         62 $text .= $rule->write."\n";
152 16         103 foreach my $req ($rule->require()) {
153 16 100       86 $require{$req->[0]} = 1 if defined $req->[0];
154             }
155             }
156              
157             #TODO keep original require if current is include, for test parsing
158 7         12 my $require_line;
159             my $count;
160 7         20 foreach my $req (sort keys %require) {
161 9 50       18 next if(!$req);
162 9         20 $require_line .= ', "'.$req.'"';
163 9         11 $count++;
164             };
165 7         25 $require_line =~ s/^, //;
166 7 100       17 $require_line = '['.$require_line.']' if ($count > 1);
167              
168 7         18 $self->require($require_line);
169              
170 7 50       36 $require_line = "require $require_line;\n" if $require_line;
171              
172 7         29 return $require_line.$text;
173             }
174              
175             =head2 equals
176              
177             $object->equals($test_object): return 1 if $object and $test_object are equals
178              
179             =cut
180              
181             sub equals {
182 0     0 1 0 my $self = shift;
183 0         0 my $object = shift;
184              
185 0 0       0 return 0 unless (defined $object);
186 0 0       0 return 0 unless ($object->isa('Net::Sieve::Script'));
187              
188 0         0 my @accessors = qw( require );
189              
190 0         0 foreach my $accessor ( @accessors ) {
191 0         0 my $myvalue = $self->$accessor;
192 0         0 my $theirvalue = $object->$accessor;
193 0 0       0 if (defined $myvalue) {
194 0 0       0 return 0 unless (defined $theirvalue);
195 0 0       0 return 0 unless ($myvalue eq $theirvalue);
196             } else {
197 0 0       0 return 0 if (defined $theirvalue);
198             }
199             }
200              
201 0 0       0 if (defined $self->rules) {
202 0         0 my @myrules = sort { $a->priority() <=> $b->priority() } @{$self->rules()};
  0         0  
  0         0  
203 0         0 my @theirrules = sort { $a->priority() <=> $b->priority() } @{$object->rules()} ;
  0         0  
  0         0  
204 0 0       0 return 0 unless ($#myrules == $#theirrules);
205              
206 0 0       0 unless ($#myrules == -1) {
207 0         0 foreach my $index (0..$#myrules) {
208 0         0 my $myrule = $myrules[$index];
209 0         0 my $theirrule = $theirrules[$index];
210 0 0       0 if (defined ($myrule)) {
211 0 0       0 return 0 unless ($myrule->isa(
212             'Net::Sieve::Script::Rule'));
213 0 0       0 return 0 unless ($myrule->equals($theirrule));
214             } else {
215 0 0       0 return 0 if (defined ($theirrule));
216             }
217             }
218             }
219              
220             } else {
221 0 0       0 return 0 if (defined ($object->rules));
222             }
223 0         0 return 1;
224             }
225              
226              
227             =head2 read_rules
228              
229             $script->read_rules() : read rules from raw
230             $script->read_rules($some_text) : parse text rules
231             use of read_rules set $script->rules()
232              
233             Return 1 on success
234              
235             =cut
236              
237             sub read_rules
238             {
239 6     6 1 1604 my $self = shift;
240 6   66     22 my $text_rules = shift || $self->raw();
241              
242 6         26 my @LISTS = qw((\[.*?\]|".*?"));
243            
244 6 100       92 $self->require($1) if ( $text_rules =~ m/require @LISTS;/si );
245              
246             #read rules from raw or from $text_rules if set
247 6         34 my $script_raw = $self->_strip($text_rules);
248              
249 6         9 my @Rules;
250              
251             # for simple vacation RFC 5230
252 6 100       15 if ($script_raw =~m/^(vacation .*)$/) {
253 1         7 push @Rules, Net::Sieve::Script::Rule->new(ctrl => 'vacation',block => $1,order =>1)
254             }
255              
256 6         6 my $order;
257 6         34 while ($script_raw =~m/(if|else|elsif) (.*?)\{(.*?)}([\s;]?)/isg) {
258 13         30 my $ctrl = lc($1);
259 13         24 my $test_list = $2;
260 13         24 my $block = $3;
261              
262 13         14 ++$order;
263              
264             # break if more than 50 rules
265 13 50       22 die "50 rules does not sound reasonable !"
266             if ( $order >= 50 );
267              
268 13         31 my $pRule = Net::Sieve::Script::Rule->new (
269             ctrl => $ctrl,
270             test_list => $test_list,
271             block => $block,
272             order => $order
273             );
274              
275 13         91 push @Rules, $pRule;
276             };
277              
278 6         25 $self->rules(\@Rules);
279 6         73 $self->max_priority($order);
280              
281 6         31 return 1;
282             }
283              
284             =head2 find_rule
285              
286             Return L pointer find by priority
287              
288             Return 0 on error, 1 on not find
289              
290             =cut
291              
292             sub find_rule
293             {
294 13     13 1 277 my $self = shift;
295 13         11 my $priority = shift;
296 13 100 100     22 return 0 if $priority > $self->max_priority || $priority <= 0;
297 11 50       64 return 0 if not defined $self->rules;
298              
299 11         36 foreach my $rule (@{$self->rules}) {
  11         15  
300 28 100       87 return $rule if ($rule->priority == $priority );
301             }
302              
303 0         0 return 1;
304             }
305              
306             =head2 swap_rules
307              
308             Swap priorities,
309             now don't take care of if/else/elsif
310              
311             Return 1 on success, 0 on error
312              
313             =cut
314              
315             sub swap_rules
316             {
317 4     4 1 8 my $self = shift;
318 4         5 my $swap1 = shift;
319 4         6 my $swap2 = shift;
320              
321 4 100       12 return 0 if $swap1 == $swap2;
322              
323 3         7 my $pr1 = $self->find_rule($swap1);
324 3         14 my $pr2 = $self->find_rule($swap2);
325            
326 3 100       17 return 0 if ref($pr1) ne 'Net::Sieve::Script::Rule';
327 2 100       7 return 0 if ref($pr2) ne 'Net::Sieve::Script::Rule';
328              
329 1         2 my $mem_pr2 = $pr2->priority();
330 1         5 $pr2->priority($pr1->priority());
331 1         37 $pr1->priority($mem_pr2);
332              
333 1         7 return 1;
334             }
335              
336             =head2 reorder_rules
337              
338             Reorder rules with a list of number, start with 1, and with blanck separator. Useful for ajax sort functions.
339              
340             Thank you jeanne for your help in brain storming.
341              
342             Return 1 on success, 0 on error
343              
344             =cut
345              
346             sub reorder_rules
347             {
348 5     5 1 233 my $self = shift;
349 5         8 my $list = shift;
350              
351 5 100       15 return 0 if ( ! $list );
352              
353 4         14 my @swap = split ' ',$list;
354              
355 4 50       12 return 0 if ( ! scalar @swap );
356 4 100       10 return 0 if ( scalar @swap != $self->max_priority );
357              
358 1         5 my @new_ordered_rules;
359 1         2 foreach my $swap ( @swap ) {
360 4 50       11 if ($swap =~ m/\d+/) {
361 4         8 my $rule = $self->find_rule($swap);
362 4         17 push @new_ordered_rules, $rule;
363             }
364             }
365              
366 1         2 my $i=1;
367 1         2 foreach my $rule (@new_ordered_rules) {
368 4         6 $rule->priority($i);
369 4         14 $i++;
370             };
371              
372 1         4 return 1;
373             }
374              
375             =head2 delete_rule
376              
377             Delete rule and change priority, delete rule take care for 'if' test
378              
379             if deleted is 'if'
380             delete next if next is 'else'
381             change next in 'if' next is 'elsif'
382              
383             Return : 1 on success, 0 on error
384              
385             =cut
386              
387             sub delete_rule
388             {
389 9     9 1 1608 my $self = shift;
390 9         12 my $id = shift;
391 9         13 my $deleted = 0;
392 9 50       20 my @Rules = defined $self->rules?@{$self->rules}:();
  9         47  
393 9         38 my @NewRules = ();
394 9         12 my $order = 0;
395            
396 9         24 for ( my $i = 0; $i < scalar(@Rules); $i++ ) {
397 27         56 my $rule = $Rules[$i];
398 27         33 my $next=$i+1;
399 27 100       45 if ($rule->priority == $id) {
400 8         30 $deleted = 1;
401 8 100 100     28 if ( defined $Rules[$next] && $rule->alternate eq 'if') {
402 6 100       41 $Rules[$next]->alternate('if')
403             if ($Rules[$next]->alternate eq 'elsif' );
404              
405 6 100       37 if ($Rules[$next]->alternate eq 'else' ) {
406 2         11 $i++;
407 2         7 $rule = $Rules[$i];
408             }
409             }
410             }
411             else {
412 19         69 ++$order;
413 19         34 $rule->priority($order);
414 19         102 push @NewRules, $rule;
415             }
416             }
417              
418 9         26 $self->max_priority($order);
419 9         49 $self->rules(\@NewRules);
420            
421 9         73 return $deleted;
422             }
423              
424             =head2 add_rule
425              
426             Purpose : add a rule in end of script
427              
428             Return : priority on success, 0 on error
429              
430             Argument : Net::Sieve::Script::Rule object
431              
432             =cut
433              
434             sub add_rule
435             {
436 18     18 1 510 my $self = shift;
437 18         21 my $rule = shift;
438              
439 18 100       43 return 0 if ref($rule) ne 'Net::Sieve::Script::Rule';
440              
441 17         33 my $order = $self->max_priority();
442 17 100       73 my @Rules = defined $self->rules?@{$self->rules}:();
  13         53  
443              
444 17         68 ++$order;
445 17         33 $rule->priority($order);
446 17         73 push @Rules, $rule;
447              
448 17         32 $self->max_priority($order);
449 17         80 $self->rules(\@Rules);
450              
451 17         111 return $order;
452             }
453              
454             # private and exported tool _strip
455             # strip a string or strip raw
456             # return a string
457             # usefull for parsing or tests
458             #
459             # default remove require line or set $keep_require
460              
461             sub _strip {
462 50     50   1000 my ( $self, $script_raw, $keep_require ) = @_;
463              
464 50 100       89 if ( ref($self) eq 'Net::Sieve::Script' ) {
465 11 100       22 $script_raw = $self->raw() if (! $script_raw );
466             } else {
467 39         44 $script_raw = $self;
468             }
469              
470 50         133 $script_raw =~ s/\#.*//g; # hash-comment
471 50         65 $script_raw =~ s!/\*.*.\*/!!g; # bracket-comment
472 50         55 $script_raw =~ s/\t/ /g; # remove tabs
473 50         101 $script_raw =~ s/\(/ \( /g; # add white-space around (
474 50         101 $script_raw =~ s/\)/ \) /g; # add white-space around )
475             #$script_raw =~ s/\s+\[/ \[ /g; # add white-space around [
476             #$script_raw =~ s/\]\s+/ \] /g; # add white-space around ]
477 50         99 $script_raw =~ s/\]\s*,/\],/g; # add white-space around ]
478 50         223 $script_raw =~ s/"\s*,/", /g; # add white-space after , in list
479 50         121 $script_raw =~ s/"\s+;/";/g; # remove white-space between " and ;
480 50         486 $script_raw =~ s/\s+/ /g; # remove doubs white-space
481 50         90 $script_raw =~ s/^\s+//; # trim
482 50         207 $script_raw =~ s/\s+$//; #trim
483              
484 50 50       163 $script_raw =~ s/require.*?["\]];\s+//sgi if (!$keep_require); #remove require
485              
486 50         141 return $script_raw;
487             }
488              
489             =head1 BUGS
490              
491             Rewrite a hand made script will lose comments. Verify parsing success with parsing_ok method before write a new script.
492              
493             =head1 SUPPORT
494              
495             Please report any bugs or feature requests to "bug-net-sieve-script at rt.cpan.org", or through the web interface at L.
496             I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
497              
498              
499             =head1 AUTHOR
500              
501             Yves Agostini -
502              
503              
504             =head1 COPYRIGHT
505              
506             Copyright 2017 Yves Agostini -
507              
508             This program is free software; you can redistribute
509             it and/or modify it under the same terms as Perl itself.
510              
511             The full text of the license can be found in the
512             LICENSE file included with this module.
513              
514              
515             =head1 SEE ALSO
516              
517             L
518              
519             =cut
520              
521             1;
522             # The preceding line will help the module return a true value
523