File Coverage

lib/Net/Sieve/Script/Condition.pm
Criterion Covered Total %
statement 125 158 79.1
branch 49 86 56.9
condition 10 14 71.4
subroutine 7 8 87.5
pod 3 3 100.0
total 194 269 72.1


line stmt bran cond sub pod time code
1             package Net::Sieve::Script::Condition;
2 8     8   44 use strict;
  8         14  
  8         214  
3 8     8   37 use warnings;
  8         11  
  8         191  
4              
5 8     8   35 use base qw(Class::Accessor::Fast);
  8         11  
  8         453  
6              
7 8     8   37 use vars qw($VERSION);
  8         10  
  8         11271  
8              
9             $VERSION = '0.09';
10              
11             __PACKAGE__->mk_accessors(qw(test not id condition parent AllConds key_list header_list address_part match_type comparator require));
12              
13             my @FILO;
14             my $ids = 0;
15             my %Conditions;
16              
17             sub new
18             {
19 90     90 1 7313 my ($class, $param) = @_;
20              
21 90   33     337 my $self = bless ({}, ref ($class) || $class);
22 90         156 my $require;
23              
24 90         173 my @ADDRESS_PART = qw((:all |:localpart |:domain ));
25             #Syntax: ":comparator"
26 90         131 my @COMPARATOR_NAME = qw(i;octet|i;ascii-casemap);
27             # my @MATCH_TYPE = qw((:\w+ ));
28             # regex expired draft will be removed
29 90         158 my @MATCH_TYPE = qw((:is |:contains |:matches ));
30 90         136 my @MATCH_SIZE = qw((:over |:under ));
31             # match relationnal RFC 5231
32 90         225 my @MATCH_REL = qw((:value .*? |:count .*? ));
33             # match :
34 90         147 my @LISTS = qw((\[.*?\]|".*?"));
35              
36             #my @header_list = qw(From To Cc Bcc Sender Resent-From Resent-To List-Id);
37              
38 90         171 $param =~ s/\t/ /g;
39 90         588 $param =~ s/\s+/ /g;
40 90         218 $param =~ s/^\s+//;
41 90         277 $param =~ s/\s+$//;
42 90         155 $param =~ s/[\r\n]//gs;
43              
44             return undef if
45 90 50       405 $param !~ m/^(not )?(address|envelope|header|size|allof|anyof|exists|false|true)(.*)/i;
46              
47 90         1773 my $not = lc($1);
48 90         308 my $test = lc($2);
49 90         188 my $args = $3;
50              
51 90         291 $self->not($not);
52 90         647 $self->test($test);
53              
54             # to manage tree access
55 90         410 $ids++;
56 90         205 $self->id($ids);
57 90         446 $Conditions{$ids} = $self;
58 90         236 $self->AllConds(\%Conditions);
59              
60             # clean args
61 90         593 $args =~ s/^\s+//g;
62 90         257 $args =~ s/\s+$//g;
63 90         212 $args =~ s/\s+(\s+[\(\)],?\s+)\s+/$1/g;
64              
65             # substitute ',' separator by ' ' in string-list
66             # to easy parse test-list
67             # better :
68 90         419 1 while ($args =~ s/(\[[^\]]+?)",\s*/$1" /);
69             #$args =~ s/",\s+"/" "/g;
70              
71             #recursiv search for anyof/allof conditions
72 90         216 my @COND = $self->condition();
73 90         356 my $count;
74 90         262 while ( $args =~ s/(.*)\(([^\(].*?)\)(.*)/$1$3/s ) {
75 16         46 my $first = $1;
76 16         24 my $last = $3;
77 16         33 my $subs = $2;
78              
79 16         25 $count++;
80 16 50       32 die "50 test lists does not sound reasonable !"
81             if ( $count >= 50);
82              
83 16         20 my @condition_list;
84 16         49 my @condition_list_string = split ( ',', $subs );
85 16         34 foreach my $sub_condition (@condition_list_string) {
86 35         98 my $new_subs = Net::Sieve::Script::Condition->new($sub_condition);
87 35 50       75 next if (!$new_subs);
88 35 100 100     63 if ( $new_subs->test eq 'anyof' || $new_subs->test eq 'allof' ) {
89 6         35 my $child_tab = pop @FILO;
90 6         14 $new_subs->condition($child_tab);
91             # set parent infos for tree management
92 6         25 foreach my $child ( @{$child_tab} ) {
  6         10  
93 13         38 $child->parent($new_subs);
94             }
95             };
96 35 100 66     347 (!$first && !$last) ?
97             push @COND, $new_subs : push @condition_list, $new_subs;
98             }
99            
100 16 100 66     118 (!$first && !$last) ?
101             $self->condition(\@COND) : push @FILO, \@condition_list;
102              
103             };
104             # set parent infos for tree management
105 90         190 foreach my $child ( @COND ) {
106 112 100       248 $child->parent($self) if $child;
107             } ;
108              
109 90         148 my ($address,$comparator,$match,$string,$key_list);
110             # RFC Syntax : address [ADDRESS-PART] [COMPARATOR] [MATCH-TYPE]
111             #
112 90 100       172 if ( $test eq 'address' ) {
113 14         400 ($address,$comparator,$match,$string,$key_list) = $args =~ m/@ADDRESS_PART?(:comparator "(?:@COMPARATOR_NAME)" )?@MATCH_TYPE?@LISTS @LISTS$/gi;
114             };
115             # RFC Syntax : envelope [COMPARATOR] [ADDRESS-PART] [MATCH-TYPE]
116             #
117 90 50       197 if ( $test eq 'envelope' ) {
118 0         0 ($comparator,$address,$match,$string,$key_list) = $args =~ m/(:comparator "(?:@COMPARATOR_NAME)" )?@ADDRESS_PART?@MATCH_TYPE?@LISTS @LISTS$/gi;
119             };
120             # RFC Syntax : header [COMPARATOR] [MATCH-TYPE]
121             #
122 90 100       158 if ( $test eq 'header' ) {
123             # only for regex old draft
124 52         992 ($match,$comparator,$string,$key_list) = $args =~ m/(:regex )?(:comparator "(?:@COMPARATOR_NAME)" )?@LISTS @LISTS$/gi;
125             # match relationnal RFC 5231
126 52 100       145 if (!$match) {
127 49         950 ($match,$comparator,$string,$key_list) = $args =~ m/@MATCH_REL?(:comparator "(?:@COMPARATOR_NAME)" )?@LISTS @LISTS$/gi;
128             };
129             # RFC 5228 !
130 52 100       132 if (!$match) {
131 48         778 ($comparator,$match,$string,$key_list) = $args =~ m/(:comparator "(?:@COMPARATOR_NAME)" )?@MATCH_TYPE?@LISTS @LISTS$/gi;
132             }
133 52 100       133 if (!$match) {
134 3         254 ($match,$comparator,$string,$key_list) = $args =~ m/@MATCH_TYPE?(:comparator "(?:@COMPARATOR_NAME)" )?@LISTS @LISTS$/gi;
135             }
136             };
137             # RFC Syntax : size <":over" / ":under">
138 90 100       166 if ( $test eq 'size' ) {
139 2         83 ($match,$string) = $args =~ m/@MATCH_SIZE(.*)$/gi;
140             };
141             # RFC Syntax : exists
142 90 100       157 if ( $test eq 'exists' ) {
143 2         57 ($string) = $args =~ m/@LISTS$/gi;
144             }
145             # find require
146 90 100       640 if (lc($match) eq ':regex ') {
147 3         4 push @{$require}, 'regex';
  3         6  
148             };
149 90         320 $self->require($require);
150              
151              
152 90         2064 $self->address_part(lc($address));
153 90         1103 $self->match_type(lc($match));
154 90         1293 $self->comparator(lc($comparator));
155 90         647 $self->header_list($string);
156 90         508 $self->key_list($key_list);
157              
158              
159 90         646 return $self;
160             }
161              
162             # see head2 equals
163              
164             sub equals {
165 0     0 1 0 my $self = shift;
166 0         0 my $object = shift;
167              
168 0 0       0 return 0 unless (defined $object);
169 0 0       0 return 0 unless ($object->isa('Net::Sieve::Script::Condition'));
170              
171             # Should we test "id" ? Probably not it's internal to the
172             # representaion of this object, and not a part of what actually makes
173             # it a sieve "condition"
174              
175 0         0 my @accessors = qw( test not address_part match_type comparator require key_list header_list address_part );
176              
177 0         0 foreach my $accessor ( @accessors ) {
178 0         0 my $myvalue = $self->$accessor;
179 0         0 my $theirvalue = $object->$accessor;
180 0 0       0 if (defined $myvalue) {
181 0 0       0 return 0 unless (defined $theirvalue);
182 0 0       0 if ($accessor ne 'key_list') {
183 0         0 $theirvalue=~tr/[A-Z]/[a-z]/;
184 0         0 $myvalue=~tr/[A-Z]/[a-z]/;
185             };
186 0 0       0 return 0 unless ($myvalue eq $theirvalue);
187             } else {
188 0 0       0 return 0 if (defined $theirvalue);
189             }
190             }
191              
192 0 0       0 if (defined $self->condition) {
193 0         0 my $tmp = $self->condition;
194 0         0 my @myconds = @$tmp;
195 0         0 $tmp = $object->condition;
196 0         0 my @theirconds = @$tmp;
197 0 0       0 return 0 unless ($#myconds == $#theirconds);
198              
199 0 0       0 unless ($#myconds == -1) {
200 0         0 foreach my $index (0..$#myconds) {
201 0         0 my $mycond = $myconds[$index];
202 0         0 my $theircond = $theirconds[$index];
203 0 0       0 if (defined ($mycond)) {
204 0 0       0 return 0 unless ($mycond->isa(
205             'Net::Sieve::Script::Condition'));
206 0 0       0 return 0 unless ($mycond->equals($theircond));
207             } else {
208 0 0       0 return 0 if (defined ($theircond));
209             }
210             }
211             }
212              
213             } else {
214 0 0       0 return 0 if (defined ($object->condition));
215             }
216 0         0 return 1;
217             }
218              
219             # see head2 write
220              
221             sub write {
222 52     52 1 130 my $self = shift;
223 52   100     151 my $recursiv_level = shift || 0;
224 52         67 my $text_condition = "";
225              
226 52         58 $recursiv_level++;
227 52 100       107 if (defined $self->condition() ) {
228 18         84 $text_condition = ' ' x $recursiv_level;
229 18 50       52 $text_condition .= $self->not.' ' if ($self->not);
230 18         98 $text_condition .= $self->test." ( ";
231 18         75 foreach my $sub_cond ( @{$self->condition()} ) {
  18         35  
232 51 100       125 next if ! $sub_cond;
233 39 100       74 if (defined $sub_cond->condition() ) {
234 6         33 $text_condition .= "\n".(' ' x $recursiv_level).$sub_cond->write($recursiv_level).",\n";
235 6         16 next;};
236 33         160 $text_condition .= "\n".(' ' x $recursiv_level).' '. $sub_cond->_write_test().',';
237             }
238 18         69 $text_condition =~ s/,$//;
239 18         32 $text_condition .= ' )';
240             }
241             else {
242 34         152 $text_condition = $self->_write_test();
243             };
244              
245 52         158 return $text_condition;
246             }
247              
248             # private method
249             # _write_test
250             # return single line text
251              
252             sub _write_test {
253 67     67   77 my $self = shift;
254 67         117 my $line = $self->not.' '.$self->test.' ';
255            
256 67 100       425 my $comparator = ($self->comparator)?':comparator '.$self->comparator : '';
257            
258 67 100       304 if ( $self->test eq 'address' ) {
    50          
    100          
    100          
259 17         66 $line .= $self->address_part.' '.$comparator.' '.$self->match_type;
260             }
261             elsif ( $self->test eq 'envelope' ) {
262 0         0 $line .= $comparator.' '.$self->address_part.' '.$self->match_type;
263             }
264             elsif ( $self->test eq 'header' ) {
265 43 100       426 if ($self->match_type eq ':regex ') {
266 6         25 $line .= $self->match_type.' '.$self->comparator;
267             }
268             else {
269 37         153 $line .= $self->comparator.' '.$self->match_type;
270             }
271             }
272             elsif ( $self->test eq 'size' ) {
273 3         66 $line .= $self->match_type;
274             };
275            
276              
277 67 50       433 my $header_list = ($self->header_list)?$self->header_list:'';
278 67 100       401 my $key_list = ($self->key_list)?$self->key_list:'';
279              
280 67         396 $line.=' '.$header_list.' '.$key_list;
281              
282 67         200 $line =~ s/^\s+//;
283 67         212 $line =~ s/\s+$//;
284 67         313 $line =~ s/ +/ /g;
285             # restore ", " in [ ]
286 67         385 1 while ( $line =~ s/(\[[^\]]+?)" "/$1", "/);
287              
288 67         190 return $line;
289             }
290              
291              
292             =head1 NAME
293              
294             Net::Sieve::Script::Condition - parse and write conditions in sieve scripts
295              
296             =head1 SYNOPSIS
297              
298             use Net::Sieve::Script::Condition;
299              
300             my $cond = Net::Sieve::Script::Condition->new('header');
301             $cond->match_type(':contains');
302             $cond->key_list('"[Test4]"');
303             $cond->header_list('"Subject"');
304              
305             print $cond->write();
306              
307             or
308              
309             my $cond = Net::Sieve::Script::Condition->new(
310             'anyof (
311             header :contains "Subject" "[Test]",
312             header :contains "Subject" "[Test2]")'
313             );
314              
315             print $cond->write();
316              
317             =head1 DESCRIPTION
318              
319             Parse and write condition part of Sieve rules, see L.
320              
321             Support RFC 5228, 5231 (relationnal) and regex draft
322              
323             =head1 CONSTRUCTOR
324              
325             =head2 new
326              
327             Match and set accessors for each condition object in conditions tree, "test" is mandatory
328              
329             Internal
330              
331             id : id for condition, set by creation order
332             condition : array of sub conditions
333             parent : parent of sub condition
334             AllConds : array of pointers for all conditions
335              
336             Condition parts
337             not : 'not' or nothing
338             test : 'header', 'address', 'exists', ...
339             key_list : "subject" or ["To", "Cc"]
340             header_list : "text" or ["text1", "text2"]
341             address_part : ':all ', ':localpart ', ...
342             match_type : ':is ', ':contains ', ...
343             comparator : string part
344              
345             =head1 METHODS
346              
347             =head2 equals
348              
349             Purpose : test conditions
350             Return : 1 on equals conditions
351              
352             =head2 write
353              
354             Purpose : write rule conditions in text format
355             Return : multi-line formatted text
356              
357             =head1 AUTHOR
358              
359             Yves Agostini
360             CPAN ID: YVESAGO
361             yvesago@cpan.org
362              
363             =head1 COPYRIGHT
364              
365             This program is free software; you can redistribute
366             it and/or modify it under the same terms as Perl itself.
367              
368             The full text of the license can be found in the
369             LICENSE file included with this module.
370              
371             =cut
372              
373             return 1;