File Coverage

blib/lib/Mail/Query.pm
Criterion Covered Total %
statement 15 51 29.4
branch 0 26 0.0
condition n/a
subroutine 5 12 41.6
pod 2 8 25.0
total 22 97 22.6


line stmt bran cond sub pod time code
1             package Mail::Query;
2              
3             require 5.005_62;
4 1     1   5495 use strict;
  1         3  
  1         33  
5 1     1   5 use warnings;
  1         2  
  1         30  
6 1     1   3 use base 'Mail::Audit';
  1         6  
  1         838  
7 1     1   34166 use Parse::RecDescent;
  1         50315  
  1         8  
8             our $VERSION = '0.01';
9              
10             sub new {
11 1     1 1 38 my $package = shift;
12            
13 1         14 my $self = $package->SUPER::new(@_);
14 1         125669 $self->{parser} = new Parse::RecDescent($self->grammar);
15 0           return $self;
16             }
17              
18             sub query {
19 0     0 0   my ($self, $query) = @_;
20 0           local $self->{parser}{local}{mq} = $self; # Circular ref, but local. No sweat.
21 0           return $self->{parser}->where_clause($query);
22             }
23              
24             sub compare {
25 0     0 0   my ($self, $field, $op, $string) = @_;
26            
27             # We handle =, <, and >
28 0 0         return !$self->compare($field, '=', $string) if $op eq '!=';
29 0 0         return !$self->compare($field, '<', $string) if $op eq '>=';
30 0 0         return !$self->compare($field, '>', $string) if $op eq '<=';
31              
32              
33             # This should be date-aware, at the least. So far we punt.
34 0           my $val = $self->get($field);
35             #warn "comparing: '$val' $op '$string'";
36 0 0         return $val eq $string if $op eq '=';
37 0 0         return $val lt $string if $op eq '<';
38 0 0         return $val gt $string if $op eq '>';
39            
40 0           die "Unknown operator '$op'";
41             }
42              
43             sub between {
44 0     0 0   my ($self, $field, $one, $two) = @_;
45            
46             # This should be date-aware, at the least. So far we punt.
47 0 0         ($one, $two) = ($two, $one) if $one gt $two;
48 0 0         return 0 unless $one lt $field;
49 0 0         return 0 unless $field lt $two;
50 0           return 1;
51             }
52              
53             sub like {
54 0     0 0   my ($self, $field, $pattern) = @_;
55            
56 0 0         if ($pattern->[1] eq 'regex') {
57 0           (my $pat = $pattern->[0]) =~ s/([\@\$])/\\$1/g; # A limited quotemeta (is this a good idea?)
58             #warn "$field =~ $pat";
59 0           my $result = eval "\$self->get(\$field) =~ $pat"; # eval to maintain 5.004 compat
60 0 0         warn "Error in pattern $pat" unless defined $result;
61 0           return $result;
62             }
63            
64             # $pattern->[1] eq 'string'
65             # A string like 'boo%hoo' maps to /^boo.*hoo$/
66 0           my $string = quotemeta($pattern->[0]);
67 0           $string =~ s/%/.*/;
68 0           return $self->get($field) =~ /^$string$/;
69             }
70              
71             sub exists {
72 0     0 0   my ($self, $field) = @_;
73             #my $val = $self->get($field); warn "checking for defined($field): ", defined($val);
74              
75 0           return defined $self->get($field);
76             }
77              
78             # We implement a 'Recipient' field, which is any of To, Cc, or Bcc
79             # We also make 'body' a header-like field, for queries like "body LIKE /blah/"
80             sub get {
81 0     0 1   my ($self, $field) = @_;
82 0 0         return join '', @{$self->body} if lc($field) eq 'body';
  0            
83 0 0         return join ', ', map {$self->SUPER::get($_)} qw(To Cc Bcc) if lc($field) eq 'recipient';
  0            
84 0           return $self->SUPER::get($field);
85             }
86              
87             sub grammar {
88 0     0 0   return <<'EOF';
89             # Excised from http://www.contrib.andrew.cmu.edu/~shadow/sql/sql2bnf.aug92.txt
90            
91             where_clause: search_condition /^\z/ {$return = $item{search_condition}}
92             |
93            
94             search_condition:
95             {$return = grep {$_} @{$item[1]}}
96            
97             bool_term:
98             {$return = !grep {!$_} @{$item[1]}}
99            
100             bool_factor: not(?) bool_primary {$return = @{$item[1]} ? !$item[2] : $item[2]}
101             # Don't support IS TRUE and IS NOT UNKNOWN and all that crap
102            
103             bool_primary: '(' search_condition ')' {$return = $item[3]}
104             | predicate
105            
106             predicate: comparison_predicate
107             | between_predicate
108             | like_predicate
109             | null_predicate
110             # There's more here, but I'm skipping for now.
111            
112             # These only accept header field names as the LHS, and don't allow functions yet.
113             comparison_predicate: header comp_op string {$return = $thisparser->{local}{mq}->compare(@item[1,2,3])}
114            
115             between_predicate: header not(?) between string /AND/i string
116             {my $x = $thisparser->{local}{mq}->between(@item[1,4,6]);
117             $return = @{$item[2]} ? !$x : $x}
118            
119             like_predicate: header not(?) like rhs {my $x = $thisparser->{local}{mq}->like(@item[1,4]);
120             $return = @{$item[2]} ? !$x : $x}
121            
122             null_predicate: header is not(?) null {my $x = $thisparser->{local}{mq}->exists($item[1]);
123             $return = @{$item[3]} ? $x : !$x}
124            
125             rhs: string {$return = [$item[1], 'string']}
126             | regex {$return = [$item[1], 'regex' ]}
127            
128             # With a true $arg[0], returns a two-element listref.
129             string: {my @x = extract_quotelike($text);
130             if ($x[0] and ($x[3] =~ m/^q+$/ or $x[4] =~ m/^['"]$/) ) { # Strings only, not regexes & so on
131             substr($text,0,pos($text)) = '';
132             $return = $x[5];
133             } else {
134             $return = undef;
135             }
136             }
137            
138             regex: {local $_ = extract_quotelike($text);
139             $return = (m/^m/ or m/^\//) ? $_ : undef}
140            
141             comp_op: '=' | '!=' | '<=' | '>=' | '<' | '>'
142            
143             header: /[\w-]+/ # dashes are allowed, very common in headers.
144            
145             not: /NOT/i
146             is: /IS/i
147             like: /LIKE/i
148             null: /NULL/i
149             between: /BETWEEN/i
150              
151             EOF
152             }
153              
154             1;
155             __END__