File Coverage

blib/lib/Mail/Google/Procmailrc.pm
Criterion Covered Total %
statement 18 188 9.5
branch 0 38 0.0
condition 0 35 0.0
subroutine 6 23 26.0
pod 14 17 82.3
total 38 301 12.6


line stmt bran cond sub pod time code
1             package Mail::Google::Procmailrc;
2 1     1   30809 use 5.010000;
  1         3  
  1         40  
3 1     1   6 use strict;
  1         2  
  1         39  
4 1     1   5 use warnings;
  1         5  
  1         31  
5 1     1   2010 use XML::Fast;
  1         14684  
  1         49  
6 1     1   2491 use Data::Dumper;
  1         11024  
  1         73  
7 1     1   9 use Carp;
  1         4  
  1         2463  
8             require Exporter;
9             our @ISA = qw(Exporter);
10             our %EXPORT_TAGS = ( 'all' => [ qw( ) ] );
11             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
12             our @EXPORT = qw( );
13             our $VERSION = '0.022';
14             =head1 NAME
15              
16             Mail::Google::Procmailrc - Perl module that allows easy conversion from Gmail mail filters to Procmail rules
17              
18             =head1 SYNOPSIS
19              
20             use Mail::Google::Procmailrc;
21             my $o = Mail::Google::Procmailrc->new();
22             $o->convert(, );
23              
24             or, you can use it with the helper script
25              
26             google-to-procmailrc --input ./mailFilters.xml --output test-procmail.rc --mdir-path $HOME/somemail
27              
28             =head1 DESCRIPTION
29              
30             You may want at some point, for some reason to export all your gmail mail rules as
31             procmail filters.
32              
33             If you use a mail setup involving OfflineIMAP fetching multiple folders(labels) from
34             Google, you'll notice that there is a certain overhead involved.
35              
36             That's because OfflineIMAP needs to tell the IMAP server, which messages it has, in
37             order to retrieve only the ones that it doesn't, and then for each new message fetched
38             it also needs to update the SQLite local dbs with the statuses of the new messages.
39             And it has to do that for every folder(label). That highly depends on which labels you
40             fetch with OfflineIMAP.
41              
42             If you want to make the sync faster, you can consider only fetching the "[Gmail]/All Mail"
43             folder or the "INBOX" folder, but then you still have to solve mail triage.
44              
45             Procmail is quite good for mail triage, but the mailFilters.xml file that you can
46             export from Gmail is not suited for use with Procmail AFAIK.
47              
48             This module aims to solve that problem by converting mailFilters.xml to a set of
49             procmail rules (effectively a procmailrc file).
50              
51             Normally, you'd use the script that comes with this module to migrate your Gmail rules
52             to procmail and then you can just maintain the procmail rules. At least that's how I(plan to) use it.
53              
54             =cut
55              
56             our $tmplt1= qq{:%s
57             * ^From:.*%s.*
58             %s
59              
60             };
61              
62             our $tmplt2 = qq{:%s
63             * ^To: .*%s.*
64             %s
65              
66             };
67              
68             our $tmplt3 = qq{:%s
69             * ^Subject: .*%s.*
70             %s
71             };
72              
73             our $tmplt4 = qq{:%sB
74             * .*%s.*
75             %s
76              
77             };
78              
79             our $tmplt_catchall = qq{:0
80             *.*
81             %s
82              
83             };
84              
85              
86             =head1 METHODS
87              
88              
89             =head2 new($folders_path, $archive_dir, $trash_dir)
90              
91             The constructor receives $folders_path which is the directory where all the
92             mail folders resulting from the triage will be placed. The other two, archive and trash
93             will be used with the same role as archiving e-mail, and the B<[Gmail]/Trash> folder.
94              
95             =cut
96              
97             sub new {
98             #my ($class_name,$folders_path,$archive_dir,$trash_dir) = @_;
99 0     0 1   my ($class_name,$c) = @_;
100 0           my $o = bless {},$class_name;
101             # TODO check for folders_path
102 0           $o->{folders_path} = $c->{mdir_path} ;
103 0   0       $o->{archive_dir} = $c->{archive_dir} // 'archive';
104 0   0       $o->{trash_dir} = $c->{trash_dir} // 'trash';
105 0   0       $o->{inbox_dir} = $c->{inbox_dir} // 'inbox';
106 0   0       $o->{debug} = $c->{debug} // 0;
107 0 0         $o->{debug_rule_count} = 0 if $o->{debug};
108 0           $o->{labels_found} = {};
109 0           return $o;
110             };
111              
112             =head2 generate_create_dirs_script
113              
114             This method will generate a script called B . This script
115             will create all the folders. Afterwards, there will be a one-to-one mapping between
116             the labels present in your mailFilters.xml and folders on disk. They will be empty, but
117             after running procmail they will be filled with the mails that correspond to them.
118              
119             =cut
120              
121             sub generate_create_dirs_script {
122 0     0 1   my ($self) = @_;
123 0           my $create_script = "create.sh";
124 0           open my $fh,">$create_script";
125 0           print $fh "#!/bin/bash\n";
126 0           my $p = $self->{folders_path};
127 0           my @labels;
128 0           push @labels, (keys %{ $self->{labels_found} } );
  0            
129 0           push @labels, $self->{archive_dir};
130 0           push @labels, $self->{trash_dir};
131 0           push @labels, $self->{inbox_dir};
132 0           for my $l ( @labels ) {
133 0           print $fh qq{mkdir -p "$p/$l"\n};
134             };
135 0           close $fh;
136 0           `chmod +x $create_script`;
137             };
138              
139             =head2 generate_rule_catchall()
140              
141             Returns a catchall rule for the inbox
142              
143             =cut
144              
145             sub generate_rule_catchall {
146 0     0 1   my ($self) = @_;
147 0           my $dir = $self->{folders_path}.'/'.$self->{inbox_dir}.'/';
148 0           $dir = qq{"$dir"};
149 0           $self->apply_debug_info(\$dir);
150 0           return sprintf($tmplt_catchall,$dir);
151             };
152              
153             =head2 convert($input,$output)
154              
155             This method takes the input XML and uses L to convert it to
156             the procmail rules file.
157              
158             =cut
159              
160             sub convert {
161 0     0 1   my ($self,$i,$o) = @_;
162 0           my $google_filters_xml = undef;
163             {
164 0           local $/ = undef;
  0            
165 0           open my $fh,"<$i";
166 0           $google_filters_xml = <$fh>;
167 0           close $fh;
168             };
169 0           my $xml_nested = xml2hash($google_filters_xml);
170 0           my $buf = $self->adapt($xml_nested);
171 0           open my $fh,">$o";
172 0           print $fh $buf ;
173 0           close $fh;
174 0           $self->generate_create_dirs_script;
175             };
176              
177             =head2 adapt($x)
178              
179             Receives as parameter a nested hash structure generated by XML::Fast from parsing
180             the mailFilters.xml file, then traverses that structure. This is where the conversion
181             happens.
182              
183             =cut
184              
185             sub adapt {
186 0     0 1   my ($self,$x) = @_;
187 0           my $buf;
188 0           for my $o (@{ $x->{feed}->{entry} }) {
  0            
189 0 0         next if $o->{title} ne 'Mail Filter';
190 0 0         next if !exists $o->{"apps:property"};
191 0 0         next if ref($o->{"apps:property"}) ne "ARRAY";
192 0           my $adapt_hash = {};
193 0           for my $p (@{ $o->{"apps:property"} }) {
  0            
194 0           my $key = $p->{'-name' };
195 0           my $val = $p->{'-value'};
196 0           $adapt_hash->{$key} = $val;
197             };
198 0           $buf .= $self->adapt_rule($adapt_hash);
199             };
200              
201 0           $buf .= $self->generate_rule_catchall();
202 0           return $buf;
203             };
204              
205             =head2 collect_labels
206              
207             Collects labels found so that B can be generated. See the L method for
208             details.
209              
210             =cut
211              
212             sub collect_label {
213 0     0 0   my ($self,$l) = @_;
214 0           $self->{labels_found}->{$l} = 1;
215             };
216              
217             =head2 compute_flag()
218              
219             Generates appropriate flag if the rule chain has not ended yet.
220              
221             =cut
222              
223             sub compute_flag {
224 0     0 1   my ($self) = @_;
225 0           my $r = $self->{rules_remaining};
226 0           my $m = $self->{multi_rule};
227 0           my $g = $self->{goes_to_inbox};
228 0 0         return "0 wc" if $g;
229 0 0         if($m) {
230 0 0         return "0 wc" if $r > 1;
231 0 0         return "0" if $r == 1;
232 0           croak "[E] r < 0 ?!";
233             } else {
234 0           return "0";
235             };
236             };
237              
238             =head2 rule_from
239              
240             Generic "From:" rule conversion.
241              
242             =cut
243              
244             sub rule_from {
245 0     0 1   my ($self, $h) = @_;
246 0           my $buf = "";
247 0           my $rules_remaining = $self->{rules_remaining};
248 0 0 0       if(exists $h->{from} && exists $h->{label} && $rules_remaining > 0) {
      0        
249 0           my $dir = $self->{folders_path}.'/'.$h->{label}.'/';
250 0           my $flag = $self->compute_flag;
251 0           $dir = qq{"$dir"};
252 0           $self->apply_debug_info(\$dir);
253 0           $buf = sprintf($tmplt1, $flag, $h->{from},$dir);
254 0           $self->{rules_remaining}--;
255             };
256 0           return $buf;
257             };
258              
259              
260             =head2 apply_debug_info()
261              
262             Adds a B header to the e-mail so you can trace back to the rule
263             that matched it.
264              
265             This is triggered through the B<--debug> parameter.
266              
267             =cut
268              
269              
270             sub apply_debug_info {
271 0     0 1   my ($self,$refdir) = @_;
272 0 0 0       if($self->{debug} && ref($refdir) eq "SCALAR") {
273 0           my $rule_no = $self->{debug_rule_count};
274 0           my $olddir = $$refdir;
275 0           $$refdir = qq<
276             {
277             :0 fb
278             | formail -I "X-Procmail-Debug: $rule_no"
279              
280             :0 a:
281             $olddir
282             }
283              
284             >;
285 0           $self->{debug_rule_count}++;
286             };
287             };
288              
289              
290             =head2 rule_to
291              
292             Generic "To:" rule conversion.
293              
294             =cut
295              
296             sub rule_to {
297 0     0 1   my ($self, $h) = @_;
298 0           my $buf = "";
299 0           my $rules_remaining = $self->{rules_remaining};
300 0 0 0       if(exists $h->{to} && exists $h->{label} && $rules_remaining > 0) {
      0        
301 0           my $dir = $self->{folders_path}.'/'.$h->{label}.'/';
302 0           my $flag = $self->compute_flag;
303 0           $dir = qq{"$dir"};
304 0           $self->apply_debug_info(\$dir);
305 0           $buf = sprintf($tmplt2,$flag,$h->{to},$dir);
306 0           $self->{rules_remaining}--;
307             };
308 0           return $buf;
309             };
310              
311             sub rule_body {
312 0     0 0   my ($self, $h) = @_;
313 0           my $buf = "";
314 0           my $rules_remaining = $self->{rules_remaining};
315 0 0 0       if(exists $h->{hasTheWord} && exists $h->{label} && $rules_remaining > 0) {
      0        
316 0           my $dir = $self->{folders_path}.'/'.$h->{label}.'/';
317 0           my $flag = $self->compute_flag;
318 0           $dir = qq{"$dir"};
319 0           $self->apply_debug_info(\$dir);
320 0           $buf = sprintf($tmplt4,$flag,$h->{hasTheWord},$dir);
321 0           $self->{rules_remaining}--;
322             };
323 0           return $buf;
324             };
325              
326             sub rule_subject {
327 0     0 0   my ($self, $h) = @_;
328 0           my $buf = "";
329 0           my $rules_remaining = $self->{rules_remaining};
330 0 0 0       if(exists $h->{subject} && exists $h->{label} && $rules_remaining > 0) {
      0        
331 0           my $dir = $self->{folders_path}.'/'.$h->{label}.'/';
332 0           my $flag = $self->compute_flag;
333 0           $dir = qq{"$dir"};
334 0           $self->apply_debug_info(\$dir);
335 0           $buf = sprintf($tmplt3,$flag,$h->{subject},$dir);
336 0           $self->{rules_remaining}--;
337             };
338 0           return $buf;
339             };
340              
341              
342             =head2 rule_archive
343              
344             Uses the L , L, L, L methods to archive e-mail.
345              
346             =cut
347              
348             sub rule_archive {
349 0     0 1   my ($self, $h) = @_;
350 0           my $buf = "";
351 0 0         if($h->{'shouldArchive'}) {
352 0           $h->{label} = $self->{archive_dir};
353 0           $buf .= $self->rule_from($h);
354 0           $buf .= $self->rule_to($h);
355 0           $buf .= $self->rule_subject($h);
356 0           $buf .= $self->rule_body($h);
357             };
358 0           return $buf;
359             };
360              
361              
362             =head2 rule_trash
363              
364             Same as L but for trash.
365              
366             =cut
367              
368             sub rule_trash {
369 0     0 1   my ($self, $h) = @_;
370 0           my $buf = "";
371 0 0         if($h->{'shouldTrash'}) {
372 0           $h->{label} = $self->{trash_dir};
373 0           $buf .= $self->rule_from($h);
374 0           $buf .= $self->rule_to($h);
375 0           $buf .= $self->rule_subject($h);
376 0           $buf .= $self->rule_body($h);
377             };
378 0           return $buf;
379             };
380              
381             =head2 check_multiple_rules($h)
382              
383             Checks if there are multiple locations where the mail
384             should be placed. Stores whether there are multiple such
385             places and how many of them are. This will later be used in order to
386              
387             =cut
388              
389             sub check_multiple_rules {
390 0     0 1   my ($self,$h) = @_;
391 0           my $count = 0;
392 0           $count += defined($h->{shouldTrash} );
393 0           $count += defined($h->{shouldArchive});
394 0           $count += defined($h->{hasTheWord});
395 0 0         if(defined($h->{label})) {
396 0           $count += defined($h->{from});
397 0           $count += defined($h->{to} );
398             };
399 0           $self->{rules_remaining} = $count;
400 0           $self->{multi_rule} = $count > 1;
401              
402             # if goes_to_inbox is true, the message will slide down all the way to inbox
403 0           $self->{goes_to_inbox} = !defined($h->{shouldArchive});
404             };
405              
406              
407             =head2 escape_fields
408              
409             Escape the field values used for the procmail filters
410              
411             =cut
412              
413             sub escape_fields {
414 0     0 1   my ($self,$h) = @_;
415              
416 0           for(qw/from to subject/) {
417 0 0         next if ! defined($h->{$_});
418 0           $h->{$_} =~ s/\[/\\[/g;
419 0           $h->{$_} =~ s/\]/\\]/g;
420 0           $h->{$_} =~ s/-/\\-/g;
421             };
422             };
423              
424             =head2 adapt_rule($h)
425              
426             Converts a Gmail mail filter to procmail rules.
427             Returns a string with the procmail rule.
428              
429             =cut
430              
431             sub adapt_rule {
432 0     0 1   my ($self,$h) = @_;
433 0           my $buf = "";
434              
435 0           $self->check_multiple_rules($h);
436 0 0         $self->collect_label($h->{label}) if($h->{label});
437 0           $self->escape_fields($h);
438              
439 0           $buf .= $self->rule_from($h);
440 0           $buf .= $self->rule_to($h);
441 0           $buf .= $self->rule_body($h);
442 0           $buf .= $self->rule_subject($h);
443 0           $buf .= $self->rule_archive($h);
444 0           $buf .= $self->rule_trash($h);
445              
446 0 0         if($buf ne "") {
447 0           $buf .= "\n#".("="x70)."\n";
448             };
449              
450 0           return $buf;
451             };
452              
453              
454             1;
455             __END__