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   41 use strict;
  8         12  
  8         324  
3 8     8   38 use warnings;
  8         16  
  8         196  
4              
5 8     8   37 use base qw(Class::Accessor::Fast);
  8         10  
  8         585  
6              
7 8     8   37 use vars qw($VERSION);
  8         12  
  8         17281  
8              
9             $VERSION = '0.08';
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 8280 my ($class, $param) = @_;
20              
21 90   33     503 my $self = bless ({}, ref ($class) || $class);
22 90         116 my $require;
23              
24 90         187 my @ADDRESS_PART = qw((:all |:localpart |:domain ));
25             #Syntax: ":comparator"
26 90         176 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         127 my @MATCH_SIZE = qw((:over |:under ));
31             # match relationnal RFC 5231
32 90         168 my @MATCH_REL = qw((:value .*? |:count .*? ));
33             # match :
34 90         114 my @LISTS = qw((\[.*?\]|".*?"));
35              
36             #my @header_list = qw(From To Cc Bcc Sender Resent-From Resent-To List-Id);
37              
38 90         158 $param =~ s/\t/ /g;
39 90         749 $param =~ s/\s+/ /g;
40 90         215 $param =~ s/^\s+//;
41 90         312 $param =~ s/\s+$//;
42 90         141 $param =~ s/[\r\n]//gs;
43              
44             return undef if
45 90 50       447 $param !~ m/^(not )?(address|envelope|header|size|allof|anyof|exists|false|true)(.*)/i;
46              
47 90         12662 my $not = lc($1);
48 90         272 my $test = lc($2);
49 90         175 my $args = $3;
50              
51 90         306 $self->not($not);
52 90         716 $self->test($test);
53              
54             # to manage tree access
55 90         461 $ids++;
56 90         255 $self->id($ids);
57 90         506 $Conditions{$ids} = $self;
58 90         281 $self->AllConds(\%Conditions);
59              
60             # clean args
61 90         663 $args =~ s/^\s+//g;
62 90         287 $args =~ s/\s+$//g;
63 90         255 $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         574 1 while ($args =~ s/(\[[^\]]+?)",\s*/$1" /);
69             #$args =~ s/",\s+"/" "/g;
70              
71             #recursiv search for anyof/allof conditions
72 90         246 my @COND = $self->condition();
73 90         548 my $count;
74 90         316 while ( $args =~ s/(.*)\(([^\(].*?)\)(.*)/$1$3/s ) {
75 16         36 my $first = $1;
76 16         36 my $last = $3;
77 16         31 my $subs = $2;
78              
79 16         23 $count++;
80 16 50       40 die "50 test lists does not sound reasonable !"
81             if ( $count >= 50);
82              
83 16         25 my @condition_list;
84 16         60 my @condition_list_string = split ( ',', $subs );
85 16         32 foreach my $sub_condition (@condition_list_string) {
86 35         134 my $new_subs = Net::Sieve::Script::Condition->new($sub_condition);
87 35 50       100 next if (!$new_subs);
88 35 100 100     86 if ( $new_subs->test eq 'anyof' || $new_subs->test eq 'allof' ) {
89 6         52 my $child_tab = pop @FILO;
90 6         17 $new_subs->condition($child_tab);
91             # set parent infos for tree management
92 6         28 foreach my $child ( @{$child_tab} ) {
  6         12  
93 13         54 $child->parent($new_subs);
94             }
95             };
96 35 100 66     553 (!$first && !$last) ?
97             push @COND, $new_subs : push @condition_list, $new_subs;
98             }
99            
100 16 100 66     171 (!$first && !$last) ?
101             $self->condition(\@COND) : push @FILO, \@condition_list;
102              
103             };
104             # set parent infos for tree management
105 90         189 foreach my $child ( @COND ) {
106 112 100       393 $child->parent($self) if $child;
107             } ;
108              
109 90         157 my ($address,$comparator,$match,$string,$key_list);
110             # RFC Syntax : address [ADDRESS-PART] [COMPARATOR] [MATCH-TYPE]
111             #
112 90 100       231 if ( $test eq 'address' ) {
113 14         610 ($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       181 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       185 if ( $test eq 'header' ) {
123             # only for regex old draft
124 52         1216 ($match,$comparator,$string,$key_list) = $args =~ m/(:regex )?(:comparator "(?:@COMPARATOR_NAME)" )?@LISTS @LISTS$/gi;
125             # match relationnal RFC 5231
126 52 100       149 if (!$match) {
127 49         1234 ($match,$comparator,$string,$key_list) = $args =~ m/@MATCH_REL?(:comparator "(?:@COMPARATOR_NAME)" )?@LISTS @LISTS$/gi;
128             };
129             # RFC 5228 !
130 52 100       178 if (!$match) {
131 48         1012 ($comparator,$match,$string,$key_list) = $args =~ m/(:comparator "(?:@COMPARATOR_NAME)" )?@MATCH_TYPE?@LISTS @LISTS$/gi;
132             }
133 52 100       163 if (!$match) {
134 3         249 ($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       194 if ( $test eq 'size' ) {
139 2         117 ($match,$string) = $args =~ m/@MATCH_SIZE(.*)$/gi;
140             };
141             # RFC Syntax : exists
142 90 100       171 if ( $test eq 'exists' ) {
143 2         90 ($string) = $args =~ m/@LISTS$/gi;
144             }
145             # find require
146 90 100       1484 if (lc($match) eq ':regex ') {
147 3         4 push @{$require}, 'regex';
  3         8  
148             };
149 90         309 $self->require($require);
150              
151              
152 90         5181 $self->address_part(lc($address));
153 90         1672 $self->match_type(lc($match));
154 90         5016 $self->comparator(lc($comparator));
155 90         741 $self->header_list($string);
156 90         500 $self->key_list($key_list);
157              
158              
159 90         748 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 152 my $self = shift;
223 52   100     203 my $recursiv_level = shift || 0;
224 52         65 my $text_condition = "";
225              
226 52         53 $recursiv_level++;
227 52 100       145 if (defined $self->condition() ) {
228 18         104 $text_condition = ' ' x $recursiv_level;
229 18 50       56 $text_condition .= $self->not.' ' if ($self->not);
230 18         119 $text_condition .= $self->test." ( ";
231 18         80 foreach my $sub_cond ( @{$self->condition()} ) {
  18         44  
232 51 100       154 next if ! $sub_cond;
233 39 100       91 if (defined $sub_cond->condition() ) {
234 6         58 $text_condition .= "\n".(' ' x $recursiv_level).$sub_cond->write($recursiv_level).",\n";
235 6         11 next;};
236 33         195 $text_condition .= "\n".(' ' x $recursiv_level).' '. $sub_cond->_write_test().',';
237             }
238 18         64 $text_condition =~ s/,$//;
239 18         41 $text_condition .= ' )';
240             }
241             else {
242 34         184 $text_condition = $self->_write_test();
243             };
244              
245 52         197 return $text_condition;
246             }
247              
248             # private method
249             # _write_test
250             # return single line text
251              
252             sub _write_test {
253 67     67   78 my $self = shift;
254 67         154 my $line = $self->not.' '.$self->test.' ';
255            
256 67 100       625 my $comparator = ($self->comparator)?':comparator '.$self->comparator : '';
257            
258 67 100       403 if ( $self->test eq 'address' ) {
    50          
    100          
    100          
259 17         93 $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       597 if ($self->match_type eq ':regex ') {
266 6         37 $line .= $self->match_type.' '.$self->comparator;
267             }
268             else {
269 37         198 $line .= $self->comparator.' '.$self->match_type;
270             }
271             }
272             elsif ( $self->test eq 'size' ) {
273 3         61 $line .= $self->match_type;
274             };
275            
276              
277 67 50       566 my $header_list = ($self->header_list)?$self->header_list:'';
278 67 100       532 my $key_list = ($self->key_list)?$self->key_list:'';
279              
280 67         489 $line.=' '.$header_list.' '.$key_list;
281              
282 67         218 $line =~ s/^\s+//;
283 67         228 $line =~ s/\s+$//;
284 67         581 $line =~ s/ +/ /g;
285             # restore ", " in [ ]
286 67         524 1 while ( $line =~ s/(\[[^\]]+?)" "/$1", "/);
287              
288 67         219 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 formated text
356              
357             =head1 AUTHOR
358              
359             Yves Agostini
360             CPAN ID: YVESAGO
361             Univ Metz
362             agostini@univ-metz.fr
363             http://www.crium.univ-metz.fr
364              
365             =head1 COPYRIGHT
366              
367             This program is free software; you can redistribute
368             it and/or modify it under the same terms as Perl itself.
369              
370             The full text of the license can be found in the
371             LICENSE file included with this module.
372              
373             =cut
374              
375             return 1;