Subversion Repositories sysadmin_scripts

Rev

Rev 38 | Rev 42 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 38 Rev 40
Line 22... Line 22...
22
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23
#    GNU General Public License for more details.
23
#    GNU General Public License for more details.
24
#
24
#
25
#    You should have received a copy of the GNU General Public License
25
#    You should have received a copy of the GNU General Public License
26
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
 
27
#
-
 
28
#    for required libraries
-
 
29
#    apt-get -y install libnet-imap-simple-ssl-perl libyaml-tiny-perl libhash-merge-simple-perl libclone-perl
27
 
30
 
28
use strict;
31
use strict;
29
use warnings;
32
use warnings;
30
use Net::IMAP::Simple; # libnet-imap-simple-ssl-perl
33
use Net::IMAP::Simple; # libnet-imap-simple-ssl-perl
31
use POSIX; # to get floor and ceil
34
use POSIX; # to get floor and ceil
-
 
35
use YAML::Tiny; # apt-get libyaml-tiny-perl under debian
-
 
36
use Clone 'clone'; # libclone-perl
-
 
37
use Hash::Merge::Simple qw/ merge clone_merge /; # libhash-merge-simple-perl
-
 
38
 
32
#use Data::Dumper;
39
use Data::Dumper;
33
 
40
 
34
# globals
41
# globals
35
my $CONFIG_FILE_NAME = 'archiveIMAP.cfg';
42
my $CONFIG_FILE_NAME = 'archiveIMAP.yaml';
-
 
43
 
36
my $server = 'localhost'; # used if not specified in definition
44
# the default values for everything. These are overridden
-
 
45
# by default values in the conf file (.yaml) or by individual
-
 
46
# accounts entries.
-
 
47
my $config = {
-
 
48
   'default' => {
-
 
49
      # where the mail is going to
-
 
50
      'target' => {
-
 
51
                  # these have no defaults. They should be in the configuration file
-
 
52
                  # 'password' => 'password',
-
 
53
                  # 'username' => 'username',
37
# Following are added to ignore in definition in account (%accounts)
54
                  # hierarchy can be any combination of <path>, <month> and <year>
-
 
55
                  'hierarchy' => '<path>',
-
 
56
                  # default target server
-
 
57
                  'server' => 'localhost',
-
 
58
                 },
-
 
59
      # where the mail is coming from
-
 
60
      'source' => {
38
my @ignore = ( 'Deleted Messages','Drafts','Junk E-mail','Junk','Trash' );
61
                  # these have no defaults. They should be in the configuration file
-
 
62
                  # 'password' => 'password',
-
 
63
                  # 'username' => 'username',
-
 
64
                  # Anything older than this is archived
39
# these are folders we may want to copy, but we never want to delete
65
                  # number of days unless followed by 'M' or 'Y', in which case it is
-
 
66
                  # multiplied by the number of days in a year (365.2425) or days in a month (30.5)
40
my @systemFolders = ('Outbox','Sent Items','INBOX','Inbox');
67
                  # may be a float, ie 1.25Y is the same as 15M
-
 
68
                  'age' => '1Y',
-
 
69
                  # if set to 1, any folders emptied out will be deleted EXCEPT system folders
41
my $deleteEmptyFolders = 1;
70
                  'deleteEmptyFolders' => 0,
-
 
71
                  # default source server
-
 
72
                  'server' => 'localhost',
-
 
73
                  # these folders are considered system folders and never deleted, case insensitive
-
 
74
                  'system' => [
-
 
75
                                 'Outbox',
-
 
76
                                 'Sent Items',
-
 
77
                                 'INBOX'
-
 
78
                              ],
-
 
79
                  # these folders are ignored, ie not processed at all. Case insensitive
-
 
80
                  'ignore' => [
-
 
81
                                 'Deleted Messages',
-
 
82
                                 'Drafts',
-
 
83
                                 'Junk E-mail',
-
 
84
                                 'Junk',
-
 
85
                                 'Trash'
-
 
86
                               ],
-
 
87
                  # if 1, after successful copy to target, remove from source
42
my $deleteOnSuccess = 1;
88
                  'deleteOnSuccess' => 0
-
 
89
               },
-
 
90
         
43
my %accounts;  # this must be configured in archiveIMAP.cfg. See sample.
91
      # if 1, does a dry run showing what would have happened
-
 
92
      'testing' => 0,
-
 
93
      # if 0, will not be processed
-
 
94
      'enabled' => 1,
-
 
95
   }
-
 
96
   };
-
 
97
 
44
 
98
 
45
#
99
#
46
# find where the script is actually located as cfg should be there
100
# find where the script is actually located as cfg should be there
47
#
101
#
48
sub getScriptLocation {
102
sub getScriptLocation {
Line 56... Line 110...
56
# Read the configuration file from current location 
110
# Read the configuration file from current location 
57
# and return it as a string
111
# and return it as a string
58
#
112
#
59
sub readConfig {
113
sub readConfig {
60
   my $scriptLocation = &getScriptLocation();
114
   my $scriptLocation = &getScriptLocation();
61
   my $return = '';
-
 
62
   if ( -e "$scriptLocation/$CONFIG_FILE_NAME" ) {
115
   if ( -e "$scriptLocation/$CONFIG_FILE_NAME" ) {
63
      open CFG,"<$scriptLocation/$CONFIG_FILE_NAME" or die "Could not read $scriptLocation/$CONFIG_FILE_NAME: $!\n";
116
      my $yaml = YAML::Tiny->read( "$scriptLocation/$CONFIG_FILE_NAME" );
-
 
117
      # use clone_merge to merge conf file into $config
-
 
118
      # overwrites anything in $config if it exists in the config file
64
      $return = join ( '', <CFG> );
119
      $config = clone_merge( $config, $yaml->[0] );
65
      close CFG;
120
      return 1;
66
   }
121
   }
-
 
122
   return 0;
-
 
123
}
-
 
124
 
-
 
125
# merges default into current account, overwriting anything not defined in account with
-
 
126
# value from default EXCEPT arrays labeled in @tags, which will be merged together.
-
 
127
sub fixupAccount {
-
 
128
   my ( $default, $account ) = @_;
-
 
129
 
-
 
130
   # these arrays, part of source, will be appended together instead of being overwritten
-
 
131
   my @tags = ( 'ignore', 'system' );
-
 
132
   # merge the tags in question. NOTE: they can only be in source
-
 
133
   foreach my $tag ( @tags) {
-
 
134
      if ( $default->{'source'}->{$tag} && $account->{'source'}->{$tag} ) {
-
 
135
         my @j = ( @{$default->{'source'}->{$tag}}, @{$account->{'source'}->{$tag}} );
-
 
136
         $account->{'source'}->{$tag} = \@j;
-
 
137
      }
-
 
138
   }
-
 
139
   # now, merge account and default, with account taking precedence.
-
 
140
   return  clone_merge(  $default, $account );
-
 
141
   #my $c = clone_merge(  $default, $account );
67
   return $return;
142
   #return $c;
68
}
143
}
69
 
144
 
70
#
145
#
71
# Open an IMAP connection
146
# Open an IMAP connection
72
#
147
#
Line 81... Line 156...
81
   return $imap;
156
   return $imap;
82
}
157
}
83
 
158
 
84
#
159
#
85
# returns a string in proper format for RFC which is $age days ago
160
# returns a string in proper format for RFC which is $age days ago
-
 
161
# $age is a float, possibly followed by a single character modifier
86
#
162
#
87
sub getDate {
163
sub getDate {
88
   my $age = shift;
164
   my $age = shift;
-
 
165
   # allow modifier to age which contains 'Y' (years) or 'M' (months)
-
 
166
   # Simply set multiplier to the correct value, then multiply the value
-
 
167
   $age = lc( $age );
-
 
168
   if ( $age =~ m/([0-9.]+)([a-z])/ ) {
-
 
169
      my $multiplier = ($2 == 'y' ? 365.2425 : ( $2 == 'm' ? 30.5 : MAXINT) );
-
 
170
      $age = floor( $1 * $multiplier);
-
 
171
   }
89
   my @months = ('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
172
   my @months = ('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
90
   my @now = localtime(time - 24 * 60 * 60 * $age);
173
   my @now = localtime(time - 24 * 60 * 60 * $age);
91
   $now[4] = @months[$now[4]];
174
   $now[4] = @months[$now[4]];
92
   $now[5] += 1900;
175
   $now[5] += 1900;
93
   my $date = sprintf( "%d-%s-%d", $now[3],$now[4],$now[5] ) ; # '1-Jan-2014';
176
   my $date = sprintf( "%d-%s-%d", $now[3],$now[4],$now[5] ) ; # '1-Jan-2014';
Line 153... Line 236...
153
   print "\n\t\tDeleting empty folder $folder";
236
   print "\n\t\tDeleting empty folder $folder";
154
   $sourceAccount->folder_unsubscribe($folder);
237
   $sourceAccount->folder_unsubscribe($folder);
155
   $sourceAccount->delete_mailbox( $folder );
238
   $sourceAccount->delete_mailbox( $folder );
156
}
239
}
157
 
240
 
158
#
-
 
159
# If folder has messages that match the criteria, move them to target
-
 
160
# creates the target folder if necessary
-
 
161
#
-
 
162
sub processFolder {
-
 
163
   my ( $sourceAccount, $targetAccount, $folder, $dateBefore, $sourceDelimiter, $targetDelimiter, $deleteOnSuccess ) = @_;
-
 
164
   my $numMessages = 0;
-
 
165
   $sourceAccount->expunge_mailbox( $folder ); # clean it up so we don't copy deleted messages
-
 
166
   $sourceAccount->select( $folder ) or die "Could not connect to folder $folder\n"; # move into the correct folder for the source
-
 
167
   my @ids = $sourceAccount->search_sent_before( $dateBefore ); # use sent_before to get the sent date from message
-
 
168
#   print join( "\n\t\t\t", @ids ) . "\n";
-
 
169
   return 0 unless @ids; # we have nothing to copy, so exit
-
 
170
   if ( $sourceDelimiter ne $targetDelimiter ) { # different delimiters, so we have to change them in the folder def
-
 
171
      # they may contain meta chars, so escape them. We can use \Q \E in the pattern to match, but the 
-
 
172
      # second part of the s/// will substitute literal EXCEPT for the delimiter and the dollar sign
-
 
173
      # I'm escaping the double quote (what I'm using for the delimiter) "just in case" but I'm not doing
-
 
174
      # the dollar sign, so this will fail, fail, fail if there is an issue there.
-
 
175
      my $escapedTargetDelimiter = $targetDelimiter eq '"' ? '\\' .  $targetDelimiter : $targetDelimiter;
-
 
176
      $folder =~ s"\Q$sourceDelimiter\E"$escapedTargetDelimiter"g;
-
 
177
   }
-
 
178
   if ( $targetAccount->select( $folder ) || &makeFolder( $targetAccount, $folder, $targetDelimiter ) ) {
-
 
179
      foreach my $id ( @ids ) {
-
 
180
         my @flags = $sourceAccount->msg_flags( $id );
-
 
181
         my $message = $sourceAccount->get( $id ) or die $sourceAccount->errstr;
-
 
182
         if ( $targetAccount->put( $folder, $message, @flags ) ) {
-
 
183
            $sourceAccount->delete( $id ) if ( $deleteOnSuccess ) ;
-
 
184
            $numMessages++;
-
 
185
         } else {
-
 
186
            die "Could not write to target, aborting\n$targetAccount->errstr\n";
-
 
187
         }
-
 
188
      }
-
 
189
   } else {
-
 
190
      warn "\t\t$folder not found in target and could not create it\n";
-
 
191
   }
-
 
192
   return $numMessages;
-
 
193
}
-
 
194
 
-
 
195
# this is an alternative to maintaining the hierarchy of the original system.
-
 
196
# instead, it will take 
-
 
197
 
-
 
198
sub processFolderByDate {
-
 
199
   my ( $sourceAccount, $targetAccount, $folder, $targetFormat, $dateBefore, $sourceDelimiter, $targetDelimiter, $deleteOnSuccess ) = @_;
-
 
200
   my $numMessages = 0;
-
 
201
   $sourceAccount->expunge_mailbox( $folder ); # clean it up so we don't copy deleted messages
-
 
202
   $sourceAccount->select( $folder ) or die "Could not connect to folder $folder\n"; # move into the correct folder for the source
-
 
203
   my @ids = $sourceAccount->search_sent_before( $dateBefore ); # use sent_before to get the sent date from message
-
 
204
#   print join( "\n\t\t\t", @ids ) . "\n";
-
 
205
   return 0 unless @ids; # we have nothing to copy, so exit
-
 
206
 
-
 
207
   if ( $sourceDelimiter ne $targetDelimiter ) { # different delimiters, so we have to change them in the folder def
-
 
208
      # they may contain meta chars, so escape them. We can use \Q \E in the pattern to match, but the 
-
 
209
      # second part of the s/// will substitute literal EXCEPT for the delimiter and the dollar sign
-
 
210
      # I'm escaping the double quote (what I'm using for the delimiter) "just in case" but I'm not doing
-
 
211
      # the dollar sign, so this will fail, fail, fail if there is an issue there.
-
 
212
      my $escapedTargetDelimiter = $targetDelimiter eq '"' ? '\\' .  $targetDelimiter : $targetDelimiter;
-
 
213
      $folder =~ s"\Q$sourceDelimiter\E"$escapedTargetDelimiter"g;
-
 
214
   }
-
 
215
   if ( $targetAccount->select( $folder ) || &makeFolder( $targetAccount, $folder, $targetDelimiter ) ) {
-
 
216
      foreach my $id ( @ids ) {
-
 
217
         my @flags = $sourceAccount->msg_flags( $id );
-
 
218
         my $message = $sourceAccount->get( $id ) or die $sourceAccount->errstr;
-
 
219
         if ( $targetAccount->put( $folder, $message, @flags ) ) {
-
 
220
            $sourceAccount->delete( $id ) if ( $deleteOnSuccess ) ;
-
 
221
            $numMessages++;
-
 
222
         } else {
-
 
223
            die "Could not write to target, aborting\n$targetAccount->errstr\n";
-
 
224
         }
-
 
225
      }
-
 
226
   } else {
-
 
227
      warn "\t\t$folder not found in target and could not create it\n";
-
 
228
   }
-
 
229
 
-
 
230
 
-
 
231
   return $numMessages;
-
 
232
}
-
 
233
 
241
 
234
#
-
 
235
# main processing loop to handle one account
242
# main process loop to handle one account
236
#
243
#
237
sub processAccount {
244
sub processAccount {
238
   my ( $source, $target, $age, $ignore, $deleteOnSuccess, $deleteEmptyFolders, $hierarchy ) = @_;
-
 
239
   $heirarchy = '' unless $hierarchy;
245
   my $account = shift;
240
   
246
 
241
   my $date = getDate( $age );
247
   my $date = &getDate( $age );
242
   print "\t" . ( $deleteOnSuccess ? 'Moving' : 'Copying' ) . " all messages before $date\n";
248
   print "\t" . ( $deleteOnSuccess ? 'Moving' : 'Copying' ) . " all messages before $date\n";
243
   
249
   
244
   # open and log into both source and target, and get the separator used
250
   # open and log into both source and target, and get the separator used
245
   my $sourceAccount = &openIMAPConnection( $$source{'server'}, $$source{'username'}, $$source{'password'} );
251
   my $sourceAccount = &openIMAPConnection( $$source{'server'}, $$source{'username'}, $$source{'password'} );
246
   my $targetAccount = &openIMAPConnection( $$target{'server'}, $$target{'username'}, $$target{'password'} );
252
   my $targetAccount = &openIMAPConnection( $$target{'server'}, $$target{'username'}, $$target{'password'} );
Line 255... Line 261...
255
   my $count = 0; # count the number of messages processed
261
   my $count = 0; # count the number of messages processed
256
   my $processedCount = 0; # count the number of folders processed
262
   my $processedCount = 0; # count the number of folders processed
257
   foreach my $folder ( @$folderList ) {
263
   foreach my $folder ( @$folderList ) {
258
      print "\t$folder";
264
      print "\t$folder";
259
      my $messages;
265
      my $messages;
260
      if ( $heirarchy ) {
-
 
261
         $messages = &processFolderByDate( $sourceAccount, $targetAccount, $folder, $hierarchy, $date, $$source{'separator'}, $$target{'separator'}, $deleteOnSuccess );
-
 
262
      } else {
-
 
263
         $messages = &processFolder( $sourceAccount, $targetAccount, $folder, $date, $$source{'separator'}, $$target{'separator'}, $deleteOnSuccess );
266
      $messages = &processFolder( $sourceAccount, $targetAccount, $folder, $date, $$source{'separator'}, $$target{'separator'}, $deleteOnSuccess );
264
      }
-
 
265
      $sourceAccount->expunge_mailbox( $folder );
267
      $TESTING ? print "Would expunge $folder\n" : $sourceAccount->expunge_mailbox( $folder );
266
      # delete folder if empty and client has requested it.
268
      # delete folder if empty and client has requested it.
267
      &deleteAFolder( $sourceAccount, $folder, $$source{'separator'}, $$source{'systemfolder'} ) if ( $deleteEmptyFolders );
269
      ( $TESTING ? print "Would delete $folder\n" : &deleteAFolder( $sourceAccount, $folder, $$source{'separator'}, $$source{'systemfolder'} ) ) if ( $deleteEmptyFolders );
268
      print "\n\t\t$messages processed\n";
270
      print "\n\t\t$messages processed\n";
269
      $count += $messages;
271
      $count += $messages;
270
      $processedCount++;
272
      $processedCount++;
271
      # next line used only for testing. Dies after 5 folders on first account
273
      # next line used only for testing. Dies after 5 folders on first account
272
      # last if $processedCount > 5;
274
      # last if $processedCount > 5;
Line 274... Line 276...
274
   $sourceAccount->quit;
276
   $sourceAccount->quit;
275
   $targetAccount->quit;
277
   $targetAccount->quit;
276
   return $count;
278
   return $count;
277
}
279
}
278
 
280
 
-
 
281
 
-
 
282
 
279
#######################################################################
283
#######################################################################
280
#                   Main                                              #
284
#                   Main                                              #
281
#######################################################################
285
#######################################################################
282
 
286
 
283
# read and evaluate configuration file
287
# read and evaluate configuration file
284
if ( my $config = readConfig() ) {
288
&readConfig() || die "could not load config file\n";
285
   eval $config; 
289
#print Dumper( $config ); die;
286
   die "Error in configuraration file $CONFIG_FILE_NAME\n$@" if $@;
290
foreach my $account ( keys %{$config->{'accounts'}} ) {
287
} else {
-
 
288
   die "Configurastion file $CONFIG_FILE_NAME must be located in script directory\n";
291
    $config->{'accounts'}->{$account} = &fixupAccount( $config->{'default'}, $config->{'accounts'}->{$account} );
289
}
292
}
290
 
293
 
-
 
294
#print Dumper( $config ) ; die;
291
 
295
 
-
 
296
# just a place to gather some stats
292
my %processed;
297
my %processed;
293
$processed{'Accounts'} = 0;
298
$processed{'Accounts'} = 0;
294
$processed{'Messages'} = 0;
299
$processed{'Messages'} = 0;
295
 
300
 
-
 
301
# grab only the accounts for simplicity
-
 
302
my $accounts = $config->{'accounts'};
-
 
303
# now, process each in turn
296
foreach my $account ( keys %accounts ) {
304
foreach my $account ( keys %$accounts ) {
297
   # talk to user
305
   # talk to user
298
   print "Processing account $account\n";
306
   print "Processing account $account\n";
299
   # Set some defaults from globals if they were not defined
307
   # do the account. This is the main worker bee
300
   $accounts{$account}{'source'}{'server'} = $server unless exists( $accounts{$account}{'source'}{'server'} );
308
   $accounts->{$account}->{'processed'} = &processAccount( $accounts->{$account} );
301
   $accounts{$account}{'target'}{'server'} = $server unless exists( $accounts{$account}{'target'}{'server'} );
-
 
302
   $accounts{$account}{'deleteOnSuccess'} = $deleteOnSuccess unless exists $accounts{$account}{'deleteOnSuccess'};
-
 
303
   $accounts{$account}{'deleteEmptyFolders'} = $deleteEmptyFolders unless exists $accounts{$account}{'deleteEmptyFolders'};
-
 
304
   if ( exists $accounts{$account}{'ignore'} ) {
-
 
305
      foreach my $element ( @ignore ) {
-
 
306
         push $accounts{$account}{'ignore'}, $element;
-
 
307
      }
-
 
308
   } else {
-
 
309
      $accounts{$account}{'ignore'} = \@ignore;
-
 
310
   }
-
 
311
   
-
 
312
   if ( exists $accounts{$account}{'source'}{'systemFolders'} ) {
-
 
313
      foreach my $element ( @systemFolders ) {
-
 
314
         push $accounts{$account}{'source'}{'systemFolders'}, $element;
-
 
315
      }
-
 
316
   } else {
-
 
317
      $accounts{$account}{'source'}{'systemFolders'} = \@systemFolders;
-
 
318
   }
-
 
319
    
-
 
320
   # do the account
-
 
321
   $accounts{$account}{'processed'} =
-
 
322
            &processAccount( 
-
 
323
                     $accounts{$account}{'source'},
-
 
324
                     $accounts{$account}{'target'},
-
 
325
                     $accounts{$account}{'age'},   
-
 
326
                     $accounts{$account}{'ignore'},
-
 
327
                     $accounts{$account}{'deleteOnSuccess'},
-
 
328
                     $accounts{$account}{'deleteEmptyFolders'}
-
 
329
                     );
-
 
330
   print "Done, $accounts{$account}{'processed'} messages copied\n";
309
   print "Done, $accounts->{$account}->{'processed'} messages copied\n";
331
   $processed{'Accounts'}++;
310
   $processed{'Accounts'}++;
332
   $processed{'Messages'} += $accounts{$account}{'processed'};
311
   $processed{'Messages'} += $accounts->{$account}->{'processed'};
333
 
-
 
334
} # foreach loop
312
} # foreach loop
335
 
313
 
336
print "$processed{'Accounts'} accounts processed, $processed{'Messages'} messages\n";
314
print "$processed{'Accounts'} accounts processed, $processed{'Messages'} messages\n";
337
 
315
 
-
 
316
 
338
1;
317
1;
339
  
318